diff --git a/core/src/primitive/layer.rs b/core/src/primitive/layer.rs index 53d05ff..7d0b770 100644 --- a/core/src/primitive/layer.rs +++ b/core/src/primitive/layer.rs @@ -2,7 +2,7 @@ use std::ops::{Index, IndexMut}; use crate::{ UiRegion, WidgetId, - render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives}, + render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst}, util::to_mut, }; @@ -40,7 +40,7 @@ struct Child { tail: usize, } -pub type PrimitiveLayers = Layers; +pub type DrawLayers = Layers; impl Layers { pub fn new() -> Layers { @@ -120,7 +120,7 @@ impl Layers { } } -impl PrimitiveLayers { +impl DrawLayers { pub fn write( &mut self, layer: LayerId, diff --git a/core/src/render/atlas.rs b/core/src/render/atlas.rs index a860a64..2325f4a 100644 --- a/core/src/render/atlas.rs +++ b/core/src/render/atlas.rs @@ -98,17 +98,10 @@ impl GlyphAtlas { return None; } - 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 upload = self.allocate(w, h); + let PatchRect { x, y, .. } = upload.rect; + write_glyph(&mut self.pages[upload.layer as usize].image, image, x, y); + self.uploads.push(upload); let scale = 1.0 / PAGE as f32; let entry = GlyphEntry { @@ -119,20 +112,30 @@ impl GlyphAtlas { width: w, height: h, is_colored: matches!(image.content, Content::Color), - layer, + layer: upload.layer, }; self.entries.insert(key, Some(entry)); Some(entry) } - fn allocate(&mut self, w: u32, h: u32) -> (u32, u32, u32) { + /// Reserves room for a `w` by `h` glyph, adding a page if none has it. + fn allocate(&mut self, w: u32, h: u32) -> PageUpload { + let rect = |x, y| PatchRect { + x, + y, + width: w, + height: h, + }; 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 as u32, x, y); + return PageUpload { + layer: i as u32, + rect: rect(x, y), + }; } self.pages.push(Page { @@ -141,12 +144,15 @@ impl GlyphAtlas { y: PAD, shelf_height: h + PAD, }); - (self.pages.len() as u32 - 1, PAD, PAD) + PageUpload { + layer: self.pages.len() as u32 - 1, + rect: rect(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. + /// upload. A new page needs nothing more: 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 diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index f8f038d..152836f 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -1,5 +1,3 @@ -use std::ops::Range; - use crate::{ UiData, UiRenderState, render::{ @@ -51,14 +49,10 @@ struct RenderLayer { instance: ArrBuf, primitives: PrimitiveBuffers, primitive_group: BindGroup, - 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, + texture_instance: ArrBuf, + /// Which texture each entry of `texture_instance` samples, in the same + /// order. Not in the vertex buffer because it names a bind group. + texture_slots: Vec, } impl UiRenderNode { @@ -68,21 +62,21 @@ impl UiRenderNode { pass.set_bind_group(3, &self.mask_group, &[]); for i in &self.active { let layer = &self.layers[i]; - if layer.draws.is_empty() { - continue; - } pass.set_bind_group(1, &layer.primitive_group, &[]); - 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()); + if layer.instance.len() > 0 { + pass.set_bind_group(2, self.pages.group(), &[]); + pass.set_vertex_buffer(0, layer.instance.buffer.slice(..)); + pass.draw(0..4, 0..layer.instance.len() as u32); + } + if !layer.texture_slots.is_empty() { + pass.set_vertex_buffer(0, layer.texture_instance.buffer.slice(..)); + for (i, &slot) in layer.texture_slots.iter().enumerate() { + let Some(group) = self.textures.group(slot) else { + continue; + }; + pass.set_bind_group(2, group, &[]); + pass.draw(0..4, i as u32..i as u32 + 1); + } } } } @@ -95,12 +89,12 @@ impl UiRenderNode { ui_render: &mut UiRenderState, ) { self.active.clear(); - for (i, primitives) in ui_render.layers.iter_mut() { + for (i, draws) in ui_render.layers.iter_mut() { self.active.push(i); - for change in primitives.apply_free() { + for change in draws.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 { + if h.layer == i && h.kind == change.kind && h.inst_idx == change.old { h.inst_idx = change.new; break; } @@ -119,21 +113,34 @@ impl UiRenderNode { ), primitives, primitive_group, - draws: Vec::new(), + texture_instance: ArrBuf::new( + device, + BufferUsages::VERTEX | BufferUsages::COPY_DST, + "texture instance", + ), + texture_slots: Vec::new(), } }); - if primitives.updated { + if draws.updated { rlayer .instance - .update(device, queue, primitives.instances()); - rlayer.primitives.update(device, queue, primitives.data()); + .update(device, queue, draws.primitives.instances()); + rlayer + .primitives + .update(device, queue, draws.primitives.data()); rlayer.primitive_group = Self::primitive_group( device, &self.primitive_layout, rlayer.primitives.buffers(), ); - rlayer.plan_draws(primitives.instances()); - primitives.updated = false; + rlayer + .texture_instance + .update(device, queue, draws.textures.instances()); + rlayer.texture_slots.clear(); + rlayer + .texture_slots + .extend(draws.textures.instances().iter().map(|inst| inst.idx)); + draws.updated = false; } } if ui.masks.changed { @@ -312,9 +319,8 @@ impl UiRenderNode { }) } - /// 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. + /// Group 2: the texture this draw samples, and the sampler. No `count` on + /// either entry -- plain Vulkan 1.0 / GLES sampling is all this needs. fn texture_layout(device: &Device) -> BindGroupLayout { device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[ @@ -372,24 +378,3 @@ impl UiRenderNode { 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 index 4dacd3d..710746a 100644 --- a/core/src/render/page.rs +++ b/core/src/render/page.rs @@ -1,18 +1,17 @@ -use image::EncodableLayout; use wgpu::*; use crate::GlyphAtlas; use super::{ atlas::PAGE, - texture::{array_view, texture_group}, + texture::{array_view, texture_group, write_region}, }; /// 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 +/// One array rather than a texture per page because a layer index is ordinary +/// Vulkan 1.0 / GLES sampling, where a `binding_array` would need /// `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack. pub struct GpuPages { device: Device, @@ -43,33 +42,17 @@ impl GpuPages { 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, + let dst = TexelCopyTextureInfo { + texture: &self.texture, + mip_level: 0, + origin: Origin3d { + x: upload.rect.x, + y: upload.rect.y, + z: upload.layer, }, - 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, - }, - ); + aspect: TextureAspect::All, + }; + write_region(&self.queue, dst, page, upload.rect); } } diff --git a/core/src/render/primitive.rs b/core/src/render/primitive.rs index 8183593..ffa5c82 100644 --- a/core/src/render/primitive.rs +++ b/core/src/render/primitive.rs @@ -11,30 +11,89 @@ use crate::{ use bytemuck::Pod; use wgpu::*; -pub struct Primitives { - instances: Vec, - assoc: Vec, - data: PrimitiveData, - free: Vec, +/// Everything one layer draws. A texture binds its own group 2, so it draws on +/// its own and cannot join the instanced draw the primitives share. +pub struct LayerDraws { + pub primitives: Primitives, + pub textures: InstanceList, pub updated: bool, } -impl Default for Primitives { +impl Default for LayerDraws { fn default() -> Self { Self { - instances: Default::default(), - assoc: Default::default(), - data: Default::default(), - free: Vec::new(), + primitives: Default::default(), + textures: Default::default(), updated: true, } } } -/// 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. +/// Which of a layer's two lists an instance is in. They index independently, +/// so a handle or a renumbering naming only a position would be ambiguous. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InstanceKind { + Primitive, + Texture, +} + +/// A texture instance's `idx` names the texture to bind rather than a group-1 +/// entry, so it is not in `primitives!` and has no buffer of its own. pub const TEXTURE_BINDING: u32 = 2; +/// Instances, the widget each belongs to, and the slots waiting to be reused. +#[derive(Default)] +pub struct InstanceList { + instances: Vec, + assoc: Vec, + free: Vec, +} + +impl InstanceList { + pub fn instances(&self) -> &Vec { + &self.instances + } + + fn push(&mut self, id: WidgetId, inst: PrimitiveInstance) -> usize { + if let Some(i) = self.free.pop() { + self.instances[i] = inst; + self.assoc[i] = id; + i + } else { + let i = self.instances.len(); + self.instances.push(inst); + self.assoc.push(id); + i + } + } + + fn free(&mut self, i: usize) -> MaskIdx { + self.free.push(i); + self.instances[i].mask_idx + } + + fn apply_free(&mut self, kind: InstanceKind) -> impl Iterator { + self.free.sort_by(|a, b| b.cmp(a)); + let instances = &mut self.instances; + let assoc = &mut self.assoc; + self.free.drain(..).filter_map(move |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, + kind, + old, + new: i, + }) + }) + } +} + pub trait Primitive: Pod { const BINDING: u32; fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec; @@ -89,8 +148,6 @@ macro_rules! primitives { } $( - // Each is uploaded as its WGSL counterpart, so its fields have to - // sit at WGSL's offsets -- including an align Rust would not pick. unsafe impl bytemuck::Pod for $ty {} unsafe impl bytemuck::Zeroable for $ty {} impl Primitive for $ty { @@ -112,32 +169,63 @@ pub struct PrimitiveInst

{ pub mask_idx: MaskIdx, } +/// The instanced half of a layer: a list, plus the group-1 data it reads. +#[derive(Default)] +pub struct Primitives { + list: InstanceList, + data: PrimitiveData, +} + impl Primitives { - pub fn write( + fn write( &mut self, - layer: usize, PrimitiveInst { id, primitive, region, mask_idx, }: PrimitiveInst

, - ) -> PrimitiveHandle { - let data_idx = P::vec(&mut self.data).add(primitive); - self.push( - layer, + ) -> usize { + let idx = P::vec(&mut self.data).add(primitive) as u32; + self.list.push( id, PrimitiveInstance { region, - idx: data_idx as u32, + idx, mask_idx, binding: P::BINDING, }, - Some(data_idx), ) } - /// Writes an instance that samples `texture` instead of a group-1 buffer. + fn free(&mut self, i: usize) -> MaskIdx { + // The instance says where its group-1 entry is, so the handle need not. + let inst = self.list.instances[i]; + self.data.free(inst.binding, inst.idx as usize); + self.list.free(i) + } + + pub fn data(&self) -> &PrimitiveData { + &self.data + } + + pub fn instances(&self) -> &Vec { + &self.list.instances + } +} + +impl LayerDraws { + pub fn write(&mut self, layer: usize, inst: PrimitiveInst

) -> PrimitiveHandle { + self.updated = true; + PrimitiveHandle { + layer, + kind: InstanceKind::Primitive, + inst_idx: self.primitives.write(inst), + } + } + + /// Writes an instance that samples the texture in slot `texture` instead of + /// reading a group-1 buffer. pub fn write_texture( &mut self, layer: usize, @@ -145,86 +233,59 @@ impl Primitives { 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 inst_idx = if let Some(i) = self.free.pop() { - self.instances[i] = inst; - self.assoc[i] = id; - i - } else { - let i = self.instances.len(); - self.instances.push(inst); - self.assoc.push(id); - i - }; PrimitiveHandle { layer, - inst_idx, - data_idx, - binding: inst.binding, + kind: InstanceKind::Texture, + inst_idx: self.textures.push( + id, + PrimitiveInstance { + region, + idx: texture, + mask_idx, + binding: TEXTURE_BINDING, + }, + ), } } - /// 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 }) - }) + let Self { + primitives, + textures, + .. + } = self; + primitives + .list + .apply_free(InstanceKind::Primitive) + .chain(textures.apply_free(InstanceKind::Texture)) } pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { self.updated = true; - if let Some(i) = h.data_idx { - self.data.free(h.binding, i); + match h.kind { + InstanceKind::Primitive => self.primitives.free(h.inst_idx), + InstanceKind::Texture => self.textures.free(h.inst_idx), } - self.free.push(h.inst_idx); - self.instances[h.inst_idx].mask_idx - } - - pub fn data(&self) -> &PrimitiveData { - &self.data - } - - pub fn instances(&self) -> &Vec { - &self.instances } pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion { self.updated = true; - &mut self.instances[h.inst_idx].region + &mut self.list_mut(h.kind).instances[h.inst_idx].region + } + + fn list_mut(&mut self, kind: InstanceKind) -> &mut InstanceList { + match kind { + InstanceKind::Primitive => &mut self.primitives.list, + InstanceKind::Texture => &mut self.textures, + } } } pub struct PrimitiveChange { pub id: WidgetId, + pub kind: InstanceKind, pub old: usize, pub new: usize, } @@ -232,10 +293,8 @@ pub struct PrimitiveChange { #[derive(Debug)] pub struct PrimitiveHandle { pub layer: usize, + pub kind: InstanceKind, pub inst_idx: usize, - /// `None` for a texture instance, which has no group-1 entry to free. - pub data_idx: Option, - pub binding: u32, } primitives!( @@ -265,9 +324,6 @@ impl RectPrimitive { /// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph /// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects. -/// -/// `align(8)` because `vec2` aligns `GlyphInfo` to 8, which pads it to -/// 32 bytes. #[repr(C, align(8))] #[derive(Debug, Copy, Clone)] pub struct GlyphPrimitive { diff --git a/core/src/render/shader.wgsl b/core/src/render/shader.wgsl index 248722a..09b16ba 100644 --- a/core/src/render/shader.wgsl +++ b/core/src/render/shader.wgsl @@ -45,8 +45,8 @@ struct UiVec2 { abs: vec2, } -// Whatever this run of instances samples: the glyph atlas, whose layers are -// its pages, or one standalone image as an array of one. +// What this draw samples: the glyph atlas, whose layers are its pages, or one +// standalone image as an array of one. @group(2) @binding(0) var tex: texture_2d_array; @group(2) @binding(1) diff --git a/core/src/render/texture.rs b/core/src/render/texture.rs index b3c98ad..1e1239a 100644 --- a/core/src/render/texture.rs +++ b/core/src/render/texture.rs @@ -1,4 +1,4 @@ -use image::{DynamicImage, EncodableLayout, GenericImageView}; +use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage}; use wgpu::{util::DeviceExt, *}; use crate::{PatchRect, TextureUpdate, Textures}; @@ -90,41 +90,55 @@ impl GpuTextures { let Some(Some(slot)) = self.slots.get(i as usize) else { return; }; - if rect.width == 0 || rect.height == 0 { - return; + let dst = TexelCopyTextureInfo { + texture: &slot.texture, + mip_level: 0, + origin: Origin3d { + x: rect.x, + y: rect.y, + z: 0, + }, + aspect: TextureAspect::All, + }; + match image.as_rgba8() { + Some(rgba) => write_region(&self.queue, dst, rgba, rect), + // The texture is rgba8, so any other layout has to be converted -- + // and converting the rectangle is cheaper than the whole image. + None => { + let sub = image + .view(rect.x, rect.y, rect.width, rect.height) + .to_image(); + write_region(&self.queue, dst, &sub, PatchRect { x: 0, y: 0, ..rect }); + } } - // `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, - }, - 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, - }, - ); } } -/// 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 +/// Uploads `rect` of `src` without copying it out first: `write_texture` takes +/// a row stride, so a region can be addressed where it already is. +pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) { + if rect.width == 0 || rect.height == 0 { + return; + } + let stride = src.width() * 4; + queue.write_texture( + dst, + src.as_bytes(), + TexelCopyBufferLayout { + offset: (rect.y * stride + rect.x * 4) as u64, + bytes_per_row: Some(stride), + rows_per_image: Some(rect.height), + }, + Extent3d { + width: rect.width, + height: rect.height, + depth_or_array_layers: 1, + }, + ); +} + +/// One array texture and a sampler, so the atlas and a standalone image share a +/// layout and the shader samples whichever is bound -- an image being a /// single-layer texture viewed as an array of one. pub fn texture_group( device: &Device, diff --git a/core/src/render/util/mod.rs b/core/src/render/util/mod.rs index ee3de03..215c853 100644 --- a/core/src/render/util/mod.rs +++ b/core/src/render/util/mod.rs @@ -33,6 +33,9 @@ impl ArrBuf { queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data)); resized } + pub fn len(&self) -> usize { + self.len + } fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer { let mut size = size as u64; if usage.contains(BufferUsages::STORAGE) { diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 83f66f5..f2a5cd7 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1,13 +1,13 @@ use crate::{ - ActiveData, Axis, IdLike, MaskIdx, Painter, PixelRegion, PrimitiveLayers, SizeCtx, - StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets, + ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget, + UiRegion, UiRsc, UiVec2, WidgetId, Widgets, ui::cache::Cache, util::{HashMap, HashSet, Vec2, forget_ref}, }; pub struct UiRenderState { pub active: HashMap, - pub layers: PrimitiveLayers, + pub layers: DrawLayers, pub(super) output_size: Vec2, pub cache: Cache, @@ -243,14 +243,11 @@ impl UiRenderState { } pub fn debug_layers(&self) { - for ((idx, depth), primitives) in self.layers.iter_depth() { + for ((idx, depth), draws) in self.layers.iter_depth() { let indent = " ".repeat(depth * 2); - let len = primitives.instances().len(); - print!("{indent}{idx}: {len} primitives"); - if len >= 1 { - print!(" ({})", primitives.instances()[0].binding); - } - println!(); + let primitives = draws.primitives.instances().len(); + let textures = draws.textures.instances().len(); + println!("{indent}{idx}: {primitives} primitives, {textures} textures"); } } diff --git a/src/default/render.rs b/src/default/render.rs index 45520c1..32e8722 100644 --- a/src/default/render.rs +++ b/src/default/render.rs @@ -83,9 +83,6 @@ impl UiRenderer { .block_on() .expect("Could not get adapter!"); - // 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 {