Skip to content

fix(capture): AU-alignment split — aggregate NALs into one buffer per access unit - #23

Merged
andrescera merged 1 commit into
mainfrom
todo15/au-alignment-wip
Jul 30, 2026
Merged

fix(capture): AU-alignment split — aggregate NALs into one buffer per access unit#23
andrescera merged 1 commit into
mainfrom
todo15/au-alignment-wip

Conversation

@andrescera

Copy link
Copy Markdown
Member

What

Fixes the alignment=au output-buffer contract: the element advertises
alignment=(string)au on both pad templates but pushed one GstBuffer per NAL
unit, so a multi-slice picture (common at 2160p) fragmented across multiple
buffers that each claimed to be a whole access unit.

frame_callback() now partitions each libuvc delivery into access units via a
new split_access_units() and emits exactly one buffer per AU. Boundary
detection, in priority order: an Access Unit Delimiter when present (exact, no
heuristic), else first-slice-of-new-picture detection (a one-bit test on the
slice payload, covering both H.264 and H.265), else never split.

Why

Downstream (h264parse, v4l2slh264dec/mppvideodec, the muxers) reads the
alignment=au caps claim to find frame boundaries. A device that emits
multi-slice pictures — the realistic case at 2160p — was violating that
contract silently; nothing failed loudly, but the output was mis-framed. Caps
are untouched (still alignment=au — the contract is correct, the emitter was
not); compat_caps_contract passes unmodified.

How to verify

Local: characterization-first — au_single_slice_characterization was green
on the unmodified tree before any behavior change; both new multi-slice cases
(au_multi_slice_aud, au_aud_less_fallback) were red first (3 buffers / 2
buffers where 1 was expected), green after the fix. 103/103 baseline ctest,
106/106 after (103 + 3 new), 161/161 under -DENABLE_SANITIZERS=ON with
zero ASan/TSan findings.

Board hardware evidence is frozen and cited in the commit trailer
(Board-Evidence-SHA256):
.omo/evidence/device-quality-wave3/task-15-board-proof.md in the root
workspace repo — real Osmo Pocket 3 capture at both 1080p30 (byte-identical to
the pre-aggregation path through h264parse) and 2160p30 (full SPS-derived
caps: 3840x2160/high/5.2/4:2:0), with an exact 90-in/90-out buffer count
verified via identity probes on both sides of h264parse at 2160p30 — the
AU-alignment contract holds with zero drift at the resolution most likely to
expose a splitting bug.

Risks

  • The plan's acceptance criteria for this todo also ask for a latency-tracer
    comparison against a recorded baseline (issues.md:2178-2183). That
    measurement was not independently re-taken on this board session — the
    freeze file says so explicitly rather than fabricating a number. The
    pre-existing design reasoning (an AU is emitted exactly when the last NAL of
    its delivery used to be pushed, so aggregation adds no additional wait) is
    recorded but is design reasoning, not a board measurement.
  • GST_BUFFER_OFFSET is now an access-unit counter rather than a per-NAL
    counter. It was already documented as a frame counter, so this makes the
    documentation and behavior agree; unchanged for the single-slice case.

… access unit

The element advertises `alignment=au` on both pad templates but pushed one
GstBuffer per NAL unit, so any multi-slice picture (common at 2160p) fragmented
across multiple buffers that each claimed to be a whole access unit. Downstream
(`h264parse`, `v4l2slh264dec`/`mppvideodec`, the muxers) trusts that claim to
find frame boundaries.

Fix: `frame_callback()` now partitions each libuvc delivery into access units
via `split_access_units()` and emits exactly one buffer per AU. Boundary
detection, in priority order: an Access Unit Delimiter when present (exact,
no heuristic), else first-slice-of-new-picture detection via a one-bit test
on the slice payload (H.264 `first_mb_in_slice`/H.265
`first_slice_segment_in_pic_flag`), else never split (deliberately biased
toward under-splitting). PTS convention: an aggregated AU carries the
delivery's arrival running-time, identical to what its first slice would have
received under the old per-NAL path. No caps change, no new timing constant.

Board-proven at both resolutions on the Osmo Pocket 3 (192.168.78.131,
2026-07-30 ~08:26-08:43Z): 1080p30 parses cleanly through `h264parse`,
byte-identical to the pre-aggregation path; 2160p30 parses cleanly with full
SPS-derived caps (3840x2160/high/5.2/4:2:0); exact 90-in/90-out buffer count
verified via identity probes on both sides of `h264parse` for a 90-buffer
2160p30 request, confirming the contract holds with zero drift at the
resolution most likely to expose a splitting bug.

Local gate: 103/103 baseline ctest, both new AU-alignment cases red before the
fix (3 buffers / 2 buffers where 1 was expected), 106/106 after (103 + 3 new),
161/161 under `-DENABLE_SANITIZERS=ON` with zero ASan/TSan findings.
`compat_caps_contract` unmodified; `alignment=au` unchanged.

Board-Evidence-SHA256: 4585c999f72a419edd079cf5b10e1af86105d0eee6cfbfbf76b7ab43e8683800
@andrescera
andrescera merged commit ad631a2 into main Jul 30, 2026
5 checks passed
@andrescera
andrescera deleted the todo15/au-alignment-wip branch July 30, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant