From b788c2b827be9b976a06fb14856f87a46f54b9ca Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:55:43 +0000 Subject: [PATCH 1/2] Use an array texture for the image atlas This has a few tradeoffs: 1) Massively increases the number of images we can support (untested...) 2) Simplifies atlas allocation code 3) Doesn't allow any images larger than 2048x2048 to be included (currently). This is not a fundamental limitation 4) Shouldn't impact compatibility, as 256 layers are supported even in `downlevel_webgl2_defaults` 5) Reduces the memory efficiency of our atlases (as we just abandon partially-filled layers) --- vello/src/lib.rs | 4 ++ vello/src/recording.rs | 16 ++++--- vello/src/render.rs | 17 ++++--- vello/src/wgpu_engine.rs | 16 +++++-- vello_encoding/src/image_cache.rs | 66 ++++++++++++++++++--------- vello_encoding/src/resolve.rs | 38 +++++---------- vello_shaders/shader/fine.wgsl | 19 ++++---- vello_shaders/shader/shared/ptcl.wgsl | 1 + 8 files changed, 104 insertions(+), 73 deletions(-) diff --git a/vello/src/lib.rs b/vello/src/lib.rs index a2537e8e..e5f90686 100644 --- a/vello/src/lib.rs +++ b/vello/src/lib.rs @@ -523,12 +523,14 @@ impl Renderer { let target_proxy = ImageProxy::new( width, height, + 1, ImageFormat::from_wgpu(target.format) .expect("`TargetTexture` always has a supported texture format"), ); let surface_proxy = ImageProxy::new( width, height, + 1, ImageFormat::from_wgpu(surface.texture.format()) .ok_or(Error::UnsupportedSurfaceFormat)?, ); @@ -792,12 +794,14 @@ impl Renderer { let target_proxy = ImageProxy::new( width, height, + 1, ImageFormat::from_wgpu(target.format) .expect("`TargetTexture` always has a supported texture format"), ); let surface_proxy = ImageProxy::new( width, height, + 1, ImageFormat::from_wgpu(surface.texture.format()) .ok_or(Error::UnsupportedSurfaceFormat)?, ); diff --git a/vello/src/recording.rs b/vello/src/recording.rs index 05d5b002..517e7ac1 100644 --- a/vello/src/recording.rs +++ b/vello/src/recording.rs @@ -47,6 +47,7 @@ pub struct ImageProxy { pub height: u32, pub format: ImageFormat, pub id: ResourceId, + pub layers: u32, } #[derive(Clone, Copy)] @@ -68,7 +69,7 @@ pub enum Command { UploadUniform(BufferProxy, Vec), /// Commands the data to be uploaded to the given image. UploadImage(ImageProxy, Vec), - WriteImage(ImageProxy, [u32; 2], Image), + WriteImage(ImageProxy, [u32; 3], Image), Download(BufferProxy), /// Commands to clear the buffer from an offset on for a length of the given size. /// If the size is [None], it clears until the end. @@ -145,13 +146,13 @@ impl Recording { data: impl Into>, ) -> ImageProxy { let data = data.into(); - let image_proxy = ImageProxy::new(width, height, format); + let image_proxy = ImageProxy::new(width, height, 1, format); self.push(Command::UploadImage(image_proxy, data)); image_proxy } - pub fn write_image(&mut self, proxy: ImageProxy, x: u32, y: u32, image: Image) { - self.push(Command::WriteImage(proxy, [x, y], image)); + pub fn write_image(&mut self, proxy: ImageProxy, x: u32, y: u32, layer: u32, image: Image) { + self.push(Command::WriteImage(proxy, [x, y, layer], image)); } pub fn dispatch(&mut self, shader: ShaderId, wg_size: (u32, u32, u32), resources: R) @@ -257,12 +258,13 @@ impl ImageFormat { } impl ImageProxy { - pub fn new(width: u32, height: u32, format: ImageFormat) -> Self { + pub fn new(width: u32, height: u32, layers: u32, format: ImageFormat) -> Self { let id = ResourceId::next(); Self { width, height, format, + layers, id, } } @@ -273,8 +275,8 @@ impl ResourceProxy { Self::Buffer(BufferProxy::new(size, name)) } - pub fn new_image(width: u32, height: u32, format: ImageFormat) -> Self { - Self::Image(ImageProxy::new(width, height, format)) + pub fn new_image(width: u32, height: u32, layers: u32, format: ImageFormat) -> Self { + Self::Image(ImageProxy::new(width, height, layers, format)) } pub fn as_buf(&self) -> Option<&BufferProxy> { diff --git a/vello/src/render.rs b/vello/src/render.rs index 72e36ef8..acc27d3e 100644 --- a/vello/src/render.rs +++ b/vello/src/render.rs @@ -136,7 +136,7 @@ impl Render { let (layout, ramps, images) = resolver.resolve(encoding, &mut packed); let gradient_image = if ramps.height == 0 { - ResourceProxy::new_image(1, 1, ImageFormat::Rgba8) + ResourceProxy::new_image(1, 1, 1, ImageFormat::Rgba8) } else { let data: &[u8] = bytemuck::cast_slice(ramps.data); ResourceProxy::Image(recording.upload_image( @@ -147,12 +147,17 @@ impl Render { )) }; let image_atlas = if images.images.is_empty() { - ImageProxy::new(1, 1, ImageFormat::Rgba8) + ImageProxy::new(1, 1, 1, ImageFormat::Rgba8) } else { - ImageProxy::new(images.width, images.height, ImageFormat::Rgba8) + ImageProxy::new( + images.width, + images.height, + images.layers, + ImageFormat::Rgba8, + ) }; - for image in images.images { - recording.write_image(image_atlas, image.1, image.2, image.0.clone()); + for (image, x, y, layer) in images.images { + recording.write_image(image_atlas, *x, *y, *layer, image.clone()); } let cpu_config = RenderConfig::new(&layout, params.width, params.height, ¶ms.base_color); @@ -459,7 +464,7 @@ impl Render { recording.free_resource(draw_monoid_buf); recording.free_resource(bin_header_buf); recording.free_resource(path_buf); - let out_image = ImageProxy::new(params.width, params.height, ImageFormat::Rgba8); + let out_image = ImageProxy::new(params.width, params.height, 1, ImageFormat::Rgba8); let blend_spill_buf = BufferProxy::new( buffer_sizes.blend_spill.size_in_bytes().into(), "vello.blend_spill", diff --git a/vello/src/wgpu_engine.rs b/vello/src/wgpu_engine.rs index 2107fdec..d51d7ee7 100644 --- a/vello/src/wgpu_engine.rs +++ b/vello/src/wgpu_engine.rs @@ -465,7 +465,7 @@ impl WgpuEngine { self.bind_map .insert_image(image_proxy.id, texture, texture_view); } - Command::WriteImage(proxy, [x, y], image) => { + Command::WriteImage(proxy, [x, y, layer], image) => { let (texture, _) = self.bind_map.get_or_create_image(*proxy, device); let format = proxy.format.to_wgpu(); let block_size = format @@ -482,7 +482,11 @@ impl WgpuEngine { wgpu::ImageCopyTexture { texture, mip_level: 0, - origin: wgpu::Origin3d { x: *x, y: *y, z: 0 }, + origin: wgpu::Origin3d { + x: *x, + y: *y, + z: *layer, + }, aspect: TextureAspect::All, }, wgpu::Extent3d { @@ -496,7 +500,11 @@ impl WgpuEngine { wgpu::ImageCopyTexture { texture, mip_level: 0, - origin: wgpu::Origin3d { x: *x, y: *y, z: 0 }, + origin: wgpu::Origin3d { + x: *x, + y: *y, + z: *layer, + }, aspect: TextureAspect::All, }, image.data.data(), @@ -899,7 +907,7 @@ impl BindMap { size: wgpu::Extent3d { width: proxy.width, height: proxy.height, - depth_or_array_layers: 1, + depth_or_array_layers: proxy.layers, }, mip_level_count: 1, sample_count: 1, diff --git a/vello_encoding/src/image_cache.rs b/vello_encoding/src/image_cache.rs index 2d8709ef..4342eb40 100644 --- a/vello_encoding/src/image_cache.rs +++ b/vello_encoding/src/image_cache.rs @@ -6,22 +6,27 @@ use peniko::Image; use std::collections::hash_map::Entry; use std::collections::HashMap; -const DEFAULT_ATLAS_SIZE: i32 = 1024; -const MAX_ATLAS_SIZE: i32 = 8192; +const ATLAS_SIZE: i32 = 2048; +const MAX_ATLAS_LAYERS: u32 = 255; #[derive(Default)] pub struct Images<'a> { pub width: u32, pub height: u32, - pub images: &'a [(Image, u32, u32)], + pub layers: u32, + pub images: &'a [(Image, u32, u32, u32)], } pub(crate) struct ImageCache { atlas: AtlasAllocator, /// Map from image blob id to atlas location. - map: HashMap, + map: HashMap, /// List of all allocated images with associated atlas location. - images: Vec<(Image, u32, u32)>, + images: Vec<(Image, u32, u32, u32)>, + /// The current layer we're resolving for + layer: u32, + /// The number of layers we use. + layers: u32, } impl Default for ImageCache { @@ -33,9 +38,11 @@ impl Default for ImageCache { impl ImageCache { pub(crate) fn new() -> Self { Self { - atlas: AtlasAllocator::new(size2(DEFAULT_ATLAS_SIZE, DEFAULT_ATLAS_SIZE)), + atlas: AtlasAllocator::new(size2(ATLAS_SIZE, ATLAS_SIZE)), + layer: 0, map: Default::default(), images: Default::default(), + layers: 4, } } @@ -44,37 +51,54 @@ impl ImageCache { width: self.atlas.size().width as u32, height: self.atlas.size().height as u32, images: &self.images, + layers: self.layers, } } - pub(crate) fn bump_size(&mut self) -> bool { - let new_size = self.atlas.size().width * 2; - if new_size > MAX_ATLAS_SIZE { - return false; - } - self.atlas = AtlasAllocator::new(size2(new_size, new_size)); - self.map.clear(); - self.images.clear(); - true - } - pub(crate) fn clear(&mut self) { self.atlas.clear(); self.map.clear(); self.images.clear(); + self.layer = 0; } - pub(crate) fn get_or_insert(&mut self, image: &Image) -> Option<(u32, u32)> { + pub(crate) fn get_or_insert(&mut self, image: &Image) -> Option<(u32, u32, u32)> { match self.map.entry(image.data.id()) { Entry::Occupied(occupied) => Some(*occupied.get()), Entry::Vacant(vacant) => { + if image.width > ATLAS_SIZE as u32 || image.height > ATLAS_SIZE as u32 { + // We currently cannot support images larger than 2048 in any axis. + // We should probably still support that, but I think the fallback + // might end up being a second "atlas" + // We choose not to re-size the atlas in that case, because it + // would add a large amount of unused data. + return None; + } let alloc = self .atlas - .allocate(size2(image.width as _, image.height as _))?; + .allocate(size2(image.width as _, image.height as _)); + let alloc = match alloc { + Some(alloc) => alloc, + None => { + if self.layer >= MAX_ATLAS_LAYERS { + return None; + } + // We implement a greedy system for layers; if we ever get an image that won't fit. + self.layer += 1; + if self.layer >= self.layers { + self.layers = (self.layers * 2).min(MAX_ATLAS_LAYERS); + debug_assert!(self.layer < self.layers); + } + self.atlas.clear(); + // This should never fail, as it's a fresh atlas + self.atlas + .allocate(size2(image.width as _, image.height as _))? + } + }; let x = alloc.rectangle.min.x as u32; let y = alloc.rectangle.min.y as u32; - self.images.push((image.clone(), x, y)); - Some(*vacant.insert((x, y))) + self.images.push((image.clone(), x, y, self.layer)); + Some(*vacant.insert((x, y, self.layer))) } } } diff --git a/vello_encoding/src/resolve.rs b/vello_encoding/src/resolve.rs index 78249355..88fcdf7c 100644 --- a/vello_encoding/src/resolve.rs +++ b/vello_encoding/src/resolve.rs @@ -284,9 +284,11 @@ impl Resolver { if pos < *draw_data_offset { data.extend_from_slice(&encoding.draw_data[pos..*draw_data_offset]); } - if let Some((x, y)) = self.pending_images[*index].xy { - let xy = (x << 16) | y; - data.extend_from_slice(bytemuck::bytes_of(&xy)); + if let Some((x, y, z)) = self.pending_images[*index].xyz { + // 11 bits for x, 11 bits for y, 10 bits for z + // TODO: Check that there's no overflow. + let xyz = (x << 21) | (y << 10) | z; + data.extend_from_slice(bytemuck::bytes_of(&xyz)); pos = *draw_data_offset + 4; } else { // If we get here, we failed to allocate a slot for this image in the atlas. @@ -462,7 +464,7 @@ impl Resolver { let index = self.pending_images.len(); self.pending_images.push(PendingImage { image: image.clone(), - xy: None, + xyz: None, }); self.patches.push(ResolvedPatch::Image { index, @@ -476,27 +478,11 @@ impl Resolver { fn resolve_pending_images(&mut self) { self.image_cache.clear(); - 'outer: loop { - // Loop over the images, attempting to allocate them all into the atlas. - for pending_image in &mut self.pending_images { - if let Some(xy) = self.image_cache.get_or_insert(&pending_image.image) { - pending_image.xy = Some(xy); - } else { - // We failed to allocate. Try to bump the atlas size. - if self.image_cache.bump_size() { - // We were able to increase the atlas size. Restart the outer loop. - continue 'outer; - } else { - // If the atlas is already maximum size, there's nothing we can do. Set - // the xy field to None so this image isn't rendered and then carry on-- - // other images might still fit. - pending_image.xy = None; - } - } - } - // If we made it here, we've either successfully allocated all images or we reached - // the maximum atlas size. - break; + // Loop over the images, attempting to allocate them all into the atlas. + for pending_image in &mut self.pending_images { + // We might have failed to allocate. We continue in that case, because another image might still fit + // (for example, images larger than `ATLAS_SIZE`px in any dimension will currently never fit) + pending_image.xyz = self.image_cache.get_or_insert(&pending_image.image); } } } @@ -531,7 +517,7 @@ pub enum Patch { #[derive(Clone, Debug)] struct PendingImage { image: Image, - xy: Option<(u32, u32)>, + xyz: Option<(u32, u32, u32)>, } #[derive(Clone, Debug)] diff --git a/vello_shaders/shader/fine.wgsl b/vello_shaders/shader/fine.wgsl index f7fedef2..4e7289a7 100644 --- a/vello_shaders/shader/fine.wgsl +++ b/vello_shaders/shader/fine.wgsl @@ -807,7 +807,7 @@ fn read_image(cmd_ix: u32) -> CmdImage { let m3 = bitcast(info[info_offset + 3u]); let matrx = vec4(m0, m1, m2, m3); let xlat = vec2(bitcast(info[info_offset + 4u]), bitcast(info[info_offset + 5u])); - let xy = info[info_offset + 6u]; + let xyz = info[info_offset + 6u]; let width_height = info[info_offset + 7u]; let sample_alpha = info[info_offset + 8u]; let alpha = f32(sample_alpha & 0xFFu) / 255.0; @@ -815,11 +815,12 @@ fn read_image(cmd_ix: u32) -> CmdImage { let x_extend = (sample_alpha >> 10u) & 0x3u; let y_extend = (sample_alpha >> 8u) & 0x3u; // The following are not intended to be bitcasts - let x = f32(xy >> 16u); - let y = f32(xy & 0xffffu); + let x = f32(xyz >> 21u); + let y = f32((xyz >> 10u) & 0x7ffu); + let index = i32(xyz & 0x3ffu); let width = f32(width_height >> 16u); let height = f32(width_height & 0xffffu); - return CmdImage(matrx, xlat, vec2(x, y), vec2(width, height), x_extend, y_extend, quality, alpha); + return CmdImage(matrx, xlat, vec2(x, y), index, vec2(width, height), x_extend, y_extend, quality, alpha); } fn read_end_clip(cmd_ix: u32) -> CmdEndClip { @@ -1169,7 +1170,7 @@ fn main( // TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max); // Nearest neighbor sampling - let fg_rgba = premul_alpha(textureLoad(image_atlas, vec2(atlas_uv_clamped), 0)); + let fg_rgba = premul_alpha(textureLoad(image_atlas, vec2(atlas_uv_clamped), image.index)); let fg_i = fg_rgba * area[i] * image.alpha; rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; } @@ -1191,10 +1192,10 @@ fn main( // atlas_offset are integers let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped)); let uv_frac = fract(atlas_uv); - let a = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xy), 0)); - let b = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xw), 0)); - let c = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zy), 0)); - let d = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zw), 0)); + let a = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xy), image.index)); + let b = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xw), image.index)); + let c = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zy), image.index)); + let d = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zw), image.index)); // Bilinear sampling let fg_rgba = mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x); let fg_i = fg_rgba * area[i] * image.alpha; diff --git a/vello_shaders/shader/shared/ptcl.wgsl b/vello_shaders/shader/shared/ptcl.wgsl index d0b41cbe..4902d773 100644 --- a/vello_shaders/shader/shared/ptcl.wgsl +++ b/vello_shaders/shader/shared/ptcl.wgsl @@ -96,6 +96,7 @@ struct CmdImage { matrx: vec4, xlat: vec2, atlas_offset: vec2, + index: i32, extents: vec2, x_extend_mode: u32, y_extend_mode: u32, From e562882ddfb4ca3456a13f1cc02d31d8cec006dc Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:11:29 +0000 Subject: [PATCH 2/2] Actually use array textures... --- vello/src/recording.rs | 2 ++ vello/src/render.rs | 2 +- vello/src/shaders.rs | 2 +- vello/src/wgpu_engine.rs | 57 +++++++++++++++++++++------------- vello_shaders/shader/fine.wgsl | 12 +++---- 5 files changed, 45 insertions(+), 30 deletions(-) diff --git a/vello/src/recording.rs b/vello/src/recording.rs index 517e7ac1..ddf934fe 100644 --- a/vello/src/recording.rs +++ b/vello/src/recording.rs @@ -99,6 +99,8 @@ pub enum BindType { Image(ImageFormat), /// A storage image with read only access. ImageRead(ImageFormat), + /// A storage array texture with read only access. + ImageArrayRead(ImageFormat), // TODO: Uniform, Sampler, maybe others } diff --git a/vello/src/render.rs b/vello/src/render.rs index acc27d3e..e7b610e5 100644 --- a/vello/src/render.rs +++ b/vello/src/render.rs @@ -147,7 +147,7 @@ impl Render { )) }; let image_atlas = if images.images.is_empty() { - ImageProxy::new(1, 1, 1, ImageFormat::Rgba8) + ImageProxy::new(1, 1, 2, ImageFormat::Rgba8) } else { ImageProxy::new( images.width, diff --git a/vello/src/shaders.rs b/vello/src/shaders.rs index 835ae6b0..c86cf37c 100644 --- a/vello/src/shaders.rs +++ b/vello/src/shaders.rs @@ -214,7 +214,7 @@ pub(crate) fn full_shaders( Buffer, Image(ImageFormat::Rgba8), ImageRead(ImageFormat::Rgba8), - ImageRead(ImageFormat::Rgba8), + ImageArrayRead(ImageFormat::Rgba8), // Mask LUT buffer, used only when MSAA is enabled. BufReadOnly, ]; diff --git a/vello/src/wgpu_engine.rs b/vello/src/wgpu_engine.rs index d51d7ee7..05a94579 100644 --- a/vello/src/wgpu_engine.rs +++ b/vello/src/wgpu_engine.rs @@ -782,26 +782,31 @@ impl WgpuEngine { }, count: None, }, - BindType::Image(format) | BindType::ImageRead(format) => { - wgpu::BindGroupLayoutEntry { - binding: i as u32, - visibility, - ty: if bind_type == BindType::ImageRead(format) { - wgpu::BindingType::Texture { - sample_type: wgpu::TextureSampleType::Float { filterable: true }, - view_dimension: TextureViewDimension::D2, - multisampled: false, - } - } else { - wgpu::BindingType::StorageTexture { - access: wgpu::StorageTextureAccess::WriteOnly, - format: format.to_wgpu(), - view_dimension: TextureViewDimension::D2, - } + BindType::Image(format) + | BindType::ImageRead(format) + | BindType::ImageArrayRead(format) => wgpu::BindGroupLayoutEntry { + binding: i as u32, + visibility, + ty: match bind_type { + BindType::ImageRead(_) => wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + multisampled: false, }, - count: None, - } - } + BindType::Image(_) => wgpu::BindingType::StorageTexture { + access: wgpu::StorageTextureAccess::WriteOnly, + format: format.to_wgpu(), + view_dimension: TextureViewDimension::D2, + }, + BindType::ImageArrayRead(_) => wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2Array, + multisampled: false, + }, + _ => unreachable!(), + }, + count: None, + }, }) .collect::>() } @@ -918,7 +923,11 @@ impl BindMap { }); let texture_view = texture.create_view(&wgpu::TextureViewDescriptor { label: None, - dimension: Some(TextureViewDimension::D2), + dimension: Some(if proxy.layers > 1 { + TextureViewDimension::D2Array + } else { + TextureViewDimension::D2 + }), aspect: TextureAspect::All, mip_level_count: None, base_mip_level: 0, @@ -1094,7 +1103,7 @@ impl<'a> TransientBindMap<'a> { size: wgpu::Extent3d { width: proxy.width, height: proxy.height, - depth_or_array_layers: 1, + depth_or_array_layers: proxy.layers, }, mip_level_count: 1, sample_count: 1, @@ -1105,7 +1114,11 @@ impl<'a> TransientBindMap<'a> { }); let texture_view = texture.create_view(&wgpu::TextureViewDescriptor { label: None, - dimension: Some(TextureViewDimension::D2), + dimension: Some(if proxy.layers > 1 { + TextureViewDimension::D2Array + } else { + TextureViewDimension::D2 + }), aspect: TextureAspect::All, mip_level_count: None, base_mip_level: 0, diff --git a/vello_shaders/shader/fine.wgsl b/vello_shaders/shader/fine.wgsl index 4e7289a7..fa460ad8 100644 --- a/vello_shaders/shader/fine.wgsl +++ b/vello_shaders/shader/fine.wgsl @@ -45,7 +45,7 @@ var output: texture_storage_2d; var gradients: texture_2d; @group(0) @binding(7) -var image_atlas: texture_2d; +var image_atlas: texture_2d_array; // MSAA-only bindings and utilities #ifdef msaa @@ -1170,7 +1170,7 @@ fn main( // TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max); // Nearest neighbor sampling - let fg_rgba = premul_alpha(textureLoad(image_atlas, vec2(atlas_uv_clamped), image.index)); + let fg_rgba = premul_alpha(textureLoad(image_atlas, vec2(atlas_uv_clamped), image.index, 0)); let fg_i = fg_rgba * area[i] * image.alpha; rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; } @@ -1192,10 +1192,10 @@ fn main( // atlas_offset are integers let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped)); let uv_frac = fract(atlas_uv); - let a = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xy), image.index)); - let b = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xw), image.index)); - let c = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zy), image.index)); - let d = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zw), image.index)); + let a = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xy), image.index, 0)); + let b = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xw), image.index, 0)); + let c = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zy), image.index, 0)); + let d = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.zw), image.index, 0)); // Bilinear sampling let fg_rgba = mix(mix(a, b, uv_frac.y), mix(c, d, uv_frac.y), uv_frac.x); let fg_i = fg_rgba * area[i] * image.alpha;