Files
daedalus-v4l2/daemon/src/shadow_decoder.c
T
marfrit dbf01eddb8 daemon: shadow_decoder wiring (PR-Q3a.1)
Toolchain plumbing for the upcoming daedalus-decoder shadow-mode
path.  Production behaviour is unchanged.

What lands here:

  1. CMake links libdaedalus_decoder via pkg-config.  Static archive,
     so no .so dependency change in the daemon's link map.
  2. ffmpeg_loader resolves ff_h264_set_mb_inspect_cb NULL-tolerantly.
     Stock libavcodec lacks the symbol (logged as INFO at startup);
     the marfrit-packages ffmpeg-v4l2-request-fourier fork's 0016
     patch exports it.  The shadow path activates only when both
     env DAEDALUS_SHADOW_MODE=1 AND the symbol resolves.
  3. New shadow_decoder.[ch] module:
       - shadow_decoder_create() gates on env + symbol presence,
         returns NULL in production state (the common case).
       - shadow_decoder_install_cb() registers a per-MB callback on
         the H.264 AVCodecContext; lazily-created daedalus_decoder
         context will pick up dimensions from the first AVFrame.
       - shadow_decoder_on_frame() logs per-frame MB-observed count.
     Every entry point is NULL-safe so decoder.c stays clean of
     conditionals.
  4. decoder.{c,h} grow a `struct shadow_decoder *shadow` field on
     daedalus_decoder.  Install hook fires once per H.264 codec open;
     frame hook fires after each successful avcodec_receive_frame.

PR-Q3a.1 scope ENDS here.  The callback just counts MBs; no
daedalus_decoder_append_mb or flush_frame yet.  Real-coeffs / edges
extraction needs the patched FFmpeg source-tree headers
(DAEDALUS_FFMPEG_SRC) to introspect H264Context internals — that
lands in PR-Q3a.2.

dejavu-check: this path is daedalus-decoder's frame-major UMA
dispatch architecture (one cmdbuf per frame, one submit) running
alongside libavcodec's reference decode for validation.  It is NOT
per-kernel libavcodec function-pointer substitution.  No new
libavcodec patches; the existing 0016 callback is the only intercept
point.

Verified on hertz:

  - Build: clean, libdaedalus_decoder.a linked.
  - Disabled state (env unset OR symbol absent): no shadow log
    lines, daemon init continues normally, INFO logs
    "libavcodec lacks ff_h264_set_mb_inspect_cb (stock build,
    no daedalus-fourier 0016 patch) — shadow-mode unavailable".
  - Enabled state would require ffmpeg-v4l2-request-fourier .deb
    rebuilt with patches 0016/0017 deployed to hertz (current .deb
    release 10 predates them).  That's a deployment task, separate
    from this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:15:13 +02:00

163 lines
4.8 KiB
C

/* SPDX-License-Identifier: BSD-2-Clause */
/*
* shadow_decoder.c — env-gated parallel daedalus-decoder wiring.
*
* PR-Q3a.1 scope: prove the toolchain.
*
* 1. DAEDALUS_SHADOW_MODE=1 + libavcodec carries marfrit-packages
* 0016 (ff_h264_set_mb_inspect_cb) → shadow path active.
* 2. Per-MB callback fires on every macroblock libavcodec emits.
* We only count the firings here.
* 3. Frame boundary creates a daedalus_decoder context lazily
* (sized from the first AVFrame); destroy + recreate on
* resolution change.
* 4. Per-frame log line surfaces MB count + has_qpu state.
*
* No daedalus_decoder_append_mb / flush_frame calls yet — that
* needs H264Context introspection which depends on the patched
* FFmpeg source-tree headers (DAEDALUS_FFMPEG_SRC) and lands in
* PR-Q3a.2. This module's job here is to confirm the link
* survives, the callback resolves, the context creates, and
* tearing the path back down doesn't perturb the production
* AVFrame → V4L2 pipeline.
*/
#include "shadow_decoder.h"
#include "ffmpeg_loader.h"
#include "log.h"
#include <libavcodec/avcodec.h>
#include <libavutil/frame.h>
#include <daedalus_decoder.h>
#include <stdlib.h>
#include <string.h>
struct shadow_decoder {
struct ffmpeg_loader *loader;
daedalus_decoder *dec; /* lazily created on first frame */
int ctx_w; /* coded-frame width at last create */
int ctx_h;
uint64_t mbs_this_frame;
uint64_t total_frames;
uint64_t total_mbs;
};
static void shadow_mb_inspect(void *opaque,
const struct H264Context *h __attribute__((unused)),
int mb_x __attribute__((unused)),
int mb_y __attribute__((unused)))
{
struct shadow_decoder *sh = opaque;
sh->mbs_this_frame++;
}
struct shadow_decoder *shadow_decoder_create(struct ffmpeg_loader *loader)
{
const char *env = getenv("DAEDALUS_SHADOW_MODE");
if (!env || strcmp(env, "1") != 0)
return NULL;
if (!loader || !loader->ff_h264_set_mb_inspect_cb) {
log_warn("shadow_decoder: DAEDALUS_SHADOW_MODE=1 set but "
"libavcodec lacks ff_h264_set_mb_inspect_cb — disabled");
return NULL;
}
struct shadow_decoder *sh = calloc(1, sizeof(*sh));
if (!sh) {
log_err("shadow_decoder: out of memory");
return NULL;
}
sh->loader = loader;
log_info("shadow_decoder: enabled (DAEDALUS_SHADOW_MODE=1, "
"daedalus-decoder version %s)",
daedalus_decoder_version());
return sh;
}
void shadow_decoder_destroy(struct shadow_decoder *sh)
{
if (!sh)
return;
if (sh->dec)
daedalus_decoder_destroy(sh->dec);
log_info("shadow_decoder: shutdown — observed %llu frames / %llu MBs",
(unsigned long long) sh->total_frames,
(unsigned long long) sh->total_mbs);
free(sh);
}
void shadow_decoder_install_cb(struct shadow_decoder *sh,
struct AVCodecContext *avctx)
{
if (!sh || !avctx)
return;
/*
* Loader's optional-symbol pointer was checked at create time
* (we wouldn't be non-NULL otherwise), so the call is safe.
*/
sh->loader->ff_h264_set_mb_inspect_cb(avctx, shadow_mb_inspect, sh);
log_info("shadow_decoder: per-MB callback installed on H.264 ctx");
}
/*
* Ensure the daedalus_decoder context matches the frame's dimensions.
* Rounds up to the H.264 macroblock grid (16-pixel multiples) — the
* coded picture is always 16-aligned even when the displayed crop
* isn't. Returns 0 on success, -1 on failure (ctx left NULL; caller
* logs and continues without shadow dispatch this frame).
*/
static int shadow_ensure_ctx(struct shadow_decoder *sh, int w, int h)
{
int coded_w = (w + 15) & ~15;
int coded_h = (h + 15) & ~15;
if (sh->dec && sh->ctx_w == coded_w && sh->ctx_h == coded_h)
return 0;
if (sh->dec) {
daedalus_decoder_destroy(sh->dec);
sh->dec = NULL;
}
sh->dec = daedalus_decoder_create(coded_w, coded_h);
if (!sh->dec) {
log_warn("shadow_decoder: daedalus_decoder_create(%dx%d) "
"failed — shadow dispatch skipped this stream",
coded_w, coded_h);
sh->ctx_w = sh->ctx_h = 0;
return -1;
}
sh->ctx_w = coded_w;
sh->ctx_h = coded_h;
log_info("shadow_decoder: ctx ready (%dx%d coded, has_qpu=%d)",
coded_w, coded_h, daedalus_decoder_has_qpu(sh->dec));
return 0;
}
void shadow_decoder_on_frame(struct shadow_decoder *sh,
const struct AVFrame *fr)
{
if (!sh || !fr)
return;
(void) shadow_ensure_ctx(sh, fr->width, fr->height);
sh->total_frames++;
sh->total_mbs += sh->mbs_this_frame;
uint64_t expected = (uint64_t) ((fr->width + 15) >> 4) *
(uint64_t) ((fr->height + 15) >> 4);
log_info("shadow_decoder: frame #%llu %dx%d — %llu MBs observed "
"(expected %llu)",
(unsigned long long) sh->total_frames,
fr->width, fr->height,
(unsigned long long) sh->mbs_this_frame,
(unsigned long long) expected);
sh->mbs_this_frame = 0;
}