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