aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/044-specs/layout_taffy/src
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-19 06:18:49 +0100
committereunhhu <[email protected]>2026-10-19 06:18:49 +0100
commit6215b1177e377b512aaa1296faa6311f809135f2 (patch)
treed2be13b3e5d4cb0b7150b0a6d186fc313842de18 /research/experiments/044-specs/layout_taffy/src
parent8071832bc7857634f66c94592b27b188c882ea54 (diff)
downloadpure-6215b1177e377b512aaa1296faa6311f809135f2.tar.gz
pure-6215b1177e377b512aaa1296faa6311f809135f2.zip
140's 8, box layout, written in aup with no new opcode (aup's examples/layout.aup: a subset of CSS Flexbox 1 written down first, one line, row or column, padding, gap, six justify-content, four align-items, grow and shrink with 9.7's loop, cross sizes stretched, nothing sized by content): 2,000 random trees (29,388 nodes; 7,659 items frozen at their padding by the loop) are Taffy 0.15's within 0.01 (layout_taffy, built by cargo; the worst 8.3e-5) and layout.py's bit for bit on the three paths, the paths the same; the single-pass subset first tried refused 66% of trees, so the loop came in; Taffy given min sizes equal to the padding (given 0 it floors at 0 while resolving lengths, where 9.7 floors the content box), the reference adding from the left (sum compensates since Python 3.12); module_specs in make check: 100 trees (tests/specs/layout); bars: 116 lines against Python's 54, 4,754 ns a node, Taffy's 644 (7.4x), pure Python 5,396 (Pure 1.14x faster), 1,494 instructions a node (9.7's loop 44%); four frictions; aup's rows read by position no longer copy (99,067 ns a node before); the suite table's row 8; CHECKPOINT's direction row
Diffstat (limited to 'research/experiments/044-specs/layout_taffy/src')
-rw-r--r--research/experiments/044-specs/layout_taffy/src/main.rs168
1 files changed, 168 insertions, 0 deletions
diff --git a/research/experiments/044-specs/layout_taffy/src/main.rs b/research/experiments/044-specs/layout_taffy/src/main.rs
new file mode 100644
index 0000000..5d66df3
--- /dev/null
+++ b/research/experiments/044-specs/layout_taffy/src/main.rs
@@ -0,0 +1,168 @@
+//! Research 140: the known implementation box layout (spec 8) is checked against. Each line of stdin is a tree of
+//! flex containers, a JSON object a node: `dir` ("row", "column"), `w`, `h` (a length, or null: auto), `grow`, `shrink`,
+//! `basis` (a length, or null: auto), `pad` ([left, right, top, bottom]), `gap`, `justify` ("start", "end", "center",
+//! "space-between", "space-around", "space-evenly"), `align` ("start", "end", "center", "stretch") and `kids`. Taffy lays
+//! it out (flexbox, no wrap, no rounding) in the root's size; each line of stdout is the nodes' boxes in preorder,
+//! [x, y, w, h], each relative to its parent's. Min sizes are 0, which 9.7 floors at an item's padding (its content box
+//! at zero); Taffy, given 0, floors at 0 while it resolves lengths (its padding only bounds the size it ends with), so
+//! it is given the padding.
+//!
+//! `layout_taffy --time ROUNDS TREES` times Taffy instead, on the trees layout_check.py writes (each a u32 count, its
+//! nodes' rows of 14 binary64s in preorder, as the app takes them, and their boxes): each tree built and laid out, its
+//! boxes the file's within 0.01 first, then the median of seven runs of ROUNDS passes. One JSON line.
+use serde_json::Value;
+use std::io::{self, BufRead, Write};
+use std::time::Instant;
+use taffy::prelude::*;
+
+fn length(v: &Value) -> Dimension {
+ v.as_f64().map_or(Dimension::auto(), |x| Dimension::length(x as f32))
+}
+
+fn node(tree: &mut TaffyTree<()>, v: &Value) -> NodeId {
+ let kids: Vec<NodeId> = v["kids"].as_array().map_or(vec![], |k| k.iter().map(|c| node(tree, c)).collect());
+ let pad: Vec<f32> = v["pad"].as_array().map_or(vec![0.0; 4], |p| p.iter().map(|x| x.as_f64().unwrap() as f32).collect());
+ let gap = v["gap"].as_f64().unwrap_or(0.0) as f32;
+ let style = Style {
+ display: Display::Flex,
+ flex_direction: if v["dir"] == "column" { FlexDirection::Column } else { FlexDirection::Row },
+ flex_wrap: FlexWrap::NoWrap,
+ size: Size { width: length(&v["w"]), height: length(&v["h"]) },
+ min_size: Size {
+ width: LengthPercentageAuto::length(pad[0] + pad[1]),
+ height: LengthPercentageAuto::length(pad[2] + pad[3]),
+ },
+ flex_grow: v["grow"].as_f64().unwrap_or(0.0) as f32,
+ flex_shrink: v["shrink"].as_f64().unwrap_or(0.0) as f32,
+ flex_basis: length(&v["basis"]),
+ padding: Rect {
+ left: LengthPercentage::length(pad[0]),
+ right: LengthPercentage::length(pad[1]),
+ top: LengthPercentage::length(pad[2]),
+ bottom: LengthPercentage::length(pad[3]),
+ },
+ gap: Size { width: LengthPercentage::length(gap), height: LengthPercentage::length(gap) },
+ justify_content: match v["justify"].as_str().unwrap_or("start") {
+ "end" => JustifyContent::END,
+ "center" => JustifyContent::CENTER,
+ "space-between" => JustifyContent::SPACE_BETWEEN,
+ "space-around" => JustifyContent::SPACE_AROUND,
+ "space-evenly" => JustifyContent::SPACE_EVENLY,
+ _ => JustifyContent::START,
+ },
+ align_items: match v["align"].as_str().unwrap_or("stretch") {
+ "start" => AlignItems::START,
+ "end" => AlignItems::END,
+ "center" => AlignItems::CENTER,
+ _ => AlignItems::STRETCH,
+ },
+ ..Default::default()
+ };
+ tree.new_with_children(style, &kids).unwrap()
+}
+
+fn boxes(tree: &TaffyTree<()>, n: NodeId, out: &mut Vec<[f32; 4]>) {
+ let l = tree.layout(n).unwrap();
+ out.push([l.location.x, l.location.y, l.size.width, l.size.height]);
+ for c in tree.children(n).unwrap() {
+ boxes(tree, c, out);
+ }
+}
+
+/// A row's node: parent, column, w, h (-1: auto), grow, shrink, basis, left, right, top, bottom, gap, justify, align.
+fn style_of(r: &[f64]) -> Style {
+ let length = |x: f64| if x < 0.0 { Dimension::auto() } else { Dimension::length(x as f32) };
+ let (l, rt, t, b) = (r[7] as f32, r[8] as f32, r[9] as f32, r[10] as f32);
+ Style {
+ display: Display::Flex,
+ flex_direction: if r[1] != 0.0 { FlexDirection::Column } else { FlexDirection::Row },
+ size: Size { width: length(r[2]), height: length(r[3]) },
+ min_size: Size { width: LengthPercentageAuto::length(l + rt), height: LengthPercentageAuto::length(t + b) },
+ flex_grow: r[4] as f32,
+ flex_shrink: r[5] as f32,
+ flex_basis: if r[0] < 0.0 { Dimension::auto() } else { length(r[6]) },
+ padding: Rect {
+ left: LengthPercentage::length(l),
+ right: LengthPercentage::length(rt),
+ top: LengthPercentage::length(t),
+ bottom: LengthPercentage::length(b),
+ },
+ gap: Size { width: LengthPercentage::length(r[11] as f32), height: LengthPercentage::length(r[11] as f32) },
+ justify_content: [JustifyContent::START, JustifyContent::END, JustifyContent::CENTER, JustifyContent::SPACE_BETWEEN,
+ JustifyContent::SPACE_AROUND, JustifyContent::SPACE_EVENLY][r[12] as usize],
+ align_items: [AlignItems::START, AlignItems::END, AlignItems::CENTER, AlignItems::STRETCH][r[13] as usize],
+ ..Default::default()
+ }
+}
+
+/// A tree of rows built and laid out in its root's size: its nodes, in the rows' order.
+fn lay(size: Size<f32>, styles: &[(i64, Style)]) -> (TaffyTree<()>, Vec<NodeId>) {
+ let mut tree: TaffyTree<()> = TaffyTree::new();
+ tree.disable_rounding();
+ let ids: Vec<NodeId> = styles.iter().map(|(_, s)| tree.new_leaf(s.clone()).unwrap()).collect();
+ for (k, (parent, _)) in styles.iter().enumerate().skip(1) {
+ tree.add_child(ids[*parent as usize], ids[k]).unwrap();
+ }
+ let space = Size { width: AvailableSpace::Definite(size.width), height: AvailableSpace::Definite(size.height) };
+ tree.compute_layout(ids[0], space).unwrap();
+ (tree, ids)
+}
+
+fn time(rounds: usize, path: &str) {
+ let data = std::fs::read(path).unwrap();
+ let number = |at: usize| f64::from_le_bytes(data[at..at + 8].try_into().unwrap());
+ let (mut trees, mut wants, mut at, mut nodes) = (vec![], vec![], 0, 0);
+ while at < data.len() {
+ let n = u32::from_le_bytes(data[at..at + 4].try_into().unwrap()) as usize;
+ let rows: Vec<Vec<f64>> = (0..n).map(|k| (0..14).map(|f| number(at + 4 + 112 * k + 8 * f)).collect()).collect();
+ let boxes: Vec<f64> = (0..4 * n).map(|f| number(at + 4 + 112 * n + 8 * f)).collect();
+ let size = Size { width: rows[0][2] as f32, height: rows[0][3] as f32 };
+ trees.push((size, rows.iter().map(|r| (r[0] as i64, style_of(r))).collect::<Vec<_>>()));
+ wants.push(boxes);
+ at += 4 + 112 * n + 32 * n;
+ nodes += n;
+ }
+ for ((size, styles), want) in trees.iter().zip(&wants) {
+ let (tree, ids) = lay(*size, styles);
+ for (k, id) in ids.iter().enumerate() {
+ let l = tree.layout(*id).unwrap();
+ let got = [l.location.x, l.location.y, l.size.width, l.size.height];
+ assert!((0..4).all(|f| (got[f] as f64 - want[4 * k + f]).abs() <= 0.01), "a box not the reference's");
+ }
+ }
+ let mut runs = vec![];
+ for _ in 0..7 {
+ let start = Instant::now();
+ for _ in 0..rounds {
+ for (size, styles) in &trees {
+ std::hint::black_box(lay(*size, styles));
+ }
+ }
+ runs.push(start.elapsed().as_nanos() as f64 / rounds as f64);
+ }
+ runs.sort_by(|a, b| a.partial_cmp(b).unwrap());
+ println!("{{\"trees\":{},\"nodes\":{},\"taffy_ns_per_node\":{:.1}}}", trees.len(), nodes, runs[3] / nodes as f64);
+}
+
+fn main() {
+ let args: Vec<String> = std::env::args().collect();
+ if args.len() == 4 && args[1] == "--time" {
+ return time(args[2].parse().unwrap(), &args[3]);
+ }
+ let stdout = io::stdout();
+ let mut out = io::BufWriter::new(stdout.lock());
+ for line in io::stdin().lock().lines() {
+ let v: Value = serde_json::from_str(&line.unwrap()).unwrap();
+ let mut tree: TaffyTree<()> = TaffyTree::new();
+ tree.disable_rounding();
+ let root = node(&mut tree, &v);
+ let space = Size {
+ width: AvailableSpace::Definite(v["w"].as_f64().unwrap() as f32),
+ height: AvailableSpace::Definite(v["h"].as_f64().unwrap() as f32),
+ };
+ tree.compute_layout(root, space).unwrap();
+ let mut b = vec![];
+ boxes(&tree, root, &mut b);
+ writeln!(out, "{}", serde_json::to_string(&b).unwrap()).unwrap();
+ }
+}