Trim cosmic-text's shape run cache (#15037)

# Objective

- Fixes https://github.com/bevyengine/bevy/pull/14991. The `cosmic-text`
shape run cache requires manual cleanup for old text that no longer
needs to be cached.

## Solution

- Add a system to trim the cache.
- Add an `average fps` indicator to the `text_debug` example.

## Testing

Tested with `cargo run --example text_debug`.
- **No shape run cache**: 82fps with ~1fps variance.
- **Shape run cache no trim**: 90-100fps with ~2-4fps variance
- **Shape run cache trim age = 1**: 90-100fps with ~2-8fps variance
- **Shape run cache trim age = 2**: 90-100fps with ~2-4fps variance
- **Shape run cache trim age = 2000**: 80-120fps with ~2-6fps variance

The shape run cache seems to increase average FPS but also increases
frame time variance (when there is dynamic text).
This commit is contained in:
UkoeHB 2024-09-10 18:28:05 -05:00 committed by GitHub
parent cacf3929db
commit fa51e26052
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 78 additions and 7 deletions

View file

@ -123,7 +123,8 @@ impl Plugin for TextPlugin {
.ambiguous_with(CameraUpdateSystem), .ambiguous_with(CameraUpdateSystem),
remove_dropped_font_atlas_sets, remove_dropped_font_atlas_sets,
), ),
); )
.add_systems(Last, trim_cosmic_cache);
if let Some(render_app) = app.get_sub_app_mut(RenderApp) { if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.add_systems( render_app.add_systems(

View file

@ -1,7 +1,12 @@
use std::sync::Arc; use std::sync::Arc;
use bevy_asset::{AssetId, Assets}; use bevy_asset::{AssetId, Assets};
use bevy_ecs::{component::Component, entity::Entity, reflect::ReflectComponent, system::Resource}; use bevy_ecs::{
component::Component,
entity::Entity,
reflect::ReflectComponent,
system::{ResMut, 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;
@ -407,3 +412,13 @@ fn buffer_dimensions(buffer: &Buffer) -> Vec2 {
Vec2::new(width.ceil(), height).ceil() Vec2::new(width.ceil(), height).ceil()
} }
/// Discards stale data cached in `FontSystem`.
pub(crate) fn trim_cosmic_cache(mut pipeline: ResMut<TextPipeline>) {
// A trim age of 2 was found to reduce frame time variance vs age of 1 when tested with dynamic text.
// See https://github.com/bevyengine/bevy/pull/15037
//
// We assume only text updated frequently benefits from the shape cache (e.g. animated text, or
// text that is dynamically measured for UI).
pipeline.font_system_mut().shape_run_cache.trim(2);
}

View file

@ -1,5 +1,7 @@
//! Shows various text layout options. //! Shows various text layout options.
use std::{collections::VecDeque, time::Duration};
use bevy::{ use bevy::{
color::palettes::css::*, color::palettes::css::*,
diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin}, diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin},
@ -154,7 +156,15 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
builder.spawn(( builder.spawn((
TextBundle::from_sections([ TextBundle::from_sections([
TextSection::new( TextSection::new(
"This text changes in the bottom right", "",
TextStyle {
font: font.clone(),
font_size: 25.0,
..default()
},
),
TextSection::new(
"\nThis text changes in the bottom right",
TextStyle { TextStyle {
font: font.clone(), font: font.clone(),
font_size: 25.0, font_size: 25.0,
@ -223,10 +233,23 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
} }
fn change_text_system( fn change_text_system(
mut fps_history: Local<VecDeque<f64>>,
mut time_history: Local<VecDeque<Duration>>,
time: Res<Time>, time: Res<Time>,
diagnostics: Res<DiagnosticsStore>, diagnostics: Res<DiagnosticsStore>,
mut query: Query<&mut Text, With<TextChanges>>, mut query: Query<&mut Text, With<TextChanges>>,
) { ) {
time_history.push_front(time.elapsed());
time_history.truncate(120);
let avg_fps = (time_history.len() as f64)
/ (time_history.front().copied().unwrap_or_default()
- time_history.back().copied().unwrap_or_default())
.as_secs_f64()
.max(0.0001);
fps_history.push_front(avg_fps);
fps_history.truncate(120);
let fps_variance = std_deviation(fps_history.make_contiguous()).unwrap_or_default();
for mut text in &mut query { for mut text in &mut query {
let mut fps = 0.0; let mut fps = 0.0;
if let Some(fps_diagnostic) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) { if let Some(fps_diagnostic) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
@ -244,12 +267,44 @@ fn change_text_system(
} }
} }
text.sections[0].value = format!( text.sections[0].value =
"This text changes in the bottom right - {fps:.1} fps, {frame_time:.3} ms/frame", format!("{avg_fps:.1} avg fps, {fps_variance:.1} frametime variance",);
text.sections[1].value = format!(
"\nThis text changes in the bottom right - {fps:.1} fps, {frame_time:.3} ms/frame",
); );
text.sections[3].value = format!("{fps:.1}"); text.sections[4].value = format!("{fps:.1}");
text.sections[5].value = format!("{frame_time:.3}"); text.sections[6].value = format!("{frame_time:.3}");
}
}
fn mean(data: &[f64]) -> Option<f64> {
let sum = data.iter().sum::<f64>();
let count = data.len();
match count {
positive if positive > 0 => Some(sum / count as f64),
_ => None,
}
}
fn std_deviation(data: &[f64]) -> Option<f64> {
match (mean(data), data.len()) {
(Some(data_mean), count) if count > 0 => {
let variance = data
.iter()
.map(|value| {
let diff = data_mean - *value;
diff * diff
})
.sum::<f64>()
/ count as f64;
Some(variance.sqrt())
}
_ => None,
} }
} }