From a59cc2ac322612bd6af4420b11e043bc1f740f91 Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Thu, 26 Feb 2026 16:24:16 +0100 Subject: [PATCH 1/5] util/box: Use integer min/max for intersection wlr_box_intersection only operates on integers, so we shouldn't use fmin/fmax. Do the usual and add a local integer min/max helper. --- util/box.c | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/util/box.c b/util/box.c index aae09888f..489c5ad2e 100644 --- a/util/box.c +++ b/util/box.c @@ -4,6 +4,14 @@ #include #include +static int max(int a, int b) { + return a > b ? a : b; +} + +static int min(int a, int b) { + return a < b ? a : b; +} + void wlr_box_closest_point(const struct wlr_box *box, double x, double y, double *dest_x, double *dest_y) { // if box is empty, then it contains no points, so no closest point either @@ -56,10 +64,10 @@ bool wlr_box_intersection(struct wlr_box *dest, const struct wlr_box *box_a, return false; } - int x1 = fmax(box_a->x, box_b->x); - int y1 = fmax(box_a->y, box_b->y); - int x2 = fmin(box_a->x + box_a->width, box_b->x + box_b->width); - int y2 = fmin(box_a->y + box_a->height, box_b->y + box_b->height); + int x1 = max(box_a->x, box_b->x); + int y1 = max(box_a->y, box_b->y); + int x2 = min(box_a->x + box_a->width, box_b->x + box_b->width); + int y2 = min(box_a->y + box_a->height, box_b->y + box_b->height); dest->x = x1; dest->y = y1; From f1f1570216173014624bfb5d7bcd057f217118c1 Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Thu, 26 Feb 2026 16:41:48 +0100 Subject: [PATCH 2/5] util/box: Add wlr_box_intersects wlr_box_intersection generates a new box based on the intersection of two boxes. Often we simply want to know *if* two boxes intersected, which we can answer much cheaper. Add wlr_box_intersects, in similar vein as wlr_box_contains_box but returning true for any overlap. --- include/wlr/util/box.h | 7 +++++++ util/box.c | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/include/wlr/util/box.h b/include/wlr/util/box.h index 64dcbc5cf..f6809e0c3 100644 --- a/include/wlr/util/box.h +++ b/include/wlr/util/box.h @@ -107,6 +107,13 @@ void wlr_fbox_transform(struct wlr_fbox *dest, const struct wlr_fbox *box, #ifdef WLR_USE_UNSTABLE +/** + * Checks whether two boxes intersect. + * + * Returns false if either box is empty. + */ +bool wlr_box_intersects(const struct wlr_box *a, const struct wlr_box *b); + /** * Returns true if the two boxes are equal, false otherwise. */ diff --git a/util/box.c b/util/box.c index 489c5ad2e..62d405488 100644 --- a/util/box.c +++ b/util/box.c @@ -102,6 +102,15 @@ bool wlr_box_contains_box(const struct wlr_box *bigger, const struct wlr_box *sm smaller->y + smaller->height <= bigger->y + bigger->height; } +bool wlr_box_intersects(const struct wlr_box *a, const struct wlr_box *b) { + if (wlr_box_empty(a) || wlr_box_empty(b)) { + return false; + } + + return a->x < b->x + b->width && b->x < a->x + a->width && + a->y < b->y + b->height && b->y < a->y + a->height; +} + void wlr_box_transform(struct wlr_box *dest, const struct wlr_box *box, enum wl_output_transform transform, int width, int height) { struct wlr_box src = {0}; From 92059bcab0c2e288848e5cf3d0f98cf65ff05f71 Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Thu, 26 Feb 2026 17:12:41 +0100 Subject: [PATCH 3/5] tests: Initial test and benchmark setup Add a unit test for wlr_box and benchmark for wlr_scene_node_at as our first test examples, lowering the barrier for adding more tests as suitable. --- meson.build | 4 ++ meson.options | 1 + test/bench_scene.c | 154 +++++++++++++++++++++++++++++++++++++++++++++ test/meson.build | 10 +++ test/test_box.c | 149 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 318 insertions(+) create mode 100644 test/bench_scene.c create mode 100644 test/meson.build create mode 100644 test/test_box.c diff --git a/meson.build b/meson.build index f054cc250..310120355 100644 --- a/meson.build +++ b/meson.build @@ -178,6 +178,10 @@ if get_option('examples') subdir('tinywl') endif +if get_option('tests') + subdir('test') +endif + pkgconfig = import('pkgconfig') pkgconfig.generate( lib_wlr, diff --git a/meson.options b/meson.options index 5863764aa..d8a8ca940 100644 --- a/meson.options +++ b/meson.options @@ -7,5 +7,6 @@ option('backends', type: 'array', choices: ['auto', 'drm', 'libinput', 'x11'], v option('allocators', type: 'array', choices: ['auto', 'gbm', 'udmabuf'], value: ['auto'], description: 'Select built-in allocators') option('session', type: 'feature', value: 'auto', description: 'Enable session support') +option('tests', type: 'boolean', value: true, description: 'Build tests and benchmarks') option('color-management', type: 'feature', value: 'auto', description: 'Enable support for color management') option('libliftoff', type: 'feature', value: 'auto', description: 'Enable support for libliftoff') diff --git a/test/bench_scene.c b/test/bench_scene.c new file mode 100644 index 000000000..6d6b270f0 --- /dev/null +++ b/test/bench_scene.c @@ -0,0 +1,154 @@ +#include +#include +#include + +struct tree_spec { + // Parameters for the tree we'll construct + int depth; + int branching; + int rect_size; + int spread; + + // Stats around the tree we built + int tree_count; + int rect_count; + int max_x; + int max_y; +}; + +static int max(int a, int b) { + return a > b ? a : b; +} + +static double timespec_diff_msec(struct timespec *start, struct timespec *end) { + return (double)(end->tv_sec - start->tv_sec) * 1e3 + + (double)(end->tv_nsec - start->tv_nsec) / 1e6; +} + +static bool build_tree(struct wlr_scene_tree *parent, struct tree_spec *spec, + int depth, int x, int y) { + + if (depth == spec->depth) { + float color[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; + struct wlr_scene_rect *rect = + wlr_scene_rect_create(parent, spec->rect_size, spec->rect_size, color); + if (rect == NULL) { + fprintf(stderr, "wlr_scene_rect_create failed\n"); + return false; + } + wlr_scene_node_set_position(&rect->node, x, y); + spec->max_x = max(spec->max_x, x + spec->rect_size); + spec->max_y = max(spec->max_y, y + spec->rect_size); + spec->rect_count++; + return true; + } + + for (int i = 0; i < spec->branching; i++) { + struct wlr_scene_tree *child = wlr_scene_tree_create(parent); + if (child == NULL) { + fprintf(stderr, "wlr_scene_tree_create failed\n"); + return false; + } + spec->tree_count++; + int offset = i * spec->spread; + wlr_scene_node_set_position(&child->node, offset, offset); + if (!build_tree(child, spec, depth + 1, x + offset, y + offset)) { + return false; + } + } + return true; +} + +static bool bench_create_tree(struct wlr_scene *scene, struct tree_spec *spec) { + struct timespec start, end; + clock_gettime(CLOCK_MONOTONIC, &start); + if (!build_tree(&scene->tree, spec, 0, 0, 0)) { + fprintf(stderr, "build_tree failed\n"); + return false; + } + clock_gettime(CLOCK_MONOTONIC, &end); + + printf("Built tree with %d tree nodes, %d rect nodes\n\n", + spec->tree_count, spec->rect_count); + + double elapsed = timespec_diff_msec(&start, &end); + int nodes = spec->tree_count + spec->rect_count; + printf("create test tree: %d nodes, %.3f ms, %.0f nodes/ms\n", + nodes, elapsed, nodes / elapsed); + return true; +} + +static void bench_scene_node_at(struct wlr_scene *scene, struct tree_spec *spec) { + struct timespec start, end; + int iters = 10000; + int hits = 0; + + clock_gettime(CLOCK_MONOTONIC, &start); + for (int i = 0; i < iters; i++) { + // Spread lookups across the tree extent + double lx = (double)(i * 97 % spec->max_x); + double ly = (double)(i * 53 % spec->max_y); + double nx, ny; + struct wlr_scene_node *node = + wlr_scene_node_at(&scene->tree.node, lx, ly, &nx, &ny); + if (node != NULL) { + hits++; + } + } + clock_gettime(CLOCK_MONOTONIC, &end); + + double elapsed = timespec_diff_msec(&start, &end); + int nodes = (spec->tree_count + spec->rect_count) * iters; + printf("wlr_scene_node_at: %d iters, %.3f ms, %.0f nodes/ms (hits: %d/%d)\n", + iters, elapsed, nodes / elapsed, hits, iters); +} + +static void noop_iterator(struct wlr_scene_buffer *buffer, + int sx, int sy, void *user_data) { + (void)buffer; + (void)sx; + (void)sy; + int *cnt = user_data; + (*cnt)++; +} + +static void bench_scene_node_for_each_buffer(struct wlr_scene *scene, struct tree_spec *spec) { + struct timespec start, end; + int iters = 10000; + int hits = 0; + + clock_gettime(CLOCK_MONOTONIC, &start); + for (int i = 0; i < iters; i++) { + wlr_scene_node_for_each_buffer(&scene->tree.node, + noop_iterator, &hits); + } + clock_gettime(CLOCK_MONOTONIC, &end); + + double elapsed = timespec_diff_msec(&start, &end); + int nodes = (spec->tree_count + spec->rect_count) * iters; + printf("wlr_scene_node_for_each_buffer: %d iters, %.3f ms, %.0f nodes/ms (hits: %d/%d)\n", + iters, elapsed, nodes / elapsed, hits, iters); +} + +int main(void) { + struct wlr_scene *scene = wlr_scene_create(); + if (scene == NULL) { + fprintf(stderr, "wlr_scene_create failed\n"); + return 99; + } + + struct tree_spec spec = { + .depth = 5, + .branching = 5, + .rect_size = 10, + .spread = 100, + }; + if (!bench_create_tree(scene, &spec)) { + return 99; + } + bench_scene_node_at(scene, &spec); + bench_scene_node_for_each_buffer(scene, &spec); + + wlr_scene_node_destroy(&scene->tree.node); + return 0; +} diff --git a/test/meson.build b/test/meson.build new file mode 100644 index 000000000..f51b2c02c --- /dev/null +++ b/test/meson.build @@ -0,0 +1,10 @@ +test( + 'box', + executable('test-box', 'test_box.c', dependencies: wlroots), +) + +benchmark( + 'scene', + executable('bench-scene', 'bench_scene.c', dependencies: wlroots), + timeout: 30, +) diff --git a/test/test_box.c b/test/test_box.c new file mode 100644 index 000000000..fcd95a8e2 --- /dev/null +++ b/test/test_box.c @@ -0,0 +1,149 @@ +#include +#include +#include + +static void test_box_empty(void) { + // NULL is empty + assert(wlr_box_empty(NULL)); + + // Zero width/height + struct wlr_box box = { .x = 0, .y = 0, .width = 0, .height = 10 }; + assert(wlr_box_empty(&box)); + box = (struct wlr_box){ .x = 0, .y = 0, .width = 10, .height = 0 }; + assert(wlr_box_empty(&box)); + + // Negative width/height + box = (struct wlr_box){ .x = 0, .y = 0, .width = -1, .height = 10 }; + assert(wlr_box_empty(&box)); + box = (struct wlr_box){ .x = 0, .y = 0, .width = 10, .height = -1 }; + assert(wlr_box_empty(&box)); + + // Valid box + box = (struct wlr_box){ .x = 0, .y = 0, .width = 10, .height = 10 }; + assert(!wlr_box_empty(&box)); +} + +static void test_box_intersection(void) { + struct wlr_box dest; + + // Overlapping + struct wlr_box a = { .x = 0, .y = 0, .width = 100, .height = 100 }; + struct wlr_box b = { .x = 50, .y = 50, .width = 100, .height = 100 }; + assert(wlr_box_intersection(&dest, &a, &b)); + assert(dest.x == 50 && dest.y == 50 && + dest.width == 50 && dest.height == 50); + + // Non-overlapping + b = (struct wlr_box){ .x = 200, .y = 200, .width = 50, .height = 50 }; + assert(!wlr_box_intersection(&dest, &a, &b)); + assert(dest.width == 0 && dest.height == 0); + + // Touching edges + b = (struct wlr_box){ .x = 100, .y = 0, .width = 50, .height = 50 }; + assert(!wlr_box_intersection(&dest, &a, &b)); + + // Self-intersection + assert(wlr_box_intersection(&dest, &a, &a)); + assert(dest.x == a.x && dest.y == a.y && + dest.width == a.width && dest.height == a.height); + + // Empty input + struct wlr_box empty = { .x = 0, .y = 0, .width = 0, .height = 0 }; + assert(!wlr_box_intersection(&dest, &a, &empty)); + + // NULL input + assert(!wlr_box_intersection(&dest, &a, NULL)); + assert(!wlr_box_intersection(&dest, NULL, &a)); +} + +static void test_box_intersects_box(void) { + // Overlapping + struct wlr_box a = { .x = 0, .y = 0, .width = 100, .height = 100 }; + struct wlr_box b = { .x = 50, .y = 50, .width = 100, .height = 100 }; + assert(wlr_box_intersects(&a, &b)); + + // Non-overlapping + b = (struct wlr_box){ .x = 200, .y = 200, .width = 50, .height = 50 }; + assert(!wlr_box_intersects(&a, &b)); + + // Touching edges + b = (struct wlr_box){ .x = 100, .y = 0, .width = 50, .height = 50 }; + assert(!wlr_box_intersects(&a, &b)); + + // Self-intersection + assert(wlr_box_intersects(&a, &a)); + + // Empty input + struct wlr_box empty = { .x = 0, .y = 0, .width = 0, .height = 0 }; + assert(!wlr_box_intersects(&a, &empty)); + + // NULL input + assert(!wlr_box_intersects(&a, NULL)); + assert(!wlr_box_intersects(NULL, &a)); +} + +static void test_box_contains_point(void) { + struct wlr_box box = { .x = 10, .y = 20, .width = 100, .height = 50 }; + + // Interior point + assert(wlr_box_contains_point(&box, 50, 40)); + + // Inclusive lower bound + assert(wlr_box_contains_point(&box, 10, 20)); + + // Exclusive upper bound + assert(!wlr_box_contains_point(&box, 110, 70)); + assert(!wlr_box_contains_point(&box, 110, 40)); + assert(!wlr_box_contains_point(&box, 50, 70)); + + // Outside + assert(!wlr_box_contains_point(&box, 5, 40)); + assert(!wlr_box_contains_point(&box, 50, 15)); + + // Empty box + struct wlr_box empty = { .x = 0, .y = 0, .width = 0, .height = 0 }; + assert(!wlr_box_contains_point(&empty, 0, 0)); + + // NULL + assert(!wlr_box_contains_point(NULL, 0, 0)); +} + +static void test_box_contains_box(void) { + struct wlr_box outer = { .x = 0, .y = 0, .width = 100, .height = 100 }; + + // Fully contained + struct wlr_box inner = { .x = 10, .y = 10, .width = 50, .height = 50 }; + assert(wlr_box_contains_box(&outer, &inner)); + + // Self-containment + assert(wlr_box_contains_box(&outer, &outer)); + + // Partial overlap — not contained + struct wlr_box partial = { .x = 50, .y = 50, .width = 100, .height = 100 }; + assert(!wlr_box_contains_box(&outer, &partial)); + + // Empty inner + struct wlr_box empty = { .x = 0, .y = 0, .width = 0, .height = 0 }; + assert(!wlr_box_contains_box(&outer, &empty)); + + // Empty outer + assert(!wlr_box_contains_box(&empty, &inner)); + + // NULL + assert(!wlr_box_contains_box(&outer, NULL)); + assert(!wlr_box_contains_box(NULL, &outer)); +} + +int main(void) { +#ifdef NDEBUG + fprintf(stderr, "NDEBUG must be disabled for tests\n"); + return 1; +#endif + + test_box_empty(); + test_box_intersection(); + test_box_intersects_box(); + test_box_contains_point(); + test_box_contains_box(); + return 0; +} From ada3c5a4e1e5e53bee289a8e8063260273edb1b1 Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Fri, 27 Feb 2026 13:24:45 +0100 Subject: [PATCH 4/5] ci: Run tests and benchmarks --- .builds/alpine.yml | 3 +++ .builds/archlinux.yml | 4 ++++ .builds/freebsd.yml | 3 +++ 3 files changed, 10 insertions(+) diff --git a/.builds/alpine.yml b/.builds/alpine.yml index affd85411..f8d75d8b1 100644 --- a/.builds/alpine.yml +++ b/.builds/alpine.yml @@ -35,6 +35,9 @@ tasks: cd wlroots ninja -C build sudo ninja -C build install + - test: | + cd wlroots + meson test -C build --verbose - build-features-disabled: | cd wlroots meson setup build --reconfigure -Dauto_features=disabled diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml index fae04ab31..84e04eda7 100644 --- a/.builds/archlinux.yml +++ b/.builds/archlinux.yml @@ -37,6 +37,10 @@ tasks: - clang: | cd wlroots/build-clang ninja + - test: | + cd wlroots/build-gcc + meson test --verbose + meson test --benchmark --verbose - smoke-test: | cd wlroots/build-gcc/tinywl sudo modprobe vkms diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index a99e9c911..3bfe37489 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -32,6 +32,9 @@ tasks: meson setup build --fatal-meson-warnings -Dauto_features=enabled -Dallocators=gbm ninja -C build sudo ninja -C build install + - test: | + cd wlroots + meson test -C build --verbose - tinywl: | cd wlroots/tinywl make From ba5dbf037971c527c375a55a204ddd75e7dbd792 Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Thu, 26 Feb 2026 16:50:16 +0100 Subject: [PATCH 5/5] Adopt wlr_box_intersects where useful This makes wlr_scene_node_at roughly 50% faster, and gives a minor boost to node modification as well. Before: create test tree: 7030 nodes, 473.510 s, 15 nodes/ms wlr_scene_node_at: 10000 iters, 894.945 s, 78552 nodes/ms (hits: 10/10000) wlr_scene_node_for_each_buffer: 10000 iters, 330.597 s, 212646 nodes/ms (hits: 0/10000) After: create test tree: 7030 nodes, 385.930 s, 18 nodes/ms wlr_scene_node_at: 10000 iters, 586.013 s, 119963 nodes/ms (hits: 10/10000) wlr_scene_node_for_each_buffer: 10000 iters, 334.559 s, 210127 nodes/ms (hits: 0/10000) --- types/output/cursor.c | 3 +-- types/output/output.c | 2 +- types/scene/wlr_scene.c | 5 ++--- types/wlr_output_layout.c | 6 ++---- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/types/output/cursor.c b/types/output/cursor.c index 5e93b0b2e..4a823dab1 100644 --- a/types/output/cursor.c +++ b/types/output/cursor.c @@ -159,9 +159,8 @@ static void output_cursor_update_visible(struct wlr_output_cursor *cursor) { struct wlr_box cursor_box; output_cursor_get_box(cursor, &cursor_box); - struct wlr_box intersection; cursor->visible = - wlr_box_intersection(&intersection, &output_box, &cursor_box); + wlr_box_intersects(&output_box, &cursor_box); } static bool output_pick_cursor_format(struct wlr_output *output, diff --git a/types/output/output.c b/types/output/output.c index 3715950ce..b1da61ed1 100644 --- a/types/output/output.c +++ b/types/output/output.c @@ -615,7 +615,7 @@ static bool output_basic_test(struct wlr_output *output, }; struct wlr_box dst_box; output_state_get_buffer_dst_box(state, &dst_box); - if (!wlr_box_intersection(&output_box, &output_box, &dst_box)) { + if (!wlr_box_intersects(&output_box, &dst_box)) { wlr_log(WLR_ERROR, "Primary buffer is entirely off-screen or 0-sized"); return false; } diff --git a/types/scene/wlr_scene.c b/types/scene/wlr_scene.c index 51373e15a..2ca93c8cd 100644 --- a/types/scene/wlr_scene.c +++ b/types/scene/wlr_scene.c @@ -238,7 +238,7 @@ static bool _scene_nodes_in_box(struct wlr_scene_node *node, struct wlr_box *box struct wlr_box node_box = { .x = lx, .y = ly }; scene_node_get_size(node, &node_box.width, &node_box.height); - if (wlr_box_intersection(&node_box, &node_box, box) && + if (wlr_box_intersects(&node_box, box) && iterator(node, lx, ly, user_data)) { return true; } @@ -2673,8 +2673,7 @@ static void scene_output_for_each_scene_buffer(const struct wlr_box *output_box, struct wlr_box node_box = { .x = lx, .y = ly }; scene_node_get_size(node, &node_box.width, &node_box.height); - struct wlr_box intersection; - if (wlr_box_intersection(&intersection, output_box, &node_box)) { + if (wlr_box_intersects(output_box, &node_box)) { struct wlr_scene_buffer *scene_buffer = wlr_scene_buffer_from_node(node); user_iterator(scene_buffer, lx, ly, user_data); diff --git a/types/wlr_output_layout.c b/types/wlr_output_layout.c index ef2751179..7226809e7 100644 --- a/types/wlr_output_layout.c +++ b/types/wlr_output_layout.c @@ -260,14 +260,12 @@ bool wlr_output_layout_contains_point(struct wlr_output_layout *layout, bool wlr_output_layout_intersects(struct wlr_output_layout *layout, struct wlr_output *reference, const struct wlr_box *target_lbox) { - struct wlr_box out_box; - if (reference == NULL) { struct wlr_output_layout_output *l_output; wl_list_for_each(l_output, &layout->outputs, link) { struct wlr_box output_box; output_layout_output_get_box(l_output, &output_box); - if (wlr_box_intersection(&out_box, &output_box, target_lbox)) { + if (wlr_box_intersects(&output_box, target_lbox)) { return true; } } @@ -281,7 +279,7 @@ bool wlr_output_layout_intersects(struct wlr_output_layout *layout, struct wlr_box output_box; output_layout_output_get_box(l_output, &output_box); - return wlr_box_intersection(&out_box, &output_box, target_lbox); + return wlr_box_intersects(&output_box, target_lbox); } }