diff --git a/sugarloaf/src/renderer/batch.rs b/sugarloaf/src/renderer/batch.rs index 11fd7997..d484e72d 100644 --- a/sugarloaf/src/renderer/batch.rs +++ b/sugarloaf/src/renderer/batch.rs @@ -24,20 +24,33 @@ pub struct RunUnderline { pub shape: UnderlineShape, } -/// Batch geometry vertex. -/// Supports rounded rectangles with per-corner radii and per-edge border widths. -/// Also supports GPU-rendered underlines with various styles. +/// Per-quad instance for instanced rendering (96 bytes). +/// One per quad — vertex shader generates 4 corners from vertex_id. +#[repr(C)] +#[derive(Debug, Clone, Copy, Zeroable, Pod)] +pub struct QuadInstance { + pub pos: [f32; 3], // top-left x, y + depth (12) + pub color: [f32; 4], // fill / underline color (16) + pub uv_rect: [f32; 4], // [left, top, right, bottom] (16) + pub layers: [i32; 2], // [color_layer, mask_layer] (8) + pub size: [f32; 2], // width, height in pixels (8) + pub corner_radii: [f32; 4], // [tl, tr, br, bl] (16) + pub underline_style: i32, // 0=none, 1=regular, 2=dashed, 3=dotted, 4=curly (4) + pub clip_rect: [f32; 4], // [x, y, w, h] physical pixels (16) +} + +/// Vertex for non-quad geometry (lines, triangles, arcs). #[repr(C)] #[derive(Debug, Clone, Copy, Zeroable, Pod)] pub struct Vertex { pub pos: [f32; 3], - pub color: [f32; 4], // Background color / underline color + pub color: [f32; 4], pub uv: [f32; 2], pub layers: [i32; 2], - pub corner_radii: [f32; 4], // [top_left, top_right, bottom_right, bottom_left] / for underlines: [thickness, 0, 0, 0] - pub rect_size: [f32; 2], // For underlines: [width, height] - pub underline_style: i32, // 0 = none, 1 = regular, 2 = dashed, 3 = dotted, 4 = curly - pub clip_rect: [f32; 4], // [x, y, width, height] in physical pixels (0,0,0,0 = no clip) + pub corner_radii: [f32; 4], + pub rect_size: [f32; 2], + pub underline_style: i32, + pub clip_rect: [f32; 4], } impl Vertex { @@ -90,6 +103,7 @@ impl From<[f32; 4]> for Rect { struct Batch { image: Option, mask: Option, + instances: Vec, vertices: Vec, subpix: bool, order: u8, @@ -99,11 +113,17 @@ impl Batch { fn clear(&mut self) { self.image = None; self.mask = None; + self.instances.clear(); self.vertices.clear(); self.subpix = false; self.order = 0; } + #[inline] + fn is_empty(&self) -> bool { + self.instances.is_empty() && self.vertices.is_empty() + } + #[allow(clippy::too_many_arguments)] #[inline] fn add_line( @@ -119,13 +139,13 @@ impl Batch { mask: Option, subpix: bool, ) -> bool { - if !self.vertices.is_empty() && subpix != self.subpix { + if !self.is_empty() && subpix != self.subpix { return false; } - if !self.vertices.is_empty() && self.image != image { + if !self.is_empty() && self.image != image { return false; } - if !self.vertices.is_empty() && self.mask != mask { + if !self.is_empty() && self.mask != mask { return false; } self.subpix = subpix; @@ -235,13 +255,13 @@ impl Batch { mask: Option, subpix: bool, ) -> bool { - if !self.vertices.is_empty() && subpix != self.subpix { + if !self.is_empty() && subpix != self.subpix { return false; } - if !self.vertices.is_empty() && self.image != image { + if !self.is_empty() && self.image != image { return false; } - if !self.vertices.is_empty() && self.mask != mask { + if !self.is_empty() && self.mask != mask { return false; } @@ -301,13 +321,13 @@ impl Batch { mask: Option, subpix: bool, ) -> bool { - if !self.vertices.is_empty() && subpix != self.subpix { + if !self.is_empty() && subpix != self.subpix { return false; } - if !self.vertices.is_empty() && self.image != image { + if !self.is_empty() && self.image != image { return false; } - if !self.vertices.is_empty() && self.mask != mask { + if !self.is_empty() && self.mask != mask { return false; } self.subpix = subpix; @@ -423,13 +443,13 @@ impl Batch { subpix: bool, clip_rect: [f32; 4], ) -> bool { - if !self.vertices.is_empty() && subpix != self.subpix { + if !self.is_empty() && subpix != self.subpix { return false; } - if !self.vertices.is_empty() && self.image != image { + if !self.is_empty() && self.image != image { return false; } - if !self.vertices.is_empty() && self.mask != mask { + if !self.is_empty() && self.mask != mask { return false; } self.subpix = subpix; @@ -454,13 +474,13 @@ impl Batch { corner_radii: [f32; 4], clip_rect: [f32; 4], ) -> bool { - if !self.vertices.is_empty() && subpix != self.subpix { + if !self.is_empty() && subpix != self.subpix { return false; } - if !self.vertices.is_empty() && self.image != image { + if !self.is_empty() && self.image != image { return false; } - if !self.vertices.is_empty() && self.mask != mask { + if !self.is_empty() && self.mask != mask { return false; } self.subpix = subpix; @@ -493,13 +513,13 @@ impl Batch { thickness: f32, clip_rect: [f32; 4], ) -> bool { - if !self.vertices.is_empty() && self.subpix { + if !self.is_empty() && self.subpix { return false; } - if !self.vertices.is_empty() && self.image.is_some() { + if !self.is_empty() && self.image.is_some() { return false; } - if !self.vertices.is_empty() && self.mask.is_some() { + if !self.is_empty() && self.mask.is_some() { return false; } self.subpix = false; @@ -533,79 +553,74 @@ impl Batch { underline_style: i32, clip_rect: [f32; 4], ) { - let x = rect.x; - let y = rect.y; - let w = rect.width; - let h = rect.height; - const DEFAULT_COORDS: [f32; 4] = [0., 0., 1., 1.]; let coords = coords.unwrap_or(&DEFAULT_COORDS); - let l = coords[0]; - let t = coords[1]; - let r = coords[2]; - let b = coords[3]; - // Create the four corner vertices - let v0 = Vertex { - pos: [x, y, depth], - color: *color, - uv: [l, t], - layers, - corner_radii, - rect_size: [w, h], - underline_style, - clip_rect, - }; - let v1 = Vertex { - pos: [x, y + h, depth], - color: *color, - uv: [l, b], - layers, - corner_radii, - rect_size: [w, h], - underline_style, - clip_rect, - }; - let v2 = Vertex { - pos: [x + w, y + h, depth], - color: *color, - uv: [r, b], - layers, - corner_radii, - rect_size: [w, h], - underline_style, - clip_rect, - }; - let v3 = Vertex { - pos: [x + w, y, depth], + self.instances.push(QuadInstance { + pos: [rect.x, rect.y, depth], color: *color, - uv: [r, t], + uv_rect: *coords, layers, + size: [rect.width, rect.height], corner_radii, - rect_size: [w, h], underline_style, clip_rect, - }; - - // Add vertices directly in the order they'll be drawn - // First triangle: v0, v1, v2 - self.vertices.push(v0); - self.vertices.push(v1); - self.vertices.push(v2); - - // Second triangle: v2, v3, v0 - self.vertices.push(v2); - self.vertices.push(v3); - self.vertices.push(v0); + }); } #[inline] - fn build_display_list(&self, list: &mut Vec) { - // Since vertices are already in draw order, we can just copy them - list.extend_from_slice(&self.vertices); + fn build_display_list( + &self, + inst_list: &mut Vec, + vert_list: &mut Vec, + cmds: &mut Vec, + ) { + let color_layer = self.image.unwrap_or(0); + let mask_layer = self.mask.unwrap_or(0); + + if !self.instances.is_empty() { + let offset = inst_list.len() as u32; + inst_list.extend_from_slice(&self.instances); + cmds.push(DrawCmd::Instanced { + offset, + count: self.instances.len() as u32, + color_layer, + mask_layer, + }); + } + + if !self.is_empty() { + let offset = vert_list.len() as u32; + vert_list.extend_from_slice(&self.vertices); + cmds.push(DrawCmd::Vertices { + offset, + count: self.vertices.len() as u32, + color_layer, + mask_layer, + }); + } } } +/// Draw command emitted by the batch system. +#[derive(Debug, Clone, Copy)] +pub enum DrawCmd { + /// Instanced quad draw (one instance per quad, 4 verts from vertex_id). + Instanced { + offset: u32, + count: u32, + color_layer: i32, + mask_layer: i32, + }, + /// Triangle-list draw for non-quad geometry (lines, triangles, arcs). + Vertices { + offset: u32, + count: u32, + color_layer: i32, + mask_layer: i32, + }, +} + pub struct BatchManager { /// Pool of reusable batches pool: Vec, @@ -1033,17 +1048,22 @@ impl BatchManager { } #[inline] - pub fn build_display_list(&mut self, list: &mut Vec) { + pub fn build_display_list( + &mut self, + instances: &mut Vec, + vertices: &mut Vec, + cmds: &mut Vec, + ) { // Sort batches by draw order (painter's algorithm) // Secondary sort: unmasked batches (backgrounds) before masked batches (text) // This ensures backgrounds render before text at the same draw order level self.active.sort_by_key(|b| (b.order, b.mask.is_some())); for batch in &self.active { - if batch.vertices.is_empty() { + if batch.instances.is_empty() && batch.vertices.is_empty() { continue; } - batch.build_display_list(list); + batch.build_display_list(instances, vertices, cmds); } } diff --git a/sugarloaf/src/renderer/compositor.rs b/sugarloaf/src/renderer/compositor.rs index 209d554a..83f5c667 100644 --- a/sugarloaf/src/renderer/compositor.rs +++ b/sugarloaf/src/renderer/compositor.rs @@ -6,7 +6,7 @@ // Compositor with vertex capture for text run caching use crate::layout::{SpanStyleDecoration, UnderlineShape}; -use crate::renderer::batch::{BatchManager, RunUnderline}; +use crate::renderer::batch::{BatchManager, DrawCmd, QuadInstance, RunUnderline}; pub use crate::renderer::batch::{Rect, Vertex}; use crate::renderer::image_cache::glyph::GlyphCacheSession; use crate::renderer::text::*; @@ -105,8 +105,13 @@ impl Compositor { } #[inline] - pub fn finish(&mut self, vertices: &mut Vec) { - self.batches.build_display_list(vertices); + pub fn finish( + &mut self, + instances: &mut Vec, + vertices: &mut Vec, + cmds: &mut Vec, + ) { + self.batches.build_display_list(instances, vertices, cmds); self.batches.reset(); } diff --git a/sugarloaf/src/renderer/mod.rs b/sugarloaf/src/renderer/mod.rs index 40a302bf..a6c93d81 100644 --- a/sugarloaf/src/renderer/mod.rs +++ b/sugarloaf/src/renderer/mod.rs @@ -81,10 +81,13 @@ struct Globals { #[derive(Debug)] pub struct MetalRenderer { pipeline_state: RenderPipelineState, + instanced_pipeline_state: RenderPipelineState, vertex_buffer: Buffer, + instance_buffer: Buffer, uniform_buffer: Buffer, sampler: SamplerState, supported_vertex_buffer: usize, + supported_instance_buffer: usize, current_transform: [f32; 16], // Image pipeline (separate from text) image_pipeline_state: RenderPipelineState, @@ -233,13 +236,48 @@ impl MetalRenderer { .new_render_pipeline_state(&pipeline_descriptor) .expect("Failed to create render pipeline state"); - // Create vertex buffer + // Instanced pipeline (vs_instanced + fs_main, no vertex descriptor) + let instanced_vertex_fn = library + .get_function("vs_instanced", None) + .expect("Failed to get instanced vertex function"); + let instanced_pipeline_descriptor = RenderPipelineDescriptor::new(); + instanced_pipeline_descriptor.set_vertex_function(Some(&instanced_vertex_fn)); + instanced_pipeline_descriptor.set_fragment_function(Some(&fragment_function)); + // No vertex descriptor — instance data read from buffer(0) directly + + let inst_color = instanced_pipeline_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + inst_color.set_pixel_format(MTLPixelFormat::BGRA8Unorm); + inst_color.set_blending_enabled(true); + inst_color.set_source_rgb_blend_factor(MTLBlendFactor::SourceAlpha); + inst_color.set_destination_rgb_blend_factor(MTLBlendFactor::OneMinusSourceAlpha); + inst_color.set_rgb_blend_operation(MTLBlendOperation::Add); + inst_color.set_source_alpha_blend_factor(MTLBlendFactor::One); + inst_color.set_destination_alpha_blend_factor(MTLBlendFactor::OneMinusSourceAlpha); + inst_color.set_alpha_blend_operation(MTLBlendOperation::Add); + + let instanced_pipeline_state = context + .device + .new_render_pipeline_state(&instanced_pipeline_descriptor) + .expect("Failed to create instanced pipeline state"); + + // Create vertex buffer (for lines/triangles/arcs — rare) let vertex_buffer = context.device.new_buffer( (mem::size_of::() * supported_vertex_buffer) as u64, MTLResourceOptions::StorageModeShared, ); vertex_buffer.set_label("sugarloaf::rich_text vertex buffer"); + // Create instance buffer (for quads — the hot path) + let supported_instance_buffer = 20_000usize; + let instance_buffer = context.device.new_buffer( + (mem::size_of::() * supported_instance_buffer) as u64, + MTLResourceOptions::StorageModeShared, + ); + instance_buffer.set_label("sugarloaf::rich_text instance buffer"); + // Create uniform buffer let uniform_buffer = context.device.new_buffer( mem::size_of::() as u64, @@ -351,9 +389,12 @@ impl MetalRenderer { Self { pipeline_state, + instanced_pipeline_state, vertex_buffer, + instance_buffer, sampler, supported_vertex_buffer, + supported_instance_buffer, current_transform: [0.0; 16], uniform_buffer, image_pipeline_state, @@ -375,83 +416,95 @@ impl MetalRenderer { pub fn render( &mut self, + instances: &[batch::QuadInstance], vertices: &[Vertex], + draw_cmds: &[batch::DrawCmd], images: &ImageCache, render_encoder: &RenderCommandEncoderRef, context: &MetalContext, ) { - if vertices.is_empty() { + if instances.is_empty() && vertices.is_empty() { return; } - // Expand vertex buffer if needed - if vertices.len() > self.supported_vertex_buffer { - self.supported_vertex_buffer = (vertices.len() as f32 * 1.25) as usize; - - // Recreate vertex buffer with larger size - self.vertex_buffer = context.device.new_buffer( - (mem::size_of::() * self.supported_vertex_buffer) as u64, - MTLResourceOptions::StorageModeShared, - ); - self.vertex_buffer - .set_label("sugarloaf::rich_text vertex buffer (resized)"); + // Upload instance buffer + if !instances.is_empty() { + if instances.len() > self.supported_instance_buffer { + self.supported_instance_buffer = + (instances.len() as f32 * 1.25) as usize; + self.instance_buffer = context.device.new_buffer( + (mem::size_of::() + * self.supported_instance_buffer) as u64, + MTLResourceOptions::StorageModeShared, + ); + } + unsafe { + std::ptr::copy_nonoverlapping( + instances.as_ptr(), + self.instance_buffer.contents() as *mut batch::QuadInstance, + instances.len(), + ); + } } - // Copy vertex data to buffer - let vertex_data = self.vertex_buffer.contents() as *mut Vertex; - unsafe { - std::ptr::copy_nonoverlapping(vertices.as_ptr(), vertex_data, vertices.len()); + // Upload vertex buffer (lines/triangles/arcs — rare) + if !vertices.is_empty() { + if vertices.len() > self.supported_vertex_buffer { + self.supported_vertex_buffer = + (vertices.len() as f32 * 1.25) as usize; + self.vertex_buffer = context.device.new_buffer( + (mem::size_of::() * self.supported_vertex_buffer) as u64, + MTLResourceOptions::StorageModeShared, + ); + } + unsafe { + std::ptr::copy_nonoverlapping( + vertices.as_ptr(), + self.vertex_buffer.contents() as *mut Vertex, + vertices.len(), + ); + } } - // Set up render state - render_encoder.set_render_pipeline_state(&self.pipeline_state); - render_encoder.set_vertex_buffer(0, Some(&self.vertex_buffer), 0); - + // Update transform let transform = orthographic_projection(context.size.width, context.size.height); if self.current_transform != transform { let globals = Globals { transform }; - let uniform_data = self.uniform_buffer.contents() as *mut Globals; unsafe { - *uniform_data = globals; + *(self.uniform_buffer.contents() as *mut Globals) = globals; } self.current_transform = transform; } - render_encoder.set_vertex_buffer(1, Some(&self.uniform_buffer), 0); - - // Set sampler render_encoder.set_fragment_sampler_state(0, Some(&self.sampler)); - // Implement proper batching by atlas to avoid lifetime issues let color_textures = images.get_metal_textures(); let mask_texture = images.get_mask_texture(); - // Group vertices by their texture binding requirements - // layers[0] = color_layer (0 = no color texture, 1+ = color atlas index) - // layers[1] = mask_layer (0 = no mask texture, 1 = mask atlas) - - let mut current_vertex = 0usize; - while current_vertex < vertices.len() { - let start = current_vertex; - let current_color_layer = vertices[start].layers[0]; - let current_mask_layer = vertices[start].layers[1]; - - // Find the end of this batch (consecutive vertices with same layers) - let mut end = start; - while end < vertices.len() - && vertices[end].layers[0] == current_color_layer - && vertices[end].layers[1] == current_mask_layer - { - end += 1; - } + let mut current_pipeline_instanced = false; + // Start with neither pipeline set — force first bind + let mut pipeline_set = false; + + for cmd in draw_cmds { + let (color_layer, mask_layer) = match cmd { + batch::DrawCmd::Instanced { + color_layer, + mask_layer, + .. + } => (*color_layer, *mask_layer), + batch::DrawCmd::Vertices { + color_layer, + mask_layer, + .. + } => (*color_layer, *mask_layer), + }; - // Bind appropriate textures for this batch - if current_color_layer > 0 { - // Use color atlas (current_color_layer is 1-based, so subtract 1 for 0-based index) - let atlas_index = (current_color_layer - 1) as usize; - if atlas_index < color_textures.len() { + // Bind textures + if color_layer > 0 { + let idx = (color_layer - 1) as usize; + if idx < color_textures.len() { render_encoder - .set_fragment_texture(0, Some(color_textures[atlas_index])); + .set_fragment_texture(0, Some(color_textures[idx])); } else { render_encoder.set_fragment_texture(0, None); } @@ -459,7 +512,7 @@ impl MetalRenderer { render_encoder.set_fragment_texture(0, None); } - if current_mask_layer > 0 { + if mask_layer > 0 { if let Some(mask_tex) = mask_texture { render_encoder.set_fragment_texture(1, Some(mask_tex)); } else { @@ -469,14 +522,48 @@ impl MetalRenderer { render_encoder.set_fragment_texture(1, None); } - // Draw this batch - render_encoder.draw_primitives( - MTLPrimitiveType::Triangle, - start as u64, - (end - start) as u64, - ); - - current_vertex = end; + match cmd { + batch::DrawCmd::Instanced { offset, count, .. } => { + if !pipeline_set || !current_pipeline_instanced { + render_encoder + .set_render_pipeline_state(&self.instanced_pipeline_state); + render_encoder + .set_vertex_buffer(1, Some(&self.uniform_buffer), 0); + current_pipeline_instanced = true; + pipeline_set = true; + } + let byte_offset = *offset as u64 + * mem::size_of::() as u64; + render_encoder.set_vertex_buffer( + 0, + Some(&self.instance_buffer), + byte_offset, + ); + render_encoder.draw_primitives_instanced( + MTLPrimitiveType::TriangleStrip, + 0, + 4, // 4 vertices per quad (triangle strip) + *count as u64, + ); + } + batch::DrawCmd::Vertices { offset, count, .. } => { + if !pipeline_set || current_pipeline_instanced { + render_encoder + .set_render_pipeline_state(&self.pipeline_state); + render_encoder + .set_vertex_buffer(0, Some(&self.vertex_buffer), 0); + render_encoder + .set_vertex_buffer(1, Some(&self.uniform_buffer), 0); + current_pipeline_instanced = false; + pipeline_set = true; + } + render_encoder.draw_primitives( + MTLPrimitiveType::Triangle, + *offset as u64, + *count as u64, + ); + } + } } } } @@ -548,7 +635,9 @@ pub struct BackgroundImagePixels { pub struct Renderer { brush_type: RendererType, comp: Compositor, + instances: Vec, vertices: Vec, + draw_cmds: Vec, images: ImageCache, glyphs: GlyphCache, text_run_manager: TextRunManager, @@ -668,7 +757,9 @@ impl Renderer { Self { brush_type, comp: Compositor::new(), + instances: vec![], vertices: vec![], + draw_cmds: vec![], images: ImageCache::new(context), glyphs: GlyphCache::new(), text_run_manager: TextRunManager::new(), @@ -704,8 +795,9 @@ impl Renderer { crate::sugarloaf::graphics::GraphicDataEntry, >, ) { - // Always clear vertices first + self.instances.clear(); self.vertices.clear(); + self.draw_cmds.clear(); let library = state.content.font_library(); // Iterate over all content states and render visible ones @@ -932,9 +1024,27 @@ impl Renderer { upload_background_image_texture(context, &pixels); } + self.instances.clear(); self.vertices.clear(); + self.draw_cmds.clear(); self.images.process_atlases(context); - self.comp.finish(&mut self.vertices); + self.comp.finish( + &mut self.instances, + &mut self.vertices, + &mut self.draw_cmds, + ); + + let inst_bytes = + self.instances.len() * std::mem::size_of::(); + let vert_bytes = self.vertices.len() * std::mem::size_of::(); + println!( + "gpu upload: {} instances ({:.2} MB) + {} verts ({:.2} MB) = {:.2} MB", + self.instances.len(), + inst_bytes as f64 / (1024.0 * 1024.0), + self.vertices.len(), + vert_bytes as f64 / (1024.0 * 1024.0), + (inst_bytes + vert_bytes) as f64 / (1024.0 * 1024.0), + ); } #[inline] @@ -2012,7 +2122,9 @@ impl Renderer { let Self { brush_type, images, + instances, vertices, + draw_cmds, image_draws, image_textures, background_image_texture, @@ -2026,7 +2138,8 @@ impl Renderer { let has_images = !image_draws.is_empty(); let has_background = background_image_texture.is_some(); - if (color_views.is_empty() || vertices.is_empty()) + if (color_views.is_empty() + || (instances.is_empty() && vertices.is_empty())) && !has_images && !has_background { @@ -2248,7 +2361,14 @@ impl Renderer { } // Text pipeline - brush.render(&self.vertices, &self.images, render_encoder, context); + brush.render( + &self.instances, + &self.vertices, + &self.draw_cmds, + &self.images, + render_encoder, + context, + ); // AboveText images (z >= 0): after text if has_images { diff --git a/sugarloaf/src/renderer/renderer.metal b/sugarloaf/src/renderer/renderer.metal index eff0a987..5a8a7780 100644 --- a/sugarloaf/src/renderer/renderer.metal +++ b/sugarloaf/src/renderer/renderer.metal @@ -6,7 +6,28 @@ struct Globals { float4x4 transform; }; -// Vertex input structure - matches Vertex struct exactly +// QuadInstance - matches Rust QuadInstance struct (96 bytes, per-instance) +// All packed types to match C/Rust layout without Metal alignment padding. +struct QuadInstance { + packed_float3 pos; // 12 bytes, offset 0 + packed_float4 color; // 16 bytes, offset 12 + packed_float4 uv_rect; // 16 bytes, offset 28 + packed_int2 layers; // 8 bytes, offset 44 + packed_float2 size; // 8 bytes, offset 52 + packed_float4 corner_radii; // 16 bytes, offset 60 + int underline_style; // 4 bytes, offset 76 + packed_float4 clip_rect; // 16 bytes, offset 80 +}; + +// Unit quad corners for triangle strip (TL, BL, TR, BR) +constant float2 UNIT_QUAD[4] = { + float2(0.0, 0.0), + float2(0.0, 1.0), + float2(1.0, 0.0), + float2(1.0, 1.0), +}; + +// Vertex input structure - matches Vertex struct (for lines/triangles/arcs) struct VertexInput { float3 v_pos [[attribute(0)]]; // Position (12 bytes) float4 v_color [[attribute(1)]]; // Background color / underline color (16 bytes) @@ -51,6 +72,33 @@ vertex VertexOutput vs_main( return out; } +// Instanced vertex shader — one QuadInstance per quad, vertex_id picks corner +vertex VertexOutput vs_instanced( + uint vertex_id [[vertex_id]], + uint instance_id [[instance_id]], + const device QuadInstance* instances [[buffer(0)]], + constant Globals& globals [[buffer(1)]] +) { + const device QuadInstance& inst = instances[instance_id]; + float2 sz = float2(inst.size); + float4 uv_r = float4(inst.uv_rect); + float2 unit = UNIT_QUAD[vertex_id]; + float2 pos = float2(inst.pos[0], inst.pos[1]) + unit * sz; + float2 uv = mix(uv_r.xy, uv_r.zw, unit); + + VertexOutput out; + out.position = globals.transform * float4(pos, inst.pos[2], 1.0); + out.f_color = float4(inst.color); + out.f_uv = uv; + out.color_layer = int2(inst.layers).x; + out.mask_layer = int2(inst.layers).y; + out.corner_radii = float4(inst.corner_radii); + out.rect_size = sz; + out.underline_style = inst.underline_style; + out.clip_rect = float4(inst.clip_rect); + return out; +} + // Pick the corner radius based on which quadrant the point is in float pick_corner_radius(float2 center_to_point, float4 corner_radii) { if (center_to_point.x < 0.0) { diff --git a/sugarloaf/src/renderer/renderer.wgsl b/sugarloaf/src/renderer/renderer.wgsl index b90d9e47..d01dfa30 100644 --- a/sugarloaf/src/renderer/renderer.wgsl +++ b/sugarloaf/src/renderer/renderer.wgsl @@ -7,16 +7,29 @@ struct Globals { @group(1) @binding(0) var color_texture: texture_2d; // RGBA texture for color glyphs @group(1) @binding(1) var mask_texture: texture_2d; // R8 texture for alpha masks +// Per-instance data for instanced quad rendering (matches Rust QuadInstance) +struct QuadInstanceInput { + @location(0) pos: vec3, + @location(1) color: vec4, + @location(2) uv_rect: vec4, + @location(3) layers: vec2, + @location(4) size: vec2, + @location(5) corner_radii: vec4, + @location(6) underline_style: i32, + @location(7) clip_rect: vec4, +} + +// Per-vertex data for non-quad geometry (lines, triangles, arcs) struct VertexInput { @builtin(vertex_index) vertex_index: u32, @location(0) v_pos: vec3, - @location(1) v_color: vec4, // Background color / underline color + @location(1) v_color: vec4, @location(2) v_uv: vec2, @location(3) layers: vec2, - @location(4) corner_radii: vec4, // [top_left, top_right, bottom_right, bottom_left] / for underlines: [thickness, 0, 0, 0] - @location(5) rect_size: vec2, // For underlines: [width, height] - @location(6) underline_style: i32, // 0 = none, 1 = regular, 2 = dashed, 3 = dotted, 4 = curly - @location(7) clip_rect: vec4, // [x, y, width, height] in pixels (0,0,0,0 = no clip) + @location(4) corner_radii: vec4, + @location(5) rect_size: vec2, + @location(6) underline_style: i32, + @location(7) clip_rect: vec4, } struct VertexOutput { @@ -31,6 +44,36 @@ struct VertexOutput { @location(7) @interpolate(flat) clip_rect: vec4, } +// Unit quad corners for triangle strip: TL, BL, TR, BR +const UNIT_QUAD = array, 4>( + vec2(0.0, 0.0), + vec2(0.0, 1.0), + vec2(1.0, 0.0), + vec2(1.0, 1.0), +); + +@vertex +fn vs_instanced( + @builtin(vertex_index) vertex_id: u32, + input: QuadInstanceInput, +) -> VertexOutput { + let unit = UNIT_QUAD[vertex_id]; + let pos = input.pos.xy + unit * input.size; + let uv = mix(input.uv_rect.xy, input.uv_rect.zw, unit); + + var out: VertexOutput; + out.position = globals.transform * vec4(pos, input.pos.z, 1.0); + out.f_color = input.color; + out.f_uv = uv; + out.color_layer = input.layers.x; + out.mask_layer = input.layers.y; + out.corner_radii = input.corner_radii; + out.rect_size = input.size; + out.underline_style = input.underline_style; + out.clip_rect = input.clip_rect; + return out; +} + @vertex fn vs_main(input: VertexInput) -> VertexOutput { var out: VertexOutput;