From b234497d21b56d54d591c1b0021e8309a2d3758a Mon Sep 17 00:00:00 2001 From: AIris <4+iris-ai@noreply.localhost> Date: Sun, 13 Sep 2026 18:56:59 -0400 Subject: [PATCH] Draw the glyph atlas as an array texture and images with their own bind groups + primitive rendering overhaul Replaces the bindless `binding_array>` the renderer bound every texture through. That array needs `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack, so the old shape did not run there at all. The two things being bound want opposite treatment, so they are now split: - **Glyph atlas pages become layers of one `texture_2d_array`.** A glyph primitive carries a `layer` instead of a view/sampler index pair. A layer index is an ordinary sampling operand, so this needs nothing beyond plain Vulkan 1.0 / GLES. Growing the atlas recreates the array with headroom and `copy_texture_to_texture`s the old layers across, no readback. - **A standalone image gets its own texture and its own bind group,** and draws in its own call. It no longer needs a per-instance entry in `PrimitiveData`: the bind group has already picked the texture. `Primitives` keeps images in a list of their own as a result, with `PrimitiveChange::is_image` naming which list a renumbering belongs to -- the two have independent index spaces, so `(layer, inst_idx)` alone would collide between them. Two notes on judgement calls, since this slice was rebuilt on top of `main` rather than transplanted: - The source version renamed `GlyphEntry::is_colored` to `is_color` and added a second `IS_COLOR` flag constant beside the existing `GlyphEntry::IS_COLORED`. Both dropped: #10's naming and its `flags()` are kept, and UVs stay `Vec2` rather than going back to `[f32; 2]`. - `ImageGpu` no longer holds the `Texture` behind its view, which removes an `#[allow(dead_code)]`. A `TextureView` keeps its own reference to the texture, checked by rendering rather than assumed -- see below. ### Verification ``` cargo fmt --all --check cargo clippy --workspace --all-targets --locked -- -D warnings cargo test --workspace --locked ``` All clean; the 4 text-edit tests pass. The only clippy output is the pre-existing future-incompatibility notice about `naga`/`wgpu`/`winit`. Because this is a rendering change, it was also run for real rather than only compiled. The `tabs` example was rendered on this machine's GPU -- Venus onto an RX 7900 XT, confirmed from the loaded ICD (`libvulkan_virtio.so` on `/dev/dri/renderD128`) rather than assumed, since a failed Vulkan init here silently falls back to llvmpipe and would make the screenshots meaningless. Screenshots before and after the change are **byte-identical** (same md5) in two scenes: the default tab, which exercises text (the atlas path) and rects, and the image tab with a standalone image pushed at startup, which exercises the per-image bind group. The image-tab scene needed a temporary local edit to the example to push the image without a click; that edit is not part of this branch. The same comparison, re-run after dropping the `Texture` field, is still byte-identical -- which is the check that the view alone keeps it alive. --------- Co-authored-by: iris <2+iris@noreply.localhost> Reviewed-on: https://git.arirex.me/iris/iris/pulls/11 Reviewed-by: iris <2+iris@noreply.localhost> Co-authored-by: AIris <4+iris-ai@noreply.localhost> --- core/src/primitive/layer.rs | 6 +- core/src/primitive/text.rs | 35 +- core/src/primitive/texture.rs | 37 +- core/src/render/atlas.rs | 85 +++-- core/src/render/data.rs | 6 +- core/src/render/mod.rs | 393 ++++++++++----------- core/src/render/page.rs | 156 +++++++++ core/src/render/primitive.rs | 508 +++++++++++++++++----------- core/src/render/shader.wgsl | 204 ----------- core/src/render/shader/glyph.wgsl | 33 ++ core/src/render/shader/prelude.wgsl | 96 ++++++ core/src/render/shader/rect.wgsl | 39 +++ core/src/render/shader/texture.wgsl | 10 + core/src/render/texture.rs | 336 ++++++++++-------- core/src/render/util/mod.rs | 16 +- core/src/ui/mod.rs | 8 +- core/src/ui/painter.rs | 72 ++-- core/src/ui/render_state.rs | 21 +- core/src/ui/size.rs | 8 +- examples/tabs/main.rs | 4 +- src/default/render.rs | 13 +- src/widget/image.rs | 2 +- tests/draw_cost.rs | 205 +++++++++++ 23 files changed, 1406 insertions(+), 887 deletions(-) create mode 100644 core/src/render/page.rs 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 create mode 100644 tests/draw_cost.rs diff --git a/core/src/primitive/layer.rs b/core/src/primitive/layer.rs index 54f6a96..3fb2411 100644 --- a/core/src/primitive/layer.rs +++ b/core/src/primitive/layer.rs @@ -1,7 +1,7 @@ use std::ops::{Index, IndexMut}; use crate::{ - render::{MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, Primitives}, + render::{LayerDraws, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst}, util::to_mut, }; @@ -39,7 +39,7 @@ struct Child { tail: usize, } -pub type PrimitiveLayers = Layers; +pub type DrawLayers = Layers; impl Layers { pub fn new() -> Layers { @@ -119,7 +119,7 @@ impl Layers { } } -impl PrimitiveLayers { +impl DrawLayers { pub fn write( &mut self, layer: LayerId, diff --git a/core/src/primitive/text.rs b/core/src/primitive/text.rs index a077ada..11284f6 100644 --- a/core/src/primitive/text.rs +++ b/core/src/primitive/text.rs @@ -1,6 +1,5 @@ use crate::{ - Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, Textures, UiColor, - util::Vec2, + Align, GlyphAtlas, GlyphEntry, GlyphKey, PlacedGlyph, RegionAlign, UiColor, util::Vec2, }; use parley::{ Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, GenericFamily, Layout, @@ -159,7 +158,7 @@ impl TextBuffer { } impl TextData { - pub fn place(&mut self, buffer: &TextBuffer, textures: &mut Textures) -> Vec { + pub fn place(&mut self, buffer: &TextBuffer) -> Vec { let mut placed = Vec::new(); for line in buffer.layout.lines() { for item in line.items() { @@ -185,17 +184,14 @@ impl TextData { subpixel, coords: coords_hash, }; - let Some(entry) = self.glyph_entry( - GlyphRaster { - key, - font: font_ref, - font_size, - coords, - subpixel, - glyph_id: glyph.id, - }, - textures, - ) else { + let Some(entry) = self.glyph_entry(GlyphRaster { + key, + font: font_ref, + font_size, + coords, + subpixel, + glyph_id: glyph.id, + }) else { continue; }; placed.push(PlacedGlyph { @@ -211,11 +207,7 @@ impl TextData { placed } - fn glyph_entry( - &mut self, - glyph: GlyphRaster<'_>, - textures: &mut Textures, - ) -> Option { + fn glyph_entry(&mut self, glyph: GlyphRaster<'_>) -> Option { if let Some(entry) = self.atlas.get(&glyph.key) { return entry; } @@ -237,7 +229,7 @@ impl TextData { .render(&mut scaler, glyph.glyph_id as u16); if let Some(image) = image { - self.atlas.insert(glyph.key, &image, textures) + self.atlas.insert(glyph.key, &image) } else { self.atlas.insert_empty(glyph.key); None @@ -278,10 +270,9 @@ impl TextData { buffer: &mut TextBuffer, attrs: &TextAttrs, width: Option, - textures: &mut Textures, ) -> RenderedText { buffer.shape(self, attrs, width); - let glyphs = self.place(buffer, textures); + let glyphs = self.place(buffer); RenderedText { glyphs, size: buffer.size(), diff --git a/core/src/primitive/texture.rs b/core/src/primitive/texture.rs index 95c210e..4671c29 100644 --- a/core/src/primitive/texture.rs +++ b/core/src/primitive/texture.rs @@ -1,7 +1,4 @@ -use crate::{ - render::TexturePrimitive, - util::{RefCounter, Vec2}, -}; +use crate::util::{RefCounter, Vec2}; use image::{DynamicImage, GenericImageView}; use std::{ ops::Index, @@ -10,7 +7,7 @@ use std::{ #[derive(Debug, Clone)] pub struct TextureHandle { - inner: TexturePrimitive, + slot: u32, size: Vec2, counter: RefCounter, send: Sender, @@ -31,6 +28,8 @@ pub enum TextureUpdate<'a> { Set(u32, &'a DynamicImage), Patch(u32, PatchRect, &'a DynamicImage), Free(u32), + /// Added and freed before the renderer drained either update. It still has + /// to push a slot to stay lined up with `images`; `Free` then empties it. PushFree, SetFree, } @@ -64,14 +63,8 @@ impl Textures { pub fn add(&mut self, image: impl Into) -> TextureHandle { let image = image.into(); let size = image.dimensions().into(); - let view_idx = self.push(image); - // 0 == default in renderer; TODO: actually create samplers here - let sampler_idx = 0; TextureHandle { - inner: TexturePrimitive { - view_idx, - sampler_idx, - }, + slot: self.push(image), size, counter: RefCounter::new(), send: self.send.clone(), @@ -92,15 +85,20 @@ impl Textures { } pub fn image_mut(&mut self, handle: &TextureHandle) -> &mut DynamicImage { - self.images[handle.inner.view_idx as usize] + self.images[handle.slot as usize] .as_mut() .expect("texture was freed while still held") } /// Queue an upload of just `rect`, after writing it with `image_mut`. pub fn patch(&mut self, handle: &TextureHandle, rect: PatchRect) { - self.updates - .push(Update::Patch(handle.inner.view_idx, rect)); + self.updates.push(Update::Patch(handle.slot, rect)); + } + + /// How many textures are live, which is what a ui can ask; the renderer's + /// copies follow from the updates it drains. + pub fn count(&self) -> usize { + self.images.iter().flatten().count() } pub fn free(&mut self) { @@ -131,8 +129,9 @@ impl Textures { } impl TextureHandle { - pub fn primitive(&self) -> TexturePrimitive { - self.inner + /// Index into `Textures`, and into the renderer's parallel slots. + pub fn slot(&self) -> u32 { + self.slot } pub fn size(&self) -> Vec2 { self.size @@ -142,7 +141,7 @@ impl TextureHandle { impl Drop for TextureHandle { fn drop(&mut self) { if self.counter.drop() { - let _ = self.send.send(self.inner.view_idx); + let _ = self.send.send(self.slot); } } } @@ -151,7 +150,7 @@ impl Index<&TextureHandle> for Textures { type Output = DynamicImage; fn index(&self, index: &TextureHandle) -> &Self::Output { - self.images[index.inner.view_idx as usize].as_ref().unwrap() + self.images[index.slot as usize].as_ref().unwrap() } } diff --git a/core/src/render/atlas.rs b/core/src/render/atlas.rs index dc0d1de..2325f4a 100644 --- a/core/src/render/atlas.rs +++ b/core/src/render/atlas.rs @@ -1,11 +1,12 @@ use crate::{ - PatchRect, TextureHandle, Textures, + PatchRect, util::{HashMap, Vec2}, }; use image::RgbaImage; use swash::scale::image::{Content, Image}; -const PAGE: u32 = 1024; +/// Side of one page, and so of every layer of `render::page`'s array texture. +pub(crate) const PAGE: u32 = 1024; /// Transparent margin kept around every glyph, so that sampling one cannot /// pick up its neighbour along a shared edge. @@ -35,8 +36,8 @@ pub struct GlyphEntry { pub width: u32, pub height: u32, pub is_colored: bool, - pub view_idx: u32, - pub sampler_idx: u32, + /// Which atlas array layer this glyph is on. + pub layer: u32, } impl GlyphEntry { @@ -48,18 +49,26 @@ impl GlyphEntry { } struct Page { - handle: TextureHandle, + image: RgbaImage, x: u32, y: u32, shelf_height: u32, } +/// A rectangle of one page the renderer has not uploaded yet. +#[derive(Clone, Copy)] +pub struct PageUpload { + pub layer: u32, + pub rect: PatchRect, +} + #[derive(Default)] pub struct GlyphAtlas { pages: Vec, /// `None` for a glyph that rasterised to nothing -- a space, say. Cached /// too, so it is not re-rasterised on every layout. entries: HashMap>, + uploads: Vec, } impl GlyphAtlas { @@ -67,12 +76,7 @@ impl GlyphAtlas { self.entries.get(key).copied() } - pub fn insert( - &mut self, - key: GlyphKey, - image: &Image, - textures: &mut Textures, - ) -> Option { + pub fn insert(&mut self, key: GlyphKey, image: &Image) -> Option { let w = image.placement.width; let h = image.placement.height; if w == 0 || h == 0 { @@ -94,23 +98,11 @@ impl GlyphAtlas { return None; } - let (page_idx, x, y) = self.allocate(w, h, textures); - let page = &self.pages[page_idx]; + let upload = self.allocate(w, h); + let PatchRect { x, y, .. } = upload.rect; + write_glyph(&mut self.pages[upload.layer as usize].image, image, x, y); + self.uploads.push(upload); - let img = textures.image_mut(&page.handle); - let rgba = img.as_mut_rgba8().expect("atlas page is rgba8"); - write_glyph(rgba, image, x, y); - - let handle = page.handle.clone(); - let rect = PatchRect { - x, - y, - width: w, - height: h, - }; - textures.patch(&handle, rect); - - let page = &self.pages[page_idx]; let scale = 1.0 / PAGE as f32; let entry = GlyphEntry { uv_min: Vec2::new(x as f32 * scale, y as f32 * scale), @@ -120,39 +112,60 @@ impl GlyphAtlas { width: w, height: h, is_colored: matches!(image.content, Content::Color), - view_idx: page.handle.primitive().view_idx, - sampler_idx: page.handle.primitive().sampler_idx, + layer: upload.layer, }; self.entries.insert(key, Some(entry)); Some(entry) } - fn allocate(&mut self, w: u32, h: u32, textures: &mut Textures) -> (usize, u32, u32) { + /// Reserves room for a `w` by `h` glyph, adding a page if none has it. + fn allocate(&mut self, w: u32, h: u32) -> PageUpload { + let rect = |x, y| PatchRect { + x, + y, + width: w, + height: h, + }; if let Some((i, (x, y))) = self .pages .iter_mut() .enumerate() .find_map(|(i, page)| page.allocate(w, h).map(|position| (i, position))) { - return (i, x, y); + return PageUpload { + layer: i as u32, + rect: rect(x, y), + }; } - let handle = textures.add(RgbaImage::new(PAGE, PAGE)); self.pages.push(Page { - handle, + image: RgbaImage::new(PAGE, PAGE), x: PAD + w + PAD, y: PAD, shelf_height: h + PAD, }); - (self.pages.len() - 1, PAD, PAD) + PageUpload { + layer: self.pages.len() as u32 - 1, + rect: rect(PAD, PAD), + } + } + + /// Drains what has been written since the last call, for the renderer to + /// upload. A new page needs nothing more: wgpu leaves the rest of a fresh + /// layer transparent, which is what an atlas wants. + pub fn uploads(&mut self) -> impl Iterator { + let pages = &self.pages; + self.uploads + .drain(..) + .map(|upload| (upload, &pages[upload.layer as usize].image)) } pub fn insert_empty(&mut self, key: GlyphKey) { self.entries.insert(key, None); } - pub fn page_count(&self) -> usize { - self.pages.len() + pub fn page_count(&self) -> u32 { + self.pages.len() as u32 } pub fn glyph_count(&self) -> usize { 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 f40cf76..185b964 100644 --- a/core/src/render/mod.rs +++ b/core/src/render/mod.rs @@ -1,8 +1,6 @@ -use std::num::NonZero; - use crate::{ UiData, UiRenderState, - render::{data::PrimitiveInstance, texture::GpuTextures, util::ArrBuf}, + render::{data::PrimitiveInstance, util::ArrBuf}, util::{HashMap, Vec2}, }; use data::WindowUniform; @@ -13,6 +11,7 @@ use wgpu::{ mod atlas; mod data; +mod page; mod primitive; mod texture; mod util; @@ -21,42 +20,63 @@ 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"); pub struct UiRenderNode { - uniform_group: BindGroup, - primitive_layout: BindGroupLayout, - rsc_layout: BindGroupLayout, - rsc_group: BindGroup, + shared_layout: BindGroupLayout, + shared_group: BindGroup, + format: TextureFormat, - pipeline: RenderPipeline, + /// One per registered primitive, in id order. + primitives: Vec, layers: HashMap, active: Vec, window_buffer: Buffer, - textures: GpuTextures, masks: ArrBuf, } struct RenderLayer { + /// One per registered primitive, `None` where this layer draws none. + primitives: Vec>, +} + +/// What draws one registered primitive. +struct PrimitivePipeline { + data_layout: BindGroupLayout, + pipeline: RenderPipeline, + render: Box, +} + +/// One list's vertex buffer and the data its shader reads. +struct ListBuffers { instance: ArrBuf, - primitives: PrimitiveBuffers, - primitive_group: BindGroup, + data: ArrBuf, + group: Option, + /// What the primitive asked to keep per instance, if anything. + bindings: Vec, } 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(2, &self.rsc_group, &[]); + pass.set_bind_group(0, &self.shared_group, &[]); for i in &self.active { let layer = &self.layers[i]; - if layer.instance.len() == 0 { - continue; + 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); + pass.set_bind_group(1, group, &[]); + pass.set_vertex_buffer(0, list.instance.buffer.slice(..)); + primitive.render.draw( + pass, + ListDraw { + instances: list.instance.len() as u32, + bindings: &list.bindings, + }, + ); } - pass.set_bind_group(1, &layer.primitive_group, &[]); - pass.set_vertex_buffer(0, layer.instance.buffer.slice(..)); - pass.draw(0..4, 0..layer.instance.len() as u32); } } @@ -67,55 +87,56 @@ impl UiRenderNode { ui: &mut UiData, ui_render: &mut UiRenderState, ) { + // Before the layers: each list is given its pipeline's data layout. + self.build_pipelines(device, queue, &ui.primitives); self.active.clear(); - for (i, primitives) in ui_render.layers.iter_mut() { + for (i, draws) in ui_render.layers.iter_mut() { self.active.push(i); - for change in primitives.apply_free() { + for change in draws.apply_free() { if let Some(inst) = ui_render.active.get_mut(&change.id) { for h in &mut inst.primitives { - if h.layer == i && h.inst_idx == change.old { + if h.layer == i && h.kind == change.kind && h.inst_idx == change.old { h.inst_idx = change.new; break; } } } } - let rlayer = self.layers.entry(i).or_insert_with(|| { - let primitives = PrimitiveBuffers::new(device); - let primitive_group = - Self::primitive_group(device, &self.primitive_layout, primitives.buffers()); - RenderLayer { - instance: ArrBuf::new( - device, - BufferUsages::VERTEX | BufferUsages::COPY_DST, - "instance", - ), - primitives, - primitive_group, + let rlayer = self.layers.entry(i).or_insert_with(RenderLayer::new); + if draws.updated { + let lists = draws.primitives(); + // The zip would otherwise skip a list with no pipeline. + assert!(lists.len() <= self.primitives.len()); + rlayer.primitives.resize_with(lists.len(), || None); + for ((buffers, list), primitive) in rlayer + .primitives + .iter_mut() + .zip(lists) + .zip(&self.primitives) + { + let Some(list) = list else { + continue; + }; + buffers + .get_or_insert_with(|| ListBuffers::new(device)) + .update(device, queue, primitive, list); } - }); - if primitives.updated { - rlayer - .instance - .update(device, queue, primitives.instances()); - rlayer.primitives.update(device, queue, primitives.data()); - rlayer.primitive_group = Self::primitive_group( - device, - &self.primitive_layout, - rlayer.primitives.buffers(), - ); - primitives.updated = false; + draws.updated = false; } } - let mut changed = false; - changed |= self.textures.update(&mut ui.textures); + for primitive in &mut self.primitives { + primitive.render.update(ui); + } if ui.masks.changed { ui.masks.changed = false; - self.masks.update(device, queue, &ui.masks[..]); - changed = true; - } - if changed { - self.rsc_group = Self::rsc_group(device, &self.rsc_layout, &self.textures, &self.masks); + if self.masks.update(device, queue, &ui.masks[..]) { + self.shared_group = Self::shared_group( + device, + &self.shared_layout, + &self.window_buffer, + &self.masks, + ); + } } } @@ -128,17 +149,7 @@ impl UiRenderNode { queue.write_buffer(&self.window_buffer, 0, bytemuck::cast_slice(slice)); } - pub fn new( - device: &Device, - queue: &Queue, - config: &SurfaceConfiguration, - limits: UiLimits, - ) -> Self { - let shader = device.create_shader_module(ShaderModuleDescriptor { - label: Some("UI Shape Shader"), - source: ShaderSource::Wgsl(SHAPE_SHADER.into()), - }); - + pub fn new(device: &Device, config: &SurfaceConfiguration) -> Self { let window_uniform = WindowUniform { width: config.width as f32, height: config.height as f32, @@ -149,67 +160,73 @@ 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: &core::array::from_fn::<_, { PrimitiveBuffers::LEN }, _>(|i| { - BindGroupLayoutEntry { - binding: i as u32, - 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 tex_manager = GpuTextures::new(device, queue); + let shared_layout = Self::shared_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 rsc_layout = Self::rsc_layout(device, &limits); - let rsc_group = Self::rsc_group(device, &rsc_layout, &tex_manager, &masks); + Self { + shared_layout, + shared_group, + format: config.format, + primitives: Vec::new(), + window_buffer, + layers: HashMap::default(), + active: Vec::new(), + masks, + } + } - let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { - label: Some("UI Shape Pipeline Layout"), - bind_group_layouts: &[&uniform_layout, &primitive_layout, &rsc_layout], - immediate_size: 0, + /// 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, device: &Device, queue: &Queue, registry: &PrimitiveRegistry) { + for source in ®istry.sources()[self.primitives.len()..] { + let render = (source.render)(device, queue); + let data_layout = Self::data_layout(device, source.stride); + let mut groups = vec![&self.shared_layout, &data_layout]; + groups.extend(render.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, + render, + }); + } + } + + 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()), }); - let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { - label: Some("UI Shape Pipeline"), - layout: Some(&pipeline_layout), + 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, })], @@ -232,90 +249,42 @@ impl UiRenderNode { }, multiview_mask: None, cache: None, - }); - - Self { - uniform_group, - primitive_layout, - rsc_layout, - rsc_group, - pipeline, - window_buffer, - layers: HashMap::default(), - active: Vec::new(), - textures: tex_manager, - 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( - device: &Device, - layout: &BindGroupLayout, - buffers: [(u32, &Buffer); PrimitiveBuffers::LEN], - ) -> BindGroup { - device.create_bind_group(&BindGroupDescriptor { - layout, - entries: &buffers.map(|(binding, buf)| BindGroupEntry { - binding, - resource: buf.as_entire_binding(), - }), - label: Some("ui primitives"), - }) - } - - fn rsc_layout(device: &Device, limits: &UiLimits) -> BindGroupLayout { + /// 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: &[ BindGroupLayoutEntry { binding: 0, - visibility: ShaderStages::FRAGMENT, - ty: BindingType::Texture { - sample_type: TextureSampleType::Float { filterable: false }, - view_dimension: TextureViewDimension::D2, - multisampled: false, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: BufferSize::new(size_of::() as u64), }, - count: Some(NonZero::new(limits.max_textures).unwrap()), + count: None, }, BindGroupLayoutEntry { binding: 1, visibility: ShaderStages::FRAGMENT, - ty: BindingType::Sampler(SamplerBindingType::NonFiltering), - count: Some(NonZero::new(limits.max_samplers).unwrap()), - }, - BindGroupLayoutEntry { - binding: 2, - visibility: ShaderStages::FRAGMENT, ty: BindingType::Buffer { ty: BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, - min_binding_size: None, + min_binding_size: BufferSize::new(size_of::() as u64), }, count: None, }, ], - label: Some("ui rsc"), + label: Some("ui shared"), }) } - fn rsc_group( + fn shared_group( device: &Device, layout: &BindGroupLayout, - tex_manager: &GpuTextures, + window: &Buffer, masks: &ArrBuf, ) -> BindGroup { device.create_bind_group(&BindGroupDescriptor { @@ -323,45 +292,85 @@ impl UiRenderNode { entries: &[ BindGroupEntry { binding: 0, - resource: BindingResource::TextureViewArray(&tex_manager.views()), + resource: window.as_entire_binding(), }, BindGroupEntry { binding: 1, - resource: BindingResource::SamplerArray(&tex_manager.samplers()), - }, - BindGroupEntry { - binding: 2, resource: masks.buffer.as_entire_binding(), }, ], - label: Some("ui rsc"), + label: Some("ui shared"), }) } - pub fn view_count(&self) -> usize { - self.textures.view_count() + /// Layout for a list of one primitive's data. Every size in the ui is + /// stated, so "is the buffer big enough for one entry?" is answered when + /// the bind group is made; a `None` size is wgpu's to check on every draw. + fn data_layout(device: &Device, stride: u64) -> 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: BufferSize::new(stride), + }, + count: None, + }], + label: Some("ui primitive data"), + }) } } -pub struct UiLimits { - max_textures: u32, - max_samplers: u32, -} - -impl Default for UiLimits { - fn default() -> Self { +impl RenderLayer { + fn new() -> Self { Self { - max_textures: 100000, - max_samplers: 1000, + primitives: Vec::new(), } } } -impl UiLimits { - pub fn max_binding_array_elements_per_shader_stage(&self) -> u32 { - self.max_textures + self.max_samplers +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, + bindings: Vec::new(), + } } - pub fn max_binding_array_sampler_elements_per_shader_stage(&self) -> u32 { - self.max_samplers + + fn update( + &mut self, + device: &Device, + queue: &Queue, + primitive: &PrimitivePipeline, + list: &InstanceList, + ) { + self.bindings.clear(); + primitive.render.instance_bindings(list, &mut self.bindings); + self.instance.update(device, queue, list.instances()); + let resized = self.data.update(device, queue, list.data()); + if list.instances().is_empty() { + self.group = None; + } else if resized || self.group.is_none() { + self.group = Some(device.create_bind_group(&BindGroupDescriptor { + layout: &primitive.data_layout, + entries: &[BindGroupEntry { + binding: 0, + resource: self.data.buffer.as_entire_binding(), + }], + label: Some("ui primitive data"), + })); + } } } diff --git a/core/src/render/page.rs b/core/src/render/page.rs new file mode 100644 index 0000000..421aada --- /dev/null +++ b/core/src/render/page.rs @@ -0,0 +1,156 @@ +use wgpu::*; + +use crate::{GlyphAtlas, UiData}; + +use super::{ + atlas::PAGE, + primitive::{ListDraw, PrimitiveRender}, + texture::{default_sampler, sampled_group, sampled_layout, write_region}, +}; + +/// Draws glyphs from the atlas, which it owns: one array texture bound once +/// for a whole list, since every glyph in it reads the same pages. +pub struct GlyphRender { + pages: GpuPages, + layout: BindGroupLayout, + sampler: Sampler, +} + +impl GlyphRender { + pub fn new(device: &Device, queue: &Queue) -> Self { + let layout = sampled_layout(device, TextureViewDimension::D2Array, "ui atlas"); + let sampler = default_sampler(device); + Self { + pages: GpuPages::new(device, queue, &layout, &sampler), + layout, + sampler, + } + } +} + +impl PrimitiveRender for GlyphRender { + fn layout(&self) -> Option<&BindGroupLayout> { + Some(&self.layout) + } + + fn update(&mut self, ui: &mut UiData) { + self.pages + .update(&mut ui.text.atlas, &self.layout, &self.sampler); + } + + fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) { + pass.set_bind_group(2, self.pages.group(), &[]); + pass.draw(0..4, 0..list.instances); + } +} + +/// The glyph atlas on the GPU: one array texture whose layers are the pages +/// `GlyphAtlas` packs. +/// +/// One array rather than a texture per page because a layer index is ordinary +/// Vulkan 1.0 / GLES sampling, where a `binding_array` would need +/// `VK_EXT_descriptor_indexing`, which a real share of Android GPUs lack. +pub struct GpuPages { + device: Device, + queue: Queue, + texture: Texture, + group: BindGroup, +} + +impl GpuPages { + 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(), + group: atlas_group(device, layout, &texture, sampler), + texture, + } + } + + 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 { + texture: &self.texture, + mip_level: 0, + origin: Origin3d { + x: upload.rect.x, + y: upload.rect.y, + z: upload.layer, + }, + aspect: TextureAspect::All, + }; + write_region(&self.queue, dst, page, upload.rect); + } + } + + pub fn group(&self) -> &BindGroup { + &self.group + } + + /// Doubles until `needed` fits and copies the old layers across GPU side. + /// 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 { + layers *= 2; + } + let texture = create_array(&self.device, layers); + let mut encoder = self + .device + .create_command_encoder(&CommandEncoderDescriptor { + label: Some("atlas grow"), + }); + encoder.copy_texture_to_texture( + self.texture.as_image_copy(), + texture.as_image_copy(), + Extent3d { + width: PAGE, + height: PAGE, + depth_or_array_layers: old, + }, + ); + self.queue.submit(std::iter::once(encoder.finish())); + self.group = atlas_group(&self.device, layout, &texture, sampler); + self.texture = texture; + } +} + +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 { + device.create_texture(&TextureDescriptor { + label: Some("glyph atlas"), + size: Extent3d { + width: PAGE, + height: PAGE, + depth_or_array_layers: layers, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8Unorm, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::COPY_SRC, + view_formats: &[], + }) +} diff --git a/core/src/render/primitive.rs b/core/src/render/primitive.rs index 849cbe0..a4b483d 100644 --- a/core/src/render/primitive.rs +++ b/core/src/render/primitive.rs @@ -1,115 +1,247 @@ -use std::ops::{Deref, DerefMut}; +use std::{any::TypeId, marker::PhantomData}; use crate::{ - Color, UiRegion, WidgetId, + Color, TextureHandle, UiData, UiRegion, WidgetId, render::{ - ArrBuf, data::{MaskIdx, PrimitiveInstance}, + page::GlyphRender, + texture::ImageRender, }, - util::Vec2, + util::{HashMap, Vec2}, }; use bytemuck::Pod; -use wgpu::*; +use wgpu::{BindGroupLayout, Device, Queue, RenderPass}; -pub struct Primitives { +/// One instance of a primitive, laid out as the struct its shader reads. +/// +/// 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; + + /// Made once, the first time the renderer sees this primitive. It owns + /// whatever the shader samples and records the primitive's own draws; the + /// default owns nothing and draws every instance in one call. + fn render(device: &Device, queue: &Queue) -> Box + where + Self: Sized, + { + let _ = (device, queue); + Box::new(Instanced) + } +} + +/// The renderer's half of a primitive: what it samples, what it uploads, and +/// what draws it records. +/// +/// Everything a draw shares -- the pipeline, the window and masks, the list's +/// own data and instance buffer -- is set before this is called. What is left +/// is what only this primitive knows: its group 2, and how many draws its +/// instances are. +pub trait PrimitiveRender { + /// The layout its shader reads at group 2. `None` for a primitive whose + /// shader samples nothing, whose pipeline then has no group 2 at all. + fn layout(&self) -> Option<&BindGroupLayout> { + None + } + + /// Uploads whatever this primitive owns, once a frame, before any draw. + fn update(&mut self, ui: &mut UiData) { + let _ = ui; + } + + /// Keeps what the primitive needs per instance at draw time, read from + /// the list's own data. A primitive that binds nothing per instance -- + /// most of them -- leaves this empty and draws in one call. + fn instance_bindings(&self, list: &InstanceList, out: &mut Vec) { + let _ = (list, out); + } + + fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>); +} + +/// What a `PrimitiveRender` draws: this list's instances, and whatever +/// `instance_bindings` kept for them. +pub struct ListDraw<'a> { + pub instances: u32, + pub bindings: &'a [u32], +} + +/// The default: nothing sampled, every instance in one call. +pub struct Instanced; + +impl PrimitiveRender for Instanced { + fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) { + pass.draw(0..4, 0..list.instances); + } +} + +/// Which registered primitive an instance is. +pub struct PrimitiveKind

{ + id: u32, + _p: PhantomData, +} + +impl

PrimitiveKind

{ + fn new(id: u32) -> Self { + Self { + id, + _p: PhantomData, + } + } +} + +impl

Clone for PrimitiveKind

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

Copy for PrimitiveKind

{} + +/// Every primitive a ui can draw, in the order they were first drawn. +#[derive(Default)] +pub struct PrimitiveRegistry { + kinds: Vec, + ids: HashMap, +} + +pub struct PrimitiveSource { + pub wgsl: &'static str, + pub label: &'static str, + /// Size of one instance's entry, stated as the data binding's minimum. + pub stride: u64, + pub render: fn(&Device, &Queue) -> Box, +} + +impl PrimitiveRegistry { + /// 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, + render: P::render, + }); + kinds.len() as u32 - 1 + }); + PrimitiveKind::new(id) + } + + pub fn sources(&self) -> &[PrimitiveSource] { + &self.kinds + } +} + +/// 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, - data: PrimitiveData, free: Vec, + /// `stride` bytes of the primitive's own data per instance. + data: Vec, + /// From the type the list was made for, so a write is never checked. + stride: usize, +} + +impl InstanceList { + fn new() -> Self { + Self { + instances: Vec::new(), + assoc: Vec::new(), + free: Vec::new(), + data: Vec::new(), + stride: size_of::

(), + } + } + + pub fn instances(&self) -> &[PrimitiveInstance] { + &self.instances + } + + pub fn data(&self) -> &[u8] { + &self.data + } + + pub fn stride(&self) -> usize { + self.stride + } + + fn push(&mut self, id: WidgetId, inst: PrimitiveInstance, data: &[u8]) -> 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); + i + } else { + let i = self.instances.len(); + self.instances.push(inst); + self.assoc.push(id); + self.data.extend_from_slice(data); + i + } + } + + fn free(&mut self, i: usize) -> MaskIdx { + self.free.push(i); + self.instances[i].mask_idx + } + + fn apply_free(&mut self, kind: u32) -> impl Iterator { + 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); + 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]; + Some(PrimitiveChange { + id, + kind, + old: last, + new: i, + }) + }) + } +} + +/// 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. + primitives: Vec>, pub updated: bool, } -impl Default for Primitives { +impl Default for LayerDraws { fn default() -> Self { Self { - instances: Default::default(), - assoc: Default::default(), - data: Default::default(), - free: Vec::new(), + primitives: Vec::new(), updated: true, } } } -pub trait Primitive: Pod { - const BINDING: u32; - fn vec(data: &mut PrimitiveData) -> &mut PrimitiveVec; -} - -macro_rules! primitives { - ($($name:ident: $ty:ty => $binding:expr,)*) => { - #[derive(Default)] - pub struct PrimitiveData { - $(pub(crate) $name: PrimitiveVec<$ty>,)* - } - - 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 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, -} - -impl Primitives { +impl LayerDraws { pub fn write( &mut self, layer: usize, PrimitiveInst { + kind, id, primitive, region, @@ -117,65 +249,67 @@ impl Primitives { }: PrimitiveInst

, ) -> PrimitiveHandle { self.updated = true; - let vec = P::vec(&mut self.data); - let i = vec.add(primitive); - let inst = PrimitiveInstance { - region, - idx: i as u32, - mask_idx, - binding: P::BINDING, - }; - let inst_i = if let Some(i) = self.free.pop() { - self.instances[i] = inst; - self.assoc[i] = id; - i - } else { - let i = self.instances.len(); - self.instances.push(inst); - self.assoc.push(id); - i - }; - PrimitiveHandle::new::

(layer, inst_i, i) + // Grown on first use rather than sized from the registry, which a + // layer cannot see. + if self.primitives.len() <= kind.id as usize { + self.primitives.resize_with(kind.id as usize + 1, || None); + } + let inst_idx = self.primitives[kind.id as usize] + .get_or_insert_with(InstanceList::new::

) + .push( + id, + PrimitiveInstance { region, mask_idx }, + bytemuck::bytes_of(&primitive), + ); + PrimitiveHandle { + layer, + kind: kind.id, + inst_idx, + } + } + + pub fn primitives(&self) -> &[Option] { + &self.primitives } - /// returns (old index, new index) pub fn apply_free(&mut self) -> impl Iterator { - self.free.sort_by(|a, b| b.cmp(a)); - self.free.drain(..).filter_map(|i| { - self.instances.swap_remove(i); - self.assoc.swap_remove(i); - if i == self.instances.len() { - return None; - } - let id = self.assoc[i]; - let old = self.instances.len(); - Some(PrimitiveChange { id, old, new: i }) - }) + self.primitives + .iter_mut() + .enumerate() + .filter_map(|(kind, list)| Some((kind as u32, list.as_mut()?))) + .flat_map(|(kind, list)| list.apply_free(kind)) } pub fn free(&mut self, h: &PrimitiveHandle) -> MaskIdx { self.updated = true; - self.data.free(h.binding, h.data_idx); - self.free.push(h.inst_idx); - self.instances[h.inst_idx].mask_idx - } - - pub fn data(&self) -> &PrimitiveData { - &self.data - } - - pub fn instances(&self) -> &Vec { - &self.instances + self.list(h).free(h.inst_idx) } pub fn region_mut(&mut self, h: &PrimitiveHandle) -> &mut UiRegion { self.updated = true; - &mut self.instances[h.inst_idx].region + &mut self.list(h).instances[h.inst_idx].region } + + /// A handle is only ever made by `write`, which is what created the list. + fn list(&mut self, h: &PrimitiveHandle) -> &mut InstanceList { + self.primitives[h.kind as usize] + .as_mut() + .expect("handle names a primitive this layer never drew") + } +} + +pub struct PrimitiveInst

{ + pub kind: PrimitiveKind

, + pub id: WidgetId, + pub primitive: P, + pub region: UiRegion, + pub mask_idx: MaskIdx, } pub struct PrimitiveChange { pub id: WidgetId, + /// Which registered primitive's list moved, since they index separately. + pub kind: u32, pub old: usize, pub new: usize, } @@ -183,30 +317,12 @@ pub struct PrimitiveChange { #[derive(Debug)] pub struct PrimitiveHandle { pub layer: usize, + pub kind: u32, pub inst_idx: usize, - pub data_idx: usize, - pub binding: u32, } -impl PrimitiveHandle { - fn new(layer: usize, inst_idx: usize, data_idx: usize) -> Self { - Self { - layer, - inst_idx, - data_idx, - binding: P::BINDING, - } - } -} - -primitives!( - rects: RectPrimitive => 0, - textures: TexturePrimitive => 1, - glyphs: GlyphPrimitive => 2, -); - #[repr(C)] -#[derive(Copy, Clone)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct RectPrimitive { pub color: Color, pub radius: f32, @@ -214,6 +330,10 @@ pub struct RectPrimitive { pub inner_radius: f32, } +impl Primitive for RectPrimitive { + const WGSL: &'static str = include_str!("shader/rect.wgsl"); +} + impl RectPrimitive { pub fn color(color: Color) -> Self { Self { @@ -225,71 +345,51 @@ impl RectPrimitive { } } -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct TexturePrimitive { - pub view_idx: u32, - pub sampler_idx: u32, -} - -#[repr(C)] +/// `color` is multiplied by the atlas alpha for a mask glyph; a colour glyph +/// takes the texel unchanged, which `GlyphEntry::IS_COLORED` selects. +#[repr(C, align(8))] #[derive(Debug, Copy, Clone)] pub struct GlyphPrimitive { pub uv_min: Vec2, pub uv_max: Vec2, - pub view_idx: u32, - pub sampler_idx: u32, + /// Which atlas array layer this glyph is on. + pub layer: u32, pub color: Color, pub flags: u32, } -pub struct PrimitiveVec { - vec: Vec, - free: Vec, +// 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 { + const WGSL: &'static str = include_str!("shader/glyph.wgsl"); + + fn render(device: &Device, queue: &Queue) -> Box { + Box::new(GlyphRender::new(device, queue)) + } } -impl PrimitiveVec { - pub fn new() -> Self { +/// One drawn image. Its shader reads nothing per instance; the slot names the +/// texture to bind for it. +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct TexturePrimitive { + pub slot: u32, +} + +impl Primitive for TexturePrimitive { + const WGSL: &'static str = include_str!("shader/texture.wgsl"); + + fn render(device: &Device, queue: &Queue) -> Box { + Box::new(ImageRender::new(device, queue)) + } +} + +impl From<&TextureHandle> for TexturePrimitive { + fn from(handle: &TextureHandle) -> Self { Self { - vec: Vec::new(), - free: Vec::new(), + slot: handle.slot(), } } - 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 - } } diff --git a/core/src/render/shader.wgsl b/core/src/render/shader.wgsl deleted file mode 100644 index 1e6df68..0000000 --- a/core/src/render/shader.wgsl +++ /dev/null @@ -1,204 +0,0 @@ -const RECT: u32 = 0u; -const TEXTURE: u32 = 1u; -const GLYPH: u32 = 2u; - -@group(0) @binding(0) -var window: WindowUniform; -@group(1) @binding(RECT) -var rects: array; -@group(1) @binding(TEXTURE) -var textures: array; -@group(1) @binding(GLYPH) -var glyphs: array; - -struct Rect { - color: u32, - radius: f32, - thickness: f32, - inner_radius: f32, -} - -struct TextureInfo { - view_idx: u32, - sampler_idx: u32, -} - -struct GlyphInfo { - uv_min: vec2, - uv_max: vec2, - view_idx: u32, - sampler_idx: 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, -} - -@group(2) @binding(0) -var views: binding_array>; -@group(2) @binding(1) -var samplers: binding_array; -@group(2) @binding(2) -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, textures[i]); - } - 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; -} - -// TODO: this seems really inefficient (per frag indexing)? -fn draw_texture(region: Region, info: TextureInfo) -> vec4 { - return textureSample(views[info.view_idx], samplers[info.sampler_idx], region.uv); -} - -fn draw_glyph(region: Region, g: GlyphInfo) -> vec4 { - let uv = mix(g.uv_min, g.uv_max, region.uv); - let texel = textureSample(views[g.view_idx], samplers[g.sampler_idx], uv); - 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..ba39d19 --- /dev/null +++ b/core/src/render/shader/glyph.wgsl @@ -0,0 +1,33 @@ +// Matches `GlyphEntry::IS_COLORED`. +const COLORED: u32 = 1u; + +// The glyph atlas, whose array layers are its pages. +@group(2) @binding(0) +var atlas: texture_2d_array; +@group(2) @binding(1) +var samp: sampler; + +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(atlas, samp, uv, i32(g.layer)); + if (g.flags & COLORED) != 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..02c47e0 --- /dev/null +++ b/core/src/render/shader/prelude.wgsl @@ -0,0 +1,96 @@ +// 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. 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; +@group(0) @binding(1) +var masks: array; + +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) @interpolate(flat) mask_idx: u32, + @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..9ec7d5c --- /dev/null +++ b/core/src/render/shader/texture.wgsl @@ -0,0 +1,10 @@ +// The image this instance draws, bound for it alone. +@group(2) @binding(0) +var image: texture_2d; +@group(2) @binding(1) +var samp: sampler; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return masked(in, textureSample(image, samp, in.uv)); +} diff --git a/core/src/render/texture.rs b/core/src/render/texture.rs index b7af152..af1a1ed 100644 --- a/core/src/render/texture.rs +++ b/core/src/render/texture.rs @@ -1,119 +1,119 @@ -use image::{DynamicImage, EncodableLayout, GenericImageView}; +use image::{DynamicImage, EncodableLayout, GenericImageView, RgbaImage}; use wgpu::{util::DeviceExt, *}; -use crate::{PatchRect, TextureUpdate, Textures}; +use crate::{ + PatchRect, TextureUpdate, Textures, UiData, + render::{ + TexturePrimitive, + primitive::{ListDraw, PrimitiveRender}, + }, +}; +/// Draws standalone images, which it owns. Each is its own texture, so each +/// instance binds its own and is a draw of its own. +pub struct ImageRender { + textures: GpuTextures, + layout: BindGroupLayout, + sampler: Sampler, +} + +impl ImageRender { + pub fn new(device: &Device, queue: &Queue) -> Self { + Self { + textures: GpuTextures::new(device, queue), + layout: sampled_layout(device, TextureViewDimension::D2, "ui image"), + sampler: default_sampler(device), + } + } +} + +impl PrimitiveRender for ImageRender { + fn layout(&self) -> Option<&BindGroupLayout> { + Some(&self.layout) + } + + fn update(&mut self, ui: &mut UiData) { + self.textures + .update(&mut ui.textures, &self.layout, &self.sampler); + } + + fn instance_bindings(&self, list: &super::InstanceList, out: &mut Vec) { + let slots = list + .data() + .chunks_exact(list.stride()) + .map(|data| bytemuck::pod_read_unaligned::(data).slot); + out.extend(slots); + } + + fn draw<'a>(&'a self, pass: &mut RenderPass<'a>, list: ListDraw<'a>) { + for (i, &slot) in list.bindings.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); + } + } +} + +/// 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, queue: Queue, - /// Parallel to `views`; patches require textures rather than views. - textures: Vec>, - views: Vec, - view_count: usize, - samplers: Vec, - null_view: TextureView, - no_views: Vec, + slots: Vec>, +} + +struct ImageGpu { + /// Kept for `patch`, which needs the texture rather than the view. + texture: Texture, + group: BindGroup, } impl GpuTextures { - pub fn update(&mut self, textures: &mut Textures) -> bool { - let mut bindings_changed = false; + pub fn new(device: &Device, queue: &Queue) -> Self { + Self { + device: device.clone(), + queue: queue.clone(), + slots: Vec::new(), + } + } + + pub fn update(&mut self, textures: &mut Textures, layout: &BindGroupLayout, sampler: &Sampler) { for update in textures.updates() { - bindings_changed |= match update { + match update { TextureUpdate::Push(image) => { - self.push(image); - true + let image = self.create(image, layout, sampler); + self.slots.push(Some(image)); } TextureUpdate::Set(i, image) => { - self.set(i, image); - true + let image = self.create(image, layout, sampler); + self.slots[i as usize] = Some(image); } - TextureUpdate::Patch(i, rect, image) => { - self.patch(i, rect, image); - false - } - TextureUpdate::SetFree => { - self.view_count += 1; - true - } - TextureUpdate::Free(i) => { - self.free(i); - true - } - TextureUpdate::PushFree => { - self.push_free(); - true - } - }; + TextureUpdate::Patch(i, rect, image) => self.patch(i, rect, image), + TextureUpdate::PushFree => self.slots.push(None), + TextureUpdate::SetFree => {} + TextureUpdate::Free(i) => self.slots[i as usize] = None, + } } - bindings_changed - } - fn set(&mut self, i: u32, image: &DynamicImage) { - self.view_count += 1; - let (texture, view) = self.create(image); - self.textures[i as usize] = Some(texture); - self.views[i as usize] = view; - } - fn free(&mut self, i: u32) { - self.view_count -= 1; - self.textures[i as usize] = None; - self.views[i as usize] = self.null_view.clone(); - } - fn push(&mut self, image: &DynamicImage) { - self.view_count += 1; - let (texture, view) = self.create(image); - self.textures.push(Some(texture)); - self.views.push(view); - } - fn push_free(&mut self) { - self.view_count += 1; - self.textures.push(None); - self.views.push(self.null_view.clone()); } - fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) { - let Some(texture) = &self.textures[i as usize] else { - return; - }; - if rect.width == 0 || rect.height == 0 { - return; - } - // `write_texture` requires tightly packed rows, unlike the atlas image. - let sub = image - .view(rect.x, rect.y, rect.width, rect.height) - .to_image(); - self.queue.write_texture( - TexelCopyTextureInfo { - texture, - mip_level: 0, - origin: Origin3d { - x: rect.x, - y: rect.y, - z: 0, - }, - aspect: TextureAspect::All, - }, - sub.as_bytes(), - TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(rect.width * 4), - rows_per_image: Some(rect.height), - }, - Extent3d { - width: rect.width, - height: rect.height, - depth_or_array_layers: 1, - }, - ); + pub fn group(&self, slot: u32) -> Option<&BindGroup> { + self.slots.get(slot as usize)?.as_ref().map(|i| &i.group) } - fn create(&self, image: &DynamicImage) -> (Texture, TextureView) { - let image = image.to_rgba8(); - let (width, height) = image.dimensions(); + 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( &self.queue, &TextureDescriptor { - label: None, + label: Some("image"), size: Extent3d { width, height, @@ -127,63 +127,115 @@ impl GpuTextures { view_formats: &[], }, wgt::TextureDataOrder::MipMajor, - image.as_bytes(), + rgba.as_bytes(), ); let view = texture.create_view(&TextureViewDescriptor::default()); - (texture, view) + let group = sampled_group(&self.device, layout, &view, sampler, "ui image"); + ImageGpu { texture, group } } - pub fn new(device: &Device, queue: &Queue) -> Self { - let null_view = null_texture_view(device); - Self { - device: device.clone(), - queue: queue.clone(), - textures: Vec::new(), - views: Vec::new(), - samplers: vec![default_sampler(device)], - no_views: vec![null_view.clone()], - null_view, - view_count: 0, + fn patch(&mut self, i: u32, rect: PatchRect, image: &DynamicImage) { + let Some(Some(slot)) = self.slots.get(i as usize) else { + return; + }; + let dst = TexelCopyTextureInfo { + texture: &slot.texture, + mip_level: 0, + origin: Origin3d { + x: rect.x, + y: rect.y, + z: 0, + }, + aspect: TextureAspect::All, + }; + match image.as_rgba8() { + Some(rgba) => write_region(&self.queue, dst, rgba, rect), + // The texture is rgba8, so any other layout has to be converted -- + // and converting the rectangle is cheaper than the whole image. + None => { + let sub = image + .view(rect.x, rect.y, rect.width, rect.height) + .to_image(); + write_region(&self.queue, dst, &sub, PatchRect { x: 0, y: 0, ..rect }); + } } } - - pub fn views(&self) -> Vec<&TextureView> { - if self.views.is_empty() { - &self.no_views - } else { - &self.views - } - .iter() - .by_ref() - .collect() - } - - pub fn samplers(&self) -> Vec<&Sampler> { - self.samplers.iter().by_ref().collect() - } - - pub fn view_count(&self) -> usize { - self.view_count - } } -pub fn null_texture_view(device: &Device) -> TextureView { - device - .create_texture(&TextureDescriptor { - label: Some("null"), - size: Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, +pub fn write_region(queue: &Queue, dst: TexelCopyTextureInfo, src: &RgbaImage, rect: PatchRect) { + if rect.width == 0 || rect.height == 0 { + return; + } + let stride = src.width() * 4; + queue.write_texture( + dst, + src.as_bytes(), + TexelCopyBufferLayout { + offset: (rect.y * stride + rect.x * 4) as u64, + bytes_per_row: Some(stride), + rows_per_image: Some(rect.height), + }, + Extent3d { + width: rect.width, + height: rect.height, + depth_or_array_layers: 1, + }, + ); +} + +/// What a primitive that samples binds: a texture, and the sampler that reads +/// it. +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), }, - mip_level_count: 1, - sample_count: 1, - dimension: TextureDimension::D2, - format: TextureFormat::Rgba8Unorm, - usage: TextureUsages::TEXTURE_BINDING, - view_formats: &[], - }) - .create_view(&TextureViewDescriptor::default()) + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(sampler), + }, + ], + label: Some(label), + }) +} + +/// The layout for one of those. The dimension differs -- the atlas is an +/// array of pages and an image is not -- and nothing else does. +pub 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: dimension, + multisampled: false, + }, + count: None, + }, + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Sampler(SamplerBindingType::NonFiltering), + count: None, + }, + ], + label: Some(label), + }) } pub fn default_sampler(device: &Device) -> Sampler { diff --git a/core/src/render/util/mod.rs b/core/src/render/util/mod.rs index c9d48ff..0b2cba3 100644 --- a/core/src/render/util/mod.rs +++ b/core/src/render/util/mod.rs @@ -21,17 +21,25 @@ impl ArrBuf { _pd: PhantomData, } } - pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) { - if self.len != data.len() { + /// Returns whether the `Buffer` was recreated, which stales any cached + /// `BindGroup` holding it. + pub fn update(&mut self, device: &Device, queue: &Queue, data: &[T]) -> bool { + let resized = self.len != data.len(); + if resized { self.len = data.len(); self.buffer = Self::init_buf(device, std::mem::size_of_val(data), self.usage, self.label); } queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(data)); + resized + } + pub fn len(&self) -> usize { + self.len } fn init_buf(device: &Device, size: usize, usage: BufferUsages, label: &'static str) -> Buffer { let mut size = size as u64; if usage.contains(BufferUsages::STORAGE) { + // A binding cannot be empty or under the layout's minimum. size = size.max(std::mem::size_of::() as u64); } device.create_buffer(&BufferDescriptor { @@ -41,8 +49,4 @@ impl ArrBuf { usage, }) } - #[allow(clippy::len_without_is_empty)] - pub fn len(&self) -> usize { - self.len - } } diff --git a/core/src/ui/mod.rs b/core/src/ui/mod.rs index 2998cbc..607dd82 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; @@ -7,13 +9,15 @@ mod render_state; mod size; pub use active::*; -pub use painter::Painter; +pub use painter::{Painter, PrimitiveLike}; pub use render_state::*; 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 052b049..5ec130b 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -1,7 +1,10 @@ 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::{ + GlyphPrimitive, Mask, MaskIdx, Primitive, PrimitiveHandle, PrimitiveInst, PrimitiveKind, + TexturePrimitive, + }, util::Vec2, }; @@ -21,15 +24,26 @@ pub struct Painter<'a> { impl<'a> Painter<'a> { fn primitive_at(&mut self, primitive: P, region: UiRegion) { + let kind = self.rsc.ui_mut().primitives.kind::

(); + self.write(kind, primitive, region); + } + + /// Takes the kind, for a caller writing many of one primitive. + fn write(&mut self, kind: PrimitiveKind

, primitive: P, region: UiRegion) { let h = self.state.layers.write( self.layer, PrimitiveInst { + kind, id: self.id, primitive, region, mask_idx: self.mask, }, ); + self.push_primitive(h); + } + + fn push_primitive(&mut self, h: PrimitiveHandle) { if self.mask != MaskIdx::NONE { // TODO: I have no clue if this works at all :joy: self.rsc.ui_mut().masks.push_ref(self.mask); @@ -38,11 +52,13 @@ impl<'a> Painter<'a> { } /// Writes a primitive to be rendered - pub fn primitive(&mut self, primitive: P) { + pub fn primitive(&mut self, primitive: impl PrimitiveLike) { + let primitive = primitive.into_primitive(self); self.primitive_at(primitive, self.region) } - pub fn primitive_within(&mut self, primitive: P, region: UiRegion) { + pub fn primitive_within(&mut self, primitive: impl PrimitiveLike, region: UiRegion) { + let primitive = primitive.into_primitive(self); self.primitive_at(primitive, region.within(&self.region)); } @@ -75,21 +91,6 @@ impl<'a> Painter<'a> { ); } - pub fn texture_within(&mut self, handle: &TextureHandle, region: UiRegion) { - self.textures.push(handle.clone()); - self.primitive_at(handle.primitive(), region.within(&self.region)); - } - - pub fn texture(&mut self, handle: &TextureHandle) { - self.textures.push(handle.clone()); - self.primitive(handle.primitive()); - } - - pub fn texture_at(&mut self, handle: &TextureHandle, region: UiRegion) { - self.textures.push(handle.clone()); - self.primitive_at(handle.primitive(), region); - } - pub fn render_text( &mut self, buffer: &mut TextBuffer, @@ -97,10 +98,12 @@ impl<'a> Painter<'a> { width: Option, ) -> RenderedText { let ui = self.rsc.ui_mut(); - ui.text.render(buffer, attrs, width, &mut ui.textures) + ui.text.render(buffer, attrs, width) } + // TODO: merge the text methods into the primitive ones. 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; @@ -108,12 +111,12 @@ 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( + self.write( + kind, GlyphPrimitive { uv_min: glyph.entry.uv_min, uv_max: glyph.entry.uv_max, - view_idx: glyph.entry.view_idx, - sampler_idx: glyph.entry.sampler_idx, + layer: glyph.entry.layer, color: text.color, flags: glyph.entry.flags(), }, @@ -169,3 +172,28 @@ impl<'a> Painter<'a> { self.state.size_ctx(self.id, self.region.size(), self.rsc) } } + +/// What `Painter::primitive` takes: a primitive, or something that yields one +/// and does whatever else drawing it needs. +pub trait PrimitiveLike { + type Primitive: Primitive; + fn into_primitive(self, painter: &mut Painter) -> Self::Primitive; +} + +impl PrimitiveLike for P { + type Primitive = P; + fn into_primitive(self, _: &mut Painter) -> P { + self + } +} + +impl PrimitiveLike for &TextureHandle { + type Primitive = TexturePrimitive; + + /// Retains a share of the handle, so the slot the primitive names cannot + /// be freed and reused while it is still drawn. + fn into_primitive(self, painter: &mut Painter) -> TexturePrimitive { + painter.textures.push(self.clone()); + self.into() + } +} diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index ad8afab..4bd7a90 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -1,13 +1,13 @@ use crate::{ - ActiveData, Axis, IdLike, MaskIdx, Painter, PixelRegion, PrimitiveLayers, SizeCtx, - StrongWidget, UiRegion, UiRsc, UiVec2, WidgetId, Widgets, + ActiveData, Axis, DrawLayers, IdLike, MaskIdx, Painter, PixelRegion, SizeCtx, StrongWidget, + UiRegion, UiRsc, UiVec2, WidgetId, Widgets, ui::cache::Cache, util::{HashMap, HashSet, Vec2, forget_ref}, }; pub struct UiRenderState { pub active: HashMap, - pub layers: PrimitiveLayers, + pub layers: DrawLayers, pub(super) output_size: Vec2, pub cache: Cache, @@ -243,14 +243,14 @@ impl UiRenderState { } pub fn debug_layers(&self) { - for ((idx, depth), primitives) in self.layers.iter_depth() { + for ((idx, depth), draws) in self.layers.iter_depth() { let indent = " ".repeat(depth * 2); - let len = primitives.instances().len(); - print!("{indent}{idx}: {len} primitives"); - if len >= 1 { - print!(" ({})", primitives.instances()[0].binding); - } - println!(); + let counts: Vec = draws + .primitives() + .iter() + .map(|l| l.as_ref().map_or(0, |l| l.instances().len()).to_string()) + .collect(); + println!("{indent}{idx}: [{}]", counts.join(", ")); } } @@ -308,7 +308,6 @@ impl UiRenderState { source, cache: &mut self.cache, text: &mut ui.text, - textures: &mut ui.textures, widgets: &ui.widgets, outer, output_size: self.output_size, diff --git a/core/src/ui/size.rs b/core/src/ui/size.rs index a874cf9..931a1b2 100644 --- a/core/src/ui/size.rs +++ b/core/src/ui/size.rs @@ -1,11 +1,10 @@ use crate::{ - Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, Textures, - UiVec2, WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2, + Axis, AxisT, IdLike, Len, RenderedText, Size, TextAttrs, TextBuffer, TextData, UiVec2, + WidgetAxisFns, WidgetId, Widgets, XAxis, YAxis, ui::cache::Cache, util::Vec2, }; pub struct SizeCtx<'a> { pub text: &'a mut TextData, - pub textures: &'a mut Textures, pub(super) source: WidgetId, pub(super) widgets: &'a Widgets, pub(super) cache: &'a mut Cache, @@ -33,7 +32,6 @@ impl SizeCtx<'_> { .get_dyn_dynamic(id) .desired_len::(&mut SizeCtx { text: self.text, - textures: self.textures, source: self.source, widgets: self.widgets, cache: self.cache, @@ -82,7 +80,7 @@ impl SizeCtx<'_> { attrs: &TextAttrs, width: Option, ) -> RenderedText { - self.text.render(buffer, attrs, width, self.textures) + self.text.render(buffer, attrs, width) } pub fn label(&self, id: WidgetId) -> &String { diff --git a/examples/tabs/main.rs b/examples/tabs/main.rs index 6316924..ba314c6 100644 --- a/examples/tabs/main.rs +++ b/examples/tabs/main.rs @@ -212,10 +212,10 @@ impl DefaultAppState for Client { render: &mut UiRenderState, ) { let new = format!( - "widgets: {}\nactive: {}\nviews: {}", + "widgets: {}\nactive: {}\ntextures: {}", rsc.widgets().len(), render.active_widgets(), - self.ui_state.renderer.ui.view_count(), + rsc.ui().textures.count(), ); if new != *rsc.widgets()[self.info].content { *rsc.widgets_mut()[self.info].content = new; diff --git a/src/default/render.rs b/src/default/render.rs index 5fbaf33..b5e5484 100644 --- a/src/default/render.rs +++ b/src/default/render.rs @@ -1,4 +1,4 @@ -use iris_core::{UiData, UiLimits, UiRenderNode, UiRenderState}; +use iris_core::{UiData, UiRenderNode, UiRenderState}; use pollster::FutureExt; use std::sync::Arc; use wgpu::*; @@ -83,18 +83,9 @@ impl UiRenderer { .block_on() .expect("Could not get adapter!"); - let ui_limits = UiLimits::default(); - let (device, queue) = adapter .request_device(&DeviceDescriptor { - required_features: Features::TEXTURE_BINDING_ARRAY - | Features::PARTIALLY_BOUND_BINDING_ARRAY - | Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, required_limits: Limits { - max_binding_array_elements_per_shader_stage: ui_limits - .max_binding_array_elements_per_shader_stage(), - max_binding_array_sampler_elements_per_shader_stage: ui_limits - .max_binding_array_sampler_elements_per_shader_stage(), max_buffer_size: 1 << 30, ..Default::default() }, @@ -126,7 +117,7 @@ impl UiRenderer { let encoder = Self::create_encoder(&device); - let ui = UiRenderNode::new(&device, &queue, &config, ui_limits); + let ui = UiRenderNode::new(&device, &config); Self { surface, diff --git a/src/widget/image.rs b/src/widget/image.rs index 244bbb8..86d2027 100644 --- a/src/widget/image.rs +++ b/src/widget/image.rs @@ -7,7 +7,7 @@ pub struct Image { impl Widget for Image { fn draw(&mut self, painter: &mut Painter) { - painter.texture(&self.handle); + painter.primitive(&self.handle); } fn desired_width(&mut self, _: &mut SizeCtx) -> Len { diff --git a/tests/draw_cost.rs b/tests/draw_cost.rs new file mode 100644 index 0000000..6e36c35 --- /dev/null +++ b/tests/draw_cost.rs @@ -0,0 +1,205 @@ +//! What one frame of `UiRenderNode::draw` costs on the CPU, against the number +//! of layers it walks. Recording only: the pass is built and dropped without +//! being submitted, so this is the loop's cost and not the GPU's. +//! +//! cargo test --release --test draw_cost -- --ignored --nocapture +//! +//! **Read the instruction count, not the clock.** Wall time here swings by 2x +//! between runs of one binary on this machine -- more under `cargo test` than +//! run directly -- while instructions retired are stable to 0.1%: +//! +//! perf stat -e instructions:u target/release/.../draw_cost-* --ignored +//! +//! Measured that way on 2026-09-13, drawing each primitive through its own +//! `PrimitiveRender` rather than a match in the renderer costs **6 +//! instructions per list drawn**, which is 0.1% of a frame at both 256 and +//! 1024 layers. Recording one list into the pass costs wgpu ~5,400. +//! +//! The instance is leaked on purpose. Dropping the last one makes the Vulkan +//! loader unload Mesa's ICD, which faults when a thread that touched Vulkan +//! exits -- and libtest runs every test on a spawned thread. + +use std::time::Instant; + +use iris::prelude::*; +use iris_core::{ + GlyphPrimitive, MaskIdx, PrimitiveInst, RectPrimitive, TextureHandle, TexturePrimitive, UiData, + UiRegion, UiRenderNode, UiRenderState, +}; +use wgpu::{Color as GpuColor, *}; + +const SIZE: u32 = 1024; +const FRAMES: u32 = 200; +/// Reported as the best of this many batches. The mean moves by 15% between +/// runs on this machine, which is more than the thing being measured. +const BATCHES: u32 = 8; + +fn gpu() -> Option<(Device, Queue)> { + // Probed rather than assumed: this machine's Vulkan device comes and goes, + // and GL is what is left when it is gone. + let all = Instance::new(&InstanceDescriptor::default()); + let instance = match pollster::block_on(all.request_adapter(&RequestAdapterOptions::default())) + { + Ok(_) => all, + Err(_) => Instance::new(&InstanceDescriptor { + backends: Backends::GL, + ..Default::default() + }), + }; + // Leaked rather than dropped: see the note at the top of the file. + let instance: &'static Instance = Box::leak(Box::new(instance)); + let adapter = + pollster::block_on(instance.request_adapter(&RequestAdapterOptions::default())).ok()?; + println!("adapter: {:?}", adapter.get_info()); + pollster::block_on(adapter.request_device(&DeviceDescriptor::default())).ok() +} + +fn config(format: TextureFormat) -> SurfaceConfiguration { + SurfaceConfiguration { + usage: TextureUsages::RENDER_ATTACHMENT, + format, + width: SIZE, + height: SIZE, + present_mode: PresentMode::Fifo, + desired_maximum_frame_latency: 2, + alpha_mode: CompositeAlphaMode::Auto, + view_formats: vec![], + } +} + +/// Every layer draws all three primitives, so the renderer takes a different +/// path for each list it walks -- which is the case a single-primitive layer +/// would never exercise. Images are bound per instance, so there are few. +fn fill( + ui: &mut UiData, + render: &mut UiRenderState, + layers: usize, + per_layer: usize, +) -> Vec { + let rect = ui.primitives.kind::(); + let glyph = ui.primitives.kind::(); + let texture = ui.primitives.kind::(); + let id = ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id(); + let handles: Vec<_> = (0..4) + .map(|_| ui.textures.add(image::RgbaImage::new(4, 4))) + .collect(); + + let mut layer = 0; + for _ in 0..layers { + for _ in 0..per_layer { + render.layers.write( + layer, + PrimitiveInst { + kind: rect, + id, + primitive: RectPrimitive::color(UiColor::WHITE), + region: UiRegion::FULL, + mask_idx: MaskIdx::NONE, + }, + ); + render.layers.write( + layer, + PrimitiveInst { + kind: glyph, + id, + primitive: GlyphPrimitive { + uv_min: vec2(0.0, 0.0), + uv_max: vec2(1.0, 1.0), + layer: 0, + color: UiColor::WHITE, + flags: 0, + }, + region: UiRegion::FULL, + mask_idx: MaskIdx::NONE, + }, + ); + } + for h in &handles[..2] { + render.layers.write( + layer, + PrimitiveInst { + kind: texture, + id, + primitive: TexturePrimitive::from(h), + region: UiRegion::FULL, + mask_idx: MaskIdx::NONE, + }, + ); + } + layer = render.layers.next(layer); + } + handles +} + +fn frame_cost(device: &Device, queue: &Queue, layers: usize, per_layer: usize) -> f64 { + let format = TextureFormat::Bgra8Unorm; + let mut node = UiRenderNode::new(device, &config(format)); + let mut ui = UiData::default(); + let mut render = UiRenderState::new(); + let _handles = fill(&mut ui, &mut render, layers, per_layer); + node.update(device, queue, &mut ui, &mut render); + + let target = device.create_texture(&TextureDescriptor { + label: Some("draw cost"), + size: Extent3d { + width: SIZE, + height: SIZE, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + let view = target.create_view(&TextureViewDescriptor::default()); + + let record = |frames: u32| { + let start = Instant::now(); + for _ in 0..frames { + let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor::default()); + { + let pass = &mut encoder.begin_render_pass(&RenderPassDescriptor { + color_attachments: &[Some(RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(GpuColor::BLACK), + store: StoreOp::Store, + }, + depth_slice: None, + })], + ..Default::default() + }); + node.draw(pass); + } + drop(encoder.finish()); + } + start.elapsed().as_secs_f64() / frames as f64 + }; + record(FRAMES / 4); + (0..BATCHES) + .map(|_| record(FRAMES)) + .fold(f64::MAX, f64::min) +} + +#[test] +#[ignore = "measurement, not a check"] +fn draw_cost_by_layer_count() { + let Some((device, queue)) = gpu() else { + panic!("no wgpu device; see the this-machine-graphics notes"); + }; + println!( + "layers, each 8 rects + 8 glyphs + 2 images: us/frame (us per layer), best of {BATCHES}" + ); + let base = frame_cost(&device, &queue, 1, 8) * 1e6; + for layers in [8, 64, 256, 1024] { + let per_frame = frame_cost(&device, &queue, layers, 8) * 1e6; + // Net of the empty pass, which is the same in any version of this. + println!( + "{layers:>5}: {per_frame:8.1} us ({:.3} us)", + (per_frame - base).max(0.0) / layers as f64 + ); + } +}