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()) }