203f53470cc336e2d1d7c929151b4a572e91a80f
A mask is about to reference a primitive already drawn and evaluate it at the masked pixel (docs/LAYOUT.md's "Masks with a shape"), which the data layout could not answer: a primitive's placement lived in its layer's *vertex* buffer, invisible to the fragment stage, and `rects`/`glyphs` were per layer too -- so a mask whose shape is a rounded container in one layer, clipping content a `Stack` put in another, would have read the wrong layer's rect with nothing on screen to say so. So the instances and the per-primitive data become one arena (`UiRenderState::primitives`), bound once per frame; a layer keeps only its draw *order*, which is what its vertex buffer now is -- one `u32` slot per instance instead of eight attributes. The vertex stage reads the placement it is drawing from `instances[slot]`; the fragment stage can read any other primitive's from the same buffer, which is what the mask work needs and the reason there is no second copy for masks. Arena slots are stable (nothing is compacted), so a `Mask` can hold one across frames. A slot freed during a redraw is therefore not reusable until every layer's order has been compacted around it -- otherwise the reused slot would draw twice, once through the stale order entry -- which is what `Primitives::freed` and `UiRenderState::apply_free` are. That compaction moved out of `UiRenderNode::update` into `UiRenderState:: update`: it is bookkeeping over `active`, not GPU work, and the harness (which has no renderer) needs it too. Same 164 tests, the `--phone` screenshot unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Languages
Rust
53%
Kotlin
44.4%
Shell
2.6%