Files
daedalus-v4l2/kernel/daedalus_v4l2_main.c
T
marfrit 895f57c63a Phase 8.2: kernel ↔ daemon chardev bridge with round-trip test
Adds /dev/daedalus-v4l2 misc chardev to the kernel module. The
chardev is the IPC channel for the future userspace decoder
daemon: kernel enqueues REQ_* messages, daemon read()s them,
processes, write()s RESP_* back.

Wire protocol (pre-1.0, header in include/daedalus_v4l2_proto.h):
- struct daedalus_msg_hdr: magic (D04V) + version + type +
  cookie + payload_len + reserved
- Request/response separated by high bit of type field
- Max 64 KiB payload per message
- Cookie correlates request with matching response

Kernel implementation (kernel/daedalus_v4l2_chardev.{c,h}):
- Single-instance chardev (-EBUSY on second open)
- In-kernel FIFO bounded at 64 messages
- Blocking + non-blocking read; poll() with EPOLLIN on queued
- write() parses + validates header, logs response at pr_debug
- Bad magic → -EBADMSG, bad version → -EPROTO, oversize → -EMSGSIZE
- All error paths free resources

Phase 8.2 test trigger via debugfs:
- /sys/kernel/debug/daedalus_v4l2/test_ping — any byte
  enqueues a PING with a fixed 24-byte payload. Removed in
  Phase 8.4 when real REQ_DECODE from V4L2 path takes over.

Userspace verification tool (tools/test_chardev_pingpong.c):
- Real C program, proper error reporting via strerror
- Validates the 6-step round-trip: open → empty-queue EAGAIN →
  trigger ping → read PING → verify all fields → write PONG → close
- Builds with -Wall -Wextra clean

Verification on hertz (Pi 5, 6.12.75+rpt-rpi-2712):
  $ sudo insmod daedalus_v4l2.ko
  $ sudo tools/test_chardev_pingpong
  opening /dev/daedalus-v4l2...
    non-blocking read on empty queue: EAGAIN ✓
    injected PING via debugfs ✓
    read PING: magic ✓ version ✓ type=PING ✓ cookie=0x1234 ✓ payload=24 bytes
      payload: "DAEDALUS-V4L2-PING-PL"
    wrote PONG (cookie=0x1234) ✓
  ALL TESTS PASSED.
  $ sudo rmmod daedalus_v4l2      # clean

Per correctness-before-speed: full kerneldoc on structs, 8-tab
kernel style, SPDX headers, proper error paths, real test
program (not "I ran it once"), failure-mode coverage documented.

Phase 8.3 next: userspace daemon with dlopen'd FFmpeg parse path.

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

229 lines
6.4 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
/*
* daedalus-v4l2 — V4L2 stateless decoder shim.
*
* Out-of-tree Linux kernel module that exposes a /dev/videoNN
* V4L2 device for the daedalus-fourier kernel library. Real
* decoding work happens in a userspace daemon (this module
* forwards bitstream + stateless-codec control structs via a
* chardev bridge — that part lands in Phase 8.2).
*
* Phase 8.1 (this commit): minimal viable skeleton. Registers a
* platform device + v4l2_device + video_device and answers
* VIDIOC_QUERYCAP with reasonable values. Other ioctls fall
* through to v4l2-core defaults; modprobe / rmmod is a clean
* round-trip.
*
* Project: https://git.reauktion.de/reauktion/daedalus-v4l2
* Sibling kernel library: https://git.reauktion.de/marfrit/daedalus-fourier
*/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <media/v4l2-device.h>
#include <media/v4l2-dev.h>
#include <media/v4l2-ioctl.h>
#include "daedalus_v4l2_chardev.h"
#define DAEDALUS_DRV_NAME "daedalus_v4l2"
#define DAEDALUS_VIDEO_NAME "daedalus"
/**
* struct daedalus_dev - top-level device state
* @pdev: owning platform device (synthesised in module_init)
* @v4l2_dev: V4L2 device parent for any video_device we register
* @vdev: video_device exposed as /dev/videoNN
*
* One-instance singleton for Phase 8.1. Multi-instance support
* (one decoder per /dev/videoNN) lands when m2m wiring goes in.
*/
struct daedalus_dev {
struct platform_device *pdev;
struct v4l2_device v4l2_dev;
struct video_device vdev;
};
/*
* V4L2 ioctl dispatch table. Phase 8.1 only implements
* VIDIOC_QUERYCAP; everything else returns -ENOTTY via the
* v4l2-core's default handler when the op is NULL.
*/
static int daedalus_querycap(struct file *file, void *priv,
struct v4l2_capability *cap)
{
strscpy(cap->driver, DAEDALUS_DRV_NAME, sizeof(cap->driver));
/*
* cap->card is 32 bytes incl. NUL terminator. Pick a name
* that fits without truncation.
*/
strscpy(cap->card, "daedalus-fourier V3D7+NEON",
sizeof(cap->card));
snprintf(cap->bus_info, sizeof(cap->bus_info),
"platform:%s", DAEDALUS_DRV_NAME);
return 0;
}
static const struct v4l2_ioctl_ops daedalus_ioctl_ops = {
.vidioc_querycap = daedalus_querycap,
/*
* Phase 8.2+ adds:
* .vidioc_enum_fmt_vid_{cap,out}_mplane
* .vidioc_g/s_fmt_vid_{cap,out}_mplane
* .vidioc_reqbufs / vidioc_{q,dq,query}buf
* .vidioc_streamon / .vidioc_streamoff
* stateless-codec controls via the v4l2_ctrl_handler.
*/
};
/*
* Phase 8.1 placeholder for .release. We DON'T yet have a
* vb2_queue (that's Phase 8.2), so the real vb2_fop_release
* isn't usable. Use the minimal v4l2_fh_release for now; the
* Phase 8.2 patch swaps this for vb2_fop_release.
*/
static int daedalus_release_phase81(struct file *file)
{
return v4l2_fh_release(file);
}
/*
* File operations. v4l2_fh_open provides the default open the
* v4l2-core machinery expects; .release is our Phase 8.1
* placeholder; .unlocked_ioctl uses the kernel's video_ioctl2
* dispatcher against our v4l2_ioctl_ops table.
*/
static const struct v4l2_file_operations daedalus_fops = {
.owner = THIS_MODULE,
.open = v4l2_fh_open,
.release = daedalus_release_phase81,
.unlocked_ioctl = video_ioctl2,
};
static void daedalus_vdev_release(struct video_device *vdev)
{
/*
* The video_device is embedded inside our daedalus_dev which
* lives as long as the platform_device. Nothing to free here
* directly; this no-op release just satisfies v4l2-core's
* requirement that .release be set.
*/
}
static int daedalus_probe(struct platform_device *pdev)
{
struct daedalus_dev *dev;
int ret;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
dev->pdev = pdev;
platform_set_drvdata(pdev, dev);
ret = v4l2_device_register(&pdev->dev, &dev->v4l2_dev);
if (ret) {
dev_err(&pdev->dev, "v4l2_device_register: %d\n", ret);
return ret;
}
/* Set up video_device. Embedded; vdev->release is no-op. */
strscpy(dev->vdev.name, DAEDALUS_VIDEO_NAME, sizeof(dev->vdev.name));
dev->vdev.fops = &daedalus_fops;
dev->vdev.ioctl_ops = &daedalus_ioctl_ops;
dev->vdev.release = daedalus_vdev_release;
dev->vdev.v4l2_dev = &dev->v4l2_dev;
dev->vdev.vfl_dir = VFL_DIR_M2M; /* mem2mem: bitstream in, frames out */
dev->vdev.device_caps = V4L2_CAP_VIDEO_M2M_MPLANE
| V4L2_CAP_STREAMING;
video_set_drvdata(&dev->vdev, dev);
ret = video_register_device(&dev->vdev, VFL_TYPE_VIDEO, -1);
if (ret) {
dev_err(&pdev->dev, "video_register_device: %d\n", ret);
v4l2_device_unregister(&dev->v4l2_dev);
return ret;
}
v4l2_info(&dev->v4l2_dev,
"daedalus-v4l2 registered as /dev/video%d (Phase 8.1 skeleton)\n",
dev->vdev.num);
return 0;
}
static void daedalus_remove(struct platform_device *pdev)
{
struct daedalus_dev *dev = platform_get_drvdata(pdev);
video_unregister_device(&dev->vdev);
v4l2_device_unregister(&dev->v4l2_dev);
}
static struct platform_driver daedalus_platform_driver = {
.probe = daedalus_probe,
.remove = daedalus_remove,
.driver = {
.name = DAEDALUS_DRV_NAME,
},
};
/*
* The platform device that our driver binds to. Synthesised
* at module load time since we have no device tree node yet
* (out-of-tree module; not vendored into the rpi DT).
*/
static struct platform_device *daedalus_platform_device;
static int __init daedalus_init(void)
{
int ret;
ret = daedalus_chardev_init();
if (ret)
return ret;
daedalus_platform_device = platform_device_alloc(DAEDALUS_DRV_NAME, -1);
if (!daedalus_platform_device) {
ret = -ENOMEM;
goto err_chardev;
}
ret = platform_device_add(daedalus_platform_device);
if (ret) {
platform_device_put(daedalus_platform_device);
goto err_chardev;
}
ret = platform_driver_register(&daedalus_platform_driver);
if (ret) {
platform_device_unregister(daedalus_platform_device);
goto err_chardev;
}
return 0;
err_chardev:
daedalus_chardev_exit();
return ret;
}
static void __exit daedalus_exit(void)
{
platform_driver_unregister(&daedalus_platform_driver);
platform_device_unregister(daedalus_platform_device);
daedalus_chardev_exit();
}
module_init(daedalus_init);
module_exit(daedalus_exit);
MODULE_AUTHOR("Markus Fritsche <fritsche.markus@gmail.com>");
MODULE_DESCRIPTION("V4L2 stateless decoder shim for daedalus-fourier (Pi 5 / VC7)");
MODULE_LICENSE("GPL v2");
MODULE_VERSION("0.0.1");