From c5540bf19c24c4da51755f4ec49422c9e50f1763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Wed, 8 Feb 2023 23:08:37 +0100 Subject: [PATCH] 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). --- examples/hello-world.rs | 2 + src/error.rs | 3 +- src/lib.rs | 2 +- src/text_atlas.rs | 159 ++++++++++++++++++++++++++++++++++------ src/text_render.rs | 55 ++++++-------- 5 files changed, 162 insertions(+), 59 deletions(-) diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 3c5c6fd..8faea88 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -139,6 +139,8 @@ async fn run() { queue.submit(Some(encoder.finish())); frame.present(); + + atlas.trim(); } Event::WindowEvent { event: WindowEvent::CloseRequested, diff --git a/src/error.rs b/src/error.rs index 0de51f9..e9d75da 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +use crate::ContentType; use std::{ error::Error, fmt::{self, Display, Formatter}, @@ -6,7 +7,7 @@ use std::{ /// An error that occurred while preparing text for rendering. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PrepareError { - AtlasFull, + AtlasFull(ContentType), } impl Display for PrepareError { diff --git a/src/lib.rs b/src/lib.rs index 83dd6e5..51b8f98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ mod text_render; pub use error::{PrepareError, RenderError}; pub use text_atlas::TextAtlas; -use text_render::ContentType; +pub use text_render::ContentType; pub use text_render::TextRenderer; // Re-export all top-level types from `cosmic-text` for convenience. diff --git a/src/text_atlas.rs b/src/text_atlas.rs index 777f6d9..37103ae 100644 --- a/src/text_atlas.rs +++ b/src/text_atlas.rs @@ -1,16 +1,16 @@ use crate::{text_render::ContentType, CacheKey, GlyphDetails, GlyphToRender, Params, Resolution}; use etagere::{size2, Allocation, BucketedAtlasAllocator}; 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::{ - BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutEntry, BindingResource, - BindingType, BlendState, Buffer, BufferBindingType, BufferDescriptor, BufferUsages, - ColorTargetState, ColorWrites, DepthStencilState, Device, Extent3d, FilterMode, FragmentState, - MultisampleState, PipelineLayout, PipelineLayoutDescriptor, PrimitiveState, Queue, - RenderPipeline, RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, ShaderModule, - ShaderModuleDescriptor, ShaderSource, ShaderStages, Texture, TextureDescriptor, - TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureView, - TextureViewDescriptor, TextureViewDimension, VertexFormat, VertexState, + BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutEntry, + BindingResource, BindingType, BlendState, Buffer, BufferBindingType, BufferDescriptor, + BufferUsages, ColorTargetState, ColorWrites, DepthStencilState, Device, Extent3d, FilterMode, + FragmentState, MultisampleState, PipelineLayout, PipelineLayoutDescriptor, PrimitiveState, + Queue, RenderPipeline, RenderPipelineDescriptor, Sampler, SamplerBindingType, + SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, Texture, + TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages, + TextureView, TextureViewDescriptor, TextureViewDimension, VertexFormat, VertexState, }; #[allow(dead_code)] @@ -18,26 +18,28 @@ pub(crate) struct InnerAtlas { pub texture: Texture, pub texture_view: TextureView, pub packer: BucketedAtlasAllocator, - pub width: u32, - pub height: u32, + pub size: u32, pub glyph_cache: LruCache, + pub glyphs_in_use: HashSet, pub num_atlas_channels: usize, + pub max_texture_dimension_2d: u32, } impl InnerAtlas { + const INITIAL_SIZE: u32 = 256; + fn new(device: &Device, _queue: &Queue, num_atlas_channels: usize) -> Self { let max_texture_dimension_2d = device.limits().max_texture_dimension_2d; - let width = max_texture_dimension_2d; - let height = max_texture_dimension_2d; + let size = Self::INITIAL_SIZE.min(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 let texture = device.create_texture(&TextureDescriptor { label: Some("glyphon atlas"), size: Extent3d { - width, - height, + width: size, + height: size, depth_or_array_layers: 1, }, mip_level_count: 1, @@ -54,15 +56,17 @@ impl InnerAtlas { let texture_view = texture.create_view(&TextureViewDescriptor::default()); let glyph_cache = LruCache::unbounded(); + let glyphs_in_use = HashSet::new(); Self { texture, texture_view, packer, - width, - height, + size, glyph_cache, + glyphs_in_use, num_atlas_channels, + max_texture_dimension_2d, } } @@ -71,16 +75,81 @@ impl InnerAtlas { loop { let allocation = self.packer.allocate(size); + if allocation.is_some() { return allocation; } // Try to free least recently used allocation - let (_, value) = self.glyph_cache.pop_lru()?; - self.packer - .deallocate(value.atlas_id.expect("cache corrupt")); + let (_, mut value) = self.glyph_cache.peek_lru()?; + + 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(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: match self.num_atlas_channels { + 1 => TextureFormat::R8Unorm, + 4 => TextureFormat::Rgba8UnormSrgb, + _ => panic!("unexpected number of channels"), + }, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + }); + + 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(); + } } /// An atlas containing a cache of rasterized glyphs that can be rendered. @@ -93,6 +162,8 @@ pub struct TextAtlas { Arc, )>, pub(crate) bind_group: Arc, + pub(crate) bind_group_layout: BindGroupLayout, + pub(crate) sampler: Sampler, pub(crate) color_atlas: InnerAtlas, pub(crate) mask_atlas: InnerAtlas, pub(crate) pipeline_layout: PipelineLayout, @@ -251,6 +322,8 @@ impl TextAtlas { params_buffer, cached_pipelines: Vec::new(), bind_group, + bind_group_layout, + sampler, color_atlas, mask_atlas, pipeline_layout, @@ -260,8 +333,23 @@ impl TextAtlas { } } - pub(crate) fn contains_cached_glyph(&self, glyph: &CacheKey) -> bool { - self.mask_atlas.glyph_cache.contains(glyph) || self.color_atlas.glyph_cache.contains(glyph) + pub fn trim(&mut self) { + 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> { @@ -317,4 +405,29 @@ impl TextAtlas { 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"), + })); + } } diff --git a/src/text_render.rs b/src/text_render.rs index 32298c3..3197c45 100644 --- a/src/text_render.rs +++ b/src/text_render.rs @@ -1,8 +1,8 @@ use crate::{ - CacheKey, FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, Params, PrepareError, - RenderError, Resolution, SwashCache, SwashContent, TextArea, TextAtlas, + FontSystem, GlyphDetails, GlyphToRender, GpuCacheStatus, Params, PrepareError, RenderError, + Resolution, SwashCache, SwashContent, TextArea, TextAtlas, }; -use std::{collections::HashSet, iter, mem::size_of, num::NonZeroU32, slice, sync::Arc}; +use std::{iter, mem::size_of, num::NonZeroU32, slice, sync::Arc}; use wgpu::{ Buffer, BufferDescriptor, BufferUsages, DepthStencilState, Device, Extent3d, ImageCopyTexture, ImageDataLayout, IndexFormat, MultisampleState, Origin3d, Queue, RenderPass, RenderPipeline, @@ -16,7 +16,6 @@ pub struct TextRenderer { index_buffer: Buffer, index_buffer_size: u64, vertices_to_render: u32, - glyphs_in_use: HashSet, screen_resolution: Resolution, pipeline: Arc, } @@ -53,7 +52,6 @@ impl TextRenderer { index_buffer, index_buffer_size, vertices_to_render: 0, - glyphs_in_use: HashSet::new(), screen_resolution: Resolution { width: 0, height: 0, @@ -88,20 +86,16 @@ impl TextRenderer { }); } - self.glyphs_in_use.clear(); - for text_area in text_areas.iter() { for run in text_area.buffer.layout_runs() { for glyph in run.glyphs.iter() { - self.glyphs_in_use.insert(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; } 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; } @@ -129,7 +123,9 @@ impl TextRenderer { // Find a position in the packer let allocation = match inner.try_allocate(width, height) { Some(a) => a, - None => return Err(PrepareError::AtlasFull), + None => { + return Err(PrepareError::AtlasFull(content_type)); + } }; let atlas_min = allocation.rectangle.min; @@ -173,19 +169,17 @@ impl TextRenderer { (GpuCacheStatus::SkipRasterization, None, inner) }; - if !inner.glyph_cache.contains(&glyph.cache_key) { - inner.glyph_cache.put( - glyph.cache_key, - GlyphDetails { - width: width as u16, - height: height as u16, - gpu_cache, - atlas_id, - top: image.placement.top as i16, - left: image.placement.left as i16, - }, - ); - } + inner.put( + glyph.cache_key, + GlyphDetails { + width: width as u16, + height: height as u16, + gpu_cache, + atlas_id, + top: image.placement.top as i16, + left: image.placement.left as i16, + }, + ); } } } @@ -385,13 +379,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` if self.screen_resolution != atlas.params.screen_resolution { return Err(RenderError::ScreenResolutionChanged); @@ -409,8 +396,8 @@ impl TextRenderer { } #[repr(u32)] -#[derive(Clone, Copy, Eq, PartialEq)] -pub(crate) enum ContentType { +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum ContentType { Color = 0, Mask = 1, }