Skip to content

Commit b6efe0f

Browse files
committed
Limit FontAtlasSets (#5708)
# Objective Fixes #5636 Summary: The FontAtlasSet caches generated font textures per font size. Since font size can be any arbitrary floating point number it is possible for the user to generate thousands of font texture inadvertently by changing the font size over time. This results in a memory leak as these generated font textures fill the available memory. ## Solution We limit the number of possible font sizes that we will cache and throw an error if the user attempts to generate more. This error encourages the user to use alternative, less performance intensive methods to accomplish the same goal. If the user requires more font sizes and the alternative solutions wont work there is now a TextSettings Resource that the user can set to configure this limit. --- ## Changelog The number of cached font sizes per font is now limited with a default limit of 100 font sizes per font. This limit is configurable via the new TextSettings struct.
1 parent 6c5403c commit b6efe0f

File tree

7 files changed

+72
-10
lines changed

7 files changed

+72
-10
lines changed

crates/bevy_text/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ pub enum TextError {
77
NoSuchFont,
88
#[error("failed to add glyph to newly-created atlas {0:?}")]
99
FailedToAddGlyph(GlyphId),
10+
#[error("exceeded {0:?} available TextAltases for font. This can be caused by using an excessive number of font sizes. If you are changing font sizes dynamically consider using Transform::scale to modify the size. If you need more font sizes modify TextSettings.max_font_atlases." )]
11+
ExceedMaxTextAtlases(usize),
1012
}

crates/bevy_text/src/font_atlas_set.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{error::TextError, Font, FontAtlas};
1+
use crate::{error::TextError, Font, FontAtlas, TextSettings};
22
use ab_glyph::{GlyphId, OutlinedGlyph, Point};
33
use bevy_asset::{Assets, Handle};
44
use bevy_math::Vec2;
@@ -14,6 +14,7 @@ type FontSizeKey = FloatOrd;
1414
#[uuid = "73ba778b-b6b5-4f45-982d-d21b6b86ace2"]
1515
pub struct FontAtlasSet {
1616
font_atlases: HashMap<FontSizeKey, Vec<FontAtlas>>,
17+
queue: Vec<FontSizeKey>,
1718
}
1819

1920
#[derive(Debug, Clone)]
@@ -26,6 +27,7 @@ impl Default for FontAtlasSet {
2627
fn default() -> Self {
2728
FontAtlasSet {
2829
font_atlases: HashMap::with_capacity_and_hasher(1, Default::default()),
30+
queue: Vec::new(),
2931
}
3032
}
3133
}
@@ -50,7 +52,22 @@ impl FontAtlasSet {
5052
texture_atlases: &mut Assets<TextureAtlas>,
5153
textures: &mut Assets<Image>,
5254
outlined_glyph: OutlinedGlyph,
55+
text_settings: &TextSettings,
5356
) -> Result<GlyphAtlasInfo, TextError> {
57+
if !text_settings.allow_dynamic_font_size {
58+
if self.font_atlases.len() >= text_settings.max_font_atlases.get() {
59+
return Err(TextError::ExceedMaxTextAtlases(
60+
text_settings.max_font_atlases.get(),
61+
));
62+
}
63+
} else {
64+
// Clear last space in queue to make room for new font size
65+
while self.queue.len() >= text_settings.max_font_atlases.get() - 1 {
66+
if let Some(font_size_key) = self.queue.pop() {
67+
self.font_atlases.remove(&font_size_key);
68+
}
69+
}
70+
}
5471
let glyph = outlined_glyph.glyph();
5572
let glyph_id = glyph.id;
5673
let glyph_position = glyph.position;
@@ -65,6 +82,7 @@ impl FontAtlasSet {
6582
Vec2::splat(512.0),
6683
)]
6784
});
85+
self.queue.insert(0, FloatOrd(font_size));
6886
let glyph_texture = Font::get_outlined_glyph_texture(outlined_glyph);
6987
let add_char_to_font_atlas = |atlas: &mut FontAtlas| -> bool {
7088
atlas.add_glyph(
@@ -106,11 +124,17 @@ impl FontAtlasSet {
106124
}
107125

108126
pub fn get_glyph_atlas_info(
109-
&self,
127+
&mut self,
110128
font_size: f32,
111129
glyph_id: GlyphId,
112130
position: Point,
113131
) -> Option<GlyphAtlasInfo> {
132+
// Move to front of used queue.
133+
let some_index = self.queue.iter().position(|x| *x == FloatOrd(font_size));
134+
if let Some(index) = some_index {
135+
let key = self.queue.remove(index);
136+
self.queue.insert(0, key);
137+
}
114138
self.font_atlases
115139
.get(&FloatOrd(font_size))
116140
.and_then(|font_atlases| {

crates/bevy_text/src/glyph_brush.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use glyph_brush_layout::{
77
FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph, SectionText, ToSectionText,
88
};
99

10-
use crate::{error::TextError, Font, FontAtlasSet, GlyphAtlasInfo, TextAlignment};
10+
use crate::{error::TextError, Font, FontAtlasSet, GlyphAtlasInfo, TextAlignment, TextSettings};
1111

1212
pub struct GlyphBrush {
1313
fonts: Vec<FontArc>,
@@ -43,6 +43,7 @@ impl GlyphBrush {
4343
Ok(section_glyphs)
4444
}
4545

46+
#[allow(clippy::too_many_arguments)]
4647
pub fn process_glyphs(
4748
&self,
4849
glyphs: Vec<SectionGlyph>,
@@ -51,6 +52,7 @@ impl GlyphBrush {
5152
fonts: &Assets<Font>,
5253
texture_atlases: &mut Assets<TextureAtlas>,
5354
textures: &mut Assets<Image>,
55+
text_settings: &TextSettings,
5456
) -> Result<Vec<PositionedGlyph>, TextError> {
5557
if glyphs.is_empty() {
5658
return Ok(Vec::new());
@@ -104,7 +106,12 @@ impl GlyphBrush {
104106
.get_glyph_atlas_info(section_data.2, glyph_id, glyph_position)
105107
.map(Ok)
106108
.unwrap_or_else(|| {
107-
font_atlas_set.add_glyph_to_atlas(texture_atlases, textures, outlined_glyph)
109+
font_atlas_set.add_glyph_to_atlas(
110+
texture_atlases,
111+
textures,
112+
outlined_glyph,
113+
text_settings,
114+
)
108115
})?;
109116

110117
let texture_atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap();

crates/bevy_text/src/lib.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,34 @@ pub mod prelude {
2828

2929
use bevy_app::prelude::*;
3030
use bevy_asset::AddAsset;
31-
use bevy_ecs::schedule::ParallelSystemDescriptorCoercion;
31+
use bevy_ecs::{schedule::ParallelSystemDescriptorCoercion, system::Resource};
3232
use bevy_render::{RenderApp, RenderStage};
3333
use bevy_sprite::SpriteSystem;
3434
use bevy_window::ModifiesWindows;
35+
use std::num::NonZeroUsize;
3536

3637
#[derive(Default)]
3738
pub struct TextPlugin;
3839

40+
/// [`TextPlugin`] settings
41+
#[derive(Resource)]
42+
pub struct TextSettings {
43+
/// Maximum number of font atlases supported in a ['FontAtlasSet']
44+
pub max_font_atlases: NonZeroUsize,
45+
/// Allows font size to be set dynamically exceeding the amount set in max_font_atlases.
46+
/// Note each font size has to be generated which can have a strong performance impact.
47+
pub allow_dynamic_font_size: bool,
48+
}
49+
50+
impl Default for TextSettings {
51+
fn default() -> Self {
52+
Self {
53+
max_font_atlases: NonZeroUsize::new(16).unwrap(),
54+
allow_dynamic_font_size: false,
55+
}
56+
}
57+
}
58+
3959
impl Plugin for TextPlugin {
4060
fn build(&self, app: &mut App) {
4161
app.add_asset::<Font>()
@@ -46,6 +66,7 @@ impl Plugin for TextPlugin {
4666
.register_type::<VerticalAlign>()
4767
.register_type::<HorizontalAlign>()
4868
.init_asset_loader::<FontLoader>()
69+
.init_resource::<TextSettings>()
4970
.insert_resource(TextPipeline::default())
5071
.add_system_to_stage(
5172
CoreStage::PostUpdate,

crates/bevy_text/src/pipeline.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use glyph_brush_layout::{FontId, SectionText};
1111

1212
use crate::{
1313
error::TextError, glyph_brush::GlyphBrush, scale_value, Font, FontAtlasSet, PositionedGlyph,
14-
TextAlignment, TextSection,
14+
TextAlignment, TextSection, TextSettings,
1515
};
1616

1717
#[derive(Default, Resource)]
@@ -49,6 +49,7 @@ impl TextPipeline {
4949
font_atlas_set_storage: &mut Assets<FontAtlasSet>,
5050
texture_atlases: &mut Assets<TextureAtlas>,
5151
textures: &mut Assets<Image>,
52+
text_settings: &TextSettings,
5253
) -> Result<TextLayoutInfo, TextError> {
5354
let mut scaled_fonts = Vec::new();
5455
let sections = sections
@@ -103,6 +104,7 @@ impl TextPipeline {
103104
fonts,
104105
texture_atlases,
105106
textures,
107+
text_settings,
106108
)?;
107109

108110
Ok(TextLayoutInfo { glyphs, size })

crates/bevy_text/src/text2d.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use bevy_window::{WindowId, WindowScaleFactorChanged, Windows};
2323

2424
use crate::{
2525
Font, FontAtlasSet, HorizontalAlign, Text, TextError, TextLayoutInfo, TextPipeline,
26-
VerticalAlign,
26+
TextSettings, VerticalAlign,
2727
};
2828

2929
/// The calculated size of text drawn in 2D scene.
@@ -153,6 +153,7 @@ pub fn update_text2d_layout(
153153
mut textures: ResMut<Assets<Image>>,
154154
fonts: Res<Assets<Font>>,
155155
windows: Res<Windows>,
156+
text_settings: Res<TextSettings>,
156157
mut scale_factor_changed: EventReader<WindowScaleFactorChanged>,
157158
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
158159
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
@@ -190,13 +191,15 @@ pub fn update_text2d_layout(
190191
&mut *font_atlas_set_storage,
191192
&mut *texture_atlases,
192193
&mut *textures,
194+
text_settings.as_ref(),
193195
) {
194196
Err(TextError::NoSuchFont) => {
195197
// There was an error processing the text layout, let's add this entity to the
196198
// queue for further processing
197199
queue.insert(entity);
198200
}
199-
Err(e @ TextError::FailedToAddGlyph(_)) => {
201+
Err(e @ TextError::FailedToAddGlyph(_))
202+
| Err(e @ TextError::ExceedMaxTextAtlases(_)) => {
200203
panic!("Fatal error when processing text: {}.", e);
201204
}
202205
Ok(info) => {

crates/bevy_ui/src/widget/text.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use bevy_ecs::{
88
use bevy_math::Vec2;
99
use bevy_render::texture::Image;
1010
use bevy_sprite::TextureAtlas;
11-
use bevy_text::{Font, FontAtlasSet, Text, TextError, TextLayoutInfo, TextPipeline};
11+
use bevy_text::{Font, FontAtlasSet, Text, TextError, TextLayoutInfo, TextPipeline, TextSettings};
1212
use bevy_window::Windows;
1313

1414
#[derive(Debug, Default)]
@@ -44,6 +44,7 @@ pub fn text_system(
4444
mut textures: ResMut<Assets<Image>>,
4545
fonts: Res<Assets<Font>>,
4646
windows: Res<Windows>,
47+
text_settings: Res<TextSettings>,
4748
ui_scale: Res<UiScale>,
4849
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
4950
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
@@ -116,13 +117,15 @@ pub fn text_system(
116117
&mut *font_atlas_set_storage,
117118
&mut *texture_atlases,
118119
&mut *textures,
120+
text_settings.as_ref(),
119121
) {
120122
Err(TextError::NoSuchFont) => {
121123
// There was an error processing the text layout, let's add this entity to the
122124
// queue for further processing
123125
new_queue.push(entity);
124126
}
125-
Err(e @ TextError::FailedToAddGlyph(_)) => {
127+
Err(e @ TextError::FailedToAddGlyph(_))
128+
| Err(e @ TextError::ExceedMaxTextAtlases(_)) => {
126129
panic!("Fatal error when processing text: {}.", e);
127130
}
128131
Ok(info) => {

0 commit comments

Comments
 (0)