/* SPDX-License-Identifier: BSD-2-Clause */ /* * test_m2m_decode — minimal V4L2 m2m stateless decoder client. * * Drives /dev/videoNN through one full QBUF/DQBUF round-trip: * 1. open the m2m device * 2. S_FMT on OUTPUT (VP9_FRAME) and CAPTURE (NV12M) * 3. REQBUFS 1 on both queues * 4. mmap the OUTPUT buffer, copy bitstream into it, QBUF * 5. QBUF the CAPTURE buffer * 6. STREAMON both queues * 7. poll for completion * 8. DQBUF capture * 9. mmap+dump the CAPTURE buffer to a file * * Phase 8.5 verification harness — confirms the kernel's m2m * wiring works end-to-end against a real bitstream. * * Usage: * test_m2m_decode [w] [h] [codec] * defaults: w=128 h=96 codec=vp9 * codec: vp9 | av1 | h264 */ #include #include #include #include #include #include #include #include #include #include #include #include #define V4L2_DEV "/dev/video0" #define POLL_TIMEOUT_MS 5000 static void die(const char *msg) { perror(msg); exit(1); } static void *read_file(const char *path, size_t *out_len) { struct stat st; void *buf; int fd; ssize_t n; fd = open(path, O_RDONLY); if (fd < 0) die("open bitstream"); if (fstat(fd, &st) < 0) die("fstat"); buf = malloc(st.st_size); if (!buf) die("malloc bitstream"); n = read(fd, buf, st.st_size); if (n != st.st_size) die("read bitstream short"); close(fd); *out_len = (size_t) st.st_size; return buf; } 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; struct v4l2_format fmt; struct v4l2_requestbuffers reqbuf; struct v4l2_buffer buf; struct v4l2_plane planes[2]; struct v4l2_exportbuffer expbuf; int fd, rc, i; void *out_map; void *cap_y, *cap_uv; size_t cap_y_size, cap_uv_size; uint32_t out_buf_offset; enum v4l2_buf_type t; if (argc < 3) { fprintf(stderr, "usage: %s [w] [h] [codec]\n" " codec: vp9 | av1 | h264 (default vp9)\n", argv[0]); return 2; } bitstream_path = argv[1]; out_path = argv[2]; if (argc >= 5) { 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); fd = open(V4L2_DEV, O_RDWR); if (fd < 0) die("open " V4L2_DEV); /* --- S_FMT OUTPUT (compressed) --- */ memset(&fmt, 0, sizeof(fmt)); 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 = 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); /* --- S_FMT CAPTURE (NV12M) --- */ memset(&fmt, 0, sizeof(fmt)); fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; fmt.fmt.pix_mp.width = w; fmt.fmt.pix_mp.height = h; fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_NV12M; if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) die("S_FMT CAPTURE"); printf("CAPTURE planes = %u, [0].sizeimage=%u [1].sizeimage=%u\n", fmt.fmt.pix_mp.num_planes, fmt.fmt.pix_mp.plane_fmt[0].sizeimage, fmt.fmt.pix_mp.plane_fmt[1].sizeimage); cap_y_size = fmt.fmt.pix_mp.plane_fmt[0].sizeimage; cap_uv_size = fmt.fmt.pix_mp.plane_fmt[1].sizeimage; /* --- REQBUFS OUTPUT --- */ memset(&reqbuf, 0, sizeof(reqbuf)); reqbuf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; reqbuf.memory = V4L2_MEMORY_MMAP; reqbuf.count = 1; if (ioctl(fd, VIDIOC_REQBUFS, &reqbuf) < 0) die("REQBUFS OUTPUT"); printf("OUTPUT REQBUFS -> %u\n", reqbuf.count); /* --- REQBUFS CAPTURE --- */ memset(&reqbuf, 0, sizeof(reqbuf)); reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; reqbuf.memory = V4L2_MEMORY_MMAP; reqbuf.count = 1; if (ioctl(fd, VIDIOC_REQBUFS, &reqbuf) < 0) die("REQBUFS CAPTURE"); printf("CAPTURE REQBUFS -> %u\n", reqbuf.count); /* --- QUERYBUF OUTPUT[0] + mmap + fill --- */ memset(&buf, 0, sizeof(buf)); memset(planes, 0, sizeof(planes)); buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; buf.memory = V4L2_MEMORY_MMAP; buf.index = 0; buf.m.planes = planes; buf.length = 1; if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) die("QUERYBUF OUTPUT"); out_buf_offset = planes[0].m.mem_offset; out_map = mmap(NULL, planes[0].length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, out_buf_offset); if (out_map == MAP_FAILED) die("mmap OUTPUT"); if (bs_len > planes[0].length) { fprintf(stderr, "bitstream too big: %zu > %u\n", bs_len, planes[0].length); return 1; } memcpy(out_map, bitstream, bs_len); planes[0].bytesused = (uint32_t) bs_len; if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) die("QBUF OUTPUT"); printf("QBUF OUTPUT[0] bytesused=%zu\n", bs_len); /* --- QBUF CAPTURE[0] --- */ memset(&buf, 0, sizeof(buf)); memset(planes, 0, sizeof(planes)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; buf.memory = V4L2_MEMORY_MMAP; buf.index = 0; buf.m.planes = planes; buf.length = 2; if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) die("QBUF CAPTURE"); printf("QBUF CAPTURE[0]\n"); /* --- STREAMON both --- */ t = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; if (ioctl(fd, VIDIOC_STREAMON, &t) < 0) die("STREAMON OUTPUT"); t = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; if (ioctl(fd, VIDIOC_STREAMON, &t) < 0) die("STREAMON CAPTURE"); printf("STREAMON both\n"); /* --- poll for CAPTURE completion --- */ { struct pollfd p = { .fd = fd, .events = POLLIN | POLLOUT }; rc = poll(&p, 1, POLL_TIMEOUT_MS); if (rc < 0) die("poll"); if (rc == 0) { fprintf(stderr, "poll timeout\n"); return 1; } printf("poll revents=0x%x\n", p.revents); } /* --- DQBUF OUTPUT (return the bitstream buffer) --- */ memset(&buf, 0, sizeof(buf)); memset(planes, 0, sizeof(planes)); buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; buf.memory = V4L2_MEMORY_MMAP; buf.m.planes = planes; buf.length = 1; if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) die("DQBUF OUTPUT"); printf("DQBUF OUTPUT[%u] flags=0x%x\n", buf.index, buf.flags); /* --- DQBUF CAPTURE (get the decoded frame) --- */ memset(&buf, 0, sizeof(buf)); memset(planes, 0, sizeof(planes)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; buf.memory = V4L2_MEMORY_MMAP; buf.m.planes = planes; buf.length = 2; if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) die("DQBUF CAPTURE"); printf("DQBUF CAPTURE[%u] flags=0x%x payloads=[%u, %u]\n", buf.index, buf.flags, planes[0].bytesused, planes[1].bytesused); if (buf.flags & V4L2_BUF_FLAG_ERROR) { fprintf(stderr, "CAPTURE buffer flagged ERROR\n"); return 1; } /* --- mmap CAPTURE plane 0 + 1 and dump --- */ { struct v4l2_buffer qb; struct v4l2_plane pl[2]; memset(&qb, 0, sizeof(qb)); memset(pl, 0, sizeof(pl)); qb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; qb.memory = V4L2_MEMORY_MMAP; qb.index = 0; qb.m.planes = pl; qb.length = 2; if (ioctl(fd, VIDIOC_QUERYBUF, &qb) < 0) die("QUERYBUF CAPTURE"); cap_y = mmap(NULL, pl[0].length, PROT_READ, MAP_SHARED, fd, pl[0].m.mem_offset); if (cap_y == MAP_FAILED) die("mmap cap Y"); cap_uv = mmap(NULL, pl[1].length, PROT_READ, MAP_SHARED, fd, pl[1].m.mem_offset); if (cap_uv == MAP_FAILED) die("mmap cap UV"); } { FILE *of = fopen(out_path, "wb"); size_t y_actual = planes[0].bytesused ? planes[0].bytesused : cap_y_size; size_t uv_actual = planes[1].bytesused ? planes[1].bytesused : cap_uv_size; if (!of) die("fopen out"); fwrite(cap_y, 1, y_actual, of); fwrite(cap_uv, 1, uv_actual, of); fclose(of); printf("wrote %zu Y + %zu UV bytes to %s\n", y_actual, uv_actual, out_path); } /* --- STREAMOFF, cleanup --- */ t = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; ioctl(fd, VIDIOC_STREAMOFF, &t); t = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; ioctl(fd, VIDIOC_STREAMOFF, &t); close(fd); free(bitstream); printf("OK\n"); (void) expbuf; (void) i; return 0; }