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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 869 additions and 328 deletions

View File

@ -77,12 +77,12 @@ jobs:
- name: Run PR benchmarks
run: |
# Compile benchmarks
cargo bench --bench compile_demo_art_iai -- --baseline=master --output-format=json | jq -sc > /tmp/compile_output.json
cargo bench --bench compile_demo_art_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/compile_output.json
# Runtime benchmarks
cargo bench --bench update_executor_iai -- --baseline=master --output-format=json | jq -sc > /tmp/update_output.json
cargo bench --bench run_once_iai -- --baseline=master --output-format=json | jq -sc > /tmp/run_once_output.json
cargo bench --bench run_cached_iai -- --baseline=master --output-format=json | jq -sc > /tmp/run_cached_output.json
cargo bench --bench update_executor_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/update_output.json
cargo bench --bench run_once_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/run_once_output.json
cargo bench --bench run_cached_iai -- --baseline=master --output-format=json | jq -sc | sed 's/\\"//g' > /tmp/run_cached_output.json
- name: Make old comments collapsed by default
uses: actions/github-script@v7
@ -147,11 +147,18 @@ jobs:
let hasSignificantChanges = false;
for (const benchmark of benchmarkOutput) {
if (benchmark.callgrind_summary && benchmark.callgrind_summary.summaries) {
const summary = benchmark.callgrind_summary.summaries[0];
const irDiff = summary.events.Ir;
if (irDiff.diff_pct !== null) {
if (benchmark.profiles && benchmark.profiles.length > 0) {
const profile = benchmark.profiles[0];
if (profile.summaries && profile.summaries.parts && profile.summaries.parts.length > 0) {
const part = profile.summaries.parts[0];
if (part.metrics_summary && part.metrics_summary.Callgrind && part.metrics_summary.Callgrind.Ir) {
const irData = part.metrics_summary.Callgrind.Ir;
if (irData.diffs && irData.diffs.diff_pct !== null) {
const irDiff = {
diff_pct: parseFloat(irData.diffs.diff_pct),
old: irData.metrics.Both[1].Int,
new: irData.metrics.Both[0].Int
};
hasResults = true;
const changePercentage = formatPercentage(irDiff.diff_pct);
const color = irDiff.diff_pct > 0 ? "red" : "lime";
@ -163,19 +170,23 @@ jobs:
sectionBody += "<details>\n<summary>Detailed metrics</summary>\n\n```\n";
sectionBody += `Baselines: master| HEAD\n`;
for (const [eventKind, costsDiff] of Object.entries(summary.events)) {
if (costsDiff.diff_pct !== null) {
const changePercentage = formatPercentage(costsDiff.diff_pct);
const line = `${padRight(eventKind, 20)} ${padLeft(formatNumber(costsDiff.old), 11)}|${padLeft(formatNumber(costsDiff.new), 11)} ${padLeft(changePercentage, 15)}`;
for (const [metricName, metricData] of Object.entries(part.metrics_summary.Callgrind)) {
if (metricData.diffs && metricData.diffs.diff_pct !== null) {
const changePercentage = formatPercentage(parseFloat(metricData.diffs.diff_pct));
const oldValue = metricData.metrics.Both[1].Int || metricData.metrics.Both[1].Float;
const newValue = metricData.metrics.Both[0].Int || metricData.metrics.Both[0].Float;
const line = `${padRight(metricName, 20)} ${padLeft(formatNumber(Math.round(oldValue)), 11)}|${padLeft(formatNumber(Math.round(newValue)), 11)} ${padLeft(changePercentage, 15)}`;
sectionBody += `${line}\n`;
}
}
sectionBody += "```\n</details>\n\n";
if (Math.abs(irDiff.diff_pct) > 5) {
significantChanges = true;
hasSignificantChanges = true;
if (Math.abs(irDiff.diff_pct) > 5) {
significantChanges = true;
hasSignificantChanges = true;
}
}
}
}
}

1
Cargo.lock generated
View File

@ -2100,6 +2100,7 @@ name = "graphene-core"
version = "0.1.0"
dependencies = [
"base64",
"bitflags 2.9.3",
"bytemuck",
"ctor",
"dyn-any",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -46,8 +46,8 @@ pub(crate) trait CefEventHandler: Clone + Send + Sync + 'static {
#[cfg(feature = "accelerated_paint")]
fn draw_gpu(&self, shared_texture: SharedTextureHandle);
fn load_resource(&self, path: PathBuf) -> Option<Resource>;
/// Scheudule the main event loop to run the cef event loop after the timeout
/// [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
/// Schedule the main event loop to run the CEF event loop after the timeout.
/// See [`_cef_browser_process_handler_t::on_schedule_message_pump_work`] for more documentation.
fn schedule_cef_message_loop_work(&self, scheduled_time: Instant);
fn initialized_web_communication(&self);
fn receive_web_message(&self, message: &[u8]);

View File

@ -46,6 +46,10 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::DocumentStructureChanged)),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph(
NodeGraphMessageDiscriminant::RunDocumentGraph,
))),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderRulers)),
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)),
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),

View File

@ -99,14 +99,11 @@ impl MessageHandler<AnimationMessage, ()> for AnimationMessageHandler {
}
}
AnimationMessage::UpdateTime => {
if self.is_playing() {
responses.add(PortfolioMessage::SubmitActiveGraphRender);
if self.live_preview_recently_zero {
// Update the restart and pause/play buttons
responses.add(PortfolioMessage::UpdateDocumentWidgets);
self.live_preview_recently_zero = false;
}
responses.add(PortfolioMessage::SubmitActiveGraphRender);
if self.is_playing() && self.live_preview_recently_zero {
// Update the restart and pause/play buttons
responses.add(PortfolioMessage::UpdateDocumentWidgets);
self.live_preview_recently_zero = false;
}
}
AnimationMessage::RestartAnimation => {

View File

@ -2604,7 +2604,7 @@ impl DocumentMessageHandler {
layout: Layout::WidgetLayout(document_bar_layout),
layout_target: LayoutTarget::DocumentBar,
});
responses.add(NodeGraphMessage::ForceRunDocumentGraph);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
pub fn update_layers_panel_control_bar_widgets(&self, layers_panel_open: bool, responses: &mut VecDeque<Message>) {

View File

@ -145,74 +145,12 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
category: "General",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(2), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::network(generic!(T), 0)],
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
call_argument: generic!(T),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::freeze_real_time::IDENTIFIER),
call_argument: generic!(T),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::boundless_footprint::IDENTIFIER),
call_argument: generic!(T),
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
inputs: vec![NodeInput::value(TaggedValue::None, true)],
implementation: DocumentNodeImplementation::ProtoNode(memo::memo::IDENTIFIER),
call_argument: generic!(T),
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Memoize".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Freeze Real Time".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(7, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Boundless Footprint".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 0)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
},
..Default::default()
}),
input_metadata: vec![("Data", "TODO").into()],
output_names: vec!["Data".to_string()],
..Default::default()
@ -1517,7 +1455,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(3), 0)],
exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: vec![
DocumentNode {
inputs: vec![NodeInput::network(concrete!(Table<Vector>), 0), NodeInput::network(concrete!(vector::style::Fill), 1)],
@ -1531,18 +1469,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
call_argument: generic!(T),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::freeze_real_time::IDENTIFIER),
call_argument: generic!(T),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::boundless_footprint::IDENTIFIER),
call_argument: generic!(T),
..Default::default()
},
]
.into_iter()
.enumerate()
@ -1576,22 +1502,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Freeze Real Time".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Boundless Footprint".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 0)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()
@ -1615,7 +1525,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(4), 0)],
exports: vec![NodeInput::node(NodeId(2), 0)],
nodes: [
DocumentNode {
inputs: vec![NodeInput::network(concrete!(Table<Vector>), 0)],
@ -1644,18 +1554,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
call_argument: generic!(T),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::freeze_real_time::IDENTIFIER),
call_argument: generic!(T),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(3), 0)],
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::boundless_footprint::IDENTIFIER),
call_argument: generic!(T),
..Default::default()
},
]
.into_iter()
.enumerate()
@ -1702,22 +1600,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Freeze Real Time".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Boundless Footprint".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(28, 0)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()
@ -1781,7 +1663,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(3), 0)],
exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: [
DocumentNode {
inputs: vec![
@ -1799,18 +1681,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
call_argument: generic!(T),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::freeze_real_time::IDENTIFIER),
call_argument: generic!(T),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(transform_nodes::boundless_footprint::IDENTIFIER),
call_argument: generic!(T),
..Default::default()
},
]
.into_iter()
.enumerate()
@ -1845,22 +1715,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Freeze Real Time".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(14, 0)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Boundless Footprint".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(21, 0)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()

View File

@ -43,6 +43,7 @@ pub(super) fn post_process_nodes(mut custom: Vec<DocumentNodeDefinition>) -> Vec
fields,
description,
properties,
context_features,
} = metadata;
let Some(implementations) = &node_registry.get(id) else { continue };
@ -59,10 +60,11 @@ pub(super) fn post_process_nodes(mut custom: Vec<DocumentNodeDefinition>) -> Vec
node_template: NodeTemplate {
document_node: DocumentNode {
inputs,
call_argument: (input_type.clone()),
call_argument: input_type.clone(),
implementation: DocumentNodeImplementation::ProtoNode(id.clone()),
visible: true,
skip_deduplication: false,
context_features: ContextDependencies::from(context_features.as_slice()),
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {

View File

@ -16,6 +16,7 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
use graph_craft::{Type, concrete};
use graphene_std::Artboard;
use graphene_std::ContextDependencies;
use graphene_std::math::quad::Quad;
use graphene_std::subpath::Subpath;
use graphene_std::table::Table;
@ -1248,11 +1249,7 @@ impl NodeNetworkInterface {
/// Returns the display name of the node. If the display name is empty, it will return "Untitled Node" or "Untitled Layer" depending on the node type.
pub fn display_name(&self, node_id: &NodeId, network_path: &[NodeId]) -> String {
let is_layer = self
.node_metadata(node_id, network_path)
.expect("Could not get persistent node metadata in untitled_layer_label")
.persistent_metadata
.is_layer();
let is_layer = self.is_layer(node_id, network_path);
let Some(reference) = self.reference(node_id, network_path) else {
log::error!("Could not get reference in untitled_layer_label");
@ -4205,6 +4202,18 @@ impl NodeNetworkInterface {
node.call_argument = call_argument;
}
pub fn set_context_features(&mut self, node_id: &NodeId, network_path: &[NodeId], context_features: ContextDependencies) {
let Some(network) = self.network_mut(network_path) else {
log::error!("Could not get nested network in set_context_features");
return;
};
let Some(node) = network.nodes.get_mut(node_id) else {
log::error!("Could not get node in set_context_features");
return;
};
node.context_features = context_features;
}
pub fn set_input(&mut self, input_connector: &InputConnector, new_input: NodeInput, network_path: &[NodeId]) {
if matches!(input_connector, InputConnector::Export(_)) && matches!(new_input, NodeInput::Network { .. }) {
// TODO: Add support for flattening NodeInput::Network exports in flatten_with_fns https://github.com/GraphiteEditor/Graphite/issues/1762

View File

@ -450,14 +450,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::transform_nodes::transform::IDENTIFIER,
aliases: &["graphene_core::transform::TransformNode"],
},
NodeReplacement {
node: graphene_std::transform_nodes::boundless_footprint::IDENTIFIER,
aliases: &["graphene_core::transform::BoundlessFootprintNode"],
},
NodeReplacement {
node: graphene_std::transform_nodes::freeze_real_time::IDENTIFIER,
aliases: &["graphene_core::transform::FreezeRealTimeNode"],
},
// ???
NodeReplacement {
node: graphene_std::vector::spline::IDENTIFIER,
@ -485,7 +477,13 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
},
NodeReplacement {
node: graphene_std::ops::identity::IDENTIFIER,
aliases: &["graphene_core::transform::CullNode"],
aliases: &[
"graphene_core::transform::CullNode",
"graphene_core::transform::BoundlessFootprintNode",
"graphene_core::transform::FreezeRealTimeNode",
"graphene_core::transform_nodes::BoundlessFootprintNode",
"graphene_core::transform_nodes::FreezeRealTimeNode",
],
},
NodeReplacement {
node: graphene_std::vector::flatten_path::IDENTIFIER,
@ -560,7 +558,7 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
let mut default_template = NodeTemplate::default();
default_template.document_node.implementation = DocumentNodeImplementation::ProtoNode(new.clone());
document.network_interface.replace_implementation(node_id, &network_path, &mut default_template);
document.network_interface.set_call_argument(node_id, &network_path, graph_craft::Type::Generic("T".into()));
document.network_interface.set_call_argument(node_id, &network_path, default_template.document_node.call_argument);
}
}
}
@ -587,9 +585,7 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
// Upgrade old nodes to use `Context` instead of `()` or `Footprint` as their call argument
if node.call_argument == graph_craft::concrete!(()) || node.call_argument == graph_craft::concrete!(graphene_std::transform::Footprint) {
document
.network_interface
.set_call_argument(node_id, network_path, graph_craft::concrete!(graphene_std::Context).into());
document.network_interface.set_call_argument(node_id, network_path, graph_craft::concrete!(graphene_std::Context));
}
// Only nodes that have not been modified and still refer to a definition can be updated
@ -1051,6 +1047,16 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
document.network_interface.add_import(TaggedValue::U32(0), false, 1, "Loop Level", "TODO", &node_path);
}
// Add context features to nodes that don't have them (fine-grained context caching migration)
if node.context_features == graphene_std::ContextDependencies::default() {
if let Some(reference) = document.network_interface.reference(node_id, network_path).cloned().flatten() {
if let Some(node_definition) = resolve_document_node_type(&reference) {
let context_features = node_definition.node_template.document_node.context_features;
document.network_interface.set_context_features(node_id, network_path, context_features);
}
}
}
// ==================================
// PUT ALL MIGRATIONS ABOVE THIS LINE
// ==================================

View File

@ -18,6 +18,7 @@ dealloc_nodes = []
graphene-core-shaders = { workspace = true, features = ["std"] }
# Workspace dependencies
bitflags = { workspace = true }
bytemuck = { workspace = true }
node-macro = { workspace = true }
num-traits = { workspace = true }

View File

@ -1,4 +1,4 @@
use crate::{Ctx, ExtractAnimationTime, ExtractTime};
use crate::{Ctx, ExtractAnimationTime, ExtractRealTime};
const DAY: f64 = 1000. * 3600. * 24.;
@ -21,17 +21,17 @@ pub enum AnimationTimeMode {
}
#[node_macro::node(category("Animation"))]
fn real_time(ctx: impl Ctx + ExtractTime, _primary: (), mode: RealTimeMode) -> f64 {
let time = ctx.try_time().unwrap_or_default();
fn real_time(ctx: impl Ctx + ExtractRealTime, _primary: (), mode: RealTimeMode) -> f64 {
let real_time = ctx.try_real_time().unwrap_or_default();
// TODO: Implement proper conversion using and existing time implementation
match mode {
RealTimeMode::Utc => time,
RealTimeMode::Year => (time / DAY / 365.25).floor() + 1970.,
RealTimeMode::Hour => (time / 1000. / 3600.).floor() % 24.,
RealTimeMode::Minute => (time / 1000. / 60.).floor() % 60.,
RealTimeMode::Utc => real_time,
RealTimeMode::Year => (real_time / DAY / 365.25).floor() + 1970.,
RealTimeMode::Hour => (real_time / 1000. / 3600.).floor() % 24., // TODO: Factor in a chosen timezone
RealTimeMode::Minute => (real_time / 1000. / 60.).floor() % 60., // TODO: Factor in a chosen timezone
RealTimeMode::Second => (time / 1000.).floor() % 60.,
RealTimeMode::Millisecond => time % 1000.,
RealTimeMode::Second => (real_time / 1000.).floor() % 60.,
RealTimeMode::Millisecond => real_time % 1000.,
}
}
@ -40,13 +40,13 @@ fn animation_time(ctx: impl Ctx + ExtractAnimationTime) -> f64 {
ctx.try_animation_time().unwrap_or_default()
}
// These nodes require more sophistcated algorithms for giving the correct result
// These nodes require more sophisticated algorithms for giving the correct result
// #[node_macro::node(category("Animation"))]
// fn month(ctx: impl Ctx + ExtractTime) -> f64 {
// ((ctx.try_time().unwrap_or_default() / DAY / 365.25 % 1.) * 12.).floor()
// fn month(ctx: impl Ctx + ExtractRealTime) -> f64 {
// ((ctx.try_real_time().unwrap_or_default() / DAY / 365.25 % 1.) * 12.).floor()
// }
// #[node_macro::node(category("Animation"))]
// fn day(ctx: impl Ctx + ExtractTime) -> f64 {
// (ctx.try_time().unwrap_or_default() / DAY
// fn day(ctx: impl Ctx + ExtractRealTime) -> f64 {
// (ctx.try_real_time().unwrap_or_default() / DAY
// }

View File

@ -2,6 +2,7 @@ use crate::transform::Footprint;
pub use graphene_core_shaders::context::{ArcCtx, Ctx};
use std::any::Any;
use std::borrow::Borrow;
use std::hash::{Hash, Hasher};
use std::panic::Location;
use std::sync::Arc;
@ -17,8 +18,8 @@ pub trait ExtractFootprint {
}
}
pub trait ExtractTime {
fn try_time(&self) -> Option<f64>;
pub trait ExtractRealTime {
fn try_real_time(&self) -> Option<f64>;
}
pub trait ExtractAnimationTime {
@ -31,19 +32,106 @@ pub trait ExtractIndex {
// Consider returning a slice or something like that
pub trait ExtractVarArgs {
// Call this lifetime 'b so it is less likely to coflict when auto generating the function signature for implementation
fn vararg(&self, index: usize) -> Result<DynRef<'_>, VarArgsResult>;
fn varargs_len(&self) -> Result<usize, VarArgsResult>;
fn hash_varargs(&self, hasher: &mut dyn Hasher);
}
// Consider returning a slice or something like that
pub trait CloneVarArgs: ExtractVarArgs {
// fn box_clone(&self) -> Vec<DynBox>;
fn arc_clone(&self) -> Option<Arc<dyn ExtractVarArgs + Send + Sync>>;
}
pub trait ExtractAll: ExtractFootprint + ExtractIndex + ExtractTime + ExtractAnimationTime + ExtractVarArgs {}
// Inject* traits for providing context features to downstream nodes
pub trait InjectFootprint {}
pub trait InjectRealTime {}
pub trait InjectAnimationTime {}
pub trait InjectIndex {}
pub trait InjectVarArgs {}
impl<T: ?Sized + ExtractFootprint + ExtractIndex + ExtractTime + ExtractAnimationTime + ExtractVarArgs> ExtractAll for T {}
// Modify* marker traits for context-transparent nodes
pub trait ModifyFootprint: ExtractFootprint + InjectFootprint {}
pub trait ModifyRealTime: ExtractRealTime + InjectRealTime {}
pub trait ModifyAnimationTime: ExtractAnimationTime + InjectAnimationTime {}
pub trait ModifyIndex: ExtractIndex + InjectIndex {}
pub trait ModifyVarArgs: ExtractVarArgs + InjectVarArgs {}
pub trait ExtractAll: ExtractFootprint + ExtractIndex + ExtractRealTime + ExtractAnimationTime + ExtractVarArgs {}
impl<T: ?Sized + ExtractFootprint + ExtractIndex + ExtractRealTime + ExtractAnimationTime + ExtractVarArgs> ExtractAll for T {}
impl<T: Ctx> InjectFootprint for T {}
impl<T: Ctx> InjectRealTime for T {}
impl<T: Ctx> InjectIndex for T {}
impl<T: Ctx> InjectAnimationTime for T {}
impl<T: Ctx> InjectVarArgs for T {}
impl<T: Ctx + InjectFootprint + ExtractFootprint> ModifyFootprint for T {}
impl<T: Ctx + InjectRealTime + ExtractRealTime> ModifyRealTime for T {}
impl<T: Ctx + InjectIndex + ExtractIndex> ModifyIndex for T {}
impl<T: Ctx + InjectAnimationTime + ExtractAnimationTime> ModifyAnimationTime for T {}
impl<T: Ctx + InjectVarArgs + ExtractVarArgs> ModifyVarArgs for T {}
// Public enum for flexible node macro codegen
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum ContextFeature {
ExtractFootprint,
ExtractRealTime,
ExtractAnimationTime,
ExtractIndex,
ExtractVarArgs,
InjectFootprint,
InjectRealTime,
InjectAnimationTime,
InjectIndex,
InjectVarArgs,
}
// Internal bitflags for fast compiler analysis
use bitflags::bitflags;
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, Default)]
pub struct ContextFeatures: u32 {
const FOOTPRINT = 1 << 0;
const REAL_TIME = 1 << 1;
const ANIMATION_TIME = 1 << 2;
const INDEX = 1 << 3;
const VARARGS = 1 << 4;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, Default)]
pub struct ContextDependencies {
pub extract: ContextFeatures,
pub inject: ContextFeatures,
}
impl From<&[ContextFeature]> for ContextDependencies {
fn from(features: &[ContextFeature]) -> Self {
let mut extract = ContextFeatures::empty();
let mut inject = ContextFeatures::empty();
for feature in features {
extract |= match feature {
ContextFeature::ExtractFootprint => ContextFeatures::FOOTPRINT,
ContextFeature::ExtractRealTime => ContextFeatures::REAL_TIME,
ContextFeature::ExtractAnimationTime => ContextFeatures::ANIMATION_TIME,
ContextFeature::ExtractIndex => ContextFeatures::INDEX,
ContextFeature::ExtractVarArgs => ContextFeatures::VARARGS,
_ => ContextFeatures::empty(),
};
inject |= match feature {
ContextFeature::InjectFootprint => ContextFeatures::FOOTPRINT,
ContextFeature::InjectRealTime => ContextFeatures::REAL_TIME,
ContextFeature::InjectAnimationTime => ContextFeatures::ANIMATION_TIME,
ContextFeature::InjectIndex => ContextFeatures::INDEX,
ContextFeature::InjectVarArgs => ContextFeatures::VARARGS,
_ => ContextFeatures::empty(),
};
}
Self { extract, inject }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VarArgsResult {
@ -76,9 +164,9 @@ impl<T: ExtractFootprint + Sync> ExtractFootprint for Option<T> {
})
}
}
impl<T: ExtractTime + Sync> ExtractTime for Option<T> {
fn try_time(&self) -> Option<f64> {
self.as_ref().and_then(|x| x.try_time())
impl<T: ExtractRealTime + Sync> ExtractRealTime for Option<T> {
fn try_real_time(&self) -> Option<f64> {
self.as_ref().and_then(|x| x.try_real_time())
}
}
impl<T: ExtractAnimationTime + Sync> ExtractAnimationTime for Option<T> {
@ -101,15 +189,21 @@ impl<T: ExtractVarArgs + Sync> ExtractVarArgs for Option<T> {
let Some(inner) = self else { return Err(VarArgsResult::NoVarArgs) };
inner.varargs_len()
}
fn hash_varargs(&self, hasher: &mut dyn Hasher) {
if let Some(inner) = self {
inner.hash_varargs(hasher)
}
}
}
impl<T: ExtractFootprint + Sync> ExtractFootprint for Arc<T> {
fn try_footprint(&self) -> Option<&Footprint> {
(**self).try_footprint()
}
}
impl<T: ExtractTime + Sync> ExtractTime for Arc<T> {
fn try_time(&self) -> Option<f64> {
(**self).try_time()
impl<T: ExtractRealTime + Sync> ExtractRealTime for Arc<T> {
fn try_real_time(&self) -> Option<f64> {
(**self).try_real_time()
}
}
impl<T: ExtractAnimationTime + Sync> ExtractAnimationTime for Arc<T> {
@ -130,6 +224,10 @@ impl<T: ExtractVarArgs + Sync> ExtractVarArgs for Arc<T> {
fn varargs_len(&self) -> Result<usize, VarArgsResult> {
(**self).varargs_len()
}
fn hash_varargs(&self, hasher: &mut dyn Hasher) {
(**self).hash_varargs(hasher)
}
}
impl<T: CloneVarArgs + Sync> CloneVarArgs for Option<T> {
fn arc_clone(&self) -> Option<Arc<dyn ExtractVarArgs + Send + Sync>> {
@ -145,6 +243,10 @@ impl<T: ExtractVarArgs + Sync> ExtractVarArgs for &T {
fn varargs_len(&self) -> Result<usize, VarArgsResult> {
(*self).varargs_len()
}
fn hash_varargs(&self, hasher: &mut dyn Hasher) {
(*self).hash_varargs(hasher)
}
}
impl<T: CloneVarArgs + Sync> CloneVarArgs for Arc<T> {
fn arc_clone(&self) -> Option<Arc<dyn ExtractVarArgs + Send + Sync>> {
@ -160,9 +262,9 @@ impl ExtractFootprint for ContextImpl<'_> {
self.footprint
}
}
impl ExtractTime for ContextImpl<'_> {
fn try_time(&self) -> Option<f64> {
self.time
impl ExtractRealTime for ContextImpl<'_> {
fn try_real_time(&self) -> Option<f64> {
self.real_time
}
}
impl ExtractIndex for ContextImpl<'_> {
@ -180,6 +282,10 @@ impl ExtractVarArgs for ContextImpl<'_> {
let Some(inner) = self.varargs else { return Err(VarArgsResult::NoVarArgs) };
Ok(inner.len())
}
fn hash_varargs(&self, _hasher: &mut dyn Hasher) {
todo!()
}
}
impl ExtractFootprint for OwnedContextImpl {
@ -187,8 +293,8 @@ impl ExtractFootprint for OwnedContextImpl {
self.footprint.as_ref()
}
}
impl ExtractTime for OwnedContextImpl {
fn try_time(&self) -> Option<f64> {
impl ExtractRealTime for OwnedContextImpl {
fn try_real_time(&self) -> Option<f64> {
self.real_time
}
}
@ -210,7 +316,7 @@ impl ExtractVarArgs for OwnedContextImpl {
};
return parent.vararg(index);
};
inner.get(index).map(|x| x.as_ref()).ok_or(VarArgsResult::IndexOutOfBounds)
inner.get(index).map(|x| x.as_ref() as DynRef<'_>).ok_or(VarArgsResult::IndexOutOfBounds)
}
fn varargs_len(&self) -> Result<usize, VarArgsResult> {
@ -222,6 +328,20 @@ impl ExtractVarArgs for OwnedContextImpl {
};
Ok(inner.len())
}
fn hash_varargs(&self, mut hasher: &mut dyn Hasher) {
match (&self.varargs, &self.parent) {
(Some(inner), _) => {
for arg in inner.iter() {
arg.hash(&mut hasher);
}
}
(None, Some(parent)) => {
parent.hash_varargs(hasher);
}
_ => (),
};
}
}
impl CloneVarArgs for Arc<OwnedContextImpl> {
@ -232,7 +352,7 @@ impl CloneVarArgs for Arc<OwnedContextImpl> {
pub type Context<'a> = Option<Arc<OwnedContextImpl>>;
type DynRef<'a> = &'a (dyn Any + Send + Sync);
type DynBox = Box<dyn Any + Send + Sync>;
type DynBox = Box<dyn AnyHash + Send + Sync>;
#[derive(dyn_any::DynAny)]
pub struct OwnedContextImpl {
@ -249,7 +369,7 @@ impl std::fmt::Debug for OwnedContextImpl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OwnedContextImpl")
.field("footprint", &self.footprint)
.field("varargs", &self.varargs)
.field("varargs_len", &self.varargs.as_ref().map(|x| x.len()))
.field("parent", &self.parent.as_ref().map(|_| "<Parent>"))
.field("index", &self.index)
.field("real_time", &self.real_time)
@ -265,11 +385,10 @@ impl Default for OwnedContextImpl {
}
}
impl std::hash::Hash for OwnedContextImpl {
impl Hash for OwnedContextImpl {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.footprint.hash(state);
self.varargs.as_ref().map(|x| Arc::as_ptr(x).addr()).hash(state);
self.parent.as_ref().map(|x| Arc::as_ptr(x).addr()).hash(state);
self.hash_varargs(state);
self.index.hash(state);
self.real_time.map(|x| x.to_bits()).hash(state);
self.animation_time.map(|x| x.to_bits()).hash(state);
@ -279,21 +398,29 @@ impl std::hash::Hash for OwnedContextImpl {
impl OwnedContextImpl {
#[track_caller]
pub fn from<T: ExtractAll + CloneVarArgs>(value: T) -> Self {
let footprint = value.try_footprint().copied();
let index = value.try_index();
let time = value.try_time();
let frame_time = value.try_animation_time();
let parent = match value.varargs_len() {
Ok(x) if x > 0 => value.arc_clone(),
_ => None,
};
OwnedContextImpl::from_flags(value, ContextFeatures::all())
}
#[track_caller]
pub fn from_flags<T: ExtractAll + CloneVarArgs>(value: T, bitflags: ContextFeatures) -> Self {
let footprint = bitflags.contains(ContextFeatures::FOOTPRINT).then(|| value.try_footprint().copied()).flatten();
let index = bitflags.contains(ContextFeatures::INDEX).then(|| value.try_index()).flatten();
let real_time = bitflags.contains(ContextFeatures::REAL_TIME).then(|| value.try_real_time()).flatten();
let animation_time = bitflags.contains(ContextFeatures::ANIMATION_TIME).then(|| value.try_animation_time()).flatten();
let parent = bitflags
.contains(ContextFeatures::VARARGS)
.then(|| match value.varargs_len() {
Ok(x) if x > 0 => value.arc_clone(),
_ => None,
})
.flatten();
OwnedContextImpl {
footprint,
varargs: None,
parent,
index,
real_time: time,
animation_time: frame_time,
real_time,
animation_time,
}
}
pub const fn empty() -> Self {
@ -308,6 +435,30 @@ impl OwnedContextImpl {
}
}
pub trait DynHash {
fn dyn_hash(&self, state: &mut dyn Hasher);
}
impl<H: Hash + ?Sized> DynHash for H {
fn dyn_hash(&self, mut state: &mut dyn Hasher) {
self.hash(&mut state);
}
}
impl Hash for dyn AnyHash {
fn hash<H: Hasher>(&self, state: &mut H) {
self.dyn_hash(state);
}
}
impl Hash for Box<dyn AnyHash + Send + Sync> {
fn hash<H: Hasher>(&self, state: &mut H) {
(**self).dyn_hash(state);
}
}
pub trait AnyHash: DynHash + Any {}
impl<T: DynHash + Any> AnyHash for T {}
impl OwnedContextImpl {
pub fn set_footprint(&mut self, footprint: Footprint) {
self.footprint = Some(footprint);
@ -316,15 +467,15 @@ impl OwnedContextImpl {
self.footprint = Some(footprint);
self
}
pub fn with_real_time(mut self, time: f64) -> Self {
self.real_time = Some(time);
pub fn with_real_time(mut self, real_time: f64) -> Self {
self.real_time = Some(real_time);
self
}
pub fn with_animation_time(mut self, animation_time: f64) -> Self {
self.animation_time = Some(animation_time);
self
}
pub fn with_vararg(mut self, value: Box<dyn Any + Send + Sync>) -> Self {
pub fn with_vararg(mut self, value: Box<dyn AnyHash + Send + Sync>) -> Self {
assert!(self.varargs.is_none_or(|value| value.is_empty()));
self.varargs = Some(Arc::new([value]));
self
@ -350,9 +501,8 @@ impl OwnedContextImpl {
pub struct ContextImpl<'a> {
pub(crate) footprint: Option<&'a Footprint>,
varargs: Option<&'a [DynRef<'a>]>,
// This could be converted into a single enum to save extra bytes
index: Option<Vec<usize>>,
time: Option<f64>,
index: Option<Vec<usize>>, // This could be converted into a single enum to save extra bytes
real_time: Option<f64>,
}
impl<'a> ContextImpl<'a> {

View File

@ -0,0 +1,121 @@
use crate::Artboard;
use crate::context::{CloneVarArgs, Context, ContextFeatures, Ctx, ExtractAll};
use crate::gradient::GradientStops;
use crate::raster_types::{CPU, GPU, Raster};
use crate::table::Table;
use crate::uuid::NodeId;
use crate::vector::Vector;
use crate::{Graphic, OwnedContextImpl};
use core::f64;
use glam::{DAffine2, DVec2};
use graphene_core_shaders::color::Color;
/// Node for filtering context features based on requirements
/// This node is inserted by the compiler to "zero out" unused context parts
#[node_macro::node(category("Internal"))]
async fn context_modification<T>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> (),
Context -> bool,
Context -> u32,
Context -> u64,
Context -> f32,
Context -> f64,
Context -> String,
Context -> DAffine2,
Context -> DVec2,
Context -> Vec<DVec2>,
Context -> Vec<NodeId>,
Context -> Vec<f64>,
Context -> Vec<f32>,
Context -> Table<Vector>,
Context -> Table<Graphic>,
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> Table<Artboard>,
Context -> Table<GradientStops>,
Context -> GradientStops,
)]
value: impl Node<Context<'static>, Output = T>,
features_to_keep: ContextFeatures,
) -> T {
let new_context = OwnedContextImpl::from_flags(ctx, features_to_keep);
value.eval(Some(new_context.into())).await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transform::Footprint;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
/// Test that the hash of a nullified context remains stable even when nullified inputs change
#[test]
fn test_nullified_context_hash_stability() {
use crate::Context;
use std::sync::Arc;
// Create original contexts using the Context type (Option<Arc<OwnedContextImpl>>)
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),
));
// Test nullifying different features - hash should remain stable for each nullification
let features_to_keep = ContextFeatures::empty(); // Nullify everything
// Create nullified context - this should only keep features specified in features_to_keep
let nullified_ctx = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), features_to_keep);
// Calculate hash of nullified context
let mut hasher1 = DefaultHasher::new();
nullified_ctx.hash(&mut hasher1);
let hash1 = hasher1.finish();
// Create a different original context with changed values
let changed_ctx: Context = Some(Arc::new(
OwnedContextImpl::empty()
.with_footprint(Footprint::default()) // Same footprint
.with_index(2)
.with_real_time(999.9) // Different real time
.with_vararg(Box::new("test"))
.with_animation_time(888.8), // Different animation time
));
// Create nullified context from the changed original - should have same hash since everything is nullified
let nullified_changed_ctx = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), features_to_keep);
let mut hasher2 = DefaultHasher::new();
nullified_changed_ctx.hash(&mut hasher2);
let hash2 = hasher2.finish();
// Hash should be the same because all features were nullified
assert_eq!(hash1, hash2, "Hash of nullified context should remain stable regardless of input changes when features are nullified");
// Test partial nullification - keep only footprint
let partial_features = ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS;
let partial_nullified1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), partial_features);
let partial_nullified2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), partial_features);
let mut hasher3 = DefaultHasher::new();
partial_nullified1.hash(&mut hasher3);
let hash3 = hasher3.finish();
let mut hasher4 = DefaultHasher::new();
partial_nullified2.hash(&mut hasher4);
let hash4 = hasher4.finish();
// These should be the same because both have the same footprint (Footprint::default()) and varargs
// and other features are nullified
assert_eq!(hash3, hash4, "Hash should be stable when keeping only footprint and footprint values are the same");
}
}

View File

@ -7,6 +7,7 @@ pub mod blending_nodes;
pub mod bounds;
pub mod consts;
pub mod context;
pub mod context_modification;
pub mod debug;
pub mod extract_xy;
pub mod generic;

View File

@ -1,4 +1,4 @@
use crate::{Node, NodeIO, NodeIOTypes, ProtoNodeIdentifier, Type, WasmNotSend};
use crate::{ContextFeature, Node, NodeIO, NodeIOTypes, ProtoNodeIdentifier, Type, WasmNotSend};
use dyn_any::{DynAny, StaticType};
use std::collections::HashMap;
use std::marker::PhantomData;
@ -16,6 +16,7 @@ pub struct NodeMetadata {
pub fields: Vec<FieldMetadata>,
pub description: &'static str,
pub properties: Option<&'static str>,
pub context_features: Vec<ContextFeature>,
}
// Translation struct between macro and definition

View File

@ -20,7 +20,7 @@ impl Default for Font {
}
}
/// A cache of all loaded font data and preview urls along with the default font (send from `init_app` in `editor_api.rs`)
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, DynAny)]
#[derive(Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, DynAny)]
pub struct FontCache {
/// Actual font file data used for rendering a font
font_file_data: HashMap<Font, Vec<u8>>,
@ -28,6 +28,15 @@ pub struct FontCache {
preview_urls: HashMap<Font, String>,
}
impl std::fmt::Debug for FontCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FontCache")
.field("font_file_data", &self.font_file_data.keys().collect::<Vec<_>>())
.field("preview_urls", &self.preview_urls)
.finish()
}
}
impl FontCache {
/// Returns the font family name if the font is cached, otherwise returns the fallback font family name if that is cached
pub fn resolve_font<'a>(&'a self, font: &'a Font) -> Option<&'a Font> {

View File

@ -1,16 +1,16 @@
use crate::gradient::GradientStops;
use crate::raster_types::{CPU, GPU, Raster};
use crate::table::Table;
use crate::transform::{ApplyTransform, Footprint, Transform};
use crate::transform::{ApplyTransform, Transform};
use crate::vector::Vector;
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, Graphic, OwnedContextImpl};
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, Graphic, InjectFootprint, ModifyFootprint, OwnedContextImpl};
use core::f64;
use glam::{DAffine2, DVec2};
use graphene_core_shaders::color::Color;
#[node_macro::node(category(""))]
async fn transform<T: ApplyTransform + 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
ctx: impl Ctx + CloneVarArgs + ExtractAll + ModifyFootprint,
#[implementations(
Context -> DAffine2,
Context -> DVec2,
@ -46,7 +46,7 @@ async fn transform<T: ApplyTransform + 'n + 'static>(
#[node_macro::node(category(""))]
fn replace_transform<Data, TransformInput: Transform>(
_: impl Ctx,
_: impl Ctx + InjectFootprint,
#[implementations(Table<Vector>, Table<Raster<CPU>>, Table<Graphic>, Table<Color>, Table<GradientStops>)] mut data: Table<Data>,
#[implementations(DAffine2)] transform: TransformInput,
) -> Table<Data> {
@ -91,43 +91,3 @@ fn decompose_rotation(_: impl Ctx, transform: DAffine2) -> f64 {
fn decompose_scale(_: impl Ctx, transform: DAffine2) -> DVec2 {
transform.decompose_scale()
}
#[node_macro::node(category("Debug"))]
async fn boundless_footprint<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> Table<Vector>,
Context -> Table<Graphic>,
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
Context -> String,
Context -> f64,
)]
transform_target: impl Node<Context<'static>, Output = T>,
) -> T {
let ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::BOUNDLESS);
transform_target.eval(ctx.into_context()).await
}
#[node_macro::node(category("Debug"))]
async fn freeze_real_time<T: 'n + 'static>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Context -> Table<Vector>,
Context -> Table<Graphic>,
Context -> Table<Raster<CPU>>,
Context -> Table<Raster<GPU>>,
Context -> Table<Color>,
Context -> Table<GradientStops>,
Context -> String,
Context -> f64,
)]
transform_target: impl Node<Context<'static>, Output = T>,
) -> T {
let ctx = OwnedContextImpl::from(ctx).with_real_time(0.);
transform_target.eval(ctx.into_context()).await
}

View File

@ -2,13 +2,24 @@ use crate::gradient::GradientStops;
use crate::raster_types::{CPU, Raster};
use crate::table::{Table, TableRowRef};
use crate::vector::Vector;
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractIndex, ExtractVarArgs, Graphic, OwnedContextImpl};
use crate::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractIndex, ExtractVarArgs, Graphic, InjectIndex, InjectVarArgs, OwnedContextImpl};
use glam::DVec2;
use graphene_core_shaders::color::Color;
#[repr(transparent)]
#[derive(dyn_any::DynAny)]
struct HashableDVec2(DVec2);
impl std::hash::Hash for HashableDVec2 {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.x.to_bits().hash(state);
self.0.y.to_bits().hash(state);
}
}
#[node_macro::node(name("Instance on Points"), category("Instancing"), path(graphene_core::vector))]
async fn instance_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx,
ctx: impl ExtractAll + CloneVarArgs + Sync + Ctx + InjectIndex + InjectVarArgs,
points: Table<Vector>,
#[implementations(
Context -> Table<Graphic>,
@ -26,7 +37,7 @@ async fn instance_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>
let mut iteration = async |index, point| {
let transformed_point = transform.transform_point2(point);
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_vararg(Box::new(transformed_point));
let new_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_vararg(Box::new(HashableDVec2(transformed_point)));
let generated_instance = instance.eval(new_ctx.into_context()).await;
for mut generated_row in generated_instance.into_iter() {
@ -52,7 +63,7 @@ async fn instance_on_points<T: Into<Graphic> + Default + Send + Clone + 'static>
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn instance_repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
ctx: impl ExtractAll + CloneVarArgs + Ctx,
ctx: impl ExtractAll + CloneVarArgs + Ctx + InjectIndex,
#[implementations(
Context -> Table<Graphic>,
Context -> Table<Vector>,
@ -84,8 +95,8 @@ async fn instance_repeat<T: Into<Graphic> + Default + Send + Clone + 'static>(
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn instance_position(ctx: impl Ctx + ExtractVarArgs) -> DVec2 {
match ctx.vararg(0).map(|dynamic| dynamic.downcast_ref::<DVec2>()) {
Ok(Some(position)) => return *position,
match ctx.vararg(0).map(|dynamic| dynamic.downcast_ref::<HashableDVec2>()) {
Ok(Some(position)) => return position.0,
Ok(_) => warn!("Extracted value of incorrect type"),
Err(e) => warn!("Cannot extract position vararg: {e:?}"),
}

View File

@ -7,7 +7,7 @@ use glam::IVec2;
use graphene_core::memo::MemoHashGuard;
pub use graphene_core::uuid::NodeId;
pub use graphene_core::uuid::generate_uuid;
use graphene_core::{Context, Cow, MemoHash, ProtoNodeIdentifier, Type};
use graphene_core::{Context, ContextDependencies, Cow, MemoHash, ProtoNodeIdentifier, Type};
use log::Metadata;
use rustc_hash::FxHashMap;
use std::collections::HashMap;
@ -60,6 +60,9 @@ pub struct DocumentNode {
/// The path to this node and its inputs and outputs as of when [`NodeNetwork::generate_node_paths`] was called.
#[serde(skip)]
pub original_location: OriginalLocation,
/// List of Extract and Inject annotations for the Context.
#[serde(default)]
pub context_features: ContextDependencies,
}
/// Represents the original location of a node input/output when [`NodeNetwork::generate_node_paths`] was called, allowing the types and errors to be derived.
@ -92,6 +95,7 @@ impl Default for DocumentNode {
visible: true,
skip_deduplication: Default::default(),
original_location: OriginalLocation::default(),
context_features: Default::default(),
}
}
}
@ -159,6 +163,7 @@ impl DocumentNode {
construction_args: args,
original_location: self.original_location,
skip_deduplication: self.skip_deduplication,
context_features: self.context_features,
}
}
}

View File

@ -16,7 +16,7 @@ use graphene_core::uuid::NodeId;
use graphene_core::vector::Vector;
use graphene_core::vector::style::Fill;
use graphene_core::vector::style::GradientStops;
use graphene_core::{Artboard, Color, Graphic, MemoHash, Node, Type};
use graphene_core::{Artboard, Color, ContextFeatures, Graphic, MemoHash, Node, Type};
use graphene_svg_renderer::RenderMetadata;
use std::fmt::Display;
use std::hash::Hash;
@ -217,6 +217,7 @@ tagged_value! {
BrushStrokes(Vec<BrushStroke>),
BrushCache(BrushCache),
DocumentNode(DocumentNode),
ContextFeatures(ContextFeatures),
Curve(graphene_raster_nodes::curve::Curve),
Footprint(graphene_core::transform::Footprint),
VectorModification(Box<graphene_core::vector::VectorModification>),

View File

@ -17,7 +17,7 @@ impl Compiler {
let proto_networks = network.into_proto_networks();
proto_networks.map(move |mut proto_network| {
proto_network.resolve_inputs()?;
proto_network.insert_context_nullification_nodes()?;
proto_network.generate_stable_node_ids();
Ok(proto_network)
})

View File

@ -1,3 +1,4 @@
use crate::document::value::TaggedValue;
use crate::document::{InlineRust, value};
use crate::document::{NodeId, OriginalLocation};
pub use graphene_core::registry::*;
@ -132,6 +133,7 @@ pub struct ProtoNode {
pub identifier: ProtoNodeIdentifier,
pub original_location: OriginalLocation,
pub skip_deduplication: bool,
pub(crate) context_features: ContextDependencies,
}
impl Default for ProtoNode {
@ -142,6 +144,7 @@ impl Default for ProtoNode {
call_argument: concrete!(()),
original_location: OriginalLocation::default(),
skip_deduplication: false,
context_features: Default::default(),
}
}
}
@ -181,6 +184,7 @@ impl ProtoNode {
..Default::default()
},
skip_deduplication: false,
context_features: Default::default(),
}
}
@ -290,15 +294,137 @@ impl ProtoNetwork {
(inwards_edges, id_map)
}
/// Performs topological sort and reorders ids.
pub fn resolve_inputs(&mut self) -> Result<(), String> {
/// Inserts context nullification nodes to optimize caching.
/// This analysis is performed after topological sorting to ensure proper dependency tracking.
pub fn insert_context_nullification_nodes(&mut self) -> Result<(), String> {
// Perform topological sort once
self.reorder_ids()?;
self.find_context_dependencies(self.output);
// Perform topological sort a second time to integrate the new nodes
self.reorder_ids()?;
Ok(())
}
/// Update all of the references to a node ID in the graph with a new ID named `replacement_node_id`.
fn insert_context_nullification_node(&mut self, node_id: NodeId, context_deps: ContextFeatures) -> NodeId {
let (_, node) = &self.nodes[node_id.0 as usize];
let mut path = node.original_location.path.clone();
// Add a path extension with a placeholder value which should not conflict with existing paths
if let Some(p) = path.as_mut() {
p.push(NodeId(10))
}
let memo_node_id = NodeId(self.nodes.len() as u64);
self.nodes.push((
memo_node_id,
ProtoNode {
construction_args: ConstructionArgs::Nodes(vec![node_id]),
call_argument: concrete!(Context),
identifier: graphene_core::memo::memo::IDENTIFIER,
original_location: OriginalLocation {
path: path.clone(),
..Default::default()
},
..Default::default()
},
));
let nullification_value_node_id = NodeId(self.nodes.len() as u64);
self.nodes.push((
nullification_value_node_id,
ProtoNode {
construction_args: ConstructionArgs::Value(MemoHash::new(TaggedValue::ContextFeatures(context_deps))),
call_argument: concrete!(Context),
identifier: ProtoNodeIdentifier::new("graphene_core::value::ClonedNode"),
original_location: OriginalLocation {
path: path.clone(),
..Default::default()
},
..Default::default()
},
));
let nullification_node_id = NodeId(self.nodes.len() as u64);
self.nodes.push((
nullification_node_id,
ProtoNode {
construction_args: ConstructionArgs::Nodes(vec![memo_node_id, nullification_value_node_id]),
call_argument: concrete!(Context),
identifier: graphene_core::context_modification::context_modification::IDENTIFIER,
original_location: OriginalLocation {
path: path.clone(),
..Default::default()
},
..Default::default()
},
));
nullification_node_id
}
fn find_context_dependencies(&mut self, id: NodeId) -> (ContextFeatures, Option<NodeId>) {
let mut branch_dependencies = Vec::new();
let mut combined_deps = ContextFeatures::default();
let node_index = id.0 as usize;
let context_features = self.nodes[node_index].1.context_features;
let mut inputs = match &self.nodes[node_index].1.construction_args {
// We pretend like we have already placed context modification nodes after ourselves because value nodes don't need to be cached
ConstructionArgs::Value(_) => return (context_features.extract, Some(id)),
ConstructionArgs::Nodes(items) => items.clone(),
ConstructionArgs::Inline(_) => return (context_features.extract, Some(id)),
};
// Compute the dependencies for each branch and combine all of them
for &node in &inputs {
let branch = self.find_context_dependencies(node);
branch_dependencies.push(branch);
combined_deps |= branch.0;
}
let mut new_deps = combined_deps;
// Remove requirements which this node provides
new_deps &= !context_features.inject;
// Add requirements we have
new_deps |= context_features.extract;
// If we either introduce new dependencies, we can cache all children which don't yet need that dependency
let we_introduce_new_deps = !combined_deps.contains(new_deps);
// For diverging branches, we can add a cache node for all branches which don't reqire all dependencies
for (child_node, (deps, new_id)) in inputs.iter_mut().zip(branch_dependencies.into_iter()) {
if let Some(new_id) = new_id {
*child_node = new_id;
} else if we_introduce_new_deps || deps != combined_deps {
*child_node = self.insert_context_nullification_node(*child_node, deps);
}
}
self.nodes[node_index].1.construction_args = ConstructionArgs::Nodes(inputs);
// Which dependencies do we supply (and don't need ourselves)?
let net_injections = context_features.inject.difference(context_features.extract);
// Which dependencies still need to be met after this node?
let remaining_deps_from_children = combined_deps.difference(net_injections);
// Do we satisfy any existing dependencies?
let we_supply_existing_deps = !combined_deps.difference(remaining_deps_from_children).is_empty();
let mut new_id = None;
if we_supply_existing_deps {
// Our set of context dependencies has shrunk so we can add a cache node after the current node
new_id = Some(self.insert_context_nullification_node(id, new_deps));
}
(new_deps, new_id)
}
/// Update all of the references to a node ID in the graph with a new ID named `compose_node_id`.
fn replace_node_id(&mut self, outwards_edges: &HashMap<NodeId, Vec<NodeId>>, node_id: NodeId, replacement_node_id: NodeId) {
// Update references in other nodes to use the new node
if let Some(referring_nodes) = outwards_edges.get(&node_id) {
@ -801,7 +927,9 @@ mod test {
#[test]
fn stable_node_id_generation() {
let mut construction_network = test_network();
construction_network.resolve_inputs().expect("Error when calling 'resolve_inputs' on 'construction_network.");
construction_network
.insert_context_nullification_nodes()
.expect("Error when calling 'insert_context_nullification_nodes' on 'construction_network.");
construction_network.generate_stable_node_ids();
assert_eq!(construction_network.nodes[0].1.identifier.name.as_ref(), "value");
let ids: Vec<_> = construction_network.nodes.iter().map(|(id, _)| *id).collect();

View File

@ -6,14 +6,14 @@ use spirv_std::spirv;
///
/// So to make a fullscreen triangle around a box at (-1..1):
///
/// ```norun
/// 3 +
/// ```text
/// 3 +
/// |\
/// 2 | \
/// 2 | \
/// | \
/// 1 +-----+
/// 1 +-----+
/// | |\
/// 0 | 0 | \
/// 0 | 0 | \
/// | | \
/// -1 +-----+-----+
/// -1 0 1 2 3

View File

@ -53,6 +53,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
// into_node!(from: Table<Raster<CPU>>, to: Table<Raster<SRGBA8>>),
#[cfg(feature = "gpu")]
into_node!(from: &WasmEditorApi, to: &WgpuExecutor),
convert_node!(from: String, to: String),
// =============
// MONITOR NODES
// =============
@ -123,9 +124,19 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::transform::ReferencePoint]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::vector::misc::CentroidType]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_core::text::TextAlign]),
// Context nullification
#[cfg(feature = "gpu")]
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &WasmEditorApi, Context => graphene_std::ContextFeatures]),
#[cfg(feature = "gpu")]
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Arc<WasmSurfaceHandle>, Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => WgpuSurface, Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Option<WgpuSurface>, Context => graphene_std::ContextFeatures]),
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => WindowHandle, Context => graphene_std::ContextFeatures]),
// ==========
// MEMO NODES
// ==========
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => ()]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => bool]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Artboard>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Graphic>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Vector>]),
@ -133,7 +144,11 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Image<Color>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<GradientStops>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => GradientStops]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<DVec2>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<NodeId>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f64>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f32>]),
#[cfg(feature = "gpu")]
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Arc<WasmSurfaceHandle>]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => WindowHandle]),
@ -141,9 +156,14 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => WindowHandle]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => SurfaceFrame]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => f64]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => f32]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => u32]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => u64]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => String]),
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => RenderOutput]),
#[cfg(feature = "gpu")]
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => &WasmEditorApi]),
#[cfg(feature = "gpu")]
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => WgpuSurface]),
#[cfg(feature = "gpu")]
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Raster<GPU>>]),
@ -314,6 +334,7 @@ mod node_registry_macros {
convert_node!(from: $from, to: u128),
convert_node!(from: $from, to: isize),
convert_node!(from: $from, to: usize),
convert_node!(from: $from, to: String),
];
x
}};

View File

@ -5,6 +5,7 @@ use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput,
use graph_craft::generic;
use graph_craft::wasm_application_io::WasmEditorApi;
use graphene_std::Context;
use graphene_std::ContextFeatures;
use graphene_std::uuid::NodeId;
use std::sync::Arc;
@ -50,6 +51,10 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEdito
NodeInput::node(NodeId(1), 0),
],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::wasm_application_io::RenderNode")),
context_features: graphene_std::ContextDependencies {
extract: ContextFeatures::FOOTPRINT,
inject: ContextFeatures::FOOTPRINT | ContextFeatures::REAL_TIME | ContextFeatures::ANIMATION_TIME,
},
..Default::default()
},
]

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,

View File

@ -114,6 +114,7 @@ pub fn generate_node_substitutions() -> HashMap<ProtoNodeIdentifier, DocumentNod
implementation: DocumentNodeImplementation::ProtoNode(id.clone()),
visible: true,
skip_deduplication: false,
context_features: ContextDependencies::from(metadata.context_features.as_slice()),
..Default::default()
};

View File

@ -0,0 +1,193 @@
# Summary
Add a new compilation pass to "nullify" parts of the dynamic `Context` based on the usage within the graph to avoid unnecessary cache invalidations.
# Motivation
Caching of node outputs can only be done if the input (`Context`) the node was evaluated with has not changed between subsequent evaluations. This can lead to "false invalidation" which is when the cache is invalidated even though the node did not even depend on value that changed and still returns the same result.
```rs
// This node does not use any time information so we don't need to rerun it if the time has changed.
#[node_macro]
fn use_footprint(ctx: impl Ctx + ExtractFootprint, a: u32) -> {...}
```
To mitigate this, we introduced a relatively fine grained `Extract*` API for interacting with the context. We can use the trait annotations produced by this system to infer which parts of the context are used on which graph evaluation paths during graph compile time.
# Guide-level explanation
Our current implementation of the `OwnedContextImpl` struct contains many values which can be used to pass data to nodes. But most of the time, the majority of these fields will be unused by the nodes, but when considering the equality of two `OwnedContextImpl` instances, they have to be considered.
```rs
pub struct OwnedContextImpl {
footprint: Option<crate::transform::Footprint>,
varargs: Option<Arc<[DynBox]>>,
parent: Option<Arc<dyn ExtractVarArgs + Sync + Send>>,
index: Option<usize>,
real_time: Option<f64>,
animation_time: Option<f64>,
}
```
## Why is `Context` equality important?
In Graphene, every node has to be *idempotent* that means that when we provide it with the same input, it will return the same output. This is a really useful property for caching because we can effectively only do the computation once and then reuse the result which could be significantly cheaper.
What is the input then?
The input to all of nodes is of type `Context` which in of itself is just defined as:
```rs
pub type Context = Option<Arc<OwnedContextImpl>>;
```
We use this unified dynamic context type because this means we only have to compile one version of a node and all nodes are compatible with each other but this is not a formal limitation (and should never be considered to be a given).
The different parts of the context (e.g. `Footprint`, `index`, ...) are called *features*.
It thus makes sense that we have to check the equality of `Context` objects to test if we can reuse a cached value or not. If as in the example above a node only relies on one part of the `Context` we don't really care if some other part has changed and the contexts can be considered equal for use in **this node**.
Cache nodes compare the equality of inputs based on the hash code. To stay compatible with the existing API, we can "zero out" parts of the `OwnedContextImpl` by setting unused variants to `None`. This is done by a context modification node which is placed into the graph by the compiler.
The `ExtractAll` trait can be used to create a new Context based on the previous one which can be utilized by nodes which need to modify the context for their child nodes but don't depend on the data themselves.
```rs
#[node_macro::node(category(""))]
async fn transform(ctx: impl Ctx + ExtractAll, ...) {...}
```
## Context Feature Injection
Some nodes need to provide context features for their downstream dependencies (in the function call stack building phase). This is accomplished through `Inject*` traits that complement the `Extract*` traits:
```rs
// A node that injects index information for downstream map operations
#[node_macro::node(category("Iteration"))]
fn map_with_index<T>(
ctx: impl Ctx + InjectIndex,
collection: Vec<T>,
mapper: impl Node<T, Output = U>,
) -> Vec<U> {
collection.iter().enumerate().map(|(index, item)| {
// This node injects the current index into the context
// for the mapper node to extract via ExtractIndex
let ctx_with_index = ctx.with_injected_index(index);
mapper.eval_with_context(ctx_with_index, item)
}).collect()
}
// Downstream nodes can extract the injected index
#[node_macro::node(category("Utility"))]
fn use_index(ctx: impl Ctx + ExtractIndex, value: f64) -> f64 {
let index = ctx.index().unwrap_or(0);
value * (index as f64)
}
```
### Injection Hierarchy and Precedence
When a node both extracts and injects the same feature:
- **Extract-then-Inject**: Node extracts from upstream, processes, then injects modified version downstream
- **Inject-Override**: Injected features take precedence over upstream extracted features
- **Injection Scope**: Injected features are only available to immediate downstream nodes in the evaluation chain
# Reference-level explanation
The different `Extract*` and `Inject*` traits are exported by the node macro and are included as part of the document node definition to inform the compiler about features extracted and injected by every node. Note that the `ExtractAll` will be ignored in this analysis.
## Context Nullification Analysis
The compiler determines where to insert context nullification nodes through branch analysis:
1. **Extract Requirement Tracking**: For each branch in the graph, track the extract requirements all the way back to their corresponding inject nodes. Every extracted feature must have a corresponding inject node somewhere upstream, otherwise this is a compile error.
2. **Branch Convergence Analysis**: When two branches with different extract requirements meet (at a node that takes multiple inputs), one or both branches can have their context nullified to remove features only needed in the other branch.
3. **Post-Injection Nullification**: After an inject node, the extract needs of downstream nodes are satisfied for that inject type. At this point we can check if all the features that the inject node provides are actually used downstream, and if not, nullify them immediately.
4. **Injection Scope Optimization**: After every inject node, analyze whether all injected features are actually consumed by downstream nodes. Unused injected features can be nullified right at the injection point.
## Inject* Trait System
The injection system provides these complementary marker traits to Extract*:
```rs
pub trait InjectFootprint {}
pub trait InjectRealTime {}
pub trait InjectIndex {}
pub trait InjectVarArgs {}
```
## Context Feature Modification Traits
The modification system provides marker traits for nodes that transform context features without necessarily depending on them:
```rs
pub trait ModifyFootprint: ExtractFootprint + InjectFootprint {}
pub trait ModifyRealTime: ExtractRealTime + InjectRealTime {}
pub trait ModifyIndex: ExtractIndex + InjectIndex {}
pub trait ModifyVarArgs: ExtractVarArgs + InjectVarArgs {}
```
### Conditional Context Dependencies
Modify* traits represent a special case in context analysis:
```rs
// Transform node example - modifies footprint but doesn't need it unless downstream requires it
#[node_macro::node(category("Transform"))]
fn transform(
ctx: impl Ctx + ModifyFootprint,
input: Vector,
transform: Transform2D,
) -> Vector {
// This node can extract the footprint, modify it, and inject the result
// But if no downstream node needs the footprint, this node doesn't need it either
let modified_footprint = ctx.footprint().transform(transform);
// ... transform logic ...
}
```
### Optimization Implications for Modify* Traits
1. **Conditional Requirements**: Modify* nodes only require their features if downstream nodes extract them
2. **Pass-through Optimization**: If no downstream extraction occurs, the Modify* node can be treated as if it has no context requirements
3. **Transform Chains**: Multiple Modify* nodes can be chained together, with requirements only propagating if there's a final Extract* consumer
Example optimization:
```
[Node A] -> [ModifyFootprint] -> [ModifyFootprint] -> [ExtractRealTime]
↑ ↑ ↑
No footprint needed No footprint needed Only real time needed
[Node A] -> [ModifyFootprint] -> [ModifyFootprint] -> [ExtractFootprint]
↑ ↑ ↑
Footprint needed Footprint needed Footprint needed
```
This allows transform chains to be optimized when their modifications aren't actually consumed downstream.
Note that "downstream" in this context refers to nodes that are called later in the function call stack building phase, which is inverted compared to the usual data flow direction.
This can be implemented as a compiler pass similar to the compose node insertion.
### Error Handling
- Compile-time validation: Every Extract* must have corresponding Inject* upstream
# Drawbacks
Having an extra compiler pass will impact the performance slightly although the impact is expected to be small because we already have a backlink structure and a topological sort of proto nodes which we can repurpose.
# Rationale and alternatives
Moving this fine grained cache invalidation to a compiler pass allows us to implement this with a minimal impact on the graph runtime. Other options would consist of tracking the usage of features at graph runtime inducing overheads.
This is expected to have the biggest impact on real time applications such as animation or when working with non-footprint aware nodes which would also benefit from this optimization.
# Unresolved questions
- ~~How do we communicate to the context modification nodes which parts of the context should be "zeroed"?~~
- ~~How does this interact with "smart caching" (nodes which use e.g. the Footprint to approximate the result through upscaling)?~~
# Future possibilities
- ~~Adding `Inject*` annotation to complement the `Extract*` ones to provide even more fine grained control over caching.~~

View File

@ -1,13 +1,13 @@
use crate::WgpuExecutor;
use graphene_core::Ctx;
use graphene_core::color::SRGBA8;
use graphene_core::raster_types::{CPU, GPU, Raster};
use graphene_core::table::{Table, TableRow};
use graphene_core::{Ctx, ExtractFootprint};
use wgpu::util::{DeviceExt, TextureDataOrder};
use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages};
#[node_macro::node(category(""))]
pub async fn upload_texture<'a: 'n>(_: impl ExtractFootprint + Ctx, input: Table<Raster<CPU>>, executor: &'a WgpuExecutor) -> Table<Raster<GPU>> {
pub async fn upload_texture<'a: 'n>(_: impl Ctx, input: Table<Raster<CPU>>, executor: &'a WgpuExecutor) -> Table<Raster<GPU>> {
let device = &executor.context.device;
let queue = &executor.context.queue;
let table = input