--- cycle: 3 phase: 2 status: closed 2026-05-18 date_opened: 2026-05-18 parent: k3_mc_phase1.md --- # Cycle 3, Phase 2 — MC situation analysis ## 1. C reference - **Source**: `external/ffmpeg-snapshot/libavcodec/vp9dsp_template.c` (already vendored from cycle 1). - **Function**: `put_8tap_regular_8h_c` generated by `filter_fn_1d(8, h, mx, regular, FILTER_8TAP_REGULAR, put)` — expands to call `do_8tap_1d_c` with `ds=1` (horizontal) and the REGULAR filter bank. - **Underlying primitive**: `do_8tap_1d_c` iterates `h` rows; per row, iterates `w=8` columns; per column, computes the `FILTER_8TAP` macro: `clip((sum_{k=0..7} F[k] * src[x+k-3] + 64) >> 7, 0, 255)`. - **Spec**: VP9 specification § 8.5.1 (subpel motion compensation). ## 2. NEON reference - **Source**: `external/ffmpeg-snapshot/libavcodec/aarch64/vp9mc_neon.S` (vendored 2026-05-18, FFmpeg n7.1.3, SHA-256 `6b1d50f9821742584fdd47758057f810644aff3a008faaa774ff5b9cac4d1fef`). - **Symbol**: `ff_vp9_put_regular8_h_neon` (note: filter type baked into name, width=8 baked in, h-direction baked in) - **Signature** (VP9 `vp9_mc_func` typedef): ```c void ff_vp9_put_regular8_h_neon(uint8_t *dst, ptrdiff_t dst_stride, const uint8_t *src, ptrdiff_t src_stride, int h, int mx, int my); ``` Registers: `x0=dst, x1=dst_stride, x2=src, x3=src_stride, w4=h, w5=mx, w6=my`. - **Dependencies**: - `libavutil/aarch64/asm.S` ✓ (already vendored) - `ff_vp9_subpel_filters[3][16][8]` symbol — provided by `external/ffmpeg-snapshot/libavcodec/vp9_subpel_filters_table.c` (hand-extracted from `libavcodec/vp9dsp.c` of the same n7.1.3 pin; copying just the constant data avoids dragging in the rest of `vp9dsp.c` which would require linking the entire VP9 decoder). ## 3. Workload model Per 8×8 block output: - 8 multiplies × 8 columns × 8 rows = **512 multiplies** - 7 additions × 8 columns × 8 rows = 448 additions - 1 round (+64), 1 shift (>>7), 1 clip per pixel × 64 = 192 ops - Total ~1150 integer ops per block Per-block memory (horizontal-only filter, 8-pixel-wide output): - Read: 8 rows × (8 output cols + 7 tap overhang) = 8 × 15 = **120 source bytes** - Write: 8 rows × 8 cols = **64 dst bytes** - Total: **~184 bytes / block** Per 1080p frame (32 400 8×8 blocks, worst case all-MC): - ~5.9 MB total memory traffic - ~37 Mops compute - At GPU 4 GB/s share: 1.48 ms / frame = 675 FPS = 21.9 Mblock/s - At V3D 92 GFLOPS theoretical scalar (SMUL24 throughput ≈ FP MUL): 0.4 ms compute / frame = 2500 FPS theoretical → **compute is NOT the bottleneck** at this shape So MC is **bandwidth-bound on the QPU**, similar to LPF cycle 2. ## 4. Per-row workload diversity (vs cycle 1+2) | | IDCT (k1) | LPF (k2) | MC (k3) | |---|---|---|---| | Per-block math | Heavy butterflies (~60 ops/block via separable transform) | Light: 0-30 ops per edge × 8 rows | 8-tap convolution: 1150 ops per block | | Per-block memory | ~320 B in + 64 B out | ~64 B in + ~24 B out per edge | 120 B in + 64 B out | | Compute / memory ratio | High | Low (memory-bound, lots of skipping) | Medium (compute-rich but bandwidth-bound at GPU) | | Conditional? | No (always-execute) | Yes (fm/hev divergence per row) | No (deterministic per pixel) | | QPU mult intensity | Q14 16b×16b mults | Light (compares, small clips) | 16b×8b mults (filter × pixel) | MC is interesting because it's **compute-rich AND bandwidth-bound** — the closest match in workload shape to a real-world GPU compute kernel the V3D was designed for (graphics filtering). ## 5. Constraints carried from cycle 1+2 Same V3D 7.1 device profile (vulkaninfo unchanged). The relevant specifics for MC: - No DP4A → 8-tap convolution must be 8 separate SMUL24 + ADDs (the typical GPU "dot4" packing is not available) - shaderInt16 = false → filter coefficients widened to int32 in registers; the filter table itself can be a uint16-storage SSBO - shaderInt8 = false → source pixels widened to int32 in registers - 1024-byte (16 KiB / 16) shared mem per WG is ample for MC source staging if useful (15 cols × 8 rows × 1 byte per block-row × 32 blocks per WG = 3 840 B per row); for v1 we skip shared-mem staging and let TMU handle reads directly ## 6. What Phase 2 does *not* close - Per-block (block_y, block_x) layout / meta format. Phase 4 picks. Likely same shape as cycle 2 (uvec4 per block: dst_offset, src_offset, mx, _pad). - Filter table residency: as SSBO load every row, push-constants per dispatch (different mx per dispatch), or constant baked into shader (one filter per shader = 16 specialised shaders for the 16 mx phases). Phase 4 picks; v1 likely SSBO for simplicity. - Vertical / "hv" / "avg" / 4-pixel / 16-pixel / 32-pixel / 64-pixel variants — out of cycle 3 scope; cycle 4+ if needed. Phase 3 next: build `tests/bench_neon_mc.c`, capture M3'''.