#![cfg(feature = "wgpu")]
use bytemuck::{Pod, Zeroable};
use crate::generator::GlyphOutline;
use crate::math_segment::Segment;
use crate::segment_soa::SegmentSoa;
#[derive(Debug, Clone, Copy)]
pub struct AtlasRegion {
pub layer: u32,
pub min_x: u32,
pub min_y: u32,
pub width: u32,
pub height: u32,
pub padding_x: u32,
pub padding_y: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct SegmentGpu {
from: [f32; 2],
to: [f32; 2],
mid: [f32; 2],
tangent: [f32; 2],
tangent_unit: [f32; 2],
inv_tangent_len_sq: f32,
color_mask: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct Vertex {
clip_pos: [f32; 2],
bound_pos: [f32; 2],
seg_from: u32,
seg_to: u32,
units_per_em: f32,
}
#[derive(Default)]
struct LayerBatch {
vertices: Vec<Vertex>,
indices: Vec<u32>,
}
pub struct MtsdfGpuWriter {
pipeline: wgpu::RenderPipeline,
bgl: wgpu::BindGroupLayout,
segments: Vec<SegmentGpu>,
layers: Vec<LayerBatch>,
segment_buf: Option<wgpu::Buffer>,
segment_cap: u64,
vertex_buf: Option<wgpu::Buffer>,
vertex_cap: u64,
index_buf: Option<wgpu::Buffer>,
index_cap: u64,
bind_group: Option<wgpu::BindGroup>,
}
impl MtsdfGpuWriter {
pub fn new(device: &wgpu::Device) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("klyff_msdf gpu shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"msdf_gen.wgsl"
))),
});
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("klyff_msdf gpu bgl"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("klyff_msdf gpu pipeline layout"),
bind_group_layouts: &[&bgl],
push_constant_ranges: &[],
});
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 => Uint32, 3 => Uint32, 4 => Float32, ],
};
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("klyff_msdf gpu pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[vertex_layout],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: wgpu::TextureFormat::Rgba8Unorm,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: None,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
Self {
pipeline,
bgl,
segments: Vec::new(),
layers: Vec::new(),
segment_buf: None,
segment_cap: 0,
vertex_buf: None,
vertex_cap: 0,
index_buf: None,
index_cap: 0,
bind_group: None,
}
}
pub fn add_glyph(&mut self, outline: GlyphOutline<'_>, region: AtlasRegion) {
profiling::scope!("GpuMsdfWriter::add_glyph");
let segs = &outline.generator.cached_segment_vec;
let soa = &outline.generator.cached_soa;
let units_per_em = outline.units_per_em;
let bound = outline.bound;
let seg_from = self.segments.len() as u32;
push_segments(&mut self.segments, segs, soa);
let seg_to = self.segments.len() as u32;
let total_w = region.width as f32;
let total_h = region.height as f32;
let inner_w = (region.width - 2 * region.padding_x).max(1) as f32;
let inner_h = (region.height - 2 * region.padding_y).max(1) as f32;
let px = region.padding_x as f32;
let py = region.padding_y as f32;
let bound_size = bound.max - bound.min;
let bx = |uv_x: f32| bound.min.x + bound_size.x * (uv_x - px) / inner_w;
let by = |uv_y: f32| bound.min.y + bound_size.y * (total_h - uv_y - py) / inner_h;
let pxmin = region.min_x as f32;
let pymin = region.min_y as f32;
let pxmax = (region.min_x + region.width) as f32;
let pymax = (region.min_y + region.height) as f32;
let mk = |px_atlas: f32, py_atlas: f32, uv_x: f32, uv_y: f32| Vertex {
clip_pos: [px_atlas, py_atlas],
bound_pos: [bx(uv_x), by(uv_y)],
seg_from,
seg_to,
units_per_em,
};
let layer = region.layer as usize;
if self.layers.len() <= layer {
self.layers.resize_with(layer + 1, LayerBatch::default);
}
let batch = &mut self.layers[layer];
let v0 = batch.vertices.len() as u32;
batch.vertices.push(mk(pxmin, pymin, 0.0, 0.0));
batch.vertices.push(mk(pxmax, pymin, total_w, 0.0));
batch.vertices.push(mk(pxmin, pymax, 0.0, total_h));
batch.vertices.push(mk(pxmax, pymax, total_w, total_h));
batch.indices.push(v0);
batch.indices.push(v0 + 2);
batch.indices.push(v0 + 1);
batch.indices.push(v0 + 1);
batch.indices.push(v0 + 2);
batch.indices.push(v0 + 3);
}
pub fn write_glyphs(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
texture: &wgpu::Texture,
) {
profiling::scope!("GpuMsdfWriter::write_glyphs");
if self.layers.iter().all(|l| l.indices.is_empty()) {
return;
}
debug_assert!(
self.layers.len() as u32 <= texture.depth_or_array_layers(),
"AtlasRegion.layer ({}) exceeds texture layer count ({})",
self.layers.len() - 1,
texture.depth_or_array_layers(),
);
let tw = texture.width() as f32;
let th = texture.height() as f32;
for layer in &mut self.layers {
for v in &mut layer.vertices {
let px = v.clip_pos[0];
let py = v.clip_pos[1];
v.clip_pos = [(px / tw) * 2.0 - 1.0, 1.0 - (py / th) * 2.0];
}
}
let mut vertices_all: Vec<Vertex> = Vec::new();
let mut indices_all: Vec<u32> = Vec::new();
let mut layer_draws: Vec<(u32, i32, u32, u32)> = Vec::new();
for (layer_idx, layer) in self.layers.iter().enumerate() {
if layer.indices.is_empty() {
continue;
}
let base_vertex = vertices_all.len() as i32;
let index_start = indices_all.len() as u32;
vertices_all.extend_from_slice(&layer.vertices);
indices_all.extend_from_slice(&layer.indices);
let index_count = layer.indices.len() as u32;
layer_draws.push((layer_idx as u32, base_vertex, index_start, index_count));
}
let seg_bytes: &[u8] = bytemuck::cast_slice(&self.segments);
let seg_size = seg_bytes.len() as u64;
if self.segment_buf.is_none() || self.segment_cap < seg_size {
let new_cap = seg_size.next_power_of_two().max(256);
self.segment_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
label: Some("klyff_msdf segment storage"),
size: new_cap,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}));
self.segment_cap = new_cap;
self.bind_group = None;
}
queue.write_buffer(self.segment_buf.as_ref().unwrap(), 0, seg_bytes);
let v_bytes: &[u8] = bytemuck::cast_slice(&vertices_all);
let v_size = v_bytes.len() as u64;
if self.vertex_buf.is_none() || self.vertex_cap < v_size {
let new_cap = v_size.next_power_of_two().max(256);
self.vertex_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
label: Some("klyff_msdf vertices"),
size: new_cap,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}));
self.vertex_cap = new_cap;
}
queue.write_buffer(self.vertex_buf.as_ref().unwrap(), 0, v_bytes);
let i_bytes: &[u8] = bytemuck::cast_slice(&indices_all);
let i_size = i_bytes.len() as u64;
if self.index_buf.is_none() || self.index_cap < i_size {
let new_cap = i_size.next_power_of_two().max(256);
self.index_buf = Some(device.create_buffer(&wgpu::BufferDescriptor {
label: Some("klyff_msdf indices"),
size: new_cap,
usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
}));
self.index_cap = new_cap;
}
queue.write_buffer(self.index_buf.as_ref().unwrap(), 0, i_bytes);
if self.bind_group.is_none() {
self.bind_group = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("klyff_msdf bg"),
layout: &self.bgl,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: self.segment_buf.as_ref().unwrap().as_entire_binding(),
}],
}));
}
let vertex_buf = self.vertex_buf.as_ref().unwrap();
let index_buf = self.index_buf.as_ref().unwrap();
let bind_group = self.bind_group.as_ref().unwrap();
for (layer, base_vertex, index_start, index_count) in layer_draws {
let view = texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("klyff_msdf atlas layer view"),
dimension: Some(wgpu::TextureViewDimension::D2),
base_array_layer: layer,
array_layer_count: Some(1),
..Default::default()
});
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("klyff_msdf write pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, bind_group, &[]);
pass.set_vertex_buffer(0, vertex_buf.slice(..));
pass.set_index_buffer(index_buf.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(index_start..index_start + index_count, base_vertex, 0..1);
}
self.segments.clear();
for l in &mut self.layers {
l.vertices.clear();
l.indices.clear();
}
}
}
fn push_segments(out: &mut Vec<SegmentGpu>, segs: &[Segment], soa: &SegmentSoa) {
out.reserve(segs.len());
for (i, seg) in segs.iter().enumerate() {
out.push(SegmentGpu {
from: seg.from.to_array(),
to: seg.to.to_array(),
mid: seg.mid.to_array(),
tangent: soa.tangent[i].to_array(),
tangent_unit: soa.tangent_unit[i].to_array(),
inv_tangent_len_sq: soa.inv_tangent_len_sq[i],
color_mask: soa.color_mask[i] as u32,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shader_compiles() {
let instance = wgpu::Instance::default();
let Some(adapter) =
pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
}))
.ok()
else {
eprintln!("no wgpu adapter available; skipping shader_compiles");
return;
};
let (device, _queue) =
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("klyff_msdf shader compile test device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::Performance,
trace: wgpu::Trace::Off,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
}))
.expect("request_device");
device.push_error_scope(wgpu::ErrorFilter::Validation);
let _writer = MtsdfGpuWriter::new(&device);
let err = pollster::block_on(device.pop_error_scope());
assert!(
err.is_none(),
"shader/pipeline failed validation: {:?}",
err
);
}
}