diff --git a/crates/pf-capture/src/windows/idd_push.rs b/crates/pf-capture/src/windows/idd_push.rs index 7a37beec..c34fac88 100644 --- a/crates/pf-capture/src/windows/idd_push.rs +++ b/crates/pf-capture/src/windows/idd_push.rs @@ -1861,6 +1861,7 @@ impl IddPushCapturer { cbcr, fence_handle, fence_value, + ring_gen: self.generation, }), ) } else { @@ -1919,6 +1920,7 @@ impl IddPushCapturer { cbcr: dst_cbcr, fence_handle, fence_value, + ring_gen: self.generation, }), }), cursor: None, diff --git a/crates/pf-encode/src/enc/windows/pyrowave.rs b/crates/pf-encode/src/enc/windows/pyrowave.rs index 72fdc3b6..93001490 100644 --- a/crates/pf-encode/src/enc/windows/pyrowave.rs +++ b/crates/pf-encode/src/enc/windows/pyrowave.rs @@ -139,6 +139,10 @@ pub struct PyroWaveEncoder { // Imported plane textures, cached by the out-ring texture's raw pointer (stable per ring slot): // the full-res R8 Y plane and the half-res R8G8 CbCr plane, imported SEPARATELY (a single planar // NV12 import is unreliable on NVIDIA at arbitrary sizes). + /// The capturer ring generation the cached plane imports below belong to. A recreate bumps it, + /// and every cached import is destroyed — the COM addresses they are keyed on can be recycled + /// by the allocator after a recreate, so identity cannot rest on the pointer alone. + ring_gen: Option, y_images: Vec<(PlaneKey, pw::pyrowave_image)>, cbcr_images: Vec<(PlaneKey, pw::pyrowave_image)>, @@ -271,6 +275,7 @@ impl PyroWaveEncoder { pw_dev, pw_enc, sync: std::ptr::null_mut(), + ring_gen: None, y_images: Vec::new(), cbcr_images: Vec::new(), width, @@ -455,6 +460,25 @@ impl PyroWaveEncoder { in pyrowave mode (session_plan::output_format must set OutputFormat::pyrowave)", )?; + // Ring recreate ⇒ every cached plane import belongs to textures that no longer exist. Their + // COM addresses can be handed back out by the allocator, so a pointer-keyed hit could return + // an image bound to freed memory. Flush on the generation change rather than relying on the + // address (or the FIFO cap) to notice. + if self.ring_gen != Some(share.ring_gen) { + if self.ring_gen.is_some() { + tracing::info!( + from = ?self.ring_gen, + to = share.ring_gen, + cached = self.y_images.len() + self.cbcr_images.len(), + "pyrowave: capturer recreated its ring — flushing stale plane imports" + ); + } + for (_, img) in self.y_images.drain(..).chain(self.cbcr_images.drain(..)) { + pw::pyrowave_image_destroy(img); + } + self.ring_gen = Some(share.ring_gen); + } + // Import the fence whenever this encoder has no timeline yet — the first frame, OR a fresh // encoder after a client mode-switch rebuild (the capturer passes the persistent handle on // every frame precisely so a rebuilt encoder can re-import it). @@ -1000,6 +1024,9 @@ mod tests { cbcr: cbcr_tex, fence_handle: Some(fence_handle.0 as isize), fence_value: 1, + // One synthetic ring for the whole case: a constant generation exercises the + // steady-state cache-hit path (a changing one would flush every frame). + ring_gen: 1, }), }), cursor: None, diff --git a/crates/pf-frame/src/dxgi.rs b/crates/pf-frame/src/dxgi.rs index 60531695..8db1fbbf 100644 --- a/crates/pf-frame/src/dxgi.rs +++ b/crates/pf-frame/src/dxgi.rs @@ -52,6 +52,12 @@ pub struct PyroFrameShare { /// The fence value the capturer signalled after THIS frame's convert. The encoder's Vulkan /// acquire waits on it, so the wavelet read is ordered after the D3D11 CSC. pub fence_value: u64, + /// The capturer's ring generation, bumped every time it recreates its texture ring. The + /// PyroWave encoder caches its plane imports keyed on the texture's COM address, which carries + /// no reference — after a recreate those addresses can be recycled by the allocator, so a + /// cached import may describe a texture that no longer exists. The encoder flushes its import + /// cache whenever this changes, making cache identity independent of allocator behaviour. + pub ring_gen: u32, } /// A GPU-resident captured texture (the Windows zero-copy path: NVENC/AMF/QSV encode it in place;