From 210308c6a8bff602231140a3cf29ba4331d24a07 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Wed, 31 Dec 2025 13:05:48 -0500 Subject: [PATCH 1/4] Add render diagnostic functions for reading from a buffer --- crates/bevy_render/src/diagnostic/internal.rs | 98 ++++++++++++++++++- crates/bevy_render/src/diagnostic/mod.rs | 33 +++++++ .../render_diagnostics_additions.md | 35 +++++++ 3 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 release-content/release-notes/render_diagnostics_additions.md diff --git a/crates/bevy_render/src/diagnostic/internal.rs b/crates/bevy_render/src/diagnostic/internal.rs index 2bd360c51bee6..b555df413ff7a 100644 --- a/crates/bevy_render/src/diagnostic/internal.rs +++ b/crates/bevy_render/src/diagnostic/internal.rs @@ -11,8 +11,9 @@ use bevy_ecs::system::{Res, ResMut}; use bevy_platform::time::Instant; use std::sync::Mutex; use wgpu::{ - Buffer, BufferDescriptor, BufferUsages, CommandEncoder, ComputePass, Features, MapMode, - PipelineStatisticsTypes, QuerySet, QuerySetDescriptor, QueryType, RenderPass, + Buffer, BufferDescriptor, BufferSize, BufferSlice, BufferUsages, CommandEncoder, ComputePass, + Device, Features, MapMode, PipelineStatisticsTypes, QuerySet, QuerySetDescriptor, QueryType, + RenderPass, }; use crate::renderer::{RenderAdapterInfo, RenderDevice, RenderQueue, WgpuWrapper}; @@ -144,6 +145,42 @@ impl DiagnosticsRecorder { } impl RecordDiagnostics for DiagnosticsRecorder { + fn record_f32(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N) + where + N: Into>, + { + assert_eq!( + buffer.size(), + BufferSize::new(4).unwrap(), + "DiagnosticsRecorder::record_f32 buffer slice must be 4 bytes long" + ); + assert!( + buffer.buffer().usage().contains(BufferUsages::COPY_SRC), + "DiagnosticsRecorder::record_f32 buffer must have BufferUsages::COPY_SRC" + ); + + self.current_frame_lock() + .record_value(command_encoder, buffer, name.into(), true) + } + + fn record_u32(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N) + where + N: Into>, + { + assert_eq!( + buffer.size(), + BufferSize::new(4).unwrap(), + "DiagnosticsRecorder::record_u32 buffer slice must be 4 bytes long" + ); + assert!( + buffer.buffer().usage().contains(BufferUsages::COPY_SRC), + "DiagnosticsRecorder::record_u32 buffer must have BufferUsages::COPY_SRC" + ); + + self.current_frame_lock() + .record_value(command_encoder, buffer, name.into(), false) + } + fn begin_time_span(&self, encoder: &mut E, span_name: Cow<'static, str>) { self.current_frame_lock() .begin_time_span(encoder, span_name); @@ -174,6 +211,7 @@ struct SpanRecord { } struct FrameData { + device: Device, timestamps_query_set: Option, num_timestamps: u32, supports_timestamps_inside_passes: bool, @@ -187,6 +225,7 @@ struct FrameData { path_components: Vec>, open_spans: Vec, closed_spans: Vec, + value_buffers: Vec<(Buffer, Cow<'static, str>, bool)>, is_mapped: Arc, callback: Option>, #[cfg(feature = "tracing-tracy")] @@ -246,6 +285,7 @@ impl FrameData { }; FrameData { + device: wgpu_device.clone(), timestamps_query_set, num_timestamps: 0, supports_timestamps_inside_passes: features @@ -261,6 +301,7 @@ impl FrameData { path_components: Vec::new(), open_spans: Vec::new(), closed_spans: Vec::new(), + value_buffers: Vec::new(), is_mapped: Arc::new(AtomicBool::new(false)), callback: None, #[cfg(feature = "tracing-tracy")] @@ -367,6 +408,33 @@ impl FrameData { self.closed_spans.last_mut().unwrap() } + fn record_value( + &mut self, + command_encoder: &mut CommandEncoder, + buffer: &BufferSlice, + name: Cow<'static, str>, + is_f32: bool, + ) { + let dest_buffer = self.device.create_buffer(&BufferDescriptor { + label: Some(&format!("render_diagnostic_{name}")), + size: 4, + usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + + command_encoder.copy_buffer_to_buffer( + buffer.buffer(), + buffer.offset(), + &dest_buffer, + 0, + Some(buffer.size().into()), + ); + + command_encoder.map_buffer_on_submit(&dest_buffer, MapMode::Read, .., |_| {}); + + self.value_buffers.push((dest_buffer, name, is_f32)); + } + fn begin_time_span(&mut self, encoder: &mut impl WriteTimestamp, name: Cow<'static, str>) { let begin_instant = Instant::now(); let begin_timestamp_index = self.write_timestamp(encoder, false); @@ -464,6 +532,19 @@ impl FrameData { } } + for (buffer, diagnostic_path, is_f32) in self.value_buffers.drain(..) { + let buffer = buffer.get_mapped_range(..); + diagnostics.push(RenderDiagnostic { + path: DiagnosticPath::new(diagnostic_path), + suffix: "", + value: if is_f32 { + f32::from_le_bytes((*buffer).try_into().unwrap()) as f64 + } else { + u32::from_le_bytes((*buffer).try_into().unwrap()) as f64 + }, + }); + } + callback(RenderDiagnostics(diagnostics)); return; }; @@ -584,6 +665,19 @@ impl FrameData { } } + for (buffer, diagnostic_path, is_f32) in self.value_buffers.drain(..) { + let buffer = buffer.get_mapped_range(..); + diagnostics.push(RenderDiagnostic { + path: DiagnosticPath::new(diagnostic_path), + suffix: "", + value: if is_f32 { + f32::from_le_bytes((*buffer).try_into().unwrap()) as f64 + } else { + u32::from_le_bytes((*buffer).try_into().unwrap()) as f64 + }, + }); + } + callback(RenderDiagnostics(diagnostics)); drop(data); diff --git a/crates/bevy_render/src/diagnostic/mod.rs b/crates/bevy_render/src/diagnostic/mod.rs index d2425d3e80b65..4eb31d39d6685 100644 --- a/crates/bevy_render/src/diagnostic/mod.rs +++ b/crates/bevy_render/src/diagnostic/mod.rs @@ -11,6 +11,7 @@ mod tracy_gpu; use alloc::{borrow::Cow, sync::Arc}; use core::marker::PhantomData; +use wgpu::{BufferSlice, CommandEncoder}; use bevy_app::{App, Plugin, PreUpdate}; @@ -114,6 +115,20 @@ pub trait RecordDiagnostics: Send + Sync { } } + /// Reads a f32 from the specified buffer and uploads it as a diagnostic. + /// + /// The provided buffer slice must be 4 bytes long, and the buffer must have [`wgpu::BufferUsages::COPY_SRC`]; + fn record_f32(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N) + where + N: Into>; + + /// Reads a u32 from the specified buffer and uploads it as a diagnostic. + /// + /// The provided buffer slice must be 4 bytes long, and the buffer must have [`wgpu::BufferUsages::COPY_SRC`]; + fn record_u32(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N) + where + N: Into>; + #[doc(hidden)] fn begin_time_span(&self, encoder: &mut E, name: Cow<'static, str>); @@ -173,6 +188,24 @@ impl Drop for PassSpanGuard<'_, R, P> { } impl RecordDiagnostics for Option> { + fn record_f32(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N) + where + N: Into>, + { + if let Some(recorder) = &self { + recorder.record_f32(command_encoder, buffer, name); + } + } + + fn record_u32(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N) + where + N: Into>, + { + if let Some(recorder) = &self { + recorder.record_u32(command_encoder, buffer, name); + } + } + fn begin_time_span(&self, encoder: &mut E, name: Cow<'static, str>) { if let Some(recorder) = &self { recorder.begin_time_span(encoder, name); diff --git a/release-content/release-notes/render_diagnostics_additions.md b/release-content/release-notes/render_diagnostics_additions.md new file mode 100644 index 0000000000000..52d2f1e253ae9 --- /dev/null +++ b/release-content/release-notes/render_diagnostics_additions.md @@ -0,0 +1,35 @@ +--- +title: Render Diagnostic Additions +authors: ["@JMS55"] +pull_requests: [TODO] +--- + +Bevy's [RenderDiagnosticPlugin](https://docs.rs/bevy/0.19.0/bevy/render/diagnostic/struct.RenderDiagnosticsPlugin.html) has new methods for uploading data from GPU buffers to bevy_diagnostic. + +```rust +impl ViewNode for Foo { + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + _: QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let diagnostics = render_context.diagnostic_recorder(); + + diagnostics.record_u32( + render_context.command_encoder(), + &my_buffer.slice(..), + "my_diagnostics/foo", + ); + + diagnostics.record_f32( + render_context.command_encoder(), + &my_buffer.slice(..), + "my_diagnostics/bar", + ); + + Ok(()) + } +} +``` From 1ac6e7f9b9939d831cf3da770c92367537fbe342 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Wed, 31 Dec 2025 13:08:17 -0500 Subject: [PATCH 2/4] Misc --- .../release-notes/render_diagnostics_additions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/release-content/release-notes/render_diagnostics_additions.md b/release-content/release-notes/render_diagnostics_additions.md index 52d2f1e253ae9..a0b337904caba 100644 --- a/release-content/release-notes/render_diagnostics_additions.md +++ b/release-content/release-notes/render_diagnostics_additions.md @@ -1,7 +1,7 @@ --- title: Render Diagnostic Additions authors: ["@JMS55"] -pull_requests: [TODO] +pull_requests: [22326] --- Bevy's [RenderDiagnosticPlugin](https://docs.rs/bevy/0.19.0/bevy/render/diagnostic/struct.RenderDiagnosticsPlugin.html) has new methods for uploading data from GPU buffers to bevy_diagnostic. @@ -19,13 +19,13 @@ impl ViewNode for Foo { diagnostics.record_u32( render_context.command_encoder(), - &my_buffer.slice(..), + &my_buffer1.slice(..), // Buffer slice must be 4 bytes, and buffer must have BufferUsages::COPY_SRC "my_diagnostics/foo", ); diagnostics.record_f32( render_context.command_encoder(), - &my_buffer.slice(..), + &my_buffer2.slice(..), // Buffer slice must be 4 bytes, and buffer must have BufferUsages::COPY_SRC "my_diagnostics/bar", ); From 1ecaaea8ae5978259a4d9e1dc94a8cb612275f32 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Wed, 31 Dec 2025 13:13:30 -0500 Subject: [PATCH 3/4] Misc --- crates/bevy_render/src/diagnostic/internal.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_render/src/diagnostic/internal.rs b/crates/bevy_render/src/diagnostic/internal.rs index b555df413ff7a..e5b954e32abe5 100644 --- a/crates/bevy_render/src/diagnostic/internal.rs +++ b/crates/bevy_render/src/diagnostic/internal.rs @@ -160,7 +160,7 @@ impl RecordDiagnostics for DiagnosticsRecorder { ); self.current_frame_lock() - .record_value(command_encoder, buffer, name.into(), true) + .record_value(command_encoder, buffer, name.into(), true); } fn record_u32(&self, command_encoder: &mut CommandEncoder, buffer: &BufferSlice, name: N) @@ -178,7 +178,7 @@ impl RecordDiagnostics for DiagnosticsRecorder { ); self.current_frame_lock() - .record_value(command_encoder, buffer, name.into(), false) + .record_value(command_encoder, buffer, name.into(), false); } fn begin_time_span(&self, encoder: &mut E, span_name: Cow<'static, str>) { From 97e2b54f43763da023f1c974f3f9835e5af875bc Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:48:16 -0500 Subject: [PATCH 4/4] Add render prefix to path to match other render statistics --- crates/bevy_render/src/diagnostic/internal.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/bevy_render/src/diagnostic/internal.rs b/crates/bevy_render/src/diagnostic/internal.rs index e5b954e32abe5..ef24788ed8e1d 100644 --- a/crates/bevy_render/src/diagnostic/internal.rs +++ b/crates/bevy_render/src/diagnostic/internal.rs @@ -535,7 +535,10 @@ impl FrameData { for (buffer, diagnostic_path, is_f32) in self.value_buffers.drain(..) { let buffer = buffer.get_mapped_range(..); diagnostics.push(RenderDiagnostic { - path: DiagnosticPath::new(diagnostic_path), + path: DiagnosticPath::from_components( + core::iter::once("render") + .chain(core::iter::once(diagnostic_path.as_ref())), + ), suffix: "", value: if is_f32 { f32::from_le_bytes((*buffer).try_into().unwrap()) as f64 @@ -668,7 +671,9 @@ impl FrameData { for (buffer, diagnostic_path, is_f32) in self.value_buffers.drain(..) { let buffer = buffer.get_mapped_range(..); diagnostics.push(RenderDiagnostic { - path: DiagnosticPath::new(diagnostic_path), + path: DiagnosticPath::from_components( + core::iter::once("render").chain(core::iter::once(diagnostic_path.as_ref())), + ), suffix: "", value: if is_f32 { f32::from_le_bytes((*buffer).try_into().unwrap()) as f64