vello_gpu/vello_cpu: Remove image uploads
diff --git a/glifo/src/glyph.rs b/glifo/src/glyph.rs index 6ca2604..b0bea9f 100644 --- a/glifo/src/glyph.rs +++ b/glifo/src/glyph.rs
@@ -2421,7 +2421,7 @@ } fn atlas_image_source(&self, atlas_slot: &AtlasSlot) -> ImageSource { - ImageSource::opaque_id(ImageId::new(atlas_slot.page_index)) + ImageSource::Atlas(ImageId::new(atlas_slot.page_index)) } fn atlas_paint_transform(&self, atlas_slot: &AtlasSlot) -> Affine {
diff --git a/vello_bench/src/fine/fill.rs b/vello_bench/src/fine/fill.rs index b91bdc1..64baa32 100644 --- a/vello_bench/src/fine/fill.rs +++ b/vello_bench/src/fine/fill.rs
@@ -1,12 +1,12 @@ // Copyright 2025 the Vello Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use crate::fine::default_blend; +use crate::fine::{NoOpImageResolver, default_blend}; use criterion::{Bencher, Criterion}; use vello_common::color::palette::css::ROYAL_BLUE; use vello_common::encode::EncodedPaint; use vello_common::fearless_simd::Simd; -use vello_common::paint::{NoOpImageResolver, Paint, PremulColor}; +use vello_common::paint::{Paint, PremulColor}; use vello_common::peniko::BlendMode; use vello_cpu::fine::{Fine, FineKernel, FineResources, PaintFillAttrs}; use vello_cpu::span::{Span, TileAlignedSpan};
diff --git a/vello_bench/src/fine/mod.rs b/vello_bench/src/fine/mod.rs index 83d1a17..c46dc93 100644 --- a/vello_bench/src/fine/mod.rs +++ b/vello_bench/src/fine/mod.rs
@@ -15,11 +15,22 @@ pub use image::*; pub use pack::*; pub use rounded_blurred_rect::*; +use std::sync::Arc; pub use strip::*; +use vello_common::paint::{ImageId, ImageResolver}; use vello_common::peniko::{BlendMode, Compose, Mix}; +use vello_common::pixmap::Pixmap; pub(crate) const BENCH_WIDTH: u16 = 256; +pub(crate) struct NoOpImageResolver; + +impl ImageResolver for NoOpImageResolver { + fn resolve(&self, _id: ImageId) -> Option<Arc<Pixmap>> { + None + } +} + pub(crate) fn default_blend() -> BlendMode { BlendMode::new(Mix::Normal, Compose::SrcOver) }
diff --git a/vello_bench/src/fine/strip.rs b/vello_bench/src/fine/strip.rs index 15bde1c..cb36ce2 100644 --- a/vello_bench/src/fine/strip.rs +++ b/vello_bench/src/fine/strip.rs
@@ -2,14 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT use crate::SEED; -use crate::fine::{BENCH_WIDTH, default_blend}; +use crate::fine::{BENCH_WIDTH, NoOpImageResolver, default_blend}; use criterion::{Bencher, Criterion}; use rand::prelude::StdRng; use rand::{Rng, SeedableRng}; use vello_common::color::palette::css::ROYAL_BLUE; use vello_common::encode::EncodedPaint; use vello_common::fearless_simd::Simd; -use vello_common::paint::{NoOpImageResolver, Paint, PremulColor}; +use vello_common::paint::{Paint, PremulColor}; use vello_common::tile::Tile; use vello_cpu::fine::{Fine, FineKernel, FineResources, PaintFillAttrs}; use vello_cpu::span::{Span, TileAlignedSpan};
diff --git a/vello_common/CHANGELOG.md b/vello_common/CHANGELOG.md index 1fe6a7b..3366548 100644 --- a/vello_common/CHANGELOG.md +++ b/vello_common/CHANGELOG.md
@@ -20,6 +20,7 @@ - Breaking change: `FilterPrimitive::GaussianBlur`'s `std_deviation` is now `std_deviation_x` and `std_deviation_y`, `filter::gaussian_blur::GaussianBlur` carries a decimation plan and kernel per axis, and `DecimationSizer` scales the axes independently through `downscale_axes` and `upscale_axes`. ([#1997][] by [@theashraf][]) - A blur under a non-uniform scale or skew is scaled per axis instead of by the average scale factor, so its device-space extent follows the transform. ([#1997][] by [@theashraf][]) +- Breaking change: `ImageSource::OpaqueId`, `ImageSource::opaque_id`, and `ImageSource::opaque_id_with_transparency_hint` have been replaced by the hidden `ImageSource::Atlas`, which is only used internally for glyph caching. `ImageResolver` is now hidden as well, and `NoOpImageResolver` has been removed. Use `ImageSource::Pixmap` or `ImageSource::external_texture` instead. (by [@LaurenzV][]) ### Fixed
diff --git a/vello_common/src/encode.rs b/vello_common/src/encode.rs index 2245c5f..bbaa560 100644 --- a/vello_common/src/encode.rs +++ b/vello_common/src/encode.rs
@@ -1226,11 +1226,12 @@ #[cfg(test)] mod tests { use super::{EncodeExt, EncodedPaint, Gradient}; + use crate::TextureId; use crate::blurred_rounded_rect::BlurredRoundedRectangle; use crate::color::DynamicColor; use crate::color::palette::css::{BLACK, BLUE, GREEN}; use crate::kurbo::{Affine, Point, Rect}; - use crate::paint::{Image, ImageId, ImageSource, Tint, TintMode}; + use crate::paint::{Image, ImageSource, TextureRegion, Tint, TintMode}; use crate::peniko::{ColorStop, ColorStops}; use alloc::vec; use peniko::{ @@ -1472,7 +1473,7 @@ #[test] fn image_sampler_alpha_with_tint() { let image = Image { - image: ImageSource::opaque_id_with_transparency_hint(ImageId::new(1), false), + image: ImageSource::external_texture(TextureId(1), TextureRegion::Full, false), sampler: ImageSampler { alpha: 0.5, ..ImageSampler::default() @@ -1505,7 +1506,7 @@ #[test] fn image_sampler_alpha_with_tint_is_clamped() { let image = Image { - image: ImageSource::opaque_id_with_transparency_hint(ImageId::new(1), false), + image: ImageSource::external_texture(TextureId(1), TextureRegion::Full, false), sampler: ImageSampler { alpha: 2.0, ..ImageSampler::default() @@ -1555,7 +1556,7 @@ fn opaque_image() -> Image { Image { - image: ImageSource::opaque_id_with_transparency_hint(ImageId::new(1), false), + image: ImageSource::external_texture(TextureId(1), TextureRegion::Full, false), sampler: ImageSampler::default(), } }
diff --git a/vello_common/src/lib.rs b/vello_common/src/lib.rs index 58e9ead..8cf9c3a 100644 --- a/vello_common/src/lib.rs +++ b/vello_common/src/lib.rs
@@ -105,9 +105,7 @@ pub use crate::filter_effects; pub use crate::geometry; pub use crate::mask::Mask; - pub use crate::paint::{ - Image, ImageId, ImageSource, PaintType, TextureRegion, Tint, TintMode, - }; + pub use crate::paint::{Image, ImageSource, PaintType, TextureRegion, Tint, TintMode}; pub use crate::pixmap::{PixelMetadata, Pixels, Pixmap}; pub use crate::render_state::RenderState; pub use crate::transforms::Transforms; @@ -126,10 +124,7 @@ pub use super::shared::*; pub use crate::TextureId; pub use crate::geometry::{RectU16, SizeU16}; - pub use crate::multi_atlas::{ - AllocationStrategy, AtlasConfig, AtlasError, AtlasId, AtlasLayerDiagnostics, - AtlasSpaceDiagnostics, - }; + pub use crate::multi_atlas::{AllocationStrategy, AtlasConfig}; #[cfg(feature = "probe")] pub use crate::probe::{ ALL_PROBE_ELEMENTS, CellStatistics, Probe, ProbeFeature, ProbeResult,
diff --git a/vello_common/src/paint.rs b/vello_common/src/paint.rs index 4d25100..9978b49 100644 --- a/vello_common/src/paint.rs +++ b/vello_common/src/paint.rs
@@ -89,14 +89,11 @@ pub enum ImageSource { /// Pixmap pixels travel with the scene packet. Pixmap(Arc<Pixmap>), - // TODO: Explore whether we can merge opaque ID and external texture in some form? - /// Pixmap pixels were registered earlier; this is just a handle. - OpaqueId { - /// The image handle. - id: ImageId, - /// Whether the image may contain non-opaque pixels. - may_have_transparency: bool, - }, + /// An entry in a renderer's internal glyph atlas. + /// + /// This is used by the glyph caching backends and should not be constructed manually. + #[doc(hidden)] + Atlas(ImageId), /// An externally owned texture supplied to the renderer at render time. ExternalTexture { /// Opaque external texture handle. @@ -109,26 +106,6 @@ } impl ImageSource { - /// Create an [`ImageSource`] from a pre-registered image handle. - /// - /// Conservatively assumes the image may have non-opaque pixels. - /// Use [`Self::opaque_id_with_transparency_hint`] when you know the image is fully opaque. - pub fn opaque_id(id: ImageId) -> Self { - Self::OpaqueId { - id, - may_have_transparency: true, - } - } - - /// Create an [`ImageSource`] from a pre-registered image handle, - /// with an explicit hint about whether the image may have non-opaque pixels. - pub fn opaque_id_with_transparency_hint(id: ImageId, may_have_transparency: bool) -> Self { - Self::OpaqueId { - id, - may_have_transparency, - } - } - /// Create an image source backed by a texture supplied to the renderer at render time. /// /// # Panics @@ -155,11 +132,8 @@ pub fn may_have_transparency(&self) -> bool { match self { Self::Pixmap(p) => p.may_have_transparency(), - Self::OpaqueId { - may_have_transparency, - .. - } - | Self::ExternalTexture { + Self::Atlas(_) => true, + Self::ExternalTexture { may_have_transparency, .. } => *may_have_transparency, @@ -214,12 +188,12 @@ /// An image. pub type Image = peniko::ImageBrush<ImageSource>; -/// Trait for resolving opaque image IDs to pixmaps at rasterization time. +/// Trait for resolving atlas 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. +/// This allows delaying the resolution of `ImageSource::Atlas` until the +/// image is actually needed during rasterization, so that the atlas may be +/// updated between encoding and rendering. +#[doc(hidden)] pub trait ImageResolver: Send + Sync { /// Resolve an `ImageId` to its pixmap data. /// @@ -230,16 +204,6 @@ 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/vello_cpu/CHANGELOG.md b/vello_cpu/CHANGELOG.md index 3f898e5..09b4b1a 100644 --- a/vello_cpu/CHANGELOG.md +++ b/vello_cpu/CHANGELOG.md
@@ -20,6 +20,11 @@ ### Changed - Breaking change: The `invert` parameter of `RenderContext::fill_blurred_rounded_rect` has been removed. Use `RenderContext::fill_blurred_rounded_rect_in` with `invert` set to `true` instead. ([#1718][] by [@nicoburns][]) +- Breaking change: `ImageSource::OpaqueId`, `ImageSource::opaque_id`, and `ImageSource::opaque_id_with_transparency_hint` have been replaced by the hidden `ImageSource::Atlas`, which is only used internally for glyph caching. Use `ImageSource::Pixmap` instead. (by [@LaurenzV][]) + +### Removed + +- Breaking change: The image registry (`Resources::register_image`, `Resources::destroy_image`, `Resources::resolve_image`, and `Resources::clear_images`), and the `ImageId` re-export. Use `ImageSource::Pixmap` instead. (by [@LaurenzV][]) ### Fixed
diff --git a/vello_cpu/examples/paints.rs b/vello_cpu/examples/paints.rs index 161d35f..0c7a917 100644 --- a/vello_cpu/examples/paints.rs +++ b/vello_cpu/examples/paints.rs
@@ -121,8 +121,6 @@ let pixmap = Pixmap::from_png(Cursor::new(file)).unwrap(); Image { - // Note that only ImageSource::Pixmap is currently supported. Don't - // use ImageSource::OpaqueId. image: ImageSource::Pixmap(Arc::new(pixmap)), sampler: ImageSampler { x_extend: Extend::Repeat,
diff --git a/vello_cpu/src/fine/mod.rs b/vello_cpu/src/fine/mod.rs index f3f568e..69076ce 100644 --- a/vello_cpu/src/fine/mod.rs +++ b/vello_cpu/src/fine/mod.rs
@@ -1080,7 +1080,7 @@ EncodedPaint::Image(image) => { let pixmap = match &image.source { ImageSource::Pixmap(pixmap) => pixmap.clone(), - ImageSource::OpaqueId { id, .. } => resources + ImageSource::Atlas(id) => resources .image_resolver .resolve(*id) .unwrap_or_else(|| panic!("Image {:?} not found in registry", id)),
diff --git a/vello_cpu/src/render.rs b/vello_cpu/src/render.rs index aaa8adc..c3b4b63 100644 --- a/vello_cpu/src/render.rs +++ b/vello_cpu/src/render.rs
@@ -37,25 +37,6 @@ #[cfg(feature = "text")] pub(crate) const DEFAULT_GLYPH_ATLAS_SIZE: u16 = 4096; -// Why do we need this? The reason is that the way uploaded images work in Vello GPU -// is different from how they work in Vello CPU. -// -// In Vello GPU, all images, regardless of whether they are user-uploaded -// images or cached glyphs, are stored in an image atlas at a certain location. An image ID then -// uniquely resolves to an atlas page index + a location on that page. Whenever we want to -// cache a new glyph, we simply allocate a location in the image atlas and then return the image -// ID associated with that location. -// -// On Vello CPU, it works differently: An image ID is associated with a complete pixmap. -// If a user uploads an image, instead of blitting it into a bigger image atlas, we just -// store the user-provided pixmap and associate an image ID with the whole pixmap. However, -// for glyph caching to work we need the same semantics as in Vello GPU. Therefore, we -// use a marker to determine whether an image ID refers to a normal uploaded image or a cached -// glyph and apply special handling based on that. -// -// All IDs < than this value are reserved for normal images, all IDs >= this value are -// reserved for atlas pages. -pub(crate) const ATLAS_IMAGE_ID_BASE: u32 = u32::MAX / 2; /// Persistent resources required by Vello CPU for rendering. /// @@ -677,10 +658,6 @@ } /// 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 [`Resources::register_image`] first. pub fn set_paint(&mut self, paint: impl Into<PaintType>) { self.state.paint = paint.into(); } @@ -1006,75 +983,23 @@ } } -/// Image registry implementation. -impl Resources { - /// Register a pixmap in the image registry and return its [`ImageId`]. - pub fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId { - self.image_registry.register(pixmap) - } - - /// Remove an image from the registry. - pub fn destroy_image(&mut self, id: ImageId) -> bool { - self.image_registry.destroy(id) - } - - /// Resolve an `ImageId` to its pixmap data. - pub fn resolve_image(&self, id: ImageId) -> Option<Arc<Pixmap>> { - self.image_registry.resolve(id) - } - - /// Clear the image registry. - pub fn clear_images(&mut self) { - self.image_registry.clear(); - } -} - /// Registry that maps opaque [`ImageId`]s to [`Pixmap`] data. /// -/// Used by [`RenderContext`] to resolve `ImageSource::OpaqueId` at rasterization time. +/// Used by [`RenderContext`] to resolve `ImageSource::Atlas` at rasterization time. #[derive(Debug, Default)] pub(crate) struct ImageRegistry { images: HashMap<u32, Arc<Pixmap>>, - next_id: u32, } impl ImageRegistry { - fn register(&mut self, pixmap: Arc<Pixmap>) -> ImageId { - let id = self.next_id; - assert!( - id < ATLAS_IMAGE_ID_BASE, - "image registry exhausted non-atlas image IDs" - ); - - self.next_id += 1; - self.images.insert(id, pixmap); - ImageId::new(id) - } - #[cfg(feature = "text")] pub(crate) fn register_atlas_page(&mut self, page_index: u32, pixmap: Arc<Pixmap>) { - self.images.insert( - ImageId::new(ATLAS_IMAGE_ID_BASE + page_index).as_u32(), - pixmap, - ); - } - - pub(crate) fn destroy(&mut self, id: ImageId) -> bool { - self.images.remove(&id.as_u32()).is_some() + self.images.insert(page_index, pixmap); } #[cfg(feature = "text")] pub(crate) fn destroy_atlas_page(&mut self, page_index: u32) -> bool { - self.destroy(ImageId::new(ATLAS_IMAGE_ID_BASE + page_index)) - } - - fn resolve(&self, id: ImageId) -> Option<Arc<Pixmap>> { - self.images.get(&id.as_u32()).cloned() - } - - fn clear(&mut self) { - self.images.clear(); - self.next_id = 0; + self.images.remove(&page_index).is_some() } }
diff --git a/vello_cpu/src/text.rs b/vello_cpu/src/text.rs index ef0ca2a..7205efd 100644 --- a/vello_cpu/src/text.rs +++ b/vello_cpu/src/text.rs
@@ -11,7 +11,7 @@ //! [`Arc<Pixmap>`]s here, so the CPU renderer can read pixels directly without //! any GPU upload step. -use crate::render::{ATLAS_IMAGE_ID_BASE, DEFAULT_GLYPH_ATLAS_SIZE}; +use crate::render::DEFAULT_GLYPH_ATLAS_SIZE; use crate::{ Image, ImageSource, PaintType, Pixmap, RasterizerSettings, RenderContext, RenderMode, RenderSettings, Resources, TargetInit, color, kurbo, peniko, @@ -33,10 +33,6 @@ use vello_common::fearless_simd::Level; use vello_common::paint::ImageId; -fn atlas_page_image_id(page_index: u32) -> ImageId { - ImageId::new(ATLAS_IMAGE_ID_BASE + page_index) -} - #[derive(Debug)] pub(crate) struct GlyphAtlasResources { pub(crate) glyph_atlas: GlyphAtlas, @@ -464,7 +460,7 @@ #[inline] fn atlas_image_source(&self, atlas_slot: &AtlasSlot) -> ImageSource { - ImageSource::opaque_id(atlas_page_image_id(atlas_slot.page_index)) + ImageSource::Atlas(ImageId::new(atlas_slot.page_index)) } #[inline]
diff --git a/vello_gpu/CHANGELOG.md b/vello_gpu/CHANGELOG.md index 68e2213..0bb96f4 100644 --- a/vello_gpu/CHANGELOG.md +++ b/vello_gpu/CHANGELOG.md
@@ -24,10 +24,17 @@ - Breaking change: The `invert` parameter of `Scene::fill_blurred_rounded_rect` has been removed. Use `Scene::fill_blurred_rounded_rect_in` with `invert` set to `true` instead. ([#1718][] by [@nicoburns][]) - Breaking change: `WebGlError::ResourceTextureTooLarge` has been removed. The WebGL renderer now reports oversized resource textures as `WebGlError::Render(RenderError::ResourceTextureTooLarge)`, which names the overflowing `ResourceTexture`. (by [@LaurenzV][]) +- Breaking change: `MemorySettings::image_atlas_config` has been renamed to `glyph_atlas_config`, as the atlas now only holds cached glyphs. (by [@LaurenzV][]) +- Breaking change: `RenderError::MissingImage` has been renamed to `RenderError::MissingAtlasEntry`. (by [@LaurenzV][]) +- Breaking change: `ImageSource::OpaqueId`, `ImageSource::opaque_id`, and `ImageSource::opaque_id_with_transparency_hint` have been replaced by the hidden `ImageSource::Atlas`, which is only used internally for glyph caching. Use `ImageSource::external_texture` instead. (by [@LaurenzV][]) + +### Removed + +- Breaking change: Uploading images into the renderer's image atlas. `Renderer::upload_image`, `Renderer::destroy_image`, `Renderer::atlas_texture`, `AtlasWriter`, `WebGlRenderer::upload_image`, `WebGlRenderer::destroy_image`, `WebGlRenderer::atlas_texture`, `WebGlRenderer::atlas_info`, `WebGlAtlasWriter`, `WebGlTextureWithDimensions`, `AtlasTextureInfo`, and `RenderError::AtlasError` have been removed, and `AtlasError`, `AtlasId`, `AtlasLayerDiagnostics`, `AtlasSpaceDiagnostics`, and `ImageId` are no longer re-exported. Bind images as external textures with `ImageSource::external_texture` instead. The atlas is now only used for glyph caching. (by [@LaurenzV][]) ### Fixed -- The wgpu renderer returns `RenderError::MissingImage` instead of panicking when a scene references an image that isn't in the image cache. (by [@LaurenzV][]) +- The wgpu renderer returns `RenderError::MissingAtlasEntry` instead of panicking when a scene references an atlas entry that doesn't exist. (by [@LaurenzV][]) - The wgpu renderer returns `RenderError::ResourceTextureTooLarge` instead of panicking when a scene's alpha, paint, gradient or filter data doesn't fit into the supported texture dimensions. (by [@LaurenzV][]) - The WebGL renderer returns `RenderError::ResourceTextureTooLarge` instead of panicking when a scene's filter data doesn't fit into the supported texture dimensions. (by [@LaurenzV][]) - The WebGL renderer recovers after a scene's gradients don't fit into the gradient texture, instead of returning the same error for every following frame. (by [@LaurenzV][])
diff --git a/vello_gpu/README.md b/vello_gpu/README.md index 5577042..0b53ab6 100644 --- a/vello_gpu/README.md +++ b/vello_gpu/README.md
@@ -53,8 +53,8 @@ - `full_image_support` (enabled by default): If this feature is disabled, only external textures using `TextureRegion::Full` with nearest or bilinear sampling and opacity are supported. Enabling this feature also adds support - for uploaded images, glyph atlas caching, bicubic sampling, cropped external - textures, and colored or alpha-mask tints. + for glyph atlas caching, bicubic sampling, cropped external textures, and + colored or alpha-mask tints. - `gradient_sweep` (enabled by default): Enables rendering of sweep gradients - `wgpu_external_texture` (enabled by default): Adds a WGSL `texture_external` binding when the wgpu device has `wgpu::Features::EXTERNAL_TEXTURE` enabled,
diff --git a/vello_gpu/examples/native_webgl/src/lib.rs b/vello_gpu/examples/native_webgl/src/lib.rs index ca168f5..1acc02e 100644 --- a/vello_gpu/examples/native_webgl/src/lib.rs +++ b/vello_gpu/examples/native_webgl/src/lib.rs
@@ -14,17 +14,15 @@ use probe::ProbeIndicator; use std::cell::RefCell; use std::rc::Rc; -use vello_common::paint::ImageId; -use vello_common::{ - kurbo::{Affine, Vec2}, - paint::ImageSource, -}; -use vello_example_scenes::image::ImageScene; +use vello_common::kurbo::{Affine, Vec2}; +use vello_example_scenes::image::{COWBOY_TEXTURE_ID, FLOWER_TEXTURE_ID, ImageScene}; use vello_example_scenes::{ AnyScene, performance::{FrameTiming, PerformancePanel, PerformanceStage, WebGlGpuTimer, now}, }; -use vello_gpu::{RenderSettings, Resources, Scene, WebGlError, WebGlRendererInit}; +use vello_gpu::{ + RenderSettings, Resources, Scene, WebGlError, WebGlRendererInit, WebGlTextureBindings, +}; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use web_sys::{Event, HtmlCanvasElement, KeyboardEvent, MouseEvent, WheelEvent}; @@ -81,6 +79,7 @@ width: u32, height: u32, renderer_wrapper: RendererWrapper, + texture_bindings: WebGlTextureBindings, probe_indicator: ProbeIndicator, performance: PerformancePanel<2>, canvas: HtmlCanvasElement, @@ -117,6 +116,7 @@ width, height, renderer_wrapper, + texture_bindings: WebGlTextureBindings::new(), probe_indicator, performance: PerformancePanel::new( "Vello GPU · native WebGL2", @@ -144,7 +144,7 @@ update_page_url(app_state.current_scene); app_state.update_title(); - app_state.upload_images_to_atlas(); + app_state.upload_images_to_textures(); app_state } @@ -175,7 +175,7 @@ &self.scene, &mut self.renderer_wrapper.resources, vello_gpu::WebGlRenderTarget::DrawingBuffer, - &vello_gpu::WebGlTextureBindings::new(), + &self.texture_bindings, vello_gpu::WebGlRenderOptions::default(), ) .unwrap(); @@ -328,30 +328,19 @@ * self.transform; } - /// Upload images to the WebGL atlas texture - /// This is the WebGL analogue of the winit example's `upload_images_to_atlas` function - fn upload_images_to_atlas(&mut self) { - // 1st example — uploading pixmap directly to WebGL atlas - let pixmap1 = ImageScene::read_flower_image(); - self.renderer_wrapper - .renderer - .upload_image(&mut self.renderer_wrapper.resources, &pixmap1) - .expect("failed to upload pixmap to WebGL atlas"); - - // 2nd example — uploading from a WebGL texture - let pixmap2 = ImageScene::read_cowboy_image(); - let texture2 = self.pixmap_to_webgl_texture(&pixmap2); - self.renderer_wrapper - .renderer - .upload_image(&mut self.renderer_wrapper.resources, &texture2) - .expect("failed to upload texture to WebGL atlas"); + fn upload_images_to_textures(&mut self) { + for (id, pixmap) in [ + (FLOWER_TEXTURE_ID, ImageScene::read_flower_image()), + (COWBOY_TEXTURE_ID, ImageScene::read_cowboy_image()), + ] { + let texture = self.pixmap_to_webgl_texture(&pixmap); + self.texture_bindings + .insert(id, texture, pixmap.width(), pixmap.height()); + } } /// Convert a pixmap to WebGL texture - fn pixmap_to_webgl_texture( - &self, - pixmap: &vello_gpu::Pixmap, - ) -> vello_gpu::WebGlTextureWithDimensions { + fn pixmap_to_webgl_texture(&self, pixmap: &vello_gpu::Pixmap) -> web_sys::WebGlTexture { let width = pixmap.width(); let height = pixmap.height(); let rgba_data = pixmap.data_as_u8_slice(); @@ -396,11 +385,7 @@ web_sys::WebGl2RenderingContext::CLAMP_TO_EDGE as i32, ); - vello_gpu::WebGlTextureWithDimensions { - texture, - width, - height, - } + texture } } @@ -467,10 +452,7 @@ let scenes = { let v = vello_example_scenes::get_example_scenes( vello_example_scenes::Capabilities::default(), - vec![ - ImageSource::opaque_id(ImageId::new(0)), - ImageSource::opaque_id(ImageId::new(1)), - ], + ImageScene::external_texture_sources(), ) .into_vec(); v.into_boxed_slice() @@ -680,7 +662,7 @@ &scene, &mut resources, vello_gpu::WebGlRenderTarget::DrawingBuffer, - &vello_gpu::WebGlTextureBindings::new(), + &WebGlTextureBindings::new(), vello_gpu::WebGlRenderOptions::default(), ) .unwrap();
diff --git a/vello_gpu/examples/wgpu_video/README.md b/vello_gpu/examples/wgpu_video/README.md index 34868f3..7b85cdb 100644 --- a/vello_gpu/examples/wgpu_video/README.md +++ b/vello_gpu/examples/wgpu_video/README.md
@@ -14,7 +14,7 @@ texture; there is no CPU-side or GPU-side intermediate RGBA copy. The plane textures are zero-copy — backed by the same `IOSurface`s VT decoded into. All videos play simultaneously in a grid layout. A still image -(`assets/splash-flower.jpg`, uploaded once with `Renderer::upload_image`) sits in the +(`assets/splash-flower.jpg`, uploaded once to a texture bound as an external texture) sits in the free space between the first two videos of the top row. A large yellow "Vello GPU" text overlay is rendered on top of the grid using `Scene::glyph_run` with the bundled Roboto font, and inherits the user's pan / zoom / rotation.
diff --git a/vello_gpu/examples/wgpu_video/src/main.rs b/vello_gpu/examples/wgpu_video/src/main.rs index f39e003..9e9c26b 100644 --- a/vello_gpu/examples/wgpu_video/src/main.rs +++ b/vello_gpu/examples/wgpu_video/src/main.rs
@@ -40,12 +40,10 @@ use skrifa::raw::FileRef; use vello_common::color::palette::css::YELLOW; use vello_common::kurbo::{Affine, BezPath, Circle, Ellipse, Point, Rect, Shape, Vec2}; - use vello_common::paint::{Image, ImageId, ImageSource, TextureRegion}; - use vello_common::peniko::{ - Blob, Extend, FontData, ImageAlphaType, ImageQuality, ImageSampler, - }; - use vello_common::pixmap::{PixelMetadata, Pixmap}; + use vello_common::paint::{Image, ImageSource, TextureRegion}; + use vello_common::peniko::{Blob, Extend, FontData, ImageQuality, ImageSampler}; use vello_gpu::{RenderOptions, RenderTarget, Resources, Scene, TextureBindings, TextureId}; + use wgpu::util::DeviceExt; use winit::application::ApplicationHandler; use winit::event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoop}; @@ -68,6 +66,8 @@ /// Still image shown in its own grid cell next to the videos. const STATIC_IMAGE: &[u8] = include_bytes!("../../../../assets/splash-flower.jpg"); + /// Video slots use the texture IDs from zero upwards. + const STATIC_IMAGE_TEXTURE_ID: TextureId = TextureId(u64::MAX); /// Font for the text overlay drawn over the grid. const ROBOTO_FONT: &[u8] = include_bytes!("../../../../assets/roboto/Roboto-Regular.ttf"); @@ -140,11 +140,11 @@ .create_window(attrs) .expect("Failed to create window"), ); - let (mut render_context, mut resources) = RenderContext::new(window.clone()); + let (render_context, resources) = RenderContext::new(window.clone()); let slots = build_video_slots(&render_context); log::info!("{} video(s) loaded into grid", slots.len()); - let static_image = StaticImage::upload(&mut render_context, &mut resources); + let static_image = StaticImage::upload(&render_context); let size = window.inner_size(); let scene = Scene::new( @@ -377,8 +377,13 @@ } if cols >= 2 { - self.static_image - .draw(&mut self.scene, self.transform, cell_w, cell_h); + self.static_image.draw( + &mut self.scene, + &mut bindings, + self.transform, + cell_w, + cell_h, + ); } // The overlay uses the same transform, so it moves with the grid. @@ -539,47 +544,63 @@ slots } - /// [`STATIC_IMAGE`] after uploading to Vello's image atlas. Drawn in the free space - /// between the first two cells of the top row, without moving any video. + /// [`STATIC_IMAGE`] uploaded to a texture that is bound under [`STATIC_IMAGE_TEXTURE_ID`]. + /// Drawn in the free space between the first two cells of the top row, without moving any + /// video. struct StaticImage { - id: ImageId, + view: wgpu::TextureView, width: u16, height: u16, } impl StaticImage { - fn upload(render_context: &mut RenderContext<'_>, resources: &mut Resources) -> Self { + fn upload(render_context: &RenderContext<'_>) -> Self { let decoded = image::load_from_memory(STATIC_IMAGE) .expect("splash-flower.jpg is a valid JPEG") .into_rgba8(); let width = u16::try_from(decoded.width()).expect("image width fits in u16"); let height = u16::try_from(decoded.height()).expect("image height fits in u16"); - let pixmap = Pixmap::from_parts( - decoded.into_raw(), + + let texture = render_context.device.create_texture_with_data( + &render_context.queue, + &wgpu::TextureDescriptor { + label: Some("vello_gpu_wgpu_video static image"), + size: wgpu::Extent3d { + width: u32::from(width), + height: u32::from(height), + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + wgpu::util::TextureDataOrder::LayerMajor, + &decoded, + ); + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + Self { + view, width, height, - PixelMetadata::new(ImageAlphaType::Alpha, false), - ); - - let mut encoder = - render_context - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("vello_gpu_wgpu_video image upload"), - }); - let id = render_context - .renderer - .upload_image(resources, &mut encoder, &pixmap); - render_context.submit(encoder.finish()); - - Self { id, width, height } + } } /// The clip shapes in the first two cells are a circle and a triangle, both /// centered in their cell and no wider than `cell_h`. That leaves a strip /// `cell_w - cell_h` wide around the boundary between the two cells, which the /// image is fitted into. Draws nothing if the cells are too narrow for a gap. - fn draw(&self, scene: &mut Scene, transform: Affine, cell_w: f64, cell_h: f64) { + fn draw( + &self, + scene: &mut Scene, + bindings: &mut TextureBindings, + transform: Affine, + cell_w: f64, + cell_h: f64, + ) { // Keep a little space between the image and the videos on either side. const MARGIN: f64 = 0.9; let gap_w = cell_w - cell_h; @@ -593,10 +614,15 @@ let tx = cell_w - w * scale * 0.5; let ty = (cell_h - h * scale) * 0.5; + bindings.insert(STATIC_IMAGE_TEXTURE_ID, self.view.clone()); scene.set_transform(transform * Affine::translate((tx, ty)) * Affine::scale(scale)); scene.set_paint_transform(Affine::IDENTITY); scene.set_paint(Image { - image: ImageSource::opaque_id_with_transparency_hint(self.id, false), + image: ImageSource::external_texture( + STATIC_IMAGE_TEXTURE_ID, + TextureRegion::Full, + false, + ), sampler: ImageSampler { x_extend: Extend::Pad, y_extend: Extend::Pad,
diff --git a/vello_gpu/examples/wgpu_webgl/src/lib.rs b/vello_gpu/examples/wgpu_webgl/src/lib.rs index 105879a..457c7dc 100644 --- a/vello_gpu/examples/wgpu_webgl/src/lib.rs +++ b/vello_gpu/examples/wgpu_webgl/src/lib.rs
@@ -13,14 +13,13 @@ use vello_common::{ fearless_simd::Level, kurbo::{Affine, Point}, - paint::{ImageId, ImageSource}, }; use vello_example_scenes::{ AnyScene, - image::ImageScene, + image::{COWBOY_TEXTURE_ID, FLOWER_TEXTURE_ID, ImageScene}, performance::{FrameTiming, PerformancePanel, PerformanceStage, WebGlGpuTimer, now}, }; -use vello_gpu::{Pixmap, RenderSettings, RenderTargetConfig, Renderer, Scene}; +use vello_gpu::{Pixmap, RenderSettings, RenderTargetConfig, Renderer, Scene, TextureBindings}; use wasm_bindgen::prelude::*; use web_sys::{Event, HtmlCanvasElement, KeyboardEvent, MouseEvent, WheelEvent}; use wgpu::{ @@ -159,6 +158,7 @@ width: u32, height: u32, renderer_wrapper: RendererWrapper, + texture_bindings: TextureBindings, performance: PerformancePanel<3>, canvas: HtmlCanvasElement, } @@ -186,6 +186,7 @@ width, height, renderer_wrapper, + texture_bindings: TextureBindings::new(), performance: PerformancePanel::new( "Vello GPU · wgpu → WebGL2", [ @@ -212,8 +213,7 @@ update_page_url(app_state.current_scene); app_state.update_title(); - // Upload images to the WebGL atlas - app_state.upload_images_to_atlas(); + app_state.upload_images_to_textures(); app_state } @@ -270,7 +270,7 @@ view: &surface_texture_view, depth_view: Some(&self.renderer_wrapper.depth_texture_view), }, - &vello_gpu::TextureBindings::new(), + &self.texture_bindings, vello_gpu::RenderOptions::default(), ) .unwrap(); @@ -419,36 +419,21 @@ self.transform = self.transform.then_scale_about(zoom_factor, cursor_pos); } - fn upload_images_to_atlas(&mut self) { - let mut encoder = - self.renderer_wrapper - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Upload Image pass"), - }); - - // 1st example — uploading pixmap directly to WebGL atlas - let pixmap1 = ImageScene::read_flower_image(); - self.renderer_wrapper.renderer.upload_image( - &mut self.renderer_wrapper.resources, - &mut encoder, - &pixmap1, - ); - - // 2nd example — uploading from a WebGL texture - let pixmap2 = ImageScene::read_cowboy_image(); - let texture2 = self.upload_image_to_texture( - &self.renderer_wrapper.device, - &self.renderer_wrapper.queue, - &pixmap2, - ); - self.renderer_wrapper.renderer.upload_image( - &mut self.renderer_wrapper.resources, - &mut encoder, - &texture2, - ); - - self.renderer_wrapper.queue.submit([encoder.finish()]); + fn upload_images_to_textures(&mut self) { + for (id, pixmap) in [ + (FLOWER_TEXTURE_ID, ImageScene::read_flower_image()), + (COWBOY_TEXTURE_ID, ImageScene::read_cowboy_image()), + ] { + let texture = self.upload_image_to_texture( + &self.renderer_wrapper.device, + &self.renderer_wrapper.queue, + &pixmap, + ); + self.texture_bindings.insert( + id, + texture.create_view(&wgpu::TextureViewDescriptor::default()), + ); + } } fn upload_image_to_texture( @@ -541,10 +526,7 @@ let scenes = vello_example_scenes::get_example_scenes( vello_example_scenes::Capabilities::default(), - vec![ - ImageSource::opaque_id(ImageId::new(0)), - ImageSource::opaque_id(ImageId::new(1)), - ], + ImageScene::external_texture_sources(), ); let app_state = Rc::new(RefCell::new(AppState::new(canvas.clone(), scenes).await)); @@ -753,7 +735,7 @@ view: &surface_texture_view, depth_view: Some(&depth_texture_view), }, - &vello_gpu::TextureBindings::new(), + &TextureBindings::new(), vello_gpu::RenderOptions::default(), ) .unwrap();
diff --git a/vello_gpu/examples/wgpu_webgpu/src/lib.rs b/vello_gpu/examples/wgpu_webgpu/src/lib.rs index 39c7bd4..4776a25 100644 --- a/vello_gpu/examples/wgpu_webgpu/src/lib.rs +++ b/vello_gpu/examples/wgpu_webgpu/src/lib.rs
@@ -13,14 +13,13 @@ use vello_common::{ fearless_simd::Level, kurbo::{Affine, Vec2}, - paint::{ImageId, ImageSource}, }; use vello_example_scenes::{ AnyScene, - image::ImageScene, + image::{COWBOY_TEXTURE_ID, FLOWER_TEXTURE_ID, ImageScene}, performance::{FrameTiming, PerformancePanel, PerformanceStage, now}, }; -use vello_gpu::{Pixmap, RenderSettings, RenderTargetConfig, Renderer, Scene}; +use vello_gpu::{Pixmap, RenderSettings, RenderTargetConfig, Renderer, Scene, TextureBindings}; use wasm_bindgen::prelude::*; use web_sys::{Event, HtmlCanvasElement, KeyboardEvent, MouseEvent, WheelEvent}; use wgpu::CurrentSurfaceTexture; @@ -167,6 +166,7 @@ width: u32, height: u32, renderer_wrapper: RendererWrapper, + texture_bindings: TextureBindings, performance: PerformancePanel<3>, canvas: HtmlCanvasElement, } @@ -190,6 +190,7 @@ width, height, renderer_wrapper, + texture_bindings: TextureBindings::new(), performance: PerformancePanel::new( "Vello GPU · wgpu → WebGPU", [ @@ -216,8 +217,7 @@ update_page_url(app_state.current_scene); app_state.update_title(); - // Upload images to the renderer's texture atlas. - app_state.upload_images_to_atlas(); + app_state.upload_images_to_textures(); app_state } @@ -266,7 +266,7 @@ view: &surface_texture_view, depth_view: Some(&self.renderer_wrapper.depth_texture_view), }, - &vello_gpu::TextureBindings::new(), + &self.texture_bindings, vello_gpu::RenderOptions::default(), ) .unwrap(); @@ -411,36 +411,21 @@ * self.transform; } - fn upload_images_to_atlas(&mut self) { - let mut encoder = - self.renderer_wrapper - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Upload Image pass"), - }); - - // 1st example — uploading a pixmap directly to the texture atlas. - let pixmap1 = ImageScene::read_flower_image(); - self.renderer_wrapper.renderer.upload_image( - &mut self.renderer_wrapper.resources, - &mut encoder, - &pixmap1, - ); - - // 2nd example — uploading from a wgpu texture. - let pixmap2 = ImageScene::read_cowboy_image(); - let texture2 = self.upload_image_to_texture( - &self.renderer_wrapper.device, - &self.renderer_wrapper.queue, - &pixmap2, - ); - self.renderer_wrapper.renderer.upload_image( - &mut self.renderer_wrapper.resources, - &mut encoder, - &texture2, - ); - - self.renderer_wrapper.queue.submit([encoder.finish()]); + fn upload_images_to_textures(&mut self) { + for (id, pixmap) in [ + (FLOWER_TEXTURE_ID, ImageScene::read_flower_image()), + (COWBOY_TEXTURE_ID, ImageScene::read_cowboy_image()), + ] { + let texture = self.upload_image_to_texture( + &self.renderer_wrapper.device, + &self.renderer_wrapper.queue, + &pixmap, + ); + self.texture_bindings.insert( + id, + texture.create_view(&wgpu::TextureViewDescriptor::default()), + ); + } } fn upload_image_to_texture( @@ -533,10 +518,7 @@ let scenes = vello_example_scenes::get_example_scenes( vello_example_scenes::Capabilities::default(), - vec![ - ImageSource::opaque_id(ImageId::new(0)), - ImageSource::opaque_id(ImageId::new(1)), - ], + ImageScene::external_texture_sources(), ); let app_state = Rc::new(RefCell::new(AppState::new(canvas.clone(), scenes).await)); @@ -757,7 +739,7 @@ view: &surface_texture_view, depth_view: Some(&depth_texture_view), }, - &vello_gpu::TextureBindings::new(), + &TextureBindings::new(), vello_gpu::RenderOptions::default(), ) .unwrap();
diff --git a/vello_gpu/examples/winit/src/main.rs b/vello_gpu/examples/winit/src/main.rs index 4abac49..038383d 100644 --- a/vello_gpu/examples/winit/src/main.rs +++ b/vello_gpu/examples/winit/src/main.rs
@@ -10,12 +10,10 @@ use std::sync::Arc; use std::time::Instant; use vello_common::kurbo::{Affine, Point}; -use vello_common::paint::ImageId; -use vello_common::paint::ImageSource; -use vello_example_scenes::image::ImageScene; +use vello_example_scenes::image::{COWBOY_TEXTURE_ID, FLOWER_TEXTURE_ID, ImageScene}; use vello_example_scenes::spritesheet::{SPRITESHEET_TEXTURE_ID, SpritesheetScene}; use vello_example_scenes::{AnyScene, Capabilities, get_example_scenes}; -use vello_gpu::{Pixmap, RenderSize, Renderer, Resources, Scene, TextureBindings}; +use vello_gpu::{Pixmap, RenderSize, Renderer, Resources, Scene, TextureBindings, TextureId}; use wgpu::CurrentSurfaceTexture; use winit::{ application::ApplicationHandler, @@ -32,8 +30,7 @@ scenes: Box<[AnyScene<Scene>]>, current_scene: usize, renderers: Vec<Option<(Renderer, Resources, RenderSize, wgpu::TextureView)>>, - uploaded_images: Vec<bool>, - spritesheet_textures: Vec<Option<wgpu::Texture>>, + external_textures: Vec<Option<[(TextureId, wgpu::Texture); 3]>>, render_state: RenderState<'s>, scene: Scene, transform: Affine, @@ -64,10 +61,7 @@ } } } - let img_sources = vec![ - ImageSource::opaque_id_with_transparency_hint(ImageId::new(0), false), - ImageSource::opaque_id(ImageId::new(1)), - ]; + let img_sources = ImageScene::external_texture_sources(); let capabilities = Capabilities { external_textures: true, }; @@ -86,10 +80,7 @@ Capabilities { external_textures: true, }, - vec![ - ImageSource::opaque_id(ImageId::new(0)), - ImageSource::opaque_id(ImageId::new(1)), - ], + ImageScene::external_texture_sources(), ), 0, ); @@ -98,8 +89,7 @@ let mut app = App { context: RenderContext::new(), renderers: vec![], - uploaded_images: vec![], - spritesheet_textures: vec![], + external_textures: vec![], scenes, current_scene: start_scene_index, render_state: RenderState::Suspended(None), @@ -161,15 +151,12 @@ self.renderers .resize_with(self.context.devices.len(), || None); - self.uploaded_images - .resize(self.context.devices.len(), false); - self.spritesheet_textures + self.external_textures .resize_with(self.context.devices.len(), || None); self.renderers[surface.dev_id] .get_or_insert_with(|| create_vello_renderer(&self.context, &surface)); - self.upload_images_to_atlas(surface.dev_id); - self.ensure_spritesheet_uploaded(surface.dev_id); + self.ensure_textures_uploaded(surface.dev_id); self.render_state = RenderState::Active { surface: Box::new(surface), @@ -377,9 +364,9 @@ label: Some("Vello Render to Surface pass"), }); let mut texture_bindings = TextureBindings::new(); - if let Some(texture) = self.spritesheet_textures[surface.dev_id].as_ref() { + for (id, texture) in self.external_textures[surface.dev_id].iter().flatten() { texture_bindings.insert( - SPRITESHEET_TEXTURE_ID, + *id, texture.create_view(&wgpu::TextureViewDescriptor::default()), ); } @@ -419,44 +406,22 @@ } impl App<'_> { - fn upload_images_to_atlas(&mut self, device_id: usize) { - if self.uploaded_images[device_id] { - return; - } - - let device_handle = &self.context.devices[device_id]; - let mut encoder = - device_handle - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("Upload Image pass"), - }); - - // 1st example — uploading pixmap directly - let pixmap1 = ImageScene::read_flower_image(); - let (renderer, resources, ..) = self.renderers[device_id].as_mut().unwrap(); - renderer.upload_image(resources, &mut encoder, &pixmap1); - - // 2nd example — uploading from a texture (for cases where you already have a texture) - let pixmap2 = ImageScene::read_cowboy_image(); - let texture2 = - self.upload_image_to_texture(&device_handle.device, &device_handle.queue, &pixmap2); - let (renderer, resources, ..) = self.renderers[device_id].as_mut().unwrap(); - renderer.upload_image(resources, &mut encoder, &texture2); - - device_handle.queue.submit([encoder.finish()]); - self.uploaded_images[device_id] = true; - } - - fn ensure_spritesheet_uploaded(&mut self, device_id: usize) { - if self.spritesheet_textures[device_id].is_some() { + fn ensure_textures_uploaded(&mut self, device_id: usize) { + if self.external_textures[device_id].is_some() { return; } let device_handle = &self.context.devices[device_id]; - let pixmap = SpritesheetScene::read_spritesheet(); - let texture = - self.upload_image_to_texture(&device_handle.device, &device_handle.queue, &pixmap); - self.spritesheet_textures[device_id] = Some(texture); + let textures = [ + (FLOWER_TEXTURE_ID, ImageScene::read_flower_image()), + (COWBOY_TEXTURE_ID, ImageScene::read_cowboy_image()), + (SPRITESHEET_TEXTURE_ID, SpritesheetScene::read_spritesheet()), + ] + .map(|(id, pixmap)| { + let texture = + self.upload_image_to_texture(&device_handle.device, &device_handle.queue, &pixmap); + (id, texture) + }); + self.external_textures[device_id] = Some(textures); } fn upload_image_to_texture(
diff --git a/vello_gpu/src/draw.rs b/vello_gpu/src/draw.rs index 4bc10b2..3216678 100644 --- a/vello_gpu/src/draw.rs +++ b/vello_gpu/src/draw.rs
@@ -754,7 +754,7 @@ fn atlas_image(image_id: ImageId) -> EncodedPaint { EncodedPaint::Image(EncodedImage { - source: ImageSource::opaque_id(image_id), + source: ImageSource::Atlas(image_id), sampler: ImageSampler { x_extend: Extend::Pad, y_extend: Extend::Pad,
diff --git a/vello_gpu/src/lib.rs b/vello_gpu/src/lib.rs index 8a6a043..7b31fcd 100644 --- a/vello_gpu/src/lib.rs +++ b/vello_gpu/src/lib.rs
@@ -34,8 +34,8 @@ //! - `full_image_support` (enabled by default): If this feature is disabled, only //! external textures using `TextureRegion::Full` with nearest or bilinear //! sampling and opacity are supported. Enabling this feature also adds support -//! for uploaded images, glyph atlas caching, bicubic sampling, cropped external -//! textures, and colored or alpha-mask tints. +//! for glyph atlas caching, bicubic sampling, cropped external textures, and +//! colored or alpha-mask tints. //! - `gradient_sweep` (enabled by default): Enables rendering of sweep gradients //! - `wgpu_external_texture` (enabled by default): Adds a WGSL `texture_external` //! binding when the wgpu device has `wgpu::Features::EXTERNAL_TEXTURE` enabled, @@ -124,21 +124,19 @@ pub(crate) mod draw; pub mod util; +pub use render::{ClearSettings, Config, GpuStrip, RenderSize, TargetInit}; #[cfg(feature = "webgl")] pub use render::{ - AtlasTextureInfo, IncompatibleContextReason, IncompatiblePrecompiledShadersReason, - InvalidTextureTargetReason, ShaderCompilationStats, WebGlAtlasWriter, WebGlContextOperation, - WebGlDataTransferOperation, WebGlError, WebGlOperation, WebGlRenderOptions, WebGlRenderTarget, - WebGlRenderer, WebGlRendererInit, WebGlRendererInitStatus, WebGlResourceKind, - WebGlShaderInterfaceOperation, WebGlShaderProgramOperation, WebGlShaderStage, - WebGlTextureBindings, WebGlTextureTarget, WebGlTextureWithDimensions, + IncompatibleContextReason, IncompatiblePrecompiledShadersReason, InvalidTextureTargetReason, + ShaderCompilationStats, WebGlContextOperation, WebGlDataTransferOperation, WebGlError, + WebGlOperation, WebGlRenderOptions, WebGlRenderTarget, WebGlRenderer, WebGlRendererInit, + WebGlRendererInitStatus, WebGlResourceKind, WebGlShaderInterfaceOperation, + WebGlShaderProgramOperation, WebGlShaderStage, WebGlTextureBindings, WebGlTextureTarget, }; #[cfg(feature = "wgpu")] pub use render::{ - AtlasWriter, RenderOptions, RenderTarget, RenderTargetConfig, Renderer, TextureBinding, - TextureBindings, + RenderOptions, RenderTarget, RenderTargetConfig, Renderer, TextureBinding, TextureBindings, }; -pub use render::{ClearSettings, Config, GpuStrip, RenderSize, TargetInit}; #[cfg(all(feature = "webgl", feature = "probe"))] pub use render::{WebGlPendingProbe, WebGlProbeError, WebGlProbeOperation, WebGlProbeStatus}; #[cfg(all(feature = "wgpu", feature = "probe"))] @@ -158,14 +156,12 @@ pub use vello_gpu_shaders::webgl_bootstrap; use thiserror::Error; +use vello_common::paint::ImageId; /// Errors that can occur during rendering. #[derive(Error, Debug, Clone)] #[non_exhaustive] pub enum RenderError { - /// An image atlas allocation failed. - #[error("Atlas allocation failed: {0}")] - AtlasError(#[from] AtlasError), /// A draw referenced a [`TextureId`] that was not provided at render time. #[error("Missing texture binding for {0:?}")] MissingTextureBinding(TextureId), @@ -178,9 +174,9 @@ /// tint other than opacity. #[error("Unsupported sampling of external texture binding {0:?}")] UnsupportedExternalTextureSampling(TextureId), - /// An image paint referenced an image that has not been uploaded. - #[error("Missing image cache entry for {0:?}")] - MissingImage(ImageId), + /// A glyph paint referenced an atlas entry that does not exist. + #[error("Missing atlas entry for {0:?}")] + MissingAtlasEntry(ImageId), /// An intermediate texture allocation failed. #[error(transparent)] IntermediateTexture(#[from] IntermediateTextureError),
diff --git a/vello_gpu/src/paint.rs b/vello_gpu/src/paint.rs index a076b6f..823cf5c 100644 --- a/vello_gpu/src/paint.rs +++ b/vello_gpu/src/paint.rs
@@ -221,7 +221,7 @@ (paint_type, Some(TextureSourceId::External(*id, sampler))) } - ImageSource::OpaqueId { id, .. } => { + ImageSource::Atlas(id) => { let image_resource = self.image_cache.unwrap().get(*id).unwrap(); ( PAINT_TYPE_IMAGE,
diff --git a/vello_gpu/src/render/common/mod.rs b/vello_gpu/src/render/common/mod.rs index 907b349..d4918cb 100644 --- a/vello_gpu/src/render/common/mod.rs +++ b/vello_gpu/src/render/common/mod.rs
@@ -24,10 +24,6 @@ use vello_common::geometry::{RectU16, SizeU16}; use vello_common::record::CommandRecorder; -// TODO: If we want to use native bilinear sampling for uploaded images, -// we can pass 1 instead of 0 here. -pub(crate) const IMAGE_PADDING: u16 = 0; - /// Controls how the output target is cleared before drawing. #[derive(Debug, Clone, Copy, PartialEq)] pub enum ClearSettings<'a> { @@ -150,17 +146,17 @@ impl MemorySettings { pub(crate) fn normalize(&mut self, device_limits: &DeviceLimits) { let Self { - image_atlas_config, + glyph_atlas_config, layers_config, } = self; - image_atlas_config.atlas_size = SizeU16::from(image_atlas_config.atlas_size) + glyph_atlas_config.atlas_size = SizeU16::from(glyph_atlas_config.atlas_size) .clamp(1, device_limits.max_texture_dimension_2d) .into(); - image_atlas_config.initial_atlas_count = image_atlas_config + glyph_atlas_config.initial_atlas_count = glyph_atlas_config .initial_atlas_count - .min(image_atlas_config.max_atlases); + .min(glyph_atlas_config.max_atlases); layers_config.min_texture_size = layers_config .min_texture_size @@ -342,7 +338,7 @@ #[test] fn normalize_memory_settings_clamps_atlas_dimensions_to_backend_limits() { let mut settings = MemorySettings { - image_atlas_config: AtlasConfig { + glyph_atlas_config: AtlasConfig { initial_atlas_count: 8, max_atlases: 16, atlas_size: (8192, 2048), @@ -353,7 +349,7 @@ settings.normalize(&device_limits(4096)); - let config = settings.image_atlas_config; + let config = settings.glyph_atlas_config; assert_eq!(config.initial_atlas_count, 8); assert_eq!(config.max_atlases, 16); assert_eq!(config.atlas_size, (4096, 2048)); @@ -368,7 +364,7 @@ #[test] fn normalize_memory_settings_allows_lazy_initial_atlas_allocation() { let mut settings = MemorySettings { - image_atlas_config: AtlasConfig { + glyph_atlas_config: AtlasConfig { initial_atlas_count: 0, atlas_size: (0, 0), ..Default::default() @@ -378,7 +374,7 @@ settings.normalize(&device_limits(4096)); - let config = settings.image_atlas_config; + let config = settings.glyph_atlas_config; assert_eq!(config.initial_atlas_count, 0); assert_eq!(config.max_atlases, 8); assert_eq!(config.atlas_size, (1, 1));
diff --git a/vello_gpu/src/render/common/scene_preparer.rs b/vello_gpu/src/render/common/scene_preparer.rs index 373a76f..fd40322 100644 --- a/vello_gpu/src/render/common/scene_preparer.rs +++ b/vello_gpu/src/render/common/scene_preparer.rs
@@ -45,7 +45,7 @@ device_limits: &DeviceLimits, ) -> (Self, Resources) { settings.memory_settings.normalize(device_limits); - let resources = Resources::new(settings.memory_settings.image_atlas_config); + let resources = Resources::new(settings.memory_settings.glyph_atlas_config); let resource_texture_dimension_2d = device_limits.resource_texture_dimension_2d(); // Estimate the maximum number of gradient cache entries based on the resource texture // dimension and the maximum gradient LUT size - worst case scenario. @@ -115,9 +115,10 @@ self.paint_idxs[encoded_paint_idx] = current_idx; let gpu_paint = match paint { EncodedPaint::Image(img) => match &img.source { - ImageSource::OpaqueId { id, .. } => { - let image_resource = - image_cache.get(*id).ok_or(RenderError::MissingImage(*id))?; + ImageSource::Atlas(id) => { + let image_resource = image_cache + .get(*id) + .ok_or(RenderError::MissingAtlasEntry(*id))?; encode_image_paint(img, image_resource) } ImageSource::ExternalTexture {
diff --git a/vello_gpu/src/render/mod.rs b/vello_gpu/src/render/mod.rs index 64437d7..023f9b9 100644 --- a/vello_gpu/src/render/mod.rs +++ b/vello_gpu/src/render/mod.rs
@@ -18,12 +18,11 @@ #[cfg(feature = "webgl")] pub use webgl::{ - AtlasTextureInfo, IncompatibleContextReason, IncompatiblePrecompiledShadersReason, - InvalidTextureTargetReason, ShaderCompilationStats, WebGlAtlasWriter, WebGlContextOperation, - WebGlDataTransferOperation, WebGlError, WebGlOperation, WebGlRenderOptions, WebGlRenderTarget, - WebGlRenderer, WebGlRendererInit, WebGlRendererInitStatus, WebGlResourceKind, - WebGlShaderInterfaceOperation, WebGlShaderProgramOperation, WebGlShaderStage, - WebGlTextureBindings, WebGlTextureTarget, WebGlTextureWithDimensions, + IncompatibleContextReason, IncompatiblePrecompiledShadersReason, InvalidTextureTargetReason, + ShaderCompilationStats, WebGlContextOperation, WebGlDataTransferOperation, WebGlError, + WebGlOperation, WebGlRenderOptions, WebGlRenderTarget, WebGlRenderer, WebGlRendererInit, + WebGlRendererInitStatus, WebGlResourceKind, WebGlShaderInterfaceOperation, + WebGlShaderProgramOperation, WebGlShaderStage, WebGlTextureBindings, WebGlTextureTarget, }; #[cfg(all(feature = "webgl", feature = "probe"))] pub use webgl::{ @@ -32,8 +31,7 @@ }; #[cfg(feature = "wgpu")] pub use wgpu::{ - AtlasWriter, RenderOptions, RenderTarget, RenderTargetConfig, Renderer, TextureBinding, - TextureBindings, + RenderOptions, RenderTarget, RenderTargetConfig, Renderer, TextureBinding, TextureBindings, }; #[cfg(all(feature = "wgpu", feature = "probe"))] pub use wgpu::{WgpuPendingProbe, WgpuProbeError, WgpuProbeStatus};
diff --git a/vello_gpu/src/render/webgl/atlas.rs b/vello_gpu/src/render/webgl/atlas.rs index fd5eb6a..fb05167 100644 --- a/vello_gpu/src/render/webgl/atlas.rs +++ b/vello_gpu/src/render/webgl/atlas.rs
@@ -3,200 +3,35 @@ //! Writing image data into atlas textures. -use crate::render::webgl::bindings::WebGlTextureWithDimensions; -use crate::render::webgl::gl::resource::Framebuffer; use crate::render::webgl::{ WebGlDataTransferOperation, WebGlError, WebGlOperation, WebGlResultExt, }; -use alloc::sync::Arc; use vello_common::pixmap::Pixmap; use web_sys::{WebGl2RenderingContext, WebGlTexture}; -/// Trait for types that can write image data directly to the atlas texture in WebGL. -/// -/// This allows efficient uploading from different sources: -/// - `Pixmap`: Direct upload using raw pixel data -/// - `ImageData`: Browser `ImageData` objects -/// - Custom implementations for other image sources -pub trait WebGlAtlasWriter { - /// Get the width of the image. - fn width(&self) -> u16; - /// Get the height of the image. - fn height(&self) -> u16; - - /// Write image data to an atlas texture at the specified offset. - fn write_to_atlas( - &self, - gl: &WebGl2RenderingContext, - atlas_texture: &WebGlTexture, - offset: [u16; 2], - width: u16, - height: u16, - ) -> Result<(), WebGlError>; -} - -/// Implementation for `Pixmap` - direct upload using raw pixel data. -impl WebGlAtlasWriter for Pixmap { - fn width(&self) -> u16 { - self.width() - } - - fn height(&self) -> u16 { - self.height() - } - - fn write_to_atlas( - &self, - gl: &WebGl2RenderingContext, - atlas_texture: &WebGlTexture, - offset: [u16; 2], - width: u16, - height: u16, - ) -> Result<(), WebGlError> { - gl.active_texture(WebGl2RenderingContext::TEXTURE0); - gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(atlas_texture)); - - // Convert pixmap data to the format expected by WebGL - let rgba_data = self.data_as_u8_slice(); - - gl.tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_u8_array( - WebGl2RenderingContext::TEXTURE_2D, - 0, - offset[0] as i32, - offset[1] as i32, - width as i32, - height as i32, - WebGl2RenderingContext::RGBA, - WebGl2RenderingContext::UNSIGNED_BYTE, - Some(rgba_data), - ) - .map_js_error(WebGlOperation::DataTransfer( - WebGlDataTransferOperation::TextureUpload, - ))?; - - Ok(()) - } -} - -/// Implementation for `Arc<Pixmap>`. -impl WebGlAtlasWriter for Arc<Pixmap> { - fn width(&self) -> u16 { - self.as_ref().width() - } - - fn height(&self) -> u16 { - self.as_ref().height() - } - - fn write_to_atlas( - &self, - gl: &WebGl2RenderingContext, - atlas_texture: &WebGlTexture, - offset: [u16; 2], - width: u16, - height: u16, - ) -> Result<(), WebGlError> { - self.as_ref() - .write_to_atlas(gl, atlas_texture, offset, width, height) - } -} - -/// Implementation for `WebGlTexture` - texture-to-texture copy. -impl WebGlAtlasWriter for WebGlTexture { - fn width(&self) -> u16 { - // WebGL textures don't expose their dimensions directly - // This is a limitation - in practice, you'd need to track dimensions separately - // For now, we'll require the caller to provide correct width/height parameters - unreachable!("WebGlTexture width must be provided by caller") - } - - fn height(&self) -> u16 { - // WebGL textures don't expose their dimensions directly - // This is a limitation - in practice, you'd need to track dimensions separately - // For now, we'll require the caller to provide correct width/height parameters - unreachable!("WebGlTexture height must be provided by caller") - } - - fn write_to_atlas( - &self, - gl: &WebGl2RenderingContext, - atlas_texture: &WebGlTexture, - offset: [u16; 2], - width: u16, - height: u16, - ) -> Result<(), WebGlError> { - copy_to_texture( - gl, - |gl| { - // Attach source texture to read framebuffer - gl.framebuffer_texture_2d( - WebGl2RenderingContext::READ_FRAMEBUFFER, - WebGl2RenderingContext::COLOR_ATTACHMENT0, - WebGl2RenderingContext::TEXTURE_2D, - Some(self), - 0, - ); - }, - atlas_texture, - [u32::from(offset[0]), u32::from(offset[1])], - [u32::from(width), u32::from(height)], - ) - } -} - -impl WebGlAtlasWriter for WebGlTextureWithDimensions { - fn width(&self) -> u16 { - self.width - } - - fn height(&self) -> u16 { - self.height - } - - fn write_to_atlas( - &self, - gl: &WebGl2RenderingContext, - atlas_texture: &WebGlTexture, - offset: [u16; 2], - width: u16, - height: u16, - ) -> Result<(), WebGlError> { - self.texture - .write_to_atlas(gl, atlas_texture, offset, width, height) - } -} - -/// Copy from a source texture/framebuffer to an atlas texture. -pub(crate) fn copy_to_texture( +pub(crate) fn write_pixmap_to_atlas( gl: &WebGl2RenderingContext, - source_setup: impl FnOnce(&WebGl2RenderingContext), - dest_texture: &WebGlTexture, - dest_offset: [u32; 2], - copy_size: [u32; 2], + pixmap: &Pixmap, + atlas_texture: &WebGlTexture, + offset: [u16; 2], ) -> Result<(), WebGlError> { gl.active_texture(WebGl2RenderingContext::TEXTURE0); - let read_framebuffer = Framebuffer::new(gl)?; - gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(dest_texture)); + gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(atlas_texture)); - // Bind the READ framebuffer - gl.bind_framebuffer( - WebGl2RenderingContext::READ_FRAMEBUFFER, - Some(&read_framebuffer), - ); - - // Let the caller set up the source (attach texture/layer to read framebuffer) - source_setup(gl); - - gl.copy_tex_sub_image_2d( + gl.tex_sub_image_2d_with_i32_and_i32_and_u32_and_type_and_opt_u8_array( WebGl2RenderingContext::TEXTURE_2D, 0, - dest_offset[0] as i32, - dest_offset[1] as i32, - 0, - 0, - copy_size[0] as i32, - copy_size[1] as i32, - ); + offset[0] as i32, + offset[1] as i32, + i32::from(pixmap.width()), + i32::from(pixmap.height()), + WebGl2RenderingContext::RGBA, + WebGl2RenderingContext::UNSIGNED_BYTE, + Some(pixmap.data_as_u8_slice()), + ) + .map_js_error(WebGlOperation::DataTransfer( + WebGlDataTransferOperation::TextureUpload, + ))?; Ok(()) }
diff --git a/vello_gpu/src/render/webgl/bindings.rs b/vello_gpu/src/render/webgl/bindings.rs index ad726c8..2af4b11 100644 --- a/vello_gpu/src/render/webgl/bindings.rs +++ b/vello_gpu/src/render/webgl/bindings.rs
@@ -99,11 +99,11 @@ /// Wrapper for `WebGlTexture` with known dimensions. #[derive(Debug, Clone)] -pub struct WebGlTextureWithDimensions { +pub(crate) struct WebGlTextureWithDimensions { /// The WebGL texture. - pub texture: WebGlTexture, + pub(crate) texture: WebGlTexture, /// The width of the texture. - pub width: u16, + pub(crate) width: u16, /// The height of the texture. - pub height: u16, + pub(crate) height: u16, }
diff --git a/vello_gpu/src/render/webgl/error.rs b/vello_gpu/src/render/webgl/error.rs index d961b6f..acab953 100644 --- a/vello_gpu/src/render/webgl/error.rs +++ b/vello_gpu/src/render/webgl/error.rs
@@ -16,9 +16,6 @@ /// Rendering failed for a certain reason. #[error(transparent)] Render(#[from] RenderError), - /// An image upload was attempted without the `full_image_support` feature. - #[error("uploaded images require the `full_image_support` feature")] - FullImageSupportDisabled, /// A WebGL2 context could not be created for the canvas. #[error("WebGL2 context unavailable")] ContextUnavailable,
diff --git a/vello_gpu/src/render/webgl/mod.rs b/vello_gpu/src/render/webgl/mod.rs index 2b534c2..dcd056f 100644 --- a/vello_gpu/src/render/webgl/mod.rs +++ b/vello_gpu/src/render/webgl/mod.rs
@@ -20,6 +20,7 @@ only break in edge cases, and some of them are also only related to conversions from f64 to f32." )] +#[cfg(feature = "text")] mod atlas; mod bindings; mod error; @@ -31,8 +32,7 @@ mod resources; mod target; -pub use atlas::WebGlAtlasWriter; -pub use bindings::{WebGlTextureBindings, WebGlTextureWithDimensions}; +pub use bindings::WebGlTextureBindings; #[cfg(feature = "probe")] pub use error::WebGlProbeOperation; pub(crate) use error::WebGlResultExt; @@ -45,25 +45,28 @@ pub use init::{ShaderCompilationStats, WebGlRendererInit, WebGlRendererInitStatus}; pub use target::{WebGlRenderTarget, WebGlTextureTarget}; -use crate::render::common::{IMAGE_PADDING, ScenePreparer}; +#[cfg(feature = "text")] +use crate::RenderError; +use crate::render::common::ScenePreparer; use crate::{ - ClearSettings, RenderError, RenderSize, Resources, TargetInit, + ClearSettings, RenderSize, Resources, TargetInit, scene::Scene, schedule::IntermediateTextureAllocations, target::{DrawPassTarget, RootTarget}, }; use alloc::vec::Vec; #[cfg(feature = "text")] +use atlas::write_pixmap_to_atlas; +#[cfg(feature = "text")] use glifo::PendingClearRect; use passes::{WebGlPrograms, WebGlRendererContext}; use resources::WebGlResources; use vello_common::color::{AlphaColor, Srgb}; +use vello_common::geometry::{RectU16, SizeU16}; use vello_common::image_cache::ImageCache; use vello_common::multi_atlas::{AtlasConfig, AtlasId}; -use vello_common::{ - geometry::{RectU16, SizeU16}, - paint::ImageId, -}; +#[cfg(feature = "text")] +use vello_common::{paint::ImageId, pixmap::Pixmap}; use web_sys::{WebGl2RenderingContext, WebGlRenderbuffer, WebGlTexture}; /// Vello GPU's WebGL2 renderer. @@ -112,17 +115,6 @@ } } -/// Current allocation state of the WebGL image/glyph atlas textures. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AtlasTextureInfo { - /// Width of each atlas texture. - pub width: u16, - /// Height of each atlas texture. - pub height: u16, - /// Number of atlas textures available to the image cache. - pub texture_count: u32, -} - impl WebGlRenderer { /// Returns the shaders compiled while creating this renderer and the time spent. /// @@ -160,6 +152,7 @@ /// To render without any texture bindings, you can pass an empty [`WebGlTextureBindings`]. /// /// [`TextureId`]: vello_common::TextureId + /// [`RenderError::MissingTextureBinding`]: crate::RenderError::MissingTextureBinding pub fn render( &mut self, scene: &Scene, @@ -185,7 +178,7 @@ image_cache, upload.image_id, &upload.pixmap, - Some([dst_x, dst_y]), + [dst_x, dst_y], ) }, )?; @@ -240,8 +233,11 @@ /// `texture_bindings` provides [externally bound textures](`WebGlTextureBindings`) /// referenced by the scene. Pass `&WebGlTextureBindings::new()` if the scene does /// not use any. - #[doc(hidden)] - pub fn render_to_atlas( + #[cfg_attr( + not(feature = "text"), + expect(dead_code, reason = "only glyph caching renders into the atlas") + )] + pub(crate) fn render_to_atlas( &mut self, scene: &Scene, atlas_count: u32, @@ -404,126 +400,29 @@ &self.gl } - /// Upload image to cache and atlas in one step. Returns the `ImageId`. - /// - /// This is the WebGL analogue of the wgpu Renderer's `upload_image` method. - /// It allocates space in the image cache and uploads the image data to the atlas texture. - /// - /// # Errors - /// Returns [`WebGlError::FullImageSupportDisabled`] if the `full_image_support` feature - /// is disabled, or an error if allocating or uploading the image fails. - #[track_caller] - pub fn upload_image<T: WebGlAtlasWriter>( - &mut self, - resources: &mut Resources, - writer: &T, - ) -> Result<ImageId, WebGlError> { - self.upload_image_with(&mut resources.image_cache, writer, IMAGE_PADDING) - } - - #[track_caller] - pub(crate) fn upload_image_with<T: WebGlAtlasWriter>( - &mut self, - image_cache: &mut ImageCache, - writer: &T, - padding: u16, - ) -> Result<ImageId, WebGlError> { - if !cfg!(feature = "full_image_support") { - return Err(WebGlError::FullImageSupportDisabled); - } - - let width = writer.width(); - let height = writer.height(); - let image_id = image_cache - .allocate(width, height, padding) - .map_err(RenderError::AtlasError)?; - - if let Err(err) = self.write_to_atlas(image_cache, image_id, writer, None) { - image_cache.deallocate(image_id); - - return Err(err); - } - - Ok(image_id) - } - - /// Write pixel data to an existing atlas allocation. - /// - /// Unlike [`upload_image`](Self::upload_image), this does not allocate space in the image - /// cache. The `image_id` must have been previously allocated (e.g. via - /// `ImageCache::allocate`). This is useful for uploading CPU-side pixel data (such as - /// bitmap font glyphs) to a pre-allocated atlas region. - /// - /// If `offset_override` is `Some`, the provided offset is used instead of the - /// allocator-assigned position. Pass `None` to use the default atlas offset. - pub(crate) fn write_to_atlas<T: WebGlAtlasWriter>( + #[cfg(feature = "text")] + fn write_to_atlas( &mut self, image_cache: &ImageCache, image_id: ImageId, - writer: &T, - offset_override: Option<[u16; 2]>, + pixmap: &Pixmap, + offset: [u16; 2], ) -> Result<(), WebGlError> { let image_resource = image_cache .get(image_id) - .ok_or(RenderError::MissingImage(image_id))?; + .ok_or(RenderError::MissingAtlasEntry(image_id))?; self.resources .maybe_create_atlas_textures(&self.gl, image_cache.atlas_count() as u32)?; - let offset = offset_override.unwrap_or(image_resource.offset); - writer.write_to_atlas( + write_pixmap_to_atlas( &self.gl, + pixmap, &self.resources.atlas_textures[image_resource.atlas_id.as_u32() as usize], offset, - writer.width(), - writer.height(), - )?; - - Ok(()) + ) } - /// Destroy an image from the cache and clear the allocated slot in the atlas. - pub fn destroy_image( - &mut self, - resources: &mut Resources, - image_id: ImageId, - ) -> Result<(), WebGlError> { - if let Some(image_resource) = resources.image_cache.get(image_id) { - let padding = image_resource.padding; - self.clear_atlas_region( - image_resource.atlas_id, - [ - image_resource.offset[0] - padding, - image_resource.offset[1] - padding, - ], - image_resource.width + padding * 2, - image_resource.height + padding * 2, - )?; - - let _ = resources.image_cache.deallocate(image_id); - } - - Ok(()) - } - - /// Returns an individual image atlas texture. - pub fn atlas_texture(&self, atlas_id: AtlasId) -> &WebGlTexture { - self.resources - .atlas_textures - .get(atlas_id.as_u32() as usize) - .map(|texture| &**texture) - .unwrap() - } - - /// Returns the current allocation state of the image/glyph atlas texture. - pub fn atlas_info(&self) -> AtlasTextureInfo { - let resources = &self.resources; - AtlasTextureInfo { - width: resources.atlas_size.0, - height: resources.atlas_size.1, - texture_count: u32::try_from(resources.atlas_textures.len()).unwrap(), - } - } - + #[cfg(feature = "text")] /// Clear a specific region of an atlas texture. fn clear_atlas_region( &mut self,
diff --git a/vello_gpu/src/render/wgpu/atlas.rs b/vello_gpu/src/render/wgpu/atlas.rs index 2dea355..49da5b7 100644 --- a/vello_gpu/src/render/wgpu/atlas.rs +++ b/vello_gpu/src/render/wgpu/atlas.rs
@@ -3,150 +3,38 @@ //! Writing image data into atlas textures. -use alloc::sync::Arc; use vello_common::pixmap::Pixmap; -use wgpu::{CommandEncoder, Device, Extent3d, Queue, Texture}; +use wgpu::{Extent3d, Queue, Texture}; -/// Trait for types that can write image data directly to the atlas texture. -/// -/// This allows efficient uploading from different sources: -/// - `Pixmap`: Direct upload without intermediate texture -/// - `Texture`: Texture-to-texture copy -/// - Custom implementations for other image sources -pub trait AtlasWriter { - /// Get the width of the image. - fn width(&self) -> u16; - /// Get the height of the image. - fn height(&self) -> u16; - - /// Write image data to an atlas texture at the specified offset. - fn write_to_atlas( - &self, - device: &Device, - queue: &Queue, - encoder: &mut CommandEncoder, - atlas_texture: &Texture, - offset: [u16; 2], - width: u16, - height: u16, +pub(crate) fn write_pixmap_to_atlas( + queue: &Queue, + pixmap: &Pixmap, + atlas_texture: &Texture, + offset: [u16; 2], +) { + let width = u32::from(pixmap.width()); + let height = u32::from(pixmap.height()); + queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: atlas_texture, + mip_level: 0, + origin: wgpu::Origin3d { + x: u32::from(offset[0]), + y: u32::from(offset[1]), + z: 0, + }, + aspect: wgpu::TextureAspect::All, + }, + pixmap.data_as_u8_slice(), + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(4 * width), + rows_per_image: Some(height), + }, + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, ); } - -/// Implementation for `wgpu::Texture` - uses texture-to-texture copy -impl AtlasWriter for Texture { - fn width(&self) -> u16 { - u16::try_from(self.width()).expect("texture width exceeds the u16 atlas domain") - } - - fn height(&self) -> u16 { - u16::try_from(self.height()).expect("texture height exceeds the u16 atlas domain") - } - - fn write_to_atlas( - &self, - _device: &Device, - _queue: &Queue, - encoder: &mut CommandEncoder, - atlas_texture: &Texture, - offset: [u16; 2], - width: u16, - height: u16, - ) { - encoder.copy_texture_to_texture( - wgpu::TexelCopyTextureInfo { - texture: self, - mip_level: 0, - origin: wgpu::Origin3d::ZERO, - aspect: wgpu::TextureAspect::All, - }, - wgpu::TexelCopyTextureInfo { - texture: atlas_texture, - mip_level: 0, - origin: wgpu::Origin3d { - x: u32::from(offset[0]), - y: u32::from(offset[1]), - z: 0, - }, - aspect: wgpu::TextureAspect::All, - }, - Extent3d { - width: u32::from(width), - height: u32::from(height), - depth_or_array_layers: 1, - }, - ); - } -} - -/// Implementation for `Pixmap` - direct upload to atlas -impl AtlasWriter for Pixmap { - fn width(&self) -> u16 { - self.width() - } - - fn height(&self) -> u16 { - self.height() - } - - fn write_to_atlas( - &self, - _device: &Device, - queue: &Queue, - _encoder: &mut CommandEncoder, - atlas_texture: &Texture, - offset: [u16; 2], - width: u16, - height: u16, - ) { - let width = u32::from(width); - let height = u32::from(height); - queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: atlas_texture, - mip_level: 0, - origin: wgpu::Origin3d { - x: u32::from(offset[0]), - y: u32::from(offset[1]), - z: 0, - }, - aspect: wgpu::TextureAspect::All, - }, - self.data_as_u8_slice(), - wgpu::TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(4 * width), - rows_per_image: Some(height), - }, - Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - ); - } -} - -/// Implementation for `Arc<Pixmap>` -impl AtlasWriter for Arc<Pixmap> { - fn width(&self) -> u16 { - self.as_ref().width() - } - - fn height(&self) -> u16 { - self.as_ref().height() - } - - fn write_to_atlas( - &self, - device: &Device, - queue: &Queue, - encoder: &mut CommandEncoder, - atlas_texture: &Texture, - offset: [u16; 2], - width: u16, - height: u16, - ) { - self.as_ref() - .write_to_atlas(device, queue, encoder, atlas_texture, offset, width, height); - } -}
diff --git a/vello_gpu/src/render/wgpu/mod.rs b/vello_gpu/src/render/wgpu/mod.rs index d9e6645..51bbaaf 100644 --- a/vello_gpu/src/render/wgpu/mod.rs +++ b/vello_gpu/src/render/wgpu/mod.rs
@@ -9,6 +9,7 @@ only break in edge cases, and some of them are also only related to conversions from f64 to f32." )] +#[cfg(feature = "text")] mod atlas; mod bindings; mod passes; @@ -16,18 +17,19 @@ mod probe; mod resources; -pub use atlas::AtlasWriter; pub use bindings::{TextureBinding, TextureBindings}; #[cfg(feature = "probe")] pub use probe::{WgpuPendingProbe, WgpuProbeError, WgpuProbeStatus}; -use crate::render::common::{IMAGE_PADDING, ScenePreparer}; +use crate::render::common::ScenePreparer; use crate::{ ClearSettings, GpuStrip, RenderError, RenderSettings, RenderSize, Resources, TargetInit, render::common::DeviceLimits, scene::Scene, schedule::IntermediateTextureAllocations, target::RootTarget, }; use alloc::vec::Vec; +#[cfg(feature = "text")] +use atlas::write_pixmap_to_atlas; use core::fmt::Debug; #[cfg(feature = "text")] use glifo::PendingClearRect; @@ -37,10 +39,11 @@ use vello_common::geometry::{RectU16, SizeU16}; use vello_common::image_cache::ImageCache; use vello_common::multi_atlas::{AtlasConfig, AtlasId}; -use wgpu::{ - CommandEncoder, Device, Extent3d, Queue, RenderPassColorAttachment, RenderPassDescriptor, - Texture, TextureView, TextureViewDescriptor, -}; +#[cfg(feature = "text")] +use vello_common::pixmap::Pixmap; +#[cfg(feature = "text")] +use wgpu::Texture; +use wgpu::{CommandEncoder, Device, Extent3d, Queue, TextureView, TextureViewDescriptor}; /// Options for the renderer #[derive(Debug)] @@ -250,10 +253,9 @@ |renderer, image_cache, upload, dst_x, dst_y| { renderer.write_to_atlas( image_cache, - encoder, upload.image_id, &upload.pixmap, - Some([dst_x, dst_y]), + [dst_x, dst_y], ); Ok(()) @@ -300,8 +302,11 @@ /// `texture_bindings` provides [externally bound textures](`TextureBindings`) /// referenced by the scene. Pass `&TextureBindings::new()` if the scene does /// not use any. - #[doc(hidden)] - pub fn render_to_atlas( + #[cfg_attr( + not(feature = "text"), + expect(dead_code, reason = "only glyph caching renders into the atlas") + )] + pub(crate) fn render_to_atlas( &mut self, scene: &Scene, atlas_count: u32, @@ -449,150 +454,33 @@ Ok(()) } - /// Upload image to cache and atlas in one step. Returns the `ImageId`. - /// - /// It's used when an image is not already in the cache. - /// - /// This is a convenience method that: - /// 1. Reserves space in the image cache - /// 2. Writes the image data directly to the atlas - /// 3. Returns the `ImageId` for use in rendering - #[track_caller] - pub fn upload_image<T: AtlasWriter>( - &mut self, - resources: &mut Resources, - encoder: &mut CommandEncoder, - writer: &T, - ) -> vello_common::paint::ImageId { - self.upload_image_with(&mut resources.image_cache, encoder, writer, IMAGE_PADDING) - } - - #[track_caller] - pub(crate) fn upload_image_with<T: AtlasWriter>( - &mut self, - image_cache: &mut ImageCache, - encoder: &mut CommandEncoder, - writer: &T, - padding: u16, - ) -> vello_common::paint::ImageId { - if !cfg!(feature = "full_image_support") { - panic!("uploaded images require the `full_image_support` feature"); - } - - let width = writer.width(); - let height = writer.height(); - let image_id = image_cache.allocate(width, height, padding).unwrap(); - self.write_to_atlas(image_cache, encoder, image_id, writer, None); - image_id - } - - /// Write pixel data to an existing atlas allocation. - /// - /// Unlike [`upload_image`](Self::upload_image), this does not allocate space in the image - /// cache. The `image_id` must have been previously allocated (e.g. via - /// `ImageCache::allocate`). This is useful for uploading CPU-side pixel data (such as - /// bitmap font glyphs) to a pre-allocated atlas region. - /// - /// If `offset_override` is `Some`, the provided offset is used instead of the - /// allocator-assigned position. Pass `None` to use the default atlas offset. - pub(crate) fn write_to_atlas<T: AtlasWriter>( + #[cfg(feature = "text")] + fn write_to_atlas( &mut self, image_cache: &ImageCache, - encoder: &mut CommandEncoder, image_id: vello_common::paint::ImageId, - writer: &T, - offset_override: Option<[u16; 2]>, + pixmap: &Pixmap, + offset: [u16; 2], ) { let image_resource = image_cache.get(image_id).expect("Image resource not found"); self.resources .maybe_create_atlas_textures(&self.device, image_cache.atlas_count() as u32); - let offset = offset_override.unwrap_or(image_resource.offset); - let atlas_texture = - &self.resources.atlas_textures[image_resource.atlas_id.as_u32() as usize]; - writer.write_to_atlas( - &self.device, + write_pixmap_to_atlas( &self.queue, - encoder, - atlas_texture, + pixmap, + self.atlas_texture(image_resource.atlas_id), offset, - writer.width(), - writer.height(), ); } - /// Destroy an image from the cache and clear the allocated slot in the atlas. - pub fn destroy_image( - &mut self, - resources: &mut Resources, - encoder: &mut CommandEncoder, - image_id: vello_common::paint::ImageId, - ) { - if let Some(image_resource) = resources.image_cache.deallocate(image_id) { - let padding = image_resource.padding; - - self.clear_atlas_region( - encoder, - image_resource.atlas_id, - [ - image_resource.offset[0] - padding, - image_resource.offset[1] - padding, - ], - image_resource.width + padding * 2, - image_resource.height + padding * 2, - ); - } - } - - /// Returns an individual image atlas texture. - pub fn atlas_texture(&self, atlas_id: AtlasId) -> &Texture { + #[cfg(feature = "text")] + fn atlas_texture(&self, atlas_id: AtlasId) -> &Texture { self.resources .atlas_textures .get(atlas_id.as_u32() as usize) .unwrap() } - - /// Clear a specific region of the atlas texture. - fn clear_atlas_region( - &mut self, - encoder: &mut CommandEncoder, - atlas_id: AtlasId, - offset: [u16; 2], - width: u16, - height: u16, - ) { - let layer_view = self.resources.atlas_texture_views[atlas_id.as_u32() as usize].clone(); - - let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor { - label: Some("Clear Atlas Region"), - color_attachments: &[Some(RenderPassColorAttachment { - view: &layer_view, - resolve_target: None, - ops: wgpu::Operations { - // Don't clear entire texture, just the scissor region - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, - multiview_mask: None, - }); - - // Set scissor rectangle to limit clearing to specific region - render_pass.set_scissor_rect( - u32::from(offset[0]), - u32::from(offset[1]), - u32::from(width), - u32::from(height), - ); - // Use atlas clear pipeline to render transparent pixels - render_pass.set_pipeline(&self.programs.clear.atlas); - // Draw fullscreen quad - render_pass.draw(0..4, 0..1); - } } #[cfg(feature = "text")]
diff --git a/vello_gpu/src/render/wgpu/passes/clear.rs b/vello_gpu/src/render/wgpu/passes/clear.rs index 0824b56..dd48835 100644 --- a/vello_gpu/src/render/wgpu/passes/clear.rs +++ b/vello_gpu/src/render/wgpu/passes/clear.rs
@@ -13,15 +13,13 @@ RenderPassColorAttachment, RenderPassDescriptor, RenderPipeline, TextureView, util::DeviceExt, }; -/// Pipelines for clearing rectangles of layer textures, the root target and atlas regions. +/// Pipelines for clearing rectangles of layer textures and the root target. #[derive(Debug)] pub(crate) struct ClearPipelines { /// Layer-clear pipeline. pub(crate) layer: RenderPipeline, /// User-target rectangle-clear pipeline. pub(crate) root: RenderPipeline, - /// Pipeline for clearing atlas regions. - pub(crate) atlas: RenderPipeline, } impl ClearPipelines { @@ -87,48 +85,9 @@ let root_clear_pipeline = create_clear_pipeline("Root Clear Pipeline", render_target_config.format); - // Create atlas clear pipeline - let atlas_clear_pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Atlas Clear Pipeline Layout"), - bind_group_layouts: &[], - immediate_size: 0, - }); - // TODO: Change the atlas clear pipeline to use the existing layer clear mechanism. - let atlas_clear_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Atlas Clear Pipeline"), - layout: Some(&atlas_clear_pipeline_layout), - vertex: wgpu::VertexState { - module: &clear_shader, - // Use a different vertex shader entry point - entry_point: Some("vs_main_fullscreen"), - buffers: &[], - compilation_options: PipelineCompilationOptions::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &clear_shader, - entry_point: Some("fs_transparent"), - targets: &[Some(ColorTargetState { - format: wgpu::TextureFormat::Rgba8Unorm, - blend: None, - write_mask: ColorWrites::ALL, - })], - compilation_options: PipelineCompilationOptions::default(), - }), - primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleStrip, - ..Default::default() - }, - depth_stencil: None, - multisample: wgpu::MultisampleState::default(), - multiview_mask: None, - cache: pipeline_cache, - }); - Self { layer: clear_pipeline, root: root_clear_pipeline, - atlas: atlas_clear_pipeline, } } }
diff --git a/vello_gpu/src/resources.rs b/vello_gpu/src/resources.rs index ba3ad9f..b55534b 100644 --- a/vello_gpu/src/resources.rs +++ b/vello_gpu/src/resources.rs
@@ -23,9 +23,9 @@ } impl Resources { - pub(crate) fn new(image_atlas_config: AtlasConfig) -> Self { + pub(crate) fn new(glyph_atlas_config: AtlasConfig) -> Self { Self { - image_cache: ImageCache::new_with_config(image_atlas_config), + image_cache: ImageCache::new_with_config(glyph_atlas_config), #[cfg(feature = "text")] glyph_prep_cache: GlyphPrepCache::default(), // Will be initialized lazily.
diff --git a/vello_gpu/src/scene.rs b/vello_gpu/src/scene.rs index fceac0d..dbfe0ba 100644 --- a/vello_gpu/src/scene.rs +++ b/vello_gpu/src/scene.rs
@@ -155,8 +155,8 @@ /// Settings controlling usage of GPU memory. #[derive(Copy, Clone, Debug, Default)] pub struct MemorySettings { - /// Configuration for the atlas holding uploaded images. - pub image_atlas_config: AtlasConfig, + /// Configuration for the atlas holding cached glyphs. + pub glyph_atlas_config: AtlasConfig, /// Configuration for intermediate layer and scratch textures. pub layers_config: LayersConfig, }
diff --git a/vello_gpu/src/text.rs b/vello_gpu/src/text.rs index f2ece83..b1d190b 100644 --- a/vello_gpu/src/text.rs +++ b/vello_gpu/src/text.rs
@@ -12,7 +12,7 @@ use core::ops::RangeInclusive; -use crate::{AtlasId, Resources, Scene}; +use crate::{Resources, Scene}; use glifo::atlas::{PendingBitmapUpload, PendingClearRect}; use glifo::renderer::replay_atlas_commands; use glifo::{ @@ -23,7 +23,7 @@ use peniko::color::palette::css::BLACK; use peniko::color::{AlphaColor, Srgb}; use vello_common::kurbo::{Affine, BezPath, Rect}; -use vello_common::multi_atlas::AtlasConfig; +use vello_common::multi_atlas::{AtlasConfig, AtlasId}; use vello_common::paint::{Image, ImageSource, PaintType}; use vello_common::peniko; @@ -361,7 +361,7 @@ #[inline] fn atlas_image_source(&self, atlas_slot: &AtlasSlot) -> ImageSource { - ImageSource::opaque_id(atlas_slot.image_id) + ImageSource::Atlas(atlas_slot.image_id) } #[inline]
diff --git a/vello_gpu_shaders/shaders/clear.wesl b/vello_gpu_shaders/shaders/clear.wesl index 31f736a..5d76970 100644 --- a/vello_gpu_shaders/shaders/clear.wesl +++ b/vello_gpu_shaders/shaders/clear.wesl
@@ -23,23 +23,6 @@ return ClearVertexOutput(vec4<f32>(ndc, 0.0, 1.0), unpack4x8unorm(color)); } -// This vertex shader is used for clearing atlas regions. -@vertex -fn vs_main_fullscreen(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> { - // This generates a quad that covers the entire render target (hence "fullscreen"), - // but the actual clearing region is controlled by the scissor test set on the render pass. - // This approach is more efficient than generating region-specific geometry because: - // 1. No vertex buffer needed - geometry generated from vertex index alone - // 2. Same simple shader works for any region size - // 3. Hardware scissor test efficiently clips to the desired region - // 4. GPU rasterizer + scissor test is faster than complex vertex calculations - // - // Map vertex_index (0-3) to fullscreen quad corners in NDC: - // 0 → (-1,-1), 1 → (1,-1), 2 → (-1,1), 3 → (1,1) - let ndc = quad_corner(vertex_index) * 2.0 - vec2(1.0); - return vec4<f32>(ndc, 0.0, 1.0); -} - @fragment fn fs_main( @interpolate(flat) @@ -47,8 +30,3 @@ ) -> @location(0) vec4<f32> { return color; } - -@fragment -fn fs_transparent(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> { - return vec4<f32>(0.0, 0.0, 0.0, 0.0); -}
diff --git a/vello_tests/snapshots/external_texture_atlas_interleaving.png b/vello_tests/snapshots/external_texture_atlas_interleaving.png index a35dd48..23e425c 100644 --- a/vello_tests/snapshots/external_texture_atlas_interleaving.png +++ b/vello_tests/snapshots/external_texture_atlas_interleaving.png
@@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01045880a522829e8562753dd1e7df94d5fee0f701e9ee7a2f1ea763e00947d1 -size 154 +oid sha256:39be692c9350b7ad90f6dcf66b0c5a48b2e4ca7f3ea69881d1de3886c62431ed +size 245
diff --git a/vello_tests/snapshots/image_with_transform_skew_x_1.png b/vello_tests/snapshots/image_with_transform_skew_x_1.png index 61f26eb..278b26d 100644 --- a/vello_tests/snapshots/image_with_transform_skew_x_1.png +++ b/vello_tests/snapshots/image_with_transform_skew_x_1.png
@@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8254c076f4696a530fb15126ec7d2f4f742db8669a98d606441eed269a1122a -size 167 +oid sha256:708b03c57bed3a0316f9915fac717d82755e78407a815752c9b873fde3db9ed5 +size 193
diff --git a/vello_tests/snapshots/image_with_transform_skew_x_2.png b/vello_tests/snapshots/image_with_transform_skew_x_2.png index 3231647..eb586ff 100644 --- a/vello_tests/snapshots/image_with_transform_skew_x_2.png +++ b/vello_tests/snapshots/image_with_transform_skew_x_2.png
@@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3621a2ec080f47882f1c43436b72ecf80bc493fa56195926cda2850826d4b8e8 -size 173 +oid sha256:c524e04209cebba067b1f46034e8db005f445fb32b24f47ee0d2bdd27cec062e +size 198
diff --git a/vello_tests/snapshots/image_with_transform_skew_y_1.png b/vello_tests/snapshots/image_with_transform_skew_y_1.png index f90a2e1..dc4be15 100644 --- a/vello_tests/snapshots/image_with_transform_skew_y_1.png +++ b/vello_tests/snapshots/image_with_transform_skew_y_1.png
@@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e4845f4b959226b1a0c43e7b91f6f2d145aa3215fb726ddbed712686fff7ce7 -size 394 +oid sha256:6c15a14c85183c474fa41e1b4e36404517d69844c17089aa8d5e8f8666ae5201 +size 466
diff --git a/vello_tests/snapshots/image_with_transform_skew_y_2.png b/vello_tests/snapshots/image_with_transform_skew_y_2.png index 4cf719b..fc2fe82 100644 --- a/vello_tests/snapshots/image_with_transform_skew_y_2.png +++ b/vello_tests/snapshots/image_with_transform_skew_y_2.png
@@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd39139982043996e84c1d220ddc58c7c6a58cb818ae46543d4b045bc1a4abf5 -size 383 +oid sha256:3c1b03a0c68595eef96b5352cd2306942b548cc0d231537d919373727632618e +size 477
diff --git a/vello_tests/src/renderer.rs b/vello_tests/src/renderer.rs index 2210cf9..908a00b 100644 --- a/vello_tests/src/renderer.rs +++ b/vello_tests/src/renderer.rs
@@ -9,7 +9,7 @@ use vello_common::filter_effects::Filter; use vello_common::kurbo::{Affine, BezPath, Rect, Stroke}; use vello_common::mask::Mask; -use vello_common::paint::{ImageId, ImageSource, PaintType, TextureRegion, Tint}; +use vello_common::paint::{ImageSource, PaintType, TextureRegion, Tint}; use vello_common::peniko::{BlendMode, Fill, FontData}; use vello_common::pixmap::Pixmap; use vello_cpu::{ @@ -102,7 +102,6 @@ fn snapshot(&mut self) -> Pixmap; fn register_external_texture(&mut self, pixmap: Arc<Pixmap>) -> TextureId; fn get_image_source(&mut self, pixmap: Arc<Pixmap>) -> ImageSource; - fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId; } pub struct CpuRenderer { @@ -354,12 +353,7 @@ } fn get_image_source(&mut self, pixmap: Arc<Pixmap>) -> ImageSource { - let id = self.resources.register_image(Arc::clone(&pixmap)); - ImageSource::opaque_id_with_transparency_hint(id, pixmap.may_have_transparency()) - } - - fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId { - self.resources.register_image(pixmap) + ImageSource::Pixmap(pixmap) } } @@ -507,21 +501,6 @@ self.gpu_test_guard = Some(WGPU_TEST_MUTEX.lock().unwrap()); } } - - fn upload_image_with_resources( - &mut self, - pixmap: &Arc<Pixmap>, - label: &'static str, - ) -> ImageId { - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some(label) }); - let image_id = self - .renderer - .upload_image(&mut self.resources, &mut encoder, pixmap); - self.queue.submit([encoder.finish()]); - image_id - } } #[cfg(not(all(target_arch = "wasm32", feature = "webgl")))] @@ -882,12 +861,9 @@ } fn get_image_source(&mut self, pixmap: Arc<Pixmap>) -> ImageSource { - let image_id = self.upload_image_with_resources(&pixmap, "Upload Test Image"); - ImageSource::opaque_id_with_transparency_hint(image_id, pixmap.may_have_transparency()) - } - - fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId { - self.upload_image_with_resources(&pixmap, "Register Test Image") + let may_have_transparency = pixmap.may_have_transparency(); + let texture_id = self.register_external_texture(pixmap); + ImageSource::external_texture(texture_id, TextureRegion::Full, may_have_transparency) } } @@ -1027,12 +1003,6 @@ depth.as_ref(), )); } - - fn upload_image(&mut self, pixmap: &Arc<Pixmap>) -> ImageId { - self.renderer - .upload_image(&mut self.resources, pixmap) - .unwrap() - } } #[cfg(all(target_arch = "wasm32", feature = "webgl"))] @@ -1355,12 +1325,9 @@ } fn get_image_source(&mut self, pixmap: Arc<Pixmap>) -> ImageSource { - let image_id = self.upload_image(&pixmap); - ImageSource::opaque_id_with_transparency_hint(image_id, pixmap.may_have_transparency()) - } - - fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId { - self.upload_image(&pixmap) + let may_have_transparency = pixmap.may_have_transparency(); + let texture_id = self.register_external_texture(pixmap); + ImageSource::external_texture(texture_id, TextureRegion::Full, may_have_transparency) } }
diff --git a/vello_tests/tests/external_texture.rs b/vello_tests/tests/external_texture.rs index 4677b5a..758b630 100644 --- a/vello_tests/tests/external_texture.rs +++ b/vello_tests/tests/external_texture.rs
@@ -17,6 +17,7 @@ use crate::load_image; use crate::renderer::Renderer; + use crate::util::layout_glyphs_roboto; const SPRITES: [RectU16; 4] = [ // Checkmark. @@ -106,18 +107,17 @@ )); } - fn draw_atlas_rect(ctx: &mut impl Renderer, image: ImageSource, rect: Rect) { - ctx.set_paint_transform(Affine::IDENTITY); - ctx.set_paint(Image { - image, - sampler: ImageSampler { - x_extend: Extend::Pad, - y_extend: Extend::Pad, - quality: ImageQuality::Low, - alpha: 1.0, - }, - }); - ctx.fill_rect(&rect); + fn fill_cached_glyph(ctx: &mut impl Renderer, color: AlphaColor<Srgb>, x: f64, y: f64) { + let font_size = 32.; + let (font, glyphs) = layout_glyphs_roboto("H", font_size); + ctx.set_transform(Affine::translate((x, y))); + ctx.set_paint(color); + ctx.glyph_run(&font) + .font_size(font_size) + .atlas_cache(true) + .fill_glyphs(glyphs.into_iter()) + .unwrap(); + ctx.set_transform(Affine::IDENTITY); } fn texture_circle(ctx: &mut impl Renderer, texture_id: TextureId, circle: Circle) { @@ -420,17 +420,14 @@ #[vello_test] fn external_texture_atlas_interleaving(ctx: &mut impl Renderer) { - let atlas_red = ctx.get_image_source(solid_pixmap(254, 0, 0, 254)); let external_green = ctx.register_external_texture(solid_pixmap(0, 254, 0, 254)); - let atlas_blue = ctx.get_image_source(solid_pixmap(0, 0, 254, 254)); let external_yellow = ctx.register_external_texture(solid_pixmap(254, 254, 0, 254)); - let atlas_magenta = ctx.get_image_source(solid_pixmap(254, 0, 254, 254)); - draw_atlas_rect(ctx, atlas_red, Rect::new(10., 10., 34., 34.)); + fill_cached_glyph(ctx, AlphaColor::from_rgba8(254, 0, 0, 254), 10., 34.); fill_texture_rect(ctx, external_green, 66., 10., 24., 24., true); - draw_atlas_rect(ctx, atlas_blue, Rect::new(38., 38., 62., 62.)); + fill_cached_glyph(ctx, AlphaColor::from_rgba8(0, 0, 254, 254), 38., 62.); fill_texture_rect(ctx, external_yellow, 10., 66., 24., 24., true); - draw_atlas_rect(ctx, atlas_magenta, Rect::new(66., 66., 90., 90.)); + fill_cached_glyph(ctx, AlphaColor::from_rgba8(254, 0, 254, 254), 66., 90.); } #[vello_test]
diff --git a/vello_tests/tests/image.rs b/vello_tests/tests/image.rs index 46f33bb..e126a9b 100644 --- a/vello_tests/tests/image.rs +++ b/vello_tests/tests/image.rs
@@ -210,11 +210,11 @@ ); } -#[vello_test(gpu_diff_pixels = 64)] +#[vello_test] fn image_with_transform_skew_x_1(ctx: &mut impl Renderer) { transform( ctx, - Affine::translate((-50.0, 0.0)) * Affine::skew(tan_45(), 0.0), + Affine::translate((-49.5, 0.0)) * Affine::skew(tan_45(), 0.0), 25.0, 25.0, 75.0, @@ -222,11 +222,11 @@ ); } -#[vello_test(gpu_diff_pixels = 64)] +#[vello_test] fn image_with_transform_skew_x_2(ctx: &mut impl Renderer) { transform( ctx, - Affine::translate((50.0, 0.0)) * Affine::skew(-tan_45(), 0.0), + Affine::translate((50.5, 0.0)) * Affine::skew(-tan_45(), 0.0), 25.0, 25.0, 75.0, @@ -234,11 +234,11 @@ ); } -#[vello_test(gpu_diff_pixels = 64)] +#[vello_test] fn image_with_transform_skew_y_1(ctx: &mut impl Renderer) { transform( ctx, - Affine::translate((0.0, 50.0)) * Affine::skew(0.0, -tan_45()), + Affine::translate((0.0, 50.5)) * Affine::skew(0.0, -tan_45()), 25.0, 25.0, 75.0, @@ -246,11 +246,11 @@ ); } -#[vello_test(gpu_diff_pixels = 64)] +#[vello_test] fn image_with_transform_skew_y_2(ctx: &mut impl Renderer) { transform( ctx, - Affine::translate((0.0, -50.0)) * Affine::skew(0.0, tan_45()), + Affine::translate((0.0, -49.5)) * Affine::skew(0.0, tan_45()), 25.0, 25.0, 75.0, @@ -491,7 +491,7 @@ ); } -#[vello_test] +#[vello_test(gpu_tolerance = 1)] fn image_bilinear_5x_scale(ctx: &mut impl Renderer) { let image_source = rgb_img_2x2(ctx); quality( @@ -503,7 +503,7 @@ ); } -#[vello_test] +#[vello_test(gpu_tolerance = 1)] fn image_bilinear_10x_scale(ctx: &mut impl Renderer) { let image_source = rgb_img_2x2(ctx); quality( @@ -515,7 +515,7 @@ ); } -#[vello_test] +#[vello_test(gpu_tolerance = 1)] fn image_bilinear_with_rotation(ctx: &mut impl Renderer) { let image_source = rgb_img_2x2(ctx); quality( @@ -527,7 +527,7 @@ ); } -#[vello_test] +#[vello_test(gpu_tolerance = 1)] fn image_bilinear_with_translation(ctx: &mut impl Renderer) { let image_source = rgb_img_2x2(ctx); quality( @@ -708,11 +708,9 @@ ]; /// 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_gpu)] fn image_spritesheet(ctx: &mut impl Renderer) { - let atlas_id = ctx.register_image(load_image!("glyph_atlas")); - let atlas_src = ImageSource::opaque_id(atlas_id); + let atlas_src = ctx.get_image_source(load_image!("glyph_atlas")); let start_x = 10.0; let start_y = 8.0; @@ -755,8 +753,7 @@ /// Same as `image_spritesheet`, but renders "hello world" with a purple tint. #[vello_test(width = 60, height = 30, skip_multithreaded)] fn image_spritesheet_tinted(ctx: &mut impl Renderer) { - let atlas_id = ctx.register_image(load_image!("glyph_atlas")); - let atlas_src = ImageSource::opaque_id(atlas_id); + let atlas_src = ctx.get_image_source(load_image!("glyph_atlas")); let start_x = 10.0; let start_y = 8.0;
diff --git a/vello_tests/tests/image_atlas.rs b/vello_tests/tests/image_atlas.rs deleted file mode 100644 index 4add876..0000000 --- a/vello_tests/tests/image_atlas.rs +++ /dev/null
@@ -1,152 +0,0 @@ -// Copyright 2026 the Vello Authors -// SPDX-License-Identifier: Apache-2.0 OR MIT - -//! Tests for the image/glyph atlas configuration of the WebGL renderer. - -use vello_common::{ - kurbo::Rect, - paint::{Image, ImageSource, TextureRegion}, - peniko::ImageSampler, - pixmap::Pixmap, -}; -use vello_gpu::{ - AtlasConfig, AtlasId, AtlasTextureInfo, MemorySettings, RenderError, RenderSettings, Scene, - TextureId, WebGlError, WebGlRenderer, WebGlTextureBindings, -}; -use wasm_bindgen::JsCast; -use wasm_bindgen_test::*; -use web_sys::{HtmlCanvasElement, WebGl2RenderingContext}; - -fn create_canvas() -> HtmlCanvasElement { - web_sys::window() - .unwrap() - .document() - .unwrap() - .create_element("canvas") - .unwrap() - .dyn_into() - .unwrap() -} - -#[wasm_bindgen_test] -fn image_atlas_texture_is_created_on_first_upload() { - let canvas = create_canvas(); - canvas.set_width(100); - canvas.set_height(100); - - let atlas_config = AtlasConfig { - initial_atlas_count: 0, - atlas_size: (10, 10), - ..AtlasConfig::default() - }; - let settings = RenderSettings::default().with_memory_settings(MemorySettings { - image_atlas_config: atlas_config, - ..MemorySettings::default() - }); - let (mut renderer, mut resources) = WebGlRenderer::new_with(&canvas, settings, true).unwrap(); - - assert_eq!( - renderer.atlas_info(), - AtlasTextureInfo { - width: 10, - height: 10, - texture_count: 0, - }, - "renderer should start without allocated atlas textures" - ); - assert_eq!( - renderer.gl_context().get_error(), - WebGl2RenderingContext::NO_ERROR, - "renderer initialization should not produce a WebGL error" - ); - - renderer - .upload_image(&mut resources, &Pixmap::new(2, 2)) - .unwrap(); - - assert_eq!( - renderer.atlas_info(), - AtlasTextureInfo { - width: 10, - height: 10, - texture_count: 1, - }, - "first upload should allocate the first configured atlas texture" - ); - assert_eq!( - renderer.gl_context().get_error(), - WebGl2RenderingContext::NO_ERROR, - "first atlas upload should not produce a WebGL error" - ); -} - -/// Uploading an image that is larger than the configured atlas must fail with -/// `AtlasError::TextureTooLarge`. -/// -/// The renderer constructor configures the allocator in the returned resources, which runs the -/// `TextureTooLarge` check. -#[wasm_bindgen_test] -fn image_atlas_upload_larger_than_atlas_fails() { - let canvas = create_canvas(); - canvas.set_width(100); - canvas.set_height(100); - - let atlas_config = AtlasConfig { - atlas_size: (10, 10), - ..AtlasConfig::default() - }; - let settings = RenderSettings::default().with_memory_settings(MemorySettings { - image_atlas_config: atlas_config, - ..MemorySettings::default() - }); - - let (mut renderer, mut resources) = WebGlRenderer::new_with(&canvas, settings, true).unwrap(); - - // The image is much larger than the 10x10 atlas, so the upload must fail. - let image = Pixmap::new(64, 64); - assert!( - matches!( - renderer.upload_image(&mut resources, &image), - Err(WebGlError::Render(RenderError::AtlasError(_))) - ), - "oversized image upload should return an atlas error" - ); -} - -#[wasm_bindgen_test] -fn image_atlas_rejects_sampling_from_its_render_target() { - let canvas = create_canvas(); - let atlas_config = AtlasConfig { - initial_atlas_count: 1, - atlas_size: (1, 1), - ..AtlasConfig::default() - }; - let settings = RenderSettings::default().with_memory_settings(MemorySettings { - image_atlas_config: atlas_config, - ..MemorySettings::default() - }); - let (mut renderer, _) = WebGlRenderer::new_with(&canvas, settings, false).unwrap(); - let texture_id = TextureId(0); - let mut bindings = WebGlTextureBindings::new(); - bindings.insert( - texture_id, - renderer.atlas_texture(AtlasId::new(0)).clone(), - 1, - 1, - ); - - let mut scene = Scene::new(1, 1); - scene.set_paint(Image { - image: ImageSource::external_texture(texture_id, TextureRegion::Full, false), - sampler: ImageSampler::default(), - }); - scene.fill_rect(&Rect::new(0.0, 0.0, 1.0, 1.0)); - - assert!( - matches!( - renderer.render_to_atlas(&scene, 1, atlas_config, AtlasId::new(0), &bindings), - Err(WebGlError::Render(RenderError::TextureFeedbackLoop(id))) if id == texture_id - ), - "sampling from the render target should fail" - ); -}
diff --git a/vello_tests/tests/mod.rs b/vello_tests/tests/mod.rs index e6258fa..acceb30 100644 --- a/vello_tests/tests/mod.rs +++ b/vello_tests/tests/mod.rs
@@ -42,8 +42,6 @@ mod gpu_schedule; mod gradient; mod image; -#[cfg(all(target_arch = "wasm32", feature = "webgl"))] -mod image_atlas; mod issues; mod layer; mod mask; @@ -66,4 +64,6 @@ mod wgpu_probe; #[cfg(not(target_arch = "wasm32"))] mod wgpu_resource_texture; +#[cfg(not(target_arch = "wasm32"))] +mod wgpu_texture_target; mod wide_tile;
diff --git a/vello_tests/tests/webgl_texture_target.rs b/vello_tests/tests/webgl_texture_target.rs index 9d557fe..8cdcf06 100644 --- a/vello_tests/tests/webgl_texture_target.rs +++ b/vello_tests/tests/webgl_texture_target.rs
@@ -7,13 +7,16 @@ use vello_common::color::palette::css::{BLUE, RED}; use vello_common::color::{AlphaColor, Srgb}; use vello_common::kurbo::Rect; +use vello_common::paint::{Image, ImageSource, TextureRegion}; +use vello_common::peniko::ImageSampler; use vello_gpu::{ - RenderSettings, Resources, Scene, WebGlRenderOptions, WebGlRenderTarget, WebGlRenderer, - WebGlTextureBindings, + RenderError, RenderSettings, Resources, Scene, TextureId, WebGlError, WebGlRenderOptions, + WebGlRenderTarget, WebGlRenderer, WebGlTextureBindings, }; use vello_tests::renderer::{TestTextureTarget, create_webgl_depth_buffer, read_webgl_pixels}; use wasm_bindgen::JsCast; use wasm_bindgen_test::*; +use web_sys::WebGl2RenderingContext; const SIZE: u16 = 8; @@ -109,3 +112,47 @@ assert_all(&second.read_pixels(&gl), BLUE); } + +#[wasm_bindgen_test] +fn texture_target_rejects_sampling_from_itself() { + let (mut renderer, mut resources) = renderer(); + let gl = renderer.gl_context().clone(); + let texture = gl.create_texture().unwrap(); + gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture)); + gl.tex_storage_2d( + WebGl2RenderingContext::TEXTURE_2D, + 1, + WebGl2RenderingContext::RGBA8, + SIZE.into(), + SIZE.into(), + ); + gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, None); + let target = renderer + .create_texture_target(&texture, None, SIZE, SIZE) + .unwrap(); + let texture_id = TextureId(0); + let mut bindings = WebGlTextureBindings::new(); + bindings.insert(texture_id, texture, SIZE, SIZE); + + let mut scene = Scene::new(SIZE, SIZE); + scene.set_paint(Image { + image: ImageSource::external_texture(texture_id, TextureRegion::Full, false), + sampler: ImageSampler::default(), + }); + scene.fill_rect(&Rect::new(0.0, 0.0, f64::from(SIZE), f64::from(SIZE))); + + let result = renderer.render( + &scene, + &mut resources, + WebGlRenderTarget::Texture(&target), + &bindings, + WebGlRenderOptions::default(), + ); + assert!( + matches!( + result, + Err(WebGlError::Render(RenderError::TextureFeedbackLoop(id))) if id == texture_id + ), + "sampling from the render target should fail, got {result:?}" + ); +}
diff --git a/vello_tests/tests/wgpu_texture_target.rs b/vello_tests/tests/wgpu_texture_target.rs new file mode 100644 index 0000000..5159bf4 --- /dev/null +++ b/vello_tests/tests/wgpu_texture_target.rs
@@ -0,0 +1,71 @@ +// Copyright 2026 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Tests for rendering into a texture with the wgpu renderer. + +use vello_common::kurbo::Rect; +use vello_common::paint::{Image, ImageSource, TextureRegion}; +use vello_common::peniko::ImageSampler; +use vello_cpu::{Level, RenderMode}; +use vello_gpu::{ + RenderError, RenderOptions, RenderTarget, RenderTargetConfig, Scene, TextureBindings, TextureId, +}; + +use crate::renderer::{GpuRenderer, Renderer}; + +#[test] +fn texture_target_rejects_sampling_from_itself() { + let ctx = GpuRenderer::new(1, 1, 0, Level::baseline(), RenderMode::OptimizeSpeed); + let device = ctx.device(); + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: None, + size: wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let (mut renderer, mut resources) = vello_gpu::Renderer::new( + device, + ctx.queue(), + &RenderTargetConfig { + format: texture.format(), + width: 1, + height: 1, + }, + ); + let texture_id = TextureId(0); + let mut bindings = TextureBindings::new(); + bindings.insert(texture_id, view.clone()); + + let mut scene = Scene::new(1, 1); + scene.set_paint(Image { + image: ImageSource::external_texture(texture_id, TextureRegion::Full, false), + sampler: ImageSampler::default(), + }); + scene.fill_rect(&Rect::new(0.0, 0.0, 1.0, 1.0)); + + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + let result = renderer.render( + &scene, + &mut resources, + &mut encoder, + RenderTarget { + view: &view, + depth_view: None, + }, + &bindings, + RenderOptions::default(), + ); + assert!( + matches!(result, Err(RenderError::TextureFeedbackLoop(id)) if id == texture_id), + "sampling from the render target should fail, got {result:?}" + ); +}
diff --git a/vello_tests/vello_example_scenes/src/image.rs b/vello_tests/vello_example_scenes/src/image.rs index 9efa731..71ed040 100644 --- a/vello_tests/vello_example_scenes/src/image.rs +++ b/vello_tests/vello_example_scenes/src/image.rs
@@ -10,12 +10,18 @@ use vello_common::pixmap::{PixelMetadata, Pixmap}; use vello_common::{ kurbo::{Affine, Rect}, - paint::{Image, ImageSource}, + paint::{Image, ImageSource, TextureRegion}, peniko::{Extend, ImageQuality}, }; +use vello_gpu::TextureId; use crate::{ExampleScene, RenderingContext}; +/// The flower image's texture ID. +pub const FLOWER_TEXTURE_ID: TextureId = TextureId(1); +/// The cowboy image's texture ID. +pub const COWBOY_TEXTURE_ID: TextureId = TextureId(2); + /// Image scene state #[derive(Debug, Default)] pub struct ImageScene { @@ -174,6 +180,14 @@ } impl ImageScene { + /// The image sources for the external textures. + pub fn external_texture_sources() -> Vec<ImageSource> { + vec![ + ImageSource::external_texture(FLOWER_TEXTURE_ID, TextureRegion::Full, false), + ImageSource::external_texture(COWBOY_TEXTURE_ID, TextureRegion::Full, true), + ] + } + /// Read the flower image pub fn read_flower_image() -> Pixmap { let image_data = include_bytes!("../../../assets/splash-flower.jpg");