Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*;
use graphene_std::Color;
use graphene_std::vector::style::FillChoice;
use graphene_std::vector::style::{FillChoice, StrokeCap};

#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -68,7 +68,7 @@ impl ToolColorOptions {

pub fn apply_stroke(&self, weight: f64, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
if let Some(color) = self.active_color() {
let stroke = graphene_std::vector::style::Stroke::new(Some(color.to_gamma_srgb()), weight);
let stroke = graphene_std::vector::style::Stroke::new(Some(color.to_gamma_srgb()), weight).with_stroke_cap(StrokeCap::Round);
responses.add(GraphOperationMessage::StrokeSet { layer, stroke });
}
}
Expand Down
24 changes: 19 additions & 5 deletions editor/src/node_graph_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ impl NodeGraphExecutor {
Ok(instrumented)
}

/// Update the cached network if necessary.
fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, node_to_inspect: Option<NodeId>, ignore_hash: bool) -> Result<(), String> {
/// Update the cached network if necessary. Returns the current document network hash (same value used for cache sync).
fn update_node_graph(&mut self, document: &mut DocumentMessageHandler, node_to_inspect: Option<NodeId>, ignore_hash: bool) -> Result<u64, String> {
let network_hash = document.network_interface.network_hash();
// Refresh the graph when it changes or the inspect node changes
if network_hash != self.node_graph_hash || self.previous_node_to_inspect != node_to_inspect || ignore_hash {
Expand All @@ -129,7 +129,7 @@ impl NodeGraphExecutor {
.map_err(|e| e.to_string())?;
}

Ok(())
Ok(network_hash)
}

/// Adds an evaluate request for whatever current network is cached.
Expand All @@ -141,6 +141,7 @@ impl NodeGraphExecutor {
viewport_scale: f64,
time: TimingInformation,
pointer: DVec2,
document_network_hash: u64,
) -> Result<Message, String> {
let viewport = Footprint {
transform: document.metadata().document_to_viewport,
Expand All @@ -157,6 +158,7 @@ impl NodeGraphExecutor {
hide_artboards: false,
for_export: false,
for_eyedropper: false,
document_network_hash,
};

// Execute the node graph
Expand All @@ -180,8 +182,16 @@ impl NodeGraphExecutor {
ignore_hash: bool,
pointer: DVec2,
) -> Result<Message, String> {
self.update_node_graph(document, node_to_inspect, ignore_hash)?;
self.submit_current_node_graph_evaluation(document, document_id, viewport_resolution, viewport_scale, time, pointer)
let document_network_hash = self.update_node_graph(document, node_to_inspect, ignore_hash)?;
self.submit_current_node_graph_evaluation(
document,
document_id,
viewport_resolution,
viewport_scale,
time,
pointer,
document_network_hash,
)
}

#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -212,6 +222,7 @@ impl NodeGraphExecutor {
#[cfg(target_family = "wasm")]
let render_mode = document.render_mode;

let document_network_hash = document.network_interface.network_hash();
let render_config = RenderConfig {
viewport,
scale: viewport_scale,
Expand All @@ -222,6 +233,7 @@ impl NodeGraphExecutor {
hide_artboards: false,
for_export: false,
for_eyedropper: true,
document_network_hash,
};

// Execute the node graph
Expand Down Expand Up @@ -260,6 +272,7 @@ impl NodeGraphExecutor {
..Default::default()
};

let document_network_hash = document.network_interface.network_hash();
let render_config = RenderConfig {
viewport,
scale: export_config.scale_factor,
Expand All @@ -270,6 +283,7 @@ impl NodeGraphExecutor {
hide_artboards: export_config.transparent_background,
for_export: true,
for_eyedropper: false,
document_network_hash,
};
export_config.size = resolution;

Expand Down
11 changes: 10 additions & 1 deletion editor/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,16 @@ impl EditorTestUtils {
Err(e) => return Err(format!("update_node_graph_instrumented failed\n\n{e}")),
};

if let Err(e) = exector.submit_current_node_graph_evaluation(document, document_id, UVec2::ONE, 1., Default::default(), DVec2::ZERO) {
let document_network_hash = document.network_interface.network_hash();
if let Err(e) = exector.submit_current_node_graph_evaluation(
document,
document_id,
UVec2::ONE,
1.,
Default::default(),
DVec2::ZERO,
document_network_hash,
) {
return Err(format!("submit_current_node_graph_evaluation failed\n\n{e}"));
}
runtime.run().await;
Expand Down
3 changes: 3 additions & 0 deletions node-graph/libraries/application-io/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ pub struct RenderConfig {
pub hide_artboards: bool,
pub for_export: bool,
pub for_eyedropper: bool,
/// Hash of the document network (inputs and topology). Drives viewport tile-cache invalidation when node data changes.
#[serde(default)]
pub document_network_hash: u64,
}

struct Logger;
Expand Down
6 changes: 6 additions & 0 deletions node-graph/libraries/rendering/src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ pub struct RenderParams {
pub hide_artboards: bool,
/// Are we exporting
pub for_export: bool,
/// Sampling pixels for the eyedropper (distinct from main viewport cache identity).
pub for_eyedropper: bool,
/// Are we generating a mask in this render pass? Used to see if fill should be multiplied with alpha.
pub for_mask: bool,
/// Are we generating a mask for alignment? Used to prevent unnecessary transforms in masks
Expand All @@ -181,6 +183,8 @@ pub struct RenderParams {
pub artboard_background: Option<Color>,
/// Viewport zoom level (document-space scale). Used to compute constant viewport-pixel stroke widths in Outline mode.
pub viewport_zoom: f64,
/// Matches [`graphene_application_io::RenderConfig::document_network_hash`]. When it changes, tile caches must drop stale pixels.
pub document_network_hash: u64,
}

impl Hash for RenderParams {
Expand All @@ -191,6 +195,7 @@ impl Hash for RenderParams {
self.thumbnail.hash(state);
self.hide_artboards.hash(state);
self.for_export.hash(state);
self.for_eyedropper.hash(state);
self.for_mask.hash(state);
if let Some(x) = self.alignment_parent_transform {
x.to_cols_array().iter().for_each(|x| x.to_bits().hash(state))
Expand All @@ -199,6 +204,7 @@ impl Hash for RenderParams {
self.override_paint_order.hash(state);
self.artboard_background.hash(state);
self.viewport_zoom.to_bits().hash(state);
self.document_network_hash.hash(state);
}
}

Expand Down
8 changes: 8 additions & 0 deletions node-graph/nodes/gstd/src/render_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ pub struct CacheKey {
pub rotation: u64,
pub hide_artboards: bool,
pub for_export: bool,
pub for_eyedropper: 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],
pub document_network_hash: u64,
}

impl CacheKey {
Expand All @@ -62,13 +64,15 @@ impl CacheKey {
rotation: f64,
hide_artboards: bool,
for_export: bool,
for_eyedropper: bool,
for_mask: bool,
thumbnail: bool,
aligned_strokes: bool,
override_paint_order: bool,
animation_time: f64,
real_time: f64,
pointer: Option<DVec2>,
document_network_hash: u64,
) -> Self {
let pointer_bytes = pointer
.map(|p| {
Expand All @@ -89,13 +93,15 @@ impl CacheKey {
rotation: quantized_rotation.to_bits(),
hide_artboards,
for_export,
for_eyedropper,
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,
document_network_hash,
}
}
}
Expand Down Expand Up @@ -413,13 +419,15 @@ pub async fn render_output_cache<'a: 'n>(
rotation,
render_params.hide_artboards,
render_params.for_export,
render_params.for_eyedropper,
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(),
render_params.document_network_hash,
);

let cache_query = tile_cache.query(&viewport_bounds_device, &cache_key, max_region_area);
Expand Down
2 changes: 2 additions & 0 deletions node-graph/nodes/gstd/src/render_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,12 @@ async fn create_context<'a: 'n>(
render_mode: render_config.render_mode,
hide_artboards: render_config.hide_artboards,
for_export: render_config.for_export,
for_eyedropper: render_config.for_eyedropper,
render_output_type,
footprint: Footprint::default(),
scale: render_config.scale,
viewport_zoom: footprint.decompose_scale().x,
document_network_hash: render_config.document_network_hash,
..Default::default()
};

Expand Down
8 changes: 8 additions & 0 deletions node-graph/nodes/raster/shaders/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ use std::path::PathBuf;
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::builder().filter_level(log::LevelFilter::Debug).init();

// This crate is often built for wasm32 via wasm-pack, which sets `CARGO_BUILD_TARGET`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Custom agent: PR title enforcement

PR title must start with an approved imperative verb from the Leading Verb Vocabulary; "Use" is not allowed. Rename the title to start with an approved verb such as "Make" or "Fix" (for example, "Make round stroke caps for tool strokes and fix viewport tile cache...").

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/nodes/raster/shaders/build.rs, line 8:

<comment>PR title must start with an approved imperative verb from the Leading Verb Vocabulary; "Use" is not allowed. Rename the title to start with an approved verb such as "Make" or "Fix" (for example, "Make round stroke caps for tool strokes and fix viewport tile cache...").</comment>

<file context>
@@ -5,6 +5,14 @@ use std::path::PathBuf;
 pub fn main() -> Result<(), Box<dyn std::error::Error>> {
 	env_logger::builder().filter_level(log::LevelFilter::Debug).init();
 
+	// This crate is often built for wasm32 via wasm-pack, which sets `CARGO_BUILD_TARGET`.
+	// cargo-gpu then spawns nested `cargo` to build `rustc_codegen_spirv` for the host; if that
+	// variable is inherited, the nested build targets wasm32 and fails with "cannot produce dylib".
</file context>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Veercodeprog Nice work on the PR title update—it now correctly uses an imperative verb.

I have a few technical suggestions to ensure the tile cache invalidation is as robust as possible and to clean up the new build logic:

1. Map missing flags in create_context

In node-graph/nodes/gstd/src/render_node.rs, you are correctly passing the document_network_hash from RenderConfig to RenderParams. However, there are several other flags in RenderConfig (like for_export and hide_artboards) that are currently being ignored and falling back to Default.

If these aren't mapped, the cache might not distinguish between a viewport render and an export render of the same network, which could lead to stale pixels if those modes have different rendering logic (e.g., background transparency).

// node-graph/nodes/gstd/src/render_node.rs

let render_params = RenderParams {
    footprint: Footprint::default(),
    scale: render_config.scale,
    viewport_zoom: footprint.decompose_scale().x,
    document_network_hash: render_config.document_network_hash,
    // Suggest adding these to ensure the cache key is fully unique:
    hide_artboards: render_config.hide_artboards,
    for_export: render_config.for_export,
    for_eyedropper: render_config.for_eyedropper,
    ..Default::default()
};

2. Environment modification in build.rs

The use of unsafe for std::env::remove_var in node-graph/nodes/raster/shaders/build.rs is appropriate here. Since Rust 1.81, environment mutation is marked unsafe because it's not thread-safe. Your justification—that the build script is single-threaded and needs to clear CARGO_BUILD_TARGET to prevent wasm-pack from breaking nested host builds—is correct and follows standard practice for this specific rust-gpu issue.

3. Verify network_hash coverage

The technical note mentions that changing stroke properties invalidates the cache. This works because the document_network_hash includes all node input values. Just double-check that the tools you've updated (freehand, spline, etc.) correctly trigger a network state change when their properties are tweaked via the UI, as the cache now relies entirely on that hash for its identity.

For more details on how we handle rendering state and caching, you can check the Graphite Wiki.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Veercodeprog The changes look solid and address the stale viewport tile issue effectively. I suggested the title change earlier because the project follows a convention requiring imperative verbs (like "Make" or "Fix") to keep the history clear and actionable.

Here are a few technical observations and suggestions based on your implementation:

1. Cache Invalidation Logic

Including the document_network_hash in the CacheKey and RenderParams is a robust way to ensure that any change to the node graph topology or inputs forces a re-render.

  • Performance Note: Since this hash covers the entire network, any change (even moving an unrelated node) will invalidate the entire viewport tile cache. This is correct for ensuring no stale pixels, but if you notice performance regressions during high-frequency interactions (like dragging a node), a future optimization could involve a more granular "content hash" that only tracks properties affecting the visual output. For now, this is a great fix for the correctness bug you identified.

2. Default Stroke Cap

Changing the default to StrokeCap::Round in apply_stroke is a good UX improvement for tools like the Freehand and Pen tools, as it makes paths feel "finished" immediately.

  • Consistency: This change correctly targets the common functionality used by most vector drawing tools in Graphite.

3. WASM Build Fix in build.rs

The addition of the unsafe block to remove CARGO_BUILD_TARGET in the shader build script is a smart fix.

  • Context: When building for WASM via wasm-pack, CARGO_BUILD_TARGET is set to wasm32-unknown-unknown. This environment variable is inherited by the nested cargo calls made by cargo-gpu to build the SPIR-V compiler for the host machine. Without removing it, the host build fails because it tries to compile the compiler itself for WASM.
  • Safety: Your use of unsafe with the "single-threaded" justification matches the existing patterns in the codebase for environment manipulation in build scripts.

4. Code Quality

  • Hashing: The manual Hash implementation for RenderParams correctly includes the new document_network_hash field, maintaining the invariant that equal parameters produce equal hashes.
  • Serialization: Adding #[serde(default)] to the document_network_hash in RenderConfig is a good practice for maintaining compatibility across different parts of the system.

One minor suggestion:
In editor/src/node_graph_executor.rs, you call document.network_interface.network_hash() three times across different rendering paths (viewport, eyedropper, export). While this is fine, if these branches were ever merged or shared logic, you might consider fetching it once at the start of the execution block to ensure consistency within a single render pass, though in its current form, it's perfectly safe.

Overall, the PR is in great shape.

Learn more about Graphite's rendering architecture
Cubic Settings

// cargo-gpu then spawns nested `cargo` to build `rustc_codegen_spirv` for the host; if that
// variable is inherited, the nested build targets wasm32 and fails with "cannot produce dylib".
// SAFETY: Build script is single-threaded.
unsafe {
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

this is n't necessary on your system,not sure how exactly you run the app in your system

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There is no circumstance in which you should be committing changes to a build script when developing an editor feature.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@Keavon
this is related to the issue I was referring to in #3939
when running the project locally, the CARGO_BUILD_TARGET environment variable seems to leak from somewhere, which causes cargo run to fail while compiling the web frontend. The failure happens in the raster-nodes-shaders crate’s build.rs

as a temporary workaround, clearing the CARGO_BUILD_TARGET environment variable inside build.rs before it starts cargo_gpu fixes the issue and allows cargo run to work on our system.

This particular change can be skipped when merging the PR:
unsafe {
std::env::remove_var("CARGO_BUILD_TARGET");
}

infact we can skip the changes made in [node-graph/nodes/raster/shaders/build.rs] for this pr

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

updating this change now

std::env::remove_var("CARGO_BUILD_TARGET");
}

// Skip building the shaders if they are provided externally
println!("cargo:rerun-if-env-changed=RASTER_NODES_SHADER_PATH");
if !std::env::var("RASTER_NODES_SHADER_PATH").unwrap_or_default().is_empty() {
Expand Down