1use std::{
9 any::TypeId,
10 f32::consts::PI,
11 fmt::{self, Formatter, Write as _},
12 sync::{Arc, Mutex},
13};
14
15use bevy::{
16 color::palettes::css::{SILVER, WHITE},
17 core_pipeline::{core_3d::Opaque3d, prepass::DepthPrepass, Core3d, Core3dSystems},
18 feathers::{theme::UiTheme, FeathersPlugins},
19 pbr::PbrPlugin,
20 prelude::*,
21 render::{
22 batching::gpu_preprocessing::{
23 GpuPreprocessingSupport, IndirectParametersBuffers, IndirectParametersIndexed,
24 },
25 occlusion_culling::OcclusionCulling,
26 render_resource::{Buffer, BufferDescriptor, BufferUsages, MapMode},
27 renderer::{RenderContext, RenderDevice},
28 settings::WgpuFeatures,
29 Render, RenderApp, RenderDebugFlags, RenderPlugin, RenderStartup, RenderSystems,
30 },
31 ui_widgets::{radio_self_update, ValueChange},
32};
33use radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
34
35#[path = "../helpers/radio.rs"]
36mod radio;
37
38#[path = "../helpers/theme.rs"]
39mod theme;
40
41use bytemuck::Pod;
42
43const OUTER_RADIUS: f32 = 3.0;
45
46const OUTER_SUBDIVISION_COUNT: u32 = 5;
48
49const ROTATION_SPEED: f32 = 0.01;
52
53const SMALL_CUBE_SIZE: f32 = 0.1;
55
56const LARGE_CUBE_SIZE: f32 = 2.0;
58
59#[derive(Default, Component)]
61struct SphereParent;
62
63#[derive(Default, Component)]
65struct LargeCube;
66
67struct ReadbackIndirectParametersPlugin;
70
71#[derive(Resource, Default)]
82struct IndirectParametersStagingBuffers {
83 data: Option<Buffer>,
88 batch_sets: Option<Buffer>,
92}
93
94#[derive(Clone, Resource, Deref, DerefMut)]
105struct SavedIndirectParameters(Arc<Mutex<Option<SavedIndirectParametersData>>>);
106
107struct SavedIndirectParametersData {
111 data: Vec<IndirectParametersIndexed>,
114 count: u32,
120 occlusion_culling_supported: bool,
122 occlusion_culling_introspection_supported: bool,
130}
131
132impl SavedIndirectParameters {
133 fn new() -> Self {
134 Self(Arc::new(Mutex::new(None)))
135 }
136}
137
138fn init_saved_indirect_parameters(
139 render_device: Res<RenderDevice>,
140 gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
141 saved_indirect_parameters: Res<SavedIndirectParameters>,
142) {
143 let mut saved_indirect_parameters = saved_indirect_parameters.0.lock().unwrap();
144 *saved_indirect_parameters = Some(SavedIndirectParametersData {
145 data: vec![],
146 count: 0,
147 occlusion_culling_supported: gpu_preprocessing_support.is_culling_supported(),
148 occlusion_culling_introspection_supported: render_device
152 .features()
153 .contains(WgpuFeatures::MULTI_DRAW_INDIRECT_COUNT),
154 });
155}
156
157#[derive(Resource)]
159struct AppStatus {
160 occlusion_culling: OcclusionCullingSetting,
164}
165
166impl Default for AppStatus {
167 fn default() -> Self {
168 AppStatus {
169 occlusion_culling: OcclusionCullingSetting::On,
170 }
171 }
172}
173
174fn main() {
175 let render_debug_flags = RenderDebugFlags::ALLOW_COPIES_FROM_INDIRECT_PARAMETERS;
176
177 App::new()
178 .add_plugins((
179 DefaultPlugins
180 .set(WindowPlugin {
181 primary_window: Some(Window {
182 title: "Bevy Occlusion Culling Example".into(),
183 ..default()
184 }),
185 ..default()
186 })
187 .set(RenderPlugin {
188 debug_flags: render_debug_flags,
189 ..default()
190 })
191 .set(PbrPlugin {
192 debug_flags: render_debug_flags,
193 ..default()
194 }),
195 FeathersPlugins,
196 ))
197 .add_plugins(ReadbackIndirectParametersPlugin)
198 .insert_resource(UiTheme(theme::basic_example_theme(Color::WHITE)))
199 .init_resource::<AppStatus>()
200 .add_systems(Startup, setup)
201 .add_systems(Update, spin_small_cubes)
202 .add_systems(Update, spin_large_cube)
203 .add_systems(Update, update_status_text)
204 .add_observer(handle_selection_change)
205 .add_observer(radio_self_update)
206 .run();
207}
208
209impl Plugin for ReadbackIndirectParametersPlugin {
210 fn build(&self, app: &mut App) {
211 let saved_indirect_parameters = SavedIndirectParameters::new();
218 app.insert_resource(saved_indirect_parameters.clone());
219
220 let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
222 return;
223 };
224
225 render_app
226 .insert_resource(saved_indirect_parameters)
228 .add_systems(RenderStartup, init_saved_indirect_parameters)
230 .init_resource::<IndirectParametersStagingBuffers>()
231 .add_systems(ExtractSchedule, readback_indirect_parameters)
232 .add_systems(
233 Render,
234 create_indirect_parameters_staging_buffers
235 .in_set(RenderSystems::PrepareResourcesFlush),
236 )
237 .add_systems(
238 Core3d,
239 readback_indirect_parameters_node
243 .after(Core3dSystems::MainPass)
247 .before(Core3dSystems::PostProcess),
248 );
249 }
250}
251
252fn setup(
254 mut commands: Commands,
255 asset_server: Res<AssetServer>,
256 mut meshes: ResMut<Assets<Mesh>>,
257 mut materials: ResMut<Assets<StandardMaterial>>,
258) {
259 spawn_small_cubes(&mut commands, &mut meshes, &mut materials);
260 spawn_large_cube(&mut commands, &asset_server, &mut meshes, &mut materials);
261 spawn_light(&mut commands);
262 spawn_camera(&mut commands);
263 spawn_status_text(&mut commands);
264 spawn_buttons(&mut commands);
265}
266
267fn spawn_small_cubes(
269 commands: &mut Commands,
270 meshes: &mut Assets<Mesh>,
271 materials: &mut Assets<StandardMaterial>,
272) {
273 let small_cube = meshes.add(Cuboid::new(
275 SMALL_CUBE_SIZE,
276 SMALL_CUBE_SIZE,
277 SMALL_CUBE_SIZE,
278 ));
279
280 let small_cube_material = materials.add(StandardMaterial {
282 base_color: SILVER.into(),
283 ..default()
284 });
285
286 let sphere_parent = commands
289 .spawn(Transform::from_translation(Vec3::ZERO))
290 .insert(Visibility::default())
291 .insert(SphereParent)
292 .id();
293
294 let sphere = Sphere::new(OUTER_RADIUS)
302 .mesh()
303 .ico(OUTER_SUBDIVISION_COUNT)
304 .unwrap();
305 let sphere_positions = sphere.attribute(Mesh::ATTRIBUTE_POSITION).unwrap();
306
307 for sphere_position in sphere_positions.as_float3().unwrap() {
309 let sphere_position = Vec3::from_slice(sphere_position);
310 let small_cube = commands
311 .spawn(Mesh3d(small_cube.clone()))
312 .insert(MeshMaterial3d(small_cube_material.clone()))
313 .insert(Transform::from_translation(sphere_position))
314 .id();
315 commands.entity(sphere_parent).add_child(small_cube);
316 }
317}
318
319fn spawn_large_cube(
323 commands: &mut Commands,
324 asset_server: &AssetServer,
325 meshes: &mut Assets<Mesh>,
326 materials: &mut Assets<StandardMaterial>,
327) {
328 commands
329 .spawn(Mesh3d(meshes.add(Cuboid::new(
330 LARGE_CUBE_SIZE,
331 LARGE_CUBE_SIZE,
332 LARGE_CUBE_SIZE,
333 ))))
334 .insert(MeshMaterial3d(materials.add(StandardMaterial {
335 base_color: WHITE.into(),
336 base_color_texture: Some(asset_server.load("branding/icon.png")),
337 ..default()
338 })))
339 .insert(Transform::IDENTITY)
340 .insert(LargeCube);
341}
342
343fn spin_small_cubes(mut sphere_parents: Query<&mut Transform, With<SphereParent>>) {
348 for mut sphere_parent_transform in &mut sphere_parents {
349 sphere_parent_transform.rotate_y(ROTATION_SPEED);
350 }
351}
352
353fn spin_large_cube(mut large_cubes: Query<&mut Transform, With<LargeCube>>) {
358 for mut transform in &mut large_cubes {
359 transform.rotate(Quat::from_euler(
360 EulerRot::XYZ,
361 0.13 * ROTATION_SPEED,
362 0.29 * ROTATION_SPEED,
363 0.35 * ROTATION_SPEED,
364 ));
365 }
366}
367
368fn spawn_light(commands: &mut Commands) {
370 commands
371 .spawn(DirectionalLight::default())
372 .insert(Transform::from_rotation(Quat::from_euler(
373 EulerRot::ZYX,
374 0.0,
375 PI * -0.15,
376 PI * -0.15,
377 )));
378}
379
380fn spawn_camera(commands: &mut Commands) {
382 commands
383 .spawn(Camera3d::default())
384 .insert(Transform::from_xyz(0.0, 0.0, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
385 .insert(DepthPrepass)
386 .insert(OcclusionCulling);
387}
388
389fn spawn_status_text(commands: &mut Commands) {
391 commands.spawn((
392 Text::new(""), Node {
394 position_type: PositionType::Absolute,
395 top: px(12),
396 left: px(12),
397 ..default()
398 },
399 StatusText,
400 ));
401}
402
403fn readback_indirect_parameters_node(
404 mut render_context: RenderContext,
405 indirect_parameters_buffers: Res<IndirectParametersBuffers>,
406 indirect_parameters_mapping_buffers: Res<IndirectParametersStagingBuffers>,
407) {
408 let Some(phase_indirect_parameters_buffers) =
411 indirect_parameters_buffers.get(&TypeId::of::<Opaque3d>())
412 else {
413 return;
414 };
415
416 let (
421 Some(indexed_data_buffer),
422 Some(indexed_batch_sets_buffer),
423 Some(indirect_parameters_staging_data_buffer),
424 Some(indirect_parameters_staging_batch_sets_buffer),
425 ) = (
426 phase_indirect_parameters_buffers.indexed.data_buffer(),
427 phase_indirect_parameters_buffers
428 .indexed
429 .batch_sets_buffer(),
430 indirect_parameters_mapping_buffers.data.as_ref(),
431 indirect_parameters_mapping_buffers.batch_sets.as_ref(),
432 )
433 else {
434 return;
435 };
436
437 render_context.command_encoder().copy_buffer_to_buffer(
439 indexed_data_buffer,
440 0,
441 indirect_parameters_staging_data_buffer,
442 0,
443 indexed_data_buffer.size(),
444 );
445 render_context.command_encoder().copy_buffer_to_buffer(
446 indexed_batch_sets_buffer,
447 0,
448 indirect_parameters_staging_batch_sets_buffer,
449 0,
450 indexed_batch_sets_buffer.size(),
451 );
452}
453
454fn create_indirect_parameters_staging_buffers(
464 mut indirect_parameters_staging_buffers: ResMut<IndirectParametersStagingBuffers>,
465 indirect_parameters_buffers: Res<IndirectParametersBuffers>,
466 render_device: Res<RenderDevice>,
467) {
468 let Some(phase_indirect_parameters_buffers) =
469 indirect_parameters_buffers.get(&TypeId::of::<Opaque3d>())
470 else {
471 return;
472 };
473
474 let (Some(indexed_data_buffer), Some(indexed_batch_set_buffer)) = (
476 phase_indirect_parameters_buffers.indexed.data_buffer(),
477 phase_indirect_parameters_buffers
478 .indexed
479 .batch_sets_buffer(),
480 ) else {
481 return;
482 };
483
484 indirect_parameters_staging_buffers.data =
487 Some(render_device.create_buffer(&BufferDescriptor {
488 label: Some("indexed data staging buffer"),
489 size: indexed_data_buffer.size(),
490 usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
491 mapped_at_creation: false,
492 }));
493 indirect_parameters_staging_buffers.batch_sets =
494 Some(render_device.create_buffer(&BufferDescriptor {
495 label: Some("indexed batch set staging buffer"),
496 size: indexed_batch_set_buffer.size(),
497 usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
498 mapped_at_creation: false,
499 }));
500}
501
502fn update_status_text(
504 saved_indirect_parameters: Res<SavedIndirectParameters>,
505 mut texts: Query<&mut Text, With<StatusText>>,
506 meshes: Query<Entity, With<Mesh3d>>,
507) {
508 let total_mesh_count = meshes.iter().count();
510
511 let (
516 rendered_object_count,
517 occlusion_culling_supported,
518 occlusion_culling_introspection_supported,
519 ): (u32, bool, bool) = {
520 let saved_indirect_parameters = saved_indirect_parameters.lock().unwrap();
521 let Some(saved_indirect_parameters) = saved_indirect_parameters.as_ref() else {
522 return;
524 };
525 (
526 saved_indirect_parameters
527 .data
528 .iter()
529 .take(saved_indirect_parameters.count as usize)
530 .map(|indirect_parameters| indirect_parameters.instance_count)
531 .sum(),
532 saved_indirect_parameters.occlusion_culling_supported,
533 saved_indirect_parameters.occlusion_culling_introspection_supported,
534 )
535 };
536
537 for mut text in &mut texts {
539 text.0 = String::new();
540 if !occlusion_culling_supported {
541 text.0
542 .push_str("Occlusion culling not supported on this platform");
543 continue;
544 }
545
546 if !occlusion_culling_introspection_supported {
547 continue;
548 }
549
550 let _ = write!(
551 &mut text.0,
552 "{rendered_object_count}/{total_mesh_count} meshes rendered"
553 );
554 }
555}
556
557fn readback_indirect_parameters(
560 mut indirect_parameters_staging_buffers: ResMut<IndirectParametersStagingBuffers>,
561 saved_indirect_parameters: Res<SavedIndirectParameters>,
562) {
563 if !saved_indirect_parameters
565 .lock()
566 .unwrap()
567 .as_ref()
568 .unwrap()
569 .occlusion_culling_supported
570 {
571 return;
572 }
573
574 let (Some(data_buffer), Some(batch_sets_buffer)) = (
576 indirect_parameters_staging_buffers.data.take(),
577 indirect_parameters_staging_buffers.batch_sets.take(),
578 ) else {
579 return;
580 };
581
582 let saved_indirect_parameters_0 = (**saved_indirect_parameters).clone();
584 let saved_indirect_parameters_1 = (**saved_indirect_parameters).clone();
585 readback_buffer::<IndirectParametersIndexed>(data_buffer, move |indirect_parameters| {
586 saved_indirect_parameters_0
587 .lock()
588 .unwrap()
589 .as_mut()
590 .unwrap()
591 .data = indirect_parameters.to_vec();
592 });
593 readback_buffer::<u32>(batch_sets_buffer, move |indirect_parameters_count| {
594 saved_indirect_parameters_1
595 .lock()
596 .unwrap()
597 .as_mut()
598 .unwrap()
599 .count = indirect_parameters_count[0];
600 });
601}
602
603fn readback_buffer<T>(buffer: Buffer, callback: impl FnOnce(&[T]) + Send + 'static)
609where
610 T: Pod,
611{
612 let original_buffer = buffer.clone();
615 original_buffer
616 .slice(..)
617 .map_async(MapMode::Read, move |result| {
618 if result.is_err() {
620 return;
621 }
622
623 {
624 let buffer_view = buffer.slice(..).get_mapped_range().unwrap();
626 let indirect_parameters: &[T] = bytemuck::cast_slice(
627 &buffer_view[0..(buffer_view.len() / size_of::<T>() * size_of::<T>())],
628 );
629
630 callback(indirect_parameters);
632 }
633
634 buffer.unmap();
637 });
638}
639
640#[derive(Clone, Copy, Component)]
642struct StatusText;
643
644#[derive(Clone, Copy, Component, Default, PartialEq, Debug)]
646enum OcclusionCullingSetting {
647 #[default]
648 On,
649 Off,
650}
651
652impl fmt::Display for OcclusionCullingSetting {
653 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
654 match *self {
655 OcclusionCullingSetting::On => f.write_str("ON"),
656 OcclusionCullingSetting::Off => f.write_str("OFF"),
657 }
658 }
659}
660
661fn spawn_buttons(commands: &mut Commands) {
664 commands.spawn_scene(bsn! {
665 @main_ui_node_scene()
666 Children [
667 @feathers_option_buttons(
668 "Toggle occlusion culling",
669 &[
670 (OcclusionCullingSetting::On, "ON"),
671 (OcclusionCullingSetting::Off, "OFF"),
672 ],
673 0,
674 )
675 ]
676 });
677}
678
679fn handle_selection_change(
682 event: On<ValueChange<Entity>>,
683 mut commands: Commands,
684 new_value_query: Query<&RadioButtonOptionValue<OcclusionCullingSetting>>,
685 mut app_status: ResMut<AppStatus>,
686 cameras: Query<Entity, With<Camera3d>>,
687) {
688 let Ok(RadioButtonOptionValue(selection)) = new_value_query.get(event.value) else {
689 return;
690 };
691
692 app_status.occlusion_culling = *selection;
694
695 for camera in &cameras {
698 if app_status.occlusion_culling == OcclusionCullingSetting::On {
699 commands
700 .entity(camera)
701 .insert(DepthPrepass)
702 .insert(OcclusionCulling);
703 } else {
704 commands
705 .entity(camera)
706 .remove::<DepthPrepass>()
707 .remove::<OcclusionCulling>();
708 }
709 }
710}