Skip to content

Commit

Permalink
Move shared WGSL code to imported module (#264)
Browse files Browse the repository at this point in the history
Move most shared WGSL code into an import module `vfx_common.wgsl`, and
document it. This ensures the code remains in sync between the various passes
(init/update/render) without needing some error-prone copy/paste, while making
the code easier to read too.

This change moves `naga` and `naga_oil` from being dev-only dependencies to
being proper crate dependencies. This is required to use `naga_oil`'s import
system, like `bevy_render`. This makes the change a breaking one.
  • Loading branch information
djeedai authored Dec 27, 2023
1 parent 28da7d1 commit 5a3685d
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 203 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- Moved most shared WGSL code into an import module `vfx_common.wgsl`. This requires using `naga_oil` for import resolution, which in turns means `naga` and `naga_oil` are now dependencies of `bevy_hanabi` itself.

## [0.9.0] 2023-12-26

### Added
Expand Down
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bevy_hanabi"
version = "0.9.0"
version = "0.10.0-dev"
authors = ["Jerome Humbert <[email protected]>"]
edition = "2021"
description = "Hanabi GPU particle system for the Bevy game engine"
Expand Down Expand Up @@ -38,6 +38,9 @@ ron = "0.8"
bitflags = "2.3"
typetag = "0.2"
thiserror = "1.0"
# Same versions as Bevy 0.12 (bevy_render)
naga = "0.13"
naga_oil = "0.10"

[dependencies.bevy]
version = "0.12"
Expand All @@ -50,8 +53,6 @@ all-features = true
[dev-dependencies]
# Same versions as Bevy 0.12 (bevy_render)
wgpu = "0.17.1"
naga = "0.13"
naga_oil = "0.10"

# For procedural texture generation in examples
noise = "0.8"
Expand Down
12 changes: 12 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1773,6 +1773,9 @@ else { return c1; }
"PARTICLE_SCREEN_SPACE_SIZE".into(),
ShaderDefValue::Bool(true),
);
if name == "Update" {
shader_defs.insert("RI_MAX_SPAWN_ATOMIC".into(), ShaderDefValue::Bool(true));
}
let mut composer = Composer::default();

// Import bevy_render::view for the render shader
Expand All @@ -1791,6 +1794,15 @@ else { return c1; }
assert!(res.is_ok());
}

// Import bevy_hanabi::vfx_common
{
let min_storage_buffer_offset_alignment = 256usize;
let common_shader =
HanabiPlugin::make_common_shader(min_storage_buffer_offset_alignment);
let res = composer.add_composable_module((&common_shader).into());
assert!(res.is_ok());
}

match composer.make_naga_module(NagaModuleDescriptor {
source: code,
file_path: "init.wgsl",
Expand Down
48 changes: 44 additions & 4 deletions src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ use crate::{
render::{
extract_effect_events, extract_effects, prepare_effects, prepare_resources, queue_effects,
DispatchIndirectPipeline, DrawEffects, EffectAssetEvents, EffectBindGroups, EffectSystems,
EffectsMeta, ExtractedEffects, ParticlesInitPipeline, ParticlesRenderPipeline,
ParticlesUpdatePipeline, ShaderCache, SimParams, VfxSimulateDriverNode, VfxSimulateNode,
EffectsMeta, ExtractedEffects, GpuSpawnerParams, ParticlesInitPipeline,
ParticlesRenderPipeline, ParticlesUpdatePipeline, ShaderCache, SimParams,
VfxSimulateDriverNode, VfxSimulateNode,
},
spawn::{self, Random},
tick_spawners, update_properties_from_asset, ParticleEffect, RemovedEffectsEvent, Spawner,
Expand All @@ -46,10 +47,39 @@ pub mod simulate_graph {
}
}

// {626E7AD3-4E54-487E-B796-9A90E34CC1EC}
const HANABI_COMMON_TEMPLATE_HANDLE: Handle<Shader> =
Handle::weak_from_u128(0x626E7AD34E54487EB7969A90E34CC1ECu128);

/// Plugin to add systems related to Hanabi.
#[derive(Debug, Clone, Copy)]
pub struct HanabiPlugin;

impl HanabiPlugin {
/// Create the `vfx_common.wgsl` shader with proper alignment.
///
/// This creates a new [`Shader`] from the `vfx_common.wgsl` code, by
/// applying the given alignment for storage buffers. This produces a shader
/// ready for the specific GPU device associated with that alignment.
pub(crate) fn make_common_shader(min_storage_buffer_offset_alignment: usize) -> Shader {
let spawner_padding_code =
GpuSpawnerParams::padding_code(min_storage_buffer_offset_alignment);
let common_code = include_str!("render/vfx_common.wgsl")
.replace("{{SPAWNER_PADDING}}", &spawner_padding_code);
Shader::from_wgsl(
common_code,
std::path::Path::new(file!())
.parent()
.unwrap()
.join(format!(
"render/vfx_common_{}.wgsl",
min_storage_buffer_offset_alignment
))
.to_string_lossy(),
)
}
}

impl Plugin for HanabiPlugin {
fn build(&self, app: &mut App) {
// Register asset
Expand Down Expand Up @@ -94,8 +124,7 @@ impl Plugin for HanabiPlugin {
let render_device = app
.sub_app(RenderApp)
.world
.get_resource::<RenderDevice>()
.unwrap()
.resource::<RenderDevice>()
.clone();

let adapter_name = app
Expand All @@ -113,6 +142,17 @@ impl Plugin for HanabiPlugin {
info!("Initializing Hanabi for GPU adapter {}", adapter_name);
}

// Insert the properly aligned `vfx_common.wgsl` shader into Assets<Shader>, so
// that the automated Bevy shader processing finds it as an import. This is used
// for init/update/render shaders (but not the indirect one).
{
let common_shader = HanabiPlugin::make_common_shader(
render_device.limits().min_storage_buffer_offset_alignment as usize,
);
let mut assets = app.world.resource_mut::<Assets<Shader>>();
assets.insert(HANABI_COMMON_TEMPLATE_HANDLE, common_shader);
}

let effects_meta = EffectsMeta::new(render_device);

// Register the custom render pipeline
Expand Down
92 changes: 73 additions & 19 deletions src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use bevy::{
utils::HashMap,
};
use bitflags::bitflags;
use naga_oil::compose::{Composer, NagaModuleDescriptor};
use rand::random;
use std::marker::PhantomData;
use std::{
Expand All @@ -46,8 +47,8 @@ use crate::{
next_multiple_of,
render::batch::{BatchInput, BatchState, Batcher, EffectBatch},
spawn::EffectSpawner,
CompiledParticleEffect, EffectProperties, EffectShader, ParticleLayout, PropertyLayout,
RemovedEffectsEvent, SimulationCondition, SimulationSpace,
CompiledParticleEffect, EffectProperties, EffectShader, HanabiPlugin, ParticleLayout,
PropertyLayout, RemovedEffectsEvent, SimulationCondition, SimulationSpace,
};

mod aligned_buffer_vec;
Expand Down Expand Up @@ -246,7 +247,7 @@ impl From<&Mat4> for GpuCompressedTransform {
/// together form the spawner parameter buffer.
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, Pod, Zeroable, ShaderType)]
struct GpuSpawnerParams {
pub(crate) struct GpuSpawnerParams {
/// Transform of the effect, as a Mat4 without the last row (which is always
/// (0,0,0,1) for an affine transform), stored transposed as a mat3x4 to
/// avoid padding in WGSL. This is either added to emitted particles at
Expand All @@ -269,6 +270,33 @@ struct GpuSpawnerParams {
force_field: [GpuForceFieldSource; ForceFieldSource::MAX_SOURCES],
}

impl GpuSpawnerParams {
/// Get the aligned size of this type based on the given alignment in bytes.
pub fn aligned_size(align_size: usize) -> usize {
next_multiple_of(GpuSpawnerParams::min_size().get() as usize, align_size)
}

/// Get the WGSL padding code to append to the GPU struct to align it.
pub fn padding_code(align_size: usize) -> String {
let aligned_size = GpuSpawnerParams::aligned_size(align_size);
trace!(
"Aligning spawner params to {} bytes as device limits requires. Aligned size: {} bytes.",
align_size,
aligned_size
);

// We need to pad the Spawner WGSL struct based on the device padding so that we
// can use it as an array element but also has a direct struct binding.
if GpuSpawnerParams::min_size().get() as usize != aligned_size {
let padding_size = aligned_size - GpuSpawnerParams::min_size().get() as usize;
assert!(padding_size % 4 == 0);
format!("padding: array<u32, {}>", padding_size / 4)
} else {
"".to_string()
}
}
}

// FIXME - min_storage_buffer_offset_alignment
#[repr(C)]
#[derive(Debug, Clone, Copy, Pod, Zeroable, ShaderType)]
Expand Down Expand Up @@ -327,8 +355,7 @@ impl FromWorld for DispatchIndirectPipeline {
// so needs the proper align. Because WGSL removed the @stride attribute, we pad
// the WGSL type manually, so need to enforce min_binding_size everywhere.
let item_align = render_device.limits().min_storage_buffer_offset_alignment as usize;
let spawner_aligned_size =
next_multiple_of(GpuSpawnerParams::min_size().get() as usize, item_align);
let spawner_aligned_size = GpuSpawnerParams::aligned_size(item_align);
trace!(
"Aligning spawner params to {} bytes as device limits requires. Size: {} bytes.",
item_align,
Expand Down Expand Up @@ -403,22 +430,49 @@ impl FromWorld for DispatchIndirectPipeline {

// We need to pad the Spawner WGSL struct based on the device padding so that we
// can use it as an array element but also has a direct struct binding.
let spawner_padding_code = if GpuSpawnerParams::min_size().get() as usize
!= spawner_aligned_size
{
let padding_size = spawner_aligned_size - GpuSpawnerParams::min_size().get() as usize;
assert!(padding_size % 4 == 0);
format!("padding: array<u32, {}>", padding_size / 4)
} else {
"".to_string()
let spawner_padding_code = GpuSpawnerParams::padding_code(item_align);
let indirect_code =
include_str!("vfx_indirect.wgsl").replace("{{SPAWNER_PADDING}}", &spawner_padding_code);

// Resolve imports. Because we don't insert this shader into Bevy' pipeline
// cache, we don't get that part "for free", so we have to do it manually here.
let indirect_naga_module = {
let mut composer = Composer::default();

// Import bevy_hanabi::vfx_common
{
let common_shader = HanabiPlugin::make_common_shader(item_align);
let mut desc: naga_oil::compose::ComposableModuleDescriptor<'_> =
(&common_shader).into();
desc.shader_defs.insert(
"SPAWNER_PADDING".to_string(),
naga_oil::compose::ShaderDefValue::Bool(true),
);
let res = composer.add_composable_module(desc);
assert!(res.is_ok());
}

let shader_defs = default();

match composer.make_naga_module(NagaModuleDescriptor {
source: &indirect_code,
file_path: "vfx_indirect.wgsl",
shader_defs,
..Default::default()
}) {
Ok(naga_module) => ShaderSource::Naga(Cow::Owned(naga_module)),
Err(compose_error) => panic!(
"Failed to compose vfx_indirect.wgsl, naga_oil returned: {}",
compose_error.emit_to_string(&composer)
),
}
};
let indirect_code = include_str!("vfx_indirect.wgsl")
.to_string()
.replace("{{SPAWNER_PADDING}}", &spawner_padding_code);

debug!("Create indirect dispatch shader:\n{}", indirect_code);

let shader_module = render_device.create_shader_module(ShaderModuleDescriptor {
label: Some("hanabi:vfx_indirect_shader"),
source: ShaderSource::Wgsl(Cow::Owned(indirect_code)),
source: indirect_naga_module,
});

let pipeline = render_device.create_compute_pipeline(&RawComputePipelineDescriptor {
Expand Down Expand Up @@ -772,7 +826,7 @@ impl SpecializedComputePipeline for ParticlesUpdatePipeline {
self.render_indirect_layout.clone(),
],
shader: key.shader,
shader_defs: vec![],
shader_defs: vec!["RI_MAX_SPAWN_ATOMIC".into()],
entry_point: "main".into(),
push_constant_ranges: Vec::new(),
}
Expand Down Expand Up @@ -2049,7 +2103,7 @@ pub(crate) struct BufferBindGroups {
/// Bind group for the render graphic shader.
///
/// ```wgsl
/// @binding(0) var<storage, read> particle_buffer : ParticlesBuffer;
/// @binding(0) var<storage, read> particle_buffer : ParticleBuffer;
/// @binding(1) var<storage, read> indirect_buffer : IndirectBuffer;
/// @binding(2) var<storage, read> dispatch_indirect : DispatchIndirect;
/// ```
Expand Down
13 changes: 10 additions & 3 deletions src/render/shader_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::hash::{Hash, Hasher};
use bevy::{
asset::{Assets, Handle},
ecs::{change_detection::ResMut, system::Resource},
log::debug,
log::{debug, trace},
render::render_resource::Shader,
utils::HashMap,
};
Expand Down Expand Up @@ -40,10 +40,17 @@ impl ShaderCache {
let mut hasher = bevy::utils::AHasher::default();
source.hash(&mut hasher);
let hash = hasher.finish();
let handle = shaders.add(Shader::from_wgsl(
let shader = Shader::from_wgsl(
source.to_string(),
format!("hanabi/{}_{}.wgsl", filename, hash),
));
);
trace!(
"Shader path={} import_path={:?} imports={:?}",
shader.path,
shader.import_path,
shader.imports
);
let handle = shaders.add(shader);
debug!("Inserted new configured shader: {:?}\n{}", handle, source);
self.cache.insert(source.to_string(), handle.clone());
handle
Expand Down
Loading

0 comments on commit 5a3685d

Please sign in to comment.