/* SPDX-License-Identifier: BSD-2-Clause */ /* CLOCK_MONOTONIC under -std=c11 -CMAKE_C_EXTENSIONS=OFF. */ #define _POSIX_C_SOURCE 200809L /* * 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, * 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 [warmup]] * (default iters = 50, warmup = 5) */ #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 /* Standard benchmark loop. fn() is called n times per iteration. * * fn() now returns the dispatch's int rc. A single preflight call is * made before the hot loop; if rc != 0 (which on the QPU substrate * almost always means "SPV not found via any search path"), bench_ns * returns -1 and the caller must NOT report the kernel as measured. * * Without this, a missing SPV makes every dispatch fail fast at the * cost of one fprintf+open call (~1-5 µs), and the loop times that * cost as if it were real QPU work — producing absurdly-small ns/op * numbers that look like a QPU speedup. This is exactly what made * PR #36's bench numbers a measurement artifact. */ typedef int (*bench_fn)(void); static double bench_ns(const char *name, int iters, int warmup, int ops_per_iter, bench_fn fn) { int rc = fn(); if (rc != 0) { printf(" %-32s DISPATCH FAILED rc=%d — kernel skipped\n", name, rc); return -1; } 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 %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. 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_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 * 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 int bench_idct4(void) { return daedalus_dispatch_h264_idct4(ctx, g_sub, 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 int bench_idct8(void) { return daedalus_dispatch_h264_idct8(ctx, g_sub, 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 int bench_deblock_v(void) { return daedalus_dispatch_h264_deblock_luma_v(ctx, g_sub, deblock_dst, 16, 256, deblock_meta); } static int bench_deblock_h(void) { return daedalus_dispatch_h264_deblock_luma_h(ctx, g_sub, 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 int bench_qpel_mc20(void) { return daedalus_dispatch_h264_qpel_mc20(ctx, g_sub, qpel_dst, qpel_src, 16, 256, qpel_meta); } static int bench_qpel_mc02(void) { return daedalus_dispatch_h264_qpel_mc02(ctx, g_sub, qpel_dst, qpel_src, 16, 256, qpel_meta); } static int bench_qpel_mc22(void) { return 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; int warmup = argc > 2 ? atoi(argv[2]) : 5; ctx = daedalus_ctx_create(); if (!ctx) { 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. */ 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. */ 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. */ 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)\n", iters, warmup); printf(" ctx has_qpu=%d (CPU pass always runs; QPU pass skipped without Vulkan)\n\n", has_qpu); /* 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); /* Pass 2: QPU compute (if available). */ int qpu_failures = 0; 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); if (rows[i].qpu_ns < 0) qpu_failures++; } if (qpu_failures) { fprintf(stderr, "\nbench_h264_primitives: %d of %d QPU dispatches failed.\n" " Almost always means SPV files were not found via any of:\n" " cwd / $DAEDALUS_SHADER_DIR / binary-dir /\n" " /opt/fourier/share/daedalus-fourier / /usr/share/daedalus-fourier\n" " Set DAEDALUS_SHADER_DIR= or run from a dir where the\n" " .spv files exist (e.g. the cmake build dir).\n", qpu_failures, N_ROWS); return 2; } } /* 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); } 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"); 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; }