Merge pull request 'phase1: substrate selector API + cross-substrate bit-exact ctest' (#9) from noether/phase1-substrate-select into main

Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
2026-05-24 21:14:05 +00:00
6 changed files with 116 additions and 11 deletions
+8
View File
@@ -121,6 +121,14 @@ target_compile_options(test_idct_bitexact PRIVATE -O2)
# 320x240 QVGA — fast inner-loop test (300 MBs, sub-second).
add_test(NAME idct_bitexact COMMAND test_idct_bitexact)
# Same QVGA test re-run on the CPU NEON path (forces fallback even on
# V3D7 hosts). Catches silent drift between the V3D shader and the
# NEON reference path — both must produce identical output for the
# same coefficient input. Also keeps the bit-exact gate alive on
# hosts without V3D7 (CI runners, x86 dev boxes).
add_test(NAME idct_bitexact_cpu COMMAND test_idct_bitexact 320 240
0xfeedface5a5a5a5a cpu)
# 1920x1088 1080p — deployment-scale test (8160 MBs, ~0.25 s on hertz).
# Validates the per-MB block index + pixel offset math at full coded
# height (1088, not 1080 — see daedalus_decoder.h on H.264 coded vs
+33
View File
@@ -99,6 +99,33 @@ typedef enum {
DAEDALUS_DECODER_OUTPUT_RGBA = 1, /* Stage 5 opt-in */
} daedalus_decoder_output_format;
/* -------------------------------------------------------------------
* Substrate selector. Determines which backend daedalus-fourier
* dispatches the per-frame compute through.
*
* AUTO is the only sensible choice for production — it picks per the
* recipe table baked into daedalus-fourier (post 2026-05-23 decree:
* QPU when a V3D shader exists, CPU NEON otherwise). The explicit
* options exist for testing:
*
* - CPU forces the dispatch onto the NEON path even when V3D7 is
* available. Lets the bit-exact ctests run on hosts without a
* working Vulkan/V3D stack (CI runners, dev x86 boxes via
* cross-build), and lets us cross-check the V3D shader output
* against the NEON reference path on hosts that DO have V3D.
* - QPU is the dual — force QPU even on a CPU-preferred kernel.
* Useful for benchmarking specific QPU paths in isolation.
*
* A non-AUTO selection on a host that can't satisfy it
* (DAEDALUS_DECODER_SUBSTRATE_QPU on an x86 dev box) propagates a
* dispatch failure back through flush_frame as -3.
* ----------------------------------------------------------------- */
typedef enum {
DAEDALUS_DECODER_SUBSTRATE_AUTO = 0,
DAEDALUS_DECODER_SUBSTRATE_CPU = 1,
DAEDALUS_DECODER_SUBSTRATE_QPU = 2,
} daedalus_decoder_substrate;
/* -------------------------------------------------------------------
* Lifecycle
* ----------------------------------------------------------------- */
@@ -128,6 +155,12 @@ void daedalus_decoder_destroy(daedalus_decoder *dec);
int daedalus_decoder_set_output_format(daedalus_decoder *dec,
daedalus_decoder_output_format fmt);
/* Override the dispatch substrate for subsequent flush_frame calls.
* Default is AUTO. Same mid-frame-change restriction as
* set_output_format. */
int daedalus_decoder_set_substrate(daedalus_decoder *dec,
daedalus_decoder_substrate sub);
/* -------------------------------------------------------------------
* Per-frame submission
* ----------------------------------------------------------------- */
+40 -11
View File
@@ -41,6 +41,7 @@ daedalus_decoder *daedalus_decoder_create(int width, int height)
dec->mb_height = height >> 4;
dec->n_mbs = dec->mb_width * dec->mb_height;
dec->output_fmt = DAEDALUS_DECODER_OUTPUT_NV12;
dec->substrate = DAEDALUS_DECODER_SUBSTRATE_AUTO;
/* daedalus-fourier ctx — required. Phase 1 needs the QPU; if
* Vulkan init fails the decoder is unusable. Caller can check
@@ -86,6 +87,33 @@ int daedalus_decoder_set_output_format(daedalus_decoder *dec,
return 0;
}
int daedalus_decoder_set_substrate(daedalus_decoder *dec,
daedalus_decoder_substrate sub)
{
if (!dec)
return -1;
if (dec->mbs_appended != 0)
return -1;
if (sub != DAEDALUS_DECODER_SUBSTRATE_AUTO &&
sub != DAEDALUS_DECODER_SUBSTRATE_CPU &&
sub != DAEDALUS_DECODER_SUBSTRATE_QPU)
return -1;
dec->substrate = sub;
return 0;
}
/* Map our public substrate enum onto daedalus-fourier's. Same
* ordering by intent — we duplicate the enum for ABI isolation. */
static daedalus_substrate map_substrate(daedalus_decoder_substrate s)
{
switch (s) {
case DAEDALUS_DECODER_SUBSTRATE_CPU: return DAEDALUS_SUBSTRATE_CPU;
case DAEDALUS_DECODER_SUBSTRATE_QPU: return DAEDALUS_SUBSTRATE_QPU;
case DAEDALUS_DECODER_SUBSTRATE_AUTO:
default: return DAEDALUS_SUBSTRATE_AUTO;
}
}
int daedalus_decoder_append_mb(daedalus_decoder *dec,
const struct daedalus_decoder_mb_input *mb)
{
@@ -267,16 +295,17 @@ int daedalus_decoder_flush_frame(daedalus_decoder *dec,
* Skipping the dispatch when the partition is empty avoids the
* shader-pool warm-up cost on the common case (a typical Baseline
* stream is all-4x4 → 8x8 dispatch is no-op). */
const daedalus_substrate sub = map_substrate(dec->substrate);
if (bi4 > 0) {
int dr = daedalus_recipe_dispatch_h264_idct4(dec->dctx,
scratch_y, y_stride_int,
coeffs4, bi4, meta4);
int dr = daedalus_dispatch_h264_idct4(dec->dctx, sub,
scratch_y, y_stride_int,
coeffs4, bi4, meta4);
if (dr != 0) { rc = -3; goto cleanup; }
}
if (bi8 > 0) {
int dr = daedalus_recipe_dispatch_h264_idct8(dec->dctx,
scratch_y, y_stride_int,
coeffs8, bi8, meta8);
int dr = daedalus_dispatch_h264_idct8(dec->dctx, sub,
scratch_y, y_stride_int,
coeffs8, bi8, meta8);
if (dr != 0) { rc = -3; goto cleanup; }
}
@@ -361,11 +390,11 @@ int daedalus_decoder_flush_frame(daedalus_decoder *dec,
}
/* assert cbi == n_chroma_blocks; loop math guarantees it */
int cr_rc = daedalus_recipe_dispatch_h264_idct4(dec->dctx,
scratch_uv, chroma_w,
chroma_coeffs,
n_chroma_blocks,
chroma_meta);
int cr_rc = daedalus_dispatch_h264_idct4(dec->dctx, sub,
scratch_uv, chroma_w,
chroma_coeffs,
n_chroma_blocks,
chroma_meta);
if (cr_rc != 0) {
rc = -3;
goto chroma_cleanup;
+3
View File
@@ -64,6 +64,9 @@ struct daedalus_decoder {
/* Output format. */
daedalus_decoder_output_format output_fmt;
/* Dispatch substrate (AUTO by default — recipe-table-driven). */
daedalus_decoder_substrate substrate;
};
#endif /* DAEDALUS_DECODER_INTERNAL_H */
+22
View File
@@ -166,6 +166,23 @@ int main(int argc, char **argv)
uint64_t seed = argc > 3 ? strtoull(argv[3], NULL, 0) : 0xfeedface5a5a5a5aULL;
xs64_state = seed;
/* Optional 4th argv: "auto" (default) / "cpu" / "qpu" to pin the
* dispatch substrate. Both substrates must produce IDENTICAL
* output (the V3D shaders are bit-exact gates against the same
* spec the NEON path implements); the ctest suite runs the QVGA
* test once per substrate to catch any silent drift. */
daedalus_decoder_substrate sub = DAEDALUS_DECODER_SUBSTRATE_AUTO;
const char *sub_name = "auto";
if (argc > 4) {
if (!strcmp(argv[4], "cpu")) { sub = DAEDALUS_DECODER_SUBSTRATE_CPU; sub_name = "cpu"; }
else if (!strcmp(argv[4], "qpu")) { sub = DAEDALUS_DECODER_SUBSTRATE_QPU; sub_name = "qpu"; }
else if (!strcmp(argv[4], "auto")) { /* default */ }
else {
fprintf(stderr, "unknown substrate '%s' (want auto/cpu/qpu)\n", argv[4]);
return 1;
}
}
int mb_w = width / 16;
int mb_h = height / 16;
int n_mbs = mb_w * mb_h;
@@ -177,6 +194,11 @@ int main(int argc, char **argv)
fprintf(stderr, "SKIP: ctx create failed (Vulkan / V3D7 unavailable)\n");
return 0;
}
if (daedalus_decoder_set_substrate(dec, sub) != 0) {
fprintf(stderr, "set_substrate(%s) failed\n", sub_name);
return 1;
}
printf("substrate: %s\n", sub_name);
/* 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
+10
View File
@@ -52,6 +52,16 @@ int main(void)
EXPECT(daedalus_decoder_set_output_format(dec, DAEDALUS_DECODER_OUTPUT_NV12) == 0,
"switch back to NV12");
/* Substrate setter — same lifecycle rules. */
EXPECT(daedalus_decoder_set_substrate(dec, DAEDALUS_DECODER_SUBSTRATE_CPU) == 0,
"force CPU substrate on virgin ctx");
EXPECT(daedalus_decoder_set_substrate(dec, DAEDALUS_DECODER_SUBSTRATE_QPU) == 0,
"force QPU substrate on virgin ctx");
EXPECT(daedalus_decoder_set_substrate(dec, DAEDALUS_DECODER_SUBSTRATE_AUTO) == 0,
"back to AUTO");
EXPECT(daedalus_decoder_set_substrate(dec, (daedalus_decoder_substrate) 99) == -1,
"bogus substrate rejects");
/* Append rejects out-of-bounds + null inputs. */
int16_t coeffs[384] = {0};
struct daedalus_decoder_mb_input mb = {0};