Replace the local text queues in the text systems with flags stored in a component (#8549)

# Objective

`text_system` and `measure_text_system` both keep local queues to keep
track of text node entities that need recomputations/remeasurement,
which scales very badly with large numbers of text entities (O(n^2)) and
makes the code quite difficult to understand.

Also `text_system` filters for `Changed<Text>`, this isn't something
that it should do. When a text node entity fails to be processed by
`measure_text_system` because a font can't be found, the text node will
still be added to `text_system`'s local queue for recomputation. `Text`
should only ever be queued by `text_system` when a text node's geometry
is modified or a new measure is added.

## Solution

Remove the local text queues and use a component `TextFlags` to schedule
remeasurements and recomputations.

## Changelog
* Created a component `TextFlags` with fields `remeasure` and
`recompute`, which can be used to schedule a text `remeasure` or
`recomputation` respectively and added it to `TextBundle`.
* Removed the local text queues from `measure_text_system` and
`text_system` and instead use the `TextFlags` component to schedule
remeasurements and recomputations.

## Migration Guide

The component `TextFlags` has been added to `TextBundle`.
This commit is contained in:
ickshonpe 2023-05-08 14:57:52 +01:00 committed by GitHub
parent 845f027ac2
commit e0a94abf1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 166 additions and 91 deletions

View File

@ -1,7 +1,7 @@
//! This module contains basic node bundles used to build UIs //! This module contains basic node bundles used to build UIs
use crate::{ use crate::{
widget::{Button, UiImageSize}, widget::{Button, TextFlags, UiImageSize},
BackgroundColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, ZIndex, BackgroundColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, ZIndex,
}; };
use bevy_ecs::bundle::Bundle; use bevy_ecs::bundle::Bundle;
@ -118,6 +118,8 @@ pub struct TextBundle {
pub text: Text, pub text: Text,
/// Text layout information /// Text layout information
pub text_layout_info: TextLayoutInfo, pub text_layout_info: TextLayoutInfo,
/// Text system flags
pub text_flags: TextFlags,
/// The calculated size based on the given image /// The calculated size based on the given image
pub calculated_size: ContentSize, pub calculated_size: ContentSize,
/// Whether this node should block interaction with lower nodes /// Whether this node should block interaction with lower nodes
@ -148,6 +150,7 @@ impl Default for TextBundle {
Self { Self {
text: Default::default(), text: Default::default(),
text_layout_info: Default::default(), text_layout_info: Default::default(),
text_flags: Default::default(),
calculated_size: Default::default(), calculated_size: Default::default(),
// Transparent background // Transparent background
background_color: BackgroundColor(Color::NONE), background_color: BackgroundColor(Color::NONE),

View File

@ -1,11 +1,14 @@
use crate::{ContentSize, Measure, Node, UiScale}; use crate::{ContentSize, Measure, Node, UiScale};
use bevy_asset::Assets; use bevy_asset::Assets;
use bevy_ecs::{ use bevy_ecs::{
entity::Entity, prelude::{Component, DetectChanges},
query::{Changed, Or, With}, query::With,
system::{Local, ParamSet, Query, Res, ResMut}, reflect::ReflectComponent,
system::{Local, Query, Res, ResMut},
world::{Mut, Ref},
}; };
use bevy_math::Vec2; use bevy_math::Vec2;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::texture::Image; use bevy_render::texture::Image;
use bevy_sprite::TextureAtlas; use bevy_sprite::TextureAtlas;
use bevy_text::{ use bevy_text::{
@ -19,6 +22,27 @@ fn scale_value(value: f32, factor: f64) -> f32 {
(value as f64 * factor) as f32 (value as f64 * factor) as f32
} }
/// Text system flags
///
/// Used internally by [`measure_text_system`] and [`text_system`] to schedule text for processing.
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default)]
pub struct TextFlags {
/// If set a new measure function for the text node will be created
needs_new_measure_func: bool,
/// If set the text will be recomputed
needs_recompute: bool,
}
impl Default for TextFlags {
fn default() -> Self {
Self {
needs_new_measure_func: true,
needs_recompute: true,
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct TextMeasure { pub struct TextMeasure {
pub info: TextMeasureInfo, pub info: TextMeasureInfo,
@ -54,20 +78,48 @@ impl Measure for TextMeasure {
} }
} }
#[inline]
fn create_text_measure(
fonts: &Assets<Font>,
text_pipeline: &mut TextPipeline,
scale_factor: f64,
text: Ref<Text>,
mut content_size: Mut<ContentSize>,
mut text_flags: Mut<TextFlags>,
) {
match text_pipeline.create_text_measure(
fonts,
&text.sections,
scale_factor,
text.alignment,
text.linebreak_behavior,
) {
Ok(measure) => {
content_size.set(TextMeasure { info: measure });
// Text measure func created succesfully, so set `TextFlags` to schedule a recompute
text_flags.needs_new_measure_func = false;
text_flags.needs_recompute = true;
}
Err(TextError::NoSuchFont) => {
// Try again next frame
text_flags.needs_new_measure_func = true;
}
Err(e @ TextError::FailedToAddGlyph(_)) => {
panic!("Fatal error when processing text: {e}.");
}
};
}
/// Creates a `Measure` for text nodes that allows the UI to determine the appropriate amount of space /// Creates a `Measure` for text nodes that allows the UI to determine the appropriate amount of space
/// to provide for the text given the fonts, the text itself and the constraints of the layout. /// to provide for the text given the fonts, the text itself and the constraints of the layout.
pub fn measure_text_system( pub fn measure_text_system(
mut queued_text: Local<Vec<Entity>>,
mut last_scale_factor: Local<f64>, mut last_scale_factor: Local<f64>,
fonts: Res<Assets<Font>>, fonts: Res<Assets<Font>>,
windows: Query<&Window, With<PrimaryWindow>>, windows: Query<&Window, With<PrimaryWindow>>,
ui_scale: Res<UiScale>, ui_scale: Res<UiScale>,
mut text_pipeline: ResMut<TextPipeline>, mut text_pipeline: ResMut<TextPipeline>,
mut text_queries: ParamSet<( mut text_query: Query<(Ref<Text>, &mut ContentSize, &mut TextFlags), With<Node>>,
Query<Entity, (Changed<Text>, With<Node>)>,
Query<Entity, (With<Text>, With<Node>)>,
Query<(&Text, &mut ContentSize)>,
)>,
) { ) {
let window_scale_factor = windows let window_scale_factor = windows
.get_single() .get_single()
@ -78,48 +130,86 @@ pub fn measure_text_system(
#[allow(clippy::float_cmp)] #[allow(clippy::float_cmp)]
if *last_scale_factor == scale_factor { if *last_scale_factor == scale_factor {
// Adds all entities where the text has changed to the local queue // scale factor unchanged, only create new measure funcs for modified text
for entity in text_queries.p0().iter() { for (text, content_size, text_flags) in text_query.iter_mut() {
if !queued_text.contains(&entity) { if text.is_changed() || text_flags.needs_new_measure_func {
queued_text.push(entity); create_text_measure(
&fonts,
&mut text_pipeline,
scale_factor,
text,
content_size,
text_flags,
);
} }
} }
} else { } else {
// If the scale factor has changed, queue all text // scale factor changed, create new measure funcs for all text
for entity in text_queries.p1().iter() {
queued_text.push(entity);
}
*last_scale_factor = scale_factor; *last_scale_factor = scale_factor;
}
if queued_text.is_empty() { for (text, content_size, text_flags) in text_query.iter_mut() {
return; create_text_measure(
}
let mut new_queue = Vec::new();
let mut query = text_queries.p2();
for entity in queued_text.drain(..) {
if let Ok((text, mut content_size)) = query.get_mut(entity) {
match text_pipeline.create_text_measure(
&fonts, &fonts,
&mut text_pipeline,
scale_factor,
text,
content_size,
text_flags,
);
}
}
}
#[allow(clippy::too_many_arguments)]
#[inline]
fn queue_text(
fonts: &Assets<Font>,
text_pipeline: &mut TextPipeline,
font_atlas_warning: &mut FontAtlasWarning,
font_atlas_set_storage: &mut Assets<FontAtlasSet>,
texture_atlases: &mut Assets<TextureAtlas>,
textures: &mut Assets<Image>,
text_settings: &TextSettings,
scale_factor: f64,
text: &Text,
node: Ref<Node>,
mut text_flags: Mut<TextFlags>,
mut text_layout_info: Mut<TextLayoutInfo>,
) {
// Skip the text node if it is waiting for a new measure func
if !text_flags.needs_new_measure_func {
let node_size = Vec2::new(
scale_value(node.size().x, scale_factor),
scale_value(node.size().y, scale_factor),
);
match text_pipeline.queue_text(
fonts,
&text.sections, &text.sections,
scale_factor, scale_factor,
text.alignment, text.alignment,
text.linebreak_behavior, text.linebreak_behavior,
node_size,
font_atlas_set_storage,
texture_atlases,
textures,
text_settings,
font_atlas_warning,
YAxisOrientation::TopToBottom,
) { ) {
Ok(measure) => {
content_size.set(TextMeasure { info: measure });
}
Err(TextError::NoSuchFont) => { Err(TextError::NoSuchFont) => {
new_queue.push(entity); // There was an error processing the text layout, try again next frame
text_flags.needs_recompute = true;
} }
Err(e @ TextError::FailedToAddGlyph(_)) => { Err(e @ TextError::FailedToAddGlyph(_)) => {
panic!("Fatal error when processing text: {e}."); panic!("Fatal error when processing text: {e}.");
} }
}; Ok(info) => {
*text_layout_info = info;
text_flags.needs_recompute = false;
}
} }
} }
*queued_text = new_queue;
} }
/// Updates the layout and size information whenever the text or style is changed. /// Updates the layout and size information whenever the text or style is changed.
@ -131,7 +221,6 @@ pub fn measure_text_system(
/// It does not modify or observe existing ones. /// It does not modify or observe existing ones.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn text_system( pub fn text_system(
mut queued_text: Local<Vec<Entity>>,
mut textures: ResMut<Assets<Image>>, mut textures: ResMut<Assets<Image>>,
mut last_scale_factor: Local<f64>, mut last_scale_factor: Local<f64>,
fonts: Res<Assets<Font>>, fonts: Res<Assets<Font>>,
@ -142,11 +231,7 @@ pub fn text_system(
mut texture_atlases: ResMut<Assets<TextureAtlas>>, mut texture_atlases: ResMut<Assets<TextureAtlas>>,
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>, mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
mut text_pipeline: ResMut<TextPipeline>, mut text_pipeline: ResMut<TextPipeline>,
mut text_queries: ParamSet<( mut text_query: Query<(Ref<Node>, &Text, &mut TextLayoutInfo, &mut TextFlags)>,
Query<Entity, Or<(Changed<Text>, Changed<Node>)>>,
Query<Entity, (With<Text>, With<Node>)>,
Query<(&Node, &Text, &mut TextLayoutInfo)>,
)>,
) { ) {
// TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621
let window_scale_factor = windows let window_scale_factor = windows
@ -156,58 +241,45 @@ pub fn text_system(
let scale_factor = ui_scale.scale * window_scale_factor; let scale_factor = ui_scale.scale * window_scale_factor;
#[allow(clippy::float_cmp)]
if *last_scale_factor == scale_factor { if *last_scale_factor == scale_factor {
// Adds all entities where the text or the style has changed to the local queue // Scale factor unchanged, only recompute text for modified text nodes
for entity in text_queries.p0().iter() { for (node, text, text_layout_info, text_flags) in text_query.iter_mut() {
if !queued_text.contains(&entity) { if node.is_changed() || text_flags.needs_recompute {
queued_text.push(entity); queue_text(
}
}
} else {
// If the scale factor has changed, queue all text
for entity in text_queries.p1().iter() {
queued_text.push(entity);
}
*last_scale_factor = scale_factor;
}
let mut new_queue = Vec::new();
let mut text_query = text_queries.p2();
for entity in queued_text.drain(..) {
if let Ok((node, text, mut text_layout_info)) = text_query.get_mut(entity) {
let node_size = Vec2::new(
scale_value(node.size().x, scale_factor),
scale_value(node.size().y, scale_factor),
);
match text_pipeline.queue_text(
&fonts, &fonts,
&text.sections, &mut text_pipeline,
scale_factor, &mut font_atlas_warning,
text.alignment,
text.linebreak_behavior,
node_size,
&mut font_atlas_set_storage, &mut font_atlas_set_storage,
&mut texture_atlases, &mut texture_atlases,
&mut textures, &mut textures,
text_settings.as_ref(), &text_settings,
scale_factor,
text,
node,
text_flags,
text_layout_info,
);
}
}
} else {
// Scale factor changed, recompute text for all text nodes
*last_scale_factor = scale_factor;
for (node, text, text_layout_info, text_flags) in text_query.iter_mut() {
queue_text(
&fonts,
&mut text_pipeline,
&mut font_atlas_warning, &mut font_atlas_warning,
YAxisOrientation::TopToBottom, &mut font_atlas_set_storage,
) { &mut texture_atlases,
Err(TextError::NoSuchFont) => { &mut textures,
// There was an error processing the text layout, let's add this entity to the &text_settings,
// queue for further processing scale_factor,
new_queue.push(entity); text,
} node,
Err(e @ TextError::FailedToAddGlyph(_)) => { text_flags,
panic!("Fatal error when processing text: {e}."); text_layout_info,
} );
Ok(info) => {
*text_layout_info = info;
} }
} }
} }
}
*queued_text = new_queue;
}