pf-presenter's 120 sites are `ash` calls almost without exception, so 120 independent arguments
would have been 120 restatements of the signature — the exact noise this program exists to remove.
They get the `abi.rs` treatment instead: the Vulkan contract stated once in `lib.rs`, each site
naming which of three shapes it is.
The three are not equal, and separating them is the point. CREATE and RECORD carry no real
precondition — the device is owned, the builders are locals, nothing executes until submit. DESTROY
does: the GPU must not still be using the object, and that is established by the path (a fence wait,
a `queue_wait_idle`, a retired swapchain), not by the call. Those sites say so, because getting it
wrong is a use-after-free no type catches. The contract also tells the next person that a block
outside the three shapes needs a real proof, and that writing "as above" is the signal it doesn't
belong in them.
punktfunk-core's Windows half is finished here too: `qos_windows.rs`'s `GetLastError` reads (called
before anything can reset the thread's error slot) and `udp/windows.rs`'s control-message write,
whose argument is that `ctrl` is sized by `WSA_CMSG_SPACE(4)` — computed two lines up — so header
plus payload cannot run past it, and `write_unaligned` is used because `WSA_CMSG_DATA` offers no
alignment guarantee.
⚠️ THE WINDOWS BLIND SPOT BIT A THIRD TIME. A Linux measurement put this crate pair at 113; the
real number was 129 — `d3d11.rs`, `win32.rs`, `qos_windows.rs`, `udp/windows.rs` are all
`cfg`-hidden. Every crate in this sweep had to be finished on .47 after being "done" on .21. For a
cross-platform crate the Linux number is a lower bound, never the answer.
All three crates now deny `undocumented_unsafe_blocks`, which was the goal: it applied to 8 of 11
crates carrying unsafe, and the three exempt ones held 381 items between them — including the C ABI
surface and the presenter. Verified: Linux .21 fmt + both CI clippy steps rc=0; Windows .47 all four
closed crates clippy `-D warnings` rc=0 plus the full Windows CI clippy set and pf-capture's tests.
Only small crates remain unguarded (75 items total, largest 26).
136 lines
5.4 KiB
Rust
136 lines
5.4 KiB
Rust
//! True on-glass present timing via `VK_KHR_present_wait` (latency plan T0.2).
|
|
//!
|
|
//! The render loop's `displayed` stamp is taken when `vkQueuePresentKHR` *returns* — CPU
|
|
//! submit time, excluding the presentation engine's queue and the vblank latch, so the
|
|
//! HUD's `display` stage under-reports by up to a refresh (and hides a silent-FIFO
|
|
//! standing queue entirely). When the device offers `VK_KHR_present_id` +
|
|
//! `VK_KHR_present_wait`, each present carries a monotonically increasing id and a
|
|
//! dedicated waiter thread blocks in `vkWaitForPresentKHR` — which completes when the
|
|
//! image is actually visible — stamping the REAL on-glass time off the render loop.
|
|
//!
|
|
//! Lifecycle: `vkWaitForPresentKHR` requires the swapchain to stay alive for the call's
|
|
//! duration, so [`PresentTimer::drain`] must run before any `vkDestroySwapchainKHR`
|
|
//! (recreate and teardown both do). Waits carry a 250 ms cap: presentation ids complete
|
|
//! in submission order (a MAILBOX-replaced image's id completes with the present that
|
|
//! replaced it), so a wait only outlives that cap when the pipeline is already wedged —
|
|
//! the timeout keeps the drain bounded rather than wedging a resize with it.
|
|
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
use std::sync::{mpsc, Arc, Mutex};
|
|
|
|
use ash::vk;
|
|
|
|
/// One presented frame's identity + the true on-glass stamp the waiter filled in.
|
|
pub(crate) struct PresentedSample {
|
|
/// The frame's capture stamp (host clock) — the e2e anchor.
|
|
pub pts_ns: u64,
|
|
/// Decode-complete stamp (client clock) — the display-stage anchor.
|
|
pub decoded_ns: u64,
|
|
/// `vkWaitForPresentKHR` completion = the image is visible (client clock).
|
|
pub displayed_ns: u64,
|
|
}
|
|
|
|
struct Job {
|
|
swapchain: vk::SwapchainKHR,
|
|
present_id: u64,
|
|
pts_ns: u64,
|
|
decoded_ns: u64,
|
|
}
|
|
|
|
/// The waiter: a channel-fed thread turning (swapchain, present-id) pairs into
|
|
/// [`PresentedSample`]s. One frame in flight upstream keeps the queue depth ~1.
|
|
pub(crate) struct PresentTimer {
|
|
tx: Option<mpsc::Sender<Job>>,
|
|
/// Jobs enqueued but not yet finished — the drain barrier for swapchain teardown.
|
|
pending: Arc<AtomicUsize>,
|
|
results: Arc<Mutex<Vec<PresentedSample>>>,
|
|
join: Option<std::thread::JoinHandle<()>>,
|
|
}
|
|
|
|
impl PresentTimer {
|
|
pub(crate) fn spawn(wait_d: ash::khr::present_wait::Device) -> Self {
|
|
let (tx, rx) = mpsc::channel::<Job>();
|
|
let pending = Arc::new(AtomicUsize::new(0));
|
|
let results = Arc::new(Mutex::new(Vec::with_capacity(256)));
|
|
let (pending_t, results_t) = (pending.clone(), results.clone());
|
|
let join = std::thread::Builder::new()
|
|
.name("pf-present-wait".into())
|
|
.spawn(move || {
|
|
while let Ok(job) = rx.recv() {
|
|
// 250 ms cap — see the module doc's lifecycle note.
|
|
// SAFETY: per the Vulkan contract above - the Vulkan handles used here are
|
|
// owned by this type and live for the call, and every builder struct is a
|
|
// local that outlives it.
|
|
let r = unsafe {
|
|
wait_d.wait_for_present(job.swapchain, job.present_id, 250_000_000)
|
|
};
|
|
if r.is_ok() {
|
|
let displayed_ns = pf_client_core::session::now_ns();
|
|
results_t.lock().unwrap().push(PresentedSample {
|
|
pts_ns: job.pts_ns,
|
|
decoded_ns: job.decoded_ns,
|
|
displayed_ns,
|
|
});
|
|
}
|
|
// SUBOPTIMAL/TIMEOUT/DEVICE_LOST: no sample; the frame still showed
|
|
// (or the loop is about to find out) — never poison the window.
|
|
pending_t.fetch_sub(1, Ordering::AcqRel);
|
|
}
|
|
})
|
|
.expect("spawn pf-present-wait");
|
|
PresentTimer {
|
|
tx: Some(tx),
|
|
pending,
|
|
results,
|
|
join: Some(join),
|
|
}
|
|
}
|
|
|
|
/// Hand a successfully submitted present to the waiter.
|
|
pub(crate) fn enqueue(
|
|
&self,
|
|
swapchain: vk::SwapchainKHR,
|
|
present_id: u64,
|
|
pts_ns: u64,
|
|
decoded_ns: u64,
|
|
) {
|
|
if let Some(tx) = &self.tx {
|
|
self.pending.fetch_add(1, Ordering::AcqRel);
|
|
if tx
|
|
.send(Job {
|
|
swapchain,
|
|
present_id,
|
|
pts_ns,
|
|
decoded_ns,
|
|
})
|
|
.is_err()
|
|
{
|
|
self.pending.fetch_sub(1, Ordering::AcqRel);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Block until no wait references any swapchain — REQUIRED before
|
|
/// `vkDestroySwapchainKHR`. Bounded by the waiter's own 250 ms wait cap.
|
|
pub(crate) fn drain(&self) {
|
|
while self.pending.load(Ordering::Acquire) > 0 {
|
|
std::thread::sleep(std::time::Duration::from_millis(1));
|
|
}
|
|
}
|
|
|
|
/// Take the window's completed samples (called at the 1 s stats fold).
|
|
pub(crate) fn take_samples(&self) -> Vec<PresentedSample> {
|
|
std::mem::take(&mut *self.results.lock().unwrap())
|
|
}
|
|
}
|
|
|
|
impl Drop for PresentTimer {
|
|
fn drop(&mut self) {
|
|
// Close the channel, let in-flight waits finish (bounded), then join.
|
|
self.tx.take();
|
|
if let Some(j) = self.join.take() {
|
|
let _ = j.join();
|
|
}
|
|
}
|
|
}
|