Skip to content

Improve node graph performance #2795

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open

Conversation

adamgerhant
Copy link
Collaborator

@adamgerhant adamgerhant commented Jun 29, 2025

Move wire rendering into backend
Implement culling for offscreen nodes
Improves properties/document upgrade stability

Decreases frame time from 120ms -> 20ms when zoomed in on red dress node graph
Increases frame time from 120 -> 200ms when fully zoomed out

Old:
chrome_D18AToYUWg
New:
chrome_AMXWLyTqfv

Known issues:
Subpath rectangle_intersections_exist function is buggy, so dragging onto wire is still inconsistent

@otdavies
Copy link
Member

otdavies commented Jul 1, 2025

!build

Copy link

github-actions bot commented Jul 1, 2025

📦 Build Complete for be143ca
https://f74d22a9.graphite.pages.dev

set_to_exposed: !exposed,
start_transaction: true,
}
.into()]))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only zoom into the exposed node if node graph is closed (implemented in ExposeInput)

// Skip not exposed inputs (they still get an entry to help with finding the primary input)
let lookup = if !is_exposed {
None
} else {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor collecting frontend metadata for node inputs when sending the node graph to the frontend.

connected_to,
})
} else {
None
};

let mut exposed_outputs = Vec::new();
for (index, exposed_output) in output_types.iter().enumerate() {
if index == 0 && network_interface.has_primary_output(&node_id, breadcrumb_network_path) {
for output_index in 0..network_interface.number_of_outputs(&node_id, breadcrumb_network_path) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor output type function to return per output, rather than all types for a node

valid_types: input.valid_types.iter().map(|ty| ty.to_string()).collect(),
name: input.input_name.unwrap_or_else(|| input.ty.nested_type().to_string()),
description: input.input_description.unwrap_or_default(),
name: input.input_name,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input name logic all performed when collecting the frontend_inputs_lookup

let wires = self.collect_wires(network_interface, preferences.graph_wire_style, breadcrumb_network_path);
responses.add(FrontendMessage::UpdateNodeGraphWires { wires });
}
NodeGraphMessage::UpdateVisibleNodes => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Offscreen node culling

blank_assist,
exposeable,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move exposable into the info section

let ParameterWidgetsInfo {
document_node,
node_id,
index,
name,
description,
input_type,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Base expose widget type on the actual type rather than a hardcoded value

@@ -1903,44 +1833,37 @@ pub fn math_properties(node_id: NodeId, context: &mut NodePropertiesContext) ->
}

pub struct ParameterWidgetsInfo<'a> {
document_node: &'a DocumentNode,
document_node: Option<&'a DocumentNode>,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now an option so the error logic can be handled in the widget (once), rather than the user of the widget (multiple)

node_id: NodeId,
index: usize,
name: &'a str,
description: &'a str,
name: String,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Owned value, since it gets converted to an owned value when sending to the front end, and there needs to be logic to replace the name with the type if necessary.

description: &'a str,
name: String,
description: String,
input_type: FrontendGraphDataType,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calculcated based on the type in the new constructor

@@ -50,7 +50,7 @@ pub struct FrontendGraphInput {
pub name: String,
pub description: String,
#[serde(rename = "resolvedType")]
pub resolved_type: Option<String>,
pub resolved_type: String,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frontend shouldn't perform logic for what to display, it should all be centralized in the backend.

@@ -995,27 +990,24 @@ pub fn query_assign_colors_randomize(node_id: NodeId, context: &NodePropertiesCo
pub(crate) fn brightness_contrast_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
use graphene_std::raster::brightness_contrast::*;

// Use Classic
let use_classic = bool_widget(ParameterWidgetsInfo::new(node_id, UseClassicInput::INDEX, true, context), CheckboxInput::default());

Copy link
Collaborator Author

@adamgerhant adamgerhant Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

creating a widget requires a mutable reference to the network interface to get the input type, although this will be changed in my next pr. This is why many of the properties have to be reordered


let mut widgets = super::start_widgets_exposable(self.parameter_info, FrontendGraphDataType::General, self.exposable);
let mut widgets = super::start_widgets(self.parameter_info);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exposable now lives in the parameter info

@@ -153,16 +153,6 @@ pub struct Transform {
pub y: f64,
}

Copy link
Collaborator Author

@adamgerhant adamgerhant Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These structs are moved to node_graph/utility_types.rs, where the wire vector shape is generated

@@ -699,60 +774,51 @@ impl NodeNetworkInterface {
///
/// This function assumes that export indices and node IDs always exist within their respective
/// collections. It will panic if these assumptions are violated.
pub fn output_types(&self, node_id: &NodeId, network_path: &[NodeId]) -> Vec<Option<(Type, TypeSource)>> {
///
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactors to return single type for each output index, just like input_type function

// let mut encapsulating_path = network_path.to_vec();
// let encapsulating_node = encapsulating_path.pop().expect("No imports exist in document network");
// self.input_type(&InputConnector::node(encapsulating_node, *import_index), network_path)
(concrete!(()), TypeSource::Error("Could not type from network"))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be implemented in my next pr

}
graph_craft::document::DocumentNodeImplementation::Extract => {
output_types.push(Some((concrete!(()), TypeSource::Error("extract node"))));
self.resolved_types
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function no longer returns an option, will refactor in my next pr so that errors will display "Unknown" as the type

@@ -1124,55 +1136,39 @@ impl NodeNetworkInterface {
Some(&node.implementation)
}

pub fn input_name<'a>(&'a self, node_id: NodeId, index: usize, network_path: &[NodeId]) -> Option<&'a str> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input name no longer relies on the definition name, since it is guaranteed to exist for every input due to validate_input_metadata when opening the file

}

pub fn insert_input_properties_row(&mut self, node_id: &NodeId, index: usize, network_path: &[NodeId], row: PropertiesRow) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should never be called, since when upgrading a node the inputs and metadata are added at the same time to ensure they stay in sync

@@ -2334,7 +2358,7 @@ impl NodeNetworkInterface {
return;
};
network_metadata.transient_metadata.all_nodes_bounding_box.unload();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now a function to ensure wires are also unloaded

@@ -5679,34 +6004,6 @@ impl NodeNetworkInterface {
self.force_set_upstream_to_chain(node_id, network_path);
}
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same function existed in document.rs

pub input_name: String,
#[serde(skip)]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not be skipped, since this means renamed inputs wouldnt be saved

impl Default for PropertiesRow {
fn default() -> Self {
("", "TODO").into()
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "TODO" got replaces with "" when sending to the frontend, so theres no reason to default to it

}
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor to be a chainable constructor

@@ -6403,6 +6677,48 @@ impl DocumentNodePersistentMetadata {
}
}

#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct InputMetadata {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow same pattern as NodeNetworkMetadata/DocumentNodeMetadata


#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document upgrade struct

DocumentNodePersistentMetadata {
reference: old.reference,
display_name: old.display_name,
input_properties,
input_metadata: Vec::new(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when upgrading from an old file, replace the input metadata with an empty vec, which will be populated based on the definition in validate_input_metadata

@@ -426,6 +427,38 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
// Upgrade the document's nodes to be compatible with the latest version
document_migration_upgrades(&mut document, reset_node_definitions_on_open);

// Ensure each node has the metadata for its inputs
for (node_id, node, path) in document.network_interface.document_network().clone().recursive_nodes() {
document.network_interface.validate_input_metadata(node_id, node, &path);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensure all input metadata exists for each input, if not then it uses the default from the definition. If there is no definition reference, then it uses an empty name, which will get replaced with the type

@@ -172,9 +172,10 @@ impl EditorTestUtils {
pub fn get_node<'a, T: InputAccessor<'a, DocumentNode>>(&'a self) -> impl Iterator<Item = T> + 'a {
self.active_document()
.network_interface
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

migrated to function that already existed in document.rs

@@ -363,17 +363,6 @@ impl NodeInput {
NodeInput::Reflection(_) => false,
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

network inputs no longer exist in the top level document network

RecursiveNodeIter { nodes }
}
}

/// An iterator over all [`DocumentNode`]s, including ones that are deeply nested.
pub struct RecursiveNodeIter<'a> {
nodes: Vec<(&'a NodeId, &'a DocumentNode)>,
nodes: Vec<(&'a NodeId, &'a DocumentNode, Vec<NodeId>)>,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also includes node path

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants