Skip to main content

bevy_dev_tools/frame_time_graph/
mod.rs

1//! Module containing logic for the frame time graph
2
3use bevy_app::{Plugin, Update};
4use bevy_asset::{load_internal_asset, uuid_handle, Asset, Assets, Handle};
5use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
6use bevy_ecs::{
7    schedule::IntoScheduleConfigs,
8    system::{Res, ResMut},
9};
10use bevy_math::ops::log2;
11use bevy_reflect::TypePath;
12use bevy_render::{
13    render_resource::{AsBindGroup, ShaderType},
14    storage::ShaderBuffer,
15};
16use bevy_shader::{Shader, ShaderRef};
17use bevy_ui_render::prelude::{UiMaterial, UiMaterialPlugin};
18
19use crate::fps_overlay::{FpsOverlayConfig, FpsOverlaySystems};
20
21const FRAME_TIME_GRAPH_SHADER_HANDLE: Handle<Shader> =
22    {
    ::bevy_asset::Handle::Uuid({
            const OUTPUT: ::uuid::Uuid =
                match ::uuid::Uuid::try_parse("4e38163a-5782-47a5-af52-d9161472ab59")
                    {
                    ::uuid::__macro_support::Ok(u) => u,
                    ::uuid::__macro_support::Err(_) => {
                        ::core::panicking::panic_fmt(format_args!("invalid UUID"));
                    }
                };
            OUTPUT
        }, core::marker::PhantomData)
}uuid_handle!("4e38163a-5782-47a5-af52-d9161472ab59");
23
24/// Plugin that sets up everything to render the frame time graph material
25pub struct FrameTimeGraphPlugin;
26
27impl Plugin for FrameTimeGraphPlugin {
28    fn build(&self, app: &mut bevy_app::App) {
29        {
    let mut assets =
        app.world_mut().resource_mut::<::bevy_asset::Assets<_>>();
    assets.insert(FRAME_TIME_GRAPH_SHADER_HANDLE.id(),
            (Shader::from_wgsl)("#import bevy_ui::ui_vertex_output::UiVertexOutput\n\n@group(1) @binding(0) var<storage> values: array<f32>;\nstruct Config {\n    dt_min: f32,\n    dt_max: f32,\n    dt_min_log2: f32,\n    dt_max_log2: f32,\n    proportional_width: u32,\n}\n@group(1) @binding(1) var<uniform> config: Config;\n\nconst RED: vec4<f32> = vec4(1.0, 0.0, 0.0, 1.0);\nconst GREEN: vec4<f32> = vec4(0.0, 1.0, 0.0, 1.0);\n\n// Gets a color based on the delta time\n// TODO use customizable gradient\nfn color_from_dt(dt: f32) -> vec4<f32> {\n    return mix(GREEN, RED, dt / config.dt_max);\n}\n\n// Draw an SDF square\nfn sdf_square(pos: vec2<f32>, half_size: vec2<f32>, offset: vec2<f32>) -> f32 {\n    let p = pos - offset;\n    let dist = abs(p) - half_size;\n    let outside_dist = length(max(dist, vec2<f32>(0.0, 0.0)));\n    let inside_dist = min(max(dist.x, dist.y), 0.0);\n    return outside_dist + inside_dist;\n}\n\n@fragment\nfn fragment(in: UiVertexOutput) -> @location(0) vec4<f32> {\n    let dt_min = config.dt_min;\n    let dt_max = config.dt_max;\n    let dt_min_log2 = config.dt_min_log2;\n    let dt_max_log2 = config.dt_max_log2;\n\n    // The general algorithm is highly inspired by\n    // <https://asawicki.info/news_1758_an_idea_for_visualization_of_frame_times>\n\n    let len = arrayLength(&values);\n    var graph_width = 0.0;\n    for (var i = 0u; i <= len; i += 1u) {\n        let dt = values[len - i];\n\n        var frame_width: f32;\n        if config.proportional_width == 1u {\n            frame_width = (dt / dt_min) / f32(len);\n        } else {\n            frame_width = 0.015;\n        }\n\n        let frame_height_factor = (log2(dt) - dt_min_log2) / (dt_max_log2 - dt_min_log2);\n        let frame_height_factor_norm = min(max(0.0, frame_height_factor), 1.0);\n        let frame_height = mix(0.0, 1.0, frame_height_factor_norm);\n\n        let size = vec2(frame_width, frame_height) / 2.0;\n        let offset = vec2(1.0 - graph_width - size.x, 1. - size.y);\n        if (sdf_square(in.uv, size, offset) < 0.0) {\n            return color_from_dt(dt);\n        }\n\n        graph_width += frame_width;\n    }\n\n    return vec4(0.0, 0.0, 0.0, 0.5);\n}\n\n",
                std::path::Path::new("src/frame_time_graph/mod.rs").parent().unwrap().join("frame_time_graph.wgsl").to_string_lossy())).unwrap();
};load_internal_asset!(
30            app,
31            FRAME_TIME_GRAPH_SHADER_HANDLE,
32            "frame_time_graph.wgsl",
33            Shader::from_wgsl
34        );
35
36        // TODO: Use plugin dependencies, see https://github.com/bevyengine/bevy/issues/69
37        if !app.is_plugin_added::<FrameTimeDiagnosticsPlugin>() {
38            {
    ::core::panicking::panic_fmt(format_args!("Requires FrameTimeDiagnosticsPlugin"));
};panic!("Requires FrameTimeDiagnosticsPlugin");
39            // app.add_plugins(FrameTimeDiagnosticsPlugin);
40        }
41
42        app.add_plugins(UiMaterialPlugin::<FrametimeGraphMaterial>::default())
43            .add_systems(
44                Update,
45                update_frame_time_values.in_set(FpsOverlaySystems::UpdateText),
46            );
47    }
48}
49
50/// The config values sent to the frame time graph shader
51#[derive(#[automatically_derived]
impl ::core::fmt::Debug for FrameTimeGraphConfigUniform {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field5_finish(f,
            "FrameTimeGraphConfigUniform", "dt_min", &self.dt_min, "dt_max",
            &self.dt_max, "dt_min_log2", &self.dt_min_log2, "dt_max_log2",
            &self.dt_max_log2, "proportional_width",
            &&self.proportional_width)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for FrameTimeGraphConfigUniform {
    #[inline]
    fn clone(&self) -> FrameTimeGraphConfigUniform {
        let _: ::core::clone::AssertParamIsClone<f32>;
        let _: ::core::clone::AssertParamIsClone<u32>;
        *self
    }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for FrameTimeGraphConfigUniform { }Copy, impl bevy_render::render_resource::encase::private::ShaderSize for
    FrameTimeGraphConfigUniform where
    f32: bevy_render::render_resource::encase::private::ShaderSize,
    f32: bevy_render::render_resource::encase::private::ShaderSize,
    f32: bevy_render::render_resource::encase::private::ShaderSize,
    f32: bevy_render::render_resource::encase::private::ShaderSize,
    u32: bevy_render::render_resource::encase::private::ShaderSize {}ShaderType)]
52pub struct FrameTimeGraphConfigUniform {
53    // minimum expected delta time
54    dt_min: f32,
55    // maximum expected delta time
56    dt_max: f32,
57    dt_min_log2: f32,
58    dt_max_log2: f32,
59    // controls whether or not the bars width are proportional to their delta time
60    proportional_width: u32,
61}
62
63impl FrameTimeGraphConfigUniform {
64    /// `proportional_width`: controls whether or not the bars width are proportional to their delta time
65    pub fn new(target_fps: f32, min_fps: f32, proportional_width: bool) -> Self {
66        // we want an upper limit that is above the target otherwise the bars will disappear
67        let dt_min = 1. / (target_fps * 1.2);
68        let dt_max = 1. / min_fps;
69        Self {
70            dt_min,
71            dt_max,
72            dt_min_log2: log2(dt_min),
73            dt_max_log2: log2(dt_max),
74            proportional_width: u32::from(proportional_width),
75        }
76    }
77}
78
79/// The material used to render the frame time graph ui node
80#[derive(impl bevy_render::render_resource::AsBindGroup for FrametimeGraphMaterial {
    type Data = ();
    type Param =
        (bevy_ecs::system::lifetimeless::SRes<bevy_render::render_asset::RenderAssets<bevy_render::texture::GpuImage>>,
        bevy_ecs::system::lifetimeless::SRes<bevy_render::texture::FallbackImage>,
        bevy_ecs::system::lifetimeless::SRes<bevy_render::render_asset::RenderAssets<bevy_render::storage::GpuShaderBuffer>>);
    fn label() -> &'static str { "FrametimeGraphMaterial" }
    fn unprepared_bind_group(&self,
        layout: &bevy_render::render_resource::BindGroupLayout,
        render_device: &bevy_render::renderer::RenderDevice,
        (images, fallback_image, storage_buffers):
            &mut bevy_ecs::system::SystemParamItem<'_, '_, Self::Param>,
        force_no_bindless: bool)
        ->
            ::core::result::Result<bevy_render::render_resource::UnpreparedBindGroup,
            bevy_render::render_resource::AsBindGroupError> {
        let (uniform_binding_type, uniform_buffer_usages) =
            (bevy_render::render_resource::BufferBindingType::Uniform,
                bevy_render::render_resource::BufferUsages::UNIFORM);
        let bindings =
            bevy_render::render_resource::BindingResources(::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
                        [(0u32,
                                    bevy_render::render_resource::OwnedBindingResource::Buffer({
                                            let handle:
                                                    &bevy_asset::Handle<bevy_render::storage::ShaderBuffer> =
                                                (&self.values);
                                            storage_buffers.get(handle).ok_or_else(||
                                                                bevy_render::render_resource::AsBindGroupError::RetryNextUpdate)?.buffer.clone()
                                        })),
                                {
                                    let mut buffer =
                                        bevy_render::render_resource::encase::UniformBuffer::new(::std::vec::Vec::new());
                                    buffer.write(&self.config).unwrap();
                                    (1u32,
                                        bevy_render::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data(&bevy_render::render_resource::BufferInitDescriptor {
                                                        label: ::core::option::Option::None,
                                                        usage: uniform_buffer_usages,
                                                        contents: buffer.as_ref(),
                                                    })))
                                }])));
        ::core::result::Result::Ok(bevy_render::render_resource::UnpreparedBindGroup {
                bindings,
            })
    }
    #[allow(clippy :: unused_unit)]
    fn bind_group_data(&self) -> Self::Data { () }
    fn bind_group_layout_entries(render_device:
            &bevy_render::renderer::RenderDevice, force_no_bindless: bool)
        ->
            ::std::vec::Vec<bevy_render::render_resource::BindGroupLayoutEntry> {
        let actual_bindless_slot_count:
                ::core::option::Option<::core::num::NonZeroU32> =
            ::core::option::Option::None;
        let (uniform_binding_type, uniform_buffer_usages) =
            (bevy_render::render_resource::BufferBindingType::Uniform,
                bevy_render::render_resource::BufferUsages::UNIFORM);
        let mut bind_group_layout_entries = ::std::vec::Vec::new();
        match actual_bindless_slot_count {
            ::core::option::Option::Some(bindless_slot_count) => {
                let bindless_index_table_range =
                    bevy_render::render_resource::BindlessIndex(0)..bevy_render::render_resource::BindlessIndex(0u32);
                let used_resource_types = &[];
                bind_group_layout_entries.extend(bevy_render::render_resource::create_bindless_bind_group_layout_entries(bindless_index_table_range.end.0
                                - bindless_index_table_range.start.0,
                            bindless_slot_count.into(),
                            bevy_render::render_resource::BindingNumber(0),
                            used_resource_types).into_iter());
                ;
            }
            ::core::option::Option::None => {
                bind_group_layout_entries.push(bevy_render::render_resource::BindGroupLayoutEntry {
                        binding: 0u32,
                        visibility: bevy_render::render_resource::ShaderStages::VERTEX
                            | bevy_render::render_resource::ShaderStages::FRAGMENT,
                        ty: bevy_render::render_resource::BindingType::Buffer {
                            ty: bevy_render::render_resource::BufferBindingType::Storage {
                                read_only: true,
                            },
                            has_dynamic_offset: false,
                            min_binding_size: ::core::option::Option::None,
                        },
                        count: actual_bindless_slot_count,
                    });
                bind_group_layout_entries.push(bevy_render::render_resource::BindGroupLayoutEntry {
                        binding: 1u32,
                        visibility: bevy_render::render_resource::ShaderStages::FRAGMENT
                                | bevy_render::render_resource::ShaderStages::VERTEX |
                            bevy_render::render_resource::ShaderStages::COMPUTE,
                        ty: bevy_render::render_resource::BindingType::Buffer {
                            ty: uniform_binding_type,
                            has_dynamic_offset: false,
                            min_binding_size: ::core::option::Option::Some(<FrameTimeGraphConfigUniform
                                        as bevy_render::render_resource::ShaderType>::min_size()),
                        },
                        count: actual_bindless_slot_count,
                    });
                ;
            }
        };
        bind_group_layout_entries
    }
    fn bindless_descriptor()
        ->
            ::core::option::Option<bevy_render::render_resource::BindlessDescriptor> {
        ::core::option::Option::None
    }
}AsBindGroup, impl bevy_asset::VisitAssetDependencies for FrametimeGraphMaterial {
    fn visit_dependencies(&self,
        visit: &mut impl ::core::ops::FnMut(bevy_asset::UntypedAssetId)) {}
}Asset, const _: () =
    {
        #[allow(deprecated, reason =
        "derives on a deprecated type shouldn't be considered a usage")]
        impl bevy_reflect::TypePath for FrametimeGraphMaterial where  {
            fn type_path() -> &'static str {
                "bevy_dev_tools::frame_time_graph::FrametimeGraphMaterial"
            }
            fn short_type_path() -> &'static str { "FrametimeGraphMaterial" }
            fn type_ident() -> ::core::option::Option<&'static str> {
                ::core::option::Option::Some("FrametimeGraphMaterial")
            }
            fn crate_name() -> ::core::option::Option<&'static str> {
                ::core::option::Option::Some("bevy_dev_tools::frame_time_graph".split(':').next().unwrap())
            }
            fn module_path() -> ::core::option::Option<&'static str> {
                ::core::option::Option::Some("bevy_dev_tools::frame_time_graph")
            }
        }
    };TypePath, #[automatically_derived]
impl ::core::fmt::Debug for FrametimeGraphMaterial {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f,
            "FrametimeGraphMaterial", "values", &self.values, "config",
            &&self.config)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for FrametimeGraphMaterial {
    #[inline]
    fn clone(&self) -> FrametimeGraphMaterial {
        FrametimeGraphMaterial {
            values: ::core::clone::Clone::clone(&self.values),
            config: ::core::clone::Clone::clone(&self.config),
        }
    }
}Clone)]
81pub struct FrametimeGraphMaterial {
82    /// The history of the previous frame times value.
83    ///
84    /// This should be updated every frame to match the frame time history from the [`DiagnosticsStore`]
85    #[storage(0, read_only)]
86    pub values: Handle<ShaderBuffer>, // Vec<f32>,
87    /// The configuration values used by the shader to control how the graph is rendered
88    #[uniform(1)]
89    pub config: FrameTimeGraphConfigUniform,
90}
91
92impl UiMaterial for FrametimeGraphMaterial {
93    fn fragment_shader() -> ShaderRef {
94        FRAME_TIME_GRAPH_SHADER_HANDLE.into()
95    }
96}
97
98/// A system that updates the frame time values sent to the frame time graph
99fn update_frame_time_values(
100    mut frame_time_graph_materials: ResMut<Assets<FrametimeGraphMaterial>>,
101    mut buffers: ResMut<Assets<ShaderBuffer>>,
102    diagnostics_store: Res<DiagnosticsStore>,
103    config: Option<Res<FpsOverlayConfig>>,
104) {
105    if !config.is_none_or(|c| c.frame_time_graph_config.enabled) {
106        return;
107    }
108    let Some(frame_time) = diagnostics_store.get(&FrameTimeDiagnosticsPlugin::FRAME_TIME) else {
109        return;
110    };
111    let frame_times = frame_time
112        .values()
113        // convert to millis
114        .map(|x| *x as f32 / 1000.0)
115        .collect::<Vec<_>>();
116    for (_, material) in frame_time_graph_materials.iter_mut() {
117        let mut buffer = buffers.get_mut(&material.values).unwrap();
118
119        buffer.set_data(frame_times.clone());
120    }
121}