From 79dcc156c999ae93f7471d42778a3135549d2216 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sun, 13 Sep 2026 17:43:55 -0400 Subject: [PATCH] Give the atlas to the primitive that samples it, not to every shader `Primitive::SAMPLES` says what a primitive samples and how often it has to be bound: `Atlas` once for a list, `Image` for one instance alone. Group 2 is that, whichever it is, so a rect's pipeline has no texture and no sampler in its layout and the prelude hands out neither. The two differ only in the view dimension and in what binds them, so they share `sampled_layout` and `sampled_group`; `GpuPages` owns its bind group again and rebuilds it when the array grows. Co-Authored-By: Claude Opus 5 --- core/src/render/mod.rs | 143 +++++++++++++--------------- core/src/render/page.rs | 46 +++++---- core/src/render/primitive.rs | 46 +++++++-- core/src/render/shader/glyph.wgsl | 4 +- core/src/render/shader/prelude.wgsl | 7 +- core/src/render/shader/texture.wgsl | 2 +- core/src/render/texture.rs | 50 +++++++--- 7 files changed, 174 insertions(+), 124 deletions(-) diff --git a/core/src/render/mod.rs b/core/src/render/mod.rs index b2f425b..119c25f 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -30,6 +30,8 @@ const PRELUDE: &str = include_str!("./shader/prelude.wgsl"); pub struct UiRenderNode { shared_layout: BindGroupLayout, shared_group: BindGroup, + /// What a primitive that samples binds at group 2, one per `Sampled`. + atlas_layout: BindGroupLayout, image_layout: BindGroupLayout, format: TextureFormat, @@ -54,6 +56,7 @@ struct RenderLayer { struct PrimitivePipeline { data_layout: BindGroupLayout, pipeline: RenderPipeline, + samples: Option, } /// One list's vertex buffer and the data its shader reads. @@ -73,22 +76,27 @@ impl UiRenderNode { for (id, list) in layer.primitives.iter().enumerate() { let Some(list) = list else { continue }; let Some(group) = &list.group else { continue }; - pass.set_pipeline(&self.primitives[id].pipeline); + let primitive = &self.primitives[id]; + pass.set_pipeline(&primitive.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(..)); - 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 { + match primitive.samples { + Some(Sampled::Image) => { + 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); + } continue; - }; - pass.set_bind_group(2, image, &[]); - pass.draw(0..4, i as u32..i as u32 + 1); + } + Some(Sampled::Atlas) => pass.set_bind_group(2, self.pages.group(), &[]), + None => {} } + pass.draw(0..4, 0..list.instance.len() as u32); } } } @@ -137,22 +145,21 @@ impl UiRenderNode { draws.updated = false; } } - let mut shared_stale = self.pages.update(&mut ui.text.atlas); + self.pages + .update(&mut ui.text.atlas, &self.atlas_layout, &self.sampler); if ui.masks.changed { ui.masks.changed = false; - shared_stale |= self.masks.update(device, queue, &ui.masks[..]); + if self.masks.update(device, queue, &ui.masks[..]) { + self.shared_group = Self::shared_group( + device, + &self.shared_layout, + &self.window_buffer, + &self.masks, + ); + } } - 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); + self.textures + .update(&mut ui.textures, &self.image_layout, &self.sampler); } pub fn resize(&mut self, size: impl Into, queue: &Queue) { @@ -176,7 +183,8 @@ impl UiRenderNode { }); let shared_layout = Self::shared_layout(device); - let image_layout = Self::image_layout(device); + let atlas_layout = Self::sampled_layout(device, TextureViewDimension::D2Array, "ui atlas"); + let image_layout = Self::sampled_layout(device, TextureViewDimension::D2, "ui image"); let masks = ArrBuf::new( device, @@ -185,20 +193,14 @@ impl UiRenderNode { ); let sampler = default_sampler(device); - let pages = GpuPages::new(device, queue); + let pages = GpuPages::new(device, queue, &atlas_layout, &sampler); let textures = GpuTextures::new(device, queue); - let shared_group = Self::shared_group( - device, - &shared_layout, - &window_buffer, - &masks, - &pages, - &sampler, - ); + let shared_group = Self::shared_group(device, &shared_layout, &window_buffer, &masks); Self { shared_layout, shared_group, + atlas_layout, image_layout, format: config.format, primitives: Vec::new(), @@ -218,9 +220,11 @@ impl UiRenderNode { for source in ®istry.sources()[self.primitives.len()..] { let data_layout = Self::data_layout(device, source.stride); let mut groups = vec![&self.shared_layout, &data_layout]; - if source.textured { - groups.push(&self.image_layout); - } + groups.extend(match source.samples { + Some(Sampled::Atlas) => Some(&self.atlas_layout), + Some(Sampled::Image) => Some(&self.image_layout), + None => None, + }); let layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { label: Some(source.label), bind_group_layouts: &groups, @@ -230,6 +234,7 @@ impl UiRenderNode { self.primitives.push(PrimitivePipeline { data_layout, pipeline, + samples: source.samples, }); } } @@ -284,8 +289,7 @@ impl UiRenderNode { }) } - /// 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. + /// What every draw in the ui is given: the window and the masks. fn shared_layout(device: &Device) -> BindGroupLayout { device.create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[ @@ -309,22 +313,6 @@ 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"), }) @@ -335,8 +323,6 @@ impl UiRenderNode { layout: &BindGroupLayout, window: &Buffer, masks: &ArrBuf, - pages: &GpuPages, - sampler: &Sampler, ) -> BindGroup { device.create_bind_group(&BindGroupDescriptor { layout, @@ -349,14 +335,6 @@ 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"), }) @@ -381,20 +359,33 @@ impl UiRenderNode { }) } - /// The image one instance samples. - fn image_layout(device: &Device) -> BindGroupLayout { + /// What a primitive samples, and the sampler it reads it with. One per + /// `Sampled`, since the atlas is an array and an image is not. + fn sampled_layout( + device: &Device, + dimension: TextureViewDimension, + label: &'static str, + ) -> 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::D2, - multisampled: false, + entries: &[ + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + sample_type: TextureSampleType::Float { filterable: false }, + view_dimension: dimension, + multisampled: false, + }, + count: None, }, - count: None, - }], - label: Some("ui image"), + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::NonFiltering), + count: None, + }, + ], + label: Some(label), }) } diff --git a/core/src/render/page.rs b/core/src/render/page.rs index 92de232..61d0b3d 100644 --- a/core/src/render/page.rs +++ b/core/src/render/page.rs @@ -2,7 +2,10 @@ use wgpu::*; use crate::GlyphAtlas; -use super::{atlas::PAGE, texture::write_region}; +use super::{ + atlas::PAGE, + texture::{sampled_group, write_region}, +}; /// The glyph atlas on the GPU: one array texture whose layers are the pages /// `GlyphAtlas` packs. @@ -14,25 +17,28 @@ pub struct GpuPages { device: Device, queue: Queue, texture: Texture, - view: TextureView, + group: BindGroup, } impl GpuPages { - pub fn new(device: &Device, queue: &Queue) -> Self { + pub fn new( + device: &Device, + queue: &Queue, + layout: &BindGroupLayout, + sampler: &Sampler, + ) -> Self { let texture = create_array(device, 1); Self { device: device.clone(), queue: queue.clone(), - view: array_view(&texture), + group: atlas_group(device, layout, &texture, sampler), texture, } } - /// 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()); + 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); } for (upload, page) in atlas.uploads() { let dst = TexelCopyTextureInfo { @@ -47,15 +53,15 @@ impl GpuPages { }; write_region(&self.queue, dst, page, upload.rect); } - grew } - pub fn view(&self) -> &TextureView { - &self.view + pub fn group(&self) -> &BindGroup { + &self.group } /// Doubles until `needed` fits and copies the old layers across GPU side. - fn grow(&mut self, needed: u32) { + /// The new texture stales the group, so that is rebuilt here. + fn grow(&mut self, needed: u32, layout: &BindGroupLayout, sampler: &Sampler) { let old = self.texture.depth_or_array_layers(); let mut layers = old; while layers < needed { @@ -77,16 +83,22 @@ impl GpuPages { }, ); self.queue.submit(std::iter::once(encoder.finish())); - self.view = array_view(&texture); + self.group = atlas_group(&self.device, layout, &texture, sampler); self.texture = texture; } } -fn array_view(texture: &Texture) -> TextureView { - texture.create_view(&TextureViewDescriptor { +fn atlas_group( + device: &Device, + layout: &BindGroupLayout, + texture: &Texture, + sampler: &Sampler, +) -> BindGroup { + let view = texture.create_view(&TextureViewDescriptor { dimension: Some(TextureViewDimension::D2Array), ..Default::default() - }) + }); + sampled_group(device, layout, &view, sampler, "ui atlas") } fn create_array(device: &Device, layers: u32) -> Texture { diff --git a/core/src/render/primitive.rs b/core/src/render/primitive.rs index 1f43e47..32d4b66 100644 --- a/core/src/render/primitive.rs +++ b/core/src/render/primitive.rs @@ -15,9 +15,34 @@ 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 uses. Each instance is then a draw of its - /// own, with that image bound for it alone. - const TEXTURE: Option u32> = None; + /// What its shader samples. Nothing, for a primitive that draws from its + /// own data alone. + const SAMPLES: Option> = None; +} + +/// What a primitive samples, and how often that has to be bound. +pub enum Samples

{ + /// The glyph atlas, bound once for a whole list. + Atlas, + /// A `Textures` slot read from each instance, bound for that instance + /// alone -- so an instance is a draw of its own. + Image(fn(&P) -> u32), +} + +/// `Samples` with the type erased, which is all the renderer needs. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Sampled { + Atlas, + Image, +} + +impl

Samples

{ + const fn erase(&self) -> Sampled { + match self { + Self::Atlas => Sampled::Atlas, + Self::Image(_) => Sampled::Image, + } + } } /// Which registered primitive an instance is. @@ -55,8 +80,7 @@ pub struct PrimitiveSource { pub label: &'static str, /// Size of one instance's entry, stated as the data binding's minimum. pub stride: u64, - /// Whether an instance binds a texture of its own, from `P::TEXTURE`. - pub textured: bool, + pub samples: Option, } impl PrimitiveRegistry { @@ -68,7 +92,7 @@ impl PrimitiveRegistry { wgsl: P::WGSL, label: std::any::type_name::

(), stride: size_of::

() as u64, - textured: P::TEXTURE.is_some(), + samples: P::SAMPLES.as_ref().map(Samples::erase), }); kinds.len() as u32 - 1 }); @@ -89,7 +113,7 @@ pub struct InstanceList { 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. + /// The image each instance samples. Empty unless it samples one. slots: Vec, /// From the type the list was made for, so a write is never checked. stride: usize, @@ -220,7 +244,10 @@ impl LayerDraws { id, PrimitiveInstance { region, mask_idx }, bytemuck::bytes_of(&primitive), - P::TEXTURE.map(|slot| slot(&primitive)), + match &P::SAMPLES { + Some(Samples::Image(slot)) => Some(slot(&primitive)), + _ => None, + }, ); PrimitiveHandle { layer, @@ -325,6 +352,7 @@ unsafe impl bytemuck::Pod for GlyphPrimitive {} unsafe impl bytemuck::Zeroable for GlyphPrimitive {} impl Primitive for GlyphPrimitive { const WGSL: &'static str = include_str!("shader/glyph.wgsl"); + const SAMPLES: Option> = Some(Samples::Atlas); } /// One drawn image. Its shader reads nothing per instance; the slot names the @@ -337,7 +365,7 @@ pub struct TexturePrimitive { impl Primitive for TexturePrimitive { const WGSL: &'static str = include_str!("shader/texture.wgsl"); - const TEXTURE: Option u32> = Some(|texture| texture.slot); + const SAMPLES: Option> = Some(Samples::Image(|texture| texture.slot)); } impl From<&TextureHandle> for TexturePrimitive { diff --git a/core/src/render/shader/glyph.wgsl b/core/src/render/shader/glyph.wgsl index 08029ff..ba39d19 100644 --- a/core/src/render/shader/glyph.wgsl +++ b/core/src/render/shader/glyph.wgsl @@ -2,9 +2,9 @@ const COLORED: u32 = 1u; // The glyph atlas, whose array layers are its pages. -@group(0) @binding(2) +@group(2) @binding(0) var atlas: texture_2d_array; -@group(0) @binding(3) +@group(2) @binding(1) var samp: sampler; struct GlyphInfo { diff --git a/core/src/render/shader/prelude.wgsl b/core/src/render/shader/prelude.wgsl index 013fe4b..02c47e0 100644 --- a/core/src/render/shader/prelude.wgsl +++ b/core/src/render/shader/prelude.wgsl @@ -1,10 +1,7 @@ // 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. -// -// The rest of group 0 is bound for every draw but declared by the shaders that -// read it: the glyph atlas at binding 2 and the ui's sampler at binding 3. A -// primitive that samples an image of its own takes it at group 2 binding 0. +// shading one instance of it. What it samples, if anything, is bound at group +// 2: the texture at binding 0 and the sampler at binding 1. @group(0) @binding(0) var window: WindowUniform; diff --git a/core/src/render/shader/texture.wgsl b/core/src/render/shader/texture.wgsl index 179d0a3..9ec7d5c 100644 --- a/core/src/render/shader/texture.wgsl +++ b/core/src/render/shader/texture.wgsl @@ -1,7 +1,7 @@ // The image this instance draws, bound for it alone. @group(2) @binding(0) var image: texture_2d; -@group(0) @binding(3) +@group(2) @binding(1) var samp: sampler; @fragment diff --git a/core/src/render/texture.rs b/core/src/render/texture.rs index b546120..40e5a8f 100644 --- a/core/src/render/texture.rs +++ b/core/src/render/texture.rs @@ -26,15 +26,15 @@ impl GpuTextures { } } - pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout) { + pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) { for update in textures.updates() { match update { TextureUpdate::Push(image) => { - let image = self.create(image, layout); + let image = self.create(image, layout, sampler); self.slots.push(Some(image)); } TextureUpdate::Set(i, image) => { - let image = self.create(image, layout); + let image = self.create(image, layout, sampler); self.slots[i as usize] = Some(image); } TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image), @@ -53,7 +53,12 @@ impl GpuTextures { self.slots.iter().flatten().count() } - fn create(&self, image: &DynamicImage, layout: &BindGroupLayout) -> ImageGpu { + fn create( + &self, + image: &DynamicImage, + layout: &BindGroupLayout, + sampler: &Sampler, + ) -> ImageGpu { let rgba = image.to_rgba8(); let (width, height) = rgba.dimensions(); let texture = self.device.create_texture_with_data( @@ -75,16 +80,8 @@ impl GpuTextures { wgt::TextureDataOrder::MipMajor, rgba.as_bytes(), ); - 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"), - }); + let view = texture.create_view(&TextureViewDescriptor::default()); + let group = sampled_group(&self.device, layout, &view, sampler, "ui image"); ImageGpu { texture, group } } @@ -137,6 +134,31 @@ pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, r ); } +/// A texture and the sampler that reads it: what a primitive that samples +/// binds, whichever of the two it is. +pub fn sampled_group( + device: &Device, + layout: &BindGroupLayout, + view: &TextureView, + sampler: &Sampler, + label: &'static str, +) -> BindGroup { + device.create_bind_group(&BindGroupDescriptor { + layout, + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(view), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(sampler), + }, + ], + label: Some(label), + }) +} + pub fn default_sampler(device: &Device) -> Sampler { device.create_sampler(&SamplerDescriptor::default()) }