Files
libva-v4l2-request-fourier/tests/cap_pool_probe_pattern.c
T
claude-noether 988b848908 iter7: A+B+C — slot-leak fix, cap_pool harness, msync verify harness
Closes three internal carry items in one fork commit. iter6 deferred
these as TODOs; iter7 lands the implementations + supporting tests.

# Track B — slot-leak error recovery (src/)

iter6 documented the RequestSyncSurface error paths as a "bounded
leak we accept" — slots stayed busy=true after REINIT/DQBUF failures
until RequestTerminate ran. With pool=16 and rare errors this was
acceptable, but a sustained-error scenario could starve the pool.

Adds request_pool_force_release(pool, index) which:
1. Tries media_request_reinit on the slot's fd (cheap path)
2. Falls back to close + media_request_alloc (recovery)
3. Leaves the slot dead-busy if even alloc fails (other slots
   unaffected, pool capacity reduced by 1 until destroy)

Wires it into surface.c RequestSyncSurface error paths only for
errors before the OUTPUT-DQBUF attempt. After OUTPUT-DQBUF failure
the V4L2 buffer is in indeterminate kernel state, so a separate
error label (`error_buffer_indeterminate`) leaves the slot
dead-busy — reusing the slot would QBUF on a kernel-still-held
buffer and EINVAL.

Phase 5 sonnet review caught this discriminator subtlety pre-commit.

Files: request_pool.{h,c}, surface.c.

# Track C — cap_pool race synthetic harness (tests/)

iter5 sonnet C4 / iter6 candidate A: cap_pool resolution-change
race was organically exercised by YT's quality renegotiations
(iter6 close, 4 cap_pool_init events clean) but had no
deterministic regression test.

tests/cap_pool_probe_pattern.c — ~170-line C program: opens
libva display, vaCreateConfig, vaCreateSurfaces(small) +
vaCreateContext (triggers OUTPUT pool init at small resolution),
dispose, vaCreateSurfaces(big) + vaCreateContext (forces S_FMT
on the new resolution against an in-use OUTPUT pool — the actual
race-hitting path).

Phase 5 sonnet flagged that without vaCreateContext the test
would pass trivially (OUTPUT pool never init'd, REQBUFS(0) on
empty queue is a no-op). Fixed before commit.

tests/run_cap_pool_probe.sh — runner; greps driver stderr for
REQBUFS / EBUSY / "Unable to set format" race indicators.

# Track A — msync pixel-correctness verify harness (tests/)

iter5 sweep removed msync(MS_SYNC|MS_INVALIDATE) from CAPTURE
DQBUF path. iter5 sonnet C3 flagged: no formal pixel verification.

tests/run_msync_pixel_verify.sh — runs FFmpeg SW decode (libavcodec
reference) and FFmpeg HW decode (via our v4l2_request driver),
compares NV12 byte streams. Probes fixture dimensions via ffprobe
and uses crop=$W:$H after hwdownload to normalize MB-padding
artifacts (hantro pads height to 16-line align; SW returns
crop-aligned).

Phase 5 sonnet flagged the stride-mismatch false-failure risk
pre-commit. Fixed: explicit crop + diagnostic that distinguishes
genuine pixel divergence from MB-padding stride artifacts.

# Phase 5 sonnet code review

Verdict: APPROVE-WITH-CHANGES. Three actionable findings, all
addressed before this commit:
1. surface.c error path: separated OUTPUT-DQBUF-failure into
   error_buffer_indeterminate label, slot stays dead-busy
2. cap_pool_probe_pattern.c: added vaCreateContext to actually
   exercise the OUTPUT pool init at the small resolution
3. run_msync_pixel_verify.sh: explicit crop on HW path,
   stride-mismatch diagnostic distinguished from corruption

Empirical verification (Phase 6+7 deploy + run): pending operator
ohm-tools availability.

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

175 lines
6.1 KiB
C

/*
* cap_pool_probe_pattern.c — synthetic regression test for the
* iter5 sonnet C4 / iter6 candidate A "cap_pool resolution-change race."
*
* Exercises the surface-allocation pattern that originally tripped
* REQBUFS-EBUSY on the iter5-end driver: vaCreateSurfaces at one
* resolution, then vaDestroySurfaces, then vaCreateSurfaces at a
* different resolution. iter6's REINIT discipline + cap_pool's
* REQBUFS(0)-on-CAPTURE-and-OUTPUT during S_FMT-on-resolution-change
* (CreateSurfaces2 in surface.c) closes this race; this test anchors
* that fact with a deterministic repro.
*
* Build:
* gcc -O2 -Wall -Wextra -o cap_pool_probe_pattern \
* cap_pool_probe_pattern.c \
* $(pkg-config --cflags --libs libva libva-drm)
*
* Run:
* LIBVA_DRIVER_NAME=v4l2_request \
* LIBVA_V4L2_REQUEST_VIDEO_PATH=/dev/video1 \
* LIBVA_V4L2_REQUEST_MEDIA_PATH=/dev/media0 \
* ./cap_pool_probe_pattern
*
* Pass criterion (on iter6 driver and later):
* - Exit code 0
* - No "REQBUFS" / "EBUSY" / "Unable to request buffers" /
* "Unable to set format" lines on the v4l2-request driver's stderr
* - vainfo or visual inspection confirms the test program reached
* the "PASS" line on stdout
*
* Fail behavior pre-iter5: vaCreateSurfaces at the second resolution
* would emit REQBUFS-EBUSY because OUTPUT/CAPTURE buffers from the
* first allocation hadn't been torn down before S_FMT was attempted
* on the new resolution. iter5's CreateSurfaces2 added the dual
* REQBUFS(0) drain; iter6's REINIT keeps the OUTPUT pool's request_fd
* lifecycle clean across the destroy-recreate cycle.
*/
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <va/va.h>
#include <va/va_drm.h>
#define DRM_RENDER_NODE "/dev/dri/renderD128"
static const char *va_status_str(VAStatus s)
{
return vaErrorStr(s);
}
#define VA_OK_OR_FAIL(call, msg) do { \
VAStatus _vs = (call); \
if (_vs != VA_STATUS_SUCCESS) { \
fprintf(stderr, "FAIL: %s: %s (0x%x)\n", \
(msg), va_status_str(_vs), _vs); \
return 10; \
} \
} while (0)
int main(void)
{
int drm_fd;
VADisplay dpy;
int va_major = 0, va_minor = 0;
VAConfigID config = VA_INVALID_ID;
VAContextID context = VA_INVALID_ID;
VASurfaceID small_surfaces[4];
VASurfaceID big_surfaces[4];
const unsigned int small_w = 128, small_h = 128;
const unsigned int big_w = 1920, big_h = 1080;
/* Open render node + libva display. */
drm_fd = open(DRM_RENDER_NODE, O_RDWR);
if (drm_fd < 0) {
fprintf(stderr, "FAIL: open(%s): %s\n",
DRM_RENDER_NODE, strerror(errno));
return 1;
}
dpy = vaGetDisplayDRM(drm_fd);
if (dpy == NULL) {
fprintf(stderr, "FAIL: vaGetDisplayDRM returned NULL\n");
close(drm_fd);
return 2;
}
VA_OK_OR_FAIL(vaInitialize(dpy, &va_major, &va_minor),
"vaInitialize");
printf("libva %d.%d initialized via %s\n", va_major, va_minor,
DRM_RENDER_NODE);
/*
* vaCreateConfig with H.264 Main + VLD entrypoint forces our
* driver's RequestCreateConfig to set up the H.264 decode path,
* which is the path that reaches CreateSurfaces2 (and the
* resolution-change handling there).
*/
VA_OK_OR_FAIL(vaCreateConfig(dpy, VAProfileH264Main, VAEntrypointVLD,
NULL, 0, &config),
"vaCreateConfig(H264Main, VLD)");
/* Phase 1: allocate small probe-pattern surfaces + context.
*
* vaCreateContext on our driver triggers RequestCreateContext, which
* runs the OUTPUT pool's request_pool_init (allocates 16 OUTPUT
* V4L2 buffers via VIDIOC_CREATE_BUFS at the small resolution) and
* the device-init S_EXT_CTRLS (DECODE_MODE / START_CODE). Without
* the context, vaCreateSurfaces alone would not exercise the path
* that the iter5 C4 race fired on (REQBUFS-EBUSY when the pool
* already has buffers at the previous resolution).
*/
printf("Phase 1: vaCreateSurfaces %ux%u, count=4; vaCreateContext\n",
small_w, small_h);
VA_OK_OR_FAIL(vaCreateSurfaces(dpy, VA_RT_FORMAT_YUV420,
small_w, small_h, small_surfaces, 4,
NULL, 0),
"vaCreateSurfaces (small)");
VA_OK_OR_FAIL(vaCreateContext(dpy, config,
(int)small_w, (int)small_h, 0,
small_surfaces, 4, &context),
"vaCreateContext (small)");
/* Phase 2: dispose context + small surfaces. The driver-wide OUTPUT
* pool stays initialized (RequestDestroyContext does NOT call
* request_pool_destroy — only RequestTerminate does), so the
* REQBUFS(0) drain on the next CreateSurfaces2 is the actual
* race-hitting path.
*/
printf("Phase 2: vaDestroyContext; vaDestroySurfaces (small)\n");
VA_OK_OR_FAIL(vaDestroyContext(dpy, context),
"vaDestroyContext (small)");
context = VA_INVALID_ID;
VA_OK_OR_FAIL(vaDestroySurfaces(dpy, small_surfaces, 4),
"vaDestroySurfaces (small)");
/* Phase 3: allocate at the new (much larger) resolution. This is
* where pre-iter5 hit REQBUFS-EBUSY because OUTPUT/CAPTURE buffers
* from the small allocation hadn't been torn down before S_FMT on
* the new size. iter5's CreateSurfaces2 added the dual REQBUFS(0)
* drain; iter6's REINIT keeps the OUTPUT pool's request_fd
* lifecycle clean across the destroy-recreate cycle.
*/
printf("Phase 3: vaCreateSurfaces %ux%u, count=4 (resolution change); vaCreateContext\n",
big_w, big_h);
VA_OK_OR_FAIL(vaCreateSurfaces(dpy, VA_RT_FORMAT_YUV420,
big_w, big_h, big_surfaces, 4,
NULL, 0),
"vaCreateSurfaces (big)");
VA_OK_OR_FAIL(vaCreateContext(dpy, config,
(int)big_w, (int)big_h, 0,
big_surfaces, 4, &context),
"vaCreateContext (big)");
/* Phase 4: clean up. */
printf("Phase 4: cleanup\n");
VA_OK_OR_FAIL(vaDestroyContext(dpy, context),
"vaDestroyContext (big)");
VA_OK_OR_FAIL(vaDestroySurfaces(dpy, big_surfaces, 4),
"vaDestroySurfaces (big)");
VA_OK_OR_FAIL(vaDestroyConfig(dpy, config),
"vaDestroyConfig");
VA_OK_OR_FAIL(vaTerminate(dpy),
"vaTerminate");
close(drm_fd);
printf("PASS: cap_pool probe-pattern resolution-change handled cleanly.\n");
printf("Inspect driver stderr for absence of REQBUFS/EBUSY/Unable lines.\n");
return 0;
}