use bevy_app::{Plugin, Startup, Update};
use bevy_asset::{Assets, Handle};
use bevy_color::Color;
use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{With, Without},
reflect::ReflectResource,
resource::Resource,
schedule::{common_conditions::resource_changed, IntoScheduleConfigs, SystemSet},
system::{Commands, Query, Res, ResMut, Single},
};
use bevy_picking::Pickable;
use bevy_reflect::Reflect;
use bevy_render::storage::ShaderStorageBuffer;
use bevy_text::{Font, TextColor, TextFont, TextSpan};
use bevy_time::common_conditions::on_timer;
use bevy_ui::{
widget::{Text, TextUiWriter},
FlexDirection, GlobalZIndex, Node, PositionType, Val,
};
use bevy_ui_render::prelude::MaterialNode;
use core::time::Duration;
use tracing::warn;
use crate::frame_time_graph::{
FrameTimeGraphConfigUniform, FrameTimeGraphPlugin, FrametimeGraphMaterial,
};
pub const FPS_OVERLAY_ZINDEX: i32 = i32::MAX - 32;
const MIN_SAFE_INTERVAL: Duration = Duration::from_millis(50);
const FRAME_TIME_GRAPH_WIDTH_SCALE: f32 = 6.0;
const FRAME_TIME_GRAPH_HEIGHT_SCALE: f32 = 2.0;
#[derive(Default)]
pub struct FpsOverlayPlugin {
pub config: FpsOverlayConfig,
}
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum FpsOverlaySystems {
Customize,
UpdateText,
}
impl Plugin for FpsOverlayPlugin {
fn build(&self, app: &mut bevy_app::App) {
if !app.is_plugin_added::<FrameTimeDiagnosticsPlugin>() {
app.add_plugins(FrameTimeDiagnosticsPlugin::default());
}
if !app.is_plugin_added::<FrameTimeGraphPlugin>() {
app.add_plugins(FrameTimeGraphPlugin);
}
if self.config.refresh_interval < MIN_SAFE_INTERVAL {
warn!(
"Low refresh interval ({:?}) may degrade performance. \
Min recommended: {:?}.",
self.config.refresh_interval, MIN_SAFE_INTERVAL
);
}
app.insert_resource(self.config.clone())
.configure_sets(
Update,
FpsOverlaySystems::Customize.before(FpsOverlaySystems::UpdateText),
)
.add_systems(Startup, setup)
.add_systems(
Update,
(
(toggle_display, customize_overlay)
.run_if(resource_changed::<FpsOverlayConfig>)
.in_set(FpsOverlaySystems::Customize),
update_text
.run_if(on_timer(self.config.refresh_interval))
.in_set(FpsOverlaySystems::UpdateText),
),
);
}
}
#[derive(Resource, Clone, Reflect)]
#[reflect(Resource)]
pub struct FpsOverlayConfig {
pub text_config: TextFont,
pub text_color: Color,
pub enabled: bool,
pub refresh_interval: Duration,
pub frame_time_graph_config: FrameTimeGraphConfig,
}
impl Default for FpsOverlayConfig {
fn default() -> Self {
FpsOverlayConfig {
text_config: TextFont {
font: Handle::<Font>::default(),
font_size: 32.0,
..Default::default()
},
text_color: Color::WHITE,
enabled: true,
refresh_interval: Duration::from_millis(100),
frame_time_graph_config: FrameTimeGraphConfig::target_fps(60.0),
}
}
}
#[derive(Clone, Copy, Reflect)]
pub struct FrameTimeGraphConfig {
pub enabled: bool,
pub min_fps: f32,
pub target_fps: f32,
}
impl FrameTimeGraphConfig {
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,
}
}
}
#[derive(Component)]
struct FpsText;
#[derive(Component)]
struct FrameTimeGraph;
fn setup(
mut commands: Commands,
overlay_config: Res<FpsOverlayConfig>,
mut frame_time_graph_materials: ResMut<Assets<FrametimeGraphMaterial>>,
mut buffers: ResMut<Assets<ShaderStorageBuffer>>,
) {
commands
.spawn((
Node {
position_type: PositionType::Absolute,
flex_direction: FlexDirection::Column,
..Default::default()
},
GlobalZIndex(FPS_OVERLAY_ZINDEX),
Pickable::IGNORE,
))
.with_children(|p| {
p.spawn((
Text::new("FPS: "),
overlay_config.text_config.clone(),
TextColor(overlay_config.text_color),
FpsText,
Pickable::IGNORE,
))
.with_child((TextSpan::default(), overlay_config.text_config.clone()));
#[cfg(all(target_arch = "wasm32", not(feature = "webgpu")))]
{
if overlay_config.frame_time_graph_config.enabled {
use tracing::warn;
warn!("Frame time graph is not supported with WebGL. Consider if WebGPU is viable for your usecase.");
}
}
#[cfg(not(all(target_arch = "wasm32", not(feature = "webgpu"))))]
{
let font_size = overlay_config.text_config.font_size;
p.spawn((
Node {
width: Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE),
height: Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE),
display: if overlay_config.frame_time_graph_config.enabled {
bevy_ui::Display::DEFAULT
} else {
bevy_ui::Display::None
},
..Default::default()
},
Pickable::IGNORE,
MaterialNode::from(frame_time_graph_materials.add(FrametimeGraphMaterial {
values: buffers.add(ShaderStorageBuffer {
data: Some(vec![0, 0, 0, 0]),
..Default::default()
}),
config: FrameTimeGraphConfigUniform::new(
overlay_config.frame_time_graph_config.target_fps,
overlay_config.frame_time_graph_config.min_fps,
true,
),
})),
FrameTimeGraph,
));
}
});
}
fn update_text(
diagnostic: Res<DiagnosticsStore>,
query: Query<Entity, With<FpsText>>,
mut writer: TextUiWriter,
) {
if let Ok(entity) = query.single()
&& let Some(fps) = diagnostic.get(&FrameTimeDiagnosticsPlugin::FPS)
&& let Some(value) = fps.smoothed()
{
*writer.text(entity, 1) = format!("{value:.2}");
}
}
fn customize_overlay(
overlay_config: Res<FpsOverlayConfig>,
query: Query<Entity, With<FpsText>>,
mut writer: TextUiWriter,
) {
for entity in &query {
writer.for_each_font(entity, |mut font| {
*font = overlay_config.text_config.clone();
});
writer.for_each_color(entity, |mut color| color.0 = overlay_config.text_color);
}
}
fn toggle_display(
overlay_config: Res<FpsOverlayConfig>,
mut text_node: Single<&mut Node, (With<FpsText>, Without<FrameTimeGraph>)>,
mut graph_node: Single<&mut Node, (With<FrameTimeGraph>, Without<FpsText>)>,
) {
if overlay_config.enabled {
text_node.display = bevy_ui::Display::DEFAULT;
} else {
text_node.display = bevy_ui::Display::None;
}
if overlay_config.frame_time_graph_config.enabled {
let font_size = overlay_config.text_config.font_size;
graph_node.width = Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE);
graph_node.height = Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE);
graph_node.display = bevy_ui::Display::DEFAULT;
} else {
graph_node.display = bevy_ui::Display::None;
}
}