1use crate::core::alignment;
2use crate::core::text::Alignment;
3use crate::core::{Rectangle, Size, Transformation};
4use crate::graphics::cache;
5use crate::graphics::color;
6use crate::graphics::text::cache::{self as text_cache, Cache as BufferCache};
7use crate::graphics::text::{Editor, Paragraph, font_system, to_color};
8
9use rustc_hash::FxHashMap;
10use std::collections::hash_map;
11use std::sync::atomic::{self, AtomicU64};
12use std::sync::{self, Arc, RwLock};
13
14pub use crate::graphics::Text;
15
16const COLOR_MODE: cryoglyph::ColorMode = if color::GAMMA_CORRECTION {
17 cryoglyph::ColorMode::Accurate
18} else {
19 cryoglyph::ColorMode::Web
20};
21
22pub type Batch = Vec<Item>;
23
24#[derive(Debug)]
25pub enum Item {
26 Group {
27 transformation: Transformation,
28 text: Vec<Text>,
29 },
30 Cached {
31 transformation: Transformation,
32 cache: Cache,
33 },
34}
35
36#[derive(Debug, Clone)]
37pub struct Cache {
38 id: Id,
39 group: cache::Group,
40 text: Arc<[Text]>,
41 version: usize,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45pub struct Id(u64);
46
47impl Cache {
48 pub fn new(group: cache::Group, text: Vec<Text>) -> Option<Self> {
49 static NEXT_ID: AtomicU64 = AtomicU64::new(0);
50
51 if text.is_empty() {
52 return None;
53 }
54
55 Some(Self {
56 id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)),
57 group,
58 text: Arc::from(text),
59 version: 0,
60 })
61 }
62
63 pub fn update(&mut self, text: Vec<Text>) {
64 if self.text.is_empty() && text.is_empty() {
65 return;
66 }
67
68 self.text = Arc::from(text);
69 self.version += 1;
70 }
71}
72
73struct Upload {
74 renderer: cryoglyph::TextRenderer,
75 buffer_cache: BufferCache,
76 transformation: Transformation,
77 version: usize,
78 group_version: usize,
79 text: sync::Weak<[Text]>,
80 _atlas: sync::Weak<()>,
81}
82
83#[derive(Default)]
84pub struct Storage {
85 groups: FxHashMap<cache::Group, Group>,
86 uploads: FxHashMap<Id, Upload>,
87}
88
89struct Group {
90 atlas: cryoglyph::TextAtlas,
91 version: usize,
92 should_trim: bool,
93 handle: Arc<()>, }
95
96impl Storage {
97 fn get(&self, cache: &Cache) -> Option<(&cryoglyph::TextAtlas, &Upload)> {
98 if cache.text.is_empty() {
99 return None;
100 }
101
102 self.groups
103 .get(&cache.group)
104 .map(|group| &group.atlas)
105 .zip(self.uploads.get(&cache.id))
106 }
107
108 fn prepare(
109 &mut self,
110 device: &wgpu::Device,
111 queue: &wgpu::Queue,
112 viewport: &cryoglyph::Viewport,
113 encoder: &mut wgpu::CommandEncoder,
114 format: wgpu::TextureFormat,
115 state: &cryoglyph::Cache,
116 cache: &Cache,
117 new_transformation: Transformation,
118 bounds: Rectangle,
119 ) {
120 let group_count = self.groups.len();
121
122 let group = self.groups.entry(cache.group).or_insert_with(|| {
123 log::debug!(
124 "New text atlas: {:?} (total: {})",
125 cache.group,
126 group_count + 1
127 );
128
129 Group {
130 atlas: cryoglyph::TextAtlas::with_color_mode(
131 device, queue, state, format, COLOR_MODE,
132 ),
133 version: 0,
134 should_trim: false,
135 handle: Arc::new(()),
136 }
137 });
138
139 match self.uploads.entry(cache.id) {
140 hash_map::Entry::Occupied(entry) => {
141 let upload = entry.into_mut();
142
143 if upload.version != cache.version
144 || upload.group_version != group.version
145 || upload.transformation != new_transformation
146 {
147 if !cache.text.is_empty() {
148 let _ = prepare(
149 device,
150 queue,
151 viewport,
152 encoder,
153 &mut upload.renderer,
154 &mut group.atlas,
155 &mut upload.buffer_cache,
156 &cache.text,
157 bounds,
158 new_transformation,
159 );
160 }
161
162 group.should_trim =
164 group.should_trim || upload.version != cache.version;
165
166 upload.text = Arc::downgrade(&cache.text);
167 upload.version = cache.version;
168 upload.group_version = group.version;
169 upload.transformation = new_transformation;
170
171 upload.buffer_cache.trim();
172 }
173 }
174 hash_map::Entry::Vacant(entry) => {
175 let mut renderer = cryoglyph::TextRenderer::new(
176 &mut group.atlas,
177 device,
178 wgpu::MultisampleState::default(),
179 None,
180 );
181
182 let mut buffer_cache = BufferCache::new();
183
184 if !cache.text.is_empty() {
185 let _ = prepare(
186 device,
187 queue,
188 viewport,
189 encoder,
190 &mut renderer,
191 &mut group.atlas,
192 &mut buffer_cache,
193 &cache.text,
194 bounds,
195 new_transformation,
196 );
197 }
198
199 let _ = entry.insert(Upload {
200 renderer,
201 buffer_cache,
202 transformation: new_transformation,
203 version: 0,
204 group_version: group.version,
205 text: Arc::downgrade(&cache.text),
206 _atlas: Arc::downgrade(&group.handle),
207 });
208
209 group.should_trim = cache.group.is_singleton();
210
211 log::debug!(
212 "New text upload: {} (total: {})",
213 cache.id.0,
214 self.uploads.len()
215 );
216 }
217 }
218 }
219
220 pub fn trim(&mut self) {
221 self.uploads
222 .retain(|_id, upload| upload.text.strong_count() > 0);
223
224 self.groups.retain(|id, group| {
225 let active_uploads = Arc::weak_count(&group.handle);
226
227 if active_uploads == 0 {
228 log::debug!("Dropping text atlas: {id:?}");
229
230 return false;
231 }
232
233 if group.should_trim {
234 log::trace!("Trimming text atlas: {id:?}");
235
236 group.atlas.trim();
237 group.should_trim = false;
238
239 if !id.is_singleton() {
243 log::debug!(
244 "Invalidating text atlas: {id:?} \
245 (uploads: {active_uploads})"
246 );
247
248 group.version += 1;
249 }
250 }
251
252 true
253 });
254 }
255}
256
257pub struct Viewport(cryoglyph::Viewport);
258
259impl Viewport {
260 pub fn update(&mut self, queue: &wgpu::Queue, resolution: Size<u32>) {
261 self.0.update(
262 queue,
263 cryoglyph::Resolution {
264 width: resolution.width,
265 height: resolution.height,
266 },
267 );
268 }
269}
270
271#[derive(Clone)]
272#[allow(missing_debug_implementations)]
273pub struct Pipeline {
274 format: wgpu::TextureFormat,
275 cache: cryoglyph::Cache,
276 atlas: Arc<RwLock<cryoglyph::TextAtlas>>,
277}
278
279impl Pipeline {
280 pub fn new(
281 device: &wgpu::Device,
282 queue: &wgpu::Queue,
283 format: wgpu::TextureFormat,
284 ) -> Self {
285 let cache = cryoglyph::Cache::new(device);
286 let atlas = cryoglyph::TextAtlas::with_color_mode(
287 device, queue, &cache, format, COLOR_MODE,
288 );
289
290 Pipeline {
291 format,
292 cache,
293 atlas: Arc::new(RwLock::new(atlas)),
294 }
295 }
296
297 pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport {
298 Viewport(cryoglyph::Viewport::new(device, &self.cache))
299 }
300
301 pub fn trim(&self) {
302 self.atlas.write().expect("Write text atlas").trim();
303 }
304}
305
306#[derive(Default)]
307pub struct State {
308 renderers: Vec<cryoglyph::TextRenderer>,
309 prepare_layer: usize,
310 cache: BufferCache,
311 storage: Storage,
312}
313
314impl State {
315 pub fn new() -> Self {
316 Self::default()
317 }
318
319 pub fn prepare(
320 &mut self,
321 pipeline: &Pipeline,
322 device: &wgpu::Device,
323 queue: &wgpu::Queue,
324 viewport: &Viewport,
325 encoder: &mut wgpu::CommandEncoder,
326 batch: &Batch,
327 layer_bounds: Rectangle,
328 layer_transformation: Transformation,
329 ) {
330 let mut atlas = pipeline.atlas.write().expect("Write to text atlas");
331
332 for item in batch {
333 match item {
334 Item::Group {
335 transformation,
336 text,
337 } => {
338 if self.renderers.len() <= self.prepare_layer {
339 self.renderers.push(cryoglyph::TextRenderer::new(
340 &mut atlas,
341 device,
342 wgpu::MultisampleState::default(),
343 None,
344 ));
345 }
346
347 let renderer = &mut self.renderers[self.prepare_layer];
348 let result = prepare(
349 device,
350 queue,
351 &viewport.0,
352 encoder,
353 renderer,
354 &mut atlas,
355 &mut self.cache,
356 text,
357 layer_bounds * layer_transformation,
358 layer_transformation * *transformation,
359 );
360
361 match result {
362 Ok(()) => {
363 self.prepare_layer += 1;
364 }
365 Err(cryoglyph::PrepareError::AtlasFull) => {
366 }
370 }
371 }
372 Item::Cached {
373 transformation,
374 cache,
375 } => {
376 self.storage.prepare(
377 device,
378 queue,
379 &viewport.0,
380 encoder,
381 pipeline.format,
382 &pipeline.cache,
383 cache,
384 layer_transformation * *transformation,
385 layer_bounds * layer_transformation,
386 );
387 }
388 }
389 }
390 }
391
392 pub fn render<'a>(
393 &'a self,
394 pipeline: &'a Pipeline,
395 viewport: &'a Viewport,
396 start: usize,
397 batch: &'a Batch,
398 bounds: Rectangle<u32>,
399 render_pass: &mut wgpu::RenderPass<'a>,
400 ) -> usize {
401 let atlas = pipeline.atlas.read().expect("Read text atlas");
402 let mut layer_count = 0;
403
404 render_pass.set_scissor_rect(
405 bounds.x,
406 bounds.y,
407 bounds.width,
408 bounds.height,
409 );
410
411 for item in batch {
412 match item {
413 Item::Group { .. } => {
414 let renderer = &self.renderers[start + layer_count];
415
416 renderer
417 .render(&atlas, &viewport.0, render_pass)
418 .expect("Render text");
419
420 layer_count += 1;
421 }
422 Item::Cached { cache, .. } => {
423 if let Some((atlas, upload)) = self.storage.get(cache) {
424 upload
425 .renderer
426 .render(atlas, &viewport.0, render_pass)
427 .expect("Render cached text");
428 }
429 }
430 }
431 }
432
433 layer_count
434 }
435
436 pub fn trim(&mut self) {
437 self.cache.trim();
438 self.storage.trim();
439
440 self.prepare_layer = 0;
441 }
442}
443
444fn prepare(
445 device: &wgpu::Device,
446 queue: &wgpu::Queue,
447 viewport: &cryoglyph::Viewport,
448 encoder: &mut wgpu::CommandEncoder,
449 renderer: &mut cryoglyph::TextRenderer,
450 atlas: &mut cryoglyph::TextAtlas,
451 buffer_cache: &mut BufferCache,
452 sections: &[Text],
453 layer_bounds: Rectangle,
454 layer_transformation: Transformation,
455) -> Result<(), cryoglyph::PrepareError> {
456 let mut font_system = font_system().write().expect("Write font system");
457 let font_system = font_system.raw();
458
459 enum Allocation {
460 Paragraph(Paragraph),
461 Editor(Editor),
462 Cache(text_cache::KeyHash),
463 Raw(Arc<cryoglyph::Buffer>),
464 }
465
466 let allocations: Vec<_> = sections
467 .iter()
468 .map(|section| match section {
469 Text::Paragraph { paragraph, .. } => {
470 paragraph.upgrade().map(Allocation::Paragraph)
471 }
472 Text::Editor { editor, .. } => {
473 editor.upgrade().map(Allocation::Editor)
474 }
475 Text::Cached {
476 content,
477 bounds,
478 size,
479 line_height,
480 font,
481 shaping,
482 ..
483 } => {
484 let (key, _) = buffer_cache.allocate(
485 font_system,
486 text_cache::Key {
487 content,
488 size: f32::from(*size),
489 line_height: f32::from(*line_height),
490 font: *font,
491 bounds: Size {
492 width: bounds.width,
493 height: bounds.height,
494 },
495 shaping: *shaping,
496 },
497 );
498
499 Some(Allocation::Cache(key))
500 }
501 Text::Raw { raw, .. } => raw.buffer.upgrade().map(Allocation::Raw),
502 })
503 .collect();
504
505 let text_areas = sections.iter().zip(allocations.iter()).filter_map(
506 |(section, allocation)| {
507 let (
508 buffer,
509 bounds,
510 align_x,
511 align_y,
512 color,
513 clip_bounds,
514 transformation,
515 ) = match section {
516 Text::Paragraph {
517 position,
518 color,
519 clip_bounds,
520 transformation,
521 ..
522 } => {
523 use crate::core::text::Paragraph as _;
524
525 let Some(Allocation::Paragraph(paragraph)) = allocation
526 else {
527 return None;
528 };
529
530 (
531 paragraph.buffer(),
532 Rectangle::new(*position, paragraph.min_bounds()),
533 paragraph.align_x(),
534 paragraph.align_y(),
535 *color,
536 *clip_bounds,
537 *transformation,
538 )
539 }
540 Text::Editor {
541 position,
542 color,
543 clip_bounds,
544 transformation,
545 ..
546 } => {
547 use crate::core::text::Editor as _;
548
549 let Some(Allocation::Editor(editor)) = allocation else {
550 return None;
551 };
552
553 (
554 editor.buffer(),
555 Rectangle::new(*position, editor.bounds()),
556 Alignment::Default,
557 alignment::Vertical::Top,
558 *color,
559 *clip_bounds,
560 *transformation,
561 )
562 }
563 Text::Cached {
564 bounds,
565 align_x,
566 align_y,
567 color,
568 clip_bounds,
569 ..
570 } => {
571 let Some(Allocation::Cache(key)) = allocation else {
572 return None;
573 };
574
575 let entry =
576 buffer_cache.get(key).expect("Get cached buffer");
577
578 (
579 &entry.buffer,
580 Rectangle::new(bounds.position(), entry.min_bounds),
581 *align_x,
582 *align_y,
583 *color,
584 *clip_bounds,
585 Transformation::IDENTITY,
586 )
587 }
588 Text::Raw {
589 raw,
590 transformation,
591 } => {
592 let Some(Allocation::Raw(buffer)) = allocation else {
593 return None;
594 };
595
596 let (width, height) = buffer.size();
597
598 (
599 buffer.as_ref(),
600 Rectangle::new(
601 raw.position,
602 Size::new(
603 width.unwrap_or(layer_bounds.width),
604 height.unwrap_or(layer_bounds.height),
605 ),
606 ),
607 Alignment::Default,
608 alignment::Vertical::Top,
609 raw.color,
610 raw.clip_bounds,
611 *transformation,
612 )
613 }
614 };
615
616 let bounds = bounds * transformation * layer_transformation;
617
618 let left = match align_x {
619 Alignment::Default | Alignment::Left | Alignment::Justified => {
620 bounds.x
621 }
622 Alignment::Center => bounds.x - bounds.width / 2.0,
623 Alignment::Right => bounds.x - bounds.width,
624 };
625
626 let top = match align_y {
627 alignment::Vertical::Top => bounds.y,
628 alignment::Vertical::Center => bounds.y - bounds.height / 2.0,
629 alignment::Vertical::Bottom => bounds.y - bounds.height,
630 };
631
632 let clip_bounds = layer_bounds.intersection(
633 &(clip_bounds * transformation * layer_transformation),
634 )?;
635
636 Some(cryoglyph::TextArea {
637 buffer,
638 left,
639 top,
640 scale: transformation.scale_factor()
641 * layer_transformation.scale_factor(),
642 bounds: cryoglyph::TextBounds {
643 left: clip_bounds.x as i32,
644 top: clip_bounds.y as i32,
645 right: (clip_bounds.x + clip_bounds.width) as i32,
646 bottom: (clip_bounds.y + clip_bounds.height) as i32,
647 },
648 default_color: to_color(color),
649 })
650 },
651 );
652
653 renderer.prepare(
654 device,
655 queue,
656 encoder,
657 font_system,
658 atlas,
659 viewport,
660 text_areas,
661 &mut cryoglyph::SwashCache::new(),
662 )
663}