GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/vk/text.rs (25.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
18 use ash::vk;
19 use gpu_allocator::vulkan::{
20 Allocation, AllocationCreateDesc, AllocationScheme, Allocator,
21 };
22 use gpu_allocator::MemoryLocation;
23
24
25 use cosmic_text::{FontSystem, SwashCache};
26
27 use super::renderer::{create_cpu_buffer, destroy_cpu_buffer, AllocatedBuffer};
28 pub use crate::draw::TextSpan;
29 use crate::draw::glyphs::{AtlasChange, GlyphAtlas, GlyphVertex, ATLAS_SIZE};
30
31 struct TextFrame {
32 vertex: AllocatedBuffer,
33 vertex_count: u32,
34 /// Holds what this frame copies into the atlas image: one region's rows
35 /// packed tight, or the whole atlas after a repack. Grown on demand —
36 /// a full-size buffer per frame in flight was 8 MiB of mapped memory
37 /// per window, held whether or not a glyph ever changed again.
38 staging: AllocatedBuffer,
39 /// What this frame's staging holds for the image and the atlas
40 /// generation it brings the image to, recorded by `write_frame_buffers`
41 /// and copied by `record_upload` (which is when the image counts as
42 /// holding it).
43 pending: Option<(AtlasChange, u64)>,
44 }
45
46 /// Staging a frame starts with: room for a run of new glyphs.
47 const STAGING_START: vk::DeviceSize = 64 * 1024;
48
49 pub(crate) struct TextStage {
50 pipeline: vk::Pipeline,
51 pipeline_layout: vk::PipelineLayout,
52 descriptor_set_layout: vk::DescriptorSetLayout,
53 descriptor_pool: vk::DescriptorPool,
54 descriptor_set: vk::DescriptorSet,
55 shader_module: vk::ShaderModule,
56 sampler: vk::Sampler,
57
58 atlas_image: vk::Image,
59 atlas_view: vk::ImageView,
60 atlas_allocation: Option<Allocation>,
61 /// The atlas and glyph quads (CPU side, shared with every renderer).
62 atlas: GlyphAtlas,
63 atlas_initialized: bool,
64 /// The atlas generation the GPU image holds (copied, or about to be by
65 /// a recorded frame). One image serves every frame in flight, so this
66 /// is one number, not one per frame. 1 is the cleared atlas, which the
67 /// image is cleared to on first use rather than uploaded.
68 image_generation: u64,
69 frames: Vec<TextFrame>,
70 }
71
72 impl TextStage {
73 pub(crate) fn new(
74 device: &ash::Device,
75 allocator: &mut Allocator,
76 render_pass: vk::RenderPass,
77 frames_in_flight: usize,
78 ) -> Self {
79 unsafe {
80 let bindings = [
81 vk::DescriptorSetLayoutBinding::default()
82 .binding(0)
83 .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
84 .descriptor_count(1)
85 .stage_flags(vk::ShaderStageFlags::FRAGMENT),
86 vk::DescriptorSetLayoutBinding::default()
87 .binding(1)
88 .descriptor_type(vk::DescriptorType::SAMPLER)
89 .descriptor_count(1)
90 .stage_flags(vk::ShaderStageFlags::FRAGMENT),
91 ];
92 let descriptor_set_layout = device
93 .create_descriptor_set_layout(
94 &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
95 None,
96 )
97 .expect("Failed to create text descriptor set layout");
98 let set_layouts = [descriptor_set_layout];
99 let pipeline_layout = device
100 .create_pipeline_layout(
101 &vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts),
102 None,
103 )
104 .expect("Failed to create text pipeline layout");
105
106 let spirv = super::renderer::glyph_spirv();
107 let shader_module = device
108 .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(spirv), None)
109 .expect("Failed to create glyph shader module");
110
111 let stages = [
112 vk::PipelineShaderStageCreateInfo::default()
113 .stage(vk::ShaderStageFlags::VERTEX)
114 .module(shader_module)
115 .name(c"vs_main"),
116 vk::PipelineShaderStageCreateInfo::default()
117 .stage(vk::ShaderStageFlags::FRAGMENT)
118 .module(shader_module)
119 .name(c"fs_main"),
120 ];
121 let vertex_bindings = [vk::VertexInputBindingDescription::default()
122 .binding(0)
123 .stride(std::mem::size_of::<GlyphVertex>() as u32)
124 .input_rate(vk::VertexInputRate::VERTEX)];
125 let vertex_attributes = [
126 vk::VertexInputAttributeDescription::default()
127 .location(0)
128 .binding(0)
129 .format(vk::Format::R32G32_SFLOAT)
130 .offset(0),
131 vk::VertexInputAttributeDescription::default()
132 .location(1)
133 .binding(0)
134 .format(vk::Format::R32G32_SFLOAT)
135 .offset(8),
136 vk::VertexInputAttributeDescription::default()
137 .location(2)
138 .binding(0)
139 .format(vk::Format::R32G32B32A32_SFLOAT)
140 .offset(16),
141 vk::VertexInputAttributeDescription::default()
142 .location(3)
143 .binding(0)
144 .format(vk::Format::R32G32B32_SFLOAT)
145 .offset(32),
146 vk::VertexInputAttributeDescription::default()
147 .location(4)
148 .binding(0)
149 .format(vk::Format::R32G32_SFLOAT)
150 .offset(44),
151 ];
152 let vertex_input = vk::PipelineVertexInputStateCreateInfo::default()
153 .vertex_binding_descriptions(&vertex_bindings)
154 .vertex_attribute_descriptions(&vertex_attributes);
155 let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
156 .topology(vk::PrimitiveTopology::TRIANGLE_LIST);
157 let viewport_state = vk::PipelineViewportStateCreateInfo::default()
158 .viewport_count(1)
159 .scissor_count(1);
160 let rasterization = vk::PipelineRasterizationStateCreateInfo::default()
161 .polygon_mode(vk::PolygonMode::FILL)
162 .cull_mode(vk::CullModeFlags::NONE)
163 .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
164 .line_width(1.0);
165 let multisample = vk::PipelineMultisampleStateCreateInfo::default()
166 .rasterization_samples(vk::SampleCountFlags::TYPE_1);
167 let blend_attachments = [vk::PipelineColorBlendAttachmentState::default()
168 .blend_enable(true)
169 .src_color_blend_factor(vk::BlendFactor::SRC_ALPHA)
170 .dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
171 .color_blend_op(vk::BlendOp::ADD)
172 .src_alpha_blend_factor(vk::BlendFactor::ONE)
173 .dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
174 .alpha_blend_op(vk::BlendOp::ADD)
175 .color_write_mask(vk::ColorComponentFlags::RGBA)];
176 let color_blend = vk::PipelineColorBlendStateCreateInfo::default()
177 .attachments(&blend_attachments);
178 let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
179 let dynamic_state =
180 vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
181 let pipeline = device
182 .create_graphics_pipelines(
183 vk::PipelineCache::null(),
184 &[vk::GraphicsPipelineCreateInfo::default()
185 .stages(&stages)
186 .vertex_input_state(&vertex_input)
187 .input_assembly_state(&input_assembly)
188 .viewport_state(&viewport_state)
189 .rasterization_state(&rasterization)
190 .multisample_state(&multisample)
191 .color_blend_state(&color_blend)
192 .dynamic_state(&dynamic_state)
193 .layout(pipeline_layout)
194 .render_pass(render_pass)
195 .subpass(0)],
196 None,
197 )
198 .expect("Failed to create glyph pipeline")[0];
199
200 let atlas_image = device
201 .create_image(
202 &vk::ImageCreateInfo::default()
203 .image_type(vk::ImageType::TYPE_2D)
204 .format(vk::Format::R8G8B8A8_UNORM)
205 .extent(vk::Extent3D { width: ATLAS_SIZE, height: ATLAS_SIZE, depth: 1 })
206 .mip_levels(1)
207 .array_layers(1)
208 .samples(vk::SampleCountFlags::TYPE_1)
209 .tiling(vk::ImageTiling::OPTIMAL)
210 .usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST)
211 .initial_layout(vk::ImageLayout::UNDEFINED),
212 None,
213 )
214 .expect("Failed to create atlas image");
215 let requirements = device.get_image_memory_requirements(atlas_image);
216 let atlas_allocation = allocator
217 .allocate(&AllocationCreateDesc {
218 name: "glyph-atlas",
219 requirements,
220 location: MemoryLocation::GpuOnly,
221 linear: false,
222 allocation_scheme: AllocationScheme::GpuAllocatorManaged,
223 })
224 .expect("Failed to allocate atlas memory");
225 device
226 .bind_image_memory(atlas_image, atlas_allocation.memory(), atlas_allocation.offset())
227 .expect("Failed to bind atlas memory");
228 let atlas_view = device
229 .create_image_view(
230 &vk::ImageViewCreateInfo::default()
231 .image(atlas_image)
232 .view_type(vk::ImageViewType::TYPE_2D)
233 .format(vk::Format::R8G8B8A8_UNORM)
234 .subresource_range(
235 vk::ImageSubresourceRange::default()
236 .aspect_mask(vk::ImageAspectFlags::COLOR)
237 .level_count(1)
238 .layer_count(1),
239 ),
240 None,
241 )
242 .expect("Failed to create atlas view");
243
244 // Glyphs are sampled 1:1; NEAREST keeps them crisp.
245 let sampler = device
246 .create_sampler(
247 &vk::SamplerCreateInfo::default()
248 .mag_filter(vk::Filter::NEAREST)
249 .min_filter(vk::Filter::NEAREST)
250 .mipmap_mode(vk::SamplerMipmapMode::NEAREST)
251 .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE)
252 .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE)
253 .address_mode_w(vk::SamplerAddressMode::CLAMP_TO_EDGE),
254 None,
255 )
256 .expect("Failed to create atlas sampler");
257
258 let pool_sizes = [
259 vk::DescriptorPoolSize::default()
260 .ty(vk::DescriptorType::SAMPLED_IMAGE)
261 .descriptor_count(1),
262 vk::DescriptorPoolSize::default()
263 .ty(vk::DescriptorType::SAMPLER)
264 .descriptor_count(1),
265 ];
266 let descriptor_pool = device
267 .create_descriptor_pool(
268 &vk::DescriptorPoolCreateInfo::default()
269 .max_sets(1)
270 .pool_sizes(&pool_sizes),
271 None,
272 )
273 .expect("Failed to create text descriptor pool");
274 let descriptor_set = device
275 .allocate_descriptor_sets(
276 &vk::DescriptorSetAllocateInfo::default()
277 .descriptor_pool(descriptor_pool)
278 .set_layouts(&set_layouts),
279 )
280 .expect("Failed to allocate text descriptor set")[0];
281 let image_infos = [vk::DescriptorImageInfo::default()
282 .image_view(atlas_view)
283 .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
284 let sampler_infos = [vk::DescriptorImageInfo::default().sampler(sampler)];
285 device.update_descriptor_sets(
286 &[
287 vk::WriteDescriptorSet::default()
288 .dst_set(descriptor_set)
289 .dst_binding(0)
290 .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
291 .image_info(&image_infos),
292 vk::WriteDescriptorSet::default()
293 .dst_set(descriptor_set)
294 .dst_binding(1)
295 .descriptor_type(vk::DescriptorType::SAMPLER)
296 .image_info(&sampler_infos),
297 ],
298 &[],
299 );
300
301 let frames = (0..frames_in_flight)
302 .map(|_| TextFrame {
303 vertex: create_cpu_buffer(
304 device,
305 allocator,
306 64 * 1024,
307 vk::BufferUsageFlags::VERTEX_BUFFER,
308 "glyph-vertices",
309 ),
310 vertex_count: 0,
311 staging: create_cpu_buffer(
312 device,
313 allocator,
314 STAGING_START,
315 vk::BufferUsageFlags::TRANSFER_SRC,
316 "atlas-staging",
317 ),
318 pending: None,
319 })
320 .collect();
321
322 TextStage {
323 pipeline,
324 pipeline_layout,
325 descriptor_set_layout,
326 descriptor_pool,
327 descriptor_set,
328 shader_module,
329 sampler,
330 atlas_image,
331 atlas_view,
332 atlas_allocation: Some(atlas_allocation),
333 atlas: GlyphAtlas::new(),
334 atlas_initialized: false,
335 image_generation: 1,
336 frames,
337 }
338 }
339 }
340
341 /// Build this frame's glyph quads against `extent` (see [`GlyphAtlas::prepare`]).
342 pub(crate) fn prepare(
343 &mut self,
344 font_system: &mut FontSystem,
345 swash_cache: &mut SwashCache,
346 spans: &[TextSpan<'_>],
347 extent: vk::Extent2D,
348 ) {
349 self.atlas.prepare(font_system, swash_cache, spans, extent.width, extent.height);
350 }
351
352 /// Called after this frame's fence has been waited: copy the current text
353 /// vertices into the frame's buffer and refresh its staging copy if the
354 /// atlas changed. `pending_vertices` is RETAINED — it is the staged text
355 /// state, replaced only by the next `prepare` — so frames rendered without
356 /// a re-prepare (progressive RT refinement, animation ticks) keep their
357 /// text instead of alternating to an empty buffer.
358 pub(crate) fn write_frame_buffers(
359 &mut self,
360 device: &ash::Device,
361 allocator: &mut Allocator,
362 frame_index: usize,
363 ) {
364 let frame = &mut self.frames[frame_index];
365
366 let bytes: &[u8] = bytemuck::cast_slice(self.atlas.vertices());
367 let needed = bytes.len() as vk::DeviceSize;
368 if needed > frame.vertex.size {
369 let mut old = std::mem::replace(&mut frame.vertex, AllocatedBuffer::null());
370 destroy_cpu_buffer(device, allocator, &mut old);
371 frame.vertex = create_cpu_buffer(
372 device,
373 allocator,
374 needed.next_power_of_two(),
375 vk::BufferUsageFlags::VERTEX_BUFFER,
376 "glyph-vertices",
377 );
378 }
379 if !bytes.is_empty() {
380 frame.vertex.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()
381 [..bytes.len()]
382 .copy_from_slice(bytes);
383 }
384 frame.vertex_count = self.atlas.vertices().len() as u32;
385
386 // Stage only what changed since the image was last brought up to
387 // date: the rows of one region, or everything after a repack. Until
388 // 2026-10-06 each frame in flight re-staged and re-copied the whole
389 // 4 MiB atlas for any new glyph.
390 let change = self.atlas.changes_since(self.image_generation);
391 let frame = &mut self.frames[frame_index];
392 frame.pending = None;
393 let (row_bytes, rows, x0, y0) = match change {
394 AtlasChange::None => return,
395 AtlasChange::Full => (ATLAS_SIZE * 4, ATLAS_SIZE, 0, 0),
396 AtlasChange::Region { x, y, w, h } => (w * 4, h, x, y),
397 };
398 let needed = (row_bytes * rows) as vk::DeviceSize;
399 if needed > frame.staging.size {
400 let mut old = std::mem::replace(&mut frame.staging, AllocatedBuffer::null());
401 destroy_cpu_buffer(device, allocator, &mut old);
402 frame.staging = create_cpu_buffer(
403 device,
404 allocator,
405 needed.next_power_of_two(),
406 vk::BufferUsageFlags::TRANSFER_SRC,
407 "atlas-staging",
408 );
409 }
410 let pixels = self.atlas.pixels();
411 let mapped = frame.staging.allocation.as_mut().unwrap().mapped_slice_mut().unwrap();
412 let stride = (ATLAS_SIZE * 4) as usize;
413 for r in 0..rows as usize {
414 let src = (y0 as usize + r) * stride + x0 as usize * 4;
415 let dst = r * row_bytes as usize;
416 mapped[dst..dst + row_bytes as usize].copy_from_slice(&pixels[src..src + row_bytes as usize]);
417 }
418 frame.pending = Some((change, self.atlas.generation()));
419 }
420
421 /// Record the atlas upload (if this frame's staging is newer than the image).
422 /// Must be called outside a render pass.
423 pub(crate) fn record_upload(&mut self, device: &ash::Device, cmd: vk::CommandBuffer, frame_index: usize) {
424 let frame = &mut self.frames[frame_index];
425 let pending = frame.pending.take();
426 if pending.is_none() && self.atlas_initialized {
427 return;
428 }
429 if let Some((_, generation)) = pending {
430 self.image_generation = generation;
431 }
432 let pending = pending.map(|(change, _)| change);
433
434 let range = vk::ImageSubresourceRange::default()
435 .aspect_mask(vk::ImageAspectFlags::COLOR)
436 .level_count(1)
437 .layer_count(1);
438 let (old_layout, src_stage, src_access) = if self.atlas_initialized {
439 (
440 vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
441 vk::PipelineStageFlags::FRAGMENT_SHADER,
442 vk::AccessFlags::SHADER_READ,
443 )
444 } else {
445 (
446 vk::ImageLayout::UNDEFINED,
447 vk::PipelineStageFlags::TOP_OF_PIPE,
448 vk::AccessFlags::empty(),
449 )
450 };
451 self.atlas_initialized = true;
452
453 unsafe {
454 device.cmd_pipeline_barrier(
455 cmd,
456 src_stage,
457 vk::PipelineStageFlags::TRANSFER,
458 vk::DependencyFlags::empty(),
459 &[],
460 &[],
461 &[vk::ImageMemoryBarrier::default()
462 .src_access_mask(src_access)
463 .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
464 .old_layout(old_layout)
465 .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
466 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
467 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
468 .image(self.atlas_image)
469 .subresource_range(range)],
470 );
471 // A fresh image is cleared to the empty atlas rather than
472 // uploaded from it (`image_generation` starts at the cleared one).
473 if old_layout == vk::ImageLayout::UNDEFINED {
474 device.cmd_clear_color_image(
475 cmd,
476 self.atlas_image,
477 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
478 &vk::ClearColorValue { float32: [0.0; 4] },
479 &[range],
480 );
481 }
482 let region = match pending {
483 Some(AtlasChange::Full) => Some((0, 0, ATLAS_SIZE, ATLAS_SIZE)),
484 Some(AtlasChange::Region { x, y, w, h }) => Some((x, y, w, h)),
485 Some(AtlasChange::None) | None => None,
486 };
487 if let Some((x, y, w, h)) = region {
488 device.cmd_copy_buffer_to_image(
489 cmd,
490 frame.staging.buffer,
491 self.atlas_image,
492 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
493 &[vk::BufferImageCopy::default()
494 .buffer_offset(0)
495 .buffer_row_length(w)
496 .buffer_image_height(h)
497 .image_subresource(
498 vk::ImageSubresourceLayers::default()
499 .aspect_mask(vk::ImageAspectFlags::COLOR)
500 .layer_count(1),
501 )
502 .image_offset(vk::Offset3D { x: x as i32, y: y as i32, z: 0 })
503 .image_extent(vk::Extent3D { width: w, height: h, depth: 1 })],
504 );
505 }
506 device.cmd_pipeline_barrier(
507 cmd,
508 vk::PipelineStageFlags::TRANSFER,
509 vk::PipelineStageFlags::FRAGMENT_SHADER,
510 vk::DependencyFlags::empty(),
511 &[],
512 &[],
513 &[vk::ImageMemoryBarrier::default()
514 .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
515 .dst_access_mask(vk::AccessFlags::SHADER_READ)
516 .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
517 .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
518 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
519 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
520 .image(self.atlas_image)
521 .subresource_range(range)],
522 );
523 }
524 }
525
526 /// Record the glyph draw. Must be called inside the render pass, after the
527 /// 2D quads (text goes on top). Viewport/scissor are inherited (dynamic,
528 /// already set by the caller).
529 pub(crate) fn record_draw(&self, device: &ash::Device, cmd: vk::CommandBuffer, frame_index: usize) {
530 let frame = &self.frames[frame_index];
531 if frame.vertex_count == 0 || !self.atlas_initialized {
532 return;
533 }
534 unsafe {
535 device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
536 device.cmd_bind_descriptor_sets(
537 cmd,
538 vk::PipelineBindPoint::GRAPHICS,
539 self.pipeline_layout,
540 0,
541 &[self.descriptor_set],
542 &[],
543 );
544 device.cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
545 device.cmd_draw(cmd, frame.vertex_count, 1, 0, 0);
546 }
547 }
548
549 pub(crate) fn destroy(&mut self, device: &ash::Device, allocator: &mut Allocator) {
550 unsafe {
551 for frame in &mut self.frames {
552 let mut vertex = std::mem::replace(&mut frame.vertex, AllocatedBuffer::null());
553 destroy_cpu_buffer(device, allocator, &mut vertex);
554 let mut staging = std::mem::replace(&mut frame.staging, AllocatedBuffer::null());
555 destroy_cpu_buffer(device, allocator, &mut staging);
556 }
557 device.destroy_sampler(self.sampler, None);
558 device.destroy_image_view(self.atlas_view, None);
559 device.destroy_image(self.atlas_image, None);
560 if let Some(allocation) = self.atlas_allocation.take() {
561 let _ = allocator.free(allocation);
562 }
563 device.destroy_descriptor_pool(self.descriptor_pool, None);
564 device.destroy_descriptor_set_layout(self.descriptor_set_layout, None);
565 device.destroy_pipeline(self.pipeline, None);
566 device.destroy_pipeline_layout(self.pipeline_layout, None);
567 device.destroy_shader_module(self.shader_module, None);
568 }
569 }
570 }