From ba5bbae8e2ec2bd967577a0088dda757a7b2180c Mon Sep 17 00:00:00 2001 From: claude-noether Date: Mon, 25 May 2026 11:26:11 +0200 Subject: [PATCH] bench: H.264 primitives NEON CPU baseline (1080p budget projection) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds bench_h264_primitives — a non-ctest binary that times the H.264 pixel-math primitives at their representative per-frame N and projects 1080p frame budgets. Lets us answer "how much of the 33-ms 30fps deadline does the pixel-math layer eat on NEON alone, before the intercept patch adds entropy decode + metadata work." Results on hertz (Pi 5 / 4×Cortex-A76, NEON path): Per-kernel ns/op (CPU NEON): IDCT 4x4 luma 10.78 ns/block IDCT 8x8 luma 29.73 ns/block Deblock luma_v 18.04 ns/edge Deblock luma_h 41.65 ns/edge (H access pattern less SIMD-friendly) qpel mc20 (H half-pel) 25.66 ns/block qpel mc02 (V half-pel) 15.06 ns/block (faster than mc20!) qpel mc22 (HV half-pel) 71.50 ns/block (cascaded H+V, expected) Projected 1080p frame budgets (worst-case, CPU NEON only): IDCT 4x4 (all-4x4 MBs): 1.41 ms (130,560 blocks) IDCT 8x8 (all-8x8 MBs): 0.97 ms ( 32,640 blocks) Deblock luma_v (all MBs): 0.59 ms ( 32,640 edges) Deblock luma_h (all MBs): 1.36 ms ( 32,640 edges) qpel mc22 (all 8x8 blocks): 2.33 ms ( 32,640 blocks) Sum (IDCT 4x4 + deblock luma + MC all-mc22): 5.69 ms 30 fps deadline: 33.33 ms Margin: +27.64 ms What this validates: - The "30fps@1080p is the fine floor" memory note holds with huge headroom on the pixel-math layer alone. 17% of the deadline goes to pixel math (worst case); 83% is available for entropy decode + reference frame management + intra prediction + chroma deblock + chroma IDCT + the libavcodec intercept overhead. - The CPU-vs-QPU substrate finding from earlier (PR #10 on daedalus-decoder showed CPU NEON is 4x faster than QPU for IDCT) is consistent here. All these kernels have CPU-only recipes by default; the data suggests that's the right call for now. The recipe substrate decision can be revisited per-kernel once QPU shaders catch up. - mc22 (2D HV half-pel) is the most expensive single qpel position at ~71 ns/block — 2-7x more than the 1D variants. Real B-slice biprediction with two mc22 calls per MB would add ~4.7 ms/frame; still comfortable but worth knowing. What this DOESN'T measure (intentionally — they aren't on the critical path at NEON speeds): - Chroma IDCT (4 cb + 4 cr 4x4 per MB). At similar ns/block to luma, that's ~0.7 ms/frame. - Chroma deblock (smaller tile, simpler kernel — sub-ms). - Intra prediction (per-block, ~50 ops at NEON, but serialized in z-scan order so cache-friendly; ~0.5 ms/frame estimate). - bS=4 intra deblock variants — different algorithm, similar cost to bS<4. - chroma DC Hadamard — trivial. Adding all of those in the worst case would maybe double the 5.69 ms number to ~12 ms. Still leaves 20+ ms for entropy decode + metadata work in the intercept patch. --- CMakeLists.txt | 5 + tests/bench_h264_primitives.c | 220 ++++++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 tests/bench_h264_primitives.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a9d6ec..b186a40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -579,6 +579,11 @@ add_executable(test_chroma_dc_hadamard ) target_compile_options(test_chroma_dc_hadamard PRIVATE -O2) +# H.264 primitives latency benchmark (NEON CPU baseline). +add_executable(bench_h264_primitives tests/bench_h264_primitives.c) +target_link_libraries(bench_h264_primitives PRIVATE daedalus_core) +target_compile_options(bench_h264_primitives PRIVATE -O2) + add_executable(bench_pool_overhead tests/bench_pool_overhead.c) target_link_libraries(bench_pool_overhead PRIVATE daedalus_core) target_compile_options(bench_pool_overhead PRIVATE -O2) diff --git a/tests/bench_h264_primitives.c b/tests/bench_h264_primitives.c new file mode 100644 index 0000000..99dae83 --- /dev/null +++ b/tests/bench_h264_primitives.c @@ -0,0 +1,220 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +/* CLOCK_MONOTONIC under -std=c11 -CMAKE_C_EXTENSIONS=OFF. */ +#define _POSIX_C_SOURCE 200809L +/* + * bench_h264_primitives — NEON-path latency baseline for the H.264 + * primitive library landed across PRs #9–#23. + * + * Each kernel is exercised at a representative per-frame N for 1080p + * (8160 MBs); the per-kernel total + ns/op + ms/frame are reported. + * Lets us answer "what's the total NEON-only budget for the H.264 + * decode at 1080p" — useful for sizing intercept-patch decisions + * (which kernels NEED QPU shaders vs which are budget-fine on NEON). + * + * NOT a ctest — produces wall-time numbers, doesn't pass/fail. + * + * Invoke: ./build/bench_h264_primitives [iters] + * (default iters = 50, post-warmup = 5) + * + * NB: results are inherently approximate — single-core, includes + * loop overhead + memory access patterns that may not match what + * a real decode would hit (we touch a small set of pages repeatedly). + * The numbers are useful for relative comparison and order-of- + * magnitude sizing, not absolute perf claims. + */ + +#include "daedalus.h" + +#include +#include +#include +#include +#include + +static uint64_t xs64_state = 0xfeedface5a5a5a5aULL; +static uint64_t xs64(void) { + uint64_t x = xs64_state; + x ^= x << 13; x ^= x >> 7; x ^= x << 17; + return xs64_state = x; +} + +static double now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000.0 + ts.tv_nsec / 1.0e6; +} + +/* Per-1080p-frame counts (8160 MBs at 1920x1088). */ +#define MBS_1080P 8160 +#define LUMA_4x4_PER_MB 16 /* if transform_8x8=0 */ +#define LUMA_8x8_PER_MB 4 /* if transform_8x8=1 */ +#define CHROMA_4x4_PER_MB 8 /* 4 Cb + 4 Cr */ +#define DEBLOCK_LUMA_EDGES_PER_MB 4 /* 4 horiz + 4 vert internal+MB-edge — ~4 each */ +#define DEBLOCK_CHROMA_EDGES_PER_MB 2 /* 2 each direction */ + +/* Standard benchmark loop. fn() is called n times per iteration. */ +typedef void (*bench_fn)(void); + +static double bench_ns(const char *name, int iters, int warmup, + int ops_per_iter, bench_fn fn) +{ + for (int i = 0; i < warmup; i++) fn(); + double t0 = now_ms(); + for (int i = 0; i < iters; i++) fn(); + double t1 = now_ms(); + double total_ms = (t1 - t0); + double ns_per_op = (total_ms * 1e6) / ((double) iters * ops_per_iter); + printf(" %-32s %8.2f ns/op (%d iters x %d ops)\n", + name, ns_per_op, iters, ops_per_iter); + return ns_per_op; +} + +/* ---- Per-kernel scaffolding. Each section sets up the buffers + + * meta, then defines a static fn() that calls the corresponding + * dispatch with a representative N. */ + +static daedalus_ctx *ctx; + +/* --- IDCT 4x4 luma: N = 16 blocks per MB. Bench with 1024 blocks + * per call (64 MBs worth). Per-MB the dispatch overhead is the + * same regardless of N — we want ns per block. */ +static int16_t idct4_coeffs[1024 * 16]; +static daedalus_h264_block_meta idct4_meta[1024]; +static uint8_t idct_dst[64 * 4 * 16 * 16]; /* 64 MB-rows × ... */ + +static void bench_idct4(void) { + daedalus_dispatch_h264_idct4(ctx, DAEDALUS_SUBSTRATE_CPU, + idct_dst, 64*16, idct4_coeffs, 1024, idct4_meta); +} + +/* --- IDCT 8x8 luma: 256 8x8 blocks per call. */ +static int16_t idct8_coeffs[256 * 64]; +static daedalus_h264_block_meta idct8_meta[256]; + +static void bench_idct8(void) { + daedalus_dispatch_h264_idct8(ctx, DAEDALUS_SUBSTRATE_CPU, + idct_dst, 64*16, idct8_coeffs, 256, idct8_meta); +} + +/* --- Deblock luma_v (cycle 8 baseline; M3 path). */ +static daedalus_h264_deblock_meta deblock_meta[256]; +static uint8_t deblock_dst[256 * 16 * 16]; + +static void bench_deblock_v(void) { + daedalus_dispatch_h264_deblock_luma_v(ctx, DAEDALUS_SUBSTRATE_CPU, + deblock_dst, 16, 256, deblock_meta); +} + +static void bench_deblock_h(void) { + daedalus_dispatch_h264_deblock_luma_h(ctx, DAEDALUS_SUBSTRATE_CPU, + deblock_dst, 16, 256, deblock_meta); +} + +/* --- qpel mc20 + mc02 + mc22 (the H/V/HV anchors). */ +static uint8_t qpel_src[256 * 16 * 16]; +static uint8_t qpel_dst[256 * 16 * 16]; +static daedalus_h264_qpel_meta qpel_meta[256]; + +static void bench_qpel_mc20(void) { + daedalus_dispatch_h264_qpel_mc20(ctx, DAEDALUS_SUBSTRATE_CPU, + qpel_dst, qpel_src, 16, 256, qpel_meta); +} +static void bench_qpel_mc02(void) { + daedalus_dispatch_h264_qpel_mc02(ctx, DAEDALUS_SUBSTRATE_CPU, + qpel_dst, qpel_src, 16, 256, qpel_meta); +} +static void bench_qpel_mc22(void) { + daedalus_dispatch_h264_qpel_mc22(ctx, DAEDALUS_SUBSTRATE_CPU, + qpel_dst, qpel_src, 16, 256, qpel_meta); +} + +int main(int argc, char **argv) +{ + int iters = argc > 1 ? atoi(argv[1]) : 50; + int warmup = argc > 2 ? atoi(argv[2]) : 5; + + ctx = daedalus_ctx_create(); + if (!ctx) { + fprintf(stderr, "ctx create failed (Vulkan?)\n"); + return 1; + } + + /* Pre-fill all input buffers with random data so the NEON inner + * loops see realistic memory access patterns. */ + for (size_t i = 0; i < sizeof(idct4_coeffs)/2; i++) + idct4_coeffs[i] = (int16_t)((int)(xs64() % 1024) - 512); + for (size_t i = 0; i < sizeof(idct8_coeffs)/2; i++) + idct8_coeffs[i] = (int16_t)((int)(xs64() % 1024) - 512); + for (size_t i = 0; i < sizeof(qpel_src); i++) qpel_src[i] = (uint8_t)(xs64() & 0xff); + + /* IDCT meta: each block at offset i*16 (row layout matters less + * here since we're just measuring per-block latency). */ + for (size_t i = 0; i < 1024; i++) + idct4_meta[i].dst_off = (uint32_t)((i / 16) * 64 + (i % 16) * 4); + for (size_t i = 0; i < 256; i++) + idct8_meta[i].dst_off = (uint32_t)((i / 8) * 64 + (i % 8) * 8); + + /* Deblock meta: edge offsets within 256 16x16 tiles. */ + for (size_t i = 0; i < 256; i++) { + deblock_meta[i].dst_off = (uint32_t)(i * 256 + 4 * 16); + deblock_meta[i].alpha = 30; + deblock_meta[i].beta = 10; + for (int s = 0; s < 4; s++) deblock_meta[i].tc0[s] = (int8_t)(s + 1); + } + + /* qpel meta: src and dst at row 3 col 3 of each 16x16 tile. */ + for (size_t i = 0; i < 256; i++) { + qpel_meta[i].src_off = (uint32_t)(i * 256 + 3 * 16 + 3); + qpel_meta[i].dst_off = (uint32_t)(i * 256 + 3 * 16 + 3); + } + + printf("bench_h264_primitives: %d iters (%d warmup), substrate=CPU NEON\n", + iters, warmup); + printf("Per-call N is set per kernel; ns/op is per BLOCK or EDGE.\n\n"); + + double idct4_ns = bench_ns("IDCT 4x4 luma", iters, warmup, 1024, bench_idct4); + double idct8_ns = bench_ns("IDCT 8x8 luma", iters, warmup, 256, bench_idct8); + double debl_v_ns = bench_ns("Deblock luma_v", iters, warmup, 256, bench_deblock_v); + double debl_h_ns = bench_ns("Deblock luma_h", iters, warmup, 256, bench_deblock_h); + double qmc20_ns = bench_ns("qpel mc20 (8x8)", iters, warmup, 256, bench_qpel_mc20); + double qmc02_ns = bench_ns("qpel mc02 (8x8)", iters, warmup, 256, bench_qpel_mc02); + double qmc22_ns = bench_ns("qpel mc22 (8x8)", iters, warmup, 256, bench_qpel_mc22); + + /* Per-frame budget summary at 1080p (8160 MBs). Worst-case + * assumptions: + * - All MBs are transform_4x4 (16 4x4 IDCTs each) — so 130,560 + * IDCT 4x4 blocks per frame. If High profile transform_8x8, + * it'd be 32,640 IDCT 8x8 blocks instead. + * - All MBs are intra (no MC — qpel zero) OR all inter (no + * intra prediction). We report MC at "all inter, all qpel + * mc22" worst case. + * - Deblock: ~4 luma_v + 4 luma_h edges per MB; assume all 8 + * edges trigger filtering. */ + printf("\nProjected 1080p frame budgets (worst-case, CPU NEON only):\n"); + printf(" IDCT 4x4 (all-4x4 MBs): %7.2f ms (%d blocks)\n", + idct4_ns * MBS_1080P * 16 / 1e6, MBS_1080P * 16); + printf(" IDCT 8x8 (all-8x8 MBs): %7.2f ms (%d blocks)\n", + idct8_ns * MBS_1080P * 4 / 1e6, MBS_1080P * 4); + printf(" Deblock luma_v (all MBs): %7.2f ms (%d edges)\n", + debl_v_ns * MBS_1080P * 4 / 1e6, MBS_1080P * 4); + printf(" Deblock luma_h (all MBs): %7.2f ms (%d edges)\n", + debl_h_ns * MBS_1080P * 4 / 1e6, MBS_1080P * 4); + printf(" qpel mc22 (all 8x8 blocks): %7.2f ms (%d blocks)\n", + qmc22_ns * MBS_1080P * 4 / 1e6, MBS_1080P * 4); + + double sum_idct_4x4 = idct4_ns * MBS_1080P * 16 / 1e6; + double sum_deblock = (debl_v_ns + debl_h_ns) * MBS_1080P * 4 / 1e6; + double sum_mc = qmc22_ns * MBS_1080P * 4 / 1e6; /* worst-case all-mc22 */ + printf("\n Sum (IDCT 4x4 + deblock luma + MC all-mc22): %7.2f ms\n", + sum_idct_4x4 + sum_deblock + sum_mc); + printf(" 30 fps deadline: 33.33 ms\n"); + printf(" Margin: %+.2f ms\n", + 33.33 - (sum_idct_4x4 + sum_deblock + sum_mc)); + printf("\n(NOT included: chroma deblock, chroma IDCT, intra prediction,\n"); + printf(" CABAC/CAVLC entropy. These bench numbers are a budget LOWER\n"); + printf(" bound; the real decode stack adds 20-40%% on top.)\n"); + (void) qmc20_ns; (void) qmc02_ns; + + daedalus_ctx_destroy(ctx); + return 0; +} -- 2.47.3