Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions icd_shim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,40 @@ pub fn proxy_active() -> bool {
unsafe { DRIVER.proxy_active }
}

/// The panel's latch grid as the daemon last reported it, for the process this
/// ICD is loaded into: `anchor_ns` a latch point (`CLOCK_MONOTONIC`), `period_ns`
/// the spacing of latch points, `lead_ns` how far ahead of one a commit must be
/// made to land on it. Returns 1 and fills the out-params when a grid has been
/// reported, 0 (out-params untouched) before the first note. gamescope reaches
/// it through `libgndrm_shim.so`'s `gamenative_drm_latch_grid`, which resolves
/// this symbol from the loaded ICD: the compositor's frame-rate limit places its
/// releases on this grid so each one's commit lands in its own latch window.
///
/// # Safety
/// Each non-null pointer must be valid for a write of its type.
#[no_mangle]
pub unsafe extern "C" fn gamenative_drm_icd_latch_grid(
anchor_ns: *mut u64,
period_ns: *mut u32,
lead_ns: *mut u32,
) -> i32 {
let Some(grid) = crate::lsfg::latch_grid() else {
return 0;
};
unsafe {
if !anchor_ns.is_null() {
*anchor_ns = grid.anchor_ns;
}
if !period_ns.is_null() {
*period_ns = u32::try_from(grid.period_ns).unwrap_or(u32::MAX);
}
if !lead_ns.is_null() {
*lead_ns = u32::try_from(grid.lead_ns).unwrap_or(u32::MAX);
}
}
1
}

/// # Safety: loader-ICD v5 interface (called by the Khronos loader).
#[allow(non_snake_case)]
#[no_mangle]
Expand Down
13 changes: 12 additions & 1 deletion icd_shim/src/present.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,24 @@ pub(crate) fn arm(id: u64, sc: usize) {

/// A completed present, reported by the daemon with the panel's latch time.
pub(crate) fn record(id: u64, latch_ns: u64) {
record_at(id, latch_ns, true);
}

/// `from_latch`: the time is the panel's latch of this frame, which steers the
/// acquire gate; a release-time stand-in only answers the wait.
fn record_at(id: u64, latch_ns: u64, from_latch: bool) {
if id == 0 {
return;
}
let mut w = WAITS.lock().unwrap_or_else(|e| e.into_inner());
if let Some(i) = w.armed.iter().position(|&(armed, _)| armed == id) {
w.armed.remove(i);
}
if from_latch {
if let Some(&(_, commit_ns)) = w.committed.iter().find(|&&(c, _)| c == id) {
crate::scanout::note_latch_margin(latch_ns, commit_ns);
}
}
w.committed.retain(|&(c, _)| c != id);
if w.done.len() >= CAP {
w.done.remove(0);
Expand Down Expand Up @@ -151,7 +162,7 @@ pub(crate) fn complete_on_release(id: u64, release_ns: u64) {
w.armed.iter().any(|&(armed, _)| armed == id)
};
if armed {
record(id, release_ns);
record_at(id, release_ns, false);
}
}

Expand Down
2 changes: 1 addition & 1 deletion icd_shim/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2274,7 +2274,7 @@ unsafe fn gn_acquire_common(
// not start before this one plus a period.
let now_ns = monotonic_ns();
(*sc).next_acquire_ns =
crate::scanout::next_acquire_after((*sc).next_acquire_ns, now_ns, period);
crate::scanout::next_acquire_gated((*sc).next_acquire_ns, now_ns, period);
}
break;
}
Expand Down
144 changes: 139 additions & 5 deletions icd_shim/src/scanout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use gndrm_uapi::wire::{
};
use std::ffi::c_void;
use std::os::fd::RawFd;
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
use std::sync::Mutex;

/// Default budget for one exchange, matching the LD_PRELOAD shim's: a daemon
Expand Down Expand Up @@ -126,15 +126,126 @@ pub(crate) fn period_ns(target_fps: u32) -> u64 {
///
/// The cap is applied here rather than to a release: a frame that may not start
/// yet is the only point where waiting costs neither rendered work nor display
/// latency. `max(prev, now) + period` holds the interval at the target when the
/// guest is faster and lets it be its own rate when it is slower — never two
/// frames closer than one period, and no debt carried out of a stall.
/// latency. `prev + period` holds the interval at the target while the guest
/// keeps up: the wait for `prev` returns a hair late, and that hair must not
/// become the cadence (it did, as `max(prev, now) + period`: a 60 cap ran at
/// 59.4). Once the guest is a whole period behind it is its own rate again,
/// `now + period` — never two frames closer than one period, and no debt
/// carried out of a stall.
#[must_use]
pub(crate) fn next_acquire_after(prev_ns: u64, now_ns: u64, period_ns: u64) -> u64 {
if period_ns == 0 {
return prev_ns;
}
prev_ns.max(now_ns).saturating_add(period_ns)
if prev_ns != 0 && now_ns < prev_ns.saturating_add(period_ns) {
prev_ns + period_ns
} else {
now_ns.saturating_add(period_ns)
}
}

/// The acquire gate's lead on the panel's latch grid, ns: how far ahead of a
/// latch point the guest is let into its frame, so the frame's commit lands in
/// the middle of that point's window. Steered from the latch reports.
static GATE_LEAD_NS: AtomicU64 = AtomicU64::new(0);
static GATE_ON_GRID: AtomicBool = AtomicBool::new(false);
static GATE_ACQUIRES: AtomicU64 = AtomicU64::new(0);

/// `v` reduced into `[-period/2, period/2)`.
fn wrap_ns(v: i64, period: i64) -> i64 {
let half = period / 2;
((v + half).rem_euclid(period)) - half
}

/// The next acquire deadline: `prev + period` while the guest keeps up (the
/// wait returns a hair after the deadline, and that hair must not become the
/// cadence), `now + period` once it fell a whole period behind (no debt). Then,
/// when the panel's latch grid is known and the period is a whole number of
/// latch periods, snapped onto that grid less the lead, so every frame's commit
/// lands on its own latch point: a cadence that is merely the right rate drifts
/// across the latch deadline, where two commits share one point and one is
/// dropped.
#[must_use]
pub(crate) fn next_acquire_gated(prev_ns: u64, now_ns: u64, period_ns: u64) -> u64 {
if period_ns == 0 {
return prev_ns;
}
let mut next = next_acquire_after(prev_ns, now_ns, period_ns);
let grid = if crate::lsfg::source_cap().is_some() {
None
} else {
crate::lsfg::latch_grid()
};
let Some(grid) = grid else {
GATE_ON_GRID.store(false, Ordering::Relaxed);
return next;
};
let latch = grid.period_ns;
if latch == 0 {
return next;
}
let mult = (period_ns + latch / 2) / latch;
if mult == 0 || (mult * latch).abs_diff(period_ns) * 100 > period_ns {
GATE_ON_GRID.store(false, Ordering::Relaxed);
return next;
}
let step = mult * latch;
let mut lead = GATE_LEAD_NS.load(Ordering::Relaxed);
if lead == 0 {
// Until the latch reports steer it: half a window for the commit's own
// margin plus a frame of rendering.
lead = (latch / 2 + 4_000_000).min(step - 1);
GATE_LEAD_NS.store(lead, Ordering::Relaxed);
}
let base = grid.anchor_ns as i64 - lead as i64;
let off = wrap_ns(next as i64 - base, step as i64);
next = (next as i64 - off).max(0) as u64;
if !GATE_ON_GRID.swap(true, Ordering::Relaxed) {
crate::logf!(gndrm_obf::obf!(
"scanout frame cap: on the panel latch grid period={}us x{} lead={}us",
latch / 1000,
mult,
lead / 1000
));
}
let n = GATE_ACQUIRES.fetch_add(1, Ordering::Relaxed) + 1;
if n.is_multiple_of(600) {
crate::logf!(gndrm_obf::obf!(
"scanout frame cap: latch grid lead={}us after {} frames",
GATE_LEAD_NS.load(Ordering::Relaxed) / 1000,
n
));
}
next
}

/// A frame the daemon latched at `latch_ns`, committed by this shim at
/// `commit_ns`: steer the gate's lead so commits land half a window plus the
/// compositor's hop ahead of their latch point. Only latches on the grid count:
/// a release-time stand-in is not one.
pub(crate) fn note_latch_margin(latch_ns: u64, commit_ns: u64) {
if !GATE_ON_GRID.load(Ordering::Relaxed) || commit_ns == 0 || latch_ns <= commit_ns {
return;
}
let Some(grid) = crate::lsfg::latch_grid() else {
return;
};
let latch = grid.period_ns as i64;
if latch == 0 {
return;
}
let margin = (latch_ns - commit_ns) as i64;
if margin >= 2 * latch {
return;
}
if wrap_ns(latch_ns as i64 - grid.anchor_ns as i64, latch).abs() > latch / 8 {
return;
}
let target = latch / 2 + 2_000_000;
let err = margin - target;
let lead = GATE_LEAD_NS.load(Ordering::Relaxed) as i64;
let step = lead - err / 8;
GATE_LEAD_NS.store(step.clamp(0, latch * 64) as u64, Ordering::Relaxed);
}

/// Adopt one frame-cap note: the target it carries.
Expand Down Expand Up @@ -916,6 +1027,29 @@ mod tests {

/// The acquire schedule: one period after the previous start, never closer,
/// and never later than the guest's own next frame when it is slower.
#[test]
fn gated_cadence_does_not_carry_the_wakeup_overshoot() {
// No grid in a host test: the drift fix alone.
let period = 16_666_666;
let first = next_acquire_gated(0, 1_000_000_000, period);
assert_eq!(first, 1_016_666_666);
// The wait returned 150 us late: the next deadline is still one period on.
let second = next_acquire_gated(first, first + 150_000, period);
assert_eq!(second, first + period);
// A whole period late: no debt.
let third = next_acquire_gated(second, second + period + 5_000_000, period);
assert_eq!(third, second + period + 5_000_000 + period);
}

#[test]
fn wrap_is_centred() {
assert_eq!(wrap_ns(0, 100), 0);
assert_eq!(wrap_ns(49, 100), 49);
assert_eq!(wrap_ns(50, 100), -50);
assert_eq!(wrap_ns(-1, 100), -1);
assert_eq!(wrap_ns(151, 100), -49);
}

#[test]
fn the_acquire_schedule_spaces_frame_starts() {
let period = period_ns(40);
Expand Down
67 changes: 67 additions & 0 deletions modules/gndrm-shim/gndrm_shim.c
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,73 @@ uint32_t gamenative_drm_scanout_pitch(uint32_t w, uint32_t h)
return stride;
}

/*
* The panel's latch grid for gamescope's frame-rate limit: a latch point
* (CLOCK_MONOTONIC ns), the spacing of latch points, and how far ahead of one a
* commit has to be made to land on it. The daemon measures the grid from
* SurfaceFlinger's latch stats and pushes it to the Vulkan ICD shim loaded in
* this same process, which keeps the latest note; this LD_PRELOAD library is
* the one gamescope can bind a weak reference to, so it answers on the ICD's
* behalf. 1 with the out-params filled once a grid has been reported, 0 before
* that and when the ICD is not loaded (gamescope then keeps its free-running
* grid). The ICD is looked up by its loaded name, never loaded here.
*/
int gamenative_drm_latch_grid(uint64_t *anchor_ns, uint32_t *period_ns, uint32_t *lead_ns);
int gamenative_drm_latch_grid(uint64_t *anchor_ns, uint32_t *period_ns, uint32_t *lead_ns)
{
typedef int (*latch_grid_fn)(uint64_t *, uint32_t *, uint32_t *);
static latch_grid_fn fn;
static int looked_up;
static uint64_t last_try_ns;
if (!looked_up) {
/* The Vulkan loader opens the ICD on the first instance, so the lookup
* may run before it is there: ask again, at most once a second, since
* the caller asks on every vblank. */
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
uint64_t now_ns = (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
if (last_try_ns && now_ns - last_try_ns < 1000000000ull)
return 0;
last_try_ns = now_ns;
/* The ICD carries no DT_SONAME, so it is found by the path it was
* loaded from, read off this process's maps; RTLD_NOLOAD never loads. */
char path[512] = "";
FILE *maps = fopen("/proc/self/maps", "r");
if (maps) {
char line[1024];
while (fgets(line, sizeof line, maps)) {
char *p = strchr(line, '/');
if (!p || !strstr(p, "/libadrenotools_gamescope.so"))
continue;
p[strcspn(p, "\n")] = 0;
snprintf(path, sizeof path, "%s", p);
break;
}
fclose(maps);
}
void *h = path[0] ? dlopen(path, RTLD_NOLOAD | RTLD_LAZY) : NULL;
if (!h)
h = dlopen("libadrenotools_gamescope.so", RTLD_NOLOAD | RTLD_LAZY);
if (!h) {
static int said;
if (!said++)
log_info("latch grid: ICD %s (%s)\n",
path[0] ? "mapped but dlopen(RTLD_NOLOAD) refused it" : "not mapped yet",
path[0] ? path : "-");
return 0;
}
fn = (latch_grid_fn)dlsym(h, "gamenative_drm_icd_latch_grid");
looked_up = 1;
if (fn)
log_info("latch grid: answered from %s\n", path[0] ? path : "the loaded ICD");
else
log_info("latch grid: the loaded ICD has no gamenative_drm_icd_latch_grid\n");
}
if (!fn)
return 0;
return fn(anchor_ns, period_ns, lead_ns);
}

/* ------------------------------------------------------------------ */
/* scan-out allocator channel */
/* ------------------------------------------------------------------ */
Expand Down
7 changes: 5 additions & 2 deletions tools/exports/libadrenotools_gamescope.map
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
/* Export allowlist for libadrenotools_gamescope.so (the Vulkan ICD shim).
*
* The Khronos loader resolves exactly these three entry points by dlsym; a
* missing one means the loader cannot load the shim at all. Everything else —
* the interception/proxy internals — is local. Audited by
* missing one means the loader cannot load the shim at all. The latch-grid
* getter is read by libgndrm_shim.so on behalf of gamescope (see
* gamenative_drm_latch_grid there). Everything else — the interception/proxy
* internals — is local. Audited by
* tools/check-exports.sh. */
{
global:
vk_icdGetInstanceProcAddr;
vk_icdGetPhysicalDeviceProcAddr;
vk_icdNegotiateLoaderICDInterfaceVersion;
gamenative_drm_icd_latch_grid;
local: *;
};
1 change: 1 addition & 0 deletions tools/exports/libgndrm_shim.map
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
global:
gamenative_drm_connect;
gamenative_drm_scanout_pitch;
gamenative_drm_latch_grid;
open;
openat;
open64;
Expand Down