Skip to content

Wayland layer-shell overlay (always-on-top over fullscreen games) - #6

Open
raman78 wants to merge 3 commits into
AnotherNathan:masterfrom
raman78:pr/overlay
Open

Wayland layer-shell overlay (always-on-top over fullscreen games)#6
raman78 wants to merge 3 commits into
AnotherNathan:masterfrom
raman78:pr/overlay

Conversation

@raman78

@raman78 raman78 commented Jul 25, 2026

Copy link
Copy Markdown

What & why

On Wayland the winit/eframe path cannot keep the metrics overlay above a full-screen game — the compositor simply won't honor always-on-top for a regular top-level. This PR renders the overlay through wlr-layer-shell on the dedicated overlay layer in a Wayland session, which is the reliable way to sit above fullscreen.

X11, Windows and macOS keep the existing always-on-top viewport, unchanged.

Note that on X11 this is only as good as the window manager's handling of _NET_WM_STATE_ABOVE: testing on KWin (via XWayland) the overlay window does appear, but it does not stay above a full-screen game. That is the pre-existing behaviour of this path and the reason the Wayland back end exists — the point of the runtime choice below is that an X11 session gets the same overlay it has today, rather than a button that silently does nothing.

Overlay

  • Renders the overlay via a wlr-layer-shell surface in a Wayland session (smithay-client-toolkit + wgpu + egui), so it stays above fullscreen games.
  • Draggable, with a click-through body so it doesn't steal game input.
  • Remembers its position across restarts.
  • Matches the main window's colors.
  • Fixes for crashes when toggling the overlay off/on, closing it, and a 0×0 initial-size crash on Wayland (also proposed on its own in Fix the overlay crashing the program on Wayland #9).
  • Architecture is documented in docs/OVERLAY.md.

Choosing the back end

Which back end is used is decided at runtime, not with #[cfg(target_os = "linux")]. A single Linux build has to serve both session types, and wlr-layer-shell is a Wayland protocol: in an X11 session there is nothing to connect to, and picking it there would leave the Overlay button doing nothing.

App::new only injects the shared wgpu handles when eframe's CreationContext reports a RawDisplayHandle::Wayland — the back end winit actually picked, rather than a guess — and having those handles is what OverlayInner::uses_layer_shell() asks about, so the two cannot disagree. A session without them falls back to the viewport. main.rs makes the same call one step earlier, before any window exists, the way winit does it, so an X11 session does not build a shared wgpu stack it will not use.

The selected back end is logged once at startup (overlay backend: ...).

Not included

The main-window geometry changes that used to be part of this PR (remembering the size, the redraw while resizing, the larger minimum size) had nothing to do with the overlay and are proposed separately in #8.

Notes

The Wayland/wgpu dependencies are Linux-only ([target.'cfg(target_os = "linux")'.dependencies]).

Verified on KDE: the layer-shell back end in a Wayland session, and the viewport back end in an X11 session (via env -u WAYLAND_DISPLAY).

Overlap with #8

Both this PR and #8 change the same lines of src/app/mod.rs and src/main.rs — this branch removes the window-geometry code that used to sit there, #8 adds its own version in the same place — so whichever of the two you merge first, the other will report a conflict.

Nothing for you to resolve: merge either one in any order and I will rebase the other branch and push, which updates its PR automatically. The two changes are independent in substance; only the text overlaps.

@raman78

raman78 commented Jul 25, 2026

Copy link
Copy Markdown
Author

Pushed a revision addressing the feedback:

  • Single wgpu stack. The app now creates one wgpu::Instance/adapter/device/queue at startup and hands it to eframe via WgpuSetup::Existing; the layer-shell overlay reuses the very same handles (injected through App::newOverlay::set_gpu). No more second instance/device, and this also let me drop the old "keep the instance alive across show/hide" workaround that only existed to dodge a Vulkan teardown segfault on overlay close.

    One wrinkle worth noting: eframe 0.34's public RenderState exposes adapter/device/queue but not the instance (it's held privately in the Painter). So rather than grabbing eframe's instance after the fact, we create the instance ourselves and give it to eframe — WgpuSetup::Existing uses it verbatim for the main window, and we keep a clone for the overlay's surface. Same single instance either way.

  • Module folder. The overlay moved into app::overlay, with the Linux layer-shell backend under overlay::layer_shell.

  • While in there I also modernized some deprecated egui calls (Context::run/style, CentralPanel::show, SelectableLabel) and switched the overlay to adopt the main window's full Style instead of just its Visuals.

One thing I deliberately left out: proper HiDPI/fractional-scale handling for the overlay surface (it still renders at ppp = 1.0). I'm on a 1× display and didn't want to land scaling code I can't actually verify — happy to do it as a follow-up if you'd like it in this PR.

Let me know what else you'd like changed.

Render the always-on-top metrics overlay through a wlr-layer-shell
surface on Linux (the winit/eframe always-on-top hint is ignored on
Wayland, so the overlay could never stay above a full-screen game). The
overlay is draggable with a click-through body, remembers its position,
and adopts the main window's egui style.

It shares a single wgpu instance/adapter/device/queue with eframe
(created at startup and handed to eframe via WgpuSetup::Existing), so the
app runs one Vulkan stack rather than two. The Linux backend lives under
app::overlay::layer_shell; the approach is documented in docs/OVERLAY.md.

Also remember the main window's size and maximized state across restarts
and fix a couple of Wayland-specific window issues (a 0x0 initial-size
crash and jittery resizing).
Remembering the main window size, raising its minimum size and the
redraw-while-resizing tweak have nothing to do with the overlay, and
carrying them here made this branch touch main.rs and the settings
struct for unrelated reasons.

The window size persistence is proposed separately. The minimum size and
the resize redraw stay on the development branches for now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@raman78 raman78 changed the title Wayland layer-shell overlay (always-on-top over fullscreen games) + window-geometry fixes Wayland layer-shell overlay (always-on-top over fullscreen games) Jul 26, 2026
@raman78

raman78 commented Jul 26, 2026

Copy link
Copy Markdown
Author

Narrowed this down to the overlay. The main-window geometry work that was bundled in here (remembering the window size, redrawing while resizing, and the larger minimum size) was unrelated to it and made this branch touch main.rs and the settings struct for reasons a reviewer of an overlay change should not have to care about. It now lives in #8, and the diff here no longer changes anything about the main window.

The overlay chose its back end with #[cfg(target_os = "linux")], so every
Linux build used wlr-layer-shell. That is a Wayland protocol, and in an
X11 session there is nothing to connect to: the layer thread failed at
Connection::connect_to_env, logged an error and exited, leaving the
Overlay button doing nothing at all. Before this branch those users had a
working always-on-top overlay window.

Both back ends are now compiled on Linux and the choice is made while the
app runs. App::new only injects the shared wgpu handles when eframe's
CreationContext reports a RawDisplayHandle::Wayland, which is the back
end winit actually picked rather than a guess, and having those handles
is what OverlayInner::uses_layer_shell asks about. A session without them
falls back to the viewport instead of failing, so the two can never
disagree.

main.rs makes the same call one step earlier, before any window exists,
the way winit does it (WAYLAND_DISPLAY / WAYLAND_SOCKET), so an X11
session no longer builds a shared wgpu stack it will not use — and no
longer runs the two expects inside create_shared_gpu for a back end it
cannot reach.

The chosen back end is logged once at startup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@raman78

raman78 commented Jul 26, 2026

Copy link
Copy Markdown
Author

Follow-up: the back end is now picked per session rather than per target OS. As it stood, every Linux build went to wlr-layer-shell, which meant that in an X11 session the layer thread failed at Connection::connect_to_env, logged an error and exited, and the Overlay button did nothing — a regression against the always-on-top window those users have today. Both back ends are now compiled on Linux and the choice comes from the display handle eframe reports. Verified on KDE in both session types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant