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

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.
This commit is contained in:
2026-08-08 11:15:58 +02:00
parent 1ef212a78d
commit 69728b6f4e
+91 -6
View File
@@ -550,7 +550,15 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
Some((x, y)) => b.position(x, y),
None => b.position_centered(),
};
b.resizable().vulkan();
// HIGH_PIXEL_DENSITY: give us a backbuffer in the panel's REAL pixels. Without it
// SDL leaves the Wayland surface at buffer scale 1, so on a fractionally scaled
// output (KDE at 150 %: a 2560×1600 panel reported as 1707×1067 points) the
// swapchain is built at 1707×1067 and the compositor upscales it to the glass —
// a 2560×1600 stream is resampled DOWN and back UP, and looks it. The flag only
// widens `size_in_pixels()`; `size()` stays logical, which is what the persisted
// window size and SDL's own mouse coordinates are in, and both callers already
// use the right one.
b.resizable().vulkan().high_pixel_density();
if opts.fullscreen {
b.fullscreen();
}
@@ -644,11 +652,11 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
let native = window
.get_display()
.and_then(|d| d.get_mode())
.map(|m| Mode {
width: m.w.max(0) as u32,
height: m.h.max(0) as u32,
refresh_hz: m.refresh_rate.round().max(0.0) as u32,
})
.map(|m| native_mode(m.w, m.h, m.pixel_density, m.refresh_rate))
.ok()
// A zero-sized mode is as useless as no mode at all — only `Err` used to reach
// the fallback, so a display that reported 0×0 streamed a 0×0 request.
.filter(|m: &Mode| m.width > 0 && m.height > 0)
.unwrap_or(Mode {
width: 1920,
height: 1080,
@@ -2136,6 +2144,37 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
Ok(outcome)
}
/// An `SDL_DisplayMode` as the panel's REAL pixels — the `0 = native` stream mode.
///
/// SDL3 reports a display mode in SCREEN COORDINATES, not pixels, and hands you the ratio
/// between the two separately as `pixel_density`. On X11 and Windows that ratio is always
/// 1.0 (SDL never sets it there, and `SDL_video.c` normalizes the unset 0.0 up to 1.0), so
/// this is a no-op — but under a Wayland compositor doing FRACTIONAL scaling it is the
/// whole ballgame: KDE at 150 % advertises a 2560×1600 panel as 1707×1067 points with
/// `pixel_density` ≈ 1.4997, and taking `m.w`/`m.h` raw is what made "Native resolution"
/// negotiate 1706×1066 (1707×1067 even-floored by `render_scale::apply`) and stream a
/// blurry two-thirds-size image. `SDL_VIDEO_WAYLAND_SCALE_TO_DISPLAY=1` is the same fix
/// from the outside — it makes SDL report the native mode itself — which is why setting it
/// was a workaround.
///
/// The density is the exact `pixels / points` ratio SDL derived from the output, so the
/// multiplication recovers the panel size to the pixel rather than approximating it.
fn native_mode(w: i32, h: i32, pixel_density: f32, refresh_rate: f32) -> Mode {
// A non-finite or non-positive density is SDL telling us nothing useful; 1× at least
// preserves the pre-fix behaviour instead of collapsing the mode to zero.
let density = if pixel_density.is_finite() && pixel_density > 0.0 {
pixel_density
} else {
1.0
};
let px = |v: i32| (v.max(0) as f32 * density).round().max(0.0) as u32;
Mode {
width: px(w),
height: px(h),
refresh_hz: refresh_rate.round().max(0.0) as u32,
}
}
/// Match-window (D1): replace the params' requested w/h with the window's physical pixel
/// size — even-floored (the host's `validate_dimensions` rejects odd) and clamped to a
/// sane minimum — keeping the resolved refresh. Under `--fullscreen` the window IS the
@@ -2908,6 +2947,52 @@ fn stats_text(
mod tests {
use super::*;
/// The field report this exists for: CachyOS/KDE Plasma 6.7.4 Wayland, a 2560×1600@165
/// laptop panel at 150 % scaling. KDE advertises the output as 1707×1067 points, SDL
/// hands that back as the desktop mode with `pixel_density` = 2560/1707, and "Native
/// resolution" streamed 1706×1066 — the points, even-floored by `render_scale::apply`.
#[test]
fn native_is_the_panels_pixels_under_fractional_wayland_scaling() {
// SDL derives the density as the exact pixels-per-point ratio of the output.
let density = 2560.0 / 1707.0;
let m = native_mode(1707, 1067, density, 165.0);
assert_eq!((m.width, m.height, m.refresh_hz), (2560, 1600, 165));
// …and it survives the even-floor the host's `validate_dimensions` forces, which is
// where 1707×1067 lost its odd pixel and became the reported 1706×1066.
assert_eq!(
punktfunk_core::render_scale::apply(m.width, m.height, 1.0, 8192),
(2560, 1600)
);
assert_eq!(
punktfunk_core::render_scale::apply(1707, 1067, 1.0, 8192),
(1706, 1066),
"the pre-fix mode, kept here so the regression is legible"
);
}
#[test]
fn native_is_unchanged_where_the_density_is_one() {
// X11, Windows, and Wayland at 100 % all report 1.0 — the fix must be inert there.
let m = native_mode(2560, 1600, 1.0, 165.0);
assert_eq!((m.width, m.height, m.refresh_hz), (2560, 1600, 165));
// Integer scaling (a 200 % 4K panel reported as 1920×1080 points) doubles cleanly.
let m = native_mode(1920, 1080, 2.0, 60.0);
assert_eq!((m.width, m.height), (3840, 2160));
}
#[test]
fn a_nonsense_density_falls_back_to_one_rather_than_zeroing_the_mode() {
// SDL normalizes an unset density to 1.0, but this must not be the one place a
// driver quirk can hand the host a 0×0 mode request.
for bogus in [0.0, -1.0, f32::NAN, f32::INFINITY] {
let m = native_mode(2560, 1600, bogus, 60.0);
assert_eq!((m.width, m.height), (2560, 1600), "density {bogus}");
}
// A negative mode size is clamped, not wrapped into a huge u32.
let m = native_mode(-1, -1, 1.5, 60.0);
assert_eq!((m.width, m.height), (0, 0));
}
#[test]
fn overlay_scale_follows_dpi_and_survives_a_bogus_display() {
// 100 % / 96 dpi is the identity — the chrome keeps the size it always had.