use std::cell::Cell;
use std::rc::Rc;
use glyphon::{
Cache, Color as GlyphonColor, ColorMode, FontSystem, Resolution, SwashCache, TextArea,
TextAtlas, TextBounds, TextRenderer, Viewport,
};
use rustc_hash::FxHashMap;
use wgpu::{Device, MultisampleState, Queue, TextureFormat};
use crate::render_stats::{self, Pipeline};
use crate::widgets::font::FontWeight;
use crate::widgets::{FontFamily, LineHeight, TextAlign};
const MAX_CACHED: usize = 32;
const MAX_SIDE: u32 = 2048;
pub struct MaskSpec<'a> {
pub text: &'a str,
pub font_size: f32,
pub font_family: FontFamily,
pub font_weight: FontWeight,
pub line_height: LineHeight,
pub letter_spacing: f32,
pub fit: Option<super::text_measurer::LineFit>,
pub align: TextAlign,
pub buffer_height: f32,
pub box_width: f32,
pub size: (u32, u32),
pub offset: (f32, f32),
pub density: f32,
}
#[derive(PartialEq, Eq, Hash)]
struct MaskKey {
text: String,
font_size_bits: u32,
weight: u16,
family: FontFamily,
line_height: (u8, u32),
letter_spacing_bits: u32,
align: TextAlign,
width: u32,
height: u32,
buffer_height: u32,
box_width_bits: u32,
offset: (i32, i32),
fit: Option<super::text_measurer::LineFitKey>,
}
struct CachedMask {
#[allow(dead_code)] texture: wgpu::Texture,
view: Rc<wgpu::TextureView>,
last_used: Cell<u64>,
}
pub(super) struct Shaper {
pub(super) font_system: FontSystem,
pub(super) swash_cache: SwashCache,
#[allow(dead_code)] cache: Cache,
pub(super) atlas: TextAtlas,
pub(super) text_renderer: TextRenderer,
pub(super) viewport: Viewport,
}
impl Shaper {
pub(super) fn new(device: &Device, queue: &Queue, format: TextureFormat) -> Self {
let mut font_system = FontSystem::new();
for data in crate::get_registered_fonts() {
font_system
.db_mut()
.load_font_source(glyphon::fontdb::Source::Binary(data));
}
let cache = Cache::new(device);
let mut atlas = TextAtlas::with_color_mode(device, queue, &cache, format, ColorMode::Web);
let text_renderer =
TextRenderer::new(&mut atlas, device, MultisampleState::default(), None);
let viewport = Viewport::new(device, &cache);
Self {
font_system,
swash_cache: SwashCache::new(),
cache,
atlas,
text_renderer,
viewport,
}
}
pub(super) fn draw(
&mut self,
device: &Device,
queue: &Queue,
area: glyphon::TextArea<'_>,
view: &wgpu::TextureView,
label: &str,
) -> bool {
if let Err(e) = self.text_renderer.prepare(
device,
queue,
&mut self.font_system,
&mut self.atlas,
&self.viewport,
[area],
&mut self.swash_cache,
) {
log::error!("{label} prepare failed: {e:?}");
return false;
}
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some(label) });
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some(label),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
match self
.text_renderer
.render(&self.atlas, &self.viewport, &mut pass)
{
Ok(()) => render_stats::record_draw_calls(Pipeline::Text, 1),
Err(e) => log::error!("{label} render failed: {e:?}"),
}
}
queue.submit(std::iter::once(encoder.finish()));
self.atlas.trim();
true
}
}
pub struct TextMaskRenderer {
shaper: Option<Shaper>,
format: TextureFormat,
masks: FxHashMap<MaskKey, Rc<CachedMask>>,
frame_gen: u64,
}
impl TextMaskRenderer {
pub fn new(format: TextureFormat) -> Self {
Self {
shaper: None,
format,
masks: FxHashMap::default(),
frame_gen: 0,
}
}
pub fn begin_frame(&mut self) {
self.frame_gen += 1;
if self.masks.len() > MAX_CACHED {
let current = self.frame_gen;
self.masks
.retain(|_, mask| mask.last_used.get() + 1 >= current);
}
}
pub fn mask(
&mut self,
device: &Device,
queue: &Queue,
spec: &MaskSpec<'_>,
) -> Option<Rc<wgpu::TextureView>> {
let (width, height) = spec.size;
if spec.text.is_empty() || width == 0 || height == 0 {
return None;
}
if width > MAX_SIDE || height > MAX_SIDE {
log::warn!(
"text backdrop blur skipped: mask would be {width}x{height}, over the {MAX_SIDE} limit"
);
return None;
}
let font_size = spec.font_size * spec.density;
let weight = if spec.font_weight == FontWeight::default() {
FontWeight::NORMAL
} else {
spec.font_weight
};
let key = MaskKey {
text: spec.text.to_owned(),
font_size_bits: font_size.to_bits(),
weight: weight.0,
family: spec.font_family,
line_height: spec.line_height.key(),
letter_spacing_bits: spec.letter_spacing.to_bits(),
align: spec.align,
width,
height,
buffer_height: spec.buffer_height as u32,
box_width_bits: spec.box_width.to_bits(),
offset: (
(spec.offset.0 * 4.0).round() as i32,
(spec.offset.1 * 4.0).round() as i32,
),
fit: spec.fit.map(|fit| fit.key()),
};
if let Some(cached) = self.masks.get(&key) {
cached.last_used.set(self.frame_gen);
return Some(Rc::clone(&cached.view));
}
let format = self.format;
let shaper = self
.shaper
.get_or_insert_with(|| Shaper::new(device, queue, format));
let buffer = super::text::shape(
&mut shaper.font_system,
spec.text,
font_size,
spec.font_family,
spec.font_weight,
spec.line_height,
spec.letter_spacing,
spec.align,
(Some(spec.box_width), Some(spec.buffer_height)),
spec.fit,
spec.density,
);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Text Mask"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&Default::default());
shaper.viewport.update(queue, Resolution { width, height });
let area = TextArea {
buffer: &buffer,
left: spec.offset.0 + super::text::paint_offset(&buffer, spec.box_width, spec.align),
top: spec.offset.1,
scale: 1.0,
bounds: TextBounds {
left: 0,
top: 0,
right: width as i32,
bottom: height as i32,
},
default_color: GlyphonColor::rgb(255, 255, 255),
custom_glyphs: &[],
};
if !shaper.draw(device, queue, area, &view, "Text Mask") {
return None;
}
let view = Rc::new(view);
self.masks.insert(
key,
Rc::new(CachedMask {
texture,
view: Rc::clone(&view),
last_used: Cell::new(self.frame_gen),
}),
);
Some(view)
}
}
#[cfg(test)]
mod the_glyph_atlas_holds_a_text_not_a_history {
use super::{MaskSpec, Shaper, TextMaskRenderer};
use crate::renderer::GpuContext;
use crate::renderer::text::the_glyph_atlas_holds_a_frame_not_a_history::{
holds_one_page, page,
};
use crate::transform::Transform;
fn atlas_after(gpu: &GpuContext, frames: &[String]) -> isize {
let before = gpu.texture_bytes();
let format = wgpu::TextureFormat::Rgba8UnormSrgb;
let mut renderer = TextMaskRenderer::new(format);
let mut shaper = Shaper::new(&gpu.device, &gpu.queue, format);
shaper.font_system = crate::renderer::text::vendored_font_system();
renderer.shaper = Some(shaper);
for text in frames {
renderer.begin_frame();
let entry = page(text, Transform::IDENTITY);
let spec = MaskSpec {
text: &entry.text,
font_size: entry.font_size,
font_family: entry.font_family,
font_weight: entry.font_weight,
line_height: entry.line_height,
letter_spacing: entry.letter_spacing,
fit: None,
align: entry.align,
buffer_height: entry.rect.height,
box_width: entry.rect.width,
size: (entry.rect.width as u32, entry.rect.height as u32),
offset: (0.0, 0.0),
density: 1.0,
};
assert!(
renderer.mask(&gpu.device, &gpu.queue, &spec).is_some(),
"a frame's mask was not made"
);
}
renderer.masks.clear();
gpu.texture_bytes() - before
}
#[test]
fn a_frosted_text_that_changes_every_frame_needs_no_more_atlas_than_one_frame() {
holds_one_page(atlas_after);
}
}