feat(windows-host): rotate out-ring on repeat + size HDR ring at open (audit §5.3/§5.4)

§5.3 (C3): repeat_last() now copies the last frame into a FRESH rotated out-ring slot instead of re-handing last_present's slot, so a repeat (static desktop) never re-hands a slot still encoding under pipeline_depth>1. OUT_RING(3) > max depth(2) keeps the rotated slot free — the out-ring rotation contract now holds for repeats too, not just the synchronous-loop assumption.

§5.4 (C4): when enabling advanced color for a 10-bit client, trust set_advanced_color success and size the ring FP16 directly, instead of racing the advanced_color_enabled poll (which could size SDR while the driver composes FP16 -> format mismatch -> an immediate ring recreate + dropped first frames).

Verified: host clippy (nvenc) clean on the RTX box. On-glass to confirm: HDR-client first-frame + static-desktop pipelining.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 13:18:05 +00:00
parent ed583650a6
commit b0d28380b5
+27 -7
View File
@@ -371,12 +371,18 @@ impl IddPushCapturer {
// SDR-only client leaves the display alone (and still gets a tone-mapped picture, never a freeze, // SDR-only client leaves the display alone (and still gets a tone-mapped picture, never a freeze,
// if the user does enable HDR). // if the user does enable HDR).
unsafe { unsafe {
if client_10bit && crate::vdisplay::sudovda::set_advanced_color(target.target_id, true) // If we ENABLE advanced color for a 10-bit client, trust it (the driver will compose FP16) and
{ // size the ring FP16 directly — don't race the advanced_color_enabled poll, which may not have
// settled within 250 ms and would size the ring SDR while the driver composes FP16 → a format
// mismatch → an immediate ring recreate + dropped first frames (audit §5.4).
let enabled_hdr =
client_10bit && crate::vdisplay::sudovda::set_advanced_color(target.target_id, true);
if enabled_hdr {
// Let the colorspace change settle before the driver composes + we size the ring. // Let the colorspace change settle before the driver composes + we size the ring.
std::thread::sleep(Duration::from_millis(250)); std::thread::sleep(Duration::from_millis(250));
} }
let display_hdr = crate::vdisplay::sudovda::advanced_color_enabled(target.target_id); let display_hdr =
enabled_hdr || crate::vdisplay::sudovda::advanced_color_enabled(target.target_id);
let ring_fmt = if display_hdr { let ring_fmt = if display_hdr {
DXGI_FORMAT_R16G16B16A16_FLOAT DXGI_FORMAT_R16G16B16A16_FLOAT
} else { } else {
@@ -810,14 +816,28 @@ impl IddPushCapturer {
})) }))
} }
fn repeat_last(&self) -> Option<CapturedFrame> { fn repeat_last(&mut self) -> Option<CapturedFrame> {
self.last_present.as_ref().map(|(tex, pf)| CapturedFrame { // Copy the last presented frame into a FRESH rotated out-ring slot so a repeat (static desktop, no
// new driver frame) never re-hands a slot that may still be encoding under pipeline_depth>1 — the
// out-ring rotation IS the texture-ownership contract, and repeats must honor it too (audit §5.3).
// OUT_RING(3) > the max pipeline_depth(2) guarantees the rotated slot is not in flight.
let (src, pf) = self.last_present.clone()?;
let i = self.out_idx;
let dst = self.out_ring.get(i)?.0.clone();
// SAFETY: GPU copy on the owning thread's immediate context; src/dst are our out-ring textures of
// identical format/size (src is a previous out-ring slot; dst the next).
unsafe {
self.context.CopyResource(&dst, &src);
}
self.out_idx = (i + 1) % self.out_ring.len();
self.last_present = Some((dst.clone(), pf));
Some(CapturedFrame {
width: self.width, width: self.width,
height: self.height, height: self.height,
pts_ns: now_ns(), pts_ns: now_ns(),
format: *pf, format: pf,
payload: FramePayload::D3d11(D3d11Frame { payload: FramePayload::D3d11(D3d11Frame {
texture: tex.clone(), texture: dst,
device: self.device.clone(), device: self.device.clone(),
}), }),
}) })