Path: blob/main/crates/bevy_dev_tools/src/fps_overlay.rs
6849 views
//! Module containing logic for FPS overlay.12use bevy_app::{Plugin, Startup, Update};3use bevy_asset::{Assets, Handle};4use bevy_color::Color;5use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};6use bevy_ecs::{7component::Component,8entity::Entity,9prelude::Local,10query::{With, Without},11resource::Resource,12schedule::{common_conditions::resource_changed, IntoScheduleConfigs},13system::{Commands, Query, Res, ResMut, Single},14};15use bevy_picking::Pickable;16use bevy_render::storage::ShaderStorageBuffer;17use bevy_text::{Font, TextColor, TextFont, TextSpan};18use bevy_time::Time;19use bevy_ui::{20widget::{Text, TextUiWriter},21FlexDirection, GlobalZIndex, Node, PositionType, Val,22};23use bevy_ui_render::prelude::MaterialNode;24use core::time::Duration;2526use crate::frame_time_graph::{27FrameTimeGraphConfigUniform, FrameTimeGraphPlugin, FrametimeGraphMaterial,28};2930/// [`GlobalZIndex`] used to render the fps overlay.31///32/// We use a number slightly under `i32::MAX` so you can render on top of it if you really need to.33pub const FPS_OVERLAY_ZINDEX: i32 = i32::MAX - 32;3435// Used to scale the frame time graph based on the fps text size36const FRAME_TIME_GRAPH_WIDTH_SCALE: f32 = 6.0;37const FRAME_TIME_GRAPH_HEIGHT_SCALE: f32 = 2.0;3839/// A plugin that adds an FPS overlay to the Bevy application.40///41/// This plugin will add the [`FrameTimeDiagnosticsPlugin`] if it wasn't added before.42///43/// Note: It is recommended to use native overlay of rendering statistics when possible for lower overhead and more accurate results.44/// The correct way to do this will vary by platform:45/// - **Metal**: setting env variable `MTL_HUD_ENABLED=1`46#[derive(Default)]47pub struct FpsOverlayPlugin {48/// Starting configuration of overlay, this can be later be changed through [`FpsOverlayConfig`] resource.49pub config: FpsOverlayConfig,50}5152impl Plugin for FpsOverlayPlugin {53fn build(&self, app: &mut bevy_app::App) {54// TODO: Use plugin dependencies, see https://github.com/bevyengine/bevy/issues/6955if !app.is_plugin_added::<FrameTimeDiagnosticsPlugin>() {56app.add_plugins(FrameTimeDiagnosticsPlugin::default());57}5859if !app.is_plugin_added::<FrameTimeGraphPlugin>() {60app.add_plugins(FrameTimeGraphPlugin);61}6263app.insert_resource(self.config.clone())64.add_systems(Startup, setup)65.add_systems(66Update,67(68(toggle_display, customize_overlay)69.run_if(resource_changed::<FpsOverlayConfig>),70update_text,71),72);73}74}7576/// Configuration options for the FPS overlay.77#[derive(Resource, Clone)]78pub struct FpsOverlayConfig {79/// Configuration of text in the overlay.80pub text_config: TextFont,81/// Color of text in the overlay.82pub text_color: Color,83/// Displays the FPS overlay if true.84pub enabled: bool,85/// The period after which the FPS overlay re-renders.86///87/// Defaults to once every 100 ms.88pub refresh_interval: Duration,89/// Configuration of the frame time graph90pub frame_time_graph_config: FrameTimeGraphConfig,91}9293impl Default for FpsOverlayConfig {94fn default() -> Self {95FpsOverlayConfig {96text_config: TextFont {97font: Handle::<Font>::default(),98font_size: 32.0,99..Default::default()100},101text_color: Color::WHITE,102enabled: true,103refresh_interval: Duration::from_millis(100),104// TODO set this to display refresh rate if possible105frame_time_graph_config: FrameTimeGraphConfig::target_fps(60.0),106}107}108}109110/// Configuration of the frame time graph111#[derive(Clone, Copy)]112pub struct FrameTimeGraphConfig {113/// Is the graph visible114pub enabled: bool,115/// The minimum acceptable FPS116///117/// Anything below this will show a red bar118pub min_fps: f32,119/// The target FPS120///121/// Anything above this will show a green bar122pub target_fps: f32,123}124125impl FrameTimeGraphConfig {126/// Constructs a default config for a given target fps127pub fn target_fps(target_fps: f32) -> Self {128Self {129target_fps,130..Self::default()131}132}133}134135impl Default for FrameTimeGraphConfig {136fn default() -> Self {137Self {138enabled: true,139min_fps: 30.0,140target_fps: 60.0,141}142}143}144145#[derive(Component)]146struct FpsText;147148#[derive(Component)]149struct FrameTimeGraph;150151fn setup(152mut commands: Commands,153overlay_config: Res<FpsOverlayConfig>,154mut frame_time_graph_materials: ResMut<Assets<FrametimeGraphMaterial>>,155mut buffers: ResMut<Assets<ShaderStorageBuffer>>,156) {157commands158.spawn((159Node {160// We need to make sure the overlay doesn't affect the position of other UI nodes161position_type: PositionType::Absolute,162flex_direction: FlexDirection::Column,163..Default::default()164},165// Render overlay on top of everything166GlobalZIndex(FPS_OVERLAY_ZINDEX),167Pickable::IGNORE,168))169.with_children(|p| {170p.spawn((171Text::new("FPS: "),172overlay_config.text_config.clone(),173TextColor(overlay_config.text_color),174FpsText,175Pickable::IGNORE,176))177.with_child((TextSpan::default(), overlay_config.text_config.clone()));178179let font_size = overlay_config.text_config.font_size;180p.spawn((181Node {182width: Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE),183height: Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE),184display: if overlay_config.frame_time_graph_config.enabled {185bevy_ui::Display::DEFAULT186} else {187bevy_ui::Display::None188},189..Default::default()190},191Pickable::IGNORE,192MaterialNode::from(frame_time_graph_materials.add(FrametimeGraphMaterial {193values: buffers.add(ShaderStorageBuffer {194// Initialize with dummy data because the default (`data: None`) will195// cause a panic in the shader if the frame time graph is constructed196// with `enabled: false`.197data: Some(vec![0, 0, 0, 0]),198..Default::default()199}),200config: FrameTimeGraphConfigUniform::new(201overlay_config.frame_time_graph_config.target_fps,202overlay_config.frame_time_graph_config.min_fps,203true,204),205})),206FrameTimeGraph,207));208});209}210211fn update_text(212diagnostic: Res<DiagnosticsStore>,213query: Query<Entity, With<FpsText>>,214mut writer: TextUiWriter,215time: Res<Time>,216config: Res<FpsOverlayConfig>,217mut time_since_rerender: Local<Duration>,218) {219*time_since_rerender += time.delta();220if *time_since_rerender >= config.refresh_interval {221*time_since_rerender = Duration::ZERO;222for entity in &query {223if let Some(fps) = diagnostic.get(&FrameTimeDiagnosticsPlugin::FPS)224&& let Some(value) = fps.smoothed()225{226*writer.text(entity, 1) = format!("{value:.2}");227}228}229}230}231232fn customize_overlay(233overlay_config: Res<FpsOverlayConfig>,234query: Query<Entity, With<FpsText>>,235mut writer: TextUiWriter,236) {237for entity in &query {238writer.for_each_font(entity, |mut font| {239*font = overlay_config.text_config.clone();240});241writer.for_each_color(entity, |mut color| color.0 = overlay_config.text_color);242}243}244245fn toggle_display(246overlay_config: Res<FpsOverlayConfig>,247mut text_node: Single<&mut Node, (With<FpsText>, Without<FrameTimeGraph>)>,248mut graph_node: Single<&mut Node, (With<FrameTimeGraph>, Without<FpsText>)>,249) {250if overlay_config.enabled {251text_node.display = bevy_ui::Display::DEFAULT;252} else {253text_node.display = bevy_ui::Display::None;254}255256if 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_node.width = Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE);260graph_node.height = Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE);261262graph_node.display = bevy_ui::Display::DEFAULT;263} else {264graph_node.display = bevy_ui::Display::None;265}266}267268269