Path: blob/main/crates/bevy_dev_tools/src/fps_overlay.rs
6595 views
//! Module containing logic for FPS overlay.12use bevy_app::{Plugin, Startup, Update};3use bevy_asset::{Assets, Handle};4use bevy_camera::visibility::Visibility;5use bevy_color::Color;6use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};7use bevy_ecs::{8change_detection::DetectChangesMut,9component::Component,10entity::Entity,11prelude::Local,12query::With,13resource::Resource,14schedule::{common_conditions::resource_changed, IntoScheduleConfigs},15system::{Commands, Query, Res, ResMut},16};17use bevy_render::storage::ShaderStorageBuffer;18use bevy_text::{Font, TextColor, TextFont, TextSpan};19use bevy_time::Time;20use bevy_ui::{21widget::{Text, TextUiWriter},22FlexDirection, GlobalZIndex, Node, PositionType, Val,23};24use bevy_ui_render::prelude::MaterialNode;25use core::time::Duration;2627use crate::frame_time_graph::{28FrameTimeGraphConfigUniform, FrameTimeGraphPlugin, FrametimeGraphMaterial,29};3031/// [`GlobalZIndex`] used to render the fps overlay.32///33/// We use a number slightly under `i32::MAX` so you can render on top of it if you really need to.34pub const FPS_OVERLAY_ZINDEX: i32 = i32::MAX - 32;3536// Used to scale the frame time graph based on the fps text size37const FRAME_TIME_GRAPH_WIDTH_SCALE: f32 = 6.0;38const FRAME_TIME_GRAPH_HEIGHT_SCALE: f32 = 2.0;3940/// A plugin that adds an FPS overlay to the Bevy application.41///42/// This plugin will add the [`FrameTimeDiagnosticsPlugin`] if it wasn't added before.43///44/// Note: It is recommended to use native overlay of rendering statistics when possible for lower overhead and more accurate results.45/// The correct way to do this will vary by platform:46/// - **Metal**: setting env variable `MTL_HUD_ENABLED=1`47#[derive(Default)]48pub struct FpsOverlayPlugin {49/// Starting configuration of overlay, this can be later be changed through [`FpsOverlayConfig`] resource.50pub config: FpsOverlayConfig,51}5253impl Plugin for FpsOverlayPlugin {54fn build(&self, app: &mut bevy_app::App) {55// TODO: Use plugin dependencies, see https://github.com/bevyengine/bevy/issues/6956if !app.is_plugin_added::<FrameTimeDiagnosticsPlugin>() {57app.add_plugins(FrameTimeDiagnosticsPlugin::default());58}5960if !app.is_plugin_added::<FrameTimeGraphPlugin>() {61app.add_plugins(FrameTimeGraphPlugin);62}6364app.insert_resource(self.config.clone())65.add_systems(Startup, setup)66.add_systems(67Update,68(69(toggle_display, customize_overlay)70.run_if(resource_changed::<FpsOverlayConfig>),71update_text,72),73);74}75}7677/// Configuration options for the FPS overlay.78#[derive(Resource, Clone)]79pub struct FpsOverlayConfig {80/// Configuration of text in the overlay.81pub text_config: TextFont,82/// Color of text in the overlay.83pub text_color: Color,84/// Displays the FPS overlay if true.85pub enabled: bool,86/// The period after which the FPS overlay re-renders.87///88/// Defaults to once every 100 ms.89pub refresh_interval: Duration,90/// Configuration of the frame time graph91pub frame_time_graph_config: FrameTimeGraphConfig,92}9394impl Default for FpsOverlayConfig {95fn default() -> Self {96FpsOverlayConfig {97text_config: TextFont {98font: Handle::<Font>::default(),99font_size: 32.0,100..Default::default()101},102text_color: Color::WHITE,103enabled: true,104refresh_interval: Duration::from_millis(100),105// TODO set this to display refresh rate if possible106frame_time_graph_config: FrameTimeGraphConfig::target_fps(60.0),107}108}109}110111/// Configuration of the frame time graph112#[derive(Clone, Copy)]113pub struct FrameTimeGraphConfig {114/// Is the graph visible115pub enabled: bool,116/// The minimum acceptable FPS117///118/// Anything below this will show a red bar119pub min_fps: f32,120/// The target FPS121///122/// Anything above this will show a green bar123pub target_fps: f32,124}125126impl FrameTimeGraphConfig {127/// Constructs a default config for a given target fps128pub fn target_fps(target_fps: f32) -> Self {129Self {130target_fps,131..Self::default()132}133}134}135136impl Default for FrameTimeGraphConfig {137fn default() -> Self {138Self {139enabled: true,140min_fps: 30.0,141target_fps: 60.0,142}143}144}145146#[derive(Component)]147struct FpsText;148149#[derive(Component)]150struct FrameTimeGraph;151152fn setup(153mut commands: Commands,154overlay_config: Res<FpsOverlayConfig>,155mut frame_time_graph_materials: ResMut<Assets<FrametimeGraphMaterial>>,156mut buffers: ResMut<Assets<ShaderStorageBuffer>>,157) {158commands159.spawn((160Node {161// We need to make sure the overlay doesn't affect the position of other UI nodes162position_type: PositionType::Absolute,163flex_direction: FlexDirection::Column,164..Default::default()165},166// Render overlay on top of everything167GlobalZIndex(FPS_OVERLAY_ZINDEX),168))169.with_children(|p| {170p.spawn((171Text::new("FPS: "),172overlay_config.text_config.clone(),173TextColor(overlay_config.text_color),174FpsText,175))176.with_child((TextSpan::default(), overlay_config.text_config.clone()));177178let font_size = overlay_config.text_config.font_size;179p.spawn((180Node {181width: Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE),182height: Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE),183display: if overlay_config.frame_time_graph_config.enabled {184bevy_ui::Display::DEFAULT185} else {186bevy_ui::Display::None187},188..Default::default()189},190MaterialNode::from(frame_time_graph_materials.add(FrametimeGraphMaterial {191values: buffers.add(ShaderStorageBuffer {192// Initialize with dummy data because the default (`data: None`) will193// cause a panic in the shader if the frame time graph is constructed194// with `enabled: false`.195data: Some(vec![0, 0, 0, 0]),196..Default::default()197}),198config: FrameTimeGraphConfigUniform::new(199overlay_config.frame_time_graph_config.target_fps,200overlay_config.frame_time_graph_config.min_fps,201true,202),203})),204FrameTimeGraph,205));206});207}208209fn update_text(210diagnostic: Res<DiagnosticsStore>,211query: Query<Entity, With<FpsText>>,212mut writer: TextUiWriter,213time: Res<Time>,214config: Res<FpsOverlayConfig>,215mut time_since_rerender: Local<Duration>,216) {217*time_since_rerender += time.delta();218if *time_since_rerender >= config.refresh_interval {219*time_since_rerender = Duration::ZERO;220for entity in &query {221if let Some(fps) = diagnostic.get(&FrameTimeDiagnosticsPlugin::FPS)222&& let Some(value) = fps.smoothed()223{224*writer.text(entity, 1) = format!("{value:.2}");225}226}227}228}229230fn customize_overlay(231overlay_config: Res<FpsOverlayConfig>,232query: Query<Entity, With<FpsText>>,233mut writer: TextUiWriter,234) {235for entity in &query {236writer.for_each_font(entity, |mut font| {237*font = overlay_config.text_config.clone();238});239writer.for_each_color(entity, |mut color| color.0 = overlay_config.text_color);240}241}242243fn toggle_display(244overlay_config: Res<FpsOverlayConfig>,245mut query: Query<&mut Visibility, With<FpsText>>,246mut graph_style: Query<&mut Node, With<FrameTimeGraph>>,247) {248for mut visibility in &mut query {249visibility.set_if_neq(match overlay_config.enabled {250true => Visibility::Visible,251false => Visibility::Hidden,252});253}254255if let Ok(mut graph_style) = graph_style.single_mut() {256if overlay_config.frame_time_graph_config.enabled {257// Scale the frame time graph based on the font size of the overlay258let font_size = overlay_config.text_config.font_size;259graph_style.width = Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE);260graph_style.height = Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE);261262graph_style.display = bevy_ui::Display::DEFAULT;263} else {264graph_style.display = bevy_ui::Display::None;265}266}267}268269270