Files
Emilio Cobos Álvarez 571e8708dd Bug 1982990 - Skip GDK frame clock synchronization on X11. r=stransky,gtk-reviewers
See the comments on the bug. The TLDR is that gtk_window_resize waits
for the GTK frame clock via gtk_widget_queue_resize_no_redraw:

 * https://gitlab.gnome.org/GNOME/gtk/-/blob/b30343717dc9b02cf157d2ea87da585d8d518845/gtk/gtkwindow.c#L5436

On automation that never advances (that waits for _NET_WM_FRAME_DRAWN,
and the counter is stuck at 1...). Since GTK has an opt-out for this
precise mechanism, and the benefit we get from it seems not-huge, I
think this is probably the least risky?

Alternatives would be:

 * Try to get an updated mutter on CI or so, in case this is fixed in
   the ubuntu repos already (hopefully?).

 * Go through gdk_window_resize rather than gtk_window_resize on X11,
   which skips the gtk wait and does a direct X11 request. That is
   closer to what gtk_window_move does, but I'm a bit worried about some
   GTK state getting out of sync due to that.

Differential Revision: https://phabricator.services.mozilla.com/D323370
2026-09-04 08:43:09 +00:00

397 lines
13 KiB
C++

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsWindowX11.h"
#include <X11/Xatom.h>
#include <X11/extensions/XShm.h>
#include <X11/extensions/Xfixes.h>
#include <X11/extensions/shape.h>
#include <gdk/gdkkeysyms-compat.h>
#include "GLContextEGL.h" // for GLContextEGL::FindVisual()
#include "GLContextGLX.h" // for GLContextGLX::FindVisual()
#include "WindowSurfaceX11Image.h"
#include "WindowSurfaceX11SHM.h"
#include "gfxXlibSurface.h"
#include "mozilla/PodOperations.h"
#include "mozilla/gfx/gfxVars.h"
#include "mozilla/layers/CompositorBridgeChild.h"
#include "nsWindow.h"
#include "xpcpublic.h"
using namespace mozilla;
using namespace mozilla::gfx;
using namespace mozilla::layers;
using namespace mozilla::widget;
using mozilla::gl::GLContextEGL;
using mozilla::gl::GLContextGLX;
void nsWindowX11::GetWorkspaceID(nsAString& workspaceID) {
workspaceID.Truncate();
if (!mShell) {
return;
}
LOG("nsWindow::GetWorkspaceID()\n");
// Get the gdk window for this widget.
GdkWindow* gdk_window = GetToplevelGdkWindow();
if (!gdk_window) {
LOG(" missing Gdk window, quit.");
return;
}
if (WorkspaceManagementDisabled()) {
LOG(" WorkspaceManagementDisabled, quit.");
return;
}
GdkAtom cardinal_atom = gdk_x11_xatom_to_atom(XA_CARDINAL);
GdkAtom type_returned;
int format_returned;
int length_returned;
long* wm_desktop;
if (!gdk_property_get(gdk_window, gdk_atom_intern("_NET_WM_DESKTOP", FALSE),
cardinal_atom,
0, // offset
INT32_MAX, // length
FALSE, // delete
&type_returned, &format_returned, &length_returned,
(guchar**)&wm_desktop)) {
LOG(" gdk_property_get() failed, quit.");
return;
}
LOG(" got workspace ID %d", (int32_t)wm_desktop[0]);
workspaceID.AppendInt((int32_t)wm_desktop[0]);
g_free(wm_desktop);
}
void nsWindowX11::MoveToWorkspace(const nsAString& workspaceIDStr) {
nsresult rv = NS_OK;
int32_t workspaceID = workspaceIDStr.ToInteger(&rv);
LOG("nsWindow::MoveToWorkspace() ID %d", workspaceID);
if (NS_FAILED(rv)) {
LOG(" MoveToWorkspace disabled, quit");
return;
}
if (!SendWorkspaceMoveRequest(workspaceID)) {
mDeferredWorkspaceID = Some(workspaceID);
LOG(" deferred until the window is shown");
}
}
bool nsWindowX11::SendWorkspaceMoveRequest(int32_t workspaceID) {
if (!mShell || !mIsMapped) {
return false;
}
// Get the gdk window for this widget.
GdkWindow* gdk_window = GetToplevelGdkWindow();
if (!gdk_window) {
LOG(" failed to get GdkWindow, quit.");
return false;
}
GdkDisplay* display = gdk_window_get_display(gdk_window);
gdk_x11_window_move_to_desktop(gdk_window,
static_cast<uint32_t>(workspaceID));
gdk_display_flush(display);
LOG(" moved to workspace");
return true;
}
Window nsWindowX11::GetX11Window() {
return gdk_x11_window_get_xid(mGdkWindow);
}
// Configure GL visual on X11.
bool nsWindowX11::ConfigureX11GLVisual() {
auto* screen = gtk_widget_get_screen(mShell);
int visualId = 0;
bool haveVisual = false;
if (gfxVars::UseEGL()) {
haveVisual = GLContextEGL::FindVisual(&visualId);
}
// We are on GLX or use it as a fallback on Mesa, see
// https://gitlab.freedesktop.org/mesa/mesa/-/issues/149
if (!haveVisual) {
auto* display = GDK_DISPLAY_XDISPLAY(gtk_widget_get_display(mShell));
int screenNumber = GDK_SCREEN_XNUMBER(screen);
haveVisual = GLContextGLX::FindVisual(display, screenNumber, &visualId);
}
GdkVisual* gdkVisual = nullptr;
if (haveVisual) {
// If we're using CSD, rendering will go through mContainer, but
// it will inherit this visual as it is a child of mShell.
gdkVisual = gdk_x11_screen_lookup_visual(screen, visualId);
}
if (!gdkVisual) {
NS_WARNING("We're missing X11 Visual!");
// We try to use a fallback alpha visual
GdkScreen* screen = gtk_widget_get_screen(mShell);
gdkVisual = gdk_screen_get_rgba_visual(screen);
}
if (gdkVisual) {
gtk_widget_set_visual(mShell, gdkVisual);
mHasAlphaVisual = true;
return true;
}
return false;
}
void nsWindowX11::ConfigureToplevelWindowNative() {
if (xpc::IsInAutomation()) {
// Opt out of the _NET_WM_FRAME_DRAWN protocol, so the GDK frame clock
// doesn't get stuck waiting for the compositor. We composite the window
// contents ourselves anyway, so it would only help with non-CSD titlebars
// and window shadows.
//
// We've observed the frame clock getting stuck at least on ubuntu 24.04 on
// automation, see bug 1982990.
//
// TODO(emilio): Should we do this async? We get very little value from it
// it seems, and the compositor could get stuck on real hardware too?
gdk_x11_window_set_frame_sync_enabled(GetToplevelGdkWindow(), FALSE);
}
}
void nsWindowX11::CreateNative() {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
gtk_widget_set_double_buffered(GTK_WIDGET(mContainer), FALSE);
#pragma GCC diagnostic pop
mSurfaceProvider.Initialize(GetX11Window());
}
void nsWindowX11::DestroyNative() { UnlockNativePointer(); }
void nsWindowX11::SetCompositorHint(WindowComposeRequest aState) {
gulong value = aState;
GdkAtom cardinal_atom = gdk_x11_xatom_to_atom(XA_CARDINAL);
gdk_property_change(GetToplevelGdkWindow(),
gdk_atom_intern("_NET_WM_BYPASS_COMPOSITOR", FALSE),
cardinal_atom,
32, // format
GDK_PROP_MODE_REPLACE, (guchar*)&value, 1);
}
/* XApp progress support currently works by setting a property
* on a window with this Atom name. A supporting window manager
* will notice this and pass it along to whatever handling has
* been implemented on that end (e.g. passing it on to a taskbar
* widget.) There is no issue if WM support is lacking, this is
* simply ignored in that case.
*
* See https://github.com/linuxmint/xapps/blob/master/libxapp/xapp-gtk-window.c
* for further details.
*/
#define PROGRESS_HINT "_NET_WM_XAPP_PROGRESS"
static void set_window_hint_cardinal(Window xid, const gchar* atom_name,
gulong cardinal) {
GdkDisplay* display;
display = gdk_display_get_default();
if (cardinal > 0) {
XChangeProperty(GDK_DISPLAY_XDISPLAY(display), xid,
gdk_x11_get_xatom_by_name_for_display(display, atom_name),
XA_CARDINAL, 32, PropModeReplace, (guchar*)&cardinal, 1);
} else {
XDeleteProperty(GDK_DISPLAY_XDISPLAY(display), xid,
gdk_x11_get_xatom_by_name_for_display(display, atom_name));
}
}
void nsWindowX11::SetProgress(unsigned long progressPercent) {
progressPercent = MIN(progressPercent, 100);
set_window_hint_cardinal(GDK_WINDOW_XID(GetToplevelGdkWindow()),
PROGRESS_HINT, progressPercent);
}
static bool SupportsPointerBarriers(Display* aDisplay) {
MOZ_ASSERT(StaticPrefs::dom_pointer_lock_native_lock_enabled());
// XXX We expect the capability doesn't change as we use only one display
// everywhere.
static const bool sSupported = [&] {
int eventBase = -1;
int errorBase = -1;
if (!XFixesQueryExtension(aDisplay, &eventBase, &errorBase)) {
return false;
}
int major = 0;
int minor = 0;
if (!XFixesQueryVersion(aDisplay, &major, &minor)) {
return false;
}
return major >= 5;
}();
return sSupported;
}
void nsWindowX11::UpdateNativePointerBarriers() {
if (!StaticPrefs::dom_pointer_lock_native_lock_enabled()) {
MOZ_ASSERT(!mIsNativePointerLocked);
MOZ_ASSERT(!mNativePointerBarriers);
return;
}
if (NS_WARN_IF(!mGdkWindow)) {
return;
}
Display* display = GDK_DISPLAY_XDISPLAY(gdk_window_get_display(mGdkWindow));
if (NS_WARN_IF(!SupportsPointerBarriers(display))) {
MOZ_ASSERT(!mNativePointerBarriers);
return;
}
if (mNativePointerBarriers) {
XFixesDestroyPointerBarrier(display, mNativePointerBarriers->mLeft);
XFixesDestroyPointerBarrier(display, mNativePointerBarriers->mRight);
XFixesDestroyPointerBarrier(display, mNativePointerBarriers->mTop);
XFixesDestroyPointerBarrier(display, mNativePointerBarriers->mBottom);
mNativePointerBarriers.reset();
}
if (mIsNativePointerLocked) {
Window window = GetX11Window();
mNativePointerBarriers.emplace(
XFixesCreatePointerBarrier(
display, window, mClientArea.X(), mClientArea.Y(), mClientArea.X(),
mClientArea.YMost(), BarrierPositiveX, 0, nullptr),
XFixesCreatePointerBarrier(display, window, mClientArea.XMost(),
mClientArea.Y(), mClientArea.XMost(),
mClientArea.YMost(), BarrierNegativeX, 0,
nullptr),
XFixesCreatePointerBarrier(
display, window, mClientArea.X(), mClientArea.Y(),
mClientArea.XMost(), mClientArea.Y(), BarrierPositiveY, 0, nullptr),
XFixesCreatePointerBarrier(display, window, mClientArea.X(),
mClientArea.YMost(), mClientArea.XMost(),
mClientArea.YMost(), BarrierNegativeY, 0,
nullptr));
}
}
void nsWindowX11::LockNativePointer(
NativePointerLockMode aNativePointerLockMode) {
if (!StaticPrefs::dom_pointer_lock_native_lock_enabled()) {
MOZ_ASSERT(!mIsNativePointerLocked);
MOZ_ASSERT(!mNativePointerBarriers);
return;
}
if (mIsNativePointerLocked) {
MOZ_ASSERT(mNativePointerBarriers);
return;
}
mIsNativePointerLocked = true;
UpdateNativePointerBarriers();
}
void nsWindowX11::UnlockNativePointer() {
if (!mIsNativePointerLocked) {
MOZ_ASSERT(!mNativePointerBarriers);
return;
}
MOZ_ASSERT(StaticPrefs::dom_pointer_lock_native_lock_enabled());
mIsNativePointerLocked = false;
UpdateNativePointerBarriers();
}
void nsWindowX11::NativeShow(bool aAction) {
if (aAction) {
// unset our flag now that our window has been shown
mNeedsShow = true;
auto removeShow = MakeScopeExit([&] { mNeedsShow = false; });
LOG("nsWindowX11::NativeShow show\n");
// Set up usertime/startupID metadata for the created window.
// On X11 we use gtk_window_set_startup_id() so we need to call it
// before show.
SetUserTimeAndStartupTokenForActivatedWindow();
LOG(" calling gtk_widget_show(mShell)\n");
gtk_widget_show(mShell);
if (mDeferredWorkspaceID &&
SendWorkspaceMoveRequest(*mDeferredWorkspaceID)) {
mDeferredWorkspaceID.reset();
}
if (mX11HiddenPopupPositioned) {
LOG(" re-position hidden popup window [%d, %d]", mClientArea.x,
mClientArea.y);
gtk_window_move(GTK_WINDOW(mShell), mClientArea.x, mClientArea.y);
mX11HiddenPopupPositioned = false;
}
} else {
LOG("nsWindow::NativeShow hide\n");
// Workaround window freezes on GTK versions before 3.21.2 by
// ensuring that configure events get dispatched to windows before
// they are unmapped. See bug 1225044.
if (gtk_check_version(3, 21, 2) != nullptr && mPendingConfigures > 0) {
GtkAllocation allocation;
gtk_widget_get_allocation(GTK_WIDGET(mShell), &allocation);
GdkEventConfigure event;
PodZero(&event);
event.type = GDK_CONFIGURE;
event.window = mGdkWindow;
event.send_event = TRUE;
event.x = allocation.x;
event.y = allocation.y;
event.width = allocation.width;
event.height = allocation.height;
auto* shellClass = GTK_WIDGET_GET_CLASS(mShell);
for (unsigned int i = 0; i < mPendingConfigures; i++) {
(void)shellClass->configure_event(mShell, &event);
}
mPendingConfigures = 0;
}
gtk_widget_hide(mShell);
}
}
void nsWindowX11::OnMapNative() {
if (mIsDragPopup) {
if (GtkWidget* parent = gtk_widget_get_parent(mShell)) {
gtk_widget_set_opacity(parent, 0.0);
}
}
if (CompositorBridgeChild* remoteRenderer = GetRemoteRenderer()) {
remoteRenderer->SendResume();
remoteRenderer->SendForcePresent(wr::RenderReasons::WIDGET);
}
// Set window manager hint to keep fullscreen windows composited.
//
// If the window were to get unredirected, there could be visible
// tearing because Gecko does not align its framebuffer updates with
// vblank.
//
// This must be (re-)applied whenever the shell's X window is created,
// including after CSD-triggered re-realize in SetCustomTitlebar().
SetCompositorHint(GTK_WIDGET_COMPOSITED_ENABLED);
XFlush(DefaultXDisplay());
}