mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
add frame_time graph to fps_overlay
This commit is contained in:
parent
ed44eb3913
commit
d68ff02db7
5 changed files with 288 additions and 9 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
103
crates/bevy_dev_tools/src/frame_time_graph/mod.rs
Normal file
103
crates/bevy_dev_tools/src/frame_time_graph/mod.rs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue