forked from unom/punktfunk
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:
@@ -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");
|
||||
|
||||
@@ -98,7 +98,19 @@ pub struct PyroWaveEncoder {
|
||||
unsafe impl Send for PyroWaveEncoder {}
|
||||
|
||||
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 BgraToYuvPlanes variant lands
|
||||
// (design/pyrowave-444-hdr.md Phase 3). Threaded now so that flip is one-file.
|
||||
bail!("pyrowave 4:4:4 encode not implemented yet (Phase 3)");
|
||||
}
|
||||
if width % 2 != 0 || height % 2 != 0 {
|
||||
bail!("pyrowave 4:2:0 needs even dimensions (got {width}x{height})");
|
||||
}
|
||||
@@ -771,7 +783,8 @@ mod tests {
|
||||
context.Flush();
|
||||
|
||||
// Encode the shared textures through the real backend.
|
||||
let mut enc = PyroWaveEncoder::open(w, h, 60, 100_000_000).expect("PyroWaveEncoder::open");
|
||||
let mut enc = PyroWaveEncoder::open(w, h, 60, 100_000_000, crate::ChromaFormat::Yuv420)
|
||||
.expect("PyroWaveEncoder::open");
|
||||
let frame = CapturedFrame {
|
||||
width: w,
|
||||
height: h,
|
||||
|
||||
@@ -248,10 +248,11 @@ fn open_video_backend(
|
||||
if fps == 0 || fps > 1000 {
|
||||
anyhow::bail!("invalid refresh/fps {fps}: must be 1..=1000 Hz");
|
||||
}
|
||||
// 4:4:4 is HEVC-only. The negotiator should never pass `Yuv444` for another codec (it gates on
|
||||
// `codec == H265`), but defend the contract here so a future caller can't silently emit a stream
|
||||
// no decoder expects: a non-HEVC 4:4:4 request degrades to 4:2:0 with a warning.
|
||||
let chroma = if chroma.is_444() && codec != Codec::H265 {
|
||||
// 4:4:4 is HEVC- and PyroWave-only. The negotiator should never pass `Yuv444` for another
|
||||
// codec (it gates on the codec + `can_encode_444`), but defend the contract here so a future
|
||||
// caller can't silently emit a stream no decoder expects: an unsupported 4:4:4 request
|
||||
// degrades to 4:2:0 with a warning.
|
||||
let chroma = if chroma.is_444() && codec != Codec::H265 && codec != Codec::PyroWave {
|
||||
tracing::warn!(
|
||||
?codec,
|
||||
"4:4:4 requested for a non-HEVC codec — encoding 4:2:0"
|
||||
@@ -267,7 +268,7 @@ fn open_video_backend(
|
||||
if codec == Codec::PyroWave {
|
||||
#[cfg(feature = "pyrowave")]
|
||||
{
|
||||
return pyrowave::PyroWaveEncoder::open(width, height, fps, bitrate_bps)
|
||||
return pyrowave::PyroWaveEncoder::open(width, height, fps, bitrate_bps, chroma)
|
||||
.map(|e| (Box::new(e) as Box<dyn Encoder>, "pyrowave"));
|
||||
}
|
||||
#[cfg(not(feature = "pyrowave"))]
|
||||
@@ -369,8 +370,17 @@ fn open_video_backend(
|
||||
that ALSO preferred CODEC_PYROWAVE can display it (lab override; \
|
||||
normal sessions negotiate it instead)"
|
||||
);
|
||||
pyrowave::PyroWaveEncoder::open(width, height, fps, bitrate_bps)
|
||||
.map(|e| (Box::new(e) as Box<dyn Encoder>, "pyrowave"))
|
||||
// The lab override forces the wavelet stream onto a session negotiated for
|
||||
// another codec — that session's chroma may be HEVC-4:4:4, which the
|
||||
// pyrowave encoder doesn't do yet, so pin the override to 4:2:0.
|
||||
pyrowave::PyroWaveEncoder::open(
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
bitrate_bps,
|
||||
ChromaFormat::Yuv420,
|
||||
)
|
||||
.map(|e| (Box::new(e) as Box<dyn Encoder>, "pyrowave"))
|
||||
}
|
||||
#[cfg(not(feature = "pyrowave"))]
|
||||
{
|
||||
@@ -418,8 +428,8 @@ fn open_video_backend(
|
||||
if codec == Codec::PyroWave {
|
||||
#[cfg(feature = "pyrowave")]
|
||||
{
|
||||
let _ = (format, cuda, bit_depth, chroma);
|
||||
return pyrowave::PyroWaveEncoder::open(width, height, fps, bitrate_bps)
|
||||
let _ = (format, cuda, bit_depth);
|
||||
return pyrowave::PyroWaveEncoder::open(width, height, fps, bitrate_bps, chroma)
|
||||
.map(|e| (Box::new(e) as Box<dyn Encoder>, "pyrowave"));
|
||||
}
|
||||
#[cfg(not(feature = "pyrowave"))]
|
||||
@@ -859,6 +869,13 @@ pub fn vaapi_codec_support() -> CodecSupport {
|
||||
pub fn can_encode_444(codec: Codec) -> bool {
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
if codec == Codec::PyroWave {
|
||||
// PyroWave does its own RGB→YCbCr CSC (capture always hands it a full-chroma source),
|
||||
// so 4:4:4 needs no GPU encode probe — only the full-res-chroma CSC variant, which
|
||||
// hasn't landed yet (design/pyrowave-444-hdr.md: Phase 2 Linux, Phase 3 Windows).
|
||||
// Flip per-OS when it does.
|
||||
return false;
|
||||
}
|
||||
if codec != Codec::H265 {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user