Skip to content

Commit

Permalink
Add DebugSettings for GPU capture (#415)
Browse files Browse the repository at this point in the history
Add a new `DebugSettings` resource controlling some debugging settings.

Add the ability via debugging settings to instruct a GPU debugger
attached to the application to capture one or more GPU frames, either
right now or each time a new effect is spawned.
  • Loading branch information
djeedai authored Jan 12, 2025
1 parent a49c923 commit 25a0e66
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 24 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added new `DebugSettings` resource, allowing to instruct a GPU debugger to start capturing API commands,
either right away or each time a new effect is spawned.

### Changed

- Changed the `ParticleEffect` component to require the `CompiledParticleEffect` and `SyncToRenderWorld` components.
Expand Down
2 changes: 1 addition & 1 deletion src/graph/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1470,7 +1470,7 @@ pub enum BuiltInOperator {
/// - Otherwise, the initial value is `true`.
///
/// At the end of the update pass, if the particle has both the
/// [`Attribute::AGE`] and [`Attribute::LIFETIME`], then the flag is
/// [`Attribute::AGE`] and [`Attribute::LIFETIME`], then the flag is
/// re-evaluated as:
///
/// ```wgsl
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ pub use graph::*;
pub use modifier::*;
pub use plugin::{EffectSystems, HanabiPlugin};
pub use properties::*;
pub use render::{LayoutFlags, ShaderCache};
pub use render::{DebugSettings, LayoutFlags, ShaderCache};
pub use spawn::{
tick_initializers, Cloner, CpuValue, EffectCloner, EffectInitializer, EffectInitializers,
EffectSpawner, Initializer, Random, Spawner,
Expand Down Expand Up @@ -1347,16 +1347,19 @@ impl CompiledParticleEffect {
.map(|effect_group_shader_source| {
let init = shader_cache.get_or_insert(
&asset.name,
"init",
&effect_group_shader_source.init,
shaders,
);
let update = shader_cache.get_or_insert(
&asset.name,
"update",
&effect_group_shader_source.update,
shaders,
);
let render = shader_cache.get_or_insert(
&asset.name,
"render",
&effect_group_shader_source.render,
shaders,
);
Expand Down
8 changes: 5 additions & 3 deletions src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ use crate::{
render::{
add_effects, batch_effects, extract_effect_events, extract_effects,
on_remove_cached_effect, on_remove_cached_properties, prepare_bind_groups, prepare_effects,
prepare_gpu_resources, queue_effects, DispatchIndirectPipeline, DrawEffects,
prepare_gpu_resources, queue_effects, DebugSettings, DispatchIndirectPipeline, DrawEffects,
EffectAssetEvents, EffectBindGroups, EffectCache, EffectsMeta, ExtractedEffects,
GpuDispatchIndirect, GpuParticleGroup, GpuRenderEffectMetadata, GpuRenderGroupIndirect,
GpuSpawnerParams, ParticlesInitPipeline, ParticlesRenderPipeline, ParticlesUpdatePipeline,
PropertyCache, ShaderCache, SimParams, StorageType as _, VfxSimulateDriverNode,
VfxSimulateNode,
PropertyCache, RenderDebugSettings, ShaderCache, SimParams, StorageType as _,
VfxSimulateDriverNode, VfxSimulateNode,
},
spawn::{self, Random},
tick_initializers,
Expand Down Expand Up @@ -184,6 +184,7 @@ impl Plugin for HanabiPlugin {
app.init_asset::<EffectAsset>()
.insert_resource(Random(spawn::new_rng()))
.init_resource::<ShaderCache>()
.init_resource::<DebugSettings>()
.init_resource::<Time<EffectSimulation>>()
.configure_sets(
PostUpdate,
Expand Down Expand Up @@ -274,6 +275,7 @@ impl Plugin for HanabiPlugin {
.insert_resource(effects_meta)
.insert_resource(effect_cache)
.insert_resource(property_cache)
.init_resource::<RenderDebugSettings>()
.init_resource::<EffectBindGroups>()
.init_resource::<DispatchIndirectPipeline>()
.init_resource::<ParticlesInitPipeline>()
Expand Down
22 changes: 17 additions & 5 deletions src/render/effect_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,17 @@ use crate::{
/// for a single effect.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EffectSlices {
/// Slices into the underlying BufferVec of the group.
/// Slices into the underlying [`BufferVec`]` of the group.
///
/// The length of this vector is the number of particle groups plus one.
/// The range of the first group is (slices[0]..slices[1]), the index of
/// the second group is (slices[1]..slices[2]), etc.
///
/// This is measured in items, not bytes.
pub slices: Vec<u32>,
/// The index of the buffer.
/// Index of the buffer in the [`EffectCache`].
pub buffer_index: u32,
/// Particle layout of the slice.
/// Particle layout of the effect.
pub particle_layout: ParticleLayout,
}

Expand Down Expand Up @@ -125,6 +125,15 @@ struct SimulateBindGroupKey {
size: u32,
}

impl SimulateBindGroupKey {
/// Invalid key, often used as placeholder.
pub const INVALID: Self = Self {
buffer: None,
offset: u32::MAX,
size: 0,
};
}

/// Storage for a single kind of effects, sharing the same buffer(s).
///
/// Currently only accepts a single unique item size (particle size), fixed at
Expand Down Expand Up @@ -211,12 +220,15 @@ impl EffectBuffer {
particle_layout.min_binding_size().get(),
);

// Calculate the clamped capacity of the group, in number of particles.
let capacity = capacity.max(Self::MIN_CAPACITY);
debug_assert!(
capacity > 0,
"Attempted to create a zero-sized effect buffer."
);

// Allocate the particle buffer itself, containing the attributes of each
// particle.
let particle_capacity_bytes: BufferAddress =
capacity as u64 * particle_layout.min_binding_size().get();
let particle_label = label
Expand Down Expand Up @@ -381,7 +393,7 @@ impl EffectBuffer {
free_slices: vec![],
asset,
simulate_bind_group: None,
simulate_bind_group_key: default(),
simulate_bind_group_key: SimulateBindGroupKey::INVALID,
}
}

Expand Down Expand Up @@ -498,7 +510,7 @@ impl EffectBuffer {
/// [`create_sim_bind_group()`]: self::EffectBuffer::create_sim_bind_group
fn invalidate_sim_bind_group(&mut self) {
self.simulate_bind_group = None;
self.simulate_bind_group_key = default();
self.simulate_bind_group_key = SimulateBindGroupKey::INVALID;
}

/// Return the cached bind group for the init and update passes.
Expand Down
129 changes: 118 additions & 11 deletions src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{
borrow::Cow,
num::{NonZero, NonZeroU32, NonZeroU64},
ops::Range,
time::Duration,
};
use std::{iter, marker::PhantomData};

Expand Down Expand Up @@ -1394,6 +1395,73 @@ pub(crate) fn extract_effect_events(
*images = image_events.read().copied().collect();
}

/// Debugging settings.
///
/// Settings used to debug Hanabi. These have no effect on the actual behavior
/// of Hanabi, but may affect its performance.
///
/// # Example
///
/// ```
/// # use bevy::prelude::*;
/// # use bevy_hanabi::*;
/// fn startup(mut debug_settings: ResMut<DebugSettings>) {
/// // Each time a new effect is spawned, capture 2 frames
/// debug_settings.start_capture_on_new_effect = true;
/// debug_settings.capture_frame_count = 2;
/// }
/// ```
#[derive(Debug, Default, Clone, Copy, Resource)]
pub struct DebugSettings {
/// Enable automatically starting a GPU debugger capture as soon as this
/// frame starts rendering (extract phase).
///
/// Enable this feature to automatically capture one or more GPU frames when
/// the [`extract_effects`] system runs next. This instructs any attached
/// GPU debugger to start a capture; this has no effect if no debugger
/// is attached.
///
/// If a capture is already on-going this has no effect; the on-going
/// capture needs to be terminated first. Note however that a capture can
/// stop and another start in the same frame.
///
/// This value is not reset automatically. If you set this to `true`, you
/// should set it back to `false` on next frame to avoid capturing forever.
pub start_capture_this_frame: bool,

/// Enable automatically starting a GPU debugger capture when one or more
/// effects are spawned.
///
/// Enable this feature to automatically capture one or more GPU frames when
/// a new effect is spawned (as detected by ECS change detection). This
/// instructs any attached GPU debugger to start a capture; this has no
/// effect if no debugger is attached.
pub start_capture_on_new_effect: bool,

/// Number of frames to capture with a GPU debugger.
///
/// By default this value is zero, and a GPU debugger capture runs for a
/// single frame. If a non-zero frame count is specified here, the capture
/// will instead stop once the specified number of frames has been recorded.
///
/// You should avoid setting this to a value too large, to prevent the
/// capture size from getting out of control. A typical value is 1 to 3
/// frames, or possibly more (up to 10) for exceptional contexts. Some GPU
/// debuggers or graphics APIs might further limit this value on their own,
/// so there's no guarantee the graphics API will honor this value.
pub capture_frame_count: u32,
}

#[derive(Debug, Default, Clone, Copy, Resource)]
pub(crate) struct RenderDebugSettings {
/// Is a GPU debugger capture on-going?
is_capturing: bool,
/// Start time of any on-going GPU debugger capture.
capture_start: Duration,
/// Number of frames captured so far for on-going GPU debugger capture.
captured_frames: u32,
}

/// System extracting data for rendering of all active [`ParticleEffect`]
/// components.
///
Expand All @@ -1404,6 +1472,11 @@ pub(crate) fn extract_effect_events(
///
/// This system runs in parallel of [`extract_effect_events`].
///
/// If any GPU debug capture is configured to start or stop in
/// [`DebugSettings`], they do so at the beginning of this system. This ensures
/// that all GPU commands produced by Hanabi are recorded (but may miss some
/// from Bevy itself, if another Bevy system runs before this one).
///
/// [`ParticleEffect`]: crate::ParticleEffect
pub(crate) fn extract_effects(
real_time: Extract<Res<Time<Real>>>,
Expand Down Expand Up @@ -1432,10 +1505,44 @@ pub(crate) fn extract_effects(
>,
mut sim_params: ResMut<SimParams>,
mut extracted_effects: ResMut<ExtractedEffects>,
render_device: Res<RenderDevice>,
debug_settings: Extract<Res<DebugSettings>>,
mut render_debug_settings: ResMut<RenderDebugSettings>,
effects_meta: Res<EffectsMeta>,
) {
trace!("extract_effects");

// Manage GPU debug capture
if render_debug_settings.is_capturing {
render_debug_settings.captured_frames += 1;

// Stop any pending capture if needed
if render_debug_settings.captured_frames >= debug_settings.capture_frame_count {
render_device.wgpu_device().stop_capture();
render_debug_settings.is_capturing = false;
warn!(
"Stopped GPU debug capture after {} frames, at t={}s.",
render_debug_settings.captured_frames,
real_time.elapsed().as_secs_f64()
);
}
}
if !render_debug_settings.is_capturing {
// If no pending capture, consider starting a new one
if debug_settings.start_capture_this_frame
|| (debug_settings.start_capture_on_new_effect && !query.p1().is_empty())
{
render_device.wgpu_device().start_capture();
render_debug_settings.is_capturing = true;
render_debug_settings.capture_start = real_time.elapsed();
render_debug_settings.captured_frames = 0;
warn!(
"Started GPU debug capture at t={}s.",
render_debug_settings.capture_start.as_secs_f64()
);
}
}

// Save simulation params into render world
sim_params.time = time.elapsed_secs_f64();
sim_params.delta_time = time.delta_secs();
Expand Down Expand Up @@ -1501,19 +1608,19 @@ pub(crate) fn extract_effects(
maybe_inherited_visibility,
maybe_view_visibility,
initializers,
effect,
compiled_effect,
maybe_properties,
transform,
) in query.p0().iter_mut()
{
// Check if shaders are configured
let effect_shaders = effect.get_configured_shaders();
let effect_shaders = compiled_effect.get_configured_shaders();
if effect_shaders.is_empty() {
continue;
}

// Check if hidden, unless always simulated
if effect.simulation_condition == SimulationCondition::WhenVisible
if compiled_effect.simulation_condition == SimulationCondition::WhenVisible
&& !maybe_inherited_visibility
.map(|cv| cv.get())
.unwrap_or(true)
Expand All @@ -1523,7 +1630,7 @@ pub(crate) fn extract_effects(
}

// Check if asset is available, otherwise silently ignore
let Some(asset) = effects.get(&effect.asset) else {
let Some(asset) = effects.get(&compiled_effect.asset) else {
trace!(
"EffectAsset not ready; skipping ParticleEffect instance on entity {:?}.",
main_entity
Expand All @@ -1532,7 +1639,7 @@ pub(crate) fn extract_effects(
};

#[cfg(feature = "2d")]
let z_sort_key_2d = effect.z_layer_2d;
let z_sort_key_2d = compiled_effect.z_layer_2d;

let property_layout = asset.property_layout();
let property_data = if let Some(properties) = maybe_properties {
Expand All @@ -1551,27 +1658,27 @@ pub(crate) fn extract_effects(
};

let texture_layout = asset.module().texture_layout();
let layout_flags = effect.layout_flags;
let mesh = match effect.mesh {
let layout_flags = compiled_effect.layout_flags;
let mesh = match compiled_effect.mesh {
None => effects_meta.default_mesh.clone(),
Some(ref mesh) => (*mesh).clone(),
};
let alpha_mode = effect.alpha_mode;
let alpha_mode = compiled_effect.alpha_mode;

trace!(
"Extracted instance of effect '{}' on entity {:?} (render entity {:?}): texture_layout_count={} texture_count={} layout_flags={:?}",
asset.name,
main_entity,
render_entity.id(),
texture_layout.layout.len(),
effect.textures.len(),
compiled_effect.textures.len(),
layout_flags,
);

extracted_effects.effects.push(ExtractedEffect {
render_entity: *render_entity,
main_entity: main_entity.into(),
handle: effect.asset.clone_weak(),
handle: compiled_effect.asset.clone_weak(),
particle_layout: asset.particle_layout().clone(),
property_layout,
property_data,
Expand All @@ -1580,7 +1687,7 @@ pub(crate) fn extract_effects(
layout_flags,
mesh,
texture_layout,
textures: effect.textures.clone(),
textures: compiled_effect.textures.clone(),
alpha_mode,
effect_shaders: effect_shaders.to_vec(),
#[cfg(feature = "2d")]
Expand Down
Loading

0 comments on commit 25a0e66

Please sign in to comment.