dbf01eddb8
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>
163 lines
4.8 KiB
C
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;
|
|
}
|