perf(latency): tier-0 attribution + tier-1 send-path levers from the latency plan
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 54s
decky / build-publish (push) Successful in 18s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
apple / swift (push) Successful in 1m23s
ci / rust (push) Failing after 4m15s
arch / build-publish (push) Failing after 4m35s
deb / build-publish (push) Failing after 4m4s
docker / deploy-docs (push) Successful in 24s
ci / bench (push) Successful in 5m35s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 3m45s
windows-host / package (push) Failing after 9m8s
release / apple (push) Successful in 5m49s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 5m25s
flatpak / build-publish (push) Failing after 8m3s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m58s
android / android (push) Successful in 14m9s
apple / screenshots (push) Successful in 6m28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 7m0s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 9m28s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 12m26s
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 54s
decky / build-publish (push) Successful in 18s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
apple / swift (push) Successful in 1m23s
ci / rust (push) Failing after 4m15s
arch / build-publish (push) Failing after 4m35s
deb / build-publish (push) Failing after 4m4s
docker / deploy-docs (push) Successful in 24s
ci / bench (push) Successful in 5m35s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 3m45s
windows-host / package (push) Failing after 9m8s
release / apple (push) Successful in 5m49s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 5m25s
flatpak / build-publish (push) Failing after 8m3s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m58s
android / android (push) Successful in 14m9s
apple / screenshots (push) Successful in 6m28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 7m0s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 9m28s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 12m26s
design/latency-reduction-2026-07.md T0.1/T0.2/T1.2/T1.3:
- T1.2 rate-capped front-loaded pacing: the paced overflow's budget is now
min(0.9x slack, overflow wire time at ~3x the live encoder bitrate)
(PUNKTFUNK_PACE_FACTOR, 0 = legacy deadline-only spread). A 300 KB-1 MB
frame's tail leaves in ~2-5 ms instead of smearing across ~15 ms at 60 fps;
GameStream schedule byte-identical (pins unchanged).
- T1.3 data-first wire order: packetize emits every block's data shards before
any parity (per-block parity pools keep all blocks' parity alive for the
second pass), so lossless completion stops waiting behind the parity tail.
EOF = last emitted packet; receiver already order-agnostic.
- T0.1 staged 0xCF: HostTiming gains an append-extensible per-stage tail
(queue/encode/pace us; seal+channel-wait derived as residual) - no cap bit
needed, old peers read the 13-byte prefix. Joined client-side into
Stats::host_{queue,encode,xfer,pace}_ms, the OSD detailed tier, and the
probe's report.
- T0.2 true on-glass present timing: VK_KHR_present_id/present_wait enabled
when supported; a PresentTimer waiter thread resolves each present id to
real visibility, replacing the submit-time display stamp (which undercounts
by up to a refresh and hides a silent-FIFO standing queue).
Validated on .21: core 185 + host 185 tests, pf-presenter 19, clippy
-D warnings across all five touched crates; loss-harness recovery curve
unchanged; C ABI harness round-trips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1171,28 +1171,50 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
|
||||
};
|
||||
if did_present {
|
||||
presented_video = true;
|
||||
let displayed_ns = session::now_ns();
|
||||
if opts.json_status && !st.ready_announced {
|
||||
st.ready_announced = true;
|
||||
println!("{{\"ready\":true}}");
|
||||
}
|
||||
// The `displayed` stamp (same clamp rules as the pump's windows).
|
||||
let clock_offset_ns = st
|
||||
.clock_offset
|
||||
.as_ref()
|
||||
.map_or(0, |o| o.load(Ordering::Relaxed));
|
||||
let e2e = (displayed_ns as i128 + clock_offset_ns as i128 - pts_ns as i128)
|
||||
.max(0) as u64;
|
||||
if e2e > 0 && e2e < 10_000_000_000 {
|
||||
st.win_e2e_us.push(e2e / 1000);
|
||||
if presenter.present_timing_active() {
|
||||
// T0.2: hand the frame's stamps to the present-wait waiter — the
|
||||
// e2e/display samples arrive via `take_presented_samples` with a
|
||||
// TRUE on-glass stamp instead of the submit-time one below.
|
||||
presenter.note_presented(pts_ns, decoded_ns);
|
||||
} else {
|
||||
let displayed_ns = session::now_ns();
|
||||
// The `displayed` stamp (same clamp rules as the pump's windows).
|
||||
let clock_offset_ns = st
|
||||
.clock_offset
|
||||
.as_ref()
|
||||
.map_or(0, |o| o.load(Ordering::Relaxed));
|
||||
let e2e = (displayed_ns as i128 + clock_offset_ns as i128 - pts_ns as i128)
|
||||
.max(0) as u64;
|
||||
if e2e > 0 && e2e < 10_000_000_000 {
|
||||
st.win_e2e_us.push(e2e / 1000);
|
||||
}
|
||||
st.win_disp_us
|
||||
.push(displayed_ns.saturating_sub(decoded_ns) / 1000);
|
||||
}
|
||||
st.win_disp_us
|
||||
.push(displayed_ns.saturating_sub(decoded_ns) / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Fold the presenter window into the shared stats line once per second.
|
||||
if st.win_start.elapsed() >= Duration::from_secs(1) {
|
||||
// On-glass samples the present-wait waiter completed this window (empty
|
||||
// when timing is inactive — the legacy submit-time pushes fill in then).
|
||||
let clock_offset_ns = st
|
||||
.clock_offset
|
||||
.as_ref()
|
||||
.map_or(0, |o| o.load(Ordering::Relaxed));
|
||||
for s in presenter.take_presented_samples() {
|
||||
let e2e = (s.displayed_ns as i128 + clock_offset_ns as i128 - s.pts_ns as i128)
|
||||
.max(0) as u64;
|
||||
if e2e > 0 && e2e < 10_000_000_000 {
|
||||
st.win_e2e_us.push(e2e / 1000);
|
||||
}
|
||||
st.win_disp_us
|
||||
.push(s.displayed_ns.saturating_sub(s.decoded_ns) / 1000);
|
||||
}
|
||||
let (e2e_p50, e2e_p95) = session::window_percentiles(&mut st.win_e2e_us);
|
||||
let (disp_p50, _) = session::window_percentiles(&mut st.win_disp_us);
|
||||
st.presented = PresentedWindow {
|
||||
@@ -1635,6 +1657,14 @@ fn stats_text(
|
||||
" · decode {:.1} · display {:.1} ms",
|
||||
s.decode_ms, p.display_ms
|
||||
));
|
||||
// Extended 0xCF host-stage split (T0.1): its own line so the per-stage attribution
|
||||
// (queue → encode → seal/xfer → pace) reads as the host pipeline in order.
|
||||
if s.staged {
|
||||
text.push_str(&format!(
|
||||
"\nhost: queue {:.1} · encode {:.1} · xfer {:.1} · pace {:.1} ms",
|
||||
s.host_queue_ms, s.host_encode_ms, s.host_xfer_ms, s.host_pace_ms
|
||||
));
|
||||
}
|
||||
}
|
||||
if s.lost > 0 {
|
||||
text.push_str(&format!("\nlost {} ({:.1}%)", s.lost, s.lost_pct));
|
||||
@@ -1830,6 +1860,11 @@ mod tests {
|
||||
host_ms: 1.2,
|
||||
net_ms: 0.9,
|
||||
split: true,
|
||||
host_queue_ms: 0.3,
|
||||
host_encode_ms: 0.5,
|
||||
host_xfer_ms: 0.1,
|
||||
host_pace_ms: 0.3,
|
||||
staged: true,
|
||||
decode_ms: 1.8,
|
||||
lost: 3,
|
||||
lost_pct: 0.4,
|
||||
@@ -1866,7 +1901,12 @@ mod tests {
|
||||
let detailed = text(StatsVerbosity::Detailed);
|
||||
assert!(detailed.contains("vulkan · HDR→SDR"));
|
||||
assert!(detailed.contains("host 1.2 · net 0.9 · decode 1.8 · display 1.1 ms"));
|
||||
assert!(detailed.contains("host: queue 0.3 · encode 0.5 · xfer 0.1 · pace 0.3 ms"));
|
||||
assert!(detailed.contains("lost 3 (0.4%)"));
|
||||
assert!(
|
||||
!normal.contains("queue"),
|
||||
"host-stage split is Detailed-only"
|
||||
);
|
||||
}
|
||||
|
||||
/// Compact omits the latency term until the presenter's first e2e window lands.
|
||||
|
||||
@@ -28,6 +28,7 @@ use pf_client_core::video::{CpuFrame, VkVideoFrame};
|
||||
mod gpu;
|
||||
mod overlay_pipe;
|
||||
mod present;
|
||||
mod present_timing;
|
||||
mod reconfig;
|
||||
mod resources;
|
||||
mod setup;
|
||||
@@ -187,6 +188,16 @@ pub struct Presenter {
|
||||
/// The submit fence has a submission pending (wait before recording again — also
|
||||
/// what makes the single staging buffer safe to overwrite).
|
||||
submitted: bool,
|
||||
/// `VK_KHR_present_wait` on-glass timing (latency plan T0.2) — `None` when the
|
||||
/// device lacks the present-id/present-wait pair; the run loop then keeps its
|
||||
/// submit-time display stamp.
|
||||
present_timer: Option<present_timing::PresentTimer>,
|
||||
/// Monotonic present id (global counter — strictly increasing per swapchain, which
|
||||
/// is all the spec asks). 0 = nothing presented with an id yet.
|
||||
next_present_id: u64,
|
||||
/// The last successful id-carrying present, awaiting its [`Presenter::note_presented`]
|
||||
/// claim from the run loop (which owns the frame's pts/decode stamps).
|
||||
last_presented: Option<(vk::SwapchainKHR, u64)>,
|
||||
}
|
||||
|
||||
impl Presenter {
|
||||
@@ -220,6 +231,30 @@ impl Presenter {
|
||||
unsafe { self.device.device_wait_idle() }.ok();
|
||||
}
|
||||
|
||||
/// True when `VK_KHR_present_wait` drives the display stamp — the run loop then
|
||||
/// defers its e2e/display windows to [`Presenter::take_presented_samples`] instead
|
||||
/// of stamping at `present()` return.
|
||||
pub(crate) fn present_timing_active(&self) -> bool {
|
||||
self.present_timer.is_some()
|
||||
}
|
||||
|
||||
/// Claim the just-submitted present for on-glass timing. Call right after a
|
||||
/// `present()` that returned `true`, with that frame's capture + decode stamps
|
||||
/// (the presenter itself never sees them). No-op when timing is inactive.
|
||||
pub(crate) fn note_presented(&mut self, pts_ns: u64, decoded_ns: u64) {
|
||||
if let (Some(t), Some((sc, id))) = (&self.present_timer, self.last_presented.take()) {
|
||||
t.enqueue(sc, id, pts_ns, decoded_ns);
|
||||
}
|
||||
}
|
||||
|
||||
/// Take the window's completed on-glass samples (empty when timing is inactive).
|
||||
pub(crate) fn take_presented_samples(&self) -> Vec<present_timing::PresentedSample> {
|
||||
self.present_timer
|
||||
.as_ref()
|
||||
.map(|t| t.take_samples())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The device handles the console-UI overlay renders on (§6.1). Valid for the
|
||||
/// presenter's lifetime; the run loop drops the overlay first.
|
||||
pub fn shared_device(&self) -> SharedDevice {
|
||||
@@ -237,6 +272,10 @@ impl Presenter {
|
||||
|
||||
impl Drop for Presenter {
|
||||
fn drop(&mut self) {
|
||||
// The present-wait waiter references the swapchain — stop it (its Drop joins
|
||||
// after in-flight waits complete, bounded by their 250 ms cap) BEFORE the
|
||||
// swapchain teardown below.
|
||||
self.present_timer.take();
|
||||
unsafe {
|
||||
{
|
||||
// Insurance against a straggling submitter (the run loop joins the
|
||||
|
||||
@@ -569,20 +569,32 @@ impl Presenter {
|
||||
let swapchains = [self.swapchain];
|
||||
let indices = [index];
|
||||
let present_sems = [render_sem];
|
||||
// On-glass timing (T0.2): attach a monotonically increasing present id the
|
||||
// PresentTimer's `vkWaitForPresentKHR` resolves to real visibility.
|
||||
let ids = [self.next_present_id + 1];
|
||||
let mut pid_info = vk::PresentIdKHR::default().present_ids(&ids);
|
||||
let mut present_info = vk::PresentInfoKHR::default()
|
||||
.wait_semaphores(&present_sems)
|
||||
.swapchains(&swapchains)
|
||||
.image_indices(&indices);
|
||||
if self.present_timer.is_some() {
|
||||
self.next_present_id += 1;
|
||||
present_info = present_info.push_next(&mut pid_info);
|
||||
}
|
||||
// Same queue external-sync rule as the submit above. Scoped tightly: the
|
||||
// OUT_OF_DATE arm re-enters the lock via recreate_swapchain's queue drain.
|
||||
let present_res = {
|
||||
let _q = self.queue_lock.guard();
|
||||
self.swap_d.queue_present(
|
||||
self.queue,
|
||||
&vk::PresentInfoKHR::default()
|
||||
.wait_semaphores(&present_sems)
|
||||
.swapchains(&swapchains)
|
||||
.image_indices(&indices),
|
||||
)
|
||||
self.swap_d.queue_present(self.queue, &present_info)
|
||||
};
|
||||
match present_res {
|
||||
Ok(_) => Ok(true),
|
||||
Ok(_) => {
|
||||
// A failed present's id may never signal — claimable only on Ok.
|
||||
if self.present_timer.is_some() {
|
||||
self.last_presented = Some((self.swapchain, self.next_present_id));
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => {
|
||||
self.recreate_swapchain(window)?;
|
||||
Ok(false)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//! 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.
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,14 @@ impl Presenter {
|
||||
// The old swapchain and everything tied to its images dies NOW: the fence
|
||||
// quiesce covered our own command buffers, the queue drain above covered the
|
||||
// presentation engine's semaphore waits — nothing can still reference them.
|
||||
// The present-wait waiter is the one remaining referent: `vkWaitForPresentKHR`
|
||||
// requires the swapchain alive for the call, so drain it first (bounded by the
|
||||
// waiter's 250 ms cap; ids complete in order so this is normally instant).
|
||||
if let Some(t) = &self.present_timer {
|
||||
t.drain();
|
||||
}
|
||||
// An unclaimed last present belonged to the dying swapchain — drop the claim.
|
||||
self.last_presented = None;
|
||||
let (overlay_views, overlay_framebuffers) = self.overlay_pipe.take_targets();
|
||||
unsafe {
|
||||
for fb in overlay_framebuffers {
|
||||
|
||||
@@ -129,17 +129,30 @@ impl Presenter {
|
||||
let dev_props = unsafe { instance.get_physical_device_properties(pdev) };
|
||||
let dev_is_13 = vk::api_version_major(dev_props.api_version) > 1
|
||||
|| vk::api_version_minor(dev_props.api_version) >= 3;
|
||||
let mut have_pid = vk::PhysicalDevicePresentIdFeaturesKHR::default();
|
||||
let mut have_pwait = vk::PhysicalDevicePresentWaitFeaturesKHR::default();
|
||||
let mut have_f11 = vk::PhysicalDeviceVulkan11Features::default();
|
||||
let mut have_f12 = vk::PhysicalDeviceVulkan12Features::default();
|
||||
let mut have_f13 = vk::PhysicalDeviceVulkan13Features::default();
|
||||
// Present-id/present-wait (on-glass timing, latency plan T0.2): query the feature
|
||||
// structs only when the device lists both extensions.
|
||||
let present_wait_exts =
|
||||
has(ash::khr::present_id::NAME) && has(ash::khr::present_wait::NAME);
|
||||
let mut have_f2 = vk::PhysicalDeviceFeatures2::default()
|
||||
.push_next(&mut have_f11)
|
||||
.push_next(&mut have_f12)
|
||||
.push_next(&mut have_f13);
|
||||
if present_wait_exts {
|
||||
have_f2 = have_f2.push_next(&mut have_pid).push_next(&mut have_pwait);
|
||||
}
|
||||
unsafe { instance.get_physical_device_features2(pdev, &mut have_f2) };
|
||||
// Copy the one base-features fact out NOW: `have_f2` mutably borrows the 11/12/13
|
||||
// structs through its pNext chain, so any later use of it would pin those borrows.
|
||||
// Copy the one base-features fact out NOW: `have_f2` mutably borrows the chained
|
||||
// structs through its pNext chain, so any later use of it would pin those borrows —
|
||||
// every read of a chained struct below must come after this, have_f2's last use.
|
||||
let have_shader_int16 = have_f2.features.shader_int16;
|
||||
let present_wait_ok = present_wait_exts
|
||||
&& have_pid.present_id == vk::TRUE
|
||||
&& have_pwait.present_wait == vk::TRUE;
|
||||
let features_ok = have_f11.sampler_ycbcr_conversion == vk::TRUE
|
||||
&& have_f12.timeline_semaphore == vk::TRUE
|
||||
&& have_f13.synchronization2 == vk::TRUE;
|
||||
@@ -224,6 +237,15 @@ impl Presenter {
|
||||
);
|
||||
}
|
||||
|
||||
// Present-id/present-wait: enable when fully supported — the presenter then runs
|
||||
// the on-glass PresentTimer; otherwise the display stamp stays submit-time.
|
||||
if present_wait_ok {
|
||||
dev_exts.push(ash::khr::present_id::NAME.as_ptr());
|
||||
dev_exts.push(ash::khr::present_wait::NAME.as_ptr());
|
||||
}
|
||||
let mut en_pid = vk::PhysicalDevicePresentIdFeaturesKHR::default().present_id(true);
|
||||
let mut en_pwait = vk::PhysicalDevicePresentWaitFeaturesKHR::default().present_wait(true);
|
||||
|
||||
// Enable only the features the video path needs, and only where supported
|
||||
// (harmless when the path is off; reported to FFmpeg via device_features).
|
||||
let mut en_f11 = vk::PhysicalDeviceVulkan11Features::default()
|
||||
@@ -240,6 +262,9 @@ impl Presenter {
|
||||
.push_next(&mut en_f11)
|
||||
.push_next(&mut en_f12)
|
||||
.push_next(&mut en_f13);
|
||||
if present_wait_ok {
|
||||
en_f2 = en_f2.push_next(&mut en_pid).push_next(&mut en_pwait);
|
||||
}
|
||||
en_f2.features.shader_int16 = if pyrowave_ok { vk::TRUE } else { vk::FALSE };
|
||||
|
||||
let priorities = [1.0f32];
|
||||
@@ -265,6 +290,15 @@ impl Presenter {
|
||||
}
|
||||
.context("vkCreateDevice")?;
|
||||
let swap_d = ash::khr::swapchain::Device::new(&instance, &device);
|
||||
let present_timer = present_wait_ok.then(|| {
|
||||
super::present_timing::PresentTimer::spawn(ash::khr::present_wait::Device::new(
|
||||
&instance, &device,
|
||||
))
|
||||
});
|
||||
tracing::info!(
|
||||
present_wait = present_wait_ok,
|
||||
"on-glass present timing (VK_KHR_present_wait)"
|
||||
);
|
||||
let hdr_metadata_d =
|
||||
has_hdr_metadata.then(|| ash::ext::hdr_metadata::Device::new(&instance, &device));
|
||||
let queue = unsafe { device.get_device_queue(qfi, 0) };
|
||||
@@ -441,6 +475,9 @@ impl Presenter {
|
||||
staging: None,
|
||||
video: None,
|
||||
submitted: false,
|
||||
present_timer,
|
||||
next_present_id: 0,
|
||||
last_presented: None,
|
||||
};
|
||||
p.recreate_swapchain(window)?;
|
||||
Ok(p)
|
||||
|
||||
Reference in New Issue
Block a user