diff --git a/Cargo.lock b/Cargo.lock index 599b75a033..2a28d56c5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2307,6 +2307,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "wgpu", "wgpu-executor", ] diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message.rs index db99d4f987..2d924899a8 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message.rs +++ b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message.rs @@ -5,4 +5,5 @@ use crate::messages::prelude::*; pub enum PreferencesDialogMessage { MayRequireRestart, Confirm, + Update, } diff --git a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs index 33755de44b..c7555150e0 100644 --- a/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs +++ b/editor/src/messages/dialog/preferences_dialog/preferences_dialog_message_handler.rs @@ -3,6 +3,7 @@ use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle; use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; +use graphene_std::render_node::{EditorPreferences, wgpu_available}; #[derive(ExtractField)] pub struct PreferencesDialogMessageContext<'a> { @@ -34,6 +35,7 @@ impl MessageHandler {} } } @@ -57,7 +59,12 @@ impl PreferencesDialogMessageHandler { { let header = vec![TextLabel::new("Navigation").italic(true).widget_instance()]; - let zoom_rate_description = "Adjust how fast zooming occurs when using the scroll wheel or pinch gesture (relative to a default of 50)."; + let zoom_rate_description = " + Adjust how fast zooming occurs when using the scroll wheel or pinch gesture.\n\ + \n\ + *Default: 50.* + " + .trim(); let zoom_rate_label = vec![ Separator::new(SeparatorStyle::Unrelated).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), @@ -85,7 +92,12 @@ impl PreferencesDialogMessageHandler { ]; let checkbox_id = CheckboxId::new(); - let zoom_with_scroll_description = "Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads)."; + let zoom_with_scroll_description = " + Use the scroll wheel for zooming instead of vertically panning (not recommended for trackpads).\n\ + \n\ + *Default: Off.* + " + .trim(); let zoom_with_scroll = vec![ Separator::new(SeparatorStyle::Unrelated).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), @@ -116,12 +128,18 @@ impl PreferencesDialogMessageHandler { { let header = vec![TextLabel::new("Editing").italic(true).widget_instance()]; + let selection_label_description = " + Choose how targets are selected within dragged rectangular and lasso areas.\n\ + \n\ + *Default: Touched.* + " + .trim(); let selection_label = vec![ Separator::new(SeparatorStyle::Unrelated).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), TextLabel::new("Selection") .tooltip_label("Selection") - .tooltip_description("Choose how targets are selected within dragged rectangular and lasso areas.") + .tooltip_description(selection_label_description) .widget_instance(), ]; @@ -175,7 +193,12 @@ impl PreferencesDialogMessageHandler { { let header = vec![TextLabel::new("Interface").italic(true).widget_instance()]; - let scale_description = "Adjust the scale of the entire user interface (100% is default)."; + let scale_description = " + Adjust the scale of the entire user interface.\n\ + \n\ + *Default: 100%.* + " + .trim(); let scale_label = vec![ Separator::new(SeparatorStyle::Unrelated).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), @@ -215,7 +238,12 @@ impl PreferencesDialogMessageHandler { { let header = vec![TextLabel::new("Experimental").italic(true).widget_instance()]; - let node_graph_section_description = "Configure the appearance of the wires running between node connections in the graph."; + let node_graph_section_description = " + Configure the appearance of the wires running between node connections in the graph.\n\ + \n\ + *Default: Direct.* + " + .trim(); let node_graph_wires_label = vec![ Separator::new(SeparatorStyle::Unrelated).widget_instance(), Separator::new(SeparatorStyle::Unrelated).widget_instance(), @@ -244,39 +272,16 @@ impl PreferencesDialogMessageHandler { graph_wire_style, ]; - let checkbox_id = CheckboxId::new(); - let vello_description = "Use the experimental Vello renderer instead of SVG-based rendering.".to_string(); - #[cfg(target_family = "wasm")] - let mut vello_description = vello_description; - #[cfg(target_family = "wasm")] - vello_description.push_str("\n\n(Your browser must support WebGPU.)"); - - let use_vello = vec![ - Separator::new(SeparatorStyle::Unrelated).widget_instance(), - Separator::new(SeparatorStyle::Unrelated).widget_instance(), - CheckboxInput::new(preferences.use_vello && preferences.supports_wgpu()) - .tooltip_label("Vello Renderer") - .tooltip_description(vello_description.clone()) - .disabled(!preferences.supports_wgpu()) - .on_update(|checkbox_input: &CheckboxInput| PreferencesMessage::UseVello { use_vello: checkbox_input.checked }.into()) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Vello Renderer") - .tooltip_label("Vello Renderer") - .tooltip_description(vello_description) - .disabled(!preferences.supports_wgpu()) - .for_checkbox(checkbox_id) - .widget_instance(), - ]; - let checkbox_id = CheckboxId::new(); let brush_tool_description = " - Enable the Brush tool to support basic raster-based layer painting.\n\ - \n\ - This legacy experimental tool has performance and quality limitations and is slated for replacement in future versions of Graphite that will have a renewed focus on raster graphics editing.\n\ - \n\ - Content created with the Brush tool may not be compatible with future versions of Graphite. - " + Enable the Brush tool to support basic raster-based layer painting.\n\ + \n\ + This legacy experimental tool has performance and quality limitations and is slated for replacement in future versions of Graphite that will have a renewed focus on raster graphics editing.\n\ + \n\ + Content created with the Brush tool may not be compatible with future versions of Graphite.\n\ + \n\ + *Default: Off.* + " .trim(); let brush_tool = vec![ Separator::new(SeparatorStyle::Unrelated).widget_instance(), @@ -294,49 +299,126 @@ impl PreferencesDialogMessageHandler { .widget_instance(), ]; - rows.extend_from_slice(&[header, node_graph_wires_label, graph_wire_style, use_vello, brush_tool]); + rows.extend_from_slice(&[header, node_graph_wires_label, graph_wire_style, brush_tool]); } // ============= // COMPATIBILITY // ============= - #[cfg(not(target_family = "wasm"))] { - let header = vec![TextLabel::new("Compatibility").italic(true).widget_instance()]; - - let ui_acceleration_description = " - Use the CPU to draw the Graphite user interface (areas outside of the canvas) instead of the GPU. This does not affect the rendering of artwork in the canvas, which remains hardware accelerated.\n\ - \n\ - Disabling UI acceleration may slightly degrade performance, so this should be used as a workaround only if issues are observed with displaying the UI. This setting may become enabled automatically if Graphite launches, detects that it cannot draw the UI normally, and restarts in compatibility mode. - " - .trim(); + let wgpu_available = wgpu_available().unwrap_or(false); + let is_desktop = cfg!(not(target_family = "wasm")); + if wgpu_available || is_desktop { + let header = vec![TextLabel::new("Compatibility").italic(true).widget_instance()]; + rows.push(header); + } - let checkbox_id = CheckboxId::new(); - let ui_acceleration = vec![ - Separator::new(SeparatorStyle::Unrelated).widget_instance(), - Separator::new(SeparatorStyle::Unrelated).widget_instance(), - CheckboxInput::new(preferences.disable_ui_acceleration) - .tooltip_label("Disable UI Acceleration") - .tooltip_description(ui_acceleration_description) - .on_update(|number_input: &CheckboxInput| Message::Batched { - messages: Box::new([ - PreferencesDialogMessage::MayRequireRestart.into(), - PreferencesMessage::DisableUIAcceleration { - disable_ui_acceleration: number_input.checked, - } - .into(), - ]), - }) - .for_label(checkbox_id) - .widget_instance(), - TextLabel::new("Disable UI Acceleration") - .tooltip_label("Disable UI Acceleration") - .tooltip_description(ui_acceleration_description) - .for_checkbox(checkbox_id) - .widget_instance(), - ]; + if wgpu_available { + let vello_description = "Auto uses Vello renderer when GPU is available."; + let vello_renderer_label = vec![ + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + TextLabel::new("Vello Renderer") + .tooltip_label("Vello Renderer") + .tooltip_description(vello_description) + .widget_instance(), + ]; + let vello_preference = RadioInput::new(vec![ + RadioEntryData::new("Auto").label("Auto").on_update(move |_| { + PreferencesMessage::VelloPreference { + preference: graph_craft::wasm_application_io::VelloPreference::Auto, + } + .into() + }), + RadioEntryData::new("Disabled").label("Disabled").on_update(move |_| { + PreferencesMessage::VelloPreference { + preference: graph_craft::wasm_application_io::VelloPreference::Disabled, + } + .into() + }), + ]) + .selected_index(Some(preferences.vello_preference as u32)) + .widget_instance(); + let vello_preference = vec![ + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + vello_preference, + ]; + rows.extend_from_slice(&[vello_renderer_label, vello_preference]); + + let render_tile_resolution_description = " + Maximum X or Y resolution per render tile. Larger tiles may improve performance but can cause flickering or missing content in complex artwork if set too high.\n\ + \n\ + *Default: 1280 px.* + " + .trim(); + let render_tile_resolution_label = vec![ + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + TextLabel::new("Render Tile Resolution") + .tooltip_label("Render Tile Resolution") + .tooltip_description(render_tile_resolution_description) + .widget_instance(), + ]; + let render_tile_resolution = vec![ + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + NumberInput::new(Some(preferences.max_render_region_size as f64)) + .tooltip_label("Render Tile Resolution") + .tooltip_description(render_tile_resolution_description) + .mode_range() + .int() + .min(256.) + .max(4096.) + .increment_step(256.) + .unit(" px") + .on_update(|number_input: &NumberInput| { + let size = number_input.value.unwrap_or(EditorPreferences::default().max_render_region_size as f64) as u32; + PreferencesMessage::MaxRenderRegionSize { size }.into() + }) + .widget_instance(), + ]; + + rows.extend_from_slice(&[render_tile_resolution_label, render_tile_resolution]); + } - rows.extend_from_slice(&[header, ui_acceleration]); + if is_desktop { + let ui_acceleration_description = " + Use the CPU to draw the Graphite user interface (areas outside of the canvas) instead of the GPU. This does not affect the rendering of artwork in the canvas, which remains hardware accelerated.\n\ + \n\ + Disabling UI acceleration may slightly degrade performance, so this should be used as a workaround only if issues are observed with displaying the UI. This setting may become enabled automatically if Graphite launches, detects that it cannot draw the UI normally, and restarts in compatibility mode.\n\ + \n\ + *Default: Off.* + " + .trim(); + + let checkbox_id = CheckboxId::new(); + let ui_acceleration = vec![ + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + Separator::new(SeparatorStyle::Unrelated).widget_instance(), + CheckboxInput::new(preferences.disable_ui_acceleration) + .tooltip_label("Disable UI Acceleration") + .tooltip_description(ui_acceleration_description) + .on_update(|number_input: &CheckboxInput| Message::Batched { + messages: Box::new([ + PreferencesDialogMessage::MayRequireRestart.into(), + PreferencesMessage::DisableUIAcceleration { + disable_ui_acceleration: number_input.checked, + } + .into(), + ]), + }) + .for_label(checkbox_id) + .widget_instance(), + TextLabel::new("Disable UI Acceleration") + .tooltip_label("Disable UI Acceleration") + .tooltip_description(ui_acceleration_description) + .for_checkbox(checkbox_id) + .widget_instance(), + ]; + + rows.push(ui_acceleration); + } } Layout(rows.into_iter().map(|r| LayoutGroup::Row { widgets: r }).collect()) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 461e443a35..234c0f383a 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -92,7 +92,6 @@ pub struct DocumentMessageHandler { pub document_ptz: PTZ, /// The current mode that the user has set for rendering the document within the viewport. /// This is usually "Normal" but can be set to "Outline" or "Pixels" to see the canvas differently. - #[serde(alias = "view_mode")] pub render_mode: RenderMode, /// Sets whether or not all the viewport overlays should be drawn on top of the artwork. /// This includes tool interaction visualizations (like the transform cage and path anchors/handles), the grid, and more. diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 78cce0bfad..c9d5c57e68 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -1365,10 +1365,11 @@ impl MessageHandler> for Portfolio } } PortfolioMessage::UpdateVelloPreference => { - let active = if cfg!(target_family = "wasm") { false } else { preferences.use_vello }; + // TODO: Resend this message once the GPU context is initialized to avoid having the hole punch be stuck in an invalid state + let active = if cfg!(target_family = "wasm") { false } else { preferences.use_vello() }; responses.add(FrontendMessage::UpdateViewportHolePunch { active }); responses.add(NodeGraphMessage::RunDocumentGraph); - self.persistent_data.use_vello = preferences.use_vello; + self.persistent_data.use_vello = preferences.use_vello(); } } } diff --git a/editor/src/messages/preferences/preferences_message.rs b/editor/src/messages/preferences/preferences_message.rs index 2dcb0a80d9..ad9d570f6e 100644 --- a/editor/src/messages/preferences/preferences_message.rs +++ b/editor/src/messages/preferences/preferences_message.rs @@ -10,7 +10,7 @@ pub enum PreferencesMessage { ResetToDefaults, // Per-preference messages - UseVello { use_vello: bool }, + VelloPreference { preference: graph_craft::wasm_application_io::VelloPreference }, SelectionMode { selection_mode: SelectionMode }, BrushTool { enabled: bool }, ModifyLayout { zoom_with_scroll: bool }, @@ -18,4 +18,5 @@ pub enum PreferencesMessage { ViewportZoomWheelRate { rate: f64 }, UIScale { scale: f64 }, DisableUIAcceleration { disable_ui_acceleration: bool }, + MaxRenderRegionSize { size: u32 }, } diff --git a/editor/src/messages/preferences/preferences_message_handler.rs b/editor/src/messages/preferences/preferences_message_handler.rs index e143c03855..e11296acbb 100644 --- a/editor/src/messages/preferences/preferences_message_handler.rs +++ b/editor/src/messages/preferences/preferences_message_handler.rs @@ -5,6 +5,7 @@ use crate::messages::preferences::SelectionMode; use crate::messages::prelude::*; use crate::messages::tool::utility_types::ToolType; use graph_craft::wasm_application_io::EditorPreferences; +use graphene_std::application_io::GetEditorPreferences; #[derive(ExtractField)] pub struct PreferencesMessageContext<'a> { @@ -16,12 +17,13 @@ pub struct PreferencesMessageContext<'a> { pub struct PreferencesMessageHandler { pub selection_mode: SelectionMode, pub zoom_with_scroll: bool, - pub use_vello: bool, + pub vello_preference: graph_craft::wasm_application_io::VelloPreference, pub brush_tool: bool, pub graph_wire_style: GraphWireStyle, pub viewport_zoom_wheel_rate: f64, pub ui_scale: f64, pub disable_ui_acceleration: bool, + pub max_render_region_size: u32, } impl PreferencesMessageHandler { @@ -35,13 +37,18 @@ impl PreferencesMessageHandler { pub fn editor_preferences(&self) -> EditorPreferences { EditorPreferences { - use_vello: self.use_vello && self.supports_wgpu(), + vello_preference: self.vello_preference, + max_render_region_size: self.max_render_region_size, } } pub fn supports_wgpu(&self) -> bool { graph_craft::wasm_application_io::wgpu_available().unwrap_or_default() } + + pub fn use_vello(&self) -> bool { + self.editor_preferences().use_vello() + } } impl Default for PreferencesMessageHandler { @@ -49,12 +56,13 @@ impl Default for PreferencesMessageHandler { Self { selection_mode: SelectionMode::Touched, zoom_with_scroll: matches!(MappingVariant::default(), MappingVariant::ZoomWithScroll), - use_vello: EditorPreferences::default().use_vello, + vello_preference: EditorPreferences::default().vello_preference, brush_tool: false, graph_wire_style: GraphWireStyle::default(), viewport_zoom_wheel_rate: VIEWPORT_ZOOM_WHEEL_RATE, ui_scale: UI_SCALE_DEFAULT, disable_ui_acceleration: false, + max_render_region_size: EditorPreferences::default().max_render_region_size, } } } @@ -82,10 +90,11 @@ impl MessageHandler> for Prefe } // Per-preference messages - PreferencesMessage::UseVello { use_vello } => { - self.use_vello = use_vello; + PreferencesMessage::VelloPreference { preference } => { + self.vello_preference = preference; responses.add(PortfolioMessage::UpdateVelloPreference); responses.add(PortfolioMessage::EditorPreferences); + responses.add(PreferencesDialogMessage::Update); } PreferencesMessage::BrushTool { enabled } => { self.brush_tool = enabled; @@ -120,6 +129,11 @@ impl MessageHandler> for Prefe PreferencesMessage::DisableUIAcceleration { disable_ui_acceleration } => { self.disable_ui_acceleration = disable_ui_acceleration; } + PreferencesMessage::MaxRenderRegionSize { size } => { + self.max_render_region_size = size; + responses.add(PortfolioMessage::UpdateVelloPreference); + responses.add(PortfolioMessage::EditorPreferences); + } } responses.add(FrontendMessage::TriggerSavePreferences { preferences: self.clone() }); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 9b86f28f84..e2a7be830b 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -370,6 +370,8 @@ impl NodeRuntime { executor.context.queue.submit([encoder.finish()]); surface_texture.present(); + // TODO: Figure out if we can explicityl destroy the wgpu texture here to reduce the allocation pressure. We might also be able to use a texture allocation pool + let frame = graphene_std::application_io::SurfaceFrame { surface_id: surface.window_id, resolution: logical_resolution, diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 71661bca2d..da92c228ef 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -202,20 +202,20 @@ impl EditorHandle { if !EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { handle(|handle| { // Process all messages that have been queued up - let messages = MESSAGE_BUFFER.take(); - - for message in messages { - handle.dispatch(message); - } - - handle.dispatch(InputPreprocessorMessage::CurrentTime { - timestamp: js_sys::Date::now() as u64, - }); - handle.dispatch(AnimationMessage::IncrementFrameCounter); + let mut messages = MESSAGE_BUFFER.take(); + messages.push( + InputPreprocessorMessage::CurrentTime { + timestamp: js_sys::Date::now() as u64, + } + .into(), + ); + messages.push(AnimationMessage::IncrementFrameCounter.into()); // Used by auto-panning, but this could possibly be refactored in the future, see: // - handle.dispatch(BroadcastMessage::TriggerEvent(EventMessage::AnimationFrame)); + messages.push(BroadcastMessage::TriggerEvent(EventMessage::AnimationFrame).into()); + + handle.dispatch(Message::Batched { messages: messages.into() }); }); } diff --git a/node-graph/graph-craft/src/wasm_application_io.rs b/node-graph/graph-craft/src/wasm_application_io.rs index 9a956d52b6..6ca9ffa065 100644 --- a/node-graph/graph-craft/src/wasm_application_io.rs +++ b/node-graph/graph-craft/src/wasm_application_io.rs @@ -69,6 +69,10 @@ pub struct WasmApplicationIo { static WGPU_AVAILABLE: std::sync::atomic::AtomicI8 = std::sync::atomic::AtomicI8::new(-1); +/// Returns: +/// - `None` if the availability of WGPU has not been determined yet +/// - `Some(true)` if WGPU is available +/// - `Some(false)` if WGPU is not available pub fn wgpu_available() -> Option { match WGPU_AVAILABLE.load(Ordering::SeqCst) { -1 => None, @@ -332,24 +336,38 @@ pub type WasmSurfaceHandle = SurfaceHandle; #[cfg(feature = "wgpu")] pub type WasmSurfaceHandleFrame = graphene_application_io::SurfaceHandleFrame; +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, specta::Type, serde::Serialize, serde::Deserialize)] +pub enum VelloPreference { + Auto, + Disabled, +} + #[derive(Clone, Debug, PartialEq, Hash, specta::Type, serde::Serialize, serde::Deserialize)] pub struct EditorPreferences { - pub use_vello: bool, + pub vello_preference: VelloPreference, + /// Maximum render region size in pixels along one dimension of the square area. + pub max_render_region_size: u32, } impl graphene_application_io::GetEditorPreferences for EditorPreferences { fn use_vello(&self) -> bool { - self.use_vello + match self.vello_preference { + VelloPreference::Auto => wgpu_available().unwrap_or(false), + VelloPreference::Disabled => false, + } + } + + fn max_render_region_area(&self) -> u32 { + let size = self.max_render_region_size.min(u32::MAX.isqrt()); + size.pow(2) } } impl Default for EditorPreferences { fn default() -> Self { Self { - #[cfg(target_family = "wasm")] - use_vello: false, - #[cfg(not(target_family = "wasm"))] - use_vello: true, + vello_preference: VelloPreference::Auto, + max_render_region_size: 1280, } } } diff --git a/node-graph/graphene-cli/src/main.rs b/node-graph/graphene-cli/src/main.rs index 41f4e15515..1bf96b458e 100644 --- a/node-graph/graphene-cli/src/main.rs +++ b/node-graph/graphene-cli/src/main.rs @@ -125,7 +125,10 @@ async fn main() -> Result<(), Box> { let wgpu_executor_ref = application_io_arc.gpu_executor().unwrap(); let device = wgpu_executor_ref.context.device.clone(); - let preferences = EditorPreferences { use_vello: true }; + let preferences = EditorPreferences { + vello_preference: graph_craft::wasm_application_io::VelloPreference::Auto, + max_render_region_size: EditorPreferences::default().max_render_region_size, + }; let editor_api = Arc::new(WasmEditorApi { font_cache: FontCache::default(), application_io: Some(application_io_for_api), diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 8dadda45de..e1c87d9ba3 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -141,6 +141,7 @@ fn node_registry() -> HashMap, input: Context, fn_params: [Context => Arc, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate, Context => graphene_std::ContextFeatures]), + async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderOutput, 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, Context => graphene_std::ContextFeatures]), async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => WindowHandle, Context => graphene_std::ContextFeatures]), diff --git a/node-graph/interpreted-executor/src/util.rs b/node-graph/interpreted-executor/src/util.rs index 562e70456a..8ecf5bf18e 100644 --- a/node-graph/interpreted-executor/src/util.rs +++ b/node-graph/interpreted-executor/src/util.rs @@ -28,7 +28,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc NodeGraphUpdateSender for std::sync::Mutex { pub trait GetEditorPreferences { fn use_vello(&self) -> bool; + fn max_render_region_area(&self) -> u32; } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] @@ -261,6 +262,10 @@ impl GetEditorPreferences for DummyPreferences { fn use_vello(&self) -> bool { false } + + fn max_render_region_area(&self) -> u32 { + 1024 * 1024 + } } pub struct EditorApi { diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 74372383ac..750fe5e9db 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -280,6 +280,16 @@ impl RenderMetadata { value.transform = transform * value.transform; } } + + /// Merge another RenderMetadata into this one. + /// Values from `other` take precedence for duplicate keys. + pub fn merge(&mut self, other: &RenderMetadata) { + self.upstream_footprints.extend(other.upstream_footprints.iter().map(|(k, v)| (*k, *v))); + self.local_transforms.extend(other.local_transforms.iter().map(|(k, v)| (*k, *v))); + self.first_element_source_id.extend(other.first_element_source_id.iter().map(|(k, v)| (*k, *v))); + self.click_targets.extend(other.click_targets.iter().map(|(k, v)| (*k, v.clone()))); + self.clip_targets.extend(other.clip_targets.iter().copied()); + } } // TODO: Rename to "Graphical" diff --git a/node-graph/nodes/gstd/Cargo.toml b/node-graph/nodes/gstd/Cargo.toml index a2fd8ccb50..012c8417e3 100644 --- a/node-graph/nodes/gstd/Cargo.toml +++ b/node-graph/nodes/gstd/Cargo.toml @@ -51,6 +51,7 @@ node-macro = { workspace = true } reqwest = { workspace = true } image = { workspace = true } base64 = { workspace = true } +wgpu = { workspace = true } # Optional workspace dependencies wasm-bindgen = { workspace = true, optional = true } diff --git a/node-graph/nodes/gstd/src/lib.rs b/node-graph/nodes/gstd/src/lib.rs index 66b496a2f3..dd203c976e 100644 --- a/node-graph/nodes/gstd/src/lib.rs +++ b/node-graph/nodes/gstd/src/lib.rs @@ -1,4 +1,5 @@ pub mod any; +pub mod render_cache; pub mod render_node; pub mod text; #[cfg(feature = "wasm")] diff --git a/node-graph/nodes/gstd/src/render_cache.rs b/node-graph/nodes/gstd/src/render_cache.rs new file mode 100644 index 0000000000..335285befc --- /dev/null +++ b/node-graph/nodes/gstd/src/render_cache.rs @@ -0,0 +1,581 @@ +//! Tile-based render caching for efficient viewport panning. + +use core_types::math::bbox::AxisAlignedBbox; +use core_types::transform::{Footprint, RenderQuality, Transform}; +use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl}; +use glam::{DVec2, IVec2, UVec2}; +use graph_craft::document::value::RenderOutput; +use graph_craft::wasm_application_io::WasmEditorApi; +use graphene_application_io::{ApplicationIo, ImageTexture}; +use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams}; +use std::collections::HashSet; +use std::hash::Hash; +use std::sync::{Arc, Mutex}; + +use crate::render_node::RenderOutputType; + +pub const TILE_SIZE: u32 = 256; +pub const MAX_CACHE_MEMORY_BYTES: usize = 512 * 1024 * 1024; +const BYTES_PER_PIXEL: usize = 4; + +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub struct TileCoord { + pub x: i32, + pub y: i32, +} + +#[derive(Debug, Clone)] +pub struct CachedRegion { + pub texture: wgpu::Texture, + pub texture_size: UVec2, + pub scene_bounds: AxisAlignedBbox, + pub tiles: Vec, + pub metadata: rendering::RenderMetadata, + last_access: u64, + memory_size: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CacheKey { + pub render_mode_hash: u64, + pub hide_artboards: bool, + pub for_export: bool, + pub for_mask: bool, + pub thumbnail: bool, + pub aligned_strokes: bool, + pub override_paint_order: bool, + pub animation_time_ms: i64, + pub real_time_ms: i64, + pub pointer: [u8; 16], +} + +impl CacheKey { + pub fn new( + render_mode_hash: u64, + hide_artboards: bool, + for_export: bool, + for_mask: bool, + thumbnail: bool, + aligned_strokes: bool, + override_paint_order: bool, + animation_time: f64, + real_time: f64, + pointer: Option, + ) -> Self { + let pointer_bytes = pointer + .map(|p| { + let mut bytes = [0u8; 16]; + bytes[..8].copy_from_slice(&p.x.to_le_bytes()); + bytes[8..].copy_from_slice(&p.y.to_le_bytes()); + bytes + }) + .unwrap_or([0u8; 16]); + Self { + render_mode_hash, + hide_artboards, + for_export, + for_mask, + thumbnail, + aligned_strokes, + override_paint_order, + animation_time_ms: (animation_time * 1000.0).round() as i64, + real_time_ms: (real_time * 1000.0).round() as i64, + pointer: pointer_bytes, + } + } +} + +impl Default for CacheKey { + fn default() -> Self { + Self { + render_mode_hash: 0, + hide_artboards: false, + for_export: false, + for_mask: false, + thumbnail: false, + aligned_strokes: false, + override_paint_order: false, + animation_time_ms: 0, + real_time_ms: 0, + pointer: [0u8; 16], + } + } +} + +#[derive(Debug)] +struct TileCacheImpl { + regions: Vec, + timestamp: u64, + total_memory: usize, + cache_key: CacheKey, + current_scale: f64, +} + +impl Default for TileCacheImpl { + fn default() -> Self { + Self { + regions: Vec::new(), + timestamp: 0, + total_memory: 0, + cache_key: CacheKey::default(), + current_scale: 0.0, + } + } +} + +#[derive(Clone, Default, dyn_any::DynAny, Debug)] +pub struct TileCache(Arc>); + +#[derive(Debug, Clone)] +pub struct RenderRegion { + pub scene_bounds: AxisAlignedBbox, + pub tiles: Vec, + pub scale: f64, +} + +#[derive(Debug)] +pub struct CacheQuery { + pub cached_regions: Vec, + pub missing_regions: Vec, +} + +fn scene_bounds_to_tiles(bounds: &AxisAlignedBbox, scale: f64) -> Vec { + let pixel_start = bounds.start * scale; + let pixel_end = bounds.end * scale; + let tile_start_x = (pixel_start.x / TILE_SIZE as f64).floor() as i32; + let tile_start_y = (pixel_start.y / TILE_SIZE as f64).floor() as i32; + let tile_end_x = (pixel_end.x / TILE_SIZE as f64).ceil() as i32; + let tile_end_y = (pixel_end.y / TILE_SIZE as f64).ceil() as i32; + + let mut tiles = Vec::new(); + for y in tile_start_y..tile_end_y { + for x in tile_start_x..tile_end_x { + tiles.push(TileCoord { x, y }); + } + } + tiles +} + +fn tile_scene_start(tile: &TileCoord, scale: f64) -> DVec2 { + DVec2::new(tile.x as f64, tile.y as f64) * (TILE_SIZE as f64 / scale) +} + +fn tile_to_scene_bounds(coord: &TileCoord, scale: f64) -> AxisAlignedBbox { + let tile_scene_size = TILE_SIZE as f64 / scale; + let start = tile_scene_start(coord, scale); + AxisAlignedBbox { + start, + end: start + DVec2::splat(tile_scene_size), + } +} + +fn tiles_to_scene_bounds(tiles: &[TileCoord], scale: f64) -> AxisAlignedBbox { + if tiles.is_empty() { + return AxisAlignedBbox::ZERO; + } + let mut result = tile_to_scene_bounds(&tiles[0], scale); + for tile in &tiles[1..] { + result = result.union(&tile_to_scene_bounds(tile, scale)); + } + result +} + +impl TileCacheImpl { + fn query(&mut self, viewport_bounds: &AxisAlignedBbox, scale: f64, cache_key: &CacheKey, max_region_area: u32) -> CacheQuery { + if &self.cache_key != cache_key || (self.current_scale - scale).abs() > 0.001 { + self.invalidate_all(); + self.cache_key = cache_key.clone(); + self.current_scale = scale; + } + + let required_tiles = scene_bounds_to_tiles(viewport_bounds, scale); + let required_tile_set: HashSet<_> = required_tiles.iter().cloned().collect(); + let mut cached_regions = Vec::new(); + let mut covered_tiles = HashSet::new(); + + for region in &mut self.regions { + let region_tiles: HashSet<_> = region.tiles.iter().cloned().collect(); + if region_tiles.iter().any(|t| required_tile_set.contains(t)) { + region.last_access = self.timestamp; + self.timestamp += 1; + cached_regions.push(region.clone()); + covered_tiles.extend(region_tiles); + } + } + + let missing_tiles: Vec<_> = required_tiles.into_iter().filter(|t| !covered_tiles.contains(t)).collect(); + let missing_regions = group_into_regions(&missing_tiles, scale, max_region_area); + CacheQuery { cached_regions, missing_regions } + } + + fn store_regions(&mut self, new_regions: Vec) { + for mut region in new_regions { + region.last_access = self.timestamp; + self.timestamp += 1; + self.total_memory += region.memory_size; + self.regions.push(region); + } + self.evict_until_under_budget(); + } + + fn evict_until_under_budget(&mut self) { + while self.total_memory > MAX_CACHE_MEMORY_BYTES && !self.regions.is_empty() { + if let Some((oldest_idx, _)) = self.regions.iter().enumerate().min_by_key(|(_, r)| r.last_access) { + let removed = self.regions.remove(oldest_idx); + removed.texture.destroy(); + self.total_memory = self.total_memory.saturating_sub(removed.memory_size); + } else { + break; + } + } + } + + fn invalidate_all(&mut self) { + for region in &self.regions { + region.texture.destroy(); + } + self.regions.clear(); + self.total_memory = 0; + } +} + +impl TileCache { + pub fn query(&self, viewport_bounds: &AxisAlignedBbox, scale: f64, cache_key: &CacheKey, max_region_area: u32) -> CacheQuery { + self.0.lock().unwrap().query(viewport_bounds, scale, cache_key, max_region_area) + } + + pub fn store_regions(&self, regions: Vec) { + self.0.lock().unwrap().store_regions(regions); + } +} + +fn group_into_regions(tiles: &[TileCoord], scale: f64, max_region_area: u32) -> Vec { + if tiles.is_empty() { + return Vec::new(); + } + + let tile_set: HashSet<_> = tiles.iter().cloned().collect(); + let mut visited = HashSet::new(); + let mut regions = Vec::new(); + + for &tile in tiles { + if visited.contains(&tile) { + continue; + } + let region_tiles = flood_fill(&tile, &tile_set, &mut visited); + let scene_bounds = tiles_to_scene_bounds(®ion_tiles, scale); + let region = RenderRegion { + scene_bounds, + tiles: region_tiles, + scale, + }; + regions.extend(split_oversized_region(region, scale, max_region_area)); + } + regions +} + +/// Recursively subdivides a region until all sub-regions have area <= max_region_area. +/// Uses axis-aligned splits on the longest dimension. +fn split_oversized_region(region: RenderRegion, scale: f64, max_region_area: u32) -> Vec { + let pixel_size = region.scene_bounds.size() * scale; + let area = (pixel_size.x * pixel_size.y) as u32; + + // Base case: region is small enough + if area <= max_region_area { + return vec![region]; + } + + // Determine split axis: choose the longer dimension + let split_horizontally = pixel_size.x > pixel_size.y; + + // Split tiles into two groups based on midpoint + let mut group1 = Vec::new(); + let mut group2 = Vec::new(); + + if split_horizontally { + // Find midpoint X in tile coordinates + let min_x = region.tiles.iter().map(|t| t.x).min().unwrap(); + let max_x = region.tiles.iter().map(|t| t.x).max().unwrap(); + let mid_x = (min_x + max_x) / 2; + + for &tile in ®ion.tiles { + if tile.x <= mid_x { + group1.push(tile); + } else { + group2.push(tile); + } + } + } else { + // Split vertically - find midpoint Y + let min_y = region.tiles.iter().map(|t| t.y).min().unwrap(); + let max_y = region.tiles.iter().map(|t| t.y).max().unwrap(); + let mid_y = (min_y + max_y) / 2; + + for &tile in ®ion.tiles { + if tile.y <= mid_y { + group1.push(tile); + } else { + group2.push(tile); + } + } + } + + // Edge case: if split produces empty group, return as-is (can't split further) + if group1.is_empty() || group2.is_empty() { + return vec![region]; + } + + // Create sub-regions and recursively subdivide + let mut result = Vec::new(); + for tiles in [group1, group2] { + if !tiles.is_empty() { + let sub_region = RenderRegion { + scene_bounds: tiles_to_scene_bounds(&tiles, scale), + tiles, + scale, + }; + result.extend(split_oversized_region(sub_region, scale, max_region_area)); + } + } + + result +} + +fn flood_fill(start: &TileCoord, tile_set: &HashSet, visited: &mut HashSet) -> Vec { + let mut result = Vec::new(); + let mut stack = vec![*start]; + + while let Some(current) = stack.pop() { + if visited.contains(¤t) || !tile_set.contains(¤t) { + continue; + } + visited.insert(current); + result.push(current); + + for neighbor in [ + TileCoord { x: current.x - 1, y: current.y }, + TileCoord { x: current.x + 1, y: current.y }, + TileCoord { x: current.x, y: current.y - 1 }, + TileCoord { x: current.x, y: current.y + 1 }, + ] { + if tile_set.contains(&neighbor) && !visited.contains(&neighbor) { + stack.push(neighbor); + } + } + } + result +} + +#[node_macro::node(category(""))] +pub async fn render_output_cache<'a: 'n>( + ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync, + editor_api: &'a WasmEditorApi, + data: impl Node, Output = RenderOutput> + Send + Sync, + #[data] tile_cache: TileCache, +) -> RenderOutput { + let footprint = ctx.footprint(); + let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::()) else { + log::warn!("render_output_cache: missing or invalid render params, falling back to direct render"); + let context = OwnedContextImpl::empty().with_footprint(*footprint); + return data.eval(context.into_context()).await; + }; + + // Fall back to direct render for non-Vello or zero-size viewports + let physical_resolution = footprint.resolution; + if !matches!(render_params.render_output_type, RenderOutputTypeRequest::Vello) || physical_resolution.x == 0 || physical_resolution.y == 0 { + let context = OwnedContextImpl::empty().with_footprint(*footprint).with_vararg(Box::new(render_params.clone())); + return data.eval(context.into_context()).await; + } + + let logical_scale = footprint.decompose_scale().x; + let device_scale = render_params.scale; + let physical_scale = logical_scale * device_scale; + let viewport_bounds = footprint.viewport_bounds_in_local_space(); + + let cache_key = CacheKey::new( + render_params.render_mode as u64, + render_params.hide_artboards, + render_params.for_export, + render_params.for_mask, + render_params.thumbnail, + render_params.aligned_strokes, + render_params.override_paint_order, + ctx.try_animation_time().unwrap_or(0.0), + ctx.try_real_time().unwrap_or(0.0), + ctx.try_pointer_position(), + ); + + let max_region_area = editor_api.editor_preferences.max_render_region_area(); + let cache_query = tile_cache.query(&viewport_bounds, logical_scale, &cache_key, max_region_area); + + let mut new_regions = Vec::new(); + for missing_region in &cache_query.missing_regions { + if missing_region.tiles.is_empty() { + continue; + } + let region = render_missing_region(missing_region, |ctx| data.eval(ctx), ctx.clone(), render_params, logical_scale, device_scale).await; + new_regions.push(region); + } + + tile_cache.store_regions(new_regions.clone()); + + let all_regions: Vec<_> = cache_query.cached_regions.into_iter().chain(new_regions.into_iter()).collect(); + + // If no regions, fall back to direct render + if all_regions.is_empty() { + let context = OwnedContextImpl::empty().with_footprint(*footprint).with_vararg(Box::new(render_params.clone())); + return data.eval(context.into_context()).await; + } + + let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap(); + let (output_texture, combined_metadata) = composite_cached_regions(&all_regions, &viewport_bounds, physical_resolution, logical_scale, physical_scale, exec); + + RenderOutput { + data: RenderOutputType::Texture(ImageTexture { texture: output_texture }), + metadata: combined_metadata, + } +} + +async fn render_missing_region( + region: &RenderRegion, + render_fn: F, + ctx: impl Ctx + ExtractAll + CloneVarArgs, + render_params: &RenderParams, + logical_scale: f64, + device_scale: f64, +) -> CachedRegion +where + F: Fn(Context<'static>) -> Fut, + Fut: std::future::Future, +{ + let min_tile = region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y))); + let max_tile = region.tiles.iter().fold(IVec2::new(i32::MIN, i32::MIN), |acc, t| acc.max(IVec2::new(t.x, t.y))); + + let tile_scene_size = TILE_SIZE as f64 / logical_scale; + let region_scene_start = DVec2::new(min_tile.x as f64 * tile_scene_size, min_tile.y as f64 * tile_scene_size); + + // Calculate pixel size from tile boundaries to avoid rounding gaps + // Use round() on boundaries to ensure adjacent tiles share the same edge + let pixel_start = (min_tile.as_dvec2() * TILE_SIZE as f64 * device_scale).round().as_ivec2(); + let pixel_end = ((max_tile + IVec2::ONE).as_dvec2() * TILE_SIZE as f64 * device_scale).round().as_ivec2(); + let region_pixel_size = (pixel_end - pixel_start).max(IVec2::ONE).as_uvec2(); + + let region_transform = glam::DAffine2::from_scale(DVec2::splat(logical_scale)) * glam::DAffine2::from_translation(-region_scene_start); + let region_footprint = Footprint { + transform: region_transform, + resolution: region_pixel_size, + quality: RenderQuality::Full, + }; + + let region_params = render_params.clone(); + let region_ctx = OwnedContextImpl::from(ctx).with_footprint(region_footprint).with_vararg(Box::new(region_params)).into_context(); + let mut result = render_fn(region_ctx).await; + + let RenderOutputType::Texture(rendered_texture) = result.data else { + unreachable!("render_missing_region: expected texture output from Vello render"); + }; + + // Transform metadata from region pixel space to document space + let pixel_to_document = glam::DAffine2::from_translation(region_scene_start) * glam::DAffine2::from_scale(DVec2::splat(1.0 / logical_scale)); + result.metadata.apply_transform(pixel_to_document); + + let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL; + + CachedRegion { + texture: rendered_texture.texture, + texture_size: region_pixel_size, + scene_bounds: region.scene_bounds.clone(), + tiles: region.tiles.clone(), + metadata: result.metadata, + last_access: 0, + memory_size, + } +} + +fn composite_cached_regions( + regions: &[CachedRegion], + viewport_bounds: &AxisAlignedBbox, + output_resolution: UVec2, + logical_scale: f64, + physical_scale: f64, + exec: &wgpu_executor::WgpuExecutor, +) -> (wgpu::Texture, rendering::RenderMetadata) { + let device = &exec.context.device; + let queue = &exec.context.queue; + + // TODO: Use texture pool to reuse existing unused textures instead of allocating fresh ones every time + let output_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("viewport_output"), + size: wgpu::Extent3d { + width: output_resolution.x, + height: output_resolution.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("composite") }); + let mut combined_metadata = rendering::RenderMetadata::default(); + + // Calculate viewport pixel offset using round() to match region boundary calculations + let device_scale = physical_scale / logical_scale; + let viewport_pixel_start = (viewport_bounds.start * physical_scale).round().as_ivec2(); + + for region in regions { + let min_tile = region.tiles.iter().fold(IVec2::new(i32::MAX, i32::MAX), |acc, t| acc.min(IVec2::new(t.x, t.y))); + + // Use round() on tile boundaries to match render_missing_region calculation + let region_pixel_start = (min_tile.as_dvec2() * TILE_SIZE as f64 * device_scale).round().as_ivec2(); + let offset_pixels = region_pixel_start - viewport_pixel_start; + + let (src_x, dst_x, width) = if offset_pixels.x >= 0 { + (0, offset_pixels.x as u32, region.texture_size.x.min(output_resolution.x.saturating_sub(offset_pixels.x as u32))) + } else { + let skip = (-offset_pixels.x) as u32; + (skip, 0, region.texture_size.x.saturating_sub(skip).min(output_resolution.x)) + }; + + let (src_y, dst_y, height) = if offset_pixels.y >= 0 { + (0, offset_pixels.y as u32, region.texture_size.y.min(output_resolution.y.saturating_sub(offset_pixels.y as u32))) + } else { + let skip = (-offset_pixels.y) as u32; + (skip, 0, region.texture_size.y.saturating_sub(skip).min(output_resolution.y)) + }; + + if width > 0 && height > 0 { + encoder.copy_texture_to_texture( + wgpu::TexelCopyTextureInfo { + texture: ®ion.texture, + mip_level: 0, + origin: wgpu::Origin3d { x: src_x, y: src_y, z: 0 }, + aspect: wgpu::TextureAspect::All, + }, + wgpu::TexelCopyTextureInfo { + texture: &output_texture, + mip_level: 0, + origin: wgpu::Origin3d { x: dst_x, y: dst_y, z: 0 }, + aspect: wgpu::TextureAspect::All, + }, + wgpu::Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + ); + } + + // Transform metadata from document space to viewport logical pixels + let mut region_metadata = region.metadata.clone(); + let document_to_viewport = glam::DAffine2::from_scale(DVec2::splat(logical_scale)) * glam::DAffine2::from_translation(-viewport_bounds.start); + region_metadata.apply_transform(document_to_viewport); + combined_metadata.merge(®ion_metadata); + } + + queue.submit([encoder.finish()]); + (output_texture, combined_metadata) +} diff --git a/node-graph/nodes/gstd/src/render_node.rs b/node-graph/nodes/gstd/src/render_node.rs index 01972d040c..735cb4af4a 100644 --- a/node-graph/nodes/gstd/src/render_node.rs +++ b/node-graph/nodes/gstd/src/render_node.rs @@ -18,6 +18,9 @@ use std::sync::Arc; use vector_types::GradientStops; use wgpu_executor::RenderContext; +// Re-export render_output_cache from render_cache module +pub use crate::render_cache::render_output_cache; + /// List of (canvas id, image data) pairs for embedding images as canvases in the final SVG string. type ImageData = HashMap, u64>; @@ -28,9 +31,9 @@ pub enum RenderIntermediateType { } #[derive(Clone, dyn_any::DynAny)] pub struct RenderIntermediate { - ty: RenderIntermediateType, - metadata: RenderMetadata, - contains_artboard: bool, + pub(crate) ty: RenderIntermediateType, + pub(crate) metadata: RenderMetadata, + pub(crate) contains_artboard: bool, } #[node_macro::node(category(""))]