Skip to content

Commit 1610aa9

Browse files
Remove FontAtlasSets (#21345)
# Objective The `FontAtlasSets` resource contains a map from `AssetId<Font>`s to `FontAtlasSet`s. `FontAtlasSet` has another map from a `FontAtlasKey` (tuple of the font size bit cast into a `u32` and `FontSmoothing`) to a vec that contains the actual font atlases for the font. The redirection through the additional map doesn't serve much purpose, only individual fonts are looked up (except when freeing unused fonts), not the set of all atlases for a particular font face. ## Solution * Remove `FontAtlasSet`. * Rename `FontAtlasSets` to `FontAtlasSet`. * Add `AssetId<Font>` to `FontAtlasKey`. * Change the font atlas map to a `HashMap<FontAtlasKey, Vec<FontAtlas>>` * Move the `FontAtlasSet` methods `add_glyph_to_atlas`, `get_glyph_atlas_info`, and `get_outlined_glyph_texture` into the `font_atlas` module and rework them into free functions. * Change `FontAtlasSet` into a newtype around the map and remove the manual method implementations in favour of deriving `Deref` and `DerefMut`. We could consider renaming `FontAlasSet` to `FontAtlasMap`, but it doesn't seem necessary, and mathematically a map is a set so it's not incorrect. ## Testing The output from all of the text examples should be unchanged. Naive benchmarks suggest this is a modest improvement in performance at a very high load (5% ish), but this is more about reducing complexity to make more significant optimisations possible. Freeing the unused atlases when a font asset is removed will be slower, it has to filter all the font atlas vecs now instead of just removing the entry for the font asset, but this isn't done very often and the number of entries should be relatively low. --------- Co-authored-by: Dimitrios Loukadakis <[email protected]>
1 parent 40df90c commit 1610aa9

File tree

11 files changed

+217
-278
lines changed

11 files changed

+217
-278
lines changed

crates/bevy_sprite/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ impl Plugin for SpritePlugin {
8787
bevy_text::detect_text_needs_rerender::<Text2d>,
8888
update_text2d_layout
8989
.after(bevy_camera::CameraUpdateSystems)
90-
.after(bevy_text::remove_dropped_font_atlas_sets),
90+
.after(bevy_text::free_unused_font_atlases_system),
9191
calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds),
9292
)
9393
.chain()

crates/bevy_sprite/src/text2d.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use bevy_image::prelude::*;
2020
use bevy_math::{FloatOrd, Vec2, Vec3};
2121
use bevy_reflect::{prelude::ReflectDefault, Reflect};
2222
use bevy_text::{
23-
ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache, TextBounds,
23+
ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSet, LineBreak, SwashCache, TextBounds,
2424
TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot,
2525
TextSpanAccess, TextWriter,
2626
};
@@ -165,7 +165,7 @@ pub fn update_text2d_layout(
165165
fonts: Res<Assets<Font>>,
166166
camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>,
167167
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
168-
mut font_atlas_sets: ResMut<FontAtlasSets>,
168+
mut font_atlas_set: ResMut<FontAtlasSet>,
169169
mut text_pipeline: ResMut<TextPipeline>,
170170
mut text_query: Query<(
171171
Entity,
@@ -240,7 +240,7 @@ pub fn update_text2d_layout(
240240
scale_factor as f64,
241241
&block,
242242
text_bounds,
243-
&mut font_atlas_sets,
243+
&mut font_atlas_set,
244244
&mut texture_atlases,
245245
&mut textures,
246246
computed.as_mut(),
@@ -321,7 +321,7 @@ mod tests {
321321
app.init_resource::<Assets<Font>>()
322322
.init_resource::<Assets<Image>>()
323323
.init_resource::<Assets<TextureAtlasLayout>>()
324-
.init_resource::<FontAtlasSets>()
324+
.init_resource::<FontAtlasSet>()
325325
.init_resource::<TextPipeline>()
326326
.init_resource::<CosmicFontSystem>()
327327
.init_resource::<SwashCache>()

crates/bevy_text/src/font_atlas.rs

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ use bevy_asset::{Assets, Handle, RenderAssetUsages};
22
use bevy_image::{prelude::*, ImageSampler, ToExtents};
33
use bevy_math::{IVec2, UVec2};
44
use bevy_platform::collections::HashMap;
5-
use wgpu_types::{TextureDimension, TextureFormat};
5+
use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
66

7-
use crate::{FontSmoothing, GlyphAtlasLocation, TextError};
7+
use crate::{FontSmoothing, GlyphAtlasInfo, GlyphAtlasLocation, TextError};
88

99
/// Rasterized glyphs are cached, stored in, and retrieved from, a `FontAtlas`.
1010
///
@@ -118,3 +118,139 @@ impl core::fmt::Debug for FontAtlas {
118118
.finish()
119119
}
120120
}
121+
122+
/// Adds the given subpixel-offset glyph to the given font atlases
123+
pub fn add_glyph_to_atlas(
124+
font_atlases: &mut Vec<FontAtlas>,
125+
texture_atlases: &mut Assets<TextureAtlasLayout>,
126+
textures: &mut Assets<Image>,
127+
font_system: &mut cosmic_text::FontSystem,
128+
swash_cache: &mut cosmic_text::SwashCache,
129+
layout_glyph: &cosmic_text::LayoutGlyph,
130+
font_smoothing: FontSmoothing,
131+
) -> Result<GlyphAtlasInfo, TextError> {
132+
let physical_glyph = layout_glyph.physical((0., 0.), 1.0);
133+
134+
let (glyph_texture, offset) =
135+
get_outlined_glyph_texture(font_system, swash_cache, &physical_glyph, font_smoothing)?;
136+
let mut add_char_to_font_atlas = |atlas: &mut FontAtlas| -> Result<(), TextError> {
137+
atlas.add_glyph(
138+
textures,
139+
texture_atlases,
140+
physical_glyph.cache_key,
141+
&glyph_texture,
142+
offset,
143+
)
144+
};
145+
if !font_atlases
146+
.iter_mut()
147+
.any(|atlas| add_char_to_font_atlas(atlas).is_ok())
148+
{
149+
// Find the largest dimension of the glyph, either its width or its height
150+
let glyph_max_size: u32 = glyph_texture
151+
.texture_descriptor
152+
.size
153+
.height
154+
.max(glyph_texture.width());
155+
// Pick the higher of 512 or the smallest power of 2 greater than glyph_max_size
156+
let containing = (1u32 << (32 - glyph_max_size.leading_zeros())).max(512);
157+
font_atlases.push(FontAtlas::new(
158+
textures,
159+
texture_atlases,
160+
UVec2::splat(containing),
161+
font_smoothing,
162+
));
163+
164+
font_atlases.last_mut().unwrap().add_glyph(
165+
textures,
166+
texture_atlases,
167+
physical_glyph.cache_key,
168+
&glyph_texture,
169+
offset,
170+
)?;
171+
}
172+
173+
Ok(get_glyph_atlas_info(font_atlases, physical_glyph.cache_key).unwrap())
174+
}
175+
176+
/// Get the texture of the glyph as a rendered image, and its offset
177+
pub fn get_outlined_glyph_texture(
178+
font_system: &mut cosmic_text::FontSystem,
179+
swash_cache: &mut cosmic_text::SwashCache,
180+
physical_glyph: &cosmic_text::PhysicalGlyph,
181+
font_smoothing: FontSmoothing,
182+
) -> Result<(Image, IVec2), TextError> {
183+
// NOTE: Ideally, we'd ask COSMIC Text to honor the font smoothing setting directly.
184+
// However, since it currently doesn't support that, we render the glyph with antialiasing
185+
// and apply a threshold to the alpha channel to simulate the effect.
186+
//
187+
// This has the side effect of making regular vector fonts look quite ugly when font smoothing
188+
// is turned off, but for fonts that are specifically designed for pixel art, it works well.
189+
//
190+
// See: https://github.com/pop-os/cosmic-text/issues/279
191+
let image = swash_cache
192+
.get_image_uncached(font_system, physical_glyph.cache_key)
193+
.ok_or(TextError::FailedToGetGlyphImage(physical_glyph.cache_key))?;
194+
195+
let cosmic_text::Placement {
196+
left,
197+
top,
198+
width,
199+
height,
200+
} = image.placement;
201+
202+
let data = match image.content {
203+
cosmic_text::SwashContent::Mask => {
204+
if font_smoothing == FontSmoothing::None {
205+
image
206+
.data
207+
.iter()
208+
// Apply a 50% threshold to the alpha channel
209+
.flat_map(|a| [255, 255, 255, if *a > 127 { 255 } else { 0 }])
210+
.collect()
211+
} else {
212+
image
213+
.data
214+
.iter()
215+
.flat_map(|a| [255, 255, 255, *a])
216+
.collect()
217+
}
218+
}
219+
cosmic_text::SwashContent::Color => image.data,
220+
cosmic_text::SwashContent::SubpixelMask => {
221+
// TODO: implement
222+
todo!()
223+
}
224+
};
225+
226+
Ok((
227+
Image::new(
228+
Extent3d {
229+
width,
230+
height,
231+
depth_or_array_layers: 1,
232+
},
233+
TextureDimension::D2,
234+
data,
235+
TextureFormat::Rgba8UnormSrgb,
236+
RenderAssetUsages::MAIN_WORLD,
237+
),
238+
IVec2::new(left, top),
239+
))
240+
}
241+
242+
/// Generates the [`GlyphAtlasInfo`] for the given subpixel-offset glyph.
243+
pub fn get_glyph_atlas_info(
244+
font_atlases: &mut [FontAtlas],
245+
cache_key: cosmic_text::CacheKey,
246+
) -> Option<GlyphAtlasInfo> {
247+
font_atlases.iter().find_map(|atlas| {
248+
atlas
249+
.get_glyph_index(cache_key)
250+
.map(|location| GlyphAtlasInfo {
251+
location,
252+
texture_atlas: atlas.texture_atlas.id(),
253+
texture: atlas.texture.id(),
254+
})
255+
})
256+
}

0 commit comments

Comments
 (0)