feat(pyrowave): negotiation plumbing for 4:4:4 + HDR — thread chroma/depth/ColorInfo end to end

Phase 1 of design/pyrowave-444-hdr.md. No behavior change yet: the handshake's
4:4:4 gate now admits PyroWave (probe = can_encode_444(codec), capture gate
inherently satisfied — the wavelet path always ingests an RGB source and does
its own CSC), but can_encode_444 stays false for PyroWave until the per-OS
full-res-chroma CSC variants land (Phase 2 Linux, Phase 3 Windows), so every
session still resolves 4:2:0/8-bit.

- Both host encoders take the negotiated ChromaFormat (bail on 444 for now);
  the PUNKTFUNK_ENCODER=pyrowave lab override pins 4:2:0.
- Bitrate: the automatic ~1.6 bpp pin resolves AFTER depth+chroma and scales
  x1.625 for 4:4:4 / x1.15 for 10-bit (factors from the Phase-0 fixture
  matrix); the mid-stream mode-switch re-resolve threads the session's values.
- Client: PyroWaveDecoder builds its plane ring (full-res chroma when 444) and
  creates the upstream decoder from the negotiated chroma, keeps chroma fixed
  across mid-stream resizes, drops the even-dims requirement for 444, and
  returns the negotiated Welcome ColorInfo as the frame colour contract
  instead of hardcoded BT.709 (the wavelet bitstream has no VUI).

Verified on .21 (RTX 5070 Ti): clippy -D warnings (host+client+encode), host
186 tests, client + pf-encode tests, fmt, and the pyrowave_smoke GPU
round-trip through the patched vendored lib (97cf15e3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 12:37:12 +02:00
co-authored by Claude Fable 5
parent 97cf15e3b7
commit 5eb930e71d
9 changed files with 202 additions and 57 deletions
+17 -3
View File
@@ -211,7 +211,19 @@ fn budget_for(bitrate_bps: u64, fps: u32) -> usize {
}
impl PyroWaveEncoder {
pub fn open(width: u32, height: u32, fps: u32, bitrate_bps: u64) -> Result<Self> {
pub fn open(
width: u32,
height: u32,
fps: u32,
bitrate_bps: u64,
chroma: crate::ChromaFormat,
) -> Result<Self> {
if chroma.is_444() {
// Negotiation can't reach here yet: `can_encode_444` returns false for PyroWave
// until the full-res-chroma rgb2yuv variant lands (design/pyrowave-444-hdr.md
// Phase 2). The parameter is threaded now so that flip is one-file.
bail!("pyrowave 4:4:4 encode not implemented yet (Phase 2)");
}
if width % 2 != 0 || height % 2 != 0 {
bail!("pyrowave 4:2:0 needs even dimensions (got {width}x{height})");
}
@@ -1335,7 +1347,8 @@ mod tests {
#[ignore = "needs a real Vulkan 1.3 compute device (run on a GPU host, not the build box)"]
fn pyrowave_smoke() {
let (w, h) = (256u32, 256u32);
let mut enc = PyroWaveEncoder::open(w, h, 60, 40_000_000).expect("open");
let mut enc =
PyroWaveEncoder::open(w, h, 60, 40_000_000, crate::ChromaFormat::Yuv420).expect("open");
assert!(!enc.caps().supports_rfi);
let colors = [
@@ -1499,7 +1512,8 @@ mod tests {
// Odd-block geometry on purpose: 256 aligns clean, 144 → aligned 160 exercises the
// block-grid overhang. ~1.6 bpp at 60 fps.
let (w, h) = (256u32, 144u32);
let mut enc = PyroWaveEncoder::open(w, h, 60, 4_000_000).expect("open");
let mut enc =
PyroWaveEncoder::open(w, h, 60, 4_000_000, crate::ChromaFormat::Yuv420).expect("open");
let dump = |name: &str, bytes: &[u8]| {
std::fs::write(dir.join(name), bytes).expect("write fixture");