# Iteration 5b — Phase 7 v2 (verification, β + Commit D)

Captured 2026-05-12 evening on fresnel `linux-fresnel-fourier 7.0-1`, fork tip `70196f8`, backend SHA `2c6ff82cbdc156ff8910d0c7fe58e75eeecdfd6e6a1caabb049c8adf43a098b8`. β architecture survived empirical contact; Commit D fix-forward addressed the ffmpeg-vaapi-copy-passes-surfaces_count=0 surprise the first run revealed.

## Verdict

**PARTIAL PASS.** VP9 unblocked directly. MPEG-2 maintained. VP8 mechanically unblocked (decode runs, output diverges from kdirect). HEVC unchanged (separate kernel-rejection issue, pre-existing). H.264 unchanged (Bug 4 deferred as planned).

| Codec | Pre-β | Post-β + D | kdirect | sw | Verdict |
|---|---|---|---|---|---|
| H.264 1080p30 | `71ac099b…` keyframe partial | `71ac099b…` unchanged | `1e7a0bc9…` | `1e7a0bc9…` | **PARTIAL** — Bug 4 (deferred to iter6) |
| HEVC 720p | `06b2c5a0…` all-zero | `06b2c5a0…` still all-zero | `9340b832…` | `9340b832…` | **FAIL** — separate kernel-rejection issue |
| VP9 720p | `06b2c5a0…` all-zero | **`4f1565e8…`** | `4f1565e8…` | `4f1565e8…` | **PASS** ✓✓✓ (libva == kdirect == sw) |
| MPEG-2 720p | `19eefbf4…` worked | `19eefbf4…` maintained | `19eefbf4…` | `7be8cad7…` | **PASS** (libva == kdirect; HW≠SW is codec precision drift, not Bug 2) |
| VP8 720p | `06b2c5a0…` all-zero | `bcc57ed5…` real content but diverges | `136ce5cb…` | `136ce5cb…` | **PARTIAL** — decode runs, output mismatch (256 unique bytes, real keyframe content `93 8e 8a 89 …`) |

## Per Phase 1 lock criteria

1. **HEVC + VP9 + VP8 libva == kdirect == sw**: **PARTIAL — 1 of 3 (VP9 only).**
2. **MPEG-2 unchanged**: **PASS** (after Commit D fix-forward — pre-D was all-zero, post-D matches pre-iter5b).
3. **H.264 keyframe still decodes**: **PASS** (unchanged).
4. **Control-payload anchors hold**: **PASS** (β didn't touch control submission).

## What happened in two acts

### Act 1 — β alone: regression on MPEG-2, no progress elsewhere

Initial post-β sweep returned all-zero for every libva codec. Strace of HEVC: kernel decoder returned `DQBUF` with `V4L2_BUF_FLAG_ERROR` for every frame. Strace of MPEG-2 (post-β): kernel decoder succeeded (10 DQBUF, 0 ERROR) but the userspace read still got all-zero. The MPEG-2 success/userspace-zero combination pointed at a surface-state issue.

Phase 3 baseline cross-check showed:
- Pre-iter5b HEVC trace.11115+: every DQBUF had ERROR flag too — **pre-iter5b HEVC was already broken at the kernel level**, β didn't introduce this.
- Pre-iter5b MPEG-2 trace.11169: 0 ERROR, decode succeeded, userspace got real pixels — **pre-iter5b MPEG-2 worked end-to-end via libva**.

So β fixed the OUTPUT pixel format mismatch (cosmetic improvement for HEVC; correct format for VP9/VP8) but exposed an MPEG-2 regression. Root cause traced to `context.c:262` comment: `ffmpeg vaapi-copy passes surfaces_count==0 to vaCreateContext`. My β walk of `surfaces_ids[]` for the destination_* fill was therefore a no-op for vaapi-copy → `destination_planes_count` stayed 0 → `surface_bind_slot`'s `for (j=0; j<0; ...)` was a no-op → `destination_data[]` never assigned → `copy_surface_to_image` memcpy of 0 bytes from a NULL src.

### Act 2 — Commit D: cache format-uniform state in driver_data, walk surface_heap, lazy-fill on late CreateSurfaces2

Commit `70196f8` adds:
- `request.h` driver_data fields: `fmt_valid`, `fmt_planes_count`, `fmt_buffers_count`, `fmt_format_height`, `fmt_sizes[]`, `fmt_bytesperlines[]`.
- `surface.h/c`: new `surface_fill_format_uniform(driver_data, surface_object)` helper, idempotent on `destination_planes_count != 0`.
- `context.c::RequestCreateContext`: populates the cache after `v4l2_get_format(CAPTURE)`; walks `surface_heap` (not just `surfaces_ids[]`) to fill every existing surface.
- `context.c::RequestCreateContext`: lazy-fill via the helper after surface allocation.
- `context.c::RequestDestroyContext`: invalidates `fmt_valid = false`.

Post-D sweep: MPEG-2 restored, VP9 PASS, VP8 partial, HEVC unchanged.

## The remaining failures

### HEVC: pre-existing kernel-rejection issue

Pre-β HEVC trace.11115 (Phase 3 baseline, same kernel) showed 4 of 4 DQBUFs with `V4L2_BUF_FLAG_ERROR`. Post-β: same. The kernel rkvdec driver rejects HEVC decode for some reason independent of OUTPUT pixel format.

Per the iter2-close docs, HEVC was claimed PASS via transitive proof: backend control payload byte-matched kdirect's payload. The transitive proof verified controls were CORRECTLY SHAPED, not that the kernel ACCEPTED them. Kdirect submits the same shape and the kernel decodes; libva submits the same shape and the kernel returns ERROR. The difference must be in V4L2 protocol order, request_fd binding, sequence numbers, or some other contract aspect not captured in the control-payload bytes.

**Bug 5** (new): HEVC libva decode triggers `DQBUF` `V4L2_BUF_FLAG_ERROR` from rkvdec. Same kernel + same controls + same fixture works via kdirect. Difference is in the libva backend's V4L2 protocol behavior. Out of iter5b scope.

### VP8: partial — decode runs but output diverges

VP8 libva returns `bcc57ed5…` (4 147 200 bytes, 256 unique bytes, 74.8% zero, real keyframe content). Pre-β was all-zero `06b2c5a0…`. So β did unlock VP8 decode at some level. But the output bytes differ from kdirect's `136ce5cb…`.

Frame 1 first 16 bytes: `93 8e 8a 89 85 72 8c 6d 82 79 92 7e 80 80 80 80` — plausible BBB intro pixels (neutral grey-to-mid-tone). Frames 1, 2, 3 differ from each other (motion content present).

Likely candidates:
- Slot rotation: backend reads from one cap_pool slot while kernel wrote to another (similar pattern as H.264 inter-frame Bug 4).
- Partial fill: each frame's lower portion is zero (74.8% zero is a lot).
- Per-frame DPB binding issue.

**Bug 6** (new): VP8 libva decode produces non-zero but non-matching output. Out of iter5b scope.

## Architecture verdict

The β refactor delivered its core goal: **CreateSurfaces2 no longer touches V4L2 device state; CreateContext owns the OUTPUT-side lifecycle**. The Phase 7 α' failure mode is structurally eliminated — there's no in-CreateSurfaces2 destructive teardown branch to corrupt state mid-stream.

The 2-CRIT findings from Phase 5 v2 (CRIT-1 NULL guard, CRIT-2 missing `request_pool_destroy`) were correct and incorporated; without them, Phase 7 wouldn't have gotten past the first CreateContext.

Commit D was the surprise: the architecturally cleaner β created a new edge case (vaapi-copy late-surface flow) that pre-β didn't have because CreateSurfaces2's "fill destination_* immediately" pattern happened to handle it. The fix-forward restores the equivalent behavior via lazy-fill at both sites.

## What iter5b-β actually shipped

- **VP9 directly verifiable** (no more transitive-proof dependency). Campaign scoreboard: 5/5 with 1 newly direct + 4 mixed (1 prior direct MPEG-2, 1 keyframe-partial H.264, 2 partial/transitive VP8+HEVC).
- **MPEG-2 not regressed.**
- **H.264 not regressed.**
- **Bug 5** (HEVC kernel rejection) and **Bug 6** (VP8 partial output) exposed for iter6+ work.
- **Architecture cleaned up.** surface.c::CreateSurfaces2 is now ~70 LOC (was ~250). context.c::RequestCreateContext owns the OUTPUT lifecycle clearly.

## Substrate state at Phase 7 close

- Fork tip `70196f8` on noether + fresnel + gitea.
- Backend installed SHA `2c6ff82cbdc156ff8910d0c7fe58e75eeecdfd6e6a1caabb049c8adf43a098b8`.
- Kernel `linux-fresnel-fourier 7.0-1` (unchanged).
- Test fixtures unchanged.
- Phase 7 sweep artifacts at `/tmp/iter5b_p7v2/` on fresnel.

## Memory rules — candidates at iter5b-β close

The Phase 5 v2 reviewer's "before claiming no change needed, grep all call sites" discipline saved Phase 6 from CRIT-2 silently breaking the second CreateContext. Worth a memory rule:

**Proposed**: `feedback_grep_all_callsites_before_no_change_claim.md` — When a plan says "no change needed" for a code region, grep all symbols/functions/macros the change touches and enumerate the call sites. The author's surface read at Phase 4 v2 missed `request_pool_destroy` not being in DestroyContext; only the reviewer's enumeration caught it.

The Commit D fix-forward exposes a sibling rule: **trust but verify the iter6 comment**. The iter6 comment at context.c:262 explicitly named the ffmpeg-vaapi-copy surfaces_count=0 case. Phase 4 v2 plan acknowledged the comment but didn't trace the implication for the destination_* walk. Comment read → implication not derived.

Both rules can be folded into one: **plans that touch lifecycle code must enumerate ALL existing comments AND grep ALL call sites for affected symbols; "no change needed" claims must be backed by explicit enumeration**. Defer the memory rule write to Phase 8 close.

## Phase 7 → Phase 8 handoff

Phase 7 closes here. iter5b reaches partial-PASS:

- ✓ VP9 directly unblocked (the primary iter5b-β goal achieved for 1 of the 3 target codecs).
- ✓ MPEG-2 not regressed (Commit D fix-forward).
- ✓ H.264 not regressed.
- ✗ HEVC remains broken (Bug 5 — pre-existing, exposed).
- ✗ VP8 partial (Bug 6 — new, decode-runs-but-output-diverges).

The campaign scoreboard becomes "5/5 with 1 direct (MPEG-2) + 1 newly direct (VP9) + 3 mixed (H.264 keyframe-partial + VP8 partial + HEVC transitive-claimed-but-empirically-failing)."

Phase 8 close documents the iter5b-β shipped state and lists Bug 5 + Bug 6 + Bug 4 as iter6+ work targets. Memory rule update covers the empirical lessons.
