Add the leveled boundary helpers and the deep group element copy

This commit is contained in:
Dennis Kobert
2026-08-22 11:48:26 +00:00
parent 8eaa541c8c
commit 5821816228
10 changed files with 399 additions and 17 deletions

View File

@@ -606,6 +606,63 @@ where
}
}
/// The outcome of materializing a leveled edge's whole flat span.
pub enum LevelStatus<'a> {
Batch(crate::node::RecordBatch<'a>, crate::gpoll::Finality),
Pending,
Error(crate::gpoll::GraphError),
}
/// Evaluates a leveled edge's whole flat span into one batch: an exact total
/// fills once, a lower bound drains by guess-and-double until a short fill,
/// each reply's hint seeding the next guess. The boundary consumers' driver;
/// reducers inline the same protocol with their span offsets.
pub fn materialize_level<'a, 'e, C, N>(node: &'a N, input: &'a C, arena: &'a crate::arena::Arena) -> LevelStatus<'a>
where
C: crate::context::InjectIndex + Copy,
N: Node<C, Output = RecordValue<'e>>,
{
use crate::gpoll::{Extent, GraphError, Level};
use crate::node::BatchStatus;
let sized = match node.extent(input, Level::Total) {
GPoll::Final(Extent::Exactly(count)) => Ok(count),
GPoll::Final(Extent::AtLeast(bound)) => Err(bound),
GPoll::Pending => return LevelStatus::Pending,
_ => return LevelStatus::Error(GraphError::new("materialize over a non-exact extent")),
};
match sized {
Ok(count) => match materialize_batch(node, input, 0..count as u64, arena) {
BatchStatus::Lent(batch, finality, _) => LevelStatus::Batch(batch, finality),
BatchStatus::Filled(batch, finality, _) => LevelStatus::Batch(batch.into_shared(), finality),
BatchStatus::Pending => LevelStatus::Pending,
BatchStatus::Error(error) => LevelStatus::Error(error),
_ => LevelStatus::Error(GraphError::new("materialize batch failed")),
},
Err(bound) => {
let mut guess = bound.max(16);
loop {
let (batch, finality, hint) = match materialize_batch(node, input, 0..guess as u64, arena) {
BatchStatus::Lent(batch, finality, hint) => (batch, finality, hint),
BatchStatus::Filled(batch, finality, hint) => (batch.into_shared(), finality, hint),
BatchStatus::Pending => return LevelStatus::Pending,
BatchStatus::Error(error) => return LevelStatus::Error(error),
_ => return LevelStatus::Error(GraphError::new("materialize batch failed")),
};
let filled = batch.len();
if filled < guess {
break LevelStatus::Batch(batch, finality);
}
match hint {
Extent::Exactly(total) if total <= filled => break LevelStatus::Batch(batch, finality),
Extent::Exactly(total) => guess = total,
Extent::AtLeast(more) => guess = (guess * 2).max(more),
Extent::Free => guess *= 2,
}
}
}
}
}
/// A record edge at a caller-chosen lifetime; the lifetime is a trait
/// parameter for the same constrained-position reason as
/// [`DerivedRecordEdge`].
@@ -671,6 +728,16 @@ impl<'a, N> RecordEdgeInput<'a, N> {
{
self.node.eval(ctx)
}
/// [`materialize_level`] over the edge: the wire's whole flat span as one
/// batch.
pub fn materialize_level<'e, 'b, C>(&'b self, ctx: &'b C, arena: &'b crate::arena::Arena) -> LevelStatus<'b>
where
N: Node<C, Output = RecordValue<'e>>,
C: crate::context::InjectIndex + Copy,
{
materialize_level(self.node, ctx, arena)
}
}
/// The raw lazy edge handed to a poll kernel whose wire rides records while
@@ -1178,10 +1245,33 @@ pub fn element_dims<T>() -> (usize, usize) {
}
}
/// Deep clone-out overrides for element types whose plain clone borrows the
/// evaluation's arena (a `Graphic` holding a group interior). The generic
/// element glue consults this registry, so every layout carrying such an
/// element deep-copies at memo and capture seams regardless of which
/// constructor built the glue. The override must produce a value of the
/// element's own type that owns all of its content, so the generic re-park
/// replays it unchanged.
static DEEP_ELEMENT_CLONES: std::sync::LazyLock<std::sync::Mutex<std::collections::HashMap<std::any::TypeId, unsafe fn(*const u8) -> Box<dyn std::any::Any + Send + Sync>>>> =
std::sync::LazyLock::new(Default::default);
/// Registers `clone_out` as the deep clone-out for elements of `T`. Called at
/// startup from the crate that owns the type.
pub fn register_deep_element_clone<T: 'static>(clone_out: unsafe fn(*const u8) -> Box<dyn std::any::Any + Send + Sync>) {
DEEP_ELEMENT_CLONES.lock().unwrap().insert(std::any::TypeId::of::<T>(), clone_out);
}
fn deep_element_clone(type_id: std::any::TypeId) -> Option<unsafe fn(*const u8) -> Box<dyn std::any::Any + Send + Sync>> {
DEEP_ELEMENT_CLONES.lock().unwrap().get(&type_id).copied()
}
/// The element slot a record wire of `T` carries, its erased glue bound at
/// the statically-known type.
pub fn element_write<T: Clone + Send + Sync + 'static>() -> ElementWrite {
unsafe fn clone_out<T: Clone + Send + Sync + 'static>(ptr: *const u8) -> Box<dyn std::any::Any + Send + Sync> {
if let Some(deep) = deep_element_clone(std::any::TypeId::of::<T>()) {
return unsafe { deep(ptr) };
}
Box::new(unsafe { read_element::<T>(Rec::new(ptr)) })
}
unsafe fn repark<T: Clone + Send + Sync + 'static>(value: &(dyn std::any::Any + Send + Sync), dst: *mut u8, arena: &crate::arena::Arena) -> Option<()> {
@@ -1398,6 +1488,7 @@ pub unsafe fn record_from_bytes<'e>(layout: &Layout, bytes: &'e [u8]) -> RecordV
#[derive(Clone)]
pub struct RecordCapture {
layout: Layout,
lanes: usize,
bytes: crate::arena::ArenaWeak<Box<[u8]>>,
}
@@ -1412,17 +1503,65 @@ impl RecordCapture {
/// `rec` must be a live record of `layout`.
pub unsafe fn capture(layout: &Layout, rec: Rec, arena: &crate::arena::Arena) -> Option<RecordCapture> {
let bytes = unsafe { copy_record_bytes(layout, rec) };
arena.alloc(bytes).map(|(_, weak)| RecordCapture { layout: layout.clone(), bytes: weak })
arena.alloc(bytes).map(|(_, weak)| RecordCapture {
layout: layout.clone(),
lanes: 1,
bytes: weak,
})
}
/// The captured element, cloned out through the layout's erased glue.
/// Captures every lane of a leveled wire's batch.
///
/// # Safety
/// `batch` must hold live records of `layout`.
pub unsafe fn capture_level(layout: &Layout, batch: crate::node::RecordBatch<'_>, arena: &crate::arena::Arena) -> Option<RecordCapture> {
let stride = layout.lane_stride();
let mut bytes = vec![0u8; batch.len() * stride].into_boxed_slice();
for lane in 0..batch.len() {
// SAFETY: both sides hold `len` lanes at the shared layout's stride.
unsafe { std::ptr::copy_nonoverlapping(batch.get(lane).rec().ptr(), bytes.as_mut_ptr().add(lane * stride), stride) };
}
arena.alloc(bytes).map(|(_, weak)| RecordCapture {
layout: layout.clone(),
lanes: batch.len(),
bytes: weak,
})
}
pub fn layout(&self) -> &Layout {
&self.layout
}
/// The captured lane count: one for a rank-0 capture, the whole extent
/// for a level capture.
pub fn lanes(&self) -> usize {
self.lanes
}
/// A batch view over the captured records, alive while the arena holds
/// the capture's generation.
pub fn batch<'a>(&'a self, arena: &'a crate::arena::Arena) -> Option<crate::node::RecordBatch<'a>> {
let bytes = self.bytes.upgrade(arena)?;
// SAFETY: the constructors store `lanes` records of `layout`.
Some(unsafe { crate::node::RecordBatch::new(bytes.as_ptr(), self.lanes, &self.layout) })
}
/// The captured element of the first lane, cloned out through the
/// layout's erased glue.
pub fn materialize_element(&self, arena: &crate::arena::Arena) -> Option<Box<dyn std::any::Any + Send + Sync>> {
let bytes = self.bytes.upgrade(arena)?;
Some(unsafe { (self.layout.element.clone_out)(bytes.as_ptr()) })
match self.lanes {
0 => None,
_ => Some(unsafe { (self.layout.element.clone_out)(bytes.as_ptr()) }),
}
}
/// The first lane's attributes, read out through the layout's erased glue.
pub fn materialize(&self, arena: &crate::arena::Arena) -> Option<Vec<(&'static str, Box<dyn crate::list::AnyAttributeValue>)>> {
let bytes = self.bytes.upgrade(arena)?;
if self.lanes == 0 {
return Some(Vec::new());
}
Some(
self.layout
.fields

View File

@@ -57,11 +57,11 @@ pub struct LeveledValueSource<T> {
layout: crate::record::Layout,
}
impl<T: Clone + Send + Sync + 'static> LeveledValueSource<T> {
impl<T: Clone + Send + Sync + crate::CacheHash + PartialEq + 'static> LeveledValueSource<T> {
pub fn new(values: Vec<T>) -> Self {
Self {
values,
layout: crate::record::Layout::default().with_writes(1, crate::record::element_write::<T>(), &[]),
layout: crate::record::Layout::default().with_writes(1, crate::record::element_write_hashed::<T>(), &[]),
}
}
}
@@ -93,7 +93,7 @@ where
}
/// The native record edge of a constant level: the edge type is the element's.
pub fn leveled_record_value_edge<T: Clone + Send + Sync + 'static>(values: Vec<T>) -> crate::registry::EdgeHandle {
pub fn leveled_record_value_edge<T: Clone + Send + Sync + crate::CacheHash + PartialEq + 'static>(values: Vec<T>) -> crate::registry::EdgeHandle {
crate::registry::EdgeHandle::new_record::<T>(std::sync::Arc::new(LeveledValueSource::new(values)) as std::sync::Arc<crate::registry::ErasedRecordNode>)
}

View File

@@ -0,0 +1,85 @@
//! Boundary helpers between leveled wires and the legacy editor surface:
//! the renderer's flip form materializes a wire into a group, and captured
//! wires convert to the legacy values the editor's downcasts expect.
use crate::graphic::{Graphic, group_to_legacy_list, run_to_legacy_list};
use crate::raster_types::{CPU, GPU, Raster};
use crate::{Artboard, Vector};
use core_types::Color;
use core_types::arena::Arena;
use core_types::context::InjectIndex;
use core_types::gpoll::{Finality, GraphError};
use core_types::node::Node;
use core_types::record::{Group, GroupContent, GroupItem, LevelStatus, RecordCapture, RecordValue, materialize_level};
use core_types::uuid::NodeId;
use glam::{DAffine2, DVec2};
use vector_types::GradientStops;
/// The outcome of materializing a leveled wire into a group.
pub enum LevelGroup {
Group(Group, Finality),
Pending,
Error(GraphError),
}
/// The renderer's flip form: the wire's whole extent materialized into a
/// group over the level's records, ready for the group render bridge.
pub fn materialize_group<'e, C, N>(node: &N, input: &C, arena: &Arena) -> LevelGroup
where
C: InjectIndex + Copy,
N: Node<C, Output = RecordValue<'e>>,
{
match materialize_level(node, input, arena) {
LevelStatus::Batch(batch, finality) => {
// SAFETY: a materialized batch's frames are arena-resident.
let item = unsafe { GroupItem::from_resident(batch) };
LevelGroup::Group(
Group {
row: None,
content: GroupContent::Run(item),
},
finality,
)
}
LevelStatus::Pending => LevelGroup::Pending,
LevelStatus::Error(error) => LevelGroup::Error(error),
}
}
/// The captured wire as the legacy value the editor's downcasts expect: a
/// rank-0 capture is its element, a leveled `Graphic` capture becomes its
/// legacy list through the group bridge, and another element type becomes a
/// legacy list of that element. `None` for an element type outside the
/// legacy vocabulary or a capture whose arena generation has passed.
pub fn capture_to_legacy(capture: &RecordCapture, arena: &Arena) -> Option<Box<dyn std::any::Any + Send + Sync>> {
if capture.layout().depth == 0 {
return capture.materialize_element(arena);
}
let batch = capture.batch(arena)?;
// SAFETY: the captured bytes live in the arena for the capture's generation.
let item = unsafe { GroupItem::from_resident(batch) };
fn typed<T: Clone + Send + Sync + 'static>(item: &GroupItem) -> Option<Box<dyn std::any::Any + Send + Sync>> {
run_to_legacy_list::<T>(item).map(|list| Box::new(list) as Box<dyn std::any::Any + Send + Sync>)
}
if item.typed_lanes::<Graphic>().is_some() {
let group = Group {
row: None,
content: GroupContent::Run(item),
};
return Some(Box::new(group_to_legacy_list(&group)));
}
None.or_else(|| typed::<Artboard>(&item))
.or_else(|| typed::<Vector>(&item))
.or_else(|| typed::<Raster<CPU>>(&item))
.or_else(|| typed::<Raster<GPU>>(&item))
.or_else(|| typed::<Color>(&item))
.or_else(|| typed::<GradientStops>(&item))
.or_else(|| typed::<String>(&item))
.or_else(|| typed::<f64>(&item))
.or_else(|| typed::<u64>(&item))
.or_else(|| typed::<u32>(&item))
.or_else(|| typed::<bool>(&item))
.or_else(|| typed::<NodeId>(&item))
.or_else(|| typed::<DAffine2>(&item))
.or_else(|| typed::<DVec2>(&item))
}

View File

@@ -674,7 +674,7 @@ fn group_bounding_box(group: &core_types::record::Group, transform: DAffine2, in
/// One typed run as a legacy list, elements cloned and every attribute
/// copied through its erased read.
fn run_to_legacy_list<T: Clone + Send + Sync + 'static>(item: &core_types::record::GroupItem) -> Option<List<T>> {
pub(crate) fn run_to_legacy_list<T: Clone + Send + Sync + 'static>(item: &core_types::record::GroupItem) -> Option<List<T>> {
let lanes = item.typed_lanes::<T>()?;
let mut list = List::new();
for lane in 0..lanes.len() {
@@ -690,6 +690,32 @@ fn run_to_legacy_list<T: Clone + Send + Sync + 'static>(item: &core_types::recor
Some(list)
}
/// The deep clone-out for `Graphic` elements: a plain clone of a group
/// interior would carry frame pointers into the evaluation's arena, so memo
/// and capture seams copy out the legacy-converted form, which owns all of
/// its content. The generic re-park replays it as an ordinary `Graphic`.
///
/// # Safety
/// `ptr` must point at a live parked `Graphic` element field.
unsafe fn deep_clone_graphic(ptr: *const u8) -> Box<dyn std::any::Any + Send + Sync> {
let graphic = unsafe { core_types::record::borrow_element::<Graphic>(core_types::record::Rec::new(ptr)) };
Box::new(map_groups_to_legacy(graphic))
}
const _: () = {
#[cfg(not(target_family = "wasm"))]
#[core_types::ctor::ctor]
fn register() {
core_types::record::register_deep_element_clone::<Graphic>(deep_clone_graphic);
}
#[cfg(target_family = "wasm")]
#[unsafe(export_name = "__node_registry_deep_element_graphic")]
extern "C" fn register() {
core_types::record::register_deep_element_clone::<Graphic>(deep_clone_graphic);
}
};
/// The graphic with every `Group` converted to its legacy list form.
pub fn map_groups_to_legacy(graphic: &Graphic) -> Graphic {
match graphic {

View File

@@ -1,4 +1,5 @@
pub mod artboard;
pub mod boundary;
pub mod graphic;
pub mod markers;