# HDR pipeline — investigation & implementation plan

> **Status:** Steps 0–3 SHIPPED — protocol/ABI/host in `3526517`, client apply + display
> capability-gate in `551012b`. Windows HDR live-validated; Apple/Android CI-compiled (on-glass
> pending). Step 4 (Linux) is OPEN, blocked upstream on capture. This doc is trimmed to design
> rationale + open items; the shipped code is the source of truth. The original audit (full gap
> list, per-file line refs, blocker walkthroughs) is in git history before this trim.

Goal: **true, correct HDR glass-to-glass** for punktfunk, across the host (Windows today; Linux
blocked upstream) and every client (Windows / Apple / Android / Linux). The plan was produced from a
full read of every HDR-touching subsystem cross-checked against the HDR10 standards (CICP/H.273 VUI,
SMPTE ST.2086 mastering, CEA-861.3 MaxCLL/MaxFALL) and the Sunshine/Apollo/Moonlight reference.

The original diagnosis: pixel math + the HEVC VUI we emitted were already correct (self-test
validated, matches Apollo), but nothing **measured, signalled, transported, or applied the static HDR
metadata** (mastering display colour volume + content light level). The fix was a metadata chain,
protocol-first.

## What shipped (Steps 0–3)

- **Step 0 — protocol + ABI carry colour end to end (`3526517`).** `ColorInfo` (4 CICP bytes on
  `Welcome`) + `HdrMeta` (`0xCE` datagram, bounds-checked); `NativeClient` `color`/`bit_depth` fields
  + an `HdrMeta` receiver/demux + `next_hdr_meta`. C ABI: `punktfunk_connect_ex5(... video_caps)`,
  `next_hdr_meta`, `color_info`, and **fixed `abi.rs:896` `video_caps = 0`** — the one-line root cause
  that had made Apple's complete (and correct) HDR pipeline dead code. Header regenerated. No
  rendering changes, CI-testable (round-trip + truncation + SDR back-compat).
- **Step 1 — host in-band SEI + complete VUI (`3526517`, live-validated on the RTX box).**
  Cross-platform byte logic in unit-tested `src/hdr.rs`: `hdr_meta_from_display`,
  `hevc_mastering_display_sei` SEI **type 137**, `hevc_content_light_level_sei` SEI **type 144**
  (note: NOT "type 4" — that was a drafting error). Windows `dxgi.rs`/`wgc.rs` read
  `IDXGIOutput6::GetDesc1` at capture init / output change → `HdrMeta` (MaxCLL/MaxFALL left 0, like
  Apollo); `nvenc.rs` attaches mastering + CLL SEI on every IDR for HEVC/H.264 and sends the real
  `0xCE` re-sent each keyframe. In-band SEI is read directly by decoders, so this fixed correctness
  before clients consumed the protocol and gave an Apollo on-glass parity gate. *Follow-ups:* AV1
  mastering rides METADATA OBUs (`HDR_MDCV`/`HDR_CLL`), not SEI; the Windows secure-desktop relay
  still sends only the generic baseline `0xCE` (the helper's in-band SEI carries the real grade).
- **Step 2 — clients apply the metadata (`551012b`; Apple/Android CI-compiled).** Each client drains
  `next_hdr_meta`/`nextHdrMeta` and remaps from the wire form (ST.2086 **G,B,R** order, mastering
  luminance in 0.0001 cd/m²) to the platform layout: **Windows** `SetHDRMetaData`
  (`hdr_meta_to_dxgi`: G,B,R→R,G,B reorder, 0.0001-nit→nit), dropping the 1000/1000/400 hardcode;
  **Apple** `CVBufferSetAttachment` of `kCVImageBufferMasteringDisplayColorVolumeKey` (24-byte BE) +
  `kCVImageBufferContentLightLevelInfoKey` (4-byte BE) per HDR pixel buffer — the correct path for the
  `itur_2100_PQ` layer (`CAEDRMetadata` on a PQ layer is ambiguous, deliberately avoided); **Android**
  `MediaFormat` `KEY_HDR_STATIC_INFO`, a 25-byte CTA-861.3 Type-1 blob (LE, **R,G,B** order, max-lum
  in **nits-u16**). Apple's connect also flips to `connect_ex5` advertising `videoCap10Bit|videoCapHDR`
  — the fix that resurrects Apple's previously-dead pipeline.
- **Step 3 — display-capability gate (`551012b`).** **Chosen approach: capability-gate, not an
  in-shader BT.2390 tone-map.** Rationale: with Steps 1–2 the host sends *correct* mastering metadata
  so an HDR display self-tone-maps; the remaining gap is SDR displays, best fixed by **not advertising
  HDR you can't present** — the host then sends a proper BT.709 SDR stream instead of PQ the panel
  mis-tone-maps (washed-out/dark). No guessed curve, deterministic. **Windows**
  `display_supports_hdr` (any `IDXGIOutput6` colour space == `G2084`); **Apple**
  `NSScreen.maximumExtendedDynamicRangeColorComponentValue > 1` (macOS) /
  `UIScreen.main.potentialEDRHeadroom > 1` (iOS); **Android** `Display.getHdrCapabilities`
  HDR10/HDR10+. Each ANDs with the user's HDR setting before advertising caps and logs when it drops
  to SDR.

### Wire format — design decisions worth keeping

Two layers, both back-compat-safe via the established trailing-bytes / new-datagram-tag patterns.

- **(A) Per-session colorimetry** — 4 trailing bytes on `Welcome` (offsets 60..64):
  `colour_primaries` (1=BT.709, 9=BT.2020) · `transfer_characteristics` (1=BT.709, 16=PQ/SMPTE2084,
  18=HLG) · `matrix_coeffs` (1=BT.709, 9=BT.2020-NCL — **never emit 10 (CL): no client decodes it**) ·
  `video_full_range_flag`. Decoded with `b.get(60).unwrap_or(1)` so an older host that omits them →
  BT.709 limited SDR (today's behaviour). A future mirror on `Reconfigured` announces a mid-stream
  SDR↔HDR / BT.709↔BT.2020 flip (deferred; today a mode switch never changes colour, and `0xCE`
  re-send covers mastering changes).
- **(B) Per-change mastering + CLL** — host→client datagram tag **`0xCE`**, 28 bytes, standard SEI
  fixed-point (display primaries G,B,R + white point in 1/50000 units; max/min mastering luminance in
  0.0001 cd/m²; MaxCLL/MaxFALL in nits). ST.2086 is variable, so it rides a datagram rather than the
  Welcome. **Re-sent on every IDR/RFI keyframe** so a client that dropped the best-effort datagram
  converges within a GOP; until first receipt the client uses the Welcome transfer + a documented
  generic default. **Bounds-check length before reading** (reassembler-bounds security invariant —
  truncation test required). **Omitted entirely for HLG.** Units map straight to DXGI
  `DXGI_HDR_METADATA_HDR10`, Android `KEY_HDR_STATIC_INFO`, Apple `CAEDRMetadata.hdr10`; the
  libavcodec/Linux side needs conversion — `AVMasteringDisplayMetadata` stores `AVRational`, not raw
  fixed-point.

## Out of scope (accepted — call out, don't build)

- **Dynamic metadata:** HDR10+ (ST.2094-40) and Dolby Vision RPU. We handle *static* ST.2086 only,
  with mid-stream changes carried by re-sending the static block.
- **HLG:** the transfer enum carries `18` from day one (free), but the `0xCE` mastering datagram is
  omitted for HLG (scene-referred, no mastering metadata).

## Step 4 — Linux (last; capture blocked upstream) — OPEN

- **(4a) 8-bit→Main10 NVENC upconvert shim** (`encode/linux.rs`) — Main10 transport with correct
  VUI/SEI without HDR capture (gated so we don't claim HDR transfer on SDR content).
- **Linux encode colour + side-data (the deferred Step 1c):** set
  `color_primaries/trc/colorspace/range` from the negotiated `ColorInfo` and attach
  `AV_FRAME_DATA_MASTERING_DISPLAY_METADATA` / `CONTENT_LIGHT_LEVEL` side-data (with the `AVRational`
  conversion) in `encode/linux.rs` + `vaapi.rs` — only once the encoder actually produces 10-bit, so
  the signalling matches the bits. (Linux capture is 8-bit only, so signalling BT.2020 PQ + attaching
  mastering side-data on a downconverted 8-bit stream would be *incorrect* — hence deferred out of
  Step 1.)
- **(4b) True 10-bit capture:** offer `ABGR2101010`/`P010` PipeWire formats + read colorimetry; pilot
  on Sway/wlroots; **blocked on gamescope #2126** (portals don't wire PipeWire 1.6 BT.2020/PQ).
  **Don't block the rest of the plan on it.**
- **(4c) Linux client:** `ex5` caps, P010 decode, GdkDmabufTexture CICP from Welcome,
  `wp_color_management` when GTK ≥ 4.14. (Also a standalone SDR bug: software path applies BT.601 to
  BT.709 — needs a BT.601→BT.709 sws + texture `color_state`.)

## Deferred validation (need on-glass / the RTX box)

- The mid-session `Reconfigure` "downgrade to SDR" for a monitor move HDR↔SDR.
- Confirm the **host produces SDR for an SDR client even off an HDR desktop** — on the native path the
  per-session SudoVDA follows the negotiated depth (SDR client → SDR virtual display → SDR stream), so
  it should hold end to end; verify the stale-HDR-SudoVDA edge case.

## Open questions

- **MaxCLL source:** `GetDesc1` doesn't expose it (Apollo zeroes). Static default, or measure
  per-frame peak in the PQ shader (only truly-correct, adds a readback)?
- **GameStream:** implement `SS_HDR_METADATA` (Moonlight `SS_HDR_METADATA` blob on the ENet control
  channel) for parity, or keep it deliberately SDR and steer HDR users to punktfunk/1?
- **HLG:** carry the enum from day one (free) — but do any sources actually produce HLG?
- **Linux:** is shipping the 8-bit→Main10 shim as "HDR-capable transport" acceptable, or does it risk
  advertising HDR we can't truly deliver?

## Ordering rationale

Step 0 first: it's the keystone (metadata transport is the dominant cross-cutter; the ABI
`video_caps = 0` line is a one-line root cause) and needs no hardware. Step 1 next: in-band SEI is
read directly by decoders, so it fixes correctness even before our clients consume the protocol, and
gives an Apollo-parity on-glass gate. Steps 2–3 are mechanical per-client wiring once metadata flows.
Linux is last because capture is gated on upstream we don't control; the shim delivers Main10
transport without that dependency.

Hardware dependencies: Step 0 = none (CI); Step 1 = RTX Windows host; Steps 2–3 = a real HDR display
per platform; Step 4 = a Linux GPU box + HDR-capable Wayland compositor.
