From 7e2b1612b950459324d7d7f9dd1e2193b5a0cda6 Mon Sep 17 00:00:00 2001 From: Chris Cochrun Date: Thu, 10 Jul 2025 17:07:22 -0500 Subject: [PATCH] outline schtuff --- src/cache.rs | 155 ++++++++++++++++++++++++++++++++++++++--- src/shader.wgsl | 60 ++-------------- src/stroke_shader.wgsl | 15 ++++ src/text_atlas.rs | 36 ++++++++++ src/text_render.rs | 18 ++++- 5 files changed, 216 insertions(+), 68 deletions(-) create mode 100644 src/stroke_shader.wgsl diff --git a/src/cache.rs b/src/cache.rs index 9a1e1dc..5b25141 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -25,6 +25,7 @@ struct Inner { vertex_buffers: [wgpu::VertexBufferLayout<'static>; 1], atlas_layout: BindGroupLayout, uniforms_layout: BindGroupLayout, + stroke_layout: BindGroupLayout, pipeline_layout: PipelineLayout, cache: Mutex< Vec<( @@ -87,16 +88,6 @@ impl Cache { offset: mem::size_of::() as u64 * 6, shader_location: 5, }, - wgpu::VertexAttribute { - format: VertexFormat::Uint32, - offset: mem::size_of::() as u64 * 7, - shader_location: 6, - }, - wgpu::VertexAttribute { - format: VertexFormat::Float32, - offset: mem::size_of::() as u64 * 8, - shader_location: 7, - }, ], }; @@ -146,9 +137,56 @@ impl Cache { label: Some("glyphon uniforms bind group layout"), }); + let stroke_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Stroke Bind Group Layout"), + entries: &[ + // glyph texture + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + // glyph sampler + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // stroke color + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: std::num::NonZeroU64::new(16), + }, + count: None, + }, + // stroke width + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: std::num::NonZeroU64::new(4), + }, + count: None, + }, + ], + }); + let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor { label: None, - bind_group_layouts: &[&atlas_layout, &uniforms_layout], + bind_group_layouts: &[&atlas_layout, &uniforms_layout, &stroke_bind_group_layout], push_constant_ranges: &[], }); @@ -160,6 +198,7 @@ impl Cache { atlas_layout, pipeline_layout, cache: Mutex::new(Vec::new()), + stroke_layout: stroke_bind_group_layout, })) } @@ -189,6 +228,37 @@ impl Cache { }) } + pub(crate) fn create_stroke_bind_group( + &self, + device: &Device, + mask_atlas: &TextureView, + stroke_color_buffer: &Buffer, + stroke_width_buffer: &Buffer, + ) -> BindGroup { + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Stroke Bind Group"), + layout: &self.0.stroke_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&mask_atlas), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&self.0.sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: stroke_color_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: stroke_width_buffer.as_entire_binding(), + }, + ], + }) + } + pub(crate) fn create_uniforms_bind_group(&self, device: &Device, buffer: &Buffer) -> BindGroup { device.create_bind_group(&BindGroupDescriptor { layout: &self.0.uniforms_layout, @@ -257,4 +327,67 @@ impl Cache { }) .clone() } + + pub(crate) fn get_or_create_stroke_pipeline( + &self, + device: &Device, + format: TextureFormat, + multisample: MultisampleState, + depth_stencil: Option, + ) -> RenderPipeline { + let Inner { + cache, + pipeline_layout, + shader, + vertex_buffers, + .. + } = self.0.deref(); + + let mut cache = cache.lock().expect("Write pipeline cache"); + + let stroke_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Stroke Shader"), + source: ShaderSource::Wgsl(Cow::Borrowed(include_str!("stroke_shader.wgsl"))), + }); + + cache + .iter() + .find(|(fmt, ms, ds, _)| fmt == &format && ms == &multisample && ds == &depth_stencil) + .map(|(_, _, _, p)| p.clone()) + .unwrap_or_else(|| { + let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor { + label: Some("glyphon pipeline"), + layout: Some(&pipeline_layout), + vertex: VertexState { + module: shader, + entry_point: Some("vs_main"), + buffers: vertex_buffers, + compilation_options: PipelineCompilationOptions::default(), + }, + fragment: Some(FragmentState { + module: &stroke_shader, + entry_point: Some("fs_main"), + targets: &[Some(ColorTargetState { + format, + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::default(), + })], + compilation_options: PipelineCompilationOptions::default(), + }), + primitive: PrimitiveState { + topology: PrimitiveTopology::TriangleStrip, + ..PrimitiveState::default() + }, + depth_stencil: depth_stencil.clone(), + multisample, + multiview: None, + cache: None, + }); + + cache.push((format, multisample, depth_stencil, pipeline.clone())); + + pipeline + }) + .clone() + } } diff --git a/src/shader.wgsl b/src/shader.wgsl index a88e9f6..1813a66 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -6,8 +6,6 @@ struct VertexInput { @location(3) color: u32, @location(4) content_type_with_srgb: u32, @location(5) depth: f32, - @location(6) stroke_color: u32, - @location(7) stroke_size: f32, } struct VertexOutput { @@ -15,8 +13,6 @@ struct VertexOutput { @location(0) color: vec4, @location(1) uv: vec2, @location(2) @interpolate(flat) content_type: u32, - @location(3) stroke_color: vec4, - @location(4) stroke_size: f32, }; struct Params { @@ -46,13 +42,11 @@ fn srgb_to_linear(c: f32) -> f32 { @vertex fn vs_main(in_vert: VertexInput) -> VertexOutput { - var stroke = in_vert.stroke_size; - var width = in_vert.dim & 0xffffu; - var height = (in_vert.dim & 0xffff0000u) >> 16u; - var pos = in_vert.pos; - var uv = vec2(in_vert.uv & 0xffffu, (in_vert.uv & 0xffff0000u) >> 16u); + var pos = in_vert.pos; + let width = in_vert.dim & 0xffffu; + let height = (in_vert.dim & 0xffff0000u) >> 16u; let color = in_vert.color; - let stroke_color = in_vert.stroke_color; + var uv = vec2(in_vert.uv & 0xffffu, (in_vert.uv & 0xffff0000u) >> 16u); let v = in_vert.vertex_idx; let corner_position = vec2( @@ -86,12 +80,6 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput { f32(color & 0x000000ffu) / 255.0, f32((color & 0xff000000u) >> 24u) / 255.0, ); - vert_output.stroke_color = vec4( - f32((stroke_color & 0x00ff0000u) >> 16u) / 255.0, - f32((stroke_color & 0x0000ff00u) >> 8u) / 255.0, - f32(stroke_color & 0x000000ffu) / 255.0, - f32((stroke_color & 0xff000000u) >> 24u) / 255.0, - ); } case 1u: { vert_output.color = vec4( @@ -100,12 +88,6 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput { srgb_to_linear(f32(color & 0x000000ffu) / 255.0), f32((color & 0xff000000u) >> 24u) / 255.0, ); - vert_output.stroke_color = vec4( - srgb_to_linear(f32((stroke_color & 0x00ff0000u) >> 16u) / 255.0), - srgb_to_linear(f32((stroke_color & 0x0000ff00u) >> 8u) / 255.0), - srgb_to_linear(f32(stroke_color & 0x000000ffu) / 255.0), - f32((stroke_color & 0xff000000u) >> 24u) / 255.0, - ); } default: {} } @@ -126,54 +108,20 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput { vert_output.content_type = content_type; vert_output.uv = vec2(uv) / vec2(dim); - vert_output.stroke_size = in_vert.stroke_size; return vert_output; } @fragment fn fs_main(in_frag: VertexOutput) -> @location(0) vec4 { - let dims = vec2(textureDimensions(mask_atlas_texture)); - var bit: u32 = 1; - - let current = textureSample(mask_atlas_texture, atlas_sampler, dims); - var closest_dist = distance(vec2(in_frag.position.xy), vec2(current.xy)); - var result = current; - - for (var step: u32 = 0; step <= 32; step++) { - bit = 1u << step; - for (var dy = -1; dy <= 1; dy++) { - for (var dx = -1; dx <= 1; dx++) { - if (dx == 0 && dy == 0) { - continue; - } - - let neighbour_coord = in_frag.position.xy + vec2(f32(dx * bit), f32(dy * bit)); - let neighbour = textureSample(mask_atlas_texture, atlas_sampler, neighbour_coord / dims); - let dist = distance(vec2(in_frag.position.xy), vec2(neighbour.xy)); - - if (dist < closest_dist) { - closest_dist = dist; - result = vec4(in_frag.stroke_color.rgb, in_frag.stroke_color.a * textureSampleLevel(mask_atlas_texture, atlas_sampler, neighbour.xy, 0.0).x); - } - } - } - } - - switch in_frag.content_type { case 0u: { return textureSampleLevel(color_atlas_texture, atlas_sampler, in_frag.uv, 0.0); } case 1u: { - if in_frag.stroke_size > 0.0 { - return result; - } else { return vec4(in_frag.color.rgb, in_frag.color.a * textureSampleLevel(mask_atlas_texture, atlas_sampler, in_frag.uv, 0.0).x); - } } default: { - // return result; return vec4(0.0); } } diff --git a/src/stroke_shader.wgsl b/src/stroke_shader.wgsl new file mode 100644 index 0000000..ea6eb29 --- /dev/null +++ b/src/stroke_shader.wgsl @@ -0,0 +1,15 @@ +@group(0) @binding(0) var glyph_tex: texture_2d; +@group(0) @binding(1) var glyph_sampler: sampler; +@group(0) @binding(2) var stroke_color: vec4; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) tex_coords: vec2, +}; + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let distance = textureSample(glyph_tex, glyph_sampler, in.tex_coords).r; + let alpha = smoothstep(0.5 - 0.02, 0.5 + 0.02, distance); // Optional anti-alias + return vec4(stroke_color.rgb, alpha * stroke_color.a); +} diff --git a/src/text_atlas.rs b/src/text_atlas.rs index 8313672..cbf96ab 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -244,6 +244,7 @@ pub enum ColorMode { pub struct TextAtlas { cache: Cache, pub(crate) bind_group: BindGroup, + pub(crate) stroke_bind_group: BindGroup, pub(crate) color_atlas: InnerAtlas, pub(crate) mask_atlas: InnerAtlas, pub(crate) format: TextureFormat, @@ -282,9 +283,34 @@ impl TextAtlas { &mask_atlas.texture_view, ); + // stroke color buffer + let stroke_color_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Stroke Color Buffer"), + + size: 16, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + // stroke width buffer + let stroke_width_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Stroke Width Buffer"), + size: 4, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let stroke_bind_group = cache.create_stroke_bind_group( + device, + &mask_atlas.texture_view, + &stroke_color_buffer, + &stroke_width_buffer, + ); + Self { cache: cache.clone(), bind_group, + stroke_bind_group, color_atlas, mask_atlas, format, @@ -334,6 +360,16 @@ impl TextAtlas { .get_or_create_pipeline(device, self.format, multisample, depth_stencil) } + pub(crate) fn get_or_create_stroke_pipeline( + &self, + device: &Device, + multisample: MultisampleState, + depth_stencil: Option, + ) -> RenderPipeline { + self.cache + .get_or_create_stroke_pipeline(device, self.format, multisample, depth_stencil) + } + fn rebind(&mut self, device: &wgpu::Device) { self.bind_group = self.cache.create_atlas_bind_group( device, diff --git a/src/text_render.rs b/src/text_render.rs index 39cadc6..020b710 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -19,6 +19,8 @@ pub struct TextRenderer { pipeline: RenderPipeline, glyph_vertices: Vec, glyphs_to_render: u32, + stroke_pipeline: RenderPipeline, + stroke: bool, } impl TextRenderer { @@ -37,7 +39,9 @@ impl TextRenderer { mapped_at_creation: false, }); - let pipeline = atlas.get_or_create_pipeline(device, multisample, depth_stencil); + let pipeline = atlas.get_or_create_pipeline(device, multisample, depth_stencil.clone()); + let stroke_pipeline = + atlas.get_or_create_stroke_pipeline(device, multisample, depth_stencil); Self { staging_belt: StagingBelt::new(vertex_buffer_size), @@ -46,6 +50,8 @@ impl TextRenderer { pipeline, glyph_vertices: Vec::new(), glyphs_to_render: 0, + stroke_pipeline, + stroke: false, } } @@ -64,6 +70,7 @@ impl TextRenderer { ) -> Result<(), PrepareError> { self.staging_belt.recall(); self.glyph_vertices.clear(); + atlas. let resolution = viewport.resolution(); @@ -89,6 +96,7 @@ impl TextRenderer { for run in layout_runs { if text_area.stroke_size > 0.0 { + self.stroke = true; for i in 0..2 { self.prepare_glyphs( &run, @@ -208,6 +216,14 @@ impl TextRenderer { return Ok(()); } + if self.stroke { + pass.set_pipeline(&self.stroke_pipeline); + pass.set_bind_group(0, &atlas.bind_group, &[]); + pass.set_bind_group(2, &atlas.stroke_bind_group, &[]); + pass.set_bind_group(1, &viewport.bind_group, &[]); + pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + pass.draw(0..4, 0..self.glyphs_to_render); + } pass.set_pipeline(&self.pipeline); pass.set_bind_group(0, &atlas.bind_group, &[]); pass.set_bind_group(1, &viewport.bind_group, &[]);