From e3d39cd2ecc0d98f8e23e526c4053c583c90b074 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Fri, 23 Oct 2026 14:09:35 +0530 Subject: [PATCH 1/2] icd_shim: drop the game-window report from the present path gamescope now sends the GNW1 note itself on the first top-level X map, so the shim no longer walks the X tree every 120 presents to find the game's window. This removes the walk, its cadence and give-up limits, the shim's note sender, and the x11 predicate that only it used. --- icd_shim/src/proxy.rs | 71 +---------- icd_shim/src/scanout.rs | 59 +-------- icd_shim/src/x11.rs | 261 ---------------------------------------- 3 files changed, 6 insertions(+), 385 deletions(-) diff --git a/icd_shim/src/proxy.rs b/icd_shim/src/proxy.rs index ad15745..29c684e 100644 --- a/icd_shim/src/proxy.rs +++ b/icd_shim/src/proxy.rs @@ -34,7 +34,7 @@ use crate::vk::*; use crate::wlproto::{self, wl_display, wl_proxy}; use std::collections::{HashMap, VecDeque}; use std::ffi::{c_char, c_void, CStr, CString}; -use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; // First-N diagnostics for the present pipeline handoffs (acquire -> // present -> worker commit -> compositor release). Bounded counters, so @@ -118,23 +118,6 @@ fn ring_dump(reason: &str) { } static LOG_COMMIT: AtomicUsize = AtomicUsize::new(0); -/// Presents between game-window tree walks (see `maybe_report_game_window`). -/// The splash-clear transition is one-shot, so a walk every `N` presents — a -/// handful of milliseconds of latency — is ample, and it keeps a per-frame X -/// round trip out of the present path. -const GAME_WINDOW_CHECK_EVERY: u32 = 120; -/// Walks to attempt before giving up: minutes of presents, so a guest that -/// spends its first boot compiling shaders behind a loader window is still -/// seen when its own window finally maps. A window that is not identifiable -/// after this many walks (the guest never maps one, or it maps one the -/// predicate cannot classify) will not become identifiable later, and each -/// walk is an X11 round trip on the present path. -const GAME_WINDOW_GIVE_UP: u32 = 240; -/// Presents counted; a walk runs on every `GAME_WINDOW_CHECK_EVERY`th. -static GAME_WINDOW_CHECK: AtomicU32 = AtomicU32::new(0); -/// Set once the game-window notification has been sent; stops all further -/// walks, so a signalled or idle session costs nothing. -static GAME_WINDOW_REPORTED: AtomicBool = AtomicBool::new(false); static LOG_RELEASE: AtomicUsize = AtomicUsize::new(0); const LOG_FIRST_N: usize = 8; @@ -637,44 +620,6 @@ unsafe fn x11_present_slot(sc: *mut GnSwapchain, s: *mut GnSlot, idx: i32) -> bo true } -/// Walk the bound window at a fraction of the present cadence and tell the -/// daemon (once) when the game's own window appears, so it can clear the -/// booting splash on that event instead of on the first published frame. -/// A no-op once reported, so an idle or already-signalled session costs -/// nothing; the walk itself is bounded and never hangs. -/// -/// Reached from BOTH present routes, at the same N-present cadence, so the -/// override/Wayland route (the default) runs the check too (it previously sat -/// behind the DRI3-only X11 present and never fired on the shipping -/// configuration): -/// - guest-surface route (the opt-in; `surf.x11` set): the present's own live -/// connection, with `surf.window` — the guest's own window — as the bound -/// window (see [`crate::x11::window_present`]). -/// - override route (the default; `surf.x11` null): `surf.window` is the window -/// the override resolved gamescope will paint; the walk opens its own -/// connection (see [`crate::x11::game_window_present`]). Rides the cadence, -/// never the per-frame present path. -unsafe fn maybe_report_game_window(surf: *mut GnSurface) { - if GAME_WINDOW_REPORTED.load(Ordering::Relaxed) { - return; - } - let n = GAME_WINDOW_CHECK.fetch_add(1, Ordering::Relaxed); - if !n.is_multiple_of(GAME_WINDOW_CHECK_EVERY) || n / GAME_WINDOW_CHECK_EVERY >= GAME_WINDOW_GIVE_UP { - return; - } - let present = unsafe { (*surf).x11 }; - let bound = unsafe { (*surf).window }; - let game_window_up = if present.is_null() { - unsafe { crate::x11::game_window_present(bound) } - } else { - unsafe { (*present).window_present(bound) } - }; - if game_window_up { - GAME_WINDOW_REPORTED.store(true, Ordering::Relaxed); - crate::scanout::report_game_window_present(); - } -} - /// Frees every X11 slot whose idle fence has triggered - the server is done /// with its pixmap, the slot-reuse contract the wayland arm gets from /// wl_buffer.release. @@ -1628,7 +1573,6 @@ unsafe fn commit_real(sc: *mut GnSwapchain, idx: i32, fence_ms: u128) { // and a guest waiting on a release that never comes stalls on // its next acquire. if x11_present_slot(sc, s, idx) { - maybe_report_game_window((*sc).surf); x11_reap(sc); } else { if !X11_DROP_LOGGED.swap(true, Ordering::Relaxed) { @@ -1684,11 +1628,6 @@ unsafe fn commit_real(sc: *mut GnSwapchain, idx: i32, fence_ms: u128) { wlproto::surface_commit(parent); wlproto::wl_display_flush((*(*sc).surf).dpy) }; - // Override/Wayland route (the default; `GN_VKSHIM_GUEST_SURFACE` - // unset or not exactly "1"): same tree-walk cadence as the - // guest-surface arm above, so the game's own window clears the - // splash on this route too. - maybe_report_game_window((*sc).surf); let commit_ms = t1.elapsed().as_millis(); let n = LOG_COMMIT.fetch_add(1, Ordering::Relaxed); let done = N_COMMIT_OK.fetch_add(1, Ordering::Relaxed); @@ -2432,7 +2371,6 @@ unsafe fn present_sync_commit(sc: *mut GnSwapchain, slot: *mut GnSlot, idx: i32) // The X11 present holds the slot until its idle fence triggers; the // immediate in_flight clear above is restored on success. if unsafe { x11_present_slot(sc, slot, idx) } { - maybe_report_game_window((*sc).surf); let g = unsafe { (*sc).shared.lock().unwrap() }; unsafe { (*slot).in_flight = true }; drop(g); @@ -2463,9 +2401,6 @@ unsafe fn present_sync_commit(sc: *mut GnSwapchain, slot: *mut GnSlot, idx: i32) wlproto::wl_display_flush((*(*sc).surf).dpy); } } - // Override/Wayland route (the default; `GN_VKSHIM_GUEST_SURFACE` unset - // or not exactly "1"): same tree-walk cadence as the guest-surface arm above. - maybe_report_game_window((*sc).surf); } } @@ -7571,9 +7506,7 @@ unsafe extern "C" fn gn_vkCreateWaylandSurfaceKHR( /// are presented to gamescope's override/Wayland surface, ignoring the guest's /// window content — Vulkan-only, the cheapest, and independent of Xwayland's /// DRI3. It is the default because it is the route that keeps presenting here: -/// the splash-clear detection rides the tree walk that runs on BOTH routes (see -/// `maybe_report_game_window`), so the frame path is chosen on its own merits — -/// and the guest-surface route's commit stream stalls when the guest's Vulkan +/// the guest-surface route's commit stream stalls when the guest's Vulkan /// swapchain comes up (see `docs/14-guest-surface-commit-stall.md`). /// /// Setting `GN_VKSHIM_GUEST_SURFACE=1` opts in to the guest-surface route: the diff --git a/icd_shim/src/scanout.rs b/icd_shim/src/scanout.rs index 85d8213..a8a61cb 100644 --- a/icd_shim/src/scanout.rs +++ b/icd_shim/src/scanout.rs @@ -17,8 +17,8 @@ //! pre-existing behaviour rather than a broken one. use gndrm_uapi::wire::{ - FrameCapNote, FrameCapSubscribe, GameWindowNote, LsfgConfig, ScanoutAllocReq, ScanoutAllocResp, - CAP_FRAME_CAP, FPS_UNCAPPED, SCANOUT_FORMAT_BGRA_8888, SCANOUT_REQ_AHB_HANDLE, + FrameCapNote, FrameCapSubscribe, LsfgConfig, ScanoutAllocReq, ScanoutAllocResp, CAP_FRAME_CAP, + FPS_UNCAPPED, SCANOUT_FORMAT_BGRA_8888, SCANOUT_REQ_AHB_HANDLE, }; use std::ffi::c_void; use std::os::fd::RawFd; @@ -206,60 +206,9 @@ pub(crate) fn notes_seen() -> usize { NOTES_N.load(Ordering::Relaxed) } -/// Tell the daemon the game's own window has appeared, so it can dismiss the -/// booting splash on that event instead of on the desktop's first frame. -/// -/// Fire-and-forget on the scan-out channel (the connection this module already -/// holds): unlike the allocator exchange there is no reply, and a failure costs -/// nothing — the daemon's compile-time grace clears the splash anyway. Sent -/// once per session (the caller latches); idempotent on the daemon. -/// -/// A few attempts, reconnecting between them, exactly like [`allocate`]: the -/// daemon serves a **single** scan-out reader and closes whatever connection a -/// newer one supersedes, and the other client on this same listener (the -/// LD_PRELOAD shim in gamescope's process) reconnects on its own cadence — so -/// the descriptor this module cached is frequently already dead when the note -/// is sent. A single `sendmsg` on it fails with `EPIPE`, and because the caller -/// latches `GAME_WINDOW_REPORTED` before calling this, the note would be lost -/// for the session. Reconnecting makes this connection the newest, which is -/// what gets the note read. -pub fn report_game_window_present() { - let Ok(path) = std::env::var("GAMENATIVE_DRM_SOCKET") else { - return; - }; - if path.is_empty() { - return; - } - let note = GameWindowNote { - present: GameWindowNote::PRESENT, - }; - let mut ch = CHAN.lock().unwrap_or_else(|e| e.into_inner()); - for _ in 0..3 { - if *ch < 0 { - *ch = connect(&path); - if *ch < 0 { - return; - } - } - if send_note(*ch, note) { - return; - } - // SAFETY: `*ch` is a descriptor this module owns. - unsafe { libc::close(*ch) }; - *ch = -1; - } -} - -/// Send one notification frame; `false` when the channel did not accept it. No -/// reply is read: the daemon does not answer this message, so nothing is -/// expected back on the connection. -fn send_note(fd: RawFd, note: GameWindowNote) -> bool { - send_frame(fd, ¬e.encode()) -} - /// Send one frame that expects no reply; `false` when the channel did not -/// accept it. Shared by the two fire-and-forget frames on this channel: the -/// game-window notification and the frame-cap subscribe. +/// accept it: the frame-cap subscribe, the one fire-and-forget frame on this +/// channel. fn send_frame(fd: RawFd, wire: &[u8]) -> bool { // SAFETY: `fd` is the connection this module owns, `wire` outlives the // call. MSG_NOSIGNAL: a displaced or closed channel must fail rather than diff --git a/icd_shim/src/x11.rs b/icd_shim/src/x11.rs index 64286ba..21aeacf 100644 --- a/icd_shim/src/x11.rs +++ b/icd_shim/src/x11.rs @@ -46,7 +46,6 @@ use std::ffi::{c_char, c_int, c_void}; use std::ptr; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Mutex, MutexGuard}; // --------------------------------------------------------------- xcb ABI @@ -806,13 +805,6 @@ pub struct X11Present { /// window wine named (see `resolve_target`). Equal to the named window when /// that window is already a root child. window: u32, - /// The target window's `WM_CLASS` instance, read once at bring-up: the - /// desktop reference [`X11Present::window_present`] compares the bound - /// window against. Under `desktop=shell` the target is the wine desktop - /// window and this is its class (wine's `explorer.exe`), which the game's - /// own window differs from while wine's shell/taskbar children share it. - desktop_class: [u8; 32], - desktop_class_len: usize, /// The named window's offset within `window`; `Present` places the pixmap /// here, so a decorated or repositioned client window stays placed. x_off: i16, @@ -835,216 +827,6 @@ unsafe fn check_err(c: *mut xcb_connection_t, cookie: cookie_t) -> Option { Some(code) } -/// The child half of the game-window predicate: whether `bound` has a child -/// that is a real window of its own — viewable, `InputOutput`, non-degenerate, -/// and carrying a `WM_CLASS` instance that differs from `bound`'s own -/// (`own_class`). -/// -/// Shared by both routes through [`window_present`]: on the override/Wayland -/// route (the default) the bound window is the wine desktop window and this is -/// the whole signal, while on the guest-surface route (the opt-in) it covers the -/// case where the bound window is a container too. -/// -/// A class read that fails (an empty class) is treated as **not** a match: -/// wine's own shell and taskbar share the desktop's class, but an unreadable -/// class must never let one of them clear the splash over an empty desktop. -unsafe fn game_child_present(conn: *mut xcb_connection_t, bound: u32, own_class: &[u8]) -> bool { - if own_class.is_empty() { - return false; - } - let tree = unsafe { xcb_query_tree_reply(conn, xcb_query_tree(conn, bound), ptr::null_mut()) }; - if tree.is_null() { - return false; - } - let n = unsafe { (*tree).children_len as usize }; - let kids = unsafe { (tree as *const u8).add(32).cast::() }; - let mut found = false; - for i in 0..n { - let c = unsafe { ptr::read_unaligned(kids.add(i)) }; - let Some((map_state, class_, _)) = (unsafe { window_attr(conn, c) }) else { - continue; - }; - if map_state != MAP_STATE_VIEWABLE || class_ != WINDOW_CLASS_INPUT_OUTPUT { - continue; - } - let (w, h) = unsafe { window_size(conn, c) }; - if is_degenerate(w, h) { - continue; - } - let ccls = unsafe { window_class_instance(conn, c) }; - let clen = ccls.iter().position(|&b| b == 0).unwrap_or(ccls.len()); - // An unreadable class is unknown, never a match. - if clen == 0 { - continue; - } - if ccls[..clen] != own_class[..] { - found = true; - break; - } - } - unsafe { libc::free(tree.cast()) }; - found -} - -/// The nearest ancestor-or-self of `bound` that carries a readable `WM_CLASS` -/// instance, with that instance (the bound window itself and length 0 when the -/// walk finds none). -/// -/// wine hands the guest-surface route its *client* window, which carries no -/// `WM_CLASS` at all: the class sits on the frame one level up (`hades2.exe`), -/// one level above that the wine desktop window (`explorer.exe`, the root -/// child). So the predicate resolves the class through the ancestor chain -/// rather than insisting the bound window itself be classed. -/// -/// The walk climbs at most to the bound window's root child — under wine's -/// `/desktop=` that is the desktop window — and stops there, so it can never -/// cross into the desktop's siblings or the root. A window with no readable -/// class is simply skipped; an unreadable class is "unknown", never a match. -unsafe fn nearest_classed_ancestor( - conn: *mut xcb_connection_t, - bound: u32, -) -> (u32, [u8; 32], usize) { - let mut cur = bound; - for _ in 0..MAX_ANCESTOR_DEPTH { - let cls = unsafe { window_class_instance(conn, cur) }; - let clen = cls.iter().position(|&b| b == 0).unwrap_or(cls.len()); - if clen > 0 { - return (cur, cls, clen); - } - let tree = - unsafe { xcb_query_tree_reply(conn, xcb_query_tree(conn, cur), ptr::null_mut()) }; - if tree.is_null() { - break; - } - let (root, parent) = unsafe { ((*tree).root, (*tree).parent) }; - unsafe { libc::free(tree.cast()) }; - // The root child — the wine desktop window — is the boundary: read it, - // but never walk past it (into the root or a sibling subtree). - if cur == root || parent == root || parent == 0 || parent == cur { - break; - } - cur = parent; - } - (bound, [0u8; 32], 0) -} - -/// Whether the window a present route is **bound to** is the game's own window — -/// one predicate for both route shapes. -/// -/// The bound window is `bound`, and the desktop window's `WM_CLASS` instance is -/// `desktop_class` (an empty slice when it could not be read). The window shapes -/// the two routes bind are different, which is why the rule resolves the class -/// through the ancestor chain and then has two arms: -/// - guest-surface (the opt-in): the bound window is the guest's *own* window, -/// which wine leaves **classless** (its `WM_CLASS` sits on the frame one -/// level up), so the *nearest classed ancestor-or-self* is the signal. -/// - override/Wayland (the DEFAULT): the bound window is the wine *desktop* window, -/// whose own class *is* the desktop's, and the game appears as a differing -/// class child of it — nothing about the bound window itself says "game". The -/// degenerate-ancestor case (Dishonored, with A29 parked) binds a classless -/// window that is not the desktop and is decided here too, by the same walk. -/// -/// So the bound window counts as present when the **nearest classed -/// ancestor-or-self's `WM_CLASS` instance differs from the desktop's**, or when -/// it has a **non-degenerate, viewable child whose class differs** (see -/// [`game_child_present`]); a classless bound window is skipped by the walk, not -/// treated as absent. -/// -/// An unreadable class is never a match, on either side of either comparison: -/// wine's own shell and taskbar carry the desktop's class, but an unknown class -/// must never let one of them clear the splash over an empty desktop. -unsafe fn window_present(conn: *mut xcb_connection_t, bound: u32, desktop_class: &[u8]) -> bool { - // The nearest classed ancestor-or-self (the bound window itself when it is - // classed). An unreadable class is unknown: the walk skips it, so it never - // makes the first comparison pass, and it never short-circuits the second - // arm either — `own` stays empty and the child scan simply declines. - let (ancestor, cls, clen) = unsafe { nearest_classed_ancestor(conn, bound) }; - let own = &cls[..clen]; - let present = if !own.is_empty() && !desktop_class.is_empty() && own != desktop_class { - true - } else { - unsafe { game_child_present(conn, bound, own) } - }; - unsafe { log_predicate_once(conn, bound, ancestor, own, desktop_class, present) }; - present -} - -/// One-shot, ungated line naming how [`window_present`] resolved the bound -/// window: its own `WM_CLASS` length, the nearest classed ancestor-or-self the -/// walk found and that class, the desktop reference, and the verdict. -/// -/// The splash-clear predicate failed on the guest-surface route because the -/// bound window carries no `WM_CLASS` and an early guard returned before either -/// arm ran; nothing in logcat said so, and the investigation needed an external -/// X probe to see it. This fires once per session (the swap happens before any -/// read or format), off the per-frame path — [`window_present`] runs at the -/// tree-walk cadence, never per frame — and is deliberately **not** behind -/// `GNDRM_LOG_VKSHIM`, so a real session shows it without a knob. -unsafe fn log_predicate_once( - conn: *mut xcb_connection_t, - bound: u32, - ancestor: u32, - own: &[u8], - desktop_class: &[u8], - present: bool, -) { - static LOGGED: AtomicBool = AtomicBool::new(false); - if LOGGED.swap(true, Ordering::Relaxed) { - return; - } - let bound_cls = unsafe { window_class_instance(conn, bound) }; - let bound_len = bound_cls.iter().position(|&b| b == 0).unwrap_or(0); - eprintln!( - "{} {}", - gndrm_obf::obf!("gn-vkshim:"), - gndrm_obf::obf!( - "x11 game-window predicate: bound=0x{:x} bound_class_len={} nearest_classed=0x{:x} class={} desktop_class={} present={}", - bound, - bound_len, - ancestor, - String::from_utf8_lossy(own), - String::from_utf8_lossy(desktop_class), - present - ) - ); -} - -/// Whether the window the override/Wayland route is bound to is the game's own -/// window (see [`window_present`]). -/// -/// That route (the default; `proxy::xlib_override_surface`, -/// `GN_VKSHIM_GUEST_SURFACE` unset or not exactly "1") holds no persistent X -/// connection, so — like [`resolve_override_target`] and -/// [`window_extent`] — the walk opens its own connection, reads the bound -/// window's `WM_CLASS`, runs [`window_present`] and closes it. On this route the -/// bound window is the wine desktop window, so its own class *is* the desktop's -/// and the class comparison falls through to the child scan; the same child scan -/// is what decides the degenerate-ancestor case, where the bound window is not -/// the desktop. The connection is live for the walk; nothing has to be kept -/// between walks. -/// -/// This is an X round trip, so the caller runs it at the tree-walk cadence -/// (every *N* presents, until the game is first reported), never per frame. The -/// transition it detects is one-shot, so tens of milliseconds of latency — and a -/// fresh connection per walk, which stops entirely once reported — cost nothing. -pub unsafe fn game_window_present(window: u32) -> bool { - if window == 0 { - return false; - } - let conn = unsafe { xcb_connect(ptr::null(), ptr::null_mut()) }; - if conn.is_null() || unsafe { xcb_connection_has_error(conn) } != 0 { - if !conn.is_null() { - unsafe { xcb_disconnect(conn) }; - } - return false; - } - let cls = unsafe { window_class_instance(conn, window) }; - let clen = cls.iter().position(|&b| b == 0).unwrap_or(cls.len()); - let present = unsafe { window_present(conn, window, &cls[..clen]) }; - unsafe { xcb_disconnect(conn) }; - present -} - impl X11Present { /// Opens the guest's X connection and validates the window + DRI3/Present. /// `Err` with a named reason when the route cannot work; the caller then @@ -1121,21 +903,10 @@ impl X11Present { unsafe { libc::free(pv.cast()) }; v }; - // The desktop window's own `WM_CLASS`, read once: the game's window is - // told apart from wine's shell/taskbar by carrying a different one (see - // `window_present`). Length 0 means the class could not be read, which - // is treated as "unknown", never a match. - let desktop_class = unsafe { window_class_instance(conn, target_window) }; - let desktop_class_len = desktop_class - .iter() - .position(|&b| b == 0) - .unwrap_or(desktop_class.len()); let present = X11Present { lock: Mutex::new(()), conn, window: target_window, - desktop_class, - desktop_class_len, x_off, y_off, depth, @@ -1156,38 +927,6 @@ impl X11Present { self.window } - /// Whether the window the guest-surface route is bound to is the game's own - /// window, on this present's own live connection (see [`window_present`] for - /// the rule). - /// - /// `bound` is the window the route bound — `GnSurface::window`, i.e. the - /// guest's own X window on this route — and the desktop reference is this - /// present's target window's class, read once at bring-up. Under - /// `desktop=shell` the tree is depth 3 — root -> the wine desktop window -> - /// the game's whole_window (override-redirect) -> the named client window - /// the shim was handed — and that named client window is **classless**, so - /// the predicate resolves the class from its nearest classed ancestor (the - /// game's frame) before comparing; a bound window that is a container - /// instead falls through to the child scan. - /// - /// This is an X round trip, so the caller runs it at the tree-walk cadence - /// (every *N* presents, until it first reports), never per frame. The - /// transition it detects is one-shot, so tens of milliseconds of latency - /// cost nothing. - pub unsafe fn window_present(&self, bound: u32) -> bool { - if bound == 0 { - return false; - } - let _g = self.lock(); - unsafe { - window_present( - self.conn, - bound, - &self.desktop_class[..self.desktop_class_len], - ) - } - } - /// One-shot dump of the named window, its parent chain up to the root, and /// the resolved present target, printed unconditionally like the route /// line. From 13aa4c5572c49588cd4eaf5e13b7cab6b3b4acfe Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Fri, 23 Oct 2026 14:12:25 +0530 Subject: [PATCH 2/2] daemon: clear the booting splash when the game-window note arrives The note now comes from gamescope on the first top-level X map, the same rule the X11 path uses, and the game-session desktop is black. Waiting for the next published frame no longer hides anything, so the scan-out reader clears the splash on the note's first arrival in the session and the presenter's latch goes away. --- modules/gndrm-android/src/asc.rs | 30 +----------------------------- modules/gndrm-android/src/lib.rs | 31 ++++++++++++++++--------------- 2 files changed, 17 insertions(+), 44 deletions(-) diff --git a/modules/gndrm-android/src/asc.rs b/modules/gndrm-android/src/asc.rs index ba877f5..fc959a6 100644 --- a/modules/gndrm-android/src/asc.rs +++ b/modules/gndrm-android/src/asc.rs @@ -710,9 +710,6 @@ pub struct AscState { pool: Arc>, last_publish: Option, frames: u64, - /// The booting-splash clear has fired this session (one latch, single - /// condition). - splash_cleared: bool, warned_fence_timeout: bool, /// Inodes already reported as having no registered buffer (log once each). logged_unregistered: Vec, @@ -795,7 +792,6 @@ impl AscState { )), last_publish: None, frames: 0, - splash_cleared: false, warned_fence_timeout: false, logged_unregistered: Vec::new(), logged_stale: Vec::new(), @@ -1240,30 +1236,6 @@ impl AscState { self.frames += 1; if fb.id != 0 { - // Latched booting-splash clear. The trigger is the game's own - // window appearing, reported by the ICD shim over the scan-out - // channel. The desktop's first frame is deliberately not a trigger: - // it is exactly the blue desktop the splash exists to cover. One - // latch, so the clear fires once per session. - if !self.splash_cleared && crate::GAME_WINDOW_PRESENT.load(Ordering::Relaxed) { - self.splash_cleared = true; - // UNGATED one-shot at the latch-fired moment. Latched on the - // *clear*, not on the first published frame: the first frame is - // the desktop, ~10 s before the note, so a line there can only - // report the pre-latch state and never observes this event. One - // relaxed atomic per latch; nothing formats on a later frame. - { - static CLEAR_FIRED: AtomicBool = AtomicBool::new(false); - if !CLEAR_FIRED.swap(true, Ordering::Relaxed) { - asc_log!( - "{}", - gndrm_obf::obf!("gndrm-daemon: splash-clear latch fired splash_cleared={} game_window_present={} clear_attempted=true",self.splash_cleared, - crate::GAME_WINDOW_PRESENT.load(Ordering::Relaxed)) - ); - } - } - crate::clear_booting_splash(); - } // Announce the daemon's new-frame decision to the app's frame-time // ring: only a changed primary FB is published (a re-commit of the // frame already shown is not — see `apply_commit` in `commit.rs`), @@ -1752,7 +1724,7 @@ pub fn serve_scanout( // The game-window notification shares this channel. It is recognised // by its own magic *before* the allocator decode, so a daemon that knows // it never mistakes it for, or rejects it as, a scan-out request. It is - // fire-and-forget: no reply is sent, and the shim reads none. + // fire-and-forget: no reply is sent, and the sender reads none. if let Some(note) = GameWindowNote::decode(&buf[..n]) { // Latch first, report second: the line reads GAME_WINDOW_PRESENT, so // logged ahead of the store it could only ever print `false`, diff --git a/modules/gndrm-android/src/lib.rs b/modules/gndrm-android/src/lib.rs index 6193922..f051d53 100644 --- a/modules/gndrm-android/src/lib.rs +++ b/modules/gndrm-android/src/lib.rs @@ -68,10 +68,10 @@ const E_IO: jint = -5; /// `nativeRetargetSurface` with no running session; the caller starts one. const E_NOENT: jint = -2; -/// Whether the game's own window has appeared this session, as reported by the -/// ICD shim over the scan-out channel (a [`gndrm_uapi::wire::GameWindowNote`]). -/// The presenter's latched booting-splash clear reads it; reset alongside the -/// rest of the session state by `nativeStart`/`nativeStop`. +/// Whether the game's window has appeared this session, as reported by +/// gamescope over the scan-out channel (a [`gndrm_uapi::wire::GameWindowNote`]). +/// Its first transition clears the booting splash; reset alongside the rest of +/// the session state by `nativeStart`/`nativeStop`. pub(crate) static GAME_WINDOW_PRESENT: AtomicBool = AtomicBool::new(false); /// The session's fps cap: the target the app set, which the scan-out channel @@ -174,14 +174,14 @@ pub extern "system" fn Java_app_gamenative_display_GameNativeDrm_nativeSetLsfgCo static PANEL_REFRESH_MHZ: AtomicU32 = AtomicU32::new(0); /// The app's `JavaVM`, cached from the first JNI entry point (`nativeStart`). -/// Needed by the presenter thread, which is a Rust-spawned thread with no -/// `JNIEnv` of its own: it attaches with [`JavaVM::attach_current_thread`] to -/// emit the booting-splash event. Set once per process; a missing VM only means -/// the event cannot be pushed. +/// Needed by the presenter and scan-out threads, which are Rust-spawned threads +/// with no `JNIEnv` of their own: they attach with +/// [`JavaVM::attach_current_thread`] to emit the app's events. Set once per +/// process; a missing VM only means the events cannot be pushed. static JAVA_VM: OnceLock = OnceLock::new(); /// Global ref to `GameNativeDrm`, resolved on a Java thread by -/// [`cache_java_vm`]. A name lookup from the presenter thread cannot work: an +/// [`cache_java_vm`]. A name lookup from a Rust-spawned thread cannot work: an /// attached native thread has no context class loader, so `FindClass` falls back /// to the system loader, which cannot see app classes. static CALLBACK_CLASS: OnceLock = OnceLock::new(); @@ -275,15 +275,16 @@ fn set_frame_cap_target(fps: u32) { } } -/// Latch [`GAME_WINDOW_PRESENT`]. Called by the scan-out channel reader when the -/// ICD shim reports the game's own window as present. Idempotent, so a repeat -/// notification is free. +/// Latch [`GAME_WINDOW_PRESENT`] and clear the booting splash on its first +/// transition this session. Called by the scan-out channel reader when a +/// game-window note arrives. Idempotent, so gamescope's resends are free. pub(crate) fn note_game_window_present() { if !GAME_WINDOW_PRESENT.swap(true, Ordering::Relaxed) { log_info!( "{}", - gndrm_obf::obf!("daemon: game window present reported by the shim") + gndrm_obf::obf!("daemon: game window present reported") ); + clear_booting_splash(); } } @@ -785,8 +786,8 @@ fn start( // all), so the per-frame diagnostics must re-read it rather than carry the // previous session's verdict into this one. reset_verbose_logging(); - // First JNI entry point to run: capture the VM so the presenter thread can - // push the booting-splash event when the game's own window is reported. + // First JNI entry point to run: capture the VM so the scan-out thread can + // push the booting-splash event when the game's window is reported. cache_java_vm(env); let name: String = match env.get_string(socket_name) { Ok(s) => s.to_string_lossy().into_owned(),