1use bevy_app::{Plugin, Startup, Update};
4use bevy_asset::{Assets, Handle};
5use bevy_color::Color;
6use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
7use bevy_ecs::{
8 component::Component,
9 entity::Entity,
10 prelude::Local,
11 query::{With, Without},
12 resource::Resource,
13 schedule::{common_conditions::resource_changed, IntoScheduleConfigs},
14 system::{Commands, Query, Res, ResMut, Single},
15};
16use bevy_picking::Pickable;
17use bevy_render::storage::ShaderStorageBuffer;
18use bevy_text::{Font, TextColor, TextFont, TextSpan};
19use bevy_time::Time;
20use bevy_ui::{
21 widget::{Text, TextUiWriter},
22 FlexDirection, GlobalZIndex, Node, PositionType, Val,
23};
24use bevy_ui_render::prelude::MaterialNode;
25use core::time::Duration;
26
27use crate::frame_time_graph::{
28 FrameTimeGraphConfigUniform, FrameTimeGraphPlugin, FrametimeGraphMaterial,
29};
30
31pub const FPS_OVERLAY_ZINDEX: i32 = i32::MAX - 32;
35
36const FRAME_TIME_GRAPH_WIDTH_SCALE: f32 = 6.0;
38const FRAME_TIME_GRAPH_HEIGHT_SCALE: f32 = 2.0;
39
40#[derive(Default)]
48pub struct FpsOverlayPlugin {
49 pub config: FpsOverlayConfig,
51}
52
53impl Plugin for FpsOverlayPlugin {
54 fn build(&self, app: &mut bevy_app::App) {
55 if !app.is_plugin_added::<FrameTimeDiagnosticsPlugin>() {
57 app.add_plugins(FrameTimeDiagnosticsPlugin::default());
58 }
59
60 if !app.is_plugin_added::<FrameTimeGraphPlugin>() {
61 app.add_plugins(FrameTimeGraphPlugin);
62 }
63
64 app.insert_resource(self.config.clone())
65 .add_systems(Startup, setup)
66 .add_systems(
67 Update,
68 (
69 (toggle_display, customize_overlay)
70 .run_if(resource_changed::<FpsOverlayConfig>),
71 update_text,
72 ),
73 );
74 }
75}
76
77#[derive(Resource, Clone)]
79pub struct FpsOverlayConfig {
80 pub text_config: TextFont,
82 pub text_color: Color,
84 pub enabled: bool,
86 pub refresh_interval: Duration,
90 pub frame_time_graph_config: FrameTimeGraphConfig,
92}
93
94impl Default for FpsOverlayConfig {
95 fn default() -> Self {
96 FpsOverlayConfig {
97 text_config: TextFont {
98 font: Handle::<Font>::default(),
99 font_size: 32.0,
100 ..Default::default()
101 },
102 text_color: Color::WHITE,
103 enabled: true,
104 refresh_interval: Duration::from_millis(100),
105 frame_time_graph_config: FrameTimeGraphConfig::target_fps(60.0),
107 }
108 }
109}
110
111#[derive(Clone, Copy)]
113pub struct FrameTimeGraphConfig {
114 pub enabled: bool,
116 pub min_fps: f32,
120 pub target_fps: f32,
124}
125
126impl FrameTimeGraphConfig {
127 pub fn target_fps(target_fps: f32) -> Self {
129 Self {
130 target_fps,
131 ..Self::default()
132 }
133 }
134}
135
136impl Default for FrameTimeGraphConfig {
137 fn default() -> Self {
138 Self {
139 enabled: true,
140 min_fps: 30.0,
141 target_fps: 60.0,
142 }
143 }
144}
145
146#[derive(Component)]
147struct FpsText;
148
149#[derive(Component)]
150struct FrameTimeGraph;
151
152fn 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) {
158 commands
159 .spawn((
160 Node {
161 position_type: PositionType::Absolute,
163 flex_direction: FlexDirection::Column,
164 ..Default::default()
165 },
166 GlobalZIndex(FPS_OVERLAY_ZINDEX),
168 Pickable::IGNORE,
169 ))
170 .with_children(|p| {
171 p.spawn((
172 Text::new("FPS: "),
173 overlay_config.text_config.clone(),
174 TextColor(overlay_config.text_color),
175 FpsText,
176 Pickable::IGNORE,
177 ))
178 .with_child((TextSpan::default(), overlay_config.text_config.clone()));
179
180 let font_size = overlay_config.text_config.font_size;
181 p.spawn((
182 Node {
183 width: Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE),
184 height: Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE),
185 display: if overlay_config.frame_time_graph_config.enabled {
186 bevy_ui::Display::DEFAULT
187 } else {
188 bevy_ui::Display::None
189 },
190 ..Default::default()
191 },
192 Pickable::IGNORE,
193 MaterialNode::from(frame_time_graph_materials.add(FrametimeGraphMaterial {
194 values: buffers.add(ShaderStorageBuffer {
195 data: Some(vec![0, 0, 0, 0]),
199 ..Default::default()
200 }),
201 config: FrameTimeGraphConfigUniform::new(
202 overlay_config.frame_time_graph_config.target_fps,
203 overlay_config.frame_time_graph_config.min_fps,
204 true,
205 ),
206 })),
207 FrameTimeGraph,
208 ));
209 });
210}
211
212fn update_text(
213 diagnostic: Res<DiagnosticsStore>,
214 query: Query<Entity, With<FpsText>>,
215 mut writer: TextUiWriter,
216 time: Res<Time>,
217 config: Res<FpsOverlayConfig>,
218 mut time_since_rerender: Local<Duration>,
219) {
220 *time_since_rerender += time.delta();
221 if *time_since_rerender >= config.refresh_interval {
222 *time_since_rerender = Duration::ZERO;
223 for entity in &query {
224 if let Some(fps) = diagnostic.get(&FrameTimeDiagnosticsPlugin::FPS)
225 && let Some(value) = fps.smoothed()
226 {
227 *writer.text(entity, 1) = format!("{value:.2}");
228 }
229 }
230 }
231}
232
233fn customize_overlay(
234 overlay_config: Res<FpsOverlayConfig>,
235 query: Query<Entity, With<FpsText>>,
236 mut writer: TextUiWriter,
237) {
238 for entity in &query {
239 writer.for_each_font(entity, |mut font| {
240 *font = overlay_config.text_config.clone();
241 });
242 writer.for_each_color(entity, |mut color| color.0 = overlay_config.text_color);
243 }
244}
245
246fn toggle_display(
247 overlay_config: Res<FpsOverlayConfig>,
248 mut text_node: Single<&mut Node, (With<FpsText>, Without<FrameTimeGraph>)>,
249 mut graph_node: Single<&mut Node, (With<FrameTimeGraph>, Without<FpsText>)>,
250) {
251 if overlay_config.enabled {
252 text_node.display = bevy_ui::Display::DEFAULT;
253 } else {
254 text_node.display = bevy_ui::Display::None;
255 }
256
257 if overlay_config.frame_time_graph_config.enabled {
258 let font_size = overlay_config.text_config.font_size;
260 graph_node.width = Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE);
261 graph_node.height = Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE);
262
263 graph_node.display = bevy_ui::Display::DEFAULT;
264 } else {
265 graph_node.display = bevy_ui::Display::None;
266 }
267}