Optimize UI text measurement (#15003)

# Objective

- Avoid cloning the `CosmicBuffer` every time you create a new text
measurement.

## Solution

- Inject a buffer query when calculating layout so existing buffers can
be reused.

## Testing

- I tested the `text`, `text_debug`, and `text_wrap_debug` examples.
- I did not do a performance test.
This commit is contained in:
UkoeHB 2024-09-01 06:50:54 -05:00 committed by GitHub
parent f0560b8e78
commit 41474226c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 81 additions and 27 deletions

View file

@ -1,7 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use bevy_asset::{AssetId, Assets}; use bevy_asset::{AssetId, Assets};
use bevy_ecs::{component::Component, reflect::ReflectComponent, system::Resource}; use bevy_ecs::{component::Component, entity::Entity, reflect::ReflectComponent, system::Resource};
use bevy_math::{UVec2, Vec2}; use bevy_math::{UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::texture::Image; use bevy_render::texture::Image;
@ -238,8 +238,10 @@ impl TextPipeline {
/// ///
/// Produces a [`TextMeasureInfo`] which can be used by a layout system /// Produces a [`TextMeasureInfo`] which can be used by a layout system
/// to measure the text area on demand. /// to measure the text area on demand.
#[allow(clippy::too_many_arguments)]
pub fn create_text_measure( pub fn create_text_measure(
&mut self, &mut self,
entity: Entity,
fonts: &Assets<Font>, fonts: &Assets<Font>,
sections: &[TextSection], sections: &[TextSection],
scale_factor: f64, scale_factor: f64,
@ -270,9 +272,7 @@ impl TextPipeline {
Ok(TextMeasureInfo { Ok(TextMeasureInfo {
min: min_width_content_size, min: min_width_content_size,
max: max_width_content_size, max: max_width_content_size,
// TODO: This clone feels wasteful, is there another way to structure TextMeasureInfo entity,
// that it doesn't need to own a buffer? - bytemunch
buffer: buffer.0.clone(),
}) })
} }
@ -299,23 +299,14 @@ pub struct TextLayoutInfo {
/// Size information for a corresponding [`Text`](crate::Text) component. /// Size information for a corresponding [`Text`](crate::Text) component.
/// ///
/// Generated via [`TextPipeline::create_text_measure`]. /// Generated via [`TextPipeline::create_text_measure`].
#[derive(Debug)]
pub struct TextMeasureInfo { pub struct TextMeasureInfo {
/// Minimum size for a text area in pixels, to be used when laying out widgets with taffy /// Minimum size for a text area in pixels, to be used when laying out widgets with taffy
pub min: Vec2, pub min: Vec2,
/// Maximum size for a text area in pixels, to be used when laying out widgets with taffy /// Maximum size for a text area in pixels, to be used when laying out widgets with taffy
pub max: Vec2, pub max: Vec2,
buffer: Buffer, /// The entity that is measured.
} pub entity: Entity,
impl std::fmt::Debug for TextMeasureInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TextMeasureInfo")
.field("min", &self.min)
.field("max", &self.max)
.field("buffer", &"_")
.field("font_system", &"_")
.finish()
}
} }
impl TextMeasureInfo { impl TextMeasureInfo {
@ -323,11 +314,13 @@ impl TextMeasureInfo {
pub fn compute_size( pub fn compute_size(
&mut self, &mut self,
bounds: TextBounds, bounds: TextBounds,
buffer: &mut Buffer,
font_system: &mut cosmic_text::FontSystem, font_system: &mut cosmic_text::FontSystem,
) -> Vec2 { ) -> Vec2 {
self.buffer // Note that this arbitrarily adjusts the buffer layout. We assume the buffer is always 'refreshed'
.set_size(font_system, bounds.width, bounds.height); // whenever a canonical state is required.
buffer_dimensions(&self.buffer) buffer.set_size(font_system, bounds.width, bounds.height);
buffer_dimensions(buffer)
} }
} }

View file

@ -12,7 +12,7 @@ use bevy_hierarchy::{Children, Parent};
use bevy_math::{UVec2, Vec2}; use bevy_math::{UVec2, Vec2};
use bevy_render::camera::{Camera, NormalizedRenderTarget}; use bevy_render::camera::{Camera, NormalizedRenderTarget};
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
use bevy_text::TextPipeline; use bevy_text::{CosmicBuffer, TextPipeline};
use bevy_transform::components::Transform; use bevy_transform::components::Transform;
use bevy_utils::tracing::warn; use bevy_utils::tracing::warn;
use bevy_utils::{HashMap, HashSet}; use bevy_utils::{HashMap, HashSet};
@ -95,6 +95,7 @@ pub fn ui_layout_system(
just_children_query: Query<&Children>, just_children_query: Query<&Children>,
mut removed_components: UiLayoutSystemRemovedComponentParam, mut removed_components: UiLayoutSystemRemovedComponentParam,
mut node_transform_query: Query<(&mut Node, &mut Transform)>, mut node_transform_query: Query<(&mut Node, &mut Transform)>,
#[cfg(feature = "bevy_text")] mut buffer_query: Query<&mut CosmicBuffer>,
#[cfg(feature = "bevy_text")] mut text_pipeline: ResMut<TextPipeline>, #[cfg(feature = "bevy_text")] mut text_pipeline: ResMut<TextPipeline>,
) { ) {
struct CameraLayoutInfo { struct CameraLayoutInfo {
@ -217,6 +218,8 @@ pub fn ui_layout_system(
} }
}); });
#[cfg(feature = "bevy_text")]
let text_buffers = &mut buffer_query;
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
let font_system = text_pipeline.font_system_mut(); let font_system = text_pipeline.font_system_mut();
// clean up removed nodes after syncing children to avoid potential panic (invalid SlotMap key used) // clean up removed nodes after syncing children to avoid potential panic (invalid SlotMap key used)
@ -236,6 +239,8 @@ pub fn ui_layout_system(
*camera_id, *camera_id,
camera.size, camera.size,
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
text_buffers,
#[cfg(feature = "bevy_text")]
font_system, font_system,
); );
for root in &camera.root_nodes { for root in &camera.root_nodes {

View file

@ -196,11 +196,14 @@ without UI components as a child of an entity with UI components, results may be
} }
/// Compute the layout for each window entity's corresponding root node in the layout. /// Compute the layout for each window entity's corresponding root node in the layout.
pub fn compute_camera_layout( pub fn compute_camera_layout<'a>(
&mut self, &mut self,
camera: Entity, camera: Entity,
render_target_resolution: UVec2, render_target_resolution: UVec2,
#[cfg(feature = "bevy_text")] font_system: &mut bevy_text::cosmic_text::FontSystem, #[cfg(feature = "bevy_text")] buffer_query: &'a mut bevy_ecs::prelude::Query<
&mut bevy_text::CosmicBuffer,
>,
#[cfg(feature = "bevy_text")] font_system: &'a mut bevy_text::cosmic_text::FontSystem,
) { ) {
let Some(camera_root_nodes) = self.camera_roots.get(&camera) else { let Some(camera_root_nodes) = self.camera_roots.get(&camera) else {
return; return;
@ -223,6 +226,15 @@ without UI components as a child of an entity with UI components, results may be
-> taffy::Size<f32> { -> taffy::Size<f32> {
context context
.map(|ctx| { .map(|ctx| {
#[cfg(feature = "bevy_text")]
let buffer = get_text_buffer(
crate::widget::TextMeasure::needs_buffer(
known_dimensions.height,
available_space.width,
),
ctx,
buffer_query,
);
let size = ctx.measure( let size = ctx.measure(
MeasureArgs { MeasureArgs {
width: known_dimensions.width, width: known_dimensions.width,
@ -231,6 +243,8 @@ without UI components as a child of an entity with UI components, results may be
available_height: available_space.height, available_height: available_space.height,
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
font_system, font_system,
#[cfg(feature = "bevy_text")]
buffer,
#[cfg(not(feature = "bevy_text"))] #[cfg(not(feature = "bevy_text"))]
font_system: std::marker::PhantomData, font_system: std::marker::PhantomData,
}, },
@ -284,3 +298,22 @@ with UI components as a child of an entity without UI components, results may be
} }
} }
} }
#[cfg(feature = "bevy_text")]
fn get_text_buffer<'a>(
needs_buffer: bool,
ctx: &mut NodeMeasure,
query: &'a mut bevy_ecs::prelude::Query<&mut bevy_text::CosmicBuffer>,
) -> Option<&'a mut bevy_text::cosmic_text::Buffer> {
// We avoid a query lookup whenever the buffer is not required.
if !needs_buffer {
return None;
}
let NodeMeasure::Text(crate::widget::TextMeasure { info }) = ctx else {
return None;
};
let Ok(buffer) = query.get_mut(info.entity) else {
return None;
};
Some(buffer.into_inner())
}

View file

@ -23,6 +23,8 @@ pub struct MeasureArgs<'a> {
pub available_height: AvailableSpace, pub available_height: AvailableSpace,
#[cfg(feature = "bevy_text")] #[cfg(feature = "bevy_text")]
pub font_system: &'a mut bevy_text::cosmic_text::FontSystem, pub font_system: &'a mut bevy_text::cosmic_text::FontSystem,
#[cfg(feature = "bevy_text")]
pub buffer: Option<&'a mut bevy_text::cosmic_text::Buffer>,
// When `bevy_text` is disabled, use `PhantomData` in order to keep lifetime in type signature. // When `bevy_text` is disabled, use `PhantomData` in order to keep lifetime in type signature.
#[cfg(not(feature = "bevy_text"))] #[cfg(not(feature = "bevy_text"))]
pub font_system: std::marker::PhantomData<&'a mut ()>, pub font_system: std::marker::PhantomData<&'a mut ()>,

View file

@ -19,7 +19,7 @@ use bevy_text::{
scale_value, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, JustifyText, Text, TextBounds, scale_value, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, JustifyText, Text, TextBounds,
TextError, TextLayoutInfo, TextMeasureInfo, TextPipeline, YAxisOrientation, TextError, TextLayoutInfo, TextMeasureInfo, TextPipeline, YAxisOrientation,
}; };
use bevy_utils::Entry; use bevy_utils::{tracing::error, Entry};
use taffy::style::AvailableSpace; use taffy::style::AvailableSpace;
/// Text system flags /// Text system flags
@ -47,12 +47,20 @@ pub struct TextMeasure {
pub info: TextMeasureInfo, pub info: TextMeasureInfo,
} }
impl TextMeasure {
/// Checks if the cosmic text buffer is needed for measuring the text.
pub fn needs_buffer(height: Option<f32>, available_width: AvailableSpace) -> bool {
height.is_none() && matches!(available_width, AvailableSpace::Definite(_))
}
}
impl Measure for TextMeasure { impl Measure for TextMeasure {
fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 { fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 {
let MeasureArgs { let MeasureArgs {
width, width,
height, height,
available_width, available_width,
buffer,
font_system, font_system,
.. ..
} = measure_args; } = measure_args;
@ -71,9 +79,18 @@ impl Measure for TextMeasure {
height height
.map_or_else( .map_or_else(
|| match available_width { || match available_width {
AvailableSpace::Definite(_) => self AvailableSpace::Definite(_) => {
.info if let Some(buffer) = buffer {
.compute_size(TextBounds::new_horizontal(x), font_system), self.info.compute_size(
TextBounds::new_horizontal(x),
buffer,
font_system,
)
} else {
error!("text measure failed, buffer is missing");
Vec2::default()
}
}
AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), AvailableSpace::MinContent => Vec2::new(x, self.info.min.y),
AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y),
}, },
@ -86,6 +103,7 @@ impl Measure for TextMeasure {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
#[inline] #[inline]
fn create_text_measure( fn create_text_measure(
entity: Entity,
fonts: &Assets<Font>, fonts: &Assets<Font>,
scale_factor: f64, scale_factor: f64,
text: Ref<Text>, text: Ref<Text>,
@ -96,6 +114,7 @@ fn create_text_measure(
text_alignment: JustifyText, text_alignment: JustifyText,
) { ) {
match text_pipeline.create_text_measure( match text_pipeline.create_text_measure(
entity,
fonts, fonts,
&text.sections, &text.sections,
scale_factor, scale_factor,
@ -141,6 +160,7 @@ pub fn measure_text_system(
ui_scale: Res<UiScale>, ui_scale: Res<UiScale>,
mut text_query: Query< mut text_query: Query<
( (
Entity,
Ref<Text>, Ref<Text>,
&mut ContentSize, &mut ContentSize,
&mut TextFlags, &mut TextFlags,
@ -153,7 +173,7 @@ pub fn measure_text_system(
) { ) {
let mut scale_factors: EntityHashMap<f32> = EntityHashMap::default(); let mut scale_factors: EntityHashMap<f32> = EntityHashMap::default();
for (text, content_size, text_flags, camera, mut buffer) in &mut text_query { for (entity, text, content_size, text_flags, camera, mut buffer) in &mut text_query {
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else { else {
continue; continue;
@ -176,6 +196,7 @@ pub fn measure_text_system(
{ {
let text_alignment = text.justify; let text_alignment = text.justify;
create_text_measure( create_text_measure(
entity,
&fonts, &fonts,
scale_factor.into(), scale_factor.into(),
text, text,