Stage 2 PR-a: predicted samples plumbing — caller-supplied per-MB pixels

First concrete deliverable on the daedalus-decoder Stage 2 path post
the 2026-05-25 architecture re-pin (memory: dejavu / frame-major UMA).

Q2 decision: CPU intra prediction.  libavcodec's existing NEON intra
prediction kernels generate predicted samples per MB; daedalus-decoder
accepts those samples through the API and uses them as the IDCT-add
starting state.  FFmpeg's `idct_add` semantics — dst += idct(coeffs);
clip255 — fold DESIGN.md's Stage 3 reconstruction into the existing
Stage 1 IDCT dispatch for free.  No new GPU work.

API change
----------

`daedalus_decoder_mb_input` gains a `const uint8_t *predicted` field:

    predicted [  0 .. 256) — 16×16 luma, row-major raster
    predicted [256 .. 320) — 8×8  Cb,   row-major raster
    predicted [320 .. 384) — 8×8  Cr,   row-major raster

NULL is legal and equivalent to all-zero predicted samples — preserves
the existing IDCT-isolation test contract.

Internal changes
----------------

  - `daedalus_decoder` gains predicted_y (W×H) and predicted_uv (planar
    Cb||Cr, W×H/2) buffers allocated at create, zeroed at end of every
    flush_frame so NULL `mb->predicted` is indistinguishable from
    explicit zeros from one frame to the next.
  - `append_mb` splats mb->predicted into predicted_y/_uv at raster
    (mb_y*16, mb_x*16) for luma and (mb_y*8, mb_x*8) for each chroma
    component.
  - `flush_frame` replaces `calloc(scratch_y)` and `calloc(scratch_uv)`
    with `malloc + memcpy from predicted_y/_uv` — the IDCT dispatch
    then writes residual on top, clip-adding to the predicted samples
    in place.

Test
----

`test_idct_bitexact` extended:

  - Generates random predicted samples (uint8_t) per MB alongside the
    existing random coeffs.
  - Pre-fills the reference ref_y / ref_cb / ref_cr planes with those
    same predicted samples at the corresponding raster positions
    BEFORE applying ref_idct4_add / ref_idct8_add per block.
  - Compares GPU output to reference byte-for-byte.

Result on hertz (Pi 5 V3D 7.1), all three substrates:

  test_idct_bitexact 320 240 0xfeedface5a5a5a5a {cpu, qpu, auto}
    Y bytes diff:  0/76800 (0.0000%)
    Cb bytes diff: 0/19200 (0.0000%)
    Cr bytes diff: 0/19200 (0.0000%)
  BIT-EXACT PASS on all three substrates

Catches any silent drift between substrates and any predicted-samples
plumbing mistake on either the API or the dispatch side.

Followups
---------

  - Stage 2 PR-b: deblock dispatch in flush_frame.
  - Stage 2 daemon refactor (parallel, daedalus-v4l2 daemon): replace
    avcodec_send_packet/receive_frame with a libavcodec-parser-only
    path that drives daedalus_decoder_append_mb in raster order +
    flush_frame at slice boundary.
This commit is contained in:
2026-05-25 22:58:00 +02:00
parent 0b6482bc8f
commit a7a0d56ecd
4 changed files with 170 additions and 24 deletions
+63 -15
View File
@@ -1,12 +1,16 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* test_idct_bitexact — phase1 stage1 bit-exact gate for the frame-
* scaled luma IDCT 4×4 dispatch.
* scaled luma + chroma IDCT 4×4 / 8×8 dispatch + Stage 2 predicted-
* samples plumbing.
*
* Generates a frame of random coefficients, runs daedalus_decoder
* (with predicted=0 by the scaffold's flush_frame contract), and
* compares every output byte against an inline C reference that
* mirrors the H.264 §8.5.12.1 1D butterfly.
* Generates a frame of random coefficients AND random predicted
* samples per MB, runs daedalus_decoder (which writes the predicted
* samples into its frame-scoped predicted_y/_uv buffers via
* append_mb, then pre-fills the IDCT dispatch scratch from them in
* flush_frame), and compares every output byte against an inline C
* reference that mirrors the H.264 §8.5.12.1 1D butterfly applied
* to the same predicted+coeffs inputs.
*
* Why "bit-exact": the GPU shader and the C reference apply the same
* integer arithmetic. Any rounding / sign / overflow disagreement is
@@ -30,7 +34,7 @@
* Not in scope (covered by other tests / future PRs):
* - Chroma DC / Intra16x16 DC Hadamard pre-pass
* - bit-exactness against real H.264 streams (test-vector PR)
* - non-zero predicted pixels (intra prediction lands in Stage 2a)
* - deblock (lands in Stage 2 PR-b after this one)
*/
#include "daedalus_decoder.h"
@@ -202,15 +206,25 @@ int main(int argc, char **argv)
/* Build the per-MB inputs. Each MB gets 16 luma 4×4 blocks of
* random coeffs in [-512, 511] — same range as the daedalus-fourier
* cycle-6 M1 gate uses. */
int16_t (*per_mb_coeffs)[384] = malloc((size_t) n_mbs * sizeof(*per_mb_coeffs));
if (!per_mb_coeffs) { fprintf(stderr, "alloc fail\n"); return 1; }
* cycle-6 M1 gate uses. Plus random predicted samples (uint8 each)
* to exercise the Stage 2 predicted-samples plumbing — when this
* is non-zero, flush_frame must pre-fill the IDCT-dispatch scratch
* from dec->predicted_y / dec->predicted_uv (Stage 2 PR-a) rather
* than from calloc-zero (the Stage 1 scaffold contract). The
* reference path mirrors this by pre-filling ref_y / ref_cb / ref_cr
* from the same predicted bytes BEFORE the per-block ref_idct*_add
* calls — so the test catches any mismatch between caller-supplied
* predicted and what reaches the GPU's IDCT-add starting state. */
int16_t (*per_mb_coeffs)[384] = malloc((size_t) n_mbs * sizeof(*per_mb_coeffs));
uint8_t (*per_mb_predicted)[384] = malloc((size_t) n_mbs * sizeof(*per_mb_predicted));
if (!per_mb_coeffs || !per_mb_predicted) { fprintf(stderr, "alloc fail\n"); return 1; }
for (int mb = 0; mb < n_mbs; mb++) {
for (int i = 0; i < 384; i++) {
/* Random coeffs in [-512, 511] for all of luma + Cb + Cr.
* Same range as the daedalus-fourier cycle-6 M1 gate. */
/* Random coeffs in [-512, 511] for all of luma + Cb + Cr. */
per_mb_coeffs[mb][i] = (int16_t)((int)(xs64() % 1024) - 512);
/* Random predicted samples in [0, 255]. */
per_mb_predicted[mb][i] = (uint8_t)(xs64() & 0xff);
}
}
@@ -230,6 +244,7 @@ int main(int argc, char **argv)
mb.mb_x = (uint16_t) mx;
mb.mb_y = (uint16_t) my;
mb.coeffs = per_mb_coeffs[idx];
mb.predicted = per_mb_predicted[idx];
mb.transform_8x8 = mb_8x8[idx];
if (mb_8x8[idx]) n_8x8_mbs++; else n_4x4_mbs++;
if (daedalus_decoder_append_mb(dec, &mb) != 0) {
@@ -256,9 +271,26 @@ int main(int argc, char **argv)
}
/* Compute the reference output: same per-MB → flat raster block
* layout as flush_frame uses. Branch per MB on transform_8x8. */
uint8_t *ref_y = calloc(1, y_size);
* layout as flush_frame uses. Branch per MB on transform_8x8.
*
* ref_y is pre-filled with each MB's 16×16 luma predicted samples
* at raster (my*16, mx*16), then ref_idct4_add/8_add overlay the
* residual via FFmpeg `idct_add` semantics (dst += idct(coeffs);
* clip255). This mirrors what flush_frame does on the GPU side:
* scratch_y starts from dec->predicted_y, IDCT-add writes back. */
uint8_t *ref_y = malloc(y_size);
if (!ref_y) return 1;
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
int mb_idx = my * mb_w + mx;
const uint8_t *p_y = per_mb_predicted[mb_idx]; /* [0..256) */
for (int r = 0; r < 16; r++) {
memcpy(&ref_y[((size_t) my * 16 + r) * (size_t) width
+ (size_t) mx * 16],
&p_y[r * 16], 16);
}
}
}
int16_t block_scratch[64]; /* large enough for 8x8 */
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
@@ -302,9 +334,24 @@ int main(int argc, char **argv)
size_t chroma_w = (size_t) width / 2;
size_t chroma_h = (size_t) height / 2;
size_t chroma_plane_size = chroma_w * chroma_h;
uint8_t *ref_cb = calloc(1, chroma_plane_size);
uint8_t *ref_cr = calloc(1, chroma_plane_size);
uint8_t *ref_cb = malloc(chroma_plane_size);
uint8_t *ref_cr = malloc(chroma_plane_size);
if (!ref_cb || !ref_cr) return 1;
/* Pre-fill ref_cb / ref_cr with per-MB 8x8 chroma predicted samples
* (mirrors the predicted-samples plumbing on the chroma path). */
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
int mb_idx = my * mb_w + mx;
const uint8_t *p_cb = per_mb_predicted[mb_idx] + 256;
const uint8_t *p_cr = per_mb_predicted[mb_idx] + 256 + 64;
for (int r = 0; r < 8; r++) {
memcpy(&ref_cb[((size_t) my * 8 + r) * chroma_w + (size_t) mx * 8],
&p_cb[r * 8], 8);
memcpy(&ref_cr[((size_t) my * 8 + r) * chroma_w + (size_t) mx * 8],
&p_cr[r * 8], 8);
}
}
}
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
int mb_idx = my * mb_w + mx;
@@ -386,6 +433,7 @@ int main(int argc, char **argv)
free(gpu_uv);
free(gpu_y);
free(mb_8x8);
free(per_mb_predicted);
free(per_mb_coeffs);
daedalus_decoder_destroy(dec);