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 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_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::texture::Image;
@ -238,8 +238,10 @@ impl TextPipeline {
///
/// Produces a [`TextMeasureInfo`] which can be used by a layout system
/// to measure the text area on demand.
#[allow(clippy::too_many_arguments)]
pub fn create_text_measure(
&mut self,
entity: Entity,
fonts: &Assets<Font>,
sections: &[TextSection],
scale_factor: f64,
@ -270,9 +272,7 @@ impl TextPipeline {
Ok(TextMeasureInfo {
min: min_width_content_size,
max: max_width_content_size,
// TODO: This clone feels wasteful, is there another way to structure TextMeasureInfo
// that it doesn't need to own a buffer? - bytemunch
buffer: buffer.0.clone(),
entity,
})
}
@ -299,23 +299,14 @@ pub struct TextLayoutInfo {
/// Size information for a corresponding [`Text`](crate::Text) component.
///
/// Generated via [`TextPipeline::create_text_measure`].
#[derive(Debug)]
pub struct TextMeasureInfo {
/// Minimum size for a text area in pixels, to be used when laying out widgets with taffy
pub min: Vec2,
/// Maximum size for a text area in pixels, to be used when laying out widgets with taffy
pub max: Vec2,
buffer: Buffer,
}
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()
}
/// The entity that is measured.
pub entity: Entity,
}
impl TextMeasureInfo {
@ -323,11 +314,13 @@ impl TextMeasureInfo {
pub fn compute_size(
&mut self,
bounds: TextBounds,
buffer: &mut Buffer,
font_system: &mut cosmic_text::FontSystem,
) -> Vec2 {
self.buffer
.set_size(font_system, bounds.width, bounds.height);
buffer_dimensions(&self.buffer)
// Note that this arbitrarily adjusts the buffer layout. We assume the buffer is always 'refreshed'
// whenever a canonical state is required.
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_render::camera::{Camera, NormalizedRenderTarget};
#[cfg(feature = "bevy_text")]
use bevy_text::TextPipeline;
use bevy_text::{CosmicBuffer, TextPipeline};
use bevy_transform::components::Transform;
use bevy_utils::tracing::warn;
use bevy_utils::{HashMap, HashSet};
@ -95,6 +95,7 @@ pub fn ui_layout_system(
just_children_query: Query<&Children>,
mut removed_components: UiLayoutSystemRemovedComponentParam,
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>,
) {
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")]
let font_system = text_pipeline.font_system_mut();
// 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.size,
#[cfg(feature = "bevy_text")]
text_buffers,
#[cfg(feature = "bevy_text")]
font_system,
);
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.
pub fn compute_camera_layout(
pub fn compute_camera_layout<'a>(
&mut self,
camera: Entity,
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 {
return;
@ -223,6 +226,15 @@ without UI components as a child of an entity with UI components, results may be
-> taffy::Size<f32> {
context
.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(
MeasureArgs {
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,
#[cfg(feature = "bevy_text")]
font_system,
#[cfg(feature = "bevy_text")]
buffer,
#[cfg(not(feature = "bevy_text"))]
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,
#[cfg(feature = "bevy_text")]
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.
#[cfg(not(feature = "bevy_text"))]
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,
TextError, TextLayoutInfo, TextMeasureInfo, TextPipeline, YAxisOrientation,
};
use bevy_utils::Entry;
use bevy_utils::{tracing::error, Entry};
use taffy::style::AvailableSpace;
/// Text system flags
@ -47,12 +47,20 @@ pub struct TextMeasure {
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 {
fn measure(&mut self, measure_args: MeasureArgs, _style: &taffy::Style) -> Vec2 {
let MeasureArgs {
width,
height,
available_width,
buffer,
font_system,
..
} = measure_args;
@ -71,9 +79,18 @@ impl Measure for TextMeasure {
height
.map_or_else(
|| match available_width {
AvailableSpace::Definite(_) => self
.info
.compute_size(TextBounds::new_horizontal(x), font_system),
AvailableSpace::Definite(_) => {
if let Some(buffer) = buffer {
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::MaxContent => Vec2::new(x, self.info.max.y),
},
@ -86,6 +103,7 @@ impl Measure for TextMeasure {
#[allow(clippy::too_many_arguments)]
#[inline]
fn create_text_measure(
entity: Entity,
fonts: &Assets<Font>,
scale_factor: f64,
text: Ref<Text>,
@ -96,6 +114,7 @@ fn create_text_measure(
text_alignment: JustifyText,
) {
match text_pipeline.create_text_measure(
entity,
fonts,
&text.sections,
scale_factor,
@ -141,6 +160,7 @@ pub fn measure_text_system(
ui_scale: Res<UiScale>,
mut text_query: Query<
(
Entity,
Ref<Text>,
&mut ContentSize,
&mut TextFlags,
@ -153,7 +173,7 @@ pub fn measure_text_system(
) {
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())
else {
continue;
@ -176,6 +196,7 @@ pub fn measure_text_system(
{
let text_alignment = text.justify;
create_text_measure(
entity,
&fonts,
scale_factor.into(),
text,