# Iteration 1 — Phase 4 (plan)

Implementation plan for iter1 MPEG-2 boolean correctness on fresnel. Inputs:

- [`phase0_findings_iter1.md`](phase0_findings_iter1.md) — Phase 1 lock (5 boolean criteria; #3 adjusted by Phase 3 loopback below).
- [`phase2_iter1_situation.md`](phase2_iter1_situation.md) — three bugs identified, all in libva backend.
- [`phase3_iter1_baseline.md`](phase3_iter1_baseline.md) — bugs confirmed empirically; Baseline C verbatim contract anchor; one criterion adjustment.

Per `feedback_dev_process.md` Phase 6 contract-before-code: this plan opens with the contract clauses (kernel doc + driver + sibling implementation citations), then specifies code changes that map 1:1 to those clauses. No reverse-engineering from observed Baseline B behavior, no mimicking of FFmpeg without spec-rooting.

## Phase 1 criterion #3 — re-lock with adjusted wording

Per Phase 3 → Phase 1 loopback per `feedback_dev_process.md`:

**Original (Phase 1 lock 2026-05-07)**:

> 3. End-to-end decode engages the backend. `mpv --hwdec=vaapi-copy --frames=2 --vo=null --no-audio … bbb_720p10s_mpeg2.ts` engages our backend, exits 0 with no `Failed to create decode configuration` lines.

**Adjusted (Phase 4 lock 2026-05-08)**:

> 3. End-to-end decode engages the backend. `ffmpeg -hwaccel vaapi -i bbb_720p10s_mpeg2.ts -frames:v 2 -f null -` (with `LIBVA_DRIVER_NAME=v4l2_request`, `LIBVA_V4L2_REQUEST_VIDEO_PATH=/dev/video5`, `LIBVA_V4L2_REQUEST_MEDIA_PATH=/dev/media2`) shows the `[VAAPI] libva: Trying to open /usr/lib/dri/v4l2_request_drv_video.so` chain in stderr, the libva trace shows `vaCreateConfig ret = VA_STATUS_SUCCESS`, no `Failed to create decode configuration` lines, no `EINVAL` from `VIDIOC_S_EXT_CTRLS`, and ffmpeg exits 0 cleanly.

Reason: Phase 3 Baseline A established that mpv-vaapi-copy never engages libva for MPEG-2 (mpv's hwdec policy filters MPEG-2 out before libva is loaded). The original criterion #3 was unfalsifiable through the original incantation. ffmpeg-direct does engage the path and reaches the failure mode that iter1 fixes — so it's the right anchor for Phase 7 verification.

mpv-driven MPEG-2 testing becomes a separate, smaller follow-up task (mpv `--hwdec-codecs` filter override or upstream mpv investigation), out of iter1 scope.

The other four criteria hold unchanged:

1. `vainfo` enumerates VAProfileMPEG2Simple + VAProfileMPEG2Main (regression check).
2. `vaCreateConfig(VAProfileMPEG2Main, VLD)` returns `VA_STATUS_SUCCESS`.
4. DMA-BUF GL import HW frames byte-identical to SW reference for 2 distinct frames at `+02s` seek.
5. T4 H.264 reference hashes (`f623d5f7…`, `7d7bc6f2…`) at `+30s` continue to match.

## Contract clauses (cite-before-code)

### Clause 1 — Three split controls submitted in one batched VIDIOC_S_EXT_CTRLS

**Authority**: Linux mainline `include/uapi/linux/v4l2-controls.h:1988-2105` defines three separate stateless controls for MPEG-2:

```c
#define V4L2_CID_STATELESS_MPEG2_SEQUENCE     (V4L2_CID_CODEC_STATELESS_BASE+220)  /* 0xa409dc */
#define V4L2_CID_STATELESS_MPEG2_PICTURE      (V4L2_CID_CODEC_STATELESS_BASE+221)  /* 0xa409dd */
#define V4L2_CID_STATELESS_MPEG2_QUANTISATION (V4L2_CID_CODEC_STATELESS_BASE+222)  /* 0xa409de */
```

**Reference implementation**: FFmpeg `libavcodec/v4l2_request_mpeg2.c:130-155` builds a 3-element `v4l2_ext_control` array and submits via a single `ff_v4l2_request_decode_frame()` call (which calls `VIDIOC_S_EXT_CTRLS` once with the whole batch).

**Empirical anchor**: Phase 3 Baseline C strace verbatim (from `phase3_iter1_baseline.md`) shows `ffmpeg -hwaccel v4l2request` submits exactly `count=3, ctrl_class=0xf010000` per frame.

**Implication for iter1**: `mpeg2_set_controls` builds a 3-entry `struct v4l2_ext_control` array and submits via the existing `v4l2_set_controls(driver_data->video_fd, surface_object->request_fd, controls, 3)` API (same call shape as `src/h264.c:986`). One VIDIOC_S_EXT_CTRLS per frame, not three.

### Clause 2 — `v4l2_ctrl_mpeg2_sequence` field layout (12 bytes)

**Authority**: `linux/v4l2-controls.h:2009-2017`:

```c
struct v4l2_ctrl_mpeg2_sequence {
    __u16 horizontal_size;             /* offset 0 */
    __u16 vertical_size;               /* offset 2 */
    __u32 vbv_buffer_size;             /* offset 4 */
    __u16 profile_and_level_indication;/* offset 8 */
    __u8  chroma_format;               /* offset 10 */
    __u8  flags;                       /* offset 11 — V4L2_MPEG2_SEQ_FLAG_PROGRESSIVE only */
};
```

Total 12 bytes. The `flags` field has exactly one defined bit:

```c
#define V4L2_MPEG2_SEQ_FLAG_PROGRESSIVE 0x01    /* v4l2-controls.h:1985 */
```

**VAAPI source mapping** (no permutation, direct):

| Sequence field | Source: `VAPictureParameterBufferMPEG2 *picture` |
|---|---|
| `horizontal_size` | `picture->horizontal_size` |
| `vertical_size`   | `picture->vertical_size` |
| `vbv_buffer_size` | `surface_object->source_size` (the OUTPUT buffer's size — matches FFmpeg's `controls->pic.output->size` per `v4l2_request_mpeg2.c:51`) |
| `profile_and_level_indication` | 0 (current `mpeg2.c:70` already hardcodes; VAAPI doesn't expose profile/level on the picture buffer) |
| `chroma_format`   | 1 (4:2:0; current `mpeg2.c:72` hardcodes; iter1 keeps this since fixture is 4:2:0 and campaign codec scope locked at 8-bit 4:2:0) |
| `flags`           | `picture->picture_coding_extension.bits.progressive_frame ? V4L2_MPEG2_SEQ_FLAG_PROGRESSIVE : 0` |

**Note on `progressive_frame` vs `progressive_sequence`**: VAAPI's `VAPictureParameterBufferMPEG2` does not expose the bitstream's `progressive_sequence` flag from the sequence-extension header — only `progressive_frame` from the picture-coding extension. They're the same bit for typical MPEG-2 streams (BBB is progressive throughout). iter1 uses `progressive_frame` as the proxy. This is documented in the patch as a known approximation (good enough for binding-cell correctness; refinement is post-iter1).

### Clause 3 — `v4l2_ctrl_mpeg2_picture` field layout (32 bytes)

**Authority**: `linux/v4l2-controls.h:2056-2065`:

```c
struct v4l2_ctrl_mpeg2_picture {
    __u64 backward_ref_ts;     /* offset  0 */
    __u64 forward_ref_ts;      /* offset  8 */
    __u32 flags;               /* offset 16 */
    __u8  f_code[2][2];        /* offset 20 — packed [4 bytes] */
    __u8  picture_coding_type; /* offset 24 */
    __u8  picture_structure;   /* offset 25 */
    __u8  intra_dc_precision;  /* offset 26 */
    __u8  reserved[5];         /* offset 27 — must be zero per kernel doc */
};
```

Total 32 bytes. **Reserved bytes must be zeroed** (`v4l2-controls.h:2052: "@reserved: padding field. Should be zeroed by applications."`). Fix shape: `memset(&picture, 0, sizeof picture)` before populating; populating only listed fields leaves reserved zero.

`flags` defined bits (`v4l2-controls.h:2026-2033`):

```c
#define V4L2_MPEG2_PIC_FLAG_TOP_FIELD_FIRST    0x0001
#define V4L2_MPEG2_PIC_FLAG_FRAME_PRED_DCT     0x0002
#define V4L2_MPEG2_PIC_FLAG_CONCEALMENT_MV     0x0004
#define V4L2_MPEG2_PIC_FLAG_Q_SCALE_TYPE       0x0008
#define V4L2_MPEG2_PIC_FLAG_INTRA_VLC          0x0010
#define V4L2_MPEG2_PIC_FLAG_ALT_SCAN           0x0020
#define V4L2_MPEG2_PIC_FLAG_REPEAT_FIRST       0x0040
#define V4L2_MPEG2_PIC_FLAG_PROGRESSIVE        0x0080
```

`picture_coding_type` enum (`v4l2-controls.h:1990-1993`):

```c
#define V4L2_MPEG2_PIC_CODING_TYPE_I    1
#define V4L2_MPEG2_PIC_CODING_TYPE_P    2
#define V4L2_MPEG2_PIC_CODING_TYPE_B    3
#define V4L2_MPEG2_PIC_CODING_TYPE_D    4
```

`picture_structure` enum (`v4l2-controls.h:1995-1997`):

```c
#define V4L2_MPEG2_PIC_TOP_FIELD     0x1
#define V4L2_MPEG2_PIC_BOTTOM_FIELD  0x2
#define V4L2_MPEG2_PIC_FRAME         0x3
```

**VAAPI source mapping**:

| Picture field | Source: `VAPictureParameterBufferMPEG2 *picture`, etc. | Notes |
|---|---|---|
| `backward_ref_ts` | `v4l2_timeval_to_ns(&backward_reference_surface->timestamp)` if `picture->backward_reference_picture != VA_INVALID_ID`, else `0` | Existing `mpeg2.c:112-118` logic preserved; just routes to picture struct instead of slice_params |
| `forward_ref_ts`  | `v4l2_timeval_to_ns(&forward_reference_surface->timestamp)` if `picture->forward_reference_picture != VA_INVALID_ID`, else `0` | Same |
| `flags` (bitwise OR of:) | | |
| `_TOP_FIELD_FIRST` | `picture->picture_coding_extension.bits.top_field_first` |
| `_FRAME_PRED_DCT` | `picture->picture_coding_extension.bits.frame_pred_frame_dct` |
| `_CONCEALMENT_MV` | `picture->picture_coding_extension.bits.concealment_motion_vectors` |
| `_Q_SCALE_TYPE` | `picture->picture_coding_extension.bits.q_scale_type` |
| `_INTRA_VLC` | `picture->picture_coding_extension.bits.intra_vlc_format` |
| `_ALT_SCAN` | `picture->picture_coding_extension.bits.alternate_scan` |
| `_REPEAT_FIRST` | `picture->picture_coding_extension.bits.repeat_first_field` |
| `_PROGRESSIVE` | `picture->picture_coding_extension.bits.progressive_frame` |
| `f_code[0][0]` | `(picture->f_code >> 12) & 0x0f` | Existing `mpeg2.c:75` logic preserved |
| `f_code[0][1]` | `(picture->f_code >>  8) & 0x0f` |
| `f_code[1][0]` | `(picture->f_code >>  4) & 0x0f` |
| `f_code[1][1]` | `(picture->f_code >>  0) & 0x0f` |
| `picture_coding_type` | `picture->picture_coding_type` (VAAPI uses I=1, P=2, B=3, D=4 — same as new V4L2 macros) |
| `picture_structure`   | `picture->picture_coding_extension.bits.picture_structure` (VAAPI uses TOP=1, BOTTOM=2, FRAME=3 — same as new V4L2 macros) |
| `intra_dc_precision`  | `picture->picture_coding_extension.bits.intra_dc_precision` |
| `reserved[5]` | zero (via `memset`) |

**No semantic change in source extraction** — same VAAPI field reads as the current `mpeg2.c:74-100`. Only the destination struct layout changes (field reorder + booleans collapsed into `flags`).

### Clause 4 — `v4l2_ctrl_mpeg2_quantisation` field layout (256 bytes)

**Authority**: `linux/v4l2-controls.h:2089-2096`:

```c
struct v4l2_ctrl_mpeg2_quantisation {
    __u8 intra_quantiser_matrix[64];        /* offset   0 */
    __u8 non_intra_quantiser_matrix[64];    /* offset  64 */
    __u8 chroma_intra_quantiser_matrix[64]; /* offset 128 */
    __u8 chroma_non_intra_quantiser_matrix[64]; /* offset 192 */
};
```

Total 256 bytes. Matrices in **zigzag scanning order** per kernel doc (`v4l2-controls.h:2076`: "in zigzag scanning order").

**Quantisation submission semantics** — what to do when VAAPI doesn't supply an IQ matrix:

The current `mpeg2.c:126-151` only sends `V4L2_CID_MPEG_VIDEO_MPEG2_QUANTIZATION` when `iqmatrix_set` is true (the consumer sent a `VAIQMatrixBufferType` for this surface). With the **new** API the question is: does the kernel allow omission of `STATELESS_MPEG2_QUANTISATION` from the batch when the bitstream uses default matrices?

Reading kernel `drivers/media/platform/verisilicon/hantro_mpeg2.c:hantro_mpeg2_dec_copy_qtable` (line 12-26 in the mainline reference): the function takes `const struct v4l2_ctrl_mpeg2_quantisation *ctrl` and applies zigzag-to-raster permutation while copying the four matrices into the hardware qtable. The driver's `hantro_g1_mpeg2_dec.c::config_decoding_engine` (the engine entry that uses qtable) calls this unconditionally. **The kernel always reads all four matrices**; it doesn't have a "no quantisation control set" code path that synthesizes defaults.

So either:
- (a) the libva backend always sends `QUANTISATION` in the batch, populating from `iqmatrix` if `iqmatrix_set`, or filling with the MPEG-2 default matrices otherwise; or
- (b) the kernel rejects (EINVAL) the batch if `QUANTISATION` is absent.

FFmpeg's reference `v4l2_request_mpeg2.c:110-116` always populates and submits `QUANTISATION` from the codec's internal matrix state (which has the bitstream-provided or spec-default matrices, depending on whether the bitstream supplied custom ones). So FFmpeg takes path (a).

**Phase 4 decision**: iter1 takes path (a). When `iqmatrix_set` is false, populate the 4 matrices with the canonical MPEG-2 default values from the spec:

- `intra_quantiser_matrix` = `{8, 16, 16, 19, 16, 19, 22, 22, 22, 22, 22, 22, 26, 24, 26, 27, 27, 27, 26, 26, 26, 26, 27, 27, 27, 29, 29, 29, 34, 34, 34, 29, 29, 29, 34, 37, 38, 37, 35, 35, 38, 41, 41, 38, 38, 41, 41, 45, 45, 45, 49, 49, 56, 56, 64, 69, 83, ...}` (ISO/IEC 13818-2 Table 7-3 default intra matrix in zigzag scan order — same as Baseline C's verbatim payload first row).
- `non_intra_quantiser_matrix` = all 16's (ISO/IEC 13818-2 default non-intra matrix, flat 16).
- `chroma_intra_quantiser_matrix` = same as `intra_quantiser_matrix` (spec default for 4:2:0).
- `chroma_non_intra_quantiser_matrix` = all 16's (spec default for 4:2:0).

VAAPI's `VAIQMatrixBufferMPEG2` matrices are also in zigzag scanning order (per VAAPI spec, `va_dec_mpeg2.h`). Direct memcpy works; no permutation in the libva backend.

**Empirical anchor**: Baseline C's QUANTISATION first row (`8, 16, 16, 19, 16, 19, 22, 22, ...`) matches the spec default, indicating the BBB-720p10s_mpeg2.ts fixture uses default matrices (no custom matrices in the sequence-extension). For iter1 verification: when `iqmatrix_set` is false on this fixture, the synthesized defaults must produce a payload byte-identical to Baseline C's QUANTISATION 256 bytes.

### Clause 5 — Per-frame submission shape

**Authority**: existing `src/h264.c:986` shows the established pattern in this fork:

```c
rc = v4l2_set_controls(driver_data->video_fd, surface->request_fd,
                       controls, num_controls);
```

`v4l2_set_controls` (defined `src/v4l2.c:475-489`) wraps `VIDIOC_S_EXT_CTRLS` with `request_fd` set to the per-surface request_fd (binds the controls to the request, not to the device-wide state).

**Implication for iter1**: `mpeg2_set_controls` builds the 3-entry array, calls `v4l2_set_controls` once. Replaces the current two-call pattern (`v4l2_set_control(SLICE_PARAMS)` + conditional `v4l2_set_control(QUANTIZATION)`).

### Clause 6 — `RequestCreateConfig` MPEG-2 case must `break;`, not fall through

**Authority**: C language semantics. `src/config.c:55-69` switch falls through from `case VAProfileMPEG2Simple/Main:` (lines 64-65) to `default:` (line 67) which returns the error. H.264 cases at `src/config.c:57-62` have an explicit `break;` at line 63. The H.264 `// FIXME` comment at line 62 indicates profile-specific validation logic was anticipated but never landed; treating MPEG-2 the same way (no validation, just accept the profile) is the smallest patch and matches H.264's current behavior.

**Empirical anchor**: Phase 3 Baseline B verified the patch shape in scratch — adding `break;` for MPEG-2 cases lets `vaCreateConfig` return `VA_STATUS_SUCCESS` and the V4L2 setup proceeds normally up to the next failure point (`VIDIOC_S_EXT_CTRLS` with old CIDs).

## Diff scope

### File 1: `src/config.c` — add `break;` for MPEG-2 cases

3 lines added, no other changes:

```diff
@@ -63,6 +63,9 @@ VAStatus RequestCreateConfig(VADriverContextP context, VAProfile profile,
 		break;
 	case VAProfileMPEG2Simple:
 	case VAProfileMPEG2Main:
+		// MPEG-2 enabled iter1; same shape as H.264 — no profile-
+		// specific config validation in the libva backend (validation
+		// happens at vaCreateContext / control submission time).
+		break;
 	case VAProfileHEVCMain:
 	default:
 		return VA_STATUS_ERROR_UNSUPPORTED_PROFILE;
```

Also drop the now-stale `#include <mpeg2-ctrls.h>` line (line 37) — no `V4L2_CID_MPEG_VIDEO_MPEG2_*` symbols are referenced in `config.c` after this edit; the kernel UAPI for MPEG-2 control IDs comes from `<linux/v4l2-controls.h>` (pulled transitively via `<linux/videodev2.h>`).

### File 2: `src/mpeg2.c` — full rewrite of `mpeg2_set_controls`

Per Clause 1–5. New shape (commented to map 1:1 to clause numbers):

```c
int mpeg2_set_controls(struct request_data *driver_data,
                       struct object_context *context_object,
                       struct object_surface *surface_object)
{
    VAPictureParameterBufferMPEG2 *picture =
        &surface_object->params.mpeg2.picture;
    VAIQMatrixBufferMPEG2 *iqmatrix =
        &surface_object->params.mpeg2.iqmatrix;
    bool iqmatrix_set = surface_object->params.mpeg2.iqmatrix_set;

    /* Clause 2: v4l2_ctrl_mpeg2_sequence (12 bytes) */
    struct v4l2_ctrl_mpeg2_sequence sequence;
    /* Clause 3: v4l2_ctrl_mpeg2_picture (32 bytes; reserved[5] must be zero) */
    struct v4l2_ctrl_mpeg2_picture pic;
    /* Clause 4: v4l2_ctrl_mpeg2_quantisation (256 bytes) */
    struct v4l2_ctrl_mpeg2_quantisation quant;

    struct object_surface *forward_reference_surface;
    struct object_surface *backward_reference_surface;
    int rc;

    memset(&sequence, 0, sizeof sequence);
    memset(&pic, 0, sizeof pic);    /* zeros pic.reserved[5] per Clause 3 */
    memset(&quant, 0, sizeof quant);

    /* --- Clause 2: SEQUENCE --- */
    sequence.horizontal_size = picture->horizontal_size;
    sequence.vertical_size = picture->vertical_size;
    sequence.vbv_buffer_size = surface_object->source_size;
    sequence.profile_and_level_indication = 0;  /* not exposed by VAAPI picture buffer */
    sequence.chroma_format = 1;                 /* 4:2:0 — campaign codec scope */
    if (picture->picture_coding_extension.bits.progressive_frame)
        sequence.flags |= V4L2_MPEG2_SEQ_FLAG_PROGRESSIVE;

    /* --- Clause 3: PICTURE --- */
    forward_reference_surface =
        SURFACE(driver_data, picture->forward_reference_picture);
    if (forward_reference_surface != NULL)
        pic.forward_ref_ts =
            v4l2_timeval_to_ns(&forward_reference_surface->timestamp);

    backward_reference_surface =
        SURFACE(driver_data, picture->backward_reference_picture);
    if (backward_reference_surface != NULL)
        pic.backward_ref_ts =
            v4l2_timeval_to_ns(&backward_reference_surface->timestamp);

    if (picture->picture_coding_extension.bits.top_field_first)
        pic.flags |= V4L2_MPEG2_PIC_FLAG_TOP_FIELD_FIRST;
    if (picture->picture_coding_extension.bits.frame_pred_frame_dct)
        pic.flags |= V4L2_MPEG2_PIC_FLAG_FRAME_PRED_DCT;
    if (picture->picture_coding_extension.bits.concealment_motion_vectors)
        pic.flags |= V4L2_MPEG2_PIC_FLAG_CONCEALMENT_MV;
    if (picture->picture_coding_extension.bits.q_scale_type)
        pic.flags |= V4L2_MPEG2_PIC_FLAG_Q_SCALE_TYPE;
    if (picture->picture_coding_extension.bits.intra_vlc_format)
        pic.flags |= V4L2_MPEG2_PIC_FLAG_INTRA_VLC;
    if (picture->picture_coding_extension.bits.alternate_scan)
        pic.flags |= V4L2_MPEG2_PIC_FLAG_ALT_SCAN;
    if (picture->picture_coding_extension.bits.repeat_first_field)
        pic.flags |= V4L2_MPEG2_PIC_FLAG_REPEAT_FIRST;
    if (picture->picture_coding_extension.bits.progressive_frame)
        pic.flags |= V4L2_MPEG2_PIC_FLAG_PROGRESSIVE;

    pic.f_code[0][0] = (picture->f_code >> 12) & 0x0f;
    pic.f_code[0][1] = (picture->f_code >>  8) & 0x0f;
    pic.f_code[1][0] = (picture->f_code >>  4) & 0x0f;
    pic.f_code[1][1] = (picture->f_code >>  0) & 0x0f;
    pic.picture_coding_type = picture->picture_coding_type;
    pic.picture_structure = picture->picture_coding_extension.bits.picture_structure;
    pic.intra_dc_precision = picture->picture_coding_extension.bits.intra_dc_precision;
    /* pic.reserved[5] zero via memset */

    /* --- Clause 4: QUANTISATION --- */
    if (iqmatrix_set) {
        memcpy(quant.intra_quantiser_matrix,
               iqmatrix->intra_quantiser_matrix, 64);
        memcpy(quant.non_intra_quantiser_matrix,
               iqmatrix->non_intra_quantiser_matrix, 64);
        memcpy(quant.chroma_intra_quantiser_matrix,
               iqmatrix->chroma_intra_quantiser_matrix, 64);
        memcpy(quant.chroma_non_intra_quantiser_matrix,
               iqmatrix->chroma_non_intra_quantiser_matrix, 64);
    } else {
        /* MPEG-2 spec defaults: ISO/IEC 13818-2 Table 7-3 default
         * intra; flat 16 for non-intra. Matrices are in zigzag
         * scanning order per kernel doc (v4l2-controls.h:2076). */
        static const __u8 default_intra[64] = {
            8, 16, 16, 19, 16, 19, 22, 22, 22, 22, 22, 22,
            26, 24, 26, 27, 27, 27, 26, 26, 26, 26, 27, 27,
            27, 29, 29, 29, 34, 34, 34, 29, 29, 29, 34, 37,
            38, 37, 35, 35, 38, 41, 41, 38, 38, 41, 41, 45,
            45, 45, 49, 49, 56, 56, 64, 69, 83 /* zigzag, ... */
        };
        /* non_intra/chroma_non_intra: all 16's (memset to 16) */
        memcpy(quant.intra_quantiser_matrix, default_intra, 64);
        memcpy(quant.chroma_intra_quantiser_matrix, default_intra, 64);
        memset(quant.non_intra_quantiser_matrix, 16, 64);
        memset(quant.chroma_non_intra_quantiser_matrix, 16, 64);
    }

    /* --- Clause 1+5: batched submission --- */
    struct v4l2_ext_control ctrls[3] = {
        { .id = V4L2_CID_STATELESS_MPEG2_SEQUENCE,
          .ptr = &sequence, .size = sizeof sequence },
        { .id = V4L2_CID_STATELESS_MPEG2_PICTURE,
          .ptr = &pic,      .size = sizeof pic      },
        { .id = V4L2_CID_STATELESS_MPEG2_QUANTISATION,
          .ptr = &quant,    .size = sizeof quant    },
    };

    rc = v4l2_set_controls(driver_data->video_fd,
                           surface_object->request_fd,
                           ctrls, 3);
    if (rc < 0)
        return VA_STATUS_ERROR_OPERATION_FAILED;

    return 0;
}
```

Drop `#include <mpeg2-ctrls.h>` from `mpeg2.c:38`. Add `#include <linux/v4l2-controls.h>` if not transitively included via `<linux/videodev2.h>` — verify at compile time.

`default_intra[64]` table needs the full 64 entries spelled out — abbreviated above with `...`; the actual Phase 6 patch will list all 64 verbatim from ISO/IEC 13818-2 Table 7-3. The first 57 values shown above are correct; remaining 7 from the spec to be transcribed verbatim during Phase 6.

### File 3: `include/mpeg2-ctrls.h` — delete

The file's preamble explicitly warns:

> It turns out that these structs are not stable yet and will undergo more changes. So keep them private until they are stable and ready to become part of the official public API.

The structs DID stabilize and DID become public — at different CIDs and with different layouts. The local header now actively masks the kernel's modern definitions.

Delete the file entirely. Audit `git grep "mpeg2-ctrls.h"` for any remaining includes (Phase 2 found two: `src/config.c:37` and `src/mpeg2.c:38`, both removed in File 1 + File 2 above; verify no other references exist).

### File 4: `src/picture.c` — no changes

Phase 2 verified: `codec_store_buffer` handles `VAPictureParameterBufferType` and `VAIQMatrixBufferType` for MPEG-2 correctly (lines 87-92, 140-146). `codec_set_controls` dispatches to `mpeg2_set_controls` (lines 186-191). The unconditional `params.h264.matrix_set = false` reset at line 287 is benign for MPEG-2 (union aliasing puts it in mpeg2-struct space, RenderPicture overwrites that byte before mpeg2_set_controls reads anything).

### File 5: `src/context.c` — no changes

Phase 2 + Phase 3 verified: the unconditional H.264 device-init at lines 142-155 is intentional best-effort — `(void)v4l2_set_controls(...)` discards the return value. The EINVAL on hantro is silently swallowed, doesn't propagate. Auxiliary noise in the trace, not blocking MPEG-2.

### Files 6+: meson, headers, includes — verify only

- `meson.build`, `src/meson.build` — no changes. `mpeg2.c` is already in the sources list; no new files.
- `<linux/v4l2-controls.h>` symbol checks — verify that `V4L2_CID_STATELESS_MPEG2_{SEQUENCE,PICTURE,QUANTISATION}`, `struct v4l2_ctrl_mpeg2_{sequence,picture,quantisation}`, and the flag macros (`V4L2_MPEG2_SEQ_FLAG_PROGRESSIVE`, `V4L2_MPEG2_PIC_FLAG_*`, `V4L2_MPEG2_PIC_CODING_TYPE_*`, `V4L2_MPEG2_PIC_*_FIELD`/`FRAME`) are all available via `<linux/videodev2.h>` on the build host. Phase 0 build already showed `linux-api-headers` is present and the H.264 + HEVC + MPEG-2 stateless symbols are all defined; this is a sanity-check, not a likely failure.

## Implementation order (Phase 6)

Phase 6 lands in three logical commits, each independently testable:

1. **Commit A — config.c break**: 1-file diff, 3 lines added. Verifies the substrate fix in isolation. Phase 7 partial verification: criteria #1, #2 should pass (vainfo enumeration unchanged, vaCreateConfig succeeds). Criterion #3-#5 still fail/regress until subsequent commits land.

2. **Commit B — mpeg2.c rewrite + drop mpeg2-ctrls.h include from config.c and mpeg2.c**: the bulk of the work. Verifies the new control submission. Phase 7 partial verification: criteria #1, #2, #3 should pass (vaCreateConfig + S_EXT_CTRLS both succeed); criterion #4 (DMA-BUF GL pixel verify) should pass for the bbb fixture; criterion #5 (H.264 regression) holds.

3. **Commit C — delete `include/mpeg2-ctrls.h`**: pure cleanup. No behavior change (after Commit B, no remaining symbols are referenced). Phase 7 verification re-run, all 5 criteria still green.

If Commit A or B causes Phase 7 regression that doesn't loop back cleanly to Phase 4, treat the regression as a finding (don't paper over it).

## Phase 7 verification harness

Each verification uses fresnel directly. All 5 criteria run in one pass; raw output captured to `phase0_evidence/2026-05-08/iter1_phase7/`.

```bash
# Re-build + install
ssh fresnel '
cd ~/src/libva-v4l2-request-fourier
git pull --ff-only
ninja -C build
sudo ninja -C build install
'

# Criterion 1: vainfo regression (must list MPEG-2 Simple + Main on hantro)
ssh fresnel '
LIBVA_DRIVER_NAME=v4l2_request \
LIBVA_V4L2_REQUEST_VIDEO_PATH=/dev/video5 \
LIBVA_V4L2_REQUEST_MEDIA_PATH=/dev/media2 \
vainfo --display drm --device /dev/dri/renderD128 2>&1 | \
  grep -E "VAProfileMPEG2(Simple|Main)"
'
# Expected: 2 lines, both with VAEntrypointVLD

# Criteria 2 + 3: vaCreateConfig + ffmpeg-driven decode (no errors, exit 0)
ssh fresnel '
mkdir -p /tmp/iter1_phase7
LIBVA_DRIVER_NAME=v4l2_request \
LIBVA_V4L2_REQUEST_VIDEO_PATH=/dev/video5 \
LIBVA_V4L2_REQUEST_MEDIA_PATH=/dev/media2 \
LIBVA_TRACE=/tmp/iter1_phase7/libva.trace \
ffmpeg -hide_banner -loglevel info -hwaccel vaapi \
  -i ~/fourier-test/bbb_720p10s_mpeg2.ts -frames:v 2 -f null - 2>&1
'
# Expected: exit 0, no "Failed to create decode configuration",
# libva trace shows vaCreateConfig SUCCESS, no EINVAL on S_EXT_CTRLS.

# Criterion 4: DMA-BUF GL pixel verify (HW=SW byte-identical at +02s seek)
ssh fresnel '
mkdir -p /tmp/iter1_phase7/png_hw /tmp/iter1_phase7/png_sw
WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 \
LIBVA_DRIVER_NAME=v4l2_request \
LIBVA_V4L2_REQUEST_VIDEO_PATH=/dev/video5 \
LIBVA_V4L2_REQUEST_MEDIA_PATH=/dev/media2 \
mpv --hwdec=vaapi --frames=2 --vo=image --no-audio \
    --no-input-default-bindings --start=00:00:02 \
    --vo-image-outdir=/tmp/iter1_phase7/png_hw \
    ~/fourier-test/bbb_720p10s_mpeg2.ts

mpv --hwdec=no --frames=2 --vo=image --no-audio \
    --no-input-default-bindings --start=00:00:02 \
    --vo-image-outdir=/tmp/iter1_phase7/png_sw \
    ~/fourier-test/bbb_720p10s_mpeg2.ts

sha256sum /tmp/iter1_phase7/png_hw/*.jpg /tmp/iter1_phase7/png_sw/*.jpg
'
# Expected: HW frame 1 hash == SW frame 1 hash; HW frame 2 hash ==
# SW frame 2 hash; frame 1 hash != frame 2 hash (real motion).
# NOTE: this assumes mpv-vaapi (DMA-BUF) WILL engage for MPEG-2 even
# though mpv-vaapi-copy (cached-mmap) doesn't. To be verified in Phase 7
# baseline; if mpv-vaapi also filters MPEG-2 out, fall back to ffmpeg
# -hwaccel vaapi -vf hwdownload,format=nv12 -f rawvideo, hash the YUV.

# Criterion 5: H.264 regression — T4 reference hashes still match
ssh fresnel '
WAYLAND_DISPLAY=wayland-0 XDG_RUNTIME_DIR=/run/user/1000 \
LIBVA_DRIVER_NAME=v4l2_request \
LIBVA_V4L2_REQUEST_VIDEO_PATH=/dev/video3 \
LIBVA_V4L2_REQUEST_MEDIA_PATH=/dev/media1 \
mpv --hwdec=vaapi --frames=2 --vo=image --no-audio \
    --no-input-default-bindings --start=00:00:30 \
    --vo-image-outdir=/tmp/iter1_phase7/png_h264 \
    ~/fourier-test/bbb_1080p30_h264.mp4
sha256sum /tmp/iter1_phase7/png_h264/*.jpg
'
# Expected: f623d5f7a41697f67dd227275c6f1b21ffc257f65626d32fde8229357f8764c9 (frame 1)
#           7d7bc6f2146dda8b2d223bba622c4b9fbe9674181ff1e02afe286b620342e0a8 (frame 2)

# Extra (cross-validator anchor): byte-compare post-fix S_EXT_CTRLS payload
# vs Baseline C verbatim.
ssh fresnel '
mkdir -p /tmp/iter1_phase7/cross
sudo sh -c "echo 1 > /sys/kernel/tracing/events/v4l2/enable; \
            echo 1 > /sys/kernel/tracing/tracing_on"
strace -ff -tt -y -v -e trace=ioctl \
  -o /tmp/iter1_phase7/cross/ffmpeg.strace \
  env LIBVA_DRIVER_NAME=v4l2_request \
      LIBVA_V4L2_REQUEST_VIDEO_PATH=/dev/video5 \
      LIBVA_V4L2_REQUEST_MEDIA_PATH=/dev/media2 \
  ffmpeg -hide_banner -loglevel error -hwaccel vaapi \
    -i ~/fourier-test/bbb_720p10s_mpeg2.ts -frames:v 2 -f null -
sudo sh -c "echo 0 > /sys/kernel/tracing/tracing_on; \
            echo 0 > /sys/kernel/tracing/events/v4l2/enable"
grep "VIDIOC_S_EXT_CTRLS" /tmp/iter1_phase7/cross/ffmpeg.strace.* | head
'
# Expected (frame 1): one batched call, ctrl_class=0xf010000, count=3,
# CIDs 0xa409dc/dd/de in order, sizes 12/32/256.
# SEQUENCE bytes: "\0\5\320\2\0\30\25\0\0\0\1\1" (1280×720 progressive, chroma=1).
# QUANTISATION first 4 bytes: 8, 16, 16, 19 (MPEG-2 default intra zigzag).
# PICTURE values vary frame-to-frame but structurally match Baseline C.
```

### Pass/fail decision

All five criteria PASS → Phase 7 closes green; proceed to Phase 8 (memory update + close iter1).

Any criterion FAIL → Phase 7 → Phase 4 loopback per `feedback_dev_process.md`. Pre-identified loopback triggers:

1. **`VIDIOC_S_EXT_CTRLS` returns EINVAL post-fix**. Likely causes: (a) `pic.reserved[5]` not zeroed (memset was forgotten), (b) struct sizes mismatch kernel expectation (e.g., padding differences), (c) flag bit value collisions. Mitigation: sanity-check via `pahole` or `sizeof`, compare to Baseline C's verbatim sizes (12 / 32 / 256).

2. **Pixel verify fails: HW frames hash-differ from SW**. Likely causes: (a) f_code packing wrong (`(picture->f_code >> N) & 0x0f` byte-order), (b) field-vs-frame structure misinterpretation on a non-progressive bitstream (BBB is progressive, so unlikely), (c) reference timestamps wrong (forward_ref_ts/backward_ref_ts off), (d) IQ matrix interpretation differs (custom matrices in fixture's sequence-extension that VAAPI passes in different order than expected). Mitigation: dump a single VAIQMatrixBufferMPEG2 payload via LIBVA_TRACE_BUFDATA, verify byte-for-byte against the bitstream's actual matrices (and against Baseline C's QUANTISATION first row for the default-matrices case).

3. **mpv `--hwdec=vaapi` filters MPEG-2 out same as `--hwdec=vaapi-copy`** — criterion 4 untestable as written. Mitigation: switch to `ffmpeg -hwaccel vaapi -vf hwdownload,format=nv12 -f rawvideo - | sha256sum` for both HW and SW; compare YUV bytes directly. Fall-forward only — don't loop the criterion definition.

4. **H.264 regression**. Bug 1 fix touches a switch case other H.264 cases use; if H.264 regresses, the patch was applied to the wrong location. Mitigation: re-read Baseline D vs scratch-Bug1 diff and locate the offending change.

5. **`include/mpeg2-ctrls.h` deletion breaks an unaudited consumer**. Mitigation: `git grep -l "mpeg2-ctrls"` after deletion to confirm zero remaining references; build catches anything missed.

## Out of scope (LOCKED for iter1)

- HEVC, VP9, VP8 work — separate iterations.
- vaDeriveImage cache-stale fix (T4 finding) — Phase 4 cross-cutting work; not iter1.
- chromium-fourier 149 install on fresnel.
- Performance metrics for MPEG-2.
- Long-duration MPEG-2 stress (>10s).
- Other MPEG-2 containers (PS, MP4, etc.).
- mpv `--hwdec=vaapi-copy` engaging MPEG-2 (separate follow-up; criterion #3 adjusted to ffmpeg-direct).
- The H.264 device-init unconditional EINVAL on hantro (`src/context.c:142-155`) — auxiliary noise, intentionally swallowed.
- Refining `sequence.profile_and_level_indication` (current 0; VAAPI doesn't expose) — informational field, kernel doesn't reject.
- Refining `sequence.chroma_format` from hardcoded 1 (4:2:0) to lifted from VAAPI — campaign codec scope is 4:2:0 only.
- Refining `progressive_sequence` derivation from `progressive_frame` proxy — works for BBB, sufficient for iter1 binding cell.
- Upstream Linux engagement (per `feedback_no_upstream.md`) — kernel-side works; nothing to file.

## What "iteration 1 close" looks like

Per `phase0_findings_iter1.md`:

- All 5 Phase 1 criteria green (with #3 wording adjusted by this Phase 4 plan).
- `phase8_iteration1_close.md` document summarizing the bug, contract, fix, binding-cell numbers.
- Second-codec passing on the campaign-level scoreboard (1/5 → 2/5).
- Memory entry per Phase 8 (lesson distilled).
- Debug-instrumentation sweep (any `printf` / `fprintf` from Phase 6 removed; no scratch state remaining).
- Phase 5 sonnet-architect review pass signed off.
- Commits all authored as `claude-noether` per memory `feedback_gitea_as_claude_noether.md`.

## Phase 5 entry point

Phase 5 (second-model review) inputs are this plan + the four Phase 3 baselines. Per `feedback_dev_process.md`:

> Goal, situation, measurements, plan get pasted into DokuWiki. Markus reviews and redacts, then initiates the handover to a fresh model instance. Claude does not curate the artifact going to the reviewer — that would re-introduce the blind-spot accumulation the review is meant to escape. Do not summarize when handing over; paste the actual artifacts.

Concretely: the artifacts to hand over are the four primary documents in this campaign repo (`phase0_findings_iter1.md`, `phase2_iter1_situation.md`, `phase3_iter1_baseline.md`, `phase4_iter1_plan.md`) plus the `phase0_evidence/2026-05-07/iter1_phase3/` raw output. No summary, no executive overview, no "the gist is" framing — Markus has the raw bundle, the reviewer reads it directly.
