From 4d9839f3800411605646faa7e5f4d5d14986a7ee Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sun, 13 Sep 2026 14:07:37 -0400 Subject: [PATCH] Draw each primitive with its own pipeline, registered rather than declared The `primitives!` macro, `PrimitiveData`, `PrimitiveVec`, `PrimitiveBuffers`, the `Primitive` trait and the shader's dispatch switch are gone. A primitive is now a registration: its WGSL and, implicitly, the size of the entry that WGSL reads. Everything else -- its instance list, its free list, its buffers, its bind group and its pipeline -- follows from that, so adding one is a `register` call and a shader file, with nothing per-type to remember and no cross-type dispatch to extend. Nothing dispatches dynamically. Push, free, renumber, upload and draw are identical for every primitive; what differs is the entry size and the pipeline, which are data. So `InstanceList` carries a runtime stride and its instances' data as bytes, and one concrete type serves every primitive and the textures. Measured against a typed list it costs 0.2ns per write, where a trait object costs 1.6ns. Because each type has its own list, an instance's index is also its data index: `@builtin(instance_index)` replaces the `idx` field, the `binding` field goes with the switch, and `PrimitiveInstance` drops from 28 bytes to 20. `PrimitiveVec`'s free list merges into the instance list's, so an instance and its data are freed by one `swap_remove` rather than two arenas kept in step. `shader.wgsl` becomes `shader/prelude.wgsl` plus one file per primitive. The prelude carries the window, masks, sampled texture, vertex shader and `masked()`, and is compiled ahead of each primitive's own source -- which is also what a caller's own primitive would be. Masks move back into group 0 beside the window uniform, since every pipeline shares one layout. Within a layer, types now draw in registration order: a rect under a glyph under a texture. Order within a layer was never meaningful -- freeing an instance swaps another into its place -- so this replaces an accident with a defined order, and backgrounds land under their content. Verified with a headless run per case: text over its own rect and an image over its own rect in one layer, a masked stack clipping, the text-layout tab, and adding three images and deleting one. --- core/src/primitive/layer.rs | 12 +- core/src/render/data.rs | 6 +- core/src/render/mod.rs | 406 ++++++++++++++++------------ core/src/render/primitive.rs | 376 +++++++++++--------------- core/src/render/shader.wgsl | 198 -------------- core/src/render/shader/glyph.wgsl | 24 ++ core/src/render/shader/prelude.wgsl | 102 +++++++ core/src/render/shader/rect.wgsl | 39 +++ core/src/render/shader/texture.wgsl | 7 + core/src/render/util/mod.rs | 6 +- core/src/ui/mod.rs | 6 +- core/src/ui/painter.rs | 33 ++- core/src/ui/render_state.rs | 2 +- src/widget/rect.rs | 15 +- src/widget/text/edit.rs | 2 + 15 files changed, 614 insertions(+), 620 deletions(-) delete mode 100644 core/src/render/shader.wgsl create mode 100644 core/src/render/shader/glyph.wgsl create mode 100644 core/src/render/shader/prelude.wgsl create mode 100644 core/src/render/shader/rect.wgsl create mode 100644 core/src/render/shader/texture.wgsl diff --git a/core/src/primitive/layer.rs b/core/src/primitive/layer.rs index 7d0b770..51c4048 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::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst}, + render::{LayerDraws, MaskIdx, PrimitiveHandle, PrimitiveKind}, util::to_mut, }; @@ -121,12 +121,16 @@ impl Layers { } impl DrawLayers { - pub fn write( + pub fn write( &mut self, layer: LayerId, - info: PrimitiveInst

, + kind: PrimitiveKind

, + id: WidgetId, + primitive: P, + region: UiRegion, + mask_idx: MaskIdx, ) -> PrimitiveHandle { - self[layer].write(layer, info) + self[layer].write(layer, kind, id, primitive, region, mask_idx) } pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { diff --git a/core/src/render/data.rs b/core/src/render/data.rs index 2953065..032539f 100644 --- a/core/src/render/data.rs +++ b/core/src/render/data.rs @@ -12,20 +12,16 @@ pub struct WindowUniform { #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct PrimitiveInstance { pub region: UiRegion, - pub binding: u32, - pub idx: u32, pub mask_idx: MaskIdx, } impl PrimitiveInstance { - const ATTRIBS: [VertexAttribute; 7] = vertex_attr_array![ + const ATTRIBS: [VertexAttribute; 5] = vertex_attr_array![ 0 => Float32x2, 1 => Float32x2, 2 => Float32x2, 3 => Float32x2, 4 => Uint32, - 5 => Uint32, - 6 => Uint32, ]; pub fn desc() -> VertexBufferLayout<'static> { diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index 152836f..d161560 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -25,16 +25,22 @@ pub use atlas::*; pub use data::{Mask, MaskIdx}; pub use primitive::*; -const SHAPE_SHADER: &str = include_str!("./shader.wgsl"); +const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); +const TEXTURE_SHADER: &str = include_str!("./shader/texture.wgsl"); pub struct UiRenderNode { - uniform_group: BindGroup, - primitive_layout: BindGroupLayout, + device: Device, + shared_layout: BindGroupLayout, + shared_group: BindGroup, + data_layout: BindGroupLayout, texture_layout: BindGroupLayout, - mask_layout: BindGroupLayout, - mask_group: BindGroup, + pipeline_layout: PipelineLayout, + format: TextureFormat, - pipeline: RenderPipeline, + /// One per registered primitive, in id order. The texture pipeline is + /// apart because a texture binds group 2 per instance, not per draw. + pipelines: Vec, + texture_pipeline: RenderPipeline, layers: HashMap, active: Vec, @@ -46,30 +52,41 @@ pub struct UiRenderNode { } struct RenderLayer { - instance: ArrBuf, - primitives: PrimitiveBuffers, - primitive_group: BindGroup, - 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. + /// 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 list's vertex buffer and the data its shader reads at group 1. +struct ListBuffers { + instance: ArrBuf, + data: ArrBuf, + group: Option, +} + 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, &[]); + pass.set_bind_group(0, &self.shared_group, &[]); for i in &self.active { let layer = &self.layers[i]; - pass.set_bind_group(1, &layer.primitive_group, &[]); - 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); + // Types run in registration order, so a rect is under a glyph is + // under a texture. Ordering beyond that is what `Layers` is for -- + // freeing an instance swaps another into its place. + pass.set_bind_group(2, self.pages.group(), &[]); + for (id, list) in layer.primitives.iter().enumerate() { + let (Some(group), true) = (&list.group, list.instance.len() > 0) else { + continue; + }; + pass.set_pipeline(&self.pipelines[id]); + pass.set_bind_group(1, 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() { - pass.set_vertex_buffer(0, layer.texture_instance.buffer.slice(..)); + pass.set_pipeline(&self.texture_pipeline); + 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; @@ -101,52 +118,40 @@ impl UiRenderNode { } } } - 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, - texture_instance: ArrBuf::new( - device, - BufferUsages::VERTEX | BufferUsages::COPY_DST, - "texture instance", - ), - texture_slots: Vec::new(), - } - }); + let rlayer = self + .layers + .entry(i) + .or_insert_with(|| RenderLayer::new(device)); if draws.updated { - rlayer - .instance - .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(), - ); + .resize_with(draws.primitives().len(), || ListBuffers::new(device)); + for (list, draws) in rlayer.primitives.iter_mut().zip(draws.primitives()) { + list.update(device, queue, &self.data_layout, draws); + } rlayer - .texture_instance - .update(device, queue, draws.textures.instances()); + .textures + .update(device, queue, &self.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(draws.textures.instances().iter().map(|inst| inst.idx)); + .extend(slots.iter().copied().map(u32::from_ne_bytes)); draws.updated = false; } } + self.build_pipelines(&ui.primitives); if ui.masks.changed { ui.masks.changed = false; if self.masks.update(device, queue, &ui.masks[..]) { - self.mask_group = Self::mask_group(device, &self.mask_layout, &self.masks); + self.shared_group = Self::shared_group( + device, + &self.shared_layout, + &self.window_buffer, + &self.masks, + ); } } self.pages @@ -165,11 +170,6 @@ impl UiRenderNode { } pub fn new(device: &Device, queue: &Queue, config: &SurfaceConfiguration) -> Self { - let shader = device.create_shader_module(ShaderModuleDescriptor { - label: Some("UI Shape Shader"), - source: ShaderSource::Wgsl(SHAPE_SHADER.into()), - }); - let window_uniform = WindowUniform { width: config.width as f32, height: config.height as f32, @@ -180,74 +180,95 @@ impl UiRenderNode { usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, }); - let uniform_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, - ty: BindingType::Buffer { - ty: BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - label: Some("window"), - }); - - let uniform_group = Self::bind_group_0(device, &uniform_layout, &window_buffer); - - let primitive_layout = device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &PrimitiveBuffers::BINDINGS.map(|binding| BindGroupLayoutEntry { - binding, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Buffer { - ty: BufferBindingType::Storage { read_only: true }, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }), - label: Some("primitive"), - }); + let shared_layout = Self::shared_layout(device); + let data_layout = Self::data_layout(device); + let texture_layout = Self::texture_layout(device); let masks = ArrBuf::new( device, BufferUsages::STORAGE | BufferUsages::COPY_DST, "ui 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 shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks); let sampler = default_sampler(device); let pages = GpuPages::new(device, queue, &texture_layout, &sampler); let textures = GpuTextures::new(device, queue); + // One layout for every pipeline, so a primitive that does not sample + // or read a buffer binds what is there instead of needing its own. let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { - label: Some("UI Shape Pipeline Layout"), - bind_group_layouts: &[ - &uniform_layout, - &primitive_layout, - &texture_layout, - &mask_layout, - ], + label: Some("ui"), + bind_group_layouts: &[&shared_layout, &data_layout, &texture_layout], immediate_size: 0, }); - let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { - label: Some("UI Shape Pipeline"), - layout: Some(&pipeline_layout), + let texture_pipeline = Self::pipeline( + device, + &pipeline_layout, + config.format, + TEXTURE_SHADER, + "texture", + ); + + Self { + device: device.clone(), + shared_layout, + shared_group, + data_layout, + texture_layout, + pipeline_layout, + format: config.format, + pipelines: Vec::new(), + texture_pipeline, + window_buffer, + layers: HashMap::default(), + active: Vec::new(), + sampler, + pages, + textures, + masks, + } + } + + /// Compiles a pipeline for every primitive registered since the last call. + /// Sources only ever arrive at the end, so an id keeps its pipeline. + fn build_pipelines(&mut self, registry: &PrimitiveRegistry) { + for source in ®istry.sources()[self.pipelines.len()..] { + self.pipelines.push(Self::pipeline( + &self.device, + &self.pipeline_layout, + self.format, + source.wgsl, + source.label, + )); + } + } + + fn pipeline( + device: &Device, + layout: &PipelineLayout, + format: TextureFormat, + wgsl: &str, + label: &str, + ) -> RenderPipeline { + let module = device.create_shader_module(ShaderModuleDescriptor { + label: Some(label), + source: ShaderSource::Wgsl(format!("{PRELUDE}\n{wgsl}").into()), + }); + device.create_render_pipeline(&RenderPipelineDescriptor { + label: Some(label), + layout: Some(layout), vertex: VertexState { - module: &shader, + module: &module, entry_point: Some("vs_main"), buffers: &[PrimitiveInstance::desc()], compilation_options: Default::default(), }, fragment: Some(FragmentState { - module: &shader, + module: &module, entry_point: Some("fs_main"), targets: &[Some(ColorTargetState { - format: config.format, + format, blend: Some(BlendState::ALPHA_BLENDING), write_mask: ColorWrites::ALL, })], @@ -270,52 +291,74 @@ impl UiRenderNode { }, multiview_mask: None, cache: None, - }); - - Self { - uniform_group, - primitive_layout, - texture_layout, - mask_layout, - mask_group, - pipeline, - window_buffer, - layers: HashMap::default(), - active: Vec::new(), - sampler, - pages, - textures, - masks, - } - } - - fn bind_group_0( - device: &Device, - layout: &BindGroupLayout, - window_buffer: &Buffer, - ) -> BindGroup { - device.create_bind_group(&BindGroupDescriptor { - layout, - entries: &[BindGroupEntry { - binding: 0, - resource: window_buffer.as_entire_binding(), - }], - label: Some("ui window"), }) } - fn primitive_group( + /// Group 0: what every draw in the ui shares. + fn shared_layout(device: &Device) -> BindGroupLayout { + device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + 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 shared"), + }) + } + + fn shared_group( device: &Device, layout: &BindGroupLayout, - buffers: [(u32, &Buffer); PrimitiveBuffers::LEN], + window: &Buffer, + masks: &ArrBuf, ) -> BindGroup { device.create_bind_group(&BindGroupDescriptor { layout, - entries: &buffers.map(|(binding, buf)| BindGroupEntry { - binding, - resource: buf.as_entire_binding(), - }), - label: Some("ui primitives"), + entries: &[ + BindGroupEntry { + binding: 0, + resource: window.as_entire_binding(), + }, + BindGroupEntry { + binding: 1, + resource: masks.buffer.as_entire_binding(), + }, + ], + label: Some("ui shared"), + }) + } + + /// Group 1: one list's per-instance data, whatever its shader reads it as. + fn data_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 primitive data"), }) } @@ -345,36 +388,55 @@ impl UiRenderNode { }) } - /// 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: masks.buffer.as_entire_binding(), - }], - label: Some("ui masks"), - }) - } - pub fn texture_count(&self) -> usize { self.textures.count() } } + +impl RenderLayer { + fn new(device: &Device) -> Self { + Self { + primitives: Vec::new(), + textures: ListBuffers::new(device), + texture_slots: Vec::new(), + } + } +} + +impl ListBuffers { + fn new(device: &Device) -> Self { + Self { + instance: ArrBuf::new( + device, + BufferUsages::VERTEX | BufferUsages::COPY_DST, + "instance", + ), + data: ArrBuf::new( + device, + BufferUsages::STORAGE | BufferUsages::COPY_DST, + "primitive data", + ), + group: None, + } + } + + fn update( + &mut self, + device: &Device, + queue: &Queue, + layout: &BindGroupLayout, + list: &InstanceList, + ) { + self.instance.update(device, queue, list.instances()); + if self.data.update(device, queue, list.data()) || self.group.is_none() { + self.group = Some(device.create_bind_group(&BindGroupDescriptor { + layout, + entries: &[BindGroupEntry { + binding: 0, + resource: self.data.buffer.as_entire_binding(), + }], + label: Some("ui primitive data"), + })); + } + } +} diff --git a/core/src/render/primitive.rs b/core/src/render/primitive.rs index ffa5c82..5cc13c3 100644 --- a/core/src/render/primitive.rs +++ b/core/src/render/primitive.rs @@ -1,68 +1,143 @@ -use std::ops::{Deref, DerefMut}; +use std::marker::PhantomData; use crate::{ Color, UiRegion, WidgetId, - render::{ - ArrBuf, - data::{MaskIdx, PrimitiveInstance}, - }, + render::data::{MaskIdx, PrimitiveInstance}, util::Vec2, }; use bytemuck::Pod; -use wgpu::*; -/// 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, +/// 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

{ + id: u32, + _p: PhantomData, } -impl Default for LayerDraws { - fn default() -> Self { +impl

PrimitiveKind

{ + const fn new(id: u32) -> Self { Self { - primitives: Default::default(), - textures: Default::default(), - updated: true, + id, + _p: PhantomData, } } + + pub fn id(&self) -> u32 { + self.id + } } -/// 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. +impl

Clone for PrimitiveKind

{ + fn clone(&self) -> Self { + *self + } +} + +impl

Copy for PrimitiveKind

{} + +pub const RECT: PrimitiveKind = PrimitiveKind::new(0); +pub const GLYPH: PrimitiveKind = PrimitiveKind::new(1); + +/// 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 +/// nothing here knows its type. +/// +/// A source is compiled after `prelude.wgsl` and supplies its data at +/// `@group(1) @binding(0)` and an `fs_main` shading one instance. +pub struct PrimitiveRegistry { + kinds: Vec, +} + +pub struct PrimitiveSource { + pub wgsl: &'static str, + pub label: &'static str, +} + +impl Default for PrimitiveRegistry { + fn default() -> Self { + let mut kinds = Self { kinds: Vec::new() }; + assert_eq!( + kinds + .register::(include_str!("shader/rect.wgsl"), "rect") + .id(), + RECT.id() + ); + assert_eq!( + kinds + .register::(include_str!("shader/glyph.wgsl"), "glyph") + .id(), + GLYPH.id() + ); + kinds + } +} + +impl PrimitiveRegistry { + pub fn register( + &mut self, + wgsl: &'static str, + label: &'static str, + ) -> PrimitiveKind

{ + self.kinds.push(PrimitiveSource { wgsl, label }); + PrimitiveKind::new(self.kinds.len() as u32 - 1) + } + + pub fn sources(&self) -> &[PrimitiveSource] { + &self.kinds + } +} + +/// 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, + Primitive(u32), 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. +/// Instances, the widget each belongs to, the slots waiting to be reused, and +/// `stride` bytes of 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)] pub struct InstanceList { instances: Vec, assoc: Vec, free: Vec, + data: Vec, + stride: usize, } impl InstanceList { - pub fn instances(&self) -> &Vec { + pub fn instances(&self) -> &[PrimitiveInstance] { &self.instances } - fn push(&mut self, id: WidgetId, inst: PrimitiveInstance) -> usize { + pub fn data(&self) -> &[u8] { + &self.data + } + + 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; + self.data[i * self.stride..][..self.stride].copy_from_slice(data); i } else { let i = self.instances.len(); self.instances.push(inst); self.assoc.push(id); + self.data.extend_from_slice(data); i } } @@ -76,156 +151,75 @@ impl InstanceList { self.free.sort_by(|a, b| b.cmp(a)); let instances = &mut self.instances; let assoc = &mut self.assoc; + let data = &mut self.data; + let stride = self.stride; self.free.drain(..).filter_map(move |i| { instances.swap_remove(i); assoc.swap_remove(i); - if i == instances.len() { + let last = instances.len(); + data.copy_within(last * stride..(last + 1) * stride, i * stride); + data.truncate(last * stride); + if i == last { return None; } let id = assoc[i]; - let old = instances.len(); Some(PrimitiveChange { id, kind, - old, + old: last, new: i, }) }) } } -pub trait Primitive: Pod { - const BINDING: u32; - fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec; +/// 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. +pub struct LayerDraws { + primitives: Vec, + pub textures: InstanceList, + pub updated: bool, } -macro_rules! primitives { - ($($name:ident: $ty:ty => $binding:expr,)*) => { - #[derive(Default)] - pub struct PrimitiveData { - $(pub(crate) $name: PrimitiveVec<$ty>,)* +impl Default for LayerDraws { + fn default() -> Self { + Self { + primitives: Vec::new(), + textures: InstanceList::default(), + updated: true, } - - pub struct PrimitiveBuffers { - $($name: ArrBuf<$ty>,)* - } - - impl PrimitiveBuffers { - pub fn update(&mut self, device: &Device, queue: &Queue, data: &PrimitiveData) { - $(self.$name.update(device, queue, &data.$name);)* - } - } - - impl PrimitiveBuffers { - pub const LEN: usize = primitives!(@count $($name)*); - pub const BINDINGS: [u32; Self::LEN] = [$(<$ty>::BINDING,)*]; - pub fn buffers(&self) -> [(u32, &Buffer); Self::LEN] { - [ - $((<$ty>::BINDING, &self.$name.buffer),)* - ] - } - pub fn new(device: &Device) -> Self { - Self { - $($name: ArrBuf::new( - device, - BufferUsages::STORAGE | BufferUsages::COPY_DST, - stringify!($name), - ),)* - } - } - } - - impl PrimitiveData { - pub fn clear(&mut self) { - $(self.$name.clear();)* - } - pub fn free(&mut self, binding: u32, idx: usize) { - match binding { - $(<$ty>::BINDING => self.$name.free(idx),)* - _ => unreachable!() - } - } - } - - $( - unsafe impl bytemuck::Pod for $ty {} - unsafe impl bytemuck::Zeroable for $ty {} - impl Primitive for $ty { - const BINDING: u32 = $binding; - fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec { - &mut data.$name - } - } - )* - }; - (@count $t1:tt $($t:tt)+) => { 1 + primitives!(@count $($t)+) }; - (@count $t:tt) => { 1 }; -} - -pub struct PrimitiveInst

{ - pub id: WidgetId, - pub primitive: P, - pub region: UiRegion, - 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 { - fn write( - &mut self, - PrimitiveInst { - id, - primitive, - region, - mask_idx, - }: PrimitiveInst

, - ) -> usize { - let idx = P::vec(&mut self.data).add(primitive) as u32; - self.list.push( - id, - PrimitiveInstance { - region, - idx, - mask_idx, - binding: P::BINDING, - }, - ) - } - - 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 { + pub fn write( + &mut self, + layer: usize, + kind: PrimitiveKind

, + id: WidgetId, + primitive: P, + region: UiRegion, + mask_idx: MaskIdx, + ) -> PrimitiveHandle { self.updated = true; + if self.primitives.len() <= kind.id as usize { + self.primitives + .resize_with(kind.id as usize + 1, Default::default); + } + let inst_idx = self.primitives[kind.id as usize].push( + id, + PrimitiveInstance { region, mask_idx }, + bytemuck::bytes_of(&primitive), + ); PrimitiveHandle { layer, - kind: InstanceKind::Primitive, - inst_idx: self.primitives.write(inst), + kind: InstanceKind::Primitive(kind.id), + inst_idx, } } - /// Writes an instance that samples the texture in slot `texture` instead of - /// reading a group-1 buffer. + /// 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, @@ -240,16 +234,16 @@ impl LayerDraws { kind: InstanceKind::Texture, inst_idx: self.textures.push( id, - PrimitiveInstance { - region, - idx: texture, - mask_idx, - binding: TEXTURE_BINDING, - }, + PrimitiveInstance { region, mask_idx }, + bytemuck::bytes_of(&texture), ), } } + pub fn primitives(&self) -> &[InstanceList] { + &self.primitives + } + pub fn apply_free(&mut self) -> impl Iterator { let Self { primitives, @@ -257,17 +251,15 @@ impl LayerDraws { .. } = self; primitives - .list - .apply_free(InstanceKind::Primitive) + .iter_mut() + .enumerate() + .flat_map(|(i, list)| list.apply_free(InstanceKind::Primitive(i as u32))) .chain(textures.apply_free(InstanceKind::Texture)) } pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { self.updated = true; - match h.kind { - InstanceKind::Primitive => self.primitives.free(h.inst_idx), - InstanceKind::Texture => self.textures.free(h.inst_idx), - } + self.list_mut(h.kind).free(h.inst_idx) } pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion { @@ -277,7 +269,7 @@ impl LayerDraws { fn list_mut(&mut self, kind: InstanceKind) -> &mut InstanceList { match kind { - InstanceKind::Primitive => &mut self.primitives.list, + InstanceKind::Primitive(i) => &mut self.primitives[i as usize], InstanceKind::Texture => &mut self.textures, } } @@ -297,11 +289,6 @@ pub struct PrimitiveHandle { pub inst_idx: usize, } -primitives!( - rects: RectPrimitive => 0, - glyphs: GlyphPrimitive => 1, -); - #[repr(C)] #[derive(Copy, Clone)] pub struct RectPrimitive { @@ -311,6 +298,9 @@ pub struct RectPrimitive { pub inner_radius: f32, } +unsafe impl bytemuck::Pod for RectPrimitive {} +unsafe impl bytemuck::Zeroable for RectPrimitive {} + impl RectPrimitive { pub fn color(color: Color) -> Self { Self { @@ -335,53 +325,5 @@ pub struct GlyphPrimitive { pub flags: u32, } -pub struct PrimitiveVec { - vec: Vec, - free: Vec, -} - -impl PrimitiveVec { - pub fn new() -> Self { - Self { - vec: Vec::new(), - free: Vec::new(), - } - } - pub fn add(&mut self, t: T) -> usize { - if let Some(i) = self.free.pop() { - self.vec[i] = t; - i - } else { - let i = self.vec.len(); - self.vec.push(t); - i - } - } - pub fn free(&mut self, i: usize) { - self.free.push(i); - } - pub fn clear(&mut self) { - self.free.clear(); - self.vec.clear(); - } -} - -impl Default for PrimitiveVec { - fn default() -> Self { - Self::new() - } -} - -impl Deref for PrimitiveVec { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.vec - } -} - -impl DerefMut for PrimitiveVec { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.vec - } -} +unsafe impl bytemuck::Pod for GlyphPrimitive {} +unsafe impl bytemuck::Zeroable for GlyphPrimitive {} diff --git a/core/src/render/shader.wgsl b/core/src/render/shader.wgsl deleted file mode 100644 index 09b16ba..0000000 --- a/core/src/render/shader.wgsl +++ /dev/null @@ -1,198 +0,0 @@ -const RECT: u32 = 0u; -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; -@group(1) @binding(RECT) -var rects: array; -@group(1) @binding(GLYPH) -var glyphs: array; - -struct Rect { - color: u32, - radius: f32, - thickness: f32, - inner_radius: f32, -} - -struct GlyphInfo { - uv_min: vec2, - uv_max: vec2, - layer: u32, - color: u32, - flags: u32, -} - -struct Mask { - x: UiSpan, - y: UiSpan, -} - -struct UiSpan { - start: UiScalar, - end: UiScalar, -} - -struct UiScalar { - rel: f32, - abs: f32, -} - -struct UiVec2 { - rel: vec2, - abs: vec2, -} - -// 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) -var samp: sampler; -@group(3) @binding(0) -var masks: array; - -struct WindowUniform { - dim: vec2, -}; - -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, -} - -struct VertexOutput { - @location(0) top_left: vec2, - @location(1) bot_right: vec2, - @location(2) uv: vec2, - @location(3) binding: u32, - @location(4) idx: u32, - @location(5) mask_idx: u32, - @builtin(position) clip_position: vec4, -}; - -struct Region { - pos: vec2, - uv: vec2, - top_left: vec2, - bot_right: vec2, -} - -@vertex -fn vs_main( - @builtin(vertex_index) vi: u32, - in: InstanceInput, -) -> VertexOutput { - var out: VertexOutput; - - 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 = floor(top_left_rel * window.dim) + floor(top_left_abs); - let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs); - let size = bot_right - top_left; - - let uv = vec2( - f32(vi % 2u), - f32(vi / 2u) - ); - 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.top_left = top_left; - out.bot_right = bot_right; - out.mask_idx = in.mask_idx; - - return out; -} - -@fragment -fn fs_main( - in: VertexOutput -) -> @location(0) vec4 { - let pos = in.clip_position.xy; - let region = Region(pos, in.uv, in.top_left, in.bot_right); - let i = in.idx; - var color: vec4; - switch in.binding { - case RECT: { - color = draw_rounded_rect(region, rects[i]); - } - case TEXTURE: { - color = draw_texture(region); - } - case GLYPH: { - color = draw_glyph(region, glyphs[i]); - } - default: { - color = vec4(1.0, 0.0, 1.0, 1.0); - } - } - if in.mask_idx != 4294967295u { - let mask = masks[in.mask_idx]; - let tl = UiVec2(vec2(mask.x.start.rel, mask.y.start.rel), vec2(mask.x.start.abs, mask.y.start.abs)); - let br = UiVec2(vec2(mask.x.end.rel, mask.y.end.rel), vec2(mask.x.end.abs, mask.y.end.abs)); - - let top_left = floor(tl.rel * window.dim) + floor(tl.abs); - let bot_right = floor(br.rel * window.dim) + floor(br.abs); - if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y { - color *= 0.0; - } - } - return color; -} - -fn draw_texture(region: Region) -> vec4 { - 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(tex, samp, uv, i32(g.layer)); - if (g.flags & 1u) != 0u { - return texel; - } - var color = unpack4x8unorm(g.color); - color.a *= texel.a; - return color; -} - -fn draw_rounded_rect(region: Region, rect: Rect) -> vec4 { - var color = unpack4x8unorm(rect.color); - - let edge = 0.5; - - let size = region.bot_right - region.top_left; - let corner = size / 2.0; - let center = region.top_left + corner; - - let dist = distance_from_rect(region.pos, center, corner, rect.radius); - color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist); - - if rect.thickness > 0.0 { - let dist2 = distance_from_rect(region.pos, center, corner - rect.thickness, rect.inner_radius); - color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2); - } - - return color; -} - -fn distance_from_rect(pixel_pos: vec2, rect_center: vec2, rect_corner: vec2, radius: f32) -> f32 { - // vec from center to pixel - let p = pixel_pos - rect_center; - // vec from inner rect corner to pixel - let q = abs(p) - (rect_corner - radius); - return length(max(q, vec2(0.0))) - radius; -} - diff --git a/core/src/render/shader/glyph.wgsl b/core/src/render/shader/glyph.wgsl new file mode 100644 index 0000000..7ab34fb --- /dev/null +++ b/core/src/render/shader/glyph.wgsl @@ -0,0 +1,24 @@ +struct GlyphInfo { + uv_min: vec2, + uv_max: vec2, + // Which layer of the atlas array this glyph's page is. + layer: u32, + color: u32, + flags: u32, +} + +@group(1) @binding(0) +var glyphs: array; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let g = glyphs[in.idx]; + let uv = mix(g.uv_min, g.uv_max, in.uv); + let texel = textureSample(tex, samp, uv, i32(g.layer)); + if (g.flags & 1u) != 0u { + return masked(in, texel); + } + var color = unpack4x8unorm(g.color); + color.a *= texel.a; + return masked(in, color); +} diff --git a/core/src/render/shader/prelude.wgsl b/core/src/render/shader/prelude.wgsl new file mode 100644 index 0000000..67f1e32 --- /dev/null +++ b/core/src/render/shader/prelude.wgsl @@ -0,0 +1,102 @@ +// Prepended to every primitive's shader, which supplies only its own data +// struct at group 1 and an `fs_main` that shades one instance. + +@group(0) @binding(0) +var window: WindowUniform; +@group(0) @binding(1) +var masks: array; + +// The texture 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) +var samp: sampler; + +struct WindowUniform { + dim: vec2, +}; + +struct Mask { + x: UiSpan, + y: UiSpan, +} + +struct UiSpan { + start: UiScalar, + end: UiScalar, +} + +struct UiScalar { + rel: f32, + abs: f32, +} + +struct InstanceInput { + @location(0) x_start: vec2, + @location(1) x_end: vec2, + @location(2) y_start: vec2, + @location(3) y_end: vec2, + @location(4) mask_idx: u32, +} + +struct VertexOutput { + @location(0) top_left: vec2, + @location(1) bot_right: vec2, + @location(2) uv: vec2, + @location(3) mask_idx: u32, + // The instance's own index, which is also where its data sits in group 1. + @location(4) @interpolate(flat) idx: u32, + @builtin(position) clip_position: vec4, +}; + +@vertex +fn vs_main( + @builtin(vertex_index) vi: u32, + @builtin(instance_index) ii: u32, + in: InstanceInput, +) -> VertexOutput { + var out: VertexOutput; + + 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 = floor(top_left_rel * window.dim) + floor(top_left_abs); + let bot_right = floor(bot_right_rel * window.dim) + floor(bot_right_abs); + let size = bot_right - top_left; + + let uv = vec2( + f32(vi % 2u), + f32(vi / 2u) + ); + 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.top_left = top_left; + out.bot_right = bot_right; + out.mask_idx = in.mask_idx; + out.idx = ii; + + return out; +} + +fn masked(in: VertexOutput, color: vec4) -> vec4 { + if in.mask_idx == 4294967295u { + return color; + } + let mask = masks[in.mask_idx]; + let tl = vec2(mask.x.start.rel, mask.y.start.rel); + let tl_abs = vec2(mask.x.start.abs, mask.y.start.abs); + let br = vec2(mask.x.end.rel, mask.y.end.rel); + let br_abs = vec2(mask.x.end.abs, mask.y.end.abs); + + let top_left = floor(tl * window.dim) + floor(tl_abs); + let bot_right = floor(br * window.dim) + floor(br_abs); + let pos = in.clip_position.xy; + if pos.x < top_left.x || pos.x > bot_right.x || pos.y < top_left.y || pos.y > bot_right.y { + return color * 0.0; + } + return color; +} diff --git a/core/src/render/shader/rect.wgsl b/core/src/render/shader/rect.wgsl new file mode 100644 index 0000000..6d8694d --- /dev/null +++ b/core/src/render/shader/rect.wgsl @@ -0,0 +1,39 @@ +struct Rect { + color: u32, + radius: f32, + thickness: f32, + inner_radius: f32, +} + +@group(1) @binding(0) +var rects: array; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let rect = rects[in.idx]; + var color = unpack4x8unorm(rect.color); + + let edge = 0.5; + let size = in.bot_right - in.top_left; + let corner = size / 2.0; + let center = in.top_left + corner; + let pos = in.clip_position.xy; + + let dist = distance_from_rect(pos, center, corner, rect.radius); + color.a *= 1.0 - smoothstep(-min(edge, rect.radius), edge, dist); + + if rect.thickness > 0.0 { + let dist2 = distance_from_rect(pos, center, corner - rect.thickness, rect.inner_radius); + color.a *= smoothstep(-min(edge, rect.inner_radius), edge, dist2); + } + + return masked(in, color); +} + +fn distance_from_rect(pixel_pos: vec2, rect_center: vec2, rect_corner: vec2, radius: f32) -> f32 { + // vec from center to pixel + let p = pixel_pos - rect_center; + // vec from inner rect corner to pixel + let q = abs(p) - (rect_corner - radius); + return length(max(q, vec2(0.0))) - radius; +} diff --git a/core/src/render/shader/texture.wgsl b/core/src/render/shader/texture.wgsl new file mode 100644 index 0000000..96df4f5 --- /dev/null +++ b/core/src/render/shader/texture.wgsl @@ -0,0 +1,7 @@ +// No group 1: the texture is bound at group 2 for this instance alone, so +// there is nothing per-instance left to look up. + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return masked(in, textureSample(tex, samp, in.uv, 0)); +} diff --git a/core/src/render/util/mod.rs b/core/src/render/util/mod.rs index 215c853..5cb1e15 100644 --- a/core/src/render/util/mod.rs +++ b/core/src/render/util/mod.rs @@ -39,7 +39,11 @@ impl ArrBuf { fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer { let mut size = size as u64; if usage.contains(BufferUsages::STORAGE) { - size = size.max(std::mem::size_of::() as u64); + // An empty storage buffer is still bound, and a binding has to be + // non-empty and a multiple of four however small `T` is. + size = size + .max(std::mem::size_of::() as u64) + .next_multiple_of(4); } device.create_buffer(&BufferDescriptor { label: Some(label), diff --git a/core/src/ui/mod.rs b/core/src/ui/mod.rs index 2998cbc..11bd715 100644 --- a/core/src/ui/mod.rs +++ b/core/src/ui/mod.rs @@ -1,4 +1,6 @@ -use crate::{Mask, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena}; +use crate::{ + Mask, PrimitiveRegistry, TextData, Textures, WeakWidget, WidgetId, Widgets, util::TrackedArena, +}; mod active; mod cache; @@ -14,6 +16,8 @@ pub use size::*; #[derive(Default)] pub struct UiData { pub widgets: Widgets, + /// Every primitive this ui can draw. + pub primitives: PrimitiveRegistry, pub textures: Textures, pub text: TextData, pub masks: TrackedArena, diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index af6d630..7471f72 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -1,7 +1,9 @@ +use bytemuck::Pod; + use crate::{ Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId, - render::{GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst}, + render::{GLYPH, GlyphPrimitive, Mask, MaskIdx, PrimitiveHandle, PrimitiveKind}, util::Vec2, }; @@ -20,16 +22,11 @@ pub struct Painter<'a> { } impl<'a> Painter<'a> { - fn primitive_at(&mut self, primitive: P, region: UiRegion) { - let h = self.state.layers.write( - self.layer, - PrimitiveInst { - id: self.id, - primitive, - region, - mask_idx: self.mask, - }, - ); + 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); self.push_primitive(h); } @@ -42,12 +39,17 @@ impl<'a> Painter<'a> { } /// Writes a primitive to be rendered - pub fn primitive(&mut self, primitive: P) { - self.primitive_at(primitive, self.region) + pub fn primitive(&mut self, kind: PrimitiveKind

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

, + primitive: P, + region: UiRegion, + ) { + self.primitive_at(kind, primitive, region.within(&self.region)); } pub fn set_mask(&mut self, region: UiRegion) { @@ -115,6 +117,7 @@ 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( + GLYPH, GlyphPrimitive { uv_min: glyph.entry.uv_min, uv_max: glyph.entry.uv_max, diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index f2a5cd7..140de06 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -245,7 +245,7 @@ impl UiRenderState { pub fn debug_layers(&self) { for ((idx, depth), draws) in self.layers.iter_depth() { let indent = " ".repeat(depth * 2); - let primitives = draws.primitives.instances().len(); + 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"); } diff --git a/src/widget/rect.rs b/src/widget/rect.rs index f72820e..e2c1fee 100644 --- a/src/widget/rect.rs +++ b/src/widget/rect.rs @@ -29,12 +29,15 @@ impl Rect { impl Widget for Rect { fn draw(&mut self, painter: &mut Painter) { - painter.primitive(RectPrimitive { - color: self.color, - radius: self.radius, - thickness: self.thickness, - inner_radius: self.inner_radius, - }); + painter.primitive( + RECT, + RectPrimitive { + color: self.color, + radius: self.radius, + thickness: self.thickness, + inner_radius: self.inner_radius, + }, + ); } fn desired_width(&mut self, _: &mut SizeCtx) -> Len { diff --git a/src/widget/text/edit.rs b/src/widget/text/edit.rs index 608ad00..ac44f51 100644 --- a/src/widget/text/edit.rs +++ b/src/widget/text/edit.rs @@ -73,6 +73,7 @@ impl Widget for TextEdit { let size = vec2(rect.width() as f32, rect.height() as f32); let top_left = vec2(rect.x0 as f32, rect.y0 as f32); painter.primitive_within( + RECT, RectPrimitive::color(Color::SKY), size.align(Align::TOP_LEFT).offset(top_left).within(®ion), ); @@ -82,6 +83,7 @@ impl Widget for TextEdit { let size = vec2(caret.width() as f32, caret.height() as f32); let top_left = vec2(caret.x0 as f32, caret.y0 as f32); painter.primitive_within( + RECT, RectPrimitive::color(Color::WHITE), size.align(Align::TOP_LEFT).offset(top_left).within(®ion), );