Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions changelog.d/8503-taffy-013.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### Fixed

- Port the TUI and Yoga layout backends to taffy 0.13. Yoga nodes now stay in
their owning thread, avoiding an unsafe `Send` assertion for taffy's compact
style storage, while each JS worker registers a GC scanner for its stored
measure callbacks. Existing Yoga overflow alignment remains explicitly
`unsafe`, matching the pre-upgrade behavior.
2 changes: 1 addition & 1 deletion crates/perry-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ rand = "0.10"
regex = { workspace = true, optional = true }
# Taffy — flexbox / grid layout engine for the perry/tui module
# (#358 Phase 3). Same crate Bevy and Dioxus use; pure Rust, no FFI.
taffy = { version = "0.7", default-features = false, features = ["std", "flexbox", "taffy_tree"] }
taffy = { version = "0.13", default-features = false, features = ["std", "flexbox", "taffy_tree"] }
fancy-regex = { version = "0.18", optional = true }
itoa.workspace = true
ryu.workspace = true
Expand Down
18 changes: 9 additions & 9 deletions crates/perry-runtime/src/tui/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,17 @@ fn box_style_to_taffy(s: &BoxStyle) -> Style {
FlexDirection::Column => taffy::FlexDirection::Column,
};
let justify = match s.justify_content {
JustifyContent::Start => Some(taffy::JustifyContent::Start),
JustifyContent::Center => Some(taffy::JustifyContent::Center),
JustifyContent::End => Some(taffy::JustifyContent::End),
JustifyContent::SpaceBetween => Some(taffy::JustifyContent::SpaceBetween),
JustifyContent::SpaceAround => Some(taffy::JustifyContent::SpaceAround),
JustifyContent::Start => Some(taffy::JustifyContent::START),
JustifyContent::Center => Some(taffy::JustifyContent::CENTER),
JustifyContent::End => Some(taffy::JustifyContent::END),
JustifyContent::SpaceBetween => Some(taffy::JustifyContent::SPACE_BETWEEN),
JustifyContent::SpaceAround => Some(taffy::JustifyContent::SPACE_AROUND),
};
let align = match s.align_items {
AlignItems::Start => Some(taffy::AlignItems::Start),
AlignItems::Center => Some(taffy::AlignItems::Center),
AlignItems::End => Some(taffy::AlignItems::End),
AlignItems::Stretch => Some(taffy::AlignItems::Stretch),
AlignItems::Start => Some(taffy::AlignItems::START),
AlignItems::Center => Some(taffy::AlignItems::CENTER),
AlignItems::End => Some(taffy::AlignItems::END),
AlignItems::Stretch => Some(taffy::AlignItems::STRETCH),
};
Style {
display: Display::Flex,
Expand Down
136 changes: 99 additions & 37 deletions crates/perry-runtime/src/yoga.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
//! constants (`YGEnums.ts`).
//!
//! Nodes are plain integer handles (an `f64` id, not a tagged pointer) into
//! a per-process registry. `calculateLayout` builds a fresh `TaffyTree` from
//! a per-thread registry. `calculateLayout` builds a fresh `TaffyTree` from
//! the registry, runs the solver (calling stored JS measure callbacks for
//! leaf text nodes), and writes the relative computed `Layout` back onto each
//! node for `getComputed*` to read — mirroring how `tui/layout.rs` works.

use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::sync::Mutex;

Expand Down Expand Up @@ -115,38 +116,46 @@ impl YogaNode {
}
}

static YOGA_NODES: Mutex<Option<HashMap<u32, YogaNode>>> = Mutex::new(None);
crate::perry_thread_local! {
static YOGA_NODES: RefCell<Option<HashMap<u32, YogaNode>>> = const { RefCell::new(None) };
// The GC scanner registry is also thread-local, so this latch must be
// per-thread: every JS worker that creates yoga nodes needs its own entry.
static GC_SCANNER_REGISTERED: Cell<bool> = const { Cell::new(false) };
}
static YOGA_NEXT_ID: Mutex<u32> = Mutex::new(1);
static GC_SCANNER_REGISTERED: Mutex<bool> = Mutex::new(false);

fn with_nodes<R, F: FnOnce(&mut HashMap<u32, YogaNode>) -> R>(f: F) -> R {
ensure_gc_scanner_registered();
let mut guard = crate::gc::lock_gc_root_registry(&YOGA_NODES);
if guard.is_none() {
*guard = Some(HashMap::new());
}
f(guard.as_mut().unwrap())
YOGA_NODES.with(|nodes| {
let mut nodes = nodes.borrow_mut();
if nodes.is_none() {
*nodes = Some(HashMap::new());
}
f(nodes.as_mut().unwrap())
})
}

fn ensure_gc_scanner_registered() {
let mut reg = GC_SCANNER_REGISTERED.lock().unwrap();
if !*reg {
crate::gc::gc_register_mutable_root_scanner_named("perry_yoga", yoga_root_scanner);
*reg = true;
}
GC_SCANNER_REGISTERED.with(|registered| {
if !registered.get() {
crate::gc::gc_register_mutable_root_scanner_named("perry_yoga", yoga_root_scanner);
registered.set(true);
}
});
}

/// Keep stored measure callbacks alive across GC between `setMeasureFunc`
/// and `calculateLayout`.
fn yoga_root_scanner(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) {
let mut guard = crate::gc::lock_gc_root_registry(&YOGA_NODES);
if let Some(map) = guard.as_mut() {
for node in map.values_mut() {
if (node.measure.to_bits() & !POINTER_MASK) == POINTER_TAG {
visitor.visit_nanbox_f64_slot(&mut node.measure);
YOGA_NODES.with(|nodes| {
if let Some(map) = nodes.borrow_mut().as_mut() {
for node in map.values_mut() {
if (node.measure.to_bits() & !POINTER_MASK) == POINTER_TAG {
visitor.visit_nanbox_f64_slot(&mut node.measure);
}
}
}
}
});
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -240,37 +249,37 @@ fn map_flex_direction(v: u32) -> FlexDirection {

fn map_justify(v: u32) -> Option<JustifyContent> {
Some(match v {
0 => JustifyContent::FlexStart,
1 => JustifyContent::Center,
2 => JustifyContent::FlexEnd,
3 => JustifyContent::SpaceBetween,
4 => JustifyContent::SpaceAround,
5 => JustifyContent::SpaceEvenly,
0 => JustifyContent::FLEX_START,
1 => JustifyContent::CENTER,
2 => JustifyContent::FLEX_END,
3 => JustifyContent::SPACE_BETWEEN,
4 => JustifyContent::SPACE_AROUND,
5 => JustifyContent::SPACE_EVENLY,
_ => return None,
})
}

fn map_align(v: u32) -> Option<AlignItems> {
Some(match v {
1 => AlignItems::FlexStart,
2 => AlignItems::Center,
3 => AlignItems::FlexEnd,
4 => AlignItems::Stretch,
5 => AlignItems::Baseline,
1 => AlignItems::FLEX_START,
2 => AlignItems::CENTER,
3 => AlignItems::FLEX_END,
4 => AlignItems::STRETCH,
5 => AlignItems::BASELINE,
// 0 = Auto; 6/7/8 (space-*) aren't valid for align-items in taffy.
_ => return None,
})
}

fn map_align_content(v: u32) -> Option<AlignContent> {
Some(match v {
1 => AlignContent::FlexStart,
2 => AlignContent::Center,
3 => AlignContent::FlexEnd,
4 => AlignContent::Stretch,
6 => AlignContent::SpaceBetween,
7 => AlignContent::SpaceAround,
8 => AlignContent::SpaceEvenly,
1 => AlignContent::FLEX_START,
2 => AlignContent::CENTER,
3 => AlignContent::FLEX_END,
4 => AlignContent::STRETCH,
6 => AlignContent::SPACE_BETWEEN,
7 => AlignContent::SPACE_AROUND,
8 => AlignContent::SPACE_EVENLY,
_ => return None,
})
}
Expand Down Expand Up @@ -738,3 +747,56 @@ keep!(KEEP_YOGA_10: extern "C" fn(f64, f64, f64) -> f64 = js_yoga_set_enum);
keep!(KEEP_YOGA_11: extern "C" fn(f64, f64, f64, f64) -> f64 = js_yoga_calculate_layout);
keep!(KEEP_YOGA_12: extern "C" fn(f64, f64) -> f64 = js_yoga_get_computed);
keep!(KEEP_YOGA_13: extern "C" fn(f64, f64, f64) -> f64 = js_yoga_get_computed_edge);

#[cfg(test)]
mod tests {
use super::*;
use taffy::AlignmentSafety;

#[test]
fn alignment_mappings_keep_yogas_unsafe_overflow_behavior() {
for alignment in [
map_justify(0),
map_justify(1),
map_justify(2),
map_justify(3),
map_justify(4),
map_justify(5),
map_align_content(1),
map_align_content(2),
map_align_content(3),
map_align_content(4),
map_align_content(6),
map_align_content(7),
map_align_content(8),
] {
assert_eq!(alignment.unwrap().safety, AlignmentSafety::Unsafe);
}

for alignment in [
map_align(1),
map_align(2),
map_align(3),
map_align(4),
map_align(5),
] {
assert_eq!(alignment.unwrap().safety, AlignmentSafety::Unsafe);
}
}

#[test]
fn yoga_registry_and_gc_registration_are_per_thread() {
fn exercise_thread(id: u32) {
GC_SCANNER_REGISTERED.with(|registered| assert!(!registered.get()));
with_nodes(|nodes| {
assert!(nodes.is_empty());
nodes.insert(id, YogaNode::new());
});
GC_SCANNER_REGISTERED.with(|registered| assert!(registered.get()));
with_nodes(|nodes| assert_eq!(nodes.len(), 1));
}

std::thread::spawn(|| exercise_thread(1)).join().unwrap();
std::thread::spawn(|| exercise_thread(2)).join().unwrap();
}
}
Loading