Skip to main content

bevy_render/diagnostic/
internal.rs

1use alloc::{borrow::Cow, sync::Arc};
2use core::{
3    ops::{DerefMut, Range},
4    sync::atomic::{AtomicBool, Ordering},
5};
6use std::thread::{self, ThreadId};
7
8use bevy_diagnostic::{Diagnostic, DiagnosticMeasurement, DiagnosticPath, DiagnosticsStore};
9use bevy_ecs::resource::Resource;
10use bevy_ecs::system::{Res, ResMut};
11use bevy_platform::time::Instant;
12use std::sync::Mutex;
13use wgpu::{
14    Buffer, BufferDescriptor, BufferSize, BufferSlice, BufferUsages, CommandEncoder, ComputePass,
15    Device, Features, MapMode, PipelineStatisticsTypes, QuerySet, QuerySetDescriptor, QueryType,
16    RenderPass,
17};
18
19use crate::renderer::{wgpu_wrapper, RenderAdapterInfo, RenderDevice, RenderQueue};
20
21use super::RecordDiagnostics;
22
23// buffer offset must be divisible by 256, so this constant must be divisible by 32 (=256/8)
24const MAX_TIMESTAMP_QUERIES: u32 = 256;
25const MAX_PIPELINE_STATISTICS: u32 = 128;
26
27const TIMESTAMP_SIZE: u64 = 8;
28const PIPELINE_STATISTICS_SIZE: u64 = 40;
29
30struct DiagnosticsRecorderInternal {
31    timestamp_period_ns: f32,
32    features: Features,
33    current_frame: Mutex<FrameData>,
34    submitted_frames: Vec<FrameData>,
35    finished_frames: Vec<FrameData>,
36    #[cfg(feature = "tracing-tracy")]
37    tracy_gpu_context: Option<tracy_client::GpuContext>,
38}
39
40wgpu_wrapper!(struct WgpuDiagnosticsRecorderInternal(DiagnosticsRecorderInternal));
41
42/// Records diagnostics into [`QuerySet`]'s keeping track of the mapping between
43/// spans and indices to the corresponding entries in the [`QuerySet`].
44#[derive(Resource)]
45pub struct DiagnosticsRecorder(WgpuDiagnosticsRecorderInternal);
46
47impl DiagnosticsRecorder {
48    /// Creates the new `DiagnosticsRecorder`.
49    pub fn new(
50        adapter_info: &RenderAdapterInfo,
51        device: &RenderDevice,
52        queue: &RenderQueue,
53    ) -> DiagnosticsRecorder {
54        let features = device.features();
55
56        #[cfg(feature = "tracing-tracy")]
57        let tracy_gpu_context =
58            super::tracy_gpu::new_tracy_gpu_context(adapter_info, device, queue);
59        let _ = adapter_info; // Prevent unused variable warnings when tracing-tracy is not enabled
60
61        DiagnosticsRecorder(WgpuDiagnosticsRecorderInternal::new(
62            DiagnosticsRecorderInternal {
63                timestamp_period_ns: queue.get_timestamp_period(),
64                features,
65                current_frame: Mutex::new(FrameData::new(
66                    device,
67                    features,
68                    #[cfg(feature = "tracing-tracy")]
69                    tracy_gpu_context.clone(),
70                )),
71                submitted_frames: Vec::new(),
72                finished_frames: Vec::new(),
73                #[cfg(feature = "tracing-tracy")]
74                tracy_gpu_context,
75            },
76        ))
77    }
78
79    fn current_frame_mut(&mut self) -> &mut FrameData {
80        self.0.current_frame.get_mut().expect("lock poisoned")
81    }
82
83    fn current_frame_lock(&self) -> impl DerefMut<Target = FrameData> + '_ {
84        self.0.current_frame.lock().expect("lock poisoned")
85    }
86
87    /// Begins recording diagnostics for a new frame.
88    pub fn begin_frame(&mut self) {
89        let internal = &mut self.0;
90        let mut idx = 0;
91        while idx < internal.submitted_frames.len() {
92            let timestamp = internal.timestamp_period_ns;
93            if internal.submitted_frames[idx].run_mapped_callback(timestamp) {
94                let removed = internal.submitted_frames.swap_remove(idx);
95                internal.finished_frames.push(removed);
96            } else {
97                idx += 1;
98            }
99        }
100
101        self.current_frame_mut().begin();
102    }
103
104    /// Copies data from [`QuerySet`]'s to a [`Buffer`], after which it can be downloaded to CPU.
105    ///
106    /// Should be called before [`DiagnosticsRecorder::finish_frame`].
107    pub fn resolve(&mut self, encoder: &mut CommandEncoder) {
108        self.current_frame_mut().resolve(encoder);
109    }
110
111    /// Finishes recording diagnostics for the current frame.
112    ///
113    /// The specified `callback` will be invoked when diagnostics become available.
114    ///
115    /// Should be called after [`DiagnosticsRecorder::resolve`],
116    /// and **after** all commands buffers have been queued.
117    pub fn finish_frame(
118        &mut self,
119        device: &RenderDevice,
120        callback: impl FnOnce(RenderDiagnostics) + Send + Sync + 'static,
121    ) {
122        #[cfg(feature = "tracing-tracy")]
123        let tracy_gpu_context = self.0.tracy_gpu_context.clone();
124
125        let internal = &mut self.0;
126        internal
127            .current_frame
128            .get_mut()
129            .expect("lock poisoned")
130            .finish(callback);
131
132        // reuse one of the finished frames, if we can
133        let new_frame = match internal.finished_frames.pop() {
134            Some(frame) => frame,
135            None => FrameData::new(
136                device,
137                internal.features,
138                #[cfg(feature = "tracing-tracy")]
139                tracy_gpu_context,
140            ),
141        };
142
143        let old_frame = core::mem::replace(
144            internal.current_frame.get_mut().expect("lock poisoned"),
145            new_frame,
146        );
147        internal.submitted_frames.push(old_frame);
148    }
149}
150
151impl RecordDiagnostics for DiagnosticsRecorder {
152    fn record_f32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
153    where
154        N: Into<Cow<'static, str>>,
155    {
156        assert_eq!(
157            buffer.size(),
158            BufferSize::new(4).unwrap().get(),
159            "DiagnosticsRecorder::record_f32 buffer slice must be 4 bytes long"
160        );
161        assert!(
162            buffer.buffer().usage().contains(BufferUsages::COPY_SRC),
163            "DiagnosticsRecorder::record_f32 buffer must have BufferUsages::COPY_SRC"
164        );
165
166        self.current_frame_lock()
167            .record_value(command_encoder, buffer, name.into(), true);
168    }
169
170    fn record_u32<N>(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N)
171    where
172        N: Into<Cow<'static, str>>,
173    {
174        assert_eq!(
175            buffer.size(),
176            BufferSize::new(4).unwrap().get(),
177            "DiagnosticsRecorder::record_u32 buffer slice must be 4 bytes long"
178        );
179        assert!(
180            buffer.buffer().usage().contains(BufferUsages::COPY_SRC),
181            "DiagnosticsRecorder::record_u32 buffer must have BufferUsages::COPY_SRC"
182        );
183
184        self.current_frame_lock()
185            .record_value(command_encoder, buffer, name.into(), false);
186    }
187
188    fn begin_time_span<E: WriteTimestamp>(&self, encoder: &mut E, span_name: Cow<'static, str>) {
189        self.current_frame_lock()
190            .begin_time_span(encoder, span_name);
191    }
192
193    fn end_time_span<E: WriteTimestamp>(&self, encoder: &mut E) {
194        self.current_frame_lock().end_time_span(encoder);
195    }
196
197    fn begin_pass_span<P: Pass>(&self, pass: &mut P, span_name: Cow<'static, str>) {
198        self.current_frame_lock().begin_pass(pass, span_name);
199    }
200
201    fn end_pass_span<P: Pass>(&self, pass: &mut P) {
202        self.current_frame_lock().end_pass(pass);
203    }
204}
205
206struct SpanRecord {
207    thread_id: ThreadId,
208    path_range: Range<usize>,
209    pass_kind: Option<PassKind>,
210    begin_timestamp_index: Option<u32>,
211    end_timestamp_index: Option<u32>,
212    begin_instant: Option<Instant>,
213    end_instant: Option<Instant>,
214    pipeline_statistics_index: Option<u32>,
215}
216
217struct FrameData {
218    device: Device,
219    timestamps_query_set: Option<QuerySet>,
220    num_timestamps: u32,
221    supports_timestamps_inside_passes: bool,
222    supports_timestamps_inside_encoders: bool,
223    pipeline_statistics_query_set: Option<QuerySet>,
224    num_pipeline_statistics: u32,
225    buffer_size: u64,
226    pipeline_statistics_buffer_offset: u64,
227    resolve_buffer: Option<Buffer>,
228    read_buffer: Option<Buffer>,
229    path_components: Vec<Cow<'static, str>>,
230    open_spans: Vec<SpanRecord>,
231    closed_spans: Vec<SpanRecord>,
232    value_buffers: Vec<(Buffer, Cow<'static, str>, bool)>,
233    is_mapped: Arc<AtomicBool>,
234    callback: Option<Box<dyn FnOnce(RenderDiagnostics) + Send + Sync + 'static>>,
235    #[cfg(feature = "tracing-tracy")]
236    tracy_gpu_context: Option<tracy_client::GpuContext>,
237}
238
239impl FrameData {
240    fn new(
241        device: &RenderDevice,
242        features: Features,
243        #[cfg(feature = "tracing-tracy")] tracy_gpu_context: Option<tracy_client::GpuContext>,
244    ) -> FrameData {
245        let wgpu_device = device.wgpu_device();
246        let mut buffer_size = 0;
247
248        let timestamps_query_set = if features.contains(Features::TIMESTAMP_QUERY) {
249            buffer_size += u64::from(MAX_TIMESTAMP_QUERIES) * TIMESTAMP_SIZE;
250            Some(wgpu_device.create_query_set(&QuerySetDescriptor {
251                label: Some("timestamps_query_set"),
252                ty: QueryType::Timestamp,
253                count: MAX_TIMESTAMP_QUERIES,
254            }))
255        } else {
256            None
257        };
258
259        let pipeline_statistics_buffer_offset = buffer_size;
260
261        let pipeline_statistics_query_set =
262            if features.contains(Features::PIPELINE_STATISTICS_QUERY) {
263                buffer_size += u64::from(MAX_PIPELINE_STATISTICS) * PIPELINE_STATISTICS_SIZE;
264                Some(wgpu_device.create_query_set(&QuerySetDescriptor {
265                    label: Some("pipeline_statistics_query_set"),
266                    ty: QueryType::PipelineStatistics(PipelineStatisticsTypes::all()),
267                    count: MAX_PIPELINE_STATISTICS,
268                }))
269            } else {
270                None
271            };
272
273        let (resolve_buffer, read_buffer) = if buffer_size > 0 {
274            let resolve_buffer = wgpu_device.create_buffer(&BufferDescriptor {
275                label: Some("render_statistics_resolve_buffer"),
276                size: buffer_size,
277                usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC,
278                mapped_at_creation: false,
279            });
280            let read_buffer = wgpu_device.create_buffer(&BufferDescriptor {
281                label: Some("render_statistics_read_buffer"),
282                size: buffer_size,
283                usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
284                mapped_at_creation: false,
285            });
286            (Some(resolve_buffer), Some(read_buffer))
287        } else {
288            (None, None)
289        };
290
291        FrameData {
292            device: wgpu_device.clone(),
293            timestamps_query_set,
294            num_timestamps: 0,
295            supports_timestamps_inside_passes: features
296                .contains(Features::TIMESTAMP_QUERY_INSIDE_PASSES),
297            supports_timestamps_inside_encoders: features
298                .contains(Features::TIMESTAMP_QUERY_INSIDE_ENCODERS),
299            pipeline_statistics_query_set,
300            num_pipeline_statistics: 0,
301            buffer_size,
302            pipeline_statistics_buffer_offset,
303            resolve_buffer,
304            read_buffer,
305            path_components: Vec::new(),
306            open_spans: Vec::new(),
307            closed_spans: Vec::new(),
308            value_buffers: Vec::new(),
309            is_mapped: Arc::new(AtomicBool::new(false)),
310            callback: None,
311            #[cfg(feature = "tracing-tracy")]
312            tracy_gpu_context,
313        }
314    }
315
316    fn begin(&mut self) {
317        self.num_timestamps = 0;
318        self.num_pipeline_statistics = 0;
319        self.path_components.clear();
320        self.open_spans.clear();
321        self.closed_spans.clear();
322    }
323
324    fn write_timestamp(
325        &mut self,
326        encoder: &mut impl WriteTimestamp,
327        is_inside_pass: bool,
328    ) -> Option<u32> {
329        // `encoder.write_timestamp` is unsupported on WebGPU.
330        if !self.supports_timestamps_inside_encoders {
331            return None;
332        }
333
334        if is_inside_pass && !self.supports_timestamps_inside_passes {
335            return None;
336        }
337
338        if self.num_timestamps >= MAX_TIMESTAMP_QUERIES {
339            return None;
340        }
341
342        let set = self.timestamps_query_set.as_ref()?;
343        let index = self.num_timestamps;
344        encoder.write_timestamp(set, index);
345        self.num_timestamps += 1;
346        Some(index)
347    }
348
349    fn write_pipeline_statistics(
350        &mut self,
351        encoder: &mut impl WritePipelineStatistics,
352    ) -> Option<u32> {
353        if self.num_pipeline_statistics >= MAX_PIPELINE_STATISTICS {
354            return None;
355        }
356
357        let set = self.pipeline_statistics_query_set.as_ref()?;
358        let index = self.num_pipeline_statistics;
359        encoder.begin_pipeline_statistics_query(set, index);
360        self.num_pipeline_statistics += 1;
361        Some(index)
362    }
363
364    fn open_span(
365        &mut self,
366        pass_kind: Option<PassKind>,
367        name: Cow<'static, str>,
368    ) -> &mut SpanRecord {
369        let thread_id = thread::current().id();
370
371        let parent = self.open_spans.iter().rfind(|v| v.thread_id == thread_id);
372
373        let path_range = match &parent {
374            Some(parent) if parent.path_range.end == self.path_components.len() => {
375                parent.path_range.start..parent.path_range.end + 1
376            }
377            Some(parent) => {
378                self.path_components
379                    .extend_from_within(parent.path_range.clone());
380                self.path_components.len() - parent.path_range.len()..self.path_components.len() + 1
381            }
382            None => self.path_components.len()..self.path_components.len() + 1,
383        };
384
385        self.path_components.push(name);
386
387        self.open_spans.push(SpanRecord {
388            thread_id,
389            path_range,
390            pass_kind,
391            begin_timestamp_index: None,
392            end_timestamp_index: None,
393            begin_instant: None,
394            end_instant: None,
395            pipeline_statistics_index: None,
396        });
397
398        self.open_spans.last_mut().unwrap()
399    }
400
401    fn close_span(&mut self) -> &mut SpanRecord {
402        let thread_id = thread::current().id();
403
404        let iter = self.open_spans.iter();
405        let (index, _) = iter
406            .enumerate()
407            .rfind(|(_, v)| v.thread_id == thread_id)
408            .unwrap();
409
410        let span = self.open_spans.swap_remove(index);
411        self.closed_spans.push(span);
412        self.closed_spans.last_mut().unwrap()
413    }
414
415    fn record_value(
416        &mut self,
417        command_encoder: &mut CommandEncoder,
418        buffer: &BufferSlice,
419        name: Cow<'static, str>,
420        is_f32: bool,
421    ) {
422        let dest_buffer = self.device.create_buffer(&BufferDescriptor {
423            label: Some(&format!("render_diagnostic_{name}")),
424            size: 4,
425            usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
426            mapped_at_creation: false,
427        });
428
429        command_encoder.copy_buffer_to_buffer(
430            buffer.buffer(),
431            buffer.offset(),
432            &dest_buffer,
433            0,
434            Some(buffer.size()),
435        );
436
437        command_encoder.map_buffer_on_submit(&dest_buffer, MapMode::Read, .., |_| {});
438
439        self.value_buffers.push((dest_buffer, name, is_f32));
440    }
441
442    fn begin_time_span(&mut self, encoder: &mut impl WriteTimestamp, name: Cow<'static, str>) {
443        let begin_instant = Instant::now();
444        let begin_timestamp_index = self.write_timestamp(encoder, false);
445
446        let span = self.open_span(None, name);
447        span.begin_instant = Some(begin_instant);
448        span.begin_timestamp_index = begin_timestamp_index;
449    }
450
451    fn end_time_span(&mut self, encoder: &mut impl WriteTimestamp) {
452        let end_timestamp_index = self.write_timestamp(encoder, false);
453
454        let span = self.close_span();
455        span.end_timestamp_index = end_timestamp_index;
456        span.end_instant = Some(Instant::now());
457    }
458
459    fn begin_pass<P: Pass>(&mut self, pass: &mut P, name: Cow<'static, str>) {
460        let begin_instant = Instant::now();
461
462        let begin_timestamp_index = self.write_timestamp(pass, true);
463        let pipeline_statistics_index = self.write_pipeline_statistics(pass);
464
465        let span = self.open_span(Some(P::KIND), name);
466        span.begin_instant = Some(begin_instant);
467        span.begin_timestamp_index = begin_timestamp_index;
468        span.pipeline_statistics_index = pipeline_statistics_index;
469    }
470
471    fn end_pass(&mut self, pass: &mut impl Pass) {
472        let end_timestamp_index = self.write_timestamp(pass, true);
473
474        let span = self.close_span();
475        span.end_timestamp_index = end_timestamp_index;
476
477        if span.pipeline_statistics_index.is_some() {
478            pass.end_pipeline_statistics_query();
479        }
480
481        span.end_instant = Some(Instant::now());
482    }
483
484    fn resolve(&mut self, encoder: &mut CommandEncoder) {
485        let Some(resolve_buffer) = &self.resolve_buffer else {
486            return;
487        };
488
489        match &self.timestamps_query_set {
490            Some(set) if self.num_timestamps > 0 => {
491                encoder.resolve_query_set(set, 0..self.num_timestamps, resolve_buffer, 0);
492            }
493            _ => {}
494        }
495
496        match &self.pipeline_statistics_query_set {
497            Some(set) if self.num_pipeline_statistics > 0 => {
498                encoder.resolve_query_set(
499                    set,
500                    0..self.num_pipeline_statistics,
501                    resolve_buffer,
502                    self.pipeline_statistics_buffer_offset,
503                );
504            }
505            _ => {}
506        }
507
508        let Some(read_buffer) = &self.read_buffer else {
509            return;
510        };
511
512        encoder.copy_buffer_to_buffer(resolve_buffer, 0, read_buffer, 0, self.buffer_size);
513    }
514
515    fn diagnostic_path(&self, range: &Range<usize>, field: &str) -> DiagnosticPath {
516        DiagnosticPath::from_components(
517            core::iter::once("render")
518                .chain(self.path_components[range.clone()].iter().map(|v| &**v))
519                .chain(core::iter::once(field)),
520        )
521    }
522
523    fn finish(&mut self, callback: impl FnOnce(RenderDiagnostics) + Send + Sync + 'static) {
524        let Some(read_buffer) = &self.read_buffer else {
525            // we still have cpu timings, so let's use them
526
527            let mut diagnostics = Vec::new();
528
529            for span in &self.closed_spans {
530                if let (Some(begin), Some(end)) = (span.begin_instant, span.end_instant) {
531                    diagnostics.push(RenderDiagnostic {
532                        path: self.diagnostic_path(&span.path_range, "elapsed_cpu"),
533                        suffix: "ms",
534                        value: (end - begin).as_secs_f64() * 1000.0,
535                    });
536                }
537            }
538
539            for (buffer, diagnostic_path, is_f32) in self.value_buffers.drain(..) {
540                let buffer = buffer.get_mapped_range(..).unwrap();
541                diagnostics.push(RenderDiagnostic {
542                    path: DiagnosticPath::from_components(
543                        core::iter::once("render")
544                            .chain(core::iter::once(diagnostic_path.as_ref())),
545                    ),
546                    suffix: "",
547                    value: if is_f32 {
548                        f32::from_le_bytes((*buffer).try_into().unwrap()) as f64
549                    } else {
550                        u32::from_le_bytes((*buffer).try_into().unwrap()) as f64
551                    },
552                });
553            }
554
555            callback(RenderDiagnostics(diagnostics));
556            return;
557        };
558
559        self.callback = Some(Box::new(callback));
560
561        let is_mapped = self.is_mapped.clone();
562        read_buffer.slice(..).map_async(MapMode::Read, move |res| {
563            if let Err(e) = res {
564                bevy_log::warn!("Failed to download render statistics buffer: {e}");
565                return;
566            }
567
568            is_mapped.store(true, Ordering::Release);
569        });
570    }
571
572    // returns true if the frame is considered finished, false otherwise
573    fn run_mapped_callback(&mut self, timestamp_period_ns: f32) -> bool {
574        let Some(read_buffer) = &self.read_buffer else {
575            return true;
576        };
577        if !self.is_mapped.load(Ordering::Acquire) {
578            // need to wait more
579            return false;
580        }
581        let Some(callback) = self.callback.take() else {
582            return true;
583        };
584
585        let data = read_buffer.slice(..).get_mapped_range().unwrap();
586
587        let timestamps = data[..(self.num_timestamps * 8) as usize]
588            .as_chunks()
589            .0
590            .iter()
591            .map(|&v| u64::from_le_bytes(v))
592            .collect::<Vec<u64>>();
593
594        let start = self.pipeline_statistics_buffer_offset as usize;
595        let len = (self.num_pipeline_statistics as usize) * 40;
596        let pipeline_statistics = data[start..start + len]
597            .as_chunks()
598            .0
599            .iter()
600            .map(|&v| u64::from_le_bytes(v))
601            .collect::<Vec<u64>>();
602
603        let mut diagnostics = Vec::new();
604
605        for span in &self.closed_spans {
606            if let (Some(begin), Some(end)) = (span.begin_instant, span.end_instant) {
607                diagnostics.push(RenderDiagnostic {
608                    path: self.diagnostic_path(&span.path_range, "elapsed_cpu"),
609                    suffix: "ms",
610                    value: (end - begin).as_secs_f64() * 1000.0,
611                });
612            }
613
614            if let (Some(begin), Some(end)) = (span.begin_timestamp_index, span.end_timestamp_index)
615            {
616                let begin = timestamps[begin as usize] as f64;
617                let end = timestamps[end as usize] as f64;
618                let value = (end - begin) * (timestamp_period_ns as f64) / 1e6;
619
620                #[cfg(feature = "tracing-tracy")]
621                {
622                    // Calling span_alloc() and end_zone() here instead of in open_span() and close_span() means that tracy does not know where each GPU command was recorded on the CPU timeline.
623                    // Unfortunately we must do it this way, because tracy does not play nicely with multithreaded command recording. The start/end pairs would get all mixed up.
624                    // The GPU spans themselves are still accurate though, and it's probably safe to assume that each GPU span in frame N belongs to the corresponding CPU render node span from frame N-1.
625                    if let Some(tracy_gpu_context) = &self.tracy_gpu_context {
626                        let name = &self.path_components[span.path_range.clone()].join("/");
627                        let mut tracy_gpu_span =
628                            tracy_gpu_context.span_alloc(name, "", "", 0).unwrap();
629                        tracy_gpu_span.end_zone();
630                        tracy_gpu_span.upload_timestamp_start(begin as i64);
631                        tracy_gpu_span.upload_timestamp_end(end as i64);
632                    }
633                }
634
635                diagnostics.push(RenderDiagnostic {
636                    path: self.diagnostic_path(&span.path_range, "elapsed_gpu"),
637                    suffix: "ms",
638                    value,
639                });
640            }
641
642            if let Some(index) = span.pipeline_statistics_index {
643                let index = (index as usize) * 5;
644
645                if span.pass_kind == Some(PassKind::Render) {
646                    diagnostics.push(RenderDiagnostic {
647                        path: self.diagnostic_path(&span.path_range, "vertex_shader_invocations"),
648                        suffix: "",
649                        value: pipeline_statistics[index] as f64,
650                    });
651
652                    diagnostics.push(RenderDiagnostic {
653                        path: self.diagnostic_path(&span.path_range, "clipper_invocations"),
654                        suffix: "",
655                        value: pipeline_statistics[index + 1] as f64,
656                    });
657
658                    diagnostics.push(RenderDiagnostic {
659                        path: self.diagnostic_path(&span.path_range, "clipper_primitives_out"),
660                        suffix: "",
661                        value: pipeline_statistics[index + 2] as f64,
662                    });
663
664                    diagnostics.push(RenderDiagnostic {
665                        path: self.diagnostic_path(&span.path_range, "fragment_shader_invocations"),
666                        suffix: "",
667                        value: pipeline_statistics[index + 3] as f64,
668                    });
669                }
670
671                if span.pass_kind == Some(PassKind::Compute) {
672                    diagnostics.push(RenderDiagnostic {
673                        path: self.diagnostic_path(&span.path_range, "compute_shader_invocations"),
674                        suffix: "",
675                        value: pipeline_statistics[index + 4] as f64,
676                    });
677                }
678            }
679        }
680
681        for (buffer, diagnostic_path, is_f32) in self.value_buffers.drain(..) {
682            let buffer = buffer.get_mapped_range(..).unwrap();
683            diagnostics.push(RenderDiagnostic {
684                path: DiagnosticPath::from_components(
685                    core::iter::once("render").chain(core::iter::once(diagnostic_path.as_ref())),
686                ),
687                suffix: "",
688                value: if is_f32 {
689                    f32::from_le_bytes((*buffer).try_into().unwrap()) as f64
690                } else {
691                    u32::from_le_bytes((*buffer).try_into().unwrap()) as f64
692                },
693            });
694        }
695
696        callback(RenderDiagnostics(diagnostics));
697
698        drop(data);
699        read_buffer.unmap();
700        self.is_mapped.store(false, Ordering::Release);
701
702        true
703    }
704}
705
706/// Resource which stores render diagnostics of the most recent frame.
707#[derive(Debug, Default, Clone, Resource)]
708pub struct RenderDiagnostics(Vec<RenderDiagnostic>);
709
710/// A render diagnostic which has been recorded, but not yet stored in [`DiagnosticsStore`].
711#[derive(Debug, Clone, Resource)]
712pub struct RenderDiagnostic {
713    pub path: DiagnosticPath,
714    pub suffix: &'static str,
715    pub value: f64,
716}
717
718/// Stores render diagnostics before they can be synced with the main app.
719///
720/// This mutex is locked twice per frame:
721///  1. in `PreUpdate`, during [`sync_diagnostics`],
722///  2. after rendering has finished and statistics have been downloaded from GPU.
723#[derive(Debug, Default, Clone, Resource)]
724pub struct RenderDiagnosticsMutex(pub(crate) Arc<Mutex<Option<RenderDiagnostics>>>);
725
726/// Updates render diagnostics measurements.
727pub fn sync_diagnostics(mutex: Res<RenderDiagnosticsMutex>, mut store: ResMut<DiagnosticsStore>) {
728    let Some(diagnostics) = mutex.0.lock().ok().and_then(|mut v| v.take()) else {
729        return;
730    };
731
732    let time = Instant::now();
733
734    for diagnostic in &diagnostics.0 {
735        if store.get(&diagnostic.path).is_none() {
736            store.add(Diagnostic::new(diagnostic.path.clone()).with_suffix(diagnostic.suffix));
737        }
738
739        store
740            .get_mut(&diagnostic.path)
741            .unwrap()
742            .add_measurement(DiagnosticMeasurement {
743                time,
744                value: diagnostic.value,
745            });
746    }
747}
748
749pub trait WriteTimestamp {
750    fn write_timestamp(&mut self, query_set: &QuerySet, index: u32);
751}
752
753impl WriteTimestamp for CommandEncoder {
754    fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) {
755        if cfg!(target_os = "macos") {
756            // When using tracy (and thus this function), rendering was flickering on macOS Tahoe.
757            // See: https://github.com/bevyengine/bevy/issues/22257
758            // The issue seems to be triggered when `write_timestamp` is called very close to frame
759            // presentation.
760            return;
761        }
762        CommandEncoder::write_timestamp(self, query_set, index);
763    }
764}
765
766impl WriteTimestamp for RenderPass<'_> {
767    fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) {
768        RenderPass::write_timestamp(self, query_set, index);
769    }
770}
771
772impl WriteTimestamp for ComputePass<'_> {
773    fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) {
774        ComputePass::write_timestamp(self, query_set, index);
775    }
776}
777
778pub trait WritePipelineStatistics {
779    fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32);
780
781    fn end_pipeline_statistics_query(&mut self);
782}
783
784impl WritePipelineStatistics for RenderPass<'_> {
785    fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32) {
786        RenderPass::begin_pipeline_statistics_query(self, query_set, index);
787    }
788
789    fn end_pipeline_statistics_query(&mut self) {
790        RenderPass::end_pipeline_statistics_query(self);
791    }
792}
793
794impl WritePipelineStatistics for ComputePass<'_> {
795    fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32) {
796        ComputePass::begin_pipeline_statistics_query(self, query_set, index);
797    }
798
799    fn end_pipeline_statistics_query(&mut self) {
800        ComputePass::end_pipeline_statistics_query(self);
801    }
802}
803
804pub trait Pass: WritePipelineStatistics + WriteTimestamp {
805    const KIND: PassKind;
806}
807
808impl Pass for RenderPass<'_> {
809    const KIND: PassKind = PassKind::Render;
810}
811
812impl Pass for ComputePass<'_> {
813    const KIND: PassKind = PassKind::Compute;
814}
815
816#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
817pub enum PassKind {
818    Render,
819    Compute,
820}