/* 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; }