Skip to main content

bevy_render/diagnostic/
mod.rs

1//! Infrastructure for recording render diagnostics.
2//!
3//! For more info, see [`RenderDiagnosticsPlugin`].
4
5mod erased_render_asset_diagnostic_plugin;
6pub(crate) mod internal;
7mod mesh_allocator_diagnostic_plugin;
8mod render_asset_diagnostic_plugin;
9#[cfg(feature = "tracing-tracy")]
10mod tracy_gpu;
11
12use alloc::{borrow::Cow, sync::Arc};
13use bevy_ecs::{
14    schedule::IntoScheduleConfigs,
15    system::{Res, ResMut},
16    world::{FromWorld, World},
17};
18use core::marker::PhantomData;
19use wgpu::{BufferSlice, CommandEncoder};
20
21use bevy_app::{App, Plugin, PreUpdate};
22
23use crate::{
24    renderer::{PendingCommandBuffers, RenderGraph, RenderGraphSystems},
25    GpuResourceAppExt, Render, RenderApp, RenderSystems,
26};
27
28use self::internal::{sync_diagnostics, Pass, RenderDiagnosticsMutex, WriteTimestamp};
29pub use self::{
30    erased_render_asset_diagnostic_plugin::ErasedRenderAssetDiagnosticPlugin,
31    internal::DiagnosticsRecorder, mesh_allocator_diagnostic_plugin::MeshAllocatorDiagnosticPlugin,
32    render_asset_diagnostic_plugin::RenderAssetDiagnosticPlugin,
33};
34
35use crate::renderer::RenderDevice;
36
37/// Enables collecting render diagnostics, such as CPU/GPU elapsed time per render pass,
38/// as well as pipeline statistics (number of primitives, number of shader invocations, etc).
39///
40/// To access the diagnostics, you can use the [`DiagnosticsStore`](bevy_diagnostic::DiagnosticsStore) resource,
41/// add [`LogDiagnosticsPlugin`](bevy_diagnostic::LogDiagnosticsPlugin), or use [Tracy](https://github.com/bevyengine/bevy/blob/main/docs/profiling.md#tracy-renderqueue).
42///
43/// To record diagnostics in your own passes:
44///  1. First, obtain the diagnostic recorder using [`RenderContext::diagnostic_recorder`](crate::renderer::RenderContext::diagnostic_recorder).
45///
46///     It won't do anything unless [`RenderDiagnosticsPlugin`] is present,
47///     so you're free to omit `#[cfg]` clauses.
48///     ```ignore
49///     let diagnostics = render_context.diagnostic_recorder();
50///     ```
51///  2. Begin the span inside a command encoder, or a render/compute pass encoder.
52///     ```ignore
53///     let time_span = diagnostics.time_span(render_context.command_encoder(), "shadows");
54///     ```
55///  3. End the span, providing the encoder (or the same render/compute pass).
56///     ```ignore
57///     time_span.end(render_context.command_encoder());
58///     ```
59///
60/// # Supported platforms
61/// Timestamp queries and pipeline statistics are currently supported only on Vulkan and DX12.
62/// On other platforms (Metal, WebGPU, WebGL2) only CPU time will be recorded.
63#[derive(Default)]
64pub struct RenderDiagnosticsPlugin;
65
66impl Plugin for RenderDiagnosticsPlugin {
67    fn build(&self, app: &mut App) {
68        let render_diagnostics_mutex = RenderDiagnosticsMutex::default();
69        app.insert_resource(render_diagnostics_mutex.clone())
70            .add_systems(PreUpdate, sync_diagnostics);
71
72        if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
73            render_app.insert_resource(render_diagnostics_mutex);
74        }
75    }
76
77    fn finish(&self, app: &mut App) {
78        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
79            return;
80        };
81
82        render_app.init_gpu_resource::<DiagnosticsRecorder>();
83
84        render_app
85            .add_systems(
86                Render,
87                begin_diagnostics_frame
88                    .after(RenderSystems::ExtractCommands)
89                    .before(RenderSystems::PrepareAssets),
90            )
91            .add_systems(
92                RenderGraph,
93                (
94                    resolve_encoder
95                        .after(RenderGraphSystems::Render)
96                        .before(RenderGraphSystems::Submit),
97                    finish_diagnostics_frame.in_set(RenderGraphSystems::Finish),
98                ),
99            );
100    }
101}
102
103impl FromWorld for DiagnosticsRecorder {
104    fn from_world(world: &mut World) -> Self {
105        DiagnosticsRecorder::new(world.resource(), world.resource(), world.resource())
106    }
107}
108
109/// Starts the diagnostics recorder for the frame.
110pub fn begin_diagnostics_frame(mut recorder: ResMut<DiagnosticsRecorder>) {
111    recorder.begin_frame();
112}
113
114/// Resolves the encoder used for diagnostic recording
115pub fn resolve_encoder(
116    mut recorder: ResMut<DiagnosticsRecorder>,
117    render_device: Res<RenderDevice>,
118    mut pending_buffers: ResMut<PendingCommandBuffers>,
119) {
120    let mut encoder =
121        render_device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
122    recorder.resolve(&mut encoder);
123    pending_buffers.push_encoder(encoder, "resolve_diagnostics");
124}
125
126/// Ends the current frame for the diagnostics recorder and syncs it with the main world.
127fn finish_diagnostics_frame(
128    mut recorder: ResMut<DiagnosticsRecorder>,
129    render_device: Res<RenderDevice>,
130    mutex: Res<RenderDiagnosticsMutex>,
131) {
132    let mutex = mutex.0.clone();
133    recorder.finish_frame(&render_device, move |diagnostics| {
134        *mutex.lock().unwrap() = Some(diagnostics);
135    });
136}
137
138/// Allows recording diagnostic spans.
139pub trait RecordDiagnostics: Send + Sync {
140    /// Begin a time span, which will record elapsed CPU and GPU time.
141    ///
142    /// Returns a guard, which will panic on drop unless you end the span.
143    fn time_span<E, N>(&self, encoder: &mut E, name: N) -> TimeSpanGuard<'_, Self, E>
144    where
145        E: WriteTimestamp,
146        N: Into<Cow<'static, str>>,
147    {
148        self.begin_time_span(encoder, name.into());
149        TimeSpanGuard {
150            recorder: self,
151            marker: PhantomData,
152        }
153    }
154
155    /// Begin a pass span, which will record elapsed CPU and GPU time,
156    /// as well as pipeline statistics on supported platforms.
157    ///
158    /// Returns a guard, which will panic on drop unless you end the span.
159    fn pass_span<P, N>(&self, pass: &mut P, name: N) -> PassSpanGuard<'_, Self, P>
160    where
161        P: Pass,
162        N: Into<Cow<'static, str>>,
163    {
164        let name = name.into();
165        self.begin_pass_span(pass, name.clone());
166        PassSpanGuard {
167            recorder: self,
168            name,
169            marker: PhantomData,
170        }
171    }
172
173    /// Reads an `f32` from the specified buffer and uploads it as a diagnostic.
174    ///
175    /// The provided buffer slice must be 4 bytes long, and the buffer must have [`wgpu::BufferUsages::COPY_SRC`];
176    fn record_f32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
177    where
178        N: Into<Cow<'static, str>>;
179
180    /// Reads a `u32` from the specified buffer and uploads it as a diagnostic.
181    ///
182    /// The provided buffer slice must be 4 bytes long, and the buffer must have [`wgpu::BufferUsages::COPY_SRC`];
183    fn record_u32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
184    where
185        N: Into<Cow<'static, str>>;
186
187    #[doc(hidden)]
188    fn begin_time_span<E: WriteTimestamp>(&self, encoder: &mut E, name: Cow<'static, str>);
189
190    #[doc(hidden)]
191    fn end_time_span<E: WriteTimestamp>(&self, encoder: &mut E);
192
193    #[doc(hidden)]
194    fn begin_pass_span<P: Pass>(&self, pass: &mut P, name: Cow<'static, str>);
195
196    #[doc(hidden)]
197    fn end_pass_span<P: Pass>(&self, pass: &mut P);
198}
199
200/// Guard returned by [`RecordDiagnostics::time_span`].
201///
202/// Will panic on drop unless [`TimeSpanGuard::end`] is called.
203pub struct TimeSpanGuard<'a, R: ?Sized, E> {
204    recorder: &'a R,
205    marker: PhantomData<E>,
206}
207
208impl<R: RecordDiagnostics + ?Sized, E: WriteTimestamp> TimeSpanGuard<'_, R, E> {
209    /// End the span.
210    pub fn end(self, encoder: &mut E) {
211        self.recorder.end_time_span(encoder);
212        core::mem::forget(self);
213    }
214}
215
216impl<R: ?Sized, E> Drop for TimeSpanGuard<'_, R, E> {
217    fn drop(&mut self) {
218        bevy_log::error!("TimeSpanScope::end was never called");
219    }
220}
221
222/// Guard returned by [`RecordDiagnostics::pass_span`].
223///
224/// Will panic on drop unless [`PassSpanGuard::end`] is called.
225pub struct PassSpanGuard<'a, R: ?Sized, P> {
226    recorder: &'a R,
227    name: Cow<'static, str>,
228    marker: PhantomData<P>,
229}
230
231impl<R: RecordDiagnostics + ?Sized, P: Pass> PassSpanGuard<'_, R, P> {
232    /// End the span. You have to provide the same pass which was used to begin the span.
233    pub fn end(self, pass: &mut P) {
234        self.recorder.end_pass_span(pass);
235        core::mem::forget(self);
236    }
237}
238
239impl<R: ?Sized, P> Drop for PassSpanGuard<'_, R, P> {
240    fn drop(&mut self) {
241        panic!("PassSpanGuard::end was never called for {}", self.name)
242    }
243}
244
245impl<T: RecordDiagnostics> RecordDiagnostics for Option<Arc<T>> {
246    fn record_f32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
247    where
248        N: Into<Cow<'static, str>>,
249    {
250        if let Some(recorder) = &self {
251            recorder.record_f32(command_encoder, buffer, name);
252        }
253    }
254
255    fn record_u32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
256    where
257        N: Into<Cow<'static, str>>,
258    {
259        if let Some(recorder) = &self {
260            recorder.record_u32(command_encoder, buffer, name);
261        }
262    }
263
264    fn begin_time_span<E: WriteTimestamp>(&self, encoder: &mut E, name: Cow<'static, str>) {
265        if let Some(recorder) = &self {
266            recorder.begin_time_span(encoder, name);
267        }
268    }
269
270    fn end_time_span<E: WriteTimestamp>(&self, encoder: &mut E) {
271        if let Some(recorder) = &self {
272            recorder.end_time_span(encoder);
273        }
274    }
275
276    fn begin_pass_span<P: Pass>(&self, pass: &mut P, name: Cow<'static, str>) {
277        if let Some(recorder) = &self {
278            recorder.begin_pass_span(pass, name);
279        }
280    }
281
282    fn end_pass_span<P: Pass>(&self, pass: &mut P) {
283        if let Some(recorder) = &self {
284            recorder.end_pass_span(pass);
285        }
286    }
287}
288
289impl<'a, T: RecordDiagnostics> RecordDiagnostics for Option<&'a T> {
290    fn record_f32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
291    where
292        N: Into<Cow<'static, str>>,
293    {
294        if let Some(recorder) = self {
295            recorder.record_f32(command_encoder, buffer, name);
296        }
297    }
298
299    fn record_u32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
300    where
301        N: Into<Cow<'static, str>>,
302    {
303        if let Some(recorder) = self {
304            recorder.record_u32(command_encoder, buffer, name);
305        }
306    }
307
308    fn begin_time_span<E: WriteTimestamp>(&self, encoder: &mut E, name: Cow<'static, str>) {
309        if let Some(recorder) = self {
310            recorder.begin_time_span(encoder, name);
311        }
312    }
313
314    fn end_time_span<E: WriteTimestamp>(&self, encoder: &mut E) {
315        if let Some(recorder) = self {
316            recorder.end_time_span(encoder);
317        }
318    }
319
320    fn begin_pass_span<P: Pass>(&self, pass: &mut P, name: Cow<'static, str>) {
321        if let Some(recorder) = self {
322            recorder.begin_pass_span(pass, name);
323        }
324    }
325
326    fn end_pass_span<P: Pass>(&self, pass: &mut P) {
327        if let Some(recorder) = self {
328            recorder.end_pass_span(pass);
329        }
330    }
331}