Skip to content

Commit 7c8bd8a

Browse files
merge: brush-cache + master
2 parents 9aaad86 + 0f79c05 commit 7c8bd8a

4 files changed

Lines changed: 264 additions & 4 deletions

File tree

node-graph/graph-craft/src/document/value.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use super::DocumentNode;
22
use crate::application_io::PlatformEditorApi;
33
use crate::application_io::resource::Resource;
44
use crate::proto::{Any as DAny, FutureAny};
5-
use brush_nodes::Stroke;
65
use brush_nodes::brush_stroke::{BrushStroke, BrushTrace};
6+
use brush_nodes::{BrushCache, Stroke};
77
use core_types::color::SRGBA8;
88
use core_types::list::{Item, List, NodeIdPath};
99
use core_types::transform::Footprint;
@@ -99,6 +99,7 @@ macro_rules! tagged_value {
9999
#[serde(alias = "BrushStrokeTable")]
100100
BrushStrokes(Vec<BrushStroke>),
101101
Strokes(Vec<Stroke>),
102+
BrushCache(BrushCache),
102103
// =======================
103104
// AUTO-GENERATED VARIANTS
104105
// =======================
@@ -143,6 +144,7 @@ macro_rules! tagged_value {
143144
Self::GradientRamp(ramp) => ramp.cache_hash(state),
144145
Self::BrushStrokes(strokes) => strokes.cache_hash(state),
145146
Self::Strokes(strokes) => strokes.cache_hash(state),
147+
Self::BrushCache(cache) => cache.cache_hash(state),
146148
// =======================
147149
// NON-SERIALIZED VARIANTS
148150
// =======================
@@ -210,6 +212,7 @@ macro_rules! tagged_value {
210212
let list: List<Stroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
211213
Box::new(list)
212214
}
215+
Self::BrushCache(cache) => Box::new(Item::new_from_element(cache)),
213216
// =======================
214217
// AUTO-GENERATED VARIANTS
215218
// =======================
@@ -277,6 +280,7 @@ macro_rules! tagged_value {
277280
let list: List<Stroke> = strokes.into_iter().map(core_types::list::Item::new_from_element).collect();
278281
Arc::new(list)
279282
}
283+
Self::BrushCache(cache) => Arc::new(Item::new_from_element(cache)),
280284
// =======================
281285
// AUTO-GENERATED VARIANTS
282286
// =======================
@@ -307,6 +311,7 @@ macro_rules! tagged_value {
307311
Self::GradientRamp(_) => item!(Gradient),
308312
Self::BrushStrokes(_) => item!(BrushTrace),
309313
Self::Strokes(_) => list!(Stroke),
314+
Self::BrushCache(_) => item!(BrushCache),
310315
// =======================
311316
// AUTO-GENERATED VARIANTS
312317
// =======================
@@ -348,6 +353,7 @@ macro_rules! tagged_value {
348353
x if x == TypeId::of::<Vec<BrushStroke>>() => Ok(TaggedValue::BrushStrokes(*downcast(input).unwrap())),
349354
x if x == TypeId::of::<Item<BrushTrace>>() => Ok(TaggedValue::BrushStrokes(downcast::<Item<BrushTrace>>(input).unwrap().into_element().0.iter_element_values().cloned().collect())),
350355
x if x == TypeId::of::<List<Stroke>>() => Ok(TaggedValue::Strokes(downcast::<List<Stroke>>(input).unwrap().into_iter().map(Item::into_element).collect())),
356+
x if x == TypeId::of::<Item<BrushCache>>() => Ok(TaggedValue::BrushCache(downcast::<Item<BrushCache>>(input).unwrap().into_element())),
351357
// =======================
352358
// AUTO-GENERATED VARIANTS
353359
// =======================
@@ -383,6 +389,7 @@ macro_rules! tagged_value {
383389
x if x == TypeId::of::<Vec<BrushStroke>>() => Ok(TaggedValue::BrushStrokes(input.downcast_ref::<Vec<BrushStroke>>().unwrap().clone())),
384390
x if x == TypeId::of::<Item<BrushTrace>>() => Ok(TaggedValue::BrushStrokes(input.downcast_ref::<Item<BrushTrace>>().unwrap().element().0.iter_element_values().cloned().collect())),
385391
x if x == TypeId::of::<List<Stroke>>() => Ok(TaggedValue::Strokes(input.downcast_ref::<List<Stroke>>().unwrap().iter_element_values().cloned().collect())),
392+
x if x == TypeId::of::<Item<BrushCache>>() => Ok(TaggedValue::BrushCache(input.downcast_ref::<Item<BrushCache>>().unwrap().element().clone())),
386393
// =======================
387394
// AUTO-GENERATED VARIANTS
388395
// =======================
@@ -412,6 +419,7 @@ macro_rules! tagged_value {
412419
$( if name == std::any::type_name::<$ty>() { return Some(TaggedValue::$identifier(Default::default())) } )*
413420
if name == std::any::type_name::<BrushTrace>() { return Some(TaggedValue::BrushStrokes(Vec::new())) }
414421
if name == std::any::type_name::<List<Stroke>>() { return Some(TaggedValue::Strokes(Vec::new())) }
422+
if name == std::any::type_name::<BrushCache>() { return Some(TaggedValue::BrushCache(Default::default())) }
415423
// Unranked types without a variant route through `TypeDefault`, with `to_dynany`/`to_any` constructing the actual default at execution time
416424
macro_rules! check_bare {
417425
($type_default:ty) => {
@@ -469,6 +477,7 @@ macro_rules! tagged_value {
469477
Self::GradientRamp(ramp) => format!("GradientRamp({ramp:?})"),
470478
Self::BrushStrokes(strokes) => format!("BrushStrokes({strokes:?})"),
471479
Self::Strokes(strokes) => format!("Strokes({strokes:?})"),
480+
Self::BrushCache(cache) => format!("{cache:?}"),
472481
// =======================
473482
// AUTO-GENERATED VARIANTS
474483
// =======================
@@ -739,7 +748,6 @@ impl TaggedValue {
739748
///
740749
/// Routes legacy variant names into modern variants, in typed Rust. Each legacy name is also matched against the historical `#[serde(alias = "...")]` spellings the deleted variant accepted, so old-shape inner payloads are caught:
741750
///
742-
/// - `BrushCache` → `TaggedValue::None` (purely runtime cache; no payload to preserve)
743751
/// - `Graphic` (or alias `GraphicGroup`/`Group`) → `TaggedValue::TypeDefault(list!(Graphic))`
744752
/// - `Artboard` (or alias `ArtboardGroup`) → `TaggedValue::TypeDefault(list!(Artboard))`
745753
/// - `Raster` (or alias `ImageFrame`/`RasterData`/`Image`):
@@ -764,7 +772,6 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize
764772
&& let Some((tag, content)) = map.iter().next()
765773
{
766774
match tag.as_str() {
767-
"BrushCache" => return Ok(MemoHash::new(TaggedValue::None)),
768775
"Graphic" | "GraphicGroup" | "Group" => return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Graphic)))),
769776
"Artboard" | "ArtboardGroup" => return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Artboard)))),
770777
"Raster" | "ImageFrame" | "RasterData" | "Image" => {

node-graph/graph-craft/src/proto.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1059,7 +1059,7 @@ mod test {
10591059
// If this assert fails: These NodeIds seem to be changing when you modify TaggedValue, just update them.
10601060
assert_eq!(
10611061
ids,
1062-
vec![NodeId(12331852515109999872), NodeId(5084548161767585362), NodeId(14635346976242256925), NodeId(16015195863711239715)]
1062+
vec![NodeId(9617677014563055585), NodeId(3306304180790283913), NodeId(4482673701109291121), NodeId(1535890178157254933)]
10631063
);
10641064
}
10651065

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
//! Opaque render state cached per footprint.
2+
//!
3+
//! ```ignore
4+
//! let state: SomeState = cache.take(ctx.footprint()).unwrap_or_default();
5+
//! // ...render, freely mutating the state
6+
//! cache.store(ctx.footprint(), state);
7+
//! ```
8+
9+
use core_types::transform::Footprint;
10+
use glam::DMat2;
11+
use std::sync::{Arc, Mutex};
12+
13+
const STALE_EPOCHS: u64 = 2;
14+
const MAX_VIEWS: usize = 3;
15+
16+
#[derive(Clone)]
17+
pub struct BrushCache {
18+
state: Arc<Mutex<State>>,
19+
nonce: u64, // Avoid deduplication of cache entries across different brush nodes.
20+
}
21+
22+
impl Default for BrushCache {
23+
fn default() -> Self {
24+
Self {
25+
state: Default::default(),
26+
nonce: core_types::uuid::generate_uuid(),
27+
}
28+
}
29+
}
30+
31+
impl BrushCache {
32+
pub fn take<S: std::any::Any + Send + Sync>(&self, footprint: &Footprint) -> Option<S> {
33+
let mut guard = self.state.lock().unwrap();
34+
let state = guard.take(footprint)?;
35+
match state.downcast() {
36+
Ok(state) => Some(*state),
37+
Err(state) => {
38+
guard.store(footprint, state);
39+
None
40+
}
41+
}
42+
}
43+
44+
pub fn store<S: std::any::Any + Send + Sync>(&self, footprint: &Footprint, state: S) {
45+
self.state.lock().unwrap().store(footprint, Box::new(state));
46+
}
47+
}
48+
49+
impl PartialEq for BrushCache {
50+
fn eq(&self, _: &Self) -> bool {
51+
true
52+
}
53+
}
54+
55+
impl std::fmt::Debug for BrushCache {
56+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57+
f.debug_struct("BrushCache").field("slots", &self.state.lock().unwrap().slots.len()).finish()
58+
}
59+
}
60+
61+
impl core_types::CacheHash for BrushCache {
62+
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
63+
state.write_u64(self.nonce);
64+
}
65+
}
66+
67+
unsafe impl dyn_any::StaticType for BrushCache {
68+
type Static = BrushCache;
69+
}
70+
71+
#[cfg(feature = "serde")]
72+
impl serde::Serialize for BrushCache {
73+
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
74+
serializer.serialize_unit()
75+
}
76+
}
77+
78+
#[cfg(feature = "serde")]
79+
impl<'de> serde::Deserialize<'de> for BrushCache {
80+
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
81+
serde::de::IgnoredAny::deserialize(deserializer)?;
82+
Ok(Self::default())
83+
}
84+
}
85+
86+
type BoxedData = Box<dyn std::any::Any + Send + Sync>;
87+
88+
#[derive(Default)]
89+
struct State {
90+
epoch: u64,
91+
slots: Vec<Slot>,
92+
}
93+
94+
struct Slot {
95+
footprint: Footprint,
96+
epoch: u64,
97+
data: BoxedData,
98+
}
99+
100+
impl Slot {
101+
fn view(&self) -> DMat2 {
102+
self.footprint.transform.matrix2
103+
}
104+
}
105+
106+
impl State {
107+
fn take(&mut self, footprint: &Footprint) -> Option<BoxedData> {
108+
self.touch(footprint.transform.matrix2);
109+
let index = self.slots.iter().position(|slot| slot.footprint == *footprint);
110+
let hit = index.map(|index| {
111+
let slot = self.slots.remove(index);
112+
if slot.epoch == self.epoch {
113+
self.epoch += 1;
114+
}
115+
slot.data
116+
});
117+
self.retire();
118+
hit
119+
}
120+
121+
fn store(&mut self, footprint: &Footprint, data: BoxedData) {
122+
self.touch(footprint.transform.matrix2);
123+
self.slots.retain(|slot| slot.footprint != *footprint);
124+
self.slots.push(Slot {
125+
footprint: *footprint,
126+
epoch: self.epoch,
127+
data,
128+
});
129+
self.retire();
130+
}
131+
132+
fn touch(&mut self, view: DMat2) {
133+
self.slots.sort_by_key(|slot| slot.view() == view);
134+
}
135+
136+
fn retire(&mut self) {
137+
let epoch = self.epoch;
138+
self.slots.retain(|slot| epoch - slot.epoch < STALE_EPOCHS);
139+
while self.slots.chunk_by(|a, b| a.view() == b.view()).count() > MAX_VIEWS {
140+
let front = self.slots[0].view();
141+
let group = self.slots.iter().take_while(|slot| slot.view() == front).count();
142+
self.slots.drain(..group.max(1));
143+
}
144+
}
145+
}
146+
147+
#[cfg(test)]
148+
mod tests {
149+
use super::*;
150+
use core_types::transform::RenderQuality;
151+
use glam::{DAffine2, DVec2, UVec2};
152+
153+
struct Dummy;
154+
155+
fn view(zoom: f64, rotation: f64, pan: DVec2) -> Footprint {
156+
Footprint {
157+
transform: DAffine2::from_scale_angle_translation(DVec2::splat(zoom), rotation, pan),
158+
resolution: UVec2::new(1920, 1080),
159+
quality: RenderQuality::Full,
160+
}
161+
}
162+
163+
fn thumbnail(zoom: f64) -> Footprint {
164+
Footprint {
165+
resolution: UVec2::new(150, 150),
166+
..view(zoom, 0., DVec2::ZERO)
167+
}
168+
}
169+
170+
fn live(cache: &BrushCache) -> usize {
171+
cache.state.lock().unwrap().slots.len()
172+
}
173+
174+
fn render(cache: &BrushCache, footprint: &Footprint) -> bool {
175+
let hit = cache.take::<Dummy>(footprint).is_some();
176+
cache.store(footprint, Dummy);
177+
hit
178+
}
179+
180+
#[test]
181+
fn continuous_zoom_is_bounded_by_views() {
182+
let cache = BrushCache::default();
183+
for step in 0..100 {
184+
render(&cache, &view(1. + step as f64 * 0.01, 0., DVec2::ZERO));
185+
}
186+
assert!(live(&cache) <= MAX_VIEWS);
187+
}
188+
189+
#[test]
190+
fn continuous_rotation_is_bounded_by_views() {
191+
let cache = BrushCache::default();
192+
for step in 0..100 {
193+
render(&cache, &view(2., step as f64 * 0.01, DVec2::ZERO));
194+
}
195+
assert!(live(&cache) <= MAX_VIEWS);
196+
}
197+
198+
#[test]
199+
fn zooming_reclaims_pan_slots() {
200+
let cache = BrushCache::default();
201+
for step in 0..30 {
202+
render(&cache, &view(1., 0., DVec2::splat(step as f64 * 100.)));
203+
}
204+
for step in 1..=3 {
205+
render(&cache, &view(1. + step as f64, 0., DVec2::ZERO));
206+
}
207+
assert_eq!(live(&cache), 3);
208+
}
209+
210+
#[test]
211+
fn frames_may_hold_many_footprints_per_view() {
212+
let cache = BrushCache::default();
213+
let footprints: Vec<_> = (0..5).map(|step| view(1., 0., DVec2::splat(step as f64 * 100.))).collect();
214+
for frame in 0..10 {
215+
for footprint in &footprints {
216+
assert_eq!(render(&cache, footprint), frame > 0, "footprint evicted while its frame still renders it");
217+
}
218+
}
219+
assert_eq!(live(&cache), 5);
220+
}
221+
222+
#[test]
223+
fn thumbnail_drift_is_bounded_and_keeps_the_view() {
224+
let cache = BrushCache::default();
225+
for step in 0..100 {
226+
render(&cache, &thumbnail(1. + step as f64 * 0.001));
227+
}
228+
assert!(live(&cache) <= MAX_VIEWS);
229+
230+
let viewport = view(2., 0., DVec2::ZERO);
231+
render(&cache, &viewport);
232+
for step in 0..50 {
233+
render(&cache, &thumbnail(2. + step as f64 * 0.001));
234+
assert!(render(&cache, &viewport), "thumbnail churn evicted the viewport slot");
235+
}
236+
}
237+
238+
#[test]
239+
fn settled_view_retires_stale_slots() {
240+
let cache = BrushCache::default();
241+
for step in 0..3 {
242+
render(&cache, &view(1. + step as f64, 0., DVec2::ZERO));
243+
}
244+
assert_eq!(live(&cache), 3);
245+
for _ in 0..STALE_EPOCHS {
246+
render(&cache, &view(1., 0., DVec2::ZERO));
247+
}
248+
assert_eq!(live(&cache), 1);
249+
}
250+
}

node-graph/libraries/brush-types/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
pub mod cache;
2+
pub use cache::BrushCache;
3+
14
use core_types::CacheHash;
25
use core_types::bounds::{BoundingBox, RenderBoundingBox};
36
use core_types::render_complexity::RenderComplexity;

0 commit comments

Comments
 (0)