use bytemuck::{Pod, Zeroable};
use crossfont::{BitmapBuffer, FontDesc, GlyphKey, Rasterize, Rasterizer, Size, Style};
use eframe::egui;
use egui_wgpu::{CallbackResources, CallbackTrait, ScreenDescriptor, wgpu};
use std::collections::{HashMap, HashSet};
use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::{Mutex, mpsc};
use unicode_width::UnicodeWidthChar;
const ATLAS_SIZE: u32 = 1024;
#[derive(Clone)]
pub struct GlyphSpan {
pub text: String,
pub color: egui::Color32,
pub weight: u16,
pub italic: bool,
}
#[derive(Clone)]
pub struct GlyphRun {
pub spans: Vec<GlyphSpan>,
pub position: egui::Pos2,
pub clip_bounds: egui::Rect,
pub family: String,
pub size: f32,
pub cell_width: f32,
pub line_height: f32,
pub descent: f32,
pub lcd: bool,
}
#[derive(Clone, Copy)]
pub struct FontMetrics {
pub cell_width: f32,
pub line_height: f32,
pub descent: f32,
}
pub fn font_metrics(family: &str, weight: u16, size: f32) -> FontMetrics {
let fallback = FontMetrics {
cell_width: size * 0.6,
line_height: size * 1.3,
descent: -size * 0.25,
};
let Ok(mut rasterizer) = Rasterizer::new() else {
return fallback;
};
let size = Size::from_px(size.round());
let style = Style::Specific(style_name(weight, false));
let Ok(font) = rasterizer.load_font(&FontDesc::new(family, style), size) else {
return fallback;
};
if rasterizer
.get_glyph(GlyphKey {
character: 'm',
font_key: font,
size,
})
.is_err()
{
return fallback;
}
let Ok(metrics) = rasterizer.metrics(font, size) else {
return fallback;
};
FontMetrics {
cell_width: metrics.average_advance as f32,
line_height: metrics.line_height as f32,
descent: metrics.descent,
}
}
#[derive(Clone, Eq, Hash, PartialEq)]
struct GlyphCacheKey {
character: char,
family: String,
weight: u16,
italic: bool,
pixel_size: u32,
}
struct RasterRequest {
key: GlyphCacheKey,
}
struct RasterResponse {
key: GlyphCacheKey,
glyph: Option<RasterizedGlyph>,
}
struct RasterizedGlyph {
width: u32,
height: u32,
top: i32,
left: i32,
pixels: Vec<u8>,
color: bool,
}
struct RasterWorker {
requests: mpsc::Sender<RasterRequest>,
responses: Mutex<mpsc::Receiver<RasterResponse>>,
}
impl RasterWorker {
fn new() -> Self {
let (request_tx, request_rx) = mpsc::channel::<RasterRequest>();
let (response_tx, response_rx) = mpsc::channel::<RasterResponse>();
std::thread::spawn(move || raster_loop(request_rx, response_tx));
Self {
requests: request_tx,
responses: Mutex::new(response_rx),
}
}
fn request(&self, key: GlyphCacheKey) -> bool {
self.requests.send(RasterRequest { key }).is_ok()
}
fn receive(&self) -> Option<RasterResponse> {
self.responses.lock().ok()?.try_recv().ok()
}
fn prewarm(&self, keys: Vec<GlyphCacheKey>) -> Vec<RasterResponse> {
let count = keys.len();
for key in keys {
if !self.request(key) {
return Vec::new();
}
}
let Ok(responses) = self.responses.lock() else {
return Vec::new();
};
(0..count).filter_map(|_| responses.recv().ok()).collect()
}
}
fn raster_loop(requests: mpsc::Receiver<RasterRequest>, responses: mpsc::Sender<RasterResponse>) {
profiling::register_thread!("Glyph rasterizer");
let Ok(mut rasterizer) = Rasterizer::new() else {
return;
};
let mut fonts = HashMap::new();
while let Ok(request) = requests.recv() {
let font_key = (
request.key.family.clone(),
request.key.weight,
request.key.italic,
request.key.pixel_size,
);
let size = Size::from_px(f32::from_bits(request.key.pixel_size));
let font = if let Some(font) = fonts.get(&font_key) {
Some(*font)
} else {
let style = Style::Specific(style_name(request.key.weight, request.key.italic));
rasterizer
.load_font(&FontDesc::new(&request.key.family, style), size)
.ok()
.inspect(|font| {
fonts.insert(font_key, *font);
})
};
let glyph = font.and_then(|font| {
let glyph = rasterizer
.get_glyph(GlyphKey {
character: request.key.character,
font_key: font,
size,
})
.ok()?;
let (pixels, color) = match glyph.buffer {
BitmapBuffer::Rgb(mask) => {
let mut pixels = Vec::with_capacity(mask.len() / 3 * 4);
for channels in mask.chunks_exact(3) {
pixels.extend_from_slice(&[
channels[0],
channels[1],
channels[2],
((u16::from(channels[0])
+ u16::from(channels[1])
+ u16::from(channels[2]))
/ 3) as u8,
]);
}
(pixels, false)
}
BitmapBuffer::Rgba(pixels) => (pixels, true),
};
Some(RasterizedGlyph {
width: glyph.width.max(0) as u32,
height: glyph.height.max(0) as u32,
top: glyph.top,
left: glyph.left,
pixels,
color,
})
});
let _ = responses.send(RasterResponse {
key: request.key,
glyph,
});
}
}
fn style_name(weight: u16, italic: bool) -> String {
let weight = match weight {
100..=250 => "Thin",
251..=350 => "Light",
351..=450 => "Regular",
451..=550 => "Medium",
551..=650 => "SemiBold",
651..=750 => "Bold",
751..=850 => "ExtraBold",
_ => "Black",
};
if italic {
format!("{weight} Italic")
} else {
weight.to_string()
}
}
#[derive(Clone, Copy)]
struct AtlasGlyph {
uv_min: [f32; 2],
uv_max: [f32; 2],
width: f32,
height: f32,
top: i32,
left: i32,
color: bool,
}
struct Atlas {
texture: wgpu::Texture,
bind_group: wgpu::BindGroup,
glyphs: HashMap<GlyphCacheKey, AtlasGlyph>,
x: u32,
y: u32,
row_height: u32,
}
impl Atlas {
fn new(device: &wgpu::Device, layout: &wgpu::BindGroupLayout, sampler: &wgpu::Sampler) -> Self {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("arreliny glyph atlas"),
size: wgpu::Extent3d {
width: ATLAS_SIZE,
height: ATLAS_SIZE,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("arreliny glyph atlas bind group"),
layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
],
});
Self {
texture,
bind_group,
glyphs: HashMap::new(),
x: 1,
y: 1,
row_height: 0,
}
}
fn insert(
&mut self,
queue: &wgpu::Queue,
key: GlyphCacheKey,
glyph: RasterizedGlyph,
) -> Option<AtlasGlyph> {
let width = glyph.width.max(1);
let height = glyph.height.max(1);
if self.x + width + 1 >= ATLAS_SIZE {
self.x = 1;
self.y += self.row_height + 1;
self.row_height = 0;
}
if self.y + height + 1 >= ATLAS_SIZE {
return None;
}
if !glyph.pixels.is_empty() {
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: self.x,
y: self.y,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
&glyph.pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(glyph.width * 4),
rows_per_image: Some(glyph.height),
},
wgpu::Extent3d {
width: glyph.width,
height: glyph.height,
depth_or_array_layers: 1,
},
);
}
let atlas_glyph = AtlasGlyph {
uv_min: [
self.x as f32 / ATLAS_SIZE as f32,
self.y as f32 / ATLAS_SIZE as f32,
],
uv_max: [
(self.x + glyph.width) as f32 / ATLAS_SIZE as f32,
(self.y + glyph.height) as f32 / ATLAS_SIZE as f32,
],
width: glyph.width as f32,
height: glyph.height as f32,
top: glyph.top,
left: glyph.left,
color: glyph.color,
};
self.x += width + 1;
self.row_height = self.row_height.max(height);
self.glyphs.insert(key, atlas_glyph);
Some(atlas_glyph)
}
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct Vertex {
position: [f32; 2],
uv: [f32; 2],
color: [f32; 4],
mode: f32,
}
struct Quad {
position: [f32; 2],
size: [f32; 2],
uv_min: [f32; 2],
uv_max: [f32; 2],
color: [f32; 4],
mode: f32,
clip: egui::Rect,
}
pub struct GlyphRenderer {
pipeline: wgpu::RenderPipeline,
atlas_layout: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
atlas: Atlas,
vertex_buffer: wgpu::Buffer,
vertex_capacity: usize,
vertex_count: u32,
rasterizer: RasterWorker,
pending: HashSet<GlyphCacheKey>,
failed: HashSet<GlyphCacheKey>,
last_frame_hash: Option<u64>,
}
impl GlyphRenderer {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
family: &str,
size: f32,
weight: u16,
scale: f32,
) -> Self {
let atlas_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("arreliny glyph atlas layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("arreliny glyph atlas sampler"),
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let atlas = Atlas::new(device, &atlas_layout, &sampler);
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("arreliny glyph shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("terminal_text.wgsl").into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("arreliny glyph pipeline layout"),
bind_group_layouts: &[Some(&atlas_layout)],
immediate_size: 0,
});
let vertex_layout = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![
0 => Float32x2,
1 => Float32x2,
2 => Float32x4,
3 => Float32
],
};
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("arreliny glyph pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
compilation_options: Default::default(),
buffers: &[vertex_layout],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let vertex_capacity = 4096;
let vertex_buffer = create_vertex_buffer(device, vertex_capacity);
let rasterizer = RasterWorker::new();
let mut renderer = Self {
pipeline,
atlas_layout,
sampler,
atlas,
vertex_buffer,
vertex_capacity,
vertex_count: 0,
rasterizer,
pending: HashSet::new(),
failed: HashSet::new(),
last_frame_hash: None,
};
renderer.prewarm_ascii(queue, family, size, weight, scale);
renderer
}
fn reset_atlas(&mut self, device: &wgpu::Device) {
self.atlas = Atlas::new(device, &self.atlas_layout, &self.sampler);
}
fn glyph(&mut self, key: GlyphCacheKey) -> Option<AtlasGlyph> {
if let Some(glyph) = self.atlas.glyphs.get(&key) {
return Some(*glyph);
}
if !self.failed.contains(&key)
&& !self.pending.contains(&key)
&& self.rasterizer.request(key.clone())
{
self.pending.insert(key);
}
None
}
fn prewarm_ascii(
&mut self,
queue: &wgpu::Queue,
family: &str,
size: f32,
weight: u16,
scale: f32,
) {
let bold_weight = [100, 300, 400, 700, 900]
.into_iter()
.find(|candidate| *candidate > weight)
.unwrap_or(900);
let pixel_size = (size * scale).round().to_bits();
let mut keys = Vec::with_capacity(95 * 4);
for (weight, italic) in [
(weight, false),
(bold_weight, false),
(weight, true),
(bold_weight, true),
] {
for character in ' '..='~' {
keys.push(GlyphCacheKey {
character,
family: family.to_string(),
weight,
italic,
pixel_size,
});
}
}
for response in self.rasterizer.prewarm(keys) {
if let Some(glyph) = response.glyph {
let _ = self.atlas.insert(queue, response.key, glyph);
}
}
}
fn receive_glyphs(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) -> bool {
let mut changed = false;
while let Some(response) = self.rasterizer.receive() {
self.pending.remove(&response.key);
if let Some(glyph) = response.glyph {
if self
.atlas
.insert(queue, response.key.clone(), glyph)
.is_none()
{
self.reset_atlas(device);
self.last_frame_hash = None;
}
changed = true;
} else {
self.failed.insert(response.key);
}
}
changed
}
}
fn create_vertex_buffer(device: &wgpu::Device, capacity: usize) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("arreliny glyph vertices"),
size: (capacity * std::mem::size_of::<Vertex>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
pub struct TerminalTextCallback {
pub runs: Vec<GlyphRun>,
}
impl CallbackTrait for TerminalTextCallback {
fn prepare(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
screen: &ScreenDescriptor,
_: &mut wgpu::CommandEncoder,
resources: &mut CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
profiling::scope!("renderer.prepare");
let Some(renderer) = resources.get_mut::<GlyphRenderer>() else {
return Vec::new();
};
let scale = screen.pixels_per_point;
let glyphs_changed = renderer.receive_glyphs(device, queue);
let frame_hash = {
profiling::scope!("renderer.hash_frame");
frame_hash(&self.runs, screen)
};
if !glyphs_changed && renderer.last_frame_hash == Some(frame_hash) {
return Vec::new();
}
let screen_size = [
screen.size_in_pixels[0] as f32,
screen.size_in_pixels[1] as f32,
];
let mut vertices = Vec::new();
{
profiling::scope!("renderer.build_vertices");
for run in &self.runs {
let mut column = 0usize;
for span in &run.spans {
for character in span.text.chars() {
let width = character.width().unwrap_or(0);
if character != ' ' && character != '\u{200d}' {
let key = GlyphCacheKey {
character,
family: run.family.clone(),
weight: span.weight,
italic: span.italic,
pixel_size: (run.size * scale).round().to_bits(),
};
if let Some(glyph) = renderer.glyph(key) {
let baseline = (run.line_height + run.descent) * scale;
let x = (run.position.x * scale).round()
+ column as f32 * run.cell_width * scale
+ glyph.left as f32;
let y =
(run.position.y * scale).round() + baseline - glyph.top as f32;
let color = color_array(span.color);
let mode = if glyph.color {
2.0
} else if run.lcd {
1.0
} else {
0.0
};
push_quad(
&mut vertices,
Quad {
position: [x, y],
size: [glyph.width, glyph.height],
uv_min: glyph.uv_min,
uv_max: glyph.uv_max,
color,
mode,
clip: run.clip_bounds,
},
scale,
screen_size,
);
}
}
column += width;
}
}
}
}
if vertices.len() > renderer.vertex_capacity {
renderer.vertex_capacity = vertices.len().next_power_of_two();
renderer.vertex_buffer = create_vertex_buffer(device, renderer.vertex_capacity);
}
if !vertices.is_empty() {
profiling::scope!("renderer.upload_vertices");
queue.write_buffer(&renderer.vertex_buffer, 0, bytemuck::cast_slice(&vertices));
}
renderer.vertex_count = vertices.len() as u32;
renderer.last_frame_hash = Some(frame_hash);
Vec::new()
}
fn paint(
&self,
_: egui::PaintCallbackInfo,
render_pass: &mut wgpu::RenderPass<'static>,
resources: &CallbackResources,
) {
let Some(renderer) = resources.get::<GlyphRenderer>() else {
return;
};
if renderer.vertex_count == 0 {
return;
}
render_pass.set_pipeline(&renderer.pipeline);
render_pass.set_bind_group(0, &renderer.atlas.bind_group, &[]);
render_pass.set_vertex_buffer(0, renderer.vertex_buffer.slice(..));
render_pass.draw(0..renderer.vertex_count, 0..1);
}
}
fn frame_hash(runs: &[GlyphRun], screen: &ScreenDescriptor) -> u64 {
let mut hash = DefaultHasher::new();
screen.size_in_pixels.hash(&mut hash);
screen.pixels_per_point.to_bits().hash(&mut hash);
for run in runs {
run.position.x.to_bits().hash(&mut hash);
run.position.y.to_bits().hash(&mut hash);
run.clip_bounds.min.x.to_bits().hash(&mut hash);
run.clip_bounds.min.y.to_bits().hash(&mut hash);
run.clip_bounds.max.x.to_bits().hash(&mut hash);
run.clip_bounds.max.y.to_bits().hash(&mut hash);
run.family.hash(&mut hash);
run.size.to_bits().hash(&mut hash);
run.cell_width.to_bits().hash(&mut hash);
run.line_height.to_bits().hash(&mut hash);
run.descent.to_bits().hash(&mut hash);
run.lcd.hash(&mut hash);
for span in &run.spans {
span.text.hash(&mut hash);
span.color.to_array().hash(&mut hash);
span.weight.hash(&mut hash);
span.italic.hash(&mut hash);
}
}
hash.finish()
}
fn color_array(color: egui::Color32) -> [f32; 4] {
let alpha = color.a() as f32 / 255.0;
[
color.r() as f32 / 255.0 * alpha,
color.g() as f32 / 255.0 * alpha,
color.b() as f32 / 255.0 * alpha,
alpha,
]
}
fn push_quad(vertices: &mut Vec<Vertex>, quad: Quad, scale: f32, screen: [f32; 2]) {
let position = quad.position;
let size = quad.size;
let mut uv_min = quad.uv_min;
let mut uv_max = quad.uv_max;
let color = quad.color;
let mode = quad.mode;
let clip = quad.clip;
if size[0] <= 0.0 || size[1] <= 0.0 {
return;
}
let clip_min = [clip.left() * scale, clip.top() * scale];
let clip_max = [clip.right() * scale, clip.bottom() * scale];
let mut min = position;
let mut max = [position[0] + size[0], position[1] + size[1]];
if max[0] <= clip_min[0]
|| max[1] <= clip_min[1]
|| min[0] >= clip_max[0]
|| min[1] >= clip_max[1]
{
return;
}
if min[0] < clip_min[0] {
let ratio = (clip_min[0] - min[0]) / size[0];
uv_min[0] += (uv_max[0] - uv_min[0]) * ratio;
min[0] = clip_min[0];
}
if min[1] < clip_min[1] {
let ratio = (clip_min[1] - min[1]) / size[1];
uv_min[1] += (uv_max[1] - uv_min[1]) * ratio;
min[1] = clip_min[1];
}
if max[0] > clip_max[0] {
let ratio = (max[0] - clip_max[0]) / size[0];
uv_max[0] -= (uv_max[0] - uv_min[0]) * ratio;
max[0] = clip_max[0];
}
if max[1] > clip_max[1] {
let ratio = (max[1] - clip_max[1]) / size[1];
uv_max[1] -= (uv_max[1] - uv_min[1]) * ratio;
max[1] = clip_max[1];
}
let ndc = |point: [f32; 2]| {
[
point[0] / screen[0] * 2.0 - 1.0,
1.0 - point[1] / screen[1] * 2.0,
]
};
let top_left = Vertex {
position: ndc([min[0], min[1]]),
uv: [uv_min[0], uv_min[1]],
color,
mode,
};
let top_right = Vertex {
position: ndc([max[0], min[1]]),
uv: [uv_max[0], uv_min[1]],
color,
mode,
};
let bottom_left = Vertex {
position: ndc([min[0], max[1]]),
uv: [uv_min[0], uv_max[1]],
color,
mode,
};
let bottom_right = Vertex {
position: ndc([max[0], max[1]]),
uv: [uv_max[0], uv_max[1]],
color,
mode,
};
vertices.extend_from_slice(&[
top_left,
bottom_left,
top_right,
top_right,
bottom_left,
bottom_right,
]);
}