diff --git a/core/src/primitive/layer.rs b/core/src/primitive/layer.rs index 51c4048..3fb2411 100644 --- a/core/src/primitive/layer.rs +++ b/core/src/primitive/layer.rs @@ -1,8 +1,7 @@ use std::ops::{Index, IndexMut}; use crate::{ - UiRegion, WidgetId, - render::{LayerDraws, MaskIdx, PrimitiveHandle, PrimitiveKind}, + render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst}, util::to_mut, }; @@ -121,32 +120,17 @@ impl Layers { } impl DrawLayers { - pub fn write( + pub fn write( &mut self, layer: LayerId, - kind: PrimitiveKind

, - id: WidgetId, - primitive: P, - region: UiRegion, - mask_idx: MaskIdx, + info: PrimitiveInst

, ) -> PrimitiveHandle { - self[layer].write(layer, kind, id, primitive, region, mask_idx) + self[layer].write(layer, info) } pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { self[h.layer].free(h) } - - pub fn write_texture( - &mut self, - layer: LayerId, - id: WidgetId, - texture: u32, - region: UiRegion, - mask_idx: MaskIdx, - ) -> PrimitiveHandle { - self[layer].write_texture(layer, id, texture, region, mask_idx) - } } impl Default for Layers { diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index 9fe0777..98270bc 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -26,19 +26,15 @@ pub use data::{Mask, MaskIdx}; pub use primitive::*; const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); -const TEXTURE_SHADER: &str = include_str!("./shader/texture.wgsl"); pub struct UiRenderNode { shared_layout: BindGroupLayout, shared_group: BindGroup, - texture_data_layout: BindGroupLayout, texture_layout: BindGroupLayout, format: TextureFormat, - /// One per registered primitive, in id order. The texture pipeline is - /// apart because a texture binds group 2 per instance, not per draw. + /// One per registered primitive, in id order. primitives: Vec, - texture_pipeline: RenderPipeline, layers: HashMap, active: Vec, @@ -50,11 +46,10 @@ pub struct UiRenderNode { } struct RenderLayer { - /// One per registered primitive, in id order, matching `LayerDraws`. - primitives: Vec, - textures: ListBuffers, - /// Which texture each entry of `textures` samples, in the same order. - texture_slots: Vec, + /// One per registered primitive, in id order, matching `LayerDraws` -- + /// `None` where this layer draws none, so a primitive nobody uses costs no + /// buffers per layer. + primitives: Vec>, } /// What draws one registered primitive. The group 1 layout is its own rather @@ -62,6 +57,7 @@ struct RenderLayer { struct PrimitivePipeline { data_layout: BindGroupLayout, pipeline: RenderPipeline, + texture: PrimitiveTexture, } /// One list's vertex buffer and the data its shader reads at group 1. @@ -69,6 +65,8 @@ struct ListBuffers { instance: ArrBuf, data: ArrBuf, group: Option, + /// For a `PerInstance` primitive, the texture each instance binds. + slots: Vec, } impl UiRenderNode { @@ -80,32 +78,33 @@ impl UiRenderNode { // under a texture. Ordering beyond that is what `Layers` is for -- // freeing an instance swaps another into its place. for (id, list) in layer.primitives.iter().enumerate() { + let Some(list) = list else { + continue; + }; let Some(group) = &list.group else { continue; }; - pass.set_pipeline(&self.primitives[id].pipeline); - // Both after the pipeline: each primitive has its own pipeline - // layout, and a change drops the groups from where they differ. + let primitive = &self.primitives[id]; + pass.set_pipeline(&primitive.pipeline); + // Both groups after the pipeline: each primitive has its own + // pipeline layout, and a change drops the groups from where + // the two layouts differ. pass.set_bind_group(1, group, &[]); - pass.set_bind_group(2, self.pages.group(), &[]); pass.set_vertex_buffer(0, list.instance.buffer.slice(..)); - pass.draw(0..4, 0..list.instance.len() as u32); - } - if !layer.texture_slots.is_empty() { - // Group 1 too, unread as it is: a layer holding only an image - // never ran the loop above, so nothing is bound there. - let Some(data) = &layer.textures.group else { - continue; - }; - pass.set_pipeline(&self.texture_pipeline); - pass.set_bind_group(1, data, &[]); - pass.set_vertex_buffer(0, layer.textures.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); + match primitive.texture { + PrimitiveTexture::Atlas => { + pass.set_bind_group(2, self.pages.group(), &[]); + pass.draw(0..4, 0..list.instance.len() as u32); + } + PrimitiveTexture::PerInstance => { + for (i, &slot) in list.slots.iter().enumerate() { + let Some(texture) = self.textures.group(slot) else { + continue; + }; + pass.set_bind_group(2, texture, &[]); + pass.draw(0..4, i as u32..i as u32 + 1); + } + } } } } @@ -135,35 +134,33 @@ impl UiRenderNode { } } } - let rlayer = self - .layers - .entry(i) - .or_insert_with(|| RenderLayer::new(device)); + let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new); if draws.updated { rlayer .primitives - .resize_with(draws.primitives().len(), || ListBuffers::new(device)); - for (id, (list, draws)) in rlayer + .resize_with(draws.primitives().len(), || None); + for (id, (buffers, list)) in rlayer .primitives .iter_mut() .zip(draws.primitives()) .enumerate() { + let Some(list) = list else { + continue; + }; // Indexed, not zipped: a missing pipeline should say so // rather than quietly leave the list unbuilt. - let layout = &self.primitives[id].data_layout; - list.update(device, queue, layout, draws); + let primitive = &self.primitives[id]; + buffers + .get_or_insert_with(|| ListBuffers::new(device)) + .update( + device, + queue, + primitive.texture, + &primitive.data_layout, + list, + ); } - rlayer - .textures - .update(device, queue, &self.texture_data_layout, &draws.textures); - rlayer.texture_slots.clear(); - // Read rather than cast: the payload is a byte vec, so it - // carries no alignment a `u32` slice could borrow. - let slots = draws.textures.data().as_chunks::<4>().0; - rlayer - .texture_slots - .extend(slots.iter().copied().map(u32::from_ne_bytes)); draws.updated = false; } } @@ -218,30 +215,12 @@ impl UiRenderNode { let pages = GpuPages::new(device, queue, &texture_layout, &sampler); let textures = GpuTextures::new(device, queue); - // A texture instance's data is the slot it binds, which its shader - // never reads -- but group 1 is in the layout, so it is bound anyway. - let texture_data_layout = Self::data_layout(device, size_of::() as u64); - let texture_pipeline = Self::pipeline( - device, - &Self::pipeline_layout( - device, - &shared_layout, - &texture_data_layout, - &texture_layout, - ), - config.format, - TEXTURE_SHADER, - "texture", - ); - Self { shared_layout, shared_group, - texture_data_layout, texture_layout, format: config.format, primitives: Vec::new(), - texture_pipeline, window_buffer, layers: HashMap::default(), active: Vec::new(), @@ -267,6 +246,7 @@ impl UiRenderNode { self.primitives.push(PrimitivePipeline { data_layout, pipeline, + texture: source.texture, }); } } @@ -438,11 +418,9 @@ impl UiRenderNode { } impl RenderLayer { - fn new(device: &Device) -> Self { + fn new() -> Self { Self { primitives: Vec::new(), - textures: ListBuffers::new(device), - texture_slots: Vec::new(), } } } @@ -461,6 +439,7 @@ impl ListBuffers { "primitive data", ), group: None, + slots: Vec::new(), } } @@ -468,9 +447,18 @@ impl ListBuffers { &mut self, device: &Device, queue: &Queue, + texture: PrimitiveTexture, layout: &BindGroupLayout, list: &InstanceList, ) { + if texture == PrimitiveTexture::PerInstance { + self.slots.clear(); + // Read rather than cast: the payload is a byte vec, so it carries + // no alignment a `u32` slice could borrow. + let slots = list.data().as_chunks::<4>().0; + self.slots + .extend(slots.iter().copied().map(u32::from_ne_bytes)); + } self.instance.update(device, queue, list.instances()); let resized = self.data.update(device, queue, list.data()); // An empty list has no buffer big enough for one entry, and nothing diff --git a/core/src/render/primitive.rs b/core/src/render/primitive.rs index 048ebea..0b42b1f 100644 --- a/core/src/render/primitive.rs +++ b/core/src/render/primitive.rs @@ -7,6 +7,14 @@ use crate::{ }; use bytemuck::Pod; +/// One instance of a registered primitive, laid out as the struct that +/// primitive's shader reads at `@group(1) @binding(0)`. +/// +/// A `PrimitiveKind

` is only minted by `register::

`, and `write` takes +/// the kind and the value together, so holding one is the proof that `P` has a +/// list of its own to go in and a write needs no check. +pub trait Primitive: Pod {} + /// Which registered primitive an instance is, and so which list it lives in /// and which pipeline draws it. The type ties a `write` to what its shader reads. pub struct PrimitiveKind

{ @@ -37,6 +45,7 @@ impl

Copy for PrimitiveKind

{} pub const RECT: PrimitiveKind = PrimitiveKind::new(0); pub const GLYPH: PrimitiveKind = PrimitiveKind::new(1); +pub const TEXTURE: PrimitiveKind = PrimitiveKind::new(2); /// Every primitive a ui can draw, in id order. Registering one is all the /// wiring it needs: its list, buffers, free list and pipeline follow, and @@ -44,6 +53,9 @@ pub const GLYPH: PrimitiveKind = PrimitiveKind::new(1); /// /// A source is compiled after `prelude.wgsl` and supplies its data at /// `@group(1) @binding(0)` and an `fs_main` shading one instance. +/// +/// Order is draw order within a layer, so a primitive registered later is +/// drawn over one registered earlier. pub struct PrimitiveRegistry { kinds: Vec, } @@ -54,30 +66,55 @@ pub struct PrimitiveSource { /// Size of one instance's entry, which the renderer states as the group 1 /// binding's minimum rather than leaving it to be inferred. pub stride: u64, + pub texture: PrimitiveTexture, +} + +/// What a primitive samples at group 2, which is the whole of why some of them +/// cannot share one instanced draw. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PrimitiveTexture { + /// The shared glyph atlas, bound once for the layer. + Atlas, + /// The `Textures` slot in the first four bytes of its own data, bound for + /// that instance alone -- so one draw call each. + PerInstance, } impl Default for PrimitiveRegistry { fn default() -> Self { + use PrimitiveTexture::*; let mut registry = Self { kinds: Vec::new() }; - let rect = registry.register::(include_str!("shader/rect.wgsl"), "rect"); - let glyph = registry.register::(include_str!("shader/glyph.wgsl"), "glyph"); + let rect = + registry.register::(include_str!("shader/rect.wgsl"), "rect", Atlas); + let glyph = + registry.register::(include_str!("shader/glyph.wgsl"), "glyph", Atlas); + let texture = registry.register::( + include_str!("shader/texture.wgsl"), + "texture", + PerInstance, + ); // The built-ins have constant ids so a widget can name one without the // registry; registering them first is what makes those constants true. - assert_eq!((rect.id(), glyph.id()), (RECT.id(), GLYPH.id())); + assert_eq!( + (rect.id(), glyph.id(), texture.id()), + (RECT.id(), GLYPH.id(), TEXTURE.id()) + ); registry } } impl PrimitiveRegistry { - pub fn register( + pub fn register( &mut self, wgsl: &'static str, label: &'static str, + texture: PrimitiveTexture, ) -> PrimitiveKind

{ self.kinds.push(PrimitiveSource { wgsl, label, stride: size_of::

() as u64, + texture, }); PrimitiveKind::new(self.kinds.len() as u32 - 1) } @@ -87,21 +124,13 @@ impl PrimitiveRegistry { } } -/// Which of a layer's 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(u32), - Texture, -} - -/// Instances, the widget each belongs to, the slots waiting to be reused, and -/// `stride` bytes of data per instance at the same index. +/// One registered primitive's instances in one layer: the instances, the +/// widget each belongs to, the slots waiting to be reused, and `stride` bytes +/// of that primitive's data per instance at the same index. /// -/// That data is the struct a primitive's shader reads, or the slot a texture -/// binds. Keeping both here is what holds them in step through a -/// `swap_remove`. -#[derive(Default)] +/// `stride` comes from the type the list was made for, so a write is never +/// checked against it. The data rides here rather than beside the list so the +/// two stay in step through a `swap_remove`. pub struct InstanceList { instances: Vec, assoc: Vec, @@ -111,6 +140,16 @@ pub struct InstanceList { } impl InstanceList { + fn new() -> Self { + Self { + instances: Vec::new(), + assoc: Vec::new(), + free: Vec::new(), + data: Vec::new(), + stride: size_of::

(), + } + } + pub fn instances(&self) -> &[PrimitiveInstance] { &self.instances } @@ -120,14 +159,6 @@ impl InstanceList { } fn push(&mut self, id: WidgetId, inst: PrimitiveInstance, data: &[u8]) -> usize { - if self.instances.is_empty() { - self.stride = data.len(); - } - debug_assert_eq!( - data.len(), - self.stride, - "primitive written to the wrong list" - ); if let Some(i) = self.free.pop() { self.instances[i] = inst; self.assoc[i] = id; @@ -147,7 +178,7 @@ impl InstanceList { self.instances[i].mask_idx } - fn apply_free(&mut self, kind: InstanceKind) -> impl Iterator { + fn apply_free(&mut self, kind: u32) -> impl Iterator { self.free.sort_by(|a, b| b.cmp(a)); let instances = &mut self.instances; let assoc = &mut self.assoc; @@ -173,11 +204,13 @@ impl InstanceList { } } -/// Everything one layer draws. A texture binds its own group 2, so it draws on -/// its own and cannot join the instanced draw a primitive gets. +/// Everything one layer draws, one list per registered primitive. They index +/// independently, so a handle or a renumbering naming only a position would be +/// ambiguous between them. pub struct LayerDraws { - primitives: Vec, - pub textures: InstanceList, + /// `None` until this layer draws that primitive, because only the write + /// knows the type the list is for. + primitives: Vec>, pub updated: bool, } @@ -185,99 +218,85 @@ impl Default for LayerDraws { fn default() -> Self { Self { primitives: Vec::new(), - textures: InstanceList::default(), updated: true, } } } impl LayerDraws { - pub fn write( + pub fn write( &mut self, layer: usize, - kind: PrimitiveKind

, - id: WidgetId, - primitive: P, - region: UiRegion, - mask_idx: MaskIdx, + PrimitiveInst { + kind, + id, + primitive, + region, + mask_idx, + }: PrimitiveInst

, ) -> PrimitiveHandle { self.updated = true; + // Grown on first use rather than sized from the registry, which a + // layer cannot see. if self.primitives.len() <= kind.id as usize { - self.primitives - .resize_with(kind.id as usize + 1, Default::default); + self.primitives.resize_with(kind.id as usize + 1, || None); } - let inst_idx = self.primitives[kind.id as usize].push( - id, - PrimitiveInstance { region, mask_idx }, - bytemuck::bytes_of(&primitive), - ); + let inst_idx = self.primitives[kind.id as usize] + .get_or_insert_with(InstanceList::new::

) + .push( + id, + PrimitiveInstance { region, mask_idx }, + bytemuck::bytes_of(&primitive), + ); PrimitiveHandle { layer, - kind: InstanceKind::Primitive(kind.id), + kind: kind.id, inst_idx, } } - /// Writes an instance that samples the texture in slot `texture`, which is - /// bound for it alone rather than read from a buffer. - pub fn write_texture( - &mut self, - layer: usize, - id: WidgetId, - texture: u32, - region: UiRegion, - mask_idx: MaskIdx, - ) -> PrimitiveHandle { - self.updated = true; - PrimitiveHandle { - layer, - kind: InstanceKind::Texture, - inst_idx: self.textures.push( - id, - PrimitiveInstance { region, mask_idx }, - bytemuck::bytes_of(&texture), - ), - } - } - - pub fn primitives(&self) -> &[InstanceList] { + pub fn primitives(&self) -> &[Option] { &self.primitives } pub fn apply_free(&mut self) -> impl Iterator { - let Self { - primitives, - textures, - .. - } = self; - primitives + self.primitives .iter_mut() .enumerate() - .flat_map(|(i, list)| list.apply_free(InstanceKind::Primitive(i as u32))) - .chain(textures.apply_free(InstanceKind::Texture)) + .filter_map(|(kind, list)| Some((kind as u32, list.as_mut()?))) + .flat_map(|(kind, list)| list.apply_free(kind)) } pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { self.updated = true; - self.list_mut(h.kind).free(h.inst_idx) + self.list(h).free(h.inst_idx) } pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion { self.updated = true; - &mut self.list_mut(h.kind).instances[h.inst_idx].region + &mut self.list(h).instances[h.inst_idx].region } - fn list_mut(&mut self, kind: InstanceKind) -> &mut InstanceList { - match kind { - InstanceKind::Primitive(i) => &mut self.primitives[i as usize], - InstanceKind::Texture => &mut self.textures, - } + /// A handle is only ever made by `write`, which is what created the list. + fn list(&mut self, h: &PrimitiveHandle) -> &mut InstanceList { + self.primitives[h.kind as usize] + .as_mut() + .expect("handle names a primitive this layer never drew") } } +pub struct PrimitiveInst

{ + pub kind: PrimitiveKind

, + pub id: WidgetId, + pub primitive: P, + pub region: UiRegion, + pub mask_idx: MaskIdx, +} + pub struct PrimitiveChange { pub id: WidgetId, - pub kind: InstanceKind, + /// Which registered primitive's list moved, since they index separately. + pub kind: u32, pub old: usize, pub new: usize, } @@ -285,7 +304,7 @@ pub struct PrimitiveChange { #[derive(Debug)] pub struct PrimitiveHandle { pub layer: usize, - pub kind: InstanceKind, + pub kind: u32, pub inst_idx: usize, } @@ -300,6 +319,7 @@ pub struct RectPrimitive { unsafe impl bytemuck::Pod for RectPrimitive {} unsafe impl bytemuck::Zeroable for RectPrimitive {} +impl Primitive for RectPrimitive {} impl RectPrimitive { pub fn color(color: Color) -> Self { @@ -327,3 +347,16 @@ pub struct GlyphPrimitive { unsafe impl bytemuck::Pod for GlyphPrimitive {} unsafe impl bytemuck::Zeroable for GlyphPrimitive {} +impl Primitive for GlyphPrimitive {} + +/// One drawn image. Its shader reads nothing: the slot names the texture bound +/// for this instance alone, which is what `PrimitiveTexture::PerInstance` does. +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct TexturePrimitive { + pub slot: u32, +} + +unsafe impl bytemuck::Pod for TexturePrimitive {} +unsafe impl bytemuck::Zeroable for TexturePrimitive {} +impl Primitive for TexturePrimitive {} diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 7471f72..c0135bd 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -1,9 +1,10 @@ -use bytemuck::Pod; - use crate::{ Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId, - render::{GLYPH, GlyphPrimitive, Mask, MaskIdx, PrimitiveHandle, PrimitiveKind}, + render::{ + GLYPH, GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, + PrimitiveKind, TEXTURE, TexturePrimitive, + }, util::Vec2, }; @@ -22,11 +23,22 @@ pub struct Painter<'a> { } impl<'a> Painter<'a> { - fn primitive_at(&mut self, kind: PrimitiveKind

, primitive: P, region: UiRegion) { - let h = self - .state - .layers - .write(self.layer, kind, self.id, primitive, region, self.mask); + fn primitive_at( + &mut self, + kind: PrimitiveKind

, + primitive: P, + region: UiRegion, + ) { + let h = self.state.layers.write( + self.layer, + PrimitiveInst { + kind, + id: self.id, + primitive, + region, + mask_idx: self.mask, + }, + ); self.push_primitive(h); } @@ -39,11 +51,11 @@ impl<'a> Painter<'a> { } /// Writes a primitive to be rendered - pub fn primitive(&mut self, kind: PrimitiveKind

, primitive: P) { + pub fn primitive(&mut self, kind: PrimitiveKind

, primitive: P) { self.primitive_at(kind, primitive, self.region) } - pub fn primitive_within( + pub fn primitive_within( &mut self, kind: PrimitiveKind

, primitive: P, @@ -91,11 +103,13 @@ impl<'a> Painter<'a> { pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) { self.textures.push(handle.clone()); - let h = - self.state - .layers - .write_texture(self.layer, self.id, handle.slot(), region, self.mask); - self.push_primitive(h); + self.primitive_at( + TEXTURE, + TexturePrimitive { + slot: handle.slot(), + }, + region, + ); } pub fn render_text( diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 140de06..4bd7a90 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -245,9 +245,12 @@ impl UiRenderState { pub fn debug_layers(&self) { for ((idx, depth), draws) in self.layers.iter_depth() { let indent = " ".repeat(depth * 2); - let primitives: usize = draws.primitives().iter().map(|l| l.instances().len()).sum(); - let textures = draws.textures.instances().len(); - println!("{indent}{idx}: {primitives} primitives, {textures} textures"); + let counts: Vec = draws + .primitives() + .iter() + .map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string()) + .collect(); + println!("{indent}{idx}: [{}]", counts.join(", ")); } }