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 = group.should_trim || upload.version != cache.version;
164
165 upload.text = Arc::downgrade(&cache.text);
166 upload.version = cache.version;
167 upload.group_version = group.version;
168 upload.transformation = new_transformation;
169
170 upload.buffer_cache.trim();
171 }
172 }
173 hash_map::Entry::Vacant(entry) => {
174 let mut renderer = cryoglyph::TextRenderer::new(
175 &mut group.atlas,
176 device,
177 wgpu::MultisampleState::default(),
178 None,
179 );
180
181 let mut buffer_cache = BufferCache::new();
182
183 if !cache.text.is_empty() {
184 let _ = prepare(
185 device,
186 queue,
187 viewport,
188 encoder,
189 &mut renderer,
190 &mut group.atlas,
191 &mut buffer_cache,
192 &cache.text,
193 bounds,
194 new_transformation,
195 );
196 }
197
198 let _ = entry.insert(Upload {
199 renderer,
200 buffer_cache,
201 transformation: new_transformation,
202 version: 0,
203 group_version: group.version,
204 text: Arc::downgrade(&cache.text),
205 _atlas: Arc::downgrade(&group.handle),
206 });
207
208 group.should_trim = cache.group.is_singleton();
209
210 log::debug!(
211 "New text upload: {} (total: {})",
212 cache.id.0,
213 self.uploads.len()
214 );
215 }
216 }
217 }
218
219 pub fn trim(&mut self) {
220 self.uploads
221 .retain(|_id, upload| upload.text.strong_count() > 0);
222
223 self.groups.retain(|id, group| {
224 let active_uploads = Arc::weak_count(&group.handle);
225
226 if active_uploads == 0 {
227 log::debug!("Dropping text atlas: {id:?}");
228
229 return false;
230 }
231
232 if group.should_trim {
233 log::trace!("Trimming text atlas: {id:?}");
234
235 group.atlas.trim();
236 group.should_trim = false;
237
238 if !id.is_singleton() {
242 log::debug!(
243 "Invalidating text atlas: {id:?} \
244 (uploads: {active_uploads})"
245 );
246
247 group.version += 1;
248 }
249 }
250
251 true
252 });
253 }
254}
255
256pub struct Viewport(cryoglyph::Viewport);
257
258impl Viewport {
259 pub fn update(&mut self, queue: &wgpu::Queue, resolution: Size<u32>) {
260 self.0.update(
261 queue,
262 cryoglyph::Resolution {
263 width: resolution.width,
264 height: resolution.height,
265 },
266 );
267 }
268}
269
270#[derive(Clone)]
271pub struct Pipeline {
272 format: wgpu::TextureFormat,
273 cache: cryoglyph::Cache,
274 atlas: Arc<RwLock<cryoglyph::TextAtlas>>,
275}
276
277impl Pipeline {
278 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
279 let cache = cryoglyph::Cache::new(device);
280 let atlas =
281 cryoglyph::TextAtlas::with_color_mode(device, queue, &cache, format, COLOR_MODE);
282
283 Pipeline {
284 format,
285 cache,
286 atlas: Arc::new(RwLock::new(atlas)),
287 }
288 }
289
290 pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport {
291 Viewport(cryoglyph::Viewport::new(device, &self.cache))
292 }
293
294 pub fn trim(&self) {
295 self.atlas.write().expect("Write text atlas").trim();
296 }
297}
298
299#[derive(Default)]
300pub struct State {
301 renderers: Vec<cryoglyph::TextRenderer>,
302 prepare_layer: usize,
303 cache: BufferCache,
304 storage: Storage,
305}
306
307impl State {
308 pub fn new() -> Self {
309 Self::default()
310 }
311
312 pub fn prepare(
313 &mut self,
314 pipeline: &Pipeline,
315 device: &wgpu::Device,
316 queue: &wgpu::Queue,
317 viewport: &Viewport,
318 encoder: &mut wgpu::CommandEncoder,
319 batch: &Batch,
320 layer_bounds: Rectangle,
321 layer_transformation: Transformation,
322 ) {
323 let mut atlas = pipeline.atlas.write().expect("Write to text atlas");
324
325 for item in batch {
326 match item {
327 Item::Group {
328 transformation,
329 text,
330 } => {
331 if self.renderers.len() <= self.prepare_layer {
332 self.renderers.push(cryoglyph::TextRenderer::new(
333 &mut atlas,
334 device,
335 wgpu::MultisampleState::default(),
336 None,
337 ));
338 }
339
340 let renderer = &mut self.renderers[self.prepare_layer];
341 let result = prepare(
342 device,
343 queue,
344 &viewport.0,
345 encoder,
346 renderer,
347 &mut atlas,
348 &mut self.cache,
349 text,
350 layer_bounds * layer_transformation,
351 layer_transformation * *transformation,
352 );
353
354 match result {
355 Ok(()) => {
356 self.prepare_layer += 1;
357 }
358 Err(cryoglyph::PrepareError::AtlasFull) => {
359 }
363 }
364 }
365 Item::Cached {
366 transformation,
367 cache,
368 } => {
369 self.storage.prepare(
370 device,
371 queue,
372 &viewport.0,
373 encoder,
374 pipeline.format,
375 &pipeline.cache,
376 cache,
377 layer_transformation * *transformation,
378 layer_bounds * layer_transformation,
379 );
380 }
381 }
382 }
383 }
384
385 pub fn render<'a>(
386 &'a self,
387 pipeline: &'a Pipeline,
388 viewport: &'a Viewport,
389 start: usize,
390 batch: &'a Batch,
391 bounds: Rectangle<u32>,
392 render_pass: &mut wgpu::RenderPass<'a>,
393 ) -> usize {
394 let atlas = pipeline.atlas.read().expect("Read text atlas");
395 let mut layer_count = 0;
396
397 render_pass.set_scissor_rect(bounds.x, bounds.y, bounds.width, bounds.height);
398
399 for item in batch {
400 match item {
401 Item::Group { .. } => {
402 let renderer = &self.renderers[start + layer_count];
403
404 renderer
405 .render(&atlas, &viewport.0, render_pass)
406 .expect("Render text");
407
408 layer_count += 1;
409 }
410 Item::Cached { cache, .. } => {
411 if let Some((atlas, upload)) = self.storage.get(cache) {
412 upload
413 .renderer
414 .render(atlas, &viewport.0, render_pass)
415 .expect("Render cached text");
416 }
417 }
418 }
419 }
420
421 layer_count
422 }
423
424 pub fn trim(&mut self) {
425 self.cache.trim();
426 self.storage.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, .. } => paragraph.upgrade().map(Allocation::Paragraph),
458 Text::Editor { editor, .. } => editor.upgrade().map(Allocation::Editor),
459 Text::Cached {
460 content,
461 bounds,
462 size,
463 line_height,
464 font,
465 shaping,
466 align_x,
467 ..
468 } => {
469 let (key, _) = buffer_cache.allocate(
470 font_system,
471 text_cache::Key {
472 content,
473 size: f32::from(*size),
474 line_height: f32::from(*line_height),
475 font: *font,
476 align_x: *align_x,
477 bounds: Size {
478 width: bounds.width,
479 height: bounds.height,
480 },
481 shaping: *shaping,
482 },
483 );
484
485 Some(Allocation::Cache(key))
486 }
487 Text::Raw { raw, .. } => raw.buffer.upgrade().map(Allocation::Raw),
488 })
489 .collect();
490
491 let text_areas = sections
492 .iter()
493 .zip(allocations.iter())
494 .filter_map(|(section, allocation)| {
495 let (buffer, position, color, clip_bounds, transformation) = match section {
496 Text::Paragraph {
497 position,
498 color,
499 clip_bounds,
500 transformation,
501 ..
502 } => {
503 let Some(Allocation::Paragraph(paragraph)) = allocation else {
504 return None;
505 };
506
507 (
508 paragraph.buffer(),
509 *position,
510 *color,
511 *clip_bounds,
512 *transformation,
513 )
514 }
515 Text::Editor {
516 position,
517 color,
518 clip_bounds,
519 transformation,
520 ..
521 } => {
522 let Some(Allocation::Editor(editor)) = allocation else {
523 return None;
524 };
525
526 (
527 editor.buffer(),
528 *position,
529 *color,
530 *clip_bounds,
531 *transformation,
532 )
533 }
534 Text::Cached {
535 bounds,
536 align_x,
537 align_y,
538 color,
539 clip_bounds,
540 ..
541 } => {
542 let Some(Allocation::Cache(key)) = allocation else {
543 return None;
544 };
545
546 let entry = buffer_cache.get(key).expect("Get cached buffer");
547
548 let mut position = bounds.position();
549
550 position.x = match align_x {
551 Alignment::Default | Alignment::Left | Alignment::Justified => position.x,
552 Alignment::Center => position.x - entry.min_bounds.width / 2.0,
553 Alignment::Right => position.x - entry.min_bounds.width,
554 };
555
556 position.y = match align_y {
557 alignment::Vertical::Top => position.y,
558 alignment::Vertical::Center => position.y - entry.min_bounds.height / 2.0,
559 alignment::Vertical::Bottom => position.y - entry.min_bounds.height,
560 };
561
562 (
563 &entry.buffer,
564 position,
565 *color,
566 *clip_bounds,
567 Transformation::IDENTITY,
568 )
569 }
570 Text::Raw {
571 raw,
572 transformation,
573 } => {
574 let Some(Allocation::Raw(buffer)) = allocation else {
575 return None;
576 };
577
578 (
579 buffer.as_ref(),
580 raw.position,
581 raw.color,
582 raw.clip_bounds,
583 *transformation,
584 )
585 }
586 };
587
588 let position = position * transformation * layer_transformation;
589
590 let clip_bounds = layer_bounds
591 .intersection(&(clip_bounds * transformation * layer_transformation))?;
592
593 Some(cryoglyph::TextArea {
594 buffer,
595 left: position.x,
596 top: position.y,
597 scale: transformation.scale_factor() * layer_transformation.scale_factor(),
598 bounds: cryoglyph::TextBounds {
599 left: clip_bounds.x.round() as i32,
600 top: clip_bounds.y.round() as i32,
601 right: (clip_bounds.x + clip_bounds.width).round() as i32,
602 bottom: (clip_bounds.y + clip_bounds.height).round() as i32,
603 },
604 default_color: to_color(color),
605 })
606 });
607
608 renderer.prepare(
609 device,
610 queue,
611 encoder,
612 font_system,
613 atlas,
614 viewport,
615 text_areas,
616 &mut cryoglyph::SwashCache::new(),
617 )
618}