Merge perf/first-frame-latency: driver proto v4 + first-frame/resize latency (P0-P2)
Brings the first-frame-latency branch (P0.1 transition tracing, P1.1/P1.2 Welcome-time display prep, P2 in-place resize; pf-driver-proto v3 -> v4 with IOCTL_UPDATE_MODES) onto current main. The branch predates the W6.2/W7 splits, so git's rename detection carried most of it into the moved crates (pf-capture idd_push, pf-vdisplay manager/pf_vdisplay, pf-win-display, pf-driver-proto, the driver workspace) and the punktfunk1.rs remainder was re-homed by hand: - native/handshake.rs: welcome/start trace marks + the Welcome-time display prep spawn (the prep thread BECOMES the stream thread; hand-off via a SyncSender<SessionContext>). negotiate() gains bringup/quit/stop and returns the PrepHandle. - native.rs: bringup/resize_ms creation + the stop/quit flags hoisted BEFORE the handshake (the close watcher splits: flags pre-handshake, lifecycle events post-handshake where `hello` exists); punch_done stamp; the data plane adopts the prep thread's result or builds inline. - native/stream.rs: SessionContext/SendStats carry the trace; send_loop finishes it on the first video packet; the resize path gains the in-place fast path (try_inplace_resize) with the full rebuild as fallback, restructured so both share the post-rebuild bookkeeping; prepare_display/PreparedDisplay/ PrepHandle; build_pipeline(+retry) thread the stage marks. - session_status/mgmt: ttff_ms + last_resize_ms per session (union with the lifecycle-events fields main added to the same spots). - pf-capture: Capturer gains capture_target_id() + resize_output() defaults. - pf-vdisplay manager: perf's faster activation poll (60x50ms) + the settle floor before the PnP sweep, on main's knobs/no-trait shape. Also: packaging/windows/build-gamepad-drivers.ps1 is ASCII again (an em-dash from the pf-mouse work tripped windows-host.yml's locale-safety gate on main). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,24 @@ pub trait Capturer: Send {
|
||||
fn pipeline_depth(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
/// The OS display-target id this capturer is bound to (Windows IDD-push), so the resize path
|
||||
/// can verify the display it just reconfigured is STILL the one this capturer serves (an
|
||||
/// in-place resize keeps the target; a re-arrival fallback mints a new one, which needs a
|
||||
/// fresh capturer). `None` = the backend has no such identity (every non-IDD backend).
|
||||
fn capture_target_id(&self) -> Option<u32> {
|
||||
None
|
||||
}
|
||||
|
||||
/// HOST-INITIATED output resize (latency plan P2.3): the session's resize handler has ALREADY
|
||||
/// committed the display's new mode (the manager's in-place mode set), so a capable capturer
|
||||
/// re-sizes its capture surface NOW — no descriptor-poll debounce (that machinery stays, for
|
||||
/// EXTERNAL changes only) and no teardown: the capture pipeline and its send thread survive;
|
||||
/// only the encoder is swapped by the caller once the first new-size frame arrives. Returns
|
||||
/// `true` when handled; `false` (the default) routes the caller to the full-rebuild path.
|
||||
fn resize_output(&mut self, _width: u32, _height: u32) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A deterministic moving test pattern (BGRx). Lets the spike exercise the encode → file →
|
||||
|
||||
@@ -698,8 +698,24 @@ impl IddPushCapturer {
|
||||
let enabled_hdr = client_10bit
|
||||
&& pf_win_display::win_display::set_advanced_color(target.target_id, true);
|
||||
if enabled_hdr {
|
||||
// Let the colorspace change settle before the driver composes + we size the ring.
|
||||
std::thread::sleep(Duration::from_millis(250));
|
||||
// Let the colorspace change settle before the driver composes + we size the ring:
|
||||
// poll the CCD advanced-color state instead of a fixed sleep (latency plan P0.4),
|
||||
// ceiling = the old 250 ms. A read that never flips within the ceiling proceeds
|
||||
// exactly like the fixed sleep did — the ring is sized FP16 from `enabled_hdr`
|
||||
// either way (the set succeeded; only the driver's compose flip may lag, which the
|
||||
// stash/format-guard machinery absorbs).
|
||||
let hdr_settle = Instant::now();
|
||||
while hdr_settle.elapsed() < Duration::from_millis(250) {
|
||||
if crate::win_display::advanced_color_enabled(target.target_id) == Some(true) {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(25));
|
||||
}
|
||||
tracing::debug!(
|
||||
target_id = target.target_id,
|
||||
settle_ms = hdr_settle.elapsed().as_millis() as u64,
|
||||
"IDD push: advanced-color (HDR) enable settle"
|
||||
);
|
||||
}
|
||||
// A failed open-time read defaults to SDR (unless the 10-bit path enabled HDR above) —
|
||||
// there is no "last known" yet; the descriptor poller corrects a wrong guess mid-session.
|
||||
@@ -983,7 +999,14 @@ impl IddPushCapturer {
|
||||
self.no_first_frame_diagnosis(st)
|
||||
);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
// Event-driven wait (latency plan P0.6): the driver signals the frame-ready event on
|
||||
// every publish, so wake on it instead of a blind sleep — the 20 ms timeout keeps the
|
||||
// driver_status polls above live (status writes don't signal the event). Consuming a
|
||||
// signal here is fine: `next_frame` re-checks the atomic `latest` token, never the
|
||||
// event, for truth.
|
||||
// SAFETY: `self.event` is this capturer's owned, live auto-reset event handle;
|
||||
// `WaitForSingleObject` only reads the handle and the 20 ms timeout bounds the wait.
|
||||
let _ = unsafe { WaitForSingleObject(HANDLE(self.event.as_raw_handle()), 20) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1615,6 +1638,39 @@ impl Capturer for IddPushCapturer {
|
||||
// always has its own texture).
|
||||
pf_host_config::config().idd_depth.clamp(1, OUT_RING)
|
||||
}
|
||||
|
||||
fn capture_target_id(&self) -> Option<u32> {
|
||||
Some(self.target_id)
|
||||
}
|
||||
|
||||
fn resize_output(&mut self, width: u32, height: u32) -> bool {
|
||||
// Host-initiated resize (latency plan P2.3): the session's resize handler has already
|
||||
// committed the display's new mode (the manager's in-place mode set), so recreate the ring
|
||||
// at the new size NOW — no DescriptorPoller two-strike debounce (that stays, unchanged,
|
||||
// for EXTERNAL changes: HDR flips, game mode-sets). The driver re-attaches to the fresh
|
||||
// ring and republishes; on an in-place mode set the OS's mode-set full redraw gives the
|
||||
// stash/first frame within the recover window. Same recover-or-drop arming as the
|
||||
// poller-driven recreate, so a ring that can't re-attach still fails the session cleanly
|
||||
// instead of freezing.
|
||||
if (width, height) == (self.width, self.height) {
|
||||
return true; // already at the requested size (refresh-only change) — nothing to do
|
||||
}
|
||||
tracing::info!(
|
||||
target_id = self.target_id,
|
||||
from = format!("{}x{}", self.width, self.height),
|
||||
to = format!("{width}x{height}"),
|
||||
"IDD push: host-initiated resize — recreating the ring at the new mode"
|
||||
);
|
||||
self.recovering_since.get_or_insert_with(Instant::now);
|
||||
if let Err(e) = self.recreate_ring(self.display_hdr, width, height) {
|
||||
tracing::warn!(
|
||||
error = %format!("{e:#}"),
|
||||
"IDD push: host-initiated ring recreate failed — falling back to a full rebuild"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A 4:4:4 session while the display is HDR: there is no 10-bit full-chroma source (the FP16
|
||||
|
||||
Reference in New Issue
Block a user