// 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 #include #include #include #include #include #include #include #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; daedalus_platform_device = platform_device_alloc(DAEDALUS_DRV_NAME, -1); if (!daedalus_platform_device) return -ENOMEM; ret = platform_device_add(daedalus_platform_device); if (ret) { platform_device_put(daedalus_platform_device); return ret; } ret = platform_driver_register(&daedalus_platform_driver); if (ret) { platform_device_unregister(daedalus_platform_device); return ret; } return 0; } static void __exit daedalus_exit(void) { platform_driver_unregister(&daedalus_platform_driver); platform_device_unregister(daedalus_platform_device); } module_init(daedalus_init); module_exit(daedalus_exit); MODULE_AUTHOR("Markus Fritsche "); MODULE_DESCRIPTION("V4L2 stateless decoder shim for daedalus-fourier (Pi 5 / VC7)"); MODULE_LICENSE("GPL v2"); MODULE_VERSION("0.0.1");