Wayland: suspend window after frame timeout or suspend state
This is a pretty popular approach that took a while for me to wrap my head around and which only recently got "official" support through an update (xdg_shell version 6), so I think that this is all-in-all a better option than the overkill 2000Hz ticking we have now :P Basically, we wait for a frame event and, if either too much time passes or we get the new `suspended` state, we consider the window as "hidden" and stop drawing, ticking by the low usage rate. This should work great for KDE and Mutter, which support the new state, but not yet for sway, which is still stuck at a very old xdg_shell version and thus falls back to the timeout approach. Be aware that if we rely on timing out the engine will have to stall for the whole timeout, which _could_ be problematic but doensn't seem like it. Further testing is needed. Special thanks go to the guys over at #wayland on OFTC, who very patiently explained me this approach way too many times.
This commit is contained in:
parent
a9bb8509f2
commit
2e07dcf1e7
|
@ -127,14 +127,6 @@ void OS_LinuxBSD::alert(const String &p_alert, const String &p_title) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int OS_LinuxBSD::get_low_processor_usage_mode_sleep_usec() const {
|
|
||||||
if (DisplayServer::get_singleton() == nullptr || DisplayServer::get_singleton()->get_name() != "Wayland" || is_in_low_processor_usage_mode()) {
|
|
||||||
return OS::get_low_processor_usage_mode_sleep_usec();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 500; // Roughly 2000 FPS, improves frame time when emulating VSync.
|
|
||||||
}
|
|
||||||
|
|
||||||
void OS_LinuxBSD::initialize() {
|
void OS_LinuxBSD::initialize() {
|
||||||
crash_handler.initialize();
|
crash_handler.initialize();
|
||||||
|
|
||||||
|
|
|
@ -127,8 +127,6 @@ public:
|
||||||
|
|
||||||
virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override;
|
virtual void alert(const String &p_alert, const String &p_title = "ALERT!") override;
|
||||||
|
|
||||||
virtual int get_low_processor_usage_mode_sleep_usec() const override;
|
|
||||||
|
|
||||||
virtual bool _check_internal_feature_support(const String &p_feature) override;
|
virtual bool _check_internal_feature_support(const String &p_feature) override;
|
||||||
|
|
||||||
void run();
|
void run();
|
||||||
|
|
|
@ -867,11 +867,11 @@ bool DisplayServerWayland::window_is_focused(WindowID p_window_id) const {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DisplayServerWayland::window_can_draw(DisplayServer::WindowID p_window_id) const {
|
bool DisplayServerWayland::window_can_draw(DisplayServer::WindowID p_window_id) const {
|
||||||
return frame;
|
return !suspended;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DisplayServerWayland::can_any_window_draw() const {
|
bool DisplayServerWayland::can_any_window_draw() const {
|
||||||
return frame;
|
return !suspended;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayServerWayland::window_set_ime_active(const bool p_active, DisplayServer::WindowID p_window_id) {
|
void DisplayServerWayland::window_set_ime_active(const bool p_active, DisplayServer::WindowID p_window_id) {
|
||||||
|
@ -1143,7 +1143,32 @@ void DisplayServerWayland::process_events() {
|
||||||
|
|
||||||
wayland_thread.keyboard_echo_keys();
|
wayland_thread.keyboard_echo_keys();
|
||||||
|
|
||||||
frame = wayland_thread.get_reset_frame();
|
if (!suspended) {
|
||||||
|
if (emulate_vsync) {
|
||||||
|
// Due to various reasons, we manually handle display synchronization by
|
||||||
|
// waiting for a frame event (request to draw) or, if available, the actual
|
||||||
|
// window's suspend status. When a window is suspended, we can avoid drawing
|
||||||
|
// altogether, either because the compositor told us that we don't need to or
|
||||||
|
// because the pace of the frame events became unreliable.
|
||||||
|
bool frame = wayland_thread.wait_frame_suspend_ms(1000);
|
||||||
|
if (!frame) {
|
||||||
|
suspended = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (wayland_thread.is_suspended()) {
|
||||||
|
suspended = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suspended) {
|
||||||
|
DEBUG_LOG_WAYLAND("Window suspended.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (wayland_thread.get_reset_frame()) {
|
||||||
|
// At last, a sign of life! We're no longer suspended.
|
||||||
|
suspended = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wayland_thread.mutex.unlock();
|
wayland_thread.mutex.unlock();
|
||||||
|
|
||||||
|
|
|
@ -117,7 +117,7 @@ class DisplayServerWayland : public DisplayServer {
|
||||||
|
|
||||||
Context context;
|
Context context;
|
||||||
|
|
||||||
bool frame = false;
|
bool suspended = false;
|
||||||
bool emulate_vsync = false;
|
bool emulate_vsync = false;
|
||||||
|
|
||||||
String rendering_driver;
|
String rendering_driver;
|
||||||
|
|
|
@ -469,7 +469,7 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
|
if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
|
||||||
registry->xdg_wm_base = (struct xdg_wm_base *)wl_registry_bind(wl_registry, name, &xdg_wm_base_interface, MAX(2, MIN(5, (int)version)));
|
registry->xdg_wm_base = (struct xdg_wm_base *)wl_registry_bind(wl_registry, name, &xdg_wm_base_interface, MAX(2, MIN(6, (int)version)));
|
||||||
registry->xdg_wm_base_name = name;
|
registry->xdg_wm_base_name = name;
|
||||||
|
|
||||||
xdg_wm_base_add_listener(registry->xdg_wm_base, &xdg_wm_base_listener, nullptr);
|
xdg_wm_base_add_listener(registry->xdg_wm_base, &xdg_wm_base_listener, nullptr);
|
||||||
|
@ -1063,9 +1063,10 @@ void WaylandThread::_xdg_toplevel_on_configure(void *data, struct xdg_toplevel *
|
||||||
WindowState *ws = (WindowState *)data;
|
WindowState *ws = (WindowState *)data;
|
||||||
ERR_FAIL_NULL(ws);
|
ERR_FAIL_NULL(ws);
|
||||||
|
|
||||||
// Expect the window to be in windowed mode. The mode will get overridden if
|
// Expect the window to be in a plain state. It will get properly set if the
|
||||||
// the compositor reports otherwise.
|
// compositor reports otherwise below.
|
||||||
ws->mode = DisplayServer::WINDOW_MODE_WINDOWED;
|
ws->mode = DisplayServer::WINDOW_MODE_WINDOWED;
|
||||||
|
ws->suspended = false;
|
||||||
|
|
||||||
uint32_t *state = nullptr;
|
uint32_t *state = nullptr;
|
||||||
wl_array_for_each(state, states) {
|
wl_array_for_each(state, states) {
|
||||||
|
@ -1078,6 +1079,10 @@ void WaylandThread::_xdg_toplevel_on_configure(void *data, struct xdg_toplevel *
|
||||||
ws->mode = DisplayServer::WINDOW_MODE_FULLSCREEN;
|
ws->mode = DisplayServer::WINDOW_MODE_FULLSCREEN;
|
||||||
} break;
|
} break;
|
||||||
|
|
||||||
|
case XDG_TOPLEVEL_STATE_SUSPENDED: {
|
||||||
|
ws->suspended = true;
|
||||||
|
} break;
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
// We don't care about the other states (for now).
|
// We don't care about the other states (for now).
|
||||||
} break;
|
} break;
|
||||||
|
@ -1176,9 +1181,10 @@ void WaylandThread::libdecor_frame_on_configure(struct libdecor_frame *frame, st
|
||||||
|
|
||||||
libdecor_window_state window_state = LIBDECOR_WINDOW_STATE_NONE;
|
libdecor_window_state window_state = LIBDECOR_WINDOW_STATE_NONE;
|
||||||
|
|
||||||
// Expect the window to be in windowed mode. The mode will get overridden if
|
// Expect the window to be in a plain state. It will get properly set if the
|
||||||
// the compositor reports otherwise.
|
// compositor reports otherwise below.
|
||||||
ws->mode = DisplayServer::WINDOW_MODE_WINDOWED;
|
ws->mode = DisplayServer::WINDOW_MODE_WINDOWED;
|
||||||
|
ws->suspended = false;
|
||||||
|
|
||||||
if (libdecor_configuration_get_window_state(configuration, &window_state)) {
|
if (libdecor_configuration_get_window_state(configuration, &window_state)) {
|
||||||
if (window_state & LIBDECOR_WINDOW_STATE_MAXIMIZED) {
|
if (window_state & LIBDECOR_WINDOW_STATE_MAXIMIZED) {
|
||||||
|
@ -1188,6 +1194,10 @@ void WaylandThread::libdecor_frame_on_configure(struct libdecor_frame *frame, st
|
||||||
if (window_state & LIBDECOR_WINDOW_STATE_FULLSCREEN) {
|
if (window_state & LIBDECOR_WINDOW_STATE_FULLSCREEN) {
|
||||||
ws->mode = DisplayServer::WINDOW_MODE_FULLSCREEN;
|
ws->mode = DisplayServer::WINDOW_MODE_FULLSCREEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window_state & LIBDECOR_WINDOW_STATE_SUSPENDED) {
|
||||||
|
ws->suspended = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window_state_update_size(ws, width, height);
|
window_state_update_size(ws, width, height);
|
||||||
|
@ -3872,6 +3882,102 @@ bool WaylandThread::get_reset_frame() {
|
||||||
return old_frame;
|
return old_frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dispatches events until a frame event is received, a window is reported as
|
||||||
|
// suspended or the timeout expires.
|
||||||
|
bool WaylandThread::wait_frame_suspend_ms(int p_timeout) {
|
||||||
|
if (main_window.suspended) {
|
||||||
|
// The window is suspended! The compositor is telling us _explicitly_ that we
|
||||||
|
// don't need to draw, without letting us guess through the frame event's
|
||||||
|
// timing and stuff like that. Our job here is done.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame) {
|
||||||
|
// We already have a frame! Probably it got there while the caller locked :D
|
||||||
|
frame = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct pollfd poll_fd;
|
||||||
|
poll_fd.fd = wl_display_get_fd(wl_display);
|
||||||
|
poll_fd.events = POLLIN | POLLHUP;
|
||||||
|
|
||||||
|
int begin_ms = OS::get_singleton()->get_ticks_msec();
|
||||||
|
int remaining_ms = p_timeout;
|
||||||
|
|
||||||
|
while (remaining_ms > 0) {
|
||||||
|
// Empty the event queue while it's full.
|
||||||
|
while (wl_display_prepare_read(wl_display) != 0) {
|
||||||
|
if (wl_display_dispatch_pending(wl_display) == -1) {
|
||||||
|
// Oh no. We'll check and handle any display error below.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (main_window.suspended) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame) {
|
||||||
|
// We had a frame event in the queue :D
|
||||||
|
frame = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int werror = wl_display_get_error(wl_display);
|
||||||
|
|
||||||
|
if (werror) {
|
||||||
|
if (werror == EPROTO) {
|
||||||
|
struct wl_interface *wl_interface = nullptr;
|
||||||
|
uint32_t id = 0;
|
||||||
|
|
||||||
|
int error_code = wl_display_get_protocol_error(wl_display, (const struct wl_interface **)&wl_interface, &id);
|
||||||
|
CRASH_NOW_MSG(vformat("Wayland protocol error %d on interface %s@%d.", error_code, wl_interface ? wl_interface->name : "unknown", id));
|
||||||
|
} else {
|
||||||
|
CRASH_NOW_MSG(vformat("Wayland client error code %d.", werror));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wl_display_flush(wl_display);
|
||||||
|
|
||||||
|
// Wait for the event file descriptor to have new data.
|
||||||
|
poll(&poll_fd, 1, remaining_ms);
|
||||||
|
|
||||||
|
if (poll_fd.revents | POLLIN) {
|
||||||
|
// Load the queues with fresh new data.
|
||||||
|
wl_display_read_events(wl_display);
|
||||||
|
} else {
|
||||||
|
// Oh well... Stop signaling that we want to read.
|
||||||
|
wl_display_cancel_read(wl_display);
|
||||||
|
|
||||||
|
// We've got no new events :(
|
||||||
|
// We won't even bother with checking the frame flag.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's try dispatching now...
|
||||||
|
wl_display_dispatch_pending(wl_display);
|
||||||
|
|
||||||
|
if (main_window.suspended) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame) {
|
||||||
|
frame = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining_ms -= OS::get_singleton()->get_ticks_msec() - begin_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_LOG_WAYLAND_THREAD("Frame timeout.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WaylandThread::is_suspended() const {
|
||||||
|
return main_window.suspended;
|
||||||
|
}
|
||||||
|
|
||||||
void WaylandThread::destroy() {
|
void WaylandThread::destroy() {
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -177,6 +177,7 @@ public:
|
||||||
|
|
||||||
Rect2i rect;
|
Rect2i rect;
|
||||||
DisplayServer::WindowMode mode = DisplayServer::WINDOW_MODE_WINDOWED;
|
DisplayServer::WindowMode mode = DisplayServer::WINDOW_MODE_WINDOWED;
|
||||||
|
bool suspended = false;
|
||||||
|
|
||||||
// These are true by default as it isn't guaranteed that we'll find an
|
// These are true by default as it isn't guaranteed that we'll find an
|
||||||
// xdg-shell implementation with wm_capabilities available. If and once we
|
// xdg-shell implementation with wm_capabilities available. If and once we
|
||||||
|
@ -939,6 +940,9 @@ public:
|
||||||
|
|
||||||
void set_frame();
|
void set_frame();
|
||||||
bool get_reset_frame();
|
bool get_reset_frame();
|
||||||
|
bool wait_frame_suspend_ms(int p_timeout);
|
||||||
|
|
||||||
|
bool is_suspended() const;
|
||||||
|
|
||||||
Error init();
|
Error init();
|
||||||
void destroy();
|
void destroy();
|
||||||
|
|
Loading…
Reference in New Issue