vello_hybrid: Replace the 2D texture array with individual atlas textures
diff --git a/sparse_strips/vello_hybrid/src/draw.rs b/sparse_strips/vello_hybrid/src/draw.rs index 96431d4..cd72526 100644 --- a/sparse_strips/vello_hybrid/src/draw.rs +++ b/sparse_strips/vello_hybrid/src/draw.rs
@@ -6,13 +6,13 @@ use crate::GpuStrip; use crate::paint::{ COLOR_SOURCE_LAYER, COLOR_SOURCE_SHIFT, EXTERNAL_TEXTURE_SLOT_SHIFT, PaintResolver, + TextureSourceId, }; use crate::rect::{RectPart, split_rect}; use crate::scene::{RecordedDraw, RecordedPath}; use crate::target::{DrawTarget, LayerTextureRegion}; use crate::util::{Ranges, VecExt, pack_opacity, pack_u16_pair}; use alloc::vec::Vec; -use vello_common::TextureId; use vello_common::geometry::RectU16; use vello_common::kurbo::Rect; use vello_common::paint::Paint; @@ -27,7 +27,7 @@ pub(crate) struct Draw { /// Ranges selecting this draw's strips from [`DrawBuffers::strips`]. pub(crate) strip_ranges: Ranges, - /// Runs that require an externally supplied texture binding. + /// Runs that require external texture bindings. pub(crate) external_texture_runs: Vec<ExternalTextureRun>, /// Whether any strip in this draw samples a child layer. pub(crate) has_child_layer: bool, @@ -39,12 +39,12 @@ &mut self, strips: &mut Vec<GpuStrip>, mut gpu_strip: GpuStrip, - external_texture_id: Option<TextureId>, + texture_source: Option<TextureSourceId>, ) { if let Some(slot) = assign_external_texture_slot( &mut self.external_texture_runs, self.strip_ranges.len(), - external_texture_id, + texture_source, ) { gpu_strip.paint_and_rect_flag |= u32::from(slot) << EXTERNAL_TEXTURE_SLOT_SHIFT; } @@ -69,11 +69,11 @@ } impl OpaqueDraw { - fn push(&mut self, mut strip: GpuStrip, external_texture_id: Option<TextureId>) { + fn push(&mut self, mut strip: GpuStrip, texture_source: Option<TextureSourceId>) { if let Some(slot) = assign_external_texture_slot( &mut self.external_texture_runs, self.strips.len(), - external_texture_id, + texture_source, ) { strip.paint_and_rect_flag |= u32::from(slot) << EXTERNAL_TEXTURE_SLOT_SHIFT; } @@ -156,12 +156,12 @@ } } - fn push_opaque(&mut self, strip: GpuStrip, external_texture_id: Option<TextureId>) -> bool { + fn push_opaque(&mut self, strip: GpuStrip, texture_source: Option<TextureSourceId>) -> bool { if !self.state.use_depth_buffer || !self.state.target.enable_depth() { return false; } - self.opaque.push(strip, external_texture_id); + self.opaque.push(strip, texture_source); true } @@ -202,7 +202,7 @@ builder .draw - .push(builder.strips, strip, paint.external_texture_id); + .push(builder.strips, strip, paint.texture_source); }, |builder, segment| { let shifted = segment.shift(geometry_shift); @@ -216,10 +216,10 @@ depth_index, ); - if !paint.opaque || !builder.push_opaque(strip, paint.external_texture_id) { + if !paint.opaque || !builder.push_opaque(strip, paint.texture_source) { builder .draw - .push(builder.strips, strip, paint.external_texture_id); + .push(builder.strips, strip, paint.texture_source); } }, ); @@ -261,12 +261,8 @@ depth_index, ); - if !(paint.opaque - && part.frac == 0 - && self.push_opaque(strip, paint.external_texture_id)) - { - self.draw - .push(self.strips, strip, paint.external_texture_id); + if !(paint.opaque && part.frac == 0 && self.push_opaque(strip, paint.texture_source)) { + self.draw.push(self.strips, strip, paint.texture_source); } } } @@ -450,31 +446,33 @@ } } -/// Number of externally supplied textures that can be sampled by one strip draw. +/// Number of external textures that can be sampled by one strip draw. pub(crate) const EXTERNAL_TEXTURE_SLOT_COUNT: usize = 4; /// External texture bindings for one strip draw. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub(crate) struct ExternalTextureBindings { - texture_ids: [Option<TextureId>; EXTERNAL_TEXTURE_SLOT_COUNT], + texture_sources: [Option<TextureSourceId>; EXTERNAL_TEXTURE_SLOT_COUNT], } impl ExternalTextureBindings { pub(crate) const EMPTY: Self = Self { - texture_ids: [None; EXTERNAL_TEXTURE_SLOT_COUNT], + texture_sources: [None; EXTERNAL_TEXTURE_SLOT_COUNT], }; - /// Return the existing slot for `texture_id`, or insert it into the first empty slot. + /// Return the existing slot for `texture_source`, or insert it into the first empty slot. /// Returns `None` when all four slots are occupied by other textures. #[inline] - fn get_or_insert(&mut self, texture_id: TextureId) -> Option<u8> { + fn get_or_insert(&mut self, texture_source: TextureSourceId) -> Option<u8> { // This iteration order assumes slots are assigned without leaving "holes" in-between, // i.e. if we hit `None`, any later slot is also guaranteed to be `None`. - for (slot, candidate) in self.texture_ids.iter_mut().enumerate() { + for (slot, candidate) in self.texture_sources.iter_mut().enumerate() { match *candidate { - Some(id) if id == texture_id => return Some(u8::try_from(slot).unwrap()), + Some(source) if source == texture_source => { + return Some(u8::try_from(slot).unwrap()); + } None => { - *candidate = Some(texture_id); + *candidate = Some(texture_source); return Some(u8::try_from(slot).unwrap()); } _ => {} @@ -485,8 +483,8 @@ } #[inline] - pub(crate) fn as_array(self) -> [Option<TextureId>; EXTERNAL_TEXTURE_SLOT_COUNT] { - self.texture_ids + pub(crate) fn as_array(self) -> [Option<TextureSourceId>; EXTERNAL_TEXTURE_SLOT_COUNT] { + self.texture_sources } } @@ -503,19 +501,19 @@ fn assign_external_texture_slot( runs: &mut Vec<ExternalTextureRun>, strips_len: usize, - external_texture_id: Option<TextureId>, + texture_source: Option<TextureSourceId>, ) -> Option<u8> { - let texture_id = external_texture_id?; + let texture_source = texture_source?; if let Some(slot) = runs .last_mut() - .and_then(|run| run.bindings.get_or_insert(texture_id)) + .and_then(|run| run.bindings.get_or_insert(texture_source)) { return Some(slot); } let mut bindings = ExternalTextureBindings::EMPTY; - let slot = bindings.get_or_insert(texture_id).unwrap(); + let slot = bindings.get_or_insert(texture_source).unwrap(); let strips_start = if runs.is_empty() { 0 } else { strips_len }; runs.push(ExternalTextureRun { bindings, @@ -579,7 +577,7 @@ mod tests { use super::{Draw, DrawBuffers, DrawBuilder, DrawState, ExternalTextureRun, OpaqueDraw}; use crate::GpuStrip; - use crate::paint::{EXTERNAL_TEXTURE_SLOT_SHIFT, PaintResolver}; + use crate::paint::{EXTERNAL_TEXTURE_SLOT_SHIFT, PaintResolver, TextureSourceId}; use crate::scene::{RecordedDraw, RecordedRect}; use crate::target::{ DrawTarget, LayerTextureId, LayerTextureRegion, RootTarget, TextureParity, TextureRegion, @@ -589,7 +587,9 @@ use vello_common::TextureId; use vello_common::encode::{EncodedImage, EncodedPaint}; use vello_common::geometry::RectU16; + use vello_common::image_cache::ImageCache; use vello_common::kurbo::{Affine, Rect, Vec2}; + use vello_common::multi_atlas::{AtlasConfig, AtlasId}; use vello_common::paint::{ImageId, ImageSource, IndexedPaint, Paint, PremulColor}; use vello_common::peniko::color::palette::css::BLUE; use vello_common::peniko::{Extend, ImageQuality, ImageSampler}; @@ -676,7 +676,15 @@ .collect() } - fn run_states(runs: &[ExternalTextureRun]) -> Vec<([Option<TextureId>; 4], usize)> { + fn texture_ids<const N: usize>() -> [TextureId; N] { + core::array::from_fn(|index| TextureId(index as u64)) + } + + fn external_sources<const N: usize>() -> [TextureSourceId; N] { + texture_ids().map(TextureSourceId::External) + } + + fn run_states(runs: &[ExternalTextureRun]) -> Vec<([Option<TextureSourceId>; 4], usize)> { runs.iter() .map(|run| (run.bindings.as_array(), run.strips_start)) .collect() @@ -699,9 +707,9 @@ }) } - fn atlas_image() -> EncodedPaint { + fn atlas_image(image_id: ImageId) -> EncodedPaint { EncodedPaint::Image(EncodedImage { - source: ImageSource::opaque_id(ImageId::new(0)), + source: ImageSource::opaque_id(image_id), sampler: ImageSampler { x_extend: Extend::Pad, y_extend: Extend::Pad, @@ -728,8 +736,7 @@ #[test] fn texture_runs() { - let texture_a = TextureId(10); - let texture_b = TextureId(20); + let [texture_a, texture_b] = texture_ids(); let encoded = [external(texture_a), external(texture_b)]; let offsets = [0, 0]; let resolver = PaintResolver::new(&encoded, &offsets); @@ -747,19 +754,21 @@ assert_eq!( run_states(&draw.external_texture_runs), - [([Some(texture_a), Some(texture_b), None, None], 0)] + [( + [ + Some(TextureSourceId::External(texture_a)), + Some(TextureSourceId::External(texture_b)), + None, + None, + ], + 0, + )] ); } #[test] fn fifth_external_texture_starts_a_fresh_run() { - let textures = [ - TextureId(10), - TextureId(20), - TextureId(30), - TextureId(40), - TextureId(50), - ]; + let textures: [TextureId; 5] = texture_ids(); let encoded = textures.map(external); let offsets = [0; 5]; let resolver = PaintResolver::new(&encoded, &offsets); @@ -780,14 +789,22 @@ [ ( [ - Some(textures[0]), - Some(textures[1]), - Some(textures[2]), - Some(textures[3]) + Some(TextureSourceId::External(textures[0])), + Some(TextureSourceId::External(textures[1])), + Some(TextureSourceId::External(textures[2])), + Some(TextureSourceId::External(textures[3])) ], 0 ), - ([Some(textures[4]), Some(textures[0]), None, None], 4), + ( + [ + Some(TextureSourceId::External(textures[4])), + Some(TextureSourceId::External(textures[0])), + None, + None, + ], + 4, + ), ] ); assert_eq!( @@ -802,7 +819,7 @@ #[test] fn texture_runs_coalesce_distinct_paints_for_same_texture() { - let texture = TextureId(10); + let [texture] = texture_ids(); let encoded = [external(texture), external(texture)]; let resolver = PaintResolver::new(&encoded, &[0, 3]); let mut case = DrawCase::new(RootTarget::UserSurface, RectU16::new(0, 0, 8, 8)); @@ -813,15 +830,20 @@ assert_eq!( run_states(&draw.external_texture_runs), - [([Some(texture), None, None, None], 0)] + [( + [Some(TextureSourceId::External(texture)), None, None, None], + 0, + )] ); } #[test] - fn texture_runs_collapse_across_atlas_images() { - let texture = TextureId(10); - let encoded = [external(texture), atlas_image()]; - let resolver = PaintResolver::new(&encoded, &[0, 0]); + fn texture_runs_include_atlas_images() { + let [texture] = texture_ids(); + let mut image_cache = ImageCache::new_with_config(AtlasConfig::default()); + let image_id = image_cache.allocate(1, 1, 0).unwrap(); + let encoded = [external(texture), atlas_image(image_id)]; + let resolver = PaintResolver::new(&encoded, &[0, 0]).with_image_cache(&image_cache); let mut case = DrawCase::new(RootTarget::UserSurface, RectU16::new(0, 0, 16, 8)); let mut draw = Draw::default(); @@ -830,26 +852,21 @@ } assert_eq!(draw.strip_ranges.len(), 3); - // Images in the atlas are handled separately from external textures, so - // it's fine to collapse them. assert_eq!( - run_states(&draw.external_texture_runs), - [([Some(texture), None, None, None], 0)] + draw.external_texture_runs[0].bindings.as_array(), + [ + Some(TextureSourceId::External(texture)), + Some(TextureSourceId::Atlas(AtlasId::new(0))), + None, + None, + ] ); + assert_eq!(draw.external_texture_runs[0].strips_start, 0); } #[test] fn opaque_reverse_rebases_texture_runs() { - let textures = [ - TextureId(10), - TextureId(20), - TextureId(30), - TextureId(40), - TextureId(50), - TextureId(60), - TextureId(70), - TextureId(80), - ]; + let textures: [TextureSourceId; 8] = external_sources(); let mut draw = OpaqueDraw::default(); for (x, texture_id) in [ @@ -962,7 +979,7 @@ #[test] fn draw_clear() { - let texture_id = TextureId(10); + let [texture_id] = texture_ids(); let encoded = [external(texture_id)]; let offsets = [0]; let resolver = PaintResolver::new(&encoded, &offsets);
diff --git a/sparse_strips/vello_hybrid/src/lib.rs b/sparse_strips/vello_hybrid/src/lib.rs index ae2d520..2281b8c 100644 --- a/sparse_strips/vello_hybrid/src/lib.rs +++ b/sparse_strips/vello_hybrid/src/lib.rs
@@ -128,6 +128,9 @@ /// A draw referenced a [`TextureId`] that was not provided at render time. #[error("Missing texture binding for {0:?}")] MissingTextureBinding(TextureId), + /// A texture binding aliases the active render target. + #[error("Texture binding {0:?} aliases the active render target")] + TextureFeedbackLoop(TextureId), /// An intermediate texture allocation failed. #[error(transparent)] IntermediateTexture(#[from] IntermediateTextureError),
diff --git a/sparse_strips/vello_hybrid/src/paint.rs b/sparse_strips/vello_hybrid/src/paint.rs index 135dc46..69c1a7c 100644 --- a/sparse_strips/vello_hybrid/src/paint.rs +++ b/sparse_strips/vello_hybrid/src/paint.rs
@@ -6,6 +6,8 @@ use crate::util::pack_u16_pair; use vello_common::TextureId; use vello_common::encode::{EncodedKind, EncodedPaint}; +use vello_common::image_cache::ImageCache; +use vello_common::multi_atlas::AtlasId; use vello_common::paint::{ImageSource, Paint}; const COLOR_SOURCE_PAYLOAD: u32 = 0; @@ -24,6 +26,15 @@ pub(crate) const EXTERNAL_TEXTURE_SLOT_SHIFT: u32 = 24; const PAINT_TEXTURE_INDEX_MASK: u32 = (1 << EXTERNAL_TEXTURE_SLOT_SHIFT) - 1; +/// Texture sampled by an image paint. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum TextureSourceId { + /// A renderer-owned image atlas. + Atlas(AtlasId), + /// A texture supplied through the render-time bindings. + External(TextureId), +} + /// Shader-ready paint metadata for a strip. #[derive(Clone, Copy)] pub(crate) struct PackedPaint { @@ -31,8 +42,8 @@ payload: PaintPayload, /// Packed paint kind, source, and data offset. pub(crate) paint: u32, - /// External texture required by this paint, if any. - pub(crate) external_texture_id: Option<TextureId>, + /// Texture required by this paint, if any. + pub(crate) texture_source: Option<TextureSourceId>, /// Whether the paint is fully opaque. pub(crate) opaque: bool, } @@ -62,6 +73,8 @@ encoded: &'a [EncodedPaint], /// GPU data offset corresponding to each encoded paint. gpu_offsets: &'a [u32], + /// Resolves internal images to their atlas textures. + image_cache: Option<&'a ImageCache>, } impl<'a> PaintResolver<'a> { @@ -69,9 +82,16 @@ Self { encoded, gpu_offsets, + image_cache: None, } } + /// Add the image cache needed to resolve internal image paints. + pub(crate) fn with_image_cache(mut self, image_cache: &'a ImageCache) -> Self { + self.image_cache = Some(image_cache); + self + } + #[inline] pub(crate) fn pack(self, paint: &Paint) -> PackedPaint { match paint { @@ -79,7 +99,7 @@ payload: PaintPayload::Solid(color.as_premul_rgba8().to_u32()), paint: (COLOR_SOURCE_PAYLOAD << COLOR_SOURCE_SHIFT) | (PAINT_TYPE_SOLID << PAINT_TYPE_SHIFT), - external_texture_id: None, + texture_source: None, opaque: color.is_opaque(), }, Paint::Indexed(indexed_paint) => { @@ -87,10 +107,18 @@ let gpu_offset = self.gpu_offsets[paint_id]; let encoded_paint = &self.encoded[paint_id]; - let (paint_type, external_texture_id) = match encoded_paint { + let (paint_type, texture_source) = match encoded_paint { EncodedPaint::Image(encoded_image) => match &encoded_image.source { - ImageSource::ExternalTexture { id, .. } => (PAINT_TYPE_IMAGE, Some(*id)), - ImageSource::OpaqueId { .. } => (PAINT_TYPE_IMAGE, None), + ImageSource::ExternalTexture { id, .. } => { + (PAINT_TYPE_IMAGE, Some(TextureSourceId::External(*id))) + } + ImageSource::OpaqueId { id, .. } => { + let image_resource = self.image_cache.unwrap().get(*id).unwrap(); + ( + PAINT_TYPE_IMAGE, + Some(TextureSourceId::Atlas(image_resource.atlas_id)), + ) + } ImageSource::Pixmap(_) => unimplemented!("Unsupported image source"), }, EncodedPaint::Gradient(gradient) => { @@ -113,7 +141,7 @@ paint: (COLOR_SOURCE_PAYLOAD << COLOR_SOURCE_SHIFT) | (paint_type << PAINT_TYPE_SHIFT) | (gpu_offset & PAINT_TEXTURE_INDEX_MASK), - external_texture_id, + texture_source, opaque: !encoded_paint.may_have_transparency(), } }
diff --git a/sparse_strips/vello_hybrid/src/render/common.rs b/sparse_strips/vello_hybrid/src/render/common.rs index 0330850..cacfde2 100644 --- a/sparse_strips/vello_hybrid/src/render/common.rs +++ b/sparse_strips/vello_hybrid/src/render/common.rs
@@ -37,8 +37,6 @@ pub(crate) struct DeviceLimits { /// Maximum width or height of a two-dimensional texture. pub(crate) max_texture_dimension_2d: u16, - /// Maximum number of layers in an image atlas texture array. - pub(crate) max_texture_array_layers: u16, } impl DeviceLimits { @@ -119,8 +117,6 @@ .clamp(1, device_limits.max_texture_dimension_2d) .into(); - let supported_max_atlases = usize::from(device_limits.max_texture_array_layers); - image_atlas_config.max_atlases = image_atlas_config.max_atlases.min(supported_max_atlases); image_atlas_config.initial_atlas_count = image_atlas_config .initial_atlas_count .min(image_atlas_config.max_atlases); @@ -198,15 +194,14 @@ use vello_common::multi_atlas::AtlasConfig; use vello_common::record::CommandRecorder; - fn device_limits(max_texture_dimension_2d: u16, max_texture_array_layers: u16) -> DeviceLimits { + fn device_limits(max_texture_dimension_2d: u16) -> DeviceLimits { DeviceLimits { max_texture_dimension_2d, - max_texture_array_layers, } } #[test] - fn normalize_memory_settings_clamps_atlas_config_to_backend_limits() { + fn normalize_memory_settings_clamps_atlas_dimensions_to_backend_limits() { let mut settings = MemorySettings { image_atlas_config: AtlasConfig { initial_atlas_count: 8, @@ -217,24 +212,18 @@ ..Default::default() }; - settings.normalize(&device_limits(4096, 4)); + settings.normalize(&device_limits(4096)); let config = settings.image_atlas_config; - assert_eq!(config.initial_atlas_count, 4); - assert_eq!(config.max_atlases, 4); + assert_eq!(config.initial_atlas_count, 8); + assert_eq!(config.max_atlases, 16); assert_eq!(config.atlas_size, (4096, 2048)); } #[test] fn resource_texture_dimension_is_capped_at_4k() { - assert_eq!( - device_limits(16384, 256).resource_texture_dimension_2d(), - 4096 - ); - assert_eq!( - device_limits(2048, 256).resource_texture_dimension_2d(), - 2048 - ); + assert_eq!(device_limits(16384).resource_texture_dimension_2d(), 4096); + assert_eq!(device_limits(2048).resource_texture_dimension_2d(), 2048); } #[test] @@ -248,7 +237,7 @@ ..Default::default() }; - settings.normalize(&device_limits(4096, 8)); + settings.normalize(&device_limits(4096)); let config = settings.image_atlas_config; assert_eq!(config.initial_atlas_count, 0); @@ -257,23 +246,6 @@ } #[test] - fn normalize_memory_settings_caps_atlas_count_to_backend_limit() { - let mut settings = MemorySettings { - image_atlas_config: AtlasConfig { - initial_atlas_count: 8, - max_atlases: 8, - ..Default::default() - }, - ..Default::default() - }; - - settings.normalize(&device_limits(4096, 1)); - - assert_eq!(settings.image_atlas_config.initial_atlas_count, 1); - assert_eq!(settings.image_atlas_config.max_atlases, 1); - } - - #[test] fn normalize_memory_settings_clamps_minimum_to_maximum() { let mut settings = MemorySettings { layers_config: LayersConfig { @@ -284,7 +256,7 @@ ..Default::default() }; - settings.normalize(&device_limits(8192, 256)); + settings.normalize(&device_limits(8192)); assert_eq!( settings.layers_config.min_texture_size, @@ -307,7 +279,7 @@ ..Default::default() }; - settings.normalize(&device_limits(768, 256)); + settings.normalize(&device_limits(768)); assert_eq!( settings.layers_config.min_texture_size, @@ -494,8 +466,7 @@ #[derive(Debug, Clone, Copy, Zeroable, Pod)] #[allow(dead_code, reason = "Clippy fails when --no-default-features")] pub(crate) struct GpuEncodedImage { - /// Packed rendering quality, extend modes, and atlas index. - /// Bits 6-13: `atlas_index` (8 bits, supports up to 256 atlases) + /// Packed rendering quality and extend modes. /// Bits 4-5: `extend_y` (2 bits) /// Bits 2-3: `extend_x` (2 bits) /// Bits 0-1: `quality` (2 bits) @@ -643,29 +614,16 @@ ((x as u32) << 16) | (y as u32) } -/// Pack image `quality`, extend modes, `atlas_index`, and source type into a single u32. -/// `is_external`: stored in bit 14 -/// `atlas_index`: stored in bits 6-13 (8 bits, supports up to 256 atlases) +/// Pack image `quality` and extend modes into a single u32. /// `extend_y`: stored in bits 4-5 (2 bits) /// `extend_x`: stored in bits 2-3 (2 bits) /// `quality`: stored in bits 0-1 (2 bits) #[inline(always)] -pub(crate) fn pack_image_params( - quality: u32, - extend_x: u32, - extend_y: u32, - atlas_index: u32, - is_external: bool, -) -> u32 { +pub(crate) fn pack_image_params(quality: u32, extend_x: u32, extend_y: u32) -> u32 { debug_assert!(extend_x <= 3, "extend_x must be 0-3 (2 bits)"); debug_assert!(extend_y <= 3, "extend_y must be 0-3 (2 bits)"); debug_assert!(quality <= 3, "quality must be 0-3 (2 bits)"); - debug_assert!(atlas_index <= 255, "atlas_index must be 0-255 (8 bits)"); - (u32::from(is_external) << 14) - | (atlas_index << 6) - | (extend_y << 4) - | (extend_x << 2) - | quality + (extend_y << 4) | (extend_x << 2) | quality } /// Pack an optional [`Tint`](vello_common::paint::Tint) into a (`tint_color_u32`, `tint_mode_u32`) pair for the GPU.
diff --git a/sparse_strips/vello_hybrid/src/render/webgl/mod.rs b/sparse_strips/vello_hybrid/src/render/webgl/mod.rs index 13971a0..a720386 100644 --- a/sparse_strips/vello_hybrid/src/render/webgl/mod.rs +++ b/sparse_strips/vello_hybrid/src/render/webgl/mod.rs
@@ -33,7 +33,7 @@ copy::GpuCopyInstance, filter::{FilterContext, FilterInstanceData, FilterPassPlan}, gradient_cache::GradientRampCache, - paint::PaintResolver, + paint::{PaintResolver, TextureSourceId}, render::{ Config, common::{ @@ -91,7 +91,7 @@ }); /// First texture unit from which the strip program samples external textures. -const EXTERNAL_TEXTURE_UNIT_START: u32 = 5; +const EXTERNAL_TEXTURE_UNIT_START: u32 = 4; /// Query the WebGL context for the max texture size. fn get_max_texture_dimension_2d(gl: &WebGl2RenderingContext) -> u32 { @@ -101,13 +101,6 @@ .unwrap() as u32 } -fn get_max_texture_array_layers(gl: &WebGl2RenderingContext) -> u32 { - gl.get_parameter(WebGl2RenderingContext::MAX_ARRAY_TEXTURE_LAYERS) - .unwrap() - .as_f64() - .unwrap() as u32 -} - /// Result of polling WebGL renderer initialization. #[derive(Debug)] #[expect( @@ -201,15 +194,15 @@ } } -/// Current allocation state of the WebGL image/glyph atlas texture. +/// Current allocation state of the WebGL image/glyph atlas textures. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AtlasTextureInfo { - /// Width of the currently bound texture array. + /// Width of each atlas texture. pub width: u16, - /// Height of the currently bound texture array. + /// Height of each atlas texture. pub height: u16, - /// Number of real atlas layers available to the image cache. - pub layer_count: u32, + /// Number of atlas textures available to the image cache. + pub texture_count: u32, } impl WebGlRenderer { @@ -364,8 +357,6 @@ let device_limits = DeviceLimits { max_texture_dimension_2d: u16::try_from(get_max_texture_dimension_2d(&gl)) .unwrap_or(u16::MAX), - max_texture_array_layers: u16::try_from(get_max_texture_array_layers(&gl)) - .unwrap_or(u16::MAX), }; settings.memory_settings.normalize(&device_limits); if use_depth_buffer { @@ -461,6 +452,7 @@ true, RootTarget::UserSurface, texture_bindings, + None, )?; #[cfg(feature = "text")] @@ -503,13 +495,14 @@ texture_bindings: &WebGlTextureBindings, ) -> Result<(), RenderError> { self.programs - .maybe_resize_atlas_texture_array(&self.gl, atlas_count); + .maybe_create_atlas_textures(&self.gl, atlas_count); let (atlas_width, atlas_height) = atlas_config.atlas_size; let atlas_render_size = RenderSize { width: u32::from(atlas_width), height: u32::from(atlas_height), }; + let render_target_texture = self.atlas_texture(atlas_id).clone(); let atlas_framebuffer = self .programs @@ -521,25 +514,18 @@ WebGl2RenderingContext::FRAMEBUFFER, Some(&atlas_framebuffer), ); - self.gl.framebuffer_texture_layer( + self.gl.framebuffer_texture_2d( WebGl2RenderingContext::FRAMEBUFFER, WebGl2RenderingContext::COLOR_ATTACHMENT0, - Some(&self.programs.resources.atlas_texture_array.texture), + WebGl2RenderingContext::TEXTURE_2D, + Some(&self.programs.resources.atlas_textures[atlas_id.as_u32() as usize]), 0, - atlas_id.as_u32() as i32, ); // Set the view framebuffer override so the scheduler renders to the // atlas layer instead of the default framebuffer. self.programs.resources.view_framebuffer_override = Some(atlas_framebuffer); - // Swap in the stub atlas texture array to avoid binding the real atlas - // texture as a shader input while it is also the render target. - core::mem::swap( - &mut self.programs.resources.atlas_texture_array, - &mut self.programs.resources.stub_atlas_texture_array, - ); - // TODO: Explore using an option instead of a dummy image cache. let dummy_image_cache = self .dummy_image_cache @@ -552,15 +538,10 @@ false, RootTarget::AtlasLayer, texture_bindings, + Some(&render_target_texture), ); self.dummy_image_cache = Some(dummy_image_cache); - // Restore the real atlas texture array. - core::mem::swap( - &mut self.programs.resources.atlas_texture_array, - &mut self.programs.resources.stub_atlas_texture_array, - ); - // Restore the default view framebuffer and cache the atlas FBO for reuse. self.programs.resources.atlas_render_framebuffer = self.programs.resources.view_framebuffer_override.take(); @@ -595,10 +576,16 @@ clear: bool, root_output_target: RootTarget, texture_bindings: &WebGlTextureBindings, + render_target_texture: Option<&WebGlTexture>, ) -> Result<(), RenderError> { let encoded_paints = &scene.encoded_paints; - self.prepare_gpu_encoded_paints(encoded_paints, image_cache, texture_bindings)?; + self.prepare_gpu_encoded_paints( + encoded_paints, + image_cache, + texture_bindings, + render_target_texture, + )?; let mut required_texture_size = self .layers_config @@ -614,7 +601,8 @@ let current_allocations = self.current_allocations(); - let paint_resolver = PaintResolver::new(encoded_paints, &self.paint_idxs); + let paint_resolver = + PaintResolver::new(encoded_paints, &self.paint_idxs).with_image_cache(image_cache); let schedule = Schedule::try_new( &mut self.schedule_storage, scene, @@ -732,12 +720,11 @@ let image_resource = image_cache.get(image_id).expect("Image resource not found"); self.programs - .maybe_resize_atlas_texture_array(&self.gl, image_cache.atlas_count() as u32); + .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_layer( + writer.write_to_atlas( &self.gl, - &self.programs.resources.atlas_texture_array.texture, - image_resource.atlas_id.as_u32(), + &self.programs.resources.atlas_textures[image_resource.atlas_id.as_u32() as usize], offset, writer.width(), writer.height(), @@ -760,29 +747,27 @@ } } - /// Returns a reference to the underlying atlas texture array. - /// - /// This is a 2D array texture (`TextureViewDimension::D2Array`) containing all - /// atlas layers used by the image cache. Each layer holds cached image data - /// (e.g., rasterised glyphs) that the renderer samples during draw calls. Before the first - /// layer is allocated, this returns the 1x1 placeholder texture. - pub fn atlas_texture(&self) -> &WebGlTexture { - &self.programs.resources.atlas_texture_array.texture + /// Returns an individual image atlas texture. + pub fn atlas_texture(&self, atlas_id: AtlasId) -> &WebGlTexture { + self.programs + .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.programs.resources; AtlasTextureInfo { - width: u16::try_from(resources.atlas_texture_array.size.width) - .expect("atlas texture width exceeds the u16 renderer domain"), - height: u16::try_from(resources.atlas_texture_array.size.height) - .expect("atlas texture height exceeds the u16 renderer domain"), - layer_count: resources.atlas_layer_count, + width: resources.atlas_size.0, + height: resources.atlas_size.1, + texture_count: u32::try_from(resources.atlas_textures.len()).unwrap(), } } - /// Clear a specific region of the atlas texture array. + /// Clear a specific region of an atlas texture. fn clear_atlas_region(&mut self, atlas_id: AtlasId, offset: [u16; 2], width: u16, height: u16) { let _state_guard = WebGlStateGuard::for_clear_atlas_region(&self.gl); let temp_framebuffer = Framebuffer::new(&self.gl); @@ -791,19 +776,18 @@ self.gl .bind_framebuffer(WebGl2RenderingContext::FRAMEBUFFER, Some(&temp_framebuffer)); - // Attach the specific atlas layer to the framebuffer - self.gl.framebuffer_texture_layer( + self.gl.framebuffer_texture_2d( WebGl2RenderingContext::FRAMEBUFFER, WebGl2RenderingContext::COLOR_ATTACHMENT0, - Some(&self.programs.resources.atlas_texture_array.texture), + WebGl2RenderingContext::TEXTURE_2D, + Some(&self.programs.resources.atlas_textures[atlas_id.as_u32() as usize]), 0, - atlas_id.as_u32() as i32, ); // Set viewport to match the atlas texture dimensions - let atlas_size = &self.programs.resources.atlas_texture_array.size; + let (atlas_width, atlas_height) = self.programs.resources.atlas_size; self.gl - .viewport(0, 0, atlas_size.width as i32, atlas_size.height as i32); + .viewport(0, 0, i32::from(atlas_width), i32::from(atlas_height)); // Enable scissor test and set scissor rectangle to our region self.gl.enable(WebGl2RenderingContext::SCISSOR_TEST); @@ -824,6 +808,7 @@ encoded_paints: &[EncodedPaint], image_cache: &ImageCache, texture_bindings: &WebGlTextureBindings, + render_target_texture: Option<&WebGlTexture>, ) -> Result<(), RenderError> { self.encoded_paints .resize_with(encoded_paints.len(), || GPU_PAINT_PLACEHOLDER); @@ -842,8 +827,11 @@ ImageSource::ExternalTexture { id, source_region, .. } => { - if texture_bindings.get(*id).is_none() { - return Err(RenderError::MissingTextureBinding(*id)); + let texture = texture_bindings + .get(*id) + .ok_or(RenderError::MissingTextureBinding(*id))?; + if render_target_texture.is_some_and(|target| target == texture) { + return Err(RenderError::TextureFeedbackLoop(*id)); } Self::encode_external_texture_paint(img, *source_region) } @@ -891,8 +879,6 @@ image.sampler.quality as u32, image.sampler.x_extend as u32, image.sampler.y_extend as u32, - image_resource.atlas_id.as_u32(), - false, ); let (tint, tint_mode) = pack_tint(image.tint); @@ -918,8 +904,6 @@ image.sampler.quality as u32, image.sampler.x_extend as u32, image.sampler.y_extend as u32, - 0, - true, ); let (tint, tint_mode) = pack_tint(image.tint); @@ -1130,8 +1114,6 @@ alphas_texture: WebGlUniformLocation, /// Layer input texture location. layer_input_texture: WebGlUniformLocation, - /// Atlas texture location. - atlas_texture_array: WebGlUniformLocation, /// Encoded paints texture location for fragment shader. encoded_paints_texture_fs: WebGlUniformLocation, /// Encoded paints texture location for vertex shader. @@ -1155,12 +1137,10 @@ alphas_texture: Texture, /// Height of alpha texture. alpha_texture_height: u32, - /// Texture array for atlas data (multiple atlases supported) - pub(crate) atlas_texture_array: WebGlTextureArray, - /// Configured dimensions used when promoting the placeholder to a real atlas. + /// One 2D texture per image atlas. + pub(crate) atlas_textures: Vec<Texture>, + /// Configured atlas dimensions. pub(crate) atlas_size: (u16, u16), - /// Number of real atlas layers currently allocated. - pub(crate) atlas_layer_count: u32, /// Encoded paints texture for image metadata. encoded_paints_texture: Texture, /// Height of encoded paints texture. @@ -1187,11 +1167,7 @@ /// Dimensions of the intermediate layer and scratch textures. texture_size: SizeU16, - /// Placeholder 1x1 atlas texture array, used during `render_to_atlas` to avoid - /// binding the real atlas texture while it is also the render target. - stub_atlas_texture_array: WebGlTextureArray, - - /// Cached framebuffer for rendering into an atlas layer in `render_to_atlas`. + /// Cached framebuffer for rendering into an atlas texture in `render_to_atlas`. /// Reused to avoid create/delete overhead on every call. atlas_render_framebuffer: Option<Framebuffer>, @@ -1543,104 +1519,17 @@ self.resources.texture_size = texture_size; } - /// Resize atlas texture array to accommodate more atlases. - fn maybe_resize_atlas_texture_array( + /// Create any newly allocated atlas textures. + fn maybe_create_atlas_textures( &mut self, gl: &WebGl2RenderingContext, required_atlas_count: u32, ) { - let current_atlas_count = self.resources.atlas_layer_count; - if required_atlas_count <= current_atlas_count { - return; - } let (width, height) = self.resources.atlas_size; - - if current_atlas_count == 0 { - // Growing from the 1x1 placeholder allocated for `initial_atlas_count == 0`: there is no - // atlas data to preserve, so re-specify the backing store in place on the existing - // texture object rather than creating a new one and deleting the stub. Preserving the GL - // texture name is required on Mali-G52 under the Android WebView GL compositor: replacing - // the stub object mid-session hangs the native compositor, causing an ANR, and flushing - // around the swap does not help. - gl.active_texture(WebGl2RenderingContext::TEXTURE0); - gl.bind_texture( - WebGl2RenderingContext::TEXTURE_2D_ARRAY, - Some(&self.resources.atlas_texture_array.texture), - ); - gl.tex_image_3d_with_opt_u8_array( - WebGl2RenderingContext::TEXTURE_2D_ARRAY, - 0, - WebGl2RenderingContext::RGBA8 as i32, - width as i32, - height as i32, - required_atlas_count as i32, - 0, - WebGl2RenderingContext::RGBA, - WebGl2RenderingContext::UNSIGNED_BYTE, - None, - ) - .unwrap(); - self.resources.atlas_texture_array.size = WebGlTextureSize { - width: u32::from(width), - height: u32::from(height), - }; - } else { - // Growing an atlas that already holds data: allocate a new, larger `TEXTURE_2D_ARRAY`, - // copy the existing layers across, then swap. On Mali-G52 under the Android WebView GL - // compositor, `glFlush`ing after the copy is required before the next composite; - // otherwise, the compositor hangs and causes an ANR. Unbinding the atlas and flushing - // before allocation are precautionary. - gl.active_texture(WebGl2RenderingContext::TEXTURE0); - gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D_ARRAY, None); - gl.active_texture(WebGl2RenderingContext::TEXTURE2); - gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D_ARRAY, None); - gl.flush(); - - let new_atlas_texture_array = - create_atlas_texture_array(gl, width, height, required_atlas_count); - self.copy_atlas_texture_data(gl, &new_atlas_texture_array, current_atlas_count); - - // Replace the old resources (dropping the old array frees its GL texture). - self.resources.atlas_texture_array = new_atlas_texture_array; - - // This is the critical flush that prevents the Mali-G52 compositor hang. - gl.flush(); - } - - self.resources.atlas_layer_count = required_atlas_count; - // Cached FBOs were attached to the previous store; drop them so we recreate on next use. - self.resources.atlas_render_framebuffer = None; - } - - /// Copy texture data from the old atlas texture array to a new one. - /// This is necessary when resizing the texture array to preserve existing atlas data. - fn copy_atlas_texture_data( - &self, - gl: &WebGl2RenderingContext, - new_atlas_texture_array: &WebGlTextureArray, - layer_count_to_copy: u32, - ) { - let WebGlTextureSize { width, height, .. } = self.resources.atlas_texture_array.size(); - - // Copy each layer from the old atlas to the new one - for layer in 0..layer_count_to_copy { - copy_to_texture_array_layer( - gl, - |gl| { - // Attach source layer to READ framebuffer - gl.framebuffer_texture_layer( - WebGl2RenderingContext::READ_FRAMEBUFFER, - WebGl2RenderingContext::COLOR_ATTACHMENT0, - Some(&self.resources.atlas_texture_array.texture), - 0, - layer as i32, - ); - }, - &new_atlas_texture_array.texture, - layer, - [0, 0], - [width, height], - ); + while self.resources.atlas_textures.len() < required_atlas_count as usize { + self.resources + .atlas_textures + .push(create_atlas_texture(gl, width, height)); } } @@ -2038,7 +1927,6 @@ original_read_framebuffer: Option<WebGlFramebuffer>, original_active_texture: Option<u32>, original_texture_2d: Option<WebGlTexture>, - original_texture_2d_array: Option<WebGlTexture>, original_pixel_pack_buffer: Option<WebGlBuffer>, blend_enabled: bool, scissor_enabled: bool, @@ -2085,15 +1973,6 @@ None }; - // Save current 2D array texture binding if requested - let original_texture_2d_array = if config.texture_2d_array { - gl.get_parameter(WebGl2RenderingContext::TEXTURE_BINDING_2D_ARRAY) - .ok() - .and_then(|v| v.dyn_into::<WebGlTexture>().ok()) - } else { - None - }; - let original_pixel_pack_buffer = if config.pixel_pack_buffer { gl.get_parameter(WebGl2RenderingContext::PIXEL_PACK_BUFFER_BINDING) .ok() @@ -2144,7 +2023,6 @@ original_read_framebuffer, original_active_texture, original_texture_2d, - original_texture_2d_array, original_pixel_pack_buffer, blend_enabled, scissor_enabled, @@ -2173,8 +2051,7 @@ gl, WebGlStateConfig { read_framebuffer: true, - active_texture: true, - texture_2d_array: true, + texture_2d: true, ..Default::default() }, ) @@ -2267,14 +2144,6 @@ ); } - // Restore original 2D array texture binding if it was saved - if self.config.texture_2d_array { - self.gl.bind_texture( - WebGl2RenderingContext::TEXTURE_2D_ARRAY, - self.original_texture_2d_array.as_ref(), - ); - } - if self.config.pixel_pack_buffer { self.gl.bind_buffer( WebGl2RenderingContext::PIXEL_PACK_BUFFER, @@ -2294,8 +2163,6 @@ pub(crate) active_texture: bool, /// Save/restore 2D texture binding (`TEXTURE_BINDING_2D`) pub(crate) texture_2d: bool, - /// Save/restore 2D array texture binding (`TEXTURE_BINDING_2D_ARRAY`) - pub(crate) texture_2d_array: bool, /// Save/restore pixel pack buffer binding (`PIXEL_PACK_BUFFER_BINDING`) pub(crate) pixel_pack_buffer: bool, /// Save/restore blending state @@ -2336,7 +2203,6 @@ // Get texture uniform locations. let alphas_texture_name = render::fragment::ALPHAS_TEXTURE; let layer_input_texture_name = render::fragment::LAYER_INPUT_TEXTURE; - let atlas_texture_array_name = render::fragment::ATLAS_TEXTURE_ARRAY; let encoded_paints_texture_fs_name = render::fragment::ENCODED_PAINTS_TEXTURE; let encoded_paints_texture_vs_name = render::vertex::ENCODED_PAINTS_TEXTURE; let gradient_texture_name = render::fragment::GRADIENT_TEXTURE; @@ -2357,9 +2223,6 @@ layer_input_texture: gl .get_uniform_location(program, layer_input_texture_name) .unwrap(), - atlas_texture_array: gl - .get_uniform_location(program, atlas_texture_array_name) - .unwrap(), encoded_paints_texture_fs: gl .get_uniform_location(program, encoded_paints_texture_fs_name) .unwrap(), @@ -2501,16 +2364,6 @@ ) } -/// Create a texture array with the requested filtering and clamp-to-edge wrapping. -fn create_texture_array(gl: &WebGl2RenderingContext, min_filter: u32, mag_filter: u32) -> Texture { - create_texture_inner( - gl, - WebGl2RenderingContext::TEXTURE_2D_ARRAY, - min_filter, - mag_filter, - ) -} - fn create_texture_inner( gl: &WebGl2RenderingContext, target: u32, @@ -2672,18 +2525,9 @@ .. } = *image_cache.atlas_manager().config(); let atlas_size = (atlas_width, atlas_height); - let atlas_layer_count = initial_atlas_count as u32; - let atlas_texture_array = if atlas_layer_count == 0 { - // Texture arrays cannot have zero layers. Keep a tiny bindable placeholder until the image - // cache makes its first real allocation. - create_atlas_texture_array(gl, 1, 1, 1) - } else { - create_atlas_texture_array(gl, atlas_width, atlas_height, atlas_layer_count) - }; - - // Create a 1x1 stub atlas texture array for use during render_to_atlas. - // This avoids binding the real atlas as a shader input while it is the render target. - let stub_atlas_texture_array = create_atlas_texture_array(gl, 1, 1, 1); + let atlas_textures = (0..initial_atlas_count) + .map(|_| create_atlas_texture(gl, atlas_width, atlas_height)) + .collect(); // Create and configure encoded paints texture. let encoded_paints_texture = create_placeholder_rgba32ui_texture(gl); @@ -2702,9 +2546,8 @@ strips_buffer, alphas_texture, alpha_texture_height: 0, - atlas_texture_array, + atlas_textures, atlas_size, - atlas_layer_count, encoded_paints_texture, encoded_paints_texture_height: 0, gradient_texture, @@ -2719,7 +2562,6 @@ depth_attachment_array: js_sys::Array::of1(&WebGl2RenderingContext::DEPTH.into()), resource_texture_dimension_2d, texture_size, - stub_atlas_texture_array, atlas_render_framebuffer: None, filter_data_texture, filter_data_texture_height: 0, @@ -2763,31 +2605,24 @@ } } -/// Create an atlas texture array. -pub(crate) fn create_atlas_texture_array( +/// Create an image atlas texture. +pub(crate) fn create_atlas_texture( gl: &WebGl2RenderingContext, width: u16, height: u16, - layer_count: u32, -) -> WebGlTextureArray { - debug_assert!( - layer_count > 0, - "atlas texture arrays must have at least one physical layer" - ); - let atlas_texture = create_texture_array( +) -> Texture { + let atlas_texture = create_texture( gl, WebGl2RenderingContext::NEAREST, WebGl2RenderingContext::NEAREST, ); - // Initialize with empty texture array data - gl.tex_image_3d_with_opt_u8_array( - WebGl2RenderingContext::TEXTURE_2D_ARRAY, + gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_array_buffer_view( + WebGl2RenderingContext::TEXTURE_2D, 0, WebGl2RenderingContext::RGBA8 as i32, width as i32, height as i32, - layer_count as i32, 0, WebGl2RenderingContext::RGBA, WebGl2RenderingContext::UNSIGNED_BYTE, @@ -2795,7 +2630,7 @@ ) .unwrap(); - WebGlTextureArray::new(atlas_texture, u32::from(width), u32::from(height)) + atlas_texture } /// Create a framebuffer for a texture. @@ -2930,14 +2765,19 @@ /// Bind the external texture slots. fn bind_external_textures(&self, bindings: &ExternalTextureBindings) { - for (slot, texture_id) in bindings.as_array().into_iter().enumerate() { + for (slot, texture_source) in bindings.as_array().into_iter().enumerate() { self.gl.active_texture( WebGl2RenderingContext::TEXTURE0 + EXTERNAL_TEXTURE_UNIT_START + u32::try_from(slot).unwrap(), ); - let texture: &WebGlTexture = match texture_id { - Some(texture_id) => self.texture_bindings.get(texture_id).unwrap(), + let texture: &WebGlTexture = match texture_source { + Some(TextureSourceId::Atlas(atlas_id)) => { + &self.programs.resources.atlas_textures[atlas_id.as_u32() as usize] + } + Some(TextureSourceId::External(texture_id)) => { + self.texture_bindings.get(texture_id).unwrap() + } None => &self.programs.resources.placeholder_external_texture, }; self.gl @@ -3046,38 +2886,29 @@ self.gl .uniform1i(Some(&self.programs.strip_uniforms.layer_input_texture), 1); - // Bind atlas texture array for image rendering - self.gl.active_texture(WebGl2RenderingContext::TEXTURE2); - self.gl.bind_texture( - WebGl2RenderingContext::TEXTURE_2D_ARRAY, - Some(&self.programs.resources.atlas_texture_array.texture), - ); - self.gl - .uniform1i(Some(&self.programs.strip_uniforms.atlas_texture_array), 2); - // Bind encoded paints texture for image metadata - self.gl.active_texture(WebGl2RenderingContext::TEXTURE3); + self.gl.active_texture(WebGl2RenderingContext::TEXTURE2); self.gl.bind_texture( WebGl2RenderingContext::TEXTURE_2D, Some(&self.programs.resources.encoded_paints_texture), ); self.gl.uniform1i( Some(&self.programs.strip_uniforms.encoded_paints_texture_fs), - 3, + 2, ); self.gl.uniform1i( Some(&self.programs.strip_uniforms.encoded_paints_texture_vs), - 3, + 2, ); // Bind gradient texture for gradient rendering - self.gl.active_texture(WebGl2RenderingContext::TEXTURE4); + self.gl.active_texture(WebGl2RenderingContext::TEXTURE3); self.gl.bind_texture( WebGl2RenderingContext::TEXTURE_2D, Some(&self.programs.resources.gradient_texture), ); self.gl - .uniform1i(Some(&self.programs.strip_uniforms.gradient_texture), 4); + .uniform1i(Some(&self.programs.strip_uniforms.gradient_texture), 3); // External textures are rebound per run while drawing. Start from the placeholder so the // sampler is valid for draws that don't reference one. @@ -3456,12 +3287,11 @@ /// Get the height of the image. fn height(&self) -> u16; - /// Write image data to a specific layer of an atlas texture array at the specified offset. - fn write_to_atlas_layer( + /// Write image data to an atlas texture at the specified offset. + fn write_to_atlas( &self, gl: &WebGl2RenderingContext, - atlas_texture_array: &WebGlTexture, - layer: u32, + atlas_texture: &WebGlTexture, offset: [u16; 2], width: u16, height: u16, @@ -3478,35 +3308,27 @@ self.height() } - fn write_to_atlas_layer( + fn write_to_atlas( &self, gl: &WebGl2RenderingContext, - atlas_texture_array: &WebGlTexture, - layer: u32, + atlas_texture: &WebGlTexture, offset: [u16; 2], width: u16, height: u16, ) { - // Bind the atlas texture array gl.active_texture(WebGl2RenderingContext::TEXTURE0); - gl.bind_texture( - WebGl2RenderingContext::TEXTURE_2D_ARRAY, - Some(atlas_texture_array), - ); + 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(); - // Upload the image data to the specific layer and region of the atlas texture array - gl.tex_sub_image_3d_with_opt_u8_array( - WebGl2RenderingContext::TEXTURE_2D_ARRAY, + 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, - layer as i32, width as i32, height as i32, - 1, WebGl2RenderingContext::RGBA, WebGl2RenderingContext::UNSIGNED_BYTE, Some(rgba_data), @@ -3525,17 +3347,16 @@ self.as_ref().height() } - fn write_to_atlas_layer( + fn write_to_atlas( &self, gl: &WebGl2RenderingContext, - atlas_texture_array: &WebGlTexture, - layer: u32, + atlas_texture: &WebGlTexture, offset: [u16; 2], width: u16, height: u16, ) { self.as_ref() - .write_to_atlas_layer(gl, atlas_texture_array, layer, offset, width, height); + .write_to_atlas(gl, atlas_texture, offset, width, height); } } @@ -3555,16 +3376,15 @@ unreachable!("WebGlTexture height must be provided by caller") } - fn write_to_atlas_layer( + fn write_to_atlas( &self, gl: &WebGl2RenderingContext, - atlas_texture_array: &WebGlTexture, - layer: u32, + atlas_texture: &WebGlTexture, offset: [u16; 2], width: u16, height: u16, ) { - copy_to_texture_array_layer( + copy_to_texture( gl, |gl| { // Attach source texture to read framebuffer @@ -3576,8 +3396,7 @@ 0, ); }, - atlas_texture_array, - layer, + atlas_texture, [u32::from(offset[0]), u32::from(offset[1])], [u32::from(width), u32::from(height)], ); @@ -3604,71 +3423,39 @@ self.height } - fn write_to_atlas_layer( + fn write_to_atlas( &self, gl: &WebGl2RenderingContext, - atlas_texture_array: &WebGlTexture, - layer: u32, + atlas_texture: &WebGlTexture, offset: [u16; 2], width: u16, height: u16, ) { self.texture - .write_to_atlas_layer(gl, atlas_texture_array, layer, offset, width, height); + .write_to_atlas(gl, atlas_texture, offset, width, height); } } -/// Wrapper for `WebGlTexture` array with known dimensions. -#[derive(Debug)] -pub(crate) struct WebGlTextureArray { - /// The WebGL texture array. - texture: Texture, - /// The size of the texture array. - size: WebGlTextureSize, -} - -impl WebGlTextureArray { - /// Create a new WebGL texture array wrapper. - fn new(texture: Texture, width: u32, height: u32) -> Self { - Self { - texture, - size: WebGlTextureSize { width, height }, - } - } - - /// Get the size of the texture array, similar to WGPU's `texture.size()`. - fn size(&self) -> WebGlTextureSize { - self.size - } -} - -/// Size information for WebGL texture arrays, similar to WGPU's `Extent3d`. -#[derive(Debug, Clone, Copy)] -struct WebGlTextureSize { - /// The width of the texture. - width: u32, - /// The height of the texture. - height: u32, -} - -/// Helper function to copy from a source texture/framebuffer to a destination texture array layer. -fn copy_to_texture_array_layer( +/// Copy from a source texture/framebuffer to an atlas texture. +fn copy_to_texture( gl: &WebGl2RenderingContext, source_setup: impl FnOnce(&WebGl2RenderingContext), - dest_texture_array: &WebGlTexture, - dest_layer: u32, + dest_texture: &WebGlTexture, dest_offset: [u32; 2], copy_size: [u32; 2], ) { + let _active_texture_guard = WebGlStateGuard::with_config( + gl, + WebGlStateConfig { + active_texture: true, + ..Default::default() + }, + ); + + gl.active_texture(WebGl2RenderingContext::TEXTURE0); let _state_guard = WebGlStateGuard::for_texture_copy(gl); let read_framebuffer = Framebuffer::new(gl); - - // Bind destination texture array - gl.active_texture(WebGl2RenderingContext::TEXTURE0); - gl.bind_texture( - WebGl2RenderingContext::TEXTURE_2D_ARRAY, - Some(dest_texture_array), - ); + gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(dest_texture)); // Bind the READ framebuffer gl.bind_framebuffer( @@ -3679,13 +3466,11 @@ // Let the caller set up the source (attach texture/layer to read framebuffer) source_setup(gl); - // Copy from READ framebuffer to destination array layer - gl.copy_tex_sub_image_3d( - WebGl2RenderingContext::TEXTURE_2D_ARRAY, + gl.copy_tex_sub_image_2d( + WebGl2RenderingContext::TEXTURE_2D, 0, dest_offset[0] as i32, dest_offset[1] as i32, - dest_layer as i32, 0, 0, copy_size[0] as i32,
diff --git a/sparse_strips/vello_hybrid/src/render/webgl/probe.rs b/sparse_strips/vello_hybrid/src/render/webgl/probe.rs index 458481b..6fb809a 100644 --- a/sparse_strips/vello_hybrid/src/render/webgl/probe.rs +++ b/sparse_strips/vello_hybrid/src/render/webgl/probe.rs
@@ -81,7 +81,6 @@ framebuffer: true, active_texture: true, texture_2d: true, - texture_2d_array: true, pixel_pack_buffer: true, viewport: true, ..Default::default() @@ -160,6 +159,7 @@ true, RootTarget::AtlasLayer, &texture_bindings, + Some(&probe_texture), ); let probe_framebuffer = self .programs
diff --git a/sparse_strips/vello_hybrid/src/render/wgpu/mod.rs b/sparse_strips/vello_hybrid/src/render/wgpu/mod.rs index ed256a5..0573677 100644 --- a/sparse_strips/vello_hybrid/src/render/wgpu/mod.rs +++ b/sparse_strips/vello_hybrid/src/render/wgpu/mod.rs
@@ -27,7 +27,7 @@ copy::GpuCopyInstance, filter::{FilterContext, FilterInstanceData, FilterPassPlan}, gradient_cache::GradientRampCache, - paint::PaintResolver, + paint::{PaintResolver, TextureSourceId}, render::{ Config, common::{ @@ -185,8 +185,6 @@ let device_limits = DeviceLimits { max_texture_dimension_2d: u16::try_from(limits.max_texture_dimension_2d) .unwrap_or(u16::MAX), - max_texture_array_layers: u16::try_from(limits.max_texture_array_layers) - .unwrap_or(u16::MAX), }; settings.memory_settings.normalize(&device_limits); let resources = Resources::new(settings.memory_settings.image_atlas_config); @@ -360,13 +358,7 @@ label: Some("Render to Atlas Encoder"), }); - Programs::maybe_resize_atlas_texture_array( - device, - &mut encoder, - &mut self.programs.resources, - &self.programs.atlas_bind_group_layout, - atlas_count, - ); + Programs::maybe_create_atlas_textures(device, &mut self.programs.resources, atlas_count); let (atlas_width, atlas_height) = atlas_config.atlas_size; let atlas_render_size = RenderSize { @@ -375,28 +367,7 @@ }; let layer_view = - self.programs - .resources - .atlas_texture_array - .create_view(&TextureViewDescriptor { - label: Some("Atlas Layer Render View"), - format: Some(wgpu::TextureFormat::Rgba8Unorm), - dimension: Some(wgpu::TextureViewDimension::D2), - aspect: wgpu::TextureAspect::All, - base_mip_level: 0, - mip_level_count: Some(1), - base_array_layer: atlas_id.as_u32(), - array_layer_count: Some(1), - usage: None, - }); - - // Swap in the stub atlas bind group to avoid the read-write conflict: - // the real atlas texture is used as the render target (COLOR_TARGET), so it - // cannot also be bound as a shader resource (TEXTURE_BINDING) in the same pass. - core::mem::swap( - &mut self.programs.resources.atlas_bind_group, - &mut self.programs.resources.stub_atlas_bind_group, - ); + self.programs.resources.atlas_texture_views[atlas_id.as_u32() as usize].clone(); let encoded_paints = &scene.encoded_paints; let dummy_image_cache = self @@ -419,12 +390,6 @@ ); self.dummy_image_cache = Some(dummy_image_cache); - // Restore the real atlas bind group. - core::mem::swap( - &mut self.programs.resources.atlas_bind_group, - &mut self.programs.resources.stub_atlas_bind_group, - ); - // Submit immediately so the atlas content is committed before subsequent // render() calls overwrite the shared alpha/config/paint resources. queue.submit(Some(encoder.finish())); @@ -465,7 +430,12 @@ texture_bindings: &TextureBindings, ) -> Result<(), RenderError> { self.programs.depth_cleared_this_frame = false; - self.prepare_gpu_encoded_paints(encoded_paints, image_cache, texture_bindings)?; + self.prepare_gpu_encoded_paints( + encoded_paints, + image_cache, + texture_bindings, + view.texture(), + )?; let required_texture_size = self .layers_config .required_intermediate_texture_size(&scene.recorder)?; @@ -478,7 +448,8 @@ .texture_size .max(required_texture_size); let current_allocations = self.current_allocations(); - let paint_resolver = PaintResolver::new(encoded_paints, &self.paint_idxs); + let paint_resolver = + PaintResolver::new(encoded_paints, &self.paint_idxs).with_image_cache(image_cache); let schedule = Schedule::try_new( &mut self.schedule_storage, scene, @@ -516,7 +487,7 @@ view, depth_view, texture_bindings, - external_paint_source_bind_groups: HashMap::new(), + external_texture_bind_groups: HashMap::new(), scratch_buffers: &mut self.scratch_buffers, }; @@ -616,20 +587,19 @@ ) { let image_resource = image_cache.get(image_id).expect("Image resource not found"); - Programs::maybe_resize_atlas_texture_array( + Programs::maybe_create_atlas_textures( device, - encoder, &mut self.programs.resources, - &self.programs.atlas_bind_group_layout, image_cache.atlas_count() as u32, ); let offset = offset_override.unwrap_or(image_resource.offset); - writer.write_to_atlas_layer( + let atlas_texture = + &self.programs.resources.atlas_textures[image_resource.atlas_id.as_u32() as usize]; + writer.write_to_atlas( device, queue, encoder, - &self.programs.resources.atlas_texture_array, - image_resource.atlas_id.as_u32(), + atlas_texture, offset, writer.width(), writer.height(), @@ -659,13 +629,13 @@ } } - /// Returns a reference to the underlying atlas texture array. - /// - /// This is a 2D array texture (`TextureViewDimension::D2Array`) containing all - /// atlas layers used by the image cache. Each layer holds cached image data - /// (e.g., rasterised glyphs) that the renderer samples during draw calls. - pub fn atlas_texture(&self) -> &Texture { - &self.programs.resources.atlas_texture_array + /// Returns an individual image atlas texture. + pub fn atlas_texture(&self, atlas_id: AtlasId) -> &Texture { + self.programs + .resources + .atlas_textures + .get(atlas_id.as_u32() as usize) + .unwrap() } /// Clear a specific region of the atlas texture. @@ -677,23 +647,8 @@ width: u16, height: u16, ) { - // Create a texture view for the specific atlas layer let layer_view = - self.programs - .resources - .atlas_texture_array - .create_view(&TextureViewDescriptor { - label: Some("Atlas Layer Clear View"), - format: Some(wgpu::TextureFormat::Rgba8Unorm), - dimension: Some(wgpu::TextureViewDimension::D2), - aspect: wgpu::TextureAspect::All, - base_mip_level: 0, - mip_level_count: Some(1), - base_array_layer: atlas_id.as_u32(), - array_layer_count: Some(1), - // Inherit usage from the texture - usage: None, - }); + self.programs.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"), @@ -731,6 +686,7 @@ encoded_paints: &[EncodedPaint], image_cache: &ImageCache, texture_bindings: &TextureBindings, + render_target_texture: &Texture, ) -> Result<(), RenderError> { self.encoded_paints .resize_with(encoded_paints.len(), || GPU_PAINT_PLACEHOLDER); @@ -749,8 +705,12 @@ ImageSource::ExternalTexture { id, source_region, .. } => { - if texture_bindings.get(*id).is_none() { - return Err(RenderError::MissingTextureBinding(*id)); + let texture_view = texture_bindings + .get(*id) + .ok_or(RenderError::MissingTextureBinding(*id))?; + + if texture_view.texture() == render_target_texture { + return Err(RenderError::TextureFeedbackLoop(*id)); } self.encode_external_texture_paint(img, *source_region) } @@ -798,8 +758,6 @@ image.sampler.quality as u32, image.sampler.x_extend as u32, image.sampler.y_extend as u32, - image_resource.atlas_id.as_u32(), - false, ); let (tint, tint_mode) = pack_tint(image.tint); @@ -826,8 +784,6 @@ image.sampler.quality as u32, image.sampler.x_extend as u32, image.sampler.y_extend as u32, - 0, - true, ); let (tint, tint_mode) = pack_tint(image.tint); @@ -938,12 +894,12 @@ renderer.atlas_clear_scratch.resize(byte_count, 0); queue.write_texture( wgpu::TexelCopyTextureInfo { - texture: renderer.atlas_texture(), + texture: renderer.atlas_texture(AtlasId::new(rect.page_index)), mip_level: 0, origin: wgpu::Origin3d { x: rect.x as u32, y: rect.y as u32, - z: rect.page_index, + z: 0, }, aspect: wgpu::TextureAspect::All, }, @@ -980,8 +936,8 @@ encoded_paints_bind_group_layout: BindGroupLayout, /// Bind group layout for gradient texture gradient_bind_group_layout: BindGroupLayout, - /// Bind group layout for atlas textures - atlas_bind_group_layout: BindGroupLayout, + /// Bind group layout for external textures. + external_texture_bind_group_layout: BindGroupLayout, /// Bind group layout for filter data texture. filter_bind_group_layout: BindGroupLayout, /// Filter input layouts. @@ -1027,18 +983,16 @@ alphas_texture: Texture, /// Maximum width or height of packed resource textures. resource_texture_dimension_2d: u32, - /// Textures for atlas data (multiple atlases supported) - atlas_texture_array: Texture, - /// View for atlas texture array - atlas_texture_array_view: TextureView, - /// Configured dimensions used when promoting the placeholder to a real atlas. + /// One 2D texture per image atlas. + atlas_textures: Vec<Texture>, + /// Default view corresponding to each image atlas texture. + atlas_texture_views: Vec<TextureView>, + /// Configured atlas dimensions. atlas_size: (u16, u16), - /// Number of real atlas layers currently exposed by the texture view. - atlas_layer_count: u32, - /// Bind group for paint sources: an atlas texture array plus four external texture slots. - atlas_bind_group: BindGroup, /// Transparent 1x1 placeholder used for unoccupied external texture slots. placeholder_external_texture_view: TextureView, + /// Bind group used when a draw does not sample an external texture. + empty_external_texture_bind_group: BindGroup, /// Texture for encoded paints encoded_paints_texture: Texture, /// Bind group for encoded paints @@ -1058,10 +1012,6 @@ /// Layer config buffer. layer_config_buffer: Buffer, - /// Placeholder paint-source bind group with a 1x1 dummy atlas texture, used during - /// `render_to_atlas` to avoid a read-write conflict on the real atlas texture. - stub_atlas_bind_group: BindGroup, - /// Layer textures by parity. layer_textures: [Vec<TextureView>; 2], /// Scratch texture. @@ -1171,26 +1121,16 @@ }, count: None, }; - let paint_source_layout_entries = [ - wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: wgpu::TextureViewDimension::D2Array, - multisampled: false, - }, - count: None, - }, + let external_texture_layout_entries = [ + external_texture_layout_entry(0), external_texture_layout_entry(1), external_texture_layout_entry(2), external_texture_layout_entry(3), - external_texture_layout_entry(4), ]; - let atlas_bind_group_layout = + let external_texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Paint Source Bind Group Layout"), - entries: &paint_source_layout_entries, + label: Some("External Texture Bind Group Layout"), + entries: &external_texture_layout_entries, }); let encoded_paints_bind_group_layout = @@ -1238,7 +1178,7 @@ label: Some("Strip Pipeline Layout"), bind_group_layouts: &[ Some(&strip_bind_group_layout), - Some(&atlas_bind_group_layout), + Some(&external_texture_bind_group_layout), Some(&encoded_paints_bind_group_layout), Some(&gradient_bind_group_layout), ], @@ -1688,30 +1628,16 @@ .. } = image_cache.atlas_manager().config(); let atlas_size = (*atlas_width, *atlas_height); - let atlas_layer_count = *initial_atlas_count as u32; - let (atlas_texture_array, atlas_texture_array_view) = if atlas_layer_count == 0 { - // Texture arrays cannot have zero layers. Keep a tiny bindable placeholder until the - // image cache makes its first real allocation. - Self::create_atlas_texture_array(device, 1, 1, 1) - } else { - Self::create_atlas_texture_array(device, *atlas_width, *atlas_height, atlas_layer_count) - }; - let atlas_bind_group = Self::create_paint_source_bind_group( + let atlas_textures: Vec<_> = (0..*initial_atlas_count) + .map(|_| Self::create_atlas_texture(device, *atlas_width, *atlas_height)) + .collect(); + let atlas_texture_views = atlas_textures + .iter() + .map(|texture| texture.create_view(&TextureViewDescriptor::default())) + .collect(); + let empty_external_texture_bind_group = Self::create_external_texture_bind_group( device, - &atlas_bind_group_layout, - &atlas_texture_array_view, - [&placeholder_external_texture_view; EXTERNAL_TEXTURE_SLOT_COUNT], - ); - - // Create a 1x1 stub atlas texture array for use during render_to_atlas. - // This avoids the read-write conflict that occurs when the real atlas is both - // a shader input (bind group) and render target in the same pass. - let (_stub_atlas_texture, stub_atlas_view) = - Self::create_atlas_texture_array(device, 1, 1, 1); - let stub_atlas_bind_group = Self::create_paint_source_bind_group( - device, - &atlas_bind_group_layout, - &stub_atlas_view, + &external_texture_bind_group_layout, [&placeholder_external_texture_view; EXTERNAL_TEXTURE_SLOT_COUNT], ); @@ -1776,13 +1702,11 @@ layer_config_buffer, alphas_texture, resource_texture_dimension_2d, - atlas_texture_array, - atlas_texture_array_view, + atlas_textures, + atlas_texture_views, atlas_size, - atlas_layer_count, - atlas_bind_group, placeholder_external_texture_view, - stub_atlas_bind_group, + empty_external_texture_bind_group, encoded_paints_texture, encoded_paints_bind_group, gradient_texture, @@ -1802,7 +1726,7 @@ strip_bind_group_layout, encoded_paints_bind_group_layout, gradient_bind_group_layout, - atlas_bind_group_layout, + external_texture_bind_group_layout, filter_bind_group_layout, filter_pipeline, blend_pipeline, @@ -2017,31 +1941,13 @@ }) } - fn create_atlas_texture_array( - device: &Device, - width: u16, - height: u16, - atlas_count: u32, - ) -> (Texture, TextureView) { - debug_assert!( - atlas_count > 0, - "atlas texture arrays must have at least one physical layer" - ); - // In WGPU's GLES backend, heuristics classify a one-layer texture as D2 even when it was - // created as D2Array. Allocate at least two physical layers on WASM to keep the backend's - // classification consistent with the D2Array view. - // See https://github.com/gfx-rs/wgpu/blob/61e5124eb9530d3b3865556a7da4fd320d03ddc5/wgpu-hal/src/gles/mod.rs#L470-L517. - #[cfg(target_arch = "wasm32")] - let depth_or_array_layers = atlas_count.max(2); - #[cfg(not(target_arch = "wasm32"))] - let depth_or_array_layers = atlas_count; - - let atlas_texture_array = device.create_texture(&wgpu::TextureDescriptor { - label: Some("Atlas Texture Array"), + fn create_atlas_texture(device: &Device, width: u16, height: u16) -> Texture { + device.create_texture(&wgpu::TextureDescriptor { + label: Some("Atlas Texture"), size: Extent3d { width: u32::from(width), height: u32::from(height), - depth_or_array_layers, + depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, @@ -2052,28 +1958,6 @@ | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::RENDER_ATTACHMENT, view_formats: &[], - }); - - let atlas_texture_array_view = - Self::create_atlas_texture_array_view(&atlas_texture_array, atlas_count); - - (atlas_texture_array, atlas_texture_array_view) - } - - fn create_atlas_texture_array_view( - atlas_texture_array: &Texture, - atlas_count: u32, - ) -> TextureView { - atlas_texture_array.create_view(&TextureViewDescriptor { - label: Some("Atlas Texture Array View"), - format: None, - dimension: Some(wgpu::TextureViewDimension::D2Array), - aspect: wgpu::TextureAspect::All, - base_mip_level: 0, - mip_level_count: None, - base_array_layer: 0, - array_layer_count: Some(atlas_count), - usage: None, }) } @@ -2127,37 +2011,32 @@ texture.create_view(&TextureViewDescriptor::default()) } - fn create_paint_source_bind_group( + fn create_external_texture_bind_group( device: &Device, - atlas_bind_group_layout: &BindGroupLayout, - atlas_texture_array_view: &TextureView, - external_texture_views: [&TextureView; EXTERNAL_TEXTURE_SLOT_COUNT], + external_texture_bind_group_layout: &BindGroupLayout, + texture_views: [&TextureView; EXTERNAL_TEXTURE_SLOT_COUNT], ) -> BindGroup { let entries = [ wgpu::BindGroupEntry { binding: 0, - resource: wgpu::BindingResource::TextureView(atlas_texture_array_view), + resource: wgpu::BindingResource::TextureView(texture_views[0]), }, wgpu::BindGroupEntry { binding: 1, - resource: wgpu::BindingResource::TextureView(external_texture_views[0]), + resource: wgpu::BindingResource::TextureView(texture_views[1]), }, wgpu::BindGroupEntry { binding: 2, - resource: wgpu::BindingResource::TextureView(external_texture_views[1]), + resource: wgpu::BindingResource::TextureView(texture_views[2]), }, wgpu::BindGroupEntry { binding: 3, - resource: wgpu::BindingResource::TextureView(external_texture_views[2]), - }, - wgpu::BindGroupEntry { - binding: 4, - resource: wgpu::BindingResource::TextureView(external_texture_views[3]), + resource: wgpu::BindingResource::TextureView(texture_views[3]), }, ]; device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("Paint Source Bind Group"), - layout: atlas_bind_group_layout, + label: Some("External Texture Bind Group"), + layout: external_texture_bind_group_layout, entries: &entries, }) } @@ -2466,118 +2345,33 @@ } } - /// Resize the texture array to accommodate more atlases. - fn maybe_resize_atlas_texture_array( + /// Create any newly allocated atlas textures. + fn maybe_create_atlas_textures( device: &Device, - encoder: &mut CommandEncoder, resources: &mut GpuResources, - atlas_bind_group_layout: &BindGroupLayout, required_atlas_count: u32, ) { - let current_atlas_count = resources.atlas_layer_count; - if required_atlas_count > current_atlas_count { - let (width, height) = resources.atlas_size; - let physical_layer_count = resources.atlas_texture_array.size().depth_or_array_layers; - - // WGPU's WebGL backend allocates at least two physical layers to keep the texture - // classified as D2Array. Expose an already-allocated layer without replacing the - // texture when that spare capacity is available. - if current_atlas_count > 0 && required_atlas_count <= physical_layer_count { - let new_atlas_texture_array_view = Self::create_atlas_texture_array_view( - &resources.atlas_texture_array, - required_atlas_count, - ); - let new_atlas_bind_group = Self::create_paint_source_bind_group( - device, - atlas_bind_group_layout, - &new_atlas_texture_array_view, - [&resources.placeholder_external_texture_view; EXTERNAL_TEXTURE_SLOT_COUNT], - ); - resources.atlas_texture_array_view = new_atlas_texture_array_view; - resources.atlas_bind_group = new_atlas_bind_group; - resources.atlas_layer_count = required_atlas_count; - return; - } - - // Create new texture array with more layers - let (new_atlas_texture_array, new_atlas_texture_array_view) = - Self::create_atlas_texture_array(device, width, height, required_atlas_count); - - if current_atlas_count > 0 { - // Copy existing atlas data from old texture array to new one. A zero-depth copy is - // still validated against the placeholder's 1x1 extent by WGPU, so skip it when - // promoting the placeholder. - Self::copy_atlas_texture_data( - encoder, - &resources.atlas_texture_array, - &new_atlas_texture_array, - current_atlas_count, - width, - height, - ); - } - - // Update the bind group with the new texture array view - let new_atlas_bind_group = Self::create_paint_source_bind_group( - device, - atlas_bind_group_layout, - &new_atlas_texture_array_view, - [&resources.placeholder_external_texture_view; EXTERNAL_TEXTURE_SLOT_COUNT], - ); - - // Replace the old resources - resources.atlas_texture_array = new_atlas_texture_array; - resources.atlas_texture_array_view = new_atlas_texture_array_view; - resources.atlas_bind_group = new_atlas_bind_group; - resources.atlas_layer_count = required_atlas_count; + let (width, height) = resources.atlas_size; + while resources.atlas_textures.len() < required_atlas_count as usize { + let texture = Self::create_atlas_texture(device, width, height); + let texture_view = texture.create_view(&TextureViewDescriptor::default()); + resources.atlas_textures.push(texture); + resources.atlas_texture_views.push(texture_view); } } - fn create_external_paint_source_bind_group( + fn create_run_external_texture_bind_group( &self, device: &Device, - external_texture_views: [&TextureView; EXTERNAL_TEXTURE_SLOT_COUNT], + texture_views: [&TextureView; EXTERNAL_TEXTURE_SLOT_COUNT], ) -> BindGroup { - Self::create_paint_source_bind_group( + Self::create_external_texture_bind_group( device, - &self.atlas_bind_group_layout, - &self.resources.atlas_texture_array_view, - external_texture_views, + &self.external_texture_bind_group_layout, + texture_views, ) } - /// Copy texture data from the old atlas texture array to a new one. - /// This is necessary when resizing the texture array to preserve existing atlas data. - fn copy_atlas_texture_data( - encoder: &mut CommandEncoder, - old_atlas_texture_array: &Texture, - new_atlas_texture_array: &Texture, - layer_count_to_copy: u32, - width: u16, - height: u16, - ) { - // Copy all layers from old texture array to new texture array - encoder.copy_texture_to_texture( - wgpu::TexelCopyTextureInfo { - texture: old_atlas_texture_array, - mip_level: 0, - origin: wgpu::Origin3d { x: 0, y: 0, z: 0 }, - aspect: wgpu::TextureAspect::All, - }, - wgpu::TexelCopyTextureInfo { - texture: new_atlas_texture_array, - mip_level: 0, - origin: wgpu::Origin3d { x: 0, y: 0, z: 0 }, - aspect: wgpu::TextureAspect::All, - }, - Extent3d { - width: u32::from(width), - height: u32::from(height), - depth_or_array_layers: layer_count_to_copy, - }, - ); - } - /// Upload alpha data to the texture. fn upload_alpha_texture(&mut self, queue: &Queue, alphas: &mut Vec<u8>) { if alphas.is_empty() { @@ -2765,31 +2559,35 @@ view: &'a TextureView, depth_view: Option<&'a TextureView>, texture_bindings: &'a TextureBindings, - external_paint_source_bind_groups: HashMap<ExternalTextureBindings, BindGroup>, + external_texture_bind_groups: HashMap<ExternalTextureBindings, BindGroup>, scratch_buffers: &'a mut ScratchBuffers, } impl RendererContext<'_> { - fn external_paint_source_bind_group_for_textures( + fn external_texture_bind_group_for_textures( &mut self, bindings: ExternalTextureBindings, ) -> &BindGroup { - match self.external_paint_source_bind_groups.entry(bindings) { + let atlas_texture_views = &self.programs.resources.atlas_texture_views; + let placeholder = &self.programs.resources.placeholder_external_texture_view; + let texture_bindings = self.texture_bindings; + match self.external_texture_bind_groups.entry(bindings) { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { - let texture_views = bindings.as_array().map(|texture_id| { - texture_id.map_or( - &self.programs.resources.placeholder_external_texture_view, - |texture_id| { - self.texture_bindings.get(texture_id).expect( - "external texture bindings were validated during paint preparation", - ) - }, - ) + let texture_sources = bindings.as_array(); + let texture_views = core::array::from_fn(|slot| match texture_sources[slot] { + Some(TextureSourceId::Atlas(atlas_id)) => { + atlas_texture_views[atlas_id.as_u32() as usize].clone() + } + Some(TextureSourceId::External(texture_id)) => texture_bindings + .get(texture_id) + .expect("external texture binding was validated during paint preparation") + .clone(), + None => placeholder.clone(), }); let bind_group = self .programs - .create_external_paint_source_bind_group(self.device, texture_views); + .create_run_external_texture_bind_group(self.device, texture_views.each_ref()); entry.insert(bind_group) } } @@ -2812,10 +2610,9 @@ } // TODO: We currently allocate a new strips buffer for each render pass. A more efficient // approach would be to re-use buffers or slices of a larger buffer. - // Create bind groups for all external textures passed in by the user that are used this - // pass. + // Create bind groups for all external textures used by this pass. for run in external_texture_runs { - self.external_paint_source_bind_group_for_textures(run.bindings); + self.external_texture_bind_group_for_textures(run.bindings); } self.programs @@ -2910,21 +2707,25 @@ let draw_strip_runs = |render_pass: &mut wgpu::RenderPass<'_>, first_instance, count| { if external_texture_runs.is_empty() { - render_pass.set_bind_group(1, &self.programs.resources.atlas_bind_group, &[]); + render_pass.set_bind_group( + 1, + &self.programs.resources.empty_external_texture_bind_group, + &[], + ); render_pass.draw(0..4, first_instance..first_instance + count); return; } - // Each run is drawn with a different external texture binding. Runs go from + // Each run is drawn with different external texture bindings. Runs go from // `run.strips_start` to the next run's `strips_start`; the last run goes to the end of // the strips buffer. for (i, run) in external_texture_runs.iter().enumerate() { - let paint_source_bind_group = self - .external_paint_source_bind_groups + let external_texture_bind_group = self + .external_texture_bind_groups .get(&run.bindings) .unwrap(); - render_pass.set_bind_group(1, paint_source_bind_group, &[]); + render_pass.set_bind_group(1, external_texture_bind_group, &[]); let start = u32::try_from(run.strips_start).unwrap(); let end = external_texture_runs .get(i + 1) @@ -3347,14 +3148,13 @@ /// Get the height of the image. fn height(&self) -> u16; - /// Write image data to a specific layer of an atlas texture array at the specified offset. - fn write_to_atlas_layer( + /// 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, - layer: u32, offset: [u16; 2], width: u16, height: u16, @@ -3371,13 +3171,12 @@ u16::try_from(self.height()).expect("texture height exceeds the u16 atlas domain") } - fn write_to_atlas_layer( + fn write_to_atlas( &self, _device: &Device, _queue: &Queue, encoder: &mut CommandEncoder, atlas_texture: &Texture, - layer: u32, offset: [u16; 2], width: u16, height: u16, @@ -3395,7 +3194,7 @@ origin: wgpu::Origin3d { x: u32::from(offset[0]), y: u32::from(offset[1]), - z: layer, + z: 0, }, aspect: wgpu::TextureAspect::All, }, @@ -3418,13 +3217,12 @@ self.height() } - fn write_to_atlas_layer( + fn write_to_atlas( &self, _device: &Device, queue: &Queue, _encoder: &mut CommandEncoder, atlas_texture: &Texture, - layer: u32, offset: [u16; 2], width: u16, height: u16, @@ -3438,7 +3236,7 @@ origin: wgpu::Origin3d { x: u32::from(offset[0]), y: u32::from(offset[1]), - z: layer, + z: 0, }, aspect: wgpu::TextureAspect::All, }, @@ -3467,26 +3265,17 @@ self.as_ref().height() } - fn write_to_atlas_layer( + fn write_to_atlas( &self, device: &Device, queue: &Queue, encoder: &mut CommandEncoder, atlas_texture: &Texture, - layer: u32, offset: [u16; 2], width: u16, height: u16, ) { - self.as_ref().write_to_atlas_layer( - device, - queue, - encoder, - atlas_texture, - layer, - offset, - width, - height, - ); + self.as_ref() + .write_to_atlas(device, queue, encoder, atlas_texture, offset, width, height); } }
diff --git a/sparse_strips/vello_sparse_shaders/shaders/helpers/image.wesl b/sparse_strips/vello_sparse_shaders/shaders/helpers/image.wesl index 3b78761..68c3e5d 100644 --- a/sparse_strips/vello_sparse_shaders/shaders/helpers/image.wesl +++ b/sparse_strips/vello_sparse_shaders/shaders/helpers/image.wesl
@@ -8,10 +8,6 @@ const IMAGE_QUALITY_MEDIUM: u32 = 1u; const IMAGE_QUALITY_HIGH: u32 = 2u; -/// Image source kinds. -const IMAGE_SOURCE_ATLAS: u32 = 0u; -const IMAGE_SOURCE_EXTERNAL: u32 = 1u; - /// Tint mode constants. const TINT_MODE_ALPHA_MASK: u32 = 0u; const TINT_MODE_MULTIPLY: u32 = 1u; @@ -22,8 +18,6 @@ // bits 0-1: quality // bits 2-3: extend_x // bits 4-5: extend_y -// bits 6-13: atlas_index -// bit 14: source_kind (0=atlas, 1=external texture) // texel0.y: image_size, packed as [width:16, height:16] // texel0.z: image_offset, packed as [x:16, y:16] // texel0.w/texel1.x/texel1.y/texel1.z: transform matrix [a, b, c, d] @@ -50,12 +44,6 @@ return vec2<f32>(f32(texel0.z >> 16u), f32(texel0.z & 0xFFFFu)); } -/// The atlas index containing this image. -fn get_image_atlas_index(texel0: vec4<u32>) -> u32 { return (texel0.x >> 6u) & 0xFFu; } - -/// Whether the image is sourced from the atlas or the externally bound texture. -fn get_image_source_kind(texel0: vec4<u32>) -> u32 { return (texel0.x >> 14u) & 0x1u; } - /// 2x2 linear part of the affine transform (columns [a,b] and [c,d]). fn get_image_transform(texel0: vec4<u32>, texel1: vec4<u32>) -> mat2x2<f32> { return mat2x2<f32>( @@ -69,88 +57,6 @@ return vec2<f32>(bitcast<f32>(texel1.w), bitcast<f32>(texel2.x)); } -/// Number of transparent padding pixels around the image in the atlas. -fn get_image_padding(texel2: vec4<u32>) -> f32 { return f32(texel2.w); } - -// Bilinear filtering -// -// Bilinear filtering consists of sampling the 4 surrounding pixels of the target point and -// interpolating them with a bilinear filter. -fn bilinear_sample( - tex: texture_2d_array<f32>, - coords: vec2<f32>, - atlas_idx: i32, - image_offset: vec2<f32>, - image_size: vec2<f32>, - _extend_modes: vec2<u32>, - _image_padding: f32, -) -> vec4<f32> { - let atlas_max = image_offset + image_size - vec2(1.0); - let atlas_uv_clamped = clamp(coords, image_offset, atlas_max); - let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped)); - let uv_frac = fract(coords); - let a = textureLoad(tex, vec2<i32>(uv_quad.xy), atlas_idx, 0); - let b = textureLoad(tex, vec2<i32>(uv_quad.xw), atlas_idx, 0); - let c = textureLoad(tex, vec2<i32>(uv_quad.zy), atlas_idx, 0); - let d = textureLoad(tex, vec2<i32>(uv_quad.zw), atlas_idx, 0); - return mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x); -} - -// Bicubic filtering using Mitchell filter with B=1/3, C=1/3 -// -// Cubic resampling consists of sampling the 16 surrounding pixels of the target point and -// interpolating them with a cubic filter. The generated matrix is 4x4 and represent the coefficients -// of the cubic function used to calculate weights based on the `x_fract` and `y_fract` of the -// location we are looking at. -fn bicubic_sample( - tex: texture_2d_array<f32>, - coords: vec2<f32>, - atlas_idx: i32, - image_offset: vec2<f32>, - image_size: vec2<f32>, - _extend_modes: vec2<u32>, - _image_padding: f32, -) -> vec4<f32> { - let atlas_max = image_offset + image_size - vec2(1.0); - let frac_coords = fract(coords + 0.5); - // Get cubic weights for x and y directions - let cx = cubic_weights(frac_coords.x); - let cy = cubic_weights(frac_coords.y); - - // Sample 4x4 grid around coords - let s00 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-1.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); - let s10 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-0.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); - let s20 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(0.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); - let s30 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(1.5, -1.5), image_offset, atlas_max)), atlas_idx, 0); - - let s01 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-1.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); - let s11 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-0.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); - let s21 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(0.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); - let s31 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(1.5, -0.5), image_offset, atlas_max)), atlas_idx, 0); - - let s02 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-1.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); - let s12 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-0.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); - let s22 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(0.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); - let s32 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(1.5, 0.5), image_offset, atlas_max)), atlas_idx, 0); - - let s03 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-1.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); - let s13 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(-0.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); - let s23 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(0.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); - let s33 = textureLoad(tex, vec2<i32>(clamp(coords + vec2(1.5, 1.5), image_offset, atlas_max)), atlas_idx, 0); - - // Interpolate in x direction for each row - let row0 = cx.x * s00 + cx.y * s10 + cx.z * s20 + cx.w * s30; - let row1 = cx.x * s01 + cx.y * s11 + cx.z * s21 + cx.w * s31; - let row2 = cx.x * s02 + cx.y * s12 + cx.z * s22 + cx.w * s32; - let row3 = cx.x * s03 + cx.y * s13 + cx.z * s23 + cx.w * s33; - // Interpolate in y direction - let result = cy.x * row0 + cy.y * row1 + cy.z * row2 + cy.w * row3; - - // Clamp alpha first, then clamp premultiplied color channels against it. - let a = clamp(result.a, 0.0, 1.0); - return vec4<f32>(clamp(result.rgb, vec3(0.0), vec3(a)), a); -} - // Cubic resampler logic borrowed from Skia (same as CPU cubic_resampler function) // Mitchell-Netravali cubic filter coefficients with parameters B=1/3 and C=1/3 const MF: array<vec4<f32>, 4> = array<vec4<f32>, 4>(
diff --git a/sparse_strips/vello_sparse_shaders/shaders/render.wesl b/sparse_strips/vello_sparse_shaders/shaders/render.wesl index 54f8dbe..0f28076 100644 --- a/sparse_strips/vello_sparse_shaders/shaders/render.wesl +++ b/sparse_strips/vello_sparse_shaders/shaders/render.wesl
@@ -40,19 +40,11 @@ }; import package::helpers::geometry::{pixel_to_ndc, quad_corner}; import package::helpers::image::{ - IMAGE_QUALITY_HIGH, - IMAGE_QUALITY_MEDIUM, - IMAGE_SOURCE_EXTERNAL, TINT_MODE_MULTIPLY, - bicubic_sample, - bilinear_sample, - get_image_atlas_index, get_image_extend_modes, get_image_offset, - get_image_padding, get_image_quality, get_image_size, - get_image_source_kind, get_image_transform, get_image_translate }; @@ -222,18 +214,15 @@ var<uniform> config: Config; @group(1) @binding(0) -var atlas_texture_array: texture_2d_array<f32>; - -@group(1) @binding(1) var external_texture_0: texture_2d<f32>; -@group(1) @binding(2) +@group(1) @binding(1) var external_texture_1: texture_2d<f32>; -@group(1) @binding(3) +@group(1) @binding(2) var external_texture_2: texture_2d<f32>; -@group(1) @binding(4) +@group(1) @binding(3) var external_texture_3: texture_2d<f32>; @group(2) @binding(0) @@ -410,10 +399,7 @@ let image_offset = get_image_offset(image_texel0); let image_size = get_image_size(image_texel0); let image_extend_modes = get_image_extend_modes(image_texel0); - let image_atlas_index = get_image_atlas_index(image_texel0); let image_quality = get_image_quality(image_texel0); - let image_source_kind = get_image_source_kind(image_texel0); - let image_padding = get_image_padding(image_texel2); let packed_tint = image_texel2.y; let image_tint = unpack4x8unorm(packed_tint); let is_multiply = image_texel2.z == TINT_MODE_MULTIPLY; @@ -431,72 +417,40 @@ // using GPU-native bilinear sampling var sample_color: vec4<f32>; - if image_source_kind == IMAGE_SOURCE_EXTERNAL { - let external_texture_slot = - (paint_and_rect_flag >> EXTERNAL_TEXTURE_SLOT_SHIFT) & 0x3u; - let final_xy = image_offset + extended_xy; - if external_texture_slot == 0u { - sample_color = sample_external_image( - external_texture_0, - image_quality, - final_xy, - image_offset, - image_size, - ); - } else if external_texture_slot == 1u { - sample_color = sample_external_image( - external_texture_1, - image_quality, - final_xy, - image_offset, - image_size, - ); - } else if external_texture_slot == 2u { - sample_color = sample_external_image( - external_texture_2, - image_quality, - final_xy, - image_offset, - image_size, - ); - } else { - sample_color = sample_external_image( - external_texture_3, - image_quality, - final_xy, - image_offset, - image_size, - ); - } - } else if image_quality == IMAGE_QUALITY_HIGH { - let final_xy = image_offset + extended_xy; - sample_color = bicubic_sample( - atlas_texture_array, + let external_texture_slot = + (paint_and_rect_flag >> EXTERNAL_TEXTURE_SLOT_SHIFT) & 0x3u; + let final_xy = image_offset + extended_xy; + if external_texture_slot == 0u { + sample_color = sample_external_image( + external_texture_0, + image_quality, final_xy, - i32(image_atlas_index), image_offset, image_size, - image_extend_modes, - image_padding, ); - } else if image_quality == IMAGE_QUALITY_MEDIUM { - let final_xy = image_offset + extended_xy - vec2(0.5); - sample_color = bilinear_sample( - atlas_texture_array, + } else if external_texture_slot == 1u { + sample_color = sample_external_image( + external_texture_1, + image_quality, final_xy, - i32(image_atlas_index), image_offset, image_size, - image_extend_modes, - image_padding, + ); + } else if external_texture_slot == 2u { + sample_color = sample_external_image( + external_texture_2, + image_quality, + final_xy, + image_offset, + image_size, ); } else { - let final_xy = image_offset + extended_xy; - sample_color = textureLoad( - atlas_texture_array, - vec2<u32>(final_xy), - i32(image_atlas_index), - 0, + sample_color = sample_external_image( + external_texture_3, + image_quality, + final_xy, + image_offset, + image_size, ); }
diff --git a/sparse_strips/vello_sparse_tests/tests/image_atlas.rs b/sparse_strips/vello_sparse_tests/tests/image_atlas.rs index 7524469..8cab391 100644 --- a/sparse_strips/vello_sparse_tests/tests/image_atlas.rs +++ b/sparse_strips/vello_sparse_tests/tests/image_atlas.rs
@@ -3,20 +3,60 @@ //! Tests for the image/glyph atlas configuration of the WebGL renderer. -use vello_common::pixmap::Pixmap; -use vello_hybrid::{AtlasConfig, AtlasTextureInfo, MemorySettings, RenderSettings, WebGlRenderer}; +use vello_common::{ + geometry::RectU16, + kurbo::Rect, + paint::{Image, ImageSource}, + peniko::ImageSampler, + pixmap::Pixmap, +}; +use vello_hybrid::{ + AtlasConfig, AtlasId, AtlasTextureInfo, MemorySettings, RenderError, RenderSettings, Scene, + TextureId, WebGlRenderer, WebGlTextureBindings, WebGlTextureWithDimensions, +}; use wasm_bindgen::JsCast; use wasm_bindgen_test::*; -use web_sys::{HtmlCanvasElement, WebGl2RenderingContext}; +use web_sys::{HtmlCanvasElement, WebGl2RenderingContext, WebGlTexture}; -#[wasm_bindgen_test] -fn image_atlas_placeholder_is_promoted_on_first_upload() { - let document = web_sys::window().unwrap().document().unwrap(); - let canvas = document +fn create_canvas() -> HtmlCanvasElement { + web_sys::window() + .unwrap() + .document() + .unwrap() .create_element("canvas") .unwrap() - .dyn_into::<HtmlCanvasElement>() - .unwrap(); + .dyn_into() + .unwrap() +} + +fn solid_texture(gl: &WebGl2RenderingContext, pixel: [u8; 4]) -> WebGlTexture { + let texture = gl.create_texture().unwrap(); + gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture)); + gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( + WebGl2RenderingContext::TEXTURE_2D, + 0, + WebGl2RenderingContext::RGBA8 as i32, + 1, + 1, + 0, + WebGl2RenderingContext::RGBA, + WebGl2RenderingContext::UNSIGNED_BYTE, + Some(&pixel), + ) + .unwrap(); + texture +} + +fn texture_binding_2d(gl: &WebGl2RenderingContext) -> WebGlTexture { + gl.get_parameter(WebGl2RenderingContext::TEXTURE_BINDING_2D) + .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); @@ -37,11 +77,11 @@ assert_eq!( renderer.atlas_info(), AtlasTextureInfo { - width: 1, - height: 1, - layer_count: 0, + width: 10, + height: 10, + texture_count: 0, }, - "renderer should start with only the 1x1 placeholder atlas texture" + "renderer should start without allocated atlas textures" ); assert_eq!( renderer.gl_context().get_error(), @@ -56,9 +96,9 @@ AtlasTextureInfo { width: 10, height: 10, - layer_count: 1, + texture_count: 1, }, - "first upload should promote the placeholder to the configured atlas" + "first upload should allocate the first configured atlas texture" ); assert_eq!( renderer.gl_context().get_error(), @@ -70,17 +110,12 @@ /// Uploading an image that is larger than the configured atlas must fail with /// `AtlasError::TextureTooLarge`. /// -/// The renderer constructor configures both the 10x10 GPU atlas texture and the allocator in the -/// returned resources, which runs the `TextureTooLarge` check. +/// The renderer constructor configures the allocator in the returned resources, which runs the +/// `TextureTooLarge` check. #[wasm_bindgen_test] #[should_panic(expected = "TextureTooLarge")] fn image_atlas_upload_larger_than_atlas_fails() { - let document = web_sys::window().unwrap().document().unwrap(); - let canvas = document - .create_element("canvas") - .unwrap() - .dyn_into::<HtmlCanvasElement>() - .unwrap(); + let canvas = create_canvas(); canvas.set_width(100); canvas.set_height(100); @@ -102,3 +137,97 @@ let image = Pixmap::new(64, 64); renderer.upload_image(&mut resources, &image); } + +#[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 { + memory_settings: MemorySettings { + image_atlas_config: atlas_config, + ..MemorySettings::default() + }, + ..RenderSettings::default() + }; + let (mut renderer, _) = WebGlRenderer::new_with(&canvas, settings, false); + let texture_id = TextureId(0); + let mut bindings = WebGlTextureBindings::new(); + bindings.insert(texture_id, renderer.atlas_texture(AtlasId::new(0)).clone()); + + let mut scene = Scene::new(1, 1); + scene.set_paint(Image { + image: ImageSource::external_texture(texture_id, RectU16::new(0, 0, 1, 1), 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(RenderError::TextureFeedbackLoop(id)) if id == texture_id + ), + "sampling from the render target should fail" + ); +} + +#[wasm_bindgen_test] +fn texture_copy_restores_texture_zero_binding_when_another_unit_is_active() { + let canvas = create_canvas(); + let settings = RenderSettings { + memory_settings: MemorySettings { + image_atlas_config: AtlasConfig { + initial_atlas_count: 1, + max_atlases: 1, + atlas_size: (1, 1), + ..AtlasConfig::default() + }, + ..MemorySettings::default() + }, + ..RenderSettings::default() + }; + let (mut renderer, mut resources) = WebGlRenderer::new_with(&canvas, settings, true); + let gl = renderer.gl_context().clone(); + + let source = solid_texture(&gl, [255, 0, 0, 255]); + let texture_zero = solid_texture(&gl, [0, 255, 0, 255]); + let texture_seven = solid_texture(&gl, [0, 0, 255, 255]); + + gl.active_texture(WebGl2RenderingContext::TEXTURE0); + gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture_zero)); + gl.active_texture(WebGl2RenderingContext::TEXTURE7); + gl.bind_texture(WebGl2RenderingContext::TEXTURE_2D, Some(&texture_seven)); + + renderer.upload_image( + &mut resources, + &WebGlTextureWithDimensions { + texture: source, + width: 1, + height: 1, + }, + ); + + assert_eq!( + gl.get_parameter(WebGl2RenderingContext::ACTIVE_TEXTURE) + .unwrap() + .as_f64() + .unwrap() as u32, + WebGl2RenderingContext::TEXTURE7, + "the texture copy should restore the active texture unit", + ); + assert_eq!( + texture_binding_2d(&gl), + texture_seven, + "the texture copy should preserve the active unit's binding", + ); + + gl.active_texture(WebGl2RenderingContext::TEXTURE0); + assert_eq!( + texture_binding_2d(&gl), + texture_zero, + "the texture copy should preserve texture unit 0's binding", + ); +}