git.lucas.co / cce-ui
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 }