feat(client,host): PyroWave Apple Metal decoder + per-mode bitrate pin
- clients/apple: native Metal wavelet decoder + compute shaders (Phase 5), decoding PyroWave without embedding MoltenVK. - pf-client-core: plumb user_flags/completeness through Decoder::decode_frame so the PyroWave backend parses chunk-aligned + partial AUs; gate the param's unused-warning to exactly the non-pyrowave builds (fixes -D warnings on the featureless Linux client build). - punktfunk-host: on a mid-stream mode switch, re-resolve the "Automatic" PyroWave bitrate for the new mode's ~1.6 bpp operating point (explicit rates and H.26x ABR stay put); reject sub-128px PyroWave modes before the encoder rebuild instead of after the ack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -428,6 +428,15 @@ pub fn validate_dimensions(codec: Codec, width: u32, height: u32) -> Result<()>
|
||||
if width % 2 != 0 || height % 2 != 0 {
|
||||
anyhow::bail!("invalid encode resolution {width}x{height}: dimensions must be even");
|
||||
}
|
||||
// PyroWave's 5-level wavelet decomposition needs ≥ 4·2⁵ px per axis (upstream
|
||||
// `MinimumImageSize` — the band mirroring breaks below it); reject a tiny mode here
|
||||
// (e.g. a match-window resize dragged to a sliver) instead of failing the encoder
|
||||
// rebuild after the switch was acked.
|
||||
if codec == Codec::PyroWave && (width < 128 || height < 128) {
|
||||
anyhow::bail!(
|
||||
"invalid PyroWave resolution {width}x{height}: the wavelet needs at least 128px per axis"
|
||||
);
|
||||
}
|
||||
let max = codec.max_dimension();
|
||||
if width > max || height > max {
|
||||
anyhow::bail!(
|
||||
|
||||
@@ -1172,7 +1172,7 @@ mod tests {
|
||||
buf.row_stride_in_bytes = [w as usize, (w / 2) as usize, (w / 2) as usize];
|
||||
buf.plane_size_in_bytes = [y.len(), cb.len(), cr.len()];
|
||||
assert_eq!(
|
||||
pw::pyrowave_decoder_decode_cpu_buffer_synchronous(dec, &mut buf),
|
||||
pw::pyrowave_decoder_decode_cpu_buffer_synchronous(dec, &buf),
|
||||
pw::pyrowave_result_PYROWAVE_SUCCESS
|
||||
);
|
||||
pw::pyrowave_decoder_destroy(dec);
|
||||
|
||||
@@ -1606,6 +1606,9 @@ async fn serve_session(
|
||||
}
|
||||
});
|
||||
let bitrate_kbps = welcome.bitrate_kbps; // resolved encoder bitrate (Hello clamped, or default)
|
||||
// "Automatic" request: the resolved rate is a host default — for PyroWave a per-mode
|
||||
// bpp pin the data plane re-resolves on a mid-stream mode switch.
|
||||
let bitrate_auto = hello.bitrate_kbps == 0;
|
||||
let bit_depth = welcome.bit_depth; // resolved encode bit depth (8, or 10 when negotiated)
|
||||
// Resolved chroma — derive the typed value back from the wire byte the Welcome carried (so the
|
||||
// session uses exactly what the client was told). `Yuv444` only when the handshake gate passed.
|
||||
@@ -1703,6 +1706,7 @@ async fn serve_session(
|
||||
bitrate_rx,
|
||||
compositor,
|
||||
bitrate_kbps,
|
||||
bitrate_auto,
|
||||
bit_depth,
|
||||
chroma,
|
||||
codec,
|
||||
@@ -3948,6 +3952,11 @@ struct SessionContext {
|
||||
compositor: crate::vdisplay::Compositor,
|
||||
/// Negotiated encoder bitrate (kbps).
|
||||
bitrate_kbps: u32,
|
||||
/// The client asked for "Automatic" (`Hello::bitrate_kbps == 0`), so `bitrate_kbps` came from
|
||||
/// the host's codec-aware default. For PyroWave that default is the ~1.6 bpp operating point of
|
||||
/// the NEGOTIATED MODE (`resolve_bitrate_kbps_for`) — a mid-stream mode switch re-resolves it
|
||||
/// for the new mode (the pin follows the resolution; an explicit client rate stays put).
|
||||
bitrate_auto: bool,
|
||||
/// Negotiated encode bit depth (8, or 10 = HEVC Main10).
|
||||
bit_depth: u8,
|
||||
/// Negotiated chroma subsampling (4:2:0, or 4:4:4 when the client + host + GPU all support it).
|
||||
@@ -4027,6 +4036,7 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
|
||||
bitrate_rx,
|
||||
compositor,
|
||||
mut bitrate_kbps,
|
||||
bitrate_auto,
|
||||
bit_depth,
|
||||
// The resolved chroma is already captured in `plan` (above); ignore the duplicate here.
|
||||
chroma: _,
|
||||
@@ -4363,11 +4373,29 @@ fn virtual_stream(ctx: SessionContext) -> Result<()> {
|
||||
}
|
||||
if let Some(new_mode) = want {
|
||||
tracing::info!(?new_mode, "rebuilding pipeline for mode switch");
|
||||
// PyroWave's Automatic bitrate is a per-mode ~1.6 bpp pin (resolve_bitrate_kbps_for) —
|
||||
// a resolution change moves the operating point (1080p→4K quadruples the pixel rate),
|
||||
// so re-resolve it for the new mode. Explicit client rates stay put (the operator knows
|
||||
// the link), and the H.26x codecs keep their mode-independent rate (ABR owns it).
|
||||
let mode_bitrate = if bitrate_auto && plan.codec == crate::encode::Codec::PyroWave {
|
||||
resolve_bitrate_kbps_for(plan.codec, 0, &new_mode)
|
||||
} else {
|
||||
bitrate_kbps
|
||||
};
|
||||
// Build the new pipeline BEFORE dropping the old one: the host already acked
|
||||
// the switch as accepted, so a rebuild failure must not kill an otherwise
|
||||
// healthy session — keep streaming the current mode and log instead.
|
||||
match build_pipeline(&mut vd, new_mode, bitrate_kbps, bit_depth, plan, &quit) {
|
||||
match build_pipeline(&mut vd, new_mode, mode_bitrate, bit_depth, plan, &quit) {
|
||||
Ok(next_pipe) => {
|
||||
if mode_bitrate != bitrate_kbps {
|
||||
tracing::info!(
|
||||
from_kbps = bitrate_kbps,
|
||||
to_kbps = mode_bitrate,
|
||||
"pinned PyroWave bitrate re-resolved for the new mode"
|
||||
);
|
||||
bitrate_kbps = mode_bitrate;
|
||||
live_bitrate.store(mode_bitrate, Ordering::Relaxed);
|
||||
}
|
||||
let old_display_gen = cur_display_gen;
|
||||
// The destructuring assignment drops the OLD capturer (→ its display lease) as
|
||||
// each binding is replaced — the new pipeline is already up (create-before-drop).
|
||||
|
||||
Reference in New Issue
Block a user