989818c2e6
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).
270 lines
11 KiB
C
270 lines
11 KiB
C
/* 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 <stdint.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <time.h>
|
||
|
||
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. */
|
||
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 %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 void bench_idct4(void) {
|
||
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 void bench_idct8(void) {
|
||
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 void bench_deblock_v(void) {
|
||
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, 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 void bench_qpel_mc20(void) {
|
||
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, g_sub,
|
||
qpel_dst, qpel_src, 16, 256, qpel_meta);
|
||
}
|
||
static void bench_qpel_mc22(void) {
|
||
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). */
|
||
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);
|
||
}
|
||
|
||
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;
|
||
}
|