"Native resolution" streamed the compositor's points, not the panel's pixels — and the window was never high-DPI either #112

Merged
enricobuehler merged 1 commits from worktree-wayland-native-pixel-density into main 2026-08-08 10:09:56 +00:00
Owner

Field report (0.24.0-1, CachyOS / KDE Plasma 6.7.4 / Wayland, 2560×1600@165 laptop panel at 150 % scaling): "Native resolution" negotiated 1706×1066 instead of the panel's real mode, and the stream looked blurry. At 100 % scaling it was sharp, but the laptop UI was unusably small.

Two independent defects, and they stack — which is why the reporter's own workaround of forcing 2560×1600 by hand still left the image soft until they also set SDL_VIDEO_WAYLAND_SCALE_TO_DISPLAY=1.

1. Native resolved to logical points

crates/pf-presenter/src/run.rs is the only place the client ever asks what "Native" means, and it read the mode raw:

.map(|m| Mode { width: m.w.max(0) as u32, height: m.h.max(0) as u32,  })

Display::get_mode() is SDL_GetDesktopDisplayMode, and an SDL3 display mode is in screen coordinates, not pixels — the ratio to real pixels is a separate field, pixel_density, which this never read. SDL's Wayland backend fills the desktop mode with the compositor's logical size on purpose (SDL_waylandvideo.c:1023) and stashes the scale there, unless SDL_VIDEO_WAYLAND_SCALE_TO_DISPLAY=1 flips it to report native pixels. That is exactly why the reporter's env var changed the picture.

The chain to the logged number: KWin advertises the output as 1707×1067 points (matching what KDE's own panel shows) → SDL hands that back → clients/session/src/main.rs:295 substitutes it for the stored width: 0, height: 0render_scale::apply even-floors both odd axes (the host's validate_dimensions rejects odd) → 1706×1066.

Multiplying by pixel_density recovers 2560×1600 exactly, because SDL derives the density as the output's exact pixels-per-point ratio rather than the nominal 1.5.

This is inert off Wayland. SDL never sets a density on the X11 or Windows backends, and SDL_video.c:821 normalizes the unset 0.0 up to 1.0. WIN_GetWindowSizeInPixels returns the real client rect in device pixels. The bug needed a compositor doing fractional scaling. (The Apple client was already correct — it resolves Native from UIScreen.nativeBounds / backingScaleFactor.)

2. The window was never high-DPI, so presentation blurred anyway

The SDL window was built as b.resizable().vulkan() with no HIGH_PIXEL_DENSITY. Without that flag SDL leaves the Wayland surface at buffer scale 1 (SDL_waylandwindow.c:58), so SDL_GetWindowSizeInPixels returns the logical size and the Vulkan swapchain gets built at 1707×1067 — which KWin then upscales to the glass. Even a correct 2560×1600 stream was resampled down and back up. The same flaw silently shrank Match window, which asks the host for size_in_pixels().

The surrounding code was already written for pixels ≠ points: the swapchain (vk/reconfig.rs:63), match-window, and pointer mapping all use size_in_pixels(), while window-size persistence uses logical size(). The flag just makes those two stop being the same number. display_scale() starts reporting 1.5 into a swapchain that is 1.5× larger, so the OSD keeps the on-screen size it already had.

Also

Only an Err from SDL reached the 1920×1080 fallback, so a display reporting a 0×0 mode sent a 0×0 request. Now a zero-sized mode falls back too.

Verification

On home-worker-5 (CachyOS — the reporter's own distro, real SDL 3.4.14):

  • cargo clippy --workspace --all-targets --locked -- -D warnings — clean (full CI parity, not just the touched crate)
  • cargo test -p pf-presenter — 18/18 pass, three of them new
  • cargo fmt --all --check — clean

The new tests pin the field-reported numbers directly: native_mode(1707, 1067, 2560.0/1707.0, 165.0) must be 2560×1600@165, render_scale::apply on it must stay 2560×1600, and the pre-fix apply(1707, 1067, …) == (1706, 1066) is kept alongside so the regression stays legible. Two more cover density 1.0 / 2.0 staying inert-or-exact and a nonsense density (0.0, negative, NaN, ∞) falling back to 1× rather than zeroing the mode.

Not verified on the reporter's glass — worth a confirmation run at 150 % before this is called closed.

Field report (0.24.0-1, CachyOS / KDE Plasma 6.7.4 / Wayland, 2560×1600@165 laptop panel at **150 % scaling**): "Native resolution" negotiated **1706×1066** instead of the panel's real mode, and the stream looked blurry. At 100 % scaling it was sharp, but the laptop UI was unusably small. Two independent defects, and they stack — which is why the reporter's own workaround of forcing 2560×1600 by hand *still* left the image soft until they also set `SDL_VIDEO_WAYLAND_SCALE_TO_DISPLAY=1`. ## 1. Native resolved to logical points `crates/pf-presenter/src/run.rs` is the only place the client ever asks what "Native" means, and it read the mode raw: ```rust .map(|m| Mode { width: m.w.max(0) as u32, height: m.h.max(0) as u32, … }) ``` `Display::get_mode()` is `SDL_GetDesktopDisplayMode`, and an SDL3 display mode is in **screen coordinates**, not pixels — the ratio to real pixels is a separate field, `pixel_density`, which this never read. SDL's Wayland backend fills the desktop mode with the compositor's logical size on purpose (`SDL_waylandvideo.c:1023`) and stashes the scale there, *unless* `SDL_VIDEO_WAYLAND_SCALE_TO_DISPLAY=1` flips it to report native pixels. That is exactly why the reporter's env var changed the picture. The chain to the logged number: KWin advertises the output as 1707×1067 points (matching what KDE's own panel shows) → SDL hands that back → `clients/session/src/main.rs:295` substitutes it for the stored `width: 0, height: 0` → `render_scale::apply` even-floors both odd axes (the host's `validate_dimensions` rejects odd) → **1706×1066**. Multiplying by `pixel_density` recovers 2560×1600 *exactly*, because SDL derives the density as the output's exact pixels-per-point ratio rather than the nominal 1.5. **This is inert off Wayland.** SDL never sets a density on the X11 or Windows backends, and `SDL_video.c:821` normalizes the unset `0.0` up to `1.0`. `WIN_GetWindowSizeInPixels` returns the real client rect in device pixels. The bug needed a compositor doing *fractional* scaling. (The Apple client was already correct — it resolves Native from `UIScreen.nativeBounds` / `backingScaleFactor`.) ## 2. The window was never high-DPI, so presentation blurred anyway The SDL window was built as `b.resizable().vulkan()` with no `HIGH_PIXEL_DENSITY`. Without that flag SDL leaves the Wayland surface at buffer scale 1 (`SDL_waylandwindow.c:58`), so `SDL_GetWindowSizeInPixels` returns the *logical* size and the Vulkan swapchain gets built at 1707×1067 — which KWin then upscales to the glass. Even a correct 2560×1600 stream was resampled down and back up. The same flaw silently shrank **Match window**, which asks the host for `size_in_pixels()`. The surrounding code was already written for pixels ≠ points: the swapchain (`vk/reconfig.rs:63`), match-window, and pointer mapping all use `size_in_pixels()`, while window-size persistence uses logical `size()`. The flag just makes those two stop being the same number. `display_scale()` starts reporting 1.5 into a swapchain that is 1.5× larger, so the OSD keeps the on-screen size it already had. ## Also Only an `Err` from SDL reached the 1920×1080 fallback, so a display reporting a 0×0 mode sent a 0×0 request. Now a zero-sized mode falls back too. ## Verification On **home-worker-5** (CachyOS — the reporter's own distro, real SDL 3.4.14): - `cargo clippy --workspace --all-targets --locked -- -D warnings` — clean (full CI parity, not just the touched crate) - `cargo test -p pf-presenter` — 18/18 pass, three of them new - `cargo fmt --all --check` — clean The new tests pin the field-reported numbers directly: `native_mode(1707, 1067, 2560.0/1707.0, 165.0)` must be 2560×1600@165, `render_scale::apply` on it must stay 2560×1600, and the pre-fix `apply(1707, 1067, …) == (1706, 1066)` is kept alongside so the regression stays legible. Two more cover density 1.0 / 2.0 staying inert-or-exact and a nonsense density (`0.0`, negative, NaN, ∞) falling back to 1× rather than zeroing the mode. Not verified on the reporter's glass — worth a confirmation run at 150 % before this is called closed.
enricobuehler added 1 commit 2026-08-08 09:27:37 +00:00
fix(pf-presenter): "Native resolution" streamed the compositor's POINTS, not the panel's pixels
ci / bun-nix (pull_request) Successful in 23s
ci / web (pull_request) Successful in 1m11s
ci / docs-site (pull_request) Successful in 1m19s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m33s
apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 3m25s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m22s
android / android (pull_request) Successful in 4m41s
ci / rust (pull_request) Successful in 6m52s
69728b6f4e
A CachyOS / KDE Plasma 6.7.4 Wayland client with its 2560x1600@165 laptop panel at
150 % scaling negotiated 1706x1066 for "Native resolution" and streamed a visibly
blurry image. Two independent defects, and they stack — which is why forcing the mode
to 2560x1600 by hand did not fully fix it either.

1. `SDL_GetDesktopDisplayMode` reports a mode in SCREEN COORDINATES and hands the
   pixels-per-point ratio back separately as `pixel_density`. We read `m.w`/`m.h` raw.
   KDE advertises that panel as 1707x1067 points with a density of ~1.4997,
   `render_scale::apply` even-floors both odd axes, and 1706x1066 goes on the wire —
   exactly the mode in the reporter's handshake log. Multiplying by the density
   recovers 2560x1600 to the pixel, because SDL derives it as the output's exact
   pixels/points ratio. On X11 and Windows SDL never sets a density and `SDL_video.c`
   normalizes the unset 0.0 to 1.0, so this is inert there: the bug needed a
   compositor doing FRACTIONAL scaling.

2. The SDL window was created without `HIGH_PIXEL_DENSITY`, so the Wayland surface
   stayed at buffer scale 1 — the Vulkan swapchain was built at 1707x1067 and KWin
   upscaled it to the glass. Even a correct 2560x1600 stream was resampled down and
   then back up. The same flaw silently shrank "Match window", which asks the host for
   `size_in_pixels()`. The reporter's `SDL_VIDEO_WAYLAND_SCALE_TO_DISPLAY=1` workaround
   is this same fix applied from outside SDL, which is why it helped.

The surrounding code was already written for pixels != points — the swapchain,
match-window and pointer mapping all read `size_in_pixels()` while window-size
persistence reads logical `size()` — so the flag only makes those two stop being the
same number. `display_scale()` starts reporting 1.5 into a swapchain that is 1.5x
larger, leaving the OSD the size it already was.

Also closes a smaller hole on the way past: only an `Err` from SDL reached the
1920x1080 fallback, so a display that reported a 0x0 mode sent a 0x0 request.

Verified on home-worker-5 (CachyOS — the reporter's distro, real SDL 3.4.14):
`cargo clippy --all-targets -p pf-presenter -- -D warnings` clean and 18/18
pf-presenter tests pass, three of them new and pinned to the field-reported numbers.
enricobuehler merged commit 2a2427afc8 into main 2026-08-08 10:09:56 +00:00
enricobuehler deleted branch worktree-wayland-native-pixel-density 2026-08-08 10:09:57 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#112