--- a/include/scenefx/types/wlr_scene.h +++ b/include/scenefx/types/wlr_scene.h @@ -462,6 +462,11 @@ */ struct wlr_scene_tree *wlr_scene_tree_create(struct wlr_scene_tree *parent); +/** Experimental: copy a tree-local clip; NULL removes it. Width/height must be nonnegative. */ +void wlr_scene_tree_set_clip(struct wlr_scene_tree *tree, const struct wlr_box *clip); +/** Experimental: copy the currently applied tree-local clip; false if unset. */ +bool wlr_scene_tree_get_clip(struct wlr_scene_tree *tree, struct wlr_box *clip); + /** * Add a node displaying a single surface to the scene-graph. * --- a/types/scene/wlr_scene.c +++ b/types/scene/wlr_scene.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -40,6 +41,43 @@ #define DMABUF_FEEDBACK_DEBOUNCE_FRAMES 30 #define HIGHLIGHT_DAMAGE_FADEOUT_TIME 250 +// Experimental sidecar preserves the public scene structs shared with wlroots. +// A production primitive needs an upstream-owned, scalable attachment design. +struct scene_tree_clip { + struct scene_tree_clip *next; + struct wlr_scene_tree *tree; + struct wlr_box box; + struct wl_listener destroy; + bool active; // Private reservation; public scene ABI is unchanged. +}; +static struct scene_tree_clip *scene_tree_clips; + +static struct scene_tree_clip *scene_tree_clip_storage(struct wlr_scene_tree *tree) { + for (struct scene_tree_clip *it = scene_tree_clips; it; it = it->next) { + if (it->tree == tree) return it; + } + return NULL; +} + +// Inactive reservations are absent to get/render/hit, but survive NULL setters. +static struct scene_tree_clip *scene_tree_clip_find(struct wlr_scene_tree *tree) { + struct scene_tree_clip *entry = scene_tree_clip_storage(tree); + return entry && entry->active ? entry : NULL; +} + +static void scene_tree_clip_remove(struct scene_tree_clip *clip) { + struct scene_tree_clip **link = &scene_tree_clips; + while (*link != clip) link = &(*link)->next; + *link = clip->next; + wl_list_remove(&clip->destroy.link); + free(clip); +} + +static void scene_tree_clip_destroy(struct wl_listener *listener, void *data) { + struct scene_tree_clip *clip = wl_container_of(listener, clip, destroy); + scene_tree_clip_remove(clip); +} + struct wlr_scene_tree *wlr_scene_tree_from_node(struct wlr_scene_node *node) { assert(node->type == WLR_SCENE_NODE_TREE); struct wlr_scene_tree *tree = wl_container_of(node, tree, node); @@ -245,7 +283,20 @@ return NULL; } + // WHY: admission failure must precede parent publication and listeners. + // CONTRACT: every factory tree has an inactive reservation until destroy. + // INVARIANT: failed reservation leaves no published node or listener. + struct scene_tree_clip *entry = calloc(1, sizeof(*entry)); + if (entry == NULL) { + free(tree); + return NULL; + } scene_tree_init(tree, parent); + entry->tree = tree; + entry->destroy.notify = scene_tree_clip_destroy; + wl_signal_add(&tree->node.events.destroy, &entry->destroy); + entry->next = scene_tree_clips; + scene_tree_clips = entry; return tree; } @@ -673,6 +724,73 @@ } #endif +// WHY: int translation or Pixman endpoint overflow must not expose content. +// CONTRACT: reject unrepresentable coordinates before narrowing; aliases work. +// INVARIANT: render and input share the same fail-closed translation policy. +static bool scene_clip_translate(const struct wlr_box *box, int x, int y, + struct wlr_box *result) { + int64_t x1 = (int64_t)box->x + x, y1 = (int64_t)box->y + y; + int64_t x2 = x1 + box->width, y2 = y1 + box->height; + if (box->width < 0 || box->height < 0 || x1 < INT_MIN || x1 > INT_MAX || + y1 < INT_MIN || y1 > INT_MAX || x2 > INT_MAX || y2 > INT_MAX) { + *result = (struct wlr_box){0}; + return false; + } + *result = (struct wlr_box){(int)x1, (int)y1, box->width, box->height}; + return true; +} + +// WHY: leaf visibility is also the render/damage boundary for SceneFX effects. +// CONTRACT: clips are tree-local and intersect across all ancestors. +// INVARIANT: a descendant cannot expose pixels beyond any ancestor clip. +static void scene_node_intersect_clip(struct wlr_scene_node *node, + pixman_region32_t *region) { + for (struct wlr_scene_node *ancestor = node; ancestor != NULL; + ancestor = ancestor->parent ? &ancestor->parent->node : NULL) { + if (ancestor->type != WLR_SCENE_NODE_TREE) { + continue; + } + struct scene_tree_clip *clip = scene_tree_clip_find(wlr_scene_tree_from_node(ancestor)); + if (!clip) { + continue; + } + int x, y; + if (!wlr_scene_node_coords(ancestor, &x, &y)) { + pixman_region32_clear(region); + return; + } + struct wlr_box translated; + if (!scene_clip_translate(&clip->box, x, y, &translated)) { + pixman_region32_clear(region); + return; + } + pixman_region32_intersect_rect(region, region, + translated.x, translated.y, translated.width, translated.height); + } +} + +static bool scene_node_point_inside_clip(struct wlr_scene_node *node, + double lx, double ly) { + for (struct wlr_scene_node *ancestor = node; ancestor != NULL; + ancestor = ancestor->parent ? &ancestor->parent->node : NULL) { + if (ancestor->type != WLR_SCENE_NODE_TREE) { + continue; + } + struct scene_tree_clip *entry = scene_tree_clip_find(wlr_scene_tree_from_node(ancestor)); + if (!entry) { + continue; + } + int x, y; + if (!wlr_scene_node_coords(ancestor, &x, &y)) return false; + struct wlr_box clip; + if (!scene_clip_translate(&entry->box, x, y, &clip) || + !wlr_box_contains_point(&clip, lx, ly)) { + return false; + } + } + return true; +} + static bool scene_node_update_iterator(struct wlr_scene_node *node, int lx, int ly, void *_data) { struct scene_update_data *data = _data; @@ -695,11 +813,13 @@ pixman_region32_union(&node->visible, &node->visible, data->visible); pixman_region32_intersect_rect(&node->visible, &node->visible, lx, ly, box.width, box.height); + scene_node_intersect_clip(node, &node->visible); if (data->calculate_visibility) { pixman_region32_t opaque; pixman_region32_init(&opaque); scene_node_opaque_region(node, lx, ly, &opaque); + scene_node_intersect_clip(node, &opaque); pixman_region32_subtract(data->visible, data->visible, &opaque); pixman_region32_fini(&opaque); } @@ -749,7 +869,11 @@ int width, height; scene_node_get_size(node, &width, &height); - pixman_region32_union_rect(visible, visible, x, y, width, height); + pixman_region32_t bounds; + pixman_region32_init_rect(&bounds, x, y, width, height); + scene_node_intersect_clip(node, &bounds); + pixman_region32_union(visible, visible, &bounds); + pixman_region32_fini(&bounds); } static void scene_update_region(struct wlr_scene *scene, @@ -863,6 +987,37 @@ pixman_region32_fini(damage); } +void wlr_scene_tree_set_clip(struct wlr_scene_tree *tree, const struct wlr_box *clip) { + // WHY: mandatory clip changes must never allocate or silently fail open. + // CONTRACT: only factory-created trees may be passed, never embedded root. + // INVARIANT: NULL deactivates without releasing the reservation. + assert(tree && (!clip || (clip->width >= 0 && clip->height >= 0))); + struct scene_tree_clip *entry = scene_tree_clip_storage(tree); + if (entry == NULL) { + // Keep the precondition enforced even in NDEBUG builds. + abort(); + } + if (!clip && !entry->active) return; + if (clip && entry->active && memcmp(&entry->box, clip, sizeof(*clip)) == 0) return; + // WHY: private samples need the clip applied by this setter, including NULL. + // CONTRACT: passive sampled logging only; timing neutrality NOT_PROVEN. + // INVARIANT: no transaction generation, commit/present or GPU identity inferred. + wlr_log(WLR_INFO, "SPIKE_CLIP tree=%p rect=%d,%d %dx%d", + (void *)tree, clip ? clip->x : 0, clip ? clip->y : 0, + clip ? clip->width : 0, clip ? clip->height : 0); + entry->active = clip != NULL; + if (clip) entry->box = *clip; + scene_node_update(&tree->node, NULL); +} + +bool wlr_scene_tree_get_clip(struct wlr_scene_tree *tree, struct wlr_box *clip) { + assert(tree && clip); + struct scene_tree_clip *entry = scene_tree_clip_find(tree); + if (!entry) return false; + *clip = entry->box; + return true; +} + struct wlr_scene_rect *wlr_scene_rect_create(struct wlr_scene_tree *parent, int width, int height, const float color[static 4]) { assert(parent); @@ -1829,7 +1984,7 @@ int *lx_ptr, int *ly_ptr) { assert(node); - int lx = 0, ly = 0; + int64_t lx = 0, ly = 0; bool enabled = true; while (true) { lx += node->x; @@ -1842,8 +1997,14 @@ node = &node->parent->node; } - *lx_ptr = lx; - *ly_ptr = ly; + // WHY: translated ancestor coordinates can exceed the public int range. + // CONTRACT/INVARIANT: no narrowing overflow; unrepresentable is invisible. + if (lx < INT_MIN || lx > INT_MAX || ly < INT_MIN || ly > INT_MAX) { + *lx_ptr = *ly_ptr = 0; + return false; + } + *lx_ptr = (int)lx; + *ly_ptr = (int)ly; return enabled; } @@ -1883,6 +2044,9 @@ static bool scene_node_at_iterator(struct wlr_scene_node *node, int lx, int ly, void *data) { struct node_at_data *at_data = data; + if (!scene_node_point_inside_clip(node, at_data->lx, at_data->ly)) { + return false; + } double rx = at_data->lx - lx; double ry = at_data->ly - ly; @@ -2739,6 +2903,13 @@ const struct render_data *data) { struct wlr_scene_output *scene_output = data->output; struct wlr_scene_node *node = entry->node; + for (struct wlr_scene_node *ancestor = node; ancestor != NULL; + ancestor = ancestor->parent ? &ancestor->parent->node : NULL) { + if (ancestor->type == WLR_SCENE_NODE_TREE && + scene_tree_clip_find(wlr_scene_tree_from_node(ancestor))) { + return SCANOUT_INELIGIBLE; + } + } if (!scene_output->scene->direct_scanout) { return SCANOUT_INELIGIBLE;