Files
punktfunk/crates/pf-encode/src/enc/pyrowave_wire.rs
T
enricobuehler 00f759ec72 style(pf-encode): clear the clippy gate on the HDR/PyroWave additions
`cargo clippy --workspace --all-targets --locked -- -D warnings` was red on
main — three lints landed with the GNOME 50 HDR + PyroWave 4:4:4 work:

* pyrowave_wire.rs: `aw / 2 >> level` tripped clippy::precedence. Rust already
  binds `/` tighter than `>>`, so this always parsed as `(aw / 2) >> level`
  (subband dim at half res, then one halving per DWT level) — the parens are
  purely explicit, no change in behaviour.
* linux/mod.rs: `probe_can_encode_10bit` sat after `mod hdr_tests`
  (clippy::items_after_test_module) — moved above the test module, unchanged.

Lint-only; no functional change. fmt/clippy/test all green afterwards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 10:23:47 +02:00

269 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Shared PyroWave AU wire-framing (design/pyrowave-codec-plan.md §4.4) — the single source of
//! truth for the on-wire access-unit shape, used by BOTH the Linux (dmabuf/CSC) and Windows (NV12
//! zero-copy) host encoders. It turns pyrowave's packetized bitstream into either the **dense**
//! single-packet AU or the **datagram-aligned** windowed AU. Pure (no GPU/FFI) so it is unit-tested
//! on any platform and both encoders emit byte-identical framing — the clients parse this exact
//! layout, so it must stay in ONE place.
//!
//! Datagram-aligned AU: each `chunk`-sized window opens with a 4-byte prefix (`u16` used-length +
//! `u16` kind) and carries either WHOLE self-delimiting codec packets (`WIN_PACKED` — several small
//! ones share a window) or one fragment of an oversized ATOMIC packet (a `FRAG` chain — pyrowave's
//! 32×32 blocks are atomic and can exceed a shard). A lost shard zeroes its window (`used = 0`) so
//! the receiver skips it and drops any fragment chain it interrupts. Padding after `used` is zeroed.
/// The 4-byte per-window framing prefix (`u16` used-length + `u16` kind).
pub(crate) const WINDOW_PREFIX: usize = 4;
/// Window kinds: whole packets / an oversized packet's fragments.
const WIN_PACKED: u16 = 0;
const WIN_FRAG_FIRST: u16 = 1;
const WIN_FRAG_CONT: u16 = 2;
const WIN_FRAG_LAST: u16 = 3;
/// The packetize boundary to request from pyrowave: for a `wire_chunk` shard it is the shard payload
/// minus the 4-byte window prefix (so a whole codec packet + its prefix fits one shard); for the
/// dense case it is the whole-bitstream cap (one packet per AU).
pub(crate) fn packet_boundary(wire_chunk: Option<usize>, dense_cap: usize) -> usize {
wire_chunk.map(|c| c - WINDOW_PREFIX).unwrap_or(dense_cap)
}
/// Patch the frame's `BitstreamSequenceHeader` to signal `ycbcr_range = LIMITED`. pyrowave's C API
/// fills the header with `= {}` (all VUI fields zeroed) and offers NO way to set colour/range, so it
/// signals `ycbcr_range = 0 = YCBCR_RANGE_FULL` — but BOTH host CSCs (`rgb2yuv.comp` on Linux, the
/// D3D11 `BgraToYuvPlanes` on Windows) always emit BT.709 **LIMITED** YCbCr (black = Y16). A client
/// that honours the VUI (the Apple wavelet decoder reads `(word1 >> 30) & 1`) then skips the
/// limited→full expansion and shows washed-out, raised blacks. Patching the bit makes the bitstream
/// HONEST for every client — clients that hardcode limited (the Vulkan `video_pyrowave` path) are
/// unaffected, and pyrowave's own decode ignores the flag (it reconstructs raw YCbCr). The other
/// zeroed VUI fields (BT.709 primaries / transform / transfer) are already correct.
///
/// `seq_offset` is the byte offset of the frame's 8-byte `BitstreamSequenceHeader` in `bitstream` —
/// the SOF packet's offset. The colour bits live in the little-endian second word's top byte
/// (`seq_offset + 7`): `color_primaries` bit 27 (`0x08`), `transfer_function` bit 28 (`0x10`),
/// `ycbcr_transform` bit 29 (`0x20`), `ycbcr_range` bit 30 (`0x40`); `chroma_siting` bit 31 stays 0
/// (CENTER — the pyrowave CSCs use the centre-sited 2×2 box, unlike the left-cosited P010 path).
/// Range is ALWAYS stamped LIMITED (both CSCs emit studio range); `bt2020_pq` additionally stamps
/// BT.2020 primaries + PQ transfer + BT.2020 matrix — upstream's own enum semantics
/// (`pyrowave_common.hpp`), matching the session's negotiated `ColorInfo`.
pub(crate) fn stamp_color_bits(bitstream: &mut [u8], seq_offset: usize, bt2020_pq: bool) {
if let Some(b) = bitstream.get_mut(seq_offset + 7) {
*b |= 0x40;
if bt2020_pq {
*b |= 0x08 | 0x10 | 0x20;
}
}
}
/// The wavelet block space's total 32x32-block count for a mode — the exact counting walk of
/// upstream `WaveletBuffers::init_block_meta` (also ported to the Apple `WaveletLayout`, whose
/// golden tests pin it against real host AUs). Needed because the vendored RDO pass packs the
/// block index into 16 bits (`RDOperation.block_offset_saving` — see
/// `patches/0002-rdo-saving-clamp.patch`): a mode whose count exceeds `u16::MAX` would wrap
/// inside the rate controller, so the host guards such modes out (≈8K 4:4:4 territory).
pub(crate) fn block_count_32x32(width: u32, height: u32, chroma444: bool) -> u32 {
const LEVELS: u32 = 5;
let align = |v: u32| ((v + 31) & !31).max(128);
let (aw, ah) = (align(width), align(height));
let mut count = 0u32;
for level in (0..LEVELS).rev() {
let lw = (aw / 2) >> level;
let lh = (ah / 2) >> level;
let blocks_x8 = lw.div_ceil(8);
let blocks_y8 = lh.div_ceil(8);
let per_band = blocks_x8.div_ceil(4) * blocks_y8.div_ceil(4);
let bands = if level == LEVELS - 1 { 4 } else { 3 };
for component in 0..3u32 {
if level == 0 && component != 0 && !chroma444 {
continue;
}
count += per_band * bands;
}
}
count
}
/// Frame pyrowave's `packets` (each an `(offset, size)` into `bitstream`) into the wire AU.
/// `wire_chunk = None` copies the single dense packet; `Some(chunk)` produces the windowed
/// datagram-aligned AU (a whole number of `chunk`-sized windows).
pub(crate) fn build_au(
packets: &[(usize, usize)],
bitstream: &[u8],
wire_chunk: Option<usize>,
) -> Vec<u8> {
let Some(chunk) = wire_chunk else {
// Dense (default): boundary == whole buffer → the AU is exactly one pyrowave packet.
let (off, size) = packets[0];
return bitstream[off..off + size].to_vec();
};
let payload_max = chunk - WINDOW_PREFIX;
let mut au: Vec<u8> = Vec::with_capacity((packets.len() + 1) * chunk);
// The currently-open PACKED window: (start offset of its prefix, bytes used).
let mut open: Option<(usize, usize)> = None;
let close = |au: &mut Vec<u8>, open: &mut Option<(usize, usize)>, chunk: usize| {
if let Some((start, used)) = open.take() {
au[start..start + 2].copy_from_slice(&(used as u16).to_le_bytes());
au[start + 2..start + 4].copy_from_slice(&WIN_PACKED.to_le_bytes());
au.resize(start + chunk, 0);
}
};
for &(off, size) in packets {
let bytes = &bitstream[off..off + size];
if size <= payload_max {
let fits = open.is_some_and(|(_, used)| used + size <= payload_max);
if !fits {
close(&mut au, &mut open, chunk);
let start = au.len();
au.resize(start + WINDOW_PREFIX, 0);
open = Some((start, 0));
}
au.extend_from_slice(bytes);
if let Some((_, used)) = open.as_mut() {
*used += size;
}
} else {
// Oversized packet: its own FRAG chain of full windows.
close(&mut au, &mut open, chunk);
let mut o = 0usize;
while o < size {
let take = (size - o).min(payload_max);
let kind = if o == 0 {
WIN_FRAG_FIRST
} else if o + take == size {
WIN_FRAG_LAST
} else {
WIN_FRAG_CONT
};
let start = au.len();
au.resize(start + WINDOW_PREFIX, 0);
au[start..start + 2].copy_from_slice(&(take as u16).to_le_bytes());
au[start + 2..start + 4].copy_from_slice(&kind.to_le_bytes());
au.extend_from_slice(&bytes[o..o + take]);
au.resize(start + chunk, 0);
o += take;
}
}
}
close(&mut au, &mut open, chunk);
au
}
#[cfg(test)]
mod tests {
use super::*;
/// Walk a windowed AU back into the flat codec-packet stream (the client's parse), asserting the
/// framing invariants the encoder promises: whole windows, in-bounds `used`, zeroed padding.
fn walk(au: &[u8], chunk: usize) -> Vec<u8> {
assert_eq!(au.len() % chunk, 0, "AU is a whole number of windows");
let mut out = Vec::new();
let mut frag: Vec<u8> = Vec::new();
for win in au.chunks(chunk) {
let used = u16::from_le_bytes([win[0], win[1]]) as usize;
let kind = u16::from_le_bytes([win[2], win[3]]);
assert!(WINDOW_PREFIX + used <= win.len(), "window overrun");
assert!(
win[WINDOW_PREFIX + used..].iter().all(|&b| b == 0),
"non-zero padding after used"
);
let body = &win[WINDOW_PREFIX..WINDOW_PREFIX + used];
match kind {
0 => out.extend_from_slice(body),
1 => frag = body.to_vec(),
2 => frag.extend_from_slice(body),
3 => {
frag.extend_from_slice(body);
out.extend_from_slice(&frag);
frag.clear();
}
k => panic!("unknown window kind {k}"),
}
}
out
}
#[test]
fn dense_is_the_single_packet() {
let bs = (0u8..=200).collect::<Vec<u8>>();
let au = build_au(&[(10, 50)], &bs, None);
assert_eq!(au, bs[10..60]);
}
#[test]
fn packed_windows_pack_small_packets_and_reconstruct() {
// Three small packets that share windows; walking must reproduce them concatenated in order.
let bs: Vec<u8> = (0..255u32).map(|i| i as u8).collect();
let packets = [(0, 20), (20, 20), (40, 100)];
let chunk = 64; // payload_max = 60
let au = build_au(&packets, &bs, Some(chunk));
let flat = walk(&au, chunk);
let mut expect = Vec::new();
for &(o, s) in &packets {
expect.extend_from_slice(&bs[o..o + s]);
}
assert_eq!(flat, expect);
}
#[test]
fn oversized_packet_fragments_and_reassembles() {
// One atomic packet larger than a window → a FRAG chain the walk reassembles exactly.
let bs: Vec<u8> = (0..1000u32).map(|i| i as u8).collect();
let chunk = 64; // payload_max = 60
let au = build_au(&[(0, 500)], &bs, Some(chunk));
assert_eq!(walk(&au, chunk), bs[0..500]);
}
#[test]
fn boundary_reserves_the_window_prefix() {
assert_eq!(packet_boundary(Some(1408), 999_999), 1404);
assert_eq!(packet_boundary(None, 777), 777);
}
#[test]
fn block_count_matches_the_apple_layout_invariant() {
// 256x144 (the golden-fixture geometry, aligned 256x160): recompute via the same walk
// the validated Apple WaveletLayout uses and pin a few mode-level facts.
let manual = |w: u32, h: u32, c444: bool| {
let align = |v: u32| ((v + 31) & !31).max(128);
let (aw, ah) = (align(w), align(h));
let mut n = 0u32;
for level in (0..5u32).rev() {
let per = (((aw / 2) >> level).div_ceil(8).div_ceil(4))
* (((ah / 2) >> level).div_ceil(8).div_ceil(4));
let bands = if level == 4 { 4 } else { 3 };
for c in 0..3 {
if level == 0 && c != 0 && !c444 {
continue;
}
n += per * bands;
}
}
n
};
for (w, h) in [(256, 144), (1920, 1080), (3840, 2160), (7680, 4320)] {
assert_eq!(block_count_32x32(w, h, false), manual(w, h, false));
assert_eq!(block_count_32x32(w, h, true), manual(w, h, true));
}
// 4:4:4 fits comfortably at 4K; the 16-bit RDO block index wraps around 8K 4:4:4.
assert!(block_count_32x32(3840, 2160, true) <= u16::MAX as u32);
assert!(block_count_32x32(7680, 4320, true) > u16::MAX as u32);
assert!(block_count_32x32(7680, 4320, false) <= u16::MAX as u32);
}
#[test]
fn stamp_color_bits_sets_range_and_hdr_bits() {
let mut bs = vec![0u8; 16];
stamp_color_bits(&mut bs, 0, false);
// ycbcr_range = bit 30 of the LE second word = bit 6 of byte 7 (0x40); nothing else touched.
assert_eq!(bs[7], 0x40);
assert!(bs[..7].iter().all(|&b| b == 0));
assert!(bs[8..].iter().all(|&b| b == 0));
// Idempotent; an out-of-range offset is a silent no-op (never panics).
stamp_color_bits(&mut bs, 0, false);
assert_eq!(bs[7], 0x40);
stamp_color_bits(&mut bs, 100, false);
// HDR adds BT.2020 primaries (0x08) + PQ transfer (0x10) + BT.2020 matrix (0x20);
// chroma_siting (0x80) stays CENTER.
stamp_color_bits(&mut bs, 0, true);
assert_eq!(bs[7], 0x78);
}
}