diff --git a/iris/core/src/primitive/layer.rs b/iris/core/src/primitive/layer.rs index a3610ef..1db6ac6 100644 --- a/iris/core/src/primitive/layer.rs +++ b/iris/core/src/primitive/layer.rs @@ -1,10 +1,6 @@ use std::ops::{Index, IndexMut}; -use crate::{ - UiRegion, WidgetId, - render::{MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives}, - util::to_mut, -}; +use crate::{render::LayerOrder, util::to_mut}; pub type LayerId = usize; @@ -40,7 +36,10 @@ struct Child { tail: usize, } -pub type PrimitiveLayers = Layers; +/// The draw order of every layer. The primitives themselves live in one +/// arena beside this (`UiRenderState::primitives`); a layer names the +/// slots it draws, which is what its vertex buffer is. +pub type PrimitiveLayers = Layers; impl Layers { pub fn new() -> Layers { @@ -120,32 +119,6 @@ impl Layers { } } -impl PrimitiveLayers { - pub fn write( - &mut self, - layer: LayerId, - info: PrimitiveInst

, - ) -> PrimitiveHandle { - self[layer].write(layer, info) - } - - pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { - self[h.layer].free(h) - } - - pub fn write_image( - &mut self, - layer: LayerId, - id: WidgetId, - texture_idx: u32, - region: UiRegion, - mask_idx: MaskIdx, - move_idx: MoveIdx, - ) -> PrimitiveHandle { - self[layer].write_image(layer, id, texture_idx, region, mask_idx, move_idx) - } -} - impl Default for Layers { fn default() -> Self { Self::new() diff --git a/iris/core/src/render/data.rs b/iris/core/src/render/data.rs index c5c7bdf..eed375b 100644 --- a/iris/core/src/render/data.rs +++ b/iris/core/src/render/data.rs @@ -8,6 +8,15 @@ pub struct WindowUniform { pub height: f32, } +/// One primitive's placement and what to draw there, in the one arena +/// every layer shares (`Primitives`). Read from a storage buffer by +/// **both** shader stages: the vertex stage for the corners of the +/// primitive it is drawing, the fragment stage for the corners of a +/// *mask's* primitive, which is generally a different one and often in +/// another layer. A layer's vertex buffer carries only the slot +/// ([`instance_slot_layout`]), so there is exactly one copy of a +/// placement and a mask cannot disagree with what was drawn. See +/// LAYOUT.md's "Masks with a shape". #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct PrimitiveInstance { @@ -18,24 +27,17 @@ pub struct PrimitiveInstance { pub move_idx: MoveIdx, } -impl PrimitiveInstance { - const ATTRIBS: [VertexAttribute; 8] = vertex_attr_array![ - 0 => Float32x2, - 1 => Float32x2, - 2 => Float32x2, - 3 => Float32x2, - 4 => Uint32, - 5 => Uint32, - 6 => Uint32, - 7 => Uint32, - ]; - - pub fn desc() -> VertexBufferLayout<'static> { - VertexBufferLayout { - array_stride: std::mem::size_of::() as BufferAddress, - step_mode: VertexStepMode::Instance, - attributes: &Self::ATTRIBS, - } +/// The vertex layout of a layer's draw order: one `u32` slot into the +/// global instance arena per instance, stepped per instance. Everything a +/// primitive is made of used to be here as eight vertex attributes; it +/// moved into the storage buffer above so the fragment stage can read it +/// too. +pub fn instance_slot_layout() -> VertexBufferLayout<'static> { + const ATTRIBS: [VertexAttribute; 1] = vertex_attr_array![0 => Uint32]; + VertexBufferLayout { + array_stride: std::mem::size_of::() as BufferAddress, + step_mode: VertexStepMode::Instance, + attributes: &ATTRIBS, } } diff --git a/iris/core/src/render/mod.rs b/iris/core/src/render/mod.rs index 1b8dd12..5452c97 100644 --- a/iris/core/src/render/mod.rs +++ b/iris/core/src/render/mod.rs @@ -1,6 +1,10 @@ use crate::{ UiData, UiRenderState, - render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf}, + render::{ + data::{PrimitiveInstance, instance_slot_layout}, + texture::GpuTextures, + util::ArrBuf, + }, util::{HashMap, Vec2}, }; use data::WindowUniform; @@ -120,6 +124,11 @@ impl WgpuErrorLog { pub struct UiRenderNode { uniform_group: BindGroup, primitive_layout: BindGroupLayout, + /// Group 1: `rects` and `glyphs`. Global and bound once per frame, + /// not per layer -- a mask referencing a rect drawn in another layer + /// has to be able to read it (see `Primitives`). + primitives: PrimitiveBuffers, + primitive_group: BindGroup, rsc_layout: BindGroupLayout, rsc_group: BindGroup, @@ -129,6 +138,9 @@ pub struct UiRenderNode { active: Vec, window_buffer: Buffer, textures: GpuTextures, + /// Every primitive's placement, read by the vertex stage for the + /// primitive being drawn and by the fragment stage for a mask's. + instances: ArrBuf, masks: ArrBuf, move_offsets: ArrBuf, /// Group 3: the masks and move-offsets storage buffers, on their own -- @@ -146,16 +158,16 @@ pub struct UiRenderNode { masks_group: BindGroup, } +/// One layer's vertex buffers: the slots it draws, in order. The +/// primitives themselves are in `UiRenderNode::instances`. 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. + order: ArrBuf, + /// A standalone image's slots, kept apart from `order` because each + /// one draws with its own bind group -- see `UiRenderNode::draw`. + images: ArrBuf, + /// The texture slot each entry of `images` draws with, in the same + /// order, refreshed alongside it. Not in the vertex buffer itself + /// because it names a bind group, not shader data. image_tex_indices: Vec, } @@ -163,6 +175,8 @@ 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, &[]); + // Group 1 is global now, so it is set here rather than per layer. + pass.set_bind_group(1, &self.primitive_group, &[]); // Set once, not per layer or per image: masks/move_offsets are read // by every primitive and every standalone image alike, and living // in their own group (rather than folded into group 2 alongside the @@ -172,14 +186,13 @@ impl UiRenderNode { pass.set_bind_group(3, &self.masks_group, &[]); for i in &self.active { let layer = &self.layers[i]; - if layer.instance.len() == 0 && layer.image_instance.len() == 0 { + if layer.order.len() == 0 && layer.images.len() == 0 { continue; } - pass.set_bind_group(1, &layer.primitive_group, &[]); - if layer.instance.len() > 0 { + if layer.order.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); + pass.set_vertex_buffer(0, layer.order.buffer.slice(..)); + pass.draw(0..4, 0..layer.order.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" @@ -188,8 +201,8 @@ impl UiRenderNode { // 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(..)); + if layer.images.len() > 0 { + pass.set_vertex_buffer(0, layer.images.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); @@ -206,67 +219,45 @@ impl UiRenderNode { ui_render: &mut UiRenderState, ) -> FrameUpdateStats { self.active.clear(); - for (i, primitives) in ui_render.layers.iter_mut() { + for (i, order) in ui_render.layers.iter_mut() { self.active.push(i); - 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 - { - h.inst_idx = change.new; - break; - } - } - } - } - let rlayer = self.layers.entry(i).or_insert_with(|| { - let primitives = PrimitiveBuffers::new(device); - let primitive_group = - Self::primitive_group(device, &self.primitive_layout, primitives.buffers()); - RenderLayer { - instance: ArrBuf::new( - device, - BufferUsages::VERTEX | BufferUsages::COPY_DST, - "instance", - ), - primitives, - primitive_group, - image_instance: ArrBuf::new( - device, - BufferUsages::VERTEX | BufferUsages::COPY_DST, - "image instance", - ), - image_tex_indices: Vec::new(), - } - }); - if primitives.updated { - rlayer - .instance - .update(device, queue, primitives.instances()); - rlayer.primitives.update(device, queue, primitives.data()); - rlayer.primitive_group = Self::primitive_group( + let rlayer = self.layers.entry(i).or_insert_with(|| RenderLayer { + order: ArrBuf::new( device, - &self.primitive_layout, - rlayer.primitives.buffers(), - ); - rlayer - .image_instance - .update(device, queue, primitives.image_instances()); - rlayer.image_tex_indices = primitives - .image_instances() + BufferUsages::VERTEX | BufferUsages::COPY_DST, + "layer order", + ), + images: ArrBuf::new( + device, + BufferUsages::VERTEX | BufferUsages::COPY_DST, + "layer image order", + ), + image_tex_indices: Vec::new(), + }); + if order.updated { + rlayer.order.update(device, queue, order.order()); + rlayer.images.update(device, queue, order.images()); + rlayer.image_tex_indices = order + .images() .iter() - .map(|inst| inst.idx) + .map(|&slot| ui_render.primitives.instance(slot).idx) .collect(); - primitives.updated = false; + order.updated = false; } } + let instances_resized = if ui_render.primitives.updated { + ui_render.primitives.updated = false; + let resized = self + .instances + .update(device, queue, ui_render.primitives.instances()); + self.primitives + .update(device, queue, ui_render.primitives.data()); + self.primitive_group = + Self::primitive_group(device, &self.primitive_layout, self.primitives.buffers()); + resized + } else { + false + }; let masks_resized = if ui.masks.changed { ui.masks.changed = false; self.masks.update(device, queue, &ui.masks[..]) @@ -280,9 +271,14 @@ impl UiRenderNode { } else { false }; - if masks_resized || moves_resized { - self.masks_group = - Self::masks_group(device, &self.masks_layout, &self.masks, &self.move_offsets); + if masks_resized || moves_resized || instances_resized { + self.masks_group = Self::masks_group( + device, + &self.masks_layout, + &self.masks, + &self.move_offsets, + &self.instances, + ); } let rebuild_main = self.textures.update(&mut ui.textures, &self.rsc_layout); if rebuild_main { @@ -408,6 +404,14 @@ impl UiRenderNode { }); let tex_manager = GpuTextures::new(device, queue); + let primitives = PrimitiveBuffers::new(device); + let primitive_group = + Self::primitive_group(device, &primitive_layout, primitives.buffers()); + let instances = ArrBuf::new( + device, + BufferUsages::STORAGE | BufferUsages::COPY_DST, + "ui instances", + ); let masks = ArrBuf::new( device, BufferUsages::STORAGE | BufferUsages::COPY_DST, @@ -422,7 +426,8 @@ impl UiRenderNode { let rsc_layout = Self::rsc_layout(device); let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager); let masks_layout = Self::masks_layout(device); - let masks_group = Self::masks_group(device, &masks_layout, &masks, &move_offsets); + let masks_group = + Self::masks_group(device, &masks_layout, &masks, &move_offsets, &instances); let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { label: Some("UI Shape Pipeline Layout"), @@ -440,7 +445,7 @@ impl UiRenderNode { vertex: VertexState { module: &shader, entry_point: Some("vs_main"), - buffers: &[PrimitiveInstance::desc()], + buffers: &[instance_slot_layout()], compilation_options: Default::default(), }, fragment: Some(FragmentState { @@ -486,6 +491,8 @@ impl UiRenderNode { Ok(Self { uniform_group, primitive_layout, + primitives, + primitive_group, rsc_layout, rsc_group, pipeline, @@ -493,6 +500,7 @@ impl UiRenderNode { layers: HashMap::default(), active: Vec::new(), textures: tex_manager, + instances, masks, move_offsets, masks_layout, @@ -627,6 +635,16 @@ impl UiRenderNode { }, count: None, }, + BindGroupLayoutEntry { + binding: 2, + visibility: ShaderStages::VERTEX | 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"), }) @@ -637,6 +655,7 @@ impl UiRenderNode { layout: &BindGroupLayout, masks: &ArrBuf, move_offsets: &ArrBuf, + instances: &ArrBuf, ) -> BindGroup { device.create_bind_group(&BindGroupDescriptor { layout, @@ -649,6 +668,10 @@ impl UiRenderNode { binding: 1, resource: move_offsets.buffer.as_entire_binding(), }, + BindGroupEntry { + binding: 2, + resource: instances.buffer.as_entire_binding(), + }, ], label: Some("ui masks"), }) diff --git a/iris/core/src/render/primitive.rs b/iris/core/src/render/primitive.rs index f4cfc3c..3c81c01 100644 --- a/iris/core/src/render/primitive.rs +++ b/iris/core/src/render/primitive.rs @@ -11,46 +11,11 @@ use crate::{ use bytemuck::Pod; use wgpu::*; -pub struct Primitives { - instances: Vec, - 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 -- see TEXTURES.md's "Recommended shape". `idx` on each - /// `PrimitiveInstance` here is the texture's slot in `Textures`/ - /// `GpuTextures`, not an index into `data`; there is no per-image entry - /// in `data` because a bind group already picks the texture; nothing - /// left to look up per-instance. - images: Vec, - image_assoc: Vec, - image_free: Vec, - - pub updated: bool, -} - -impl Default for Primitives { - fn default() -> Self { - Self { - instances: Default::default(), - 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. +/// has to match the shader's `TEXTURE` constant and flag "this instance is +/// drawn with its own bind group" to the code below. pub const IMAGE_BINDING: u32 = 1; pub trait Primitive: Pod { @@ -134,18 +99,61 @@ macro_rules! primitives { (@count $t:tt) => { 1 }; } -pub struct PrimitiveInst

{ - pub id: WidgetId, - pub primitive: P, - pub region: UiRegion, - pub mask_idx: MaskIdx, - pub move_idx: MoveIdx, +/// Every primitive instance in the tree, in one arena that all layers +/// share, plus the per-primitive data (`rects`, `glyphs`) they index. +/// +/// **Why one arena rather than one per layer**, which is what this was: +/// the fragment stage evaluates a *mask's* primitive at the masked pixel +/// (LAYOUT.md's "Masks with a shape"), and the widget that owns a mask is +/// routinely in a different layer from the content it clips -- a rounded +/// container in one layer, a `Stack`'s child content in the layer below. +/// A per-layer buffer cannot answer that lookup at all: only one layer's +/// group is bound at a time, so the mask would silently read another +/// layer's rect. Both buffers are therefore global and bound once per +/// frame, and a layer keeps only its draw *order* ([`LayerOrder`]). +/// +/// Slots are stable for a primitive's whole life: nothing here is +/// compacted, so a `Mask` can hold a slot across frames. +pub struct Primitives { + instances: Vec, + assoc: Vec, + /// Slots freed since the last [`Self::apply_free`]. Deliberately not + /// reusable yet: the layer that drew one still names it in its draw + /// order until that call compacts the order, so handing it out again + /// first would draw the new primitive twice -- once through the stale + /// order entry and once through the new one. + freed: Vec, + /// Slots [`Self::apply_free`] released, which is what [`Self::alloc`] + /// hands out. + reusable: Vec, + data: PrimitiveData, + /// Whether the instance arena or the per-primitive data changed since + /// the last upload -- one flag for both, since they are uploaded + /// together. + pub updated: bool, +} + +impl Default for Primitives { + fn default() -> Self { + Self { + instances: Default::default(), + assoc: Default::default(), + freed: Vec::new(), + reusable: Vec::new(), + data: Default::default(), + updated: true, + } + } } impl Primitives { - pub fn write( + /// Writes a primitive into the arena and hands back its slot and its + /// entry in the per-primitive data. The caller (`UiRenderState`) puts + /// the slot into a layer's draw order -- an instance that no layer + /// names is never rasterized, which is what a mask shape drawn only to + /// be *referenced* uses. + pub fn alloc( &mut self, - layer: usize, PrimitiveInst { id, primitive, @@ -153,154 +161,118 @@ impl Primitives { mask_idx, move_idx, }: PrimitiveInst

, - ) -> 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, - move_idx, - binding: P::BINDING, - }; - let inst_i = 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::new::

(layer, inst_i, i) + ) -> (u32, usize) { + let data_idx = P::vec(&mut self.data).add(primitive); + let slot = self.push( + PrimitiveInstance { + region, + idx: data_idx as u32, + mask_idx, + move_idx, + binding: P::BINDING, + }, + id, + ); + (slot, data_idx) } - /// 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( + /// A standalone image, which has no `PrimitiveData` entry to allocate + /// -- its bind group already picks the texture, so `texture_idx` rides + /// in the otherwise-unused `idx` field and names the bind group the + /// draw call selects. + pub fn alloc_image( &mut self, - layer: usize, id: WidgetId, texture_idx: u32, region: UiRegion, mask_idx: MaskIdx, move_idx: MoveIdx, - ) -> PrimitiveHandle { + ) -> u32 { + self.push( + PrimitiveInstance { + region, + idx: texture_idx, + mask_idx, + move_idx, + binding: IMAGE_BINDING, + }, + id, + ) + } + + fn push(&mut self, inst: PrimitiveInstance, id: WidgetId) -> u32 { self.updated = true; - let inst = PrimitiveInstance { - region, - idx: texture_idx, - mask_idx, - move_idx, - binding: IMAGE_BINDING, - }; - let inst_i = if let Some(i) = self.image_free.pop() { - self.images[i] = inst; - self.image_assoc[i] = id; + let slot = if let Some(i) = self.reusable.pop() { + self.instances[i] = inst; + self.assoc[i] = id; i } else { - let i = self.images.len(); - self.images.push(inst); - self.image_assoc.push(id); - i + self.instances.push(inst); + self.assoc.push(id); + self.instances.len() - 1 }; - PrimitiveHandle { - layer, - inst_idx: inst_i, - data_idx: 0, - binding: IMAGE_BINDING, - } - } - - pub fn image_instances(&self) -> &Vec { - &self.images - } - - /// returns (old index, new index) for both lists this layer keeps -- - /// `PrimitiveChange::is_image` says which, since the two have separate - /// index spaces and `old`/`new` alone would collide between them. - /// - /// Both lists free with `swap_remove`, so a layer's draw order was - /// already undefined before images existed: nothing here may assume one - /// primitive stays adjacent to another once anything in the layer has - /// been freed. - pub fn apply_free(&mut self) -> Vec { - let mut changes = - Self::apply_free_list(&mut self.free, &mut self.instances, &mut self.assoc, false); - changes.extend(Self::apply_free_list( - &mut self.image_free, - &mut self.images, - &mut self.image_assoc, - true, - )); - changes - } - - fn apply_free_list( - free: &mut Vec, - instances: &mut Vec, - assoc: &mut Vec, - is_image: bool, - ) -> Vec { - free.sort_by(|a, b| b.cmp(a)); - free.drain(..) - .filter_map(|i| { - instances.swap_remove(i); - assoc.swap_remove(i); - if i == instances.len() { - return None; - } - let id = assoc[i]; - let old = instances.len(); - Some(PrimitiveChange { - id, - is_image, - old, - new: i, - }) - }) - .collect() + slot as u32 } + /// Retires a slot, answering the mask it was drawn under so the caller + /// can drop that mask's ref. The slot itself only becomes reusable at + /// the next [`Self::apply_free`] -- see `freed`. 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 { + let slot = h.slot as usize; + if h.binding != IMAGE_BINDING { self.data.free(h.binding, h.data_idx); - self.free.push(h.inst_idx); - self.instances[h.inst_idx].mask_idx } + self.freed.push(slot); + self.instances[slot].mask_idx } - /// How many instances are still bound for the GPU -- the O(1) half of - /// the orphan check, so the O(primitives) walk below only runs on a - /// frame that already looks wrong. See + /// Hands this frame's freed slots back for reuse. Called once per + /// frame from `UiRenderState::update`, **after** every layer has + /// compacted its draw order, since that order is the only thing still + /// naming them. + pub fn release_freed(&mut self) { + self.reusable.append(&mut self.freed); + } + + /// Which widget drew the primitive in `slot` -- how a draw-order + /// change finds the handle it has to renumber. + pub fn owner(&self, slot: u32) -> WidgetId { + self.assoc[slot as usize] + } + + pub fn clear(&mut self) { + self.updated = true; + self.instances.clear(); + self.assoc.clear(); + self.freed.clear(); + self.reusable.clear(); + self.data.clear(); + } + + /// How many instances are still live -- the O(1) half of the orphan + /// check, so the O(primitives) walk below only runs on a frame that + /// already looks wrong. See /// [`crate::UiRenderState::orphaned_primitives`]. pub fn live_count(&self) -> usize { - (self.instances.len() - self.free.len()) + (self.images.len() - self.image_free.len()) + self.instances.len() - self.freed.len() - self.reusable.len() } - /// Every instance that is still bound for the GPU, as `(inst_idx, - /// owner, is_image)` -- everything except the slots already handed to - /// [`Self::free`] and waiting for [`Self::apply_free`] to compact them - /// away. Only [`crate::UiRenderState::orphaned_primitives`] uses this, - /// to check that every drawn primitive still belongs to a live widget. - pub fn live_instances(&self) -> impl Iterator + '_ { - let free: HashSet = self.free.iter().copied().collect(); - let image_free: HashSet = self.image_free.iter().copied().collect(); - let rects = (0..self.instances.len()) - .filter(move |i| !free.contains(i)) - .map(|i| (i, self.assoc[i], false)); - let images = (0..self.images.len()) - .filter(move |i| !image_free.contains(i)) - .map(|i| (i, self.image_assoc[i], true)); - rects.chain(images) + /// Every live instance as `(slot, owner, is_image)` -- everything + /// except the freed and the reusable. Only + /// [`crate::UiRenderState::orphaned_primitives`] uses this, to check + /// that every live primitive still belongs to a live widget. + pub fn live_instances(&self) -> impl Iterator + '_ { + let dead: HashSet = self.freed.iter().chain(&self.reusable).copied().collect(); + (0..self.instances.len()) + .filter(move |i| !dead.contains(i)) + .map(|i| { + ( + i as u32, + self.assoc[i], + self.instances[i].binding == IMAGE_BINDING, + ) + }) } pub fn data(&self) -> &PrimitiveData { @@ -311,46 +283,141 @@ impl Primitives { &self.instances } + pub fn instance(&self, slot: u32) -> &PrimitiveInstance { + &self.instances[slot as usize] + } + 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.slot as usize].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, +/// One layer's draw order: the slots of the global arena it draws, in the +/// order they were written. The vertex buffer of a layer is exactly this. +/// +/// Both lists free with `swap_remove`, so a layer's draw order was already +/// undefined before this split: nothing here may assume one primitive +/// stays adjacent to another once anything in the layer has been freed. +#[derive(Default)] +pub struct LayerOrder { + order: Vec, + /// Standalone images, kept apart because each draws with its own bind + /// group rather than sharing the layer's one instanced draw -- see + /// `UiRenderNode::draw`. + images: Vec, + free: Vec, + image_free: Vec, + pub updated: bool, } +impl LayerOrder { + pub fn push(&mut self, slot: u32, is_image: bool) -> usize { + self.updated = true; + let list = if is_image { + &mut self.images + } else { + &mut self.order + }; + list.push(slot); + list.len() - 1 + } + + /// Marks a position for removal. Deferred to [`Self::apply_free`] like + /// the arena's own, so that a position is only renumbered once per + /// frame however many were dropped. + pub fn free(&mut self, pos: usize, is_image: bool) { + self.updated = true; + if is_image { + self.image_free.push(pos); + } else { + self.free.push(pos); + } + } + + /// Compacts both lists, answering every primitive whose position + /// moved so its handle can be corrected. + pub fn apply_free(&mut self) -> Vec { + let mut changes = Self::apply_free_list(&mut self.free, &mut self.order, false); + changes.extend(Self::apply_free_list( + &mut self.image_free, + &mut self.images, + true, + )); + changes + } + + fn apply_free_list( + free: &mut Vec, + list: &mut Vec, + is_image: bool, + ) -> Vec { + // Descending, so removing a contiguous tail costs no renumbering + // at all -- which is what freeing one widget's primitives is. + free.sort_by(|a, b| b.cmp(a)); + free.drain(..) + .filter_map(|pos| { + list.swap_remove(pos); + if pos == list.len() { + return None; + } + Some(OrderChange { + slot: list[pos], + is_image, + pos, + }) + }) + .collect() + } + + pub fn order(&self) -> &Vec { + &self.order + } + + pub fn images(&self) -> &Vec { + &self.images + } +} + +/// A primitive whose position in a layer's draw order moved when +/// something before it was freed -- `slot` names which primitive, so its +/// owner's handle can be found and pointed at `pos`. +pub struct OrderChange { + pub slot: u32, + /// Which of the layer's two lists moved: their positions are + /// independent index spaces, so a handle matching on position alone + /// could take an image's renumbering for a rect's. + pub is_image: bool, + pub pos: usize, +} + +/// Where one primitive lives: its stable slot in the global arena, and +/// where in a layer's draw order it currently sits. A handle with no +/// layer position (`pos == NOT_DRAWN`) is a primitive that exists to be +/// *referenced* -- a mask's shape -- and is never rasterized. #[derive(Debug)] pub struct PrimitiveHandle { pub layer: usize, - pub inst_idx: usize, + pub pos: usize, + pub slot: u32, pub data_idx: usize, 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, - } + pub fn is_image(&self) -> bool { + self.binding == IMAGE_BINDING } } +pub struct PrimitiveInst

{ + pub id: WidgetId, + pub primitive: P, + pub region: UiRegion, + pub mask_idx: MaskIdx, + pub move_idx: MoveIdx, +} + primitives!( rects: RectPrimitive => 0, glyphs: GlyphPrimitive => 2, diff --git a/iris/core/src/render/shader.wgsl b/iris/core/src/render/shader.wgsl index 9b01ec7..0b6a737 100644 --- a/iris/core/src/render/shader.wgsl +++ b/iris/core/src/render/shader.wgsl @@ -83,6 +83,13 @@ var samp: sampler; var masks: array; @group(3) @binding(1) var move_offsets: array; +// Every primitive's placement, in one arena all layers share. The vertex +// stage reads the primitive it is drawing (its slot arrives as the only +// vertex attribute); the fragment stage reads a *mask's* primitive, which +// is generally a different one in a different layer. See LAYOUT.md's +// "Masks with a shape" and `Primitives` in primitive.rs. +@group(3) @binding(2) +var instances: array; // The bound on the parent walk, kept in step with `PARENT_CHAIN_LIMIT` in // render_state.rs, which walks the identical chain on the CPU side for @@ -117,15 +124,20 @@ struct WindowUniform { dim: vec2, }; +/// Mirrors `PrimitiveInstance` in data.rs -- the placement and what to +/// draw there. `x`/`y` are the `UiRegion`'s two spans. +struct PrimitiveInstance { + x: UiSpan, + y: UiSpan, + binding: u32, + idx: u32, + mask_idx: u32, + move_idx: u32, +} + +/// A layer's draw order: one slot into `instances` per instance drawn. struct InstanceInput { - @location(0) x_start: vec2, - @location(1) x_end: vec2, - @location(2) y_start: vec2, - @location(3) y_end: vec2, - @location(4) binding: u32, - @location(5) idx: u32, - @location(6) mask_idx: u32, - @location(7) move_idx: u32, + @location(0) slot: u32, } struct VertexOutput { @@ -151,13 +163,14 @@ fn vs_main( in: InstanceInput, ) -> VertexOutput { var out: VertexOutput; + let inst = instances[in.slot]; - let top_left_rel = vec2(in.x_start.x, in.y_start.x); - let top_left_abs = vec2(in.x_start.y, in.y_start.y); - let bot_right_rel = vec2(in.x_end.x, in.y_end.x); - let bot_right_abs = vec2(in.x_end.y, in.y_end.y); + let top_left_rel = vec2(inst.x.start.rel, inst.y.start.rel); + let top_left_abs = vec2(inst.x.start.abs, inst.y.start.abs); + let bot_right_rel = vec2(inst.x.end.rel, inst.y.end.rel); + let bot_right_abs = vec2(inst.x.end.abs, inst.y.end.abs); - let move_delta = resolve_move(in.move_idx); + let move_delta = resolve_move(inst.move_idx); let top_left = floor(top_left_rel * window.dim) + floor(top_left_abs) + move_delta; let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs) + move_delta; let size = bot_right - top_left; @@ -169,11 +182,11 @@ fn vs_main( let pos = (top_left + uv * size) / window.dim * 2.0 - 1.0; out.clip_position = vec4(pos.x, -pos.y, 0.0, 1.0); out.uv = uv; - out.binding = in.binding; - out.idx = in.idx; + out.binding = inst.binding; + out.idx = inst.idx; out.top_left = top_left; out.bot_right = bot_right; - out.mask_idx = in.mask_idx; + out.mask_idx = inst.mask_idx; return out; } diff --git a/iris/core/src/ui/painter.rs b/iris/core/src/ui/painter.rs index 0eec9a3..7912780 100644 --- a/iris/core/src/ui/painter.rs +++ b/iris/core/src/ui/painter.rs @@ -26,7 +26,7 @@ pub struct Painter<'a> { impl<'a> Painter<'a> { fn primitive_at(&mut self, primitive: P, region: UiRegion) { - let h = self.state.layers.write( + let h = self.state.write_primitive( self.layer, PrimitiveInst { id: self.id, @@ -223,7 +223,7 @@ impl<'a> Painter<'a> { /// 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( + let h = self.state.write_image( self.layer, self.id, texture_idx, diff --git a/iris/core/src/ui/render_state.rs b/iris/core/src/ui/render_state.rs index 46d687e..ce3cae8 100644 --- a/iris/core/src/ui/render_state.rs +++ b/iris/core/src/ui/render_state.rs @@ -4,7 +4,7 @@ use std::time::{Duration, Instant}; use crate::{ ActiveData, IdLike, MaskIdx, MoveIdx, Painter, PixelRegion, PrimitiveLayers, RegionAlign, StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets, - render::{IMAGE_BINDING, MoveOffset}, + render::{MoveOffset, Primitive, PrimitiveHandle, PrimitiveInst, Primitives}, util::{HashMap, HashSet, Id, Vec2}, }; @@ -25,6 +25,10 @@ pub enum RedrawKind { pub struct UiRenderState { pub active: HashMap, + /// Every primitive in the tree, in one arena -- see [`Primitives`] for + /// why it is not per layer. + pub primitives: Primitives, + /// What each layer draws, in order: slots into `primitives`. pub layers: PrimitiveLayers, pub(super) output_size: Vec2, /// Physical pixels per `dp` -- see `Len::dp`'s field doc. `1.0` (an @@ -124,6 +128,7 @@ impl UiRenderState { pub fn new() -> Self { Self { active: Default::default(), + primitives: Default::default(), layers: Default::default(), output_size: Vec2::ZERO, density: 1.0, @@ -161,6 +166,70 @@ impl UiRenderState { ) } + /// Writes a primitive into the arena and into `layer`'s draw order. + pub(super) fn write_primitive( + &mut self, + layer: usize, + inst: PrimitiveInst

, + ) -> PrimitiveHandle { + let (slot, data_idx) = self.primitives.alloc(inst); + let pos = self.layers[layer].push(slot, false); + PrimitiveHandle { + layer, + pos, + slot, + data_idx, + binding: P::BINDING, + } + } + + /// A standalone image, which draws with its own bind group rather + /// than sharing the layer's one instanced draw. + pub(super) fn write_image( + &mut self, + layer: usize, + id: WidgetId, + texture_idx: u32, + region: UiRegion, + mask_idx: MaskIdx, + move_idx: MoveIdx, + ) -> PrimitiveHandle { + let slot = self + .primitives + .alloc_image(id, texture_idx, region, mask_idx, move_idx); + let pos = self.layers[layer].push(slot, true); + PrimitiveHandle { + layer, + pos, + slot, + data_idx: 0, + binding: crate::render::IMAGE_BINDING, + } + } + + /// Compacts every layer's draw order around the primitives freed + /// this frame, corrects the handles that moved, and only then hands + /// the arena slots back for reuse -- that order is the whole reason + /// `Primitives::freed` exists. Once per frame, at the end of + /// [`Self::update`], so the harness (which has no renderer) applies + /// it exactly as a real backend does. + fn apply_free(&mut self) { + for (layer, order) in self.layers.iter_mut() { + for change in order.apply_free() { + let owner = self.primitives.owner(change.slot); + if let Some(active) = self.active.get_mut(&owner) { + for h in &mut active.primitives { + if h.layer == layer && h.slot == change.slot { + h.pos = change.pos; + break; + } + } + } + } + } + self.primitives.release_freed(); + } + pub fn resize(&mut self, size: impl Into) { self.output_size = size.into(); self.resized = true; @@ -232,6 +301,10 @@ impl UiRenderState { self.last_layout = layout_start.elapsed(); self.last_redraw_kind = kind; self.frame_no += 1; + // After the redraw and before anything reads the frame: every + // slot freed above is still named by its layer's draw order until + // this runs. + self.apply_free(); #[cfg(debug_assertions)] debug_assert!(self.primitive_counts_agree(), "{}", self.orphan_report(rsc),); } @@ -379,7 +452,7 @@ impl UiRenderState { // instead of redrawing. See LAYOUT.md section 3. let from = active.region; for h in &active.primitives { - let r = self.layers[h.layer].region_mut(h); + let r = self.primitives.region_mut(h); *r = r.outside(&from).within(®ion); self.region_mut_count += 1; } @@ -616,7 +689,8 @@ impl UiRenderState { let mut active = self.active.remove(&id); if let Some(active) = &mut active { for h in &active.primitives { - let mask = self.layers.free(h); + let mask = self.primitives.free(h); + self.layers[h.layer].free(h.pos, h.is_image()); if mask != MaskIdx::NONE { rsc.ui_mut().masks.remove(mask); } @@ -676,6 +750,7 @@ impl UiRenderState { rsc.on_undraw(&active); } self.layers.clear(); + self.primitives.clear(); rsc.widgets_mut().needs_redraw.clear(); rsc.free(); } @@ -718,8 +793,9 @@ impl UiRenderState { /// Primitive instances still bound for the GPU whose owner is no /// longer in `active`, or whose owner's `ActiveData` no longer names /// them: a copy nothing can move, clip, resize or free, redrawn every - /// frame at whatever position it last had. `(layer, inst_idx, owner)` - /// each. + /// frame at whatever position it last had. `(slot, owner)` each -- + /// the arena knows which primitive, not which layer's draw order still + /// names it. /// /// Asserted empty at the end of every [`Self::update`], because this /// is exactly the shape of the duplicated transcript row on Iris's @@ -728,20 +804,15 @@ impl UiRenderState { /// much alive -- it is the *earlier* set of primitives that got /// stranded when the widget was drawn a second time without the first /// draw being freed. O(primitives), debug builds only. - pub fn orphaned_primitives(&self) -> Vec<(usize, usize, WidgetId)> { + pub fn orphaned_primitives(&self) -> Vec<(u32, WidgetId)> { let mut orphans = Vec::new(); - for (layer, primitives) in self.layers.iter() { - for (inst_idx, owner, is_image) in primitives.live_instances() { - let owned = self.active.get(&owner).is_some_and(|a| { - a.primitives.iter().any(|h| { - h.layer == layer - && h.inst_idx == inst_idx - && (h.binding == IMAGE_BINDING) == is_image - }) - }); - if !owned { - orphans.push((layer, inst_idx, owner)); - } + for (slot, owner, _) in self.primitives.live_instances() { + let owned = self + .active + .get(&owner) + .is_some_and(|a| a.primitives.iter().any(|h| h.slot == slot)); + if !owned { + orphans.push((slot, owner)); } } orphans @@ -755,7 +826,7 @@ impl UiRenderState { /// transcript is tens of thousands and made a debug build on a phone /// too slow to finish a benchmark run. fn primitive_counts_agree(&self) -> bool { - let live: usize = self.layers.iter().map(|(_, p)| p.live_count()).sum(); + let live: usize = self.primitives.live_count(); let owned: usize = self.active.values().map(|a| a.primitives.len()).sum(); live == owned } @@ -769,10 +840,10 @@ impl UiRenderState { let mut lines: Vec = orphans .iter() .take(8) - .map(|(layer, idx, owner)| { + .map(|(slot, owner)| { let alive = self.active.contains_key(owner); format!( - " layer {layer} instance {idx}: owner '{}' ({owner:?}), owner still active: {alive}", + " instance {slot}: owner '{}' ({owner:?}), owner still active: {alive}", rsc.widgets().label(*owner), ) }) @@ -817,12 +888,12 @@ impl UiRenderState { } pub fn debug_layers(&self) { - for ((idx, depth), primitives) in self.layers.iter_depth() { + for ((idx, depth), order) in self.layers.iter_depth() { let indent = " ".repeat(depth * 2); - let len = primitives.instances().len(); + let len = order.order().len(); print!("{indent}{idx}: {len} primitives"); if len >= 1 { - print!(" ({})", primitives.instances()[0].binding); + print!(" ({})", self.primitives.instance(order.order()[0]).binding); } println!(); } diff --git a/iris/transcript-fixture/tests/top_edge.rs b/iris/transcript-fixture/tests/top_edge.rs index f0589c7..3589cb3 100644 --- a/iris/transcript-fixture/tests/top_edge.rs +++ b/iris/transcript-fixture/tests/top_edge.rs @@ -166,7 +166,7 @@ fn primitives_under(h: &Harness, id: WidgetId) -> Vec { .primitives .iter() .filter(|p| p.binding != IMAGE_BINDING) - .map(|p| h.render.layers[p.layer].instances()[p.inst_idx].mask_idx) + .map(|p| h.render.primitives.instance(p.slot).mask_idx) .collect(); for child in &active.children { out.extend(primitives_under(h, *child));