diff --git a/CMakeLists.txt b/CMakeLists.txt index 41e57f3..387772e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -175,7 +175,31 @@ target_compile_options(bench_flush_frame PRIVATE -O2) # so the standard ctest build doesn't pull in FFmpeg as a hard dep. option(DAEDALUS_BUILD_TOOLS "Build daedalus-decoder CLI tools (requires libavcodec)" OFF) if(DAEDALUS_BUILD_TOOLS) - pkg_check_modules(FFMPEG REQUIRED libavcodec libavformat libavutil) + # Optional path to a private FFmpeg install carrying the per-MB + # inspection callback (marfrit-packages patch 0016). When set, + # the CLI links against it instead of the system FFmpeg and the + # inspection-callback code path is compiled in. + set(DAEDALUS_FFMPEG_PREFIX "" CACHE PATH + "Path to a patched FFmpeg install (with 0016 mb-inspect-callback) for daedalus_decode_h264. Empty = use system pkg-config FFmpeg.") + + if(DAEDALUS_FFMPEG_PREFIX) + message(STATUS "daedalus_decode_h264: patched FFmpeg at ${DAEDALUS_FFMPEG_PREFIX}") + set(FFMPEG_INCLUDE_DIRS ${DAEDALUS_FFMPEG_PREFIX}/include) + set(FFMPEG_LIBRARY_DIRS ${DAEDALUS_FFMPEG_PREFIX}/lib) + # Patched libavcodec is built static (no shared libs in the private prefix). + # System pull-ins are still needed for libav* dependencies. + set(FFMPEG_LIBRARIES + ${DAEDALUS_FFMPEG_PREFIX}/lib/libavformat.a + ${DAEDALUS_FFMPEG_PREFIX}/lib/libavcodec.a + ${DAEDALUS_FFMPEG_PREFIX}/lib/libavutil.a + ${DAEDALUS_FFMPEG_PREFIX}/lib/libswresample.a + m z pthread) + set(FFMPEG_CFLAGS_OTHER "-DDAEDALUS_HAVE_H264_MB_INSPECT_CB=1") + else() + pkg_check_modules(FFMPEG REQUIRED libavcodec libavformat libavutil) + message(STATUS "daedalus_decode_h264: system FFmpeg (no inspection callback)") + endif() + add_executable(daedalus_decode_h264 tools/daedalus_decode_h264.c) target_link_libraries(daedalus_decode_h264 PRIVATE daedalus_decoder ${FFMPEG_LIBRARIES}) diff --git a/tools/daedalus_decode_h264.c b/tools/daedalus_decode_h264.c index cfccca9..ca932c8 100644 --- a/tools/daedalus_decode_h264.c +++ b/tools/daedalus_decode_h264.c @@ -50,6 +50,22 @@ #include #include +/* Per-MB inspection callback API — provided by the patched FFmpeg + * fork via marfrit-packages 0016. The H264Context struct itself + * remains internal (declared in libavcodec/h264dec.h which isn't + * installed), so we only forward-declare it here and use it + * opaquely through the callback signature. Real per-MB state + * extraction (sl->mb coefficients, mb_type, etc.) will land in + * PR-A3 alongside an internal-header include path. */ +#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB +struct H264Context; +typedef void (*ff_h264_mb_inspect_cb)(void *opaque, + const struct H264Context *h, + int mb_x, int mb_y); +void ff_h264_set_mb_inspect_cb(AVCodecContext *avctx, + ff_h264_mb_inspect_cb cb, void *opaque); +#endif + #include #include #include @@ -59,6 +75,42 @@ static const char *substrate_str = "auto"; static int max_frames = -1; +/* Inspection-callback state: per-frame counter + "each MB seen exactly + * once" check. We use a bitmap rather than a raster-order assertion + * because libavcodec's MB-level threading + multi-slice frames mean + * MBs reach the callback in non-strictly-raster order; the contract + * is "every MB fires the callback exactly once per frame", not "in + * raster order". Reset at end of each frame. */ +#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB +struct inspect_state { + int n_cbs_this_frame; + int mb_w, mb_h; + uint8_t *seen; /* mb_w * mb_h bitmap */ + int duplicate_mbs; /* same (mb_x, mb_y) seen twice this frame */ + int out_of_bounds; /* (mb_x, mb_y) outside the coded grid */ +}; + +static void inspect_cb(void *opaque, + const struct H264Context *h, + int mb_x, int mb_y) +{ + (void) h; + struct inspect_state *st = opaque; + + if (mb_x < 0 || mb_x >= st->mb_w || mb_y < 0 || mb_y >= st->mb_h) { + if (st->out_of_bounds < 4) + fprintf(stderr, " OOB cb: mb=(%d,%d) grid=%dx%d\n", + mb_x, mb_y, st->mb_w, st->mb_h); + st->out_of_bounds++; + } else { + const size_t idx = (size_t) mb_y * st->mb_w + (size_t) mb_x; + if (st->seen[idx]) st->duplicate_mbs++; + st->seen[idx] = 1; + } + st->n_cbs_this_frame++; +} +#endif + /* Extract one MB's predicted-samples block from a YUV420P AVFrame * (stock libavcodec) and pack it into the 384-byte mb_input.predicted * layout: 16x16 luma raster, then 8x8 Cb raster, then 8x8 Cr raster. @@ -206,6 +258,15 @@ int main(int argc, char **argv) AVPacket *pkt = av_packet_alloc(); AVFrame *fr = av_frame_alloc(); +#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB + struct inspect_state inspect_st = {0}; + ff_h264_set_mb_inspect_cb(avctx, inspect_cb, &inspect_st); + int inspect_total_cbs = 0; + int inspect_total_duplicate = 0; + int inspect_total_oob = 0; + int inspect_total_missing = 0; +#endif + /* ---- Create daedalus_decoder. Coded width/height come from * the bitstream's SPS via libavcodec (after the first packet * is decoded — defer creation until then). ---- */ @@ -269,6 +330,15 @@ int main(int argc, char **argv) } printf("daedalus_decode_h264: %dx%d, substrate=%s\n", W, H, substrate_str); +#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB + inspect_st.mb_w = W / 16; + inspect_st.mb_h = H / 16; + inspect_st.seen = calloc(1, (size_t) inspect_st.mb_w * inspect_st.mb_h); + if (!inspect_st.seen) { rc = 1; goto cleanup; } + printf(" inspection callback: ACTIVE (patched libavcodec)\n"); +#else + printf(" inspection callback: not built in (stock libavcodec)\n"); +#endif } /* Pack each MB's predicted samples from the AVFrame. @@ -320,6 +390,33 @@ int main(int argc, char **argv) if (out_uv_dadec[i] != out_uv_ref[i]) uv_diffs++; total_y_diffs += y_diffs; total_uv_diffs += uv_diffs; +#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB + { + const int expected = mb_w * mb_h; + /* Count MBs that fired the callback. */ + int seen_count = 0; + for (int i = 0; i < expected; i++) + if (inspect_st.seen[i]) seen_count++; + int missing = expected - seen_count; + if (missing || inspect_st.duplicate_mbs || inspect_st.out_of_bounds) { + fprintf(stderr, + " frame %d: callback invariants: fired=%d expected=%d " + "missing=%d duplicates=%d oob=%d\n", + n_frames, inspect_st.n_cbs_this_frame, expected, + missing, inspect_st.duplicate_mbs, inspect_st.out_of_bounds); + rc = 4; + } + inspect_total_cbs += inspect_st.n_cbs_this_frame; + inspect_total_duplicate += inspect_st.duplicate_mbs; + inspect_total_oob += inspect_st.out_of_bounds; + inspect_total_missing += missing; + /* Reset for next frame. */ + inspect_st.n_cbs_this_frame = 0; + inspect_st.duplicate_mbs = 0; + inspect_st.out_of_bounds = 0; + memset(inspect_st.seen, 0, (size_t) expected); + } +#endif printf(" frame %d: Y diff %zu/%zu UV diff %zu/%zu%s\n", n_frames, y_diffs, y_size, uv_diffs, uv_size, (y_diffs || uv_diffs) ? " ***" : ""); @@ -348,11 +445,21 @@ int main(int argc, char **argv) drained: printf("\n%d frames decoded; total Y diff %zu, UV diff %zu\n", n_frames, total_y_diffs, total_uv_diffs); - if (total_y_diffs || total_uv_diffs) { +#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB + printf("inspection callback: %d total invocations, %d missing, %d duplicates, %d oob\n", + inspect_total_cbs, inspect_total_missing, inspect_total_duplicate, inspect_total_oob); + if (inspect_total_missing || inspect_total_duplicate || inspect_total_oob) + rc = 4; +#endif + if (rc == 0 && (total_y_diffs || total_uv_diffs)) { printf("FAIL: daedalus-decoder output does NOT match libavcodec reference byte-for-byte\n"); rc = 4; - } else { + } else if (rc == 0) { printf("PASS: byte-exact identity-passthrough across %d frames\n", n_frames); + } else { + printf("FAIL: %s\n", + (total_y_diffs || total_uv_diffs) ? "byte-exact comparison failed" + : "inspection callback invariants violated"); } cleanup: @@ -360,6 +467,9 @@ cleanup: if (out_ref_f) fclose(out_ref_f); free(out_uv_ref); free(out_y_ref); free(out_uv_dadec);free(out_y_dadec); +#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB + free(inspect_st.seen); +#endif if (dec) daedalus_decoder_destroy(dec); av_frame_free(&fr); av_packet_free(&pkt);