add frame_time graph to fps_overlay

This commit is contained in:
IceSentry 2024-03-20 14:07:50 -04:00
parent ed44eb3913
commit d68ff02db7
5 changed files with 288 additions and 9 deletions

View file

@ -1,28 +1,36 @@
//! Module containing logic for FPS overlay.
use bevy_app::{Plugin, Startup, Update};
use bevy_asset::Handle;
use bevy_asset::{Assets, Handle};
use bevy_color::Color;
use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
use bevy_ecs::{
component::Component,
query::With,
schedule::{common_conditions::resource_changed, IntoSystemConfigs},
system::{Commands, Query, Res, Resource},
system::{Commands, Query, Res, ResMut, Resource},
};
use bevy_hierarchy::BuildChildren;
use bevy_text::{Font, Text, TextSection, TextStyle};
use bevy_ui::{
node_bundles::{NodeBundle, TextBundle},
PositionType, Style, ZIndex,
node_bundles::{MaterialNodeBundle, NodeBundle, TextBundle},
FlexDirection, PositionType, Style, Val, ZIndex,
};
use bevy_utils::default;
use crate::frame_time_graph::{
FrameTimeGraphConfigUniform, FrameTimeGraphPlugin, FrametimeGraphMaterial,
};
/// Global [`ZIndex`] 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.
@ -42,12 +50,17 @@ impl Plugin for FpsOverlayPlugin {
if !app.is_plugin_added::<FrameTimeDiagnosticsPlugin>() {
app.add_plugins(FrameTimeDiagnosticsPlugin);
}
if !app.is_plugin_added::<FrameTimeGraphPlugin>() {
app.add_plugins(FrameTimeGraphPlugin);
}
app.insert_resource(self.config.clone())
.add_systems(Startup, setup)
.add_systems(
Update,
(
customize_text.run_if(resource_changed::<FpsOverlayConfig>),
customize_overlay.run_if(resource_changed::<FpsOverlayConfig>),
update_text,
),
);
@ -59,6 +72,8 @@ impl Plugin for FpsOverlayPlugin {
pub struct FpsOverlayConfig {
/// Configuration of text in the overlay.
pub text_config: TextStyle,
/// Configuration of the frame time graph
pub frame_time_graph_config: FrameTimeGraphConfig,
}
impl Default for FpsOverlayConfig {
@ -69,6 +84,43 @@ impl Default for FpsOverlayConfig {
font_size: 32.0,
color: Color::WHITE,
},
// 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 bellow 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,
}
}
}
@ -76,12 +128,20 @@ impl Default for FpsOverlayConfig {
#[derive(Component)]
struct FpsText;
fn setup(mut commands: Commands, overlay_config: Res<FpsOverlayConfig>) {
#[derive(Component)]
struct FrameTimeGraph;
fn setup(
mut commands: Commands,
overlay_config: Res<FpsOverlayConfig>,
mut frame_time_graph_materials: ResMut<Assets<FrametimeGraphMaterial>>,
) {
commands
.spawn(NodeBundle {
style: Style {
// 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()
},
// Render overlay on top of everything
@ -96,6 +156,28 @@ fn setup(mut commands: Commands, overlay_config: Res<FpsOverlayConfig>) {
]),
FpsText,
));
if overlay_config.frame_time_graph_config.enabled {
let font_size = overlay_config.text_config.font_size;
c.spawn((
MaterialNodeBundle {
style: Style {
width: Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE),
height: Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE),
..default()
},
material: frame_time_graph_materials.add(FrametimeGraphMaterial {
values: vec![],
config: FrameTimeGraphConfigUniform::new(
overlay_config.frame_time_graph_config.target_fps,
overlay_config.frame_time_graph_config.min_fps,
true,
),
}),
..default()
},
FrameTimeGraph,
));
}
});
}
@ -109,13 +191,27 @@ fn update_text(diagnostic: Res<DiagnosticsStore>, mut query: Query<&mut Text, Wi
}
}
fn customize_text(
fn customize_overlay(
overlay_config: Res<FpsOverlayConfig>,
mut query: Query<&mut Text, With<FpsText>>,
mut graph_style: Query<&mut Style, With<FrameTimeGraph>>,
) {
for mut text in &mut query {
for section in text.sections.iter_mut() {
section.style = overlay_config.text_config.clone();
}
}
if let Ok(mut graph_style) = graph_style.get_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;
}
}
}

View file

@ -0,0 +1,68 @@
#import bevy_ui::ui_vertex_output::UiVertexOutput
@group(1) @binding(0) var<storage> values: array<f32>;
struct Config {
dt_min: f32,
dt_max: f32,
dt_min_log2: f32,
dt_max_log2: f32,
proportional_width: u32,
}
@group(1) @binding(1) var<uniform> config: Config;
const RED: vec4<f32> = vec4(1.0, 0.0, 0.0, 1.0);
const GREEN: vec4<f32> = vec4(0.0, 1.0, 0.0, 1.0);
// Gets a color based on the delta time
// TODO use customizable gradient
fn color_from_dt(dt: f32) -> vec4<f32> {
return mix(GREEN, RED, dt / config.dt_max);
}
// Draw an SDF square
fn sdf_square(pos: vec2<f32>, half_size: vec2<f32>, offset: vec2<f32>) -> f32 {
let p = pos - offset;
let dist = abs(p) - half_size;
let outside_dist = length(max(dist, vec2<f32>(0.0, 0.0)));
let inside_dist = min(max(dist.x, dist.y), 0.0);
return outside_dist + inside_dist;
}
@fragment
fn fragment(in: UiVertexOutput) -> @location(0) vec4<f32> {
let dt_min = config.dt_min;
let dt_max = config.dt_max;
let dt_min_log2 = config.dt_min_log2;
let dt_max_log2 = config.dt_max_log2;
// The general algorithm is highly inspired by
// <https://asawicki.info/news_1758_an_idea_for_visualization_of_frame_times>
let len = arrayLength(&values);
var graph_width = 0.0;
for (var i = 0u; i <= len; i += 1u) {
let dt = values[len - i];
var frame_width: f32;
if config.proportional_width == 1u {
frame_width = (dt / dt_min) / f32(len);
} else {
frame_width = 0.015;
}
let frame_height_factor = (log2(dt) - dt_min_log2) / (dt_max_log2 - dt_min_log2);
let frame_height_factor_norm = min(max(0.0, frame_height_factor), 1.0);
let frame_height = mix(0.0, 1.0, frame_height_factor_norm);
let size = vec2(frame_width, frame_height) / 2.0;
let offset = vec2(1.0 - graph_width - size.x, 1. - size.y);
if (sdf_square(in.uv, size, offset) < 0.0) {
return color_from_dt(dt);
}
graph_width += frame_width;
}
return vec4(0.0, 0.0, 0.0, 0.5);
}

View file

@ -0,0 +1,103 @@
//! Module containing logic for the frame time graph
use bevy_app::{Plugin, Update};
use bevy_asset::{load_internal_asset, Asset, Assets, Handle};
use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
use bevy_ecs::system::{Res, ResMut};
use bevy_reflect::TypePath;
use bevy_render::render_resource::{AsBindGroup, Shader, ShaderRef, ShaderType};
use bevy_ui::{UiMaterial, UiMaterialPlugin};
use crate::fps_overlay::FpsOverlayConfig;
const FRAME_TIME_GRAPH_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(2325577683959808);
/// Plugin that sets up everything to render the frame time graph material
pub struct FrameTimeGraphPlugin;
impl Plugin for FrameTimeGraphPlugin {
fn build(&self, app: &mut bevy_app::App) {
load_internal_asset!(
app,
FRAME_TIME_GRAPH_SHADER_HANDLE,
"frame_time_graph.wgsl",
Shader::from_wgsl
);
// TODO: Use plugin dependencies, see https://github.com/bevyengine/bevy/issues/69
if !app.is_plugin_added::<FrameTimeDiagnosticsPlugin>() {
app.add_plugins(FrameTimeDiagnosticsPlugin);
}
app.add_plugins(UiMaterialPlugin::<FrametimeGraphMaterial>::default())
.add_systems(Update, update_frame_time_values);
}
}
/// The config values sent to the frame time graph shader
#[derive(Debug, Clone, Copy, ShaderType)]
pub struct FrameTimeGraphConfigUniform {
// minimum expected delta time
dt_min: f32,
// maximum expected delta time
dt_max: f32,
dt_min_log2: f32,
dt_max_log2: f32,
// controls whether or not the bars width are proportional to their delta time
proportional_width: u32,
}
impl FrameTimeGraphConfigUniform {
/// `proportional_width`: controls whether or not the bars width are proportional to their delta time
pub fn new(target_fps: f32, min_fps: f32, proportional_width: bool) -> Self {
// we want an upper limit that is above the target otherwise the bars will disappear
let dt_min = 1. / (target_fps * 1.2);
let dt_max = 1. / min_fps;
Self {
dt_min,
dt_max,
dt_min_log2: dt_min.log2(),
dt_max_log2: dt_max.log2(),
proportional_width: u32::from(proportional_width),
}
}
}
/// The material used to render the frame time graph ui node
#[derive(AsBindGroup, Asset, TypePath, Debug, Clone)]
pub struct FrametimeGraphMaterial {
/// The history of the previous frame times value.
///
/// This should be updated every frame to match the frame time history from the [`DiagnosticsStore`]
#[storage(0, read_only)]
pub values: Vec<f32>,
/// The configuration values used by the shader to control how the graph is rendered
#[uniform(1)]
pub config: FrameTimeGraphConfigUniform,
}
impl UiMaterial for FrametimeGraphMaterial {
fn fragment_shader() -> ShaderRef {
FRAME_TIME_GRAPH_SHADER_HANDLE.into()
}
}
/// A system that updates the frame time values sent to the frame time graph
fn update_frame_time_values(
mut frame_time_graph_materials: ResMut<Assets<FrametimeGraphMaterial>>,
diagnostics_store: Res<DiagnosticsStore>,
config: Option<Res<FpsOverlayConfig>>,
) {
if !config.map_or(true, |c| c.frame_time_graph_config.enabled) {
return;
}
let Some(frame_time) = diagnostics_store.get(&FrameTimeDiagnosticsPlugin::FRAME_TIME) else {
return;
};
let frame_times = frame_time
.values()
// convert to millis
.map(|x| *x as f32 / 1000.0)
.collect::<Vec<_>>();
for (_, material) in frame_time_graph_materials.iter_mut() {
material.values = frame_times.clone();
}
}

View file

@ -7,6 +7,7 @@ use bevy_app::prelude::*;
#[cfg(feature = "bevy_ci_testing")]
pub mod ci_testing;
pub mod fps_overlay;
pub mod frame_time_graph;
#[cfg(feature = "bevy_ui_debug")]
pub mod debug_overlay;

View file

@ -1,7 +1,7 @@
//! Showcase how to use and configure FPS overlay.
use bevy::{
dev_tools::fps_overlay::{FpsOverlayConfig, FpsOverlayPlugin},
dev_tools::fps_overlay::{FpsOverlayConfig, FpsOverlayPlugin, FrameTimeGraphConfig},
prelude::*,
};
@ -19,6 +19,13 @@ fn main() {
// If we want, we can use a custom font
font: default(),
},
frame_time_graph_config: FrameTimeGraphConfig {
enabled: true,
// The minimum acceptable fps
min_fps: 30.0,
// The target fps
target_fps: 144.0,
},
},
},
))
@ -47,7 +54,8 @@ fn setup(mut commands: Commands) {
c.spawn(TextBundle::from_section(
concat!(
"Press 1 to change color of the overlay.\n",
"Press 2 to change size of the overlay."
"Press 2 to change size of the overlay.\n",
"Press 3 to toggle the frame time graph."
),
TextStyle {
font_size: 25.0,
@ -65,4 +73,7 @@ fn customize_config(input: Res<ButtonInput<KeyCode>>, mut overlay: ResMut<FpsOve
if input.just_pressed(KeyCode::Digit2) {
overlay.text_config.font_size -= 2.0;
}
if input.just_released(KeyCode::Digit3) {
overlay.frame_time_graph_config.enabled = !overlay.frame_time_graph_config.enabled;
}
}