diff --git a/renderer/src/pods.rs b/renderer/src/pods.rs index 49cf7cd1..3fc9793f 100644 --- a/renderer/src/pods.rs +++ b/renderer/src/pods.rs @@ -1,3 +1,4 @@ +#![allow(unused)] use std::{ fmt, mem::{self, size_of}, @@ -91,7 +92,6 @@ pub struct TextureVertex { } impl TextureVertex { - #[allow(unused)] pub fn new(position: impl Into, uv: (f32, f32)) -> Self { Self { position: position.into(), @@ -113,7 +113,6 @@ impl VertexLayout for TextureVertex { } } -#[allow(unused)] #[repr(C)] #[derive(Copy, Clone, Debug, Pod, Zeroable)] pub struct ColorVertex { @@ -121,7 +120,6 @@ pub struct ColorVertex { pub color: Color, } -#[allow(unused)] impl ColorVertex { pub fn new(position: impl Into, color: impl Into) -> Self { Self { diff --git a/renderer/src/text_layer/atlas_renderer.rs b/renderer/src/text_layer/atlas_renderer.rs index 272957ba..7e13dc04 100644 --- a/renderer/src/text_layer/atlas_renderer.rs +++ b/renderer/src/text_layer/atlas_renderer.rs @@ -21,7 +21,7 @@ pub struct AtlasRenderer { } impl AtlasRenderer { - pub fn new( + pub fn new( device: &wgpu::Device, atlas_format: wgpu::TextureFormat, shader: wgpu::ShaderModuleDescriptor<'_>, @@ -46,7 +46,8 @@ impl AtlasRenderer { write_mask: wgpu::ColorWrites::ALL, })]; - let vertex_layout = [VertexT::layout()]; + // One vertex buffer layout describing instance attributes. + let vertex_layout = [InstanceVertexT::layout()]; let pipeline = create_pipeline( "Atlas Pipeline", @@ -66,20 +67,15 @@ impl AtlasRenderer { } } - // Convert a number of instances to a batch. - pub fn batch( + // Build a batch from instance data, uploading one instance per glyph. + pub fn batch_instances( &self, context: &PreparationContext, - instances: &[InstanceT], + instances: &[InstanceVertexT], ) -> Option { if instances.is_empty() { return None; } - let mut vertices = Vec::with_capacity(instances.len() * 4); - - for instance in instances { - vertices.extend(instance.to_vertices()); - } let device = context.device; @@ -90,8 +86,8 @@ impl AtlasRenderer { ); let vertex_buffer = device.create_buffer_init(&BufferInitDescriptor { - label: Some("Atlas Vertex Buffer"), - contents: bytemuck::cast_slice(&vertices), + label: Some("Atlas Instance Buffer"), + contents: bytemuck::cast_slice(instances), usage: wgpu::BufferUsages::VERTEX, }); @@ -107,75 +103,80 @@ impl AtlasRenderer { } } -pub trait AtlasInstance { - type Vertex: Pod; - - fn to_vertices(&self) -> [Self::Vertex; 4]; -} - +// Instance vertex types used by the shaders pub mod sdf_atlas { - use massive_geometry::{Color, Point3}; - - use super::AtlasInstance; - use crate::{glyph::glyph_atlas, pods::TextureColorVertex}; - - #[derive(Debug)] - pub struct QuadInstance { - pub atlas_rect: glyph_atlas::Rectangle, - pub vertices: [Point3; 4], + use bytemuck::{Pod, Zeroable}; + + use crate::pods::Color; + + #[repr(C)] + #[derive(Copy, Clone, Debug, Pod, Zeroable)] + pub struct Instance { + // pos_lt.xy, pos_rb.xy + pub pos_lt: [f32; 2], + pub pos_rb: [f32; 2], + // uv_lt.xy, uv_rb.xy + pub uv_lt: [f32; 2], + pub uv_rb: [f32; 2], + // color rgba pub color: Color, + // depth (z in pixel space) + pub depth: f32, } - impl AtlasInstance for QuadInstance { - type Vertex = TextureColorVertex; - - fn to_vertices(&self) -> [Self::Vertex; 4] { - let r = self.atlas_rect; - // ADR: u/v normalization is done in the shader, because its probably free and we don't - // have to care about the atlas texture growing as long the rects stay the same. - let (ltx, lty) = (r.min.x as f32, r.min.y as f32); - let (rbx, rby) = (r.max.x as f32, r.max.y as f32); - - let v = &self.vertices; - let color = self.color; - [ - TextureColorVertex::new(v[0], (ltx, lty), color), - TextureColorVertex::new(v[1], (ltx, rby), color), - TextureColorVertex::new(v[2], (rbx, rby), color), - TextureColorVertex::new(v[3], (rbx, lty), color), - ] + impl super::VertexLayout for Instance { + fn layout() -> wgpu::VertexBufferLayout<'static> { + use std::mem::size_of; + use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, VertexStepMode}; + const ATTRS: [VertexAttribute; 6] = wgpu::vertex_attr_array![ + 0 => Float32x2, // pos_lt + 1 => Float32x2, // pos_rb + 2 => Float32x2, // uv_lt + 3 => Float32x2, // uv_rb + 4 => Float32x4, // color + 5 => Float32, // depth + ]; + VertexBufferLayout { + array_stride: size_of::() as BufferAddress, + step_mode: VertexStepMode::Instance, + attributes: &ATTRS, + } } } } pub mod color_atlas { - use massive_geometry::Point3; - - use super::AtlasInstance; - use crate::{glyph::glyph_atlas, pods::TextureVertex}; - - #[derive(Debug)] - pub struct QuadInstance { - pub atlas_rect: glyph_atlas::Rectangle, - pub vertices: [Point3; 4], + use bytemuck::{Pod, Zeroable}; + + use crate::pods::VertexLayout; + + #[repr(C)] + #[derive(Copy, Clone, Debug, Pod, Zeroable)] + pub struct Instance { + pub pos_lt: [f32; 2], + pub pos_rb: [f32; 2], + pub uv_lt: [f32; 2], + pub uv_rb: [f32; 2], + // depth (z in pixel space) + pub depth: f32, } - impl AtlasInstance for QuadInstance { - type Vertex = TextureVertex; - - fn to_vertices(&self) -> [Self::Vertex; 4] { - let r = self.atlas_rect; - // ADR: u/v normalization is done in the shader. Its probably free, and we don't have to - // care about the atlas texture growing as long the rects stay the same. - let (ltx, lty) = (r.min.x as f32, r.min.y as f32); - let (rbx, rby) = (r.max.x as f32, r.max.y as f32); - let v = &self.vertices; - [ - TextureVertex::new(v[0], (ltx, lty)), - TextureVertex::new(v[1], (ltx, rby)), - TextureVertex::new(v[2], (rbx, rby)), - TextureVertex::new(v[3], (rbx, lty)), - ] + impl VertexLayout for Instance { + fn layout() -> wgpu::VertexBufferLayout<'static> { + use std::mem::size_of; + use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, VertexStepMode}; + const ATTRS: [VertexAttribute; 5] = wgpu::vertex_attr_array![ + 0 => Float32x2, // pos_lt + 1 => Float32x2, // pos_rb + 2 => Float32x2, // uv_lt + 3 => Float32x2, // uv_rb + 4 => Float32, // depth + ]; + VertexBufferLayout { + array_stride: size_of::() as BufferAddress, + step_mode: VertexStepMode::Instance, + attributes: &ATTRS, + } } } } diff --git a/renderer/src/text_layer/renderer.rs b/renderer/src/text_layer/renderer.rs index 2133f548..d4e06b09 100644 --- a/renderer/src/text_layer/renderer.rs +++ b/renderer/src/text_layer/renderer.rs @@ -5,9 +5,6 @@ use std::{ use anyhow::Result; use cosmic_text::{self as text, FontSystem}; -use massive_geometry::{Point, Point3}; -use massive_scene::{Change, Id, Matrix, SceneChange, Shape, VisualRenderObj}; -use massive_shapes::{GlyphRun, RunGlyph, TextWeight}; use swash::{Weight, scale::ScaleContext}; use text::SwashContent; use wgpu::Device; @@ -17,12 +14,14 @@ use crate::{ GlyphRasterizationParam, RasterizedGlyphKey, SwashRasterizationParam, glyph_atlas, glyph_rasterization::rasterize_glyph_with_padding, }, - pods::{AsBytes, TextureColorVertex, TextureVertex, ToPod}, + pods::{AsBytes, ToPod}, renderer::{PreparationContext, RenderContext}, scene::{IdTable, LocationMatrices}, - text_layer::atlas_renderer::{AtlasRenderer, color_atlas, sdf_atlas}, + text_layer::atlas_renderer::{self, AtlasRenderer, color_atlas, sdf_atlas}, tools::QuadIndexBuffer, }; +use massive_scene::{Change, Id, Matrix, SceneChange, Shape, VisualRenderObj}; +use massive_shapes::{GlyphRun, RunGlyph, TextWeight}; pub struct TextLayerRenderer { // Optimization: This is used for get_font() only, which needs &mut. In the long run, completely @@ -49,9 +48,6 @@ pub struct TextLayerRenderer { // Quads for example? /// Visual Id -> batch table. visuals: IdTable>, - - /// The maximum quads currently in use. This may be more than the index buffer can hold. - max_quads_in_use: usize, } struct Visual { @@ -65,21 +61,6 @@ struct VisualBatches { color: Option, } -impl VisualBatches { - fn max_quads(&self) -> usize { - [ - self.sdf.as_ref().map(|b| b.quad_count).unwrap_or_default(), - self.color - .as_ref() - .map(|b| b.quad_count) - .unwrap_or_default(), - ] - .into_iter() - .max() - .unwrap() - } -} - pub struct QuadBatch { /// Contains texture reference(s) and the sampler configuration. pub fs_bind_group: wgpu::BindGroup, @@ -99,28 +80,31 @@ impl TextLayerRenderer { font_system: Arc>, target_format: wgpu::TextureFormat, ) -> Self { - Self { + let mut renderer = Self { scale_context: ScaleContext::default(), font_system, empty_glyphs: HashSet::new(), index_buffer: QuadIndexBuffer::new(device), // Instead of specifying all these consts _and_ the vertex type, a trait based spec type // would probably be better. - sdf_renderer: AtlasRenderer::new::( + sdf_renderer: AtlasRenderer::new::( device, wgpu::TextureFormat::R8Unorm, wgpu::include_wgsl!("shader/sdf_atlas.wgsl"), target_format, ), - color_renderer: AtlasRenderer::new::( + color_renderer: AtlasRenderer::new::( device, wgpu::TextureFormat::Rgba8Unorm, wgpu::include_wgsl!("shader/color_atlas.wgsl"), target_format, ), visuals: IdTable::default(), - max_quads_in_use: 0, - } + }; + + // Since we use instance drendering, the index buffer needs to hold only one quad. + renderer.index_buffer.ensure_can_index_num_quads(device, 1); + renderer } // Architecture: Optimization: @@ -176,32 +160,11 @@ impl TextLayerRenderer { locations.into_iter() } - pub fn prepare(&mut self, context: &mut PreparationContext) { - // Optimization: Visuals are iterated 4 times per render (see all_locations(), which could - // also compute max_quads). - // - // Compute only one max_quads value (which is optimal when we use one index buffer only). - let max_quads = self - .visuals - .iter_some() - .map(|v| v.batches.max_quads()) - .max() - .unwrap_or_default(); - - self.index_buffer - .ensure_can_index_num_quads(context.device, max_quads); - - self.max_quads_in_use = max_quads; - } + pub fn prepare(&mut self, _context: &mut PreparationContext) {} pub fn render(&self, matrices: &LocationMatrices, context: &mut RenderContext) { - if self.max_quads_in_use == 0 { - return; - } - // Set the shared index buffer for all quad renderers. - self.index_buffer - .set(&mut context.pass, self.max_quads_in_use); + self.index_buffer.set(&mut context.pass, 1); { context.pass.set_pipeline(self.sdf_renderer.pipeline()); @@ -239,10 +202,11 @@ impl TextLayerRenderer { pass.set_bind_group(0, &batch.fs_bind_group, &[]); pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..)); + // Draw instanced quads: 6 indices for the unit quad, one instance per glyph. pass.draw_indexed( - 0..(batch.quad_count * QuadIndexBuffer::INDICES_PER_QUAD) as u32, + 0..(QuadIndexBuffer::INDICES_PER_QUAD as u32), 0, - 0..1, + 0..(batch.quad_count as u32), ) } @@ -254,10 +218,10 @@ impl TextLayerRenderer { context: &mut PreparationContext, runs: impl Iterator, ) -> Result { - // Step 1: Get all instance data. // Performance: Compute a conservative capacity? - let mut sdf_glyphs = Vec::new(); - let mut color_glyphs = Vec::new(); + // Step 1: Build instance data directly. + let mut sdf_instances: Vec = Vec::new(); + let mut color_instances: Vec = Vec::new(); // Architecture: We should really not lock this for a longer period of time. After initial // renderers, it's usually not used anymore, because it just invokes get_font() on @@ -278,29 +242,49 @@ impl TextLayerRenderer { continue; // Glyph is empty: Not rendered. }; - // Performance: translation might be applied to two points only (lt, rb) - let vertices = - Self::glyph_vertices(run, glyph, &placement).map(|p| p + translation); + // Compute left-top/right-bottom in pixel space once. + let (lt, rb) = run.place_glyph(glyph, &placement); + let left = lt.x as f32 + translation.x as f32; + let top = lt.y as f32 + translation.y as f32; + let right = rb.x as f32 + translation.x as f32; + let bottom = rb.y as f32 + translation.y as f32; + let depth = translation.z as f32; + + let pos_lt = [left, top]; + let pos_rb = [right, bottom]; + + // Atlas rect in pixel space; normalization is done in shader. + let uv_lt = [rect.min.x as f32, rect.min.y as f32]; + let uv_rb = [rect.max.x as f32, rect.max.y as f32]; match kind { AtlasKind::Sdf => { - sdf_glyphs.push(sdf_atlas::QuadInstance { - atlas_rect: rect, - vertices, - // OO: Text color is changing per run only. - color: run.text_color, - }) + sdf_instances.push(sdf_atlas::Instance { + pos_lt, + pos_rb, + uv_lt, + uv_rb, + color: run.text_color.into(), + depth, + }); + } + AtlasKind::Color => { + color_instances.push(color_atlas::Instance { + pos_lt, + pos_rb, + uv_lt, + uv_rb, + depth, + }); } - AtlasKind::Color => color_glyphs.push(color_atlas::QuadInstance { - atlas_rect: rect, - vertices, - }), } } } - let sdf_batch = self.sdf_renderer.batch(context, &sdf_glyphs); - let color_batch = self.color_renderer.batch(context, &color_glyphs); + let sdf_batch = self.sdf_renderer.batch_instances(context, &sdf_instances); + let color_batch = self + .color_renderer + .batch_instances(context, &color_instances); Ok(VisualBatches { sdf: sdf_batch, @@ -375,27 +359,5 @@ impl TextLayerRenderer { } } - fn glyph_vertices( - run: &GlyphRun, - glyph: &RunGlyph, - placement: &text::Placement, - ) -> [Point3; 4] { - let (lt, rb) = run.place_glyph(glyph, placement); - - // Convert the pixel rect to 3D Points. - let left = lt.x as f64; - let top = lt.y as f64; - let right = rb.x as f64; - let bottom = rb.y as f64; - - // OO: might use Point3 here. - let points: [Point; 4] = [ - (left, top).into(), - (left, bottom).into(), - (right, bottom).into(), - (right, top).into(), - ]; - - points.map(|f| f.with_z(0.0)) - } + // glyph_vertices removed in instanced rendering path } diff --git a/renderer/src/text_layer/shader/color_atlas.wgsl b/renderer/src/text_layer/shader/color_atlas.wgsl index cbb6bb4b..d3d87af5 100644 --- a/renderer/src/text_layer/shader/color_atlas.wgsl +++ b/renderer/src/text_layer/shader/color_atlas.wgsl @@ -1,10 +1,17 @@ -// Vertex shader +// Vertex shader (instanced quads) var view_model: mat4x4; struct VertexInput { - @location(0) position: vec3, - @location(1) tex_coords: vec2, + @builtin(vertex_index) vertex_index: u32, + // Position rectangle in pixels: left-top and right-bottom + @location(0) pos_lt: vec2, + @location(1) pos_rb: vec2, + // Texture rect in atlas pixel space: left-top and right-bottom + @location(2) uv_lt: vec2, + @location(3) uv_rb: vec2, + // Depth in pixel space + @location(4) depth: f32, } struct VertexOutput { @@ -14,12 +21,21 @@ struct VertexOutput { } @vertex -fn vs_main( - vertex_input: VertexInput, -) -> VertexOutput { +fn vs_main(input: VertexInput) -> VertexOutput { + let i = input.vertex_index & 3u; + let use_right = (i == 2u) || (i == 3u); + let use_bottom = (i == 1u) || (i == 2u); + + let x = select(input.pos_lt.x, input.pos_rb.x, use_right); + let y = select(input.pos_lt.y, input.pos_rb.y, use_bottom); + let pos = vec3(x, y, input.depth); + + let tu = select(input.uv_lt.x, input.uv_rb.x, use_right); + let tv = select(input.uv_lt.y, input.uv_rb.y, use_bottom); + var out: VertexOutput; - out.tex_coords = vertex_input.tex_coords; - out.clip_position = view_model * vec4(vertex_input.position, 1.0); + out.tex_coords = vec2(tu, tv); + out.clip_position = view_model * vec4(pos, 1.0); return out; } diff --git a/renderer/src/text_layer/shader/sdf_atlas.wgsl b/renderer/src/text_layer/shader/sdf_atlas.wgsl index d7067fe9..c36892a2 100644 --- a/renderer/src/text_layer/shader/sdf_atlas.wgsl +++ b/renderer/src/text_layer/shader/sdf_atlas.wgsl @@ -1,11 +1,20 @@ -// Vertex shader +// Vertex shader (instanced quads) var view_model: mat4x4; struct VertexInput { - @location(0) position: vec3, - @location(1) tex_coords: vec2, - @location(2) color: vec4, + @builtin(vertex_index) vertex_index: u32, + // Instance attributes + // Position rectangle in pixels: left-top and right-bottom + @location(0) pos_lt: vec2, + @location(1) pos_rb: vec2, + // Texture rect in atlas pixel space: left-top and right-bottom + @location(2) uv_lt: vec2, + @location(3) uv_rb: vec2, + // Per-glyph color + @location(4) color: vec4, + // Depth in pixel space + @location(5) depth: f32, } struct VertexOutput { @@ -16,13 +25,23 @@ struct VertexOutput { } @vertex -fn vs_main( - vertex_input: VertexInput, -) -> VertexOutput { +fn vs_main(input: VertexInput) -> VertexOutput { + // Compute quad corner (0..3) from vertex_index & 3 for indexed draw [0,1,2,0,2,3] + let i = input.vertex_index & 3u; + let use_right = (i == 2u) || (i == 3u); + let use_bottom = (i == 1u) || (i == 2u); + + let x = select(input.pos_lt.x, input.pos_rb.x, use_right); + let y = select(input.pos_lt.y, input.pos_rb.y, use_bottom); + let pos = vec3(x, y, input.depth); + + let tu = select(input.uv_lt.x, input.uv_rb.x, use_right); + let tv = select(input.uv_lt.y, input.uv_rb.y, use_bottom); + var out: VertexOutput; - out.tex_coords = vertex_input.tex_coords; - out.clip_position = view_model * vec4(vertex_input.position, 1.0); - out.color = vertex_input.color; + out.tex_coords = vec2(tu, tv); + out.clip_position = view_model * vec4(pos, 1.0); + out.color = input.color; return out; }