Skip to content

fix(linux_window): separate the surface scale from the stage factor in mutter's PHYSICAL layout - #69

Merged
dfattal merged 1 commit into
mainfrom
fix/wayland-physical-layout-mode
Oct 2, 2026
Merged

dfattal merged 1 commit into
mainfrom
fix/wayland-physical-layout-mode

Conversation

@dfattal

@dfattal dfattal commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Companion to DisplayXR/displayxr-runtime#1785 (no merge-order dependency: this PR builds against today's runtime headers, and the runtime PR does not touch the vendored u_wayland_geom.h).

Problem

mutter has two monitor layout modes. In LOGICAL (fractional scaling; mutter 50's default) the stage is logical px. One number, the monitor scale, is then both the surface scale and the stage factor. In PHYSICAL (Ubuntu 24.04 / GNOME 46 at an integer scale, out of the box) the stage already is device px. At 200 % the stage factor is 1 while clients still draw at 2.

The Wayland window used one number for both:

  • Sizing. mode / xdg_output.logical_size is the stage factor (1 in PHYSICAL), and it was used as the surface scale. request_initial_rect() therefore declared a 720x400 rect as 1440x800 and held it. The deferred fullscreen's first configure guess was also twice the panel.
  • Positions. The window-geometry extension's rects were converted by monitor.scale (2 in PHYSICAL). The initial rect's landing check and the drag-lattice map therefore used the wrong factor.

Fix

The new dxr_wl_scale.h (pure) keeps the two factors apart:

  • dxr_wl_surface_scale_estimate() handles sizing. It returns the integer wl_output.scale when the stage factor is 1 (the PHYSICAL signature), else the stage factor.
  • dxr_wl_stage_factor() converts published rects. It tries, in order:
    1. The extension's own answer (monitor.device_scale / layout_mode, extension v11 in runtime#1785).
    2. This client's output at the monitor's origin (mode / logical_size, right in both modes).
    3. The monitor scale.

The runtime's new u_wayland_layout.h is vendored verbatim in xrt_aux/util. get_own_geometry() parses the two new fields.

Verified

This ran on a private headless mutter 50.1 (3840x2160 virtual monitor, gdctl set --layout-mode physical --scale 2), using linux_window_test with DXR_LW_TEST_WAYLAND=1 DXR_LW_TEST_WAYLAND_PANEL=0,0,3840,2160:Meta-0.

before after
PHYSICAL 200 %, extension v11 declared 1440x800, wanted 720x400; landing read as (600, 400) 2880x1600 initial rect: LANDED — (300, 200) 720x400 device px, off by (+0, +0)
PHYSICAL 200 %, extension v10 — LANDED, off by (+0, +0) (client-output fallback)
LOGICAL 200 % LANDED LANDED (unchanged)

The panel-fullscreen check (fullscreen once mapped) fails under mutter in both modes, before and after. It is written against weston and is not part of this change.

The existing lattice, drag and window tests pass. The new tests/linux_window_scale_test.cpp covers both modes with the measured inputs.

After merge

Tag it. When the runtime's test-app pin moves to that tag, u_wayland_layout.h can join the byte-identity guard in test_apps/common/dxr_linux_window_aux_guard.cmake.

🤖 Generated with Claude Code

…n mutter's PHYSICAL layout

mutter has two monitor layout modes. In LOGICAL (fractional scaling;
mutter 50's default) the stage is logical px and one number, the monitor
scale, is both the surface scale and the stage factor. In PHYSICAL (Ubuntu
24.04 / GNOME 46 at an integer scale, out of the box) the stage already is
device px: at 200 % the stage factor is 1 while clients still draw at 2.

The Wayland window treated them as one number, in two places:

- Sizing: `mode / xdg_output.logical_size` (the stage factor, 1 in
  PHYSICAL) was used as the surface scale, so request_initial_rect()
  declared a 720x400 rect as 1440x800 and held it, and the deferred
  fullscreen's first configure guess was twice the panel.
- Positions: the window-geometry extension's rects were converted by its
  monitor.scale (2 in PHYSICAL), so the initial rect's landing check read
  (600, 400) 2880x1600 for a window at (300, 200) 720x400, and the drag
  lattice map used the wrong factor.

New dxr_wl_scale.h keeps the two apart: dxr_wl_surface_scale_estimate()
(the integer wl_output.scale when the stage factor is 1, else the stage
factor) sizes; dxr_wl_stage_factor() converts published rects, preferring
the extension's own answer (monitor.device_scale / layout_mode, extension
version 11), then this client's output at the monitor's origin (mode /
logical size, right in both modes), then the monitor scale. The runtime's
new u_wayland_layout.h is vendored verbatim in xrt_aux/util.

Measured on a private headless mutter 50.1 (3840x2160 virtual monitor,
gdctl --layout-mode physical --scale 2), linux_window_test with
DXR_LW_TEST_WAYLAND=1:

  before: declared 1440x800, wanted 720x400; "the rect spans monitors of
          different scales" at (600, 400) 2880x1600
  after:  initial rect: LANDED — (300, 200) 720x400 device px, off by
          (+0, +0); same with the extension at version 10 (the output
          fallback) and unchanged in LOGICAL at 200 %.

(The panel-fullscreen check fails under mutter in both modes, before and
after; it is written against weston.)

Tests: tests/linux_window_scale_test.cpp (new).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@dfattal
dfattal merged commit 1ef24ca into main Oct 2, 2026
6 checks passed
@dfattal
dfattal deleted the fix/wayland-physical-layout-mode branch October 2, 2026 07:00
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