Graphene: Fine-grained context caching (#2500)

* RFC: Fine Grained Context Caching

* Fix typos

* Fix label

* Add description of inject traits

* Explicitly support context modification

* Start implementation of context invalidation

* Add inject trait variants
* Route Extract / Inject traits to the proto nodes

* Implement context dependency analysis

* Implement context modification node insertion

* Fix erronous force graph run message

* Fix Extract* Inject* annotations in the nodes

* Require Hash implementation for VarArgs

* Fix nullification node insertion

* Cross of done items unresolved questions section

* Update Cargo.lock

* Fix context features propagation

* Update demo artwork

* Remove BondlessFootprint and FreezeRealTime nodes

* Fix migration

* Add migrations for adding context features to old networks

* Always update real time regardless of animation state

* Cargo fmt

* Fix tests

* Readd sed command to hopefully fix profile result parsing

* Add debug output to profiling pr

* Use new totals instead of summaries for for iai results

* Even more debugging

* Use correct debug metrics (hopefully)

* Add more MemoNode implementations

* Add context features annotation to shader node macro

* Cleanup

* Time -> RealTime

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert
2025-09-05 13:44:26 +02:00
committed by GitHub
parent c081d0a9de
commit acd7ba38cc
39 changed files with 869 additions and 328 deletions

View File

@@ -41,6 +41,8 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
let struct_generics: Vec<Ident> = fields.iter().enumerate().map(|(i, _)| format_ident!("Node{}", i)).collect();
let input_ident = &input.pat_ident;
let context_features = &input.context_features;
let field_idents: Vec<_> = fields.iter().map(|f| &f.pat_ident).collect();
let field_names: Vec<_> = field_idents.iter().map(|pat_ident| &pat_ident.ident).collect();
@@ -242,7 +244,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
#name: #graphene_core::Node<'n, #input_type, Output = #fut_ident > + #graphene_core::WasmNotSync
)
}
(ParsedFieldType::Node { .. }, false) => unreachable!(),
(ParsedFieldType::Node { .. }, false) => unreachable!("Found node which takes an impl Node<> input but is not async"),
});
}
let where_clause = where_clause.clone().unwrap_or(WhereClause {
@@ -329,7 +331,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
mod #mod_name {
use super::*;
use #graphene_core as gcore;
use gcore::{Node, NodeIOTypes, concrete, fn_type, fn_type_fut, future, ProtoNodeIdentifier, WasmNotSync, NodeIO};
use gcore::{Node, NodeIOTypes, concrete, fn_type, fn_type_fut, future, ProtoNodeIdentifier, WasmNotSync, NodeIO, ContextFeature};
use gcore::value::ClonedNode;
use gcore::ops::TypeNode;
use gcore::registry::{NodeMetadata, FieldMetadata, NODE_REGISTRY, NODE_METADATA, DynAnyNode, DowncastBothNode, DynFuture, TypeErasedBox, PanicNode, RegistryValueSource, RegistryWidgetOverride};
@@ -364,6 +366,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
category: #category,
description: #description,
properties: #properties,
context_features: vec![#(ContextFeature::#context_features,)*],
fields: vec![
#(
FieldMetadata {

View File

@@ -7,8 +7,8 @@ use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::{Comma, RArrow};
use syn::{
AttrStyle, Attribute, Error, Expr, ExprTuple, FnArg, GenericParam, Ident, ItemFn, Lit, LitFloat, LitInt, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, TypeParam, Visibility,
WhereClause, parse_quote,
AttrStyle, Attribute, Error, Expr, ExprTuple, FnArg, GenericParam, Ident, ItemFn, Lit, LitFloat, LitInt, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, TraitBound, Type, TypeImplTrait,
TypeParam, TypeParamBound, Visibility, WhereClause, parse_quote,
};
use crate::codegen::generate_node_code;
@@ -149,6 +149,7 @@ pub(crate) struct Input {
pub(crate) pat_ident: PatIdent,
pub(crate) ty: Type,
pub(crate) implementations: Punctuated<Type, Comma>,
pub(crate) context_features: Vec<Ident>,
}
impl Parse for Implementation {
@@ -384,10 +385,12 @@ fn parse_inputs(inputs: &Punctuated<FnArg, Comma>) -> syn::Result<(Input, Vec<Pa
.map(|attr| parse_implementations(attr, &pat_ident.ident))
.transpose()?
.unwrap_or_default();
let context_features = parse_context_feature_idents(ty);
input = Some(Input {
pat_ident,
ty: (**ty).clone(),
implementations,
context_features,
});
} else if let Pat::Ident(pat_ident) = &**pat {
let field = parse_field(pat_ident.clone(), (**ty).clone(), attrs).map_err(|e| Error::new_spanned(pat_ident, format!("Failed to parse argument '{}': {}", pat_ident.ident, e)))?;
@@ -404,6 +407,41 @@ fn parse_inputs(inputs: &Punctuated<FnArg, Comma>) -> syn::Result<(Input, Vec<Pa
Ok((input, fields))
}
/// Parse context feature identifiers from the trait bounds of a context parameter.
fn parse_context_feature_idents(ty: &Type) -> Vec<Ident> {
let mut features = Vec::new();
// Check if this is an impl trait (impl Ctx + ...)
if let Type::ImplTrait(TypeImplTrait { bounds, .. }) = ty {
for bound in bounds {
if let TypeParamBound::Trait(TraitBound { path, .. }) = bound {
// Extract the last segment of the trait path
if let Some(segment) = path.segments.last() {
match segment.ident.to_string().as_str() {
"ExtractFootprint"
| "ExtractRealTime"
| "ExtractAnimationTime"
| "ExtractIndex"
| "ExtractVarArgs"
| "InjectFootprint"
| "InjectRealTime"
| "InjectAnimationTime"
| "InjectIndex"
| "InjectVarArgs" => {
features.push(segment.ident.clone());
}
// Skip Modify* traits as they don't affect usage tracking
// Also ignore other traits like Ctx, ExtractAll, etc.
_ => {}
}
}
}
}
}
features
}
fn parse_implementations(attr: &Attribute, name: &Ident) -> syn::Result<Punctuated<Type, Comma>> {
let content: TokenStream2 = attr.parse_args()?;
let parser = Punctuated::<Type, Comma>::parse_terminated;
@@ -817,6 +855,7 @@ mod tests {
pat_ident: pat_ident("a"),
ty: parse_quote!(f64),
implementations: Punctuated::new(),
context_features: vec![],
},
output_type: parse_quote!(f64),
is_async: false,
@@ -883,6 +922,7 @@ mod tests {
pat_ident: pat_ident("footprint"),
ty: parse_quote!(Footprint),
implementations: Punctuated::new(),
context_features: vec![],
},
output_type: parse_quote!(T),
is_async: false,
@@ -936,7 +976,7 @@ mod tests {
let attr = quote!(category("Vector: Shape"));
let input = quote!(
/// Test
fn circle(_: impl Ctx, #[default(50.)] radius: f64) -> Vector {
fn circle(_: impl Ctx + ExtractFootprint, #[default(50.)] radius: f64) -> Vector {
// Implementation details...
}
);
@@ -960,8 +1000,9 @@ mod tests {
where_clause: None,
input: Input {
pat_ident: pat_ident("_"),
ty: parse_quote!(impl Ctx),
ty: parse_quote!(impl Ctx + ExtractFootprint),
implementations: Punctuated::new(),
context_features: vec![format_ident!("ExtractFootprint")],
},
output_type: parse_quote!(Vector),
is_async: false,
@@ -1024,6 +1065,7 @@ mod tests {
pat_ident: pat_ident("image"),
ty: parse_quote!(Table<Raster<P>>),
implementations: Punctuated::new(),
context_features: vec![],
},
output_type: parse_quote!(Table<Raster<P>>),
is_async: false,
@@ -1098,6 +1140,7 @@ mod tests {
pat_ident: pat_ident("a"),
ty: parse_quote!(f64),
implementations: Punctuated::new(),
context_features: vec![],
},
output_type: parse_quote!(f64),
is_async: false,
@@ -1160,6 +1203,7 @@ mod tests {
pat_ident: pat_ident("api"),
ty: parse_quote!(&WasmEditorApi),
implementations: Punctuated::new(),
context_features: vec![],
},
output_type: parse_quote!(Table<Raster<CPU>>),
is_async: true,
@@ -1222,6 +1266,7 @@ mod tests {
pat_ident: pat_ident("input"),
ty: parse_quote!(i32),
implementations: Punctuated::new(),
context_features: vec![],
},
output_type: parse_quote!(i32),
is_async: false,

View File

@@ -295,6 +295,7 @@ impl PerPixelAdjustCodegen<'_> {
pat_ident: self.parsed.input.pat_ident.clone(),
ty: parse_quote!(impl #gcore::context::Ctx),
implementations: Default::default(),
context_features: self.parsed.input.context_features.clone(),
},
output_type: raster_gpu,
is_async: true,