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:
		
							parent
							
								
									cacf3929db
								
							
						
					
					
						commit
						fa51e26052
					
				@ -123,7 +123,8 @@ impl Plugin for TextPlugin {
 | 
			
		||||
                        .ambiguous_with(CameraUpdateSystem),
 | 
			
		||||
                    remove_dropped_font_atlas_sets,
 | 
			
		||||
                ),
 | 
			
		||||
            );
 | 
			
		||||
            )
 | 
			
		||||
            .add_systems(Last, trim_cosmic_cache);
 | 
			
		||||
 | 
			
		||||
        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
 | 
			
		||||
            render_app.add_systems(
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,12 @@
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
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_reflect::{std_traits::ReflectDefault, Reflect};
 | 
			
		||||
use bevy_render::texture::Image;
 | 
			
		||||
@ -407,3 +412,13 @@ fn buffer_dimensions(buffer: &Buffer) -> Vec2 {
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,7 @@
 | 
			
		||||
//! Shows various text layout options.
 | 
			
		||||
 | 
			
		||||
use std::{collections::VecDeque, time::Duration};
 | 
			
		||||
 | 
			
		||||
use bevy::{
 | 
			
		||||
    color::palettes::css::*,
 | 
			
		||||
    diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin},
 | 
			
		||||
@ -154,7 +156,15 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
 | 
			
		||||
        builder.spawn((
 | 
			
		||||
            TextBundle::from_sections([
 | 
			
		||||
                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 {
 | 
			
		||||
                        font: font.clone(),
 | 
			
		||||
                        font_size: 25.0,
 | 
			
		||||
@ -223,10 +233,23 @@ fn infotext_system(mut commands: Commands, asset_server: Res<AssetServer>) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn change_text_system(
 | 
			
		||||
    mut fps_history: Local<VecDeque<f64>>,
 | 
			
		||||
    mut time_history: Local<VecDeque<Duration>>,
 | 
			
		||||
    time: Res<Time>,
 | 
			
		||||
    diagnostics: Res<DiagnosticsStore>,
 | 
			
		||||
    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 {
 | 
			
		||||
        let mut fps = 0.0;
 | 
			
		||||
        if let Some(fps_diagnostic) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
 | 
			
		||||
@ -244,12 +267,44 @@ fn change_text_system(
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        text.sections[0].value = format!(
 | 
			
		||||
            "This text changes in the bottom right - {fps:.1} fps, {frame_time:.3} ms/frame",
 | 
			
		||||
        text.sections[0].value =
 | 
			
		||||
            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,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user