Make the Brush node store its cache as internal #[data] state instead of a serialized node input (#4126)

* Make the Brush node store its cache as internal #[data] state instead of a serialized node input

* Remove BrushCacheImpl::unique_id
This commit is contained in:
Keavon Chambers
2026-05-07 15:34:21 -07:00
committed by GitHub
parent 525e49f7e9
commit 0834bff2da
7 changed files with 88 additions and 94 deletions

View File

@@ -195,6 +195,7 @@ async fn brush(
/// The list of brush stroke paths drawn by the Brush tool, with each including both its coordinates and styles.
trace: Table<BrushStroke>,
/// Internal cache data used to accelerate rendering of the brush content.
#[data]
cache: BrushCache,
) -> Table<Raster<CPU>> {
if background.is_empty() {
@@ -419,6 +420,7 @@ mod test {
async fn test_brush_output_size() {
let image = brush(
(),
&BrushCache::default(),
Table::new_from_element(Raster::new_cpu(Image::<Color>::default())),
Table::new_from_element(BrushStroke {
trace: vec![crate::brush_stroke::BrushInputSample { position: DVec2::ZERO }],
@@ -431,7 +433,6 @@ mod test {
blend_mode: BlendMode::Normal,
},
}),
BrushCache::default(),
)
.await;
assert_eq!(image.element(0).unwrap().width, 20);

View File

@@ -3,37 +3,22 @@ use crate::brush_stroke::BrushStyle;
use core_types::ATTR_TRANSFORM;
use core_types::graphene_hash::CacheHashWrapper;
use core_types::table::TableRow;
use dyn_any::DynAny;
use raster_types::CPU;
use raster_types::Raster;
use std::collections::HashMap;
use std::hash::Hash;
use std::hash::Hasher;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
// TODO: This is a temporary hack, be sure to not reuse this when the brush system is replaced/rewritten.
static NEXT_BRUSH_CACHE_IMPL_ID: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, Default)]
struct BrushCacheImpl {
#[cfg_attr(feature = "serde", serde(default = "new_unique_id"))]
unique_id: u64,
// The full previous input that was cached.
#[cfg_attr(feature = "serde", serde(default))]
prev_input: Vec<BrushStroke>,
// The strokes that have been fully processed and blended into the background.
#[cfg_attr(feature = "serde", serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row"))]
background: TableRow<Raster<CPU>>,
#[cfg_attr(feature = "serde", serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row"))]
blended_image: TableRow<Raster<CPU>>,
#[cfg_attr(feature = "serde", serde(default, deserialize_with = "raster_types::image::migrate_image_frame_row"))]
last_stroke_texture: TableRow<Raster<CPU>>,
// A cache for brush textures.
#[cfg_attr(feature = "serde", serde(skip))]
brush_texture_cache: HashMap<CacheHashWrapper<BrushStyle>, Raster<CPU>>,
}
@@ -97,35 +82,6 @@ impl BrushCacheImpl {
}
}
impl Default for BrushCacheImpl {
fn default() -> Self {
Self {
unique_id: new_unique_id(),
prev_input: Vec::new(),
background: Default::default(),
blended_image: Default::default(),
last_stroke_texture: Default::default(),
brush_texture_cache: HashMap::new(),
}
}
}
impl PartialEq for BrushCacheImpl {
fn eq(&self, other: &Self) -> bool {
self.unique_id == other.unique_id
}
}
impl Hash for BrushCacheImpl {
fn hash<H: Hasher>(&self, state: &mut H) {
self.unique_id.hash(state);
}
}
fn new_unique_id() -> u64 {
NEXT_BRUSH_CACHE_IMPL_ID.fetch_add(1, Ordering::SeqCst)
}
#[derive(Clone, Debug, Default)]
pub struct BrushPlan {
pub strokes: Vec<BrushStroke>,
@@ -134,44 +90,9 @@ pub struct BrushPlan {
pub first_stroke_point_skip: usize,
}
#[derive(Debug, Default, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone)]
pub struct BrushCache(Arc<Mutex<BrushCacheImpl>>);
// A bit of a cursed implementation to work around the current node system.
// The original object is a 'prototype' that when cloned gives you a independent
// new object. Any further clones however are all the same underlying cache object.
impl Clone for BrushCache {
fn clone(&self) -> Self {
Self(Arc::new(Mutex::new(self.0.lock().unwrap().clone())))
}
}
impl PartialEq for BrushCache {
fn eq(&self, other: &Self) -> bool {
if Arc::ptr_eq(&self.0, &other.0) {
return true;
}
let s = self.0.lock().unwrap();
let o = other.0.lock().unwrap();
*s == *o
}
}
impl Hash for BrushCache {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.lock().unwrap().hash(state);
}
}
impl graphene_hash::CacheHash for BrushCache {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(&self.0.lock().unwrap().unique_id, state);
}
}
impl BrushCache {
pub fn compute_brush_plan(&self, background: TableRow<Raster<CPU>>, input: &[BrushStroke]) -> BrushPlan {
let mut inner = self.0.lock().unwrap();

View File

@@ -1,5 +1,5 @@
pub mod brush;
pub mod brush_cache;
mod brush_cache;
pub mod brush_stroke;
pub mod migrations {