# Iteration 4 — Phase 3 (baseline capture)

VP9 baseline anchor for iter4, plus 4-codec regression block under new substrate `linux-fresnel-fourier 7.0-1`. Captured 2026-05-09 22:05–22:13 CEST.

## Substrate fingerprint

```
Linux fresnel 7.0.0-fresnel-fourier #1 SMP PREEMPT Sat May 9 19:24:42 CEST 2026 aarch64
Driver: rkvdec @ /dev/video1 + /dev/media0 (advertises HEVC/H.264/VP9F)
Driver: hantro-vpu @ /dev/video2 (enc) + /dev/video3 (dec) + /dev/media1 (advertises MPEG-2/VP8)
Backend: /usr/lib/dri/v4l2_request_drv_video.so SHA256 0ab5b2ba22df19569be26228629968ee254c030cd3664ce7afd1bc0396c254ef
        (built earlier 2026-05-09 01:04, before 7.0 install at 20:06)
Sample: ~/fourier-test/bbb_720p10s_vp9.webm — 3,433,189 bytes
```

Phase 2 doc had wrong control IDs `0xa40b2c`/`0xa40b2d` — corrected to **`0xa40a2c` (VP9_FRAME)** and **`0xa40a2d` (VP9_COMPRESSED_HDR)** in-doc and verified empirically.

## Anchor 1 — SW reference frames (criterion-4 anchor)

Generated via `mpv --hwdec=no --vo=image --vo-image-format=png --frames=5 ~/fourier-test/bbb_720p10s_vp9.webm`. Decoder: libavcodec.

```
sw_ref/00000001.png  b055681b27f2b3cd4151ffdf632876cfcb18ff1e44696d9da4c3f164510b1047
sw_ref/00000002.png  faa84e17aca0f908b9456eab1a1c04ac9832436fa2ff279096de24ae1b072c87
sw_ref/00000003.png  f5d7ed402901052d8b8c0f6f421a164379e9d0da8c3be18429756ccb3998fadf
sw_ref/00000004.png  06ff497a2debe3f93b6038645976a093cbca5058fee0d121a438ec3f87863af2
sw_ref/00000005.png  df3e23ae926ccbf9a8c1a3207f0c449365ba04bb9b26573bb18a7a5d5019e82a
```

These hashes are the `iter4` Phase 7 criterion-4 reference. Persisted at `fresnel:/tmp/iter4_phase3/sw_ref/`.

## Anchor 2 — Kernel-direct VP9 control payloads (`ffmpeg -hwaccel v4l2request`)

`strace -ff -tt -y -v -s 16384 -e trace=ioctl ffmpeg -hwaccel v4l2request -i bbb_720p10s_vp9.webm -frames:v 5 -f null -`

**Verbatim ioctl observation** (frame 1, keyframe submission, after S_FMT/REQBUFS):

```
ioctl(/dev/video1, VIDIOC_S_EXT_CTRLS,
  ctrl_class=0xf010000, count=2,
  controls=[
    { id=0xa40a2c (V4L2_CID_STATELESS_VP9_FRAME),         size=168, ... },
    { id=0xa40a2d (V4L2_CID_STATELESS_VP9_COMPRESSED_HDR), size=2040, ... }
  ]) = 0
```

**Empirical struct sizes — Phase 2 doc estimates were off**:

| Control | Phase 2 estimate | Empirical 7.0 | Delta |
|---|---|---|---|
| `v4l2_ctrl_vp9_frame` | 144 B | **168 B** | +24 |
| `v4l2_ctrl_vp9_compressed_hdr` | 1947 B | **2040 B** | +93 |

The Phase 4 plan must allocate / memcpy / cast against the empirical sizes; Phase 5 review should re-cite from `<linux/v4l2-controls.h>` on a 7.0-installed host (fresnel) rather than a 6.19 working copy.

**Frame-1 keyframe FRAME payload (decoded prefix)**:

- `lf` (16 B): `ref_deltas={1,0,-1,-1}, mode_deltas={0,0}, level=3, sharpness=0, flags=3, reserved[7]=0`
- `quant` (8 B): `base_q_idx=0x2e=46, delta_q_*=0`
- `seg` (~80 B): all zeros (segmentation disabled for keyframe)
- (Full byte-by-byte decode deferred to Phase 4 mapping clauses)

**Strace + decode artifacts** persisted: `fresnel:/tmp/iter4_phase3/vp9_strace_full.*`. Tarball pulled to `noether:~/src/fresnel-fourier/iter4_phase3.tgz` (8.0 MB).

**Initial probe call observation**: ffmpeg first issues a `count=1` S_EXT_CTRLS with only `0xa40a2c` (no compressed-header) — that's the runtime probe to detect kernel CID support. rkvdec accepts (CID is registered). Subsequent submissions use `count=2`. iter4 backend should mirror this 1→2 pattern OR unconditionally send 2 (rkvdec mandatorily-requires COMPRESSED_HDR per `rkvdec-vp9.c:752`).

## Anchor 3 — VP9 mpv-vaapi engagement (negative baseline)

```
mpv -v --hwdec=vaapi --frames=2 ~/fourier-test/bbb_720p10s_vp9.webm
[vd] Opening decoder vp9
[vd] Looking at hwdec vp9-vaapi...
[vd] Using software decoding.        ← expected: backend has no VP9 yet
[vd] Selected decoder: vp9 - Google VP9
```

**Expected**: SW fallback. iter4 backend not built. After Phase 6 install, this exact command should switch to "Selected decoder: vp9_vaapi" + non-SW selection — that is the engagement check per memory `feedback_hw_decode_engagement_check.md`.

## 4-codec regression block — REGRESSION CONFIRMED

Substrate change exposed a pre-existing fork bug. Pre-existing `iter1`/`iter2`/`iter3` PASS results captured on 6.19.9 do **not** reproduce on 7.0 with the env-defaulted device path:

| Codec | Driver/device pre-7.0 | Driver/device 7.0 | mpv-vaapi engages? (no env override) |
|---|---|---|---|
| H.264 | rkvdec (probably video0) | rkvdec at /dev/video1 | NO (SW fallback) |
| MPEG-2 | hantro-dec | hantro-dec at /dev/video3 | NO |
| HEVC | rkvdec | rkvdec at /dev/video1 | NO |
| VP8 | hantro-dec | hantro-dec at /dev/video3 | NO |

**Root cause** (`request.c:149`):

```c
video_path = getenv("LIBVA_V4L2_REQUEST_VIDEO_PATH");
if (!video_path)
    video_path = "/dev/video0";
```

On 7.0, `/dev/video0` is now `rockchip-rga` (RGB color converter) — its OUTPUT formats are pure color, no compressed codec — so the backend enumerates 0 supported codec profiles. `vainfo` confirms: empty profile list under default env. With explicit env `LIBVA_V4L2_REQUEST_VIDEO_PATH=/dev/video1 LIBVA_V4L2_REQUEST_MEDIA_PATH=/dev/media0`, rkvdec advertises H.264×5 + HEVCMain. With `/dev/video3` + `/dev/media1`, hantro advertises MPEG-2×2 + VP8Version0_3.

**Why this didn't surface earlier**: 6.19.9's bind order put rkvdec or hantro at `video0` by default. 7.0's binding/probe order changed. The fork's `/dev/video0` default was always implicit-numbering-dependent; the kernel upgrade decommissioned the assumption.

**Mitigation options for iter4** (decide before Phase 4):

- **A** (cheapest, env-only): document that users must set `LIBVA_V4L2_REQUEST_VIDEO_PATH` + `LIBVA_V4L2_REQUEST_MEDIA_PATH` per codec class. Add to `~/.config/mpv/config` system-wide. No fork patch.
- **B** (in-iter4 fork patch): walk `/dev/video0..N`, query VIDIOC_QUERYCAP, pick first device whose driver name is in {`rkvdec`, `hantro-vpu`}. Adds ~30 LOC to `request.c`. Cleaner end-user experience, but expands iter4 scope beyond VP9.
- **C** (defer to iter5): document as known issue for now; ship iter4 with env-var workaround in regression tests.

**Note**: there is also an mpv-vaapi `Could not create device` failure mode visible at `[vd] Looking at hwdec ...` even with env vars set, suggesting a second issue (likely vaapi-DRM render-node path). This is potentially a separate fix from the device-path issue. Investigation deferred to Phase 6 — ffmpeg-vaapi vs mpv-vaapi may use different device discovery paths.

## Open questions resolved by this baseline

1. **Loop filter deltas**: keyframe `ref_deltas={1,0,-1,-1}, mode_deltas={0,0}` — non-zero. So for BBB the libva backend can't leave them at zero. Source: bitstream `loop_filter_level/lf_delta_enabled/lf_ref_delta` parsed by libavcodec (kernel-direct path uses VP9Context internal state). VAAPI `VADecPictureParameterBufferVP9` does NOT expose these — backend must parse the uncompressed header for them, OR fail BBB criterion-4. **Decision for Phase 4**: ADD an uncompressed-header partial parser for `lf_delta_enabled` + `lf_ref_delta[4]` + `lf_mode_delta[2]`.
2. **Quantization base_q_idx**: keyframe `base_q_idx=46`. VAAPI exposes `seg_param[0].luma_ac_quant_scale` only; need inverse mapping via VP9 spec quantization table at `[1][q]` — feasible but slow. **Decision**: also pull `base_q_idx` from uncompressed header parse (already needed for #1).
3. **Reference mode**: deferred — verbatim payload byte at offset for `reference_mode` field needs decode. Phase 4 plan will pick from explicit byte.
4. **Reset frame context, interpolation filter, segmentation feature mapping**: byte-decode + cross-validate against VAAPI fields — Phase 4 mapping clauses will cite the empirical bytes, not the FFmpeg-inferred mapping.
5. **mpv VP9 hwdec engagement**: SW fallback expected (no VP9 backend yet). After Phase 6 install, expect the engagement string to change.
6. **rkvdec readback non-zero**: deferred to Phase 6/7 — predicted yes (rkvdec passed for HEVC iter2, H.264 T4) but cannot test without engaging libva first, which the device-path issue blocks. Resolve in Phase 6 install with env override.

## New iter4 contract clauses surfaced from baseline

The Phase 4 plan must add:

- **Clause 11** (uncompressed-header partial parse): backend reads bytes 0..uncompressed_header_size from `surface_object->source_data`, runs a minimal-state VPX bool reader to extract `loop_filter_level`/`loop_filter_sharpness`/`lf_delta_enabled`/`lf_ref_delta[4]`/`lf_mode_delta[2]`/`base_q_idx`/`y_dc_delta_q`/`uv_dc_delta_q`/`uv_ac_delta_q`. ~40 LOC. Replaces the "leave loop-filter-deltas at zero" predicted approach.
- **Clause 12** (struct sizing): use empirical 168/2040 B sizes; assert `sizeof(struct v4l2_ctrl_vp9_frame) == 168 && sizeof(struct v4l2_ctrl_vp9_compressed_hdr) == 2040` at compile time so any future kernel UAPI shift fails loudly instead of silently corrupting.

## Decisions queued for the user

- **device-path mitigation**: A / B / C above. Affects iter4 LOC budget (B adds ~30 LOC; A/C none).
- **engagement test path**: `mpv-vaapi-copy` has TWO failure modes (device-path + Could-not-create-device); `ffmpeg-vaapi -hwaccel_output_format vaapi -vf hwdownload` has the device-create issue separately. Should iter4 verify HW=SW via libva at all, or use the kernel-direct-only transitive proof from iter3? (Predicted Phase 7 work.)

## Substrate state at Phase 3 close

- Phase 3 captures persisted on fresnel `/tmp/iter4_phase3/` and tar'd to `noether:~/src/fresnel-fourier/iter4_phase3.tgz` (8.0 MB).
- Phase 2 doc IDs corrected: `0xa40b2c/d` → `0xa40a2c/d` (in `phase2_iter4_situation.md`).
- Empirical struct sizes 168/2040 captured — supersede Phase 2's 144/1947 estimates.
- 4-codec regression: documented; mitigation decision pending (A/B/C).
- iter4 ready to advance to Phase 4 (plan-build) once device-path mitigation is chosen.
