Skip to content

Commit d45ae74

Browse files
ZeophliteIceSentryalice-i-cecile
authored
Add frame_time graph to fps_overlay v2 (#19277)
# Objective - Rebase of #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>
1 parent d40c5b5 commit d45ae74

File tree

7 files changed

+323
-9
lines changed

7 files changed

+323
-9
lines changed

crates/bevy_dev_tools/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" }
1818
bevy_color = { path = "../bevy_color", version = "0.17.0-dev" }
1919
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" }
2020
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
21+
bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
2122
bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" }
2223
bevy_render = { path = "../bevy_render", version = "0.17.0-dev" }
2324
bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" }
2425
bevy_time = { path = "../bevy_time", version = "0.17.0-dev" }
2526
bevy_text = { path = "../bevy_text", version = "0.17.0-dev" }
2627
bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev" }
28+
bevy_ui_render = { path = "../bevy_ui_render", version = "0.17.0-dev" }
2729
bevy_window = { path = "../bevy_window", version = "0.17.0-dev" }
2830
bevy_state = { path = "../bevy_state", version = "0.17.0-dev" }
2931

crates/bevy_dev_tools/src/fps_overlay.rs

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Module containing logic for FPS overlay.
22
33
use bevy_app::{Plugin, Startup, Update};
4-
use bevy_asset::Handle;
4+
use bevy_asset::{Assets, Handle};
55
use bevy_color::Color;
66
use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
77
use bevy_ecs::{
@@ -12,22 +12,31 @@ use bevy_ecs::{
1212
query::With,
1313
resource::Resource,
1414
schedule::{common_conditions::resource_changed, IntoScheduleConfigs},
15-
system::{Commands, Query, Res},
15+
system::{Commands, Query, Res, ResMut},
1616
};
17-
use bevy_render::view::Visibility;
17+
use bevy_render::{storage::ShaderStorageBuffer, view::Visibility};
1818
use bevy_text::{Font, TextColor, TextFont, TextSpan};
1919
use bevy_time::Time;
2020
use bevy_ui::{
2121
widget::{Text, TextUiWriter},
22-
GlobalZIndex, Node, PositionType,
22+
FlexDirection, GlobalZIndex, Node, PositionType, Val,
2323
};
24+
use bevy_ui_render::prelude::MaterialNode;
2425
use core::time::Duration;
2526

27+
use crate::frame_time_graph::{
28+
FrameTimeGraphConfigUniform, FrameTimeGraphPlugin, FrametimeGraphMaterial,
29+
};
30+
2631
/// [`GlobalZIndex`] used to render the fps overlay.
2732
///
2833
/// We use a number slightly under `i32::MAX` so you can render on top of it if you really need to.
2934
pub const FPS_OVERLAY_ZINDEX: i32 = i32::MAX - 32;
3035

36+
// Used to scale the frame time graph based on the fps text size
37+
const FRAME_TIME_GRAPH_WIDTH_SCALE: f32 = 6.0;
38+
const FRAME_TIME_GRAPH_HEIGHT_SCALE: f32 = 2.0;
39+
3140
/// A plugin that adds an FPS overlay to the Bevy application.
3241
///
3342
/// This plugin will add the [`FrameTimeDiagnosticsPlugin`] if it wasn't added before.
@@ -47,12 +56,18 @@ impl Plugin for FpsOverlayPlugin {
4756
if !app.is_plugin_added::<FrameTimeDiagnosticsPlugin>() {
4857
app.add_plugins(FrameTimeDiagnosticsPlugin::default());
4958
}
59+
60+
if !app.is_plugin_added::<FrameTimeGraphPlugin>() {
61+
app.add_plugins(FrameTimeGraphPlugin);
62+
}
63+
5064
app.insert_resource(self.config.clone())
5165
.add_systems(Startup, setup)
5266
.add_systems(
5367
Update,
5468
(
55-
(customize_text, toggle_display).run_if(resource_changed::<FpsOverlayConfig>),
69+
(toggle_display, customize_overlay)
70+
.run_if(resource_changed::<FpsOverlayConfig>),
5671
update_text,
5772
),
5873
);
@@ -72,6 +87,8 @@ pub struct FpsOverlayConfig {
7287
///
7388
/// Defaults to once every 100 ms.
7489
pub refresh_interval: Duration,
90+
/// Configuration of the frame time graph
91+
pub frame_time_graph_config: FrameTimeGraphConfig,
7592
}
7693

7794
impl Default for FpsOverlayConfig {
@@ -85,19 +102,65 @@ impl Default for FpsOverlayConfig {
85102
text_color: Color::WHITE,
86103
enabled: true,
87104
refresh_interval: Duration::from_millis(100),
105+
// TODO set this to display refresh rate if possible
106+
frame_time_graph_config: FrameTimeGraphConfig::target_fps(60.0),
107+
}
108+
}
109+
}
110+
111+
/// Configuration of the frame time graph
112+
#[derive(Clone, Copy)]
113+
pub struct FrameTimeGraphConfig {
114+
/// Is the graph visible
115+
pub enabled: bool,
116+
/// The minimum acceptable FPS
117+
///
118+
/// Anything below this will show a red bar
119+
pub min_fps: f32,
120+
/// The target FPS
121+
///
122+
/// Anything above this will show a green bar
123+
pub target_fps: f32,
124+
}
125+
126+
impl FrameTimeGraphConfig {
127+
/// Constructs a default config for a given target fps
128+
pub fn target_fps(target_fps: f32) -> Self {
129+
Self {
130+
target_fps,
131+
..Self::default()
132+
}
133+
}
134+
}
135+
136+
impl Default for FrameTimeGraphConfig {
137+
fn default() -> Self {
138+
Self {
139+
enabled: true,
140+
min_fps: 30.0,
141+
target_fps: 60.0,
88142
}
89143
}
90144
}
91145

92146
#[derive(Component)]
93147
struct FpsText;
94148

95-
fn setup(mut commands: Commands, overlay_config: Res<FpsOverlayConfig>) {
149+
#[derive(Component)]
150+
struct FrameTimeGraph;
151+
152+
fn setup(
153+
mut commands: Commands,
154+
overlay_config: Res<FpsOverlayConfig>,
155+
mut frame_time_graph_materials: ResMut<Assets<FrametimeGraphMaterial>>,
156+
mut buffers: ResMut<Assets<ShaderStorageBuffer>>,
157+
) {
96158
commands
97159
.spawn((
98160
Node {
99161
// We need to make sure the overlay doesn't affect the position of other UI nodes
100162
position_type: PositionType::Absolute,
163+
flex_direction: FlexDirection::Column,
101164
..Default::default()
102165
},
103166
// Render overlay on top of everything
@@ -111,6 +174,29 @@ fn setup(mut commands: Commands, overlay_config: Res<FpsOverlayConfig>) {
111174
FpsText,
112175
))
113176
.with_child((TextSpan::default(), overlay_config.text_config.clone()));
177+
178+
let font_size = overlay_config.text_config.font_size;
179+
p.spawn((
180+
Node {
181+
width: Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE),
182+
height: Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE),
183+
display: if overlay_config.frame_time_graph_config.enabled {
184+
bevy_ui::Display::DEFAULT
185+
} else {
186+
bevy_ui::Display::None
187+
},
188+
..Default::default()
189+
},
190+
MaterialNode::from(frame_time_graph_materials.add(FrametimeGraphMaterial {
191+
values: buffers.add(ShaderStorageBuffer::default()),
192+
config: FrameTimeGraphConfigUniform::new(
193+
overlay_config.frame_time_graph_config.target_fps,
194+
overlay_config.frame_time_graph_config.min_fps,
195+
true,
196+
),
197+
})),
198+
FrameTimeGraph,
199+
));
114200
});
115201
}
116202

@@ -135,7 +221,7 @@ fn update_text(
135221
}
136222
}
137223

138-
fn customize_text(
224+
fn customize_overlay(
139225
overlay_config: Res<FpsOverlayConfig>,
140226
query: Query<Entity, With<FpsText>>,
141227
mut writer: TextUiWriter,
@@ -151,11 +237,25 @@ fn customize_text(
151237
fn toggle_display(
152238
overlay_config: Res<FpsOverlayConfig>,
153239
mut query: Query<&mut Visibility, With<FpsText>>,
240+
mut graph_style: Query<&mut Node, With<FrameTimeGraph>>,
154241
) {
155242
for mut visibility in &mut query {
156243
visibility.set_if_neq(match overlay_config.enabled {
157244
true => Visibility::Visible,
158245
false => Visibility::Hidden,
159246
});
160247
}
248+
249+
if let Ok(mut graph_style) = graph_style.single_mut() {
250+
if overlay_config.frame_time_graph_config.enabled {
251+
// Scale the frame time graph based on the font size of the overlay
252+
let font_size = overlay_config.text_config.font_size;
253+
graph_style.width = Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE);
254+
graph_style.height = Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE);
255+
256+
graph_style.display = bevy_ui::Display::DEFAULT;
257+
} else {
258+
graph_style.display = bevy_ui::Display::None;
259+
}
260+
}
161261
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#import bevy_ui::ui_vertex_output::UiVertexOutput
2+
3+
@group(1) @binding(0) var<storage> values: array<f32>;
4+
struct Config {
5+
dt_min: f32,
6+
dt_max: f32,
7+
dt_min_log2: f32,
8+
dt_max_log2: f32,
9+
proportional_width: u32,
10+
}
11+
@group(1) @binding(1) var<uniform> config: Config;
12+
13+
const RED: vec4<f32> = vec4(1.0, 0.0, 0.0, 1.0);
14+
const GREEN: vec4<f32> = vec4(0.0, 1.0, 0.0, 1.0);
15+
16+
// Gets a color based on the delta time
17+
// TODO use customizable gradient
18+
fn color_from_dt(dt: f32) -> vec4<f32> {
19+
return mix(GREEN, RED, dt / config.dt_max);
20+
}
21+
22+
// Draw an SDF square
23+
fn sdf_square(pos: vec2<f32>, half_size: vec2<f32>, offset: vec2<f32>) -> f32 {
24+
let p = pos - offset;
25+
let dist = abs(p) - half_size;
26+
let outside_dist = length(max(dist, vec2<f32>(0.0, 0.0)));
27+
let inside_dist = min(max(dist.x, dist.y), 0.0);
28+
return outside_dist + inside_dist;
29+
}
30+
31+
@fragment
32+
fn fragment(in: UiVertexOutput) -> @location(0) vec4<f32> {
33+
let dt_min = config.dt_min;
34+
let dt_max = config.dt_max;
35+
let dt_min_log2 = config.dt_min_log2;
36+
let dt_max_log2 = config.dt_max_log2;
37+
38+
// The general algorithm is highly inspired by
39+
// <https://asawicki.info/news_1758_an_idea_for_visualization_of_frame_times>
40+
41+
let len = arrayLength(&values);
42+
var graph_width = 0.0;
43+
for (var i = 0u; i <= len; i += 1u) {
44+
let dt = values[len - i];
45+
46+
var frame_width: f32;
47+
if config.proportional_width == 1u {
48+
frame_width = (dt / dt_min) / f32(len);
49+
} else {
50+
frame_width = 0.015;
51+
}
52+
53+
let frame_height_factor = (log2(dt) - dt_min_log2) / (dt_max_log2 - dt_min_log2);
54+
let frame_height_factor_norm = min(max(0.0, frame_height_factor), 1.0);
55+
let frame_height = mix(0.0, 1.0, frame_height_factor_norm);
56+
57+
let size = vec2(frame_width, frame_height) / 2.0;
58+
let offset = vec2(1.0 - graph_width - size.x, 1. - size.y);
59+
if (sdf_square(in.uv, size, offset) < 0.0) {
60+
return color_from_dt(dt);
61+
}
62+
63+
graph_width += frame_width;
64+
}
65+
66+
return vec4(0.0, 0.0, 0.0, 0.5);
67+
}
68+

0 commit comments

Comments
 (0)