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:
2026-06-25 15:33:11 +00:00
parent c87bfe0e7b
commit 789ad49bc4
2 changed files with 52 additions and 10 deletions
@@ -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;
@@ -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<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) {
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();
}
}
}