Path: blob/main/crates/bevy_dev_tools/src/fps_overlay.rs
9350 views
//! Module containing logic for FPS overlay.12use bevy_app::{Plugin, Startup, Update};3use bevy_asset::Assets;4use bevy_color::Color;5use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};6use bevy_ecs::{7component::Component,8entity::Entity,9query::{With, Without},10reflect::ReflectResource,11resource::Resource,12schedule::{common_conditions::resource_changed, IntoScheduleConfigs, SystemSet},13system::{Commands, Query, Res, ResMut, Single},14};15use bevy_picking::Pickable;16use bevy_reflect::Reflect;17use bevy_render::storage::ShaderBuffer;18use bevy_text::{RemSize, TextColor, TextFont, TextSpan};19use bevy_time::common_conditions::on_timer;20use bevy_ui::{21widget::{Text, TextUiWriter},22ComputedUiRenderTargetInfo, FlexDirection, GlobalZIndex, Node, PositionType, Val,23};24#[cfg(not(all(target_arch = "wasm32", not(feature = "webgpu"))))]25use bevy_ui_render::prelude::MaterialNode;26use core::time::Duration;27use tracing::warn;2829#[cfg(not(all(target_arch = "wasm32", not(feature = "webgpu"))))]30use crate::frame_time_graph::FrameTimeGraphConfigUniform;31use crate::frame_time_graph::{FrameTimeGraphPlugin, FrametimeGraphMaterial};3233/// [`GlobalZIndex`] used to render the fps overlay.34///35/// We use a number slightly under `i32::MAX` so you can render on top of it if you really need to.36pub const FPS_OVERLAY_ZINDEX: i32 = i32::MAX - 32;3738// Warn the user if the interval is below this threshold.39const MIN_SAFE_INTERVAL: Duration = Duration::from_millis(50);4041// Used to scale the frame time graph based on the fps text size42const FRAME_TIME_GRAPH_WIDTH_SCALE: f32 = 6.0;43const FRAME_TIME_GRAPH_HEIGHT_SCALE: f32 = 2.0;4445/// A plugin that adds an FPS overlay to the Bevy application.46///47/// This plugin will add the [`FrameTimeDiagnosticsPlugin`] if it wasn't added before.48///49/// Note: It is recommended to use native overlay of rendering statistics when possible for lower overhead and more accurate results.50/// The correct way to do this will vary by platform:51/// - **Metal**: setting env variable `MTL_HUD_ENABLED=1`52#[derive(Default)]53pub struct FpsOverlayPlugin {54/// Starting configuration of overlay, this can be later be changed through [`FpsOverlayConfig`] resource.55pub config: FpsOverlayConfig,56}5758/// System sets for FPS overlay updates.59#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]60pub enum FpsOverlaySystems {61/// Applies config changes to the overlay UI.62Customize,63/// Updates the overlay contents.64UpdateText,65}6667impl Plugin for FpsOverlayPlugin {68fn build(&self, app: &mut bevy_app::App) {69// TODO: Use plugin dependencies, see https://github.com/bevyengine/bevy/issues/6970if !app.is_plugin_added::<FrameTimeDiagnosticsPlugin>() {71app.add_plugins(FrameTimeDiagnosticsPlugin::default());72}7374if !app.is_plugin_added::<FrameTimeGraphPlugin>() {75app.add_plugins(FrameTimeGraphPlugin);76}7778if self.config.refresh_interval < MIN_SAFE_INTERVAL {79warn!(80"Low refresh interval ({:?}) may degrade performance. \81Min recommended: {:?}.",82self.config.refresh_interval, MIN_SAFE_INTERVAL83);84}8586app.insert_resource(self.config.clone())87.configure_sets(88Update,89FpsOverlaySystems::Customize.before(FpsOverlaySystems::UpdateText),90)91.add_systems(Startup, setup)92.add_systems(93Update,94(95(toggle_display, customize_overlay)96.run_if(resource_changed::<FpsOverlayConfig>)97.in_set(FpsOverlaySystems::Customize),98update_text99.run_if(on_timer(self.config.refresh_interval))100.in_set(FpsOverlaySystems::UpdateText),101),102);103}104}105106/// Configuration options for the FPS overlay.107#[derive(Resource, Clone, Reflect)]108#[reflect(Resource)]109pub struct FpsOverlayConfig {110/// Configuration of text in the overlay.111pub text_config: TextFont,112/// Color of text in the overlay.113pub text_color: Color,114/// Displays the FPS overlay if true.115pub enabled: bool,116/// The period after which the FPS overlay re-renders.117///118/// Defaults to once every 100 ms.119pub refresh_interval: Duration,120/// Configuration of the frame time graph121pub frame_time_graph_config: FrameTimeGraphConfig,122}123124impl Default for FpsOverlayConfig {125fn default() -> Self {126FpsOverlayConfig {127text_config: TextFont::from_font_size(32.),128text_color: Color::WHITE,129enabled: true,130refresh_interval: Duration::from_millis(100),131// TODO set this to display refresh rate if possible132frame_time_graph_config: FrameTimeGraphConfig::target_fps(60.0),133}134}135}136137/// Configuration of the frame time graph138#[derive(Clone, Copy, Reflect)]139pub struct FrameTimeGraphConfig {140/// Is the graph visible141pub enabled: bool,142/// The minimum acceptable FPS143///144/// Anything below this will show a red bar145pub min_fps: f32,146/// The target FPS147///148/// Anything above this will show a green bar149pub target_fps: f32,150}151152impl FrameTimeGraphConfig {153/// Constructs a default config for a given target fps154pub fn target_fps(target_fps: f32) -> Self {155Self {156target_fps,157..Self::default()158}159}160}161162impl Default for FrameTimeGraphConfig {163fn default() -> Self {164Self {165enabled: true,166min_fps: 30.0,167target_fps: 60.0,168}169}170}171172#[derive(Component)]173struct FpsText;174175#[derive(Component)]176struct FrameTimeGraph;177178fn setup(179mut commands: Commands,180overlay_config: Res<FpsOverlayConfig>,181#[cfg_attr(182all(target_arch = "wasm32", not(feature = "webgpu")),183expect(unused, reason = "Unused variables in wasm32 without webgpu feature")184)]185(mut frame_time_graph_materials, mut buffers): (186ResMut<Assets<FrametimeGraphMaterial>>,187ResMut<Assets<ShaderBuffer>>,188),189) {190commands191.spawn((192Node {193// We need to make sure the overlay doesn't affect the position of other UI nodes194position_type: PositionType::Absolute,195flex_direction: FlexDirection::Column,196..Default::default()197},198// Render overlay on top of everything199GlobalZIndex(FPS_OVERLAY_ZINDEX),200Pickable::IGNORE,201))202.with_children(|p| {203p.spawn((204Text::new("FPS: "),205overlay_config.text_config.clone(),206TextColor(overlay_config.text_color),207FpsText,208Pickable::IGNORE,209))210.with_child((TextSpan::default(), overlay_config.text_config.clone()));211212#[cfg(all(target_arch = "wasm32", not(feature = "webgpu")))]213{214if overlay_config.frame_time_graph_config.enabled {215use tracing::warn;216217warn!("Frame time graph is not supported with WebGL. Consider if WebGPU is viable for your usecase.");218}219}220#[cfg(not(all(target_arch = "wasm32", not(feature = "webgpu"))))]221{222// Todo: Needs a better design that works with responsive sizing.223let font_size = 20.;224p.spawn((225Node {226width: Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE),227height: Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE),228display: if overlay_config.frame_time_graph_config.enabled {229bevy_ui::Display::DEFAULT230} else {231bevy_ui::Display::None232},233..Default::default()234},235Pickable::IGNORE,236MaterialNode::from(frame_time_graph_materials.add(FrametimeGraphMaterial {237values: buffers.add(ShaderBuffer {238// Initialize with dummy data because the default (`data: None`) will239// cause a panic in the shader if the frame time graph is constructed240// with `enabled: false`.241data: Some(vec![0, 0, 0, 0]),242..Default::default()243}),244config: FrameTimeGraphConfigUniform::new(245overlay_config.frame_time_graph_config.target_fps,246overlay_config.frame_time_graph_config.min_fps,247true,248),249})),250FrameTimeGraph,251));252}253});254}255256fn update_text(257diagnostic: Res<DiagnosticsStore>,258query: Query<Entity, With<FpsText>>,259mut writer: TextUiWriter,260) {261if let Ok(entity) = query.single()262&& let Some(fps) = diagnostic.get(&FrameTimeDiagnosticsPlugin::FPS)263&& let Some(value) = fps.smoothed()264{265*writer.text(entity, 1) = format!("{value:.2}");266}267}268269fn customize_overlay(270overlay_config: Res<FpsOverlayConfig>,271query: Query<Entity, With<FpsText>>,272mut writer: TextUiWriter,273) {274for entity in &query {275writer.for_each_font(entity, |mut font| {276*font = overlay_config.text_config.clone();277});278writer.for_each_color(entity, |mut color| color.0 = overlay_config.text_color);279}280}281282fn toggle_display(283overlay_config: Res<FpsOverlayConfig>,284mut text_node: Single<285(&mut Node, &ComputedUiRenderTargetInfo),286(With<FpsText>, Without<FrameTimeGraph>),287>,288mut graph_node: Single<&mut Node, (With<FrameTimeGraph>, Without<FpsText>)>,289rem_size: Res<RemSize>,290) {291if overlay_config.enabled {292text_node.0.display = bevy_ui::Display::DEFAULT;293} else {294text_node.0.display = bevy_ui::Display::None;295}296297if overlay_config.frame_time_graph_config.enabled {298// Scale the frame time graph based on the font size of the overlay299let font_size = overlay_config300.text_config301.font_size302.eval(text_node.1.logical_size(), rem_size.0);303graph_node.width = Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE);304graph_node.height = Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE);305306graph_node.display = bevy_ui::Display::DEFAULT;307} else {308graph_node.display = bevy_ui::Display::None;309}310}311312313