Phase 8.4: daemon ↔ kernel decode round-trip (VP9 end-to-end)

Wires the Phase 8.3 FFmpeg loader through the Phase 8.2 chardev
bridge: kernel injects REQ_DECODE carrying a raw VP9 access unit,
daemon hands the bitstream to libavcodec via dlopen, sends
RESP_FRAME back with a content-dependent FNV-1a digest of the
decoded YUV planes. Pure CPU decode for now — Phase 8.5 swaps in
dmabuf + QPU dispatch.

Protocol (include/daedalus_v4l2_proto.h):
- New REQ_DECODE (kernel→daemon) and RESP_FRAME (daemon→kernel)
  message types, with fixed-size payload structs.
- New DAEDALUS_CODEC_VP9/AV1/H264 enum (wire-stable so 8.6's
  AV1+H.264 work doesn't move existing values).
- New DAEDALUS_DECODE_* status enum (OK / NO_FRAME / ERR_OPEN /
  ERR_SEND / ERR_RECV / ERR_CODEC).
- Converted the prior `enum daedalus_msg_type` to #defines —
  high-bit values exceed INT_MAX and tripped -Wpedantic on
  userspace; kernel uABI headers use the same idiom.

Kernel (kernel/daedalus_v4l2_chardev.c):
- New debugfs entry /sys/kernel/debug/daedalus_v4l2/test_decode:
  writing raw bitstream bytes wraps them in a REQ_DECODE
  (codec=VP9 for Phase 8.4) and enqueues with an
  auto-incrementing cookie.
- daedalus_chardev_write learned RESP_FRAME: parses the payload
  and emits a single pr_info line with decode metadata. Keeps
  existing PONG handling on the default arm.

Daemon (daemon/src/...):
- chardev_client.{c,h} — opens /dev/daedalus-v4l2, blocking read
  loop, single-buffer write() responses (kernel chardev has only
  .write, not .write_iter, so writev lands as -EINVAL —
  discovered the hard way during first run).
- decoder.{c,h} — lazily-opened AVCodecContext per codec, shared
  AVPacket/AVFrame pair, descriptor-driven plane walker
  (av_pix_fmt_desc_get) so the same hash path covers YUV420P,
  YUV422P, YUV444P, GBRP and other 8-bit planar layouts.
  Generalised after first run decoded testsrc as GBRP (71)
  rather than the assumed YUV420P.
- `daemon` command in main.c opens the chardev and runs the loop
  until SIGINT/SIGTERM. Cookie correlation handled end-to-end.
- ffmpeg_loader gained av_pix_fmt_desc_get (23 symbols total).

Build:
- CMakeLists adds chardev_client.c + decoder.c; explicit
  -I../include for the shared protocol header.
- Still -Wall -Wextra -Wpedantic clean.

Verification on hertz (Pi 5, 6.12.75+rpt-rpi-2712):

  $ ffmpeg ... -pix_fmt yuv420p -c:v libvpx-vp9 -frames:v 1 \
           -y /tmp/vp9_test.ivf
  $ python3 ... strip IVF framing → vp9_keyframe.bin (3268 B)

  $ sudo insmod kernel/daedalus_v4l2.ko
  $ daedalus_v4l2_daemon -v daemon &
  $ sudo dd if=vp9_keyframe.bin \
         of=/sys/kernel/debug/daedalus_v4l2/test_decode

  daemon: REQ_DECODE cookie=2 → decoded yuv420p 320x240
          fnv1a=0x6ef10d71 luma=76800 chroma=38400
  kernel: RESP_FRAME cookie=2 status=0 320x240 pixfmt=0
          fnv1a=0x6ef10d71  ← matches daemon ✓

Hash properties verified:
  cookie=2  testsrc 3268 B → 0x6ef10d71  (first decode)
  cookie=3  red     44 B   → 0x7f6e5dc5  (content-dependent ✓)
  cookie=4  testsrc 3268 B → 0x6ef10d71  (deterministic ✓)
  cookie=5  64 B random    → status=101  (ERR_SEND, daemon alive)

Daemon survives bad input (FFmpeg "Invalid sync code" wrapped
into structured ERR_SEND response). Clean SIGTERM shutdown,
clean rmmod.

Phase 8.4 acceptance criteria met:
- ✓ end-to-end kernel→daemon→FFmpeg→kernel round-trip
- ✓ cookie correlation per request/response pair
- ✓ content-dependent + deterministic digest
- ✓ structured error responses (no daemon crash on bad input)
- ✓ clean teardown (SIGTERM + rmmod)
- ✓ builds clean on both kernel kbuild and daemon CMake

Per correctness-before-speed:
- Real chardev I/O (no shortcuts, no select-loop hacks)
- Real FFmpeg AVCodecContext lifecycle (lazily opened, properly
  freed on cleanup)
- Descriptor-driven plane walk (generalises across pix_fmts)
- Structured error path (not just log-and-continue)
- All resource paths cleaned up on every error branch
- Documented why FNV-1a digest, why write() not writev(), why
  pix_desc walk in docs/phase_8_4_closure.md

Phase 8.5 next: V4L2 m2m queue submits REQ_DECODE from
vidioc_qbuf; dmabuf carries actual pixel data so the chardev's
64 KiB cap doesn't gate frame size; begin substituting
daedalus_dispatch_* into the daemon's decode path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 15:22:16 +00:00
parent 873a04c622
commit 2a449632b9
11 changed files with 1098 additions and 22 deletions
+3
View File
@@ -33,11 +33,14 @@ add_executable(daedalus_v4l2_daemon
src/ffmpeg_loader.c
src/log.c
src/parser.c
src/decoder.c
src/chardev_client.c
)
target_include_directories(daedalus_v4l2_daemon
PRIVATE
src
${CMAKE_CURRENT_SOURCE_DIR}/../include
${FFMPEG_INCLUDE_DIRS}
)
+262
View File
@@ -0,0 +1,262 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* chardev_client.c — kernel-bridge client for the daedalus-v4l2 daemon.
*/
#include "chardev_client.h"
#include "decoder.h"
#include "ffmpeg_loader.h"
#include "log.h"
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#define CHARDEV_PATH "/dev/daedalus-v4l2"
#define CHARDEV_READ_BUFSZ (sizeof(struct daedalus_msg_hdr) + \
DAEDALUS_PROTO_MAX_PAYLOAD)
int chardev_client_open(struct chardev_client *cli,
struct ffmpeg_loader *loader,
volatile sig_atomic_t *stop_flag)
{
int fd, rc;
memset(cli, 0, sizeof(*cli));
cli->fd = -1;
cli->loader = loader;
cli->stop_flag = stop_flag;
fd = open(CHARDEV_PATH, O_RDWR | O_CLOEXEC);
if (fd < 0) {
rc = -errno;
log_err("open(%s): %s", CHARDEV_PATH, strerror(errno));
return rc;
}
cli->decoder = calloc(1, sizeof(*cli->decoder));
if (!cli->decoder) {
close(fd);
return -ENOMEM;
}
rc = daedalus_decoder_init(cli->decoder, loader);
if (rc < 0) {
free(cli->decoder);
cli->decoder = NULL;
close(fd);
log_err("daedalus_decoder_init: %d", rc);
return rc;
}
cli->fd = fd;
log_info("chardev: opened %s (fd %d)", CHARDEV_PATH, fd);
return 0;
}
void chardev_client_close(struct chardev_client *cli)
{
if (!cli)
return;
if (cli->decoder) {
daedalus_decoder_cleanup(cli->decoder);
free(cli->decoder);
cli->decoder = NULL;
}
if (cli->fd >= 0) {
close(cli->fd);
cli->fd = -1;
}
}
static int send_response(struct chardev_client *cli, uint32_t type,
uint32_t cookie, const void *payload,
size_t payload_len)
{
struct daedalus_msg_hdr hdr;
int rc;
if (payload_len > DAEDALUS_PROTO_MAX_PAYLOAD)
return -EMSGSIZE;
hdr.magic = DAEDALUS_PROTO_MAGIC;
hdr.version = DAEDALUS_PROTO_VERSION;
hdr.type = type;
hdr.cookie = cookie;
hdr.payload_len = (uint32_t) payload_len;
hdr.reserved = 0;
/*
* The kernel's write() path validates count == sizeof(hdr)
* + hdr.payload_len in a single call, and only implements
* .write (not .write_iter), so a writev() lands as -EINVAL.
* Marshal the message into a single buffer and write() it.
*
* Response payloads are small (struct daedalus_resp_frame =
* 36 bytes; PONG echoes <= 64 KiB). A short-lived heap
* allocation per response is fine; per-loop reuse can come
* later if profiling demands it.
*/
{
size_t total = sizeof(hdr) + payload_len;
uint8_t *out = malloc(total);
ssize_t n;
if (!out)
return -ENOMEM;
memcpy(out, &hdr, sizeof(hdr));
if (payload_len)
memcpy(out + sizeof(hdr), payload, payload_len);
for (;;) {
n = write(cli->fd, out, total);
if (n >= 0) {
if ((size_t) n != total) {
log_err("chardev: short write %zd != %zu",
n, total);
rc = -EIO;
} else {
rc = 0;
}
break;
}
if (errno == EINTR)
continue;
rc = -errno;
log_err("chardev: write: %s", strerror(errno));
break;
}
free(out);
}
return rc;
}
static int handle_req_decode(struct chardev_client *cli,
const struct daedalus_msg_hdr *hdr,
const uint8_t *payload)
{
struct daedalus_req_decode req;
struct daedalus_resp_frame resp;
int rc;
if (hdr->payload_len < sizeof(req)) {
log_err("REQ_DECODE cookie=%u: payload too short %u < %zu",
hdr->cookie, hdr->payload_len, sizeof(req));
memset(&resp, 0, sizeof(resp));
resp.status = DAEDALUS_DECODE_ERR_RECV;
return send_response(cli, DAEDALUS_MSG_RESP_FRAME,
hdr->cookie, &resp, sizeof(resp));
}
memcpy(&req, payload, sizeof(req));
if ((size_t) req.bitstream_len + sizeof(req) != hdr->payload_len) {
log_err("REQ_DECODE cookie=%u: bitstream_len %u inconsistent with payload_len %u",
hdr->cookie, req.bitstream_len, hdr->payload_len);
memset(&resp, 0, sizeof(resp));
resp.status = DAEDALUS_DECODE_ERR_RECV;
return send_response(cli, DAEDALUS_MSG_RESP_FRAME,
hdr->cookie, &resp, sizeof(resp));
}
log_info("REQ_DECODE cookie=%u codec=%u bitstream=%u bytes",
hdr->cookie, req.codec_id, req.bitstream_len);
rc = daedalus_decoder_run_request(cli->decoder, &req,
payload + sizeof(req), &resp);
if (rc < 0)
return rc;
return send_response(cli, DAEDALUS_MSG_RESP_FRAME, hdr->cookie,
&resp, sizeof(resp));
}
static int handle_ping(struct chardev_client *cli,
const struct daedalus_msg_hdr *hdr,
const uint8_t *payload)
{
log_info("PING cookie=%u plen=%u — echoing PONG",
hdr->cookie, hdr->payload_len);
return send_response(cli, DAEDALUS_MSG_PONG, hdr->cookie,
payload, hdr->payload_len);
}
static int handle_one_message(struct chardev_client *cli, uint8_t *buf)
{
struct daedalus_msg_hdr hdr;
ssize_t n;
/*
* The kernel chardev delivers exactly one message per
* read(). Pass a buffer that can hold any legal message.
*/
for (;;) {
n = read(cli->fd, buf, CHARDEV_READ_BUFSZ);
if (n >= 0)
break;
if (errno == EINTR) {
if (*cli->stop_flag)
return 0;
continue;
}
log_err("chardev: read: %s", strerror(errno));
return -errno;
}
if (n == 0)
return -EIO; /* EOF / device unplugged */
if ((size_t) n < sizeof(hdr)) {
log_err("chardev: short read %zd < hdr", n);
return -EBADMSG;
}
memcpy(&hdr, buf, sizeof(hdr));
if (hdr.magic != DAEDALUS_PROTO_MAGIC) {
log_err("chardev: bad magic 0x%08x", hdr.magic);
return -EBADMSG;
}
if (hdr.version != DAEDALUS_PROTO_VERSION) {
log_err("chardev: unsupported version %u", hdr.version);
return -EPROTO;
}
if ((size_t) n != sizeof(hdr) + hdr.payload_len) {
log_err("chardev: framing mismatch n=%zd expected %zu",
n, sizeof(hdr) + hdr.payload_len);
return -EBADMSG;
}
switch (hdr.type) {
case DAEDALUS_MSG_PING:
return handle_ping(cli, &hdr, buf + sizeof(hdr));
case DAEDALUS_MSG_REQ_DECODE:
return handle_req_decode(cli, &hdr, buf + sizeof(hdr));
default:
log_warn("chardev: unknown request type 0x%08x cookie=%u",
hdr.type, hdr.cookie);
return 0; /* skip, don't bail the loop */
}
}
int chardev_client_run(struct chardev_client *cli)
{
uint8_t *buf;
int rc = 0;
buf = malloc(CHARDEV_READ_BUFSZ);
if (!buf)
return -ENOMEM;
log_info("daemon loop started; waiting for kernel requests");
while (!*cli->stop_flag) {
rc = handle_one_message(cli, buf);
if (rc < 0) {
if (rc == -EINTR)
continue;
log_err("chardev: handle_one_message: %d", rc);
break;
}
}
log_info("daemon loop exiting (stop=%d rc=%d)", *cli->stop_flag, rc);
free(buf);
return rc;
}
+65
View File
@@ -0,0 +1,65 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* chardev_client.h — kernel-bridge client for the daedalus-v4l2 daemon.
*
* Opens /dev/daedalus-v4l2, runs a blocking read() / write() loop,
* and invokes the daemon's REQ_* handlers. Phase 8.4 understands
* REQ_DECODE; future phases extend the dispatch table.
*/
#ifndef DAEDALUS_V4L2_CHARDEV_CLIENT_H
#define DAEDALUS_V4L2_CHARDEV_CLIENT_H
#include <signal.h>
#include <stddef.h>
#include <stdint.h>
#include "daedalus_v4l2_proto.h"
struct ffmpeg_loader;
struct daedalus_decoder;
/**
* struct chardev_client - daemon-side chardev state
* @fd: open /dev/daedalus-v4l2 descriptor (-1 if not open)
* @loader: dlopen'd FFmpeg loader (borrowed; not owned)
* @decoder: per-codec AVCodecContext cache (owned)
* @stop_flag: set non-zero from a signal handler to break the loop
*/
struct chardev_client {
int fd;
struct ffmpeg_loader *loader;
struct daedalus_decoder *decoder;
volatile sig_atomic_t *stop_flag;
};
/**
* chardev_client_open - open /dev/daedalus-v4l2 O_RDWR
*
* @cli: caller-allocated; cleared on entry
* @loader: borrowed FFmpeg loader (must outlive the client)
* @stop_flag: pointer the signal handler sets to ask the loop to stop
*
* Return: 0 on success; negative errno on failure.
*/
int chardev_client_open(struct chardev_client *cli,
struct ffmpeg_loader *loader,
volatile sig_atomic_t *stop_flag);
/**
* chardev_client_run - blocking event loop
*
* Reads one message at a time, dispatches to the matching
* handler, writes the corresponding response. Returns when
* *@stop_flag is set, on chardev EOF, or on an unrecoverable
* error.
*
* Return: 0 on clean shutdown; negative errno on a fatal error.
*/
int chardev_client_run(struct chardev_client *cli);
/**
* chardev_client_close - close the chardev and free decoder state
*/
void chardev_client_close(struct chardev_client *cli);
#endif /* DAEDALUS_V4L2_CHARDEV_CLIENT_H */
+266
View File
@@ -0,0 +1,266 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* decoder.c — FFmpeg-driven decode helper for daedalus-v4l2 daemon.
*/
#include "decoder.h"
#include "ffmpeg_loader.h"
#include "log.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <libavcodec/avcodec.h>
#include <libavutil/pixfmt.h>
/*
* FNV-1a 32-bit hash. Used as a compact digest of the decoded
* frame's YUV planes so the kernel can verify "the daemon produced
* the expected pixels" without shipping the full frame through the
* 64-KiB-capped chardev wire protocol. Phase 8.5's dmabuf path
* carries actual pixel data; this digest stays useful as a
* cross-host sanity check.
*/
static uint32_t fnv1a32_init(void)
{
return 0x811c9dc5u;
}
static uint32_t fnv1a32_update(uint32_t h, const uint8_t *data, size_t len)
{
size_t i;
for (i = 0; i < len; i++)
h = (h ^ data[i]) * 0x01000193u;
return h;
}
/*
* Hash plane @p (width @w bytes per line, @h lines, stride @stride
* bytes between lines). We strip libav's row alignment padding so
* the hash matches the layout used by `ffmpeg -f rawvideo` reference
* output (which is tightly packed).
*/
static uint32_t fnv1a32_plane(uint32_t h, const uint8_t *p,
int w, int height, int stride)
{
int y;
for (y = 0; y < height; y++)
h = fnv1a32_update(h, p + (size_t) y * (size_t) stride,
(size_t) w);
return h;
}
int daedalus_decoder_init(struct daedalus_decoder *dec,
struct ffmpeg_loader *loader)
{
memset(dec, 0, sizeof(*dec));
dec->loader = loader;
dec->pkt = loader->av_packet_alloc();
if (!dec->pkt)
return -ENOMEM;
dec->frame = loader->av_frame_alloc();
if (!dec->frame) {
loader->av_packet_free(&dec->pkt);
return -ENOMEM;
}
return 0;
}
void daedalus_decoder_cleanup(struct daedalus_decoder *dec)
{
if (!dec || !dec->loader)
return;
if (dec->ctx_vp9)
dec->loader->avcodec_free_context(&dec->ctx_vp9);
if (dec->frame)
dec->loader->av_frame_free(&dec->frame);
if (dec->pkt)
dec->loader->av_packet_free(&dec->pkt);
memset(dec, 0, sizeof(*dec));
}
/*
* Lazily open the AVCodecContext for codec_id. Returns 0 on
* success, -ENOSYS on unknown codec, -EIO on FFmpeg failure.
*/
static int decoder_open_codec(struct daedalus_decoder *dec, uint32_t codec_id,
struct AVCodecContext **out)
{
struct ffmpeg_loader *fm = dec->loader;
const struct AVCodec *codec;
struct AVCodecContext *ctx;
enum AVCodecID av_id;
int rc;
switch (codec_id) {
case DAEDALUS_CODEC_VP9:
av_id = AV_CODEC_ID_VP9;
if (dec->ctx_vp9) {
*out = dec->ctx_vp9;
return 0;
}
break;
case DAEDALUS_CODEC_AV1:
case DAEDALUS_CODEC_H264:
/* Phase 8.6 wires AV1 and H.264 properly. */
log_warn("decoder: codec_id %u not yet supported", codec_id);
return -ENOSYS;
default:
log_warn("decoder: unknown codec_id %u", codec_id);
return -ENOSYS;
}
codec = fm->avcodec_find_decoder(av_id);
if (!codec) {
log_err("decoder: avcodec_find_decoder(%d) returned NULL", av_id);
return -EIO;
}
ctx = fm->avcodec_alloc_context3(codec);
if (!ctx)
return -ENOMEM;
rc = fm->avcodec_open2(ctx, codec, NULL);
if (rc < 0) {
log_err("decoder: avcodec_open2 failed: %d", rc);
fm->avcodec_free_context(&ctx);
return -EIO;
}
dec->ctx_vp9 = ctx;
*out = ctx;
log_info("decoder: opened %s context", codec->name);
return 0;
}
int daedalus_decoder_run_request(struct daedalus_decoder *dec,
const struct daedalus_req_decode *req,
const uint8_t *bitstream,
struct daedalus_resp_frame *resp)
{
struct ffmpeg_loader *fm = dec->loader;
struct AVCodecContext *ctx = NULL;
int rc;
memset(resp, 0, sizeof(*resp));
resp->codec_id = req->codec_id;
rc = decoder_open_codec(dec, req->codec_id, &ctx);
if (rc == -ENOSYS) {
resp->status = DAEDALUS_DECODE_ERR_CODEC;
return 0;
}
if (rc < 0) {
resp->status = DAEDALUS_DECODE_ERR_OPEN;
return 0;
}
fm->av_packet_unref(dec->pkt);
/*
* The kernel's REQ_DECODE payload is borrowed memory we'll
* free as soon as this function returns. Pointing the
* AVPacket at it directly is safe because avcodec_send_packet
* either fully consumes the input or copies it internally —
* by the time we return we no longer reference @bitstream.
*
* We cast away const because AVPacket->data is non-const in
* the FFmpeg API; we promise not to mutate the buffer.
*/
dec->pkt->data = (uint8_t *) (uintptr_t) bitstream;
dec->pkt->size = (int) req->bitstream_len;
rc = fm->avcodec_send_packet(ctx, dec->pkt);
if (rc < 0) {
log_err("decoder: avcodec_send_packet failed: %d", rc);
resp->status = DAEDALUS_DECODE_ERR_SEND;
return 0;
}
fm->av_frame_unref(dec->frame);
rc = fm->avcodec_receive_frame(ctx, dec->frame);
if (rc == AVERROR(EAGAIN) || rc == AVERROR_EOF) {
log_debug("decoder: no frame ready yet (rc=%d)", rc);
resp->status = DAEDALUS_DECODE_NO_FRAME;
return 0;
}
if (rc < 0) {
log_err("decoder: avcodec_receive_frame failed: %d", rc);
resp->status = DAEDALUS_DECODE_ERR_RECV;
return 0;
}
{
struct AVFrame *fr = dec->frame;
const AVPixFmtDescriptor *desc =
fm->av_pix_fmt_desc_get(fr->format);
uint32_t h = fnv1a32_init();
uint32_t luma_len = 0, chroma_len = 0;
resp->status = DAEDALUS_DECODE_OK;
resp->width = (uint32_t) fr->width;
resp->height = (uint32_t) fr->height;
resp->pix_fmt = fr->format;
/*
* Walk every plane reported by the AVPixFmtDescriptor.
* For each component, byte width = ((plane_w *
* step_minus1) >> 0) — but the descriptor only tells
* us which plane each component sits in, not the
* plane's byte stride per pixel. In practice for the
* formats we care about (YUV420P, YUV422P, YUV444P,
* GBRP, NV12), each plane has exactly one component
* at 1 byte/sample. Hash each plane at
* (width >> log2_chroma_w) × (height >> log2_chroma_h)
* for chroma planes, full-size for plane 0.
*
* This generalises cleanly to anything 8-bit-per-
* sample-per-plane; 10/12-bit (P010, YUV420P10LE) will
* need depth handling when Phase 8.6 brings HDR
* content into play.
*/
if (!desc) {
log_warn("decoder: no descriptor for pix_fmt %d",
fr->format);
} else {
int p, max_plane = 0;
int i;
for (i = 0; i < desc->nb_components; i++) {
if (desc->comp[i].plane > max_plane)
max_plane = desc->comp[i].plane;
}
for (p = 0; p <= max_plane; p++) {
int pw, ph;
if (!fr->data[p] || !fr->linesize[p])
continue;
if (p == 0) {
pw = fr->width;
ph = fr->height;
luma_len += (uint32_t) pw *
(uint32_t) ph;
} else {
pw = AV_CEIL_RSHIFT(fr->width,
desc->log2_chroma_w);
ph = AV_CEIL_RSHIFT(fr->height,
desc->log2_chroma_h);
chroma_len += (uint32_t) pw *
(uint32_t) ph;
}
h = fnv1a32_plane(h, fr->data[p], pw, ph,
fr->linesize[p]);
}
}
resp->luma_len = luma_len;
resp->chroma_len = chroma_len;
resp->fnv1a_yuv = h;
log_info("decoder: OK %dx%d fmt=%d (%s) fnv1a=0x%08x luma=%u chroma=%u",
fr->width, fr->height, fr->format,
desc ? desc->name : "?",
h, luma_len, chroma_len);
}
fm->av_frame_unref(dec->frame);
return 0;
}
+70
View File
@@ -0,0 +1,70 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* decoder.h — FFmpeg-driven decode helper for daedalus-v4l2 daemon.
*
* Encapsulates the AVCodecContext per supported codec, plus a
* single shared AVPacket / AVFrame pair (we serialise decode
* inside the chardev event loop; no concurrency).
*
* Phase 8.4 implements VP9. Phase 8.6 extends to AV1 / H.264.
*/
#ifndef DAEDALUS_V4L2_DECODER_H
#define DAEDALUS_V4L2_DECODER_H
#include <stddef.h>
#include <stdint.h>
#include "daedalus_v4l2_proto.h"
struct ffmpeg_loader;
struct AVCodecContext;
struct AVPacket;
struct AVFrame;
/**
* struct daedalus_decoder - per-daemon decoder state
* @loader: borrowed FFmpeg loader (must outlive the decoder)
* @ctx_vp9: lazily-opened VP9 AVCodecContext (NULL until first
* VP9 REQ_DECODE)
* @pkt: shared AVPacket reused across requests
* @frame: shared AVFrame reused across requests
*/
struct daedalus_decoder {
struct ffmpeg_loader *loader;
struct AVCodecContext *ctx_vp9;
struct AVPacket *pkt;
struct AVFrame *frame;
};
/**
* daedalus_decoder_init - allocate the shared packet/frame pair
*
* Return: 0 on success, -ENOMEM if FFmpeg refused to allocate.
* Codec contexts are opened lazily on first use.
*/
int daedalus_decoder_init(struct daedalus_decoder *dec,
struct ffmpeg_loader *loader);
/**
* daedalus_decoder_cleanup - free codec contexts, packet, frame
*/
void daedalus_decoder_cleanup(struct daedalus_decoder *dec);
/**
* daedalus_decoder_run_request - decode one REQ_DECODE payload
* @dec: initialised decoder
* @req: REQ_DECODE prefix (from the wire)
* @bitstream: bitstream blob (req->bitstream_len bytes)
* @resp: caller-allocated RESP_FRAME output (zeroed by callee)
*
* Populates @resp with the decode outcome. Always returns 0;
* decode-level failures are reported via @resp->status so the
* kernel sees a structured response rather than a dropped
* request.
*/
int daedalus_decoder_run_request(struct daedalus_decoder *dec,
const struct daedalus_req_decode *req,
const uint8_t *bitstream,
struct daedalus_resp_frame *resp);
#endif /* DAEDALUS_V4L2_DECODER_H */
+1
View File
@@ -90,6 +90,7 @@ int ffmpeg_loader_init(struct ffmpeg_loader *loader)
RESOLVE(libavutil, LIBAVUTIL_SONAME, av_log_set_level);
RESOLVE(libavutil, LIBAVUTIL_SONAME, av_get_media_type_string);
RESOLVE(libavutil, LIBAVUTIL_SONAME, av_version_info);
RESOLVE(libavutil, LIBAVUTIL_SONAME, av_pix_fmt_desc_get);
{
unsigned int v = loader->avformat_version();
+2
View File
@@ -33,6 +33,7 @@
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/pixdesc.h>
/**
* struct ffmpeg_loader - resolved FFmpeg API entry points
@@ -86,6 +87,7 @@ struct ffmpeg_loader {
void (*av_log_set_level)(int);
const char *(*av_get_media_type_string)(enum AVMediaType);
const char *(*av_version_info)(void);
const AVPixFmtDescriptor *(*av_pix_fmt_desc_get)(enum AVPixelFormat);
};
/**
+23
View File
@@ -10,6 +10,7 @@
*/
#include "ffmpeg_loader.h"
#include "parser.h"
#include "chardev_client.h"
#include "log.h"
#include <stdio.h>
@@ -52,12 +53,32 @@ static int cmd_parse(struct ffmpeg_loader *fm, int argc, char **argv)
return rc < 0 ? 1 : 0;
}
static int cmd_daemon(struct ffmpeg_loader *fm, int argc, char **argv)
{
struct chardev_client cli;
int rc;
(void) argc;
(void) argv;
rc = chardev_client_open(&cli, fm, &g_terminate);
if (rc < 0) {
log_err("chardev_client_open: %d", rc);
return 1;
}
rc = chardev_client_run(&cli);
chardev_client_close(&cli);
return rc < 0 ? 1 : 0;
}
static void usage(const char *progname)
{
fprintf(stderr,
"usage: %s <command> [args]\n\n"
"commands:\n"
" parse <file> Phase 8.3: demux+enumerate frames\n"
" daemon Phase 8.4: open /dev/daedalus-v4l2 and\n"
" service REQ_DECODE from the kernel\n"
"\n"
"options:\n"
" -v, --verbose enable debug logging\n"
@@ -103,6 +124,8 @@ int main(int argc, char **argv)
const char *cmd = argv[i++];
if (strcmp(cmd, "parse") == 0) {
rc = cmd_parse(&fm, argc - i, argv + i);
} else if (strcmp(cmd, "daemon") == 0) {
rc = cmd_daemon(&fm, argc - i, argv + i);
} else {
fprintf(stderr, "unknown command: %s\n", cmd);
usage(argv[0]);