
# Objective Fixes a part of #14274. Bevy has an incredibly inconsistent naming convention for its system sets, both internally and across the ecosystem. <img alt="System sets in Bevy" src="https://github.com/user-attachments/assets/d16e2027-793f-4ba4-9cc9-e780b14a5a1b" width="450" /> *Names of public system set types in Bevy* Most Bevy types use a naming of `FooSystem` or just `Foo`, but there are also a few `FooSystems` and `FooSet` types. In ecosystem crates on the other hand, `FooSet` is perhaps the most commonly used name in general. Conventions being so wildly inconsistent can make it harder for users to pick names for their own types, to search for system sets on docs.rs, or to even discern which types *are* system sets. To reign in the inconsistency a bit and help unify the ecosystem, it would be good to establish a common recommended naming convention for system sets in Bevy itself, similar to how plugins are commonly suffixed with `Plugin` (ex: `TimePlugin`). By adopting a consistent naming convention in first-party Bevy, we can softly nudge ecosystem crates to follow suit (for types where it makes sense to do so). Choosing a naming convention is also relevant now, as the [`bevy_cli` recently adopted lints](https://github.com/TheBevyFlock/bevy_cli/pull/345) to enforce naming for plugins and system sets, and the recommended naming used for system sets is still a bit open. ## Which Name To Use? Now the contentious part: what naming convention should we actually adopt? This was discussed on the Bevy Discord at the end of last year, starting [here](<https://discord.com/channels/691052431525675048/692572690833473578/1310659954683936789>). `FooSet` and `FooSystems` were the clear favorites, with `FooSet` very narrowly winning an unofficial poll. However, it seems to me like the consensus was broadly moving towards `FooSystems` at the end and after the poll, with Cart ([source](https://discord.com/channels/691052431525675048/692572690833473578/1311140204974706708)) and later Alice ([source](https://discord.com/channels/691052431525675048/692572690833473578/1311092530732859533)) and also me being in favor of it. Let's do a quick pros and cons list! Of course these are just what I thought of, so take it with a grain of salt. `FooSet`: - Pro: Nice and short! - Pro: Used by many ecosystem crates. - Pro: The `Set` suffix comes directly from the trait name `SystemSet`. - Pro: Pairs nicely with existing APIs like `in_set` and `configure_sets`. - Con: `Set` by itself doesn't actually indicate that it's related to systems *at all*, apart from the implemented trait. A set of what? - Con: Is `FooSet` a set of `Foo`s or a system set related to `Foo`? Ex: `ContactSet`, `MeshSet`, `EnemySet`... `FooSystems`: - Pro: Very clearly indicates that the type represents a collection of systems. The actual core concept, system(s), is in the name. - Pro: Parallels nicely with `FooPlugins` for plugin groups. - Pro: Low risk of conflicts with other names or misunderstandings about what the type is. - Pro: In most cases, reads *very* nicely and clearly. Ex: `PhysicsSystems` and `AnimationSystems` as opposed to `PhysicsSet` and `AnimationSet`. - Pro: Easy to search for on docs.rs. - Con: Usually results in longer names. - Con: Not yet as widely used. Really the big problem with `FooSet` is that it doesn't actually describe what it is. It describes what *kind of thing* it is (a set of something), but not *what it is a set of*, unless you know the type or check its docs or implemented traits. `FooSystems` on the other hand is much more self-descriptive in this regard, at the cost of being a bit longer to type. Ultimately, in some ways it comes down to preference and how you think of system sets. Personally, I was originally in favor of `FooSet`, but have been increasingly on the side of `FooSystems`, especially after seeing what the new names would actually look like in Avian and now Bevy. I prefer it because it usually reads better, is much more clearly related to groups of systems than `FooSet`, and overall *feels* more correct and natural to me in the long term. For these reasons, and because Alice and Cart also seemed to share a preference for it when it was previously being discussed, I propose that we adopt a `FooSystems` naming convention where applicable. ## Solution Rename Bevy's system set types to use a consistent `FooSet` naming where applicable. - `AccessibilitySystem` → `AccessibilitySystems` - `GizmoRenderSystem` → `GizmoRenderSystems` - `PickSet` → `PickingSystems` - `RunFixedMainLoopSystem` → `RunFixedMainLoopSystems` - `TransformSystem` → `TransformSystems` - `RemoteSet` → `RemoteSystems` - `RenderSet` → `RenderSystems` - `SpriteSystem` → `SpriteSystems` - `StateTransitionSteps` → `StateTransitionSystems` - `RenderUiSystem` → `RenderUiSystems` - `UiSystem` → `UiSystems` - `Animation` → `AnimationSystems` - `AssetEvents` → `AssetEventSystems` - `TrackAssets` → `AssetTrackingSystems` - `UpdateGizmoMeshes` → `GizmoMeshSystems` - `InputSystem` → `InputSystems` - `InputFocusSet` → `InputFocusSystems` - `ExtractMaterialsSet` → `MaterialExtractionSystems` - `ExtractMeshesSet` → `MeshExtractionSystems` - `RumbleSystem` → `RumbleSystems` - `CameraUpdateSystem` → `CameraUpdateSystems` - `ExtractAssetsSet` → `AssetExtractionSystems` - `Update2dText` → `Text2dUpdateSystems` - `TimeSystem` → `TimeSystems` - `AudioPlaySet` → `AudioPlaybackSystems` - `SendEvents` → `EventSenderSystems` - `EventUpdates` → `EventUpdateSystems` A lot of the names got slightly longer, but they are also a lot more consistent, and in my opinion the majority of them read much better. For a few of the names I took the liberty of rewording things a bit; definitely open to any further naming improvements. There are still also cases where the `FooSystems` naming doesn't really make sense, and those I left alone. This primarily includes system sets like `Interned<dyn SystemSet>`, `EnterSchedules<S>`, `ExitSchedules<S>`, or `TransitionSchedules<S>`, where the type has some special purpose and semantics. ## Todo - [x] Should I keep all the old names as deprecated type aliases? I can do this, but to avoid wasting work I'd prefer to first reach consensus on whether these renames are even desired. - [x] Migration guide - [x] Release notes
700 lines
25 KiB
Rust
700 lines
25 KiB
Rust
//! Demonstrates occlusion culling.
|
|
//!
|
|
//! This demo rotates many small cubes around a rotating large cube at the
|
|
//! origin. At all times, the large cube will be occluding several of the small
|
|
//! cubes. The demo displays the number of cubes that were actually rendered, so
|
|
//! the effects of occlusion culling can be seen.
|
|
|
|
use std::{
|
|
any::TypeId,
|
|
f32::consts::PI,
|
|
fmt::Write as _,
|
|
result::Result,
|
|
sync::{Arc, Mutex},
|
|
};
|
|
|
|
use bevy::{
|
|
color::palettes::css::{SILVER, WHITE},
|
|
core_pipeline::{
|
|
core_3d::{
|
|
graph::{Core3d, Node3d},
|
|
Opaque3d,
|
|
},
|
|
prepass::DepthPrepass,
|
|
},
|
|
pbr::PbrPlugin,
|
|
prelude::*,
|
|
render::{
|
|
batching::gpu_preprocessing::{
|
|
GpuPreprocessingMode, GpuPreprocessingSupport, IndirectParametersBuffers,
|
|
IndirectParametersIndexed,
|
|
},
|
|
experimental::occlusion_culling::OcclusionCulling,
|
|
render_graph::{self, NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel},
|
|
render_resource::{Buffer, BufferDescriptor, BufferUsages, MapMode},
|
|
renderer::{RenderContext, RenderDevice},
|
|
settings::WgpuFeatures,
|
|
Render, RenderApp, RenderDebugFlags, RenderPlugin, RenderSystems,
|
|
},
|
|
};
|
|
use bytemuck::Pod;
|
|
|
|
/// The radius of the spinning sphere of cubes.
|
|
const OUTER_RADIUS: f32 = 3.0;
|
|
|
|
/// The density of cubes in the other sphere.
|
|
const OUTER_SUBDIVISION_COUNT: u32 = 5;
|
|
|
|
/// The speed at which the outer sphere and large cube rotate in radians per
|
|
/// frame.
|
|
const ROTATION_SPEED: f32 = 0.01;
|
|
|
|
/// The length of each side of the small cubes, in meters.
|
|
const SMALL_CUBE_SIZE: f32 = 0.1;
|
|
|
|
/// The length of each side of the large cube, in meters.
|
|
const LARGE_CUBE_SIZE: f32 = 2.0;
|
|
|
|
/// A marker component for the immediate parent of the large sphere of cubes.
|
|
#[derive(Default, Component)]
|
|
struct SphereParent;
|
|
|
|
/// A marker component for the large spinning cube at the origin.
|
|
#[derive(Default, Component)]
|
|
struct LargeCube;
|
|
|
|
/// A plugin for the render app that reads the number of culled meshes from the
|
|
/// GPU back to the CPU.
|
|
struct ReadbackIndirectParametersPlugin;
|
|
|
|
/// The node that we insert into the render graph in order to read the number of
|
|
/// culled meshes from the GPU back to the CPU.
|
|
#[derive(Default)]
|
|
struct ReadbackIndirectParametersNode;
|
|
|
|
/// The [`RenderLabel`] that we use to identify the
|
|
/// [`ReadbackIndirectParametersNode`].
|
|
#[derive(Clone, PartialEq, Eq, Hash, Debug, RenderLabel)]
|
|
struct ReadbackIndirectParameters;
|
|
|
|
/// The intermediate staging buffers that we use to read back the indirect
|
|
/// parameters from the GPU to the CPU.
|
|
///
|
|
/// We read back the GPU indirect parameters so that we can determine the number
|
|
/// of meshes that were culled.
|
|
///
|
|
/// `wgpu` doesn't allow us to read indirect buffers back from the GPU to the
|
|
/// CPU directly. Instead, we have to copy them to a temporary staging buffer
|
|
/// first, and then read *those* buffers back from the GPU to the CPU. This
|
|
/// resource holds those temporary buffers.
|
|
#[derive(Resource, Default)]
|
|
struct IndirectParametersStagingBuffers {
|
|
/// The buffer that stores the indirect draw commands.
|
|
///
|
|
/// See [`IndirectParametersIndexed`] for more information about the memory
|
|
/// layout of this buffer.
|
|
data: Option<Buffer>,
|
|
/// The buffer that stores the *number* of indirect draw commands.
|
|
///
|
|
/// We only care about the first `u32` in this buffer.
|
|
batch_sets: Option<Buffer>,
|
|
}
|
|
|
|
/// A resource, shared between the main world and the render world, that saves a
|
|
/// CPU-side copy of the GPU buffer that stores the indirect draw parameters.
|
|
///
|
|
/// This is needed so that we can display the number of meshes that were culled.
|
|
/// It's reference counted, and protected by a lock, because we don't precisely
|
|
/// know when the GPU will be ready to present the CPU with the buffer copy.
|
|
/// Even though the rendering runs at least a frame ahead of the main app logic,
|
|
/// we don't require more precise synchronization than the lock because we don't
|
|
/// really care how up-to-date the counter of culled meshes is. If it's off by a
|
|
/// few frames, that's no big deal.
|
|
#[derive(Clone, Resource, Deref, DerefMut)]
|
|
struct SavedIndirectParameters(Arc<Mutex<SavedIndirectParametersData>>);
|
|
|
|
/// A CPU-side copy of the GPU buffer that stores the indirect draw parameters.
|
|
///
|
|
/// This is needed so that we can display the number of meshes that were culled.
|
|
struct SavedIndirectParametersData {
|
|
/// The CPU-side copy of the GPU buffer that stores the indirect draw
|
|
/// parameters.
|
|
data: Vec<IndirectParametersIndexed>,
|
|
/// The CPU-side copy of the GPU buffer that stores the *number* of indirect
|
|
/// draw parameters that we have.
|
|
///
|
|
/// All we care about is the number of indirect draw parameters for a single
|
|
/// view, so this is only one word in size.
|
|
count: u32,
|
|
/// True if occlusion culling is supported at all; false if it's not.
|
|
occlusion_culling_supported: bool,
|
|
/// True if we support inspecting the number of meshes that were culled on
|
|
/// this platform; false if we don't.
|
|
///
|
|
/// If `multi_draw_indirect_count` isn't supported, then we would have to
|
|
/// employ a more complicated approach in order to determine the number of
|
|
/// meshes that are occluded, and that would be out of scope for this
|
|
/// example.
|
|
occlusion_culling_introspection_supported: bool,
|
|
}
|
|
|
|
impl FromWorld for SavedIndirectParameters {
|
|
fn from_world(world: &mut World) -> SavedIndirectParameters {
|
|
let render_device = world.resource::<RenderDevice>();
|
|
SavedIndirectParameters(Arc::new(Mutex::new(SavedIndirectParametersData {
|
|
data: vec![],
|
|
count: 0,
|
|
// This gets set to false in `readback_indirect_buffers` if we don't
|
|
// support GPU preprocessing.
|
|
occlusion_culling_supported: true,
|
|
// In order to determine how many meshes were culled, we look at the
|
|
// indirect count buffer that Bevy only populates if the platform
|
|
// supports `multi_draw_indirect_count`. So, if we don't have that
|
|
// feature, then we don't bother to display how many meshes were
|
|
// culled.
|
|
occlusion_culling_introspection_supported: render_device
|
|
.features()
|
|
.contains(WgpuFeatures::MULTI_DRAW_INDIRECT_COUNT),
|
|
})))
|
|
}
|
|
}
|
|
|
|
/// The demo's current settings.
|
|
#[derive(Resource)]
|
|
struct AppStatus {
|
|
/// Whether occlusion culling is presently enabled.
|
|
///
|
|
/// By default, this is set to true.
|
|
occlusion_culling: bool,
|
|
}
|
|
|
|
impl Default for AppStatus {
|
|
fn default() -> Self {
|
|
AppStatus {
|
|
occlusion_culling: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let render_debug_flags = RenderDebugFlags::ALLOW_COPIES_FROM_INDIRECT_PARAMETERS;
|
|
|
|
App::new()
|
|
.add_plugins(
|
|
DefaultPlugins
|
|
.set(WindowPlugin {
|
|
primary_window: Some(Window {
|
|
title: "Bevy Occlusion Culling Example".into(),
|
|
..default()
|
|
}),
|
|
..default()
|
|
})
|
|
.set(RenderPlugin {
|
|
debug_flags: render_debug_flags,
|
|
..default()
|
|
})
|
|
.set(PbrPlugin {
|
|
debug_flags: render_debug_flags,
|
|
..default()
|
|
}),
|
|
)
|
|
.add_plugins(ReadbackIndirectParametersPlugin)
|
|
.init_resource::<AppStatus>()
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, spin_small_cubes)
|
|
.add_systems(Update, spin_large_cube)
|
|
.add_systems(Update, update_status_text)
|
|
.add_systems(Update, toggle_occlusion_culling_on_request)
|
|
.run();
|
|
}
|
|
|
|
impl Plugin for ReadbackIndirectParametersPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
// Fetch the render app.
|
|
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
|
|
render_app
|
|
.init_resource::<IndirectParametersStagingBuffers>()
|
|
.add_systems(ExtractSchedule, readback_indirect_parameters)
|
|
.add_systems(
|
|
Render,
|
|
create_indirect_parameters_staging_buffers
|
|
.in_set(RenderSystems::PrepareResourcesFlush),
|
|
)
|
|
// Add the node that allows us to read the indirect parameters back
|
|
// from the GPU to the CPU, which allows us to determine how many
|
|
// meshes were culled.
|
|
.add_render_graph_node::<ReadbackIndirectParametersNode>(
|
|
Core3d,
|
|
ReadbackIndirectParameters,
|
|
)
|
|
// We read back the indirect parameters any time after
|
|
// `EndMainPass`. Readback doesn't particularly need to execute
|
|
// before `EndMainPassPostProcessing`, but we specify that anyway
|
|
// because we want to make the indirect parameters run before
|
|
// *something* in the graph, and `EndMainPassPostProcessing` is a
|
|
// good a node as any other.
|
|
.add_render_graph_edges(
|
|
Core3d,
|
|
(
|
|
Node3d::EndMainPass,
|
|
ReadbackIndirectParameters,
|
|
Node3d::EndMainPassPostProcessing,
|
|
),
|
|
);
|
|
}
|
|
|
|
fn finish(&self, app: &mut App) {
|
|
// Create the `SavedIndirectParameters` resource that we're going to use
|
|
// to communicate between the thread that the GPU-to-CPU readback
|
|
// callback runs on and the main application threads. This resource is
|
|
// atomically reference counted. We store one reference to the
|
|
// `SavedIndirectParameters` in the main app and another reference in
|
|
// the render app.
|
|
let saved_indirect_parameters = SavedIndirectParameters::from_world(app.world_mut());
|
|
app.insert_resource(saved_indirect_parameters.clone());
|
|
|
|
// Fetch the render app.
|
|
let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
|
|
return;
|
|
};
|
|
|
|
render_app
|
|
// Insert another reference to the `SavedIndirectParameters`.
|
|
.insert_resource(saved_indirect_parameters);
|
|
}
|
|
}
|
|
|
|
/// Spawns all the objects in the scene.
|
|
fn setup(
|
|
mut commands: Commands,
|
|
asset_server: Res<AssetServer>,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) {
|
|
spawn_small_cubes(&mut commands, &mut meshes, &mut materials);
|
|
spawn_large_cube(&mut commands, &asset_server, &mut meshes, &mut materials);
|
|
spawn_light(&mut commands);
|
|
spawn_camera(&mut commands);
|
|
spawn_help_text(&mut commands);
|
|
}
|
|
|
|
/// Spawns the rotating sphere of small cubes.
|
|
fn spawn_small_cubes(
|
|
commands: &mut Commands,
|
|
meshes: &mut Assets<Mesh>,
|
|
materials: &mut Assets<StandardMaterial>,
|
|
) {
|
|
// Add the cube mesh.
|
|
let small_cube = meshes.add(Cuboid::new(
|
|
SMALL_CUBE_SIZE,
|
|
SMALL_CUBE_SIZE,
|
|
SMALL_CUBE_SIZE,
|
|
));
|
|
|
|
// Add the cube material.
|
|
let small_cube_material = materials.add(StandardMaterial {
|
|
base_color: SILVER.into(),
|
|
..default()
|
|
});
|
|
|
|
// Create the entity that the small cubes will be parented to. This is the
|
|
// entity that we rotate.
|
|
let sphere_parent = commands
|
|
.spawn(Transform::from_translation(Vec3::ZERO))
|
|
.insert(Visibility::default())
|
|
.insert(SphereParent)
|
|
.id();
|
|
|
|
// Now we have to figure out where to place the cubes. To do that, we create
|
|
// a sphere mesh, but we don't add it to the scene. Instead, we inspect the
|
|
// sphere mesh to find the positions of its vertices, and spawn a small cube
|
|
// at each one. That way, we end up with a bunch of cubes arranged in a
|
|
// spherical shape.
|
|
|
|
// Create the sphere mesh, and extract the positions of its vertices.
|
|
let sphere = Sphere::new(OUTER_RADIUS)
|
|
.mesh()
|
|
.ico(OUTER_SUBDIVISION_COUNT)
|
|
.unwrap();
|
|
let sphere_positions = sphere.attribute(Mesh::ATTRIBUTE_POSITION).unwrap();
|
|
|
|
// At each vertex, create a small cube.
|
|
for sphere_position in sphere_positions.as_float3().unwrap() {
|
|
let sphere_position = Vec3::from_slice(sphere_position);
|
|
let small_cube = commands
|
|
.spawn(Mesh3d(small_cube.clone()))
|
|
.insert(MeshMaterial3d(small_cube_material.clone()))
|
|
.insert(Transform::from_translation(sphere_position))
|
|
.id();
|
|
commands.entity(sphere_parent).add_child(small_cube);
|
|
}
|
|
}
|
|
|
|
/// Spawns the large cube at the center of the screen.
|
|
///
|
|
/// This cube rotates chaotically and occludes small cubes behind it.
|
|
fn spawn_large_cube(
|
|
commands: &mut Commands,
|
|
asset_server: &AssetServer,
|
|
meshes: &mut Assets<Mesh>,
|
|
materials: &mut Assets<StandardMaterial>,
|
|
) {
|
|
commands
|
|
.spawn(Mesh3d(meshes.add(Cuboid::new(
|
|
LARGE_CUBE_SIZE,
|
|
LARGE_CUBE_SIZE,
|
|
LARGE_CUBE_SIZE,
|
|
))))
|
|
.insert(MeshMaterial3d(materials.add(StandardMaterial {
|
|
base_color: WHITE.into(),
|
|
base_color_texture: Some(asset_server.load("branding/icon.png")),
|
|
..default()
|
|
})))
|
|
.insert(Transform::IDENTITY)
|
|
.insert(LargeCube);
|
|
}
|
|
|
|
// Spins the outer sphere a bit every frame.
|
|
//
|
|
// This ensures that the set of cubes that are hidden and shown varies over
|
|
// time.
|
|
fn spin_small_cubes(mut sphere_parents: Query<&mut Transform, With<SphereParent>>) {
|
|
for mut sphere_parent_transform in &mut sphere_parents {
|
|
sphere_parent_transform.rotate_y(ROTATION_SPEED);
|
|
}
|
|
}
|
|
|
|
/// Spins the large cube a bit every frame.
|
|
///
|
|
/// The chaotic rotation adds a bit of randomness to the scene to better
|
|
/// demonstrate the dynamicity of the occlusion culling.
|
|
fn spin_large_cube(mut large_cubes: Query<&mut Transform, With<LargeCube>>) {
|
|
for mut transform in &mut large_cubes {
|
|
transform.rotate(Quat::from_euler(
|
|
EulerRot::XYZ,
|
|
0.13 * ROTATION_SPEED,
|
|
0.29 * ROTATION_SPEED,
|
|
0.35 * ROTATION_SPEED,
|
|
));
|
|
}
|
|
}
|
|
|
|
/// Spawns a directional light to illuminate the scene.
|
|
fn spawn_light(commands: &mut Commands) {
|
|
commands
|
|
.spawn(DirectionalLight::default())
|
|
.insert(Transform::from_rotation(Quat::from_euler(
|
|
EulerRot::ZYX,
|
|
0.0,
|
|
PI * -0.15,
|
|
PI * -0.15,
|
|
)));
|
|
}
|
|
|
|
/// Spawns a camera that includes the depth prepass and occlusion culling.
|
|
fn spawn_camera(commands: &mut Commands) {
|
|
commands
|
|
.spawn(Camera3d::default())
|
|
.insert(Transform::from_xyz(0.0, 0.0, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
|
|
.insert(DepthPrepass)
|
|
.insert(OcclusionCulling);
|
|
}
|
|
|
|
/// Spawns the help text at the upper left of the screen.
|
|
fn spawn_help_text(commands: &mut Commands) {
|
|
commands.spawn((
|
|
Text::new(""),
|
|
Node {
|
|
position_type: PositionType::Absolute,
|
|
top: Val::Px(12.0),
|
|
left: Val::Px(12.0),
|
|
..default()
|
|
},
|
|
));
|
|
}
|
|
|
|
impl render_graph::Node for ReadbackIndirectParametersNode {
|
|
fn run<'w>(
|
|
&self,
|
|
_: &mut RenderGraphContext,
|
|
render_context: &mut RenderContext<'w>,
|
|
world: &'w World,
|
|
) -> Result<(), NodeRunError> {
|
|
// Extract the buffers that hold the GPU indirect draw parameters from
|
|
// the world resources. We're going to read those buffers to determine
|
|
// how many meshes were actually drawn.
|
|
let (Some(indirect_parameters_buffers), Some(indirect_parameters_mapping_buffers)) = (
|
|
world.get_resource::<IndirectParametersBuffers>(),
|
|
world.get_resource::<IndirectParametersStagingBuffers>(),
|
|
) else {
|
|
return Ok(());
|
|
};
|
|
|
|
// Get the indirect parameters buffers corresponding to the opaque 3D
|
|
// phase, since all our meshes are in that phase.
|
|
let Some(phase_indirect_parameters_buffers) =
|
|
indirect_parameters_buffers.get(&TypeId::of::<Opaque3d>())
|
|
else {
|
|
return Ok(());
|
|
};
|
|
|
|
// Grab both the buffers we're copying from and the staging buffers
|
|
// we're copying to. Remember that we can't map the indirect parameters
|
|
// buffers directly, so we have to copy their contents to a staging
|
|
// buffer.
|
|
let (
|
|
Some(indexed_data_buffer),
|
|
Some(indexed_batch_sets_buffer),
|
|
Some(indirect_parameters_staging_data_buffer),
|
|
Some(indirect_parameters_staging_batch_sets_buffer),
|
|
) = (
|
|
phase_indirect_parameters_buffers.indexed.data_buffer(),
|
|
phase_indirect_parameters_buffers
|
|
.indexed
|
|
.batch_sets_buffer(),
|
|
indirect_parameters_mapping_buffers.data.as_ref(),
|
|
indirect_parameters_mapping_buffers.batch_sets.as_ref(),
|
|
)
|
|
else {
|
|
return Ok(());
|
|
};
|
|
|
|
// Copy from the indirect parameters buffers to the staging buffers.
|
|
render_context.command_encoder().copy_buffer_to_buffer(
|
|
indexed_data_buffer,
|
|
0,
|
|
indirect_parameters_staging_data_buffer,
|
|
0,
|
|
indexed_data_buffer.size(),
|
|
);
|
|
render_context.command_encoder().copy_buffer_to_buffer(
|
|
indexed_batch_sets_buffer,
|
|
0,
|
|
indirect_parameters_staging_batch_sets_buffer,
|
|
0,
|
|
indexed_batch_sets_buffer.size(),
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Creates the staging buffers that we use to read back the indirect parameters
|
|
/// from the GPU to the CPU.
|
|
///
|
|
/// We read the indirect parameters from the GPU to the CPU in order to display
|
|
/// the number of meshes that were culled each frame.
|
|
///
|
|
/// We need these staging buffers because `wgpu` doesn't allow us to read the
|
|
/// contents of the indirect parameters buffers directly. We must first copy
|
|
/// them from the GPU to a staging buffer, and then read the staging buffer.
|
|
fn create_indirect_parameters_staging_buffers(
|
|
mut indirect_parameters_staging_buffers: ResMut<IndirectParametersStagingBuffers>,
|
|
indirect_parameters_buffers: Res<IndirectParametersBuffers>,
|
|
render_device: Res<RenderDevice>,
|
|
) {
|
|
let Some(phase_indirect_parameters_buffers) =
|
|
indirect_parameters_buffers.get(&TypeId::of::<Opaque3d>())
|
|
else {
|
|
return;
|
|
};
|
|
|
|
// Fetch the indirect parameters buffers that we're going to copy from.
|
|
let (Some(indexed_data_buffer), Some(indexed_batch_set_buffer)) = (
|
|
phase_indirect_parameters_buffers.indexed.data_buffer(),
|
|
phase_indirect_parameters_buffers
|
|
.indexed
|
|
.batch_sets_buffer(),
|
|
) else {
|
|
return;
|
|
};
|
|
|
|
// Build the staging buffers. Make sure they have the same sizes as the
|
|
// buffers we're copying from.
|
|
indirect_parameters_staging_buffers.data =
|
|
Some(render_device.create_buffer(&BufferDescriptor {
|
|
label: Some("indexed data staging buffer"),
|
|
size: indexed_data_buffer.size(),
|
|
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
}));
|
|
indirect_parameters_staging_buffers.batch_sets =
|
|
Some(render_device.create_buffer(&BufferDescriptor {
|
|
label: Some("indexed batch set staging buffer"),
|
|
size: indexed_batch_set_buffer.size(),
|
|
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
}));
|
|
}
|
|
|
|
/// Updates the app status text at the top of the screen.
|
|
fn update_status_text(
|
|
saved_indirect_parameters: Res<SavedIndirectParameters>,
|
|
mut texts: Query<&mut Text>,
|
|
meshes: Query<Entity, With<Mesh3d>>,
|
|
app_status: Res<AppStatus>,
|
|
) {
|
|
// How many meshes are in the scene?
|
|
let total_mesh_count = meshes.iter().count();
|
|
|
|
// Sample the rendered object count. Note that we don't synchronize beyond
|
|
// locking the data and therefore this will value will generally at least
|
|
// one frame behind. This is fine; this app is just a demonstration after
|
|
// all.
|
|
let (
|
|
rendered_object_count,
|
|
occlusion_culling_supported,
|
|
occlusion_culling_introspection_supported,
|
|
): (u32, bool, bool) = {
|
|
let saved_indirect_parameters = saved_indirect_parameters.lock().unwrap();
|
|
(
|
|
saved_indirect_parameters
|
|
.data
|
|
.iter()
|
|
.take(saved_indirect_parameters.count as usize)
|
|
.map(|indirect_parameters| indirect_parameters.instance_count)
|
|
.sum(),
|
|
saved_indirect_parameters.occlusion_culling_supported,
|
|
saved_indirect_parameters.occlusion_culling_introspection_supported,
|
|
)
|
|
};
|
|
|
|
// Change the text.
|
|
for mut text in &mut texts {
|
|
text.0 = String::new();
|
|
if !occlusion_culling_supported {
|
|
text.0
|
|
.push_str("Occlusion culling not supported on this platform");
|
|
continue;
|
|
}
|
|
|
|
let _ = writeln!(
|
|
&mut text.0,
|
|
"Occlusion culling {} (Press Space to toggle)",
|
|
if app_status.occlusion_culling {
|
|
"ON"
|
|
} else {
|
|
"OFF"
|
|
},
|
|
);
|
|
|
|
if !occlusion_culling_introspection_supported {
|
|
continue;
|
|
}
|
|
|
|
let _ = write!(
|
|
&mut text.0,
|
|
"{}/{} meshes rendered",
|
|
rendered_object_count, total_mesh_count
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A system that reads the indirect parameters back from the GPU so that we can
|
|
/// report how many meshes were culled.
|
|
fn readback_indirect_parameters(
|
|
mut indirect_parameters_staging_buffers: ResMut<IndirectParametersStagingBuffers>,
|
|
saved_indirect_parameters: Res<SavedIndirectParameters>,
|
|
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
|
|
) {
|
|
// If culling isn't supported on this platform, note that, and bail.
|
|
if gpu_preprocessing_support.max_supported_mode != GpuPreprocessingMode::Culling {
|
|
saved_indirect_parameters
|
|
.lock()
|
|
.unwrap()
|
|
.occlusion_culling_supported = false;
|
|
return;
|
|
}
|
|
|
|
// Grab the staging buffers.
|
|
let (Some(data_buffer), Some(batch_sets_buffer)) = (
|
|
indirect_parameters_staging_buffers.data.take(),
|
|
indirect_parameters_staging_buffers.batch_sets.take(),
|
|
) else {
|
|
return;
|
|
};
|
|
|
|
// Read the GPU buffers back.
|
|
let saved_indirect_parameters_0 = (**saved_indirect_parameters).clone();
|
|
let saved_indirect_parameters_1 = (**saved_indirect_parameters).clone();
|
|
readback_buffer::<IndirectParametersIndexed>(data_buffer, move |indirect_parameters| {
|
|
saved_indirect_parameters_0.lock().unwrap().data = indirect_parameters.to_vec();
|
|
});
|
|
readback_buffer::<u32>(batch_sets_buffer, move |indirect_parameters_count| {
|
|
saved_indirect_parameters_1.lock().unwrap().count = indirect_parameters_count[0];
|
|
});
|
|
}
|
|
|
|
// A helper function to asynchronously read an array of [`Pod`] values back from
|
|
// the GPU to the CPU.
|
|
//
|
|
// The given callback is invoked when the data is ready. The buffer will
|
|
// automatically be unmapped after the callback executes.
|
|
fn readback_buffer<T>(buffer: Buffer, callback: impl FnOnce(&[T]) + Send + 'static)
|
|
where
|
|
T: Pod,
|
|
{
|
|
// We need to make another reference to the buffer so that we can move the
|
|
// original reference into the closure below.
|
|
let original_buffer = buffer.clone();
|
|
original_buffer
|
|
.slice(..)
|
|
.map_async(MapMode::Read, move |result| {
|
|
// Make sure we succeeded.
|
|
if result.is_err() {
|
|
return;
|
|
}
|
|
|
|
{
|
|
// Cast the raw bytes in the GPU buffer to the appropriate type.
|
|
let buffer_view = buffer.slice(..).get_mapped_range();
|
|
let indirect_parameters: &[T] = bytemuck::cast_slice(
|
|
&buffer_view[0..(buffer_view.len() / size_of::<T>() * size_of::<T>())],
|
|
);
|
|
|
|
// Invoke the callback.
|
|
callback(indirect_parameters);
|
|
}
|
|
|
|
// Unmap the buffer. We have to do this before submitting any more
|
|
// GPU command buffers, or `wgpu` will assert.
|
|
buffer.unmap();
|
|
});
|
|
}
|
|
|
|
/// Adds or removes the [`OcclusionCulling`] and [`DepthPrepass`] components
|
|
/// when the user presses the spacebar.
|
|
fn toggle_occlusion_culling_on_request(
|
|
mut commands: Commands,
|
|
input: Res<ButtonInput<KeyCode>>,
|
|
mut app_status: ResMut<AppStatus>,
|
|
cameras: Query<Entity, With<Camera3d>>,
|
|
) {
|
|
// Only run when the user presses the spacebar.
|
|
if !input.just_pressed(KeyCode::Space) {
|
|
return;
|
|
}
|
|
|
|
// Toggle the occlusion culling flag in `AppStatus`.
|
|
app_status.occlusion_culling = !app_status.occlusion_culling;
|
|
|
|
// Add or remove the `OcclusionCulling` and `DepthPrepass` components as
|
|
// requested.
|
|
for camera in &cameras {
|
|
if app_status.occlusion_culling {
|
|
commands
|
|
.entity(camera)
|
|
.insert(DepthPrepass)
|
|
.insert(OcclusionCulling);
|
|
} else {
|
|
commands
|
|
.entity(camera)
|
|
.remove::<DepthPrepass>()
|
|
.remove::<OcclusionCulling>();
|
|
}
|
|
}
|
|
}
|