use wgpu::util::DeviceExt;
use wgpu::TextureFormat;
pub const MSDF_WGSL: &str = include_str!("msdf.wgsl");
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GlyphInstance {
pub rect_min: [f32; 2],
pub rect_max: [f32; 2],
pub uv_min: [f32; 2],
pub uv_max: [f32; 2],
pub color: [f32; 4],
pub px_range: f32,
pub _pad: [f32; 3],
}
impl GlyphInstance {
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn new(rect: [f32; 4], uv: [f32; 4], color: [f32; 4], px_range: f32) -> Self {
Self {
rect_min: [rect[0], rect[1]],
rect_max: [rect[2], rect[3]],
uv_min: [uv[0], uv[1]],
uv_max: [uv[2], uv[3]],
color,
px_range,
_pad: [0.0; 3],
}
}
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
struct MsdfUniforms {
vp: [f32; 4],
}
#[must_use]
pub fn median3(a: f32, b: f32, c: f32) -> f32 {
a.max(b).min(a.min(b).max(c)).max(a.min(b))
}
#[must_use]
pub fn coverage(median: f32, screen_px_range: f32) -> f32 {
(screen_px_range * (median - 0.5) + 0.5).clamp(0.0, 1.0)
}
#[must_use]
pub fn screen_px_range(px_range_atlas: f32, glyph_screen_px: f32, atlas_glyph_px: f32) -> f32 {
px_range_atlas * (glyph_screen_px / atlas_glyph_px.max(1.0))
}
#[must_use]
pub fn disk_msdf_atlas(tile: u32, radius_frac: f32, px_range: f32) -> Vec<u8> {
let tile = tile.max(2);
let cx = tile as f32 / 2.0;
let cy = tile as f32 / 2.0;
let radius = radius_frac * (tile as f32 / 2.0);
let range = px_range.max(1e-3);
let mut out = vec![0u8; (tile * tile * 4) as usize];
for y in 0..tile {
for x in 0..tile {
let px = x as f32 + 0.5;
let py = y as f32 + 0.5;
let dist = ((px - cx).powi(2) + (py - cy).powi(2)).sqrt();
let sd = radius - dist; let enc = (0.5 + sd / range).clamp(0.0, 1.0);
let b = (enc * 255.0).round() as u8;
let i = ((y * tile + x) * 4) as usize;
out[i] = b;
out[i + 1] = b;
out[i + 2] = b;
out[i + 3] = 255;
}
}
out
}
pub struct MsdfText {
pipeline: wgpu::RenderPipeline,
uniform_bgl: wgpu::BindGroupLayout,
atlas_bgl: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
uniform: wgpu::Buffer,
target_format: TextureFormat,
}
impl MsdfText {
pub fn new(device: &wgpu::Device, target_format: TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("l0_msdf"),
source: wgpu::ShaderSource::Wgsl(MSDF_WGSL.into()),
});
let uniform_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("l0_msdf_uniform_bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
count: None,
}],
});
let atlas_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("l0_msdf_atlas_bgl"),
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 blend = Some(wgpu::BlendState {
color: wgpu::BlendComponent { src_factor: wgpu::BlendFactor::One, dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, operation: wgpu::BlendOperation::Add },
alpha: wgpu::BlendComponent { src_factor: wgpu::BlendFactor::One, dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, operation: wgpu::BlendOperation::Add },
});
let attrs = [
wgpu::VertexAttribute { offset: 0, shader_location: 0, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 8, shader_location: 1, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 16, shader_location: 2, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 24, shader_location: 3, format: wgpu::VertexFormat::Float32x2 },
wgpu::VertexAttribute { offset: 32, shader_location: 4, format: wgpu::VertexFormat::Float32x4 },
wgpu::VertexAttribute { offset: 48, shader_location: 5, format: wgpu::VertexFormat::Float32 },
];
let layout = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<GlyphInstance>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &attrs,
};
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("l0_msdf_pipeline"),
layout: Some(&device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("l0_msdf_pll"),
bind_group_layouts: &[Some(&uniform_bgl), Some(&atlas_bgl)],
immediate_size: 0,
})),
vertex: wgpu::VertexState { module: &shader, entry_point: Some("msdf_vs"), compilation_options: Default::default(), buffers: &[layout] },
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("msdf_fs"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState { format: target_format, blend, write_mask: wgpu::ColorWrites::ALL })],
}),
multiview_mask: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("l0_msdf_sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
..Default::default()
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("l0_msdf_uniform"),
size: std::mem::size_of::<MsdfUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Self { pipeline, uniform_bgl, atlas_bgl, sampler, uniform, target_format }
}
#[must_use]
pub fn target_format(&self) -> TextureFormat {
self.target_format
}
pub fn atlas_bind(&self, device: &wgpu::Device, atlas_view: &wgpu::TextureView) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("l0_msdf_atlas_bind"),
layout: &self.atlas_bgl,
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(atlas_view) },
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&self.sampler) },
],
})
}
#[allow(clippy::too_many_arguments)]
pub fn render(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
atlas_bind: &wgpu::BindGroup,
glyphs: &[GlyphInstance],
load: bool,
w: u32,
h: u32,
) {
if glyphs.is_empty() {
return;
}
let u = MsdfUniforms { vp: [w.max(1) as f32, h.max(1) as f32, 0.0, 0.0] };
queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&u));
let uni_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("l0_msdf_uni_bind"),
layout: &self.uniform_bgl,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: self.uniform.as_entire_binding() }],
});
let inst = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("l0_msdf_instances"),
contents: bytemuck::cast_slice(glyphs),
usage: wgpu::BufferUsages::VERTEX,
});
let load_op = if load { wgpu::LoadOp::Load } else { wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT) };
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("l0_msdf_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations { load: load_op, store: wgpu::StoreOp::Store },
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
rp.set_pipeline(&self.pipeline);
rp.set_bind_group(0, &uni_bind, &[]);
rp.set_bind_group(1, atlas_bind, &[]);
rp.set_vertex_buffer(0, inst.slice(..));
rp.draw(0..6, 0..glyphs.len() as u32);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn median3_is_the_middle() {
assert_eq!(median3(0.1, 0.5, 0.9), 0.5);
assert_eq!(median3(0.9, 0.5, 0.1), 0.5);
assert_eq!(median3(0.5, 0.9, 0.1), 0.5);
assert_eq!(median3(0.2, 0.2, 0.8), 0.2);
assert_eq!(median3(0.7, 0.7, 0.7), 0.7);
}
#[test]
fn coverage_is_an_edge_and_sharper_with_more_range() {
assert!(coverage(0.9, 8.0) > 0.99, "inside → opaque");
assert!(coverage(0.1, 8.0) < 0.01, "outside → transparent");
assert!((coverage(0.5, 8.0) - 0.5).abs() < 1e-6, "edge → 0.5");
let soft = coverage(0.52, 4.0);
let sharp = coverage(0.52, 16.0);
assert!(sharp > soft, "more screen-px-range = crisper edge ({soft} → {sharp})");
}
#[test]
fn screen_px_range_scales_with_glyph_size() {
let small = screen_px_range(4.0, 16.0, 32.0);
let big = screen_px_range(4.0, 64.0, 32.0);
assert!((big / small - 4.0).abs() < 1e-4, "4× glyph → 4× range");
assert!(small > 0.0);
}
#[test]
fn disk_atlas_encodes_signed_distance() {
let tile = 32u32;
let atlas = disk_msdf_atlas(tile, 0.7, 6.0);
let at = |x: u32, y: u32| atlas[((y * tile + x) * 4) as usize] as f32 / 255.0;
assert!(at(tile / 2, tile / 2) > 0.5, "centre is inside the disk");
assert!(at(0, 0) < 0.5, "corner is outside the disk");
assert!(coverage(at(tile / 2, tile / 2), 8.0) > 0.9);
assert!(coverage(at(1, 1), 8.0) < 0.1);
}
#[test]
fn glyph_layout_and_shader_entry_points() {
assert_eq!(std::mem::size_of::<GlyphInstance>(), 64);
assert!(MSDF_WGSL.contains("fn msdf_vs"));
assert!(MSDF_WGSL.contains("fn msdf_fs"));
assert!(MSDF_WGSL.contains("fn median3"));
}
fn headless_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::default();
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
force_fallback_adapter: false,
compatible_surface: None,
}))
.ok()?;
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("l0-msdf-proof"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_defaults(),
memory_hints: wgpu::MemoryHints::default(),
experimental_features: wgpu::ExperimentalFeatures::disabled(),
trace: wgpu::Trace::Off,
}))
.ok()
}
fn upload_atlas(device: &wgpu::Device, queue: &wgpu::Queue, tile: u32, bytes: &[u8]) -> wgpu::TextureView {
let tex = device.create_texture(&wgpu::TextureDescriptor {
label: Some("msdf-proof-atlas"),
size: wgpu::Extent3d { width: tile, height: tile, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo { texture: &tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
bytes,
wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(tile * 4), rows_per_image: Some(tile) },
wgpu::Extent3d { width: tile, height: tile, depth_or_array_layers: 1 },
);
tex.create_view(&Default::default())
}
fn render_glyphs(w: u32, h: u32, tile: u32, atlas: &[u8], glyphs: &[GlyphInstance]) -> Option<Vec<u8>> {
let (device, queue) = headless_device()?;
let atlas_view = upload_atlas(&device, &queue, tile, atlas);
let msdf = MsdfText::new(&device, TextureFormat::Rgba8Unorm);
let atlas_bind = msdf.atlas_bind(&device, &atlas_view);
let target = device.create_texture(&wgpu::TextureDescriptor {
label: Some("msdf-proof-target"),
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&Default::default());
let mut enc = device.create_command_encoder(&Default::default());
msdf.render(&device, &queue, &mut enc, &view, &atlas_bind, glyphs, false, w, h);
let bpp = 4u32;
let unpadded = w * bpp;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padded = unpadded.div_ceil(align) * align;
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("msdf-proof-readback"),
size: (padded * h) as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
enc.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo { texture: &target, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
wgpu::TexelCopyBufferInfo { buffer: &readback, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(padded), rows_per_image: Some(h) } },
wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
);
queue.submit(Some(enc.finish()));
let slice = readback.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); });
device.poll(wgpu::PollType::wait_indefinitely()).ok()?;
rx.recv().ok()?.ok()?;
let data = slice.get_mapped_range();
let mut rgba = Vec::with_capacity((w * h * 4) as usize);
for row in 0..h {
let s = (row * padded) as usize;
rgba.extend_from_slice(&data[s..s + unpadded as usize]);
}
drop(data);
readback.unmap();
Some(rgba)
}
#[test]
fn msdf_render_proof_crisp_disk_at_any_zoom() {
let tile = 32u32;
let px_range = 6.0f32;
let radius_frac = 0.8f32;
let atlas = disk_msdf_atlas(tile, radius_frac, px_range);
let (w, h) = (48u32, 48u32);
let g = 24.0f32;
let x0 = (w as f32 - g) / 2.0;
let y0 = (h as f32 - g) / 2.0;
let spr = screen_px_range(px_range, g, tile as f32);
let glyph = GlyphInstance::new([x0, y0, x0 + g, y0 + g], [0.0, 0.0, 1.0, 1.0], [0.2, 0.9, 1.0, 1.0], spr);
let Some(px) = render_glyphs(w, h, tile, &atlas, &[glyph]) else {
eprintln!("[msdf] no GPU adapter — skipping render proof");
return;
};
let at = |buf: &[u8], stride: u32, x: u32, y: u32| -> [u8; 4] {
let i = ((y * stride + x) * 4) as usize;
[buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]
};
let centre = at(&px, w, w / 2, h / 2);
assert!(centre[1] > 180 && centre[2] > 180, "disk centre is opaque tint, got {centre:?}");
let corner = at(&px, w, 0, 0);
assert!((corner[0] as u32 + corner[1] as u32 + corner[2] as u32) < 20, "corner empty, got {corner:?}");
let lit_base = px.chunks_exact(4).filter(|p| p[3] > 128).count();
let r = radius_frac * g / 2.0;
let area = std::f32::consts::PI * r * r;
assert!(
(lit_base as f32) > area * 0.6 && (lit_base as f32) < area * 1.6,
"lit area {lit_base} ≈ disk area {area:.0} (a real shape, not a blob)"
);
let (w2, h2) = (96u32, 96u32);
let g2 = 48.0f32;
let x2 = (w2 as f32 - g2) / 2.0;
let y2 = (h2 as f32 - g2) / 2.0;
let spr2 = screen_px_range(px_range, g2, tile as f32);
let glyph2 = GlyphInstance::new([x2, y2, x2 + g2, y2 + g2], [0.0, 0.0, 1.0, 1.0], [0.2, 0.9, 1.0, 1.0], spr2);
let px2 = render_glyphs(w2, h2, tile, &atlas, &[glyph2]).expect("adapter present (base render succeeded)");
let lit_2x = px2.chunks_exact(4).filter(|p| p[3] > 128).count();
let ratio = lit_2x as f32 / lit_base as f32;
assert!(
(3.0..5.0).contains(&ratio),
"2× zoom grows area ~4× ({ratio:.2}×) — edges stay crisp, not blurred"
);
}
}