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, RenderApp,
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.add_systems(
85            RenderGraph,
86            (
87                begin_diagnostics_frame.in_set(RenderGraphSystems::Begin),
88                resolve_encoder
89                    .after(RenderGraphSystems::Render)
90                    .before(RenderGraphSystems::Submit),
91                finish_diagnostics_frame.in_set(RenderGraphSystems::Finish),
92            ),
93        );
94    }
95}
96
97impl FromWorld for DiagnosticsRecorder {
98    fn from_world(world: &mut World) -> Self {
99        DiagnosticsRecorder::new(world.resource(), world.resource(), world.resource())
100    }
101}
102
103/// Starts the diagnostics recorder for the frame.
104pub fn begin_diagnostics_frame(mut recorder: ResMut<DiagnosticsRecorder>) {
105    recorder.begin_frame();
106}
107
108/// Resolves the encoder used for diagnostic recording
109pub fn resolve_encoder(
110    mut recorder: ResMut<DiagnosticsRecorder>,
111    render_device: Res<RenderDevice>,
112    mut pending_buffers: ResMut<PendingCommandBuffers>,
113) {
114    let mut encoder =
115        render_device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
116    recorder.resolve(&mut encoder);
117    pending_buffers.push_encoder(encoder);
118}
119
120/// Ends the current frame for the diagnostics recorder and syncs it with the main world.
121fn finish_diagnostics_frame(
122    mut recorder: ResMut<DiagnosticsRecorder>,
123    render_device: Res<RenderDevice>,
124    mutex: Res<RenderDiagnosticsMutex>,
125) {
126    let mutex = mutex.0.clone();
127    recorder.finish_frame(&render_device, move |diagnostics| {
128        *mutex.lock().unwrap() = Some(diagnostics);
129    });
130}
131
132/// Allows recording diagnostic spans.
133pub trait RecordDiagnostics: Send + Sync {
134    /// Begin a time span, which will record elapsed CPU and GPU time.
135    ///
136    /// Returns a guard, which will panic on drop unless you end the span.
137    fn time_span<E, N>(&self, encoder: &mut E, name: N) -> TimeSpanGuard<'_, Self, E>
138    where
139        E: WriteTimestamp,
140        N: Into<Cow<'static, str>>,
141    {
142        self.begin_time_span(encoder, name.into());
143        TimeSpanGuard {
144            recorder: self,
145            marker: PhantomData,
146        }
147    }
148
149    /// Begin a pass span, which will record elapsed CPU and GPU time,
150    /// as well as pipeline statistics on supported platforms.
151    ///
152    /// Returns a guard, which will panic on drop unless you end the span.
153    fn pass_span<P, N>(&self, pass: &mut P, name: N) -> PassSpanGuard<'_, Self, P>
154    where
155        P: Pass,
156        N: Into<Cow<'static, str>>,
157    {
158        let name = name.into();
159        self.begin_pass_span(pass, name.clone());
160        PassSpanGuard {
161            recorder: self,
162            name,
163            marker: PhantomData,
164        }
165    }
166
167    /// Reads a f32 from the specified buffer and uploads it as a diagnostic.
168    ///
169    /// The provided buffer slice must be 4 bytes long, and the buffer must have [`wgpu::BufferUsages::COPY_SRC`];
170    fn record_f32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
171    where
172        N: Into<Cow<'static, str>>;
173
174    /// Reads a u32 from the specified buffer and uploads it as a diagnostic.
175    ///
176    /// The provided buffer slice must be 4 bytes long, and the buffer must have [`wgpu::BufferUsages::COPY_SRC`];
177    fn record_u32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
178    where
179        N: Into<Cow<'static, str>>;
180
181    #[doc(hidden)]
182    fn begin_time_span<E: WriteTimestamp>(&self, encoder: &mut E, name: Cow<'static, str>);
183
184    #[doc(hidden)]
185    fn end_time_span<E: WriteTimestamp>(&self, encoder: &mut E);
186
187    #[doc(hidden)]
188    fn begin_pass_span<P: Pass>(&self, pass: &mut P, name: Cow<'static, str>);
189
190    #[doc(hidden)]
191    fn end_pass_span<P: Pass>(&self, pass: &mut P);
192}
193
194/// Guard returned by [`RecordDiagnostics::time_span`].
195///
196/// Will panic on drop unless [`TimeSpanGuard::end`] is called.
197pub struct TimeSpanGuard<'a, R: ?Sized, E> {
198    recorder: &'a R,
199    marker: PhantomData<E>,
200}
201
202impl<R: RecordDiagnostics + ?Sized, E: WriteTimestamp> TimeSpanGuard<'_, R, E> {
203    /// End the span.
204    pub fn end(self, encoder: &mut E) {
205        self.recorder.end_time_span(encoder);
206        core::mem::forget(self);
207    }
208}
209
210impl<R: ?Sized, E> Drop for TimeSpanGuard<'_, R, E> {
211    fn drop(&mut self) {
212        bevy_log::error!("TimeSpanScope::end was never called");
213    }
214}
215
216/// Guard returned by [`RecordDiagnostics::pass_span`].
217///
218/// Will panic on drop unless [`PassSpanGuard::end`] is called.
219pub struct PassSpanGuard<'a, R: ?Sized, P> {
220    recorder: &'a R,
221    name: Cow<'static, str>,
222    marker: PhantomData<P>,
223}
224
225impl<R: RecordDiagnostics + ?Sized, P: Pass> PassSpanGuard<'_, R, P> {
226    /// End the span. You have to provide the same pass which was used to begin the span.
227    pub fn end(self, pass: &mut P) {
228        self.recorder.end_pass_span(pass);
229        core::mem::forget(self);
230    }
231}
232
233impl<R: ?Sized, P> Drop for PassSpanGuard<'_, R, P> {
234    fn drop(&mut self) {
235        panic!("PassSpanGuard::end was never called for {}", self.name)
236    }
237}
238
239impl<T: RecordDiagnostics> RecordDiagnostics for Option<Arc<T>> {
240    fn record_f32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
241    where
242        N: Into<Cow<'static, str>>,
243    {
244        if let Some(recorder) = &self {
245            recorder.record_f32(command_encoder, buffer, name);
246        }
247    }
248
249    fn record_u32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
250    where
251        N: Into<Cow<'static, str>>,
252    {
253        if let Some(recorder) = &self {
254            recorder.record_u32(command_encoder, buffer, name);
255        }
256    }
257
258    fn begin_time_span<E: WriteTimestamp>(&self, encoder: &mut E, name: Cow<'static, str>) {
259        if let Some(recorder) = &self {
260            recorder.begin_time_span(encoder, name);
261        }
262    }
263
264    fn end_time_span<E: WriteTimestamp>(&self, encoder: &mut E) {
265        if let Some(recorder) = &self {
266            recorder.end_time_span(encoder);
267        }
268    }
269
270    fn begin_pass_span<P: Pass>(&self, pass: &mut P, name: Cow<'static, str>) {
271        if let Some(recorder) = &self {
272            recorder.begin_pass_span(pass, name);
273        }
274    }
275
276    fn end_pass_span<P: Pass>(&self, pass: &mut P) {
277        if let Some(recorder) = &self {
278            recorder.end_pass_span(pass);
279        }
280    }
281}
282
283impl<'a, T: RecordDiagnostics> RecordDiagnostics for Option<&'a T> {
284    fn record_f32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
285    where
286        N: Into<Cow<'static, str>>,
287    {
288        if let Some(recorder) = self {
289            recorder.record_f32(command_encoder, buffer, name);
290        }
291    }
292
293    fn record_u32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
294    where
295        N: Into<Cow<'static, str>>,
296    {
297        if let Some(recorder) = self {
298            recorder.record_u32(command_encoder, buffer, name);
299        }
300    }
301
302    fn begin_time_span<E: WriteTimestamp>(&self, encoder: &mut E, name: Cow<'static, str>) {
303        if let Some(recorder) = self {
304            recorder.begin_time_span(encoder, name);
305        }
306    }
307
308    fn end_time_span<E: WriteTimestamp>(&self, encoder: &mut E) {
309        if let Some(recorder) = self {
310            recorder.end_time_span(encoder);
311        }
312    }
313
314    fn begin_pass_span<P: Pass>(&self, pass: &mut P, name: Cow<'static, str>) {
315        if let Some(recorder) = self {
316            recorder.begin_pass_span(pass, name);
317        }
318    }
319
320    fn end_pass_span<P: Pass>(&self, pass: &mut P) {
321        if let Some(recorder) = self {
322            recorder.end_pass_span(pass);
323        }
324    }
325}