From 0e52274053a3a5334e80becead93eb741885d640 Mon Sep 17 00:00:00 2001 From: "Pedro M. de Echanove Pasquin" Date: Thu, 21 May 2026 01:51:19 +0200 Subject: [PATCH] =?UTF-8?q?app,=20event=5Floop:=20first-frame-committed=20?= =?UTF-8?q?hook=20and=20foreign=5Ftoplevel=20app=5Fid=20only=20Two=20indep?= =?UTF-8?q?endent=20changes,=20both=20blockers=20for=20a=20working=20deskt?= =?UTF-8?q?op=20session=20through=20the=20loginmanager-daemon=20handoff=20?= =?UTF-8?q?and=20the=20dock's=20running-app=20icons.=20`App::on=5Ffirst=5F?= =?UTF-8?q?frame=5Fcommitted`=20is=20a=20new=20trait=20hook=20fired=20exac?= =?UTF-8?q?tly=20once,=20immediately=20after=20the=20very=20first=20`wl=5F?= =?UTF-8?q?surface.commit`=20of=20a=20rendered=20buffer=20on=20the=20main?= =?UTF-8?q?=20surface.=20`AppData`=20grows=20a=20`first=5Fframe=5Fcommitte?= =?UTF-8?q?d:=20bool`,=20`draw=5Fframe`=20now=20returns=20whether=20this?= =?UTF-8?q?=20call=20performed=20that=20first=20commit,=20and=20`try=5Frun?= =?UTF-8?q?`=20invokes=20the=20hook=20after=20the=20borrows=20held=20durin?= =?UTF-8?q?g=20the=20draw=20are=20released.=20Used=20by=20the=20loginmanag?= =?UTF-8?q?er-daemon-aware=20crustace=20path=20to=20signal=20"ready=20to?= =?UTF-8?q?=20be=20presented"=20back=20to=20the=20daemon=20as=20soon=20as?= =?UTF-8?q?=20the=20GPU=20has=20the=20first=20frame=20=E2=80=94=20the=20ac?= =?UTF-8?q?tual=20present=20can=20still=20be=20deferred=20under=20VT=20swi?= =?UTF-8?q?tching=20(no=20DRM=20master=20yet),=20but=20the=20client-side?= =?UTF-8?q?=20commit=20is=20the=20right=20edge=20for=20handoff.=20`topleve?= =?UTF-8?q?l=5Fdisplay=5Fid`=20in=20the=20foreign-toplevel-list=20handler?= =?UTF-8?q?=20no=20longer=20falls=20back=20to=20`info.title`=20or=20`info.?= =?UTF-8?q?identifier`=20when=20`info.app=5Fid`=20is=20empty.=20Smithay=20?= =?UTF-8?q?creates=20each=20`ext-foreign-toplevel-list-v1`=20handle=20with?= =?UTF-8?q?=20`app=5Fid=20=3D=20""`=20and=20`init=5Fnew=5Finstance`=20flus?= =?UTF-8?q?hes=20a=20`done`=20immediately,=20so=20subscribers=20used=20to?= =?UTF-8?q?=20see=20the=20protocol-level=20identifier=20(a=2032-char=20`Al?= =?UTF-8?q?phanumeric`=20random=20token)=20as=20the=20"app=20id"=20of=20ev?= =?UTF-8?q?ery=20new=20toplevel=20=E2=80=94=20and=20remained=20stuck=20on?= =?UTF-8?q?=20it=20whenever=20the=20client's=20real=20`set=5Fapp=5Fid`=20a?= =?UTF-8?q?rrived=20between=20done=20events=20but=20the=20subscribing=20ap?= =?UTF-8?q?p's=20matcher=20couldn't=20resolve=20it=20to=20a=20`.desktop`?= =?UTF-8?q?=20entry.=20Returning=20the=20raw=20`app=5Fid`=20(empty=20or=20?= =?UTF-8?q?not)=20makes=20that=20first=20transient=20`done`=20ignorable=20?= =?UTF-8?q?by=20the=20consumer's=20own=20empty-string=20guard;=20the=20sec?= =?UTF-8?q?ond=20`done`,=20carrying=20the=20real=20app=20id,=20is=20proces?= =?UTF-8?q?sed=20normally.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.rs | 8 ++++++++ src/event_loop/app_data.rs | 3 +++ src/event_loop/frame.rs | 13 ++++++++++++- src/event_loop/handlers.rs | 12 +++++++++--- src/event_loop/run.rs | 7 ++++++- 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/app.rs b/src/app.rs index 32200fd..af58467 100644 --- a/src/app.rs +++ b/src/app.rs @@ -293,6 +293,14 @@ pub trait App: 'static /// Return any pending messages from external sources (timers, async, etc.). fn poll_external( &mut self ) -> Vec { vec![] } + /// Called once, immediately after the first buffer for the main surface + /// has been rendered and committed to the compositor. Note: this is the + /// client-side commit, not the compositor's present — under VT switching + /// the actual present can be deferred (no DRM master yet), so a handoff + /// signal that fires here is enough to tell a parent "ready to be + /// presented as soon as my VT becomes active". + fn on_first_frame_committed( &mut self ) {} + /// Called when the surface is asked to close (compositor request, titlebar button, /// layer-shell closed event). Return `true` to allow the application to exit, /// or `false` to cancel. diff --git a/src/event_loop/app_data.rs b/src/event_loop/app_data.rs index 88bf2e4..61a7cf7 100644 --- a/src/event_loop/app_data.rs +++ b/src/event_loop/app_data.rs @@ -232,6 +232,9 @@ pub struct AppData pub view_dirty: bool, /// Counterpart of `view_dirty` for `cached_overlays`. pub overlays_dirty: bool, + /// Set after the first commit of a rendered buffer on the main surface. + /// Used to drive `App::on_first_frame_committed` exactly once. + pub first_frame_committed: bool, } impl AppData diff --git a/src/event_loop/frame.rs b/src/event_loop/frame.rs index c270579..eae5612 100644 --- a/src/event_loop/frame.rs +++ b/src/event_loop/frame.rs @@ -31,7 +31,10 @@ pub( crate ) fn pick_shm_format( shm: &Shm ) -> ( wl_shm::Format, bool ) } } -pub( crate ) fn draw_frame( data: &mut AppData ) +/// Returns `true` when this call performed the very first commit on the main +/// surface — the run loop uses that to fire `App::on_first_frame_committed` +/// exactly once, after the borrows held by draw_frame have been released. +pub( crate ) fn draw_frame( data: &mut AppData ) -> bool { let main_view = data.cached_view.as_ref().expect( "view cache populated" ); let overlays = data.cached_overlays.as_ref().expect( "overlays cache populated" ); @@ -43,6 +46,7 @@ pub( crate ) fn draw_frame( data: &mut AppData ) let egl_ctx = data.egl_context.as_ref(); let qh = &data.qh; + let mut first_commit_now = false; if data.main.configured && data.main.needs_redraw && !data.main.frame_pending { let req_frame = | wl: &WlSurface | { let _ = wl.frame( qh, SurfaceFocus::Main ); }; @@ -60,6 +64,11 @@ pub( crate ) fn draw_frame( data: &mut AppData ) ); data.main.needs_redraw = false; data.main.last_draw = std::time::Instant::now(); + if !data.first_frame_committed + { + data.first_frame_committed = true; + first_commit_now = true; + } } for spec in overlays @@ -86,6 +95,8 @@ pub( crate ) fn draw_frame( data: &mut AppData ) ss.last_draw = std::time::Instant::now(); } } + + first_commit_now } fn draw_surface( diff --git a/src/event_loop/handlers.rs b/src/event_loop/handlers.rs index 57cbc65..04f9848 100644 --- a/src/event_loop/handlers.rs +++ b/src/event_loop/handlers.rs @@ -468,10 +468,16 @@ fn toplevel_display_id( handle: &ExtForeignToplevelHandleV1, ) -> String { + // Only ever surface the client's `app_id`. The protocol-level + // `identifier` is a server-internal random opaque token, not an + // app handle; smithay creates the toplevel handle with an empty + // `app_id` and `init_new_instance` flushes a `done` immediately, + // so apps subscribing through this binding would otherwise see + // the random identifier as their "app id" until the client's + // real `set_app_id` lands. `title` is also unsuitable — it + // changes every time the window's title text updates. let Some( info ) = list.info( handle ) else { return String::new(); }; - if !info.app_id.is_empty() { return info.app_id; } - if !info.title.is_empty() { return info.title; } - info.identifier + info.app_id } impl ForeignToplevelListHandler for AppData diff --git a/src/event_loop/run.rs b/src/event_loop/run.rs index afc9551..d95c4ca 100644 --- a/src/event_loop/run.rs +++ b/src/event_loop/run.rs @@ -300,6 +300,7 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> cached_overlays: None, view_dirty: true, overlays_dirty: true, + first_frame_committed: false, }; // Register a calloop channel so the app can send messages from any thread. @@ -525,7 +526,11 @@ pub( crate ) fn try_run( app: A ) -> Result<(), RunError> data.cached_overlays = Some( specs ); data.overlays_dirty = false; } - draw_frame( &mut data ); + let first_commit_now = draw_frame( &mut data ); + if first_commit_now + { + data.app.on_first_frame_committed(); + } // Push the freshly-laid-out widget tree to AccessKit. The // closure only runs when an AT client is actually