mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 23:08:05 +08:00
Add the leveled boundary helpers and the deep group element copy
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>)
|
||||
}
|
||||
|
||||
|
||||
85
node-graph/libraries/graphic-types/src/boundary.rs
Normal file
85
node-graph/libraries/graphic-types/src/boundary.rs
Normal 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))
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod artboard;
|
||||
pub mod boundary;
|
||||
pub mod graphic;
|
||||
pub mod markers;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user