diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index 98270bc..12c5146 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -30,7 +30,7 @@ const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); pub struct UiRenderNode { shared_layout: BindGroupLayout, shared_group: BindGroup, - texture_layout: BindGroupLayout, + image_layout: BindGroupLayout, format: TextureFormat, /// One per registered primitive, in id order. @@ -46,26 +46,22 @@ pub struct UiRenderNode { } struct RenderLayer { - /// 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. + /// One per registered primitive, `None` where this layer draws none. primitives: Vec>, } -/// What draws one registered primitive. The group 1 layout is its own rather -/// than shared, so its entry size is the minimum only for it. +/// What draws one registered primitive. struct PrimitivePipeline { data_layout: BindGroupLayout, pipeline: RenderPipeline, - texture: PrimitiveTexture, } -/// One list's vertex buffer and the data its shader reads at group 1. +/// One list's vertex buffer and the data its shader reads. struct ListBuffers { instance: ArrBuf, data: ArrBuf, group: Option, - /// For a `PerInstance` primitive, the texture each instance binds. + /// The image each instance binds. Empty unless the primitive is textured. slots: Vec, } @@ -74,37 +70,24 @@ impl UiRenderNode { pass.set_bind_group(0, &self.shared_group, &[]); for i in &self.active { let layer = &self.layers[i]; - // 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. for (id, list) in layer.primitives.iter().enumerate() { - let Some(list) = list else { - continue; - }; - let Some(group) = &list.group else { - continue; - }; - 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. + let Some(list) = list else { continue }; + let Some(group) = &list.group else { continue }; + pass.set_pipeline(&self.primitives[id].pipeline); + // After the pipeline: a change drops the groups from where two + // pipeline layouts differ, and each primitive has its own. pass.set_bind_group(1, group, &[]); pass.set_vertex_buffer(0, list.instance.buffer.slice(..)); - 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); - } - } + if list.slots.is_empty() { + pass.draw(0..4, 0..list.instance.len() as u32); + continue; + } + for (i, &slot) in list.slots.iter().enumerate() { + let Some(image) = self.textures.group(slot) else { + continue; + }; + pass.set_bind_group(2, image, &[]); + pass.draw(0..4, i as u32..i as u32 + 1); } } } @@ -117,9 +100,7 @@ impl UiRenderNode { ui: &mut UiData, ui_render: &mut UiRenderState, ) { - // Before the layers: a list is given the bind group layout of the - // pipeline that will draw it, so every registered primitive needs one - // by the time a layer is reached. + // Before the layers: each list is given its pipeline's data layout. self.build_pipelines(device, &ui.primitives); self.active.clear(); for (i, draws) in ui_render.layers.iter_mut() { @@ -150,35 +131,30 @@ impl UiRenderNode { }; // Indexed, not zipped: a missing pipeline should say so // rather than quietly leave the list unbuilt. - let primitive = &self.primitives[id]; + let layout = &self.primitives[id].data_layout; buffers .get_or_insert_with(|| ListBuffers::new(device)) - .update( - device, - queue, - primitive.texture, - &primitive.data_layout, - list, - ); + .update(device, queue, layout, list); } draws.updated = false; } } + let mut shared_stale = self.pages.update(&mut ui.text.atlas); if ui.masks.changed { ui.masks.changed = false; - if self.masks.update(device, queue, &ui.masks[..]) { - self.shared_group = Self::shared_group( - device, - &self.shared_layout, - &self.window_buffer, - &self.masks, - ); - } + shared_stale |= self.masks.update(device, queue, &ui.masks[..]); } - self.pages - .update(&mut ui.text.atlas, &self.texture_layout, &self.sampler); - self.textures - .update(&mut ui.textures, &self.texture_layout, &self.sampler); + if shared_stale { + self.shared_group = Self::shared_group( + device, + &self.shared_layout, + &self.window_buffer, + &self.masks, + &self.pages, + &self.sampler, + ); + } + self.textures.update(&mut ui.textures, &self.image_layout); } pub fn resize(&mut self, size: impl Into, queue: &Queue) { @@ -202,23 +178,30 @@ impl UiRenderNode { }); let shared_layout = Self::shared_layout(device); - let texture_layout = Self::texture_layout(device); + let image_layout = Self::image_layout(device); let masks = ArrBuf::new( device, BufferUsages::STORAGE | BufferUsages::COPY_DST, "ui 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 pages = GpuPages::new(device, queue); let textures = GpuTextures::new(device, queue); + let shared_group = Self::shared_group( + device, + &shared_layout, + &window_buffer, + &masks, + &pages, + &sampler, + ); Self { shared_layout, shared_group, - texture_layout, + image_layout, format: config.format, primitives: Vec::new(), window_buffer, @@ -236,34 +219,23 @@ impl UiRenderNode { fn build_pipelines(&mut self, device: &Device, registry: &PrimitiveRegistry) { for source in ®istry.sources()[self.primitives.len()..] { let data_layout = Self::data_layout(device, source.stride); - let layout = Self::pipeline_layout( - device, - &self.shared_layout, - &data_layout, - &self.texture_layout, - ); + let mut groups = vec![&self.shared_layout, &data_layout]; + if source.textured { + groups.push(&self.image_layout); + } + let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { + label: Some(source.label), + bind_group_layouts: &groups, + immediate_size: 0, + }); let pipeline = Self::pipeline(device, &layout, self.format, source.wgsl, source.label); self.primitives.push(PrimitivePipeline { data_layout, pipeline, - texture: source.texture, }); } } - fn pipeline_layout( - device: &Device, - shared: &BindGroupLayout, - data: &BindGroupLayout, - texture: &BindGroupLayout, - ) -> PipelineLayout { - device.create_pipeline_layout(&PipelineLayoutDescriptor { - label: Some("ui"), - bind_group_layouts: &[shared, data, texture], - immediate_size: 0, - }) - } - fn pipeline( device: &Device, layout: &PipelineLayout, @@ -314,7 +286,8 @@ impl UiRenderNode { }) } - /// Group 0: what every draw in the ui shares. + /// What every draw in the ui is given, whether or not its shader reads it: + /// the window, the masks, the glyph atlas and the one sampler. fn shared_layout(device: &Device) -> BindGroupLayout { device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[ @@ -338,6 +311,22 @@ impl UiRenderNode { }, count: None, }, + BindGroupLayoutEntry { + binding: 2, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: false }, + view_dimension: TextureViewDimension::D2Array, + multisampled: false, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 3, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::NonFiltering), + count: None, + }, ], label: Some("ui shared"), }) @@ -348,6 +337,8 @@ impl UiRenderNode { layout: &BindGroupLayout, window: &Buffer, masks: &ArrBuf, + pages: &GpuPages, + sampler: &Sampler, ) -> BindGroup { device.create_bind_group(&BindGroupDescriptor { layout, @@ -360,16 +351,22 @@ impl UiRenderNode { binding: 1, resource: masks.buffer.as_entire_binding(), }, + BindGroupEntry { + binding: 2, + resource: BindingResource::TextureView(pages.view()), + }, + BindGroupEntry { + binding: 3, + resource: BindingResource::Sampler(sampler), + }, ], label: Some("ui shared"), }) } - /// Group 1: one list's per-instance data, whatever its shader reads it as. - /// - /// One per primitive with `stride` stated, because a `None` minimum is - /// filled in from the first pipeline built against the layout -- sharing - /// one would hold every primitive to the largest. + /// One list's per-instance data. Per primitive with `stride` stated: a + /// `None` minimum is filled in from the first pipeline built against the + /// layout, so a shared one would hold every primitive to the largest. fn data_layout(device: &Device, stride: u64) -> BindGroupLayout { device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[BindGroupLayoutEntry { @@ -386,29 +383,21 @@ impl UiRenderNode { }) } - /// Group 2: the texture this draw samples, and the sampler. No `count` on - /// either entry -- plain Vulkan 1.0 / GLES sampling is all this needs. - fn texture_layout(device: &Device) -> BindGroupLayout { + /// The one image an instance samples. The sampler is shared, so there is + /// nothing else in here. + fn image_layout(device: &Device) -> BindGroupLayout { device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[ - BindGroupLayoutEntry { - binding: 0, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - sample_type: TextureSampleType::Float { filterable: false }, - view_dimension: TextureViewDimension::D2Array, - multisampled: false, - }, - count: None, + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: false }, + view_dimension: TextureViewDimension::D2, + multisampled: false, }, - BindGroupLayoutEntry { - binding: 1, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::NonFiltering), - count: None, - }, - ], - label: Some("ui texture"), + count: None, + }], + label: Some("ui image"), }) } @@ -447,22 +436,14 @@ 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.slots.clear(); + self.slots.extend_from_slice(list.slots()); 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 - // draws it, so it has no bind group either. + // An empty list has no buffer big enough to bind, and nothing to draw. if list.instances().is_empty() { self.group = None; } else if resized || self.group.is_none() { diff --git a/core/src/render/page.rs b/core/src/render/page.rs index 710746a..92de232 100644 --- a/core/src/render/page.rs +++ b/core/src/render/page.rs @@ -2,10 +2,7 @@ use wgpu::*; use crate::GlyphAtlas; -use super::{ - atlas::PAGE, - texture::{array_view, texture_group, write_region}, -}; +use super::{atlas::PAGE, texture::write_region}; /// The glyph atlas on the GPU: one array texture whose layers are the pages /// `GlyphAtlas` packs. @@ -17,29 +14,25 @@ pub struct GpuPages { device: Device, queue: Queue, texture: Texture, - group: BindGroup, + view: TextureView, } impl GpuPages { - pub fn new( - device: &Device, - queue: &Queue, - layout: &BindGroupLayout, - sampler: &Sampler, - ) -> Self { + pub fn new(device: &Device, queue: &Queue) -> Self { let texture = create_array(device, 1); - let group = texture_group(device, layout, &array_view(&texture), sampler); Self { device: device.clone(), queue: queue.clone(), + view: array_view(&texture), texture, - group, } } - pub fn update(&mut self, atlas: &mut GlyphAtlas, layout: &BindGroupLayout, sampler: &Sampler) { - if atlas.page_count() > self.texture.depth_or_array_layers() { - self.grow(atlas.page_count(), layout, sampler); + /// Returns whether the array was replaced, which stales the view. + pub fn update(&mut self, atlas: &mut GlyphAtlas) -> bool { + let grew = atlas.page_count() > self.texture.depth_or_array_layers(); + if grew { + self.grow(atlas.page_count()); } for (upload, page) in atlas.uploads() { let dst = TexelCopyTextureInfo { @@ -54,15 +47,15 @@ impl GpuPages { }; write_region(&self.queue, dst, page, upload.rect); } + grew } - pub fn group(&self) -> &BindGroup { - &self.group + pub fn view(&self) -> &TextureView { + &self.view } /// Doubles until `needed` fits and copies the old layers across GPU side. - /// The new view invalidates the old group, so that is rebuilt here. - fn grow(&mut self, needed: u32, layout: &BindGroupLayout, sampler: &Sampler) { + fn grow(&mut self, needed: u32) { let old = self.texture.depth_or_array_layers(); let mut layers = old; while layers < needed { @@ -84,11 +77,18 @@ impl GpuPages { }, ); self.queue.submit(std::iter::once(encoder.finish())); - self.group = texture_group(&self.device, layout, &array_view(&texture), sampler); + self.view = array_view(&texture); self.texture = texture; } } +fn array_view(texture: &Texture) -> TextureView { + texture.create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }) +} + fn create_array(device: &Device, layers: u32) -> Texture { device.create_texture(&TextureDescriptor { label: Some("glyph atlas"), diff --git a/core/src/render/primitive.rs b/core/src/render/primitive.rs index 0b42b1f..0f236da 100644 --- a/core/src/render/primitive.rs +++ b/core/src/render/primitive.rs @@ -1,38 +1,39 @@ -use std::marker::PhantomData; +use std::{any::TypeId, marker::PhantomData}; use crate::{ Color, UiRegion, WidgetId, render::data::{MaskIdx, PrimitiveInstance}, - util::Vec2, + util::{HashMap, Vec2}, }; use bytemuck::Pod; -/// One instance of a registered primitive, laid out as the struct that -/// primitive's shader reads at `@group(1) @binding(0)`. +/// One instance of a primitive, laid out as the struct its shader reads. /// -/// 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 {} +/// The type carries its own shader, so drawing one is all the wiring it needs: +/// its list, free list, buffers and pipeline follow from being registered. +pub trait Primitive: Pod + 'static { + /// Compiled after `prelude.wgsl`, which states what it declares and what + /// it is given. + const WGSL: &'static str; + /// Reads the image an instance samples, for a primitive that draws one. + /// Each instance is then a draw of its own, bound for it alone. + const TEXTURE: Option u32> = None; +} -/// 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. +/// Which registered primitive an instance is. Only `PrimitiveRegistry::kind` +/// mints one, so holding it is the proof that `P` has a list to go in. pub struct PrimitiveKind

{ id: u32, _p: PhantomData, } impl

PrimitiveKind

{ - const fn new(id: u32) -> Self { + fn new(id: u32) -> Self { Self { id, _p: PhantomData, } } - - pub fn id(&self) -> u32 { - self.id - } } impl

Clone for PrimitiveKind

{ @@ -43,80 +44,50 @@ impl

Clone for PrimitiveKind

{ 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 -/// 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. -/// -/// Order is draw order within a layer, so a primitive registered later is -/// drawn over one registered earlier. +/// Every primitive a ui can draw, in the order they were first drawn. pub struct PrimitiveRegistry { kinds: Vec, + ids: HashMap, } pub struct PrimitiveSource { pub wgsl: &'static str, pub label: &'static str, - /// Size of one instance's entry, which the renderer states as the group 1 - /// binding's minimum rather than leaving it to be inferred. + /// Size of one instance's entry, stated as the data binding's minimum. 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, + /// Whether an instance binds a texture of its own, from `P::TEXTURE`. + pub textured: bool, } impl Default for PrimitiveRegistry { + /// The built-ins are registered up front rather than on first use, so that + /// what a ui happens to draw first cannot decide anything about them. fn default() -> Self { - use PrimitiveTexture::*; - let mut registry = Self { kinds: Vec::new() }; - 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(), texture.id()), - (RECT.id(), GLYPH.id(), TEXTURE.id()) - ); + let mut registry = Self { + kinds: Vec::new(), + ids: HashMap::default(), + }; + registry.kind::(); + registry.kind::(); + registry.kind::(); registry } } impl PrimitiveRegistry { - 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, + /// Registers `P` if this is the first time it has been drawn. + pub fn kind(&mut self) -> PrimitiveKind

{ + let Self { kinds, ids } = self; + let id = *ids.entry(TypeId::of::

()).or_insert_with(|| { + kinds.push(PrimitiveSource { + wgsl: P::WGSL, + label: std::any::type_name::

(), + stride: size_of::

() as u64, + textured: P::TEXTURE.is_some(), + }); + kinds.len() as u32 - 1 }); - PrimitiveKind::new(self.kinds.len() as u32 - 1) + PrimitiveKind::new(id) } pub fn sources(&self) -> &[PrimitiveSource] { @@ -124,18 +95,18 @@ impl PrimitiveRegistry { } } -/// 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. -/// -/// `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`. +/// One registered primitive's instances in one layer. Everything per-instance +/// rides here, so it stays in step through a `swap_remove`. pub struct InstanceList { instances: Vec, + /// The widget each instance belongs to, for renumbering its handles. assoc: Vec, free: Vec, + /// `stride` bytes of the primitive's own data per instance. data: Vec, + /// The image each instance samples, from `P::TEXTURE`. Empty without it. + slots: Vec, + /// From the type the list was made for, so a write is never checked. stride: usize, } @@ -146,6 +117,7 @@ impl InstanceList { assoc: Vec::new(), free: Vec::new(), data: Vec::new(), + slots: Vec::new(), stride: size_of::

(), } } @@ -158,17 +130,31 @@ impl InstanceList { &self.data } - fn push(&mut self, id: WidgetId, inst: PrimitiveInstance, data: &[u8]) -> usize { + pub fn slots(&self) -> &[u32] { + &self.slots + } + + fn push( + &mut self, + id: WidgetId, + inst: PrimitiveInstance, + data: &[u8], + slot: Option, + ) -> usize { 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); + if let Some(slot) = slot { + self.slots[i] = slot; + } i } else { let i = self.instances.len(); self.instances.push(inst); self.assoc.push(id); self.data.extend_from_slice(data); + self.slots.extend(slot); i } } @@ -183,10 +169,14 @@ impl InstanceList { let instances = &mut self.instances; let assoc = &mut self.assoc; let data = &mut self.data; + let slots = &mut self.slots; let stride = self.stride; self.free.drain(..).filter_map(move |i| { instances.swap_remove(i); assoc.swap_remove(i); + if !slots.is_empty() { + slots.swap_remove(i); + } let last = instances.len(); data.copy_within(last * stride..(last + 1) * stride, i * stride); data.truncate(last * stride); @@ -204,9 +194,7 @@ impl InstanceList { } } -/// 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. +/// Everything one layer draws, one list per registered primitive. pub struct LayerDraws { /// `None` until this layer draws that primitive, because only the write /// knows the type the list is for. @@ -247,6 +235,7 @@ impl LayerDraws { id, PrimitiveInstance { region, mask_idx }, bytemuck::bytes_of(&primitive), + P::TEXTURE.map(|slot| slot(&primitive)), ); PrimitiveHandle { layer, @@ -309,7 +298,7 @@ pub struct PrimitiveHandle { } #[repr(C)] -#[derive(Copy, Clone)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct RectPrimitive { pub color: Color, pub radius: f32, @@ -317,9 +306,9 @@ pub struct RectPrimitive { pub inner_radius: f32, } -unsafe impl bytemuck::Pod for RectPrimitive {} -unsafe impl bytemuck::Zeroable for RectPrimitive {} -impl Primitive for RectPrimitive {} +impl Primitive for RectPrimitive { + const WGSL: &'static str = include_str!("shader/rect.wgsl"); +} impl RectPrimitive { pub fn color(color: Color) -> Self { @@ -345,18 +334,23 @@ pub struct GlyphPrimitive { pub flags: u32, } +// Manual rather than derived: the align(8) leaves four bytes of padding, which +// is how WGSL lays the struct out. unsafe impl bytemuck::Pod for GlyphPrimitive {} unsafe impl bytemuck::Zeroable for GlyphPrimitive {} -impl Primitive for GlyphPrimitive {} +impl Primitive for GlyphPrimitive { + const WGSL: &'static str = include_str!("shader/glyph.wgsl"); +} -/// One drawn image. Its shader reads nothing: the slot names the texture bound -/// for this instance alone, which is what `PrimitiveTexture::PerInstance` does. +/// One drawn image. Its shader reads nothing per instance; the slot names the +/// texture to bind for it. #[repr(C)] -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct TexturePrimitive { pub slot: u32, } -unsafe impl bytemuck::Pod for TexturePrimitive {} -unsafe impl bytemuck::Zeroable for TexturePrimitive {} -impl Primitive for TexturePrimitive {} +impl Primitive for TexturePrimitive { + const WGSL: &'static str = include_str!("shader/texture.wgsl"); + const TEXTURE: Option u32> = Some(|texture| texture.slot); +} diff --git a/core/src/render/shader/glyph.wgsl b/core/src/render/shader/glyph.wgsl index 7ab34fb..376b4fd 100644 --- a/core/src/render/shader/glyph.wgsl +++ b/core/src/render/shader/glyph.wgsl @@ -1,3 +1,6 @@ +// Matches `GlyphEntry::IS_COLORED`. +const COLORED: u32 = 1u; + struct GlyphInfo { uv_min: vec2, uv_max: vec2, @@ -14,8 +17,8 @@ var glyphs: array; 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 { + let texel = textureSample(atlas, samp, uv, i32(g.layer)); + if (g.flags & COLORED) != 0u { return masked(in, texel); } var color = unpack4x8unorm(g.color); diff --git a/core/src/render/shader/prelude.wgsl b/core/src/render/shader/prelude.wgsl index 766ef9d..c967a24 100644 --- a/core/src/render/shader/prelude.wgsl +++ b/core/src/render/shader/prelude.wgsl @@ -1,16 +1,16 @@ -// Prepended to every primitive's shader, which supplies only its own data -// struct at group 1 and an `fs_main` that shades one instance. +// Prepended to every primitive's shader, which declares its own instance data +// as `var : array` at group 1 binding 0, and an `fs_main` +// shading one instance of it. A primitive that samples an image of its own +// takes it at group 2 binding 0; see texture.wgsl. @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) +// The glyph atlas, whose array layers are its pages. +@group(0) @binding(2) +var atlas: texture_2d_array; +@group(0) @binding(3) var samp: sampler; struct WindowUniform { @@ -45,7 +45,6 @@ struct VertexOutput { @location(1) bot_right: vec2, @location(2) uv: vec2, @location(3) @interpolate(flat) 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, }; diff --git a/core/src/render/shader/texture.wgsl b/core/src/render/shader/texture.wgsl index 96df4f5..e4e3cec 100644 --- a/core/src/render/shader/texture.wgsl +++ b/core/src/render/shader/texture.wgsl @@ -1,7 +1,8 @@ -// No group 1: the texture is bound at group 2 for this instance alone, so -// there is nothing per-instance left to look up. +// The image this instance draws, bound for it alone. +@group(2) @binding(0) +var image: texture_2d; @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { - return masked(in, textureSample(tex, samp, in.uv, 0)); + return masked(in, textureSample(image, samp, in.uv)); } diff --git a/core/src/render/texture.rs b/core/src/render/texture.rs index 1e1239a..b546120 100644 --- a/core/src/render/texture.rs +++ b/core/src/render/texture.rs @@ -3,7 +3,7 @@ use wgpu::{util::DeviceExt, *}; use crate::{PatchRect, TextureUpdate, Textures}; -/// The standalone images a ui draws, each its own texture and group 2 -- +/// The standalone images a ui draws, each its own texture and bind group -- /// unlike the glyph atlas in `super::page`, which is one array they share. pub struct GpuTextures { device: Device, @@ -26,15 +26,15 @@ impl GpuTextures { } } - pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) { + pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout) { for update in textures.updates() { match update { TextureUpdate::Push(image) => { - let image = self.create(image, layout, sampler); + let image = self.create(image, layout); self.slots.push(Some(image)); } TextureUpdate::Set(i, image) => { - let image = self.create(image, layout, sampler); + let image = self.create(image, layout); self.slots[i as usize] = Some(image); } TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image), @@ -45,8 +45,6 @@ impl GpuTextures { } } - /// `None` once freed. A drawn instance holds a `TextureHandle`, so its - /// slot outlives it. pub fn group(&self, slot: u32) -> Option<&BindGroup> { self.slots.get(slot as usize)?.as_ref().map(|i| &i.group) } @@ -55,12 +53,7 @@ impl GpuTextures { self.slots.iter().flatten().count() } - fn create( - &self, - image: &DynamicImage, - layout: &BindGroupLayout, - sampler: &Sampler, - ) -> ImageGpu { + fn create(&self, image: &DynamicImage, layout: &BindGroupLayout) -> ImageGpu { let rgba = image.to_rgba8(); let (width, height) = rgba.dimensions(); let texture = self.device.create_texture_with_data( @@ -82,7 +75,16 @@ impl GpuTextures { wgt::TextureDataOrder::MipMajor, rgba.as_bytes(), ); - let group = texture_group(&self.device, layout, &array_view(&texture), sampler); + let group = self.device.create_bind_group(&BindGroupDescriptor { + layout, + entries: &[BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView( + &texture.create_view(&TextureViewDescriptor::default()), + ), + }], + label: Some("ui image"), + }); ImageGpu { texture, group } } @@ -114,8 +116,6 @@ impl GpuTextures { } } -/// Uploads `rect` of `src` without copying it out first: `write_texture` takes -/// a row stride, so a region can be addressed where it already is. pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) { if rect.width == 0 || rect.height == 0 { return; @@ -137,38 +137,6 @@ pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, r ); } -/// One array texture and a sampler, so the atlas and a standalone image share a -/// layout and the shader samples whichever is bound -- an image being a -/// single-layer texture viewed as an array of one. -pub fn texture_group( - device: &Device, - layout: &BindGroupLayout, - view: &TextureView, - sampler: &Sampler, -) -> BindGroup { - device.create_bind_group(&BindGroupDescriptor { - layout, - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(view), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler(sampler), - }, - ], - label: Some("ui texture"), - }) -} - -pub fn array_view(texture: &Texture) -> TextureView { - texture.create_view(&TextureViewDescriptor { - dimension: Some(TextureViewDimension::D2Array), - ..Default::default() - }) -} - pub fn default_sampler(device: &Device) -> Sampler { device.create_sampler(&SamplerDescriptor::default()) } diff --git a/core/src/render/util/mod.rs b/core/src/render/util/mod.rs index 5cb1e15..0b2cba3 100644 --- a/core/src/render/util/mod.rs +++ b/core/src/render/util/mod.rs @@ -39,11 +39,8 @@ 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) { - // 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); + // A binding cannot be empty or under the layout's minimum. + size = size.max(std::mem::size_of::() as u64); } device.create_buffer(&BufferDescriptor { label: Some(label), diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index c0135bd..11139cb 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -2,8 +2,8 @@ use crate::{ Axis, Len, RenderedText, Size, SizeCtx, StrongWidget, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiScalar, UiVec2, Widget, WidgetId, render::{ - GLYPH, GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, - PrimitiveKind, TEXTURE, TexturePrimitive, + GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind, + TexturePrimitive, }, util::Vec2, }; @@ -23,12 +23,14 @@ pub struct Painter<'a> { } impl<'a> Painter<'a> { - fn primitive_at( - &mut self, - kind: PrimitiveKind

, - primitive: P, - region: UiRegion, - ) { + fn primitive_at(&mut self, primitive: P, region: UiRegion) { + let kind = self.rsc.ui_mut().primitives.kind::

(); + self.write(kind, primitive, region); + } + + /// For a caller with many of one primitive to write, since looking the kind + /// up is per type rather than per instance. + fn write(&mut self, kind: PrimitiveKind

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

, primitive: P) { - self.primitive_at(kind, primitive, self.region) + pub fn primitive(&mut self, primitive: P) { + self.primitive_at(primitive, 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 primitive_within(&mut self, primitive: P, region: UiRegion) { + self.primitive_at(primitive, region.within(&self.region)); } pub fn set_mask(&mut self, region: UiRegion) { @@ -104,7 +101,6 @@ impl<'a> Painter<'a> { pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) { self.textures.push(handle.clone()); self.primitive_at( - TEXTURE, TexturePrimitive { slot: handle.slot(), }, @@ -123,6 +119,7 @@ impl<'a> Painter<'a> { } pub fn glyphs(&mut self, text: &RenderedText, origin: UiRegion) { + let kind = self.rsc.ui_mut().primitives.kind::(); for glyph in text.glyphs.iter() { let mut region = origin; region.x.end = region.x.start; @@ -130,8 +127,8 @@ impl<'a> Painter<'a> { let mut region = region.offset(UiVec2::abs(glyph.offset)); 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, + self.write( + kind, GlyphPrimitive { uv_min: glyph.entry.uv_min, uv_max: glyph.entry.uv_max, diff --git a/src/widget/rect.rs b/src/widget/rect.rs index e2c1fee..f72820e 100644 --- a/src/widget/rect.rs +++ b/src/widget/rect.rs @@ -29,15 +29,12 @@ impl Rect { impl Widget for Rect { fn draw(&mut self, painter: &mut Painter) { - painter.primitive( - RECT, - RectPrimitive { - color: self.color, - radius: self.radius, - thickness: self.thickness, - inner_radius: self.inner_radius, - }, - ); + painter.primitive(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 ac44f51..608ad00 100644 --- a/src/widget/text/edit.rs +++ b/src/widget/text/edit.rs @@ -73,7 +73,6 @@ 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), ); @@ -83,7 +82,6 @@ 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), );