From 3c9e30c61b16b5416d8f8e7bc78058da48d1b5d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?N=C3=ADcolas=20F=2E=20R=2E=20A=2E=20Prado?= Date: Fri, 9 Oct 2026 10:39:54 -0400 Subject: [PATCH] Turn off screen during system sleep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hook into the PrepareForSleep D-Bus signal from logind and trigger screen sleep before the system goes to sleep. This improves the user experience when the system goes into hibernation, since currently during the time window between when userspace threads get frozen and the display driver in the kernel gets suspended, which can take a few seconds, the last image on the screen stays frozen. Furthermore, on some platforms like the Steam Machine, during this time window the last second of audio also gets repeated in a loop. By turning off the display early in the hibernation process, we avoid both issues and give a smoother experience to the user. This also mimics the behavior from KDE plasma. In order to achieve this: * Extend the backend interface to expose screen sleep functionality (which is currently only supported by the DRM backend) and make the new code a no-op when the backend doesn't support it. * Spawn a new thread and rely on libsystemd to listen for the PrepareForSleep signal from logind and turn the display off/on in response, and take an inhibitor lock to delay system sleep until the screen has been turned off. * In the DRM backend, make the atomic commit blocking for screen sleep operations and update the sleep status after commit to allow callers to wait for the sleep to actually be applied on hardware. The introduced dependency on libsystemd is optional and in practice is not new: the only backend that currently supports screen sleep is DRM, and it already requires libsystemd indirectly, since it requires the libinput backend in wlroots, which requires libseat, which in turn requires libsystemd. Signed-off-by: NĂ­colas F. R. A. Prado --- src/Backends/DRMBackend.cpp | 58 ++++++++- src/Backends/DeferredBackend.h | 37 ++++++ src/backend.h | 19 +++ src/logind_sleep.cpp | 219 +++++++++++++++++++++++++++++++++ src/main.cpp | 8 ++ src/meson.build | 8 +- src/steamcompmgr.cpp | 8 ++ src/wlserver.cpp | 6 +- 8 files changed, 354 insertions(+), 9 deletions(-) create mode 100644 src/logind_sleep.cpp diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 9aaab7b36..0453bf6ce 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -13,9 +13,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -599,6 +601,7 @@ static std::unordered_map< std::string, std::string > pnps = {}; static void drm_unset_mode( struct drm_t *drm, bool force ); static void drm_unset_connector( struct drm_t *drm ); +static void drm_update_screen_disabled( struct drm_t *drm ); static constexpr uint32_t s_kSteamDeckLCDRates[] = { @@ -934,6 +937,8 @@ static bool refresh_state( drm_t *drm ) for ( std::unique_ptr< gamescope::CDRMPlane > &pPlane : drm->planes ) pPlane->RefreshState(); + drm_update_screen_disabled( drm ); + return true; } @@ -1698,12 +1703,26 @@ gamescope::ConVar cv_drm_sleep_screens[] = { "drm_sleep_external_screen", false, "Force the external screen to be asleep", OnSleepScreenChanged }, }; -void drm_sleep_screen( gamescope::GamescopeScreenType eType, bool bSleep ) +static std::mutex s_ScreenSleepMutex; +static std::condition_variable s_ScreenSleepCV; +static bool s_bScreenDisabled[ gamescope::GAMESCOPE_SCREEN_TYPE_COUNT ] = {}; + +static void drm_update_screen_disabled( struct drm_t *drm ) { - if ( cv_drm_sleep_screens[ eType ] == bSleep ) - return; + { + std::lock_guard lock( s_ScreenSleepMutex ); + for ( bool &bDisabled : s_bScreenDisabled ) + bDisabled = true; - cv_drm_sleep_screens[ eType ] = bSleep; + for ( auto &iter : drm->connectors ) + { + gamescope::CDRMConnector *pConnector = &iter.second; + if ( pConnector->GetModeConnector()->connection == DRM_MODE_CONNECTED && + pConnector->GetProperties().CRTC_ID->GetCurrentValue() != 0 ) + s_bScreenDisabled[ pConnector->GetScreenType() ] = false; + } + } + s_ScreenSleepCV.notify_all(); } @@ -3279,6 +3298,10 @@ int drm_prepare( struct drm_t *drm, bool async, const struct FrameInfo_t *frameI if ( bCurrentlyAsleep != bSleep ) needs_modeset = true; + + // Make screen sleep commits synchronous so we can easily wait for them + if ( bSleep && bCurrentlyAsleep != bSleep ) + flags &= ~DRM_MODE_ATOMIC_NONBLOCK; } if ( !bSleep ) @@ -4305,6 +4328,30 @@ namespace gamescope return g_bSupportsSyncObjs && !cv_drm_debug_disable_explicit_sync; } + virtual bool SupportsScreenSleep() const override + { + return true; + } + + void SleepScreen( gamescope::GamescopeScreenType eType, bool bSleep ) override + { + if ( cv_drm_sleep_screens[ eType ] == bSleep ) + return; + + cv_drm_sleep_screens[ eType ] = bSleep; + } + + bool ScreenAsleep( gamescope::GamescopeScreenType eType ) override + { + return cv_drm_sleep_screens[ eType ]; + } + + bool WaitForScreenAsleep( gamescope::GamescopeScreenType eType, uint32_t uTimeoutMs ) override + { + std::unique_lock lock( s_ScreenSleepMutex ); + return s_ScreenSleepCV.wait_for( lock, std::chrono::milliseconds( uTimeoutMs ), [ eType ]{ return s_bScreenDisabled[ eType ]; } ); + } + virtual bool IsPaused() const override { return g_DRM.paused; @@ -4455,6 +4502,9 @@ namespace gamescope oProperty->OnCommit(); } } + + if (drm->flags & DRM_MODE_ATOMIC_ALLOW_MODESET) + drm_update_screen_disabled( drm ); } // Update the draw time diff --git a/src/Backends/DeferredBackend.h b/src/Backends/DeferredBackend.h index 2829fdfb6..58c1324bf 100644 --- a/src/Backends/DeferredBackend.h +++ b/src/Backends/DeferredBackend.h @@ -239,6 +239,43 @@ namespace gamescope return m_pChild->SupportsExplicitSync(); } + virtual bool SupportsScreenSleep() const override + { + // Doesn't need to be 'initted' for this check. + return m_pChild->SupportsScreenSleep(); + } + + virtual void SleepScreen( gamescope::GamescopeScreenType eType, bool bSleep ) override + { + { + std::shared_lock lock{ m_mutInit }; + if ( m_bInittedChild ) + return m_pChild->SleepScreen( eType, bSleep ); + } + } + + virtual bool ScreenAsleep( gamescope::GamescopeScreenType eType ) override + { + { + std::shared_lock lock{ m_mutInit }; + if ( m_bInittedChild ) + return m_pChild->ScreenAsleep( eType ); + } + + return false; + } + + virtual bool WaitForScreenAsleep( gamescope::GamescopeScreenType eType, uint32_t uTimeoutMs ) override + { + { + std::shared_lock lock{ m_mutInit }; + if ( m_bInittedChild ) + return m_pChild->WaitForScreenAsleep( eType, uTimeoutMs ); + } + + return false; + } + virtual bool IsPaused() const override { { diff --git a/src/backend.h b/src/backend.h index a1546c1db..d55fa8fb0 100644 --- a/src/backend.h +++ b/src/backend.h @@ -367,6 +367,25 @@ namespace gamescope virtual bool SupportsExplicitSync() const = 0; + virtual bool SupportsScreenSleep() const + { + return false; + } + + virtual void SleepScreen( gamescope::GamescopeScreenType eType, bool bSleep ) + { + } + + virtual bool ScreenAsleep( gamescope::GamescopeScreenType eType ) + { + return false; + } + + virtual bool WaitForScreenAsleep( gamescope::GamescopeScreenType eType, uint32_t uTimeoutMs ) + { + return false; + } + // Dumb helper we should remove to support multi display someday. gamescope::GamescopeScreenType GetScreenType() { diff --git a/src/logind_sleep.cpp b/src/logind_sleep.cpp new file mode 100644 index 000000000..d7efb2cd7 --- /dev/null +++ b/src/logind_sleep.cpp @@ -0,0 +1,219 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "backend.h" + +static LogScope logind_sleep_log("logind_sleep"); + +static const char *logind_service = "org.freedesktop.login1"; +static const char *logind_path = "/org/freedesktop/login1"; +static const char *logind_interface = "org.freedesktop.login1.Manager"; + +static const uint32_t screen_sleep_timeout_ms = 2000; + +struct logind_sleep_state { + sd_bus *bus; + int inhibit_lock_fd; + int stop_fd; + std::mutex stop_fd_mutex; + bool previous_screen_sleep_status[gamescope::GAMESCOPE_SCREEN_TYPE_COUNT]; + std::thread thread; + bool slept_screens; +}; + +static struct logind_sleep_state logind_sleep_state = { .inhibit_lock_fd = -1, .stop_fd = -1 }; + +static int logind_sleep_take_inhibit_lock(sd_bus *bus, int *inhibit_lock_fd) +{ + sd_bus_message *reply = NULL; + int tmp_fd; + int ret; + + ret = sd_bus_call_method(bus, logind_service, logind_path, + logind_interface, "Inhibit", NULL, &reply, "ssss", + "sleep", "Gamescope compositor", "Turn display off before sleep", + "delay"); + if (ret < 0) { + logind_sleep_log.errorf("lock: sd_bus_call_method failed %d", ret); + goto unref_message; + } + + ret = sd_bus_message_read(reply, "h", &tmp_fd); + if (ret < 0) { + logind_sleep_log.errorf("lock: sd_bus_message_read failed %d", ret); + goto unref_message; + } + + ret = fcntl(tmp_fd, F_DUPFD_CLOEXEC, 3); + if (ret < 0) { + logind_sleep_log.errorf_errno("lock: fcntl failed"); + goto unref_message; + } + + *inhibit_lock_fd = ret; + + logind_sleep_log.debugf("lock: inhibit lock successfully taken"); + +unref_message: + sd_bus_message_unref(reply); + return ret; +} + +static int logind_sleep_prepare_for_sleep_cb(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + struct logind_sleep_state *state = (struct logind_sleep_state*)userdata; + int going_to_sleep; + int ret; + + ret = sd_bus_message_read(m, "b", &going_to_sleep); + if (ret < 0) { + logind_sleep_log.errorf("callback: sd_bus_message_read failed %d", ret); + return ret; + } + + logind_sleep_log.debugf("callback: running for PrepareForSleep=%d", going_to_sleep); + + if (going_to_sleep) { + if (GetBackend()->IsVisible()) { + state->previous_screen_sleep_status[gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL] = GetBackend()->ScreenAsleep(gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL); + state->previous_screen_sleep_status[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL] = GetBackend()->ScreenAsleep(gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL); + logind_sleep_log.debugf("callback: saving screen sleep status: external=%d internal=%d", + state->previous_screen_sleep_status[gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL], + state->previous_screen_sleep_status[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL]); + GetBackend()->SleepScreen(gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL, true); + GetBackend()->SleepScreen(gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL, true); + if (!GetBackend()->WaitForScreenAsleep(gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL, screen_sleep_timeout_ms) || + !GetBackend()->WaitForScreenAsleep(gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL, screen_sleep_timeout_ms)) + logind_sleep_log.errorf("callback: timed out waiting for screen to sleep"); + state->slept_screens = true; + } + if (state->inhibit_lock_fd >= 0) { + close(state->inhibit_lock_fd); + logind_sleep_log.debugf("callback: inhibit lock released"); + } + state->inhibit_lock_fd = -1; + } else { + logind_sleep_take_inhibit_lock(state->bus, &state->inhibit_lock_fd); + if (state->slept_screens) { + GetBackend()->SleepScreen(gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL, state->previous_screen_sleep_status[gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL]); + GetBackend()->SleepScreen(gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL, state->previous_screen_sleep_status[gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL]); + state->slept_screens = false; + } + } + + return 0; +} + +static void logind_sleep_run(struct logind_sleep_state *state) +{ + sd_event *event; + sd_event_source *source; + int ret; + + pthread_setname_np(pthread_self(), "gamescope-sleep"); + + logind_sleep_log.debugf("run: initializing D-Bus connection"); + + ret = sd_bus_default_system(&state->bus); + if (ret < 0) { + logind_sleep_log.errorf("run: sd_bus_default_system failed %d", ret); + goto exit; + } + + ret = logind_sleep_take_inhibit_lock(state->bus, &state->inhibit_lock_fd); + if (ret < 0) + goto cleanup_bus; + + ret = sd_bus_match_signal(state->bus, NULL, logind_service, logind_path, + logind_interface, "PrepareForSleep", logind_sleep_prepare_for_sleep_cb, + state); + if (ret < 0) { + logind_sleep_log.errorf("run: sd_bus_match_signal failed %d", ret); + goto cleanup_inhibit_fd; + } + + ret = sd_event_default(&event); + if (ret < 0) { + logind_sleep_log.errorf("run: sd_event_default failed %d", ret); + goto cleanup_inhibit_fd; + } + + ret = sd_bus_attach_event(state->bus, event, SD_EVENT_PRIORITY_NORMAL); + if (ret < 0) { + logind_sleep_log.errorf("run: sd_bus_attach_event failed %d", ret); + goto cleanup_event; + } + + // Will call sd_event_exit() + ret = sd_event_add_io(event, &source, state->stop_fd, EPOLLIN, NULL, NULL); + if (ret < 0) { + logind_sleep_log.errorf("run: sd_event_add_io failed %d", ret); + goto cleanup_event; + } + + logind_sleep_log.debugf("run: entering event loop"); + + ret = sd_event_loop(event); + if (ret < 0) + logind_sleep_log.errorf("run: sd_event_loop failed %d", ret); + + sd_event_source_disable_unref(source); +cleanup_event: + sd_event_unref(event); +cleanup_inhibit_fd: + if (state->inhibit_lock_fd >= 0) + close(state->inhibit_lock_fd); +cleanup_bus: + sd_bus_unref(state->bus); +exit: + { + std::lock_guard lock(state->stop_fd_mutex); + + close(state->stop_fd); + state->stop_fd = -1; + } + logind_sleep_log.debugf("run: stopping"); +} + +void logind_sleep_stop() +{ + struct logind_sleep_state *state = &logind_sleep_state; + + if (!state->thread.joinable()) + return; + + { + std::lock_guard lock(state->stop_fd_mutex); + + if (state->stop_fd >= 0 && eventfd_write(state->stop_fd, 1) < 0) + logind_sleep_log.errorf_errno("stop: eventfd_write failed"); + } + + state->thread.join(); +} + +void logind_sleep_init() +{ + struct logind_sleep_state *state = &logind_sleep_state; + + if (!GetBackend()->SupportsScreenSleep()) + return; + + state->stop_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (state->stop_fd < 0) { + logind_sleep_log.errorf_errno("init: eventfd failed"); + return; + } + + state->thread = std::thread(logind_sleep_run, state); + + atexit(logind_sleep_stop); +} diff --git a/src/main.cpp b/src/main.cpp index 6a0fdb139..925936bf4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -712,6 +712,10 @@ char **g_argv; extern char **environ; +#if HAVE_SYSTEMD +void logind_sleep_init(); +#endif + int main(int argc, char **argv) { g_argc = argc; @@ -1099,6 +1103,10 @@ int main(int argc, char **argv) } #endif +#if HAVE_SYSTEMD + logind_sleep_init(); +#endif + std::thread steamCompMgrThread( steamCompMgrThreadRun, argc, argv ); handle_signal_action.sa_handler = handle_signal; diff --git a/src/meson.build b/src/meson.build index ae3ac1673..0051e4db6 100644 --- a/src/meson.build +++ b/src/meson.build @@ -22,6 +22,7 @@ sdl2_dep = dependency('SDL2', required: get_option('sdl2_backend')) avif_dep = dependency('libavif', version: '>=1.0.0', required: get_option('avif_screenshots')) pixman_dep = dependency('pixman-1') udev_dep = dependency('libudev') +systemd_dep = dependency('libsystemd', required: false) wlroots_dep = dependency( 'wlroots-0.20', @@ -148,6 +149,7 @@ if sdl2_dep.found() src += 'Backends/SDLBackend.cpp' endif +gamescope_cpp_args += '-DHAVE_SYSTEMD=@0@'.format(systemd_dep.found().to_int()) gamescope_cpp_args += '-DHAVE_DRM=@0@'.format(drm_dep.found().to_int()) gamescope_cpp_args += '-DHAVE_SDL2=@0@'.format(sdl2_dep.found().to_int()) gamescope_cpp_args += '-DHAVE_AVIF=@0@'.format(avif_dep.found().to_int()) @@ -163,6 +165,10 @@ if pipewire_dep.found() src += 'pipewire.cpp' endif +if systemd_dep.found() + src += 'logind_sleep.cpp' +endif + if openvr_dep.found() src += 'Backends/OpenVRBackend.cpp' endif @@ -200,7 +206,7 @@ gamescope_version = configure_file( xkbcommon, thread_dep, sdl2_dep, wlroots_dep, vulkan_dep, liftoff_dep, dep_xtst, dep_xmu, cap_dep, epoll_dep, pipewire_dep, librt_dep, stb_dep, displayinfo_dep, openvr_dep, dep_xcursor, avif_dep, dep_xi, - libdecor_dep, eis_dep, luajit_dep, libinput_dep, pixman_dep, udev_dep, + libdecor_dep, eis_dep, luajit_dep, libinput_dep, pixman_dep, udev_dep, systemd_dep, ], install: true, cpp_args: gamescope_cpp_args, diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp index f38ac2096..f23de384a 100644 --- a/src/steamcompmgr.cpp +++ b/src/steamcompmgr.cpp @@ -7752,11 +7752,19 @@ error(Display *dpy, XErrorEvent *ev) return 0; } +#if HAVE_SYSTEMD +void logind_sleep_stop(); +#endif + static void steamcompmgr_exit(void) { g_ImageWaiter.Shutdown(); +#if HAVE_SYSTEMD + logind_sleep_stop(); +#endif + // Clean up any commits. { gamescope_xwayland_server_t *server = NULL; diff --git a/src/wlserver.cpp b/src/wlserver.cpp index 1d7684a2c..f16e79e92 100644 --- a/src/wlserver.cpp +++ b/src/wlserver.cpp @@ -1303,18 +1303,16 @@ static void gamescope_control_take_screenshot( struct wl_client *client, struct } ); } -void drm_sleep_screen( gamescope::GamescopeScreenType eType, bool bSleep ); - static void gamescope_control_display_sleep( struct wl_client *client, struct wl_resource *resource, uint32_t display_type_flags, uint32_t flags ) { if ( flags & ( GAMESCOPE_CONTROL_DISPLAY_SLEEP_FLAGS_SLEEP | GAMESCOPE_CONTROL_DISPLAY_SLEEP_FLAGS_WAKE ) ) { const bool sleep = !!( flags & GAMESCOPE_CONTROL_DISPLAY_SLEEP_FLAGS_SLEEP ); if ( display_type_flags & GAMESCOPE_CONTROL_DISPLAY_TYPE_FLAGS_EXTERNAL_DISPLAY ) - drm_sleep_screen( gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL, sleep ); + GetBackend()->SleepScreen( gamescope::GAMESCOPE_SCREEN_TYPE_EXTERNAL, sleep ); if ( display_type_flags & GAMESCOPE_CONTROL_DISPLAY_TYPE_FLAGS_INTERNAL_DISPLAY ) - drm_sleep_screen( gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL, sleep ); + GetBackend()->SleepScreen( gamescope::GAMESCOPE_SCREEN_TYPE_INTERNAL, sleep ); } }