Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_dev_tools/src/fps_overlay.rs
9350 views
1
//! Module containing logic for FPS overlay.
2
3
use bevy_app::{Plugin, Startup, Update};
4
use bevy_asset::Assets;
5
use bevy_color::Color;
6
use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
7
use bevy_ecs::{
8
component::Component,
9
entity::Entity,
10
query::{With, Without},
11
reflect::ReflectResource,
12
resource::Resource,
13
schedule::{common_conditions::resource_changed, IntoScheduleConfigs, SystemSet},
14
system::{Commands, Query, Res, ResMut, Single},
15
};
16
use bevy_picking::Pickable;
17
use bevy_reflect::Reflect;
18
use bevy_render::storage::ShaderBuffer;
19
use bevy_text::{RemSize, TextColor, TextFont, TextSpan};
20
use bevy_time::common_conditions::on_timer;
21
use bevy_ui::{
22
widget::{Text, TextUiWriter},
23
ComputedUiRenderTargetInfo, FlexDirection, GlobalZIndex, Node, PositionType, Val,
24
};
25
#[cfg(not(all(target_arch = "wasm32", not(feature = "webgpu"))))]
26
use bevy_ui_render::prelude::MaterialNode;
27
use core::time::Duration;
28
use tracing::warn;
29
30
#[cfg(not(all(target_arch = "wasm32", not(feature = "webgpu"))))]
31
use crate::frame_time_graph::FrameTimeGraphConfigUniform;
32
use crate::frame_time_graph::{FrameTimeGraphPlugin, FrametimeGraphMaterial};
33
34
/// [`GlobalZIndex`] used to render the fps overlay.
35
///
36
/// We use a number slightly under `i32::MAX` so you can render on top of it if you really need to.
37
pub const FPS_OVERLAY_ZINDEX: i32 = i32::MAX - 32;
38
39
// Warn the user if the interval is below this threshold.
40
const MIN_SAFE_INTERVAL: Duration = Duration::from_millis(50);
41
42
// Used to scale the frame time graph based on the fps text size
43
const FRAME_TIME_GRAPH_WIDTH_SCALE: f32 = 6.0;
44
const FRAME_TIME_GRAPH_HEIGHT_SCALE: f32 = 2.0;
45
46
/// A plugin that adds an FPS overlay to the Bevy application.
47
///
48
/// This plugin will add the [`FrameTimeDiagnosticsPlugin`] if it wasn't added before.
49
///
50
/// Note: It is recommended to use native overlay of rendering statistics when possible for lower overhead and more accurate results.
51
/// The correct way to do this will vary by platform:
52
/// - **Metal**: setting env variable `MTL_HUD_ENABLED=1`
53
#[derive(Default)]
54
pub struct FpsOverlayPlugin {
55
/// Starting configuration of overlay, this can be later be changed through [`FpsOverlayConfig`] resource.
56
pub config: FpsOverlayConfig,
57
}
58
59
/// System sets for FPS overlay updates.
60
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
61
pub enum FpsOverlaySystems {
62
/// Applies config changes to the overlay UI.
63
Customize,
64
/// Updates the overlay contents.
65
UpdateText,
66
}
67
68
impl Plugin for FpsOverlayPlugin {
69
fn build(&self, app: &mut bevy_app::App) {
70
// TODO: Use plugin dependencies, see https://github.com/bevyengine/bevy/issues/69
71
if !app.is_plugin_added::<FrameTimeDiagnosticsPlugin>() {
72
app.add_plugins(FrameTimeDiagnosticsPlugin::default());
73
}
74
75
if !app.is_plugin_added::<FrameTimeGraphPlugin>() {
76
app.add_plugins(FrameTimeGraphPlugin);
77
}
78
79
if self.config.refresh_interval < MIN_SAFE_INTERVAL {
80
warn!(
81
"Low refresh interval ({:?}) may degrade performance. \
82
Min recommended: {:?}.",
83
self.config.refresh_interval, MIN_SAFE_INTERVAL
84
);
85
}
86
87
app.insert_resource(self.config.clone())
88
.configure_sets(
89
Update,
90
FpsOverlaySystems::Customize.before(FpsOverlaySystems::UpdateText),
91
)
92
.add_systems(Startup, setup)
93
.add_systems(
94
Update,
95
(
96
(toggle_display, customize_overlay)
97
.run_if(resource_changed::<FpsOverlayConfig>)
98
.in_set(FpsOverlaySystems::Customize),
99
update_text
100
.run_if(on_timer(self.config.refresh_interval))
101
.in_set(FpsOverlaySystems::UpdateText),
102
),
103
);
104
}
105
}
106
107
/// Configuration options for the FPS overlay.
108
#[derive(Resource, Clone, Reflect)]
109
#[reflect(Resource)]
110
pub struct FpsOverlayConfig {
111
/// Configuration of text in the overlay.
112
pub text_config: TextFont,
113
/// Color of text in the overlay.
114
pub text_color: Color,
115
/// Displays the FPS overlay if true.
116
pub enabled: bool,
117
/// The period after which the FPS overlay re-renders.
118
///
119
/// Defaults to once every 100 ms.
120
pub refresh_interval: Duration,
121
/// Configuration of the frame time graph
122
pub frame_time_graph_config: FrameTimeGraphConfig,
123
}
124
125
impl Default for FpsOverlayConfig {
126
fn default() -> Self {
127
FpsOverlayConfig {
128
text_config: TextFont::from_font_size(32.),
129
text_color: Color::WHITE,
130
enabled: true,
131
refresh_interval: Duration::from_millis(100),
132
// TODO set this to display refresh rate if possible
133
frame_time_graph_config: FrameTimeGraphConfig::target_fps(60.0),
134
}
135
}
136
}
137
138
/// Configuration of the frame time graph
139
#[derive(Clone, Copy, Reflect)]
140
pub struct FrameTimeGraphConfig {
141
/// Is the graph visible
142
pub enabled: bool,
143
/// The minimum acceptable FPS
144
///
145
/// Anything below this will show a red bar
146
pub min_fps: f32,
147
/// The target FPS
148
///
149
/// Anything above this will show a green bar
150
pub target_fps: f32,
151
}
152
153
impl FrameTimeGraphConfig {
154
/// Constructs a default config for a given target fps
155
pub fn target_fps(target_fps: f32) -> Self {
156
Self {
157
target_fps,
158
..Self::default()
159
}
160
}
161
}
162
163
impl Default for FrameTimeGraphConfig {
164
fn default() -> Self {
165
Self {
166
enabled: true,
167
min_fps: 30.0,
168
target_fps: 60.0,
169
}
170
}
171
}
172
173
#[derive(Component)]
174
struct FpsText;
175
176
#[derive(Component)]
177
struct FrameTimeGraph;
178
179
fn setup(
180
mut commands: Commands,
181
overlay_config: Res<FpsOverlayConfig>,
182
#[cfg_attr(
183
all(target_arch = "wasm32", not(feature = "webgpu")),
184
expect(unused, reason = "Unused variables in wasm32 without webgpu feature")
185
)]
186
(mut frame_time_graph_materials, mut buffers): (
187
ResMut<Assets<FrametimeGraphMaterial>>,
188
ResMut<Assets<ShaderBuffer>>,
189
),
190
) {
191
commands
192
.spawn((
193
Node {
194
// We need to make sure the overlay doesn't affect the position of other UI nodes
195
position_type: PositionType::Absolute,
196
flex_direction: FlexDirection::Column,
197
..Default::default()
198
},
199
// Render overlay on top of everything
200
GlobalZIndex(FPS_OVERLAY_ZINDEX),
201
Pickable::IGNORE,
202
))
203
.with_children(|p| {
204
p.spawn((
205
Text::new("FPS: "),
206
overlay_config.text_config.clone(),
207
TextColor(overlay_config.text_color),
208
FpsText,
209
Pickable::IGNORE,
210
))
211
.with_child((TextSpan::default(), overlay_config.text_config.clone()));
212
213
#[cfg(all(target_arch = "wasm32", not(feature = "webgpu")))]
214
{
215
if overlay_config.frame_time_graph_config.enabled {
216
use tracing::warn;
217
218
warn!("Frame time graph is not supported with WebGL. Consider if WebGPU is viable for your usecase.");
219
}
220
}
221
#[cfg(not(all(target_arch = "wasm32", not(feature = "webgpu"))))]
222
{
223
// Todo: Needs a better design that works with responsive sizing.
224
let font_size = 20.;
225
p.spawn((
226
Node {
227
width: Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE),
228
height: Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE),
229
display: if overlay_config.frame_time_graph_config.enabled {
230
bevy_ui::Display::DEFAULT
231
} else {
232
bevy_ui::Display::None
233
},
234
..Default::default()
235
},
236
Pickable::IGNORE,
237
MaterialNode::from(frame_time_graph_materials.add(FrametimeGraphMaterial {
238
values: buffers.add(ShaderBuffer {
239
// Initialize with dummy data because the default (`data: None`) will
240
// cause a panic in the shader if the frame time graph is constructed
241
// with `enabled: false`.
242
data: Some(vec![0, 0, 0, 0]),
243
..Default::default()
244
}),
245
config: FrameTimeGraphConfigUniform::new(
246
overlay_config.frame_time_graph_config.target_fps,
247
overlay_config.frame_time_graph_config.min_fps,
248
true,
249
),
250
})),
251
FrameTimeGraph,
252
));
253
}
254
});
255
}
256
257
fn update_text(
258
diagnostic: Res<DiagnosticsStore>,
259
query: Query<Entity, With<FpsText>>,
260
mut writer: TextUiWriter,
261
) {
262
if let Ok(entity) = query.single()
263
&& let Some(fps) = diagnostic.get(&FrameTimeDiagnosticsPlugin::FPS)
264
&& let Some(value) = fps.smoothed()
265
{
266
*writer.text(entity, 1) = format!("{value:.2}");
267
}
268
}
269
270
fn customize_overlay(
271
overlay_config: Res<FpsOverlayConfig>,
272
query: Query<Entity, With<FpsText>>,
273
mut writer: TextUiWriter,
274
) {
275
for entity in &query {
276
writer.for_each_font(entity, |mut font| {
277
*font = overlay_config.text_config.clone();
278
});
279
writer.for_each_color(entity, |mut color| color.0 = overlay_config.text_color);
280
}
281
}
282
283
fn toggle_display(
284
overlay_config: Res<FpsOverlayConfig>,
285
mut text_node: Single<
286
(&mut Node, &ComputedUiRenderTargetInfo),
287
(With<FpsText>, Without<FrameTimeGraph>),
288
>,
289
mut graph_node: Single<&mut Node, (With<FrameTimeGraph>, Without<FpsText>)>,
290
rem_size: Res<RemSize>,
291
) {
292
if overlay_config.enabled {
293
text_node.0.display = bevy_ui::Display::DEFAULT;
294
} else {
295
text_node.0.display = bevy_ui::Display::None;
296
}
297
298
if overlay_config.frame_time_graph_config.enabled {
299
// Scale the frame time graph based on the font size of the overlay
300
let font_size = overlay_config
301
.text_config
302
.font_size
303
.eval(text_node.1.logical_size(), rem_size.0);
304
graph_node.width = Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE);
305
graph_node.height = Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE);
306
307
graph_node.display = bevy_ui::Display::DEFAULT;
308
} else {
309
graph_node.display = bevy_ui::Display::None;
310
}
311
}
312
313