From 989818c2e6e17eaa786bce9478ef14164a66a2c8 Mon Sep 17 00:00:00 2001 From: claude-noether Date: Mon, 25 May 2026 20:41:38 +0200 Subject: [PATCH] bench: H.264 primitive bench now measures both substrates + comparison table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes task #166 (re-measure R-bands on post-buffer-pool dispatch path). Now that all H.264 hot-path primitives have QPU shaders and the dispatch overhead has been hammered down (tasks #160 buffer pool, #161 persistent command buffer), bench_h264_primitives no longer measures one column. Two passes — CPU NEON and QPU V3D7 compute — with a side-by-side per-kernel comparison and ratio. Headline result on hertz (Pi 5 V3D 7.1, 30 iters x 5 warmup): kernel CPU ns/op QPU ns/op winner IDCT 4x4 luma 10.79 2.47 QPU 4.36x IDCT 8x8 luma 29.69 9.23 QPU 3.22x Deblock luma_v 17.58 10.21 QPU 1.72x Deblock luma_h 38.41 9.98 QPU 3.85x qpel mc20 (8x8) 28.24 9.66 QPU 2.92x qpel mc02 (8x8) 16.96 20.54 CPU 1.21x qpel mc22 (8x8) 71.58 9.64 QPU 7.43x 1080p worst-case sum (IDCT4 + deblock luma + qpel mc22): CPU NEON only: 5.57 ms QPU only: 1.30 ms (CPU/QPU sum ratio = 4.30x) Reverses PR #10's verdict (which had CPU NEON 4x faster than QPU for IDCT-only) — the buffer-pool + persistent-cmdbuf wins land hard. Only qpel mc02 still shows CPU ahead, marginally (single- axis vertical filter, row-strided memory pattern unfriendly to the WG layout — left as a follow-up for cycle-9-style targeted tuning). Substrate decree (2026-05-23) stays in force as policy — these numbers retroactively justify it. Also tightens test_api_h264's startup recipe print: the stale "(CPU)" / "(CPU, no QPU H shader yet)" / "(CPU, bS=4 set)" labels next to deblock_lh, deblock_cv, deblock_ch and deblock_*_intra are now wrong since PRs #28, #29, #35 (those kernels are on QPU). --- tests/bench_h264_primitives.c | 195 +++++++++++++++++++++------------- tests/test_api_h264.c | 8 +- 2 files changed, 126 insertions(+), 77 deletions(-) diff --git a/tests/bench_h264_primitives.c b/tests/bench_h264_primitives.c index 99dae83..14b8002 100644 --- a/tests/bench_h264_primitives.c +++ b/tests/bench_h264_primitives.c @@ -2,25 +2,22 @@ /* 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. + * bench_h264_primitives — latency baseline for the H.264 primitive + * library landed across PRs #9–#35. * * 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). + * (8160 MBs); the per-kernel total + ns/op + ms/frame are reported, + * once per substrate (CPU NEON, QPU V3D7 compute). The QPU column + * appears only when the host has a usable Vulkan device. When both + * columns exist a CPU/QPU ratio is printed; that's the per-kernel + * data the QPU-substrate decree (2026-05-23) deliberately overrides + * but which is still useful to track over time as dispatch overhead + * shrinks (buffer pool, persistent cmdbuf, dmabuf import — tasks 160-162). * * 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. + * Invoke: ./build/bench_h264_primitives [iters [warmup]] + * (default iters = 50, warmup = 5) */ #include "daedalus.h" @@ -46,11 +43,6 @@ static double now_ms(void) { /* 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); @@ -64,16 +56,18 @@ static double bench_ns(const char *name, int iters, int warmup, 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", + printf(" %-32s %10.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. */ + * dispatch with a representative N. The substrate is read from the + * global g_sub so the same fn() can be re-driven with CPU then QPU. */ -static daedalus_ctx *ctx; +static daedalus_ctx *ctx; +static daedalus_substrate g_sub = DAEDALUS_SUBSTRATE_CPU; /* --- IDCT 4x4 luma: N = 16 blocks per MB. Bench with 1024 blocks * per call (64 MBs worth). Per-MB the dispatch overhead is the @@ -83,7 +77,7 @@ 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, + daedalus_dispatch_h264_idct4(ctx, g_sub, idct_dst, 64*16, idct4_coeffs, 1024, idct4_meta); } @@ -92,7 +86,7 @@ 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, + daedalus_dispatch_h264_idct8(ctx, g_sub, idct_dst, 64*16, idct8_coeffs, 256, idct8_meta); } @@ -101,12 +95,12 @@ 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, + daedalus_dispatch_h264_deblock_luma_v(ctx, g_sub, deblock_dst, 16, 256, deblock_meta); } static void bench_deblock_h(void) { - daedalus_dispatch_h264_deblock_luma_h(ctx, DAEDALUS_SUBSTRATE_CPU, + daedalus_dispatch_h264_deblock_luma_h(ctx, g_sub, deblock_dst, 16, 256, deblock_meta); } @@ -116,18 +110,43 @@ 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, + daedalus_dispatch_h264_qpel_mc20(ctx, g_sub, qpel_dst, qpel_src, 16, 256, qpel_meta); } static void bench_qpel_mc02(void) { - daedalus_dispatch_h264_qpel_mc02(ctx, DAEDALUS_SUBSTRATE_CPU, + daedalus_dispatch_h264_qpel_mc02(ctx, g_sub, qpel_dst, qpel_src, 16, 256, qpel_meta); } static void bench_qpel_mc22(void) { - daedalus_dispatch_h264_qpel_mc22(ctx, DAEDALUS_SUBSTRATE_CPU, + daedalus_dispatch_h264_qpel_mc22(ctx, g_sub, qpel_dst, qpel_src, 16, 256, qpel_meta); } +/* ---- One row of bench output: + * - kernel name + N + * - CPU ns/op + * - QPU ns/op (or "n/a" if Vulkan absent) + * - CPU/QPU ratio (>1 means QPU wins; <1 means CPU wins) */ +struct row { + const char *name; + int n_per_call; + bench_fn fn; + double cpu_ns; + double qpu_ns; /* -1 if not measured */ + int frame_n; /* count per 1080p frame */ +}; + +static struct row rows[] = { + {"IDCT 4x4 luma", 1024, bench_idct4, 0, -1, MBS_1080P * 16}, + {"IDCT 8x8 luma", 256, bench_idct8, 0, -1, MBS_1080P * 4}, + {"Deblock luma_v", 256, bench_deblock_v, 0, -1, MBS_1080P * 4}, + {"Deblock luma_h", 256, bench_deblock_h, 0, -1, MBS_1080P * 4}, + {"qpel mc20 (8x8)", 256, bench_qpel_mc20, 0, -1, MBS_1080P * 4}, + {"qpel mc02 (8x8)", 256, bench_qpel_mc02, 0, -1, MBS_1080P * 4}, + {"qpel mc22 (8x8)", 256, bench_qpel_mc22, 0, -1, MBS_1080P * 4}, +}; +#define N_ROWS ((int)(sizeof(rows)/sizeof(rows[0]))) + int main(int argc, char **argv) { int iters = argc > 1 ? atoi(argv[1]) : 50; @@ -138,6 +157,7 @@ int main(int argc, char **argv) fprintf(stderr, "ctx create failed (Vulkan?)\n"); return 1; } + int has_qpu = daedalus_ctx_has_qpu(ctx); /* Pre-fill all input buffers with random data so the NEON inner * loops see realistic memory access patterns. */ @@ -147,8 +167,7 @@ int main(int argc, char **argv) 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). */ + /* IDCT meta. */ 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++) @@ -162,58 +181,88 @@ int main(int argc, char **argv) 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. */ + /* qpel meta. */ 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"); + printf("bench_h264_primitives: %d iters (%d warmup)\n", iters, warmup); + printf(" ctx has_qpu=%d (CPU pass always runs; QPU pass skipped without Vulkan)\n\n", has_qpu); - 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); + /* Pass 1: CPU NEON. */ + g_sub = DAEDALUS_SUBSTRATE_CPU; + printf("== CPU NEON ==\n"); + for (int i = 0; i < N_ROWS; i++) + rows[i].cpu_ns = bench_ns(rows[i].name, iters, warmup, rows[i].n_per_call, rows[i].fn); - /* 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); + /* Pass 2: QPU compute (if available). */ + if (has_qpu) { + g_sub = DAEDALUS_SUBSTRATE_QPU; + printf("\n== QPU V3D7 compute ==\n"); + for (int i = 0; i < N_ROWS; i++) + rows[i].qpu_ns = bench_ns(rows[i].name, iters, warmup, rows[i].n_per_call, rows[i].fn); + } + + /* Summary table — both substrates side by side. */ + printf("\n== Per-kernel comparison ==\n"); + printf(" %-24s %12s %12s %8s %7s\n", + "kernel", "CPU ns/op", "QPU ns/op", "winner", "ms/frame"); + for (int i = 0; i < N_ROWS; i++) { + double cpu_ms = rows[i].cpu_ns * rows[i].frame_n / 1e6; + double qpu_ms = rows[i].qpu_ns > 0 ? rows[i].qpu_ns * rows[i].frame_n / 1e6 : -1; + const char *winner; + char ratio[16]; + if (rows[i].qpu_ns <= 0) { + winner = "CPU"; /* QPU n/a */ + snprintf(ratio, sizeof(ratio), "n/a"); + } else if (rows[i].cpu_ns < rows[i].qpu_ns) { + winner = "CPU"; + snprintf(ratio, sizeof(ratio), "%.2fx", rows[i].qpu_ns / rows[i].cpu_ns); + } else { + winner = "QPU"; + snprintf(ratio, sizeof(ratio), "%.2fx", rows[i].cpu_ns / rows[i].qpu_ns); + } + char qpu_field[16]; + if (rows[i].qpu_ns > 0) snprintf(qpu_field, sizeof(qpu_field), "%.2f", rows[i].qpu_ns); + else snprintf(qpu_field, sizeof(qpu_field), "n/a"); + char ms_field[24]; + if (qpu_ms > 0) + snprintf(ms_field, sizeof(ms_field), "%.2f/%.2f", cpu_ms, qpu_ms); + else + snprintf(ms_field, sizeof(ms_field), "%.2f/n/a", cpu_ms); + printf(" %-24s %12.2f %12s %3s %s %s\n", + rows[i].name, rows[i].cpu_ns, qpu_field, winner, ratio, ms_field); + } + + /* Per-frame budget summary at 1080p (8160 MBs). */ + double cpu_idct4 = rows[0].cpu_ns * MBS_1080P * 16 / 1e6; + double cpu_debl = (rows[2].cpu_ns + rows[3].cpu_ns) * MBS_1080P * 4 / 1e6; + double cpu_mc = rows[6].cpu_ns * MBS_1080P * 4 / 1e6; /* mc22 worst-case */ + double cpu_sum = cpu_idct4 + cpu_debl + cpu_mc; + + printf("\n== Projected 1080p worst-case (CPU NEON only) ==\n"); + printf(" IDCT 4x4 + deblock luma + qpel mc22: %.2f ms (30fps deadline 33.33)\n", cpu_sum); + printf(" Margin: %+.2f ms\n", 33.33 - cpu_sum); + + if (has_qpu) { + double qpu_idct4 = rows[0].qpu_ns * MBS_1080P * 16 / 1e6; + double qpu_debl = (rows[2].qpu_ns + rows[3].qpu_ns) * MBS_1080P * 4 / 1e6; + double qpu_mc = rows[6].qpu_ns * MBS_1080P * 4 / 1e6; + double qpu_sum = qpu_idct4 + qpu_debl + qpu_mc; + printf("\n== Projected 1080p worst-case (QPU V3D7 compute only) ==\n"); + printf(" IDCT 4x4 + deblock luma + qpel mc22: %.2f ms (30fps deadline 33.33)\n", qpu_sum); + printf(" Margin: %+.2f ms\n", 33.33 - qpu_sum); + printf("\n CPU vs QPU sum ratio: %.2fx (>1 means QPU wins)\n", + qpu_sum > 0 ? cpu_sum / qpu_sum : 0.0); + } - 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; + printf(" bound; the real decode stack adds 20-40%% on top.\n"); + printf(" Per-kernel substrate decisions belong in daedalus_core.c recipe\n"); + printf(" table; the QPU substrate decree (2026-05-23) keeps everything\n"); + printf(" on QPU regardless of these numbers as a policy choice.)\n"); daedalus_ctx_destroy(ctx); return 0; diff --git a/tests/test_api_h264.c b/tests/test_api_h264.c index edcadaa..57e2ebe 100644 --- a/tests/test_api_h264.c +++ b/tests/test_api_h264.c @@ -683,13 +683,13 @@ int main(void) printf(" H264_QPEL_MC20 recipe substrate: %d\n", (int) daedalus_recipe_substrate_for(DAEDALUS_KERNEL_H264_QPEL_MC20)); - printf(" H264_DEBLOCK_LH recipe substrate: %d (CPU, no QPU H shader yet)\n", + printf(" H264_DEBLOCK_LH recipe substrate: %d\n", (int) daedalus_recipe_substrate_for(DAEDALUS_KERNEL_H264_DEBLOCK_LH)); - printf(" H264_DEBLOCK_CV recipe substrate: %d (CPU)\n", + printf(" H264_DEBLOCK_CV recipe substrate: %d\n", (int) daedalus_recipe_substrate_for(DAEDALUS_KERNEL_H264_DEBLOCK_CV)); - printf(" H264_DEBLOCK_CH recipe substrate: %d (CPU)\n", + printf(" H264_DEBLOCK_CH recipe substrate: %d\n", (int) daedalus_recipe_substrate_for(DAEDALUS_KERNEL_H264_DEBLOCK_CH)); - printf(" H264_DEBLOCK_*_INTRA recipe substrate: %d (CPU, bS=4 set)\n", + printf(" H264_DEBLOCK_*_INTRA recipe substrate: %d (bS=4 family, all on QPU)\n", (int) daedalus_recipe_substrate_for(DAEDALUS_KERNEL_H264_DEBLOCK_LV_INTRA)); int fail = 0; -- 2.47.3