Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_dev_tools/src/diagnostics_overlay.rs
9328 views
1
//! Overlay showing diagnostics
2
//!
3
//! The window can be created using the [`DiagnosticsOverlay`] component
4
5
use alloc::borrow::Cow;
6
use core::time::Duration;
7
8
use bevy_app::prelude::*;
9
use bevy_color::{palettes, prelude::*};
10
use bevy_diagnostic::{Diagnostic, DiagnosticPath, DiagnosticsStore, FrameTimeDiagnosticsPlugin};
11
use bevy_ecs::{prelude::*, relationship::Relationship};
12
use bevy_pbr::{diagnostic::MaterialAllocatorDiagnosticPlugin, StandardMaterial};
13
use bevy_picking::prelude::*;
14
use bevy_render::diagnostic::MeshAllocatorDiagnosticPlugin;
15
use bevy_text::prelude::*;
16
use bevy_time::common_conditions::on_timer;
17
use bevy_ui::prelude::*;
18
19
/// Initial offset from the top left corner of the window
20
/// for the diagnostics overlay
21
const INITIAL_OFFSET: Val = Val::Px(32.);
22
/// Alpha value for [`BackgroundColor`] of the overlay
23
const BACKGROUND_COLOR_ALPHA: f32 = 0.75;
24
/// Row and column gap for the diagnostics overlay
25
const ROW_COLUMN_GAP: Val = Val::Px(4.);
26
/// Padding for cels of the diagnostics overlay
27
const DEFAULT_PADDING: UiRect = UiRect::all(Val::Px(4.));
28
/// Initial Z-index for the [`DiagnosticsOverlayPlane`]
29
pub const INITIAL_DIAGNOSTICS_OVERLAY_PLANE_Z_INDEX: GlobalZIndex = GlobalZIndex(1_000_000);
30
/// Alias to shorten the name
31
type StandardMaterialAllocator = MaterialAllocatorDiagnosticPlugin<StandardMaterial>;
32
33
/// Diagnostics overlay displays on a draggable and collapsible window
34
/// statistics stored on the [`DiagnosticsStore`]. Spawning an entity
35
/// with this component will create the window for you. Some presets
36
/// are also provided.
37
///
38
/// ```
39
/// # use bevy_dev_tools::diagnostics_overlay::{DiagnosticsOverlay, DiagnosticsOverlayItem, DiagnosticsOverlayStatistic};
40
/// # use bevy_ecs::prelude::{Commands, World};
41
/// # use bevy_diagnostic::DiagnosticPath;
42
/// # let mut world = World::new();
43
/// # let mut commands = world.commands();
44
/// // Spawning an overlay window from the struct
45
/// commands.spawn(DiagnosticsOverlay {
46
/// title: "Fps".into(),
47
/// diagnostic_overlay_items: vec![DiagnosticPath::new("fps").into()]
48
/// });
49
/// // Spawning an overlay window from the `new` method
50
/// commands.spawn(DiagnosticsOverlay::new(
51
/// "Fps",
52
/// vec![DiagnosticPath::new("fps").into()]
53
/// ));
54
/// // Spawning an overlay window from the `new` method using a different statistic
55
/// // and float precision
56
/// commands.spawn(DiagnosticsOverlay::new(
57
/// "Fps",
58
/// vec![DiagnosticsOverlayItem {
59
/// path: DiagnosticPath::new("fps"),
60
/// statistic: DiagnosticsOverlayStatistic::Value,
61
/// precision: 4
62
/// }]
63
/// ));
64
/// // Spawning an overlay window from the `fps` preset
65
/// commands.spawn(DiagnosticsOverlay::fps());
66
/// ```
67
///
68
/// A [`DiagnosticsOverlay`] entity will be managed by [`DiagnosticsOverlayPlugin`],
69
/// and be added as a child of the [`DiagnosticsOverlayPlane`].
70
///
71
/// If any value is showing as `Missing`, means that the [`DiagnosticPath`] is not registered,
72
/// so make sure that the plugin that writes to it is properly set up.
73
#[derive(Component)]
74
pub struct DiagnosticsOverlay {
75
/// Title that will appear on the overlay window
76
pub title: Cow<'static, str>,
77
/// Items that will appear on this overlay window
78
pub diagnostic_overlay_items: Vec<DiagnosticsOverlayItem>,
79
}
80
81
impl DiagnosticsOverlay {
82
/// Creates a new instance of a [`DiagnosticsOverlay`]
83
pub fn new(
84
title: impl Into<Cow<'static, str>>,
85
diagnostic_paths: Vec<DiagnosticsOverlayItem>,
86
) -> Self {
87
Self {
88
title: title.into(),
89
diagnostic_overlay_items: diagnostic_paths,
90
}
91
}
92
93
/// Create a [`DiagnosticsOverlay`] with the diagnostcs from [`FrameTimeDiagnosticsPlugin`]
94
pub fn fps() -> Self {
95
Self {
96
title: Cow::Owned("Fps".to_owned()),
97
diagnostic_overlay_items: vec![
98
FrameTimeDiagnosticsPlugin::FPS.into(),
99
FrameTimeDiagnosticsPlugin::FRAME_TIME.into(),
100
DiagnosticsOverlayItem {
101
path: FrameTimeDiagnosticsPlugin::FRAME_COUNT,
102
statistic: DiagnosticsOverlayStatistic::Smoothed,
103
precision: 0,
104
},
105
],
106
}
107
}
108
109
/// Create a [`DiagnosticsOverlay`] with the diagnostics from
110
/// [`MaterialAllocatorDiagnosticPlugin`] of [`StandardMaterial`] and
111
/// [`MeshAllocatorDiagnosticPlugin`]
112
pub fn mesh_and_standard_material() -> Self {
113
Self {
114
title: Cow::Owned("Mesh and standard materials".to_owned()),
115
diagnostic_overlay_items: vec![
116
DiagnosticsOverlayItem {
117
path: StandardMaterialAllocator::slabs_diagnostic_path(),
118
statistic: DiagnosticsOverlayStatistic::Smoothed,
119
precision: 0,
120
},
121
DiagnosticsOverlayItem {
122
path: StandardMaterialAllocator::slabs_size_diagnostic_path(),
123
statistic: DiagnosticsOverlayStatistic::Smoothed,
124
precision: 0,
125
},
126
DiagnosticsOverlayItem {
127
path: StandardMaterialAllocator::allocations_diagnostic_path(),
128
statistic: DiagnosticsOverlayStatistic::Smoothed,
129
precision: 0,
130
},
131
DiagnosticsOverlayItem {
132
path: MeshAllocatorDiagnosticPlugin::slabs_diagnostic_path().clone(),
133
statistic: DiagnosticsOverlayStatistic::Smoothed,
134
precision: 0,
135
},
136
DiagnosticsOverlayItem {
137
path: MeshAllocatorDiagnosticPlugin::slabs_size_diagnostic_path().clone(),
138
statistic: DiagnosticsOverlayStatistic::Smoothed,
139
precision: 0,
140
},
141
DiagnosticsOverlayItem {
142
path: MeshAllocatorDiagnosticPlugin::allocations_diagnostic_path().clone(),
143
statistic: DiagnosticsOverlayStatistic::Smoothed,
144
precision: 0,
145
},
146
],
147
}
148
}
149
}
150
151
/// Marker for the UI root that will hold all of the [`DiagnosticsOverlay`]
152
/// entities.
153
///
154
/// Initially the [`DiagnosticsOverlayPlane`] will be positioned at the
155
/// [`GlobalZIndex`] of [`INITIAL_DIAGNOSTICS_OVERLAY_PLANE_Z_INDEX`].
156
/// You are free to edit the z index of the plane or have your ui hierarchies
157
/// be relative to it.
158
#[derive(Component)]
159
pub struct DiagnosticsOverlayPlane;
160
161
/// An item to be displayed on the overlay.
162
///
163
/// Items built using `From<DiagnosticPath>` will use
164
/// [`DiagnosticsOverlayStatistic::Smoothed`].
165
pub struct DiagnosticsOverlayItem {
166
/// The statistic of the diagnostic to display
167
pub statistic: DiagnosticsOverlayStatistic,
168
/// The diagnostic to display
169
pub path: DiagnosticPath,
170
/// How many decimal places to show, default is 4
171
pub precision: usize,
172
}
173
174
impl From<DiagnosticPath> for DiagnosticsOverlayItem {
175
/// Creates an instance of [`DiagnosticsOverlayItem`]
176
/// from a [`DiagnosticPath`] using [`DiagnosticsOverlayStatistic::Smoothed`].
177
fn from(value: DiagnosticPath) -> Self {
178
Self {
179
path: value,
180
statistic: Default::default(),
181
precision: 4,
182
}
183
}
184
}
185
186
/// The statistic to use when displaying a diagnostic
187
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
188
pub enum DiagnosticsOverlayStatistic {
189
/// The most recent value of on the diagnostic store
190
Value,
191
/// The average of a window of values in the diagnostic store.
192
Average,
193
/// The smoothed average of a window of values in the diagnostic store
194
/// using the [EMA](https://en.wikipedia.org/wiki/Exponential_smoothing).
195
#[default]
196
Smoothed,
197
}
198
199
impl DiagnosticsOverlayStatistic {
200
/// Fetch the appropriate statistic from a [`Diagnostic`]
201
pub fn fetch(&self, diagnostic: &Diagnostic) -> Option<f64> {
202
match self {
203
Self::Value => diagnostic.value(),
204
Self::Average => diagnostic.average(),
205
Self::Smoothed => diagnostic.smoothed(),
206
}
207
}
208
}
209
210
/// System set for the systems of the [`DiagnosticsOverlayPlugin`]
211
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, SystemSet)]
212
pub enum DiagnosticsOverlaySystems {
213
/// Rebuild the contents of the [`DiagnosticsOverlay`] entities
214
Rebuild,
215
}
216
217
/// Plugin that builds a visual overlay to present diagnostics.
218
///
219
/// The contents of each [`DiagnosticsOverlay`] are rebuilt ever second.
220
pub struct DiagnosticsOverlayPlugin;
221
222
impl Plugin for DiagnosticsOverlayPlugin {
223
fn build(&self, app: &mut App) {
224
app.configure_sets(Update, DiagnosticsOverlaySystems::Rebuild);
225
app.add_systems(Startup, build_plane);
226
app.add_systems(
227
Update,
228
rebuild_diagnostics_list
229
.run_if(on_timer(Duration::from_secs(1)))
230
.in_set(DiagnosticsOverlaySystems::Rebuild),
231
);
232
233
app.add_observer(build_overlay);
234
app.add_observer(drag_by_header);
235
app.add_observer(collapse_on_click_to_header);
236
app.add_observer(bring_to_front);
237
}
238
}
239
240
/// Builds the Ui plane where the [`DiagnosticsOverlay`] entities
241
/// will reside.
242
fn build_plane(mut commands: Commands) {
243
commands.spawn((
244
DiagnosticsOverlayPlane,
245
Node {
246
width: Val::Percent(100.),
247
height: Val::Percent(100.),
248
..Default::default()
249
},
250
INITIAL_DIAGNOSTICS_OVERLAY_PLANE_Z_INDEX,
251
));
252
}
253
254
/// Header of the overlay
255
#[derive(Component)]
256
struct DiagnosticsOverlayHeader;
257
258
/// Section of the overlay that will have the diagnostics
259
#[derive(Component)]
260
struct DiagnosticsOverlayContents;
261
262
fn rebuild_diagnostics_list(
263
mut commands: Commands,
264
diagnostics_overlays: Query<&DiagnosticsOverlay>,
265
diagnostics_overlay_contents: Query<(Entity, &ChildOf), With<DiagnosticsOverlayContents>>,
266
diagnostics_store: Res<DiagnosticsStore>,
267
) {
268
for (entity, child_of) in diagnostics_overlay_contents {
269
commands.entity(entity).despawn_children();
270
271
let Ok(diagnostics_overlay) = diagnostics_overlays.get(child_of.get()) else {
272
panic!("DiagnosticsOverlayContents has been tempered with. Parent was not a DiagnosticsOverlay.");
273
};
274
275
for (i, diagnostic_overlay_item) in diagnostics_overlay
276
.diagnostic_overlay_items
277
.iter()
278
.enumerate()
279
{
280
let maybe_diagnostic = diagnostics_store.get(&diagnostic_overlay_item.path);
281
let diagnostic = maybe_diagnostic
282
.map(|diagnostic| {
283
format!(
284
"{}{}",
285
diagnostic_overlay_item
286
.statistic
287
.fetch(diagnostic)
288
.map(|sample| format!(
289
"{:.prec$}",
290
sample,
291
prec = diagnostic_overlay_item.precision
292
))
293
.unwrap_or("No sample".to_owned()),
294
diagnostic.suffix
295
)
296
})
297
.unwrap_or("Missing".to_owned());
298
299
commands.spawn((
300
ChildOf(entity),
301
Node {
302
grid_row: GridPlacement::start(i as i16 + 1),
303
grid_column: GridPlacement::start(1),
304
..Default::default()
305
},
306
Pickable::IGNORE,
307
children![(
308
Text::new(diagnostic_overlay_item.path.to_string()),
309
TextFont {
310
font_size: FontSize::Px(10.),
311
..Default::default()
312
},
313
Pickable::IGNORE,
314
)],
315
));
316
commands.spawn((
317
ChildOf(entity),
318
Node {
319
grid_row: GridPlacement::start(i as i16 + 1),
320
grid_column: GridPlacement::start(2),
321
..Default::default()
322
},
323
Pickable::IGNORE,
324
children![(
325
Text::new(diagnostic),
326
TextFont {
327
font_size: FontSize::Px(10.),
328
..Default::default()
329
},
330
Pickable::IGNORE,
331
)],
332
));
333
}
334
}
335
}
336
337
fn build_overlay(
338
event: On<Add, DiagnosticsOverlay>,
339
mut commands: Commands,
340
diagnostics_overlays: Query<&DiagnosticsOverlay>,
341
diagnostics_overlay_plane: Single<Entity, With<DiagnosticsOverlayPlane>>,
342
) {
343
let entity = event.entity;
344
let Ok(diagnostics_overlay) = diagnostics_overlays.get(entity) else {
345
unreachable!("DiagnosticsOverlay must be available.");
346
};
347
348
commands.entity(entity).insert((
349
Node {
350
position_type: PositionType::Absolute,
351
top: INITIAL_OFFSET,
352
left: INITIAL_OFFSET,
353
flex_direction: FlexDirection::Column,
354
..Default::default()
355
},
356
ChildOf(*diagnostics_overlay_plane),
357
children![
358
(
359
Node {
360
padding: DEFAULT_PADDING,
361
border_radius: BorderRadius::bottom(Val::Px(4.)),
362
..Default::default()
363
},
364
DiagnosticsOverlayHeader,
365
BackgroundColor(
366
palettes::tailwind::GRAY_900
367
.with_alpha(BACKGROUND_COLOR_ALPHA)
368
.into()
369
),
370
children![(
371
Text::new(diagnostics_overlay.title.as_ref()),
372
TextFont {
373
font_size: FontSize::Px(12.),
374
..Default::default()
375
},
376
Pickable::IGNORE
377
)],
378
),
379
(
380
Node {
381
display: Display::Grid,
382
row_gap: ROW_COLUMN_GAP,
383
column_gap: ROW_COLUMN_GAP,
384
padding: DEFAULT_PADDING,
385
border_radius: BorderRadius::bottom(Val::Px(4.)),
386
..Default::default()
387
},
388
DiagnosticsOverlayContents,
389
BackgroundColor(
390
palettes::tailwind::GRAY_600
391
.with_alpha(BACKGROUND_COLOR_ALPHA)
392
.into()
393
),
394
)
395
],
396
));
397
}
398
399
fn drag_by_header(
400
mut event: On<Pointer<Drag>>,
401
mut diagnostics_overlays: Query<&mut Node, With<DiagnosticsOverlay>>,
402
diagnostics_overlay_headers: Query<&ChildOf, With<DiagnosticsOverlayHeader>>,
403
) {
404
let entity = event.entity;
405
if let Ok(child_of) = diagnostics_overlay_headers.get(entity) {
406
event.propagate(false);
407
let Ok(mut node) = diagnostics_overlays.get_mut(child_of.get()) else {
408
panic!("DiagnosticsOverlayHeader has been tempered with. Parent was not a DiagnosticsOverlay.");
409
};
410
let delta = event.delta;
411
let Val::Px(top) = &mut node.top else {
412
panic!(
413
"DiagnosticsOverlay has been tempered with. Node must have `top` using `Val::Px`."
414
);
415
};
416
*top += delta.y;
417
let Val::Px(left) = &mut node.left else {
418
panic!(
419
"DiagnosticsOverlay has been tempered with. Node must have `left` using `Val::Px`."
420
);
421
};
422
*left += delta.x;
423
}
424
}
425
426
fn collapse_on_click_to_header(
427
mut event: On<Pointer<Click>>,
428
mut diagnostics_overlays: Query<&Children, With<DiagnosticsOverlay>>,
429
mut diagnostics_overlay_contents: Query<&mut Node, With<DiagnosticsOverlayContents>>,
430
diagnostics_overlay_header: Query<&ChildOf, With<DiagnosticsOverlayHeader>>,
431
) {
432
if event.duration > Duration::from_millis(250) {
433
return;
434
}
435
436
let entity = event.entity;
437
if let Ok(child_of) = diagnostics_overlay_header.get(entity) {
438
event.propagate(false);
439
440
let Ok(children) = diagnostics_overlays.get_mut(child_of.get()) else {
441
unreachable!("DiagnosticsOverlay has been tempered with. Do not despawn its children.");
442
};
443
let mut lists_iter = diagnostics_overlay_contents.iter_many_mut(children.collection());
444
445
let Some(mut node) = lists_iter.fetch_next() else {
446
panic!(
447
"DiagnosticsOverlay has been tempered with. DiagnosticsOverlay must\
448
have a child with DiagnosticsList."
449
);
450
};
451
452
let next_display_mode = match node.display {
453
Display::Grid => Display::None,
454
Display::None => Display::Grid,
455
_ => panic!(
456
"The DiagnosticsList has be tempered with. Valid Displays for a\
457
DiagnosticsList are Grid or None."
458
),
459
};
460
node.display = next_display_mode;
461
462
if lists_iter.fetch_next().is_some() {
463
panic!(
464
"DiagnosticsOverlay has been tempered with. DiagnosticsOverlay must\
465
only ever have one single child with DiagnosticsList."
466
);
467
}
468
}
469
}
470
471
fn bring_to_front(
472
mut event: On<Pointer<Press>>,
473
mut commands: Commands,
474
diagnostics_overlays: Query<(), With<DiagnosticsOverlay>>,
475
diagnostics_overlay_plane: Single<Entity, With<DiagnosticsOverlayPlane>>,
476
) {
477
let entity = event.entity;
478
if diagnostics_overlays.contains(entity) {
479
event.propagate(false);
480
commands
481
.entity(entity)
482
.remove::<ChildOf>()
483
.insert(ChildOf(*diagnostics_overlay_plane));
484
}
485
}
486
487