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:
parent
c081d0a9de
commit
acd7ba38cc
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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>) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ==================================
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:?}"),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}};
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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.~~
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue