adding together a jump-flood algorithm for rendering outlines
Some checks are pending
Format / all (push) Waiting to run
Lint / all (push) Waiting to run
Test / all (macOS-latest, beta) (push) Waiting to run
Test / all (macOS-latest, stable) (push) Waiting to run
Test / all (ubuntu-latest, beta) (push) Waiting to run
Test / all (ubuntu-latest, stable) (push) Waiting to run
Test / all (windows-latest, beta) (push) Waiting to run
Test / all (windows-latest, stable) (push) Waiting to run

This commit is contained in:
Chris Cochrun 2025-07-10 10:03:03 -05:00
parent 84de74e7c2
commit da3b02cf8a
6 changed files with 332 additions and 209 deletions

View file

@ -1,6 +1,7 @@
use cryoglyph::{
Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
TextArea, TextAtlas, TextBounds, TextRenderer, Viewport,
cosmic_text::LetterSpacing, Attrs, AttrsOwned, Buffer, Cache, Color, Family, FontSystem,
Metrics, Resolution, Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer,
Viewport,
};
use std::sync::Arc;
use wgpu::{
@ -169,6 +170,21 @@ impl winit::application::ApplicationHandler for Application {
let mut encoder =
device.create_command_encoder(&CommandEncoderDescriptor { label: None });
let text_area = TextArea {
buffer: text_buffer,
left: 10.0,
top: 10.0,
scale: 3.5,
bounds: TextBounds {
left: 0,
top: 0,
right: 600,
bottom: 160,
},
default_color: Color::rgb(255, 255, 255),
stroke_size: 5.5,
stroke_color: Color::rgb(50, 50, 255),
};
text_renderer
.prepare(
@ -178,19 +194,7 @@ impl winit::application::ApplicationHandler for Application {
font_system,
atlas,
viewport,
[TextArea {
buffer: text_buffer,
left: 10.0,
top: 10.0,
scale: 1.0,
bounds: TextBounds {
left: 0,
top: 0,
right: 600,
bottom: 160,
},
default_color: Color::rgb(0, 0, 0),
}],
[text_area],
swash_cache,
)
.unwrap();
@ -205,7 +209,7 @@ impl winit::application::ApplicationHandler for Application {
view: &view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(wgpu::Color::WHITE),
load: LoadOp::Clear(wgpu::Color::BLACK),
store: wgpu::StoreOp::Store,
},
})],

View file

@ -87,6 +87,16 @@ impl Cache {
offset: mem::size_of::<u32>() as u64 * 6,
shader_location: 5,
},
wgpu::VertexAttribute {
format: VertexFormat::Uint32,
offset: mem::size_of::<u32>() as u64 * 7,
shader_location: 6,
},
wgpu::VertexAttribute {
format: VertexFormat::Float32,
offset: mem::size_of::<u32>() as u64 * 8,
shader_location: 7,
},
],
};

View file

@ -21,11 +21,11 @@ use text_render::ContentType;
// Re-export all top-level types from `cosmic-text` for convenience.
#[doc(no_inline)]
pub use cosmic_text::{
self, Action, Affinity, Attrs, AttrsList, AttrsOwned, Buffer, BufferLine, CacheKey, Color,
Command, Cursor, Edit, Editor, Family, FamilyOwned, Font, FontSystem, LayoutCursor,
self, fontdb, Action, Affinity, Attrs, AttrsList, AttrsOwned, Buffer, BufferLine, CacheKey,
Color, Command, Cursor, Edit, Editor, Family, FamilyOwned, Font, FontSystem, LayoutCursor,
LayoutGlyph, LayoutLine, LayoutRun, LayoutRunIter, Metrics, ShapeGlyph, ShapeLine, ShapeSpan,
ShapeWord, Shaping, Stretch, Style, SubpixelBin, SwashCache, SwashContent, SwashImage, Weight,
Wrap, fontdb,
Wrap,
};
use etagere::AllocId;
@ -57,6 +57,8 @@ pub(crate) struct GlyphToRender {
color: u32,
content_type_with_srgb: [u16; 2],
depth: f32,
stroke_color: u32,
stroke_size: f32,
}
/// The screen resolution to use when rendering text.
@ -117,4 +119,8 @@ pub struct TextArea<'a> {
pub bounds: TextBounds,
// The default color of the text area.
pub default_color: Color,
/// The stroke (outline) size
pub stroke_size: f32,
/// The stroke (outline) color
pub stroke_color: Color,
}

View file

@ -6,6 +6,8 @@ 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 {
@ -13,6 +15,8 @@ struct VertexOutput {
@location(0) color: vec4<f32>,
@location(1) uv: vec2<f32>,
@location(2) @interpolate(flat) content_type: u32,
@location(3) stroke_color: vec4<f32>,
@location(4) stroke_size: f32,
};
struct Params {
@ -42,11 +46,13 @@ 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;
let width = in_vert.dim & 0xffffu;
let height = (in_vert.dim & 0xffff0000u) >> 16u;
let color = in_vert.color;
var uv = vec2<u32>(in_vert.uv & 0xffffu, (in_vert.uv & 0xffff0000u) >> 16u);
let color = in_vert.color;
let stroke_color = in_vert.stroke_color;
let v = in_vert.vertex_idx;
let corner_position = vec2<u32>(
@ -80,6 +86,12 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput {
f32(color & 0x000000ffu) / 255.0,
f32((color & 0xff000000u) >> 24u) / 255.0,
);
vert_output.stroke_color = vec4<f32>(
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<f32>(
@ -88,6 +100,12 @@ 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<f32>(
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: {}
}
@ -108,20 +126,54 @@ fn vs_main(in_vert: VertexInput) -> VertexOutput {
vert_output.content_type = content_type;
vert_output.uv = vec2<f32>(uv) / vec2<f32>(dim);
vert_output.stroke_size = in_vert.stroke_size;
return vert_output;
}
@fragment
fn fs_main(in_frag: VertexOutput) -> @location(0) vec4<f32> {
let dims = vec2<f32>(textureDimensions(mask_atlas_texture));
var bit: u32 = 1;
let current = textureSample(mask_atlas_texture, atlas_sampler, dims);
var closest_dist = distance(vec2<f32>(in_frag.position.xy), vec2<f32>(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>(f32(dx * bit), f32(dy * bit));
let neighbour = textureSample(mask_atlas_texture, atlas_sampler, neighbour_coord / dims);
let dist = distance(vec2<f32>(in_frag.position.xy), vec2<f32>(neighbour.xy));
if (dist < closest_dist) {
closest_dist = dist;
result = vec4<f32>(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<f32>(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<f32>(0.0);
}
}

View file

@ -1,7 +1,7 @@
use crate::{
Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache, text_render::ContentType,
text_render::ContentType, Cache, CacheKey, FontSystem, GlyphDetails, GpuCacheStatus, SwashCache,
};
use etagere::{Allocation, BucketedAtlasAllocator, size2};
use etagere::{size2, Allocation, BucketedAtlasAllocator};
use lru::LruCache;
use rustc_hash::FxHasher;
use std::{collections::HashSet, hash::BuildHasherDefault};

View file

@ -2,12 +2,13 @@ use crate::{
ColorMode, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, PrepareError, RenderError,
SwashCache, SwashContent, TextArea, TextAtlas, Viewport,
};
use cosmic_text::LayoutRun;
use std::{num::NonZeroU64, slice};
use wgpu::util::StagingBelt;
use wgpu::{
Buffer, BufferDescriptor, BufferUsages, COPY_BUFFER_ALIGNMENT, CommandEncoder,
DepthStencilState, Device, Extent3d, MultisampleState, Origin3d, Queue, RenderPass,
RenderPipeline, TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect,
Buffer, BufferDescriptor, BufferUsages, CommandEncoder, DepthStencilState, Device, Extent3d,
MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, TexelCopyBufferLayout,
TexelCopyTextureInfo, TextureAspect, COPY_BUFFER_ALIGNMENT,
};
/// A text renderer that uses cached glyphs to render text into an existing render pass.
@ -87,23 +88,164 @@ impl TextRenderer {
.take_while(is_run_visible);
for run in layout_runs {
if text_area.stroke_size > 0.0 {
for i in 0..2 {
self.prepare_glyphs(
&run,
&text_area,
atlas,
cache,
font_system,
device,
queue,
bounds_max_x,
bounds_max_y,
bounds_min_y,
bounds_min_x,
i == 0,
&mut metadata_to_depth,
)?;
}
} else {
self.prepare_glyphs(
&run,
&text_area,
atlas,
cache,
font_system,
device,
queue,
bounds_max_x,
bounds_max_y,
bounds_min_y,
bounds_min_x,
false,
&mut metadata_to_depth,
)?;
}
}
}
self.glyphs_to_render = self.glyph_vertices.len() as u32;
let will_render = !self.glyph_vertices.is_empty();
if !will_render {
return Ok(());
}
let vertices = self.glyph_vertices.as_slice();
let vertices_raw = unsafe {
slice::from_raw_parts(
vertices as *const _ as *const u8,
std::mem::size_of_val(vertices),
)
};
if self.vertex_buffer_size >= vertices_raw.len() as u64 {
self.staging_belt
.write_buffer(
encoder,
&self.vertex_buffer,
0,
NonZeroU64::new(vertices_raw.len() as u64).expect("Non-empty vertices"),
device,
)
.copy_from_slice(vertices_raw);
} else {
self.vertex_buffer.destroy();
let (buffer, buffer_size) = create_oversized_buffer(
device,
Some("glyphon vertices"),
vertices_raw,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
);
self.vertex_buffer = buffer;
self.vertex_buffer_size = buffer_size;
self.staging_belt.finish();
self.staging_belt = StagingBelt::new(buffer_size);
}
self.staging_belt.finish();
Ok(())
}
pub fn prepare<'a>(
&mut self,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
font_system: &mut FontSystem,
atlas: &mut TextAtlas,
viewport: &Viewport,
text_areas: impl IntoIterator<Item = TextArea<'a>>,
cache: &mut SwashCache,
) -> Result<(), PrepareError> {
self.prepare_with_depth(
device,
queue,
encoder,
font_system,
atlas,
viewport,
text_areas,
cache,
zero_depth,
)
}
/// Renders all layouts that were previously provided to `prepare`.
pub fn render(
&self,
atlas: &TextAtlas,
viewport: &Viewport,
pass: &mut RenderPass<'_>,
) -> Result<(), RenderError> {
if self.glyphs_to_render == 0 {
return Ok(());
}
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &atlas.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);
Ok(())
}
fn prepare_glyphs(
&mut self,
run: &LayoutRun,
text_area: &TextArea,
atlas: &mut TextAtlas,
cache: &mut SwashCache,
font_system: &mut FontSystem,
device: &Device,
queue: &Queue,
bounds_max_x: i32,
bounds_max_y: i32,
bounds_min_y: i32,
bounds_min_x: i32,
stroke: bool,
mut metadata_to_depth: impl FnMut(usize) -> f32,
) -> Result<(), PrepareError> {
for glyph in run.glyphs.iter() {
let physical_glyph =
glyph.physical((text_area.left, text_area.top), text_area.scale);
let physical_glyph = glyph.physical((text_area.left, text_area.top), text_area.scale);
let cache_key = physical_glyph.cache_key;
let details = if let Some(details) =
atlas.mask_atlas.glyph_cache.get(&cache_key)
{
let details = if let Some(details) = atlas.mask_atlas.glyph_cache.get(&cache_key) {
atlas.mask_atlas.glyphs_in_use.insert(cache_key);
details
} else if let Some(details) = atlas.color_atlas.glyph_cache.get(&cache_key) {
atlas.color_atlas.glyphs_in_use.insert(cache_key);
details
} else {
let Some(image) =
cache.get_image_uncached(font_system, physical_glyph.cache_key)
let Some(image) = cache.get_image_uncached(font_system, physical_glyph.cache_key)
else {
continue;
};
@ -130,13 +272,7 @@ impl TextRenderer {
match inner.try_allocate(width, height) {
Some(a) => break a,
None => {
if !atlas.grow(
device,
queue,
font_system,
cache,
content_type,
) {
if !atlas.grow(device, queue, font_system, cache, content_type) {
return Err(PrepareError::AtlasFull);
}
@ -248,9 +384,13 @@ impl TextRenderer {
height = bounds_max_y - y;
}
let color = match glyph.color_opt {
let color = if stroke {
text_area.stroke_color
} else {
match glyph.color_opt {
Some(some) => some,
None => text_area.default_color,
}
};
let depth = metadata_to_depth(glyph.metadata);
@ -268,99 +408,10 @@ impl TextRenderer {
} as u16,
],
depth,
stroke_color: text_area.stroke_color.0,
stroke_size: if stroke { text_area.stroke_size } else { 0.0 },
});
}
}
}
self.glyphs_to_render = self.glyph_vertices.len() as u32;
let will_render = !self.glyph_vertices.is_empty();
if !will_render {
return Ok(());
}
let vertices = self.glyph_vertices.as_slice();
let vertices_raw = unsafe {
slice::from_raw_parts(
vertices as *const _ as *const u8,
std::mem::size_of_val(vertices),
)
};
if self.vertex_buffer_size >= vertices_raw.len() as u64 {
self.staging_belt
.write_buffer(
encoder,
&self.vertex_buffer,
0,
NonZeroU64::new(vertices_raw.len() as u64).expect("Non-empty vertices"),
device,
)
.copy_from_slice(vertices_raw);
} else {
self.vertex_buffer.destroy();
let (buffer, buffer_size) = create_oversized_buffer(
device,
Some("glyphon vertices"),
vertices_raw,
BufferUsages::VERTEX | BufferUsages::COPY_DST,
);
self.vertex_buffer = buffer;
self.vertex_buffer_size = buffer_size;
self.staging_belt.finish();
self.staging_belt = StagingBelt::new(buffer_size);
}
self.staging_belt.finish();
Ok(())
}
pub fn prepare<'a>(
&mut self,
device: &Device,
queue: &Queue,
encoder: &mut CommandEncoder,
font_system: &mut FontSystem,
atlas: &mut TextAtlas,
viewport: &Viewport,
text_areas: impl IntoIterator<Item = TextArea<'a>>,
cache: &mut SwashCache,
) -> Result<(), PrepareError> {
self.prepare_with_depth(
device,
queue,
encoder,
font_system,
atlas,
viewport,
text_areas,
cache,
zero_depth,
)
}
/// Renders all layouts that were previously provided to `prepare`.
pub fn render(
&self,
atlas: &TextAtlas,
viewport: &Viewport,
pass: &mut RenderPass<'_>,
) -> Result<(), RenderError> {
if self.glyphs_to_render == 0 {
return Ok(());
}
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &atlas.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);
Ok(())
}
}