mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 06:38:03 +08:00
Merge the wiring surface into the registry and express the memo nodes as macro kernels
This commit is contained in:
@@ -631,14 +631,14 @@ pub type GraphErrors = Vec<GraphError>;
|
||||
/// The `TypingContext` is used to store the types of the nodes indexed by their stable node id.
|
||||
#[derive(Default, Clone, dyn_any::DynAny)]
|
||||
pub struct TypingContext {
|
||||
lookup: Cow<'static, HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>>,
|
||||
lookup: Cow<'static, HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, DynNodeConstructor>>>,
|
||||
inferred: HashMap<NodeId, NodeIOTypes>,
|
||||
constructor: HashMap<NodeId, NodeConstructor>,
|
||||
constructor: HashMap<NodeId, DynNodeConstructor>,
|
||||
}
|
||||
|
||||
impl TypingContext {
|
||||
/// Creates a new `TypingContext` with the given lookup table.
|
||||
pub fn new(lookup: &'static HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>>) -> Self {
|
||||
pub fn new(lookup: &'static HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, DynNodeConstructor>>) -> Self {
|
||||
Self {
|
||||
lookup: Cow::Borrowed(lookup),
|
||||
..Default::default()
|
||||
@@ -662,7 +662,7 @@ impl TypingContext {
|
||||
}
|
||||
|
||||
/// Returns the node constructor for a given node id.
|
||||
pub fn constructor(&self, node_id: NodeId) -> Option<NodeConstructor> {
|
||||
pub fn constructor(&self, node_id: NodeId) -> Option<DynNodeConstructor> {
|
||||
self.constructor.get(&node_id).copied()
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,21 @@ pub struct ArenaCell<T> {
|
||||
_marker: PhantomData<fn() -> T>,
|
||||
}
|
||||
|
||||
impl<T> Clone for ArenaCell<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
word: AtomicU64::new(self.word.load(Ordering::Acquire)),
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Debug for ArenaCell<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ArenaCell").field("word", &self.word.load(Ordering::Relaxed)).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for ArenaCell<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
||||
@@ -19,7 +19,6 @@ pub mod runtime;
|
||||
pub mod transform;
|
||||
pub mod uuid;
|
||||
pub mod value;
|
||||
pub mod wire;
|
||||
|
||||
pub use crate as core_types;
|
||||
pub use blending::*;
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use crate::concrete;
|
||||
use crate::context::ContextImpl;
|
||||
use crate::gnode::GNode;
|
||||
use crate::{ContextFeature, Node, NodeIO, NodeIOTypes, ProtoNodeIdentifier, Type, WasmNotSend};
|
||||
use dyn_any::{DynAny, StaticType};
|
||||
use graphene_hash::CacheHash;
|
||||
pub use no_std_types::registry::types;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hasher;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
@@ -57,12 +62,105 @@ pub enum RegistryValueSource {
|
||||
Scope(&'static str),
|
||||
}
|
||||
|
||||
type NodeRegistry = LazyLock<Mutex<HashMap<ProtoNodeIdentifier, Vec<(NodeConstructor, NodeIOTypes)>>>>;
|
||||
type NodeRegistry = LazyLock<Mutex<HashMap<ProtoNodeIdentifier, Vec<(DynNodeConstructor, NodeIOTypes)>>>>;
|
||||
|
||||
pub static NODE_REGISTRY: NodeRegistry = LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
pub static NODE_METADATA: LazyLock<Mutex<HashMap<ProtoNodeIdentifier, NodeMetadata>>> = LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
pub type ErasedGNode<T> = dyn for<'c> GNode<ContextImpl<'c>, Output = T>;
|
||||
pub type ErasedLendGNode<T> = dyn for<'c> GNode<ContextImpl<'c>, Output = &'c T>;
|
||||
|
||||
pub fn cache_key<C: CacheHash + ?Sized>(ctx: &C) -> u64 {
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
ctx.cache_hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ConstructionError {
|
||||
Arity { expected: usize, got: usize },
|
||||
Type { expected: Type, found: Type },
|
||||
}
|
||||
|
||||
pub struct EdgeHandle {
|
||||
node: Box<dyn std::any::Any>,
|
||||
ty: Type,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EdgeHandle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("EdgeHandle").field("ty", &self.ty).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl EdgeHandle {
|
||||
pub fn new<T: 'static>(node: Box<ErasedGNode<T>>) -> Self {
|
||||
Self::new_erased(node, concrete!(T))
|
||||
}
|
||||
|
||||
pub fn new_ref<T: 'static>(node: Box<ErasedLendGNode<T>>) -> Self {
|
||||
Self::new_erased(node, Type::Ref(Box::new(concrete!(T))))
|
||||
}
|
||||
|
||||
pub fn new_erased<N: ?Sized>(node: Box<N>, ty: Type) -> Self
|
||||
where
|
||||
Box<N>: std::any::Any,
|
||||
{
|
||||
Self { node: Box::new(node), ty }
|
||||
}
|
||||
|
||||
pub fn ty(&self) -> &Type {
|
||||
&self.ty
|
||||
}
|
||||
|
||||
pub fn downcast<T: 'static>(self) -> Result<Box<ErasedGNode<T>>, ConstructionError> {
|
||||
self.downcast_erased(concrete!(T))
|
||||
}
|
||||
|
||||
pub fn downcast_lend<T: 'static>(self) -> Result<Box<ErasedLendGNode<T>>, ConstructionError> {
|
||||
self.downcast_erased(Type::Ref(Box::new(concrete!(T))))
|
||||
}
|
||||
|
||||
pub fn downcast_erased<N: ?Sized>(self, expected: Type) -> Result<Box<N>, ConstructionError>
|
||||
where
|
||||
Box<N>: std::any::Any,
|
||||
{
|
||||
let found = self.ty;
|
||||
self.node.downcast::<Box<N>>().map(|node| *node).map_err(|_| ConstructionError::Type { expected, found })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NodeIoRecord {
|
||||
pub inputs: Vec<Type>,
|
||||
pub output: Type,
|
||||
}
|
||||
|
||||
pub type NodeConstructor = fn(Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError>;
|
||||
|
||||
pub struct RegistryEntry {
|
||||
pub io: NodeIoRecord,
|
||||
pub constructor: NodeConstructor,
|
||||
}
|
||||
|
||||
pub fn construct(entry: &RegistryEntry, inputs: Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError> {
|
||||
if inputs.len() != entry.io.inputs.len() {
|
||||
return Err(ConstructionError::Arity {
|
||||
expected: entry.io.inputs.len(),
|
||||
got: inputs.len(),
|
||||
});
|
||||
}
|
||||
for (handle, expected) in inputs.iter().zip(&entry.io.inputs) {
|
||||
if handle.ty() != expected {
|
||||
return Err(ConstructionError::Type {
|
||||
expected: expected.clone(),
|
||||
found: handle.ty().clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
(entry.constructor)(inputs)
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub type DynFuture<'n, T> = Pin<Box<dyn Future<Output = T> + 'n + Send>>;
|
||||
#[cfg(target_family = "wasm")]
|
||||
@@ -85,7 +183,7 @@ pub type TypeErasedPinned<'n> = Pin<Box<TypeErasedNode<'n>>>;
|
||||
|
||||
pub type SharedNodeContainer = std::sync::Arc<NodeContainer>;
|
||||
|
||||
pub type NodeConstructor = fn(Vec<SharedNodeContainer>) -> DynFuture<'static, TypeErasedBox<'static>>;
|
||||
pub type DynNodeConstructor = fn(Vec<SharedNodeContainer>) -> DynFuture<'static, TypeErasedBox<'static>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NodeContainer {
|
||||
@@ -291,3 +389,222 @@ impl<I: WasmNotSend, O: WasmNotSend> Default for PanicNode<I, O> {
|
||||
|
||||
// TODO: Evaluate safety
|
||||
unsafe impl<I: WasmNotSend, O: WasmNotSend> Sync for PanicNode<I, O> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::SourceId;
|
||||
use crate::arena::Arena;
|
||||
use crate::context::{Ctx, EvalScope, ExtractArena};
|
||||
use crate::gpoll::GPoll;
|
||||
|
||||
struct ValueNode<T>(T);
|
||||
|
||||
impl<T: Clone, Input> GNode<Input> for ValueNode<T> {
|
||||
type Output = T;
|
||||
|
||||
fn eval(&self, _input: &Input) -> GPoll<T> {
|
||||
GPoll::Final(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct LendNode(String);
|
||||
|
||||
impl<'e, Input: Ctx + ExtractArena<ArenaRef = &'e Arena>> GNode<Input> for LendNode {
|
||||
type Output = &'e String;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<&'e String> {
|
||||
match input.arena().alloc(self.0.clone()) {
|
||||
Some((parked, _)) => GPoll::Final(parked),
|
||||
None => GPoll::arena_exhausted(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
||||
EvalScope::new(Some(0.5), None, None, generations, arena)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn borrow_carrying_value_types_wire_through_the_general_constructor() {
|
||||
struct SplitBorrow<'c>(&'c str, usize);
|
||||
|
||||
struct SplitNode<Node0> {
|
||||
content: Node0,
|
||||
}
|
||||
|
||||
impl<'e, Input, Node0> GNode<Input> for SplitNode<Node0>
|
||||
where
|
||||
Input: Ctx,
|
||||
Node0: GNode<Input, Output = &'e String>,
|
||||
{
|
||||
type Output = SplitBorrow<'e>;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<SplitBorrow<'e>> {
|
||||
self.content.eval(input).map(|value| SplitBorrow(value, value.len()))
|
||||
}
|
||||
}
|
||||
|
||||
type ErasedSplitEdge = dyn for<'c> GNode<ContextImpl<'c>, Output = SplitBorrow<'c>>;
|
||||
|
||||
let arena = Arena::new(4096);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let lending = EdgeHandle::new_ref(Box::new(LendNode("held".to_string())) as Box<ErasedLendGNode<String>>);
|
||||
let upstream = lending.downcast_lend::<String>().unwrap();
|
||||
let node: Box<ErasedSplitEdge> = Box::new(SplitNode { content: upstream });
|
||||
let handle = EdgeHandle::new_erased(node, concrete!(SplitBorrow<'static>));
|
||||
assert_eq!(*handle.ty(), concrete!(SplitBorrow<'static>));
|
||||
|
||||
let wired = handle.downcast_erased::<ErasedSplitEdge>(concrete!(SplitBorrow<'static>)).unwrap();
|
||||
let GPoll::Final(split) = wired.eval(&ctx) else {
|
||||
panic!("borrow-carrying output must eval through the erased edge");
|
||||
};
|
||||
assert_eq!(split.0, "held");
|
||||
assert_eq!(split.1, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_ctx_repeat_pushes_index_levels_through_the_erased_edge() {
|
||||
use crate::context::{Derived, DeriveCtx, ExtractIndex};
|
||||
|
||||
struct RepeatNode<Node0> {
|
||||
content: Node0,
|
||||
}
|
||||
|
||||
impl<C, T, Node0> GNode<C> for RepeatNode<Node0>
|
||||
where
|
||||
C: Ctx + DeriveCtx,
|
||||
Node0: for<'x> GNode<Derived<'x, C>, Output = T>,
|
||||
{
|
||||
type Output = Vec<T>;
|
||||
|
||||
fn eval(&self, input: &C) -> GPoll<Vec<T>> {
|
||||
let spilled = input.index_head();
|
||||
let mut result = Vec::new();
|
||||
for index in 0..3 {
|
||||
let derived = input.promoted(&spilled, index);
|
||||
match self.content.eval(&derived) {
|
||||
GPoll::Final(value) => result.push(value),
|
||||
other => return other.map(|_| Vec::new()),
|
||||
}
|
||||
}
|
||||
GPoll::Final(result)
|
||||
}
|
||||
}
|
||||
|
||||
struct LevelsNode;
|
||||
|
||||
impl<Input: ExtractIndex> GNode<Input> for LevelsNode {
|
||||
type Output = Vec<usize>;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<Vec<usize>> {
|
||||
GPoll::Final(input.try_index().map(|levels| levels.collect()).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
let arena = Arena::new(1024);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let nested = RepeatNode {
|
||||
content: RepeatNode { content: LevelsNode },
|
||||
};
|
||||
let erased: Box<ErasedGNode<Vec<Vec<Vec<usize>>>>> = Box::new(nested);
|
||||
|
||||
let GPoll::Final(outer) = erased.eval(&ctx) else {
|
||||
panic!("nested repeat must evaluate");
|
||||
};
|
||||
assert_eq!(outer.len(), 3);
|
||||
assert_eq!(outer[2][1], vec![1, 2, 0]);
|
||||
assert_eq!(outer[0][0], vec![0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_ctx_footprint_replace_reaches_the_content() {
|
||||
use crate::context::{Derived, DeriveCtx, ExtractFootprint};
|
||||
use crate::transform::Footprint;
|
||||
|
||||
struct ShiftFootprintNode<Node0> {
|
||||
content: Node0,
|
||||
}
|
||||
|
||||
impl<C, T, Node0> GNode<C> for ShiftFootprintNode<Node0>
|
||||
where
|
||||
C: Ctx + DeriveCtx + ExtractFootprint,
|
||||
Node0: for<'x> GNode<Derived<'x, C>, Output = T>,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
fn eval(&self, input: &C) -> GPoll<T> {
|
||||
let mut footprint = input.try_footprint().copied().unwrap_or(Footprint::DEFAULT);
|
||||
footprint.resolution.x += 7;
|
||||
let derived = input.with_footprint(&footprint);
|
||||
self.content.eval(&derived)
|
||||
}
|
||||
}
|
||||
|
||||
struct ResolutionNode;
|
||||
|
||||
impl<Input: ExtractFootprint> GNode<Input> for ResolutionNode {
|
||||
type Output = u32;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<u32> {
|
||||
GPoll::Final(input.try_footprint().map(|footprint| footprint.resolution.x).unwrap_or(0))
|
||||
}
|
||||
}
|
||||
|
||||
let arena = Arena::new(1024);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let graph: Box<ErasedGNode<u32>> = Box::new(ShiftFootprintNode {
|
||||
content: ShiftFootprintNode { content: ResolutionNode },
|
||||
});
|
||||
assert_eq!(graph.eval(&ctx), GPoll::Final(Footprint::DEFAULT.resolution.x + 14));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construct_checks_arity_and_types() {
|
||||
fn construct_strlen(args: Vec<EdgeHandle>) -> Result<EdgeHandle, ConstructionError> {
|
||||
let mut args = args.into_iter();
|
||||
let value = args.next().ok_or(ConstructionError::Arity { expected: 1, got: 0 })?.downcast::<String>()?;
|
||||
drop(value);
|
||||
Ok(EdgeHandle::new(Box::new(ValueNode(0u32)) as Box<ErasedGNode<u32>>))
|
||||
}
|
||||
let entry = RegistryEntry {
|
||||
io: NodeIoRecord {
|
||||
inputs: vec![concrete!(String)],
|
||||
output: concrete!(u32),
|
||||
},
|
||||
constructor: construct_strlen,
|
||||
};
|
||||
|
||||
let owned = EdgeHandle::new(Box::new(ValueNode("typed".to_string())) as Box<ErasedGNode<String>>);
|
||||
assert!(construct(&entry, vec![owned]).is_ok());
|
||||
|
||||
assert_eq!(construct(&entry, vec![]).unwrap_err(), ConstructionError::Arity { expected: 1, got: 0 });
|
||||
|
||||
let mistyped = EdgeHandle::new(Box::new(ValueNode(1.0f64)) as Box<ErasedGNode<f64>>);
|
||||
assert_eq!(
|
||||
construct(&entry, vec![mistyped]).unwrap_err(),
|
||||
ConstructionError::Type {
|
||||
expected: concrete!(String),
|
||||
found: concrete!(f64),
|
||||
}
|
||||
);
|
||||
|
||||
let lent = EdgeHandle::new_ref(Box::new(LendNode("typed".to_string())) as Box<ErasedLendGNode<String>>);
|
||||
assert_eq!(
|
||||
construct(&entry, vec![lent]).unwrap_err(),
|
||||
ConstructionError::Type {
|
||||
expected: concrete!(String),
|
||||
found: Type::Ref(Box::new(concrete!(String))),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,560 +0,0 @@
|
||||
use crate::Type;
|
||||
use crate::arena::{Arena, ArenaCell};
|
||||
use crate::concrete;
|
||||
use crate::context::{ContextImpl, Ctx, ExtractArena};
|
||||
use crate::frame_table::{FrameTable, Lookup};
|
||||
use crate::gnode::GNode;
|
||||
use crate::gpoll::{Extent, Finality, GPoll};
|
||||
use graphene_hash::CacheHash;
|
||||
use std::any::Any;
|
||||
use std::hash::Hasher;
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub type ErasedGNode<T> = dyn for<'c> GNode<ContextImpl<'c>, Output = T>;
|
||||
pub type ErasedLendGNode<T> = dyn for<'c> GNode<ContextImpl<'c>, Output = &'c T>;
|
||||
|
||||
pub fn cache_key<C: CacheHash + ?Sized>(ctx: &C) -> u64 {
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
ctx.cache_hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum WireError {
|
||||
Arity { expected: usize, got: usize },
|
||||
Type { expected: Type, found: Type },
|
||||
MissingCapability { ty: Type },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct WireCapabilities {
|
||||
pub memoize: Option<fn(EdgeHandle) -> Result<EdgeHandle, WireError>>,
|
||||
pub lend: Option<fn(EdgeHandle) -> Result<EdgeHandle, WireError>>,
|
||||
}
|
||||
|
||||
fn memoize_edge<T: Clone + 'static>(edge: EdgeHandle) -> Result<EdgeHandle, WireError> {
|
||||
let content = edge.downcast::<T>()?;
|
||||
Ok(EdgeHandle::new(Box::new(MemoizeNode::new(content)) as Box<ErasedGNode<T>>))
|
||||
}
|
||||
|
||||
fn lend_edge<T: Clone + 'static>(edge: EdgeHandle) -> Result<EdgeHandle, WireError> {
|
||||
let content = edge.downcast::<T>()?;
|
||||
Ok(EdgeHandle::new_ref(Box::new(FrameMemoNode::new(content)) as Box<ErasedLendGNode<T>>))
|
||||
}
|
||||
|
||||
pub struct EdgeHandle {
|
||||
node: Box<dyn Any>,
|
||||
ty: Type,
|
||||
capabilities: WireCapabilities,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EdgeHandle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("EdgeHandle").field("ty", &self.ty).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl EdgeHandle {
|
||||
pub fn new<T: Clone + 'static>(node: Box<ErasedGNode<T>>) -> Self {
|
||||
Self::new_erased(
|
||||
node,
|
||||
concrete!(T),
|
||||
WireCapabilities {
|
||||
memoize: Some(memoize_edge::<T>),
|
||||
lend: Some(lend_edge::<T>),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_ref<T: 'static>(node: Box<ErasedLendGNode<T>>) -> Self {
|
||||
Self::new_erased(node, Type::Ref(Box::new(concrete!(T))), WireCapabilities::default())
|
||||
}
|
||||
|
||||
pub fn new_erased<N: ?Sized>(node: Box<N>, ty: Type, capabilities: WireCapabilities) -> Self
|
||||
where
|
||||
Box<N>: Any,
|
||||
{
|
||||
Self {
|
||||
node: Box::new(node),
|
||||
ty,
|
||||
capabilities,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wire_type(&self) -> &Type {
|
||||
&self.ty
|
||||
}
|
||||
|
||||
pub fn memoized(self) -> Result<EdgeHandle, WireError> {
|
||||
match self.capabilities.memoize {
|
||||
Some(wrap) => wrap(self),
|
||||
None => Err(WireError::MissingCapability { ty: self.ty }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lent(self) -> Result<EdgeHandle, WireError> {
|
||||
match self.capabilities.lend {
|
||||
Some(wrap) => wrap(self),
|
||||
None => Err(WireError::MissingCapability { ty: self.ty }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn downcast<T: 'static>(self) -> Result<Box<ErasedGNode<T>>, WireError> {
|
||||
self.downcast_erased(concrete!(T))
|
||||
}
|
||||
|
||||
pub fn downcast_lend<T: 'static>(self) -> Result<Box<ErasedLendGNode<T>>, WireError> {
|
||||
self.downcast_erased(Type::Ref(Box::new(concrete!(T))))
|
||||
}
|
||||
|
||||
pub fn downcast_erased<N: ?Sized>(self, expected: Type) -> Result<Box<N>, WireError>
|
||||
where
|
||||
Box<N>: Any,
|
||||
{
|
||||
let found = self.ty;
|
||||
self.node.downcast::<Box<N>>().map(|node| *node).map_err(|_| WireError::Type { expected, found })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NodeIoRecord {
|
||||
pub inputs: Vec<Type>,
|
||||
pub output: Type,
|
||||
}
|
||||
|
||||
pub struct RegistryEntry {
|
||||
pub io: NodeIoRecord,
|
||||
pub wire: fn(Vec<EdgeHandle>) -> Result<EdgeHandle, WireError>,
|
||||
}
|
||||
|
||||
pub fn resolve_and_wire(entry: &RegistryEntry, inputs: Vec<EdgeHandle>) -> Result<EdgeHandle, WireError> {
|
||||
if inputs.len() != entry.io.inputs.len() {
|
||||
return Err(WireError::Arity {
|
||||
expected: entry.io.inputs.len(),
|
||||
got: inputs.len(),
|
||||
});
|
||||
}
|
||||
for (handle, expected) in inputs.iter().zip(&entry.io.inputs) {
|
||||
if handle.wire_type() != expected {
|
||||
return Err(WireError::Type {
|
||||
expected: expected.clone(),
|
||||
found: handle.wire_type().clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
(entry.wire)(inputs)
|
||||
}
|
||||
|
||||
pub struct MemoizeNode<T, NodeContent> {
|
||||
cache: Mutex<Option<(u64, T, Finality)>>,
|
||||
content: NodeContent,
|
||||
}
|
||||
|
||||
impl<T, NodeContent> MemoizeNode<T, NodeContent> {
|
||||
pub fn new(content: NodeContent) -> Self {
|
||||
Self {
|
||||
cache: Mutex::new(None),
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Input, NodeContent> GNode<Input> for MemoizeNode<T, NodeContent>
|
||||
where
|
||||
T: Clone,
|
||||
Input: Ctx + CacheHash,
|
||||
NodeContent: GNode<Input, Output = T>,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<T> {
|
||||
let key = cache_key(input);
|
||||
if let Some((hash, value, finality)) = self.cache.lock().unwrap().as_ref() {
|
||||
if *hash == key {
|
||||
return match finality {
|
||||
Finality::AllFinal => GPoll::Final(value.clone()),
|
||||
Finality::Partial => GPoll::Partial(value.clone()),
|
||||
};
|
||||
}
|
||||
}
|
||||
let result = self.content.eval(input);
|
||||
match &result {
|
||||
GPoll::Final(value) => *self.cache.lock().unwrap() = Some((key, value.clone(), Finality::AllFinal)),
|
||||
GPoll::Partial(value) => *self.cache.lock().unwrap() = Some((key, value.clone(), Finality::Partial)),
|
||||
GPoll::Pending | GPoll::Fallback(_) | GPoll::Error(_) => {}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn extent(&self, input: &Input) -> GPoll<Extent> {
|
||||
self.content.extent(input)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FrameMemoNode<T, NodeContent> {
|
||||
cell: ArenaCell<FrameTable<T, 32>>,
|
||||
content: NodeContent,
|
||||
}
|
||||
|
||||
impl<T, NodeContent> FrameMemoNode<T, NodeContent> {
|
||||
pub fn new(content: NodeContent) -> Self {
|
||||
Self {
|
||||
cell: ArenaCell::new(),
|
||||
content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'e, T, Input, NodeContent> GNode<Input> for FrameMemoNode<T, NodeContent>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
Input: Ctx + CacheHash + ExtractArena<ArenaRef = &'e Arena>,
|
||||
NodeContent: GNode<Input, Output = T>,
|
||||
{
|
||||
type Output = &'e T;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<&'e T> {
|
||||
let arena = input.arena();
|
||||
let table = match self.cell.load(arena) {
|
||||
Some(table) => table,
|
||||
None => match arena.alloc(FrameTable::new()) {
|
||||
Some((table, weak)) => {
|
||||
self.cell.store(weak);
|
||||
table
|
||||
}
|
||||
None => return park(arena, self.content.eval(input)),
|
||||
},
|
||||
};
|
||||
match table.lookup(cache_key(input)) {
|
||||
Lookup::Hit(Finality::AllFinal, value) => GPoll::Final(value),
|
||||
Lookup::Hit(Finality::Partial, value) => GPoll::Partial(value),
|
||||
Lookup::Vacant(slot) => match self.content.eval(input) {
|
||||
GPoll::Final(value) => GPoll::Final(slot.publish(value, Finality::AllFinal)),
|
||||
GPoll::Partial(value) => GPoll::Partial(slot.publish(value, Finality::Partial)),
|
||||
unpublishable => {
|
||||
slot.release();
|
||||
park(arena, unpublishable)
|
||||
}
|
||||
},
|
||||
Lookup::Full => park(arena, self.content.eval(input)),
|
||||
}
|
||||
}
|
||||
|
||||
fn extent(&self, input: &Input) -> GPoll<Extent> {
|
||||
self.content.extent(input)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn park<'e, T>(arena: &'e Arena, result: GPoll<T>) -> GPoll<&'e T> {
|
||||
match result {
|
||||
GPoll::Final(value) => match arena.alloc(value) {
|
||||
Some((parked, _)) => GPoll::Final(parked),
|
||||
None => GPoll::arena_exhausted(),
|
||||
},
|
||||
GPoll::Partial(value) => match arena.alloc(value) {
|
||||
Some((parked, _)) => GPoll::Partial(parked),
|
||||
None => GPoll::arena_exhausted(),
|
||||
},
|
||||
GPoll::Fallback(boxed) => {
|
||||
let (value, error) = *boxed;
|
||||
match arena.alloc(value) {
|
||||
Some((parked, _)) => GPoll::Fallback(Box::new((parked, error))),
|
||||
None => GPoll::arena_exhausted(),
|
||||
}
|
||||
}
|
||||
GPoll::Pending => GPoll::Pending,
|
||||
GPoll::Error(error) => GPoll::Error(error),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::context::EvalScope;
|
||||
use crate::SourceId;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
struct CountingNode(AtomicU32);
|
||||
|
||||
impl<Input> GNode<Input> for CountingNode {
|
||||
type Output = u32;
|
||||
|
||||
fn eval(&self, _input: &Input) -> GPoll<u32> {
|
||||
GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
struct ValueNode<T>(T);
|
||||
|
||||
impl<T: Clone, Input> GNode<Input> for ValueNode<T> {
|
||||
type Output = T;
|
||||
|
||||
fn eval(&self, _input: &Input) -> GPoll<T> {
|
||||
GPoll::Final(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
||||
EvalScope::new(Some(0.5), None, None, generations, arena)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memo_capability_wraps_edges_type_blind() {
|
||||
let arena = Arena::new(1024);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let edge = EdgeHandle::new(Box::new(CountingNode(AtomicU32::new(0))) as Box<ErasedGNode<u32>>);
|
||||
let memoized = edge.memoized().unwrap().downcast::<u32>().unwrap();
|
||||
|
||||
assert_eq!(memoized.eval(&ctx), GPoll::Final(1));
|
||||
assert_eq!(memoized.eval(&ctx), GPoll::Final(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memo_invalidates_on_generation_bump() {
|
||||
let arena = Arena::new(1024);
|
||||
let source: SourceId = 7;
|
||||
let before = [(source, 1)];
|
||||
let after = [(source, 2)];
|
||||
let scope_before = scope_fixture(&before, &arena);
|
||||
let scope_after = scope_fixture(&after, &arena);
|
||||
|
||||
let edge = EdgeHandle::new(Box::new(CountingNode(AtomicU32::new(0))) as Box<ErasedGNode<u32>>);
|
||||
let memoized = edge.memoized().unwrap().downcast::<u32>().unwrap();
|
||||
|
||||
assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1));
|
||||
assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1));
|
||||
assert_eq!(memoized.eval(&ContextImpl::root(&scope_after)), GPoll::Final(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memoized_edges_stack_and_rewire() {
|
||||
let arena = Arena::new(1024);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let edge = EdgeHandle::new(Box::new(CountingNode(AtomicU32::new(0))) as Box<ErasedGNode<u32>>);
|
||||
let stacked = edge.memoized().unwrap().memoized().unwrap().downcast::<u32>().unwrap();
|
||||
|
||||
assert_eq!(stacked.eval(&ctx), GPoll::Final(1));
|
||||
assert_eq!(stacked.eval(&ctx), GPoll::Final(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lend_capability_turns_an_owned_edge_into_a_lending_edge() {
|
||||
let arena = Arena::new(4096);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let edge = EdgeHandle::new(Box::new(ValueNode("lent out".to_string())) as Box<ErasedGNode<String>>);
|
||||
let lending = edge.lent().unwrap();
|
||||
assert_eq!(*lending.wire_type(), Type::Ref(Box::new(concrete!(String))));
|
||||
|
||||
let node = lending.downcast_lend::<String>().unwrap();
|
||||
let GPoll::Final(first) = node.eval(&ctx) else {
|
||||
panic!("lend must fill the frame table and lend");
|
||||
};
|
||||
let GPoll::Final(second) = node.eval(&ctx) else {
|
||||
panic!("second eval must lend the published value");
|
||||
};
|
||||
assert_eq!(first, "lent out");
|
||||
assert!(std::ptr::eq(first, second));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ref_edges_report_missing_capabilities() {
|
||||
let edge = EdgeHandle::new(Box::new(ValueNode(5u32)) as Box<ErasedGNode<u32>>);
|
||||
let lending = edge.lent().unwrap();
|
||||
|
||||
match lending.memoized() {
|
||||
Err(WireError::MissingCapability { ty }) => assert_eq!(ty, Type::Ref(Box::new(concrete!(u32)))),
|
||||
other => panic!("expected missing capability, got {:?}", other.map(|handle| handle.ty)),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn borrow_carrying_value_types_wire_through_the_general_constructor() {
|
||||
struct SplitBorrow<'c>(&'c str, usize);
|
||||
|
||||
struct SplitNode<Node0> {
|
||||
content: Node0,
|
||||
}
|
||||
|
||||
impl<'e, Input, Node0> GNode<Input> for SplitNode<Node0>
|
||||
where
|
||||
Input: Ctx,
|
||||
Node0: GNode<Input, Output = &'e String>,
|
||||
{
|
||||
type Output = SplitBorrow<'e>;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<SplitBorrow<'e>> {
|
||||
self.content.eval(input).map(|value| SplitBorrow(value, value.len()))
|
||||
}
|
||||
}
|
||||
|
||||
type ErasedSplitEdge = dyn for<'c> GNode<ContextImpl<'c>, Output = SplitBorrow<'c>>;
|
||||
|
||||
let arena = Arena::new(4096);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let lending = EdgeHandle::new(Box::new(ValueNode("held".to_string())) as Box<ErasedGNode<String>>).lent().unwrap();
|
||||
let upstream = lending.downcast_lend::<String>().unwrap();
|
||||
let node: Box<ErasedSplitEdge> = Box::new(SplitNode { content: upstream });
|
||||
let handle = EdgeHandle::new_erased(node, concrete!(SplitBorrow<'static>), WireCapabilities::default());
|
||||
assert_eq!(*handle.wire_type(), concrete!(SplitBorrow<'static>));
|
||||
|
||||
let wired = handle.downcast_erased::<ErasedSplitEdge>(concrete!(SplitBorrow<'static>)).unwrap();
|
||||
let GPoll::Final(split) = wired.eval(&ctx) else {
|
||||
panic!("borrow-carrying output must eval through the erased edge");
|
||||
};
|
||||
assert_eq!(split.0, "held");
|
||||
assert_eq!(split.1, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_ctx_repeat_pushes_index_levels_through_the_erased_edge() {
|
||||
use crate::context::{Derived, DeriveCtx, ExtractIndex};
|
||||
|
||||
struct RepeatNode<Node0> {
|
||||
content: Node0,
|
||||
}
|
||||
|
||||
impl<C, T, Node0> GNode<C> for RepeatNode<Node0>
|
||||
where
|
||||
C: Ctx + DeriveCtx,
|
||||
Node0: for<'x> GNode<Derived<'x, C>, Output = T>,
|
||||
{
|
||||
type Output = Vec<T>;
|
||||
|
||||
fn eval(&self, input: &C) -> GPoll<Vec<T>> {
|
||||
let spilled = input.index_head();
|
||||
let mut result = Vec::new();
|
||||
for index in 0..3 {
|
||||
let derived = input.promoted(&spilled, index);
|
||||
match self.content.eval(&derived) {
|
||||
GPoll::Final(value) => result.push(value),
|
||||
other => return other.map(|_| Vec::new()),
|
||||
}
|
||||
}
|
||||
GPoll::Final(result)
|
||||
}
|
||||
}
|
||||
|
||||
struct LevelsNode;
|
||||
|
||||
impl<Input: ExtractIndex> GNode<Input> for LevelsNode {
|
||||
type Output = Vec<usize>;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<Vec<usize>> {
|
||||
GPoll::Final(input.try_index().map(|levels| levels.collect()).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
let arena = Arena::new(1024);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let nested = RepeatNode {
|
||||
content: RepeatNode { content: LevelsNode },
|
||||
};
|
||||
let erased: Box<ErasedGNode<Vec<Vec<Vec<usize>>>>> = Box::new(nested);
|
||||
|
||||
let GPoll::Final(outer) = erased.eval(&ctx) else {
|
||||
panic!("nested repeat must evaluate");
|
||||
};
|
||||
assert_eq!(outer.len(), 3);
|
||||
assert_eq!(outer[2][1], vec![1, 2, 0]);
|
||||
assert_eq!(outer[0][0], vec![0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_ctx_footprint_replace_reaches_the_content() {
|
||||
use crate::context::{Derived, DeriveCtx, ExtractFootprint};
|
||||
use crate::transform::Footprint;
|
||||
|
||||
struct ShiftFootprintNode<Node0> {
|
||||
content: Node0,
|
||||
}
|
||||
|
||||
impl<C, T, Node0> GNode<C> for ShiftFootprintNode<Node0>
|
||||
where
|
||||
C: Ctx + DeriveCtx + ExtractFootprint,
|
||||
Node0: for<'x> GNode<Derived<'x, C>, Output = T>,
|
||||
{
|
||||
type Output = T;
|
||||
|
||||
fn eval(&self, input: &C) -> GPoll<T> {
|
||||
let mut footprint = input.try_footprint().copied().unwrap_or(Footprint::DEFAULT);
|
||||
footprint.resolution.x += 7;
|
||||
let derived = input.with_footprint(&footprint);
|
||||
self.content.eval(&derived)
|
||||
}
|
||||
}
|
||||
|
||||
struct ResolutionNode;
|
||||
|
||||
impl<Input: ExtractFootprint> GNode<Input> for ResolutionNode {
|
||||
type Output = u32;
|
||||
|
||||
fn eval(&self, input: &Input) -> GPoll<u32> {
|
||||
GPoll::Final(input.try_footprint().map(|footprint| footprint.resolution.x).unwrap_or(0))
|
||||
}
|
||||
}
|
||||
|
||||
let arena = Arena::new(1024);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let graph: Box<ErasedGNode<u32>> = Box::new(ShiftFootprintNode {
|
||||
content: ShiftFootprintNode { content: ResolutionNode },
|
||||
});
|
||||
assert_eq!(graph.eval(&ctx), GPoll::Final(Footprint::DEFAULT.resolution.x + 14));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_and_wire_checks_arity_and_types() {
|
||||
fn wire_strlen(args: Vec<EdgeHandle>) -> Result<EdgeHandle, WireError> {
|
||||
let mut args = args.into_iter();
|
||||
let value = args.next().ok_or(WireError::Arity { expected: 1, got: 0 })?.downcast::<String>()?;
|
||||
drop(value);
|
||||
Ok(EdgeHandle::new(Box::new(ValueNode(0u32)) as Box<ErasedGNode<u32>>))
|
||||
}
|
||||
let entry = RegistryEntry {
|
||||
io: NodeIoRecord {
|
||||
inputs: vec![concrete!(String)],
|
||||
output: concrete!(u32),
|
||||
},
|
||||
wire: wire_strlen,
|
||||
};
|
||||
|
||||
let owned = EdgeHandle::new(Box::new(ValueNode("typed".to_string())) as Box<ErasedGNode<String>>);
|
||||
assert!(resolve_and_wire(&entry, vec![owned]).is_ok());
|
||||
|
||||
assert_eq!(resolve_and_wire(&entry, vec![]).unwrap_err(), WireError::Arity { expected: 1, got: 0 });
|
||||
|
||||
let mistyped = EdgeHandle::new(Box::new(ValueNode(1.0f64)) as Box<ErasedGNode<f64>>);
|
||||
assert_eq!(
|
||||
resolve_and_wire(&entry, vec![mistyped]).unwrap_err(),
|
||||
WireError::Type {
|
||||
expected: concrete!(String),
|
||||
found: concrete!(f64),
|
||||
}
|
||||
);
|
||||
|
||||
let lent = EdgeHandle::new(Box::new(ValueNode("typed".to_string())) as Box<ErasedGNode<String>>).lent().unwrap();
|
||||
assert_eq!(
|
||||
resolve_and_wire(&entry, vec![lent]).unwrap_err(),
|
||||
WireError::Type {
|
||||
expected: concrete!(String),
|
||||
found: Type::Ref(Box::new(concrete!(String))),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -314,7 +314,7 @@ pub(crate) fn generate_gnode_code(crate_ident: &CrateIdent, parsed: &ParsedNodeF
|
||||
None => quote!(#core_types::gpoll::GPoll::Pending),
|
||||
};
|
||||
let slot_check = quote! {
|
||||
let __key = #core_types::wire::cache_key(__input);
|
||||
let __key = #core_types::registry::cache_key(__input);
|
||||
{
|
||||
let __entries = self.slot.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(__state) = __entries.get(&__key) {
|
||||
@@ -395,9 +395,9 @@ pub(crate) fn generate_gnode_code(crate_ident: &CrateIdent, parsed: &ParsedNodeF
|
||||
}
|
||||
};
|
||||
|
||||
let wire = entries_tokens(parsed, &struct_name, &data_field_generic_idents, ®ular_fields);
|
||||
let entries = entries_tokens(parsed, &struct_name, &data_field_generic_idents, ®ular_fields);
|
||||
let cfg = crate::shader_nodes::modify_cfg(&parsed.attributes);
|
||||
let wire_reexport = match wire.is_empty() {
|
||||
let entries_reexport = match entries.is_empty() {
|
||||
true => quote!(),
|
||||
false => {
|
||||
let entries_name = format_ident!("{}_entries", fn_name);
|
||||
@@ -410,7 +410,7 @@ pub(crate) fn generate_gnode_code(crate_ident: &CrateIdent, parsed: &ParsedNodeF
|
||||
};
|
||||
|
||||
let top_level = quote! {
|
||||
#wire_reexport
|
||||
#entries_reexport
|
||||
|
||||
#cfg
|
||||
#[automatically_derived]
|
||||
@@ -437,7 +437,7 @@ pub(crate) fn generate_gnode_code(crate_ident: &CrateIdent, parsed: &ParsedNodeF
|
||||
};
|
||||
|
||||
Ok(GNodeTokens {
|
||||
in_mod: wire,
|
||||
in_mod: entries,
|
||||
top_level: quote! {
|
||||
#kernel
|
||||
|
||||
@@ -597,31 +597,31 @@ fn entries_tokens(parsed: &ParsedNodeFn, struct_name: &Ident, data_field_generic
|
||||
|
||||
let entries = rows.iter().map(|row| {
|
||||
let types = row.iter();
|
||||
let boxed_types = row.iter().map(|ty| quote!(::std::boxed::Box<gcore::wire::ErasedGNode<#ty>>));
|
||||
let boxed_types = row.iter().map(|ty| quote!(::std::boxed::Box<gcore::registry::ErasedGNode<#ty>>));
|
||||
let output = quote!(<#struct_name<#(#boxed_types),*> as gcore::gnode::GNode<gcore::context::ContextImpl<'static>>>::Output);
|
||||
let downcasts = names.iter().zip(row.iter()).map(|(name, ty)| {
|
||||
quote!(let #name = inputs.next().unwrap().downcast::<#ty>()?;)
|
||||
});
|
||||
quote! {
|
||||
gcore::wire::RegistryEntry {
|
||||
io: gcore::wire::NodeIoRecord {
|
||||
gcore::registry::RegistryEntry {
|
||||
io: gcore::registry::NodeIoRecord {
|
||||
inputs: vec![#(gcore::concrete!(#types)),*],
|
||||
output: gcore::concrete!(#output),
|
||||
},
|
||||
wire: |inputs| {
|
||||
constructor: |inputs| {
|
||||
if inputs.len() != #arity {
|
||||
return Err(gcore::wire::WireError::Arity { expected: #arity, got: inputs.len() });
|
||||
return Err(gcore::registry::ConstructionError::Arity { expected: #arity, got: inputs.len() });
|
||||
}
|
||||
let mut inputs = inputs.into_iter();
|
||||
#(#downcasts)*
|
||||
Ok(gcore::wire::EdgeHandle::new(::std::boxed::Box::new(#struct_name::new(#(#names),*)) as ::std::boxed::Box<gcore::wire::ErasedGNode<#output>>))
|
||||
Ok(gcore::registry::EdgeHandle::new(::std::boxed::Box::new(#struct_name::new(#(#names),*)) as ::std::boxed::Box<gcore::registry::ErasedGNode<#output>>))
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
pub fn #entries_name() -> ::std::vec::Vec<gcore::wire::RegistryEntry> {
|
||||
pub fn #entries_name() -> ::std::vec::Vec<gcore::registry::RegistryEntry> {
|
||||
vec![#(#entries),*]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use core_types::gpoll::GPoll;
|
||||
use core_types::list::{AttributeDyn, AttributeValueDyn, List, ListDyn};
|
||||
use core_types::transform::Footprint;
|
||||
use core_types::uuid::NodeId;
|
||||
use core_types::{Color, OwnedContextImpl};
|
||||
use core_types::Color;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphic_types::vector_types::GradientStops;
|
||||
use graphic_types::{Artboard, Graphic, Vector};
|
||||
@@ -49,72 +49,3 @@ fn context_modification<T>(
|
||||
let scope = ctx.scope().nullified(features_to_keep);
|
||||
value.eval(&ctx.nullified(features_to_keep, &scope))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core_types::graphene_hash::CacheHash;
|
||||
use core_types::transform::Footprint;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hasher;
|
||||
|
||||
/// Verifies that nullified context fields don't affect the cache hash — only the kept features matter.
|
||||
#[test]
|
||||
fn test_nullified_context_hash_stability() {
|
||||
use core_types::Context;
|
||||
use std::sync::Arc;
|
||||
|
||||
let original_ctx: Context = Some(Arc::new(
|
||||
OwnedContextImpl::empty()
|
||||
.with_footprint(Footprint::default())
|
||||
.with_index(1)
|
||||
.with_real_time(10.5)
|
||||
.with_vararg(Box::new("test"))
|
||||
.with_animation_time(20.25),
|
||||
));
|
||||
|
||||
// A second context with different values for the nullified fields
|
||||
let changed_ctx: Context = Some(Arc::new(
|
||||
OwnedContextImpl::empty()
|
||||
.with_footprint(Footprint::default())
|
||||
.with_index(2)
|
||||
.with_real_time(999.9)
|
||||
.with_vararg(Box::new("test"))
|
||||
.with_animation_time(888.8),
|
||||
));
|
||||
|
||||
// Nullify everything — both should hash the same regardless of their field values
|
||||
let features_to_keep = ContextFeatures::empty();
|
||||
let nullified1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), features_to_keep);
|
||||
let nullified2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), features_to_keep);
|
||||
|
||||
let mut hasher1 = DefaultHasher::new();
|
||||
nullified1.cache_hash(&mut hasher1);
|
||||
|
||||
let mut hasher2 = DefaultHasher::new();
|
||||
nullified2.cache_hash(&mut hasher2);
|
||||
|
||||
assert_eq!(
|
||||
hasher1.finish(),
|
||||
hasher2.finish(),
|
||||
"Hash of nullified context should remain stable regardless of input changes when features are nullified"
|
||||
);
|
||||
|
||||
// Keep only footprint and varargs — both have the same footprint and vararg, so hash should still match
|
||||
let partial_features = ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS;
|
||||
let partial1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), partial_features);
|
||||
let partial2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), partial_features);
|
||||
|
||||
let mut hasher3 = DefaultHasher::new();
|
||||
partial1.cache_hash(&mut hasher3);
|
||||
|
||||
let mut hasher4 = DefaultHasher::new();
|
||||
partial2.cache_hash(&mut hasher4);
|
||||
|
||||
assert_eq!(
|
||||
hasher3.finish(),
|
||||
hasher4.finish(),
|
||||
"Hash should be stable when keeping only footprint and varargs and their values are the same"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,105 @@
|
||||
use core_types::gpoll::Interrupt;
|
||||
use core_types::arena::{Arena, ArenaCell};
|
||||
use core_types::context::Ctx;
|
||||
use core_types::frame_table::{FrameTable, Lookup};
|
||||
use core_types::gnode::GNode;
|
||||
use core_types::gpoll::{Extent, Finality, GPoll, Interrupt};
|
||||
use core_types::graphene_hash::CacheHash;
|
||||
use core_types::memo::*;
|
||||
use std::hash::DefaultHasher;
|
||||
use std::hash::Hasher;
|
||||
use core_types::registry::cache_key;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
/// Helps speed up repeated renders in a computationally-heavy part of the node graph.
|
||||
///
|
||||
/// Stores the last evaluated data that flowed through this node and immediately returns that data on subsequent renders if the context has not changed.
|
||||
#[node_macro::node(category("General"), path(graphene_core::memo), skip_impl)]
|
||||
fn memoize<I: CacheHash, T: Clone>(input: I, #[data] cache: Arc<Mutex<Option<(u64, T)>>>, content: impl Node<I, Output = T>) -> Result<T, Interrupt> {
|
||||
// Caches the output of a given node called with a specific input.
|
||||
//
|
||||
// A cache miss occurs when the Option is None. In this case, the node evaluates the inner node and memoizes (stores) the result.
|
||||
//
|
||||
// A cache hit occurs when the Option is Some and has a stored hash matching the hash of the call argument. In this case, the node returns the cached value without re-evaluating the inner node.
|
||||
//
|
||||
// Currently, only one input-output pair is cached. Subsequent calls with different inputs will overwrite the previous cache.
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
input.cache_hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
|
||||
if let Some(data) = cache.lock().as_ref().unwrap().as_ref().and_then(|data| (data.0 == hash).then_some(data.1.clone())) {
|
||||
return Ok(data);
|
||||
#[node_macro::node(category("General"), path(graphene_core::memo), skip_impl, extent(memoize_extent))]
|
||||
fn memoize<I: CacheHash, T: Clone>(input: I, #[data] cache: Arc<Mutex<Option<(u64, T, Finality)>>>, content: impl Node<I, Output = T>) -> GPoll<T> {
|
||||
let key = cache_key(&input);
|
||||
if let Some((hash, value, finality)) = cache.lock().unwrap().as_ref() {
|
||||
if *hash == key {
|
||||
return match finality {
|
||||
Finality::AllFinal => GPoll::Final(value.clone()),
|
||||
Finality::Partial => GPoll::Partial(value.clone()),
|
||||
};
|
||||
}
|
||||
}
|
||||
let result = content.eval(&input);
|
||||
match &result {
|
||||
GPoll::Final(value) => *cache.lock().unwrap() = Some((key, value.clone(), Finality::AllFinal)),
|
||||
GPoll::Partial(value) => *cache.lock().unwrap() = Some((key, value.clone(), Finality::Partial)),
|
||||
GPoll::Pending | GPoll::Fallback(_) | GPoll::Error(_) => {}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
let value = content.eval(input)?;
|
||||
*cache.lock().unwrap() = Some((hash, value.clone()));
|
||||
Ok(value)
|
||||
fn memoize_extent<C, T, NodeContent>(node: &MemoizeNode<T, NodeContent>, ctx: &C) -> GPoll<Extent>
|
||||
where
|
||||
T: Clone,
|
||||
NodeContent: GNode<C, Output = T>,
|
||||
{
|
||||
node.content.extent(ctx)
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""), path(graphene_core::memo), skip_impl, extent(frame_memo_extent))]
|
||||
fn frame_memo<'e, T: Clone + 'static>(
|
||||
ctx: impl Ctx + CacheHash + ExtractArena<'e>,
|
||||
#[data] cell: ArenaCell<FrameTable<T, 32>>,
|
||||
content: impl Node<Context<'_>, Output = T>,
|
||||
) -> GPoll<&'e T> {
|
||||
let arena = ctx.arena();
|
||||
let table = match cell.load(arena) {
|
||||
Some(table) => table,
|
||||
None => match arena.alloc(FrameTable::new()) {
|
||||
Some((table, weak)) => {
|
||||
cell.store(weak);
|
||||
table
|
||||
}
|
||||
None => return park(arena, content.eval(ctx)),
|
||||
},
|
||||
};
|
||||
match table.lookup(cache_key(ctx)) {
|
||||
Lookup::Hit(Finality::AllFinal, value) => GPoll::Final(value),
|
||||
Lookup::Hit(Finality::Partial, value) => GPoll::Partial(value),
|
||||
Lookup::Vacant(slot) => match content.eval(ctx) {
|
||||
GPoll::Final(value) => GPoll::Final(slot.publish(value, Finality::AllFinal)),
|
||||
GPoll::Partial(value) => GPoll::Partial(slot.publish(value, Finality::Partial)),
|
||||
unpublishable => {
|
||||
slot.release();
|
||||
park(arena, unpublishable)
|
||||
}
|
||||
},
|
||||
Lookup::Full => park(arena, content.eval(ctx)),
|
||||
}
|
||||
}
|
||||
|
||||
fn frame_memo_extent<C, T, NodeContent>(node: &FrameMemoNode<T, NodeContent>, ctx: &C) -> GPoll<Extent>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
NodeContent: GNode<C, Output = T>,
|
||||
{
|
||||
node.content.extent(ctx)
|
||||
}
|
||||
|
||||
pub fn park<'e, T>(arena: &'e Arena, result: GPoll<T>) -> GPoll<&'e T> {
|
||||
match result {
|
||||
GPoll::Final(value) => match arena.alloc(value) {
|
||||
Some((parked, _)) => GPoll::Final(parked),
|
||||
None => GPoll::arena_exhausted(),
|
||||
},
|
||||
GPoll::Partial(value) => match arena.alloc(value) {
|
||||
Some((parked, _)) => GPoll::Partial(parked),
|
||||
None => GPoll::arena_exhausted(),
|
||||
},
|
||||
GPoll::Fallback(boxed) => {
|
||||
let (value, error) = *boxed;
|
||||
match arena.alloc(value) {
|
||||
Some((parked, _)) => GPoll::Fallback(Box::new((parked, error))),
|
||||
None => GPoll::arena_exhausted(),
|
||||
}
|
||||
}
|
||||
GPoll::Pending => GPoll::Pending,
|
||||
GPoll::Error(error) => GPoll::Error(error),
|
||||
}
|
||||
}
|
||||
|
||||
type MonitorValue<I, T> = Arc<Mutex<Option<Arc<IORecord<I, T>>>>>;
|
||||
@@ -55,3 +125,127 @@ fn serialize_monitor<I: Clone + 'static + Send + Sync, T: Clone + 'static + Send
|
||||
let io = io.lock().unwrap();
|
||||
io.as_ref().map(|output| output.clone() as Arc<dyn std::any::Any + Send + Sync>)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core_types::SourceId;
|
||||
use core_types::concrete;
|
||||
use core_types::context::{ContextImpl, EvalScope};
|
||||
use core_types::registry::{EdgeHandle, ErasedGNode, ErasedLendGNode};
|
||||
use core_types::Type;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
struct CountingNode(AtomicU32);
|
||||
|
||||
impl<Input> GNode<Input> for CountingNode {
|
||||
type Output = u32;
|
||||
|
||||
fn eval(&self, _input: &Input) -> GPoll<u32> {
|
||||
GPoll::Final(self.0.fetch_add(1, Ordering::Relaxed) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
struct PartialCountingNode(AtomicU32);
|
||||
|
||||
impl<Input> GNode<Input> for PartialCountingNode {
|
||||
type Output = u32;
|
||||
|
||||
fn eval(&self, _input: &Input) -> GPoll<u32> {
|
||||
GPoll::Partial(self.0.fetch_add(1, Ordering::Relaxed) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
struct ValueNode<T>(T);
|
||||
|
||||
impl<T: Clone, Input> GNode<Input> for ValueNode<T> {
|
||||
type Output = T;
|
||||
|
||||
fn eval(&self, _input: &Input) -> GPoll<T> {
|
||||
GPoll::Final(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn scope_fixture<'a>(generations: &'a [(SourceId, u64)], arena: &'a Arena) -> EvalScope<'a> {
|
||||
EvalScope::new(Some(0.5), None, None, generations, arena)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memoize_caches_across_evals() {
|
||||
let arena = Arena::new(1024);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let memoized = MemoizeNode::new(CountingNode(AtomicU32::new(0)));
|
||||
|
||||
assert_eq!(memoized.eval(&ctx), GPoll::Final(1));
|
||||
assert_eq!(memoized.eval(&ctx), GPoll::Final(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memo_invalidates_on_generation_bump() {
|
||||
let arena = Arena::new(1024);
|
||||
let source: SourceId = 7;
|
||||
let before = [(source, 1)];
|
||||
let after = [(source, 2)];
|
||||
let scope_before = scope_fixture(&before, &arena);
|
||||
let scope_after = scope_fixture(&after, &arena);
|
||||
|
||||
let memoized = MemoizeNode::new(CountingNode(AtomicU32::new(0)));
|
||||
|
||||
assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1));
|
||||
assert_eq!(memoized.eval(&ContextImpl::root(&scope_before)), GPoll::Final(1));
|
||||
assert_eq!(memoized.eval(&ContextImpl::root(&scope_after)), GPoll::Final(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memo_replays_partiality_on_hit() {
|
||||
let arena = Arena::new(1024);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let memoized = MemoizeNode::new(PartialCountingNode(AtomicU32::new(0)));
|
||||
|
||||
assert_eq!(memoized.eval(&ctx), GPoll::Partial(1));
|
||||
assert_eq!(memoized.eval(&ctx), GPoll::Partial(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memoized_edges_stack_and_rewire() {
|
||||
let arena = Arena::new(1024);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let edge = EdgeHandle::new(Box::new(CountingNode(AtomicU32::new(0))) as Box<ErasedGNode<u32>>);
|
||||
let memoized = EdgeHandle::new(Box::new(MemoizeNode::new(edge.downcast::<u32>().unwrap())) as Box<ErasedGNode<u32>>);
|
||||
let stacked = MemoizeNode::new(memoized.downcast::<u32>().unwrap());
|
||||
|
||||
assert_eq!(stacked.eval(&ctx), GPoll::Final(1));
|
||||
assert_eq!(stacked.eval(&ctx), GPoll::Final(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_memo_turns_an_owned_edge_into_a_lending_edge() {
|
||||
let arena = Arena::new(4096);
|
||||
let generations = [];
|
||||
let scope = scope_fixture(&generations, &arena);
|
||||
let ctx = ContextImpl::root(&scope);
|
||||
|
||||
let edge = EdgeHandle::new(Box::new(ValueNode("lent out".to_string())) as Box<ErasedGNode<String>>);
|
||||
let lending = EdgeHandle::new_ref(Box::new(FrameMemoNode::new(edge.downcast::<String>().unwrap())) as Box<ErasedLendGNode<String>>);
|
||||
assert_eq!(*lending.ty(), Type::Ref(Box::new(concrete!(String))));
|
||||
|
||||
let node = lending.downcast_lend::<String>().unwrap();
|
||||
let GPoll::Final(first) = node.eval(&ctx) else {
|
||||
panic!("lend must fill the frame table and lend");
|
||||
};
|
||||
let GPoll::Final(second) = node.eval(&ctx) else {
|
||||
panic!("second eval must lend the published value");
|
||||
};
|
||||
assert_eq!(first, "lent out");
|
||||
assert!(std::ptr::eq(first, second));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,6 @@ mod test {
|
||||
|
||||
#[test]
|
||||
pub fn passthrough_node() {
|
||||
assert_eq!(passthrough((), &4), &4);
|
||||
assert_eq!(passthrough(&(), &4), &4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1068,7 +1068,7 @@ mod graphene_test {
|
||||
use core_types::context::{ContextImpl, EvalScope, ExtractIndex};
|
||||
use core_types::gnode::{BatchStatus, GNode};
|
||||
use core_types::gpoll::{Finality, GPoll};
|
||||
use core_types::wire::{EdgeHandle, ErasedGNode, resolve_and_wire};
|
||||
use core_types::registry::{EdgeHandle, ErasedGNode, construct};
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
struct SourceNode<T>(T);
|
||||
@@ -1130,7 +1130,7 @@ mod graphene_test {
|
||||
let entries = logical_or_entries();
|
||||
let value = EdgeHandle::new(Box::new(SourceNode(true)) as Box<ErasedGNode<bool>>);
|
||||
let other_value = EdgeHandle::new(Box::new(SourceNode(false)) as Box<ErasedGNode<bool>>);
|
||||
let wired = resolve_and_wire(&entries[0], vec![value, other_value]).unwrap().downcast::<bool>().unwrap();
|
||||
let wired = construct(&entries[0], vec![value, other_value]).unwrap().downcast::<bool>().unwrap();
|
||||
|
||||
assert_eq!(GNode::eval(&wired, &ctx), GPoll::Final(true));
|
||||
}
|
||||
@@ -1150,7 +1150,7 @@ mod graphene_test {
|
||||
|
||||
let augend = EdgeHandle::new(Box::new(SourceNode(1.5f64)) as Box<ErasedGNode<f64>>);
|
||||
let addend = EdgeHandle::new(Box::new(SourceNode(2.5f64)) as Box<ErasedGNode<f64>>);
|
||||
let wired = resolve_and_wire(&entries[0], vec![augend, addend]).unwrap().downcast::<f64>().unwrap();
|
||||
let wired = construct(&entries[0], vec![augend, addend]).unwrap().downcast::<f64>().unwrap();
|
||||
|
||||
assert_eq!(GNode::eval(&wired, &ctx), GPoll::Final(4.0));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user