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 }