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:
parent
d20643702f
commit
a74ce29c1a
5 changed files with 159 additions and 59 deletions
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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"),
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue