fix(capture): preserve rounded fractional buffer dimensions

This commit is contained in:
Samuel Onoja committed 2026-10-24 22:03:23 +01:00
1 parent ad1207b417
commit f5fff6d930
4 files changed
+80 -9

No files matched your search

+45 -3
View File
@@ -27,9 +27,34 @@ fn capture_scale(output_scale: f64, buffer_scale: i32, view: SurfaceView) -> f64
if view.dst.w <= 0 || view.dst.h <= 0 {
return output_scale;
}
let native_x = view.src.size.w * f64::from(buffer_scale) / f64::from(view.dst.w);
let native_y = view.src.size.h * f64::from(buffer_scale) / f64::from(view.dst.h);
output_scale.max(native_x).max(native_y)
let width = view.src.size.w * f64::from(buffer_scale);
let height = view.src.size.h * f64::from(buffer_scale);
let logical_width = f64::from(view.dst.w);
let logical_height = f64::from(view.dst.h);
let matches_native = |scale: f64| {
(logical_width * scale).round() == width && (logical_height * scale).round() == height
};
// Fractional clients round their backing dimensions independently. Taking
// the larger density can add a pixel on the other axis and filter every
// row or column, even though the client already rendered at this scale.
if matches_native(output_scale) {
return output_scale;
}
let scale = output_scale
.max(width / logical_width)
.max(height / logical_height);
if matches_native(scale) {
return scale;
}
// Prefer a common density whose rounded dimensions preserve both axes.
// Stay strictly below the upper rounding boundary, including FP error.
let upper = ((width + 0.5) / logical_width).min((height + 0.5) / logical_height);
let adjusted = upper * (1.0 - 8.0 * f64::EPSILON);
if adjusted >= output_scale && matches_native(adjusted) {
adjusted
} else {
scale
}
}
#[derive(Debug, Default)]
@@ -621,6 +646,23 @@ pub(crate) mod tests {
assert_eq!(capture_scale(1.75, 1, cropped), 1.75);
}
#[test]
fn sharing_preserves_independently_rounded_fractional_buffers() {
for (width, height) in [(3819., 2091.), (3819., 2092.)] {
let view = SurfaceView {
src: Rectangle::from_size((width, height).into()),
dst: (2182, 1195).into(),
offset: (0, 0).into(),
};
for output_scale in [1.25, 1.5, 1.75] {
let scale = capture_scale(output_scale, 1, view);
assert!(scale >= output_scale);
assert_eq!((2182. * scale).round(), width);
assert_eq!((1195. * scale).round(), height);
}
}
}
/// Exercise both asynchronous publication boundaries using real resources.
pub(crate) fn check_deferred_snapshot(
state: &Ferese,
+3
View File
@@ -35,6 +35,9 @@ the captured surface tree when it exceeds the display scale. For example, a 2×
application buffer stays at 2× in the stream on a 1.75× display, avoiding a
fractional downsample before the recording app scales it. The video size follows
window resizes and backing buffer density changes; closing the window ends sharing.
At fractional scales, clients can round the two backing dimensions separately.
Capture preserves those pixel dimensions rather than adding a row or column
by applying the larger axis density to both axes, which would soften text.
The picker follows your shell colors, font, corners, opacity, and blur.
+14 -5
View File
@@ -18,6 +18,8 @@ static struct xdg_wm_base *wm;
static struct wp_viewporter *viewporter;
static struct wl_surface *surface;
static struct wl_buffer *buffer;
static int logical_width = 640;
static int logical_height = 480;
static void ping(void *data, struct xdg_wm_base *base, uint32_t serial) {
(void)data;
@@ -49,7 +51,7 @@ static const struct wl_registry_listener registry_listener = {global, removed};
static void configure(void *data, struct xdg_surface *xdg, uint32_t serial) {
(void)data;
xdg_surface_ack_configure(xdg, serial);
xdg_surface_set_window_geometry(xdg, 0, 0, 640, 480);
xdg_surface_set_window_geometry(xdg, 0, 0, logical_width, logical_height);
wl_surface_attach(surface, buffer, 0, 0);
wl_surface_damage_buffer(surface, 0, 0, INT32_MAX, INT32_MAX);
wl_surface_commit(surface);
@@ -73,9 +75,15 @@ int main(int argc, char **argv) {
if (wl_display_roundtrip(display) < 0 || !compositor || !shm || !wm || !viewporter) return 3;
int integer = !strcmp(argv[1], "integer");
int fractional = !strcmp(argv[1], "fractional");
int rounded = !strcmp(argv[1], "fractional-rounded");
int ceiled = !strcmp(argv[1], "fractional-ceiled");
int cropped = !strcmp(argv[1], "cropped");
int width = fractional ? 1120 : 1280;
int height = fractional ? 840 : 960;
if (rounded || ceiled) {
logical_width = 2182;
logical_height = 1195;
}
int width = rounded || ceiled ? 3819 : fractional ? 1120 : 1280;
int height = rounded ? 2091 : ceiled ? 2092 : fractional ? 840 : 960;
int fd = memfd_create("window-capture-scale", MFD_CLOEXEC);
size_t bytes = (size_t)width * height * 4;
if (fd < 0 || ftruncate(fd, bytes)) return 4;
@@ -83,7 +91,8 @@ int main(int argc, char **argv) {
if (pixels == MAP_FAILED) return 5;
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
pixels[y * width + x] = x % 2 ? 0xffffffff : 0xff000000;
pixels[y * width + x] = (x + ((rounded || ceiled) ? y : 0)) % 2
? 0xffffffff : 0xff000000;
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, bytes);
buffer = wl_shm_pool_create_buffer(pool, 0, width, height, width * 4, WL_SHM_FORMAT_ARGB8888);
wl_shm_pool_destroy(pool);
@@ -116,7 +125,7 @@ int main(int argc, char **argv) {
wl_surface_set_buffer_scale(surface, 2);
} else {
struct wp_viewport *viewport = wp_viewporter_get_viewport(viewporter, surface);
wp_viewport_set_destination(viewport, 640, 480);
wp_viewport_set_destination(viewport, logical_width, logical_height);
if (cropped)
wp_viewport_set_source(viewport, wl_fixed_from_int(320), wl_fixed_from_int(240),
wl_fixed_from_int(640), wl_fixed_from_int(480));
@@ -96,7 +96,9 @@ class WindowCaptureScale(unittest.TestCase):
"an integer 2x host masks the original downsampling bug")
for mode, density in [("integer", 2), ("viewport", 2),
("fractional", 1.75), ("cropped", 1),
("subsurface", 2), ("subsurface-outside", 1)]:
("subsurface", 2), ("subsurface-outside", 1),
("fractional-rounded", 1.75),
("fractional-ceiled", 1.75)]:
with self.subTest(mode=mode, output_scale=scale):
client = launch([str(root / "client"), mode], stdin=subprocess.PIPE)
window = wait_for(lambda: next((w for w in call("windows")
@@ -109,6 +111,10 @@ class WindowCaptureScale(unittest.TestCase):
ready = json.loads(worker.stdout.readline())
expected = (round(640 * max(scale, density)),
round(480 * max(scale, density)))
if mode == "fractional-rounded":
expected = (3819, 2091)
elif mode == "fractional-ceiled":
expected = (3819, 2092)
self.assertEqual((ready["width"], ready["height"]), expected,
"window sharing discarded native backing pixels")
raw = root / f"{mode}.bgrx"
@@ -134,6 +140,17 @@ class WindowCaptureScale(unittest.TestCase):
"content outside the window was captured")
elif density >= scale:
for index in (0, 2):
if mode.startswith("fractional-"):
for y in (0, height // 2, height - 1):
start = (index * width * height + y * width) * 4
row = pixels[start:start + width * 4]
expected_row = b"".join(
bytes((255, 255, 255, 255)) if (x + y) % 2
else bytes((0, 0, 0, 255))
for x in range(width))
self.assertEqual(row, expected_row,
"rounded native pixels were filtered")
continue
start = (index * width * height + height // 2 * width) * 4
row = pixels[start:start + width * 4]
self.assertEqual(row, bytes((0, 0, 0, 255, 255, 255, 255, 255)) * (width // 2),