git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/vk/text.rs (33.1K)

  1 //! Text on ash: cosmic-text shaping + swash rasterization into a self-managed
  2 //! RGBA glyph atlas, drawn by the glyph.wgsl pipeline inside the renderer's
  3 //! render pass. (cosmic-text used to be reached through glyphon's re-export;
  4 //! the dependency is direct now that the wgpu path is gone, pinned to the same
  5 //! version, so shaping behavior and fonts are unchanged.)
  6 //!
  7 //! `TextSpan` mirrors what was `glyphon::TextArea` (buffer + position + scale +
  8 //! bounds + default color) — the shape the wgpu-era cutover was written against.
  9 //!
 10 //! Atlas strategy: shelf packing into a 1024² RGBA8 image with a CPU mirror.
 11 //! When new glyphs land, the whole mirror is re-uploaded before the next render
 12 //! pass (bounded 4 MiB, and only on glyph-miss frames); if the atlas fills, it is
 13 //! cleared and repacked with just the current frame's glyphs. Mask glyphs are
 14 //! stored white-with-alpha, color (emoji) glyphs as-is drawn with a white vertex
 15 //! color — glyph.wgsl multiplies either by the vertex color.
 16 
 17 use std::collections::HashMap;
 18 
 19 use ash::vk;
 20 use gpu_allocator::vulkan::{
 21     Allocation, AllocationCreateDesc, AllocationScheme, Allocator,
 22 };
 23 use gpu_allocator::MemoryLocation;
 24 
 25 use cosmic_text::{Buffer as TextBuffer, CacheKey, SwashContent};
 26 use cosmic_text::{FontSystem, SwashCache};
 27 
 28 use super::renderer::{create_cpu_buffer, destroy_cpu_buffer, AllocatedBuffer};
 29 
 30 const ATLAS_SIZE: u32 = 1024;
 31 const ATLAS_PAD: u32 = 1;
 32 
 33 /// One shaped text run to draw. `left`/`top` are physical pixels and `scale`
 34 /// multiplies the shaped (logical) glyph positions — the same contract as
 35 /// the old glyphon::TextArea, where callers pass `label.x * scale`.
 36 pub struct TextSpan<'a> {
 37     pub buffer: &'a TextBuffer,
 38     pub left: f32,
 39     pub top: f32,
 40     pub scale: f32,
 41     /// Physical-pixel clip rect (left, top, right, bottom); None = whole surface.
 42     pub bounds: Option<[i32; 4]>,
 43     /// 0..=1 sRGB + alpha, applied to glyphs without their own color.
 44     pub default_color: [f32; 4],
 45     /// Rotate the span's glyph quads by (radians, center_x, center_y) in
 46     /// physical pixels — the circular network pane's curved rim labels.
 47     pub rotation: Option<(f32, f32, f32)>,
 48     /// Fragment circle clip (center_x, center_y, radius) in physical pixels;
 49     /// zero radius disables (matches shader.wgsl's clip_circle).
 50     pub clip_circle: [f32; 3],
 51     /// Rounded-rect clip half-extents (physical px). Zero keeps `clip_circle` a plain
 52     /// circle; non-zero reinterprets it as a rounded-rect SDF clip — center
 53     /// `clip_circle.xy`, corner radius `clip_circle.z`, inner box half-size
 54     /// `clip_extents` — so plate children (labels included) cut off at rounded corners.
 55     pub clip_extents: [f32; 2],
 56 }
 57 
 58 #[repr(C)]
 59 #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
 60 struct GlyphVertex {
 61     position: [f32; 2],
 62     uv: [f32; 2],
 63     color: [f32; 4],
 64     clip_circle: [f32; 3],
 65     clip_extents: [f32; 2],
 66 }
 67 
 68 // See the matching block in `image.rs`: both pipelines feed the same glyph
 69 // shader (locations 0..=4), so both vertex structs must hold this exact layout.
 70 const _: () = {
 71     assert!(std::mem::size_of::<GlyphVertex>() == 52);
 72     assert!(std::mem::offset_of!(GlyphVertex, position) == 0);
 73     assert!(std::mem::offset_of!(GlyphVertex, uv) == 8);
 74     assert!(std::mem::offset_of!(GlyphVertex, color) == 16);
 75     assert!(std::mem::offset_of!(GlyphVertex, clip_circle) == 32);
 76     assert!(std::mem::offset_of!(GlyphVertex, clip_extents) == 44);
 77 };
 78 
 79 #[derive(Clone, Copy)]
 80 struct GlyphEntry {
 81     /// Atlas texel rect.
 82     u: u32,
 83     v: u32,
 84     w: u32,
 85     h: u32,
 86     /// Raster placement offsets (from swash).
 87     left: i32,
 88     top: i32,
 89     is_color: bool,
 90     /// Zero-sized raster (spaces): nothing to draw, but cached to skip re-rastering.
 91     empty: bool,
 92 }
 93 
 94 struct Shelf {
 95     cursor_x: u32,
 96     cursor_y: u32,
 97     row_height: u32,
 98 }
 99 
100 impl Shelf {
101     fn new() -> Self {
102         Shelf { cursor_x: ATLAS_PAD, cursor_y: ATLAS_PAD, row_height: 0 }
103     }
104 
105     fn insert(&mut self, w: u32, h: u32) -> Option<(u32, u32)> {
106         if w > ATLAS_SIZE - 2 * ATLAS_PAD || h > ATLAS_SIZE - 2 * ATLAS_PAD {
107             return None;
108         }
109         if self.cursor_x + w + ATLAS_PAD > ATLAS_SIZE {
110             self.cursor_x = ATLAS_PAD;
111             self.cursor_y += self.row_height + ATLAS_PAD;
112             self.row_height = 0;
113         }
114         if self.cursor_y + h + ATLAS_PAD > ATLAS_SIZE {
115             return None;
116         }
117         let pos = (self.cursor_x, self.cursor_y);
118         self.cursor_x += w + ATLAS_PAD;
119         self.row_height = self.row_height.max(h);
120         Some(pos)
121     }
122 }
123 
124 struct TextFrame {
125     vertex: AllocatedBuffer,
126     vertex_count: u32,
127     staging: AllocatedBuffer,
128     /// Atlas generation this frame's staging buffer last uploaded.
129     uploaded_generation: u64,
130 }
131 
132 pub(crate) struct TextStage {
133     pipeline: vk::Pipeline,
134     pipeline_layout: vk::PipelineLayout,
135     descriptor_set_layout: vk::DescriptorSetLayout,
136     descriptor_pool: vk::DescriptorPool,
137     descriptor_set: vk::DescriptorSet,
138     shader_module: vk::ShaderModule,
139     sampler: vk::Sampler,
140 
141     atlas_image: vk::Image,
142     atlas_view: vk::ImageView,
143     atlas_allocation: Option<Allocation>,
144     /// CPU mirror of the atlas (RGBA8, ATLAS_SIZE²).
145     atlas_cpu: Vec<u8>,
146     atlas_initialized: bool,
147     generation: u64,
148 
149     glyphs: HashMap<CacheKey, GlyphEntry>,
150     shelf: Shelf,
151 
152     pending_vertices: Vec<GlyphVertex>,
153     frames: Vec<TextFrame>,
154 }
155 
156 impl TextStage {
157     pub(crate) fn new(
158         device: &ash::Device,
159         allocator: &mut Allocator,
160         render_pass: vk::RenderPass,
161         frames_in_flight: usize,
162     ) -> Self {
163         unsafe {
164             let bindings = [
165                 vk::DescriptorSetLayoutBinding::default()
166                     .binding(0)
167                     .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
168                     .descriptor_count(1)
169                     .stage_flags(vk::ShaderStageFlags::FRAGMENT),
170                 vk::DescriptorSetLayoutBinding::default()
171                     .binding(1)
172                     .descriptor_type(vk::DescriptorType::SAMPLER)
173                     .descriptor_count(1)
174                     .stage_flags(vk::ShaderStageFlags::FRAGMENT),
175             ];
176             let descriptor_set_layout = device
177                 .create_descriptor_set_layout(
178                     &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
179                     None,
180                 )
181                 .expect("Failed to create text descriptor set layout");
182             let set_layouts = [descriptor_set_layout];
183             let pipeline_layout = device
184                 .create_pipeline_layout(
185                     &vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts),
186                     None,
187                 )
188                 .expect("Failed to create text pipeline layout");
189 
190             let spirv = super::renderer::glyph_spirv();
191             let shader_module = device
192                 .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(spirv), None)
193                 .expect("Failed to create glyph shader module");
194 
195             let stages = [
196                 vk::PipelineShaderStageCreateInfo::default()
197                     .stage(vk::ShaderStageFlags::VERTEX)
198                     .module(shader_module)
199                     .name(c"vs_main"),
200                 vk::PipelineShaderStageCreateInfo::default()
201                     .stage(vk::ShaderStageFlags::FRAGMENT)
202                     .module(shader_module)
203                     .name(c"fs_main"),
204             ];
205             let vertex_bindings = [vk::VertexInputBindingDescription::default()
206                 .binding(0)
207                 .stride(std::mem::size_of::<GlyphVertex>() as u32)
208                 .input_rate(vk::VertexInputRate::VERTEX)];
209             let vertex_attributes = [
210                 vk::VertexInputAttributeDescription::default()
211                     .location(0)
212                     .binding(0)
213                     .format(vk::Format::R32G32_SFLOAT)
214                     .offset(0),
215                 vk::VertexInputAttributeDescription::default()
216                     .location(1)
217                     .binding(0)
218                     .format(vk::Format::R32G32_SFLOAT)
219                     .offset(8),
220                 vk::VertexInputAttributeDescription::default()
221                     .location(2)
222                     .binding(0)
223                     .format(vk::Format::R32G32B32A32_SFLOAT)
224                     .offset(16),
225                 vk::VertexInputAttributeDescription::default()
226                     .location(3)
227                     .binding(0)
228                     .format(vk::Format::R32G32B32_SFLOAT)
229                     .offset(32),
230                 vk::VertexInputAttributeDescription::default()
231                     .location(4)
232                     .binding(0)
233                     .format(vk::Format::R32G32_SFLOAT)
234                     .offset(44),
235             ];
236             let vertex_input = vk::PipelineVertexInputStateCreateInfo::default()
237                 .vertex_binding_descriptions(&vertex_bindings)
238                 .vertex_attribute_descriptions(&vertex_attributes);
239             let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
240                 .topology(vk::PrimitiveTopology::TRIANGLE_LIST);
241             let viewport_state = vk::PipelineViewportStateCreateInfo::default()
242                 .viewport_count(1)
243                 .scissor_count(1);
244             let rasterization = vk::PipelineRasterizationStateCreateInfo::default()
245                 .polygon_mode(vk::PolygonMode::FILL)
246                 .cull_mode(vk::CullModeFlags::NONE)
247                 .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
248                 .line_width(1.0);
249             let multisample = vk::PipelineMultisampleStateCreateInfo::default()
250                 .rasterization_samples(vk::SampleCountFlags::TYPE_1);
251             let blend_attachments = [vk::PipelineColorBlendAttachmentState::default()
252                 .blend_enable(true)
253                 .src_color_blend_factor(vk::BlendFactor::SRC_ALPHA)
254                 .dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
255                 .color_blend_op(vk::BlendOp::ADD)
256                 .src_alpha_blend_factor(vk::BlendFactor::ONE)
257                 .dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
258                 .alpha_blend_op(vk::BlendOp::ADD)
259                 .color_write_mask(vk::ColorComponentFlags::RGBA)];
260             let color_blend = vk::PipelineColorBlendStateCreateInfo::default()
261                 .attachments(&blend_attachments);
262             let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
263             let dynamic_state =
264                 vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
265             let pipeline = device
266                 .create_graphics_pipelines(
267                     vk::PipelineCache::null(),
268                     &[vk::GraphicsPipelineCreateInfo::default()
269                         .stages(&stages)
270                         .vertex_input_state(&vertex_input)
271                         .input_assembly_state(&input_assembly)
272                         .viewport_state(&viewport_state)
273                         .rasterization_state(&rasterization)
274                         .multisample_state(&multisample)
275                         .color_blend_state(&color_blend)
276                         .dynamic_state(&dynamic_state)
277                         .layout(pipeline_layout)
278                         .render_pass(render_pass)
279                         .subpass(0)],
280                     None,
281                 )
282                 .expect("Failed to create glyph pipeline")[0];
283 
284             let atlas_image = device
285                 .create_image(
286                     &vk::ImageCreateInfo::default()
287                         .image_type(vk::ImageType::TYPE_2D)
288                         .format(vk::Format::R8G8B8A8_UNORM)
289                         .extent(vk::Extent3D { width: ATLAS_SIZE, height: ATLAS_SIZE, depth: 1 })
290                         .mip_levels(1)
291                         .array_layers(1)
292                         .samples(vk::SampleCountFlags::TYPE_1)
293                         .tiling(vk::ImageTiling::OPTIMAL)
294                         .usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST)
295                         .initial_layout(vk::ImageLayout::UNDEFINED),
296                     None,
297                 )
298                 .expect("Failed to create atlas image");
299             let requirements = device.get_image_memory_requirements(atlas_image);
300             let atlas_allocation = allocator
301                 .allocate(&AllocationCreateDesc {
302                     name: "glyph-atlas",
303                     requirements,
304                     location: MemoryLocation::GpuOnly,
305                     linear: false,
306                     allocation_scheme: AllocationScheme::GpuAllocatorManaged,
307                 })
308                 .expect("Failed to allocate atlas memory");
309             device
310                 .bind_image_memory(atlas_image, atlas_allocation.memory(), atlas_allocation.offset())
311                 .expect("Failed to bind atlas memory");
312             let atlas_view = device
313                 .create_image_view(
314                     &vk::ImageViewCreateInfo::default()
315                         .image(atlas_image)
316                         .view_type(vk::ImageViewType::TYPE_2D)
317                         .format(vk::Format::R8G8B8A8_UNORM)
318                         .subresource_range(
319                             vk::ImageSubresourceRange::default()
320                                 .aspect_mask(vk::ImageAspectFlags::COLOR)
321                                 .level_count(1)
322                                 .layer_count(1),
323                         ),
324                     None,
325                 )
326                 .expect("Failed to create atlas view");
327 
328             // Glyphs are sampled 1:1; NEAREST keeps them crisp.
329             let sampler = device
330                 .create_sampler(
331                     &vk::SamplerCreateInfo::default()
332                         .mag_filter(vk::Filter::NEAREST)
333                         .min_filter(vk::Filter::NEAREST)
334                         .mipmap_mode(vk::SamplerMipmapMode::NEAREST)
335                         .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE)
336                         .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE)
337                         .address_mode_w(vk::SamplerAddressMode::CLAMP_TO_EDGE),
338                     None,
339                 )
340                 .expect("Failed to create atlas sampler");
341 
342             let pool_sizes = [
343                 vk::DescriptorPoolSize::default()
344                     .ty(vk::DescriptorType::SAMPLED_IMAGE)
345                     .descriptor_count(1),
346                 vk::DescriptorPoolSize::default()
347                     .ty(vk::DescriptorType::SAMPLER)
348                     .descriptor_count(1),
349             ];
350             let descriptor_pool = device
351                 .create_descriptor_pool(
352                     &vk::DescriptorPoolCreateInfo::default()
353                         .max_sets(1)
354                         .pool_sizes(&pool_sizes),
355                     None,
356                 )
357                 .expect("Failed to create text descriptor pool");
358             let descriptor_set = device
359                 .allocate_descriptor_sets(
360                     &vk::DescriptorSetAllocateInfo::default()
361                         .descriptor_pool(descriptor_pool)
362                         .set_layouts(&set_layouts),
363                 )
364                 .expect("Failed to allocate text descriptor set")[0];
365             let image_infos = [vk::DescriptorImageInfo::default()
366                 .image_view(atlas_view)
367                 .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
368             let sampler_infos = [vk::DescriptorImageInfo::default().sampler(sampler)];
369             device.update_descriptor_sets(
370                 &[
371                     vk::WriteDescriptorSet::default()
372                         .dst_set(descriptor_set)
373                         .dst_binding(0)
374                         .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
375                         .image_info(&image_infos),
376                     vk::WriteDescriptorSet::default()
377                         .dst_set(descriptor_set)
378                         .dst_binding(1)
379                         .descriptor_type(vk::DescriptorType::SAMPLER)
380                         .image_info(&sampler_infos),
381                 ],
382                 &[],
383             );
384 
385             let atlas_bytes = (ATLAS_SIZE * ATLAS_SIZE * 4) as vk::DeviceSize;
386             let frames = (0..frames_in_flight)
387                 .map(|_| TextFrame {
388                     vertex: create_cpu_buffer(
389                         device,
390                         allocator,
391                         64 * 1024,
392                         vk::BufferUsageFlags::VERTEX_BUFFER,
393                         "glyph-vertices",
394                     ),
395                     vertex_count: 0,
396                     staging: create_cpu_buffer(
397                         device,
398                         allocator,
399                         atlas_bytes,
400                         vk::BufferUsageFlags::TRANSFER_SRC,
401                         "atlas-staging",
402                     ),
403                     uploaded_generation: 0,
404                 })
405                 .collect();
406 
407             TextStage {
408                 pipeline,
409                 pipeline_layout,
410                 descriptor_set_layout,
411                 descriptor_pool,
412                 descriptor_set,
413                 shader_module,
414                 sampler,
415                 atlas_image,
416                 atlas_view,
417                 atlas_allocation: Some(atlas_allocation),
418                 atlas_cpu: vec![0u8; (ATLAS_SIZE * ATLAS_SIZE * 4) as usize],
419                 atlas_initialized: false,
420                 generation: 1,
421                 glyphs: HashMap::new(),
422                 shelf: Shelf::new(),
423                 pending_vertices: Vec::new(),
424                 frames,
425             }
426         }
427     }
428 
429     /// Rasterize (on miss) and cache one glyph. Returns None when the atlas is full.
430     fn ensure_glyph(
431         &mut self,
432         font_system: &mut FontSystem,
433         swash_cache: &mut SwashCache,
434         key: CacheKey,
435     ) -> Option<GlyphEntry> {
436         if let Some(entry) = self.glyphs.get(&key) {
437             return Some(*entry);
438         }
439         let image = swash_cache.get_image_uncached(font_system, key)?;
440         let w = image.placement.width;
441         let h = image.placement.height;
442         if w == 0 || h == 0 || image.data.is_empty() {
443             let entry = GlyphEntry {
444                 u: 0, v: 0, w: 0, h: 0, left: 0, top: 0, is_color: false, empty: true,
445             };
446             self.glyphs.insert(key, entry);
447             return Some(entry);
448         }
449         let (u, v) = self.shelf.insert(w, h)?;
450 
451         let is_color = !matches!(image.content, SwashContent::Mask);
452         for row in 0..h {
453             for col in 0..w {
454                 let dst = (((v + row) * ATLAS_SIZE + (u + col)) * 4) as usize;
455                 let texel = match image.content {
456                     SwashContent::Mask => {
457                         let a = image.data[(row * w + col) as usize];
458                         [255, 255, 255, a]
459                     }
460                     // Color and SubpixelMask rasters are RGBA.
461                     _ => {
462                         let src = ((row * w + col) * 4) as usize;
463                         [
464                             image.data[src],
465                             image.data[src + 1],
466                             image.data[src + 2],
467                             image.data[src + 3],
468                         ]
469                     }
470                 };
471                 self.atlas_cpu[dst..dst + 4].copy_from_slice(&texel);
472             }
473         }
474         self.generation += 1;
475 
476         let entry = GlyphEntry {
477             u,
478             v,
479             w,
480             h,
481             left: image.placement.left,
482             top: image.placement.top,
483             is_color,
484             empty: false,
485         };
486         self.glyphs.insert(key, entry);
487         Some(entry)
488     }
489 
490     /// Build this frame's glyph vertices. Positions/bounds in physical pixels,
491     /// NDC computed against `extent` (wgpu convention; the shader flips for Vulkan).
492     pub(crate) fn prepare(
493         &mut self,
494         font_system: &mut FontSystem,
495         swash_cache: &mut SwashCache,
496         spans: &[TextSpan<'_>],
497         extent: vk::Extent2D,
498     ) {
499         self.pending_vertices.clear();
500         if !self.try_prepare(font_system, swash_cache, spans, extent) {
501             // Atlas full: clear and repack with only the glyphs this frame needs.
502             log::info!("glyph atlas full — clearing and repacking");
503             self.glyphs.clear();
504             self.shelf = Shelf::new();
505             self.atlas_cpu.fill(0);
506             self.generation += 1;
507             self.pending_vertices.clear();
508             if !self.try_prepare(font_system, swash_cache, spans, extent) {
509                 log::error!("glyph atlas full even after repack; text truncated this frame");
510             }
511         }
512     }
513 
514     fn try_prepare(
515         &mut self,
516         font_system: &mut FontSystem,
517         swash_cache: &mut SwashCache,
518         spans: &[TextSpan<'_>],
519         extent: vk::Extent2D,
520     ) -> bool {
521         let sw = extent.width as f32;
522         let sh = extent.height as f32;
523         for span in spans {
524             for run in span.buffer.layout_runs() {
525                 let line_y = (run.line_y * span.scale).round() as i32;
526                 for glyph in run.glyphs.iter() {
527                     let physical = glyph.physical((span.left, span.top), span.scale);
528                     let Some(entry) =
529                         self.ensure_glyph(font_system, swash_cache, physical.cache_key)
530                     else {
531                         // Distinguish "atlas full" (retryable) from "unrasterizable"
532                         // (skip): a missing swash image caches as empty above, so a
533                         // None here means the shelf rejected it.
534                         if swash_cache
535                             .get_image_uncached(font_system, physical.cache_key)
536                             .is_some()
537                         {
538                             return false;
539                         }
540                         continue;
541                     };
542                     if entry.empty {
543                         continue;
544                     }
545 
546                     // glyphon's placement formula (kept verbatim), physical pixels.
547                     let mut x0 = (physical.x + entry.left) as f32;
548                     let mut y0 = (line_y + physical.y - entry.top) as f32;
549                     let mut x1 = x0 + entry.w as f32;
550                     let mut y1 = y0 + entry.h as f32;
551                     let mut u0 = entry.u as f32;
552                     let mut v0 = entry.v as f32;
553                     let mut u1 = u0 + entry.w as f32;
554                     let mut v1 = v0 + entry.h as f32;
555 
556                     // CPU clip to span bounds, shrinking UVs proportionally.
557                     if let Some([bl, bt, br, bb]) = span.bounds {
558                         let (bl, bt, br, bb) = (bl as f32, bt as f32, br as f32, bb as f32);
559                         if x0 >= br || x1 <= bl || y0 >= bb || y1 <= bt {
560                             continue;
561                         }
562                         if x0 < bl {
563                             u0 += bl - x0;
564                             x0 = bl;
565                         }
566                         if x1 > br {
567                             u1 -= x1 - br;
568                             x1 = br;
569                         }
570                         if y0 < bt {
571                             v0 += bt - y0;
572                             y0 = bt;
573                         }
574                         if y1 > bb {
575                             v1 -= y1 - bb;
576                             y1 = bb;
577                         }
578                     }
579 
580                     let color = if entry.is_color {
581                         [1.0, 1.0, 1.0, 1.0]
582                     } else if let Some(c) = glyph.color_opt {
583                         [
584                             c.r() as f32 / 255.0,
585                             c.g() as f32 / 255.0,
586                             c.b() as f32 / 255.0,
587                             c.a() as f32 / 255.0,
588                         ]
589                     } else {
590                         span.default_color
591                     };
592 
593                     // Corner positions, optionally rotated about the span's center
594                     // (physical px) before the NDC mapping.
595                     let corners = match span.rotation {
596                         None => [[x0, y0], [x1, y0], [x0, y1], [x1, y1]],
597                         Some((angle, cx, cy)) => {
598                             let (sin_a, cos_a) = angle.sin_cos();
599                             let rot = |px: f32, py: f32| {
600                                 let (dx, dy) = (px - cx, py - cy);
601                                 [cx + dx * cos_a - dy * sin_a, cy + dx * sin_a + dy * cos_a]
602                             };
603                             [rot(x0, y0), rot(x1, y0), rot(x0, y1), rot(x1, y1)]
604                         }
605                     };
606                     let ndc = |p: [f32; 2]| {
607                         [(p[0] / sw) * 2.0 - 1.0, 1.0 - (p[1] / sh) * 2.0]
608                     };
609                     let uv = |u: f32, v: f32| [u / ATLAS_SIZE as f32, v / ATLAS_SIZE as f32];
610                     let clip_circle = span.clip_circle;
611                     let clip_extents = span.clip_extents;
612                     let tl = GlyphVertex { position: ndc(corners[0]), uv: uv(u0, v0), color, clip_circle, clip_extents };
613                     let tr = GlyphVertex { position: ndc(corners[1]), uv: uv(u1, v0), color, clip_circle, clip_extents };
614                     let bl = GlyphVertex { position: ndc(corners[2]), uv: uv(u0, v1), color, clip_circle, clip_extents };
615                     let br = GlyphVertex { position: ndc(corners[3]), uv: uv(u1, v1), color, clip_circle, clip_extents };
616                     self.pending_vertices.extend([tl, tr, bl, tr, br, bl]);
617                 }
618             }
619         }
620         true
621     }
622 
623     /// Called after this frame's fence has been waited: copy the current text
624     /// vertices into the frame's buffer and refresh its staging copy if the
625     /// atlas changed. `pending_vertices` is RETAINED — it is the staged text
626     /// state, replaced only by the next `prepare` — so frames rendered without
627     /// a re-prepare (progressive RT refinement, animation ticks) keep their
628     /// text instead of alternating to an empty buffer.
629     pub(crate) fn write_frame_buffers(
630         &mut self,
631         device: &ash::Device,
632         allocator: &mut Allocator,
633         frame_index: usize,
634     ) {
635         let frame = &mut self.frames[frame_index];
636 
637         let bytes: &[u8] = bytemuck::cast_slice(&self.pending_vertices);
638         let needed = bytes.len() as vk::DeviceSize;
639         if needed > frame.vertex.size {
640             let mut old = std::mem::replace(&mut frame.vertex, AllocatedBuffer::null());
641             destroy_cpu_buffer(device, allocator, &mut old);
642             frame.vertex = create_cpu_buffer(
643                 device,
644                 allocator,
645                 needed.next_power_of_two(),
646                 vk::BufferUsageFlags::VERTEX_BUFFER,
647                 "glyph-vertices",
648             );
649         }
650         if !bytes.is_empty() {
651             frame.vertex.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()
652                 [..bytes.len()]
653                 .copy_from_slice(bytes);
654         }
655         frame.vertex_count = self.pending_vertices.len() as u32;
656 
657         let frame = &mut self.frames[frame_index];
658         if frame.uploaded_generation != self.generation {
659             frame.staging.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()
660                 [..self.atlas_cpu.len()]
661                 .copy_from_slice(&self.atlas_cpu);
662         }
663     }
664 
665     /// Record the atlas upload (if this frame's staging is newer than the image).
666     /// Must be called outside a render pass.
667     pub(crate) fn record_upload(&mut self, device: &ash::Device, cmd: vk::CommandBuffer, frame_index: usize) {
668         let frame = &mut self.frames[frame_index];
669         if frame.uploaded_generation == self.generation {
670             return;
671         }
672         frame.uploaded_generation = self.generation;
673 
674         let range = vk::ImageSubresourceRange::default()
675             .aspect_mask(vk::ImageAspectFlags::COLOR)
676             .level_count(1)
677             .layer_count(1);
678         let (old_layout, src_stage, src_access) = if self.atlas_initialized {
679             (
680                 vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
681                 vk::PipelineStageFlags::FRAGMENT_SHADER,
682                 vk::AccessFlags::SHADER_READ,
683             )
684         } else {
685             (
686                 vk::ImageLayout::UNDEFINED,
687                 vk::PipelineStageFlags::TOP_OF_PIPE,
688                 vk::AccessFlags::empty(),
689             )
690         };
691         self.atlas_initialized = true;
692 
693         unsafe {
694             device.cmd_pipeline_barrier(
695                 cmd,
696                 src_stage,
697                 vk::PipelineStageFlags::TRANSFER,
698                 vk::DependencyFlags::empty(),
699                 &[],
700                 &[],
701                 &[vk::ImageMemoryBarrier::default()
702                     .src_access_mask(src_access)
703                     .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
704                     .old_layout(old_layout)
705                     .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
706                     .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
707                     .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
708                     .image(self.atlas_image)
709                     .subresource_range(range)],
710             );
711             device.cmd_copy_buffer_to_image(
712                 cmd,
713                 frame.staging.buffer,
714                 self.atlas_image,
715                 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
716                 &[vk::BufferImageCopy::default()
717                     .buffer_offset(0)
718                     .buffer_row_length(ATLAS_SIZE)
719                     .buffer_image_height(ATLAS_SIZE)
720                     .image_subresource(
721                         vk::ImageSubresourceLayers::default()
722                             .aspect_mask(vk::ImageAspectFlags::COLOR)
723                             .layer_count(1),
724                     )
725                     .image_extent(vk::Extent3D {
726                         width: ATLAS_SIZE,
727                         height: ATLAS_SIZE,
728                         depth: 1,
729                     })],
730             );
731             device.cmd_pipeline_barrier(
732                 cmd,
733                 vk::PipelineStageFlags::TRANSFER,
734                 vk::PipelineStageFlags::FRAGMENT_SHADER,
735                 vk::DependencyFlags::empty(),
736                 &[],
737                 &[],
738                 &[vk::ImageMemoryBarrier::default()
739                     .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
740                     .dst_access_mask(vk::AccessFlags::SHADER_READ)
741                     .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
742                     .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
743                     .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
744                     .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
745                     .image(self.atlas_image)
746                     .subresource_range(range)],
747             );
748         }
749     }
750 
751     /// Record the glyph draw. Must be called inside the render pass, after the
752     /// 2D quads (text goes on top). Viewport/scissor are inherited (dynamic,
753     /// already set by the caller).
754     pub(crate) fn record_draw(&self, device: &ash::Device, cmd: vk::CommandBuffer, frame_index: usize) {
755         let frame = &self.frames[frame_index];
756         if frame.vertex_count == 0 || !self.atlas_initialized {
757             return;
758         }
759         unsafe {
760             device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
761             device.cmd_bind_descriptor_sets(
762                 cmd,
763                 vk::PipelineBindPoint::GRAPHICS,
764                 self.pipeline_layout,
765                 0,
766                 &[self.descriptor_set],
767                 &[],
768             );
769             device.cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
770             device.cmd_draw(cmd, frame.vertex_count, 1, 0, 0);
771         }
772     }
773 
774     pub(crate) fn destroy(&mut self, device: &ash::Device, allocator: &mut Allocator) {
775         unsafe {
776             for frame in &mut self.frames {
777                 let mut vertex = std::mem::replace(&mut frame.vertex, AllocatedBuffer::null());
778                 destroy_cpu_buffer(device, allocator, &mut vertex);
779                 let mut staging = std::mem::replace(&mut frame.staging, AllocatedBuffer::null());
780                 destroy_cpu_buffer(device, allocator, &mut staging);
781             }
782             device.destroy_sampler(self.sampler, None);
783             device.destroy_image_view(self.atlas_view, None);
784             device.destroy_image(self.atlas_image, None);
785             if let Some(allocation) = self.atlas_allocation.take() {
786                 let _ = allocator.free(allocation);
787             }
788             device.destroy_descriptor_pool(self.descriptor_pool, None);
789             device.destroy_descriptor_set_layout(self.descriptor_set_layout, None);
790             device.destroy_pipeline(self.pipeline, None);
791             device.destroy_pipeline_layout(self.pipeline_layout, None);
792             device.destroy_shader_module(self.shader_module, None);
793         }
794     }
795 }