vello_cpu: Add `ImageResolver` for resolving opaque image IDs at rasterization time
diff --git a/sparse_strips/vello_bench/src/fine/fill.rs b/sparse_strips/vello_bench/src/fine/fill.rs index e2d6d9c..e462349 100644 --- a/sparse_strips/vello_bench/src/fine/fill.rs +++ b/sparse_strips/vello_bench/src/fine/fill.rs
@@ -6,7 +6,7 @@ use vello_common::color::palette::css::ROYAL_BLUE; use vello_common::encode::EncodedPaint; use vello_common::fearless_simd::Simd; -use vello_common::paint::{Paint, PremulColor}; +use vello_common::paint::{NoOpImageResolver, Paint, PremulColor}; use vello_common::peniko::BlendMode; use vello_cpu::fine::{Fine, FineKernel}; use vello_dev_macros::vello_bench; @@ -59,7 +59,16 @@ fine: &mut Fine<S, N>, ) { b.iter(|| { - fine.fill(0, width, paint, blend_mode, encoded_paints, None, None); + fine.fill( + 0, + width, + paint, + blend_mode, + encoded_paints, + &NoOpImageResolver, + None, + None, + ); std::hint::black_box(&fine); });
diff --git a/sparse_strips/vello_bench/src/fine/strip.rs b/sparse_strips/vello_bench/src/fine/strip.rs index cbf04b4..43f4ec1 100644 --- a/sparse_strips/vello_bench/src/fine/strip.rs +++ b/sparse_strips/vello_bench/src/fine/strip.rs
@@ -10,7 +10,7 @@ use vello_common::color::palette::css::ROYAL_BLUE; use vello_common::encode::EncodedPaint; use vello_common::fearless_simd::Simd; -use vello_common::paint::{Paint, PremulColor}; +use vello_common::paint::{NoOpImageResolver, Paint, PremulColor}; use vello_common::tile::Tile; use vello_cpu::fine::{Fine, FineKernel}; use vello_dev_macros::vello_bench; @@ -57,6 +57,7 @@ paint, default_blend(), encoded_paints, + &NoOpImageResolver, Some(&alphas), None, );
diff --git a/sparse_strips/vello_common/src/paint.rs b/sparse_strips/vello_common/src/paint.rs index 4f4c0ed..04e1083 100644 --- a/sparse_strips/vello_common/src/paint.rs +++ b/sparse_strips/vello_common/src/paint.rs
@@ -139,6 +139,29 @@ /// An image. pub type Image = peniko::ImageBrush<ImageSource>; +/// Trait for resolving opaque image IDs to pixmaps at rasterization time. +/// +/// This allows delaying the resolution of `ImageSource::OpaqueId` until the +/// image is actually needed during rasterization, enabling patterns like +/// dynamic sprite atlases where the image data may be updated between +/// encoding and rendering. +pub trait ImageResolver { + /// Resolve an `ImageId` to its pixmap data. + /// + /// Returns `None` if the image ID is not found in the registry. + fn resolve(&self, id: ImageId) -> Option<Arc<Pixmap>>; +} + +/// A no-op image resolver that always returns `None`. +#[derive(Debug, Clone, Copy, Default)] +pub struct NoOpImageResolver; + +impl ImageResolver for NoOpImageResolver { + fn resolve(&self, _id: ImageId) -> Option<Arc<Pixmap>> { + None + } +} + /// A premultiplied color. #[derive(Debug, Clone, PartialEq, Copy)] pub struct PremulColor {
diff --git a/sparse_strips/vello_cpu/src/dispatch/mod.rs b/sparse_strips/vello_cpu/src/dispatch/mod.rs index 63aa85d..dd7dc94 100644 --- a/sparse_strips/vello_cpu/src/dispatch/mod.rs +++ b/sparse_strips/vello_cpu/src/dispatch/mod.rs
@@ -13,7 +13,7 @@ use vello_common::encode::EncodedPaint; use vello_common::filter_effects::Filter; use vello_common::mask::Mask; -use vello_common::paint::Paint; +use vello_common::paint::{ImageResolver, Paint}; use vello_common::strip::Strip; use vello_common::strip_generator::StripStorage; @@ -77,6 +77,7 @@ width: u16, height: u16, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ); fn composite_at_offset( &self, @@ -89,6 +90,7 @@ dst_buffer_height: u16, render_mode: RenderMode, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ); fn strip_storage_mut(&mut self) -> &mut StripStorage; }
diff --git a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs index c2137be..5826368 100644 --- a/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs +++ b/sparse_strips/vello_cpu/src/dispatch/multi_threaded.rs
@@ -27,7 +27,7 @@ use vello_common::fearless_simd::{Level, Simd, dispatch}; use vello_common::filter_effects::Filter; use vello_common::mask::Mask; -use vello_common::paint::Paint; +use vello_common::paint::{ImageResolver, NoOpImageResolver, Paint}; use vello_common::render_graph::RenderGraph; use vello_common::strip::Strip; use vello_common::strip_generator::{StripGenerator, StripStorage}; @@ -170,9 +170,10 @@ width: u16, height: u16, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ) { use crate::fine::F32Kernel; - dispatch!(self.level, simd => self.rasterize_with::<_, F32Kernel>(simd, buffer, width, height, encoded_paints)); + dispatch!(self.level, simd => self.rasterize_with::<_, F32Kernel>(simd, buffer, width, height, encoded_paints, image_resolver)); } #[cfg(feature = "u8_pipeline")] @@ -182,9 +183,10 @@ width: u16, height: u16, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ) { use crate::fine::U8Kernel; - dispatch!(self.level, simd => self.rasterize_with::<_, U8Kernel>(simd, buffer, width, height, encoded_paints)); + dispatch!(self.level, simd => self.rasterize_with::<_, U8Kernel>(simd, buffer, width, height, encoded_paints, image_resolver)); } fn init(&mut self) { @@ -379,7 +381,12 @@ width: u16, height: u16, encoded_paints: &[EncodedPaint], + _image_resolver: &dyn ImageResolver, ) { + // Note: Multi-threaded dispatcher does not support ImageSource::OpaqueId. + // Images with OpaqueId will panic at rasterization time. + let noop_resolver = NoOpImageResolver; + let mut buffer = Regions::new(width, height, buffer); let fines = ThreadLocal::new(); let wide = &self.wide; @@ -408,7 +415,7 @@ let alphas = thread_idx .map(|i| alpha_slots[i as usize].as_slice()) .unwrap_or(&[]); - fine.run_cmd(cmd, alphas, encoded_paints, &wide.attrs); + fine.run_cmd(cmd, alphas, encoded_paints, &noop_resolver, &wide.attrs); } fine.pack(region); @@ -578,6 +585,7 @@ width: u16, height: u16, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ) { assert!(self.flushed, "attempted to rasterize before flushing"); @@ -585,21 +593,23 @@ #[cfg(all(feature = "u8_pipeline", not(feature = "f32_pipeline")))] { let _ = render_mode; - self.rasterize_u8(buffer, width, height, encoded_paints); + self.rasterize_u8(buffer, width, height, encoded_paints, image_resolver); } // Only f32 pipeline enabled #[cfg(all(feature = "f32_pipeline", not(feature = "u8_pipeline")))] { let _ = render_mode; - self.rasterize_f32(buffer, width, height, encoded_paints); + self.rasterize_f32(buffer, width, height, encoded_paints, image_resolver); } // Both pipelines enabled #[cfg(all(feature = "f32_pipeline", feature = "u8_pipeline"))] match render_mode { - RenderMode::OptimizeSpeed => self.rasterize_u8(buffer, width, height, encoded_paints), + RenderMode::OptimizeSpeed => { + self.rasterize_u8(buffer, width, height, encoded_paints, image_resolver); + } RenderMode::OptimizeQuality => { - self.rasterize_f32(buffer, width, height, encoded_paints); + self.rasterize_f32(buffer, width, height, encoded_paints, image_resolver); } } } @@ -615,6 +625,7 @@ _dst_buffer_height: u16, _render_mode: RenderMode, _encoded_paints: &[EncodedPaint], + _image_resolver: &dyn ImageResolver, ) { // TODO: Implement composite_at_offset for multi-threaded dispatcher. unimplemented!("composite_at_offset is not implemented for multi-threaded dispatcher");
diff --git a/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs b/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs index 1467db2..5eccdba 100644 --- a/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs +++ b/sparse_strips/vello_cpu/src/dispatch/single_threaded.rs
@@ -15,7 +15,7 @@ use vello_common::fearless_simd::{Level, Simd}; use vello_common::filter_effects::Filter; use vello_common::mask::Mask; -use vello_common::paint::{Paint, PremulColor}; +use vello_common::paint::{ImageResolver, Paint, PremulColor}; use vello_common::pixmap::Pixmap; use vello_common::render_graph::{RenderGraph, RenderNodeKind}; use vello_common::strip::Strip; @@ -94,10 +94,11 @@ width: u16, height: u16, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ) { use crate::fine::F32Kernel; use vello_common::fearless_simd::dispatch; - dispatch!(self.level, simd => self.rasterize_with::<_, F32Kernel>(simd, buffer, width, height, encoded_paints)); + dispatch!(self.level, simd => self.rasterize_with::<_, F32Kernel>(simd, buffer, width, height, encoded_paints, image_resolver)); } /// Rasterizes the scene using u8 precision (fast). @@ -111,10 +112,11 @@ width: u16, height: u16, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ) { use crate::fine::U8Kernel; use vello_common::fearless_simd::dispatch; - dispatch!(self.level, simd => self.rasterize_with::<_, U8Kernel>(simd, buffer, width, height, encoded_paints)); + dispatch!(self.level, simd => self.rasterize_with::<_, U8Kernel>(simd, buffer, width, height, encoded_paints, image_resolver)); } /// Core rasterization dispatcher that chooses between simple and filter-aware paths. @@ -132,6 +134,7 @@ width: u16, height: u16, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ) { let mut layer_manager = LayerManager::new(); @@ -143,11 +146,19 @@ width, height, encoded_paints, + image_resolver, &mut layer_manager, ); } else { // Use simple direct rasterization for scenes without filters. - self.rasterize_simple::<S, F>(simd, buffer, width, height, encoded_paints); + self.rasterize_simple::<S, F>( + simd, + buffer, + width, + height, + encoded_paints, + image_resolver, + ); } } @@ -168,6 +179,7 @@ width: u16, height: u16, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, layer_manager: &mut LayerManager, ) { let mut fine = Fine::<S, F>::new(simd); @@ -206,6 +218,7 @@ PremulColor::from_alpha_color(TRANSPARENT), layer_manager, encoded_paints, + image_resolver, ); debug_assert_eq!( @@ -244,6 +257,7 @@ bg, layer_manager, encoded_paints, + image_resolver, ); debug_assert_eq!( @@ -274,6 +288,7 @@ /// * `clear_color` - Initial color for the tile. /// * `layer_manager` - Storage for filtered layer buffers. /// * `encoded_paints` - Paint definitions for the scene. + /// * `image_resolver` - Resolver for looking up opaque image IDs. fn process_layer_tile<S: Simd, F: FineKernel<S>>( &self, fine: &mut Fine<S, F>, @@ -283,6 +298,7 @@ clear_color: PremulColor, layer_manager: &mut LayerManager, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ) { let wtile = &self.wide.get(x, y); fine.set_coords(x, y); @@ -300,6 +316,7 @@ cmd, &self.strip_storage.alphas, encoded_paints, + image_resolver, &self.wide.attrs, ); @@ -330,6 +347,7 @@ &wtile.cmds[cmd_idx + 1], &self.strip_storage.alphas, encoded_paints, + image_resolver, &self.wide.attrs, ); cmd_idx += 1; @@ -372,6 +390,7 @@ width: u16, height: u16, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ) { let mut regions = Regions::new(width, height, buffer); let mut fine = Fine::<S, F>::new(simd); @@ -390,6 +409,7 @@ cmd, &self.strip_storage.alphas, encoded_paints, + image_resolver, &self.wide.attrs, ); } @@ -415,11 +435,12 @@ dst_buffer_width: u16, dst_buffer_height: u16, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ) { use crate::fine::F32Kernel; use vello_common::fearless_simd::dispatch; dispatch!(self.level, simd => self.composite_at_offset_with::<_, F32Kernel>( - simd, buffer, width, height, dst_x, dst_y, dst_buffer_width, dst_buffer_height, encoded_paints + simd, buffer, width, height, dst_x, dst_y, dst_buffer_width, dst_buffer_height, encoded_paints, image_resolver )); } @@ -435,11 +456,12 @@ dst_buffer_width: u16, dst_buffer_height: u16, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ) { use crate::fine::U8Kernel; use vello_common::fearless_simd::dispatch; dispatch!(self.level, simd => self.composite_at_offset_with::<_, U8Kernel>( - simd, buffer, width, height, dst_x, dst_y, dst_buffer_width, dst_buffer_height, encoded_paints + simd, buffer, width, height, dst_x, dst_y, dst_buffer_width, dst_buffer_height, encoded_paints, image_resolver )); } @@ -458,6 +480,7 @@ dst_buffer_width: u16, dst_buffer_height: u16, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ) { let mut regions = Regions::new_at_offset( width, @@ -485,6 +508,7 @@ cmd, &self.strip_storage.alphas, encoded_paints, + image_resolver, &self.wide.attrs, ); } @@ -651,19 +675,20 @@ width: u16, height: u16, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ) { // If only the u8 pipeline is enabled, then use it #[cfg(all(feature = "u8_pipeline", not(feature = "f32_pipeline")))] { let _ = render_mode; - self.rasterize_u8(buffer, width, height, encoded_paints); + self.rasterize_u8(buffer, width, height, encoded_paints, image_resolver); } // If only the f32 pipeline is enabled, then use it #[cfg(all(feature = "f32_pipeline", not(feature = "u8_pipeline")))] { let _ = render_mode; - self.rasterize_f32(buffer, width, height, encoded_paints); + self.rasterize_f32(buffer, width, height, encoded_paints, image_resolver); } // If both pipelines are enabled, select precision based on render mode parameter. @@ -671,11 +696,11 @@ match render_mode { RenderMode::OptimizeSpeed => { // Use u8 precision for faster rendering. - self.rasterize_u8(buffer, width, height, encoded_paints); + self.rasterize_u8(buffer, width, height, encoded_paints, image_resolver); } RenderMode::OptimizeQuality => { // Use f32 precision for higher quality. - self.rasterize_f32(buffer, width, height, encoded_paints); + self.rasterize_f32(buffer, width, height, encoded_paints, image_resolver); } } @@ -683,7 +708,14 @@ { // This case never gets hit because there is a compile_error in the root. // But have this code disables some warnings and makes the compile error easier to read - let _ = (buffer, render_mode, width, height, encoded_paints); + let _ = ( + buffer, + render_mode, + width, + height, + encoded_paints, + image_resolver, + ); } } @@ -698,6 +730,7 @@ dst_buffer_height: u16, render_mode: RenderMode, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, ) { #[cfg(all(feature = "u8_pipeline", not(feature = "f32_pipeline")))] { @@ -711,6 +744,7 @@ dst_buffer_width, dst_buffer_height, encoded_paints, + image_resolver, ); } @@ -726,6 +760,7 @@ dst_buffer_width, dst_buffer_height, encoded_paints, + image_resolver, ); } @@ -741,6 +776,7 @@ dst_buffer_width, dst_buffer_height, encoded_paints, + image_resolver, ); } RenderMode::OptimizeQuality => { @@ -753,6 +789,7 @@ dst_buffer_width, dst_buffer_height, encoded_paints, + image_resolver, ); } } @@ -769,6 +806,7 @@ dst_buffer_height, render_mode, encoded_paints, + image_resolver, ); } }
diff --git a/sparse_strips/vello_cpu/src/fine/mod.rs b/sparse_strips/vello_cpu/src/fine/mod.rs index 71c4da3..8ae7cc7 100644 --- a/sparse_strips/vello_cpu/src/fine/mod.rs +++ b/sparse_strips/vello_cpu/src/fine/mod.rs
@@ -36,7 +36,7 @@ use vello_common::filter_effects::Filter; use vello_common::kurbo::Affine; use vello_common::mask::Mask; -use vello_common::paint::{ImageSource, Paint, PremulColor}; +use vello_common::paint::{ImageResolver, ImageSource, Paint, PremulColor}; use vello_common::pixmap::Pixmap; use vello_common::simd::Splat4thExt; use vello_common::tile::Tile; @@ -536,6 +536,7 @@ cmd: &Cmd, alphas: &[u8], paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, attrs: &CommandAttrs, ) { match cmd { @@ -547,6 +548,7 @@ &fill_attrs.paint, fill_attrs.blend_mode, paints, + image_resolver, None, fill_attrs.mask.as_ref(), ); @@ -560,6 +562,7 @@ &fill_attrs.paint, fill_attrs.blend_mode, paints, + image_resolver, Some(&alphas[alpha_idx..]), fill_attrs.mask.as_ref(), ); @@ -661,6 +664,7 @@ fill: &Paint, blend_mode: BlendMode, encoded_paints: &[EncodedPaint], + image_resolver: &dyn ImageResolver, alphas: Option<&[u8]>, mask: Option<&Mask>, ) { @@ -813,8 +817,11 @@ } } EncodedPaint::Image(i) => { - let ImageSource::Pixmap(pixmap) = &i.source else { - panic!("vello_cpu doesn't support the opaque image source."); + let pixmap = match &i.source { + ImageSource::Pixmap(p) => p.clone(), + ImageSource::OpaqueId(id) => image_resolver + .resolve(*id) + .unwrap_or_else(|| panic!("Image {:?} not found in registry", id)), }; match (i.has_skew(), i.nearest_neighbor()) { @@ -824,14 +831,14 @@ fill_complex_paint!( i.may_have_opacities, T::plain_medium_quality_image_painter( - self.simd, i, pixmap, start_x, start_y + self.simd, i, &pixmap, start_x, start_y ) ); } else { fill_complex_paint!( i.may_have_opacities, T::high_quality_image_painter( - self.simd, i, pixmap, start_x, start_y + self.simd, i, &pixmap, start_x, start_y ) ); } @@ -842,14 +849,14 @@ fill_complex_paint!( i.may_have_opacities, T::medium_quality_image_painter( - self.simd, i, pixmap, start_x, start_y + self.simd, i, &pixmap, start_x, start_y ) ); } else { fill_complex_paint!( i.may_have_opacities, T::high_quality_image_painter( - self.simd, i, pixmap, start_x, start_y + self.simd, i, &pixmap, start_x, start_y ) ); } @@ -858,14 +865,14 @@ fill_complex_paint!( i.may_have_opacities, T::plain_nn_image_painter( - self.simd, i, pixmap, start_x, start_y + self.simd, i, &pixmap, start_x, start_y ) ); } (true, true) => { fill_complex_paint!( i.may_have_opacities, - T::nn_image_painter(self.simd, i, pixmap, start_x, start_y) + T::nn_image_painter(self.simd, i, &pixmap, start_x, start_y) ); } }
diff --git a/sparse_strips/vello_cpu/src/lib.rs b/sparse_strips/vello_cpu/src/lib.rs index 7e5ba41..9ae475a 100644 --- a/sparse_strips/vello_cpu/src/lib.rs +++ b/sparse_strips/vello_cpu/src/lib.rs
@@ -153,7 +153,7 @@ #[doc(hidden)] pub mod region; -pub use render::{RenderContext, RenderSettings}; +pub use render::{RenderContext, RenderSettings, RenderState}; pub use vello_common::fearless_simd::Level; #[cfg(feature = "text")] pub use vello_common::glyph::Glyph;
diff --git a/sparse_strips/vello_cpu/src/render.rs b/sparse_strips/vello_cpu/src/render.rs index 58f14fa..72d82c8 100644 --- a/sparse_strips/vello_cpu/src/render.rs +++ b/sparse_strips/vello_cpu/src/render.rs
@@ -11,17 +11,19 @@ use crate::dispatch::single_threaded::SingleThreadedDispatcher; use crate::kurbo::{PathEl, Point}; use alloc::boxed::Box; -#[cfg(feature = "text")] use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; +use hashbrown::HashMap; use vello_common::blurred_rounded_rect::BlurredRoundedRectangle; use vello_common::encode::{EncodeExt, EncodedPaint}; use vello_common::fearless_simd::Level; use vello_common::filter_effects::Filter; use vello_common::kurbo::{Affine, BezPath, Cap, Join, Rect, Stroke}; use vello_common::mask::Mask; -use vello_common::paint::{Paint, PaintType}; +#[cfg(feature = "text")] +use vello_common::paint::{Image, ImageSource}; +use vello_common::paint::{ImageId, ImageResolver, Paint, PaintType}; use vello_common::peniko::color::palette::css::BLACK; use vello_common::peniko::{BlendMode, Fill}; use vello_common::pixmap::Pixmap; @@ -33,7 +35,6 @@ color::{AlphaColor, Srgb}, colr::{ColrPainter, ColrRenderer}, glyph::{GlyphCaches, GlyphRenderer, GlyphRunBuilder, GlyphType, PreparedGlyph}, - paint::ImageSource, }; /// A render context for CPU-based 2D graphics rendering. @@ -75,6 +76,13 @@ dispatcher: Box<dyn Dispatcher>, #[cfg(feature = "text")] pub(crate) glyph_caches: Option<GlyphCaches>, + /// Image registry for resolving `ImageSource::OpaqueId` to pixmap data. + /// + /// This allows decoupling render commands from image data, enabling + /// patterns like spritesheet rendering. + image_registry: HashMap<u32, Arc<Pixmap>>, + /// Counter for generating unique image IDs. + next_image_id: u32, } /// Settings to apply to the render context. @@ -170,6 +178,8 @@ filter: None, #[cfg(feature = "text")] glyph_caches: Some(GlyphCaches::default()), + image_registry: HashMap::new(), + next_image_id: 0, } } @@ -424,6 +434,10 @@ } /// Set the current paint. + /// + /// If the paint is an image with `ImageSource::OpaqueId`, it will be + /// resolved to the corresponding pixmap at rasterization time. + /// Make sure to register images with [`register_image`](Self::register_image) first. pub fn set_paint(&mut self, paint: impl Into<PaintType>) { self.paint = paint.into(); } @@ -525,6 +539,7 @@ #[cfg(feature = "text")] self.glyph_caches.as_mut().unwrap().maintain(); self.blend_mode = BlendMode::default(); + self.clear_images(); } /// Push a new clip path to the clip stack. @@ -578,8 +593,14 @@ buffer.len(), ); - self.dispatcher - .rasterize(buffer, render_mode, width, height, &self.encoded_paints); + self.dispatcher.rasterize( + buffer, + render_mode, + width, + height, + &self.encoded_paints, + self, + ); } /// Render the current context into a pixmap. @@ -624,6 +645,7 @@ dst_buffer_height, self.render_settings.render_mode, &self.encoded_paints, + self, ); } @@ -655,6 +677,68 @@ f(self); } } + + /// Save the current rendering state. + pub fn take_current_state(&mut self) -> RenderState { + RenderState { + paint: self.paint.clone(), + paint_transform: self.paint_transform, + transform: self.transform, + fill_rule: self.fill_rule, + stroke: core::mem::take(&mut self.stroke), + } + } + + /// Restore the saved rendering state. + pub fn restore_state(&mut self, state: RenderState) { + self.transform = state.transform; + self.fill_rule = state.fill_rule; + self.stroke = state.stroke; + self.paint = state.paint; + self.paint_transform = state.paint_transform; + } +} + +/// Image registry implementation. +impl RenderContext { + /// Register a pixmap in the image registry and return its [`ImageId`]. + pub fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId { + let id = self.next_image_id; + self.next_image_id += 1; + self.image_registry.insert(id, pixmap); + ImageId::new(id) + } + + /// Update an existing image in the registry with new pixmap data. + pub fn update_image(&mut self, id: ImageId, pixmap: Arc<Pixmap>) { + debug_assert!( + self.image_registry.contains_key(&id.as_u32()), + "Cannot update unregistered image {id:?}" + ); + self.image_registry.insert(id.as_u32(), pixmap); + } + + /// Remove an image from the registry. + pub fn destroy_image(&mut self, id: ImageId) -> bool { + self.image_registry.remove(&id.as_u32()).is_some() + } + + /// Resolve an `ImageId` to its pixmap data. + pub fn resolve_image(&self, id: ImageId) -> Option<Arc<Pixmap>> { + self.image_registry.get(&id.as_u32()).cloned() + } + + /// Clear the image registry. + pub fn clear_images(&mut self) { + self.image_registry.clear(); + self.next_image_id = 0; + } +} + +impl ImageResolver for RenderContext { + fn resolve(&self, id: ImageId) -> Option<Arc<Pixmap>> { + self.image_registry.get(&id.as_u32()).cloned() + } } #[cfg(feature = "text")] @@ -692,7 +776,7 @@ crate::peniko::ImageQuality::Medium }; - let image = vello_common::paint::Image { + let image = Image { image: ImageSource::Pixmap(Arc::new(glyph.pixmap)), sampler: ImageSampler { x_extend: crate::peniko::Extend::Pad, @@ -747,7 +831,7 @@ let has_skew = prepared_glyph.transform.as_coeffs()[1] != 0.0 || prepared_glyph.transform.as_coeffs()[2] != 0.0; - let image = vello_common::paint::Image { + let image = Image { image: ImageSource::Pixmap(Arc::new(glyph_pixmap)), sampler: ImageSampler { x_extend: crate::peniko::Extend::Pad, @@ -954,7 +1038,7 @@ /// Saved state for recording operations. #[derive(Debug)] -struct RenderState { +pub struct RenderState { transform: Affine, fill_rule: Fill, stroke: Stroke, @@ -1138,26 +1222,6 @@ }) .collect() } - - /// Save the current rendering state. - fn take_current_state(&mut self) -> RenderState { - RenderState { - paint: self.paint.clone(), - paint_transform: self.paint_transform, - transform: self.transform, - fill_rule: self.fill_rule, - stroke: core::mem::take(&mut self.stroke), - } - } - - /// Restore the saved rendering state. - fn restore_state(&mut self, state: RenderState) { - self.transform = state.transform; - self.fill_rule = state.fill_rule; - self.stroke = state.stroke; - self.paint = state.paint; - self.paint_transform = state.paint_transform; - } } #[cfg(test)]
diff --git a/sparse_strips/vello_sparse_tests/snapshots/image_spritesheet.png b/sparse_strips/vello_sparse_tests/snapshots/image_spritesheet.png new file mode 100644 index 0000000..cd0fa4e --- /dev/null +++ b/sparse_strips/vello_sparse_tests/snapshots/image_spritesheet.png
@@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c939bbb46cd4cfd247c945967f48f064321b06ca3577d774b54d0c0a8e800ec +size 289
diff --git a/sparse_strips/vello_sparse_tests/tests/assets/glyph_atlas.png b/sparse_strips/vello_sparse_tests/tests/assets/glyph_atlas.png new file mode 100644 index 0000000..0aba633 --- /dev/null +++ b/sparse_strips/vello_sparse_tests/tests/assets/glyph_atlas.png Binary files differ
diff --git a/sparse_strips/vello_sparse_tests/tests/image.rs b/sparse_strips/vello_sparse_tests/tests/image.rs index 33b50f3..d538dcc 100644 --- a/sparse_strips/vello_sparse_tests/tests/image.rs +++ b/sparse_strips/vello_sparse_tests/tests/image.rs
@@ -562,3 +562,79 @@ ctx.pop_layer(); ctx.pop_layer(); } + +/// Parameters for a glyph sprite from an atlas. +#[derive(Clone, Copy)] +struct Sprite { + /// X position of the sprite within the atlas + atlas_x: f64, + /// Y position of the sprite within the atlas + atlas_y: f64, + /// Width of the sprite + width: f64, + /// Height of the sprite + height: f64, + /// Vertical offset from baseline (positive = render lower) + y_offset: f64, +} + +impl Sprite { + const fn new(atlas_x: f64, atlas_y: f64, width: f64, height: f64, y_offset: f64) -> Self { + Self { + atlas_x, + atlas_y, + width, + height, + y_offset, + } + } +} + +/// Sprites for "hello" from the glyph atlas. +const HELLO_WORLD: &[Sprite] = &[ + Sprite::new(1.0, 46.0, 10.0, 14.0, 0.0), // 'h' + Sprite::new(68.0, 0.0, 10.0, 12.0, 2.0), // 'e' + Sprite::new(27.0, 53.0, 5.0, 14.0, 0.0), // 'l' + Sprite::new(27.0, 53.0, 5.0, 14.0, 0.0), // 'l' + Sprite::new(80.0, 0.0, 10.0, 12.0, 2.0), // 'o' +]; + +/// Test rendering "hello" from a glyph atlas (spritesheet-style). +/// Uses `ImageSource::OpaqueId` to demonstrate the image registry pattern. +#[vello_test(width = 60, height = 30, skip_hybrid, skip_multithreaded)] +fn image_spritesheet(ctx: &mut impl Renderer) { + let atlas_id = ctx.register_image(load_image!("glyph_atlas")); + let atlas_src = ImageSource::OpaqueId(atlas_id); + + let start_x = 10.0; + let start_y = 8.0; + + let mut cursor_x = start_x; + + for glyph in HELLO_WORLD { + render_sprite(ctx, &atlas_src, glyph, cursor_x, start_y); + cursor_x += glyph.width; + } +} + +/// Render a sprite from an atlas/spritesheet at a screen position. +fn render_sprite( + ctx: &mut impl Renderer, + atlas_src: &ImageSource, + glyph: &Sprite, + screen_x: f64, + screen_y: f64, +) { + ctx.set_transform(Affine::translate((screen_x, screen_y + glyph.y_offset))); + ctx.set_paint_transform(Affine::translate((-glyph.atlas_x, -glyph.atlas_y))); + ctx.set_paint(Image { + image: atlas_src.clone(), + sampler: ImageSampler { + x_extend: Extend::Pad, + y_extend: Extend::Pad, + quality: ImageQuality::Low, + alpha: 1.0, + }, + }); + ctx.fill_rect(&Rect::new(0.0, 0.0, glyph.width, glyph.height)); +}
diff --git a/sparse_strips/vello_sparse_tests/tests/renderer.rs b/sparse_strips/vello_sparse_tests/tests/renderer.rs index 4827b0a..a0ddc2c 100644 --- a/sparse_strips/vello_sparse_tests/tests/renderer.rs +++ b/sparse_strips/vello_sparse_tests/tests/renderer.rs
@@ -8,7 +8,7 @@ use vello_common::glyph::{GlyphRenderer, GlyphRunBuilder}; use vello_common::kurbo::{Affine, BezPath, Rect, Stroke}; use vello_common::mask::Mask; -use vello_common::paint::{ImageSource, PaintType}; +use vello_common::paint::{ImageId, ImageSource, PaintType}; use vello_common::peniko::{BlendMode, Fill, FontData}; use vello_common::pixmap::Pixmap; use vello_common::recording::{Recordable, Recorder, Recording}; @@ -64,6 +64,7 @@ fn width(&self) -> u16; fn height(&self) -> u16; fn get_image_source(&mut self, pixmap: Arc<Pixmap>) -> ImageSource; + fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId; fn record(&mut self, recording: &mut Recording, f: impl FnOnce(&mut Recorder<'_>)); fn prepare_recording(&mut self, recording: &mut Recording); fn execute_recording(&mut self, recording: &Recording); @@ -215,6 +216,10 @@ ImageSource::Pixmap(pixmap) } + fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId { + Self::register_image(self, pixmap) + } + fn record(&mut self, recording: &mut Recording, f: impl FnOnce(&mut Recorder<'_>)) { Recordable::record(self, recording, f); } @@ -550,6 +555,25 @@ ImageSource::OpaqueId(image_id) } + fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId { + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Register Test Image"), + }); + + let image_id = self.renderer.borrow_mut().upload_image( + &self.device, + &self.queue, + &mut encoder, + &pixmap, + ); + + self.queue.submit([encoder.finish()]); + + image_id + } + fn record(&mut self, recording: &mut Recording, f: impl FnOnce(&mut Recorder<'_>)) { Recordable::record(&mut self.scene, recording, f); } @@ -772,6 +796,10 @@ ImageSource::OpaqueId(image_id) } + fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId { + self.renderer.borrow_mut().upload_image(&pixmap) + } + fn record(&mut self, recording: &mut Recording, f: impl FnOnce(&mut Recorder<'_>)) { self.scene.record(recording, f); }