diff --git a/core/src/primitive/layer.rs b/core/src/primitive/layer.rs index 5f62efd..53d05ff 100644 --- a/core/src/primitive/layer.rs +++ b/core/src/primitive/layer.rs @@ -133,15 +133,15 @@ impl PrimitiveLayers { self[h.layer].free(h) } - pub fn write_image( + pub fn write_texture( &mut self, layer: LayerId, id: WidgetId, - texture_idx: u32, + texture: u32, region: UiRegion, mask_idx: MaskIdx, ) -> PrimitiveHandle { - self[layer].write_image(layer, id, texture_idx, region, mask_idx) + self[layer].write_texture(layer, id, texture, region, mask_idx) } } diff --git a/core/src/primitive/text.rs b/core/src/primitive/text.rs index a077ada..11284f6 100644 --- a/core/src/primitive/text.rs +++ b/core/src/primitive/text.rs @@ -1,6 +1,5 @@ use crate::{ - Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, - util::Vec2, + Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2, }; use parley::{ Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, @@ -159,7 +158,7 @@ impl TextBuffer { } impl TextData { - pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec { + pub fn place(&mut self, buffer: &TextBuffer) -> Vec { let mut placed = Vec::new(); for line in buffer.layout.lines() { for item in line.items() { @@ -185,17 +184,14 @@ impl TextData { subpixel, coords: coords_hash, }; - let Some(entry) = self.glyph_entry( - GlyphRaster { - key, - font: font_ref, - font_size, - coords, - subpixel, - glyph_id: glyph.id, - }, - textures, - ) else { + let Some(entry) = self.glyph_entry(GlyphRaster { + key, + font: font_ref, + font_size, + coords, + subpixel, + glyph_id: glyph.id, + }) else { continue; }; placed.push(PlacedGlyph { @@ -211,11 +207,7 @@ impl TextData { placed } - fn glyph_entry( - &mut self, - glyph: GlyphRaster<'_>, - textures: &mut Textures, - ) -> Option { + fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option { if let Some(entry) = self.atlas.get(&glyph.key) { return entry; } @@ -237,7 +229,7 @@ impl TextData { .render(&mut scaler, glyph.glyph_id as u16); if let Some(image) = image { - self.atlas.insert(glyph.key, &image, textures) + self.atlas.insert(glyph.key, &image) } else { self.atlas.insert_empty(glyph.key); None @@ -278,10 +270,9 @@ impl TextData { buffer: &mut TextBuffer, attrs: &TextAttrs, width: Option, - textures: &mut Textures, ) -> RenderedText { buffer.shape(self, attrs, width); - let glyphs = self.place(buffer, textures); + let glyphs = self.place(buffer); RenderedText { glyphs, size: buffer.size(), diff --git a/core/src/primitive/texture.rs b/core/src/primitive/texture.rs index df5635b..b85930b 100644 --- a/core/src/primitive/texture.rs +++ b/core/src/primitive/texture.rs @@ -5,28 +5,12 @@ use std::{ 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 { slot: u32, - kind: TextureKind, size: Vec2, counter: RefCounter, - send: Sender<(TextureKind, u32)>, + send: Sender, } /// a texture manager for a ui @@ -34,24 +18,19 @@ 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<(TextureKind, u32)>, - recv: Receiver<(TextureKind, u32)>, + send: Sender, + recv: Receiver, } pub enum TextureUpdate<'a> { - 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. + Push(&'a DynamicImage), + Set(u32, &'a DynamicImage), Patch(u32, PatchRect, &'a DynamicImage), Free(u32), - PushFree(TextureKind), + /// Added and freed before the renderer drained either update. It still has + /// to push a slot to stay lined up with `images`; `Free` then empties it. + PushFree, SetFree, } @@ -64,8 +43,8 @@ pub struct PatchRect { } enum Update { - Push(TextureKind, u32), - Set(TextureKind, u32), + Push(u32), + Set(u32), Patch(u32, PatchRect), Free(u32), } @@ -76,54 +55,31 @@ 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 kind = TextureKind::Image; - let slot = self.push(kind, image); TextureHandle { - slot, - kind, + slot: self.push(image), size, counter: RefCounter::new(), send: self.send.clone(), } } - /// 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 { + fn push(&mut self, image: DynamicImage) -> u32 { if let Some(i) = self.free.pop() { self.images[i as usize] = Some(image); - self.updates.push(Update::Set(kind, i)); + self.updates.push(Update::Set(i)); i } else { let i = self.images.len() as u32; self.images.push(Some(image)); - self.updates.push(Update::Push(kind, i)); + self.updates.push(Update::Push(i)); i } } @@ -140,29 +96,22 @@ impl Textures { } pub fn free(&mut self) { - for (kind, idx) in self.recv.try_iter() { + for idx in self.recv.try_iter() { self.images[idx as usize] = None; self.updates.push(Update::Free(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); - } + self.free.push(idx); } } pub fn updates(&mut self) -> impl Iterator> { self.updates.drain(..).map(|u| match u { - Update::Push(kind, i) => self.images[i as usize] + Update::Push(i) => self.images[i as usize] .as_ref() - .map(|img| TextureUpdate::Push(kind, img)) - .unwrap_or(TextureUpdate::PushFree(kind)), - Update::Set(kind, i) => self.images[i as usize] + .map(TextureUpdate::Push) + .unwrap_or(TextureUpdate::PushFree), + Update::Set(i) => self.images[i as usize] .as_ref() - .map(|img| TextureUpdate::Set(kind, i, img)) + .map(|img| TextureUpdate::Set(i, img)) .unwrap_or(TextureUpdate::SetFree), Update::Patch(i, rect) => self.images[i as usize] .as_ref() @@ -174,36 +123,19 @@ impl Textures { } impl TextureHandle { + /// Index into `Textures`, and into the renderer's parallel slots. + pub fn slot(&self) -> u32 { + self.slot + } 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.kind, self.slot)); + let _ = self.send.send(self.slot); } } } diff --git a/core/src/render/atlas.rs b/core/src/render/atlas.rs index aa1b235..a860a64 100644 --- a/core/src/render/atlas.rs +++ b/core/src/render/atlas.rs @@ -1,15 +1,11 @@ use crate::{ - PatchRect, TextureHandle, Textures, + PatchRect, util::{HashMap, Vec2}, }; use image::RgbaImage; use swash::scale::image::{Content, Image}; -/// 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. +/// Side of one page, and so of every layer of `render::page`'s array texture. pub(crate) const PAGE: u32 = 1024; /// Transparent margin kept around every glyph, so that sampling one cannot @@ -40,7 +36,7 @@ pub struct GlyphEntry { pub width: u32, pub height: u32, pub is_colored: bool, - /// The atlas array layer this glyph's page occupies. + /// Which atlas array layer this glyph is on. pub layer: u32, } @@ -53,18 +49,26 @@ impl GlyphEntry { } struct Page { - handle: TextureHandle, + image: RgbaImage, x: u32, y: u32, shelf_height: u32, } +/// A rectangle of one page the renderer has not uploaded yet. +#[derive(Clone, Copy)] +pub struct PageUpload { + pub layer: u32, + pub rect: PatchRect, +} + #[derive(Default)] pub struct GlyphAtlas { pages: Vec, /// `None` for a glyph that rasterised to nothing -- a space, say. Cached /// too, so it is not re-rasterised on every layout. entries: HashMap>, + uploads: Vec, } impl GlyphAtlas { @@ -72,12 +76,7 @@ impl GlyphAtlas { self.entries.get(key).copied() } - pub fn insert( - &mut self, - key: GlyphKey, - image: &Image, - textures: &mut Textures, - ) -> Option { + pub fn insert(&mut self, key: GlyphKey, image: &Image) -> Option { let w = image.placement.width; let h = image.placement.height; if w == 0 || h == 0 { @@ -99,23 +98,18 @@ impl GlyphAtlas { return None; } - let (page_idx, x, y) = self.allocate(w, h, textures); - let page = &self.pages[page_idx]; + let (layer, x, y) = self.allocate(w, h); + write_glyph(&mut self.pages[layer as usize].image, image, x, y); + self.uploads.push(PageUpload { + layer, + rect: PatchRect { + x, + y, + width: w, + height: h, + }, + }); - let img = textures.image_mut(&page.handle); - let rgba = img.as_mut_rgba8().expect("atlas page is rgba8"); - write_glyph(rgba, image, x, y); - - let handle = page.handle.clone(); - let rect = PatchRect { - x, - y, - width: w, - height: h, - }; - textures.patch(&handle, rect); - - let page = &self.pages[page_idx]; let scale = 1.0 / PAGE as f32; let entry = GlyphEntry { uv_min: Vec2::new(x as f32 * scale, y as f32 * scale), @@ -125,38 +119,47 @@ impl GlyphAtlas { width: w, height: h, is_colored: matches!(image.content, Content::Color), - layer: page.handle.layer(), + layer, }; self.entries.insert(key, Some(entry)); Some(entry) } - fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) { + fn allocate(&mut self, w: u32, h: u32) -> (u32, u32, u32) { if let Some((i, (x, y))) = self .pages .iter_mut() .enumerate() .find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position))) { - return (i, x, y); + return (i as u32, x, y); } - let handle = textures.add_page(RgbaImage::new(PAGE, PAGE)); self.pages.push(Page { - handle, + image: RgbaImage::new(PAGE, PAGE), x: PAD + w + PAD, y: PAD, shelf_height: h + PAD, }); - (self.pages.len() - 1, PAD, PAD) + (self.pages.len() as u32 - 1, PAD, PAD) + } + + /// Drains what has been written since the last call, for the renderer to + /// upload. Nothing else is needed for a new page: wgpu leaves the rest of + /// a fresh layer transparent, which is what an atlas wants. + pub fn uploads(&mut self) -> impl Iterator { + let pages = &self.pages; + self.uploads + .drain(..) + .map(|upload| (upload, &pages[upload.layer as usize].image)) } pub fn insert_empty(&mut self, key: GlyphKey) { self.entries.insert(key, None); } - pub fn page_count(&self) -> usize { - self.pages.len() + pub fn page_count(&self) -> u32 { + self.pages.len() as u32 } pub fn glyph_count(&self) -> usize { diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index ff54d65..f8f038d 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -1,6 +1,13 @@ +use std::ops::Range; + use crate::{ UiData, UiRenderState, - render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf}, + render::{ + data::PrimitiveInstance, + page::GpuPages, + texture::{GpuTextures, default_sampler}, + util::ArrBuf, + }, util::{HashMap, Vec2}, }; use data::WindowUniform; @@ -11,6 +18,7 @@ use wgpu::{ mod atlas; mod data; +mod page; mod primitive; mod texture; mod util; @@ -24,14 +32,17 @@ const SHAPE_SHADER: &str = include_str!("./shader.wgsl"); pub struct UiRenderNode { uniform_group: BindGroup, primitive_layout: BindGroupLayout, - rsc_layout: BindGroupLayout, - rsc_group: BindGroup, + texture_layout: BindGroupLayout, + mask_layout: BindGroupLayout, + mask_group: BindGroup, pipeline: RenderPipeline, layers: HashMap, active: Vec, window_buffer: Buffer, + sampler: Sampler, + pages: GpuPages, textures: GpuTextures, masks: ArrBuf, } @@ -40,43 +51,38 @@ 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, + draws: Vec, +} + +/// A run of consecutive instances sharing one group 2: `texture` names a +/// standalone image, or `None` is the glyph atlas rects and glyphs sample. +struct Draw { + texture: Option, + instances: Range, } 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(3, &self.mask_group, &[]); for i in &self.active { let layer = &self.layers[i]; - if layer.instance.len() == 0 && layer.image_instance.len() == 0 { + if layer.draws.is_empty() { continue; } pass.set_bind_group(1, &layer.primitive_group, &[]); - 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); - } + pass.set_vertex_buffer(0, layer.instance.buffer.slice(..)); + for draw in &layer.draws { + let group = match draw.texture { + Some(slot) => match self.textures.group(slot) { + Some(group) => group, + None => continue, + }, + None => self.pages.group(), + }; + pass.set_bind_group(2, group, &[]); + pass.draw(0..4, draw.instances.clone()); } } } @@ -94,15 +100,7 @@ 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 { - // `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 - { + if h.layer == i && h.inst_idx == change.old { h.inst_idx = change.new; break; } @@ -121,12 +119,7 @@ impl UiRenderNode { ), primitives, primitive_group, - image_instance: ArrBuf::new( - device, - BufferUsages::VERTEX | BufferUsages::COPY_DST, - "image instance", - ), - image_tex_indices: Vec::new(), + draws: Vec::new(), } }); if primitives.updated { @@ -139,32 +132,20 @@ 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(); + rlayer.plan_draws(primitives.instances()); primitives.updated = false; } } - let masks_resized = if ui.masks.changed { + if ui.masks.changed { ui.masks.changed = false; - 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); + if self.masks.update(device, queue, &ui.masks[..]) { + self.mask_group = Self::mask_group(device, &self.mask_layout, &self.masks); + } } + self.pages + .update(&mut ui.text.atlas, &self.texture_layout, &self.sampler); + self.textures + .update(&mut ui.textures, &self.texture_layout, &self.sampler); } pub fn resize(&mut self, size: impl Into, queue: &Queue) { @@ -222,19 +203,28 @@ impl UiRenderNode { label: Some("primitive"), }); - let tex_manager = GpuTextures::new(device, queue); let masks = ArrBuf::new( device, BufferUsages::STORAGE | BufferUsages::COPY_DST, "ui masks", ); - let rsc_layout = Self::rsc_layout(device); - let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks); + let texture_layout = Self::texture_layout(device); + let mask_layout = Self::mask_layout(device); + let mask_group = Self::mask_group(device, &mask_layout, &masks); + + let sampler = default_sampler(device); + let pages = GpuPages::new(device, queue, &texture_layout, &sampler); + let textures = GpuTextures::new(device, queue); let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { label: Some("UI Shape Pipeline Layout"), - bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout], + bind_group_layouts: &[ + &uniform_layout, + &primitive_layout, + &texture_layout, + &mask_layout, + ], immediate_size: 0, }); let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { @@ -278,13 +268,16 @@ impl UiRenderNode { Self { uniform_group, primitive_layout, - rsc_layout, - rsc_group, + texture_layout, + mask_layout, + mask_group, pipeline, window_buffer, layers: HashMap::default(), active: Vec::new(), - textures: tex_manager, + sampler, + pages, + textures, masks, } } @@ -319,12 +312,10 @@ impl UiRenderNode { }) } - /// 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 { + /// Group 2: the texture a run of instances samples, and the sampler. No + /// `count` on either entry -- plain Vulkan 1.0 / GLES sampling, unlike the + /// `binding_array` layout this replaced. + fn texture_layout(device: &Device) -> BindGroupLayout { device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[ BindGroupLayoutEntry { @@ -340,67 +331,65 @@ impl UiRenderNode { BindGroupLayoutEntry { binding: 1, visibility: ShaderStages::FRAGMENT, - 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, - min_binding_size: None, - }, - count: None, - }, ], - label: Some("ui rsc"), + label: Some("ui texture"), }) } - /// 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, - tex_manager: &GpuTextures, - masks: &ArrBuf, - ) -> BindGroup { + /// Group 3, apart from the textures so that resizing the masks buffer + /// leaves every texture group intact. + fn mask_layout(device: &Device) -> BindGroupLayout { + device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: Some("ui masks"), + }) + } + + fn mask_group(device: &Device, layout: &BindGroupLayout, masks: &ArrBuf) -> BindGroup { device.create_bind_group(&BindGroupDescriptor { layout, - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(tex_manager.array_view()), - }, - BindGroupEntry { - binding: 1, - 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(), - }, - ], - label: Some("ui rsc"), + entries: &[BindGroupEntry { + binding: 0, + resource: masks.buffer.as_entire_binding(), + }], + label: Some("ui masks"), }) } - pub fn view_count(&self) -> usize { - self.textures.view_count() + pub fn texture_count(&self) -> usize { + self.textures.count() + } +} + +impl RenderLayer { + /// Only a texture instance breaks a run, so a ui without images plans one + /// draw however many rects and glyphs it has. + fn plan_draws(&mut self, instances: &[PrimitiveInstance]) { + self.draws.clear(); + for (i, inst) in instances.iter().enumerate() { + let i = i as u32; + let texture = (inst.binding == TEXTURE_BINDING).then_some(inst.idx); + match self.draws.last_mut() { + Some(draw) if draw.texture.is_none() && texture.is_none() => { + draw.instances.end = i + 1; + } + _ => self.draws.push(Draw { + texture, + instances: i..i + 1, + }), + } + } } } diff --git a/core/src/render/page.rs b/core/src/render/page.rs new file mode 100644 index 0000000..4dacd3d --- /dev/null +++ b/core/src/render/page.rs @@ -0,0 +1,124 @@ +use image::EncodableLayout; +use wgpu::*; + +use crate::GlyphAtlas; + +use super::{ + atlas::PAGE, + texture::{array_view, texture_group}, +}; + +/// The glyph atlas on the GPU: one array texture whose layers are the pages +/// `GlyphAtlas` packs. +/// +/// An array rather than a `binding_array>` because a layer +/// index is ordinary Vulkan 1.0 / GLES sampling, while a binding array needs +/// `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack. +pub struct GpuPages { + device: Device, + queue: Queue, + texture: Texture, + group: BindGroup, +} + +impl GpuPages { + pub fn new( + device: &Device, + queue: &Queue, + layout: &BindGroupLayout, + sampler: &Sampler, + ) -> Self { + let texture = create_array(device, 1); + let group = texture_group(device, layout, &array_view(&texture), sampler); + Self { + device: device.clone(), + queue: queue.clone(), + texture, + group, + } + } + + pub fn update(&mut self, atlas: &mut GlyphAtlas, layout: &BindGroupLayout, sampler: &Sampler) { + if atlas.page_count() > self.texture.depth_or_array_layers() { + self.grow(atlas.page_count(), layout, sampler); + } + for (upload, page) in atlas.uploads() { + let rect = upload.rect; + // `write_texture` wants tightly packed rows; the page is wider. + let sub = + image::imageops::crop_imm(page, rect.x, rect.y, rect.width, rect.height).to_image(); + self.queue.write_texture( + TexelCopyTextureInfo { + texture: &self.texture, + mip_level: 0, + origin: Origin3d { + x: rect.x, + y: rect.y, + z: upload.layer, + }, + aspect: TextureAspect::All, + }, + sub.as_bytes(), + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(rect.width * 4), + rows_per_image: Some(rect.height), + }, + Extent3d { + width: rect.width, + height: rect.height, + depth_or_array_layers: 1, + }, + ); + } + } + + pub fn group(&self) -> &BindGroup { + &self.group + } + + /// Doubles until `needed` fits and copies the old layers across GPU side. + /// The new view invalidates the old group, so that is rebuilt here. + fn grow(&mut self, needed: u32, layout: &BindGroupLayout, sampler: &Sampler) { + let old = self.texture.depth_or_array_layers(); + let mut layers = old; + while layers < needed { + layers *= 2; + } + let texture = create_array(&self.device, layers); + let mut encoder = self + .device + .create_command_encoder(&CommandEncoderDescriptor { + label: Some("atlas grow"), + }); + encoder.copy_texture_to_texture( + self.texture.as_image_copy(), + texture.as_image_copy(), + Extent3d { + width: PAGE, + height: PAGE, + depth_or_array_layers: old, + }, + ); + self.queue.submit(std::iter::once(encoder.finish())); + self.group = texture_group(&self.device, layout, &array_view(&texture), sampler); + self.texture = texture; + } +} + +fn create_array(device: &Device, layers: u32) -> Texture { + device.create_texture(&TextureDescriptor { + label: Some("glyph atlas"), + size: Extent3d { + width: PAGE, + height: PAGE, + depth_or_array_layers: layers, + }, + 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: &[], + }) +} diff --git a/core/src/render/primitive.rs b/core/src/render/primitive.rs index 9e87b49..052f5bd 100644 --- a/core/src/render/primitive.rs +++ b/core/src/render/primitive.rs @@ -16,17 +16,6 @@ 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, } @@ -37,20 +26,14 @@ 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; +/// Not in `primitives!` and with no group-1 buffer: a texture instance's `idx` +/// names the texture to bind, so there is nothing to look up per-instance. +pub const TEXTURE_BINDING: u32 = 2; pub trait Primitive: Pod { const BINDING: u32; @@ -76,13 +59,6 @@ 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] { [ @@ -144,17 +120,52 @@ impl Primitives { region, mask_idx, }: PrimitiveInst

, + ) -> PrimitiveHandle { + let data_idx = P::vec(&mut self.data).add(primitive); + self.push( + layer, + id, + PrimitiveInstance { + region, + idx: data_idx as u32, + mask_idx, + binding: P::BINDING, + }, + Some(data_idx), + ) + } + + /// Writes an instance that samples `texture` instead of a group-1 buffer. + pub fn write_texture( + &mut self, + layer: usize, + id: WidgetId, + texture: u32, + region: UiRegion, + mask_idx: MaskIdx, + ) -> PrimitiveHandle { + self.push( + layer, + id, + PrimitiveInstance { + region, + idx: texture, + mask_idx, + binding: TEXTURE_BINDING, + }, + None, + ) + } + + fn push( + &mut self, + layer: usize, + id: WidgetId, + inst: PrimitiveInstance, + data_idx: Option, ) -> PrimitiveHandle { self.updated = true; - let vec = P::vec(&mut self.data); - let i = vec.add(primitive); - let inst = PrimitiveInstance { - region, - idx: i as u32, - mask_idx, - binding: P::BINDING, - }; - let inst_i = if let Some(i) = self.free.pop() { + let inst_idx = if let Some(i) = self.free.pop() { self.instances[i] = inst; self.assoc[i] = id; i @@ -164,106 +175,36 @@ impl Primitives { self.assoc.push(id); i }; - PrimitiveHandle::new::

(layer, inst_i, 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, + inst_idx, + data_idx, + binding: inst.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() + /// 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 }) + }) } pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { self.updated = true; - 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 + if let Some(i) = h.data_idx { + self.data.free(h.binding, i); } + self.free.push(h.inst_idx); + self.instances[h.inst_idx].mask_idx } pub fn data(&self) -> &PrimitiveData { @@ -276,21 +217,12 @@ impl Primitives { pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion { self.updated = true; - if h.binding == IMAGE_BINDING { - &mut self.images[h.inst_idx].region - } else { - &mut self.instances[h.inst_idx].region - } + &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, } @@ -299,24 +231,14 @@ pub struct PrimitiveChange { pub struct PrimitiveHandle { pub layer: usize, pub inst_idx: usize, - pub data_idx: usize, + /// `None` for a texture instance, which has no group-1 entry to free. + pub data_idx: Option, pub binding: u32, } -impl PrimitiveHandle { - fn new(layer: usize, inst_idx: usize, data_idx: usize) -> Self { - Self { - layer, - inst_idx, - data_idx, - binding: P::BINDING, - } - } -} - primitives!( rects: RectPrimitive => 0, - glyphs: GlyphPrimitive => 2, + glyphs: GlyphPrimitive => 1, ); #[repr(C)] @@ -339,42 +261,17 @@ impl RectPrimitive { } } -/// 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. +/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph +/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects. #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct GlyphPrimitive { pub uv_min: Vec2, pub uv_max: Vec2, - /// 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. + /// Which atlas array layer this glyph is on. 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 37eaa2a..e7fc859 100644 --- a/core/src/render/shader.wgsl +++ b/core/src/render/shader.wgsl @@ -1,9 +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; +const GLYPH: u32 = 1u; +// No group 1 entry: a texture instance's idx names the texture bound in group 2. +const TEXTURE: u32 = 2u; @group(0) @binding(0) var window: WindowUniform; @@ -19,11 +17,13 @@ struct Rect { inner_radius: f32, } +// Scalars rather than two vec2: a vec2 member would align the struct to +// 8 and pad it to 32 bytes, which GlyphPrimitive has no field to fill. struct GlyphInfo { - uv_min: vec2, - uv_max: vec2, - // Layer of the shared atlas array texture, not a view or bind-group - // index -- a page never gets its own bind group. + uv_min_x: f32, + uv_min_y: f32, + uv_max_x: f32, + uv_max_y: f32, layer: u32, color: u32, flags: u32, @@ -49,21 +49,13 @@ 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. +// Whatever this run of instances samples: the glyph atlas, whose layers are +// its pages, or one standalone image as an array of one. @group(2) @binding(0) -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. +var tex: texture_2d_array; @group(2) @binding(1) -var image_texture: texture_2d; -@group(2) @binding(2) var samp: sampler; -@group(2) @binding(3) +@group(3) @binding(0) var masks: array; struct WindowUniform { @@ -166,12 +158,14 @@ fn fs_main( } fn draw_texture(region: Region) -> vec4 { - return textureSample(image_texture, samp, region.uv); + return textureSample(tex, samp, region.uv, 0); } fn draw_glyph(region: Region, g: GlyphInfo) -> vec4 { - let uv = mix(g.uv_min, g.uv_max, region.uv); - let texel = textureSample(atlas, samp, uv, i32(g.layer)); + let uv_min = vec2(g.uv_min_x, g.uv_min_y); + let uv_max = vec2(g.uv_max_x, g.uv_max_y); + let uv = mix(uv_min, uv_max, region.uv); + let texel = textureSample(tex, 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 7383de8..b3c98ad 100644 --- a/core/src/render/texture.rs +++ b/core/src/render/texture.rs @@ -1,289 +1,65 @@ use image::{DynamicImage, EncodableLayout, GenericImageView}; use wgpu::{util::DeviceExt, *}; -use crate::{Mask, PatchRect, TextureKind, TextureUpdate, Textures, render::util::ArrBuf}; +use crate::{PatchRect, TextureUpdate, Textures}; -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. +/// The standalone images a ui draws, each its own texture and group 2 -- +/// unlike the glyph atlas in `super::page`, which is one array they share. pub struct GpuTextures { device: Device, queue: Queue, + slots: 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, +struct ImageGpu { + /// Kept for `patch`, which needs the texture rather than the view. + texture: Texture, + group: BindGroup, } impl GpuTextures { - /// 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); + pub fn new(device: &Device, queue: &Queue) -> Self { + Self { + device: device.clone(), + queue: queue.clone(), + slots: Vec::new(), } + } + + pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) { for update in textures.updates() { match update { - TextureUpdate::Push(kind, image) => { - rebuild_main |= self.push(kind, image, rsc_layout, masks); + TextureUpdate::Push(image) => { + let image = self.create(image, layout, sampler); + self.slots.push(Some(image)); } - TextureUpdate::Set(kind, i, image) => { - rebuild_main |= self.set(kind, i, image, rsc_layout, masks); + TextureUpdate::Set(i, image) => { + let image = self.create(image, layout, sampler); + self.slots[i as usize] = Some(image); } - // 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::PushFree => self.slots.push(None), TextureUpdate::SetFree => {} - TextureUpdate::Free(i) => self.free(i), - TextureUpdate::PushFree(_kind) => self.slots.push(Slot::Empty), - } - } - rebuild_main - } - - 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) + TextureUpdate::Free(i) => self.slots[i as usize] = None, } } } - fn free(&mut self, i: u32) { - 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`. + /// `None` once freed. A drawn instance holds a `TextureHandle`, so its + /// slot outlives it. + pub fn group(&self, slot: u32) -> Option<&BindGroup> { + self.slots.get(slot as usize)?.as_ref().map(|i| &i.group) } - fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) { - let Some(&Slot::Page(layer)) = self.slots.get(i as usize) else { - return; - }; - if rect.width == 0 || rect.height == 0 { - return; - } - // `write_texture` requires tightly packed rows, unlike the atlas image. - let sub = image - .view(rect.x, rect.y, rect.width, rect.height) - .to_image(); - self.queue.write_texture( - TexelCopyTextureInfo { - texture: &self.array_texture, - mip_level: 0, - origin: Origin3d { - x: rect.x, - y: rect.y, - z: layer, - }, - aspect: TextureAspect::All, - }, - sub.as_bytes(), - TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(rect.width * 4), - rows_per_image: Some(rect.height), - }, - Extent3d { - width: rect.width, - height: rect.height, - depth_or_array_layers: 1, - }, - ); + pub fn count(&self) -> usize { + self.slots.iter().flatten().count() } - 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( + fn create( &self, image: &DynamicImage, - rsc_layout: &BindGroupLayout, - masks: &ArrBuf, + layout: &BindGroupLayout, + sampler: &Sampler, ) -> ImageGpu { let rgba = image.to_rgba8(); let (width, height) = rgba.dimensions(); @@ -306,152 +82,77 @@ impl GpuTextures { wgt::TextureDataOrder::MipMajor, rgba.as_bytes(), ); - let view = texture.create_view(&TextureViewDescriptor::default()); - let bind_group = Self::make_image_bind_group( - &self.device, - rsc_layout, - &self.array_view, - &view, - &self.sampler, - masks, - ); - ImageGpu { view, bind_group } + let group = texture_group(&self.device, layout, &array_view(&texture), sampler); + ImageGpu { texture, 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), + fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) { + let Some(Some(slot)) = self.slots.get(i as usize) else { + return; + }; + if rect.width == 0 || rect.height == 0 { + return; + } + // `write_texture` requires tightly packed rows, unlike the source image. + let sub = image + .view(rect.x, rect.y, rect.width, rect.height) + .to_image(); + self.queue.write_texture( + TexelCopyTextureInfo { + texture: &slot.texture, + mip_level: 0, + origin: Origin3d { + x: rect.x, + y: rect.y, + z: 0, }, - 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, + aspect: TextureAspect::All, }, - 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(), - slots: Vec::new(), - array_texture, - array_view, - array_capacity, - page_count: 0, - sampler, - null_view, - } - } - - pub fn array_view(&self) -> &TextureView { - &self.array_view - } - - 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.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})"), - } - } -} - -pub fn null_texture_view(device: &Device) -> TextureView { - device - .create_texture(&TextureDescriptor { - label: Some("null"), - size: Extent3d { - width: 1, - height: 1, + sub.as_bytes(), + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(rect.width * 4), + rows_per_image: Some(rect.height), + }, + Extent3d { + width: rect.width, + height: rect.height, depth_or_array_layers: 1, }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::Rgba8Unorm, - usage: TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }) - .create_view(&TextureViewDescriptor::default()) + ); + } +} + +/// One array texture and one sampler, so the atlas and a standalone image bind +/// the same layout and the shader samples whichever is bound. An image is a +/// single-layer texture viewed as an array of one. +pub fn texture_group( + device: &Device, + layout: &BindGroupLayout, + view: &TextureView, + sampler: &Sampler, +) -> BindGroup { + device.create_bind_group(&BindGroupDescriptor { + layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(view), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(sampler), + }, + ], + label: Some("ui texture"), + }) +} + +pub fn array_view(texture: &Texture) -> TextureView { + texture.create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }) } pub fn default_sampler(device: &Device) -> Sampler { diff --git a/core/src/render/util/mod.rs b/core/src/render/util/mod.rs index d4faf54..ee3de03 100644 --- a/core/src/render/util/mod.rs +++ b/core/src/render/util/mod.rs @@ -21,9 +21,8 @@ impl ArrBuf { _pd: PhantomData, } } - /// 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. + /// Returns whether the `Buffer` was recreated, which stales any cached + /// `BindGroup` holding it. pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool { let resized = self.len != data.len(); if resized { @@ -46,8 +45,4 @@ impl ArrBuf { usage, }) } - #[allow(clippy::len_without_is_empty)] - pub fn len(&self) -> usize { - self.len - } } diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index a26569c..af6d630 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -30,6 +30,10 @@ impl<'a> Painter<'a> { mask_idx: self.mask, }, ); + self.push_primitive(h); + } + + fn push_primitive(&mut self, h: PrimitiveHandle) { if self.mask != MaskIdx::NONE { // TODO: I have no clue if this works at all :joy: self.rsc.ui_mut().masks.push_ref(self.mask); @@ -76,32 +80,20 @@ impl<'a> Painter<'a> { } pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) { - self.textures.push(handle.clone()); - self.write_image(handle.image_index(), region.within(&self.region)); + self.texture_at(handle, region.within(&self.region)); } pub fn texture(&mut self, handle: &TextureHandle) { - self.textures.push(handle.clone()); - self.write_image(handle.image_index(), self.region); + self.texture_at(handle, self.region); } pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) { self.textures.push(handle.clone()); - 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); + let h = + self.state + .layers + .write_texture(self.layer, self.id, handle.slot(), region, self.mask); + self.push_primitive(h); } pub fn render_text( @@ -111,7 +103,7 @@ impl<'a> Painter<'a> { width: Option, ) -> RenderedText { let ui = self.rsc.ui_mut(); - ui.text.render(buffer, attrs, width, &mut ui.textures) + ui.text.render(buffer, attrs, width) } pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { @@ -123,13 +115,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::new( - glyph.entry.uv_min, - glyph.entry.uv_max, - glyph.entry.layer, - text.color, - glyph.entry.flags(), - ), + GlyphPrimitive { + uv_min: glyph.entry.uv_min, + uv_max: glyph.entry.uv_max, + layer: glyph.entry.layer, + color: text.color, + flags: glyph.entry.flags(), + }, region, ); } diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index ad8afab..83f66f5 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -308,7 +308,6 @@ impl UiRenderState { source, cache: &mut self.cache, text: &mut ui.text, - textures: &mut ui.textures, widgets: &ui.widgets, outer, output_size: self.output_size, diff --git a/core/src/ui/size.rs b/core/src/ui/size.rs index a874cf9..931a1b2 100644 --- a/core/src/ui/size.rs +++ b/core/src/ui/size.rs @@ -1,11 +1,10 @@ use crate::{ - Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures, - UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2, + Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2, + WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2, }; pub struct SizeCtx<'a> { pub text: &'a mut TextData, - pub textures: &'a mut Textures, pub(super) source: WidgetId, pub(super) widgets: &'a Widgets, pub(super) cache: &'a mut Cache, @@ -33,7 +32,6 @@ impl SizeCtx<'_> { .get_dyn_dynamic(id) .desired_len::(&mut SizeCtx { text: self.text, - textures: self.textures, source: self.source, widgets: self.widgets, cache: self.cache, @@ -82,7 +80,7 @@ impl SizeCtx<'_> { attrs: &TextAttrs, width: Option, ) -> RenderedText { - self.text.render(buffer, attrs, width, self.textures) + self.text.render(buffer, attrs, width) } pub fn label(&self, id: WidgetId) -> &String { diff --git a/examples/tabs/main.rs b/examples/tabs/main.rs index 6316924..1ac7a94 100644 --- a/examples/tabs/main.rs +++ b/examples/tabs/main.rs @@ -212,10 +212,10 @@ impl DefaultAppState for Client { render: &mut UiRenderState, ) { let new = format!( - "widgets: {}\nactive: {}\nviews: {}", + "widgets: {}\nactive: {}\ntextures: {}", rsc.widgets().len(), render.active_widgets(), - self.ui_state.renderer.ui.view_count(), + self.ui_state.renderer.ui.texture_count(), ); if new != *rsc.widgets()[self.info].content { *rsc.widgets_mut()[self.info].content = new; diff --git a/src/default/render.rs b/src/default/render.rs index f7c1239..45520c1 100644 --- a/src/default/render.rs +++ b/src/default/render.rs @@ -83,12 +83,9 @@ impl UiRenderer { .block_on() .expect("Could not get adapter!"); - // 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. + // No binding-array features or limits: the atlas is one + // texture_2d_array and an image its own bind group, so nothing here + // needs VK_EXT_descriptor_indexing the way the old layout did. let (device, queue) = adapter .request_device(&DeviceDescriptor { required_limits: Limits {