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 { if view.dst.w <= 0 || view.dst.h <= 0 {
return output_scale; return output_scale;
} }
let native_x = view.src.size.w * f64::from(buffer_scale) / f64::from(view.dst.w); let width = view.src.size.w * f64::from(buffer_scale);
let native_y = view.src.size.h * f64::from(buffer_scale) / f64::from(view.dst.h); let height = view.src.size.h * f64::from(buffer_scale);
output_scale.max(native_x).max(native_y) 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)] #[derive(Debug, Default)]
@@ -621,6 +646,23 @@ pub(crate) mod tests {
assert_eq!(capture_scale(1.75, 1, cropped), 1.75); 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. /// Exercise both asynchronous publication boundaries using real resources.
pub(crate) fn check_deferred_snapshot( pub(crate) fn check_deferred_snapshot(
state: &Ferese, 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 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 fractional downsample before the recording app scales it. The video size follows
window resizes and backing buffer density changes; closing the window ends sharing. 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. 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 wp_viewporter *viewporter;
static struct wl_surface *surface; static struct wl_surface *surface;
static struct wl_buffer *buffer; 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) { static void ping(void *data, struct xdg_wm_base *base, uint32_t serial) {
(void)data; (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) { static void configure(void *data, struct xdg_surface *xdg, uint32_t serial) {
(void)data; (void)data;
xdg_surface_ack_configure(xdg, serial); 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_attach(surface, buffer, 0, 0);
wl_surface_damage_buffer(surface, 0, 0, INT32_MAX, INT32_MAX); wl_surface_damage_buffer(surface, 0, 0, INT32_MAX, INT32_MAX);
wl_surface_commit(surface); 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; if (wl_display_roundtrip(display) < 0 || !compositor || !shm || !wm || !viewporter) return 3;
int integer = !strcmp(argv[1], "integer"); int integer = !strcmp(argv[1], "integer");
int fractional = !strcmp(argv[1], "fractional"); 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 cropped = !strcmp(argv[1], "cropped");
int width = fractional ? 1120 : 1280; if (rounded || ceiled) {
int height = fractional ? 840 : 960; 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); int fd = memfd_create("window-capture-scale", MFD_CLOEXEC);
size_t bytes = (size_t)width * height * 4; size_t bytes = (size_t)width * height * 4;
if (fd < 0 || ftruncate(fd, bytes)) return 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; if (pixels == MAP_FAILED) return 5;
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++) 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); 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); buffer = wl_shm_pool_create_buffer(pool, 0, width, height, width * 4, WL_SHM_FORMAT_ARGB8888);
wl_shm_pool_destroy(pool); wl_shm_pool_destroy(pool);
@@ -116,7 +125,7 @@ int main(int argc, char **argv) {
wl_surface_set_buffer_scale(surface, 2); wl_surface_set_buffer_scale(surface, 2);
} else { } else {
struct wp_viewport *viewport = wp_viewporter_get_viewport(viewporter, surface); 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) if (cropped)
wp_viewport_set_source(viewport, wl_fixed_from_int(320), wl_fixed_from_int(240), 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)); 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") "an integer 2x host masks the original downsampling bug")
for mode, density in [("integer", 2), ("viewport", 2), for mode, density in [("integer", 2), ("viewport", 2),
("fractional", 1.75), ("cropped", 1), ("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): with self.subTest(mode=mode, output_scale=scale):
client = launch([str(root / "client"), mode], stdin=subprocess.PIPE) client = launch([str(root / "client"), mode], stdin=subprocess.PIPE)
window = wait_for(lambda: next((w for w in call("windows") 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()) ready = json.loads(worker.stdout.readline())
expected = (round(640 * max(scale, density)), expected = (round(640 * max(scale, density)),
round(480 * 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, self.assertEqual((ready["width"], ready["height"]), expected,
"window sharing discarded native backing pixels") "window sharing discarded native backing pixels")
raw = root / f"{mode}.bgrx" raw = root / f"{mode}.bgrx"
@@ -134,6 +140,17 @@ class WindowCaptureScale(unittest.TestCase):
"content outside the window was captured") "content outside the window was captured")
elif density >= scale: elif density >= scale:
for index in (0, 2): 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 start = (index * width * height + height // 2 * width) * 4
row = pixels[start:start + width * 4] row = pixels[start:start + width * 4]
self.assertEqual(row, bytes((0, 0, 0, 255, 255, 255, 255, 255)) * (width // 2), self.assertEqual(row, bytes((0, 0, 0, 255, 255, 255, 255, 255)) * (width // 2),