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; +}