From 6ed833e163528e85693e650363436727a17192b5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 21 Jul 2026 10:03:40 +0530 Subject: [PATCH] Wayland: get initial window size in cells working in most cases on hyprland/sway with fractional scaling by using the fractional scale of the primary monitor --- docs/changelog.rst | 3 ++ glfw/glfw.py | 1 + glfw/source-info.json | 1 + glfw/wl_init.c | 8 ++- glfw/wl_monitor.c | 122 ++++++++++++++++++++++++++++++++++++++++-- glfw/wl_platform.h | 9 ++++ kitty/glfw-wrapper.c | 3 ++ kitty/glfw-wrapper.h | 4 ++ kitty/glfw.c | 25 ++++++--- 9 files changed, 163 insertions(+), 13 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6a640d90b..e41ccc44c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -186,6 +186,9 @@ Detailed list of changes - Wayland: fix a regression in 0.47.3 that broke dragging on the Niri compositor (:iss:`10271`) +- Wayland: get initial window size in cells working in most cases on hyprland/sway with fractional scaling by using the fractional scale of the primary monitor + + 0.48.0 [2026-07-18] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/glfw/glfw.py b/glfw/glfw.py index 1afc8c602..a80fbfddc 100755 --- a/glfw/glfw.py +++ b/glfw/glfw.py @@ -332,6 +332,7 @@ def generate_wrappers(glfw_header: str) -> None: bool glfwWaylandIsWindowFullyCreated(GLFWwindow *handle) bool glfwWaylandBeep(GLFWwindow *handle) pid_t glfwWaylandCompositorPID(void) + double glfwGetWaylandPrimaryMonitorFractionalScale(void) void glfwConfigureMomentumScroller(double friction, double min_velocity, double max_velocity, unsigned timer_interval) unsigned long long glfwDBusUserNotify(const GLFWDBUSNotificationData *n, GLFWDBusnotificationcreatedfun callback, void *data) void glfwDBusSetUserNotificationHandler(GLFWDBusnotificationactivatedfun handler) diff --git a/glfw/source-info.json b/glfw/source-info.json index dacad1d8a..e53ca7d9c 100644 --- a/glfw/source-info.json +++ b/glfw/source-info.json @@ -91,6 +91,7 @@ "staging/ext-background-effect/ext-background-effect-v1.xml", "staging/xdg-toplevel-drag/xdg-toplevel-drag-v1.xml", "unstable/pointer-gestures/pointer-gestures-unstable-v1.xml", + "unstable/xdg-output/xdg-output-unstable-v1.xml", "kwin-blur-v1.xml", "wlr-layer-shell-unstable-v1.xml" diff --git a/glfw/wl_init.c b/glfw/wl_init.c index 08d2a1a5e..37654ede4 100644 --- a/glfw/wl_init.c +++ b/glfw/wl_init.c @@ -728,6 +728,10 @@ static void registryHandleGlobal(void* data UNUSED, _glfw.wl.xdg_toplevel_tag_manager_v1 = wl_registry_bind(registry, name, &xdg_toplevel_tag_manager_v1_interface, 1); } else if (is(xdg_toplevel_drag_manager_v1)) { _glfw.wl.xdg_toplevel_drag_manager_v1 = wl_registry_bind(registry, name, &xdg_toplevel_drag_manager_v1_interface, 1); + } else if (is(zxdg_output_manager_v1)) { + _glfw.wl.xdg_output_manager = wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, MIN(version, 3u)); + for (int i = 0; i < _glfw.monitorCount; i++) + _glfwCreateXdgOutputWayland(_glfw.monitors[i]); } #undef is } @@ -840,7 +844,7 @@ get_compositor_missing_capabilities(void) { C(single_pixel_buffer, wp_single_pixel_buffer_manager_v1); C(preferred_scale, has_preferred_buffer_scale); C(idle_inhibit, idle_inhibit_manager); C(icon, xdg_toplevel_icon_manager_v1); C(bell, xdg_system_bell_v1); C(window-tag, xdg_toplevel_tag_manager_v1); C(keyboard_shortcuts_inhibit, keyboard_shortcuts_inhibit_manager); - C(key-repeat, has_key_repeat_events); C(top_level_drag, xdg_toplevel_drag_manager_v1); + C(key-repeat, has_key_repeat_events); C(top_level_drag, xdg_toplevel_drag_manager_v1); C(output_manager, xdg_output_manager); C(pointer_gestures, pointer_gestures); #define P(x) p += snprintf(p, sizeof(buf) - (p - buf), "%s ", x); if (_glfw.wl.xdg_wm_base_version < 6) P("window-state-suspended"); @@ -1031,6 +1035,8 @@ void _glfwPlatformTerminate(void) xdg_toplevel_tag_manager_v1_destroy(_glfw.wl.xdg_toplevel_tag_manager_v1); if (_glfw.wl.xdg_toplevel_drag_manager_v1) xdg_toplevel_drag_manager_v1_destroy(_glfw.wl.xdg_toplevel_drag_manager_v1); + if (_glfw.wl.xdg_output_manager) + zxdg_output_manager_v1_destroy(_glfw.wl.xdg_output_manager); if (_glfw.wl.wp_single_pixel_buffer_manager_v1) wp_single_pixel_buffer_manager_v1_destroy(_glfw.wl.wp_single_pixel_buffer_manager_v1); if (_glfw.wl.wp_cursor_shape_manager_v1) diff --git a/glfw/wl_monitor.c b/glfw/wl_monitor.c index f1be0f4b4..3407a381c 100644 --- a/glfw/wl_monitor.c +++ b/glfw/wl_monitor.c @@ -43,13 +43,14 @@ static void outputHandleGeometry(void* data, int32_t subpixel UNUSED, const char* make UNUSED, const char* model UNUSED, - int32_t transform UNUSED) + int32_t transform) { struct _GLFWmonitor *monitor = data; monitor->wl.x = x; monitor->wl.y = y; monitor->widthMM = physicalWidth; monitor->heightMM = physicalHeight; + monitor->wl.transform = transform; } static void outputHandleMode(void* data, @@ -126,10 +127,93 @@ static const struct wl_output_listener outputListener = { }; +static void xdgOutputHandleLogicalPosition(void *data, + struct zxdg_output_v1 *xdg_output UNUSED, + int32_t x, + int32_t y) +{ + struct _GLFWmonitor *monitor = data; + monitor->wl.x = x; + monitor->wl.y = y; +} + +static void xdgOutputHandleLogicalSize(void *data, + struct zxdg_output_v1 *xdg_output UNUSED, + int32_t width, + int32_t height) +{ + struct _GLFWmonitor *monitor = data; + monitor->wl.xdg_logical_width = width; + monitor->wl.xdg_logical_height = height; +} + +static void computeXdgFractionalScale(struct _GLFWmonitor *monitor) +{ + if (monitor->wl.xdg_logical_width <= 0 || monitor->wl.xdg_logical_height <= 0) return; + if (monitor->modeCount == 0 || monitor->wl.currentMode < 0) return; + GLFWvidmode *mode = &monitor->modes[monitor->wl.currentMode]; + if (mode->width <= 0 || mode->height <= 0) return; + + // For 90° and 270° rotations the compositor maps physical width↔height, + // so use the swapped axis when dividing by the logical size. + int phys_w = mode->width, phys_h = mode->height; + int32_t t = monitor->wl.transform; + if (t == WL_OUTPUT_TRANSFORM_90 || t == WL_OUTPUT_TRANSFORM_270 || + t == WL_OUTPUT_TRANSFORM_FLIPPED_90 || t == WL_OUTPUT_TRANSFORM_FLIPPED_270) + { + int tmp = phys_w; phys_w = phys_h; phys_h = tmp; + } + + double sx = (double)phys_w / monitor->wl.xdg_logical_width; + double sy = (double)phys_h / monitor->wl.xdg_logical_height; + // Use the average in case of minor rounding differences between axes. + monitor->wl.fractional_scale = (sx + sy) * 0.5; +} + +static void xdgOutputHandleDone(void *data, + struct zxdg_output_v1 *xdg_output UNUSED) +{ + struct _GLFWmonitor *monitor = data; + computeXdgFractionalScale(monitor); +} + +static void xdgOutputHandleName(void *data, + struct zxdg_output_v1 *xdg_output UNUSED, + const char *name) +{ + // wl_output already provides the name; skip to avoid duplicate frees. + (void)data; (void)name; +} + +static void xdgOutputHandleDescription(void *data, + struct zxdg_output_v1 *xdg_output UNUSED, + const char *description) +{ + (void)data; (void)description; +} + +static const struct zxdg_output_v1_listener xdgOutputListener = { + xdgOutputHandleLogicalPosition, + xdgOutputHandleLogicalSize, + xdgOutputHandleDone, + xdgOutputHandleName, + xdgOutputHandleDescription, +}; + + ////////////////////////////////////////////////////////////////////////// ////// GLFW internal API ////// ////////////////////////////////////////////////////////////////////////// +void _glfwCreateXdgOutputWayland(_GLFWmonitor* monitor) +{ + if (!_glfw.wl.xdg_output_manager || monitor->wl.xdg_output) return; + monitor->wl.xdg_output = zxdg_output_manager_v1_get_xdg_output( + _glfw.wl.xdg_output_manager, monitor->wl.output); + if (monitor->wl.xdg_output) + zxdg_output_v1_add_listener(monitor->wl.xdg_output, &xdgOutputListener, monitor); +} + void _glfwAddOutputWayland(uint32_t name, uint32_t version) { _GLFWmonitor *monitor; @@ -160,6 +244,7 @@ void _glfwAddOutputWayland(uint32_t name, uint32_t version) monitor->wl.name = name; wl_output_add_listener(output, &outputListener, monitor); + _glfwCreateXdgOutputWayland(monitor); } @@ -169,6 +254,8 @@ void _glfwAddOutputWayland(uint32_t name, uint32_t version) void _glfwPlatformFreeMonitor(_GLFWmonitor* monitor) { + if (monitor->wl.xdg_output) + zxdg_output_v1_destroy(monitor->wl.xdg_output); if (monitor->wl.output) wl_output_destroy(monitor->wl.output); } @@ -184,10 +271,11 @@ void _glfwPlatformGetMonitorPos(_GLFWmonitor* monitor, int* xpos, int* ypos) void _glfwPlatformGetMonitorContentScale(_GLFWmonitor* monitor, float* xscale, float* yscale) { - if (xscale) - *xscale = (float) monitor->wl.scale; - if (yscale) - *yscale = (float) monitor->wl.scale; + float scale = monitor->wl.fractional_scale > 0.0 + ? (float)monitor->wl.fractional_scale + : (float)monitor->wl.scale; + if (xscale) *xscale = scale; + if (yscale) *yscale = scale; } void _glfwPlatformGetMonitorWorkarea(_GLFWmonitor* monitor, @@ -246,3 +334,27 @@ GLFWAPI struct wl_output* glfwGetWaylandMonitor(GLFWmonitor* handle) _GLFW_REQUIRE_INIT_OR_RETURN(NULL); return monitor->wl.output; } + +GLFWAPI double glfwGetWaylandPrimaryMonitorFractionalScale(void) +{ + _GLFW_REQUIRE_INIT_OR_RETURN(1.0); + if (!_glfw.wl.xdg_output_manager || _glfw.monitorCount == 0) + return 1.0; + + _GLFWmonitor *monitor = _glfw.monitors[0]; + if (!monitor->wl.xdg_output) + return monitor->wl.scale > 0 ? (double)monitor->wl.scale : 1.0; + + // Block until the compositor has delivered xdg_output logical_size. + while (monitor->wl.xdg_logical_width <= 0 || monitor->wl.xdg_logical_height <= 0) + { + if (wl_display_roundtrip(_glfw.wl.display) < 0) break; + } + + if (monitor->wl.fractional_scale <= 0.0) + computeXdgFractionalScale(monitor); + + return monitor->wl.fractional_scale > 0.0 + ? monitor->wl.fractional_scale + : (monitor->wl.scale > 0 ? (double)monitor->wl.scale : 1.0); +} diff --git a/glfw/wl_platform.h b/glfw/wl_platform.h index a1627f052..68cfe0422 100644 --- a/glfw/wl_platform.h +++ b/glfw/wl_platform.h @@ -72,6 +72,7 @@ typedef VkBool32 (APIENTRY *PFN_vkGetPhysicalDeviceWaylandPresentationSupportKHR #include "wayland-xdg-system-bell-v1-client-protocol.h" #include "wayland-xdg-toplevel-tag-v1-client-protocol.h" #include "wayland-xdg-toplevel-drag-v1-client-protocol.h" +#include "wayland-xdg-output-unstable-v1-client-protocol.h" #define _glfw_dlopen(name) dlopen(name, RTLD_LAZY | RTLD_LOCAL) #define _glfw_dlclose(handle) dlclose(handle) @@ -366,6 +367,7 @@ typedef struct _GLFWlibraryWayland struct zwp_keyboard_shortcuts_inhibit_manager_v1 *keyboard_shortcuts_inhibit_manager; struct zwp_pointer_gestures_v1* pointer_gestures; struct zwp_pointer_gesture_hold_v1* pointer_gesture_hold; + struct zxdg_output_manager_v1* xdg_output_manager; int compositorVersion; int seatVersion; @@ -470,12 +472,18 @@ typedef struct _GLFWlibraryWayland typedef struct _GLFWmonitorWayland { struct wl_output* output; + struct zxdg_output_v1* xdg_output; uint32_t name; int currentMode; int x; int y; int scale; + int32_t transform; + + int32_t xdg_logical_width; + int32_t xdg_logical_height; + double fractional_scale; } _GLFWmonitorWayland; @@ -496,6 +504,7 @@ typedef struct _GLFWcursorWayland void _glfwAddOutputWayland(uint32_t name, uint32_t version); +void _glfwCreateXdgOutputWayland(_GLFWmonitor* monitor); void _glfwWaylandBeforeBufferSwap(_GLFWwindow *window); void _glfwWaylandAfterBufferSwap(_GLFWwindow *window); void _glfwSetupWaylandDataDevice(void); diff --git a/kitty/glfw-wrapper.c b/kitty/glfw-wrapper.c index f79dc7760..f3c3b9a5c 100644 --- a/kitty/glfw-wrapper.c +++ b/kitty/glfw-wrapper.c @@ -545,6 +545,9 @@ load_glfw(const char* path) { *(void **) (&glfwWaylandCompositorPID_impl) = dlsym(handle, "glfwWaylandCompositorPID"); if (glfwWaylandCompositorPID_impl == NULL) dlerror(); // clear error indicator + *(void **) (&glfwGetWaylandPrimaryMonitorFractionalScale_impl) = dlsym(handle, "glfwGetWaylandPrimaryMonitorFractionalScale"); + if (glfwGetWaylandPrimaryMonitorFractionalScale_impl == NULL) dlerror(); // clear error indicator + *(void **) (&glfwConfigureMomentumScroller_impl) = dlsym(handle, "glfwConfigureMomentumScroller"); if (glfwConfigureMomentumScroller_impl == NULL) dlerror(); // clear error indicator diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index 142ef0cfb..187b28ec9 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -2544,6 +2544,10 @@ typedef pid_t (*glfwWaylandCompositorPID_func)(void); GFW_EXTERN glfwWaylandCompositorPID_func glfwWaylandCompositorPID_impl; #define glfwWaylandCompositorPID glfwWaylandCompositorPID_impl +typedef double (*glfwGetWaylandPrimaryMonitorFractionalScale_func)(void); +GFW_EXTERN glfwGetWaylandPrimaryMonitorFractionalScale_func glfwGetWaylandPrimaryMonitorFractionalScale_impl; +#define glfwGetWaylandPrimaryMonitorFractionalScale glfwGetWaylandPrimaryMonitorFractionalScale_impl + typedef void (*glfwConfigureMomentumScroller_func)(double, double, double, unsigned); GFW_EXTERN glfwConfigureMomentumScroller_func glfwConfigureMomentumScroller_impl; #define glfwConfigureMomentumScroller glfwConfigureMomentumScroller_impl diff --git a/kitty/glfw.c b/kitty/glfw.c index 77c054ecb..3469a8da4 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -1821,13 +1821,24 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { float xscale, yscale; double xdpi, ydpi; if (global_state.is_wayland) { - // Cannot use temp window on Wayland as scale is only sent by compositor after window is displayed - get_window_content_scale(NULL, &xscale, &yscale, &xdpi, &ydpi); - for (unsigned i = 0; i < global_state.num_os_windows; i++) { - OSWindow *osw = global_state.os_windows + i; - if (osw->handle && glfwGetWindowAttrib(osw->handle, GLFW_FOCUSED)) { - get_window_content_scale(osw->handle, &xscale, &yscale, &xdpi, &ydpi); - break; + // Cannot use temp window on Wayland as scale is only sent by compositor after window is displayed. + // For the first window, query the xdg-output fractional scale of the primary monitor directly; + // this blocks until the compositor has sent size information so we get the true fractional scale + // before creating any window. + if (is_first_window && glfwGetWaylandPrimaryMonitorFractionalScale) { + debug_rendering("Querying Wayland compositor for primary monitor scale\n"); + double fscale = glfwGetWaylandPrimaryMonitorFractionalScale(); + debug_rendering("Primary monitor scale reported as: %.5f\n", fscale); + xscale = yscale = (float)fscale; + dpi_from_scale(xscale, yscale, &xdpi, &ydpi); + } else { + get_window_content_scale(NULL, &xscale, &yscale, &xdpi, &ydpi); + for (unsigned i = 0; i < global_state.num_os_windows; i++) { + OSWindow *osw = global_state.os_windows + i; + if (osw->handle && glfwGetWindowAttrib(osw->handle, GLFW_FOCUSED)) { + get_window_content_scale(osw->handle, &xscale, &yscale, &xdpi, &ydpi); + break; + } } } } else {