Measuring Input Latency with VK_EXT_present_timing

ledoge1 pts0 comments

My side quest measuring input latency with VK_EXT_present_timing – Maister's Graphics Adventures

Skip to content

About This Site<br>This is a place where I post random topics I find interesting in low-level graphics and engine programming.

ARNTZEN SOFTWARE AS

My contracting business

Archives<br>Search

Search for:

There’s been two use cases I’ve been looking at recently where having an objective and accurate metric for input latency is important. One is the push for AMD_anti_lag support in Mesa (which I need to get around to reviewing) where we need solid objective data that it’s actually helping, and also for my streaming solution PyroFling, I want some hard objective data demonstrating where the milliseconds are going.

I’ve been working on lots of plumbing in this area recently to hopefully help the ecosystem. We shouldn’t need weird hardware solutions to do this stuff. With VK_EXT_present_timing now being plumbed through the Linux driver stack, we have the API we need to do comparative analysis without too much fluff.

The Vulkan layer

I added a new layer to PyroFling repo here. I documented how it works there, but the basic gist is to read back a small region of the swapchain and compare that to a previous frame to compute a Mean Square Error (MSE) metric. When this error spikes significantly compared to the previous N frames, we assume it happened due to input. This input is synthetically generated with /dev/uinput at somewhat random points in time. Using present timing we get accurate metrics for when that present flowed through the system and we can infer latency metrics based on when we generated synthetic input and when the different frame hit the screen.

While the layer is active, the center of screen shows an "error" image. Before starting a capture, this square should be mostly black. TAA jitter on a stable scene can be seen in this view too, and that’s fine for this layer. A small delta input is generated which should show up as large deltas. E.g. if I move the camera while taking a screenshot:

After a run, we can do analysis. This is a CPU bound game on my lopsided system with 9070xt and an old Zen2 CPU.

Typical CPU bound case

Analyzing /tmp/latency-measurement-wine64-preloader-2026-07-01-12-16-26.csv<br>Average frame time: 9.21 ms (108.58 Hz)

PresentComplete is determined by stage: Dequeued<br>Dequeued: Used on Xwayland.<br>Does not exactly represent when image is flipped on screen,<br>but rather when compositor commits to displaying the image. A few milliseconds are expected.<br>FirstPixelOut: Used on most compositors.<br>Represents when GPU flips image on display controller.<br>FirstPixelVisible: Represents when photons are actually emitted by display.<br>Not supported by any known implementation.

Gap between input stimulus and PresentComplete:<br>Represents overall felt latency<br>Average 22.1 ms (confirms that the game is quite responsive)<br>Standard Deviation +/- 2.8595 ms<br>Median 22.4 ms<br>Range [16.7, 28.3] ms

Gap between input stimulus and GPU idle:<br>Represents overall felt latency under ideal VRR conditions<br>Average 21.7 ms<br>Standard Deviation +/- 2.8383 ms<br>Median 21.8 ms<br>Range [16.3, 27.9] ms

Gap between input stimulus and QueuePresent:<br>If this is large, we are likely CPU bound or application is buffering input a lot<br>Average 14.8 ms (about 1.5 frames, as expected for CPU bound game)<br>~0.5 frames for input polling jitter and 1 frame for CPU commands<br>Standard Deviation +/- 2.7076 ms<br>Median 15.0 ms<br>Range [9.49, 20.6] ms

Gap between QueuePresent and GPU idle:<br>If this is large, we are likely GPU bound and would benefit from anti-lag<br>Average 6.85 ms (<br>All numbers look just like I expect.

Heavily GPU bound case

To stress test anti-lag a bit, Cyberpunk 2077 with RT is a good candidate since it completely slams my GPU at native-res + heavy RT:

Some TAA instability comes through in the delta box. Without anti-lag, we see the culprit right away:

Average frame time: 16.809 ms (59.493 Hz)

Gap between input stimulus and PresentComplete:<br>Represents overall felt latency<br>Average 75.9 ms (yikes)<br>Standard Deviation +/- 6.4658 ms<br>Median 77.5 ms<br>Range [65.8, 86.3] ms

Gap between QueuePresent and GPU idle:<br>If this is large, we are likely GPU bound and would benefit from anti-lag<br>Average 33.2 ms<br>Standard Deviation +/- 0.9738 ms<br>Median 33.1 ms<br>Range [31.8, 36.1] ms<br>A full 2 frames of GPU latency, which is bad. We submit work to the GPU long, long before it goes idle from previous frame, oversubscribing it massively. This is what Reflex/AntiLag attacks, adding delays such that we barely keep the GPU fully subscribed, but no more. With anti_lag it looks much better:

Gap between input stimulus and PresentComplete:<br>Represents overall felt latency<br>Average 50.0 ms (clawed back 25 ms latency, nice)<br>Standard Deviation +/- 4.3821 ms<br>Median 50.5 ms<br>Range [43.4, 60.8] ms

Gap between QueuePresent and GPU idle:<br>If this is large, we are likely GPU bound and would benefit from anti-lag<br>Average 16.9 ms (about 1 frame is which what we...

input latency average bound between frame

Related Articles