From 553d0fd97abcf3b16eb20c9d14b1db62918a60f3 Mon Sep 17 00:00:00 2001 From: quake Date: Sat, 5 Sep 2026 09:30:54 +0900 Subject: [PATCH] perf(core): remove redundant layout allocations --- engine/core/src/layout.rs | 27 ++++++++++++++------------- engine/core/src/tree.rs | 8 ++------ 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/engine/core/src/layout.rs b/engine/core/src/layout.rs index 3adc27719..94497a77c 100644 --- a/engine/core/src/layout.rs +++ b/engine/core/src/layout.rs @@ -23,12 +23,12 @@ use crate::text::Fonts; use crate::tree::{LayoutRect, Tree}; /// Measure context attached to text leaves (taffy NodeContext). +/// +/// Deliberately minimal: the solve-time measure closure reads only `size`, +/// and shaping happens once in `MeasureCtx::shaped` (build/restyle) against +/// the freshly collected run — storing slot/tracking/line_height here (or +/// the run itself) would be dead weight duplicated per text node. pub struct MeasureCtx { - pub text: String, - pub slot: u8, - pub tracking: f32, - /// NAN = atlas default. - pub line_height: f32, /// Shaped size, computed ONCE when the context is (re)built. Text /// shaping is the expensive half of layout on the PSP; the taffy /// measure closure must never re-shape per solve pass. @@ -38,14 +38,14 @@ pub struct MeasureCtx { impl MeasureCtx { fn shaped( fonts: &Fonts, - text: String, + text: &str, slot: u8, tracking: f32, line_height: f32, native: bool, ) -> MeasureCtx { - let size = fonts.measure_run_provider(native, &text, slot, tracking, line_height); - MeasureCtx { text, slot, tracking, line_height, size } + let size = fonts.measure_run_provider(native, text, slot, tracking, line_height); + MeasureCtx { size } } } @@ -265,7 +265,7 @@ fn build( tree.slots[slot as usize].text_native = native; let ctx = MeasureCtx::shaped( fonts, - run, + &run, resolved.font_slot as u8, resolved.tracking, resolved.line_height, @@ -275,9 +275,10 @@ fn build( tree.slots[slot as usize].taffy = Some(nid); return Some(nid); } - let children = tree.slots[slot as usize].children.clone(); - let mut kids: Vec = Vec::with_capacity(children.len()); - for c in children { + let child_count = tree.slots[slot as usize].children.len(); + let mut kids: Vec = Vec::with_capacity(child_count); + for i in 0..child_count { + let c = tree.slots[slot as usize].children[i]; if let Some(cs) = tree.resolve(c) { if let Some(k) = build(tree, styles, fonts, taffy, cs, in_transform) { kids.push(k); @@ -389,7 +390,7 @@ pub fn relayout_root( tree.slots[slot as usize].text_native = native; let ctx = MeasureCtx::shaped( fonts, - run, + &run, resolved.font_slot as u8, resolved.tracking, resolved.line_height, diff --git a/engine/core/src/tree.rs b/engine/core/src/tree.rs index 42eea7f70..51f07bd72 100644 --- a/engine/core/src/tree.rs +++ b/engine/core/src/tree.rs @@ -320,10 +320,7 @@ impl Tree { pub fn collect_subtree(&self, id: i32, out: &mut Vec) { let Some(slot) = self.resolve(id) else { return }; out.push(slot); - // Children Vec is cloned per level to keep borrowck simple; subtree - // destruction is not a per-frame hot path. - let children = self.slots[slot as usize].children.clone(); - for c in children { + for &c in &self.slots[slot as usize].children { self.collect_subtree(c, out); } } @@ -333,8 +330,7 @@ impl Tree { pub fn collect_run(&self, slot: u32, out: &mut String) { let node = &self.slots[slot as usize]; out.push_str(&node.text); - let children = node.children.clone(); - for c in children { + for &c in &node.children { if let Some(cs) = self.resolve(c) { if self.slots[cs as usize].node_type == spec::NodeType::Text as u8 { self.collect_run(cs, out);