diff --git a/src/cache.rs b/src/cache.rs index d25cec2..103648c 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -5,25 +5,27 @@ use std::num::NonZeroUsize; use std::sync::Arc; pub(crate) struct Cache { - tessellation_cache: LruCache>>, + tessellation_cache: Option>>>, } impl Cache { - pub(crate) fn new(size: NonZeroUsize) -> Self { + pub(crate) fn new(size: NonZeroUsize, enabled: bool) -> Self { Self { - tessellation_cache: LruCache::new(size), + tessellation_cache: enabled.then(|| LruCache::new(size)), } } pub fn len(&self) -> usize { - self.tessellation_cache.len() + self.tessellation_cache.as_ref().map_or(0, LruCache::len) } pub(crate) fn get_vertex_buffers( &mut self, cache_key: &u64, ) -> Option>> { - self.tessellation_cache.get(cache_key).cloned() + self.tessellation_cache + .as_mut() + .and_then(|cache| cache.get(cache_key).cloned()) } pub(crate) fn insert_vertex_buffers( @@ -31,7 +33,9 @@ impl Cache { cache_key: u64, vertex_buffers: Arc>, ) { - self.tessellation_cache.put(cache_key, vertex_buffers); + if let Some(cache) = self.tessellation_cache.as_mut() { + cache.put(cache_key, vertex_buffers); + } } } @@ -45,7 +49,7 @@ mod tests { #[test] fn cache_returns_shared_arc_without_cloning_vertex_buffers() { - let mut cache = Cache::new(NonZeroUsize::new(4).unwrap()); + let mut cache = Cache::new(NonZeroUsize::new(4).unwrap(), true); let mut vertex_buffers = VertexBuffers::::new(); vertex_buffers.vertices.push(CustomVertex { position: [0.0, 0.0], @@ -61,4 +65,15 @@ mod tests { let cached_vertex_buffers = cache.get_vertex_buffers(&7).unwrap(); assert!(Arc::ptr_eq(&shared_vertex_buffers, &cached_vertex_buffers)); } + + #[test] + fn disabled_cache_never_stores_entries() { + let mut cache = Cache::new(NonZeroUsize::new(4).unwrap(), false); + let vertex_buffers = Arc::new(VertexBuffers::::new()); + + cache.insert_vertex_buffers(7, vertex_buffers); + + assert_eq!(cache.len(), 0); + assert!(cache.get_vertex_buffers(&7).is_none()); + } } diff --git a/src/effect.rs b/src/effect.rs index 09432d1..63382f4 100644 --- a/src/effect.rs +++ b/src/effect.rs @@ -132,6 +132,13 @@ pub(crate) struct OffscreenTexturePool { available: Vec, } +pub(crate) struct OffscreenTexturePoolStats { + pub pooled_textures: usize, + pub estimated_color_bytes: u64, + pub estimated_resolve_bytes: u64, + pub estimated_depth_stencil_bytes: u64, +} + /// Maximum number of textures to keep in the pool. const MAX_POOL_SIZE: usize = 8; @@ -142,6 +149,10 @@ impl OffscreenTexturePool { } } + pub fn clear(&mut self) { + self.available.clear(); + } + /// Return textures for reuse in future frames. /// Textures that don't match the given active configuration are dropped /// immediately, and the pool is capped at `MAX_POOL_SIZE`. @@ -185,6 +196,28 @@ impl OffscreenTexturePool { } } + pub(crate) fn stats(&self) -> OffscreenTexturePoolStats { + let mut estimated_color_bytes = 0_u64; + let mut estimated_resolve_bytes = 0_u64; + let mut estimated_depth_stencil_bytes = 0_u64; + + for texture in &self.available { + let pixels = texture.width as u64 * texture.height as u64; + estimated_color_bytes += pixels * 4 * texture.sample_count as u64; + if texture.resolve_texture.is_some() { + estimated_resolve_bytes += pixels * 4; + } + estimated_depth_stencil_bytes += pixels * 4 * texture.sample_count as u64; + } + + OffscreenTexturePoolStats { + pooled_textures: self.available.len(), + estimated_color_bytes, + estimated_resolve_bytes, + estimated_depth_stencil_bytes, + } + } + fn create_pooled_texture( device: &wgpu::Device, width: u32, diff --git a/src/lib.rs b/src/lib.rs index 5295d61..f58764b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,6 +164,7 @@ pub use gradient::types::{ pub use renderer::MathRect; pub use renderer::Renderer; pub use renderer::RendererCreationError; +pub use renderer::RendererOptions; pub use renderer::TextureLayer; pub use shape::*; pub use stroke::Stroke; diff --git a/src/renderer.rs b/src/renderer.rs index 1acaef2..035572c 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -52,6 +52,35 @@ pub type MathRect = lyon::math::Box2D; // TODO: move to the config/constructor const MAX_CACHED_SHAPES: usize = 1024; +/// Renderer construction options for runtime-tunable performance behavior. +/// +/// `enable_reusable_caches` controls renderer-managed performance caches such as +/// tessellation reuse, gradient LRUs, and texture bind-group reuse. +/// +/// Explicit resource stores like `load_shape()` geometry and loaded effects are +/// intentionally not affected, because disabling them would change renderer +/// behavior rather than just cache policy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RendererOptions { + pub enable_reusable_caches: bool, +} + +impl Default for RendererOptions { + fn default() -> Self { + Self { + enable_reusable_caches: true, + } + } +} + +impl RendererOptions { + pub const fn without_reusable_caches() -> Self { + Self { + enable_reusable_caches: false, + } + } +} + /// Semantic texture layers for a shape. Background is layer 0, Foreground is layer 1. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub enum TextureLayer { @@ -86,6 +115,7 @@ pub struct Renderer<'a> { device: Arc, queue: Arc, config: wgpu::SurfaceConfiguration, + renderer_options: RendererOptions, tessellator: FillTessellator, buffers_pool_manager: PoolManager, diff --git a/src/renderer/construction.rs b/src/renderer/construction.rs index e3595b1..6e7ed71 100644 --- a/src/renderer/construction.rs +++ b/src/renderer/construction.rs @@ -1,4 +1,5 @@ use super::*; +use std::collections::HashSet; use tracing::{info, warn}; fn pick_surface_format(surface_formats: &[wgpu::TextureFormat]) -> wgpu::TextureFormat { @@ -63,13 +64,14 @@ pub enum RendererCreationError { } impl<'a> Renderer<'a> { - pub async fn new( + pub async fn new_with_options( window: impl Into>, physical_size: (u32, u32), scale_factor: f64, vsync: bool, transparent: bool, msaa_samples: u32, + options: RendererOptions, ) -> Self { let size = physical_size; @@ -133,10 +135,31 @@ impl<'a> Renderer<'a> { size, scale_factor, msaa_sample_count, + options, ) .expect("Failed to build renderer from device") } + pub async fn new( + window: impl Into>, + physical_size: (u32, u32), + scale_factor: f64, + vsync: bool, + transparent: bool, + msaa_samples: u32, + ) -> Self { + Self::new_with_options( + window, + physical_size, + scale_factor, + vsync, + transparent, + msaa_samples, + RendererOptions::default(), + ) + .await + } + /// Shared constructor: takes the wgpu primitives produced by `new()` or /// `new_headless()` and builds the full `Renderer`. #[allow(clippy::too_many_arguments)] @@ -149,6 +172,7 @@ impl<'a> Renderer<'a> { physical_size: (u32, u32), scale_factor: f64, msaa_sample_count: u32, + renderer_options: RendererOptions, ) -> Result { if !scale_factor.is_finite() || scale_factor <= 0.0 { return Err(RendererCreationError::InvalidScaleFactor(scale_factor)); @@ -235,7 +259,11 @@ impl<'a> Renderer<'a> { let device = Arc::new(device); let queue = Arc::new(queue); - let texture_manager = TextureManager::new(device.clone(), queue.clone()); + let texture_manager = TextureManager::new( + device.clone(), + queue.clone(), + renderer_options.enable_reusable_caches, + ); let (default_shape_texture_bind_group_layer0, shape_texture_bind_group_layout_layer0) = Self::create_default_shape_texture_bind_group(&device, &queue, &and_texture_bgl_layer0); @@ -250,11 +278,13 @@ impl<'a> Renderer<'a> { config, physical_size, scale_factor, + renderer_options, fringe_width: Self::DEFAULT_FRINGE_WIDTH, tessellator: FillTessellator::new(), texture_manager, buffers_pool_manager: PoolManager::new( NonZeroUsize::new(MAX_CACHED_SHAPES).expect("Cache size to be greater than 0"), + renderer_options.enable_reusable_caches, ), and_pipeline: Arc::new(and_pipeline), and_uniforms, @@ -348,9 +378,36 @@ impl<'a> Renderer<'a> { } pub fn print_memory_usage_info(&self) { + let mut unique_cached_geometries = HashSet::new(); + let mut cached_shape_vertex_count = 0_usize; + let mut cached_shape_index_count = 0_usize; + let mut cached_shape_geometry_bytes = 0_usize; + + for cached_shape in self.shape_cache.values() { + let geometry_ptr = std::sync::Arc::as_ptr(&cached_shape.vertex_buffers) as usize; + if unique_cached_geometries.insert(geometry_ptr) { + cached_shape_vertex_count += cached_shape.vertex_buffers.vertices.capacity(); + cached_shape_index_count += cached_shape.vertex_buffers.indices.capacity(); + cached_shape_geometry_bytes += cached_shape.vertex_buffers.vertices.capacity() + * std::mem::size_of::(); + cached_shape_geometry_bytes += cached_shape.vertex_buffers.indices.capacity() + * std::mem::size_of::(); + } + } + println!("=== Memory Usage Info ==="); + println!( + "Reusable caches enabled: {}", + self.renderer_options.enable_reusable_caches + ); - println!("Cached shapes: {}", self.shape_cache.len()); + println!( + "Cached shapes: {} (~{} unique geometry bytes across {} vertices / {} indices)", + self.shape_cache.len(), + cached_shape_geometry_bytes, + cached_shape_vertex_count, + cached_shape_index_count, + ); println!("Draw tree size: {}", self.draw_tree.len()); println!( "Metadata to clips mappings: {}", @@ -414,6 +471,26 @@ impl<'a> Renderer<'a> { if let Some(buf) = &self.identity_instance_metadata_buffer { println!("Identity instance metadata buffer: {} bytes", buf.size()); } + if let Some(tex) = &self.msaa_color_texture { + let size = tex.size(); + println!( + "MSAA color texture: {}x{} samples={} (~{} bytes)", + size.width, + size.height, + self.msaa_sample_count, + size.width as u64 * size.height as u64 * 4 * self.msaa_sample_count as u64 + ); + } + if let Some(tex) = &self.depth_stencil_texture { + let size = tex.size(); + println!( + "Depth/stencil texture: {}x{} samples={} (~{} bytes)", + size.width, + size.height, + self.msaa_sample_count, + size.width as u64 * size.height as u64 * 4 * self.msaa_sample_count as u64 + ); + } println!("\n--- ARGB Compute Buffers ---"); if let Some(buf) = &self.argb_input_buffer { @@ -466,8 +543,37 @@ impl<'a> Renderer<'a> { self.decrementing_uniform_buffer.size() ); + println!("\n--- Effect Resources ---"); + println!("Loaded effects: {}", self.loaded_effects.len()); + println!("Group effects this frame: {}", self.group_effects.len()); + println!("Backdrop effects this frame: {}", self.backdrop_effects.len()); + let offscreen_pool_stats = self.offscreen_texture_pool.stats(); + println!( + "Offscreen texture pool: {} pooled textures (~{} color bytes, ~{} resolve bytes, ~{} depth/stencil bytes, ~{} total)", + offscreen_pool_stats.pooled_textures, + offscreen_pool_stats.estimated_color_bytes, + offscreen_pool_stats.estimated_resolve_bytes, + offscreen_pool_stats.estimated_depth_stencil_bytes, + offscreen_pool_stats.estimated_color_bytes + + offscreen_pool_stats.estimated_resolve_bytes + + offscreen_pool_stats.estimated_depth_stencil_bytes, + ); + if let Some(tex) = &self.backdrop_snapshot_texture { + let size = tex.size(); + println!( + "Backdrop snapshot texture: {}x{} (~{} bytes)", + size.width, + size.height, + size.width as u64 * size.height as u64 * 4 + ); + } + println!("\n--- Texture Manager ---"); - println!("{:?}", self.texture_manager.size()); + let (texture_count, bind_group_cache_count) = self.texture_manager.size(); + println!( + "Textures: {}, cached bind groups: {}", + texture_count, bind_group_cache_count + ); println!("\n--- Buffer Pool Manager ---"); self.buffers_pool_manager.print_sizes(); @@ -572,6 +678,16 @@ impl<'a> Renderer<'a> { pub async fn try_new_headless( physical_size: (u32, u32), scale_factor: f64, + ) -> Result { + Self::try_new_headless_with_options(physical_size, scale_factor, RendererOptions::default()) + .await + } + + /// Creates a headless renderer without a window surface using custom renderer options. + pub async fn try_new_headless_with_options( + physical_size: (u32, u32), + scale_factor: f64, + options: RendererOptions, ) -> Result { let size = physical_size; @@ -623,6 +739,7 @@ impl<'a> Renderer<'a> { size, scale_factor, msaa_sample_count, + options, ) } @@ -636,7 +753,17 @@ impl<'a> Renderer<'a> { /// For a non-panicking alternative (e.g. in tests), use /// [`Self::try_new_headless`] instead. pub async fn new_headless(physical_size: (u32, u32), scale_factor: f64) -> Self { - Self::try_new_headless(physical_size, scale_factor) + Self::new_headless_with_options(physical_size, scale_factor, RendererOptions::default()) + .await + } + + /// Creates a headless renderer without a window surface using custom renderer options. + pub async fn new_headless_with_options( + physical_size: (u32, u32), + scale_factor: f64, + options: RendererOptions, + ) -> Self { + Self::try_new_headless_with_options(physical_size, scale_factor, options) .await .expect("Failed to create headless renderer") } diff --git a/src/renderer/rendering.rs b/src/renderer/rendering.rs index feb60d4..6b7ebb5 100644 --- a/src/renderer/rendering.rs +++ b/src/renderer/rendering.rs @@ -388,8 +388,13 @@ impl<'a> Renderer<'a> { self.last_render_to_texture_view_cpu_time = render_to_texture_view_started_at.elapsed(); - self.offscreen_texture_pool - .recycle(&mut textures_to_recycle); + if self.renderer_options.enable_reusable_caches { + self.offscreen_texture_pool + .recycle(&mut textures_to_recycle); + } else { + textures_to_recycle.clear(); + self.offscreen_texture_pool.clear(); + } effect_output_textures.clear(); self.draw_tree diff --git a/src/shape.rs b/src/shape.rs index affb547..6a87bdd 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -1735,7 +1735,7 @@ mod tests { fn rect_tessellation_uses_shared_quad_corners() { let rect_shape = RectShape::new([(10.0, 20.0), (30.0, 50.0)], Stroke::default()); let mut tessellator = FillTessellator::new(); - let mut pool_manager = PoolManager::new(NonZeroUsize::new(1).unwrap()); + let mut pool_manager = PoolManager::new(NonZeroUsize::new(1).unwrap(), true); let tessellated_geometry = Shape::Rect(rect_shape).tessellate(&mut tessellator, &mut pool_manager, None); diff --git a/src/texture_manager.rs b/src/texture_manager.rs index 31801d8..6b5b3b2 100644 --- a/src/texture_manager.rs +++ b/src/texture_manager.rs @@ -66,6 +66,7 @@ pub struct TextureManager { device: Arc, queue: Arc, sampler: Arc, + cache_bind_groups: bool, /// Textures is raw image data, without any screen position information texture_storage: Arc>>, /// Cache for shape texture bind groups keyed by (texture_id, layout_epoch) @@ -75,12 +76,17 @@ pub struct TextureManager { type BindGroupCache = HashMap<(u64, u64), Arc>; impl TextureManager { - pub(crate) fn new(device: Arc, queue: Arc) -> Self { + pub(crate) fn new( + device: Arc, + queue: Arc, + cache_bind_groups: bool, + ) -> Self { let sampler = Self::create_sampler(&device); Self { device, queue, sampler: Arc::new(sampler), + cache_bind_groups, texture_storage: Arc::new(RwLock::new(HashMap::new())), shape_bind_group_cache: Arc::new(RwLock::new(HashMap::new())), } @@ -269,14 +275,16 @@ impl TextureManager { texture_id: u64, ) -> Result, TextureManagerError> { // Fast path: check cache - if let Some(bg) = self - .shape_bind_group_cache - .read() - .unwrap() - .get(&(texture_id, layout_epoch)) - .cloned() - { - return Ok(bg); + if self.cache_bind_groups { + if let Some(bg) = self + .shape_bind_group_cache + .read() + .unwrap() + .get(&(texture_id, layout_epoch)) + .cloned() + { + return Ok(bg); + } } // Create bind group @@ -301,10 +309,12 @@ impl TextureManager { })); // Insert into cache - self.shape_bind_group_cache - .write() - .unwrap() - .insert((texture_id, layout_epoch), bind_group.clone()); + if self.cache_bind_groups { + self.shape_bind_group_cache + .write() + .unwrap() + .insert((texture_id, layout_epoch), bind_group.clone()); + } Ok(bind_group) } diff --git a/src/util.rs b/src/util.rs index e1ba5f3..dacbef6 100644 --- a/src/util.rs +++ b/src/util.rs @@ -95,33 +95,43 @@ struct CachedGradientRampTexture { } pub(crate) struct GradientCache { - ramps: LruCache, - ramp_textures: LruCache>, - bind_groups: LruCache>, + enabled: bool, + ramps: Option>, + ramp_textures: Option>>, + bind_groups: Option>>, default_ramp_texture: Option>, } impl GradientCache { - fn new() -> Self { + fn new(enabled: bool) -> Self { Self { - ramps: LruCache::new( - NonZeroUsize::new(MAX_GRADIENT_RAMP_CACHE_SIZE) - .expect("gradient ramp cache size must be greater than 0"), - ), - ramp_textures: LruCache::new( - NonZeroUsize::new(MAX_GRADIENT_RAMP_CACHE_SIZE) - .expect("gradient ramp cache size must be greater than 0"), - ), - bind_groups: LruCache::new( - NonZeroUsize::new(MAX_GRADIENT_BIND_GROUP_CACHE_SIZE) - .expect("gradient bind group cache size must be greater than 0"), - ), + enabled, + ramps: enabled.then(|| { + LruCache::new( + NonZeroUsize::new(MAX_GRADIENT_RAMP_CACHE_SIZE) + .expect("gradient ramp cache size must be greater than 0"), + ) + }), + ramp_textures: enabled.then(|| { + LruCache::new( + NonZeroUsize::new(MAX_GRADIENT_RAMP_CACHE_SIZE) + .expect("gradient ramp cache size must be greater than 0"), + ) + }), + bind_groups: enabled.then(|| { + LruCache::new( + NonZeroUsize::new(MAX_GRADIENT_BIND_GROUP_CACHE_SIZE) + .expect("gradient bind group cache size must be greater than 0"), + ) + }), default_ramp_texture: None, } } pub(crate) fn clear_bind_groups(&mut self) { - self.bind_groups.clear(); + if let Some(bind_groups) = self.bind_groups.as_mut() { + bind_groups.clear(); + } } fn get_or_create_default_ramp_texture( @@ -129,8 +139,10 @@ impl GradientCache { device: &wgpu::Device, queue: &wgpu::Queue, ) -> Arc { - if let Some(default_ramp_texture) = &self.default_ramp_texture { - return default_ramp_texture.clone(); + if self.enabled { + if let Some(default_ramp_texture) = &self.default_ramp_texture { + return default_ramp_texture.clone(); + } } let (texture, view) = create_default_ramp_texture(device, queue); @@ -138,7 +150,11 @@ impl GradientCache { _texture: texture, view: Arc::new(view), }); - self.default_ramp_texture = Some(default_ramp_texture.clone()); + + if self.enabled { + self.default_ramp_texture = Some(default_ramp_texture.clone()); + } + default_ramp_texture } @@ -150,7 +166,11 @@ impl GradientCache { GradientRamp::Pending(_) => {} } - if let Some(ramp) = self.ramps.get(&gradient_data.ramp_cache_key).cloned() { + if let Some(ramp) = self + .ramps + .as_mut() + .and_then(|ramps| ramps.get(&gradient_data.ramp_cache_key).cloned()) + { gradient_data.ramp = ramp.clone(); return ramp; } @@ -160,8 +180,9 @@ impl GradientCache { GradientRamp::Constant(_) | GradientRamp::Sampled(_) => unreachable!(), }; - self.ramps - .put(gradient_data.ramp_cache_key.clone(), baked_ramp.clone()); + if let Some(ramps) = self.ramps.as_mut() { + ramps.put(gradient_data.ramp_cache_key.clone(), baked_ramp.clone()); + } gradient_data.ramp = baked_ramp.clone(); baked_ramp } @@ -172,8 +193,12 @@ impl GradientCache { device: &wgpu::Device, queue: &wgpu::Queue, ) -> Arc { - if let Some(ramp_texture) = self.ramp_textures.get(&gradient_data.ramp_cache_key) { - return ramp_texture.clone(); + if let Some(ramp_texture) = self + .ramp_textures + .as_mut() + .and_then(|ramp_textures| ramp_textures.get(&gradient_data.ramp_cache_key).cloned()) + { + return ramp_texture; } let ramp = self.get_or_create_ramp(gradient_data); @@ -182,8 +207,9 @@ impl GradientCache { _texture: texture, view: Arc::new(view), }); - self.ramp_textures - .put(gradient_data.ramp_cache_key.clone(), ramp_texture.clone()); + if let Some(ramp_textures) = self.ramp_textures.as_mut() { + ramp_textures.put(gradient_data.ramp_cache_key.clone(), ramp_texture.clone()); + } ramp_texture } @@ -203,8 +229,12 @@ impl GradientCache { ramp_key: gradient_data.ramp_cache_key.clone(), }; - if let Some(bind_group) = self.bind_groups.get(&cache_key) { - return bind_group.clone(); + if let Some(bind_group) = self + .bind_groups + .as_mut() + .and_then(|bind_groups| bind_groups.get(&cache_key).cloned()) + { + return bind_group; } let ramp_texture = if gradient_data.is_constant { @@ -239,16 +269,27 @@ impl GradientCache { ], })); - self.bind_groups.put(cache_key, bind_group.clone()); + if let Some(bind_groups) = self.bind_groups.as_mut() { + bind_groups.put(cache_key, bind_group.clone()); + } bind_group } fn trim(&mut self) {} fn print_sizes(&self) { - println!("Gradient ramps: {}", self.ramps.len()); - println!("Gradient ramp textures: {}", self.ramp_textures.len()); - println!("Gradient bind groups: {}", self.bind_groups.len()); + println!( + "Gradient ramps: {}", + self.ramps.as_ref().map_or(0, LruCache::len) + ); + println!( + "Gradient ramp textures: {}", + self.ramp_textures.as_ref().map_or(0, LruCache::len) + ); + println!( + "Gradient bind groups: {}", + self.bind_groups.as_ref().map_or(0, LruCache::len) + ); } } @@ -317,12 +358,12 @@ pub(crate) struct PoolManager { } impl PoolManager { - pub(crate) fn new(tesselation_cache_size: NonZeroUsize) -> Self { + pub(crate) fn new(tesselation_cache_size: NonZeroUsize, enable_reusable_caches: bool) -> Self { Self { lyon_vertex_buffers_pool: LyonVertexBuffersPool::new(), - tessellation_cache: Cache::new(tesselation_cache_size), + tessellation_cache: Cache::new(tesselation_cache_size, enable_reusable_caches), aa_fringe_scratch: AaFringeScratch::new(), - gradient_cache: GradientCache::new(), + gradient_cache: GradientCache::new(enable_reusable_caches), } } @@ -355,6 +396,7 @@ mod tests { GradientStopOffset, GradientStopPositions, GradientUnits, LinearGradientDesc, LinearGradientLine, SpreadMode, }; + use lru::LruCache; use std::sync::Arc; #[test] @@ -423,7 +465,7 @@ mod tests { }) .unwrap(); - let mut gradient_cache = GradientCache::new(); + let mut gradient_cache = GradientCache::new(true); let first_ramp = gradient_cache.get_or_create_ramp(&mut first.data); let second_ramp = gradient_cache.get_or_create_ramp(&mut second.data); @@ -436,4 +478,54 @@ mod tests { assert!(Arc::ptr_eq(&first_ramp, &second_ramp)); } + + #[test] + fn disabled_gradient_cache_does_not_retain_entries() { + let common = GradientCommonDesc { + units: GradientUnits::Local, + spread: SpreadMode::Pad, + interpolation: ColorInterpolation::SrgbLinear, + stops: vec![ + GradientStop { + positions: GradientStopPositions::Single(GradientStopOffset::LinearRadial(0.0)), + color: GradientColor::Srgb { + red: 1.0, + green: 0.0, + blue: 0.0, + alpha: 1.0, + }, + hint_to_next_segment: None, + }, + GradientStop { + positions: GradientStopPositions::Single(GradientStopOffset::LinearRadial(1.0)), + color: GradientColor::Srgb { + red: 0.0, + green: 0.0, + blue: 1.0, + alpha: 1.0, + }, + hint_to_next_segment: None, + }, + ] + .into(), + }; + + let mut gradient = Gradient::linear(LinearGradientDesc { + common, + line: LinearGradientLine { + start: [0.0, 0.0], + end: [10.0, 0.0], + }, + }) + .unwrap(); + + let mut gradient_cache = GradientCache::new(false); + let ramp = gradient_cache.get_or_create_ramp(&mut gradient.data); + + assert!(matches!( + ramp, + GradientRamp::Sampled(_) | GradientRamp::Constant(_) + )); + assert_eq!(gradient_cache.ramps.as_ref().map_or(0, LruCache::len), 0); + } }