feat(windows-drivers): publish() descriptor guard + log appender (game-capture GB3 groundwork)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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`]
|
/// 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.
|
/// detects that so `run_core` re-attaches to the new-format textures instead of dropping every frame.
|
||||||
generation: u32,
|
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.
|
// 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.
|
// SAFETY: `header` is the mapped host header; `dxgi_format` lives within it.
|
||||||
ring_format: unsafe { (*header).dxgi_format },
|
ring_format: unsafe { (*header).dxgi_format },
|
||||||
generation,
|
generation,
|
||||||
|
mismatch_logged: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,9 +285,28 @@ impl FramePublisher {
|
|||||||
let mut desc = D3D11_TEXTURE2D_DESC::default();
|
let mut desc = D3D11_TEXTURE2D_DESC::default();
|
||||||
// SAFETY: `surface` is a live ID3D11Texture2D (borrowed from IddCx); `desc` is a valid local out-param.
|
// SAFETY: `surface` is a live ID3D11Texture2D (borrowed from IddCx); `desc` is a valid local out-param.
|
||||||
unsafe { surface.GetDesc(&mut desc) };
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
self.mismatch_logged = false;
|
||||||
let start = self.next;
|
let start = self.next;
|
||||||
for attempt in 0..ring_len {
|
for attempt in 0..ring_len {
|
||||||
let slot = (start + attempt) % ring_len;
|
let slot = (start + attempt) % ring_len;
|
||||||
|
|||||||
@@ -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())
|
*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<std::fs::File>> {
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
static APPENDER: OnceLock<Option<std::sync::Mutex<std::fs::File>>> = 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) {
|
pub fn log(s: &str) {
|
||||||
if let Ok(c) = std::ffi::CString::new(s) {
|
if let Ok(c) = std::ffi::CString::new(s) {
|
||||||
// SAFETY: `c` is a valid NUL-terminated string for the duration of the call.
|
// SAFETY: `c` is a valid NUL-terminated string for the duration of the call.
|
||||||
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
|
unsafe { OutputDebugStringA(c.as_ptr().cast()) };
|
||||||
}
|
}
|
||||||
if !file_log_enabled() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
if let Ok(mut f) = std::fs::OpenOptions::new()
|
if let Some(m) = file_appender() {
|
||||||
.create(true)
|
if let Ok(mut f) = m.lock() {
|
||||||
.append(true)
|
|
||||||
.open("C:\\Users\\Public\\pfvd-driver.log")
|
|
||||||
{
|
|
||||||
let _ = writeln!(f, "{s}");
|
let _ = writeln!(f, "{s}");
|
||||||
|
let _ = f.flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user