wip: real coeffs extraction

This commit is contained in:
2026-05-26 09:56:29 +02:00
parent 69d68e0323
commit 141b708802
2 changed files with 249 additions and 19 deletions
+24
View File
@@ -195,6 +195,30 @@ if(DAEDALUS_BUILD_TOOLS)
${DAEDALUS_FFMPEG_PREFIX}/lib/libswresample.a
m z pthread)
set(FFMPEG_CFLAGS_OTHER "-DDAEDALUS_HAVE_H264_MB_INSPECT_CB=1")
# PR-A3+ optional: also point at the patched FFmpeg SOURCE TREE
# so the CLI can include libavcodec/h264dec.h directly and
# dereference H264Context fields (the side-buffer mb_inspect_coeffs
# added in marfrit-packages patch 0017, the cur_pic.f for
# pre-deblock pixel access, etc.). When set, the internal-header
# include codepath is compiled in.
set(DAEDALUS_FFMPEG_SRC "" CACHE PATH
"Path to patched FFmpeg source tree (= path to FFmpeg/ checkout where build was run; contains config.h + libavcodec/h264dec.h). Empty = h264dec.h includes are disabled.")
if(DAEDALUS_FFMPEG_SRC)
message(STATUS "daedalus_decode_h264: FFmpeg source at ${DAEDALUS_FFMPEG_SRC}")
# IMPORTANT: source tree FIRST in -I order — its
# libavutil/common.h does #include "intmath.h" with HAVE_AV_CONFIG_H,
# which resolves to libavutil/intmath.h (in the source tree
# only — that header isn't installed since it's arch-dispatched).
# The installed-prefix include path's libavutil/common.h is the
# same file textually but resolves "intmath.h" against the
# install dir where it doesn't exist.
set(FFMPEG_INCLUDE_DIRS ${DAEDALUS_FFMPEG_SRC})
set(FFMPEG_CFLAGS_OTHER
"${FFMPEG_CFLAGS_OTHER} -DDAEDALUS_HAVE_H264_MB_INSPECT_COEFFS=1 -DHAVE_AV_CONFIG_H")
# Convert space-separated string to list (CMake idiom for compile flags).
separate_arguments(FFMPEG_CFLAGS_OTHER UNIX_COMMAND "${FFMPEG_CFLAGS_OTHER}")
endif()
else()
pkg_check_modules(FFMPEG REQUIRED libavcodec libavformat libavutil)
message(STATUS "daedalus_decode_h264: system FFmpeg (no inspection callback)")
+225 -19
View File
@@ -51,14 +51,32 @@
#include <libavutil/imgutils.h>
/* 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
* fork via marfrit-packages patches 0016 + 0017.
*
* When DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS is defined (CMake sets it
* alongside DAEDALUS_FFMPEG_SRC), we include libavcodec's INTERNAL
* h264dec.h header to dereference H264Context fields — specifically
* h->mb_inspect_coeffs (the 0017 side buffer holding pre-IDCT-
* destruction sl->mb), h->cur_pic.f (pre-deblock reconstructed pixels),
* and h->cur_pic.mb_type[mb_xy] for the mb-type gate. The same
* configure-time config.h that built the static libavcodec.a is
* picked up via -DHAVE_AV_CONFIG_H + -I path; ABI match is automatic.
*
* When only DAEDALUS_HAVE_H264_MB_INSPECT_CB is defined (no source
* tree available — e.g. building against a distro-shipped patched
* libavcodec), the H264Context stays opaque and we fall back to
* identity-passthrough across all MBs.
*
* When neither is defined: stock libavcodec, no callback, identity-
* passthrough only (PR-A1b behaviour). */
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
# include "libavcodec/h264dec.h"
# include "libavcodec/h264.h" /* IS_INTRA4x4 / IS_8x8DCT / IS_INTRA_PCM */
#elif defined(DAEDALUS_HAVE_H264_MB_INSPECT_CB)
struct H264Context;
#endif
#if defined(DAEDALUS_HAVE_H264_MB_INSPECT_CB) || defined(DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS)
typedef void (*ff_h264_mb_inspect_cb)(void *opaque,
const struct H264Context *h,
int mb_x, int mb_y);
@@ -76,35 +94,170 @@ 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. */
* once" check. Bitmap, not raster-order — libavcodec's MB threading +
* multi-slice frames mean MBs reach the callback out of strict order;
* contract is "every MB fires the callback exactly once per frame".
*
* When real-coeff extraction is compiled in (PR-A3+), we ALSO maintain
* a per-MB capture buffer (real-coeffs path) so the main loop can
* drive daedalus_decoder_append_mb with REAL pre-residual P + real
* coefficients for MBs that satisfy the gate (Intra_4x4, no 8x8 DCT,
* no PCM). Other MBs stay on identity-passthrough. */
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB
struct mb_capture {
int valid; /* 1 = real-coeffs path, 0 = identity passthrough */
int16_t coeffs[256]; /* luma, column-major within 4x4, raster block order */
uint8_t predicted[256]; /* luma P recovered = pre_deblock - clipped IDCT(C) */
};
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 */
int duplicate_mbs;
int out_of_bounds;
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
struct mb_capture *captures; /* mb_w * mb_h entries */
int real_coeffs_mbs; /* count of MBs in real-coeffs path this frame */
int skipped_intra16x16;
int skipped_8x8dct;
int skipped_other;
#endif
};
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
/* Zig-zag scan position → column-major-within-4x4 idx.
* daedalus convention: block[c*4 + r] = coeff at (row=r, col=c).
* H.264 §6.4.3 / fig 6-12 4x4 luma zig-zag scan order. */
static const uint8_t zigzag_to_colmajor[16] = {
0, 4, 1, 2, 5, 8, 12, 9, 6, 3, 7, 10, 13, 14, 11, 15
};
/* H.264 §6.4.3 4x4 luma block scan within MB (z-scan).
* Maps raster-block-idx (sb_y*4+sb_x) → libavcodec sl->mb's z-scan idx.
* Z-scan happens to be its own inverse (symmetric mapping). */
static const uint8_t raster_to_zscan[16] = {
0, 1, 4, 5, 2, 3, 6, 7, 8, 9, 12, 13, 10, 11, 14, 15
};
/* H.264 4x4 IDCT — transcribed from daedalus-fourier
* tests/test_idct_bitexact.c (which itself mirrors h264_idct4_ref.c).
* Outputs row-major 16-element residual; clip + shift happens in
* the consumer. */
static void h264_idct4_butterfly(const int d[4], int out[4]) {
int e = d[0] + d[2];
int f = d[0] - d[2];
int g = (d[1] >> 1) - d[3];
int h = d[1] + (d[3] >> 1);
out[0] = e + h;
out[1] = f + g;
out[2] = f - g;
out[3] = e - h;
}
static void ref_idct4_compute(const int16_t block[16], int out[16]) {
/* block COLUMN-MAJOR: block[c*4+r] = coef at (row=r, col=c). */
int tmp[4][4];
for (int r = 0; r < 4; r++) {
int d[4] = { block[0*4+r], block[1*4+r], block[2*4+r], block[3*4+r] };
int o[4];
h264_idct4_butterfly(d, o);
for (int c = 0; c < 4; c++) tmp[r][c] = o[c];
}
for (int c = 0; c < 4; c++) {
int d[4] = { tmp[0][c], tmp[1][c], tmp[2][c], tmp[3][c] };
int o[4];
h264_idct4_butterfly(d, o);
for (int r = 0; r < 4; r++) out[r*4+c] = o[r];
}
}
#endif /* DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS */
static void inspect_cb(void *opaque,
const struct H264Context *h,
int mb_x, int mb_y)
{
(void) h;
struct inspect_state *st = opaque;
#ifndef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
(void) h;
#endif
if (mb_x < 0 || mb_x >= st->mb_w || mb_y < 0 || mb_y >= 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++;
return;
}
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++;
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
/* Real-coeffs path: extract per-MB state for daedalus-decoder
* IDCT validation on this MB. Gate: only Intra_4x4 + 4x4 transform
* + non-PCM is supported in PR-A3b — other MB flavours fall back
* to identity-passthrough in the main loop. */
struct mb_capture *cap = &st->captures[idx];
cap->valid = 0; /* default to passthrough */
const int mb_xy = mb_y * h->mb_stride + mb_x;
const uint32_t mb_type = h->cur_pic.mb_type[mb_xy];
if (!IS_INTRA4x4(mb_type)) {
if (IS_INTRA16x16(mb_type)) st->skipped_intra16x16++;
else st->skipped_other++;
return;
}
if (IS_8x8DCT(mb_type)) { st->skipped_8x8dct++; return; }
if (IS_INTRA_PCM(mb_type)) { st->skipped_other++; return; }
/* Snapshot luma pre-deblock pixels from cur_pic. */
const uint8_t *luma_plane = h->cur_pic.f->data[0];
const int luma_stride = h->cur_pic.f->linesize[0];
const uint8_t *mb_pixels = luma_plane + (ptrdiff_t) mb_y * 16 * luma_stride
+ mb_x * 16;
/* Coefficients are in sl->mb at end of entropy decode but zeroed by
* the time the callback fires (IDCT-add consumed them). Patch 0017
* preserves them in h->mb_inspect_coeffs[16 * 48] BEFORE IDCT runs,
* so we read from there. */
const int16_t *zz_mb = h->mb_inspect_coeffs; /* layout matches sl->mb 8-bit half */
for (int r_block = 0; r_block < 16; r_block++) {
const int z_block = raster_to_zscan[r_block];
const int16_t *zz_block = &zz_mb[z_block * 16];
/* Inverse zig-zag scan → column-major-within-block. */
int16_t col[16] = {0};
for (int p = 0; p < 16; p++)
col[zigzag_to_colmajor[p]] = zz_block[p];
memcpy(&cap->coeffs[r_block * 16], col, 16 * sizeof(int16_t));
/* IDCT → row-major 16-int residual. */
int idct_row[16];
ref_idct4_compute(col, idct_row);
/* P = clip(pre_deblock - ((IDCT + 32) >> 6)) for each pixel.
* Symmetric: daedalus IDCT-add will undo the subtract, including
* for saturating cases (where the same shift puts the value back
* at the same clip boundary). */
const int sb_y = r_block >> 2;
const int sb_x = r_block & 3;
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
const int pre_db = mb_pixels[(sb_y * 4 + r) * luma_stride + sb_x * 4 + c];
const int shift = (idct_row[r * 4 + c] + 32) >> 6;
int p = pre_db - shift;
if (p < 0) p = 0;
if (p > 255) p = 255;
cap->predicted[(sb_y * 4 + r) * 16 + (sb_x * 4 + c)] = (uint8_t) p;
}
}
}
cap->valid = 1;
st->real_coeffs_mbs++;
#endif
}
#endif
@@ -247,6 +400,17 @@ int main(int argc, char **argv)
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
AVCodecContext *avctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(avctx, fmt->streams[vstream]->codecpar);
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
/* Patch 0017's coefficient side buffer lives in H264Context (single
* per-stream); multi-threaded slice decode would race on it. Force
* single-thread. Also disable libavcodec's deblock so AVFrame is
* pre-deblock and the P-recovery math is exact. */
avctx->thread_count = 1;
avctx->thread_type = 0;
avctx->skip_loop_filter = AVDISCARD_ALL;
#endif
if (avcodec_open2(avctx, codec, NULL) < 0) {
fprintf(stderr, "avcodec_open2 failed\n");
avformat_close_input(&fmt); return 2;
@@ -280,6 +444,11 @@ int main(int argc, char **argv)
inspect_st.mb_h = H_round / 16;
inspect_st.seen = calloc(1, (size_t) inspect_st.mb_w * inspect_st.mb_h);
if (!inspect_st.seen) { rc = 1; goto cleanup; }
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
inspect_st.captures = calloc((size_t) inspect_st.mb_w * inspect_st.mb_h,
sizeof(*inspect_st.captures));
if (!inspect_st.captures) { rc = 1; goto cleanup; }
#endif
}
ff_h264_set_mb_inspect_cb(avctx, inspect_cb, &inspect_st);
int inspect_total_cbs = 0;
@@ -363,7 +532,26 @@ int main(int argc, char **argv)
struct daedalus_decoder_mb_input mb = {0};
for (int my = 0; my < mb_h; my++) {
for (int mx = 0; mx < mb_w; mx++) {
/* Default: identity-passthrough — luma from AVFrame,
* chroma from AVFrame, coeffs all zero. */
pack_mb_predicted(fr, mx, my, mb_pred);
memset(mb_coeffs, 0, sizeof(mb_coeffs));
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
/* Real-coeffs path: if the callback captured this MB
* as Intra_4x4 / 4x4-DCT, override luma predicted
* with the recovered P and use the real luma coeffs.
* Chroma stays identity-passthrough (PR-A3b scope —
* chroma DC Hadamard + 8x8 transform follow-ups). */
const int mb_idx = my * mb_w + mx;
const struct mb_capture *cap = &inspect_st.captures[mb_idx];
if (cap->valid) {
memcpy(mb_pred, cap->predicted, 256);
for (int i = 0; i < 256; i++)
mb_coeffs[i] = cap->coeffs[i];
}
#endif
mb.mb_x = (uint16_t) mx;
mb.mb_y = (uint16_t) my;
mb.transform_8x8 = 0;
@@ -424,6 +612,21 @@ int main(int argc, char **argv)
inspect_st.duplicate_mbs = 0;
inspect_st.out_of_bounds = 0;
memset(inspect_st.seen, 0, (size_t) expected);
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
printf(" frame %d: real-coeffs path %d MBs, "
"skipped intra16x16=%d 8x8dct=%d other=%d\n",
n_frames, inspect_st.real_coeffs_mbs,
inspect_st.skipped_intra16x16,
inspect_st.skipped_8x8dct,
inspect_st.skipped_other);
inspect_st.real_coeffs_mbs = 0;
inspect_st.skipped_intra16x16 = 0;
inspect_st.skipped_8x8dct = 0;
inspect_st.skipped_other = 0;
memset(inspect_st.captures, 0,
(size_t) expected * sizeof(*inspect_st.captures));
#endif
}
#endif
printf(" frame %d: Y diff %zu/%zu UV diff %zu/%zu%s\n",
@@ -478,6 +681,9 @@ cleanup:
free(out_uv_dadec);free(out_y_dadec);
#ifdef DAEDALUS_HAVE_H264_MB_INSPECT_CB
free(inspect_st.seen);
# ifdef DAEDALUS_HAVE_H264_MB_INSPECT_COEFFS
free(inspect_st.captures);
# endif
#endif
if (dec) daedalus_decoder_destroy(dec);
av_frame_free(&fr);