Creating or freeing a frame or region re-derives only what it touches (#915)

Any write that changed the layout graph's roster (a frame or region created, freed, re-owned or re-pointed) made the next resolve derive the whole graph: about 1.35 ms for one `CreateTexture` or `CreateFrame` on the stock UI, against 0.05 ms for a no-op.

A write now names the nodes it touched, and the resolve re-derives just those in the cached graph: a joining node is added with its edges, a leaving one is removed and its dependents seeded, a staying one has its edges and inputs re-synced. The full derivation stays the fallback. A region's owner and an art change are now layout inputs that name their region. Verify now also compares the graph itself.

A randomized test checks rects and graph against a scratch solve after every batch of creates, frees, re-owns and re-anchors. `CreateTexture` now costs 0.14 ms, `CreateFrame` 0.15 ms, a backdrop rebuild 0.22 ms.

Fixes #903

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Sam Sung
2026-10-12 01:05:23 +07:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 55729baff9
commit fa5568dcdc
32 changed files with 1318 additions and 376 deletions
+10 -14
View File
@@ -189,7 +189,7 @@ fn ensure_lines(lua: &Lua, this: &Table, n: usize) -> mlua::Result<()> {
}];
model.region_data.insert(left, d);
model.touch_measure(left); // an adopted cell can arrive text-in-hand
model.touch_layout(); // a line row entered the layout graph
model.touch_layout_region(left); // a line row entered the layout graph
}
let left_id = model.region_id(left);
@@ -217,7 +217,7 @@ fn ensure_lines(lua: &Lua, this: &Table, n: usize) -> mlua::Result<()> {
d.anchors = vec![Anchor::new(Point::Right, left_id, Point::Right, 0.0, 0.0)];
model.region_data.insert(right, d);
model.touch_measure(right); // an adopted cell can arrive text-in-hand
model.touch_layout(); // a line row entered the layout graph
model.touch_layout_region(right); // a line row entered the layout graph
}
let right_id = model.region_id(right);
@@ -447,16 +447,16 @@ fn apply_anchor(
TooltipAnchor::BottomLeft => Some((Point::TopRight, Point::BottomLeft)),
TooltipAnchor::Cursor | TooltipAnchor::None | TooltipAnchor::Preserve => None,
};
// The drop is a named retarget to no targets, not a conservative touch.
if matches!(anchor, TooltipAnchor::Cursor | TooltipAnchor::None) {
let dropped: Option<Vec<u32>> = match model.layout_inputs.get_mut(&h) {
let dropped = match model.layout_inputs.get_mut(&h) {
Some(input) if !input.anchors.is_empty() => {
Some(input.anchors.drain(..).map(|a| a.relative_to).collect())
input.anchors.clear();
true
}
_ => None,
_ => false,
};
if let Some(old_targets) = dropped {
model.touch_layout_retarget_frame(h, &old_targets, &[]);
if dropped {
model.touch_layout_frame(h);
}
}
if let Some((own, rel)) = pts {
@@ -467,11 +467,8 @@ fn apply_anchor(
let same = input.anchors.len() == 1
&& crate::script::object::anchor_bits_eq(&input.anchors[0], &new);
if !same {
// A named retarget: the cached layout graph re-points this edge instead of being
// rebuilt.
let old_targets: Vec<u32> = input.anchors.iter().map(|a| a.relative_to).collect();
input.anchors = vec![new];
model.touch_layout_retarget_frame(h, &old_targets, &[owner_id]);
model.touch_layout_frame(h);
}
}
}
@@ -716,9 +713,8 @@ fn cursor_anchor(model: &mut Model, h: FrameHandle) {
if same {
return;
}
let old_targets: Vec<u32> = input.anchors.iter().map(|a| a.relative_to).collect();
input.anchors = vec![new];
model.touch_layout_retarget_frame(h, &old_targets, &[super::SCREEN]);
model.touch_layout_frame(h);
}
pub(super) fn layout_tooltips(model: &mut Model) {
@@ -241,7 +241,8 @@ pub(in crate::script) fn install(lua: &Lua) -> mlua::Result<()> {
d.justify.set_h(crate::script::JustifyH::Right);
d.anchors = vec![Anchor::new(Point::Right, left_id, Point::Right, 0.0, 0.0)];
}
model.touch_layout(); // two line rows entered the layout graph
model.touch_layout_region(lh); // two line rows entered the layout graph
model.touch_layout_region(rh);
let t = tip_mut(&mut model, h)?;
t.left_lines.push(lh);
t.right_lines.push(rh);