diff --git a/core/src/primitive/layer.rs b/core/src/primitive/layer.rs index 54f6a96..5f62efd 100644 --- a/core/src/primitive/layer.rs +++ b/core/src/primitive/layer.rs @@ -1,6 +1,7 @@ use std::ops::{Index, IndexMut}; use crate::{ + UiRegion, WidgetId, render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives}, util::to_mut, }; @@ -131,6 +132,17 @@ impl PrimitiveLayers { pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { self[h.layer].free(h) } + + pub fn write_image( + &mut self, + layer: LayerId, + id: WidgetId, + texture_idx: u32, + region: UiRegion, + mask_idx: MaskIdx, + ) -> PrimitiveHandle { + self[layer].write_image(layer, id, texture_idx, region, mask_idx) + } } impl Default for Layers { diff --git a/core/src/primitive/texture.rs b/core/src/primitive/texture.rs index 95c210e..df5635b 100644 --- a/core/src/primitive/texture.rs +++ b/core/src/primitive/texture.rs @@ -1,19 +1,32 @@ -use crate::{ - render::TexturePrimitive, - util::{RefCounter, Vec2}, -}; +use crate::util::{RefCounter, Vec2}; use image::{DynamicImage, GenericImageView}; use std::{ ops::Index, sync::mpsc::{Receiver, Sender, channel}, }; +/// Which of the two things a texture slot holds. The two are drawn very +/// differently: a page is a layer of one shared array texture and never gets +/// its own bind group; a standalone image is the opposite, one texture and +/// one bind group, never a layer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TextureKind { + Image, + /// The array-texture layer this page was assigned. Chosen synchronously + /// by `Textures::add_page` rather than by the renderer, because glyph + /// insertion needs it in the same call, before any GPU sync happens. + Page { + layer: u32, + }, +} + #[derive(Debug, Clone)] pub struct TextureHandle { - inner: TexturePrimitive, + slot: u32, + kind: TextureKind, size: Vec2, counter: RefCounter, - send: Sender, + send: Sender<(TextureKind, u32)>, } /// a texture manager for a ui @@ -21,17 +34,24 @@ pub struct TextureHandle { pub struct Textures { free: Vec, images: Vec>, + /// Next layer to hand out to an atlas page. Pages are never freed (no + /// atlas eviction), so this only grows and `free` never holds one. + next_page_layer: u32, updates: Vec, - send: Sender, - recv: Receiver, + send: Sender<(TextureKind, u32)>, + recv: Receiver<(TextureKind, u32)>, } pub enum TextureUpdate<'a> { - Push(&'a DynamicImage), - Set(u32, &'a DynamicImage), + Push(TextureKind, &'a DynamicImage), + Set(TextureKind, u32, &'a DynamicImage), + /// Overwrite a rectangle of an existing texture, rather than replacing it. + /// The glyph atlas grows a glyph at a time, and re-uploading a whole atlas + /// per glyph is megabytes of copy for a few hundred bytes of change. + /// Only ever issued against a page -- a standalone image is never patched. Patch(u32, PatchRect, &'a DynamicImage), Free(u32), - PushFree, + PushFree(TextureKind), SetFree, } @@ -44,8 +64,8 @@ pub struct PatchRect { } enum Update { - Push(u32), - Set(u32), + Push(TextureKind, u32), + Set(TextureKind, u32), Patch(u32, PatchRect), Free(u32), } @@ -56,70 +76,93 @@ impl Textures { Self { free: Vec::new(), images: Vec::new(), + next_page_layer: 0, updates: Vec::new(), send, recv, } } + pub fn add(&mut self, image: impl Into) -> TextureHandle { let image = image.into(); let size = image.dimensions().into(); - let view_idx = self.push(image); - // 0 == default in renderer; TODO: actually create samplers here - let sampler_idx = 0; + let kind = TextureKind::Image; + let slot = self.push(kind, image); TextureHandle { - inner: TexturePrimitive { - view_idx, - sampler_idx, - }, + slot, + kind, size, counter: RefCounter::new(), send: self.send.clone(), } } - fn push(&mut self, image: DynamicImage) -> u32 { + /// Adds a page of the shared glyph atlas array. Only `atlas.rs` should + /// call this -- everything else wants `add`. + pub fn add_page(&mut self, image: impl Into) -> TextureHandle { + let image = image.into(); + let size = image.dimensions().into(); + let layer = self.next_page_layer; + self.next_page_layer += 1; + let kind = TextureKind::Page { layer }; + let slot = self.push(kind, image); + TextureHandle { + slot, + kind, + size, + counter: RefCounter::new(), + send: self.send.clone(), + } + } + + fn push(&mut self, kind: TextureKind, image: DynamicImage) -> u32 { if let Some(i) = self.free.pop() { self.images[i as usize] = Some(image); - self.updates.push(Update::Set(i)); + self.updates.push(Update::Set(kind, i)); i } else { let i = self.images.len() as u32; self.images.push(Some(image)); - self.updates.push(Update::Push(i)); + self.updates.push(Update::Push(kind, i)); i } } pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage { - self.images[handle.inner.view_idx as usize] + self.images[handle.slot as usize] .as_mut() .expect("texture was freed while still held") } /// Queue an upload of just `rect`, after writing it with `image_mut`. pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) { - self.updates - .push(Update::Patch(handle.inner.view_idx, rect)); + self.updates.push(Update::Patch(handle.slot, rect)); } pub fn free(&mut self) { - for idx in self.recv.try_iter() { + for (kind, idx) in self.recv.try_iter() { self.images[idx as usize] = None; self.updates.push(Update::Free(idx)); - self.free.push(idx); + // A page's slot is never reclaimed: `GlyphAtlas` never drops the + // handles it holds, and there is no eviction path for a hole in + // the middle of the array's layers. So `free` holds ordinary + // image slots only, and a page's layer would need a free list of + // its own were that to change. + if kind == TextureKind::Image { + self.free.push(idx); + } } } pub fn updates(&mut self) -> impl Iterator> { self.updates.drain(..).map(|u| match u { - Update::Push(i) => self.images[i as usize] + Update::Push(kind, i) => self.images[i as usize] .as_ref() - .map(TextureUpdate::Push) - .unwrap_or(TextureUpdate::PushFree), - Update::Set(i) => self.images[i as usize] + .map(|img| TextureUpdate::Push(kind, img)) + .unwrap_or(TextureUpdate::PushFree(kind)), + Update::Set(kind, i) => self.images[i as usize] .as_ref() - .map(|img| TextureUpdate::Set(i, img)) + .map(|img| TextureUpdate::Set(kind, i, img)) .unwrap_or(TextureUpdate::SetFree), Update::Patch(i, rect) => self.images[i as usize] .as_ref() @@ -131,18 +174,36 @@ impl Textures { } impl TextureHandle { - pub fn primitive(&self) -> TexturePrimitive { - self.inner - } pub fn size(&self) -> Vec2 { self.size } + + /// The bind-group index this handle draws with. Only valid for a + /// standalone image; an atlas page has no bind group of its own -- it + /// samples the shared array via `layer()` instead. Getting this wrong is + /// a caller bug (the wrong kind of handle reached the wrong draw path), + /// not a recoverable condition, so it panics rather than drawing garbage. + pub fn image_index(&self) -> u32 { + match self.kind { + TextureKind::Image => self.slot, + TextureKind::Page { .. } => panic!("image_index() called on an atlas page handle"), + } + } + + /// The layer this page occupies in the shared atlas array texture. + /// Only valid for a page handle; see `image_index`'s note. + pub fn layer(&self) -> u32 { + match self.kind { + TextureKind::Page { layer } => layer, + TextureKind::Image => panic!("layer() called on a standalone image handle"), + } + } } impl Drop for TextureHandle { fn drop(&mut self) { if self.counter.drop() { - let _ = self.send.send(self.inner.view_idx); + let _ = self.send.send((self.kind, self.slot)); } } } @@ -151,7 +212,7 @@ impl Index<&TextureHandle> for Textures { type Output = DynamicImage; fn index(&self, index: &TextureHandle) -> &Self::Output { - self.images[index.inner.view_idx as usize].as_ref().unwrap() + self.images[index.slot as usize].as_ref().unwrap() } } diff --git a/core/src/render/atlas.rs b/core/src/render/atlas.rs index dc0d1de..aa1b235 100644 --- a/core/src/render/atlas.rs +++ b/core/src/render/atlas.rs @@ -5,7 +5,12 @@ use crate::{ use image::RgbaImage; use swash::scale::image::{Content, Image}; -const PAGE: u32 = 1024; +/// Side of one atlas page, in pixels. 1024 is 4 MB at RGBA8 -- enough for a +/// few thousand glyphs at UI sizes, and small enough that a page nobody fills +/// is not a big waste. Also the fixed width/height of every layer of the +/// shared array texture in `render::texture` -- `pub(crate)` so that module +/// can size it without a second constant to keep in sync. +pub(crate) const PAGE: u32 = 1024; /// Transparent margin kept around every glyph, so that sampling one cannot /// pick up its neighbour along a shared edge. @@ -35,8 +40,8 @@ pub struct GlyphEntry { pub width: u32, pub height: u32, pub is_colored: bool, - pub view_idx: u32, - pub sampler_idx: u32, + /// The atlas array layer this glyph's page occupies. + pub layer: u32, } impl GlyphEntry { @@ -120,8 +125,7 @@ impl GlyphAtlas { width: w, height: h, is_colored: matches!(image.content, Content::Color), - view_idx: page.handle.primitive().view_idx, - sampler_idx: page.handle.primitive().sampler_idx, + layer: page.handle.layer(), }; self.entries.insert(key, Some(entry)); Some(entry) @@ -137,7 +141,7 @@ impl GlyphAtlas { return (i, x, y); } - let handle = textures.add(RgbaImage::new(PAGE, PAGE)); + let handle = textures.add_page(RgbaImage::new(PAGE, PAGE)); self.pages.push(Page { handle, x: PAD + w + PAD, diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index f40cf76..ff54d65 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -1,5 +1,3 @@ -use std::num::NonZero; - use crate::{ UiData, UiRenderState, render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf}, @@ -42,21 +40,44 @@ struct RenderLayer { instance: ArrBuf, primitives: PrimitiveBuffers, primitive_group: BindGroup, + /// A standalone image's instances, kept apart from `instance` because + /// each one draws with its own bind group -- see `UiRenderNode::draw`. + image_instance: ArrBuf, + /// The texture slot each entry of `image_instance` draws with, in the + /// same order, refreshed alongside it. Not stored in the vertex buffer + /// itself because it names a bind group, not shader data. + image_tex_indices: Vec, } impl UiRenderNode { pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) { pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &self.uniform_group, &[]); - pass.set_bind_group(2, &self.rsc_group, &[]); for i in &self.active { let layer = &self.layers[i]; - if layer.instance.len() == 0 { + if layer.instance.len() == 0 && layer.image_instance.len() == 0 { continue; } pass.set_bind_group(1, &layer.primitive_group, &[]); - pass.set_vertex_buffer(0, layer.instance.buffer.slice(..)); - pass.draw(0..4, 0..layer.instance.len() as u32); + if layer.instance.len() > 0 { + pass.set_bind_group(2, &self.rsc_group, &[]); + pass.set_vertex_buffer(0, layer.instance.buffer.slice(..)); + pass.draw(0..4, 0..layer.instance.len() as u32); + } + // Images draw after this layer's rects and glyphs, one draw call + // each with its own bind group. That draws every image "on top" + // within the layer, which loses nothing that currently exists: + // `Primitives::apply_free` frees with `swap_remove`, so a layer's + // draw order was already undefined before images had their own + // list -- nothing before this relied on interleaving a rect + // between two images at a particular position. + if layer.image_instance.len() > 0 { + pass.set_vertex_buffer(0, layer.image_instance.buffer.slice(..)); + for (k, &tex_idx) in layer.image_tex_indices.iter().enumerate() { + pass.set_bind_group(2, self.textures.image_bind_group(tex_idx), &[]); + pass.draw(0..4, k as u32..k as u32 + 1); + } + } } } @@ -73,7 +94,15 @@ impl UiRenderNode { for change in primitives.apply_free() { if let Some(inst) = ui_render.active.get_mut(&change.id) { for h in &mut inst.primitives { - if h.layer == i && h.inst_idx == change.old { + // `is_image` disambiguates: `instances` and `images` + // are separate lists with independent indices, so + // without it a rect's renumbering could be applied to + // an image handle that happened to share the same + // (layer, inst_idx). + if h.layer == i + && h.inst_idx == change.old + && (h.binding == IMAGE_BINDING) == change.is_image + { h.inst_idx = change.new; break; } @@ -92,6 +121,12 @@ impl UiRenderNode { ), primitives, primitive_group, + image_instance: ArrBuf::new( + device, + BufferUsages::VERTEX | BufferUsages::COPY_DST, + "image instance", + ), + image_tex_indices: Vec::new(), } }); if primitives.updated { @@ -104,17 +139,30 @@ impl UiRenderNode { &self.primitive_layout, rlayer.primitives.buffers(), ); + rlayer + .image_instance + .update(device, queue, primitives.image_instances()); + rlayer.image_tex_indices = primitives + .image_instances() + .iter() + .map(|inst| inst.idx) + .collect(); primitives.updated = false; } } - let mut changed = false; - changed |= self.textures.update(&mut ui.textures); - if ui.masks.changed { + let masks_resized = if ui.masks.changed { ui.masks.changed = false; - self.masks.update(device, queue, &ui.masks[..]); - changed = true; - } - if changed { + self.masks.update(device, queue, &ui.masks[..]) + } else { + false + }; + let rebuild_main = self.textures.update( + &mut ui.textures, + &self.rsc_layout, + &self.masks, + masks_resized, + ); + if rebuild_main { self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks); } } @@ -128,12 +176,7 @@ impl UiRenderNode { queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice)); } - pub fn new( - device: &Device, - queue: &Queue, - config: &SurfaceConfiguration, - limits: UiLimits, - ) -> Self { + pub fn new(device: &Device, queue: &Queue, config: &SurfaceConfiguration) -> Self { let shader = device.create_shader_module(ShaderModuleDescriptor { label: Some("UI Shape Shader"), source: ShaderSource::Wgsl(SHAPE_SHADER.into()), @@ -166,17 +209,15 @@ impl UiRenderNode { let uniform_group = Self::bind_group_0(device, &uniform_layout, &window_buffer); let primitive_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &core::array::from_fn::<_, { PrimitiveBuffers::LEN }, _>(|i| { - BindGroupLayoutEntry { - binding: i as u32, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Buffer { - ty: BufferBindingType::Storage { read_only: true }, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - } + entries: &PrimitiveBuffers::BINDINGS.map(|binding| BindGroupLayoutEntry { + binding, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, }), label: Some("primitive"), }); @@ -188,7 +229,7 @@ impl UiRenderNode { "ui masks", ); - let rsc_layout = Self::rsc_layout(device, &limits); + let rsc_layout = Self::rsc_layout(device); let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks); let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { @@ -278,7 +319,12 @@ impl UiRenderNode { }) } - fn rsc_layout(device: &Device, limits: &UiLimits) -> BindGroupLayout { + /// Group 2: the shared atlas array, one standalone-image slot (a null + /// view for the main draw, a real one for each image's own bind group -- + /// see `GpuTextures`), one sampler and the masks buffer. No `count` on + /// any entry: this needs nothing beyond plain Vulkan 1.0 / GLES + /// sampling, unlike the `binding_array` layout it replaced. + fn rsc_layout(device: &Device) -> BindGroupLayout { device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[ BindGroupLayoutEntry { @@ -286,20 +332,30 @@ impl UiRenderNode { visibility: ShaderStages::FRAGMENT, ty: BindingType::Texture { sample_type: TextureSampleType::Float { filterable: false }, - view_dimension: TextureViewDimension::D2, + view_dimension: TextureViewDimension::D2Array, multisampled: false, }, - count: Some(NonZero::new(limits.max_textures).unwrap()), + count: None, }, BindGroupLayoutEntry { binding: 1, visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::NonFiltering), - count: Some(NonZero::new(limits.max_samplers).unwrap()), + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: false }, + view_dimension: TextureViewDimension::D2, + multisampled: false, + }, + count: None, }, BindGroupLayoutEntry { binding: 2, visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::NonFiltering), + count: None, + }, + BindGroupLayoutEntry { + binding: 3, + visibility: ShaderStages::FRAGMENT, ty: BindingType::Buffer { ty: BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, @@ -312,6 +368,8 @@ impl UiRenderNode { }) } + /// The main group: rects and glyphs never sample the image slot, so it + /// gets a 1x1 null view rather than any live standalone image's. fn rsc_group( device: &Device, layout: &BindGroupLayout, @@ -323,14 +381,18 @@ impl UiRenderNode { entries: &[ BindGroupEntry { binding: 0, - resource: BindingResource::TextureViewArray(&tex_manager.views()), + resource: BindingResource::TextureView(tex_manager.array_view()), }, BindGroupEntry { binding: 1, - resource: BindingResource::SamplerArray(&tex_manager.samplers()), + resource: BindingResource::TextureView(tex_manager.null_view()), }, BindGroupEntry { binding: 2, + resource: BindingResource::Sampler(tex_manager.sampler()), + }, + BindGroupEntry { + binding: 3, resource: masks.buffer.as_entire_binding(), }, ], @@ -342,26 +404,3 @@ impl UiRenderNode { self.textures.view_count() } } - -pub struct UiLimits { - max_textures: u32, - max_samplers: u32, -} - -impl Default for UiLimits { - fn default() -> Self { - Self { - max_textures: 100000, - max_samplers: 1000, - } - } -} - -impl UiLimits { - pub fn max_binding_array_elements_per_shader_stage(&self) -> u32 { - self.max_textures + self.max_samplers - } - pub fn max_binding_array_sampler_elements_per_shader_stage(&self) -> u32 { - self.max_samplers - } -} diff --git a/core/src/render/primitive.rs b/core/src/render/primitive.rs index 849cbe0..9e87b49 100644 --- a/core/src/render/primitive.rs +++ b/core/src/render/primitive.rs @@ -16,6 +16,17 @@ pub struct Primitives { assoc: Vec, data: PrimitiveData, free: Vec, + + /// Standalone images, kept apart from `instances` because each one draws + /// with its own bind group rather than sharing the layer's one instanced + /// draw. `idx` on each `PrimitiveInstance` here is the texture's slot in + /// `Textures`/`GpuTextures`, not an index into `data`: a bind group has + /// already picked the texture, so there is nothing left to look up + /// per-instance and no per-image entry in `data` at all. + images: Vec, + image_assoc: Vec, + image_free: Vec, + pub updated: bool, } @@ -26,11 +37,21 @@ impl Default for Primitives { assoc: Default::default(), data: Default::default(), free: Vec::new(), + images: Default::default(), + image_assoc: Default::default(), + image_free: Vec::new(), updated: true, } } } +/// The `binding` tag `Painter` writes on an image instance. Distinct from any +/// `Primitive::BINDING` because images have no `PrimitiveData` entry to key +/// one from -- a bind group already selects the texture -- so this only ever +/// has to match the shader's `TEXTURE` constant and flag "this instance lives +/// in `Primitives::images`, not `Primitives::instances`" to the code below. +pub const IMAGE_BINDING: u32 = 1; + pub trait Primitive: Pod { const BINDING: u32; fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec; @@ -55,6 +76,14 @@ macro_rules! primitives { impl PrimitiveBuffers { pub const LEN: usize = primitives!(@count $($name)*); + /// The group-1 binding number each primitive's storage buffer + /// sits at, in declaration order. Not `0..LEN`: a primitive's + /// `BINDING` also tags its instances for the shader's dispatch + /// switch, and a removed primitive (as `TEXTURE` was, once + /// images stopped needing a per-instance storage entry) can + /// leave a gap, so the pipeline layout has to ask for these + /// exact numbers rather than assuming they are contiguous. + pub const BINDINGS: [u32; Self::LEN] = [$(<$ty>::BINDING,)*]; pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] { [ $((<$ty>::BINDING, &self.$name.buffer),)* @@ -138,26 +167,103 @@ impl Primitives { PrimitiveHandle::new::

(layer, inst_i, i) } - /// returns (old index, new index) - pub fn apply_free(&mut self) -> impl Iterator { - self.free.sort_by(|a, b| b.cmp(a)); - self.free.drain(..).filter_map(|i| { - self.instances.swap_remove(i); - self.assoc.swap_remove(i); - if i == self.instances.len() { - return None; - } - let id = self.assoc[i]; - let old = self.instances.len(); - Some(PrimitiveChange { id, old, new: i }) - }) + /// Writes an image instance directly -- there is no `Primitive` impl for + /// it to go through `write`, since it has nowhere in `PrimitiveData` to + /// put a per-instance entry. `texture_idx` is the slot the bind group at + /// draw time is chosen from, carried in the otherwise-unused `idx` field. + pub fn write_image( + &mut self, + layer: usize, + id: WidgetId, + texture_idx: u32, + region: UiRegion, + mask_idx: MaskIdx, + ) -> PrimitiveHandle { + self.updated = true; + let inst = PrimitiveInstance { + region, + idx: texture_idx, + mask_idx, + binding: IMAGE_BINDING, + }; + let inst_i = if let Some(i) = self.image_free.pop() { + self.images[i] = inst; + self.image_assoc[i] = id; + i + } else { + let i = self.images.len(); + self.images.push(inst); + self.image_assoc.push(id); + i + }; + PrimitiveHandle { + layer, + inst_idx: inst_i, + data_idx: 0, + binding: IMAGE_BINDING, + } + } + + pub fn image_instances(&self) -> &Vec { + &self.images + } + + /// returns (old index, new index) for both lists this layer keeps -- + /// `PrimitiveChange::is_image` says which, since the two have separate + /// index spaces and `old`/`new` alone would collide between them. + /// + /// Both lists free with `swap_remove`, so a layer's draw order was + /// already undefined before images existed: nothing here may assume one + /// primitive stays adjacent to another once anything in the layer has + /// been freed. + pub fn apply_free(&mut self) -> Vec { + let mut changes = + Self::apply_free_list(&mut self.free, &mut self.instances, &mut self.assoc, false); + changes.extend(Self::apply_free_list( + &mut self.image_free, + &mut self.images, + &mut self.image_assoc, + true, + )); + changes + } + + fn apply_free_list( + free: &mut Vec, + instances: &mut Vec, + assoc: &mut Vec, + is_image: bool, + ) -> Vec { + free.sort_by(|a, b| b.cmp(a)); + free.drain(..) + .filter_map(|i| { + instances.swap_remove(i); + assoc.swap_remove(i); + if i == instances.len() { + return None; + } + let id = assoc[i]; + let old = instances.len(); + Some(PrimitiveChange { + id, + is_image, + old, + new: i, + }) + }) + .collect() } pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { self.updated = true; - self.data.free(h.binding, h.data_idx); - self.free.push(h.inst_idx); - self.instances[h.inst_idx].mask_idx + if h.binding == IMAGE_BINDING { + self.image_free.push(h.inst_idx); + self.images[h.inst_idx].mask_idx + } else { + self.data.free(h.binding, h.data_idx); + self.free.push(h.inst_idx); + self.instances[h.inst_idx].mask_idx + } } pub fn data(&self) -> &PrimitiveData { @@ -170,12 +276,21 @@ impl Primitives { pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion { self.updated = true; - &mut self.instances[h.inst_idx].region + if h.binding == IMAGE_BINDING { + &mut self.images[h.inst_idx].region + } else { + &mut self.instances[h.inst_idx].region + } } } pub struct PrimitiveChange { pub id: WidgetId, + /// Which of `Primitives::instances`/`Primitives::images` this change + /// belongs to -- their `old`/`new` indices are independent, so a + /// consumer matching only on `(layer, inst_idx)` could apply an image's + /// renumbering to a rect's handle that happens to share the same index. + pub is_image: bool, pub old: usize, pub new: usize, } @@ -201,7 +316,6 @@ impl PrimitiveHandle { primitives!( rects: RectPrimitive => 0, - textures: TexturePrimitive => 1, glyphs: GlyphPrimitive => 2, ); @@ -225,22 +339,42 @@ impl RectPrimitive { } } -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct TexturePrimitive { - pub view_idx: u32, - pub sampler_idx: u32, -} - +/// One glyph, drawn as a sub-rectangle of the glyph atlas array. +/// +/// `color` is the text colour and is multiplied by the atlas's alpha for an +/// ordinary mask glyph; a colour glyph (emoji) carries its own colour and +/// takes the atlas texel unchanged, which is what `GlyphEntry::IS_COLORED` +/// selects. #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct GlyphPrimitive { pub uv_min: Vec2, pub uv_max: Vec2, - pub view_idx: u32, - pub sampler_idx: u32, + /// Layer of the shared atlas array texture this glyph's page occupies -- + /// not a bind-group or view index, since a page never gets one of its own. + pub layer: u32, pub color: Color, pub flags: u32, + /// Pads this struct's Rust size to match WGSL's storage-buffer layout for + /// `GlyphInfo`: two `vec2` members give the struct an 8-byte + /// alignment, which rounds the WGSL size up to 32 bytes even though the + /// fields above only total 28. `bytemuck` does not check this for us. + _pad: u32, +} + +impl GlyphPrimitive { + /// The only constructor, since `_pad` is private: callers outside this + /// module cannot write the struct literal. + pub fn new(uv_min: Vec2, uv_max: Vec2, layer: u32, color: Color, flags: u32) -> Self { + Self { + uv_min, + uv_max, + layer, + color, + flags, + _pad: 0, + } + } } pub struct PrimitiveVec { diff --git a/core/src/render/shader.wgsl b/core/src/render/shader.wgsl index 1e6df68..37eaa2a 100644 --- a/core/src/render/shader.wgsl +++ b/core/src/render/shader.wgsl @@ -1,4 +1,7 @@ const RECT: u32 = 0u; +// TEXTURE has no entry in group 1: a standalone image draws with its own +// bind group (see UiRenderNode::draw), so there is nothing per-instance left +// to look up here -- the bind group already picked the texture. const TEXTURE: u32 = 1u; const GLYPH: u32 = 2u; @@ -6,8 +9,6 @@ const GLYPH: u32 = 2u; var window: WindowUniform; @group(1) @binding(RECT) var rects: array; -@group(1) @binding(TEXTURE) -var textures: array; @group(1) @binding(GLYPH) var glyphs: array; @@ -18,16 +19,12 @@ struct Rect { inner_radius: f32, } -struct TextureInfo { - view_idx: u32, - sampler_idx: u32, -} - struct GlyphInfo { uv_min: vec2, uv_max: vec2, - view_idx: u32, - sampler_idx: u32, + // Layer of the shared atlas array texture, not a view or bind-group + // index -- a page never gets its own bind group. + layer: u32, color: u32, flags: u32, } @@ -52,11 +49,21 @@ struct UiVec2 { abs: vec2, } +// The shared glyph atlas: every page is one layer. Growing it recreates this +// texture with headroom and copies the old layers across -- see +// GpuTextures::grow_array -- rather than the binding_array> +// this replaced, which needed VK_EXT_descriptor_indexing and does not survive +// a real share of Android GPUs. @group(2) @binding(0) -var views: binding_array>; +var atlas: texture_2d_array; +// One standalone image's texture. The main draw (rects and glyphs) binds a +// 1x1 null texture here, since neither samples it; each image draw call +// binds its own -- see UiRenderNode::draw. @group(2) @binding(1) -var samplers: binding_array; +var image_texture: texture_2d; @group(2) @binding(2) +var samp: sampler; +@group(2) @binding(3) var masks: array; struct WindowUniform { @@ -135,7 +142,7 @@ fn fs_main( color = draw_rounded_rect(region, rects[i]); } case TEXTURE: { - color = draw_texture(region, textures[i]); + color = draw_texture(region); } case GLYPH: { color = draw_glyph(region, glyphs[i]); @@ -158,14 +165,13 @@ fn fs_main( return color; } -// TODO: this seems really inefficient (per frag indexing)? -fn draw_texture(region: Region, info: TextureInfo) -> vec4 { - return textureSample(views[info.view_idx], samplers[info.sampler_idx], region.uv); +fn draw_texture(region: Region) -> vec4 { + return textureSample(image_texture, samp, region.uv); } fn draw_glyph(region: Region, g: GlyphInfo) -> vec4 { let uv = mix(g.uv_min, g.uv_max, region.uv); - let texel = textureSample(views[g.view_idx], samplers[g.sampler_idx], uv); + let texel = textureSample(atlas, samp, uv, i32(g.layer)); if (g.flags & 1u) != 0u { return texel; } diff --git a/core/src/render/texture.rs b/core/src/render/texture.rs index b7af152..7383de8 100644 --- a/core/src/render/texture.rs +++ b/core/src/render/texture.rs @@ -1,78 +1,163 @@ use image::{DynamicImage, EncodableLayout, GenericImageView}; use wgpu::{util::DeviceExt, *}; -use crate::{PatchRect, TextureUpdate, Textures}; +use crate::{Mask, PatchRect, TextureKind, TextureUpdate, Textures, render::util::ArrBuf}; +use super::atlas::PAGE; + +/// What one texture slot is, GPU-side. Parallel to `Textures`' own slot +/// numbering (`TextureKind`'s `Image`/`Page`), so a slot's index means the +/// same thing on both sides without a second map to keep in sync. +enum Slot { + /// A slot that was freed, or pushed and freed within the same batch + /// before ever reaching here. + Empty, + Image(ImageGpu), + /// The array layer a page occupies. Pages are never freed (see + /// `Textures::free`), so this is the only variant that outlives a `Free`. + Page(u32), +} + +struct ImageGpu { + /// Kept because a masks or atlas-array rebuild has to build a new bind + /// group from it. The `Texture` it came from is not kept: a `TextureView` + /// holds its own reference to that, so the image survives without one. + view: TextureView, + bind_group: BindGroup, +} + +/// Owns the two kinds of texture iris draws: +/// +/// - **The glyph atlas**, one `texture_2d_array` whose layers are pages +/// (`Slot::Page`), grown by recreating the array with headroom and +/// `copy_texture_to_texture`-ing the old layers across. No feature beyond +/// Vulkan 1.0/GLES sampling is needed for this -- a layer index is an +/// ordinary sampling operand. +/// - **Standalone images** (`Slot::Image`), each its own `Texture` and +/// `BindGroup`, drawn one `draw()` call at a time with that bind group +/// bound -- see `UiRenderNode::draw`. +/// +/// This replaced one giant `binding_array>`, which needed +/// `VK_EXT_descriptor_indexing` -- an extension a real share of Android GPUs +/// lack, so the old shape did not run there at all. pub struct GpuTextures { device: Device, queue: Queue, - /// Parallel to `views`; patches require textures rather than views. - textures: Vec>, - views: Vec, - view_count: usize, - samplers: Vec, + + slots: Vec, + + array_texture: Texture, + array_view: TextureView, + array_capacity: u32, + /// Layers actually written. Only grows -- see `Slot::Page`. + page_count: u32, + + sampler: Sampler, + /// Bound in the image slot of the main draw's bind group, which has + /// nothing of its own to put there: rects and glyphs never sample it, + /// but the layout requires something bound regardless. null_view: TextureView, - no_views: Vec, } impl GpuTextures { - pub fn update(&mut self, textures: &mut Textures) -> bool { - let mut bindings_changed = false; - for update in textures.updates() { - bindings_changed |= match update { - TextureUpdate::Push(image) => { - self.push(image); - true - } - TextureUpdate::Set(i, image) => { - self.set(i, image); - true - } - TextureUpdate::Patch(i, rect, image) => { - self.patch(i, rect, image); - false - } - TextureUpdate::SetFree => { - self.view_count += 1; - true - } - TextureUpdate::Free(i) => { - self.free(i); - true - } - TextureUpdate::PushFree => { - self.push_free(); - true - } - }; + /// Applies queued `Textures` updates, then reports whether the *main* + /// bind group (the one rects and glyphs draw with) needs rebuilding -- + /// true when the atlas array was recreated (its view identity changed) + /// or the masks buffer was, since both are bound there. Pushing or + /// freeing a standalone image never touches that group: it built or drops + /// its own. + pub fn update( + &mut self, + textures: &mut Textures, + rsc_layout: &BindGroupLayout, + masks: &ArrBuf, + masks_resized: bool, + ) -> bool { + let mut rebuild_main = masks_resized; + if masks_resized { + // The masks buffer just moved, so every bind group holding a + // reference to it -- one per live standalone image -- is stale. + self.rebuild_image_bind_groups(rsc_layout, masks); } - bindings_changed + for update in textures.updates() { + match update { + TextureUpdate::Push(kind, image) => { + rebuild_main |= self.push(kind, image, rsc_layout, masks); + } + TextureUpdate::Set(kind, i, image) => { + rebuild_main |= self.set(kind, i, image, rsc_layout, masks); + } + // A patch changes texture contents, not which layer or bind + // group exists, so it never asks for a rebuild -- rebuilding + // per glyph is exactly the cost this exists to avoid. + TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image), + TextureUpdate::SetFree => {} + TextureUpdate::Free(i) => self.free(i), + TextureUpdate::PushFree(_kind) => self.slots.push(Slot::Empty), + } + } + rebuild_main } - fn set(&mut self, i: u32, image: &DynamicImage) { - self.view_count += 1; - let (texture, view) = self.create(image); - self.textures[i as usize] = Some(texture); - self.views[i as usize] = view; + + fn push( + &mut self, + kind: TextureKind, + image: &DynamicImage, + rsc_layout: &BindGroupLayout, + masks: &ArrBuf, + ) -> bool { + let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout, masks); + self.slots.push(slot); + rebuilt } + + fn set( + &mut self, + kind: TextureKind, + i: u32, + image: &DynamicImage, + rsc_layout: &BindGroupLayout, + masks: &ArrBuf, + ) -> bool { + let (slot, rebuilt) = self.make_slot(kind, image, rsc_layout, masks); + self.slots[i as usize] = slot; + rebuilt + } + + fn make_slot( + &mut self, + kind: TextureKind, + image: &DynamicImage, + rsc_layout: &BindGroupLayout, + masks: &ArrBuf, + ) -> (Slot, bool) { + match kind { + TextureKind::Image => { + let gpu = self.create_image(image, rsc_layout, masks); + (Slot::Image(gpu), false) + } + TextureKind::Page { layer } => { + let mut rebuilt = false; + if layer >= self.array_capacity { + self.grow_array(rsc_layout, masks); + rebuilt = true; + } + self.write_full_layer(layer, image); + self.page_count = self.page_count.max(layer + 1); + (Slot::Page(layer), rebuilt) + } + } + } + fn free(&mut self, i: u32) { - self.view_count -= 1; - self.textures[i as usize] = None; - self.views[i as usize] = self.null_view.clone(); - } - fn push(&mut self, image: &DynamicImage) { - self.view_count += 1; - let (texture, view) = self.create(image); - self.textures.push(Some(texture)); - self.views.push(view); - } - fn push_free(&mut self) { - self.view_count += 1; - self.textures.push(None); - self.views.push(self.null_view.clone()); + if let Some(slot) = self.slots.get_mut(i as usize) { + *slot = Slot::Empty; + } + // A page's layer is not reclaimed here either -- see `Slot::Page`. } fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) { - let Some(texture) = &self.textures[i as usize] else { + let Some(&Slot::Page(layer)) = self.slots.get(i as usize) else { return; }; if rect.width == 0 || rect.height == 0 { @@ -84,12 +169,12 @@ impl GpuTextures { .to_image(); self.queue.write_texture( TexelCopyTextureInfo { - texture, + texture: &self.array_texture, mip_level: 0, origin: Origin3d { x: rect.x, y: rect.y, - z: 0, + z: layer, }, aspect: TextureAspect::All, }, @@ -107,13 +192,105 @@ impl GpuTextures { ); } - fn create(&self, image: &DynamicImage) -> (Texture, TextureView) { - let image = image.to_rgba8(); - let (width, height) = image.dimensions(); + fn write_full_layer(&self, layer: u32, image: &DynamicImage) { + // Every page is created as exactly PAGE x PAGE (`GlyphAtlas::allocate`), + // so this is always a whole-layer write, never a crop. + let rgba = image.to_rgba8(); + self.queue.write_texture( + TexelCopyTextureInfo { + texture: &self.array_texture, + mip_level: 0, + origin: Origin3d { + x: 0, + y: 0, + z: layer, + }, + aspect: TextureAspect::All, + }, + rgba.as_bytes(), + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(PAGE * 4), + rows_per_image: Some(PAGE), + }, + Extent3d { + width: PAGE, + height: PAGE, + depth_or_array_layers: 1, + }, + ); + } + + /// Doubles the array's layer capacity (headroom, so this is rare) and + /// copies the old layers across GPU-side -- no readback. Recreates the + /// array's view, which invalidates every bind group that referenced it, + /// so this also rebuilds all of them before returning. + fn grow_array(&mut self, rsc_layout: &BindGroupLayout, masks: &ArrBuf) { + let new_capacity = self.array_capacity * 2; + let new_texture = Self::create_array_texture(&self.device, new_capacity); + if self.page_count > 0 { + let mut encoder = self + .device + .create_command_encoder(&CommandEncoderDescriptor { + label: Some("atlas array grow"), + }); + encoder.copy_texture_to_texture( + TexelCopyTextureInfo { + texture: &self.array_texture, + mip_level: 0, + origin: Origin3d::ZERO, + aspect: TextureAspect::All, + }, + TexelCopyTextureInfo { + texture: &new_texture, + mip_level: 0, + origin: Origin3d::ZERO, + aspect: TextureAspect::All, + }, + Extent3d { + width: PAGE, + height: PAGE, + depth_or_array_layers: self.page_count, + }, + ); + self.queue.submit(std::iter::once(encoder.finish())); + } + self.array_texture = new_texture; + self.array_view = self.array_texture.create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }); + self.array_capacity = new_capacity; + self.rebuild_image_bind_groups(rsc_layout, masks); + } + + fn rebuild_image_bind_groups(&mut self, rsc_layout: &BindGroupLayout, masks: &ArrBuf) { + for slot in &mut self.slots { + if let Slot::Image(gpu) = slot { + gpu.bind_group = Self::make_image_bind_group( + &self.device, + rsc_layout, + &self.array_view, + &gpu.view, + &self.sampler, + masks, + ); + } + } + } + + fn create_image( + &self, + image: &DynamicImage, + rsc_layout: &BindGroupLayout, + masks: &ArrBuf, + ) -> ImageGpu { + let rgba = image.to_rgba8(); + let (width, height) = rgba.dimensions(); let texture = self.device.create_texture_with_data( &self.queue, &TextureDescriptor { - label: None, + label: Some("image"), size: Extent3d { width, height, @@ -127,43 +304,134 @@ impl GpuTextures { view_formats: &[], }, wgt::TextureDataOrder::MipMajor, - image.as_bytes(), + rgba.as_bytes(), ); let view = texture.create_view(&TextureViewDescriptor::default()); - (texture, view) + let bind_group = Self::make_image_bind_group( + &self.device, + rsc_layout, + &self.array_view, + &view, + &self.sampler, + masks, + ); + ImageGpu { view, bind_group } + } + + /// Builds group 2 for one standalone image: the shared atlas array, this + /// image's own view, the shared sampler, and the shared masks buffer -- + /// the same layout the main draw uses with a null view in the image slot. + fn make_image_bind_group( + device: &Device, + rsc_layout: &BindGroupLayout, + array_view: &TextureView, + image_view: &TextureView, + sampler: &Sampler, + masks: &ArrBuf, + ) -> BindGroup { + device.create_bind_group(&BindGroupDescriptor { + layout: rsc_layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(array_view), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::TextureView(image_view), + }, + BindGroupEntry { + binding: 2, + resource: BindingResource::Sampler(sampler), + }, + BindGroupEntry { + binding: 3, + resource: masks.buffer.as_entire_binding(), + }, + ], + label: Some("ui rsc image"), + }) + } + + fn create_array_texture(device: &Device, capacity: u32) -> Texture { + device.create_texture(&TextureDescriptor { + label: Some("glyph atlas array"), + size: Extent3d { + width: PAGE, + height: PAGE, + depth_or_array_layers: capacity, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8Unorm, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_DST + | TextureUsages::COPY_SRC, + view_formats: &[], + }) } pub fn new(device: &Device, queue: &Queue) -> Self { + let sampler = default_sampler(device); let null_view = null_texture_view(device); + let array_capacity = 1; + let array_texture = Self::create_array_texture(device, array_capacity); + let array_view = array_texture.create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }); Self { device: device.clone(), queue: queue.clone(), - textures: Vec::new(), - views: Vec::new(), - samplers: vec![default_sampler(device)], - no_views: vec![null_view.clone()], + slots: Vec::new(), + array_texture, + array_view, + array_capacity, + page_count: 0, + sampler, null_view, - view_count: 0, } } - pub fn views(&self) -> Vec<&TextureView> { - if self.views.is_empty() { - &self.no_views - } else { - &self.views - } - .iter() - .by_ref() - .collect() + pub fn array_view(&self) -> &TextureView { + &self.array_view } - pub fn samplers(&self) -> Vec<&Sampler> { - self.samplers.iter().by_ref().collect() + pub fn null_view(&self) -> &TextureView { + &self.null_view + } + + pub fn sampler(&self) -> &Sampler { + &self.sampler + } + + /// The bind group a standalone image draws with. Panics if `idx` names an + /// atlas page or a freed slot instead -- either is a caller bug (the + /// wrong kind of instance reached this draw path), not a condition to + /// recover from. + pub fn image_bind_group(&self, idx: u32) -> &BindGroup { + match self.slots.get(idx as usize) { + Some(Slot::Image(gpu)) => &gpu.bind_group, + other => panic!("texture slot {idx} is not a live standalone image: {other:?}"), + } } pub fn view_count(&self) -> usize { - self.view_count + self.slots + .iter() + .filter(|s| !matches!(s, Slot::Empty)) + .count() + } +} + +impl std::fmt::Debug for Slot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Slot::Empty => write!(f, "Empty"), + Slot::Image(_) => write!(f, "Image"), + Slot::Page(layer) => write!(f, "Page(layer={layer})"), + } } } diff --git a/core/src/render/util/mod.rs b/core/src/render/util/mod.rs index c9d48ff..d4faf54 100644 --- a/core/src/render/util/mod.rs +++ b/core/src/render/util/mod.rs @@ -21,13 +21,18 @@ impl ArrBuf { _pd: PhantomData, } } - pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) { - if self.len != data.len() { + /// Returns whether the underlying `Buffer` was recreated -- a caller that + /// cached a `BindGroup` referencing it (as `GpuTextures` does for the + /// masks buffer) needs to know to rebuild that too. + pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool { + let resized = self.len != data.len(); + if resized { self.len = data.len(); self.buffer = Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label); } queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data)); + resized } fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer { let mut size = size as u64; diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 052b049..a26569c 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -77,17 +77,31 @@ impl<'a> Painter<'a> { pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) { self.textures.push(handle.clone()); - self.primitive_at(handle.primitive(), region.within(&self.region)); + self.write_image(handle.image_index(), region.within(&self.region)); } pub fn texture(&mut self, handle: &TextureHandle) { self.textures.push(handle.clone()); - self.primitive(handle.primitive()); + self.write_image(handle.image_index(), self.region); } pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) { self.textures.push(handle.clone()); - self.primitive_at(handle.primitive(), region); + self.write_image(handle.image_index(), region); + } + + /// A standalone image draws with its own bind group rather than sharing + /// the layer's one instanced draw, so it goes through + /// `Primitives::write_image` instead of `primitive_at`/`Primitive::vec`. + fn write_image(&mut self, texture_idx: u32, region: UiRegion) { + let h = self + .state + .layers + .write_image(self.layer, self.id, texture_idx, region, self.mask); + if self.mask != MaskIdx::NONE { + self.rsc.ui_mut().masks.push_ref(self.mask); + } + self.primitives.push(h); } pub fn render_text( @@ -109,14 +123,13 @@ impl<'a> Painter<'a> { region.x.end = region.x.start + UiScalar::abs(glyph.entry.width as f32); region.y.end = region.y.start + UiScalar::abs(glyph.entry.height as f32); self.primitive_at( - GlyphPrimitive { - uv_min: glyph.entry.uv_min, - uv_max: glyph.entry.uv_max, - view_idx: glyph.entry.view_idx, - sampler_idx: glyph.entry.sampler_idx, - color: text.color, - flags: glyph.entry.flags(), - }, + GlyphPrimitive::new( + glyph.entry.uv_min, + glyph.entry.uv_max, + glyph.entry.layer, + text.color, + glyph.entry.flags(), + ), region, ); } diff --git a/src/default/render.rs b/src/default/render.rs index 5fbaf33..f7c1239 100644 --- a/src/default/render.rs +++ b/src/default/render.rs @@ -1,4 +1,4 @@ -use iris_core::{UiData, UiLimits, UiRenderNode, UiRenderState}; +use iris_core::{UiData, UiRenderNode, UiRenderState}; use pollster::FutureExt; use std::sync::Arc; use wgpu::*; @@ -83,18 +83,15 @@ impl UiRenderer { .block_on() .expect("Could not get adapter!"); - let ui_limits = UiLimits::default(); - + // No features beyond what wgpu asks for by default, and no + // binding-array limits: the atlas is one texture_2d_array and a + // standalone image is its own ordinary bind group, neither of which + // needs descriptor indexing. The binding array this replaced asked + // for VK_EXT_descriptor_indexing unconditionally and so did not run + // on a real share of Android GPUs. let (device, queue) = adapter .request_device(&DeviceDescriptor { - required_features: Features::TEXTURE_BINDING_ARRAY - | Features::PARTIALLY_BOUND_BINDING_ARRAY - | Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, required_limits: Limits { - max_binding_array_elements_per_shader_stage: ui_limits - .max_binding_array_elements_per_shader_stage(), - max_binding_array_sampler_elements_per_shader_stage: ui_limits - .max_binding_array_sampler_elements_per_shader_stage(), max_buffer_size: 1 << 30, ..Default::default() }, @@ -126,7 +123,7 @@ impl UiRenderer { let encoder = Self::create_encoder(&device); - let ui = UiRenderNode::new(&device, &queue, &config, ui_limits); + let ui = UiRenderNode::new(&device, &queue, &config); Self { surface,