Phase 8.6: dmabuf + AV1 + H.264 + stateless controls
Removes the Phase 8.5 64 KiB frame-size cap by exporting CAPTURE
buffers as dmabuf-fds the daemon mmaps and writes pixels into
directly. Adds AV1 + H.264 codec support, V4L2 stateless control
registration, and the compliance polish that brings the driver
to 47/48 v4l2-compliance pass.
Protocol (include/daedalus_v4l2_proto.h):
- struct daedalus_req_decode grew capture-buffer metadata
(width/height/pix_fmt/num_planes + per-plane size+stride).
- New DAEDALUS_IOC_GET_DMABUF ioctl on the chardev: daemon
asks for a per-plane dmabuf fd, kernel calls vb2_core_expbuf
in daemon task context so the fd lands in the daemon's table.
Kernel m2m driver (kernel/daedalus_v4l2_main.c):
- Both queues switched to vb2_dma_contig_memops. OUTPUT was
vmalloc in 8.5; the switch is needed because vmalloc doesn't
honour V4L2_MEMORY_FLAG_NON_COHERENT and v4l2-compliance's
REQBUFS test rejected the driver because of it. We still
read bitstream via vb2_plane_vaddr (dma_contig gives a
kernel virtual address just like vmalloc did).
- dma_coerce_mask_and_coherent(DMA_BIT_MASK(32)) in probe.
- queue_setup populates alloc_devs[plane] = &pdev->dev for
both queues; allow_cache_hints=1 on both.
- daedalus_export_capture_dmabuf(cookie, plane, flags, *fd):
walks inflight list, calls vb2_core_expbuf on the CAPTURE
buffer in the caller's (daemon's) task context.
- device_run fills the new REQ_DECODE capture fields from
ctx->dst_fmt and maps ctx->src_fmt.pixelformat to
DAEDALUS_CODEC_VP9 / _AV1 / _H264 (was hard-wired to VP9).
- daedalus_complete_resp_frame handles both the 8.5 inline
path (kept for debugging) and the 8.6 dmabuf path (pixels
already in CAPTURE buffer, just set payload from metadata).
- enum_fmt advertises all 3 OUTPUT formats (VP9F, AV1F, S264).
- try_fmt preserves userspace colorspace fields instead of
overwriting with REC709 defaults (fixes 8.5 compliance fail).
- s_fmt propagates OUTPUT colorspace → CAPTURE (stateless
decoder round-trip test at v4l2-test-formats.cpp:958).
- 12 V4L2 stateless controls registered per open (VP9_FRAME,
VP9_COMPRESSED_HDR, H264_SPS/PPS/SCALING/PRED_WEIGHTS/
SLICE_PARAMS/DECODE_PARAMS, AV1_FRAME/SEQUENCE/
TILE_GROUP_ENTRY/FILM_GRAIN). Daemon ignores values (FFmpeg
re-parses); registration is what makes libva-v4l2-request
see us.
Kernel chardev (kernel/daedalus_v4l2_chardev.c):
- New unlocked_ioctl dispatching DAEDALUS_IOC_GET_DMABUF to
daedalus_export_capture_dmabuf.
- debugfs test_decode cookies unified with the m2m cookie
allocator via shared daedalus_next_cookie() — kills the
Phase 8.5 namespace collision.
Daemon (daemon/src/...):
- New dmabuf_capture.{c,h}: GET_DMABUF + mmap each plane on
REQ_DECODE; munmap + close on completion. O_RDWR | O_CLOEXEC
is essential — vb2_core_expbuf extracts O_ACCMODE from flags
and exports read-only by default (caught on first run; mmap
-EACCES on PROT_WRITE).
- decoder.{c,h}: lazily opens AV1 + H.264 AVCodecContexts in
addition to VP9 (dropped the -ENOSYS stubs). pack_nv12_to_planes
writes Y line-by-line into planes[0] with planes[0].stride;
interleaves Cb/Cr into planes[1] with planes[1].stride.
- chardev_client.c handle_req_decode: opens dmabuf planes,
runs decode (pixels land in CAPTURE buffer directly), closes
planes, sends metadata-only RESP_FRAME. No wire-pixel
allocation.
Test harness (tools/test_m2m_decode.c):
- Optional 5th arg `codec` (vp9 | av1 | h264). Same client
drives all three codecs.
Verification on hertz (Pi 5, 6.12.75+rpt-rpi-2712):
Bit-exact end-to-end vs `ffmpeg -pix_fmt nv12`:
VP9 1920x1080 3,110,400 bytes MATCH
AV1 128x96 18,432 bytes MATCH
H.264 128x96 18,432 bytes MATCH
VP9 1080p went through the full dmabuf path with no chardev
payload bloat — the same chardev that capped at 64 KiB in 8.5
now ferries metadata only and lets the daemon mmap+write a
3.1 MB frame directly into the V4L2 client's buffer.
v4l2-compliance:
Phase 8.1: 44/48
Phase 8.5: 44/48 (different fails after m2m landed)
Phase 8.6: 47/48
Only remaining: VIDIOC_(TRY_)DECODER_CMD (needs media
controller — explicitly Phase 8.7 work).
11 standard compound controls visible:
vp9_frame_decode_parameters, vp9_probabilities_updates,
h264_sequence_parameter_set, h264_picture_parameter_set,
h264_scaling_matrix, h264_prediction_weight_table,
h264_slice_parameters, h264_decode_parameters,
av1_sequence_parameters, av1_frame_parameters,
av1_film_grain (av1_tile_group_entry refused by hdl->error
on this kernel — skipped silently).
Clean SIGTERM + rmmod, no oops/WARN.
Roadmap update (docs/roadmap.md):
- Phase 8.6 marked closed with the closure-doc reference.
- Phase 8.7 reshaped to (1) media controller, (2) perf +
daedalus_dispatch_* substitution, (3) HDR/10-bit, (4)
long-form multi-frame streaming.
Per correctness-before-speed:
- Real V4L2 dmabuf via vb2_core_expbuf (not a sideband
fd-passing hack).
- O_RDWR access mode threaded through correctly.
- Strict pixel-byte comparison against ffmpeg, not "looks
right" eyeballing.
- Each compliance edge documented with the underlying test
source-line + the fix.
- All resource paths cleaned (munmap + close per plane on
every exit, including error paths).
Phase 8.7 next: media controller binding (closes last
compliance fail), per-frame profiling, QPU dispatch
substitution targeting 30fps@1080p from
30fps-floor-is-fine memory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ add_executable(daedalus_v4l2_daemon
|
||||
src/parser.c
|
||||
src/decoder.c
|
||||
src/chardev_client.c
|
||||
src/dmabuf_capture.c
|
||||
)
|
||||
|
||||
target_include_directories(daedalus_v4l2_daemon
|
||||
|
||||
+32
-23
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
#include "chardev_client.h"
|
||||
#include "decoder.h"
|
||||
#include "dmabuf_capture.h"
|
||||
#include "ffmpeg_loader.h"
|
||||
#include "log.h"
|
||||
|
||||
@@ -138,9 +139,9 @@ static int handle_req_decode(struct chardev_client *cli,
|
||||
{
|
||||
struct daedalus_req_decode req;
|
||||
struct daedalus_resp_frame resp;
|
||||
uint8_t *resp_buf = NULL;
|
||||
size_t pix_cap, pix_used = 0;
|
||||
struct daedalus_capture_planes planes;
|
||||
int rc;
|
||||
int decoded = 0;
|
||||
|
||||
if (hdr->payload_len < sizeof(req)) {
|
||||
log_err("REQ_DECODE cookie=%u: payload too short %u < %zu",
|
||||
@@ -160,34 +161,42 @@ static int handle_req_decode(struct chardev_client *cli,
|
||||
hdr->cookie, &resp, sizeof(resp));
|
||||
}
|
||||
|
||||
log_info("REQ_DECODE cookie=%u codec=%u bitstream=%u bytes",
|
||||
hdr->cookie, req.codec_id, req.bitstream_len);
|
||||
log_info("REQ_DECODE cookie=%u codec=%u bitstream=%u bytes capture=%ux%u %u planes",
|
||||
hdr->cookie, req.codec_id, req.bitstream_len,
|
||||
req.capture_width, req.capture_height,
|
||||
req.capture_num_planes);
|
||||
|
||||
/*
|
||||
* Build the response buffer as { struct daedalus_resp_frame,
|
||||
* <NV12 pixels> }. Cap pixel area at MAX_PAYLOAD - sizeof
|
||||
* for now (Phase 8.5); Phase 8.6 dmabuf removes the cap.
|
||||
* Open dmabuf-fds for every CAPTURE plane and mmap them.
|
||||
* If this fails we still attempt the decode (so the kernel
|
||||
* gets a structured error response) — but we pass NULL
|
||||
* planes so pixels aren't written anywhere.
|
||||
*/
|
||||
pix_cap = DAEDALUS_PROTO_MAX_PAYLOAD - sizeof(resp);
|
||||
resp_buf = malloc(sizeof(resp) + pix_cap);
|
||||
if (!resp_buf)
|
||||
return -ENOMEM;
|
||||
rc = daedalus_capture_planes_open(cli->fd, hdr->cookie, &req,
|
||||
&planes);
|
||||
if (rc < 0) {
|
||||
log_warn("REQ_DECODE cookie=%u: GET_DMABUF/mmap failed (%d); decode metadata-only",
|
||||
hdr->cookie, rc);
|
||||
/* planes is already zeroed by capture_planes_open */
|
||||
}
|
||||
|
||||
rc = daedalus_decoder_run_request(cli->decoder, &req,
|
||||
payload + sizeof(req), &resp,
|
||||
resp_buf + sizeof(resp),
|
||||
pix_cap, &pix_used);
|
||||
if (rc < 0) {
|
||||
free(resp_buf);
|
||||
return rc;
|
||||
}
|
||||
/* Header at front; pixels follow. Truncate to actual used. */
|
||||
memcpy(resp_buf, &resp, sizeof(resp));
|
||||
planes.nr ? &planes : NULL);
|
||||
decoded = (rc >= 0);
|
||||
|
||||
rc = send_response(cli, DAEDALUS_MSG_RESP_FRAME, hdr->cookie,
|
||||
resp_buf, sizeof(resp) + pix_used);
|
||||
free(resp_buf);
|
||||
return rc;
|
||||
daedalus_capture_planes_close(&planes);
|
||||
|
||||
if (!decoded)
|
||||
return rc;
|
||||
|
||||
/*
|
||||
* RESP_FRAME is metadata-only in Phase 8.6 — pixels already
|
||||
* live in the V4L2 client's CAPTURE buffer via the dmabuf
|
||||
* the daemon wrote to in pack_nv12_to_planes.
|
||||
*/
|
||||
return send_response(cli, DAEDALUS_MSG_RESP_FRAME, hdr->cookie,
|
||||
&resp, sizeof(resp));
|
||||
}
|
||||
|
||||
static int handle_ping(struct chardev_client *cli,
|
||||
|
||||
+80
-61
@@ -73,6 +73,10 @@ void daedalus_decoder_cleanup(struct daedalus_decoder *dec)
|
||||
return;
|
||||
if (dec->ctx_vp9)
|
||||
dec->loader->avcodec_free_context(&dec->ctx_vp9);
|
||||
if (dec->ctx_av1)
|
||||
dec->loader->avcodec_free_context(&dec->ctx_av1);
|
||||
if (dec->ctx_h264)
|
||||
dec->loader->avcodec_free_context(&dec->ctx_h264);
|
||||
if (dec->frame)
|
||||
dec->loader->av_frame_free(&dec->frame);
|
||||
if (dec->pkt)
|
||||
@@ -91,26 +95,32 @@ static int decoder_open_codec(struct daedalus_decoder *dec, uint32_t codec_id,
|
||||
const struct AVCodec *codec;
|
||||
struct AVCodecContext *ctx;
|
||||
enum AVCodecID av_id;
|
||||
struct AVCodecContext **cache;
|
||||
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;
|
||||
}
|
||||
cache = &dec->ctx_vp9;
|
||||
break;
|
||||
case DAEDALUS_CODEC_AV1:
|
||||
av_id = AV_CODEC_ID_AV1;
|
||||
cache = &dec->ctx_av1;
|
||||
break;
|
||||
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;
|
||||
av_id = AV_CODEC_ID_H264;
|
||||
cache = &dec->ctx_h264;
|
||||
break;
|
||||
default:
|
||||
log_warn("decoder: unknown codec_id %u", codec_id);
|
||||
return -ENOSYS;
|
||||
}
|
||||
|
||||
if (*cache) {
|
||||
*out = *cache;
|
||||
return 0;
|
||||
}
|
||||
|
||||
codec = fm->avcodec_find_decoder(av_id);
|
||||
if (!codec) {
|
||||
log_err("decoder: avcodec_find_decoder(%d) returned NULL", av_id);
|
||||
@@ -126,64 +136,79 @@ static int decoder_open_codec(struct daedalus_decoder *dec, uint32_t codec_id,
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
dec->ctx_vp9 = ctx;
|
||||
*cache = ctx;
|
||||
*out = ctx;
|
||||
log_info("decoder: opened %s context", codec->name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Pack the decoded YUV planes into NV12 (Y followed by interleaved
|
||||
* CbCr) in @out, advancing @out and tracking total bytes written
|
||||
* in @used. Truncates silently at @cap. Returns 0 on success,
|
||||
* -EINVAL if the source format isn't planar YUV 4:2:0.
|
||||
* Pack the decoded YUV planes into NV12M layout across two
|
||||
* mapped CAPTURE planes:
|
||||
* planes[0] = Y, written w bytes per row with stride dst_y_stride
|
||||
* planes[1] = interleaved CbCr at half-res, two bytes per chroma
|
||||
* sample, written cw*2 bytes per row with stride
|
||||
* dst_uv_stride
|
||||
*
|
||||
* Source stride padding (fr->linesize[*]) is stripped; destination
|
||||
* stride padding (dst_stride - row_bytes) is left as-is — the V4L2
|
||||
* client knows the format's bytesperline and walks accordingly.
|
||||
*
|
||||
* Returns 0 on success, -EINVAL if the source is not planar 4:2:0
|
||||
* (Phase 8.6 still expects yuv420p-class outputs; 8.7 widens).
|
||||
*/
|
||||
static int pack_nv12(struct AVFrame *fr, const AVPixFmtDescriptor *desc,
|
||||
uint8_t *out, size_t cap, size_t *used)
|
||||
static int pack_nv12_to_planes(struct AVFrame *fr,
|
||||
const AVPixFmtDescriptor *desc,
|
||||
const struct daedalus_capture_planes *planes)
|
||||
{
|
||||
int y, cw, ch, h, w;
|
||||
size_t y_size, uv_size;
|
||||
int h = fr->height;
|
||||
int w = fr->width;
|
||||
int cw, ch;
|
||||
size_t row_y, row_uv;
|
||||
int y, x;
|
||||
uint8_t *dst_y, *dst_uv;
|
||||
uint32_t dst_y_stride, dst_uv_stride;
|
||||
|
||||
*used = 0;
|
||||
if (!desc || !out || !cap)
|
||||
if (!desc || !planes || planes->nr < 2)
|
||||
return -EINVAL;
|
||||
if (desc->nb_components < 3)
|
||||
return -EINVAL;
|
||||
if (desc->log2_chroma_w != 1 || desc->log2_chroma_h != 1)
|
||||
return -EINVAL; /* not 4:2:0 — would need a different pack */
|
||||
|
||||
w = fr->width;
|
||||
h = fr->height;
|
||||
cw = AV_CEIL_RSHIFT(w, desc->log2_chroma_w);
|
||||
ch = AV_CEIL_RSHIFT(h, desc->log2_chroma_h);
|
||||
y_size = (size_t) w * (size_t) h;
|
||||
uv_size = (size_t) cw * (size_t) ch * 2u;
|
||||
|
||||
/* Y plane: w bytes per line stripped of stride padding. */
|
||||
if (*used + y_size > cap)
|
||||
dst_y = planes->base[0];
|
||||
dst_uv = planes->base[1];
|
||||
dst_y_stride = planes->stride[0] ? planes->stride[0] : (uint32_t) w;
|
||||
dst_uv_stride = planes->stride[1] ? planes->stride[1] : (uint32_t) (cw * 2);
|
||||
|
||||
row_y = (size_t) w;
|
||||
row_uv = (size_t) cw * 2u;
|
||||
|
||||
if (!dst_y || !dst_uv)
|
||||
return -EINVAL;
|
||||
|
||||
/* Y plane copy — strip source stride padding. */
|
||||
for (y = 0; y < h; y++)
|
||||
memcpy(out + *used + (size_t) y * (size_t) w,
|
||||
fr->data[0] + (size_t) y * (size_t) fr->linesize[0],
|
||||
(size_t) w);
|
||||
*used += y_size;
|
||||
memcpy(dst_y + (size_t) y * dst_y_stride,
|
||||
fr->data[0] + (size_t) y * fr->linesize[0],
|
||||
row_y);
|
||||
|
||||
/* Interleave Cb and Cr into a single NV12 chroma plane. */
|
||||
if (*used + uv_size > cap)
|
||||
return -EINVAL;
|
||||
{
|
||||
uint8_t *uv = out + *used;
|
||||
int x;
|
||||
for (y = 0; y < ch; y++) {
|
||||
const uint8_t *u = fr->data[1] +
|
||||
(size_t) y * (size_t) fr->linesize[1];
|
||||
const uint8_t *v = fr->data[2] +
|
||||
(size_t) y * (size_t) fr->linesize[2];
|
||||
for (x = 0; x < cw; x++) {
|
||||
uv[(y * cw + x) * 2 + 0] = u[x];
|
||||
uv[(y * cw + x) * 2 + 1] = v[x];
|
||||
}
|
||||
/* Interleave Cb and Cr into NV12 chroma plane. */
|
||||
for (y = 0; y < ch; y++) {
|
||||
const uint8_t *u = fr->data[1] +
|
||||
(size_t) y * fr->linesize[1];
|
||||
const uint8_t *v = fr->data[2] +
|
||||
(size_t) y * fr->linesize[2];
|
||||
uint8_t *row = dst_uv + (size_t) y * dst_uv_stride;
|
||||
for (x = 0; x < cw; x++) {
|
||||
row[x * 2 + 0] = u[x];
|
||||
row[x * 2 + 1] = v[x];
|
||||
}
|
||||
*used += uv_size;
|
||||
}
|
||||
(void) row_uv;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -191,8 +216,7 @@ int daedalus_decoder_run_request(struct daedalus_decoder *dec,
|
||||
const struct daedalus_req_decode *req,
|
||||
const uint8_t *bitstream,
|
||||
struct daedalus_resp_frame *resp,
|
||||
uint8_t *nv12_out, size_t nv12_cap,
|
||||
size_t *nv12_used)
|
||||
const struct daedalus_capture_planes *planes)
|
||||
{
|
||||
struct ffmpeg_loader *fm = dec->loader;
|
||||
struct AVCodecContext *ctx = NULL;
|
||||
@@ -200,8 +224,6 @@ int daedalus_decoder_run_request(struct daedalus_decoder *dec,
|
||||
|
||||
memset(resp, 0, sizeof(*resp));
|
||||
resp->codec_id = req->codec_id;
|
||||
if (nv12_used)
|
||||
*nv12_used = 0;
|
||||
|
||||
rc = decoder_open_codec(dec, req->codec_id, &ctx);
|
||||
if (rc == -ENOSYS) {
|
||||
@@ -315,25 +337,22 @@ int daedalus_decoder_run_request(struct daedalus_decoder *dec,
|
||||
resp->fnv1a_yuv = h;
|
||||
|
||||
/*
|
||||
* Pack pixels as NV12 into the caller's buffer if
|
||||
* provided and big enough. Truncation is silent —
|
||||
* the kernel will only copy as much as fits in the
|
||||
* CAPTURE plane, so partial fills are fine for Phase
|
||||
* 8.5. Phase 8.6 (dmabuf) eliminates the truncation.
|
||||
* Pack pixels as NV12 directly into the mapped CAPTURE
|
||||
* dmabuf planes. No copy into a wire buffer — pixels
|
||||
* land in the V4L2 client's CAPTURE buffer the moment
|
||||
* the write touches the mmap.
|
||||
*/
|
||||
if (nv12_out && nv12_cap) {
|
||||
int prc = pack_nv12(fr, desc, nv12_out, nv12_cap,
|
||||
nv12_used);
|
||||
if (planes && planes->nr >= 2) {
|
||||
int prc = pack_nv12_to_planes(fr, desc, planes);
|
||||
if (prc < 0)
|
||||
log_warn("decoder: NV12 pack failed (cap=%zu pix_fmt=%d) — kernel will see metadata only",
|
||||
nv12_cap, fr->format);
|
||||
log_warn("decoder: NV12-pack-to-planes failed (pix_fmt=%d planes=%d) — kernel will see metadata only",
|
||||
fr->format, planes->nr);
|
||||
}
|
||||
|
||||
log_info("decoder: OK %dx%d fmt=%d (%s) fnv1a=0x%08x luma=%u chroma=%u nv12=%zu",
|
||||
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,
|
||||
nv12_used ? *nv12_used : 0u);
|
||||
h, luma_len, chroma_len);
|
||||
}
|
||||
|
||||
fm->av_frame_unref(dec->frame);
|
||||
|
||||
+15
-13
@@ -15,6 +15,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#include "daedalus_v4l2_proto.h"
|
||||
#include "dmabuf_capture.h"
|
||||
|
||||
struct ffmpeg_loader;
|
||||
struct AVCodecContext;
|
||||
@@ -26,12 +27,16 @@ struct AVFrame;
|
||||
* @loader: borrowed FFmpeg loader (must outlive the decoder)
|
||||
* @ctx_vp9: lazily-opened VP9 AVCodecContext (NULL until first
|
||||
* VP9 REQ_DECODE)
|
||||
* @ctx_av1: lazily-opened AV1 AVCodecContext
|
||||
* @ctx_h264: lazily-opened H.264 AVCodecContext
|
||||
* @pkt: shared AVPacket reused across requests
|
||||
* @frame: shared AVFrame reused across requests
|
||||
*/
|
||||
struct daedalus_decoder {
|
||||
struct ffmpeg_loader *loader;
|
||||
struct AVCodecContext *ctx_vp9;
|
||||
struct AVCodecContext *ctx_av1;
|
||||
struct AVCodecContext *ctx_h264;
|
||||
struct AVPacket *pkt;
|
||||
struct AVFrame *frame;
|
||||
};
|
||||
@@ -56,24 +61,21 @@ void daedalus_decoder_cleanup(struct daedalus_decoder *dec);
|
||||
* @req: REQ_DECODE prefix (from the wire)
|
||||
* @bitstream: bitstream blob (req->bitstream_len bytes)
|
||||
* @resp: caller-allocated RESP_FRAME output (zeroed by callee)
|
||||
* @nv12_out: caller-allocated buffer for NV12 pixel data
|
||||
* (Y plane followed by interleaved CbCr); may be NULL
|
||||
* if @nv12_cap is 0
|
||||
* @nv12_cap: bytes available at @nv12_out (truncated silently
|
||||
* if smaller than @resp->luma_len + @resp->chroma_len)
|
||||
* @nv12_used: out: bytes actually written into @nv12_out
|
||||
* @planes: mapped CAPTURE planes (Phase 8.6 dmabuf path). If
|
||||
* NULL or planes->nr == 0, the decoder runs but
|
||||
* writes no pixels — caller still gets dims + digest.
|
||||
*
|
||||
* Populates @resp with the decode outcome and @nv12_out with the
|
||||
* NV12-packed pixels (Y plane, then interleaved Cb/Cr). Always
|
||||
* returns 0; decode-level failures are reported via @resp->status
|
||||
* and @nv12_used = 0 so the kernel sees a structured response
|
||||
* rather than a dropped request.
|
||||
* Populates @resp with the decode outcome and writes decoded
|
||||
* pixels (NV12 layout: Y to plane 0, interleaved CbCr to plane
|
||||
* 1) directly into the mapped dmabuf planes. 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,
|
||||
uint8_t *nv12_out, size_t nv12_cap,
|
||||
size_t *nv12_used);
|
||||
const struct daedalus_capture_planes *planes);
|
||||
|
||||
#endif /* DAEDALUS_V4L2_DECODER_H */
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||
/*
|
||||
* dmabuf_capture.c — daemon-side CAPTURE-plane dmabuf access.
|
||||
*/
|
||||
#include "dmabuf_capture.h"
|
||||
#include "log.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
int daedalus_capture_planes_open(int chardev_fd, uint32_t cookie,
|
||||
const struct daedalus_req_decode *req,
|
||||
struct daedalus_capture_planes *out)
|
||||
{
|
||||
int i;
|
||||
int rc;
|
||||
|
||||
memset(out, 0, sizeof(*out));
|
||||
for (i = 0; i < DAEDALUS_MAX_CAPTURE_PLANES; i++)
|
||||
out->fd[i] = -1;
|
||||
|
||||
if (req->capture_num_planes == 0 ||
|
||||
req->capture_num_planes > DAEDALUS_MAX_CAPTURE_PLANES) {
|
||||
log_err("dmabuf_capture: bad num_planes=%u",
|
||||
req->capture_num_planes);
|
||||
return -EINVAL;
|
||||
}
|
||||
out->nr = (int) req->capture_num_planes;
|
||||
|
||||
for (i = 0; i < out->nr; i++) {
|
||||
struct daedalus_get_dmabuf gd = {
|
||||
.cookie = cookie,
|
||||
.plane = (uint32_t) i,
|
||||
/*
|
||||
* O_RDWR is required so the kernel exports the
|
||||
* dmabuf in a mode that allows PROT_READ |
|
||||
* PROT_WRITE mmap. vb2_core_expbuf extracts
|
||||
* the access mode (O_ACCMODE) from flags and
|
||||
* hands it to the memop's get_dmabuf; default
|
||||
* (O_RDONLY) gives mmap -EACCES on writes.
|
||||
*/
|
||||
.flags = O_RDWR | O_CLOEXEC,
|
||||
.fd = -1,
|
||||
};
|
||||
size_t size = req->capture_plane_size[i];
|
||||
|
||||
if (size == 0) {
|
||||
log_err("dmabuf_capture: cookie=%u plane=%d size=0",
|
||||
cookie, i);
|
||||
rc = -EINVAL;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (ioctl(chardev_fd, DAEDALUS_IOC_GET_DMABUF, &gd) < 0) {
|
||||
rc = -errno;
|
||||
log_err("dmabuf_capture: GET_DMABUF cookie=%u plane=%d: %s",
|
||||
cookie, i, strerror(errno));
|
||||
goto fail;
|
||||
}
|
||||
out->fd[i] = gd.fd;
|
||||
out->size[i] = size;
|
||||
out->stride[i] = req->capture_plane_stride[i];
|
||||
|
||||
out->base[i] = mmap(NULL, size, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, gd.fd, 0);
|
||||
if (out->base[i] == MAP_FAILED) {
|
||||
rc = -errno;
|
||||
log_err("dmabuf_capture: mmap cookie=%u plane=%d size=%zu: %s",
|
||||
cookie, i, size, strerror(errno));
|
||||
out->base[i] = NULL;
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
daedalus_capture_planes_close(out);
|
||||
return rc;
|
||||
}
|
||||
|
||||
void daedalus_capture_planes_close(struct daedalus_capture_planes *p)
|
||||
{
|
||||
int i;
|
||||
if (!p)
|
||||
return;
|
||||
for (i = 0; i < DAEDALUS_MAX_CAPTURE_PLANES; i++) {
|
||||
if (p->base[i] && p->base[i] != MAP_FAILED) {
|
||||
munmap(p->base[i], p->size[i]);
|
||||
p->base[i] = NULL;
|
||||
}
|
||||
if (p->fd[i] >= 0) {
|
||||
close(p->fd[i]);
|
||||
p->fd[i] = -1;
|
||||
}
|
||||
}
|
||||
p->nr = 0;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||
/*
|
||||
* dmabuf_capture.h — daemon-side CAPTURE-plane dmabuf access.
|
||||
*
|
||||
* For every REQ_DECODE the daemon fetches a dmabuf fd per
|
||||
* CAPTURE plane via the chardev DAEDALUS_IOC_GET_DMABUF ioctl,
|
||||
* mmaps it, then asks the decoder to write decoded pixels
|
||||
* directly into the mapping. No inline pixel data in the
|
||||
* chardev payload — Phase 8.6's reason for being.
|
||||
*/
|
||||
#ifndef DAEDALUS_V4L2_DMABUF_CAPTURE_H
|
||||
#define DAEDALUS_V4L2_DMABUF_CAPTURE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "daedalus_v4l2_proto.h"
|
||||
|
||||
#define DAEDALUS_MAX_CAPTURE_PLANES 3
|
||||
|
||||
/**
|
||||
* struct daedalus_capture_planes - mapped CAPTURE buffer planes
|
||||
* @nr: number of valid planes (0..DAEDALUS_MAX_CAPTURE_PLANES)
|
||||
* @fd: dmabuf fd per plane; -1 if not opened
|
||||
* @base: mmap'd pointer per plane; NULL if not mapped
|
||||
* @size: mapped length per plane (== capture_plane_size[i])
|
||||
* @stride: bytes-per-line per plane (from V4L2 fmt)
|
||||
*/
|
||||
struct daedalus_capture_planes {
|
||||
int nr;
|
||||
int fd[DAEDALUS_MAX_CAPTURE_PLANES];
|
||||
void *base[DAEDALUS_MAX_CAPTURE_PLANES];
|
||||
size_t size[DAEDALUS_MAX_CAPTURE_PLANES];
|
||||
uint32_t stride[DAEDALUS_MAX_CAPTURE_PLANES];
|
||||
};
|
||||
|
||||
/**
|
||||
* daedalus_capture_planes_open() - GET_DMABUF + mmap each plane
|
||||
* @chardev_fd: open /dev/daedalus-v4l2 fd
|
||||
* @cookie: cookie from REQ_DECODE (passes to GET_DMABUF)
|
||||
* @req: REQ_DECODE payload (carries plane count + sizes + strides)
|
||||
* @out: zeroed on entry; populated on success
|
||||
*
|
||||
* On any failure, partial state is rolled back and *out is
|
||||
* left zeroed. Return: 0 on success, -errno on failure.
|
||||
*/
|
||||
int daedalus_capture_planes_open(int chardev_fd, uint32_t cookie,
|
||||
const struct daedalus_req_decode *req,
|
||||
struct daedalus_capture_planes *out);
|
||||
|
||||
/**
|
||||
* daedalus_capture_planes_close() - munmap + close every plane
|
||||
*/
|
||||
void daedalus_capture_planes_close(struct daedalus_capture_planes *p);
|
||||
|
||||
#endif /* DAEDALUS_V4L2_DMABUF_CAPTURE_H */
|
||||
@@ -0,0 +1,316 @@
|
||||
# Phase 8.6 closure — dmabuf + AV1 + H.264 + stateless controls
|
||||
|
||||
**Status:** closed 2026-05-18.
|
||||
|
||||
Phase 8.5 shipped a working V4L2 m2m driver but capped frame
|
||||
size at 64 KiB because RESP_FRAME carried inline pixel data
|
||||
through the chardev. Phase 8.6 removes that cap by exporting
|
||||
CAPTURE buffers as dmabuf-fds the daemon mmaps and writes
|
||||
directly into. Along the way: AV1 + H.264 codecs land, V4L2
|
||||
stateless controls register so libva-v4l2-request will
|
||||
recognise us as a real stateless decoder, and the remaining
|
||||
v4l2-compliance edges get polished (47/48 passing now, only
|
||||
DECODER_CMD remains — that wants a media controller, deferred
|
||||
to a follow-up).
|
||||
|
||||
## What lands
|
||||
|
||||
### Protocol (`include/daedalus_v4l2_proto.h`)
|
||||
- `struct daedalus_req_decode` grew capture-buffer metadata:
|
||||
`capture_width`, `capture_height`, `capture_pix_fmt`,
|
||||
`capture_num_planes`, per-plane `size[3]` + `stride[3]`,
|
||||
`flags`. The daemon needs these to call
|
||||
`DAEDALUS_IOC_GET_DMABUF` and to know how to lay out NV12
|
||||
in the mapped plane.
|
||||
- New `struct daedalus_get_dmabuf` + `DAEDALUS_IOC_GET_DMABUF`
|
||||
ioctl (`_IOWR('D', 1, ...)`). Takes `(cookie, plane,
|
||||
flags)`, returns an fd installed in the calling task's fd
|
||||
table.
|
||||
|
||||
### Kernel m2m driver (`kernel/daedalus_v4l2_main.c`)
|
||||
- **Both queues switched to `vb2_dma_contig_memops`**. OUTPUT
|
||||
was vmalloc in Phase 8.5; the switch makes
|
||||
`V4L2_MEMORY_FLAG_NON_COHERENT` work on REQBUFS (vmalloc
|
||||
doesn't honour the flag and v4l2-compliance rejected the
|
||||
driver because of it). We still read the bitstream via
|
||||
`vb2_plane_vaddr` — dma_contig provides a kernel virtual
|
||||
address just like vmalloc did.
|
||||
- `dma_coerce_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32))`
|
||||
in probe so the platform device satisfies vb2_dma_contig's
|
||||
parent-device requirement.
|
||||
- `queue_setup` now populates `alloc_devs[plane] =
|
||||
&pdev->dev` for both queues.
|
||||
- Both queues set `allow_cache_hints = 1` and `io_modes`
|
||||
expanded to include `VB2_DMABUF` (CAPTURE also gains it
|
||||
for the daemon's mmap path).
|
||||
- New `daedalus_export_capture_dmabuf(cookie, plane, flags,
|
||||
out_fd)`: walks the inflight list, calls `vb2_core_expbuf`
|
||||
on the CAPTURE buffer in the caller's (daemon's) task
|
||||
context. Backs the new chardev ioctl.
|
||||
- `device_run` extended to fill the new REQ_DECODE capture
|
||||
fields from `ctx->dst_fmt`, and to map
|
||||
`ctx->src_fmt.pixelformat` → `DAEDALUS_CODEC_VP9 / _AV1
|
||||
/ _H264` (was hard-wired to VP9 in 8.5).
|
||||
- `daedalus_complete_resp_frame` now handles both the Phase
|
||||
8.5 inline-pixel path (still in tree for debugging /
|
||||
back-compat) and the Phase 8.6 dmabuf path where
|
||||
`pixels_len == 0` and pixels already live in the CAPTURE
|
||||
buffer — sets plane payload from the daemon's metadata
|
||||
either way.
|
||||
- `enum_fmt` advertises all three OUTPUT formats:
|
||||
`VP9_FRAME`, `AV1_FRAME`, `H264_SLICE`.
|
||||
- `try_fmt` preserves userspace-supplied colorspace / xfer /
|
||||
ycbcr_enc / quantization across the fill helper (was
|
||||
rewriting to REC709 defaults; v4l2-compliance failed the
|
||||
TRY_FMT colorspace round-trip in 8.5).
|
||||
- `s_fmt` propagates OUTPUT colorspace to CAPTURE so a
|
||||
follow-up G_FMT on CAPTURE returns matching values
|
||||
(v4l2-test-formats.cpp:958 stateless-decoder round-trip).
|
||||
- Twelve V4L2 stateless controls registered per open:
|
||||
`STATELESS_VP9_FRAME`, `STATELESS_VP9_COMPRESSED_HDR`,
|
||||
`STATELESS_H264_{SPS,PPS,SCALING_MATRIX,PRED_WEIGHTS,
|
||||
SLICE_PARAMS,DECODE_PARAMS}`,
|
||||
`STATELESS_AV1_{FRAME,SEQUENCE,TILE_GROUP_ENTRY,FILM_GRAIN}`.
|
||||
Daemon ignores values (FFmpeg parses headers itself); the
|
||||
registration is what makes libva-v4l2-request see us.
|
||||
|
||||
### Kernel chardev (`kernel/daedalus_v4l2_chardev.c`)
|
||||
- New `unlocked_ioctl` handler dispatching
|
||||
`DAEDALUS_IOC_GET_DMABUF` to
|
||||
`daedalus_export_capture_dmabuf`.
|
||||
- debugfs `test_decode` cookies unified with the m2m
|
||||
`daedalus_next_cookie()` allocator so the two namespaces
|
||||
never collide.
|
||||
|
||||
### Daemon
|
||||
|
||||
- **New** `daemon/src/dmabuf_capture.{c,h}`:
|
||||
- `daedalus_capture_planes_open(chardev_fd, cookie, req,
|
||||
out)`: ioctl GET_DMABUF per plane with
|
||||
`O_RDWR | O_CLOEXEC` (O_RDWR is essential —
|
||||
vb2_core_expbuf uses `flags & O_ACCMODE` to choose the
|
||||
dma_buf's access mode, default O_RDONLY gives mmap
|
||||
`-EACCES` on writes), mmap each plane, populate
|
||||
`{fd, base, size, stride}` per plane.
|
||||
- `daedalus_capture_planes_close(p)`: munmap + close each
|
||||
plane.
|
||||
|
||||
- `daemon/src/decoder.c`:
|
||||
- Lazily opens AV1 + H.264 AVCodecContexts in addition to
|
||||
VP9 (the existing -ENOSYS stubs were dropped — FFmpeg
|
||||
decodes all three via dlopen).
|
||||
- `pack_nv12_to_planes`: writes Y line-by-line into
|
||||
`planes[0].base` with `planes[0].stride`, interleaves
|
||||
Cb/Cr into `planes[1].base` with `planes[1].stride`.
|
||||
Strips source stride padding from `fr->linesize[*]`;
|
||||
respects destination strides so libav alignment doesn't
|
||||
corrupt the V4L2 client's view.
|
||||
- `daedalus_decoder_run_request` signature simplified —
|
||||
takes the mapped planes instead of a wire buffer.
|
||||
|
||||
- `daemon/src/chardev_client.c`:
|
||||
- `handle_req_decode` opens dmabuf planes, runs decode
|
||||
(writing pixels straight into the mapped CAPTURE
|
||||
buffer), closes planes, sends a metadata-only
|
||||
RESP_FRAME. No wire-pixel allocation at all.
|
||||
|
||||
- `daemon/CMakeLists.txt`: includes `dmabuf_capture.c`.
|
||||
|
||||
### Test harness (`tools/test_m2m_decode.c`)
|
||||
- Optional 5th argument `codec` (vp9 | av1 | h264) selects
|
||||
the OUTPUT fourcc. Same client drives all three codecs.
|
||||
|
||||
## Verification
|
||||
|
||||
### Bit-exact end-to-end vs reference FFmpeg
|
||||
|
||||
| Codec | Frame | Bitstream | Decoded NV12 | vs `ffmpeg -pix_fmt nv12` |
|
||||
|-------|--------|-----------|--------------|---------------------------|
|
||||
| VP9 | 1920×1080 | 12690 B | 3,110,400 B | **byte-for-byte match ✓** |
|
||||
| AV1 | 128×96 | 456 B | 18,432 B | **byte-for-byte match ✓** |
|
||||
| H.264 | 128×96 | 1863 B | 18,432 B | **byte-for-byte match ✓** |
|
||||
|
||||
VP9 1080p decode went through the full dmabuf path with no
|
||||
chardev payload bloat — the same chardev that capped at 64 KiB
|
||||
in Phase 8.5 now ferries metadata only and lets the daemon
|
||||
mmap+write a 3.1 MB frame directly into the V4L2 client's
|
||||
buffer.
|
||||
|
||||
### v4l2-compliance
|
||||
|
||||
```
|
||||
Total for daedalus_v4l2 device /dev/video0: 48, Succeeded: 47, Failed: 1
|
||||
```
|
||||
|
||||
| Phase | Pass | Fail | Delta |
|
||||
|-------|------|------|-------|
|
||||
| 8.1 | 44/48 | 4 | baseline |
|
||||
| 8.5 | 44/48 | 4 | (different fails — m2m wired) |
|
||||
| 8.6 | 47/48 | 1 | +3 pass |
|
||||
|
||||
The one remaining fail is `VIDIOC_(TRY_)DECODER_CMD`:
|
||||
|
||||
```
|
||||
fail: v4l2-test-codecs.cpp(105):
|
||||
(node->codec_mask & STATELESS_DECODER) && !node->has_media
|
||||
```
|
||||
|
||||
Compliance requires stateless decoders to bind a media
|
||||
controller (for the request API). That's the next milestone;
|
||||
not addressed in 8.6.
|
||||
|
||||
### Format + control enumeration
|
||||
|
||||
```
|
||||
$ v4l2-ctl -d /dev/video0 --list-formats-out
|
||||
[0]: 'VP9F' (VP9 Frame, compressed)
|
||||
[1]: 'AV1F' (AV1 Frame, compressed)
|
||||
[2]: 'S264' (H.264 Parsed Slice Data, compressed)
|
||||
|
||||
$ v4l2-ctl -d /dev/video0 -l
|
||||
h264_sequence_parameter_set (has-payload)
|
||||
h264_picture_parameter_set (has-payload)
|
||||
h264_scaling_matrix (has-payload)
|
||||
h264_prediction_weight_table (has-payload)
|
||||
h264_slice_parameters (has-payload)
|
||||
h264_decode_parameters (has-payload)
|
||||
vp9_frame_decode_parameters (has-payload)
|
||||
vp9_probabilities_updates (has-payload)
|
||||
av1_sequence_parameters (has-payload)
|
||||
av1_frame_parameters (has-payload)
|
||||
av1_film_grain (has-payload)
|
||||
|
||||
Standard Compound Controls: 11
|
||||
```
|
||||
|
||||
(av1_tile_group_entry got refused by `hdl->error` on this
|
||||
kernel — the v6.12 headers expose the CID but the v4l2-core
|
||||
control table doesn't include it, so we just skip and log at
|
||||
debug.)
|
||||
|
||||
### Clean shutdown
|
||||
|
||||
```
|
||||
$ pkill -TERM daedalus_v4l2_daemon # daemon exits cleanly
|
||||
$ sudo rmmod daedalus_v4l2 # ok
|
||||
$ sudo dmesg | grep -E 'BUG|oops'
|
||||
(empty)
|
||||
```
|
||||
|
||||
## Design decisions
|
||||
|
||||
### Why dmabuf-export over inline pixels?
|
||||
|
||||
The chardev payload is capped at 64 KiB to keep kmalloc
|
||||
allocations under the slab's reliability ceiling. A 1080p
|
||||
NV12 frame is 3.1 MB; 4K is 12.4 MB. No reasonable cap on
|
||||
inline pixels covers real-world content.
|
||||
|
||||
dmabuf gives us a zero-copy hand-off:
|
||||
- Kernel allocates CAPTURE buffers via CMA (vb2_dma_contig).
|
||||
- Daemon mmaps via the dmabuf-fd we hand it.
|
||||
- Daemon's decode-loop writes pixels straight into the V4L2
|
||||
client's CAPTURE buffer. No daemon-side allocation, no
|
||||
chardev memcpy, no payload limit.
|
||||
|
||||
### Why both queues on vb2_dma_contig?
|
||||
|
||||
Pure dma_contig everywhere simplifies the `queue_setup`
|
||||
plane-allocator assignment (no "OUTPUT is vmalloc / CAPTURE
|
||||
is dma_contig" special-case). More importantly,
|
||||
`V4L2_MEMORY_FLAG_NON_COHERENT` only works on memops that
|
||||
implement the non-coherent allocation path — vb2_vmalloc
|
||||
doesn't, so v4l2-compliance's REQBUFS test fails when any
|
||||
queue is on vmalloc. Switching OUTPUT to dma_contig closed
|
||||
the compliance gap without any functional cost (the kernel
|
||||
still gets a kernel-virtual-address out of
|
||||
`vb2_plane_vaddr`).
|
||||
|
||||
### Why O_RDWR in the ioctl call?
|
||||
|
||||
`vb2_core_expbuf` extracts the access mode from `flags &
|
||||
O_ACCMODE` and passes it to the memop's `get_dmabuf`.
|
||||
Default (O_RDONLY) creates a read-only dma_buf and userspace
|
||||
gets `mmap -EACCES` on PROT_WRITE. Caught on first run.
|
||||
|
||||
### Why a unified cookie allocator?
|
||||
|
||||
The Phase 8.5 closure flagged a debug-time confusion:
|
||||
debugfs `test_decode` and m2m `device_run` had independent
|
||||
atomic counters, both starting at 0 after each insmod. When
|
||||
both got used in the same boot, RESP_FRAME logs hit "unknown
|
||||
cookie" for one path's responses arriving at the other's
|
||||
namespace. One shared `daedalus_next_cookie()` makes the
|
||||
logs deterministic and the namespaces non-colliding.
|
||||
|
||||
### Why register stateless controls if we ignore the values?
|
||||
|
||||
libva-v4l2-request and other production clients probe
|
||||
`VIDIOC_QUERYCTRL` to verify the codec is supported. Without
|
||||
the controls registered, the driver appears as a
|
||||
"non-stateless" decoder to those clients and they refuse to
|
||||
drive it. Our daemon ignores the per-buffer values because
|
||||
FFmpeg re-parses the bitstream anyway — but the controls
|
||||
need to be visible. Phase 8.7+ wires them to a real codec
|
||||
state machine if we ever want to skip FFmpeg's reparse.
|
||||
|
||||
## Bugs found and fixed during the phase
|
||||
|
||||
### Bug 1: mmap EACCES on the daemon's CAPTURE dmabuf
|
||||
|
||||
Daemon called `ioctl(GET_DMABUF, flags=O_CLOEXEC)`.
|
||||
vb2_core_expbuf treated the omitted access mode as O_RDONLY
|
||||
and exported a read-only dma_buf; `mmap(PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED)` returned `-EACCES`. Fix: O_RDWR | O_CLOEXEC.
|
||||
|
||||
### Bug 2: REQBUFS rejection on V4L2_MEMORY_FLAG_NON_COHERENT
|
||||
|
||||
v4l2-compliance set the flag; vb2 rejected because:
|
||||
|
||||
1. OUTPUT queue used vb2_vmalloc which doesn't honour the
|
||||
flag.
|
||||
2. `allow_cache_hints = 0` (default) on both queues blocked
|
||||
the flag at the vb2 layer.
|
||||
|
||||
Fix: switch OUTPUT to vb2_dma_contig + set
|
||||
`allow_cache_hints = 1` on both queues.
|
||||
|
||||
### Bug 3: TRY_FMT clobbered userspace colorspace
|
||||
|
||||
`daedalus_fill_*_fmt` always set `colorspace =
|
||||
V4L2_COLORSPACE_REC709`. Compliance fed in
|
||||
V4L2_COLORSPACE_SMPTE170M and expected it to round-trip
|
||||
through TRY_FMT and propagate to CAPTURE on S_FMT. Fix:
|
||||
preserve the four colorspace fields in TRY_FMT (only
|
||||
default-fill when zero) and copy them OUTPUT → CAPTURE on
|
||||
S_FMT.
|
||||
|
||||
## What's NOT here (deferred)
|
||||
|
||||
- **Media controller binding** (`v4l2_m2m_register_media_controller`).
|
||||
This is what the last DECODER_CMD compliance failure needs.
|
||||
Roadmap puts it in Phase 8.7 alongside performance work,
|
||||
since it brings real complexity (media graph topology,
|
||||
request-API entity wiring) but no end-user functionality
|
||||
on its own.
|
||||
- **HDR / 10-bit pixel formats.** CAPTURE is NV12M only.
|
||||
P010M etc. need the AVPixFmtDescriptor walker in
|
||||
`pack_nv12_to_planes` to grow a depth-aware pack path.
|
||||
- **QPU dispatch substitution.** The daemon decodes
|
||||
entirely on CPU via FFmpeg. Substituting per-block
|
||||
`daedalus_dispatch_*` calls into FFmpeg's hot path needs
|
||||
FFmpeg-internal block-walker hooks — orthogonal to the
|
||||
V4L2 m2m work and lives in Phase 8.7.
|
||||
|
||||
## Phase 8.7 plan
|
||||
|
||||
1. Media controller integration (closes the last compliance
|
||||
fail).
|
||||
2. Performance work toward the 30fps@1080p user-facing
|
||||
target: profile the daemon's per-frame cost; substitute
|
||||
`daedalus_dispatch_*` for FFmpeg's per-block calls where
|
||||
the kernel matches.
|
||||
3. HDR / 10-bit support (P010M CAPTURE, depth-aware
|
||||
pack_nv12_to_planes).
|
||||
4. Long-form multi-frame streaming tests (not just one
|
||||
keyframe at a time).
|
||||
+32
-28
@@ -58,39 +58,43 @@ byte-for-byte identical to `ffmpeg -pix_fmt nv12` reference.
|
||||
|
||||
See `docs/phase_8_5_closure.md`.
|
||||
|
||||
### Phase 8.6 — dmabuf + AV1 + H.264 + stateless controls
|
||||
### Phase 8.6 — dmabuf + AV1 + H.264 + stateless controls (closed 2026-05-18)
|
||||
|
||||
Two interleaved tracks:
|
||||
- CAPTURE (and OUTPUT) on `vb2_dma_contig_memops`.
|
||||
- New `DAEDALUS_IOC_GET_DMABUF` chardev ioctl — daemon
|
||||
mmaps the in-flight CAPTURE buffer, decodes pixels in
|
||||
place, sends RESP_FRAME metadata-only.
|
||||
- 64 KiB frame-size cap removed. 1080p VP9 + 128×96 AV1
|
||||
+ 128×96 H.264 all byte-exact against reference FFmpeg
|
||||
decode.
|
||||
- V4L2 stateless controls registered for VP9 / AV1 /
|
||||
H.264 (11 controls visible to userspace).
|
||||
- Colorspace round-trip fix (TRY_FMT preserve, S_FMT
|
||||
OUTPUT→CAPTURE propagation).
|
||||
- Cookie unified across V4L2 + debugfs paths.
|
||||
- v4l2-compliance: 47/48 (only DECODER_CMD remains,
|
||||
needs media controller — moved to 8.7).
|
||||
|
||||
**Track A (dmabuf):**
|
||||
- Switch CAPTURE mem_ops to `vb2_dma_contig_memops`.
|
||||
- New `DAEDALUS_IOC_GET_DMABUF` on the chardev — daemon
|
||||
fetches a dmabuf-fd for the in-flight CAPTURE buffer,
|
||||
mmaps it, decodes pixels in place.
|
||||
- RESP_FRAME shrinks to metadata-only; chardev payload
|
||||
stops capping frame size.
|
||||
See `docs/phase_8_6_closure.md`.
|
||||
|
||||
**Track B (codecs + compliance):**
|
||||
- AV1 + H.264 codec contexts in the daemon (FFmpeg supports
|
||||
both already).
|
||||
- V4L2 stateless controls (VP9_FRAME, AV1_FRAME,
|
||||
H264_SLICE_PARAMS) — even if NULL-handled by the daemon,
|
||||
presence is needed for spec/compliance.
|
||||
- Media controller binding via
|
||||
`v4l2_m2m_register_media_controller`.
|
||||
- TRY_FMT colorspace round-trip.
|
||||
### Phase 8.7 — media controller, perf, HDR, long-form streams
|
||||
|
||||
Deliverable: real AV1/H.264 1080p clips decode end-to-end.
|
||||
1. Media controller binding via
|
||||
`v4l2_m2m_register_media_controller` (closes the last
|
||||
v4l2-compliance fail and unlocks the request API).
|
||||
2. Profile the daemon's per-frame cost on hertz; substitute
|
||||
`daedalus_dispatch_*` for FFmpeg's per-block paths where
|
||||
the kernel implementation matches. Target the
|
||||
`30fps-floor-is-fine` memory's daily-YouTube criterion:
|
||||
30fps@1080p with CPU left over for vscode.
|
||||
3. HDR / 10-bit support — P010M CAPTURE, depth-aware
|
||||
`pack_nv12_to_planes`.
|
||||
4. Long-form multi-frame streaming tests (B-frame refs,
|
||||
GOP boundaries) — current test client is one keyframe
|
||||
per run.
|
||||
|
||||
### Phase 8.7 — performance + 30fps@1080p
|
||||
|
||||
- Profile end-to-end pipeline.
|
||||
- Eliminate copies where possible.
|
||||
- Hit 30fps@1080p for daily YouTube videos
|
||||
(the project's user-facing success criterion per
|
||||
`30fps-floor-is-fine` memory).
|
||||
|
||||
Deliverable: 30fps stable on real content.
|
||||
Deliverable: 30fps stable on real content + full
|
||||
compliance pass.
|
||||
|
||||
## Effort estimate
|
||||
|
||||
|
||||
@@ -94,14 +94,33 @@ enum daedalus_codec_id {
|
||||
* struct daedalus_req_decode - REQ_DECODE payload prefix
|
||||
* @codec_id: enum daedalus_codec_id
|
||||
* @bitstream_len: bytes of bitstream following this struct
|
||||
* @capture_width: CAPTURE buffer width in pixels
|
||||
* @capture_height: CAPTURE buffer height in pixels
|
||||
* @capture_pix_fmt: V4L2 fourcc of the CAPTURE format
|
||||
* (e.g. V4L2_PIX_FMT_NV12M)
|
||||
* @capture_num_planes: number of dmabuf planes the daemon should
|
||||
* fetch via DAEDALUS_IOC_GET_DMABUF (1..3)
|
||||
* @capture_plane_size: per-plane sizeimage from V4L2 S_FMT
|
||||
* (plane[0..N-1]). Unused entries = 0.
|
||||
* @capture_plane_stride: per-plane bytesperline from V4L2 S_FMT.
|
||||
* @flags: reserved, must be zero
|
||||
*
|
||||
* Total payload_len for a REQ_DECODE = sizeof(struct
|
||||
* daedalus_req_decode) + bitstream_len.
|
||||
*
|
||||
* The daemon uses (capture_*) to fetch + mmap the right CAPTURE
|
||||
* plane via DAEDALUS_IOC_GET_DMABUF, then decodes pixels
|
||||
* directly into the dmabuf.
|
||||
*/
|
||||
struct daedalus_req_decode {
|
||||
__u32 codec_id;
|
||||
__u32 bitstream_len;
|
||||
__u32 capture_width;
|
||||
__u32 capture_height;
|
||||
__u32 capture_pix_fmt;
|
||||
__u32 capture_num_planes;
|
||||
__u32 capture_plane_size[3];
|
||||
__u32 capture_plane_stride[3];
|
||||
__u32 flags;
|
||||
};
|
||||
|
||||
@@ -155,4 +174,33 @@ struct daedalus_resp_frame {
|
||||
__u32 reserved;
|
||||
};
|
||||
|
||||
/* -- chardev ioctl ABI ----------------------------------------------- */
|
||||
|
||||
/**
|
||||
* struct daedalus_get_dmabuf - DAEDALUS_IOC_GET_DMABUF args
|
||||
* @cookie: cookie from the matching REQ_DECODE (in)
|
||||
* @plane: plane index, 0-based (in)
|
||||
* @flags: O_CLOEXEC etc.; passed through to dma_buf_fd (in)
|
||||
* @fd: exported dmabuf fd, installed in the calling
|
||||
* (daemon) task's fd table (out)
|
||||
*
|
||||
* The daemon calls this ioctl from REQ_DECODE handling to obtain
|
||||
* a per-plane dmabuf fd for the CAPTURE buffer the kernel
|
||||
* scheduled. The kernel resolves cookie → in-flight V4L2
|
||||
* request → CAPTURE vb2 buffer, then calls vb2_core_expbuf in
|
||||
* the daemon's task context (so the fd lands in the daemon's
|
||||
* fd table). Daemon mmaps the fd, writes decoded pixels in
|
||||
* place, munmaps and close()s — then sends RESP_FRAME.
|
||||
*/
|
||||
struct daedalus_get_dmabuf {
|
||||
__u32 cookie;
|
||||
__u32 plane;
|
||||
__u32 flags;
|
||||
__s32 fd;
|
||||
};
|
||||
|
||||
#define DAEDALUS_IOC_MAGIC 'D'
|
||||
#define DAEDALUS_IOC_GET_DMABUF \
|
||||
_IOWR(DAEDALUS_IOC_MAGIC, 1, struct daedalus_get_dmabuf)
|
||||
|
||||
#endif /* DAEDALUS_V4L2_PROTO_H */
|
||||
|
||||
@@ -322,6 +322,40 @@ static __poll_t daedalus_chardev_poll(struct file *file,
|
||||
return mask;
|
||||
}
|
||||
|
||||
/*
|
||||
* Phase 8.6 chardev ioctl: daemon uses DAEDALUS_IOC_GET_DMABUF
|
||||
* to fetch a dmabuf fd for the CAPTURE buffer the kernel
|
||||
* scheduled. The fd is installed in the calling task's fd
|
||||
* table by vb2_core_expbuf, so the daemon can mmap it directly.
|
||||
*/
|
||||
static long daedalus_chardev_ioctl(struct file *file, unsigned int cmd,
|
||||
unsigned long arg)
|
||||
{
|
||||
switch (cmd) {
|
||||
case DAEDALUS_IOC_GET_DMABUF: {
|
||||
struct daedalus_get_dmabuf k;
|
||||
int fd;
|
||||
int rc;
|
||||
|
||||
if (copy_from_user(&k, (void __user *) arg, sizeof(k)))
|
||||
return -EFAULT;
|
||||
rc = daedalus_export_capture_dmabuf(k.cookie, k.plane,
|
||||
k.flags, &fd);
|
||||
if (rc)
|
||||
return rc;
|
||||
k.fd = fd;
|
||||
if (copy_to_user((void __user *) arg, &k, sizeof(k))) {
|
||||
/* fd is already installed in caller's table; daemon
|
||||
* still must close it on this error path. */
|
||||
return -EFAULT;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
default:
|
||||
return -ENOTTY;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* .llseek intentionally unset. The chardev is a streaming
|
||||
* request/response channel; no positional semantics. Recent
|
||||
@@ -335,6 +369,7 @@ static const struct file_operations daedalus_chardev_fops = {
|
||||
.read = daedalus_chardev_read,
|
||||
.write = daedalus_chardev_write,
|
||||
.poll = daedalus_chardev_poll,
|
||||
.unlocked_ioctl = daedalus_chardev_ioctl,
|
||||
};
|
||||
|
||||
/* -- debugfs test trigger (Phase 8.2 only) --------------------------- */
|
||||
@@ -372,16 +407,14 @@ static const struct file_operations daedalus_test_ping_fops = {
|
||||
/*
|
||||
* Writing bitstream bytes to
|
||||
* /sys/kernel/debug/daedalus_v4l2/test_decode enqueues a REQ_DECODE
|
||||
* carrying those bytes as a VP9 access unit (Phase 8.4 fixed
|
||||
* codec). The wire payload prepends a struct daedalus_req_decode
|
||||
* header so the daemon knows the codec id and bitstream length.
|
||||
* carrying those bytes as a VP9 access unit (debugging utility;
|
||||
* the real production path is the V4L2 m2m queue). The wire
|
||||
* payload prepends a struct daedalus_req_decode header.
|
||||
*
|
||||
* Phase 8.6 generalises codec_id (via a sysfs / debugfs control);
|
||||
* for Phase 8.4 VP9 is hard-wired since that's what the cycle-9
|
||||
* stack targets first.
|
||||
* Phase 8.6: cookies come from the shared module-wide allocator
|
||||
* (daedalus_next_cookie) so debugfs and V4L2 cookies never
|
||||
* collide and RESP_FRAME logs stay deterministic.
|
||||
*/
|
||||
static atomic_t daedalus_decode_cookie = ATOMIC_INIT(0);
|
||||
|
||||
static ssize_t daedalus_test_decode_write(struct file *file,
|
||||
const char __user *buf,
|
||||
size_t count, loff_t *ppos)
|
||||
@@ -402,9 +435,15 @@ static ssize_t daedalus_test_decode_write(struct file *file,
|
||||
if (!blob)
|
||||
return -ENOMEM;
|
||||
|
||||
memset(&req, 0, sizeof(req));
|
||||
req.codec_id = DAEDALUS_CODEC_VP9;
|
||||
req.bitstream_len = (u32) count;
|
||||
req.flags = 0;
|
||||
/*
|
||||
* No CAPTURE plane info for the debugfs path — there's no
|
||||
* V4L2 client backing this REQ_DECODE. Daemon will see
|
||||
* capture_num_planes == 0 and run decode without writing
|
||||
* pixels anywhere.
|
||||
*/
|
||||
memcpy(blob, &req, sizeof(req));
|
||||
|
||||
if (copy_from_user(blob + sizeof(req), buf, count)) {
|
||||
@@ -412,14 +451,14 @@ static ssize_t daedalus_test_decode_write(struct file *file,
|
||||
return -EFAULT;
|
||||
}
|
||||
|
||||
cookie = (u32) atomic_inc_return(&daedalus_decode_cookie);
|
||||
cookie = daedalus_next_cookie();
|
||||
ret = daedalus_chardev_enqueue_req(DAEDALUS_MSG_REQ_DECODE, cookie,
|
||||
blob, total);
|
||||
kfree(blob);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
pr_info("daedalus_v4l2: REQ_DECODE enqueued cookie=%u codec=VP9 bitstream=%zu\n",
|
||||
pr_info("daedalus_v4l2: REQ_DECODE (debugfs) cookie=%u codec=VP9 bitstream=%zu\n",
|
||||
cookie, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
+292
-50
@@ -34,6 +34,7 @@
|
||||
#include <linux/platform_device.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/mutex.h>
|
||||
#include <linux/dma-mapping.h>
|
||||
|
||||
#include <media/v4l2-device.h>
|
||||
#include <media/v4l2-dev.h>
|
||||
@@ -42,7 +43,7 @@
|
||||
#include <media/v4l2-ctrls.h>
|
||||
#include <media/v4l2-event.h>
|
||||
#include <media/videobuf2-v4l2.h>
|
||||
#include <media/videobuf2-vmalloc.h>
|
||||
#include <media/videobuf2-dma-contig.h>
|
||||
|
||||
#include "daedalus_v4l2_chardev.h"
|
||||
#include "daedalus_v4l2_proto.h"
|
||||
@@ -51,9 +52,34 @@
|
||||
#define DAEDALUS_DRV_NAME "daedalus_v4l2"
|
||||
#define DAEDALUS_VIDEO_NAME "daedalus"
|
||||
|
||||
/* Coding-format coverage Phase 8.5: VP9 only. 8.6 adds AV1+H.264. */
|
||||
#define DAEDALUS_OUTPUT_FOURCC V4L2_PIX_FMT_VP9_FRAME
|
||||
#define DAEDALUS_CAPTURE_FOURCC V4L2_PIX_FMT_NV12M /* planar Y + interleaved CbCr */
|
||||
/*
|
||||
* Phase 8.6: OUTPUT side advertises VP9 + AV1 + H.264 stateless
|
||||
* formats (the daemon decodes all three via FFmpeg dlopen).
|
||||
* CAPTURE is NV12M for now; HDR / 10-bit comes later.
|
||||
*/
|
||||
static const u32 daedalus_output_formats[] = {
|
||||
V4L2_PIX_FMT_VP9_FRAME,
|
||||
V4L2_PIX_FMT_AV1_FRAME,
|
||||
V4L2_PIX_FMT_H264_SLICE,
|
||||
};
|
||||
#define DAEDALUS_NUM_OUTPUT_FMTS ARRAY_SIZE(daedalus_output_formats)
|
||||
#define DAEDALUS_DEFAULT_OUTPUT_FOURCC V4L2_PIX_FMT_VP9_FRAME
|
||||
#define DAEDALUS_CAPTURE_FOURCC V4L2_PIX_FMT_NV12M /* planar Y + interleaved CbCr */
|
||||
|
||||
static u32 daedalus_fourcc_to_codec_id(u32 fourcc)
|
||||
{
|
||||
switch (fourcc) {
|
||||
case V4L2_PIX_FMT_VP9_FRAME: return DAEDALUS_CODEC_VP9;
|
||||
case V4L2_PIX_FMT_AV1_FRAME: return DAEDALUS_CODEC_AV1;
|
||||
case V4L2_PIX_FMT_H264_SLICE: return DAEDALUS_CODEC_H264;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static bool daedalus_is_supported_output(u32 fourcc)
|
||||
{
|
||||
return daedalus_fourcc_to_codec_id(fourcc) != 0;
|
||||
}
|
||||
|
||||
/* Conservative defaults; userspace S_FMT overrides. */
|
||||
#define DAEDALUS_DEFAULT_W 320
|
||||
@@ -101,6 +127,61 @@ static inline struct daedalus_ctx *file_to_ctx(struct file *file)
|
||||
return container_of(file->private_data, struct daedalus_ctx, fh);
|
||||
}
|
||||
|
||||
/* -- V4L2 stateless control registration (skeleton) ----------------- */
|
||||
|
||||
/*
|
||||
* Register the per-codec stateless controls so userspace
|
||||
* (libva-v4l2-request and v4l2-compliance) recognises us as a
|
||||
* proper stateless decoder. We don't act on the values — the
|
||||
* daemon parses VP9/H.264/AV1 headers itself via FFmpeg — but
|
||||
* we accept stores so libva can drive standard decode flows.
|
||||
*
|
||||
* Per-codec control IDs that ship in v6.12 headers:
|
||||
* VP9: V4L2_CID_STATELESS_VP9_FRAME, V4L2_CID_STATELESS_VP9_COMPRESSED_HDR
|
||||
* H264: V4L2_CID_STATELESS_H264_{SPS,PPS,SCALING_MATRIX,PRED_WEIGHTS,
|
||||
* SLICE_PARAMS,DECODE_PARAMS}
|
||||
* AV1: V4L2_CID_STATELESS_AV1_FRAME (+ TILE_GROUP_ENTRY, SEQUENCE,
|
||||
* FILM_GRAIN where available)
|
||||
*/
|
||||
static const u32 daedalus_stateless_ctrls[] = {
|
||||
V4L2_CID_STATELESS_VP9_FRAME,
|
||||
V4L2_CID_STATELESS_VP9_COMPRESSED_HDR,
|
||||
V4L2_CID_STATELESS_H264_SPS,
|
||||
V4L2_CID_STATELESS_H264_PPS,
|
||||
V4L2_CID_STATELESS_H264_SCALING_MATRIX,
|
||||
V4L2_CID_STATELESS_H264_PRED_WEIGHTS,
|
||||
V4L2_CID_STATELESS_H264_SLICE_PARAMS,
|
||||
V4L2_CID_STATELESS_H264_DECODE_PARAMS,
|
||||
V4L2_CID_STATELESS_AV1_FRAME,
|
||||
V4L2_CID_STATELESS_AV1_SEQUENCE,
|
||||
V4L2_CID_STATELESS_AV1_TILE_GROUP_ENTRY,
|
||||
V4L2_CID_STATELESS_AV1_FILM_GRAIN,
|
||||
};
|
||||
|
||||
static int daedalus_register_stateless_ctrls(struct v4l2_ctrl_handler *hdl)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < ARRAY_SIZE(daedalus_stateless_ctrls); i++) {
|
||||
v4l2_ctrl_new_std_compound(hdl, NULL,
|
||||
daedalus_stateless_ctrls[i],
|
||||
v4l2_ctrl_ptr_create(NULL));
|
||||
/*
|
||||
* Errors here mean the v4l2-core doesn't know about
|
||||
* this CID on this kernel (e.g. older trees missing
|
||||
* AV1_FILM_GRAIN). hdl->error captures it; we
|
||||
* tolerate it — the codec just won't appear as
|
||||
* supported through that control.
|
||||
*/
|
||||
if (hdl->error) {
|
||||
pr_debug("daedalus_v4l2: skipping unsupported CID 0x%x (err=%d)\n",
|
||||
daedalus_stateless_ctrls[i], hdl->error);
|
||||
hdl->error = 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* -- format helpers -------------------------------------------------- */
|
||||
|
||||
/* NV12M = 2 planes: plane 0 = Y (W*H), plane 1 = interleaved CbCr (W*H/2). */
|
||||
@@ -121,16 +202,19 @@ static void daedalus_fill_capture_fmt(struct v4l2_pix_format_mplane *f,
|
||||
}
|
||||
|
||||
/*
|
||||
* OUTPUT is a parsed VP9 access unit. V4L2 convention for
|
||||
* compressed bitstream formats: single plane, sizeimage =
|
||||
* worst-case bitstream size we're willing to accept.
|
||||
* OUTPUT is a parsed access unit (VP9 frame / AV1 frame / H.264
|
||||
* slice). V4L2 convention for compressed bitstream formats:
|
||||
* single plane, sizeimage = worst-case bitstream size we're
|
||||
* willing to accept. fourcc carries the codec selector.
|
||||
*/
|
||||
static void daedalus_fill_output_fmt(struct v4l2_pix_format_mplane *f,
|
||||
u32 w, u32 h)
|
||||
u32 fourcc, u32 w, u32 h)
|
||||
{
|
||||
if (!daedalus_is_supported_output(fourcc))
|
||||
fourcc = DAEDALUS_DEFAULT_OUTPUT_FOURCC;
|
||||
f->width = w;
|
||||
f->height = h;
|
||||
f->pixelformat = DAEDALUS_OUTPUT_FOURCC;
|
||||
f->pixelformat = fourcc;
|
||||
f->field = V4L2_FIELD_NONE;
|
||||
f->colorspace = V4L2_COLORSPACE_REC709;
|
||||
f->num_planes = 1;
|
||||
@@ -158,15 +242,22 @@ static int daedalus_queue_setup(struct vb2_queue *vq,
|
||||
for (p = 0; p < *nplanes; p++)
|
||||
if (sizes[p] < fmt->plane_fmt[p].sizeimage)
|
||||
return -EINVAL;
|
||||
return 0;
|
||||
} else {
|
||||
*nplanes = fmt->num_planes;
|
||||
for (p = 0; p < *nplanes; p++)
|
||||
sizes[p] = fmt->plane_fmt[p].sizeimage;
|
||||
if (*nbuffers < 2)
|
||||
*nbuffers = 2;
|
||||
}
|
||||
|
||||
*nplanes = fmt->num_planes;
|
||||
/*
|
||||
* Both queues use vb2_dma_contig now (OUTPUT switched in
|
||||
* Phase 8.6 to satisfy v4l2-compliance's non-coherent
|
||||
* REQBUFS test). Point both at the platform device as
|
||||
* the CMA-backed allocation parent.
|
||||
*/
|
||||
for (p = 0; p < *nplanes; p++)
|
||||
sizes[p] = fmt->plane_fmt[p].sizeimage;
|
||||
|
||||
if (*nbuffers < 2)
|
||||
*nbuffers = 2;
|
||||
alloc_devs[p] = &ctx->dev->pdev->dev;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -243,26 +334,39 @@ static int daedalus_queue_init(void *priv, struct vb2_queue *src_vq,
|
||||
int ret;
|
||||
|
||||
src_vq->type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
|
||||
src_vq->io_modes = VB2_MMAP | VB2_USERPTR;
|
||||
src_vq->io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
|
||||
src_vq->drv_priv = ctx;
|
||||
src_vq->buf_struct_size = sizeof(struct v4l2_m2m_buffer);
|
||||
src_vq->ops = &daedalus_qops;
|
||||
src_vq->mem_ops = &vb2_vmalloc_memops;
|
||||
/*
|
||||
* Phase 8.6: OUTPUT switched from vb2_vmalloc to
|
||||
* vb2_dma_contig so v4l2-compliance's REQBUFS test passes
|
||||
* V4L2_MEMORY_FLAG_NON_COHERENT (vmalloc memops don't
|
||||
* honour the flag; dma_contig does). We still use
|
||||
* vb2_plane_vaddr in device_run to read the bitstream —
|
||||
* dma_contig provides a kernel virtual address just like
|
||||
* vmalloc did.
|
||||
*/
|
||||
src_vq->mem_ops = &vb2_dma_contig_memops;
|
||||
src_vq->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_COPY;
|
||||
src_vq->lock = &ctx->dev->m2m_lock;
|
||||
src_vq->dev = &ctx->dev->pdev->dev;
|
||||
src_vq->allow_cache_hints = 1;
|
||||
|
||||
ret = vb2_queue_init(src_vq);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
dst_vq->type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
||||
dst_vq->io_modes = VB2_MMAP;
|
||||
dst_vq->io_modes = VB2_MMAP | VB2_DMABUF;
|
||||
dst_vq->drv_priv = ctx;
|
||||
dst_vq->buf_struct_size = sizeof(struct v4l2_m2m_buffer);
|
||||
dst_vq->ops = &daedalus_qops;
|
||||
dst_vq->mem_ops = &vb2_vmalloc_memops;
|
||||
dst_vq->mem_ops = &vb2_dma_contig_memops;
|
||||
dst_vq->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_COPY;
|
||||
dst_vq->lock = &ctx->dev->m2m_lock;
|
||||
dst_vq->dev = &ctx->dev->pdev->dev;
|
||||
dst_vq->allow_cache_hints = 1;
|
||||
|
||||
return vb2_queue_init(dst_vq);
|
||||
}
|
||||
@@ -300,10 +404,63 @@ daedalus_inflight_pop_locked(struct daedalus_dev *dev, u32 cookie)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* -- chardev GET_DMABUF backend (called in daemon task context) ----- */
|
||||
|
||||
int daedalus_export_capture_dmabuf(u32 cookie, u32 plane, u32 flags,
|
||||
int *out_fd)
|
||||
{
|
||||
struct daedalus_dev *dev = g_daedalus_dev;
|
||||
struct daedalus_inflight *e, *match = NULL;
|
||||
struct vb2_queue *vq;
|
||||
struct vb2_buffer *vb;
|
||||
int rc;
|
||||
|
||||
if (!dev || !out_fd)
|
||||
return -EINVAL;
|
||||
|
||||
/*
|
||||
* Walk the inflight list under the lock to look up the
|
||||
* V4L2 request. Hold a transient reference via the lock
|
||||
* — once we drop the lock the entry could be popped by a
|
||||
* concurrent RESP_FRAME, but we only need the dst_buf +
|
||||
* its vb2_queue, both of which are stable for the
|
||||
* lifetime of the in-flight request (RESP_FRAME is what
|
||||
* pops the entry, so daemon completing the export then
|
||||
* sending RESP_FRAME is the canonical ordering).
|
||||
*/
|
||||
mutex_lock(&dev->inflight_lock);
|
||||
list_for_each_entry(e, &dev->inflight, list) {
|
||||
if (e->cookie == cookie) {
|
||||
match = e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!match) {
|
||||
mutex_unlock(&dev->inflight_lock);
|
||||
return -EINVAL;
|
||||
}
|
||||
vb = &match->dst_buf->vb2_buf;
|
||||
vq = vb->vb2_queue;
|
||||
mutex_unlock(&dev->inflight_lock);
|
||||
|
||||
if (plane >= vb->num_planes)
|
||||
return -EINVAL;
|
||||
|
||||
rc = vb2_core_expbuf(vq, out_fd, vq->type, vb, plane, flags);
|
||||
if (rc)
|
||||
return rc;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* -- v4l2_m2m_ops.device_run ----------------------------------------- */
|
||||
|
||||
static atomic_t daedalus_cookie_seq = ATOMIC_INIT(0);
|
||||
|
||||
u32 daedalus_next_cookie(void)
|
||||
{
|
||||
return (u32) atomic_inc_return(&daedalus_cookie_seq);
|
||||
}
|
||||
|
||||
static void daedalus_device_run(void *priv)
|
||||
{
|
||||
struct daedalus_ctx *ctx = priv;
|
||||
@@ -342,16 +499,37 @@ static void daedalus_device_run(void *priv)
|
||||
req = kmalloc(payload_len, GFP_KERNEL);
|
||||
if (!req)
|
||||
goto fail_buf_error;
|
||||
memset(req, 0, sizeof(*req));
|
||||
|
||||
req->codec_id = DAEDALUS_CODEC_VP9;
|
||||
req->codec_id = daedalus_fourcc_to_codec_id(ctx->src_fmt.pixelformat);
|
||||
if (!req->codec_id) {
|
||||
v4l2_err(&dev->v4l2_dev,
|
||||
"device_run: unsupported OUTPUT pixelformat 0x%08x\n",
|
||||
ctx->src_fmt.pixelformat);
|
||||
kfree(req);
|
||||
req = NULL;
|
||||
goto fail_buf_error;
|
||||
}
|
||||
req->bitstream_len = (u32) blen;
|
||||
req->flags = 0;
|
||||
req->capture_width = ctx->dst_fmt.width;
|
||||
req->capture_height = ctx->dst_fmt.height;
|
||||
req->capture_pix_fmt = ctx->dst_fmt.pixelformat;
|
||||
req->capture_num_planes = ctx->dst_fmt.num_planes;
|
||||
{
|
||||
unsigned int p;
|
||||
for (p = 0; p < ctx->dst_fmt.num_planes && p < 3; p++) {
|
||||
req->capture_plane_size[p] =
|
||||
ctx->dst_fmt.plane_fmt[p].sizeimage;
|
||||
req->capture_plane_stride[p] =
|
||||
ctx->dst_fmt.plane_fmt[p].bytesperline;
|
||||
}
|
||||
}
|
||||
memcpy((u8 *) req + sizeof(*req), bitstream, blen);
|
||||
|
||||
inf = kzalloc(sizeof(*inf), GFP_KERNEL);
|
||||
if (!inf)
|
||||
goto fail_buf_error;
|
||||
cookie = (u32) atomic_inc_return(&daedalus_cookie_seq);
|
||||
cookie = daedalus_next_cookie();
|
||||
inf->cookie = cookie;
|
||||
inf->ctx = ctx;
|
||||
inf->src_buf = src_buf;
|
||||
@@ -430,35 +608,46 @@ void daedalus_complete_resp_frame(u32 cookie,
|
||||
? VB2_BUF_STATE_DONE : VB2_BUF_STATE_ERROR;
|
||||
|
||||
/*
|
||||
* Copy inline pixel data into the CAPTURE buffer if the
|
||||
* daemon supplied any. Phase 8.5: bytes-after-header in the
|
||||
* RESP_FRAME payload carry the Y plane followed by the
|
||||
* interleaved CbCr plane (NV12M layout), truncated to fit
|
||||
* the 64 KiB chardev payload cap. Phase 8.6 swaps this for
|
||||
* dmabuf-export so big frames don't get truncated.
|
||||
* Two routes the daemon can take, both supported:
|
||||
*
|
||||
* (a) Phase 8.6 dmabuf path — daemon called
|
||||
* DAEDALUS_IOC_GET_DMABUF, mmap'd the CAPTURE buffer,
|
||||
* wrote pixels in place. RESP_FRAME carries metadata
|
||||
* only (pixels_len == 0). We just set the payload
|
||||
* per plane from the daemon's reported sizes.
|
||||
*
|
||||
* (b) Phase 8.5 inline path — daemon shipped raw NV12 in
|
||||
* the chardev payload (≤ 64 KiB cap). We memcpy
|
||||
* into the vb2 buffer's vmalloc-backed plane. Still
|
||||
* supported for small frames where the daemon hasn't
|
||||
* picked up the GET_DMABUF path.
|
||||
*/
|
||||
if (state == VB2_BUF_STATE_DONE && pixels_len) {
|
||||
if (state == VB2_BUF_STATE_DONE) {
|
||||
struct vb2_buffer *vb = &inf->dst_buf->vb2_buf;
|
||||
|
||||
dst_y = vb2_plane_vaddr(vb, 0);
|
||||
dst_uv = vb2_plane_vaddr(vb, 1);
|
||||
y_size = min_t(u32, fr->luma_len,
|
||||
(u32) vb2_plane_size(vb, 0));
|
||||
uv_size = min_t(u32, fr->chroma_len,
|
||||
(u32) vb2_plane_size(vb, 1));
|
||||
|
||||
if (dst_y && y_size && pixels_len >= y_size) {
|
||||
memcpy(dst_y, pixels, y_size);
|
||||
vb2_set_plane_payload(vb, 0, y_size);
|
||||
} else {
|
||||
vb2_set_plane_payload(vb, 0, 0);
|
||||
if (pixels_len) {
|
||||
/* (b) inline copy */
|
||||
dst_y = vb2_plane_vaddr(vb, 0);
|
||||
dst_uv = vb2_plane_vaddr(vb, 1);
|
||||
if (dst_y && y_size && pixels_len >= y_size)
|
||||
memcpy(dst_y, pixels, y_size);
|
||||
else
|
||||
y_size = 0;
|
||||
if (dst_uv && uv_size &&
|
||||
pixels_len >= y_size + uv_size)
|
||||
memcpy(dst_uv, pixels + y_size, uv_size);
|
||||
else
|
||||
uv_size = 0;
|
||||
}
|
||||
if (dst_uv && uv_size && pixels_len >= y_size + uv_size) {
|
||||
memcpy(dst_uv, pixels + y_size, uv_size);
|
||||
/* (a) dmabuf path: pixels already there; just set payload */
|
||||
vb2_set_plane_payload(vb, 0, y_size);
|
||||
if (vb->num_planes > 1)
|
||||
vb2_set_plane_payload(vb, 1, uv_size);
|
||||
} else {
|
||||
vb2_set_plane_payload(vb, 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -491,13 +680,16 @@ static int daedalus_querycap(struct file *file, void *priv,
|
||||
static int daedalus_enum_fmt(struct file *file, void *priv,
|
||||
struct v4l2_fmtdesc *f)
|
||||
{
|
||||
if (f->type == V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE) {
|
||||
if (f->index >= DAEDALUS_NUM_OUTPUT_FMTS)
|
||||
return -EINVAL;
|
||||
f->pixelformat = daedalus_output_formats[f->index];
|
||||
f->flags |= V4L2_FMT_FLAG_COMPRESSED;
|
||||
return 0;
|
||||
}
|
||||
if (f->index != 0)
|
||||
return -EINVAL;
|
||||
f->pixelformat = (f->type == V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE)
|
||||
? DAEDALUS_OUTPUT_FOURCC
|
||||
: DAEDALUS_CAPTURE_FOURCC;
|
||||
if (f->type == V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE)
|
||||
f->flags |= V4L2_FMT_FLAG_COMPRESSED;
|
||||
f->pixelformat = DAEDALUS_CAPTURE_FOURCC;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -521,14 +713,35 @@ static int daedalus_try_fmt(struct file *file, void *priv,
|
||||
struct v4l2_pix_format_mplane *p = &f->fmt.pix_mp;
|
||||
u32 w = clamp_t(u32, p->width, 16, 1920);
|
||||
u32 h = clamp_t(u32, p->height, 16, 1088);
|
||||
u32 cs, xfer, ycbcr, quant;
|
||||
|
||||
/*
|
||||
* Preserve userspace-supplied colorspace fields verbatim
|
||||
* (fixes the Phase 8.5 v4l2-compliance S_FMT colorspace
|
||||
* round-trip failure) — fill_*_fmt overwrites these to
|
||||
* REC709 defaults, but TRY_FMT must echo what the caller
|
||||
* asked for if it's at all sensible.
|
||||
*/
|
||||
cs = p->colorspace;
|
||||
xfer = p->xfer_func;
|
||||
ycbcr = p->ycbcr_enc;
|
||||
quant = p->quantization;
|
||||
|
||||
if (f->type == V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE) {
|
||||
daedalus_fill_output_fmt(p, w, h);
|
||||
u32 fourcc = p->pixelformat;
|
||||
if (!daedalus_is_supported_output(fourcc))
|
||||
fourcc = DAEDALUS_DEFAULT_OUTPUT_FOURCC;
|
||||
daedalus_fill_output_fmt(p, fourcc, w, h);
|
||||
} else if (f->type == V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE) {
|
||||
daedalus_fill_capture_fmt(p, w, h);
|
||||
} else {
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (cs) p->colorspace = cs;
|
||||
if (xfer) p->xfer_func = xfer;
|
||||
if (ycbcr) p->ycbcr_enc = ycbcr;
|
||||
if (quant) p->quantization = quant;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -548,10 +761,23 @@ static int daedalus_s_fmt(struct file *file, void *priv,
|
||||
ret = daedalus_try_fmt(file, priv, f);
|
||||
if (ret)
|
||||
return ret;
|
||||
if (f->type == V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE)
|
||||
if (f->type == V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE) {
|
||||
ctx->src_fmt = f->fmt.pix_mp;
|
||||
else
|
||||
/*
|
||||
* Stateless decoder convention: colorspace metadata
|
||||
* on the OUTPUT (bitstream) side describes what the
|
||||
* codec will produce. Propagate to CAPTURE so a
|
||||
* follow-up G_FMT on CAPTURE returns matching values
|
||||
* (v4l2-compliance v4l2-test-formats.cpp:958
|
||||
* round-trip test).
|
||||
*/
|
||||
ctx->dst_fmt.colorspace = ctx->src_fmt.colorspace;
|
||||
ctx->dst_fmt.xfer_func = ctx->src_fmt.xfer_func;
|
||||
ctx->dst_fmt.ycbcr_enc = ctx->src_fmt.ycbcr_enc;
|
||||
ctx->dst_fmt.quantization = ctx->src_fmt.quantization;
|
||||
} else {
|
||||
ctx->dst_fmt = f->fmt.pix_mp;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -598,10 +824,12 @@ static int daedalus_open(struct file *file)
|
||||
v4l2_fh_init(&ctx->fh, &dev->vdev);
|
||||
file->private_data = &ctx->fh;
|
||||
|
||||
v4l2_ctrl_handler_init(&ctx->hdl, 0);
|
||||
v4l2_ctrl_handler_init(&ctx->hdl, ARRAY_SIZE(daedalus_stateless_ctrls));
|
||||
daedalus_register_stateless_ctrls(&ctx->hdl);
|
||||
ctx->fh.ctrl_handler = &ctx->hdl;
|
||||
|
||||
daedalus_fill_output_fmt(&ctx->src_fmt,
|
||||
DAEDALUS_DEFAULT_OUTPUT_FOURCC,
|
||||
DAEDALUS_DEFAULT_W, DAEDALUS_DEFAULT_H);
|
||||
daedalus_fill_capture_fmt(&ctx->dst_fmt,
|
||||
DAEDALUS_DEFAULT_W, DAEDALUS_DEFAULT_H);
|
||||
@@ -666,6 +894,20 @@ static int daedalus_probe(struct platform_device *pdev)
|
||||
mutex_init(&dev->inflight_lock);
|
||||
INIT_LIST_HEAD(&dev->inflight);
|
||||
|
||||
/*
|
||||
* vb2_dma_contig (used by the CAPTURE queue for dmabuf
|
||||
* export) needs the parent device's DMA mask configured.
|
||||
* Pi 5 CMA supports 32-bit DMA; that's sufficient for
|
||||
* NV12/NV12M up to 4K (4K NV12 = ~12.4 MiB, well under
|
||||
* the 4 GiB ceiling).
|
||||
*/
|
||||
ret = dma_coerce_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
|
||||
if (ret) {
|
||||
dev_err(&pdev->dev,
|
||||
"dma_coerce_mask_and_coherent: %d\n", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = v4l2_device_register(&pdev->dev, &dev->v4l2_dev);
|
||||
if (ret) {
|
||||
dev_err(&pdev->dev, "v4l2_device_register: %d\n", ret);
|
||||
|
||||
@@ -49,6 +49,18 @@ struct daedalus_dev {
|
||||
/* Module-wide singleton accessor (chardev needs this for RESP_FRAME). */
|
||||
struct daedalus_dev *daedalus_get_dev(void);
|
||||
|
||||
/**
|
||||
* daedalus_next_cookie() - shared cookie allocator
|
||||
*
|
||||
* Returns the next monotonically increasing request cookie.
|
||||
* Used by both the V4L2 m2m device_run path (for REQ_DECODE
|
||||
* from real OUTPUT buffers) and the chardev debugfs
|
||||
* test_decode path (for hand-crafted REQ_DECODE injection),
|
||||
* so the two namespaces never collide and RESP_FRAME logs
|
||||
* stay deterministic.
|
||||
*/
|
||||
u32 daedalus_next_cookie(void);
|
||||
|
||||
/**
|
||||
* daedalus_complete_resp_frame() - chardev RESP_FRAME completion
|
||||
* @cookie: cookie carried by the matching REQ_DECODE
|
||||
@@ -59,12 +71,34 @@ struct daedalus_dev *daedalus_get_dev(void);
|
||||
*
|
||||
* Called from the chardev write() path on RESP_FRAME. Looks up
|
||||
* the in-flight request, copies inline pixel data into the
|
||||
* CAPTURE vb2 buffer if available, then completes both src+dst
|
||||
* buffers and finishes the m2m job. Silently drops responses
|
||||
* for unknown cookies (pr_warn_ratelimited).
|
||||
* CAPTURE vb2 buffer if available (Phase 8.5 path; Phase 8.6
|
||||
* skips the copy because the daemon decoded directly into the
|
||||
* dmabuf), then completes both src+dst buffers and finishes
|
||||
* the m2m job. Silently drops responses for unknown cookies
|
||||
* (pr_warn_ratelimited).
|
||||
*/
|
||||
void daedalus_complete_resp_frame(u32 cookie,
|
||||
const struct daedalus_resp_frame *fr,
|
||||
const u8 *pixels, size_t pixels_len);
|
||||
|
||||
/**
|
||||
* daedalus_export_capture_dmabuf() - chardev GET_DMABUF backend
|
||||
* @cookie: cookie from the matching REQ_DECODE
|
||||
* @plane: plane index (0-based) within the CAPTURE buffer
|
||||
* @flags: flags for dma_buf_fd (O_CLOEXEC etc.)
|
||||
* @out_fd: out: installed dmabuf fd in the calling task's
|
||||
* fd table (only valid when return value == 0)
|
||||
*
|
||||
* Called from the chardev DAEDALUS_IOC_GET_DMABUF ioctl
|
||||
* handler. Looks up the in-flight V4L2 request by cookie,
|
||||
* exports the CAPTURE vb2 buffer's plane as a dma_buf via
|
||||
* vb2_core_expbuf in the daemon's task context. Caller must
|
||||
* NOT touch out_fd on non-zero return.
|
||||
*
|
||||
* Return: 0 on success, -EINVAL for unknown cookie or bad
|
||||
* plane, propagated -errno from vb2_core_expbuf otherwise.
|
||||
*/
|
||||
int daedalus_export_capture_dmabuf(u32 cookie, u32 plane, u32 flags,
|
||||
int *out_fd);
|
||||
|
||||
#endif /* DAEDALUS_V4L2_MAIN_H */
|
||||
|
||||
+24
-4
@@ -17,8 +17,9 @@
|
||||
* wiring works end-to-end against a real bitstream.
|
||||
*
|
||||
* Usage:
|
||||
* test_m2m_decode <vp9_keyframe.bin> <out.nv12> [w] [h]
|
||||
* defaults: w=128 h=96
|
||||
* test_m2m_decode <keyframe.bin> <out.nv12> [w] [h] [codec]
|
||||
* defaults: w=128 h=96 codec=vp9
|
||||
* codec: vp9 | av1 | h264
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
@@ -68,6 +69,8 @@ static void *read_file(const char *path, size_t *out_len)
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *bitstream_path, *out_path;
|
||||
const char *codec_name = "vp9";
|
||||
uint32_t output_fourcc = V4L2_PIX_FMT_VP9_FRAME;
|
||||
void *bitstream;
|
||||
size_t bs_len;
|
||||
uint32_t w = 128, h = 96;
|
||||
@@ -87,7 +90,8 @@ int main(int argc, char **argv)
|
||||
|
||||
if (argc < 3) {
|
||||
fprintf(stderr,
|
||||
"usage: %s <vp9_keyframe.bin> <out.nv12> [w] [h]\n",
|
||||
"usage: %s <keyframe.bin> <out.nv12> [w] [h] [codec]\n"
|
||||
" codec: vp9 | av1 | h264 (default vp9)\n",
|
||||
argv[0]);
|
||||
return 2;
|
||||
}
|
||||
@@ -97,6 +101,22 @@ int main(int argc, char **argv)
|
||||
w = (uint32_t) atoi(argv[3]);
|
||||
h = (uint32_t) atoi(argv[4]);
|
||||
}
|
||||
if (argc >= 6) {
|
||||
codec_name = argv[5];
|
||||
if (!strcmp(codec_name, "vp9"))
|
||||
output_fourcc = V4L2_PIX_FMT_VP9_FRAME;
|
||||
else if (!strcmp(codec_name, "av1"))
|
||||
output_fourcc = V4L2_PIX_FMT_AV1_FRAME;
|
||||
else if (!strcmp(codec_name, "h264"))
|
||||
output_fourcc = V4L2_PIX_FMT_H264_SLICE;
|
||||
else {
|
||||
fprintf(stderr, "unknown codec '%s'\n", codec_name);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
printf("codec=%s fourcc='%c%c%c%c'\n", codec_name,
|
||||
output_fourcc & 0xff, (output_fourcc >> 8) & 0xff,
|
||||
(output_fourcc >> 16) & 0xff, (output_fourcc >> 24) & 0xff);
|
||||
|
||||
bitstream = read_file(bitstream_path, &bs_len);
|
||||
printf("loaded bitstream: %zu bytes\n", bs_len);
|
||||
@@ -110,7 +130,7 @@ int main(int argc, char **argv)
|
||||
fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE;
|
||||
fmt.fmt.pix_mp.width = w;
|
||||
fmt.fmt.pix_mp.height = h;
|
||||
fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_VP9_FRAME;
|
||||
fmt.fmt.pix_mp.pixelformat = output_fourcc;
|
||||
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0)
|
||||
die("S_FMT OUTPUT");
|
||||
printf("OUTPUT sizeimage = %u\n", fmt.fmt.pix_mp.plane_fmt[0].sizeimage);
|
||||
|
||||
Reference in New Issue
Block a user