
# Objective - Rebase of https://github.com/bevyengine/bevy/pull/12561 , note that this is blocked on "up-streaming [iyes_perf_ui](https://crates.io/crates/iyes_perf_ui)" , but that work seems to also be stalled > Frame time is often more important to know than FPS but because of the temporal nature of it, just seeing a number is not enough. Seeing a graph that shows the history makes it easier to reason about performance. ## Solution > This PR adds a bar graph of the frame time history. > > Each bar is scaled based on the frame time where a bigger frame time will give a taller and wider bar. > > The color also scales with that frame time where red is at or bellow the minimum target fps and green is at or above the target maximum frame rate. Anything between those 2 values will be interpolated between green and red based on the frame time. > > The algorithm is highly inspired by this article: https://asawicki.info/news_1758_an_idea_for_visualization_of_frame_times ## Testing - Ran `cargo run --example fps_overlay --features="bevy_dev_tools"` --------- Co-authored-by: IceSentry <c.giguere42@gmail.com> Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
262 lines
8.5 KiB
Rust
262 lines
8.5 KiB
Rust
//! Module containing logic for FPS overlay.
|
|
|
|
use bevy_app::{Plugin, Startup, Update};
|
|
use bevy_asset::{Assets, Handle};
|
|
use bevy_color::Color;
|
|
use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
|
|
use bevy_ecs::{
|
|
change_detection::DetectChangesMut,
|
|
component::Component,
|
|
entity::Entity,
|
|
prelude::Local,
|
|
query::With,
|
|
resource::Resource,
|
|
schedule::{common_conditions::resource_changed, IntoScheduleConfigs},
|
|
system::{Commands, Query, Res, ResMut},
|
|
};
|
|
use bevy_render::{storage::ShaderStorageBuffer, view::Visibility};
|
|
use bevy_text::{Font, TextColor, TextFont, TextSpan};
|
|
use bevy_time::Time;
|
|
use bevy_ui::{
|
|
widget::{Text, TextUiWriter},
|
|
FlexDirection, GlobalZIndex, Node, PositionType, Val,
|
|
};
|
|
use bevy_ui_render::prelude::MaterialNode;
|
|
use core::time::Duration;
|
|
|
|
use crate::frame_time_graph::{
|
|
FrameTimeGraphConfigUniform, FrameTimeGraphPlugin, FrametimeGraphMaterial,
|
|
};
|
|
|
|
/// [`GlobalZIndex`] used to render the fps overlay.
|
|
///
|
|
/// We use a number slightly under `i32::MAX` so you can render on top of it if you really need to.
|
|
pub const FPS_OVERLAY_ZINDEX: i32 = i32::MAX - 32;
|
|
|
|
// Used to scale the frame time graph based on the fps text size
|
|
const FRAME_TIME_GRAPH_WIDTH_SCALE: f32 = 6.0;
|
|
const FRAME_TIME_GRAPH_HEIGHT_SCALE: f32 = 2.0;
|
|
|
|
/// A plugin that adds an FPS overlay to the Bevy application.
|
|
///
|
|
/// This plugin will add the [`FrameTimeDiagnosticsPlugin`] if it wasn't added before.
|
|
///
|
|
/// Note: It is recommended to use native overlay of rendering statistics when possible for lower overhead and more accurate results.
|
|
/// The correct way to do this will vary by platform:
|
|
/// - **Metal**: setting env variable `MTL_HUD_ENABLED=1`
|
|
#[derive(Default)]
|
|
pub struct FpsOverlayPlugin {
|
|
/// Starting configuration of overlay, this can be later be changed through [`FpsOverlayConfig`] resource.
|
|
pub config: FpsOverlayConfig,
|
|
}
|
|
|
|
impl Plugin for FpsOverlayPlugin {
|
|
fn build(&self, app: &mut bevy_app::App) {
|
|
// TODO: Use plugin dependencies, see https://github.com/bevyengine/bevy/issues/69
|
|
if !app.is_plugin_added::<FrameTimeDiagnosticsPlugin>() {
|
|
app.add_plugins(FrameTimeDiagnosticsPlugin::default());
|
|
}
|
|
|
|
if !app.is_plugin_added::<FrameTimeGraphPlugin>() {
|
|
app.add_plugins(FrameTimeGraphPlugin);
|
|
}
|
|
|
|
app.insert_resource(self.config.clone())
|
|
.add_systems(Startup, setup)
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
(toggle_display, customize_overlay)
|
|
.run_if(resource_changed::<FpsOverlayConfig>),
|
|
update_text,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Configuration options for the FPS overlay.
|
|
#[derive(Resource, Clone)]
|
|
pub struct FpsOverlayConfig {
|
|
/// Configuration of text in the overlay.
|
|
pub text_config: TextFont,
|
|
/// Color of text in the overlay.
|
|
pub text_color: Color,
|
|
/// Displays the FPS overlay if true.
|
|
pub enabled: bool,
|
|
/// The period after which the FPS overlay re-renders.
|
|
///
|
|
/// Defaults to once every 100 ms.
|
|
pub refresh_interval: Duration,
|
|
/// Configuration of the frame time graph
|
|
pub frame_time_graph_config: FrameTimeGraphConfig,
|
|
}
|
|
|
|
impl Default for FpsOverlayConfig {
|
|
fn default() -> Self {
|
|
FpsOverlayConfig {
|
|
text_config: TextFont {
|
|
font: Handle::<Font>::default(),
|
|
font_size: 32.0,
|
|
..Default::default()
|
|
},
|
|
text_color: Color::WHITE,
|
|
enabled: true,
|
|
refresh_interval: Duration::from_millis(100),
|
|
// TODO set this to display refresh rate if possible
|
|
frame_time_graph_config: FrameTimeGraphConfig::target_fps(60.0),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Configuration of the frame time graph
|
|
#[derive(Clone, Copy)]
|
|
pub struct FrameTimeGraphConfig {
|
|
/// Is the graph visible
|
|
pub enabled: bool,
|
|
/// The minimum acceptable FPS
|
|
///
|
|
/// Anything below this will show a red bar
|
|
pub min_fps: f32,
|
|
/// The target FPS
|
|
///
|
|
/// Anything above this will show a green bar
|
|
pub target_fps: f32,
|
|
}
|
|
|
|
impl FrameTimeGraphConfig {
|
|
/// Constructs a default config for a given target fps
|
|
pub fn target_fps(target_fps: f32) -> Self {
|
|
Self {
|
|
target_fps,
|
|
..Self::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for FrameTimeGraphConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
enabled: true,
|
|
min_fps: 30.0,
|
|
target_fps: 60.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct FpsText;
|
|
|
|
#[derive(Component)]
|
|
struct FrameTimeGraph;
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
overlay_config: Res<FpsOverlayConfig>,
|
|
mut frame_time_graph_materials: ResMut<Assets<FrametimeGraphMaterial>>,
|
|
mut buffers: ResMut<Assets<ShaderStorageBuffer>>,
|
|
) {
|
|
commands
|
|
.spawn((
|
|
Node {
|
|
// We need to make sure the overlay doesn't affect the position of other UI nodes
|
|
position_type: PositionType::Absolute,
|
|
flex_direction: FlexDirection::Column,
|
|
..Default::default()
|
|
},
|
|
// Render overlay on top of everything
|
|
GlobalZIndex(FPS_OVERLAY_ZINDEX),
|
|
))
|
|
.with_children(|p| {
|
|
p.spawn((
|
|
Text::new("FPS: "),
|
|
overlay_config.text_config.clone(),
|
|
TextColor(overlay_config.text_color),
|
|
FpsText,
|
|
))
|
|
.with_child((TextSpan::default(), overlay_config.text_config.clone()));
|
|
|
|
let font_size = overlay_config.text_config.font_size;
|
|
p.spawn((
|
|
Node {
|
|
width: Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE),
|
|
height: Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE),
|
|
display: if overlay_config.frame_time_graph_config.enabled {
|
|
bevy_ui::Display::DEFAULT
|
|
} else {
|
|
bevy_ui::Display::None
|
|
},
|
|
..Default::default()
|
|
},
|
|
MaterialNode::from(frame_time_graph_materials.add(FrametimeGraphMaterial {
|
|
values: buffers.add(ShaderStorageBuffer::default()),
|
|
config: FrameTimeGraphConfigUniform::new(
|
|
overlay_config.frame_time_graph_config.target_fps,
|
|
overlay_config.frame_time_graph_config.min_fps,
|
|
true,
|
|
),
|
|
})),
|
|
FrameTimeGraph,
|
|
));
|
|
});
|
|
}
|
|
|
|
fn update_text(
|
|
diagnostic: Res<DiagnosticsStore>,
|
|
query: Query<Entity, With<FpsText>>,
|
|
mut writer: TextUiWriter,
|
|
time: Res<Time>,
|
|
config: Res<FpsOverlayConfig>,
|
|
mut time_since_rerender: Local<Duration>,
|
|
) {
|
|
*time_since_rerender += time.delta();
|
|
if *time_since_rerender >= config.refresh_interval {
|
|
*time_since_rerender = Duration::ZERO;
|
|
for entity in &query {
|
|
if let Some(fps) = diagnostic.get(&FrameTimeDiagnosticsPlugin::FPS) {
|
|
if let Some(value) = fps.smoothed() {
|
|
*writer.text(entity, 1) = format!("{value:.2}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn customize_overlay(
|
|
overlay_config: Res<FpsOverlayConfig>,
|
|
query: Query<Entity, With<FpsText>>,
|
|
mut writer: TextUiWriter,
|
|
) {
|
|
for entity in &query {
|
|
writer.for_each_font(entity, |mut font| {
|
|
*font = overlay_config.text_config.clone();
|
|
});
|
|
writer.for_each_color(entity, |mut color| color.0 = overlay_config.text_color);
|
|
}
|
|
}
|
|
|
|
fn toggle_display(
|
|
overlay_config: Res<FpsOverlayConfig>,
|
|
mut query: Query<&mut Visibility, With<FpsText>>,
|
|
mut graph_style: Query<&mut Node, With<FrameTimeGraph>>,
|
|
) {
|
|
for mut visibility in &mut query {
|
|
visibility.set_if_neq(match overlay_config.enabled {
|
|
true => Visibility::Visible,
|
|
false => Visibility::Hidden,
|
|
});
|
|
}
|
|
|
|
if let Ok(mut graph_style) = graph_style.single_mut() {
|
|
if overlay_config.frame_time_graph_config.enabled {
|
|
// Scale the frame time graph based on the font size of the overlay
|
|
let font_size = overlay_config.text_config.font_size;
|
|
graph_style.width = Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE);
|
|
graph_style.height = Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE);
|
|
|
|
graph_style.display = bevy_ui::Display::DEFAULT;
|
|
} else {
|
|
graph_style.display = bevy_ui::Display::None;
|
|
}
|
|
}
|
|
}
|