GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/draw/glyphs.rs (17.4K)
1 //! The glyph atlas, CPU side: shaped runs ([`TextSpan`]s) rasterized through
2 //! swash into an RGBA8 atlas (a shelf packer, a cache keyed by cosmic-text's
3 //! glyph key) and turned into glyph quads. A renderer uploads [`pixels`]
4 //! whenever [`generation`] moves and draws [`vertices`] with `glyph.wgsl`.
5 //! Image quads use the same vertex and shader ([`image_quad_vertices`]).
6 //!
7 //! It was half of the Vulkan text stage; the other half — the upload and
8 //! the draw — is what each renderer keeps.
9 //!
10 //! [`pixels`]: GlyphAtlas::pixels
11 //! [`generation`]: GlyphAtlas::generation
12 //! [`vertices`]: GlyphAtlas::vertices
13
14 use std::collections::HashMap;
15
16 use cosmic_text::{CacheKey, FontSystem, SwashCache, SwashContent};
17
18 use super::{ImageQuad, TextSpan};
19
20 /// The atlas is one ATLAS_SIZE² RGBA8 texture (unorm, sampled nearest).
21 pub const ATLAS_SIZE: u32 = 1024;
22
23 /// What changed in a [`GlyphAtlas`] since a generation a renderer holds.
24 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
25 pub enum AtlasChange {
26 None,
27 /// One rectangle, in texels, covering every glyph written since.
28 Region { x: u32, y: u32, w: u32, h: u32 },
29 Full,
30 }
31 const ATLAS_PAD: u32 = 1;
32
33 #[repr(C)]
34 #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
35 pub struct GlyphVertex {
36 pub position: [f32; 2],
37 pub uv: [f32; 2],
38 pub color: [f32; 4],
39 pub clip_circle: [f32; 3],
40 pub clip_extents: [f32; 2],
41 }
42
43 // The glyph shader's vertex (locations 0..=4) for text AND image quads: both
44 // pipelines feed `glyph.wgsl`, and every renderer describes this exact layout.
45 const _: () = {
46 assert!(std::mem::size_of::<GlyphVertex>() == 52);
47 assert!(std::mem::offset_of!(GlyphVertex, position) == 0);
48 assert!(std::mem::offset_of!(GlyphVertex, uv) == 8);
49 assert!(std::mem::offset_of!(GlyphVertex, color) == 16);
50 assert!(std::mem::offset_of!(GlyphVertex, clip_circle) == 32);
51 assert!(std::mem::offset_of!(GlyphVertex, clip_extents) == 44);
52 };
53
54 #[derive(Clone, Copy)]
55 struct GlyphEntry {
56 /// Atlas texel rect.
57 u: u32,
58 v: u32,
59 w: u32,
60 h: u32,
61 /// Raster placement offsets (from swash).
62 left: i32,
63 top: i32,
64 is_color: bool,
65 /// Zero-sized raster (spaces): nothing to draw, but cached to skip re-rastering.
66 empty: bool,
67 }
68
69 struct Shelf {
70 cursor_x: u32,
71 cursor_y: u32,
72 row_height: u32,
73 }
74
75 impl Shelf {
76 fn new() -> Self {
77 Shelf { cursor_x: ATLAS_PAD, cursor_y: ATLAS_PAD, row_height: 0 }
78 }
79
80 fn insert(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
81 if w > ATLAS_SIZE - 2 * ATLAS_PAD || h > ATLAS_SIZE - 2 * ATLAS_PAD {
82 return None;
83 }
84 if self.cursor_x + w + ATLAS_PAD > ATLAS_SIZE {
85 self.cursor_x = ATLAS_PAD;
86 self.cursor_y += self.row_height + ATLAS_PAD;
87 self.row_height = 0;
88 }
89 if self.cursor_y + h + ATLAS_PAD > ATLAS_SIZE {
90 return None;
91 }
92 let pos = (self.cursor_x, self.cursor_y);
93 self.cursor_x += w + ATLAS_PAD;
94 self.row_height = self.row_height.max(h);
95 Some(pos)
96 }
97 }
98
99 /// The atlas and this frame's glyph quads.
100 pub struct GlyphAtlas {
101 /// RGBA8, ATLAS_SIZE², the texture's contents.
102 pixels: Vec<u8>,
103 /// Bumped whenever `pixels` changes; a renderer re-uploads on a change.
104 generation: u64,
105 /// The rectangle each glyph insert wrote, with the generation it made —
106 /// what [`changes_since`](Self::changes_since) unions so a renderer can
107 /// upload only what moved. Cleared by a repack.
108 dirty: Vec<(u64, [u32; 4])>,
109 /// Generations at or below this are no longer in `dirty` (a repack, or
110 /// the log's cap): a renderer that last saw one re-uploads it all.
111 dirty_floor: u64,
112 glyphs: HashMap<CacheKey, GlyphEntry>,
113 shelf: Shelf,
114 /// The staged text: replaced only by the next [`prepare`](Self::prepare),
115 /// so a frame drawn without a re-prepare keeps its text.
116 vertices: Vec<GlyphVertex>,
117 }
118
119 impl Default for GlyphAtlas {
120 fn default() -> Self {
121 Self::new()
122 }
123 }
124
125 impl GlyphAtlas {
126 pub fn new() -> Self {
127 Self {
128 pixels: vec![0u8; (ATLAS_SIZE * ATLAS_SIZE * 4) as usize],
129 generation: 1,
130 dirty: Vec::new(),
131 dirty_floor: 0,
132 glyphs: HashMap::new(),
133 shelf: Shelf::new(),
134 vertices: Vec::new(),
135 }
136 }
137
138 /// What a renderer holding generation `since` must upload to be
139 /// current: nothing, one rectangle covering every glyph written since,
140 /// or the whole atlas (a repack happened, or `since` predates the log).
141 /// Generation 1 is the cleared atlas a new one starts as.
142 pub fn changes_since(&self, since: u64) -> AtlasChange {
143 if since >= self.generation {
144 return AtlasChange::None;
145 }
146 if since < self.dirty_floor {
147 return AtlasChange::Full;
148 }
149 let mut rect: Option<[u32; 4]> = None;
150 for &(generation, [x, y, w, h]) in &self.dirty {
151 if generation <= since {
152 continue;
153 }
154 rect = Some(match rect {
155 None => [x, y, w, h],
156 Some([rx, ry, rw, rh]) => {
157 let (x0, y0) = (rx.min(x), ry.min(y));
158 let (x1, y1) = ((rx + rw).max(x + w), (ry + rh).max(y + h));
159 [x0, y0, x1 - x0, y1 - y0]
160 }
161 });
162 }
163 match rect {
164 Some([x, y, w, h]) => AtlasChange::Region { x, y, w, h },
165 None => AtlasChange::None,
166 }
167 }
168
169 /// The atlas texture's contents (RGBA8, `ATLAS_SIZE`²).
170 pub fn pixels(&self) -> &[u8] {
171 &self.pixels
172 }
173
174 /// Moves whenever [`pixels`](Self::pixels) changed.
175 pub fn generation(&self) -> u64 {
176 self.generation
177 }
178
179 /// The glyph quads the last [`prepare`](Self::prepare) built, six vertices each.
180 pub fn vertices(&self) -> &[GlyphVertex] {
181 &self.vertices
182 }
183
184 /// Rasterize (on miss) and cache one glyph. Returns None when the atlas is full.
185 fn ensure_glyph(
186 &mut self,
187 font_system: &mut FontSystem,
188 swash_cache: &mut SwashCache,
189 key: CacheKey,
190 ) -> Option<GlyphEntry> {
191 if let Some(entry) = self.glyphs.get(&key) {
192 return Some(*entry);
193 }
194 let image = swash_cache.get_image_uncached(font_system, key)?;
195 let w = image.placement.width;
196 let h = image.placement.height;
197 if w == 0 || h == 0 || image.data.is_empty() {
198 let entry = GlyphEntry {
199 u: 0, v: 0, w: 0, h: 0, left: 0, top: 0, is_color: false, empty: true,
200 };
201 self.glyphs.insert(key, entry);
202 return Some(entry);
203 }
204 let (u, v) = self.shelf.insert(w, h)?;
205
206 let is_color = !matches!(image.content, SwashContent::Mask);
207 for row in 0..h {
208 for col in 0..w {
209 let dst = (((v + row) * ATLAS_SIZE + (u + col)) * 4) as usize;
210 let texel = match image.content {
211 SwashContent::Mask => {
212 let a = image.data[(row * w + col) as usize];
213 [255, 255, 255, a]
214 }
215 // Color and SubpixelMask rasters are RGBA.
216 _ => {
217 let src = ((row * w + col) * 4) as usize;
218 [
219 image.data[src],
220 image.data[src + 1],
221 image.data[src + 2],
222 image.data[src + 3],
223 ]
224 }
225 };
226 self.pixels[dst..dst + 4].copy_from_slice(&texel);
227 }
228 }
229 self.generation += 1;
230 // Bounded: past the cap the oldest entries go, and a renderer that
231 // has fallen that far behind gets a full upload instead.
232 const DIRTY_CAP: usize = 4096;
233 if self.dirty.len() >= DIRTY_CAP {
234 let drop = self.dirty.len() / 2;
235 self.dirty_floor = self.dirty[drop - 1].0;
236 self.dirty.drain(..drop);
237 }
238 self.dirty.push((self.generation, [u, v, w, h]));
239
240 let entry = GlyphEntry {
241 u,
242 v,
243 w,
244 h,
245 left: image.placement.left,
246 top: image.placement.top,
247 is_color,
248 empty: false,
249 };
250 self.glyphs.insert(key, entry);
251 Some(entry)
252 }
253
254 /// Build this frame's glyph vertices. Positions/bounds in physical pixels,
255 /// NDC computed against the target's
256 /// `width` x `height` (Y up; the Vulkan renderer flips with its viewport).
257 pub fn prepare(
258 &mut self,
259 font_system: &mut FontSystem,
260 swash_cache: &mut SwashCache,
261 spans: &[TextSpan<'_>],
262 width: u32,
263 height: u32,
264 ) {
265 self.vertices.clear();
266 if !self.try_prepare(font_system, swash_cache, spans, width, height) {
267 // Atlas full: clear and repack with only the glyphs this frame needs.
268 log::info!("glyph atlas full — clearing and repacking");
269 self.glyphs.clear();
270 self.shelf = Shelf::new();
271 self.pixels.fill(0);
272 self.generation += 1;
273 self.dirty.clear();
274 self.dirty_floor = self.generation;
275 self.vertices.clear();
276 if !self.try_prepare(font_system, swash_cache, spans, width, height) {
277 log::error!("glyph atlas full even after repack; text truncated this frame");
278 }
279 }
280 }
281
282 fn try_prepare(
283 &mut self,
284 font_system: &mut FontSystem,
285 swash_cache: &mut SwashCache,
286 spans: &[TextSpan<'_>],
287 width: u32,
288 height: u32,
289 ) -> bool {
290 let sw = width as f32;
291 let sh = height as f32;
292 for span in spans {
293 for run in span.buffer.layout_runs() {
294 let line_y = (run.line_y * span.scale).round() as i32;
295 for glyph in run.glyphs.iter() {
296 let physical = glyph.physical((span.left, span.top), span.scale);
297 let Some(entry) =
298 self.ensure_glyph(font_system, swash_cache, physical.cache_key)
299 else {
300 // Distinguish "atlas full" (retryable) from "unrasterizable"
301 // (skip): a missing swash image caches as empty above, so a
302 // None here means the shelf rejected it.
303 if swash_cache
304 .get_image_uncached(font_system, physical.cache_key)
305 .is_some()
306 {
307 return false;
308 }
309 continue;
310 };
311 if entry.empty {
312 continue;
313 }
314
315 // glyphon's placement formula (kept verbatim), physical pixels.
316 let mut x0 = (physical.x + entry.left) as f32;
317 let mut y0 = (line_y + physical.y - entry.top) as f32;
318 let mut x1 = x0 + entry.w as f32;
319 let mut y1 = y0 + entry.h as f32;
320 let mut u0 = entry.u as f32;
321 let mut v0 = entry.v as f32;
322 let mut u1 = u0 + entry.w as f32;
323 let mut v1 = v0 + entry.h as f32;
324
325 // CPU clip to span bounds, shrinking UVs proportionally.
326 if let Some([bl, bt, br, bb]) = span.bounds {
327 let (bl, bt, br, bb) = (bl as f32, bt as f32, br as f32, bb as f32);
328 if x0 >= br || x1 <= bl || y0 >= bb || y1 <= bt {
329 continue;
330 }
331 if x0 < bl {
332 u0 += bl - x0;
333 x0 = bl;
334 }
335 if x1 > br {
336 u1 -= x1 - br;
337 x1 = br;
338 }
339 if y0 < bt {
340 v0 += bt - y0;
341 y0 = bt;
342 }
343 if y1 > bb {
344 v1 -= y1 - bb;
345 y1 = bb;
346 }
347 }
348
349 let color = if entry.is_color {
350 [1.0, 1.0, 1.0, 1.0]
351 } else if let Some(c) = glyph.color_opt {
352 [
353 c.r() as f32 / 255.0,
354 c.g() as f32 / 255.0,
355 c.b() as f32 / 255.0,
356 c.a() as f32 / 255.0,
357 ]
358 } else {
359 span.default_color
360 };
361
362 // Corner positions, optionally rotated about the span's center
363 // (physical px) before the NDC mapping.
364 let corners = match span.rotation {
365 None => [[x0, y0], [x1, y0], [x0, y1], [x1, y1]],
366 Some((angle, cx, cy)) => {
367 let (sin_a, cos_a) = angle.sin_cos();
368 let rot = |px: f32, py: f32| {
369 let (dx, dy) = (px - cx, py - cy);
370 [cx + dx * cos_a - dy * sin_a, cy + dx * sin_a + dy * cos_a]
371 };
372 [rot(x0, y0), rot(x1, y0), rot(x0, y1), rot(x1, y1)]
373 }
374 };
375 let ndc = |p: [f32; 2]| {
376 [(p[0] / sw) * 2.0 - 1.0, 1.0 - (p[1] / sh) * 2.0]
377 };
378 let uv = |u: f32, v: f32| [u / ATLAS_SIZE as f32, v / ATLAS_SIZE as f32];
379 let clip_circle = span.clip_circle;
380 let clip_extents = span.clip_extents;
381 let tl = GlyphVertex { position: ndc(corners[0]), uv: uv(u0, v0), color, clip_circle, clip_extents };
382 let tr = GlyphVertex { position: ndc(corners[1]), uv: uv(u1, v0), color, clip_circle, clip_extents };
383 let bl = GlyphVertex { position: ndc(corners[2]), uv: uv(u0, v1), color, clip_circle, clip_extents };
384 let br = GlyphVertex { position: ndc(corners[3]), uv: uv(u1, v1), color, clip_circle, clip_extents };
385 self.vertices.extend([tl, tr, bl, tr, br, bl]);
386 }
387 }
388 }
389 true
390 }
391 }
392
393 /// Image quads as the glyph shader's vertices, six per image in `images`
394 /// order, NDC against a `width` x `height` target. A renderer draws image `i`
395 /// as vertices `6i..6i+6` with that image's texture bound.
396 pub fn image_quad_vertices(images: &[ImageQuad], width: u32, height: u32) -> Vec<GlyphVertex> {
397 let sw = width as f32;
398 let sh = height as f32;
399 let mut verts: Vec<GlyphVertex> = Vec::with_capacity(images.len() * 6);
400 for q in images {
401 let (x, y, w, h) = q.rect;
402 let ndc = |px: f32, py: f32| [(px / sw) * 2.0 - 1.0, 1.0 - (py / sh) * 2.0];
403 let color = [1.0, 1.0, 1.0, q.alpha];
404 let clip_circle = [0.0; 3];
405 // Zero extents = the shader's plain-circle clip degenerate case. Inert
406 // while clip_circle.z is 0 (the clip branch never runs), but it must be
407 // a defined value, not whatever a missing attribute would read.
408 let clip_extents = [0.0; 2];
409 let tl = GlyphVertex { position: ndc(x, y), uv: [0.0, 0.0], color, clip_circle, clip_extents };
410 let tr = GlyphVertex { position: ndc(x + w, y), uv: [1.0, 0.0], color, clip_circle, clip_extents };
411 let bl = GlyphVertex { position: ndc(x, y + h), uv: [0.0, 1.0], color, clip_circle, clip_extents };
412 let br = GlyphVertex { position: ndc(x + w, y + h), uv: [1.0, 1.0], color, clip_circle, clip_extents };
413 verts.extend([tl, tr, bl, tr, br, bl]);
414 }
415 verts
416 }
417
418 #[cfg(test)]
419 mod tests {
420 use super::*;
421
422 #[test]
423 fn changes_since_unions_the_glyphs_written_after_a_generation() {
424 let mut atlas = GlyphAtlas::new();
425 assert_eq!(atlas.changes_since(1), AtlasChange::None, "a new atlas is the cleared one");
426 // Two glyphs written, as `rasterize` records them.
427 atlas.generation += 1;
428 atlas.dirty.push((atlas.generation, [10, 0, 8, 12]));
429 atlas.generation += 1;
430 atlas.dirty.push((atlas.generation, [18, 0, 6, 14]));
431 assert_eq!(atlas.changes_since(1), AtlasChange::Region { x: 10, y: 0, w: 14, h: 14 });
432 assert_eq!(atlas.changes_since(2), AtlasChange::Region { x: 18, y: 0, w: 6, h: 14 });
433 assert_eq!(atlas.changes_since(3), AtlasChange::None);
434 }
435
436 #[test]
437 fn a_repack_or_a_fallen_behind_reader_uploads_it_all() {
438 let mut atlas = GlyphAtlas::new();
439 atlas.generation += 1;
440 atlas.dirty.push((atlas.generation, [0, 0, 4, 4]));
441 // What `prepare` does on a full atlas.
442 atlas.generation += 1;
443 atlas.dirty.clear();
444 atlas.dirty_floor = atlas.generation;
445 assert_eq!(atlas.changes_since(2), AtlasChange::Full, "a reader from before the repack");
446 assert_eq!(atlas.changes_since(3), AtlasChange::None, "a reader of the cleared atlas");
447 atlas.generation += 1;
448 atlas.dirty.push((atlas.generation, [0, 0, 5, 5]));
449 assert_eq!(atlas.changes_since(3), AtlasChange::Region { x: 0, y: 0, w: 5, h: 5 });
450 }
451 }