diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index dba7e76d7c260..7f47e90c67495 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -46,7 +46,7 @@ use bevy_window::{PrimaryWindow, RawHandleWrapper}; use globals::GlobalsPlugin; use renderer::{ sync_render_statistics, RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue, - RenderStatistics, RenderStatisticsMutex, + RenderStatistics, RenderStatisticsMutex, StatisticsRecorder, }; use wgpu::Instance; @@ -380,6 +380,7 @@ impl Plugin for RenderPlugin { render_app .insert_resource(RenderInstance(instance)) .insert_resource(PipelineCache::new(device.clone())) + .insert_resource(StatisticsRecorder::new(&device, &queue)) .insert_resource(device) .insert_resource(queue) .insert_resource(render_adapter) diff --git a/crates/bevy_render/src/renderer/graph_runner.rs b/crates/bevy_render/src/renderer/graph_runner.rs index bc31f2c2ea220..790454bc9141d 100644 --- a/crates/bevy_render/src/renderer/graph_runner.rs +++ b/crates/bevy_render/src/renderer/graph_runner.rs @@ -16,7 +16,7 @@ use crate::{ renderer::{RenderContext, RenderDevice}, }; -use super::RenderStatisticsMutex; +use super::{RenderStatisticsMutex, StatisticsRecorder}; pub(crate) struct RenderGraphRunner; @@ -57,11 +57,14 @@ impl RenderGraphRunner { pub fn run( graph: &RenderGraph, render_device: RenderDevice, + mut statistics_recorder: StatisticsRecorder, queue: &wgpu::Queue, world: &World, finalizer: impl FnOnce(&mut wgpu::CommandEncoder), - ) -> Result<(), RenderGraphRunnerError> { - let mut render_context = RenderContext::new(render_device, queue); + ) -> Result { + statistics_recorder.begin_frame(); + + let mut render_context = RenderContext::new(render_device, statistics_recorder); Self::run_graph(graph, None, &mut render_context, world, &[], None)?; finalizer(render_context.command_encoder()); @@ -72,11 +75,11 @@ impl RenderGraphRunner { } let render_statistics_mutex = world.resource::().0.clone(); - render_context.download_statistics(queue, move |statistics| { + let statistics_recorder = render_context.download_statistics(queue, move |statistics| { *render_statistics_mutex.lock() = Some(statistics); }); - Ok(()) + Ok(statistics_recorder) } fn run_graph( diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index cb4a4abba3821..a2eeed323cd90 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -28,32 +28,41 @@ pub fn render_system(world: &mut World) { world.resource_scope(|world, mut graph: Mut| { graph.update(world); }); + + let statistics_recorder = world.remove_resource::().unwrap(); + let graph = world.resource::(); let render_device = world.resource::(); let render_queue = world.resource::(); - if let Err(e) = RenderGraphRunner::run( + match RenderGraphRunner::run( graph, render_device.clone(), // TODO: is this clone really necessary? + statistics_recorder, &render_queue.0, world, |encoder| { crate::view::screenshot::submit_screenshot_commands(world, encoder); }, ) { - error!("Error running render graph:"); - { - let mut src: &dyn std::error::Error = &e; - loop { - error!("> {}", src); - match src.source() { - Some(s) => src = s, - None => break, + Ok(statistics_recorder) => { + world.insert_resource(statistics_recorder); + } + Err(e) => { + error!("Error running render graph:"); + { + let mut src: &dyn std::error::Error = &e; + loop { + error!("> {}", src); + match src.source() { + Some(s) => src = s, + None => break, + } } } - } - panic!("Error running render graph: {e}"); + panic!("Error running render graph: {e}"); + } } { @@ -298,8 +307,7 @@ pub struct RenderContext { impl RenderContext { /// Creates a new [`RenderContext`] from a [`RenderDevice`]. - pub fn new(render_device: RenderDevice, queue: &Queue) -> Self { - let statistics_recorder = StatisticsRecorder::new(&render_device, queue); + pub fn new(render_device: RenderDevice, statistics_recorder: StatisticsRecorder) -> Self { Self { render_device, command_encoder: None, @@ -313,11 +321,6 @@ impl RenderContext { &self.render_device } - /// Gets the underlying [`StatisticsRecorder`]. - pub fn statistics_encoder(&mut self) -> &mut StatisticsRecorder { - &mut self.statistics_recorder - } - /// Gets the current [`CommandEncoder`]. pub fn command_encoder(&mut self) -> &mut CommandEncoder { self.command_encoder.get_or_insert_with(|| { @@ -368,15 +371,14 @@ impl RenderContext { /// Downloads [`RenderStatistics`] from GPU, asynchronously calling the callback /// when the data is available. Should be caled after `finish`. - /// - /// If the statistics aren't available, the callback won't be invoked. pub fn download_statistics( - &mut self, + mut self, queue: &Queue, callback: impl FnOnce(RenderStatistics) + Send + 'static, - ) { + ) -> StatisticsRecorder { self.statistics_recorder .download(&self.render_device, queue, callback); + self.statistics_recorder } fn flush_encoder(&mut self) { diff --git a/crates/bevy_render/src/renderer/statistics.rs b/crates/bevy_render/src/renderer/statistics.rs index 9023d1a28286e..24b4df927681d 100644 --- a/crates/bevy_render/src/renderer/statistics.rs +++ b/crates/bevy_render/src/renderer/statistics.rs @@ -5,7 +5,7 @@ use bevy_ecs::system::{Res, ResMut, Resource}; use bevy_utils::{Duration, HashMap, Instant}; use parking_lot::Mutex; use wgpu::{ - util::DownloadBuffer, Buffer, BufferDescriptor, BufferUsages, CommandEncoder, + util::DownloadBuffer, Buffer, BufferDescriptor, BufferUsages, CommandEncoder, Features, PipelineStatisticsTypes, QuerySet, QuerySetDescriptor, QueryType, Queue, RenderPass, RenderPassDescriptor, }; @@ -54,11 +54,12 @@ struct PassRecord { /// Records statistics into [`QuerySet`]'s keeping track of the mapping between /// render passes and indices to the corresponding statistics in the [`QuerySet`]. +#[derive(Resource)] pub struct StatisticsRecorder { timestamp_period: f32, - timestamps_query_set: QuerySet, + timestamps_query_set: Option, num_timestamps: u32, - pipeline_statistics_query_set: QuerySet, + pipeline_statistics_query_set: Option, num_pipeline_statistics: u32, pass_records: HashMap, buffer: Option, @@ -67,20 +68,34 @@ pub struct StatisticsRecorder { impl StatisticsRecorder { /// Creates the new `StatisticRecorder` pub fn new(device: &RenderDevice, queue: &Queue) -> StatisticsRecorder { - let timestamp_period = queue.get_timestamp_period(); + let features = device.features(); - let timestamps_query_set = device.wgpu_device().create_query_set(&QuerySetDescriptor { - label: Some("timestamps_query_set"), - ty: QueryType::Timestamp, - count: MAX_TIMESTAMP_QUERIES, - }); + let timestamp_period = if features.contains(Features::TIMESTAMP_QUERY) { + queue.get_timestamp_period() + } else { + 0.0 + }; + + let timestamps_query_set = if features.contains(Features::TIMESTAMP_QUERY_INSIDE_PASSES) { + Some(device.wgpu_device().create_query_set(&QuerySetDescriptor { + label: Some("timestamps_query_set"), + ty: QueryType::Timestamp, + count: MAX_TIMESTAMP_QUERIES, + })) + } else { + None + }; let pipeline_statistics_query_set = - device.wgpu_device().create_query_set(&QuerySetDescriptor { - label: Some("pipeline_statistics_query_set"), - ty: QueryType::PipelineStatistics(PipelineStatisticsTypes::all()), - count: MAX_PIPELINE_STATISTICS, - }); + if features.contains(Features::PIPELINE_STATISTICS_QUERY) { + Some(device.wgpu_device().create_query_set(&QuerySetDescriptor { + label: Some("pipeline_statistics_query_set"), + ty: QueryType::PipelineStatistics(PipelineStatisticsTypes::all()), + count: MAX_PIPELINE_STATISTICS, + })) + } else { + None + }; StatisticsRecorder { timestamp_period, @@ -93,6 +108,14 @@ impl StatisticsRecorder { } } + /// Begins recording statistics for a new frame. + pub fn begin_frame(&mut self) { + self.num_timestamps = 0; + self.num_pipeline_statistics = 0; + self.pass_records.clear(); + self.buffer = None; + } + fn pass_record(&mut self, name: &str) -> &mut PassRecord { self.pass_records.entry(name.into()).or_default() } @@ -100,22 +123,24 @@ impl StatisticsRecorder { fn begin_render_pass(&mut self, pass: &mut RenderPass, name: &str) { let begin_instant = Instant::now(); - let begin_timestamp_index = if self.num_timestamps < MAX_TIMESTAMP_QUERIES { - let index = self.num_timestamps; - pass.write_timestamp(&self.timestamps_query_set, index); - self.num_timestamps += 1; - Some(index) - } else { - None + let begin_timestamp_index = match &self.timestamps_query_set { + Some(set) if self.num_timestamps < MAX_TIMESTAMP_QUERIES => { + let index = self.num_timestamps; + pass.write_timestamp(set, index); + self.num_timestamps += 1; + Some(index) + } + _ => None, }; - let pipeline_statistics_index = if self.num_pipeline_statistics < MAX_PIPELINE_STATISTICS { - let index = self.num_pipeline_statistics; - pass.begin_pipeline_statistics_query(&self.pipeline_statistics_query_set, index); - self.num_pipeline_statistics += 1; - Some(index) - } else { - None + let pipeline_statistics_index = match &self.pipeline_statistics_query_set { + Some(set) if self.num_pipeline_statistics < MAX_PIPELINE_STATISTICS => { + let index = self.num_pipeline_statistics; + pass.begin_pipeline_statistics_query(set, index); + self.num_pipeline_statistics += 1; + Some(index) + } + _ => None, }; let record = self.pass_record(name); @@ -125,13 +150,14 @@ impl StatisticsRecorder { } fn end_render_pass(&mut self, pass: &mut RenderPass, name: &str) { - let end_timestamp_index = if self.num_timestamps < MAX_TIMESTAMP_QUERIES { - let index = self.num_timestamps; - pass.write_timestamp(&self.timestamps_query_set, index); - self.num_timestamps += 1; - Some(index) - } else { - None + let end_timestamp_index = match &self.timestamps_query_set { + Some(set) if self.num_timestamps < MAX_TIMESTAMP_QUERIES => { + let index = self.num_timestamps; + pass.write_timestamp(set, index); + self.num_timestamps += 1; + Some(index) + } + _ => None, }; let record = self.pass_record(name); @@ -161,6 +187,10 @@ impl StatisticsRecorder { /// Copies data from [`QuerySet`]'s to a buffer, after which it can be downloaded to CPU. pub fn resolve(&mut self, encoder: &mut CommandEncoder, device: &RenderDevice) { + if self.timestamps_query_set.is_none() && self.pipeline_statistics_query_set.is_none() { + return; + } + let (buffer_size, pipeline_statistics_offset) = self.buffer_size(); let buffer = device.wgpu_device().create_buffer(&BufferDescriptor { @@ -170,30 +200,29 @@ impl StatisticsRecorder { mapped_at_creation: false, }); - if self.num_timestamps > 0 { - encoder.resolve_query_set( - &self.timestamps_query_set, - 0..self.num_timestamps, - &buffer, - 0, - ); + match &self.timestamps_query_set { + Some(set) if self.num_timestamps > 0 => { + encoder.resolve_query_set(&set, 0..self.num_timestamps, &buffer, 0); + } + _ => {} } - if self.num_pipeline_statistics > 0 { - encoder.resolve_query_set( - &self.pipeline_statistics_query_set, - 0..self.num_pipeline_statistics, - &buffer, - pipeline_statistics_offset, - ); + match &self.pipeline_statistics_query_set { + Some(set) if self.num_pipeline_statistics > 0 => { + encoder.resolve_query_set( + &set, + 0..self.num_pipeline_statistics, + &buffer, + pipeline_statistics_offset, + ); + } + _ => {} } self.buffer = Some(buffer); } /// Downloads the statistics from GPU, asynchronously calling the callback when the data is available. - /// - /// If the statistics aren't available, the callback won't be invoked. pub fn download( &mut self, device: &RenderDevice, @@ -206,7 +235,23 @@ impl StatisticsRecorder { let num_pipeline_statistics = self.num_pipeline_statistics; let pass_records = std::mem::take(&mut self.pass_records); - let Some(buffer) = &self.buffer else { return }; + let Some(buffer) = &self.buffer else { + // we still have cpu timings, so let's use them + + let statistics = pass_records.into_iter().map(|(name, record)| { + let mut statistics = RenderPassStatistics::default(); + + if let (Some(begin), Some(end)) = (record.begin_instant, record.end_instant) { + statistics.elapsed_cpu = Some(end - begin); + } + + (name, statistics) + }); + + callback(RenderStatistics(statistics.collect())); + return; + }; + DownloadBuffer::read_buffer(device.wgpu_device(), queue, &buffer.slice(..), move |res| { let buffer = match res { Ok(v) => v, @@ -316,9 +361,14 @@ impl std::fmt::Debug for MeasuredRenderPass<'_> { } } +/// Stores [`RenderStatistics`] shared between render app and main app. +/// +/// This mutex is locked twice per frame: in `PreUpdate`, during [`sync_render_statistics`], +/// and after rendering has finished and statistics have been downloaded from GPU. #[derive(Debug, Default, Clone, Resource)] pub struct RenderStatisticsMutex(pub Arc>>); +/// Copies fresh [`RenderStatistics`] from [`RenderStatisticsMutex`]. pub fn sync_render_statistics( mutex: Res, mut statistics: ResMut,