Implement growing logic for TextAtlas

The `TextAtlas` will have an initial size of `256` (we could make this
configurable) and `try_allocate` will keep track of the glyphs in use
in the current frame, returning `None` when the LRU glyph is in use.

In that case, `TextRenderer::prepare` will return
`PrepareError::AtlasFull` with the `ContentType` of the atlas that is
full. The user of the library can then call `TextAtlas::grow` with the
provided `ContentType` to obtain a bigger atlas (by `256`).

A `TextAtlas::grow` call clears the whole atlas and, as a result, all of
the `prepare` calls need to be repeated in a frame until they all
succeed. Overall, the atlas will rarely need to grow and so the calls
will not need to be repated often.

Finally, the user needs to call `TextAtlas::trim` at the end of the
frame. This allows us to clear the glyphs in use collection in the atlas. Maybe
there is a better way to model this in an API that forces the user to
trim the atlas (e.g. make `trim` return a new type and changing `prepare` and `render` to take that type instead).
This commit is contained in:
Héctor Ramón Jiménez 2023-02-08 23:08:37 +01:00 committed by Josh Groves
parent d20643702f
commit a74ce29c1a
5 changed files with 159 additions and 59 deletions

View file

@ -140,6 +140,8 @@ async fn run() {
queue.submit(Some(encoder.finish())); queue.submit(Some(encoder.finish()));
frame.present(); frame.present();
atlas.trim();
} }
Event::WindowEvent { Event::WindowEvent {
event: WindowEvent::CloseRequested, event: WindowEvent::CloseRequested,

View file

@ -1,3 +1,4 @@
use crate::ContentType;
use std::{ use std::{
error::Error, error::Error,
fmt::{self, Display, Formatter}, fmt::{self, Display, Formatter},
@ -6,7 +7,7 @@ use std::{
/// An error that occurred while preparing text for rendering. /// An error that occurred while preparing text for rendering.
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PrepareError { pub enum PrepareError {
AtlasFull, AtlasFull(ContentType),
} }
impl Display for PrepareError { impl Display for PrepareError {

View file

@ -10,7 +10,7 @@ mod text_render;
pub use error::{PrepareError, RenderError}; pub use error::{PrepareError, RenderError};
pub use text_atlas::{ColorMode, TextAtlas}; pub use text_atlas::{ColorMode, TextAtlas};
use text_render::ContentType; pub use text_render::ContentType;
pub use text_render::TextRenderer; pub use text_render::TextRenderer;
// Re-export all top-level types from `cosmic-text` for convenience. // Re-export all top-level types from `cosmic-text` for convenience.

View file

@ -1,16 +1,16 @@
use crate::{text_render::ContentType, CacheKey, GlyphDetails, GlyphToRender, Params, Resolution}; use crate::{text_render::ContentType, CacheKey, GlyphDetails, GlyphToRender, Params, Resolution};
use etagere::{size2, Allocation, BucketedAtlasAllocator}; use etagere::{size2, Allocation, BucketedAtlasAllocator};
use lru::LruCache; use lru::LruCache;
use std::{borrow::Cow, mem::size_of, num::NonZeroU64, sync::Arc}; use std::{borrow::Cow, collections::HashSet, mem::size_of, num::NonZeroU64, sync::Arc};
use wgpu::{ use wgpu::{
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutEntry, BindingResource, BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry,
BindingType, BlendState, Buffer, BufferBindingType, BufferDescriptor, BufferUsages, BindingResource, BindingType, BlendState, Buffer, BufferBindingType, BufferDescriptor,
ColorTargetState, ColorWrites, DepthStencilState, Device, Extent3d, FilterMode, FragmentState, BufferUsages, ColorTargetState, ColorWrites, DepthStencilState, Device, Extent3d, FilterMode,
MultisampleState, PipelineLayout, PipelineLayoutDescriptor, PrimitiveState, Queue, FragmentState, MultisampleState, PipelineLayout, PipelineLayoutDescriptor, PrimitiveState,
RenderPipeline, RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, ShaderModule, Queue, RenderPipeline, RenderPipelineDescriptor, Sampler, SamplerBindingType,
ShaderModuleDescriptor, ShaderSource, ShaderStages, Texture, TextureDescriptor, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, Texture,
TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureView, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages,
TextureViewDescriptor, TextureViewDimension, VertexFormat, VertexState, TextureView, TextureViewDescriptor, TextureViewDimension, VertexFormat, VertexState,
}; };
#[allow(dead_code)] #[allow(dead_code)]
@ -19,25 +19,27 @@ pub(crate) struct InnerAtlas {
pub texture: Texture, pub texture: Texture,
pub texture_view: TextureView, pub texture_view: TextureView,
pub packer: BucketedAtlasAllocator, pub packer: BucketedAtlasAllocator,
pub width: u32, pub size: u32,
pub height: u32,
pub glyph_cache: LruCache<CacheKey, GlyphDetails>, pub glyph_cache: LruCache<CacheKey, GlyphDetails>,
pub glyphs_in_use: HashSet<CacheKey>,
pub max_texture_dimension_2d: u32,
} }
impl InnerAtlas { impl InnerAtlas {
const INITIAL_SIZE: u32 = 256;
fn new(device: &Device, _queue: &Queue, kind: Kind) -> Self { fn new(device: &Device, _queue: &Queue, kind: Kind) -> Self {
let max_texture_dimension_2d = device.limits().max_texture_dimension_2d; let max_texture_dimension_2d = device.limits().max_texture_dimension_2d;
let width = max_texture_dimension_2d; let size = Self::INITIAL_SIZE.min(max_texture_dimension_2d);
let height = max_texture_dimension_2d;
let packer = BucketedAtlasAllocator::new(size2(width as i32, height as i32)); let packer = BucketedAtlasAllocator::new(size2(size as i32, size as i32));
// Create a texture to use for our atlas // Create a texture to use for our atlas
let texture = device.create_texture(&TextureDescriptor { let texture = device.create_texture(&TextureDescriptor {
label: Some("glyphon atlas"), label: Some("glyphon atlas"),
size: Extent3d { size: Extent3d {
width, width: size,
height, height: size,
depth_or_array_layers: 1, depth_or_array_layers: 1,
}, },
mip_level_count: 1, mip_level_count: 1,
@ -51,15 +53,17 @@ impl InnerAtlas {
let texture_view = texture.create_view(&TextureViewDescriptor::default()); let texture_view = texture.create_view(&TextureViewDescriptor::default());
let glyph_cache = LruCache::unbounded(); let glyph_cache = LruCache::unbounded();
let glyphs_in_use = HashSet::new();
Self { Self {
kind, kind,
texture, texture,
texture_view, texture_view,
packer, packer,
width, size,
height,
glyph_cache, glyph_cache,
glyphs_in_use,
max_texture_dimension_2d,
} }
} }
@ -68,20 +72,82 @@ impl InnerAtlas {
loop { loop {
let allocation = self.packer.allocate(size); let allocation = self.packer.allocate(size);
if allocation.is_some() { if allocation.is_some() {
return allocation; return allocation;
} }
// Try to free least recently used allocation // Try to free least recently used allocation
let (_, value) = self.glyph_cache.pop_lru()?; let (_, mut value) = self.glyph_cache.peek_lru()?;
self.packer
.deallocate(value.atlas_id.expect("cache corrupt")); while value.atlas_id.is_none() {
let _ = self.glyph_cache.pop_lru();
(_, value) = self.glyph_cache.peek_lru()?;
}
let (key, value) = self.glyph_cache.pop_lru().unwrap();
if self.glyphs_in_use.contains(&key) {
return None;
}
self.packer.deallocate(value.atlas_id.unwrap());
} }
} }
pub fn num_channels(&self) -> usize { pub fn num_channels(&self) -> usize {
self.kind.num_channels() self.kind.num_channels()
} }
pub(crate) fn promote(&mut self, glyph: CacheKey) {
self.glyph_cache.promote(&glyph);
self.glyphs_in_use.insert(glyph);
}
pub(crate) fn put(&mut self, glyph: CacheKey, details: GlyphDetails) {
self.glyph_cache.put(glyph, details);
self.glyphs_in_use.insert(glyph);
}
pub(crate) fn grow(&mut self, device: &wgpu::Device) -> bool {
if self.size >= self.max_texture_dimension_2d {
return false;
}
// TODO: Better resizing logic (?)
let new_size = (self.size + Self::INITIAL_SIZE).min(self.max_texture_dimension_2d);
self.packer = BucketedAtlasAllocator::new(size2(new_size as i32, new_size as i32));
// Create a texture to use for our atlas
self.texture = device.create_texture(&TextureDescriptor {
label: Some("glyphon atlas"),
size: Extent3d {
width: new_size,
height: new_size,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: self.kind.texture_format(),
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
});
self.texture_view = self.texture.create_view(&TextureViewDescriptor::default());
self.size = new_size;
self.glyph_cache.clear();
self.glyphs_in_use.clear();
true
}
fn trim(&mut self) {
self.glyphs_in_use.clear();
}
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -145,6 +211,8 @@ pub struct TextAtlas {
Arc<RenderPipeline>, Arc<RenderPipeline>,
)>, )>,
pub(crate) bind_group: Arc<BindGroup>, pub(crate) bind_group: Arc<BindGroup>,
pub(crate) bind_group_layout: BindGroupLayout,
pub(crate) sampler: Sampler,
pub(crate) color_atlas: InnerAtlas, pub(crate) color_atlas: InnerAtlas,
pub(crate) mask_atlas: InnerAtlas, pub(crate) mask_atlas: InnerAtlas,
pub(crate) pipeline_layout: PipelineLayout, pub(crate) pipeline_layout: PipelineLayout,
@ -322,6 +390,8 @@ impl TextAtlas {
params_buffer, params_buffer,
cached_pipelines: Vec::new(), cached_pipelines: Vec::new(),
bind_group, bind_group,
bind_group_layout,
sampler,
color_atlas, color_atlas,
mask_atlas, mask_atlas,
pipeline_layout, pipeline_layout,
@ -331,8 +401,23 @@ impl TextAtlas {
} }
} }
pub(crate) fn contains_cached_glyph(&self, glyph: &CacheKey) -> bool { pub fn trim(&mut self) {
self.mask_atlas.glyph_cache.contains(glyph) || self.color_atlas.glyph_cache.contains(glyph) self.mask_atlas.trim();
self.color_atlas.trim();
}
pub fn grow(&mut self, device: &wgpu::Device, content_type: ContentType) -> bool {
let did_grow = match content_type {
ContentType::Mask => self.mask_atlas.grow(device),
ContentType::Color => self.color_atlas.grow(device),
};
if did_grow {
self.rebind(device);
true
} else {
false
}
} }
pub(crate) fn glyph(&self, glyph: &CacheKey) -> Option<&GlyphDetails> { pub(crate) fn glyph(&self, glyph: &CacheKey) -> Option<&GlyphDetails> {
@ -388,4 +473,29 @@ impl TextAtlas {
pipeline pipeline
}) })
} }
fn rebind(&mut self, device: &wgpu::Device) {
self.bind_group = Arc::new(device.create_bind_group(&BindGroupDescriptor {
layout: &self.bind_group_layout,
entries: &[
BindGroupEntry {
binding: 0,
resource: self.params_buffer.as_entire_binding(),
},
BindGroupEntry {
binding: 1,
resource: BindingResource::TextureView(&self.color_atlas.texture_view),
},
BindGroupEntry {
binding: 2,
resource: BindingResource::TextureView(&self.mask_atlas.texture_view),
},
BindGroupEntry {
binding: 3,
resource: BindingResource::Sampler(&self.sampler),
},
],
label: Some("glyphon bind group"),
}));
}
} }

View file

@ -1,8 +1,8 @@
use crate::{ use crate::{
CacheKey, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, Params, PrepareError, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, Params, PrepareError, RenderError,
RenderError, Resolution, SwashCache, SwashContent, TextArea, TextAtlas, Resolution, SwashCache, SwashContent, TextArea, TextAtlas,
}; };
use std::{collections::HashSet, iter, mem::size_of, slice, sync::Arc}; use std::{iter, mem::size_of, slice, sync::Arc};
use wgpu::{ use wgpu::{
Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture, Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture,
ImageDataLayout, IndexFormat, MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, ImageDataLayout, IndexFormat, MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline,
@ -16,7 +16,6 @@ pub struct TextRenderer {
index_buffer: Buffer, index_buffer: Buffer,
index_buffer_size: u64, index_buffer_size: u64,
vertices_to_render: u32, vertices_to_render: u32,
glyphs_in_use: HashSet<CacheKey>,
screen_resolution: Resolution, screen_resolution: Resolution,
pipeline: Arc<RenderPipeline>, pipeline: Arc<RenderPipeline>,
} }
@ -53,7 +52,6 @@ impl TextRenderer {
index_buffer, index_buffer,
index_buffer_size, index_buffer_size,
vertices_to_render: 0, vertices_to_render: 0,
glyphs_in_use: HashSet::new(),
screen_resolution: Resolution { screen_resolution: Resolution {
width: 0, width: 0,
height: 0, height: 0,
@ -88,20 +86,16 @@ impl TextRenderer {
}); });
} }
self.glyphs_in_use.clear();
for text_area in text_areas.iter() { for text_area in text_areas.iter() {
for run in text_area.buffer.layout_runs() { for run in text_area.buffer.layout_runs() {
for glyph in run.glyphs.iter() { for glyph in run.glyphs.iter() {
self.glyphs_in_use.insert(glyph.cache_key);
if atlas.mask_atlas.glyph_cache.contains(&glyph.cache_key) { if atlas.mask_atlas.glyph_cache.contains(&glyph.cache_key) {
atlas.mask_atlas.glyph_cache.promote(&glyph.cache_key); atlas.mask_atlas.promote(glyph.cache_key);
continue; continue;
} }
if atlas.color_atlas.glyph_cache.contains(&glyph.cache_key) { if atlas.color_atlas.glyph_cache.contains(&glyph.cache_key) {
atlas.color_atlas.glyph_cache.promote(&glyph.cache_key); atlas.color_atlas.promote(glyph.cache_key);
continue; continue;
} }
@ -129,7 +123,9 @@ impl TextRenderer {
// Find a position in the packer // Find a position in the packer
let allocation = match inner.try_allocate(width, height) { let allocation = match inner.try_allocate(width, height) {
Some(a) => a, Some(a) => a,
None => return Err(PrepareError::AtlasFull), None => {
return Err(PrepareError::AtlasFull(content_type));
}
}; };
let atlas_min = allocation.rectangle.min; let atlas_min = allocation.rectangle.min;
@ -171,19 +167,17 @@ impl TextRenderer {
(GpuCacheStatus::SkipRasterization, None, inner) (GpuCacheStatus::SkipRasterization, None, inner)
}; };
if !inner.glyph_cache.contains(&glyph.cache_key) { inner.put(
inner.glyph_cache.put( glyph.cache_key,
glyph.cache_key, GlyphDetails {
GlyphDetails { width: width as u16,
width: width as u16, height: height as u16,
height: height as u16, gpu_cache,
gpu_cache, atlas_id,
atlas_id, top: image.placement.top as i16,
top: image.placement.top as i16, left: image.placement.left as i16,
left: image.placement.left as i16, },
}, );
);
}
} }
} }
} }
@ -383,13 +377,6 @@ impl TextRenderer {
} }
{ {
// Validate that glyphs haven't been evicted from cache since `prepare`
for glyph in self.glyphs_in_use.iter() {
if !atlas.contains_cached_glyph(glyph) {
return Err(RenderError::RemovedFromAtlas);
}
}
// Validate that screen resolution hasn't changed since `prepare` // Validate that screen resolution hasn't changed since `prepare`
if self.screen_resolution != atlas.params.screen_resolution { if self.screen_resolution != atlas.params.screen_resolution {
return Err(RenderError::ScreenResolutionChanged); return Err(RenderError::ScreenResolutionChanged);
@ -407,8 +394,8 @@ impl TextRenderer {
} }
#[repr(u32)] #[repr(u32)]
#[derive(Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) enum ContentType { pub enum ContentType {
Color = 0, Color = 0,
Mask = 1, Mask = 1,
} }