mirror of
https://github.com/bevyengine/bevy
synced 2025-02-17 22:48:38 +00:00
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:
parent
f0560b8e78
commit
41474226c3
5 changed files with 81 additions and 27 deletions
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
|
|
@ -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 ()>,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Reference in a new issue