feat(pyrowave): Windows host encoder — separate-plane zero-copy D3D11→Vulkan
Wire PyroWave into the Windows host (design/pyrowave-windows-host-zerocopy.md). Before this a macOS client + Windows host that both selected PyroWave silently ran HEVC: the host never advertised CODEC_PYROWAVE and open_video_backend bailed. Approach (zero-copy, no GPU→CPU→GPU): pyrowave owns its own Vulkan device (create_device_by_compat, by render-GPU vendor/device-id — NOT LUID, invalid in Session 0). The capturer runs a BGRA→YUV BT.709-limited CSC (matching rgb2yuv.comp) into TWO SEPARATE shareable plane textures — full-res R8 Y + half-res R8G8 CbCr — which the encoder imports into pyrowave's device. Separate single/two-component textures import reliably on NVIDIA at any size; a single planar NV12 import does NOT (the vendored interop test: "only very specific resource sizes" — confirmed on-glass: 1024² fine, 720p/1080p/1440p garbage). A shared D3D11 fence, signalled after the CSC, is imported as a Vulkan timeline semaphore so the wavelet read is ordered after it. - pf-encode: enc/windows/pyrowave.rs (Encoder impl, two-plane import + Linux-style plane views); host_wire_caps advertises CODEC_PYROWAVE on Windows when the backend isn't Software; open_video_backend routes a negotiated PyroWave session first; pyrowave-sys on the Windows target; interop confirmed at open → clean HEVC fallback. - pf-encode: shared, unit-tested enc/pyrowave_wire.rs (single source of truth for the client-facing AU framing); Linux encoder uses it too. - pf-capture: dxgi.rs BgraToYuvPlanes CSC; idd_push.rs pyrowave mode — forces the virtual display SDR (the VideoProcessor can't ingest the FP16 HDR ring), a two-plane shareable out-ring, a shared fence passed every frame (so a rebuilt encoder re-imports it). Threaded via OutputFormat::pyrowave. - pf-frame: D3d11Frame::pyro carries the CbCr plane + fence; OutputFormat::pyrowave. Verified on .173 (RTX 4090): full-host build + clippy -D warnings (nvenc,amf-qsv) + fmt --all --check; pyrowave_wire unit tests; pyrowave_win_smoke GPU test round-trips distinct Y/Cb/Cr (100/180/60) exactly at 1024²/720p/1080p/1440p; Stage-0 interop validated in the real Session-0 service context on-glass. Deployed to the box. Owed: final on-glass picture/latency confirmation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -46,13 +46,6 @@ const IMPORT_CACHE_CAP: usize = 16;
|
||||
/// Headroom over the per-frame rate budget for the packetized bitstream (block headers + meta;
|
||||
/// the rate controller itself never exceeds the budget).
|
||||
const BS_SLACK: usize = 256 * 1024;
|
||||
/// Chunked-mode window framing (§4.4): 4-byte prefix per shard-sized window.
|
||||
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 DRM modifiers the PyroWave device can import as a SAMPLED image of the capture's
|
||||
/// packed-RGB format. The capture advertises these for the pyrowave passthrough instead of
|
||||
@@ -1077,8 +1070,8 @@ impl PyroWaveEncoder {
|
||||
// boundary by design.
|
||||
let cap = self.frame_budget + BS_SLACK;
|
||||
self.bitstream.resize(cap, 0);
|
||||
// Chunked mode reserves 4 bytes per window for the framing prefix.
|
||||
let boundary = self.wire_chunk.map(|c| c - WINDOW_PREFIX).unwrap_or(cap);
|
||||
// Chunked mode reserves the 4-byte window prefix from the packetize boundary (shared helper).
|
||||
let boundary = crate::pyrowave_wire::packet_boundary(self.wire_chunk, cap);
|
||||
let mut n: usize = 0;
|
||||
pw_check(
|
||||
pw::pyrowave_encoder_compute_num_packets(self.pw_enc, boundary, &mut n),
|
||||
@@ -1101,67 +1094,10 @@ impl PyroWaveEncoder {
|
||||
"packetize",
|
||||
)?;
|
||||
packets.truncate(out_n.max(1));
|
||||
let au = if let Some(chunk) = self.wire_chunk {
|
||||
// Window framing (§4.4): each `chunk`-sized window opens with a 4-byte prefix
|
||||
// (u16 used-length + u16 kind) and carries either WHOLE self-delimiting codec
|
||||
// packets (PACKED — several small ones share a window) or one fragment of an
|
||||
// oversized packet (FRAG chain — pyrowave 32×32 blocks are atomic and may
|
||||
// exceed a shard). A lost shard zeroes its window (used = 0) — the receiver
|
||||
// skips it and drops any fragment chain it interrupts.
|
||||
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 p in &packets {
|
||||
let bytes = &self.bitstream[p.offset..p.offset + p.size];
|
||||
if p.size <= payload_max {
|
||||
let fits = open.is_some_and(|(_, used)| used + p.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 += p.size;
|
||||
}
|
||||
} else {
|
||||
// Oversized packet: its own FRAG chain of full windows.
|
||||
close(&mut au, &mut open, chunk);
|
||||
let mut off = 0usize;
|
||||
while off < p.size {
|
||||
let take = (p.size - off).min(payload_max);
|
||||
let kind = if off == 0 {
|
||||
WIN_FRAG_FIRST
|
||||
} else if off + take == p.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[off..off + take]);
|
||||
au.resize(start + chunk, 0);
|
||||
off += take;
|
||||
}
|
||||
}
|
||||
}
|
||||
close(&mut au, &mut open, chunk);
|
||||
au
|
||||
} else {
|
||||
let p = &packets[0];
|
||||
self.bitstream[p.offset..p.offset + p.size].to_vec()
|
||||
};
|
||||
// Frame into the wire AU via the shared helper (byte-identical on Linux + Windows): the dense
|
||||
// single packet, or the datagram-aligned windowed AU (§4.4).
|
||||
let pkts: Vec<(usize, usize)> = packets.iter().map(|p| (p.offset, p.size)).collect();
|
||||
let au = crate::pyrowave_wire::build_au(&pkts, &self.bitstream, self.wire_chunk);
|
||||
self.frame_count += 1;
|
||||
self.pending.push_back(EncodedFrame {
|
||||
data: au,
|
||||
|
||||
Reference in New Issue
Block a user