diff --git a/examples/hello-world.rs b/examples/hello-world.rs index 782ceed..55adfc4 100644 --- a/examples/hello-world.rs +++ b/examples/hello-world.rs @@ -140,6 +140,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 1ae7d25..7c2d736 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ mod text_render; pub use error::{PrepareError, RenderError}; pub use text_atlas::{ColorMode, 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 14e832b..0b1f74a 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)] @@ -19,25 +19,27 @@ 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 max_texture_dimension_2d: u32, } impl InnerAtlas { + const INITIAL_SIZE: u32 = 256; + fn new(device: &Device, _queue: &Queue, kind: Kind) -> 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, @@ -51,15 +53,17 @@ impl InnerAtlas { let texture_view = texture.create_view(&TextureViewDescriptor::default()); let glyph_cache = LruCache::unbounded(); + let glyphs_in_use = HashSet::new(); Self { kind, texture, texture_view, packer, - width, - height, + size, glyph_cache, + glyphs_in_use, + max_texture_dimension_2d, } } @@ -68,20 +72,82 @@ 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 fn num_channels(&self) -> usize { 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)] @@ -145,6 +211,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, @@ -322,6 +390,8 @@ impl TextAtlas { params_buffer, cached_pipelines: Vec::new(), bind_group, + bind_group_layout, + sampler, color_atlas, mask_atlas, pipeline_layout, @@ -331,8 +401,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> { @@ -388,4 +473,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 5a57f04..47f2e0f 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, slice, sync::Arc}; +use std::{iter, mem::size_of, 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; @@ -171,19 +167,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, + }, + ); } } } @@ -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` if self.screen_resolution != atlas.params.screen_resolution { return Err(RenderError::ScreenResolutionChanged); @@ -407,8 +394,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, }