[Bugfix] UICollection mutations don't invalidate parent Frame's render cache #288

Closed
opened 2026-03-08 16:34:50 +00:00 by john · 0 comments
Owner

Bug

When a Frame uses clip_children=True or cache_subtree=True, its children are rendered to a RenderTexture. The texture is only rebuilt when render_dirty == true. After the first render, render_dirty is set to false (UIFrame.cpp:193).

No UICollection mutation method sets render_dirty back to true on the owning Frame. The collection has access to the owner via self->owner (a weak_ptr<UIDrawable>), but never calls owner->markContentDirty().

Affected methods (UICollection.cpp)

All of these modify the children vector but never dirty the parent:

  • append() (line ~619)
  • remove() (line ~704)
  • pop() (line ~714)
  • insert() (line ~746)
  • extend() / inplace_concat() (line ~647)
  • setitem() (line ~264) — replacing a child at index
  • ass_subscript() — slice assignment

Each calls McRFPy_API::markSceneNeedsSort() (scene-level z-index sorting), but that has nothing to do with Frame render cache invalidation.

Reproduction

box = mcrfpy.Frame(pos=(10,10), size=(400,300),
                    fill_color=mcrfpy.Color(40,40,40),
                    clip_children=True)
scene.children.append(box)

red = mcrfpy.Frame(pos=(10,10), size=(80,80),
                    fill_color=mcrfpy.Color(255,0,0))
box.children.append(red)
# First render: red box visible (render_dirty starts True)

# Later:
box.children.remove(red)
# Subsequent renders: red box STILL visible (stale RenderTexture)

Fix

Each mutation method should call markContentDirty() on the owner after modifying the vector:

// In UICollection::remove(), after vec->erase(it):
auto owner_ptr = self->owner.lock();
if (owner_ptr) {
    owner_ptr->markContentDirty();
}

Same pattern for append, pop, insert, extend, setitem, and slice operations.

Impact

This is invisible when clip_children=False (the default) because the standard render path at UIFrame.cpp:248 iterates the live vector every frame. It only manifests with render caching enabled, making it a confusing intermittent bug that depends on an unrelated flag.

## Bug When a Frame uses `clip_children=True` or `cache_subtree=True`, its children are rendered to a RenderTexture. The texture is only rebuilt when `render_dirty == true`. After the first render, `render_dirty` is set to `false` (`UIFrame.cpp:193`). **No UICollection mutation method sets `render_dirty` back to `true` on the owning Frame.** The collection has access to the owner via `self->owner` (a `weak_ptr<UIDrawable>`), but never calls `owner->markContentDirty()`. ### Affected methods (UICollection.cpp) All of these modify the children vector but never dirty the parent: - `append()` (line ~619) - `remove()` (line ~704) - `pop()` (line ~714) - `insert()` (line ~746) - `extend()` / `inplace_concat()` (line ~647) - `setitem()` (line ~264) — replacing a child at index - `ass_subscript()` — slice assignment Each calls `McRFPy_API::markSceneNeedsSort()` (scene-level z-index sorting), but that has nothing to do with Frame render cache invalidation. ### Reproduction ```python box = mcrfpy.Frame(pos=(10,10), size=(400,300), fill_color=mcrfpy.Color(40,40,40), clip_children=True) scene.children.append(box) red = mcrfpy.Frame(pos=(10,10), size=(80,80), fill_color=mcrfpy.Color(255,0,0)) box.children.append(red) # First render: red box visible (render_dirty starts True) # Later: box.children.remove(red) # Subsequent renders: red box STILL visible (stale RenderTexture) ``` ### Fix Each mutation method should call `markContentDirty()` on the owner after modifying the vector: ```cpp // In UICollection::remove(), after vec->erase(it): auto owner_ptr = self->owner.lock(); if (owner_ptr) { owner_ptr->markContentDirty(); } ``` Same pattern for `append`, `pop`, `insert`, `extend`, `setitem`, and slice operations. ### Impact This is invisible when `clip_children=False` (the default) because the standard render path at `UIFrame.cpp:248` iterates the live vector every frame. It only manifests with render caching enabled, making it a confusing intermittent bug that depends on an unrelated flag.
john closed this issue 2026-04-10 05:09:18 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
john/McRogueFace#288
No description provided.