//! Module containing logic for FPS overlay. use bevy_app::{Plugin, Startup, Update}; use bevy_asset::{Assets, Handle}; use bevy_color::Color; use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin}; use bevy_ecs::{ change_detection::DetectChangesMut, component::Component, entity::Entity, prelude::Local, query::With, resource::Resource, schedule::{common_conditions::resource_changed, IntoScheduleConfigs}, system::{Commands, Query, Res, ResMut}, }; use bevy_render::{storage::ShaderStorageBuffer, view::Visibility}; use bevy_text::{Font, TextColor, TextFont, TextSpan}; use bevy_time::Time; use bevy_ui::{ widget::{Text, TextUiWriter}, FlexDirection, GlobalZIndex, Node, PositionType, Val, }; use bevy_ui_render::prelude::MaterialNode; use core::time::Duration; use crate::frame_time_graph::{ FrameTimeGraphConfigUniform, FrameTimeGraphPlugin, FrametimeGraphMaterial, }; /// [`GlobalZIndex`] used to render the fps overlay. /// /// We use a number slightly under `i32::MAX` so you can render on top of it if you really need to. pub const FPS_OVERLAY_ZINDEX: i32 = i32::MAX - 32; // Used to scale the frame time graph based on the fps text size const FRAME_TIME_GRAPH_WIDTH_SCALE: f32 = 6.0; const FRAME_TIME_GRAPH_HEIGHT_SCALE: f32 = 2.0; /// A plugin that adds an FPS overlay to the Bevy application. /// /// This plugin will add the [`FrameTimeDiagnosticsPlugin`] if it wasn't added before. /// /// Note: It is recommended to use native overlay of rendering statistics when possible for lower overhead and more accurate results. /// The correct way to do this will vary by platform: /// - **Metal**: setting env variable `MTL_HUD_ENABLED=1` #[derive(Default)] pub struct FpsOverlayPlugin { /// Starting configuration of overlay, this can be later be changed through [`FpsOverlayConfig`] resource. pub config: FpsOverlayConfig, } impl Plugin for FpsOverlayPlugin { fn build(&self, app: &mut bevy_app::App) { // TODO: Use plugin dependencies, see https://github.com/bevyengine/bevy/issues/69 if !app.is_plugin_added::() { app.add_plugins(FrameTimeDiagnosticsPlugin::default()); } if !app.is_plugin_added::() { app.add_plugins(FrameTimeGraphPlugin); } app.insert_resource(self.config.clone()) .add_systems(Startup, setup) .add_systems( Update, ( (toggle_display, customize_overlay) .run_if(resource_changed::), update_text, ), ); } } /// Configuration options for the FPS overlay. #[derive(Resource, Clone)] pub struct FpsOverlayConfig { /// Configuration of text in the overlay. pub text_config: TextFont, /// Color of text in the overlay. pub text_color: Color, /// Displays the FPS overlay if true. pub enabled: bool, /// The period after which the FPS overlay re-renders. /// /// Defaults to once every 100 ms. pub refresh_interval: Duration, /// Configuration of the frame time graph pub frame_time_graph_config: FrameTimeGraphConfig, } impl Default for FpsOverlayConfig { fn default() -> Self { FpsOverlayConfig { text_config: TextFont { font: Handle::::default(), font_size: 32.0, ..Default::default() }, text_color: Color::WHITE, enabled: true, refresh_interval: Duration::from_millis(100), // TODO set this to display refresh rate if possible frame_time_graph_config: FrameTimeGraphConfig::target_fps(60.0), } } } /// Configuration of the frame time graph #[derive(Clone, Copy)] pub struct FrameTimeGraphConfig { /// Is the graph visible pub enabled: bool, /// The minimum acceptable FPS /// /// Anything below this will show a red bar pub min_fps: f32, /// The target FPS /// /// Anything above this will show a green bar pub target_fps: f32, } impl FrameTimeGraphConfig { /// Constructs a default config for a given target fps 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, mut frame_time_graph_materials: ResMut>, mut buffers: ResMut>, ) { commands .spawn(( Node { // We need to make sure the overlay doesn't affect the position of other UI nodes position_type: PositionType::Absolute, flex_direction: FlexDirection::Column, ..Default::default() }, // Render overlay on top of everything GlobalZIndex(FPS_OVERLAY_ZINDEX), )) .with_children(|p| { p.spawn(( Text::new("FPS: "), overlay_config.text_config.clone(), TextColor(overlay_config.text_color), FpsText, )) .with_child((TextSpan::default(), overlay_config.text_config.clone())); 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() }, MaterialNode::from(frame_time_graph_materials.add(FrametimeGraphMaterial { values: buffers.add(ShaderStorageBuffer::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, query: Query>, mut writer: TextUiWriter, time: Res