From 789ad49bc41f1c1a177357363b2fe7d7ea9a9130 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 25 Jun 2026 15:33:11 +0000 Subject: [PATCH] feat(windows-drivers): publish() descriptor guard + log appender (game-capture GB3 groundwork) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit publish() now guards width/height alongside format (CopyResource needs matching DIMS too, else garbage): drops a surface whose descriptor no longer matches the host ring (a fullscreen game mode-set the display) AND logs the actual descriptor once per mismatch episode, so a repro shows exactly what changed (GB1/Stage-0 diagnostic + the Stage-2 width/height guard). log.rs: a process-lifetime, flushed, Mutex-shared append handle (opened ONCE) replaces the per-call open/append — so the swap-chain WORKER thread's lines land. They were hidden (per-call open raced the control thread / could fail under the worker's restricted token), which is exactly why a game-break repro showed no swap-chain-processor lines (bug doc S3). This is the observability foundation the bug doc gates Stage S (S1/S2 driver resilience) on. Needs a driver rebuild + re-vendor to deploy (separate from the GB1 host-only fix). Stage 3 (trim default_modes) deprioritized: GB1 recovers from mode-sets, and trimming risks the live display-activation path. Verified: driver workspace builds clean on the RTX box (.173). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../pf-vdisplay/src/frame_transport.rs | 25 ++++++++++++- .../windows/drivers/pf-vdisplay/src/log.rs | 37 ++++++++++++++----- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/packaging/windows/drivers/pf-vdisplay/src/frame_transport.rs b/packaging/windows/drivers/pf-vdisplay/src/frame_transport.rs index c85e56a..959dc0f 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/frame_transport.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/frame_transport.rs @@ -72,6 +72,9 @@ pub struct FramePublisher { /// recreates the ring at a new format mid-session (the display's HDR mode flipped) — [`Self::is_stale`] /// detects that so `run_core` re-attaches to the new-format textures instead of dropping every frame. generation: u32, + /// Set when a surface is dropped for a descriptor mismatch (a game mode-set the display), cleared on a + /// matched publish — throttles the drop log to once per mismatch episode (game-capture bug GB1). + mismatch_logged: bool, } // SAFETY: created and used only on the swap-chain processor thread. @@ -246,6 +249,7 @@ impl FramePublisher { // SAFETY: `header` is the mapped host header; `dxgi_format` lives within it. ring_format: unsafe { (*header).dxgi_format }, generation, + mismatch_logged: false, }) } @@ -281,9 +285,28 @@ impl FramePublisher { let mut desc = D3D11_TEXTURE2D_DESC::default(); // SAFETY: `surface` is a live ID3D11Texture2D (borrowed from IddCx); `desc` is a valid local out-param. unsafe { surface.GetDesc(&mut desc) }; - if desc.Format.0 as u32 != self.ring_format { + // Descriptor guard: CopyResource needs the surface + ring textures to share format AND dimensions. + // A fullscreen game can mode-set the display, changing the surface's format/size before the host + // recreates the ring to match (game-capture bug GB1) — drop a mismatched frame (else garbage) and + // report the ACTUAL descriptor once per episode so a repro shows exactly what changed. + // SAFETY: `self.header` stays mapped for the publisher's lifetime; width/height are plain u32 fields. + let (rw, rh) = unsafe { ((*self.header).width, (*self.header).height) }; + if desc.Format.0 as u32 != self.ring_format || desc.Width != rw || desc.Height != rh { + if !self.mismatch_logged { + self.mismatch_logged = true; + dbglog!( + "[pf-vd] frame-push DROP: surface {}x{} fmt={} != ring {}x{} fmt={} — display mode-set? (host should recreate the ring)", + desc.Width, + desc.Height, + desc.Format.0 as u32, + rw, + rh, + self.ring_format + ); + } return; } + self.mismatch_logged = false; let start = self.next; for attempt in 0..ring_len { let slot = (start + attempt) % ring_len; diff --git a/packaging/windows/drivers/pf-vdisplay/src/log.rs b/packaging/windows/drivers/pf-vdisplay/src/log.rs index 617bd36..11fa282 100644 --- a/packaging/windows/drivers/pf-vdisplay/src/log.rs +++ b/packaging/windows/drivers/pf-vdisplay/src/log.rs @@ -16,21 +16,40 @@ fn file_log_enabled() -> bool { *ON.get_or_init(|| cfg!(debug_assertions) || std::env::var_os("PFVD_DEBUG_LOG").is_some()) } +/// Process-lifetime append handle to the bring-up log, opened ONCE (by whichever thread logs first) and +/// shared via a `Mutex` — so the swap-chain WORKER thread's writes land too. Per-call open/append raced +/// the control thread and/or could fail under the worker's restricted token, hiding exactly the +/// swap-chain-processor lines a game-break repro needs (game-capture bug S3). `flush` after each line so a +/// crash/stall doesn't lose the tail. +fn file_appender() -> Option<&'static std::sync::Mutex> { + use std::sync::OnceLock; + static APPENDER: OnceLock>> = OnceLock::new(); + APPENDER + .get_or_init(|| { + if !file_log_enabled() { + return None; + } + std::fs::OpenOptions::new() + .create(true) + .append(true) + .open("C:\\Users\\Public\\pfvd-driver.log") + .ok() + .map(std::sync::Mutex::new) + }) + .as_ref() +} + pub fn log(s: &str) { if let Ok(c) = std::ffi::CString::new(s) { // SAFETY: `c` is a valid NUL-terminated string for the duration of the call. unsafe { OutputDebugStringA(c.as_ptr().cast()) }; } - if !file_log_enabled() { - return; - } use std::io::Write; - if let Ok(mut f) = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open("C:\\Users\\Public\\pfvd-driver.log") - { - let _ = writeln!(f, "{s}"); + if let Some(m) = file_appender() { + if let Ok(mut f) = m.lock() { + let _ = writeln!(f, "{s}"); + let _ = f.flush(); + } } }