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:
2026-07-17 16:08:16 +02:00
19 changed files with 1544 additions and 172 deletions
+18
View File
@@ -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 →
+59 -3
View 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