From f3f8094530ed1e903f8de1631e2b9bc7fc284e92 Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Tue, 23 Dec 2025 23:50:49 +0000 Subject: [PATCH] Vendor Bevy rendering crates (Phase 1 complete) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #6, #7, #8, #9, #10 Refs #2, #122 Vendored bevy_render, bevy_core_pipeline, and bevy_pbr from Bevy v0.17.2 (commit 566358363126dd69f6e457e47f306c68f8041d2a) into libmarathon. - ~51K LOC vendored to crates/libmarathon/src/render/ - Merged bevy_render_macros into crates/macros/ - Fixed 773→0 compilation errors - Updated dependencies (encase 0.10→0.11, added 4 new deps) - Removed bevy_render/pbr/core_pipeline from app Cargo features All builds passing, macOS smoke test successful. Signed-off-by: Sienna Meridian Satterwhite --- Cargo.lock | 116 +- Cargo.toml | 2 +- crates/app/Cargo.toml | 4 +- crates/libmarathon/Cargo.toml | 44 +- crates/libmarathon/src/lib.rs | 1 + crates/libmarathon/src/render/alpha.rs | 62 + .../src/render/batching/gpu_preprocessing.rs | 2142 +++++++++++ crates/libmarathon/src/render/batching/mod.rs | 225 ++ .../render/batching/no_gpu_preprocessing.rs | 182 + crates/libmarathon/src/render/bindless.wgsl | 37 + crates/libmarathon/src/render/blit/blit.wgsl | 9 + crates/libmarathon/src/render/blit/mod.rs | 114 + crates/libmarathon/src/render/camera.rs | 695 ++++ .../src/render/color_operations.wgsl | 47 + .../core_2d/main_opaque_pass_2d_node.rs | 106 + .../core_2d/main_transparent_pass_2d_node.rs | 120 + crates/libmarathon/src/render/core_2d/mod.rs | 508 +++ .../core_3d/main_opaque_pass_3d_node.rs | 142 + .../core_3d/main_transmissive_pass_3d_node.rs | 167 + .../core_3d/main_transparent_pass_3d_node.rs | 107 + crates/libmarathon/src/render/core_3d/mod.rs | 1150 ++++++ .../deferred/copy_deferred_lighting_id.wgsl | 18 + .../src/render/deferred/copy_lighting_id.rs | 193 + crates/libmarathon/src/render/deferred/mod.rs | 186 + .../libmarathon/src/render/deferred/node.rs | 273 ++ .../src/render/diagnostic/internal.rs | 709 ++++ .../libmarathon/src/render/diagnostic/mod.rs | 188 + .../src/render/diagnostic/tracy_gpu.rs | 69 + .../src/render/erased_render_asset.rs | 431 +++ .../mip_generation/downsample_depth.wgsl | 338 ++ .../render/experimental/mip_generation/mod.rs | 783 ++++ .../src/render/experimental/mod.rs | 8 + .../mesh_preprocess_types.wgsl | 69 + .../experimental/occlusion_culling/mod.rs | 104 + .../src/render/extract_component.rs | 236 ++ .../src/render/extract_instances.rs | 137 + .../libmarathon/src/render/extract_param.rs | 177 + .../src/render/extract_resource.rs | 70 + .../fullscreen_vertex_shader/fullscreen.wgsl | 34 + .../render/fullscreen_vertex_shader/mod.rs | 41 + crates/libmarathon/src/render/globals.rs | 79 + crates/libmarathon/src/render/globals.wgsl | 16 + .../src/render/gpu_component_array_buffer.rs | 59 + crates/libmarathon/src/render/gpu_readback.rs | 414 +++ crates/libmarathon/src/render/maths.wgsl | 186 + .../libmarathon/src/render/mesh/allocator.rs | 1033 +++++ crates/libmarathon/src/render/mesh/mod.rs | 181 + crates/libmarathon/src/render/mod.rs | 317 ++ crates/libmarathon/src/render/oit/mod.rs | 297 ++ .../libmarathon/src/render/oit/oit_draw.wgsl | 48 + .../libmarathon/src/render/oit/resolve/mod.rs | 255 ++ .../src/render/oit/resolve/node.rs | 88 + .../src/render/oit/resolve/oit_resolve.wgsl | 117 + .../pbr/atmosphere/aerial_view_lut.wgsl | 65 + .../src/render/pbr/atmosphere/bindings.wgsl | 22 + .../pbr/atmosphere/bruneton_functions.wgsl | 139 + .../src/render/pbr/atmosphere/environment.rs | 332 ++ .../render/pbr/atmosphere/environment.wgsl | 39 + .../src/render/pbr/atmosphere/functions.wgsl | 528 +++ .../src/render/pbr/atmosphere/mod.rs | 510 +++ .../pbr/atmosphere/multiscattering_lut.wgsl | 139 + .../src/render/pbr/atmosphere/node.rs | 233 ++ .../src/render/pbr/atmosphere/render_sky.wgsl | 82 + .../src/render/pbr/atmosphere/resources.rs | 700 ++++ .../render/pbr/atmosphere/sky_view_lut.wgsl | 44 + .../pbr/atmosphere/transmittance_lut.wgsl | 48 + .../src/render/pbr/atmosphere/types.wgsl | 46 + .../src/render/pbr/bluenoise/stbn.ktx2 | Bin 0 -> 1573092 bytes crates/libmarathon/src/render/pbr/cluster.rs | 580 +++ .../libmarathon/src/render/pbr/components.rs | 46 + .../src/render/pbr/decal/clustered.rs | 441 +++ .../src/render/pbr/decal/clustered.wgsl | 183 + .../src/render/pbr/decal/forward.rs | 165 + .../src/render/pbr/decal/forward_decal.wgsl | 52 + .../libmarathon/src/render/pbr/decal/mod.rs | 11 + .../pbr/deferred/deferred_lighting.wgsl | 88 + .../src/render/pbr/deferred/mod.rs | 570 +++ .../pbr/deferred/pbr_deferred_functions.wgsl | 153 + .../pbr/deferred/pbr_deferred_types.wgsl | 89 + .../src/render/pbr/extended_material.rs | 406 ++ crates/libmarathon/src/render/pbr/fog.rs | 477 +++ .../src/render/pbr/light_probe/copy.wgsl | 21 + .../render/pbr/light_probe/downsample.wgsl | 439 +++ .../pbr/light_probe/environment_filter.wgsl | 185 + .../render/pbr/light_probe/environment_map.rs | 310 ++ .../pbr/light_probe/environment_map.wgsl | 279 ++ .../src/render/pbr/light_probe/generate.rs | 1186 ++++++ .../pbr/light_probe/irradiance_volume.rs | 325 ++ .../pbr/light_probe/irradiance_volume.wgsl | 73 + .../render/pbr/light_probe/light_probe.wgsl | 154 + .../src/render/pbr/light_probe/mod.rs | 731 ++++ .../src/render/pbr/lightmap/lightmap.wgsl | 99 + .../src/render/pbr/lightmap/mod.rs | 519 +++ crates/libmarathon/src/render/pbr/material.rs | 1823 +++++++++ .../src/render/pbr/material_bind_groups.rs | 1996 ++++++++++ .../src/render/pbr/mesh_material.rs | 75 + .../src/render/pbr/meshlet/asset.rs | 319 ++ .../pbr/meshlet/clear_visibility_buffer.wgsl | 18 + .../src/render/pbr/meshlet/cull_bvh.wgsl | 110 + .../src/render/pbr/meshlet/cull_clusters.wgsl | 93 + .../render/pbr/meshlet/cull_instances.wgsl | 76 + .../dummy_visibility_buffer_resolve.wgsl | 4 + .../src/render/pbr/meshlet/fill_counts.wgsl | 35 + .../src/render/pbr/meshlet/from_mesh.rs | 1109 ++++++ .../render/pbr/meshlet/instance_manager.rs | 295 ++ .../pbr/meshlet/material_pipeline_prepare.rs | 475 +++ .../pbr/meshlet/material_shade_nodes.rs | 421 +++ .../render/pbr/meshlet/meshlet_bindings.wgsl | 306 ++ .../pbr/meshlet/meshlet_cull_shared.wgsl | 207 ++ .../pbr/meshlet/meshlet_mesh_manager.rs | 161 + .../pbr/meshlet/meshlet_mesh_material.wgsl | 52 + .../render/pbr/meshlet/meshlet_preview.png | Bin 0 -> 183449 bytes .../libmarathon/src/render/pbr/meshlet/mod.rs | 307 ++ .../render/pbr/meshlet/persistent_buffer.rs | 132 + .../pbr/meshlet/persistent_buffer_impls.rs | 128 + .../src/render/pbr/meshlet/pipelines.rs | 580 +++ .../pbr/meshlet/remap_1d_to_2d_dispatch.wgsl | 24 + .../pbr/meshlet/resolve_render_targets.wgsl | 41 + .../render/pbr/meshlet/resource_manager.rs | 1224 ++++++ .../visibility_buffer_hardware_raster.wgsl | 79 + .../meshlet/visibility_buffer_raster_node.rs | 706 ++++ .../meshlet/visibility_buffer_resolve.wgsl | 240 ++ .../visibility_buffer_software_raster.wgsl | 189 + crates/libmarathon/src/render/pbr/mod.rs | 390 ++ crates/libmarathon/src/render/pbr/parallax.rs | 47 + .../src/render/pbr/pbr_material.rs | 1554 ++++++++ .../libmarathon/src/render/pbr/prepass/mod.rs | 1282 +++++++ .../src/render/pbr/prepass/prepass.wgsl | 219 ++ .../render/pbr/prepass/prepass_bindings.rs | 75 + .../render/pbr/prepass/prepass_bindings.wgsl | 13 + .../src/render/pbr/prepass/prepass_io.wgsl | 100 + .../src/render/pbr/prepass/prepass_utils.wgsl | 35 + .../pbr/render/build_indirect_params.wgsl | 142 + .../render/pbr/render/clustered_forward.wgsl | 193 + .../libmarathon/src/render/pbr/render/fog.rs | 144 + .../src/render/pbr/render/fog.wgsl | 79 + .../src/render/pbr/render/forward_io.wgsl | 60 + .../src/render/pbr/render/gpu_preprocess.rs | 2704 ++++++++++++++ .../src/render/pbr/render/light.rs | 2357 ++++++++++++ .../libmarathon/src/render/pbr/render/mesh.rs | 3312 +++++++++++++++++ .../src/render/pbr/render/mesh.wgsl | 120 + .../src/render/pbr/render/mesh_bindings.rs | 551 +++ .../src/render/pbr/render/mesh_bindings.wgsl | 11 + .../src/render/pbr/render/mesh_functions.wgsl | 168 + .../render/pbr/render/mesh_preprocess.wgsl | 373 ++ .../src/render/pbr/render/mesh_types.wgsl | 47 + .../render/pbr/render/mesh_view_bindings.rs | 818 ++++ .../render/pbr/render/mesh_view_bindings.wgsl | 119 + .../render/pbr/render/mesh_view_types.wgsl | 187 + .../libmarathon/src/render/pbr/render/mod.rs | 17 + .../src/render/pbr/render/morph.rs | 150 + .../src/render/pbr/render/morph.wgsl | 52 + .../render/pbr/render/occlusion_culling.wgsl | 30 + .../render/pbr/render/parallax_mapping.wgsl | 139 + .../src/render/pbr/render/pbr.wgsl | 107 + .../src/render/pbr/render/pbr_ambient.wgsl | 29 + .../src/render/pbr/render/pbr_bindings.wgsl | 89 + .../src/render/pbr/render/pbr_fragment.wgsl | 844 +++++ .../src/render/pbr/render/pbr_functions.wgsl | 883 +++++ .../src/render/pbr/render/pbr_lighting.wgsl | 856 +++++ .../src/render/pbr/render/pbr_prepass.wgsl | 151 + .../pbr/render/pbr_prepass_functions.wgsl | 102 + .../render/pbr/render/pbr_transmission.wgsl | 192 + .../src/render/pbr/render/pbr_types.wgsl | 151 + .../pbr/render/reset_indirect_batch_sets.wgsl | 25 + .../src/render/pbr/render/rgb9e5.wgsl | 63 + .../render/pbr/render/shadow_sampling.wgsl | 599 +++ .../src/render/pbr/render/shadows.wgsl | 231 ++ .../libmarathon/src/render/pbr/render/skin.rs | 623 ++++ .../src/render/pbr/render/skinning.wgsl | 95 + .../src/render/pbr/render/utils.wgsl | 205 + .../pbr/render/view_transformations.wgsl | 238 ++ .../src/render/pbr/render/wireframe.wgsl | 12 + crates/libmarathon/src/render/pbr/ssao/mod.rs | 757 ++++ .../src/render/pbr/ssao/preprocess_depth.wgsl | 102 + .../src/render/pbr/ssao/spatial_denoise.wgsl | 85 + .../libmarathon/src/render/pbr/ssao/ssao.wgsl | 200 + .../src/render/pbr/ssao/ssao_utils.wgsl | 13 + crates/libmarathon/src/render/pbr/ssr/mod.rs | 578 +++ .../src/render/pbr/ssr/raymarch.wgsl | 511 +++ .../libmarathon/src/render/pbr/ssr/ssr.wgsl | 194 + .../src/render/pbr/volumetric_fog/mod.rs | 114 + .../src/render/pbr/volumetric_fog/render.rs | 882 +++++ .../pbr/volumetric_fog/volumetric_fog.wgsl | 486 +++ .../libmarathon/src/render/pbr/wireframe.rs | 915 +++++ .../src/render/pipelined_rendering.rs | 204 + crates/libmarathon/src/render/prepass/mod.rs | 383 ++ crates/libmarathon/src/render/prepass/node.rs | 255 ++ crates/libmarathon/src/render/render_asset.rs | 516 +++ .../src/render/render_graph/app.rs | 174 + .../render/render_graph/camera_driver_node.rs | 99 + .../src/render/render_graph/context.rs | 283 ++ .../src/render/render_graph/edge.rs | 57 + .../src/render/render_graph/graph.rs | 918 +++++ .../src/render/render_graph/mod.rs | 56 + .../src/render/render_graph/node.rs | 420 +++ .../src/render/render_graph/node_slot.rs | 165 + .../src/render/render_phase/draw.rs | 398 ++ .../src/render/render_phase/draw_state.rs | 682 ++++ .../src/render/render_phase/mod.rs | 1911 ++++++++++ .../src/render/render_phase/rangefinder.rs | 50 + .../render_resource/batched_uniform_buffer.rs | 157 + .../src/render/render_resource/bind_group.rs | 725 ++++ .../render_resource/bind_group_entries.rs | 322 ++ .../render_resource/bind_group_layout.rs | 81 + .../bind_group_layout_entries.rs | 592 +++ .../src/render/render_resource/bindless.rs | 374 ++ .../src/render/render_resource/buffer.rs | 95 + .../src/render/render_resource/buffer_vec.rs | 587 +++ .../render_resource/gpu_array_buffer.rs | 118 + .../src/render/render_resource/mod.rs | 75 + .../src/render/render_resource/pipeline.rs | 183 + .../render/render_resource/pipeline_cache.rs | 831 +++++ .../render_resource/pipeline_specializer.rs | 259 ++ .../render/render_resource/resource_macros.rs | 39 + .../src/render/render_resource/specializer.rs | 353 ++ .../render/render_resource/storage_buffer.rs | 285 ++ .../src/render/render_resource/texture.rs | 166 + .../render/render_resource/uniform_buffer.rs | 402 ++ .../src/render/renderer/graph_runner.rs | 267 ++ crates/libmarathon/src/render/renderer/mod.rs | 662 ++++ .../src/render/renderer/raw_vulkan_init.rs | 148 + .../src/render/renderer/render_device.rs | 311 ++ .../src/render/renderer/wgpu_wrapper.rs | 50 + crates/libmarathon/src/render/settings.rs | 226 ++ crates/libmarathon/src/render/skybox/mod.rs | 305 ++ .../libmarathon/src/render/skybox/prepass.rs | 164 + .../libmarathon/src/render/skybox/skybox.wgsl | 81 + .../src/render/skybox/skybox_prepass.wgsl | 24 + crates/libmarathon/src/render/storage.rs | 135 + .../libmarathon/src/render/sync_component.rs | 42 + crates/libmarathon/src/render/sync_world.rs | 580 +++ .../src/render/texture/fallback_image.rs | 272 ++ .../src/render/texture/gpu_image.rs | 131 + .../src/render/texture/manual_texture_view.rs | 68 + crates/libmarathon/src/render/texture/mod.rs | 73 + .../src/render/texture/texture_attachment.rs | 162 + .../src/render/texture/texture_cache.rs | 108 + .../src/render/tonemapping/lut_bindings.wgsl | 5 + .../luts/AgX-default_contrast.ktx2 | Bin 0 -> 17842 bytes .../tonemapping/luts/Blender_-11_12.ktx2 | Bin 0 -> 308905 bytes .../src/render/tonemapping/luts/info.txt | 22 + .../tonemapping/luts/tony_mc_mapface.ktx2 | Bin 0 -> 354208 bytes .../libmarathon/src/render/tonemapping/mod.rs | 456 +++ .../src/render/tonemapping/node.rs | 148 + .../src/render/tonemapping/tonemapping.wgsl | 34 + .../tonemapping/tonemapping_shared.wgsl | 405 ++ .../libmarathon/src/render/upscaling/mod.rs | 88 + .../libmarathon/src/render/upscaling/node.rs | 104 + crates/libmarathon/src/render/view/mod.rs | 1135 ++++++ crates/libmarathon/src/render/view/view.wgsl | 272 ++ .../src/render/view/visibility/mod.rs | 54 + .../src/render/view/visibility/range.rs | 228 ++ .../libmarathon/src/render/view/window/mod.rs | 401 ++ .../src/render/view/window/screenshot.rs | 695 ++++ .../src/render/view/window/screenshot.wgsl | 16 + crates/libmarathon/src/sync.rs | 4 +- crates/{sync-macros => macros}/Cargo.toml | 3 +- crates/macros/src/as_bind_group.rs | 1817 +++++++++ crates/macros/src/extract_component.rs | 51 + crates/macros/src/extract_resource.rs | 26 + crates/macros/src/lib.rs | 152 + crates/macros/src/specializer.rs | 379 ++ .../tests/basic_macro_test.rs | 0 crates/sync-macros/src/lib.rs | 578 --- 265 files changed, 83142 insertions(+), 643 deletions(-) create mode 100644 crates/libmarathon/src/render/alpha.rs create mode 100644 crates/libmarathon/src/render/batching/gpu_preprocessing.rs create mode 100644 crates/libmarathon/src/render/batching/mod.rs create mode 100644 crates/libmarathon/src/render/batching/no_gpu_preprocessing.rs create mode 100644 crates/libmarathon/src/render/bindless.wgsl create mode 100644 crates/libmarathon/src/render/blit/blit.wgsl create mode 100644 crates/libmarathon/src/render/blit/mod.rs create mode 100644 crates/libmarathon/src/render/camera.rs create mode 100644 crates/libmarathon/src/render/color_operations.wgsl create mode 100644 crates/libmarathon/src/render/core_2d/main_opaque_pass_2d_node.rs create mode 100644 crates/libmarathon/src/render/core_2d/main_transparent_pass_2d_node.rs create mode 100644 crates/libmarathon/src/render/core_2d/mod.rs create mode 100644 crates/libmarathon/src/render/core_3d/main_opaque_pass_3d_node.rs create mode 100644 crates/libmarathon/src/render/core_3d/main_transmissive_pass_3d_node.rs create mode 100644 crates/libmarathon/src/render/core_3d/main_transparent_pass_3d_node.rs create mode 100644 crates/libmarathon/src/render/core_3d/mod.rs create mode 100644 crates/libmarathon/src/render/deferred/copy_deferred_lighting_id.wgsl create mode 100644 crates/libmarathon/src/render/deferred/copy_lighting_id.rs create mode 100644 crates/libmarathon/src/render/deferred/mod.rs create mode 100644 crates/libmarathon/src/render/deferred/node.rs create mode 100644 crates/libmarathon/src/render/diagnostic/internal.rs create mode 100644 crates/libmarathon/src/render/diagnostic/mod.rs create mode 100644 crates/libmarathon/src/render/diagnostic/tracy_gpu.rs create mode 100644 crates/libmarathon/src/render/erased_render_asset.rs create mode 100644 crates/libmarathon/src/render/experimental/mip_generation/downsample_depth.wgsl create mode 100644 crates/libmarathon/src/render/experimental/mip_generation/mod.rs create mode 100644 crates/libmarathon/src/render/experimental/mod.rs create mode 100644 crates/libmarathon/src/render/experimental/occlusion_culling/mesh_preprocess_types.wgsl create mode 100644 crates/libmarathon/src/render/experimental/occlusion_culling/mod.rs create mode 100644 crates/libmarathon/src/render/extract_component.rs create mode 100644 crates/libmarathon/src/render/extract_instances.rs create mode 100644 crates/libmarathon/src/render/extract_param.rs create mode 100644 crates/libmarathon/src/render/extract_resource.rs create mode 100644 crates/libmarathon/src/render/fullscreen_vertex_shader/fullscreen.wgsl create mode 100644 crates/libmarathon/src/render/fullscreen_vertex_shader/mod.rs create mode 100644 crates/libmarathon/src/render/globals.rs create mode 100644 crates/libmarathon/src/render/globals.wgsl create mode 100644 crates/libmarathon/src/render/gpu_component_array_buffer.rs create mode 100644 crates/libmarathon/src/render/gpu_readback.rs create mode 100644 crates/libmarathon/src/render/maths.wgsl create mode 100644 crates/libmarathon/src/render/mesh/allocator.rs create mode 100644 crates/libmarathon/src/render/mesh/mod.rs create mode 100644 crates/libmarathon/src/render/mod.rs create mode 100644 crates/libmarathon/src/render/oit/mod.rs create mode 100644 crates/libmarathon/src/render/oit/oit_draw.wgsl create mode 100644 crates/libmarathon/src/render/oit/resolve/mod.rs create mode 100644 crates/libmarathon/src/render/oit/resolve/node.rs create mode 100644 crates/libmarathon/src/render/oit/resolve/oit_resolve.wgsl create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/aerial_view_lut.wgsl create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/bindings.wgsl create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/bruneton_functions.wgsl create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/environment.rs create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/environment.wgsl create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/functions.wgsl create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/mod.rs create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/multiscattering_lut.wgsl create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/node.rs create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/render_sky.wgsl create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/resources.rs create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/sky_view_lut.wgsl create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/transmittance_lut.wgsl create mode 100644 crates/libmarathon/src/render/pbr/atmosphere/types.wgsl create mode 100644 crates/libmarathon/src/render/pbr/bluenoise/stbn.ktx2 create mode 100644 crates/libmarathon/src/render/pbr/cluster.rs create mode 100644 crates/libmarathon/src/render/pbr/components.rs create mode 100644 crates/libmarathon/src/render/pbr/decal/clustered.rs create mode 100644 crates/libmarathon/src/render/pbr/decal/clustered.wgsl create mode 100644 crates/libmarathon/src/render/pbr/decal/forward.rs create mode 100644 crates/libmarathon/src/render/pbr/decal/forward_decal.wgsl create mode 100644 crates/libmarathon/src/render/pbr/decal/mod.rs create mode 100644 crates/libmarathon/src/render/pbr/deferred/deferred_lighting.wgsl create mode 100644 crates/libmarathon/src/render/pbr/deferred/mod.rs create mode 100644 crates/libmarathon/src/render/pbr/deferred/pbr_deferred_functions.wgsl create mode 100644 crates/libmarathon/src/render/pbr/deferred/pbr_deferred_types.wgsl create mode 100644 crates/libmarathon/src/render/pbr/extended_material.rs create mode 100644 crates/libmarathon/src/render/pbr/fog.rs create mode 100644 crates/libmarathon/src/render/pbr/light_probe/copy.wgsl create mode 100644 crates/libmarathon/src/render/pbr/light_probe/downsample.wgsl create mode 100644 crates/libmarathon/src/render/pbr/light_probe/environment_filter.wgsl create mode 100644 crates/libmarathon/src/render/pbr/light_probe/environment_map.rs create mode 100644 crates/libmarathon/src/render/pbr/light_probe/environment_map.wgsl create mode 100644 crates/libmarathon/src/render/pbr/light_probe/generate.rs create mode 100644 crates/libmarathon/src/render/pbr/light_probe/irradiance_volume.rs create mode 100644 crates/libmarathon/src/render/pbr/light_probe/irradiance_volume.wgsl create mode 100644 crates/libmarathon/src/render/pbr/light_probe/light_probe.wgsl create mode 100644 crates/libmarathon/src/render/pbr/light_probe/mod.rs create mode 100644 crates/libmarathon/src/render/pbr/lightmap/lightmap.wgsl create mode 100644 crates/libmarathon/src/render/pbr/lightmap/mod.rs create mode 100644 crates/libmarathon/src/render/pbr/material.rs create mode 100644 crates/libmarathon/src/render/pbr/material_bind_groups.rs create mode 100644 crates/libmarathon/src/render/pbr/mesh_material.rs create mode 100644 crates/libmarathon/src/render/pbr/meshlet/asset.rs create mode 100644 crates/libmarathon/src/render/pbr/meshlet/clear_visibility_buffer.wgsl create mode 100644 crates/libmarathon/src/render/pbr/meshlet/cull_bvh.wgsl create mode 100644 crates/libmarathon/src/render/pbr/meshlet/cull_clusters.wgsl create mode 100644 crates/libmarathon/src/render/pbr/meshlet/cull_instances.wgsl create mode 100644 crates/libmarathon/src/render/pbr/meshlet/dummy_visibility_buffer_resolve.wgsl create mode 100644 crates/libmarathon/src/render/pbr/meshlet/fill_counts.wgsl create mode 100644 crates/libmarathon/src/render/pbr/meshlet/from_mesh.rs create mode 100644 crates/libmarathon/src/render/pbr/meshlet/instance_manager.rs create mode 100644 crates/libmarathon/src/render/pbr/meshlet/material_pipeline_prepare.rs create mode 100644 crates/libmarathon/src/render/pbr/meshlet/material_shade_nodes.rs create mode 100644 crates/libmarathon/src/render/pbr/meshlet/meshlet_bindings.wgsl create mode 100644 crates/libmarathon/src/render/pbr/meshlet/meshlet_cull_shared.wgsl create mode 100644 crates/libmarathon/src/render/pbr/meshlet/meshlet_mesh_manager.rs create mode 100644 crates/libmarathon/src/render/pbr/meshlet/meshlet_mesh_material.wgsl create mode 100644 crates/libmarathon/src/render/pbr/meshlet/meshlet_preview.png create mode 100644 crates/libmarathon/src/render/pbr/meshlet/mod.rs create mode 100644 crates/libmarathon/src/render/pbr/meshlet/persistent_buffer.rs create mode 100644 crates/libmarathon/src/render/pbr/meshlet/persistent_buffer_impls.rs create mode 100644 crates/libmarathon/src/render/pbr/meshlet/pipelines.rs create mode 100644 crates/libmarathon/src/render/pbr/meshlet/remap_1d_to_2d_dispatch.wgsl create mode 100644 crates/libmarathon/src/render/pbr/meshlet/resolve_render_targets.wgsl create mode 100644 crates/libmarathon/src/render/pbr/meshlet/resource_manager.rs create mode 100644 crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_hardware_raster.wgsl create mode 100644 crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_raster_node.rs create mode 100644 crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_resolve.wgsl create mode 100644 crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_software_raster.wgsl create mode 100644 crates/libmarathon/src/render/pbr/mod.rs create mode 100644 crates/libmarathon/src/render/pbr/parallax.rs create mode 100644 crates/libmarathon/src/render/pbr/pbr_material.rs create mode 100644 crates/libmarathon/src/render/pbr/prepass/mod.rs create mode 100644 crates/libmarathon/src/render/pbr/prepass/prepass.wgsl create mode 100644 crates/libmarathon/src/render/pbr/prepass/prepass_bindings.rs create mode 100644 crates/libmarathon/src/render/pbr/prepass/prepass_bindings.wgsl create mode 100644 crates/libmarathon/src/render/pbr/prepass/prepass_io.wgsl create mode 100644 crates/libmarathon/src/render/pbr/prepass/prepass_utils.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/build_indirect_params.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/clustered_forward.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/fog.rs create mode 100644 crates/libmarathon/src/render/pbr/render/fog.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/forward_io.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/gpu_preprocess.rs create mode 100644 crates/libmarathon/src/render/pbr/render/light.rs create mode 100644 crates/libmarathon/src/render/pbr/render/mesh.rs create mode 100644 crates/libmarathon/src/render/pbr/render/mesh.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/mesh_bindings.rs create mode 100644 crates/libmarathon/src/render/pbr/render/mesh_bindings.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/mesh_functions.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/mesh_preprocess.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/mesh_types.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/mesh_view_bindings.rs create mode 100644 crates/libmarathon/src/render/pbr/render/mesh_view_bindings.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/mesh_view_types.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/mod.rs create mode 100644 crates/libmarathon/src/render/pbr/render/morph.rs create mode 100644 crates/libmarathon/src/render/pbr/render/morph.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/occlusion_culling.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/parallax_mapping.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/pbr.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/pbr_ambient.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/pbr_bindings.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/pbr_fragment.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/pbr_functions.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/pbr_lighting.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/pbr_prepass.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/pbr_prepass_functions.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/pbr_transmission.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/pbr_types.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/reset_indirect_batch_sets.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/rgb9e5.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/shadow_sampling.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/shadows.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/skin.rs create mode 100644 crates/libmarathon/src/render/pbr/render/skinning.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/utils.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/view_transformations.wgsl create mode 100644 crates/libmarathon/src/render/pbr/render/wireframe.wgsl create mode 100644 crates/libmarathon/src/render/pbr/ssao/mod.rs create mode 100644 crates/libmarathon/src/render/pbr/ssao/preprocess_depth.wgsl create mode 100644 crates/libmarathon/src/render/pbr/ssao/spatial_denoise.wgsl create mode 100644 crates/libmarathon/src/render/pbr/ssao/ssao.wgsl create mode 100644 crates/libmarathon/src/render/pbr/ssao/ssao_utils.wgsl create mode 100644 crates/libmarathon/src/render/pbr/ssr/mod.rs create mode 100644 crates/libmarathon/src/render/pbr/ssr/raymarch.wgsl create mode 100644 crates/libmarathon/src/render/pbr/ssr/ssr.wgsl create mode 100644 crates/libmarathon/src/render/pbr/volumetric_fog/mod.rs create mode 100644 crates/libmarathon/src/render/pbr/volumetric_fog/render.rs create mode 100644 crates/libmarathon/src/render/pbr/volumetric_fog/volumetric_fog.wgsl create mode 100644 crates/libmarathon/src/render/pbr/wireframe.rs create mode 100644 crates/libmarathon/src/render/pipelined_rendering.rs create mode 100644 crates/libmarathon/src/render/prepass/mod.rs create mode 100644 crates/libmarathon/src/render/prepass/node.rs create mode 100644 crates/libmarathon/src/render/render_asset.rs create mode 100644 crates/libmarathon/src/render/render_graph/app.rs create mode 100644 crates/libmarathon/src/render/render_graph/camera_driver_node.rs create mode 100644 crates/libmarathon/src/render/render_graph/context.rs create mode 100644 crates/libmarathon/src/render/render_graph/edge.rs create mode 100644 crates/libmarathon/src/render/render_graph/graph.rs create mode 100644 crates/libmarathon/src/render/render_graph/mod.rs create mode 100644 crates/libmarathon/src/render/render_graph/node.rs create mode 100644 crates/libmarathon/src/render/render_graph/node_slot.rs create mode 100644 crates/libmarathon/src/render/render_phase/draw.rs create mode 100644 crates/libmarathon/src/render/render_phase/draw_state.rs create mode 100644 crates/libmarathon/src/render/render_phase/mod.rs create mode 100644 crates/libmarathon/src/render/render_phase/rangefinder.rs create mode 100644 crates/libmarathon/src/render/render_resource/batched_uniform_buffer.rs create mode 100644 crates/libmarathon/src/render/render_resource/bind_group.rs create mode 100644 crates/libmarathon/src/render/render_resource/bind_group_entries.rs create mode 100644 crates/libmarathon/src/render/render_resource/bind_group_layout.rs create mode 100644 crates/libmarathon/src/render/render_resource/bind_group_layout_entries.rs create mode 100644 crates/libmarathon/src/render/render_resource/bindless.rs create mode 100644 crates/libmarathon/src/render/render_resource/buffer.rs create mode 100644 crates/libmarathon/src/render/render_resource/buffer_vec.rs create mode 100644 crates/libmarathon/src/render/render_resource/gpu_array_buffer.rs create mode 100644 crates/libmarathon/src/render/render_resource/mod.rs create mode 100644 crates/libmarathon/src/render/render_resource/pipeline.rs create mode 100644 crates/libmarathon/src/render/render_resource/pipeline_cache.rs create mode 100644 crates/libmarathon/src/render/render_resource/pipeline_specializer.rs create mode 100644 crates/libmarathon/src/render/render_resource/resource_macros.rs create mode 100644 crates/libmarathon/src/render/render_resource/specializer.rs create mode 100644 crates/libmarathon/src/render/render_resource/storage_buffer.rs create mode 100644 crates/libmarathon/src/render/render_resource/texture.rs create mode 100644 crates/libmarathon/src/render/render_resource/uniform_buffer.rs create mode 100644 crates/libmarathon/src/render/renderer/graph_runner.rs create mode 100644 crates/libmarathon/src/render/renderer/mod.rs create mode 100644 crates/libmarathon/src/render/renderer/raw_vulkan_init.rs create mode 100644 crates/libmarathon/src/render/renderer/render_device.rs create mode 100644 crates/libmarathon/src/render/renderer/wgpu_wrapper.rs create mode 100644 crates/libmarathon/src/render/settings.rs create mode 100644 crates/libmarathon/src/render/skybox/mod.rs create mode 100644 crates/libmarathon/src/render/skybox/prepass.rs create mode 100644 crates/libmarathon/src/render/skybox/skybox.wgsl create mode 100644 crates/libmarathon/src/render/skybox/skybox_prepass.wgsl create mode 100644 crates/libmarathon/src/render/storage.rs create mode 100644 crates/libmarathon/src/render/sync_component.rs create mode 100644 crates/libmarathon/src/render/sync_world.rs create mode 100644 crates/libmarathon/src/render/texture/fallback_image.rs create mode 100644 crates/libmarathon/src/render/texture/gpu_image.rs create mode 100644 crates/libmarathon/src/render/texture/manual_texture_view.rs create mode 100644 crates/libmarathon/src/render/texture/mod.rs create mode 100644 crates/libmarathon/src/render/texture/texture_attachment.rs create mode 100644 crates/libmarathon/src/render/texture/texture_cache.rs create mode 100644 crates/libmarathon/src/render/tonemapping/lut_bindings.wgsl create mode 100644 crates/libmarathon/src/render/tonemapping/luts/AgX-default_contrast.ktx2 create mode 100644 crates/libmarathon/src/render/tonemapping/luts/Blender_-11_12.ktx2 create mode 100644 crates/libmarathon/src/render/tonemapping/luts/info.txt create mode 100644 crates/libmarathon/src/render/tonemapping/luts/tony_mc_mapface.ktx2 create mode 100644 crates/libmarathon/src/render/tonemapping/mod.rs create mode 100644 crates/libmarathon/src/render/tonemapping/node.rs create mode 100644 crates/libmarathon/src/render/tonemapping/tonemapping.wgsl create mode 100644 crates/libmarathon/src/render/tonemapping/tonemapping_shared.wgsl create mode 100644 crates/libmarathon/src/render/upscaling/mod.rs create mode 100644 crates/libmarathon/src/render/upscaling/node.rs create mode 100644 crates/libmarathon/src/render/view/mod.rs create mode 100644 crates/libmarathon/src/render/view/view.wgsl create mode 100644 crates/libmarathon/src/render/view/visibility/mod.rs create mode 100644 crates/libmarathon/src/render/view/visibility/range.rs create mode 100644 crates/libmarathon/src/render/view/window/mod.rs create mode 100644 crates/libmarathon/src/render/view/window/screenshot.rs create mode 100644 crates/libmarathon/src/render/view/window/screenshot.wgsl rename crates/{sync-macros => macros}/Cargo.toml (90%) create mode 100644 crates/macros/src/as_bind_group.rs create mode 100644 crates/macros/src/extract_component.rs create mode 100644 crates/macros/src/extract_resource.rs create mode 100644 crates/macros/src/lib.rs create mode 100644 crates/macros/src/specializer.rs rename crates/{sync-macros => macros}/tests/basic_macro_test.rs (100%) delete mode 100644 crates/sync-macros/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 9103a65..05e1059 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -766,7 +766,7 @@ dependencies = [ "bevy_reflect", "bytemuck", "derive_more 2.0.1", - "encase 0.11.2", + "encase", "serde", "thiserror 2.0.17", "wgpu-types", @@ -877,7 +877,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7449e5903594a00f007732ba232af0c527ad4e6e3d29bc3e195ec78dbd20c8b2" dependencies = [ "bevy_macro_utils", - "encase_derive_impl 0.11.2", + "encase_derive_impl", ] [[package]] @@ -1379,7 +1379,7 @@ dependencies = [ "bytemuck", "derive_more 2.0.1", "downcast-rs 2.0.2", - "encase 0.11.2", + "encase", "fixedbitset", "image", "indexmap", @@ -2866,18 +2866,6 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" -[[package]] -name = "encase" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0a05902cf601ed11d564128448097b98ebe3c6574bd7b6a653a3d56d54aa020" -dependencies = [ - "const_panic", - "encase_derive 0.10.0", - "glam 0.29.3", - "thiserror 1.0.69", -] - [[package]] name = "encase" version = "0.11.2" @@ -2885,38 +2873,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02ba239319a4f60905966390f5e52799d868103a533bb7e27822792332504ddd" dependencies = [ "const_panic", - "encase_derive 0.11.2", + "encase_derive", "glam 0.30.9", "thiserror 2.0.17", ] -[[package]] -name = "encase_derive" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "181d475b694e2dd56ae919ce7699d344d1fd259292d590c723a50d1189a2ea85" -dependencies = [ - "encase_derive_impl 0.10.0", -] - [[package]] name = "encase_derive" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5223d6c647f09870553224f6e37261fe5567bc5a4f4cf13ed337476e79990f2f" dependencies = [ - "encase_derive_impl 0.11.2", -] - -[[package]] -name = "encase_derive_impl" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f97b51c5cc57ef7c5f7a0c57c250251c49ee4c28f819f87ac32f4aceabc36792" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "encase_derive_impl", ] [[package]] @@ -4597,7 +4565,29 @@ version = "0.1.0" dependencies = [ "anyhow", "arboard", + "async-channel", "bevy", + "bevy_app", + "bevy_asset", + "bevy_camera", + "bevy_color", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_encase_derive", + "bevy_image", + "bevy_light", + "bevy_math", + "bevy_mesh", + "bevy_platform", + "bevy_reflect", + "bevy_shader", + "bevy_tasks", + "bevy_time", + "bevy_transform", + "bevy_utils", + "bevy_window", + "bitflags 2.10.0", "blake3", "blocking", "bytemuck", @@ -4606,16 +4596,26 @@ dependencies = [ "crdts", "criterion", "crossbeam-channel", + "derive_more 2.0.1", "dirs", + "downcast-rs 2.0.2", "egui", - "encase 0.10.0", + "encase", + "fixedbitset", "futures-lite", "glam 0.29.3", + "image", + "indexmap", "inventory", "iroh", "iroh-gossip", "itertools 0.14.0", + "macros", + "naga", + "nonmax", + "offset-allocator", "proptest", + "radsort", "rand 0.8.5", "raw-window-handle", "rkyv", @@ -4623,7 +4623,8 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "sync-macros", + "smallvec", + "static_assertions", "tempfile", "thiserror 2.0.17", "tokio", @@ -4631,6 +4632,8 @@ dependencies = [ "tracing", "tracing-oslog", "uuid", + "variadics_please", + "wgpu", "wgpu-types", "winit", ] @@ -4749,6 +4752,24 @@ dependencies = [ "libc", ] +[[package]] +name = "macros" +version = "0.1.0" +dependencies = [ + "anyhow", + "bevy", + "bevy_macro_utils", + "bytes", + "inventory", + "libmarathon", + "proc-macro2", + "quote", + "rkyv", + "serde", + "syn", + "tracing", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -7241,23 +7262,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync-macros" -version = "0.1.0" -dependencies = [ - "anyhow", - "bevy", - "bytes", - "inventory", - "libmarathon", - "proc-macro2", - "quote", - "rkyv", - "serde", - "syn", - "tracing", -] - [[package]] name = "sync_wrapper" version = "1.0.2" diff --git a/Cargo.toml b/Cargo.toml index acb9c74..c406488 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/libmarathon", "crates/sync-macros", "crates/app", "crates/xtask"] +members = ["crates/libmarathon", "crates/macros", "crates/app", "crates/xtask"] resolver = "2" [workspace.package] diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 38e860b..e77d010 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -12,9 +12,7 @@ headless = [] [dependencies] libmarathon = { path = "../libmarathon" } bevy = { version = "0.17", default-features = false, features = [ - "bevy_render", - "bevy_core_pipeline", - "bevy_pbr", + # bevy_render, bevy_core_pipeline, bevy_pbr are now vendored in libmarathon "bevy_ui", "bevy_text", "png", diff --git a/crates/libmarathon/Cargo.toml b/crates/libmarathon/Cargo.toml index bd3359b..6de0ab4 100644 --- a/crates/libmarathon/Cargo.toml +++ b/crates/libmarathon/Cargo.toml @@ -8,6 +8,47 @@ anyhow.workspace = true arboard = "3.4" bevy.workspace = true rkyv.workspace = true + +# Bevy subcrates required by vendored rendering (bevy_render, bevy_core_pipeline, bevy_pbr) +bevy_app = "0.17.2" +bevy_asset = "0.17.2" +bevy_camera = "0.17.2" +bevy_color = "0.17.2" +bevy_derive = "0.17.2" +bevy_diagnostic = "0.17.2" +bevy_ecs = "0.17.2" +bevy_encase_derive = "0.17.2" +bevy_image = "0.17.2" +bevy_light = "0.17.2" +bevy_math = "0.17.2" +bevy_mesh = "0.17.2" +bevy_platform = { version = "0.17.2", default-features = false } +bevy_reflect = "0.17.2" +macros = { path = "../macros" } +bevy_shader = "0.17.2" +bevy_tasks = "0.17.2" +bevy_time = "0.17.2" +bevy_transform = "0.17.2" +bevy_utils = "0.17.2" +bevy_window = "0.17.2" + +# Additional dependencies required by vendored rendering crates +wgpu = { version = "26", default-features = false, features = ["dx12", "metal"] } +naga = { version = "26", features = ["wgsl-in"] } +downcast-rs = { version = "2", default-features = false, features = ["std"] } +derive_more = { version = "2", default-features = false, features = ["from"] } +image = { version = "0.25.2", default-features = false } +bitflags = { version = "2.3", features = ["bytemuck"] } +fixedbitset = "0.5" +radsort = "0.1" +nonmax = "0.5" +smallvec = { version = "1", default-features = false } +indexmap = "2.0" +async-channel = "2.3" +offset-allocator = "0.2" +variadics_please = "1.1" +static_assertions = "1.1" + blake3 = "1.5" blocking = "1.6" bytemuck = { version = "1.14", features = ["derive"] } @@ -17,7 +58,7 @@ crdts.workspace = true crossbeam-channel = "0.5" dirs = "5.0" egui = { version = "0.33", default-features = false, features = ["bytemuck", "default_fonts"] } -encase = { version = "0.10", features = ["glam"] } +encase = { version = "0.11", features = ["glam"] } futures-lite = "2.0" glam = "0.29" inventory.workspace = true @@ -30,7 +71,6 @@ rusqlite = { version = "0.37.0", features = ["bundled"] } serde = { version = "1.0", features = ["derive"] } serde_json.workspace = true sha2 = "0.10" -sync-macros = { path = "../sync-macros" } thiserror = "2.0" tokio.workspace = true toml.workspace = true diff --git a/crates/libmarathon/src/lib.rs b/crates/libmarathon/src/lib.rs index 31891a1..2cdbde7 100644 --- a/crates/libmarathon/src/lib.rs +++ b/crates/libmarathon/src/lib.rs @@ -28,6 +28,7 @@ pub mod engine; pub mod networking; pub mod persistence; pub mod platform; +pub mod render; // Vendored Bevy rendering (bevy_render + bevy_core_pipeline + bevy_pbr) pub mod utils; pub mod sync; diff --git a/crates/libmarathon/src/render/alpha.rs b/crates/libmarathon/src/render/alpha.rs new file mode 100644 index 0000000..dd74881 --- /dev/null +++ b/crates/libmarathon/src/render/alpha.rs @@ -0,0 +1,62 @@ +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; + +// TODO: add discussion about performance. +/// Sets how a material's base color alpha channel is used for transparency. +#[derive(Debug, Default, Reflect, Copy, Clone, PartialEq)] +#[reflect(Default, Debug, Clone)] +pub enum AlphaMode { + /// Base color alpha values are overridden to be fully opaque (1.0). + #[default] + Opaque, + /// Reduce transparency to fully opaque or fully transparent + /// based on a threshold. + /// + /// Compares the base color alpha value to the specified threshold. + /// If the value is below the threshold, + /// considers the color to be fully transparent (alpha is set to 0.0). + /// If it is equal to or above the threshold, + /// considers the color to be fully opaque (alpha is set to 1.0). + Mask(f32), + /// The base color alpha value defines the opacity of the color. + /// Standard alpha-blending is used to blend the fragment's color + /// with the color behind it. + Blend, + /// Similar to [`AlphaMode::Blend`], however assumes RGB channel values are + /// [premultiplied](https://en.wikipedia.org/wiki/Alpha_compositing#Straight_versus_premultiplied). + /// + /// For otherwise constant RGB values, behaves more like [`AlphaMode::Blend`] for + /// alpha values closer to 1.0, and more like [`AlphaMode::Add`] for + /// alpha values closer to 0.0. + /// + /// Can be used to avoid “border” or “outline” artifacts that can occur + /// when using plain alpha-blended textures. + Premultiplied, + /// Spreads the fragment out over a hardware-dependent number of sample + /// locations proportional to the alpha value. This requires multisample + /// antialiasing; if MSAA isn't on, this is identical to + /// [`AlphaMode::Mask`] with a value of 0.5. + /// + /// Alpha to coverage provides improved performance and better visual + /// fidelity over [`AlphaMode::Blend`], as Bevy doesn't have to sort objects + /// when it's in use. It's especially useful for complex transparent objects + /// like foliage. + /// + /// [alpha to coverage]: https://en.wikipedia.org/wiki/Alpha_to_coverage + AlphaToCoverage, + /// Combines the color of the fragments with the colors behind them in an + /// additive process, (i.e. like light) producing lighter results. + /// + /// Black produces no effect. Alpha values can be used to modulate the result. + /// + /// Useful for effects like holograms, ghosts, lasers and other energy beams. + Add, + /// Combines the color of the fragments with the colors behind them in a + /// multiplicative process, (i.e. like pigments) producing darker results. + /// + /// White produces no effect. Alpha values can be used to modulate the result. + /// + /// Useful for effects like stained glass, window tint film and some colored liquids. + Multiply, +} + +impl Eq for AlphaMode {} diff --git a/crates/libmarathon/src/render/batching/gpu_preprocessing.rs b/crates/libmarathon/src/render/batching/gpu_preprocessing.rs new file mode 100644 index 0000000..a5d0b68 --- /dev/null +++ b/crates/libmarathon/src/render/batching/gpu_preprocessing.rs @@ -0,0 +1,2142 @@ +//! Batching functionality when GPU preprocessing is in use. + +use core::{any::TypeId, marker::PhantomData, mem}; + +use bevy_app::{App, Plugin}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + prelude::Entity, + query::{Has, With}, + resource::Resource, + schedule::IntoScheduleConfigs as _, + system::{Query, Res, ResMut, StaticSystemParam}, + world::{FromWorld, World}, +}; +use bevy_encase_derive::ShaderType; +use bevy_math::UVec4; +use bevy_platform::collections::{hash_map::Entry, HashMap, HashSet}; +use bevy_utils::{default, TypeIdMap}; +use bytemuck::{Pod, Zeroable}; +use encase::{internal::WriteInto, ShaderSize}; +use indexmap::IndexMap; +use nonmax::NonMaxU32; +use tracing::{error, info}; +use wgpu::{BindingResource, BufferUsages, DownlevelFlags, Features}; + +use crate::render::{ + experimental::occlusion_culling::OcclusionCulling, + render_phase::{ + BinnedPhaseItem, BinnedRenderPhaseBatch, BinnedRenderPhaseBatchSet, + BinnedRenderPhaseBatchSets, CachedRenderPipelinePhaseItem, PhaseItem, + PhaseItemBatchSetKey as _, PhaseItemExtraIndex, RenderBin, SortedPhaseItem, + SortedRenderPhase, UnbatchableBinnedEntityIndices, ViewBinnedRenderPhases, + ViewSortedRenderPhases, + }, + render_resource::{Buffer, GpuArrayBufferable, RawBufferVec, UninitBufferVec}, + renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue, WgpuWrapper}, + sync_world::MainEntity, + view::{ExtractedView, NoIndirectDrawing, RetainedViewEntity}, + Render, RenderApp, RenderDebugFlags, RenderSystems, +}; + +use super::{BatchMeta, GetBatchData, GetFullBatchData}; + +#[derive(Default)] +pub struct BatchingPlugin { + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl Plugin for BatchingPlugin { + fn build(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .insert_resource(IndirectParametersBuffers::new( + self.debug_flags + .contains(RenderDebugFlags::ALLOW_COPIES_FROM_INDIRECT_PARAMETERS), + )) + .add_systems( + Render, + write_indirect_parameters_buffers.in_set(RenderSystems::PrepareResourcesFlush), + ) + .add_systems( + Render, + clear_indirect_parameters_buffers.in_set(RenderSystems::ManageViews), + ); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.init_resource::(); + } +} + +/// Records whether GPU preprocessing and/or GPU culling are supported on the +/// device. +/// +/// No GPU preprocessing is supported on WebGL because of the lack of compute +/// shader support. GPU preprocessing is supported on DirectX 12, but due to [a +/// `wgpu` limitation] GPU culling is not. +/// +/// [a `wgpu` limitation]: https://github.com/gfx-rs/wgpu/issues/2471 +#[derive(Clone, Copy, PartialEq, Resource)] +pub struct GpuPreprocessingSupport { + /// The maximum amount of GPU preprocessing available on this platform. + pub max_supported_mode: GpuPreprocessingMode, +} + +impl GpuPreprocessingSupport { + /// Returns true if this GPU preprocessing support level isn't `None`. + #[inline] + pub fn is_available(&self) -> bool { + self.max_supported_mode != GpuPreprocessingMode::None + } + + /// Returns the given GPU preprocessing mode, capped to the current + /// preprocessing mode. + pub fn min(&self, mode: GpuPreprocessingMode) -> GpuPreprocessingMode { + match (self.max_supported_mode, mode) { + (GpuPreprocessingMode::None, _) | (_, GpuPreprocessingMode::None) => { + GpuPreprocessingMode::None + } + (mode, GpuPreprocessingMode::Culling) | (GpuPreprocessingMode::Culling, mode) => mode, + (GpuPreprocessingMode::PreprocessingOnly, GpuPreprocessingMode::PreprocessingOnly) => { + GpuPreprocessingMode::PreprocessingOnly + } + } + } + + /// Returns true if GPU culling is supported on this platform. + pub fn is_culling_supported(&self) -> bool { + self.max_supported_mode == GpuPreprocessingMode::Culling + } +} + +/// The amount of GPU preprocessing (compute and indirect draw) that we do. +#[derive(Clone, Copy, PartialEq)] +pub enum GpuPreprocessingMode { + /// No GPU preprocessing is in use at all. + /// + /// This is used when GPU compute isn't available. + None, + + /// GPU preprocessing is in use, but GPU culling isn't. + /// + /// This is used when the [`NoIndirectDrawing`] component is present on the + /// camera. + PreprocessingOnly, + + /// Both GPU preprocessing and GPU culling are in use. + /// + /// This is used by default. + Culling, +} + +/// The GPU buffers holding the data needed to render batches. +/// +/// For example, in the 3D PBR pipeline this holds `MeshUniform`s, which are the +/// `BD` type parameter in that mode. +/// +/// We have a separate *buffer data input* type (`BDI`) here, which a compute +/// shader is expected to expand to the full buffer data (`BD`) type. GPU +/// uniform building is generally faster and uses less system RAM to VRAM bus +/// bandwidth, but only implemented for some pipelines (for example, not in the +/// 2D pipeline at present) and only when compute shader is available. +#[derive(Resource)] +pub struct BatchedInstanceBuffers +where + BD: GpuArrayBufferable + Sync + Send + 'static, + BDI: Pod + Default, +{ + /// The uniform data inputs for the current frame. + /// + /// These are uploaded during the extraction phase. + pub current_input_buffer: InstanceInputUniformBuffer, + + /// The uniform data inputs for the previous frame. + /// + /// The indices don't generally line up between `current_input_buffer` + /// and `previous_input_buffer`, because, among other reasons, entities + /// can spawn or despawn between frames. Instead, each current buffer + /// data input uniform is expected to contain the index of the + /// corresponding buffer data input uniform in this list. + pub previous_input_buffer: InstanceInputUniformBuffer, + + /// The data needed to render buffers for each phase. + /// + /// The keys of this map are the type IDs of each phase: e.g. `Opaque3d`, + /// `AlphaMask3d`, etc. + pub phase_instance_buffers: TypeIdMap>, +} + +impl Default for BatchedInstanceBuffers +where + BD: GpuArrayBufferable + Sync + Send + 'static, + BDI: Pod + Sync + Send + Default + 'static, +{ + fn default() -> Self { + BatchedInstanceBuffers { + current_input_buffer: InstanceInputUniformBuffer::new(), + previous_input_buffer: InstanceInputUniformBuffer::new(), + phase_instance_buffers: HashMap::default(), + } + } +} + +/// The GPU buffers holding the data needed to render batches for a single +/// phase. +/// +/// These are split out per phase so that we can run the phases in parallel. +/// This is the version of the structure that has a type parameter, which +/// enables Bevy's scheduler to run the batching operations for the different +/// phases in parallel. +/// +/// See the documentation for [`BatchedInstanceBuffers`] for more information. +#[derive(Resource)] +pub struct PhaseBatchedInstanceBuffers +where + PI: PhaseItem, + BD: GpuArrayBufferable + Sync + Send + 'static, +{ + /// The buffers for this phase. + pub buffers: UntypedPhaseBatchedInstanceBuffers, + phantom: PhantomData, +} + +impl Default for PhaseBatchedInstanceBuffers +where + PI: PhaseItem, + BD: GpuArrayBufferable + Sync + Send + 'static, +{ + fn default() -> Self { + PhaseBatchedInstanceBuffers { + buffers: UntypedPhaseBatchedInstanceBuffers::default(), + phantom: PhantomData, + } + } +} + +/// The GPU buffers holding the data needed to render batches for a single +/// phase, without a type parameter for that phase. +/// +/// Since this structure doesn't have a type parameter, it can be placed in +/// [`BatchedInstanceBuffers::phase_instance_buffers`]. +pub struct UntypedPhaseBatchedInstanceBuffers +where + BD: GpuArrayBufferable + Sync + Send + 'static, +{ + /// A storage area for the buffer data that the GPU compute shader is + /// expected to write to. + /// + /// There will be one entry for each index. + pub data_buffer: UninitBufferVec, + + /// The index of the buffer data in the current input buffer that + /// corresponds to each instance. + /// + /// This is keyed off each view. Each view has a separate buffer. + pub work_item_buffers: HashMap, + + /// A buffer that holds the number of indexed meshes that weren't visible in + /// the previous frame, when GPU occlusion culling is in use. + /// + /// There's one set of [`LatePreprocessWorkItemIndirectParameters`] per + /// view. Bevy uses this value to determine how many threads to dispatch to + /// check meshes that weren't visible next frame to see if they became newly + /// visible this frame. + pub late_indexed_indirect_parameters_buffer: + RawBufferVec, + + /// A buffer that holds the number of non-indexed meshes that weren't + /// visible in the previous frame, when GPU occlusion culling is in use. + /// + /// There's one set of [`LatePreprocessWorkItemIndirectParameters`] per + /// view. Bevy uses this value to determine how many threads to dispatch to + /// check meshes that weren't visible next frame to see if they became newly + /// visible this frame. + pub late_non_indexed_indirect_parameters_buffer: + RawBufferVec, +} + +/// Holds the GPU buffer of instance input data, which is the data about each +/// mesh instance that the CPU provides. +/// +/// `BDI` is the *buffer data input* type, which the GPU mesh preprocessing +/// shader is expected to expand to the full *buffer data* type. +pub struct InstanceInputUniformBuffer +where + BDI: Pod + Default, +{ + /// The buffer containing the data that will be uploaded to the GPU. + buffer: RawBufferVec, + + /// Indices of slots that are free within the buffer. + /// + /// When adding data, we preferentially overwrite these slots first before + /// growing the buffer itself. + free_uniform_indices: Vec, +} + +impl InstanceInputUniformBuffer +where + BDI: Pod + Default, +{ + /// Creates a new, empty buffer. + pub fn new() -> InstanceInputUniformBuffer { + InstanceInputUniformBuffer { + buffer: RawBufferVec::new(BufferUsages::STORAGE), + free_uniform_indices: vec![], + } + } + + /// Clears the buffer and entity list out. + pub fn clear(&mut self) { + self.buffer.clear(); + self.free_uniform_indices.clear(); + } + + /// Returns the [`RawBufferVec`] corresponding to this input uniform buffer. + #[inline] + pub fn buffer(&self) -> &RawBufferVec { + &self.buffer + } + + /// Adds a new piece of buffered data to the uniform buffer and returns its + /// index. + pub fn add(&mut self, element: BDI) -> u32 { + match self.free_uniform_indices.pop() { + Some(uniform_index) => { + self.buffer.values_mut()[uniform_index as usize] = element; + uniform_index + } + None => self.buffer.push(element) as u32, + } + } + + /// Removes a piece of buffered data from the uniform buffer. + /// + /// This simply marks the data as free. + pub fn remove(&mut self, uniform_index: u32) { + self.free_uniform_indices.push(uniform_index); + } + + /// Returns the piece of buffered data at the given index. + /// + /// Returns [`None`] if the index is out of bounds or the data is removed. + pub fn get(&self, uniform_index: u32) -> Option { + if (uniform_index as usize) >= self.buffer.len() + || self.free_uniform_indices.contains(&uniform_index) + { + None + } else { + Some(self.get_unchecked(uniform_index)) + } + } + + /// Returns the piece of buffered data at the given index. + /// Can return data that has previously been removed. + /// + /// # Panics + /// if `uniform_index` is not in bounds of [`Self::buffer`]. + pub fn get_unchecked(&self, uniform_index: u32) -> BDI { + self.buffer.values()[uniform_index as usize] + } + + /// Stores a piece of buffered data at the given index. + /// + /// # Panics + /// if `uniform_index` is not in bounds of [`Self::buffer`]. + pub fn set(&mut self, uniform_index: u32, element: BDI) { + self.buffer.values_mut()[uniform_index as usize] = element; + } + + // Ensures that the buffers are nonempty, which the GPU requires before an + // upload can take place. + pub fn ensure_nonempty(&mut self) { + if self.buffer.is_empty() { + self.buffer.push(default()); + } + } + + /// Returns the number of instances in this buffer. + pub fn len(&self) -> usize { + self.buffer.len() + } + + /// Returns true if this buffer has no instances or false if it contains any + /// instances. + pub fn is_empty(&self) -> bool { + self.buffer.is_empty() + } + + /// Consumes this [`InstanceInputUniformBuffer`] and returns the raw buffer + /// ready to be uploaded to the GPU. + pub fn into_buffer(self) -> RawBufferVec { + self.buffer + } +} + +impl Default for InstanceInputUniformBuffer +where + BDI: Pod + Default, +{ + fn default() -> Self { + Self::new() + } +} + +/// The buffer of GPU preprocessing work items for a single view. +#[cfg_attr( + not(target_arch = "wasm32"), + expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" + ) +)] +pub enum PreprocessWorkItemBuffers { + /// The work items we use if we aren't using indirect drawing. + /// + /// Because we don't have to separate indexed from non-indexed meshes in + /// direct mode, we only have a single buffer here. + Direct(RawBufferVec), + + /// The buffer of work items we use if we are using indirect drawing. + /// + /// We need to separate out indexed meshes from non-indexed meshes in this + /// case because the indirect parameters for these two types of meshes have + /// different sizes. + Indirect { + /// The buffer of work items corresponding to indexed meshes. + indexed: RawBufferVec, + /// The buffer of work items corresponding to non-indexed meshes. + non_indexed: RawBufferVec, + /// The work item buffers we use when GPU occlusion culling is in use. + gpu_occlusion_culling: Option, + }, +} + +/// The work item buffers we use when GPU occlusion culling is in use. +pub struct GpuOcclusionCullingWorkItemBuffers { + /// The buffer of work items corresponding to indexed meshes. + pub late_indexed: UninitBufferVec, + /// The buffer of work items corresponding to non-indexed meshes. + pub late_non_indexed: UninitBufferVec, + /// The offset into the + /// [`UntypedPhaseBatchedInstanceBuffers::late_indexed_indirect_parameters_buffer`] + /// where this view's indirect dispatch counts for indexed meshes live. + pub late_indirect_parameters_indexed_offset: u32, + /// The offset into the + /// [`UntypedPhaseBatchedInstanceBuffers::late_non_indexed_indirect_parameters_buffer`] + /// where this view's indirect dispatch counts for non-indexed meshes live. + pub late_indirect_parameters_non_indexed_offset: u32, +} + +/// A GPU-side data structure that stores the number of workgroups to dispatch +/// for the second phase of GPU occlusion culling. +/// +/// The late mesh preprocessing phase checks meshes that weren't visible frame +/// to determine if they're potentially visible this frame. +#[derive(Clone, Copy, ShaderType, Pod, Zeroable)] +#[repr(C)] +pub struct LatePreprocessWorkItemIndirectParameters { + /// The number of workgroups to dispatch. + /// + /// This will be equal to `work_item_count / 64`, rounded *up*. + dispatch_x: u32, + /// The number of workgroups along the abstract Y axis to dispatch: always + /// 1. + dispatch_y: u32, + /// The number of workgroups along the abstract Z axis to dispatch: always + /// 1. + dispatch_z: u32, + /// The actual number of work items. + /// + /// The GPU indirect dispatch doesn't read this, but it's used internally to + /// determine the actual number of work items that exist in the late + /// preprocessing work item buffer. + work_item_count: u32, + /// Padding to 64-byte boundaries for some hardware. + pad: UVec4, +} + +impl Default for LatePreprocessWorkItemIndirectParameters { + fn default() -> LatePreprocessWorkItemIndirectParameters { + LatePreprocessWorkItemIndirectParameters { + dispatch_x: 0, + dispatch_y: 1, + dispatch_z: 1, + work_item_count: 0, + pad: default(), + } + } +} + +/// Returns the set of work item buffers for the given view, first creating it +/// if necessary. +/// +/// Bevy uses work item buffers to tell the mesh preprocessing compute shader +/// which meshes are to be drawn. +/// +/// You may need to call this function if you're implementing your own custom +/// render phases. See the `specialized_mesh_pipeline` example. +pub fn get_or_create_work_item_buffer<'a, I>( + work_item_buffers: &'a mut HashMap, + view: RetainedViewEntity, + no_indirect_drawing: bool, + enable_gpu_occlusion_culling: bool, +) -> &'a mut PreprocessWorkItemBuffers +where + I: 'static, +{ + let preprocess_work_item_buffers = match work_item_buffers.entry(view) { + Entry::Occupied(occupied_entry) => occupied_entry.into_mut(), + Entry::Vacant(vacant_entry) => { + if no_indirect_drawing { + vacant_entry.insert(PreprocessWorkItemBuffers::Direct(RawBufferVec::new( + BufferUsages::STORAGE, + ))) + } else { + vacant_entry.insert(PreprocessWorkItemBuffers::Indirect { + indexed: RawBufferVec::new(BufferUsages::STORAGE), + non_indexed: RawBufferVec::new(BufferUsages::STORAGE), + // We fill this in below if `enable_gpu_occlusion_culling` + // is set. + gpu_occlusion_culling: None, + }) + } + } + }; + + // Initialize the GPU occlusion culling buffers if necessary. + if let PreprocessWorkItemBuffers::Indirect { + ref mut gpu_occlusion_culling, + .. + } = *preprocess_work_item_buffers + { + match ( + enable_gpu_occlusion_culling, + gpu_occlusion_culling.is_some(), + ) { + (false, false) | (true, true) => {} + (false, true) => { + *gpu_occlusion_culling = None; + } + (true, false) => { + *gpu_occlusion_culling = Some(GpuOcclusionCullingWorkItemBuffers { + late_indexed: UninitBufferVec::new(BufferUsages::STORAGE), + late_non_indexed: UninitBufferVec::new(BufferUsages::STORAGE), + late_indirect_parameters_indexed_offset: 0, + late_indirect_parameters_non_indexed_offset: 0, + }); + } + } + } + + preprocess_work_item_buffers +} + +/// Initializes work item buffers for a phase in preparation for a new frame. +pub fn init_work_item_buffers( + work_item_buffers: &mut PreprocessWorkItemBuffers, + late_indexed_indirect_parameters_buffer: &'_ mut RawBufferVec< + LatePreprocessWorkItemIndirectParameters, + >, + late_non_indexed_indirect_parameters_buffer: &'_ mut RawBufferVec< + LatePreprocessWorkItemIndirectParameters, + >, +) { + // Add the offsets for indirect parameters that the late phase of mesh + // preprocessing writes to. + if let PreprocessWorkItemBuffers::Indirect { + gpu_occlusion_culling: + Some(GpuOcclusionCullingWorkItemBuffers { + ref mut late_indirect_parameters_indexed_offset, + ref mut late_indirect_parameters_non_indexed_offset, + .. + }), + .. + } = *work_item_buffers + { + *late_indirect_parameters_indexed_offset = late_indexed_indirect_parameters_buffer + .push(LatePreprocessWorkItemIndirectParameters::default()) + as u32; + *late_indirect_parameters_non_indexed_offset = late_non_indexed_indirect_parameters_buffer + .push(LatePreprocessWorkItemIndirectParameters::default()) + as u32; + } +} + +impl PreprocessWorkItemBuffers { + /// Adds a new work item to the appropriate buffer. + /// + /// `indexed` specifies whether the work item corresponds to an indexed + /// mesh. + pub fn push(&mut self, indexed: bool, preprocess_work_item: PreprocessWorkItem) { + match *self { + PreprocessWorkItemBuffers::Direct(ref mut buffer) => { + buffer.push(preprocess_work_item); + } + PreprocessWorkItemBuffers::Indirect { + indexed: ref mut indexed_buffer, + non_indexed: ref mut non_indexed_buffer, + ref mut gpu_occlusion_culling, + } => { + if indexed { + indexed_buffer.push(preprocess_work_item); + } else { + non_indexed_buffer.push(preprocess_work_item); + } + + if let Some(ref mut gpu_occlusion_culling) = *gpu_occlusion_culling { + if indexed { + gpu_occlusion_culling.late_indexed.add(); + } else { + gpu_occlusion_culling.late_non_indexed.add(); + } + } + } + } + } + + /// Clears out the GPU work item buffers in preparation for a new frame. + pub fn clear(&mut self) { + match *self { + PreprocessWorkItemBuffers::Direct(ref mut buffer) => { + buffer.clear(); + } + PreprocessWorkItemBuffers::Indirect { + indexed: ref mut indexed_buffer, + non_indexed: ref mut non_indexed_buffer, + ref mut gpu_occlusion_culling, + } => { + indexed_buffer.clear(); + non_indexed_buffer.clear(); + + if let Some(ref mut gpu_occlusion_culling) = *gpu_occlusion_culling { + gpu_occlusion_culling.late_indexed.clear(); + gpu_occlusion_culling.late_non_indexed.clear(); + gpu_occlusion_culling.late_indirect_parameters_indexed_offset = 0; + gpu_occlusion_culling.late_indirect_parameters_non_indexed_offset = 0; + } + } + } + } +} + +/// One invocation of the preprocessing shader: i.e. one mesh instance in a +/// view. +#[derive(Clone, Copy, Default, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct PreprocessWorkItem { + /// The index of the batch input data in the input buffer that the shader + /// reads from. + pub input_index: u32, + + /// In direct mode, the index of the mesh uniform; in indirect mode, the + /// index of the [`IndirectParametersGpuMetadata`]. + /// + /// In indirect mode, this is the index of the + /// [`IndirectParametersGpuMetadata`] in the + /// `IndirectParametersBuffers::indexed_metadata` or + /// `IndirectParametersBuffers::non_indexed_metadata`. + pub output_or_indirect_parameters_index: u32, +} + +/// The `wgpu` indirect parameters structure that specifies a GPU draw command. +/// +/// This is the variant for indexed meshes. We generate the instances of this +/// structure in the `build_indirect_params.wgsl` compute shader. +#[derive(Clone, Copy, Debug, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct IndirectParametersIndexed { + /// The number of indices that this mesh has. + pub index_count: u32, + /// The number of instances we are to draw. + pub instance_count: u32, + /// The offset of the first index for this mesh in the index buffer slab. + pub first_index: u32, + /// The offset of the first vertex for this mesh in the vertex buffer slab. + pub base_vertex: u32, + /// The index of the first mesh instance in the `MeshUniform` buffer. + pub first_instance: u32, +} + +/// The `wgpu` indirect parameters structure that specifies a GPU draw command. +/// +/// This is the variant for non-indexed meshes. We generate the instances of +/// this structure in the `build_indirect_params.wgsl` compute shader. +#[derive(Clone, Copy, Debug, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct IndirectParametersNonIndexed { + /// The number of vertices that this mesh has. + pub vertex_count: u32, + /// The number of instances we are to draw. + pub instance_count: u32, + /// The offset of the first vertex for this mesh in the vertex buffer slab. + pub base_vertex: u32, + /// The index of the first mesh instance in the `Mesh` buffer. + pub first_instance: u32, +} + +/// A structure, initialized on CPU and read on GPU, that contains metadata +/// about each batch. +/// +/// Each batch will have one instance of this structure. +#[derive(Clone, Copy, Default, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct IndirectParametersCpuMetadata { + /// The index of the first instance of this mesh in the array of + /// `MeshUniform`s. + /// + /// Note that this is the *first* output index in this batch. Since each + /// instance of this structure refers to arbitrarily many instances, the + /// `MeshUniform`s corresponding to this batch span the indices + /// `base_output_index..(base_output_index + instance_count)`. + pub base_output_index: u32, + + /// The index of the batch set that this batch belongs to in the + /// [`IndirectBatchSet`] buffer. + /// + /// A *batch set* is a set of meshes that may be multi-drawn together. + /// Multiple batches (and therefore multiple instances of + /// [`IndirectParametersGpuMetadata`] structures) can be part of the same + /// batch set. + pub batch_set_index: u32, +} + +/// A structure, written and read GPU, that records how many instances of each +/// mesh are actually to be drawn. +/// +/// The GPU mesh preprocessing shader increments the +/// [`Self::early_instance_count`] and [`Self::late_instance_count`] as it +/// determines that meshes are visible. The indirect parameter building shader +/// reads this metadata in order to construct the indirect draw parameters. +/// +/// Each batch will have one instance of this structure. +#[derive(Clone, Copy, Default, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct IndirectParametersGpuMetadata { + /// The index of the first mesh in this batch in the array of + /// `MeshInputUniform`s. + pub mesh_index: u32, + + /// The number of instances that were judged visible last frame. + /// + /// The CPU sets this value to 0, and the GPU mesh preprocessing shader + /// increments it as it culls mesh instances. + pub early_instance_count: u32, + + /// The number of instances that have been judged potentially visible this + /// frame that weren't in the last frame's potentially visible set. + /// + /// The CPU sets this value to 0, and the GPU mesh preprocessing shader + /// increments it as it culls mesh instances. + pub late_instance_count: u32, +} + +/// A structure, shared between CPU and GPU, that holds the number of on-GPU +/// indirect draw commands for each *batch set*. +/// +/// A *batch set* is a set of meshes that may be multi-drawn together. +/// +/// If the current hardware and driver support `multi_draw_indirect_count`, the +/// indirect parameters building shader increments +/// [`Self::indirect_parameters_count`] as it generates indirect parameters. The +/// `multi_draw_indirect_count` command reads +/// [`Self::indirect_parameters_count`] in order to determine how many commands +/// belong to each batch set. +#[derive(Clone, Copy, Default, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct IndirectBatchSet { + /// The number of indirect parameter commands (i.e. batches) in this batch + /// set. + /// + /// The CPU sets this value to 0 before uploading this structure to GPU. The + /// indirect parameters building shader increments this value as it creates + /// indirect parameters. Then the `multi_draw_indirect_count` command reads + /// this value in order to determine how many indirect draw commands to + /// process. + pub indirect_parameters_count: u32, + + /// The offset within the `IndirectParametersBuffers::indexed_data` or + /// `IndirectParametersBuffers::non_indexed_data` of the first indirect draw + /// command for this batch set. + /// + /// The CPU fills out this value. + pub indirect_parameters_base: u32, +} + +/// The buffers containing all the information that indirect draw commands +/// (`multi_draw_indirect`, `multi_draw_indirect_count`) use to draw the scene. +/// +/// In addition to the indirect draw buffers themselves, this structure contains +/// the buffers that store [`IndirectParametersGpuMetadata`], which are the +/// structures that culling writes to so that the indirect parameter building +/// pass can determine how many meshes are actually to be drawn. +/// +/// These buffers will remain empty if indirect drawing isn't in use. +#[derive(Resource, Deref, DerefMut)] +pub struct IndirectParametersBuffers { + /// A mapping from a phase type ID to the indirect parameters buffers for + /// that phase. + /// + /// Examples of phase type IDs are `Opaque3d` and `AlphaMask3d`. + #[deref] + pub buffers: TypeIdMap, + /// If true, this sets the `COPY_SRC` flag on indirect draw parameters so + /// that they can be read back to CPU. + /// + /// This is a debugging feature that may reduce performance. It primarily + /// exists for the `occlusion_culling` example. + pub allow_copies_from_indirect_parameter_buffers: bool, +} + +impl IndirectParametersBuffers { + /// Initializes a new [`IndirectParametersBuffers`] resource. + pub fn new(allow_copies_from_indirect_parameter_buffers: bool) -> IndirectParametersBuffers { + IndirectParametersBuffers { + buffers: TypeIdMap::default(), + allow_copies_from_indirect_parameter_buffers, + } + } +} + +/// The buffers containing all the information that indirect draw commands use +/// to draw the scene, for a single phase. +/// +/// This is the version of the structure that has a type parameter, so that the +/// batching for different phases can run in parallel. +/// +/// See the [`IndirectParametersBuffers`] documentation for more information. +#[derive(Resource)] +pub struct PhaseIndirectParametersBuffers +where + PI: PhaseItem, +{ + /// The indirect draw buffers for the phase. + pub buffers: UntypedPhaseIndirectParametersBuffers, + phantom: PhantomData, +} + +impl PhaseIndirectParametersBuffers +where + PI: PhaseItem, +{ + pub fn new(allow_copies_from_indirect_parameter_buffers: bool) -> Self { + PhaseIndirectParametersBuffers { + buffers: UntypedPhaseIndirectParametersBuffers::new( + allow_copies_from_indirect_parameter_buffers, + ), + phantom: PhantomData, + } + } +} + +/// The buffers containing all the information that indirect draw commands use +/// to draw the scene, for a single phase. +/// +/// This is the version of the structure that doesn't have a type parameter, so +/// that it can be inserted into [`IndirectParametersBuffers::buffers`] +/// +/// See the [`IndirectParametersBuffers`] documentation for more information. +pub struct UntypedPhaseIndirectParametersBuffers { + /// Information that indirect draw commands use to draw indexed meshes in + /// the scene. + pub indexed: MeshClassIndirectParametersBuffers, + /// Information that indirect draw commands use to draw non-indexed meshes + /// in the scene. + pub non_indexed: MeshClassIndirectParametersBuffers, +} + +impl UntypedPhaseIndirectParametersBuffers { + /// Creates the indirect parameters buffers. + pub fn new( + allow_copies_from_indirect_parameter_buffers: bool, + ) -> UntypedPhaseIndirectParametersBuffers { + let mut indirect_parameter_buffer_usages = BufferUsages::STORAGE | BufferUsages::INDIRECT; + if allow_copies_from_indirect_parameter_buffers { + indirect_parameter_buffer_usages |= BufferUsages::COPY_SRC; + } + + UntypedPhaseIndirectParametersBuffers { + non_indexed: MeshClassIndirectParametersBuffers::new( + allow_copies_from_indirect_parameter_buffers, + ), + indexed: MeshClassIndirectParametersBuffers::new( + allow_copies_from_indirect_parameter_buffers, + ), + } + } + + /// Reserves space for `count` new batches. + /// + /// The `indexed` parameter specifies whether the meshes that these batches + /// correspond to are indexed or not. + pub fn allocate(&mut self, indexed: bool, count: u32) -> u32 { + if indexed { + self.indexed.allocate(count) + } else { + self.non_indexed.allocate(count) + } + } + + /// Returns the number of batches currently allocated. + /// + /// The `indexed` parameter specifies whether the meshes that these batches + /// correspond to are indexed or not. + fn batch_count(&self, indexed: bool) -> usize { + if indexed { + self.indexed.batch_count() + } else { + self.non_indexed.batch_count() + } + } + + /// Returns the number of batch sets currently allocated. + /// + /// The `indexed` parameter specifies whether the meshes that these batch + /// sets correspond to are indexed or not. + pub fn batch_set_count(&self, indexed: bool) -> usize { + if indexed { + self.indexed.batch_sets.len() + } else { + self.non_indexed.batch_sets.len() + } + } + + /// Adds a new batch set to `Self::indexed_batch_sets` or + /// `Self::non_indexed_batch_sets` as appropriate. + /// + /// `indexed` specifies whether the meshes that these batch sets correspond + /// to are indexed or not. `indirect_parameters_base` specifies the offset + /// within `Self::indexed_data` or `Self::non_indexed_data` of the first + /// batch in this batch set. + #[inline] + pub fn add_batch_set(&mut self, indexed: bool, indirect_parameters_base: u32) { + if indexed { + self.indexed.batch_sets.push(IndirectBatchSet { + indirect_parameters_base, + indirect_parameters_count: 0, + }); + } else { + self.non_indexed.batch_sets.push(IndirectBatchSet { + indirect_parameters_base, + indirect_parameters_count: 0, + }); + } + } + + /// Returns the index that a newly-added batch set will have. + /// + /// The `indexed` parameter specifies whether the meshes in such a batch set + /// are indexed or not. + pub fn get_next_batch_set_index(&self, indexed: bool) -> Option { + NonMaxU32::new(self.batch_set_count(indexed) as u32) + } + + /// Clears out the buffers in preparation for a new frame. + pub fn clear(&mut self) { + self.indexed.clear(); + self.non_indexed.clear(); + } +} + +/// The buffers containing all the information that indirect draw commands use +/// to draw the scene, for a single mesh class (indexed or non-indexed), for a +/// single phase. +pub struct MeshClassIndirectParametersBuffers +where + IP: Clone + ShaderSize + WriteInto, +{ + /// The GPU buffer that stores the indirect draw parameters for the meshes. + /// + /// The indirect parameters building shader writes to this buffer, while the + /// `multi_draw_indirect` or `multi_draw_indirect_count` commands read from + /// it to perform the draws. + data: UninitBufferVec, + + /// The GPU buffer that holds the data used to construct indirect draw + /// parameters for meshes. + /// + /// The GPU mesh preprocessing shader writes to this buffer, and the + /// indirect parameters building shader reads this buffer to construct the + /// indirect draw parameters. + cpu_metadata: RawBufferVec, + + /// The GPU buffer that holds data built by the GPU used to construct + /// indirect draw parameters for meshes. + /// + /// The GPU mesh preprocessing shader writes to this buffer, and the + /// indirect parameters building shader reads this buffer to construct the + /// indirect draw parameters. + gpu_metadata: UninitBufferVec, + + /// The GPU buffer that holds the number of indirect draw commands for each + /// phase of each view, for meshes. + /// + /// The indirect parameters building shader writes to this buffer, and the + /// `multi_draw_indirect_count` command reads from it in order to know how + /// many indirect draw commands to process. + batch_sets: RawBufferVec, +} + +impl MeshClassIndirectParametersBuffers +where + IP: Clone + ShaderSize + WriteInto, +{ + fn new( + allow_copies_from_indirect_parameter_buffers: bool, + ) -> MeshClassIndirectParametersBuffers { + let mut indirect_parameter_buffer_usages = BufferUsages::STORAGE | BufferUsages::INDIRECT; + if allow_copies_from_indirect_parameter_buffers { + indirect_parameter_buffer_usages |= BufferUsages::COPY_SRC; + } + + MeshClassIndirectParametersBuffers { + data: UninitBufferVec::new(indirect_parameter_buffer_usages), + cpu_metadata: RawBufferVec::new(BufferUsages::STORAGE), + gpu_metadata: UninitBufferVec::new(BufferUsages::STORAGE), + batch_sets: RawBufferVec::new(indirect_parameter_buffer_usages), + } + } + + /// Returns the GPU buffer that stores the indirect draw parameters for + /// indexed meshes. + /// + /// The indirect parameters building shader writes to this buffer, while the + /// `multi_draw_indirect` or `multi_draw_indirect_count` commands read from + /// it to perform the draws. + #[inline] + pub fn data_buffer(&self) -> Option<&Buffer> { + self.data.buffer() + } + + /// Returns the GPU buffer that holds the CPU-constructed data used to + /// construct indirect draw parameters for meshes. + /// + /// The CPU writes to this buffer, and the indirect parameters building + /// shader reads this buffer to construct the indirect draw parameters. + #[inline] + pub fn cpu_metadata_buffer(&self) -> Option<&Buffer> { + self.cpu_metadata.buffer() + } + + /// Returns the GPU buffer that holds the GPU-constructed data used to + /// construct indirect draw parameters for meshes. + /// + /// The GPU mesh preprocessing shader writes to this buffer, and the + /// indirect parameters building shader reads this buffer to construct the + /// indirect draw parameters. + #[inline] + pub fn gpu_metadata_buffer(&self) -> Option<&Buffer> { + self.gpu_metadata.buffer() + } + + /// Returns the GPU buffer that holds the number of indirect draw commands + /// for each phase of each view. + /// + /// The indirect parameters building shader writes to this buffer, and the + /// `multi_draw_indirect_count` command reads from it in order to know how + /// many indirect draw commands to process. + #[inline] + pub fn batch_sets_buffer(&self) -> Option<&Buffer> { + self.batch_sets.buffer() + } + + /// Reserves space for `count` new batches. + /// + /// This allocates in the [`Self::cpu_metadata`], [`Self::gpu_metadata`], + /// and [`Self::data`] buffers. + fn allocate(&mut self, count: u32) -> u32 { + let length = self.data.len(); + self.cpu_metadata.reserve_internal(count as usize); + self.gpu_metadata.add_multiple(count as usize); + for _ in 0..count { + self.data.add(); + self.cpu_metadata + .push(IndirectParametersCpuMetadata::default()); + } + length as u32 + } + + /// Sets the [`IndirectParametersCpuMetadata`] for the mesh at the given + /// index. + pub fn set(&mut self, index: u32, value: IndirectParametersCpuMetadata) { + self.cpu_metadata.set(index, value); + } + + /// Returns the number of batches corresponding to meshes that are currently + /// allocated. + #[inline] + pub fn batch_count(&self) -> usize { + self.data.len() + } + + /// Clears out all the buffers in preparation for a new frame. + pub fn clear(&mut self) { + self.data.clear(); + self.cpu_metadata.clear(); + self.gpu_metadata.clear(); + self.batch_sets.clear(); + } +} + +impl Default for IndirectParametersBuffers { + fn default() -> Self { + // By default, we don't allow GPU indirect parameter mapping, since + // that's a debugging option. + Self::new(false) + } +} + +impl FromWorld for GpuPreprocessingSupport { + fn from_world(world: &mut World) -> Self { + let adapter = world.resource::(); + let device = world.resource::(); + + // Filter Android drivers that are incompatible with GPU preprocessing: + // - We filter out Adreno 730 and earlier GPUs (except 720, as it's newer + // than 730). + // - We filter out Mali GPUs with driver versions lower than 48. + fn is_non_supported_android_device(adapter_info: &RenderAdapterInfo) -> bool { + crate::render::get_adreno_model(adapter_info).is_some_and(|model| model != 720 && model <= 730) + || crate::render::get_mali_driver_version(adapter_info).is_some_and(|version| version < 48) + } + + let culling_feature_support = device.features().contains( + Features::INDIRECT_FIRST_INSTANCE + | Features::MULTI_DRAW_INDIRECT + | Features::PUSH_CONSTANTS, + ); + // Depth downsampling for occlusion culling requires 12 textures + let limit_support = device.limits().max_storage_textures_per_shader_stage >= 12 && + // Even if the adapter supports compute, we might be simulating a lack of + // compute via device limits (see `WgpuSettingsPriority::WebGL2` and + // `wgpu::Limits::downlevel_webgl2_defaults()`). This will have set all the + // `max_compute_*` limits to zero, so we arbitrarily pick one as a canary. + device.limits().max_compute_workgroup_storage_size != 0; + + let downlevel_support = adapter + .get_downlevel_capabilities() + .flags + .contains(DownlevelFlags::COMPUTE_SHADERS); + + let adapter_info = RenderAdapterInfo(WgpuWrapper::new(adapter.get_info())); + + let max_supported_mode = if device.limits().max_compute_workgroup_size_x == 0 + || is_non_supported_android_device(&adapter_info) + || adapter_info.backend == wgpu::Backend::Gl + { + info!( + "GPU preprocessing is not supported on this device. \ + Falling back to CPU preprocessing.", + ); + GpuPreprocessingMode::None + } else if !(culling_feature_support && limit_support && downlevel_support) { + info!("Some GPU preprocessing are limited on this device."); + GpuPreprocessingMode::PreprocessingOnly + } else { + info!("GPU preprocessing is fully supported on this device."); + GpuPreprocessingMode::Culling + }; + + GpuPreprocessingSupport { max_supported_mode } + } +} + +impl BatchedInstanceBuffers +where + BD: GpuArrayBufferable + Sync + Send + 'static, + BDI: Pod + Sync + Send + Default + 'static, +{ + /// Creates new buffers. + pub fn new() -> Self { + Self::default() + } + + /// Clears out the buffers in preparation for a new frame. + pub fn clear(&mut self) { + for phase_instance_buffer in self.phase_instance_buffers.values_mut() { + phase_instance_buffer.clear(); + } + } +} + +impl UntypedPhaseBatchedInstanceBuffers +where + BD: GpuArrayBufferable + Sync + Send + 'static, +{ + pub fn new() -> Self { + UntypedPhaseBatchedInstanceBuffers { + data_buffer: UninitBufferVec::new(BufferUsages::STORAGE), + work_item_buffers: HashMap::default(), + late_indexed_indirect_parameters_buffer: RawBufferVec::new( + BufferUsages::STORAGE | BufferUsages::INDIRECT, + ), + late_non_indexed_indirect_parameters_buffer: RawBufferVec::new( + BufferUsages::STORAGE | BufferUsages::INDIRECT, + ), + } + } + + /// Returns the binding of the buffer that contains the per-instance data. + /// + /// This buffer needs to be filled in via a compute shader. + pub fn instance_data_binding(&self) -> Option> { + self.data_buffer + .buffer() + .map(|buffer| buffer.as_entire_binding()) + } + + /// Clears out the buffers in preparation for a new frame. + pub fn clear(&mut self) { + self.data_buffer.clear(); + self.late_indexed_indirect_parameters_buffer.clear(); + self.late_non_indexed_indirect_parameters_buffer.clear(); + + // Clear each individual set of buffers, but don't depopulate the hash + // table. We want to avoid reallocating these vectors every frame. + for view_work_item_buffers in self.work_item_buffers.values_mut() { + view_work_item_buffers.clear(); + } + } +} + +impl Default for UntypedPhaseBatchedInstanceBuffers +where + BD: GpuArrayBufferable + Sync + Send + 'static, +{ + fn default() -> Self { + Self::new() + } +} + +/// Information about a render batch that we're building up during a sorted +/// render phase. +struct SortedRenderBatch +where + F: GetBatchData, +{ + /// The index of the first phase item in this batch in the list of phase + /// items. + phase_item_start_index: u32, + + /// The index of the first instance in this batch in the instance buffer. + instance_start_index: u32, + + /// True if the mesh in question has an index buffer; false otherwise. + indexed: bool, + + /// The index of the indirect parameters for this batch in the + /// [`IndirectParametersBuffers`]. + /// + /// If CPU culling is being used, then this will be `None`. + indirect_parameters_index: Option, + + /// Metadata that can be used to determine whether an instance can be placed + /// into this batch. + /// + /// If `None`, the item inside is unbatchable. + meta: Option>, +} + +impl SortedRenderBatch +where + F: GetBatchData, +{ + /// Finalizes this batch and updates the [`SortedRenderPhase`] with the + /// appropriate indices. + /// + /// `instance_end_index` is the index of the last instance in this batch + /// plus one. + fn flush( + self, + instance_end_index: u32, + phase: &mut SortedRenderPhase, + phase_indirect_parameters_buffers: &mut UntypedPhaseIndirectParametersBuffers, + ) where + I: CachedRenderPipelinePhaseItem + SortedPhaseItem, + { + let (batch_range, batch_extra_index) = + phase.items[self.phase_item_start_index as usize].batch_range_and_extra_index_mut(); + *batch_range = self.instance_start_index..instance_end_index; + *batch_extra_index = match self.indirect_parameters_index { + Some(indirect_parameters_index) => PhaseItemExtraIndex::IndirectParametersIndex { + range: u32::from(indirect_parameters_index) + ..(u32::from(indirect_parameters_index) + 1), + batch_set_index: None, + }, + None => PhaseItemExtraIndex::None, + }; + if let Some(indirect_parameters_index) = self.indirect_parameters_index { + phase_indirect_parameters_buffers + .add_batch_set(self.indexed, indirect_parameters_index.into()); + } + } +} + +/// A system that runs early in extraction and clears out all the +/// [`BatchedInstanceBuffers`] for the frame. +/// +/// We have to run this during extraction because, if GPU preprocessing is in +/// use, the extraction phase will write to the mesh input uniform buffers +/// directly, so the buffers need to be cleared before then. +pub fn clear_batched_gpu_instance_buffers( + gpu_batched_instance_buffers: Option< + ResMut>, + >, +) where + GFBD: GetFullBatchData, +{ + // Don't clear the entire table, because that would delete the buffers, and + // we want to reuse those allocations. + if let Some(mut gpu_batched_instance_buffers) = gpu_batched_instance_buffers { + gpu_batched_instance_buffers.clear(); + } +} + +/// A system that removes GPU preprocessing work item buffers that correspond to +/// deleted [`ExtractedView`]s. +/// +/// This is a separate system from [`clear_batched_gpu_instance_buffers`] +/// because [`ExtractedView`]s aren't created until after the extraction phase +/// is completed. +pub fn delete_old_work_item_buffers( + mut gpu_batched_instance_buffers: ResMut< + BatchedInstanceBuffers, + >, + extracted_views: Query<&ExtractedView>, +) where + GFBD: GetFullBatchData, +{ + let retained_view_entities: HashSet<_> = extracted_views + .iter() + .map(|extracted_view| extracted_view.retained_view_entity) + .collect(); + for phase_instance_buffers in gpu_batched_instance_buffers + .phase_instance_buffers + .values_mut() + { + phase_instance_buffers + .work_item_buffers + .retain(|retained_view_entity, _| { + retained_view_entities.contains(retained_view_entity) + }); + } +} + +/// Batch the items in a sorted render phase, when GPU instance buffer building +/// is in use. This means comparing metadata needed to draw each phase item and +/// trying to combine the draws into a batch. +pub fn batch_and_prepare_sorted_render_phase( + mut phase_batched_instance_buffers: ResMut>, + mut phase_indirect_parameters_buffers: ResMut>, + mut sorted_render_phases: ResMut>, + mut views: Query<( + &ExtractedView, + Has, + Has, + )>, + system_param_item: StaticSystemParam, +) where + I: CachedRenderPipelinePhaseItem + SortedPhaseItem, + GFBD: GetFullBatchData, +{ + // We only process GPU-built batch data in this function. + let UntypedPhaseBatchedInstanceBuffers { + ref mut data_buffer, + ref mut work_item_buffers, + ref mut late_indexed_indirect_parameters_buffer, + ref mut late_non_indexed_indirect_parameters_buffer, + } = phase_batched_instance_buffers.buffers; + + for (extracted_view, no_indirect_drawing, gpu_occlusion_culling) in &mut views { + let Some(phase) = sorted_render_phases.get_mut(&extracted_view.retained_view_entity) else { + continue; + }; + + // Create the work item buffer if necessary. + let work_item_buffer = get_or_create_work_item_buffer::( + work_item_buffers, + extracted_view.retained_view_entity, + no_indirect_drawing, + gpu_occlusion_culling, + ); + + // Initialize those work item buffers in preparation for this new frame. + init_work_item_buffers( + work_item_buffer, + late_indexed_indirect_parameters_buffer, + late_non_indexed_indirect_parameters_buffer, + ); + + // Walk through the list of phase items, building up batches as we go. + let mut batch: Option> = None; + + for current_index in 0..phase.items.len() { + // Get the index of the input data, and comparison metadata, for + // this entity. + let item = &phase.items[current_index]; + let entity = item.main_entity(); + let item_is_indexed = item.indexed(); + let current_batch_input_index = + GFBD::get_index_and_compare_data(&system_param_item, entity); + + // Unpack that index and metadata. Note that it's possible for index + // and/or metadata to not be present, which signifies that this + // entity is unbatchable. In that case, we break the batch here. + // If the index isn't present the item is not part of this pipeline and so will be skipped. + let Some((current_input_index, current_meta)) = current_batch_input_index else { + // Break a batch if we need to. + if let Some(batch) = batch.take() { + batch.flush( + data_buffer.len() as u32, + phase, + &mut phase_indirect_parameters_buffers.buffers, + ); + } + + continue; + }; + let current_meta = + current_meta.map(|meta| BatchMeta::new(&phase.items[current_index], meta)); + + // Determine if this entity can be included in the batch we're + // building up. + let can_batch = batch.as_ref().is_some_and(|batch| { + // `None` for metadata indicates that the items are unbatchable. + match (¤t_meta, &batch.meta) { + (Some(current_meta), Some(batch_meta)) => current_meta == batch_meta, + (_, _) => false, + } + }); + + // Make space in the data buffer for this instance. + let output_index = data_buffer.add() as u32; + + // If we can't batch, break the existing batch and make a new one. + if !can_batch { + // Break a batch if we need to. + if let Some(batch) = batch.take() { + batch.flush( + output_index, + phase, + &mut phase_indirect_parameters_buffers.buffers, + ); + } + + let indirect_parameters_index = if no_indirect_drawing { + None + } else if item_is_indexed { + Some( + phase_indirect_parameters_buffers + .buffers + .indexed + .allocate(1), + ) + } else { + Some( + phase_indirect_parameters_buffers + .buffers + .non_indexed + .allocate(1), + ) + }; + + // Start a new batch. + if let Some(indirect_parameters_index) = indirect_parameters_index { + GFBD::write_batch_indirect_parameters_metadata( + item_is_indexed, + output_index, + None, + &mut phase_indirect_parameters_buffers.buffers, + indirect_parameters_index, + ); + }; + + batch = Some(SortedRenderBatch { + phase_item_start_index: current_index as u32, + instance_start_index: output_index, + indexed: item_is_indexed, + indirect_parameters_index: indirect_parameters_index.and_then(NonMaxU32::new), + meta: current_meta, + }); + } + + // Add a new preprocessing work item so that the preprocessing + // shader will copy the per-instance data over. + if let Some(batch) = batch.as_ref() { + work_item_buffer.push( + item_is_indexed, + PreprocessWorkItem { + input_index: current_input_index.into(), + output_or_indirect_parameters_index: match ( + no_indirect_drawing, + batch.indirect_parameters_index, + ) { + (true, _) => output_index, + (false, Some(indirect_parameters_index)) => { + indirect_parameters_index.into() + } + (false, None) => 0, + }, + }, + ); + } + } + + // Flush the final batch if necessary. + if let Some(batch) = batch.take() { + batch.flush( + data_buffer.len() as u32, + phase, + &mut phase_indirect_parameters_buffers.buffers, + ); + } + } +} + +/// Creates batches for a render phase that uses bins. +pub fn batch_and_prepare_binned_render_phase( + mut phase_batched_instance_buffers: ResMut>, + phase_indirect_parameters_buffers: ResMut>, + mut binned_render_phases: ResMut>, + mut views: Query< + ( + &ExtractedView, + Has, + Has, + ), + With, + >, + param: StaticSystemParam, +) where + BPI: BinnedPhaseItem, + GFBD: GetFullBatchData, +{ + let system_param_item = param.into_inner(); + + let phase_indirect_parameters_buffers = phase_indirect_parameters_buffers.into_inner(); + + let UntypedPhaseBatchedInstanceBuffers { + ref mut data_buffer, + ref mut work_item_buffers, + ref mut late_indexed_indirect_parameters_buffer, + ref mut late_non_indexed_indirect_parameters_buffer, + } = phase_batched_instance_buffers.buffers; + + for (extracted_view, no_indirect_drawing, gpu_occlusion_culling) in &mut views { + let Some(phase) = binned_render_phases.get_mut(&extracted_view.retained_view_entity) else { + continue; + }; + + // Create the work item buffer if necessary; otherwise, just mark it as + // used this frame. + let work_item_buffer = get_or_create_work_item_buffer::( + work_item_buffers, + extracted_view.retained_view_entity, + no_indirect_drawing, + gpu_occlusion_culling, + ); + + // Initialize those work item buffers in preparation for this new frame. + init_work_item_buffers( + work_item_buffer, + late_indexed_indirect_parameters_buffer, + late_non_indexed_indirect_parameters_buffer, + ); + + // Prepare multidrawables. + + if let ( + &mut BinnedRenderPhaseBatchSets::MultidrawIndirect(ref mut batch_sets), + &mut PreprocessWorkItemBuffers::Indirect { + indexed: ref mut indexed_work_item_buffer, + non_indexed: ref mut non_indexed_work_item_buffer, + gpu_occlusion_culling: ref mut gpu_occlusion_culling_buffers, + }, + ) = (&mut phase.batch_sets, &mut *work_item_buffer) + { + let mut output_index = data_buffer.len() as u32; + + // Initialize the state for both indexed and non-indexed meshes. + let mut indexed_preparer: MultidrawableBatchSetPreparer = + MultidrawableBatchSetPreparer::new( + phase_indirect_parameters_buffers.buffers.batch_count(true) as u32, + phase_indirect_parameters_buffers + .buffers + .indexed + .batch_sets + .len() as u32, + ); + let mut non_indexed_preparer: MultidrawableBatchSetPreparer = + MultidrawableBatchSetPreparer::new( + phase_indirect_parameters_buffers.buffers.batch_count(false) as u32, + phase_indirect_parameters_buffers + .buffers + .non_indexed + .batch_sets + .len() as u32, + ); + + // Prepare each batch set. + for (batch_set_key, bins) in &phase.multidrawable_meshes { + if batch_set_key.indexed() { + indexed_preparer.prepare_multidrawable_binned_batch_set( + bins, + &mut output_index, + data_buffer, + indexed_work_item_buffer, + &mut phase_indirect_parameters_buffers.buffers.indexed, + batch_sets, + ); + } else { + non_indexed_preparer.prepare_multidrawable_binned_batch_set( + bins, + &mut output_index, + data_buffer, + non_indexed_work_item_buffer, + &mut phase_indirect_parameters_buffers.buffers.non_indexed, + batch_sets, + ); + } + } + + // Reserve space in the occlusion culling buffers, if necessary. + if let Some(gpu_occlusion_culling_buffers) = gpu_occlusion_culling_buffers { + gpu_occlusion_culling_buffers + .late_indexed + .add_multiple(indexed_preparer.work_item_count); + gpu_occlusion_culling_buffers + .late_non_indexed + .add_multiple(non_indexed_preparer.work_item_count); + } + } + + // Prepare batchables. + + for (key, bin) in &phase.batchable_meshes { + let mut batch: Option = None; + for (&main_entity, &input_index) in bin.entities() { + let output_index = data_buffer.add() as u32; + + match batch { + Some(ref mut batch) => { + batch.instance_range.end = output_index + 1; + + // Append to the current batch. + // + // If we're in indirect mode, then we write the first + // output index of this batch, so that we have a + // tightly-packed buffer if GPU culling discards some of + // the instances. Otherwise, we can just write the + // output index directly. + work_item_buffer.push( + key.0.indexed(), + PreprocessWorkItem { + input_index: *input_index, + output_or_indirect_parameters_index: match ( + no_indirect_drawing, + &batch.extra_index, + ) { + (true, _) => output_index, + ( + false, + PhaseItemExtraIndex::IndirectParametersIndex { + range: indirect_parameters_range, + .. + }, + ) => indirect_parameters_range.start, + (false, &PhaseItemExtraIndex::DynamicOffset(_)) + | (false, &PhaseItemExtraIndex::None) => 0, + }, + }, + ); + } + + None if !no_indirect_drawing => { + // Start a new batch, in indirect mode. + let indirect_parameters_index = phase_indirect_parameters_buffers + .buffers + .allocate(key.0.indexed(), 1); + let batch_set_index = phase_indirect_parameters_buffers + .buffers + .get_next_batch_set_index(key.0.indexed()); + + GFBD::write_batch_indirect_parameters_metadata( + key.0.indexed(), + output_index, + batch_set_index, + &mut phase_indirect_parameters_buffers.buffers, + indirect_parameters_index, + ); + work_item_buffer.push( + key.0.indexed(), + PreprocessWorkItem { + input_index: *input_index, + output_or_indirect_parameters_index: indirect_parameters_index, + }, + ); + batch = Some(BinnedRenderPhaseBatch { + representative_entity: (Entity::PLACEHOLDER, main_entity), + instance_range: output_index..output_index + 1, + extra_index: PhaseItemExtraIndex::IndirectParametersIndex { + range: indirect_parameters_index..(indirect_parameters_index + 1), + batch_set_index: None, + }, + }); + } + + None => { + // Start a new batch, in direct mode. + work_item_buffer.push( + key.0.indexed(), + PreprocessWorkItem { + input_index: *input_index, + output_or_indirect_parameters_index: output_index, + }, + ); + batch = Some(BinnedRenderPhaseBatch { + representative_entity: (Entity::PLACEHOLDER, main_entity), + instance_range: output_index..output_index + 1, + extra_index: PhaseItemExtraIndex::None, + }); + } + } + } + + if let Some(batch) = batch { + match phase.batch_sets { + BinnedRenderPhaseBatchSets::DynamicUniforms(_) => { + error!("Dynamic uniform batch sets shouldn't be used here"); + } + BinnedRenderPhaseBatchSets::Direct(ref mut vec) => { + vec.push(batch); + } + BinnedRenderPhaseBatchSets::MultidrawIndirect(ref mut vec) => { + // The Bevy renderer will never mark a mesh as batchable + // but not multidrawable if multidraw is in use. + // However, custom render pipelines might do so, such as + // the `specialized_mesh_pipeline` example. + vec.push(BinnedRenderPhaseBatchSet { + first_batch: batch, + batch_count: 1, + bin_key: key.1.clone(), + index: phase_indirect_parameters_buffers + .buffers + .batch_set_count(key.0.indexed()) + as u32, + }); + } + } + } + } + + // Prepare unbatchables. + for (key, unbatchables) in &mut phase.unbatchable_meshes { + // Allocate the indirect parameters if necessary. + let mut indirect_parameters_offset = if no_indirect_drawing { + None + } else if key.0.indexed() { + Some( + phase_indirect_parameters_buffers + .buffers + .indexed + .allocate(unbatchables.entities.len() as u32), + ) + } else { + Some( + phase_indirect_parameters_buffers + .buffers + .non_indexed + .allocate(unbatchables.entities.len() as u32), + ) + }; + + for main_entity in unbatchables.entities.keys() { + let Some(input_index) = GFBD::get_binned_index(&system_param_item, *main_entity) + else { + continue; + }; + let output_index = data_buffer.add() as u32; + + if let Some(ref mut indirect_parameters_index) = indirect_parameters_offset { + // We're in indirect mode, so add an indirect parameters + // index. + GFBD::write_batch_indirect_parameters_metadata( + key.0.indexed(), + output_index, + None, + &mut phase_indirect_parameters_buffers.buffers, + *indirect_parameters_index, + ); + work_item_buffer.push( + key.0.indexed(), + PreprocessWorkItem { + input_index: input_index.into(), + output_or_indirect_parameters_index: *indirect_parameters_index, + }, + ); + unbatchables + .buffer_indices + .add(UnbatchableBinnedEntityIndices { + instance_index: *indirect_parameters_index, + extra_index: PhaseItemExtraIndex::IndirectParametersIndex { + range: *indirect_parameters_index..(*indirect_parameters_index + 1), + batch_set_index: None, + }, + }); + phase_indirect_parameters_buffers + .buffers + .add_batch_set(key.0.indexed(), *indirect_parameters_index); + *indirect_parameters_index += 1; + } else { + work_item_buffer.push( + key.0.indexed(), + PreprocessWorkItem { + input_index: input_index.into(), + output_or_indirect_parameters_index: output_index, + }, + ); + unbatchables + .buffer_indices + .add(UnbatchableBinnedEntityIndices { + instance_index: output_index, + extra_index: PhaseItemExtraIndex::None, + }); + } + } + } + } +} + +/// The state that [`batch_and_prepare_binned_render_phase`] uses to construct +/// multidrawable batch sets. +/// +/// The [`batch_and_prepare_binned_render_phase`] system maintains two of these: +/// one for indexed meshes and one for non-indexed meshes. +struct MultidrawableBatchSetPreparer +where + BPI: BinnedPhaseItem, + GFBD: GetFullBatchData, +{ + /// The offset in the indirect parameters buffer at which the next indirect + /// parameters will be written. + indirect_parameters_index: u32, + /// The number of batch sets we've built so far for this mesh class. + batch_set_index: u32, + /// The number of work items we've emitted so far for this mesh class. + work_item_count: usize, + phantom: PhantomData<(BPI, GFBD)>, +} + +impl MultidrawableBatchSetPreparer +where + BPI: BinnedPhaseItem, + GFBD: GetFullBatchData, +{ + /// Creates a new [`MultidrawableBatchSetPreparer`] that will start writing + /// indirect parameters and batch sets at the given indices. + #[inline] + fn new(initial_indirect_parameters_index: u32, initial_batch_set_index: u32) -> Self { + MultidrawableBatchSetPreparer { + indirect_parameters_index: initial_indirect_parameters_index, + batch_set_index: initial_batch_set_index, + work_item_count: 0, + phantom: PhantomData, + } + } + + /// Creates batch sets and writes the GPU data needed to draw all visible + /// entities of one mesh class in the given batch set. + /// + /// The *mesh class* represents whether the mesh has indices or not. + #[inline] + fn prepare_multidrawable_binned_batch_set( + &mut self, + bins: &IndexMap, + output_index: &mut u32, + data_buffer: &mut UninitBufferVec, + indexed_work_item_buffer: &mut RawBufferVec, + mesh_class_buffers: &mut MeshClassIndirectParametersBuffers, + batch_sets: &mut Vec>, + ) where + IP: Clone + ShaderSize + WriteInto, + { + let current_indexed_batch_set_index = self.batch_set_index; + let current_output_index = *output_index; + + let indirect_parameters_base = self.indirect_parameters_index; + + // We're going to write the first entity into the batch set. Do this + // here so that we can preload the bin into cache as a side effect. + let Some((first_bin_key, first_bin)) = bins.iter().next() else { + return; + }; + let first_bin_len = first_bin.entities().len(); + let first_bin_entity = first_bin + .entities() + .keys() + .next() + .copied() + .unwrap_or(MainEntity::from(Entity::PLACEHOLDER)); + + // Traverse the batch set, processing each bin. + for bin in bins.values() { + // Record the first output index for this batch, as well as its own + // index. + mesh_class_buffers + .cpu_metadata + .push(IndirectParametersCpuMetadata { + base_output_index: *output_index, + batch_set_index: self.batch_set_index, + }); + + // Traverse the bin, pushing `PreprocessWorkItem`s for each entity + // within it. This is a hot loop, so make it as fast as possible. + for &input_index in bin.entities().values() { + indexed_work_item_buffer.push(PreprocessWorkItem { + input_index: *input_index, + output_or_indirect_parameters_index: self.indirect_parameters_index, + }); + } + + // Reserve space for the appropriate number of entities in the data + // buffer. Also, advance the output index and work item count. + let bin_entity_count = bin.entities().len(); + data_buffer.add_multiple(bin_entity_count); + *output_index += bin_entity_count as u32; + self.work_item_count += bin_entity_count; + + self.indirect_parameters_index += 1; + } + + // Reserve space for the bins in this batch set in the GPU buffers. + let bin_count = bins.len(); + mesh_class_buffers.gpu_metadata.add_multiple(bin_count); + mesh_class_buffers.data.add_multiple(bin_count); + + // Write the information the GPU will need about this batch set. + mesh_class_buffers.batch_sets.push(IndirectBatchSet { + indirect_parameters_base, + indirect_parameters_count: 0, + }); + + self.batch_set_index += 1; + + // Record the batch set. The render node later processes this record to + // render the batches. + batch_sets.push(BinnedRenderPhaseBatchSet { + first_batch: BinnedRenderPhaseBatch { + representative_entity: (Entity::PLACEHOLDER, first_bin_entity), + instance_range: current_output_index..(current_output_index + first_bin_len as u32), + extra_index: PhaseItemExtraIndex::maybe_indirect_parameters_index(NonMaxU32::new( + indirect_parameters_base, + )), + }, + bin_key: (*first_bin_key).clone(), + batch_count: self.indirect_parameters_index - indirect_parameters_base, + index: current_indexed_batch_set_index, + }); + } +} + +/// A system that gathers up the per-phase GPU buffers and inserts them into the +/// [`BatchedInstanceBuffers`] and [`IndirectParametersBuffers`] tables. +/// +/// This runs after the [`batch_and_prepare_binned_render_phase`] or +/// [`batch_and_prepare_sorted_render_phase`] systems. It takes the per-phase +/// [`PhaseBatchedInstanceBuffers`] and [`PhaseIndirectParametersBuffers`] +/// resources and inserts them into the global [`BatchedInstanceBuffers`] and +/// [`IndirectParametersBuffers`] tables. +/// +/// This system exists so that the [`batch_and_prepare_binned_render_phase`] and +/// [`batch_and_prepare_sorted_render_phase`] can run in parallel with one +/// another. If those two systems manipulated [`BatchedInstanceBuffers`] and +/// [`IndirectParametersBuffers`] directly, then they wouldn't be able to run in +/// parallel. +pub fn collect_buffers_for_phase( + mut phase_batched_instance_buffers: ResMut>, + mut phase_indirect_parameters_buffers: ResMut>, + mut batched_instance_buffers: ResMut< + BatchedInstanceBuffers, + >, + mut indirect_parameters_buffers: ResMut, +) where + PI: PhaseItem, + GFBD: GetFullBatchData + Send + Sync + 'static, +{ + // Insert the `PhaseBatchedInstanceBuffers` into the global table. Replace + // the contents of the per-phase resource with the old batched instance + // buffers in order to reuse allocations. + let untyped_phase_batched_instance_buffers = + mem::take(&mut phase_batched_instance_buffers.buffers); + if let Some(mut old_untyped_phase_batched_instance_buffers) = batched_instance_buffers + .phase_instance_buffers + .insert(TypeId::of::(), untyped_phase_batched_instance_buffers) + { + old_untyped_phase_batched_instance_buffers.clear(); + phase_batched_instance_buffers.buffers = old_untyped_phase_batched_instance_buffers; + } + + // Insert the `PhaseIndirectParametersBuffers` into the global table. + // Replace the contents of the per-phase resource with the old indirect + // parameters buffers in order to reuse allocations. + let untyped_phase_indirect_parameters_buffers = mem::replace( + &mut phase_indirect_parameters_buffers.buffers, + UntypedPhaseIndirectParametersBuffers::new( + indirect_parameters_buffers.allow_copies_from_indirect_parameter_buffers, + ), + ); + if let Some(mut old_untyped_phase_indirect_parameters_buffers) = indirect_parameters_buffers + .insert( + TypeId::of::(), + untyped_phase_indirect_parameters_buffers, + ) + { + old_untyped_phase_indirect_parameters_buffers.clear(); + phase_indirect_parameters_buffers.buffers = old_untyped_phase_indirect_parameters_buffers; + } +} + +/// A system that writes all instance buffers to the GPU. +pub fn write_batched_instance_buffers( + render_device: Res, + render_queue: Res, + gpu_array_buffer: ResMut>, +) where + GFBD: GetFullBatchData, +{ + let BatchedInstanceBuffers { + current_input_buffer, + previous_input_buffer, + phase_instance_buffers, + } = gpu_array_buffer.into_inner(); + + current_input_buffer + .buffer + .write_buffer(&render_device, &render_queue); + previous_input_buffer + .buffer + .write_buffer(&render_device, &render_queue); + + for phase_instance_buffers in phase_instance_buffers.values_mut() { + let UntypedPhaseBatchedInstanceBuffers { + ref mut data_buffer, + ref mut work_item_buffers, + ref mut late_indexed_indirect_parameters_buffer, + ref mut late_non_indexed_indirect_parameters_buffer, + } = *phase_instance_buffers; + + data_buffer.write_buffer(&render_device); + late_indexed_indirect_parameters_buffer.write_buffer(&render_device, &render_queue); + late_non_indexed_indirect_parameters_buffer.write_buffer(&render_device, &render_queue); + + for phase_work_item_buffers in work_item_buffers.values_mut() { + match *phase_work_item_buffers { + PreprocessWorkItemBuffers::Direct(ref mut buffer_vec) => { + buffer_vec.write_buffer(&render_device, &render_queue); + } + PreprocessWorkItemBuffers::Indirect { + ref mut indexed, + ref mut non_indexed, + ref mut gpu_occlusion_culling, + } => { + indexed.write_buffer(&render_device, &render_queue); + non_indexed.write_buffer(&render_device, &render_queue); + + if let Some(GpuOcclusionCullingWorkItemBuffers { + ref mut late_indexed, + ref mut late_non_indexed, + late_indirect_parameters_indexed_offset: _, + late_indirect_parameters_non_indexed_offset: _, + }) = *gpu_occlusion_culling + { + if !late_indexed.is_empty() { + late_indexed.write_buffer(&render_device); + } + if !late_non_indexed.is_empty() { + late_non_indexed.write_buffer(&render_device); + } + } + } + } + } + } +} + +pub fn clear_indirect_parameters_buffers( + mut indirect_parameters_buffers: ResMut, +) { + for phase_indirect_parameters_buffers in indirect_parameters_buffers.values_mut() { + phase_indirect_parameters_buffers.clear(); + } +} + +pub fn write_indirect_parameters_buffers( + render_device: Res, + render_queue: Res, + mut indirect_parameters_buffers: ResMut, +) { + for phase_indirect_parameters_buffers in indirect_parameters_buffers.values_mut() { + phase_indirect_parameters_buffers + .indexed + .data + .write_buffer(&render_device); + phase_indirect_parameters_buffers + .non_indexed + .data + .write_buffer(&render_device); + + phase_indirect_parameters_buffers + .indexed + .cpu_metadata + .write_buffer(&render_device, &render_queue); + phase_indirect_parameters_buffers + .non_indexed + .cpu_metadata + .write_buffer(&render_device, &render_queue); + + phase_indirect_parameters_buffers + .non_indexed + .gpu_metadata + .write_buffer(&render_device); + phase_indirect_parameters_buffers + .indexed + .gpu_metadata + .write_buffer(&render_device); + + phase_indirect_parameters_buffers + .indexed + .batch_sets + .write_buffer(&render_device, &render_queue); + phase_indirect_parameters_buffers + .non_indexed + .batch_sets + .write_buffer(&render_device, &render_queue); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn instance_buffer_correct_behavior() { + let mut instance_buffer = InstanceInputUniformBuffer::new(); + + let index = instance_buffer.add(2); + instance_buffer.remove(index); + assert_eq!(instance_buffer.get_unchecked(index), 2); + assert_eq!(instance_buffer.get(index), None); + + instance_buffer.add(5); + assert_eq!(instance_buffer.buffer().len(), 1); + } +} diff --git a/crates/libmarathon/src/render/batching/mod.rs b/crates/libmarathon/src/render/batching/mod.rs new file mode 100644 index 0000000..0600ee7 --- /dev/null +++ b/crates/libmarathon/src/render/batching/mod.rs @@ -0,0 +1,225 @@ +use bevy_ecs::{ + component::Component, + entity::Entity, + system::{ResMut, SystemParam, SystemParamItem}, +}; +use bytemuck::Pod; +use gpu_preprocessing::UntypedPhaseIndirectParametersBuffers; +use nonmax::NonMaxU32; + +use crate::render::{ + render_phase::{ + BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItemExtraIndex, + SortedPhaseItem, SortedRenderPhase, ViewBinnedRenderPhases, + }, + render_resource::{CachedRenderPipelineId, GpuArrayBufferable}, + sync_world::MainEntity, +}; + +pub mod gpu_preprocessing; +pub mod no_gpu_preprocessing; + +/// Add this component to mesh entities to disable automatic batching +#[derive(Component, Default)] +pub struct NoAutomaticBatching; + +/// Data necessary to be equal for two draw commands to be mergeable +/// +/// This is based on the following assumptions: +/// - Only entities with prepared assets (pipelines, materials, meshes) are +/// queued to phases +/// - View bindings are constant across a phase for a given draw function as +/// phases are per-view +/// - `batch_and_prepare_render_phase` is the only system that performs this +/// batching and has sole responsibility for preparing the per-object data. +/// As such the mesh binding and dynamic offsets are assumed to only be +/// variable as a result of the `batch_and_prepare_render_phase` system, e.g. +/// due to having to split data across separate uniform bindings within the +/// same buffer due to the maximum uniform buffer binding size. +#[derive(PartialEq)] +struct BatchMeta { + /// The pipeline id encompasses all pipeline configuration including vertex + /// buffers and layouts, shaders and their specializations, bind group + /// layouts, etc. + pipeline_id: CachedRenderPipelineId, + /// The draw function id defines the `RenderCommands` that are called to + /// set the pipeline and bindings, and make the draw command + draw_function_id: DrawFunctionId, + dynamic_offset: Option, + user_data: T, +} + +impl BatchMeta { + fn new(item: &impl CachedRenderPipelinePhaseItem, user_data: T) -> Self { + BatchMeta { + pipeline_id: item.cached_pipeline(), + draw_function_id: item.draw_function(), + dynamic_offset: match item.extra_index() { + PhaseItemExtraIndex::DynamicOffset(dynamic_offset) => { + NonMaxU32::new(dynamic_offset) + } + PhaseItemExtraIndex::None | PhaseItemExtraIndex::IndirectParametersIndex { .. } => { + None + } + }, + user_data, + } + } +} + +/// A trait to support getting data used for batching draw commands via phase +/// items. +/// +/// This is a simple version that only allows for sorting, not binning, as well +/// as only CPU processing, not GPU preprocessing. For these fancier features, +/// see [`GetFullBatchData`]. +pub trait GetBatchData { + /// The system parameters [`GetBatchData::get_batch_data`] needs in + /// order to compute the batch data. + type Param: SystemParam + 'static; + /// Data used for comparison between phase items. If the pipeline id, draw + /// function id, per-instance data buffer dynamic offset and this data + /// matches, the draws can be batched. + type CompareData: PartialEq; + /// The per-instance data to be inserted into the + /// [`crate::render_resource::GpuArrayBuffer`] containing these data for all + /// instances. + type BufferData: GpuArrayBufferable + Sync + Send + 'static; + /// Get the per-instance data to be inserted into the + /// [`crate::render_resource::GpuArrayBuffer`]. If the instance can be + /// batched, also return the data used for comparison when deciding whether + /// draws can be batched, else return None for the `CompareData`. + /// + /// This is only called when building instance data on CPU. In the GPU + /// instance data building path, we use + /// [`GetFullBatchData::get_index_and_compare_data`] instead. + fn get_batch_data( + param: &SystemParamItem, + query_item: (Entity, MainEntity), + ) -> Option<(Self::BufferData, Option)>; +} + +/// A trait to support getting data used for batching draw commands via phase +/// items. +/// +/// This version allows for binning and GPU preprocessing. +pub trait GetFullBatchData: GetBatchData { + /// The per-instance data that was inserted into the + /// [`crate::render_resource::BufferVec`] during extraction. + type BufferInputData: Pod + Default + Sync + Send; + + /// Get the per-instance data to be inserted into the + /// [`crate::render_resource::GpuArrayBuffer`]. + /// + /// This is only called when building uniforms on CPU. In the GPU instance + /// buffer building path, we use + /// [`GetFullBatchData::get_index_and_compare_data`] instead. + fn get_binned_batch_data( + param: &SystemParamItem, + query_item: MainEntity, + ) -> Option; + + /// Returns the index of the [`GetFullBatchData::BufferInputData`] that the + /// GPU preprocessing phase will use. + /// + /// We already inserted the [`GetFullBatchData::BufferInputData`] during the + /// extraction phase before we got here, so this function shouldn't need to + /// look up any render data. If CPU instance buffer building is in use, this + /// function will never be called. + fn get_index_and_compare_data( + param: &SystemParamItem, + query_item: MainEntity, + ) -> Option<(NonMaxU32, Option)>; + + /// Returns the index of the [`GetFullBatchData::BufferInputData`] that the + /// GPU preprocessing phase will use. + /// + /// We already inserted the [`GetFullBatchData::BufferInputData`] during the + /// extraction phase before we got here, so this function shouldn't need to + /// look up any render data. + /// + /// This function is currently only called for unbatchable entities when GPU + /// instance buffer building is in use. For batchable entities, the uniform + /// index is written during queuing (e.g. in `queue_material_meshes`). In + /// the case of CPU instance buffer building, the CPU writes the uniforms, + /// so there's no index to return. + fn get_binned_index( + param: &SystemParamItem, + query_item: MainEntity, + ) -> Option; + + /// Writes the [`gpu_preprocessing::IndirectParametersGpuMetadata`] + /// necessary to draw this batch into the given metadata buffer at the given + /// index. + /// + /// This is only used if GPU culling is enabled (which requires GPU + /// preprocessing). + /// + /// * `indexed` is true if the mesh is indexed or false if it's non-indexed. + /// + /// * `base_output_index` is the index of the first mesh instance in this + /// batch in the `MeshUniform` output buffer. + /// + /// * `batch_set_index` is the index of the batch set in the + /// [`gpu_preprocessing::IndirectBatchSet`] buffer, if this batch belongs to + /// a batch set. + /// + /// * `indirect_parameters_buffers` is the buffer in which to write the + /// metadata. + /// + /// * `indirect_parameters_offset` is the index in that buffer at which to + /// write the metadata. + fn write_batch_indirect_parameters_metadata( + indexed: bool, + base_output_index: u32, + batch_set_index: Option, + indirect_parameters_buffers: &mut UntypedPhaseIndirectParametersBuffers, + indirect_parameters_offset: u32, + ); +} + +/// Sorts a render phase that uses bins. +pub fn sort_binned_render_phase(mut phases: ResMut>) +where + BPI: BinnedPhaseItem, +{ + for phase in phases.values_mut() { + phase.multidrawable_meshes.sort_unstable_keys(); + phase.batchable_meshes.sort_unstable_keys(); + phase.unbatchable_meshes.sort_unstable_keys(); + phase.non_mesh_items.sort_unstable_keys(); + } +} + +/// Batches the items in a sorted render phase. +/// +/// This means comparing metadata needed to draw each phase item and trying to +/// combine the draws into a batch. +/// +/// This is common code factored out from +/// [`gpu_preprocessing::batch_and_prepare_sorted_render_phase`] and +/// [`no_gpu_preprocessing::batch_and_prepare_sorted_render_phase`]. +fn batch_and_prepare_sorted_render_phase( + phase: &mut SortedRenderPhase, + mut process_item: impl FnMut(&mut I) -> Option, +) where + I: CachedRenderPipelinePhaseItem + SortedPhaseItem, + GBD: GetBatchData, +{ + let items = phase.items.iter_mut().map(|item| { + let batch_data = match process_item(item) { + Some(compare_data) if I::AUTOMATIC_BATCHING => Some(BatchMeta::new(item, compare_data)), + _ => None, + }; + (item.batch_range_mut(), batch_data) + }); + + items.reduce(|(start_range, prev_batch_meta), (range, batch_meta)| { + if batch_meta.is_some() && prev_batch_meta == batch_meta { + start_range.end = range.end; + (start_range, prev_batch_meta) + } else { + (range, batch_meta) + } + }); +} diff --git a/crates/libmarathon/src/render/batching/no_gpu_preprocessing.rs b/crates/libmarathon/src/render/batching/no_gpu_preprocessing.rs new file mode 100644 index 0000000..6accfdf --- /dev/null +++ b/crates/libmarathon/src/render/batching/no_gpu_preprocessing.rs @@ -0,0 +1,182 @@ +//! Batching functionality when GPU preprocessing isn't in use. + +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::entity::Entity; +use bevy_ecs::resource::Resource; +use bevy_ecs::system::{Res, ResMut, StaticSystemParam}; +use smallvec::{smallvec, SmallVec}; +use tracing::error; +use wgpu::BindingResource; + +use crate::render::{ + render_phase::{ + BinnedPhaseItem, BinnedRenderPhaseBatch, BinnedRenderPhaseBatchSets, + CachedRenderPipelinePhaseItem, PhaseItemExtraIndex, SortedPhaseItem, + ViewBinnedRenderPhases, ViewSortedRenderPhases, + }, + render_resource::{GpuArrayBuffer, GpuArrayBufferable}, + renderer::{RenderDevice, RenderQueue}, +}; + +use super::{GetBatchData, GetFullBatchData}; + +/// The GPU buffers holding the data needed to render batches. +/// +/// For example, in the 3D PBR pipeline this holds `MeshUniform`s, which are the +/// `BD` type parameter in that mode. +#[derive(Resource, Deref, DerefMut)] +pub struct BatchedInstanceBuffer(pub GpuArrayBuffer) +where + BD: GpuArrayBufferable + Sync + Send + 'static; + +impl BatchedInstanceBuffer +where + BD: GpuArrayBufferable + Sync + Send + 'static, +{ + /// Creates a new buffer. + pub fn new(render_device: &RenderDevice) -> Self { + BatchedInstanceBuffer(GpuArrayBuffer::new(render_device)) + } + + /// Returns the binding of the buffer that contains the per-instance data. + /// + /// If we're in the GPU instance buffer building mode, this buffer needs to + /// be filled in via a compute shader. + pub fn instance_data_binding(&self) -> Option> { + self.binding() + } +} + +/// A system that clears out the [`BatchedInstanceBuffer`] for the frame. +/// +/// This needs to run before the CPU batched instance buffers are used. +pub fn clear_batched_cpu_instance_buffers( + cpu_batched_instance_buffer: Option>>, +) where + GBD: GetBatchData, +{ + if let Some(mut cpu_batched_instance_buffer) = cpu_batched_instance_buffer { + cpu_batched_instance_buffer.clear(); + } +} + +/// Batch the items in a sorted render phase, when GPU instance buffer building +/// isn't in use. This means comparing metadata needed to draw each phase item +/// and trying to combine the draws into a batch. +pub fn batch_and_prepare_sorted_render_phase( + batched_instance_buffer: ResMut>, + mut phases: ResMut>, + param: StaticSystemParam, +) where + I: CachedRenderPipelinePhaseItem + SortedPhaseItem, + GBD: GetBatchData, +{ + let system_param_item = param.into_inner(); + + // We only process CPU-built batch data in this function. + let batched_instance_buffer = batched_instance_buffer.into_inner(); + + for phase in phases.values_mut() { + super::batch_and_prepare_sorted_render_phase::(phase, |item| { + let (buffer_data, compare_data) = + GBD::get_batch_data(&system_param_item, (item.entity(), item.main_entity()))?; + let buffer_index = batched_instance_buffer.push(buffer_data); + + let index = buffer_index.index; + let (batch_range, extra_index) = item.batch_range_and_extra_index_mut(); + *batch_range = index..index + 1; + *extra_index = PhaseItemExtraIndex::maybe_dynamic_offset(buffer_index.dynamic_offset); + + compare_data + }); + } +} + +/// Creates batches for a render phase that uses bins, when GPU batch data +/// building isn't in use. +pub fn batch_and_prepare_binned_render_phase( + gpu_array_buffer: ResMut>, + mut phases: ResMut>, + param: StaticSystemParam, +) where + BPI: BinnedPhaseItem, + GFBD: GetFullBatchData, +{ + let gpu_array_buffer = gpu_array_buffer.into_inner(); + let system_param_item = param.into_inner(); + + for phase in phases.values_mut() { + // Prepare batchables. + + for bin in phase.batchable_meshes.values_mut() { + let mut batch_set: SmallVec<[BinnedRenderPhaseBatch; 1]> = smallvec![]; + for main_entity in bin.entities().keys() { + let Some(buffer_data) = + GFBD::get_binned_batch_data(&system_param_item, *main_entity) + else { + continue; + }; + let instance = gpu_array_buffer.push(buffer_data); + + // If the dynamic offset has changed, flush the batch. + // + // This is the only time we ever have more than one batch per + // bin. Note that dynamic offsets are only used on platforms + // with no storage buffers. + if !batch_set.last().is_some_and(|batch| { + batch.instance_range.end == instance.index + && batch.extra_index + == PhaseItemExtraIndex::maybe_dynamic_offset(instance.dynamic_offset) + }) { + batch_set.push(BinnedRenderPhaseBatch { + representative_entity: (Entity::PLACEHOLDER, *main_entity), + instance_range: instance.index..instance.index, + extra_index: PhaseItemExtraIndex::maybe_dynamic_offset( + instance.dynamic_offset, + ), + }); + } + + if let Some(batch) = batch_set.last_mut() { + batch.instance_range.end = instance.index + 1; + } + } + + match phase.batch_sets { + BinnedRenderPhaseBatchSets::DynamicUniforms(ref mut batch_sets) => { + batch_sets.push(batch_set); + } + BinnedRenderPhaseBatchSets::Direct(_) + | BinnedRenderPhaseBatchSets::MultidrawIndirect { .. } => { + error!( + "Dynamic uniform batch sets should be used when GPU preprocessing is off" + ); + } + } + } + + // Prepare unbatchables. + for unbatchables in phase.unbatchable_meshes.values_mut() { + for main_entity in unbatchables.entities.keys() { + let Some(buffer_data) = + GFBD::get_binned_batch_data(&system_param_item, *main_entity) + else { + continue; + }; + let instance = gpu_array_buffer.push(buffer_data); + unbatchables.buffer_indices.add(instance.into()); + } + } + } +} + +/// Writes the instance buffer data to the GPU. +pub fn write_batched_instance_buffer( + render_device: Res, + render_queue: Res, + mut cpu_batched_instance_buffer: ResMut>, +) where + GBD: GetBatchData, +{ + cpu_batched_instance_buffer.write_buffer(&render_device, &render_queue); +} diff --git a/crates/libmarathon/src/render/bindless.wgsl b/crates/libmarathon/src/render/bindless.wgsl new file mode 100644 index 0000000..6c8eff1 --- /dev/null +++ b/crates/libmarathon/src/render/bindless.wgsl @@ -0,0 +1,37 @@ +// Defines the common arrays used to access bindless resources. +// +// This need to be kept up to date with the `BINDING_NUMBERS` table in +// `bindless.rs`. +// +// You access these by indexing into the bindless index table, and from there +// indexing into the appropriate binding array. For example, to access the base +// color texture of a `StandardMaterial` in bindless mode, write +// `bindless_textures_2d[materials[slot].base_color_texture]`, where +// `materials` is the bindless index table and `slot` is the index into that +// table (which can be found in the `Mesh`). + +#define_import_path bevy_render::bindless + +#ifdef BINDLESS + +// Binding 0 is the bindless index table. +// Filtering samplers. +@group(#{MATERIAL_BIND_GROUP}) @binding(1) var bindless_samplers_filtering: binding_array; +// Non-filtering samplers (nearest neighbor). +@group(#{MATERIAL_BIND_GROUP}) @binding(2) var bindless_samplers_non_filtering: binding_array; +// Comparison samplers (typically for shadow mapping). +@group(#{MATERIAL_BIND_GROUP}) @binding(3) var bindless_samplers_comparison: binding_array; +// 1D textures. +@group(#{MATERIAL_BIND_GROUP}) @binding(4) var bindless_textures_1d: binding_array>; +// 2D textures. +@group(#{MATERIAL_BIND_GROUP}) @binding(5) var bindless_textures_2d: binding_array>; +// 2D array textures. +@group(#{MATERIAL_BIND_GROUP}) @binding(6) var bindless_textures_2d_array: binding_array>; +// 3D textures. +@group(#{MATERIAL_BIND_GROUP}) @binding(7) var bindless_textures_3d: binding_array>; +// Cubemap textures. +@group(#{MATERIAL_BIND_GROUP}) @binding(8) var bindless_textures_cube: binding_array>; +// Cubemap array textures. +@group(#{MATERIAL_BIND_GROUP}) @binding(9) var bindless_textures_cube_array: binding_array>; + +#endif // BINDLESS diff --git a/crates/libmarathon/src/render/blit/blit.wgsl b/crates/libmarathon/src/render/blit/blit.wgsl new file mode 100644 index 0000000..82521bf --- /dev/null +++ b/crates/libmarathon/src/render/blit/blit.wgsl @@ -0,0 +1,9 @@ +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +@group(0) @binding(0) var in_texture: texture_2d; +@group(0) @binding(1) var in_sampler: sampler; + +@fragment +fn fs_main(in: FullscreenVertexOutput) -> @location(0) vec4 { + return textureSample(in_texture, in_sampler, in.uv); +} diff --git a/crates/libmarathon/src/render/blit/mod.rs b/crates/libmarathon/src/render/blit/mod.rs new file mode 100644 index 0000000..7a205af --- /dev/null +++ b/crates/libmarathon/src/render/blit/mod.rs @@ -0,0 +1,114 @@ +use crate::render::FullscreenShader; +use bevy_app::{App, Plugin}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; +use bevy_ecs::prelude::*; +use crate::render::{ + render_resource::{ + binding_types::{sampler, texture_2d}, + *, + }, + renderer::RenderDevice, + RenderApp, RenderStartup, +}; +use bevy_shader::Shader; +use bevy_utils::default; + +/// Adds support for specialized "blit pipelines", which can be used to write one texture to another. +pub struct BlitPlugin; + +impl Plugin for BlitPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "blit.wgsl"); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .allow_ambiguous_resource::>() + .init_resource::>() + .add_systems(RenderStartup, init_blit_pipeline); + } +} + +#[derive(Resource)] +pub struct BlitPipeline { + pub layout: BindGroupLayout, + pub sampler: Sampler, + pub fullscreen_shader: FullscreenShader, + pub fragment_shader: Handle, +} + +pub fn init_blit_pipeline( + mut commands: Commands, + render_device: Res, + fullscreen_shader: Res, + asset_server: Res, +) { + let layout = render_device.create_bind_group_layout( + "blit_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + sampler(SamplerBindingType::NonFiltering), + ), + ), + ); + + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); + + commands.insert_resource(BlitPipeline { + layout, + sampler, + fullscreen_shader: fullscreen_shader.clone(), + fragment_shader: load_embedded_asset!(asset_server.as_ref(), "blit.wgsl"), + }); +} + +impl BlitPipeline { + pub fn create_bind_group( + &self, + render_device: &RenderDevice, + src_texture: &TextureView, + ) -> BindGroup { + render_device.create_bind_group( + None, + &self.layout, + &BindGroupEntries::sequential((src_texture, &self.sampler)), + ) + } +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +pub struct BlitPipelineKey { + pub texture_format: TextureFormat, + pub blend_state: Option, + pub samples: u32, +} + +impl SpecializedRenderPipeline for BlitPipeline { + type Key = BlitPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + RenderPipelineDescriptor { + label: Some("blit pipeline".into()), + layout: vec![self.layout.clone()], + vertex: self.fullscreen_shader.to_vertex_state(), + fragment: Some(FragmentState { + shader: self.fragment_shader.clone(), + targets: vec![Some(ColorTargetState { + format: key.texture_format, + blend: key.blend_state, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + multisample: MultisampleState { + count: key.samples, + ..default() + }, + ..default() + } + } +} diff --git a/crates/libmarathon/src/render/camera.rs b/crates/libmarathon/src/render/camera.rs new file mode 100644 index 0000000..b2e701e --- /dev/null +++ b/crates/libmarathon/src/render/camera.rs @@ -0,0 +1,695 @@ +use crate::render::{ + batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, + extract_component::{ExtractComponent, ExtractComponentPlugin}, + extract_resource::{ExtractResource, ExtractResourcePlugin}, + render_asset::RenderAssets, + render_graph::{CameraDriverNode, InternedRenderSubGraph, RenderGraph, RenderSubGraph}, + render_resource::TextureView, + sync_world::{RenderEntity, SyncToRenderWorld}, + texture::{GpuImage, ManualTextureViews}, + view::{ + ColorGrading, ExtractedView, ExtractedWindows, Hdr, Msaa, NoIndirectDrawing, + RenderVisibleEntities, RetainedViewEntity, ViewUniformOffset, + }, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; + +use bevy_app::{App, Plugin, PostStartup, PostUpdate}; +use bevy_asset::{AssetEvent, AssetEventSystems, AssetId, Assets}; +use bevy_camera::{ + primitives::Frustum, + visibility::{self, RenderLayers, VisibleEntities}, + Camera, Camera2d, Camera3d, CameraMainTextureUsages, CameraOutputMode, CameraUpdateSystems, + ClearColor, ClearColorConfig, Exposure, ManualTextureViewHandle, NormalizedRenderTarget, + Projection, RenderTargetInfo, Viewport, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + change_detection::DetectChanges, + component::Component, + entity::{ContainsEntity, Entity}, + error::BevyError, + lifecycle::HookContext, + message::MessageReader, + prelude::With, + query::{Has, QueryItem}, + reflect::ReflectComponent, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res, ResMut}, + world::DeferredWorld, +}; +use bevy_image::Image; +use bevy_math::{uvec2, vec2, Mat4, URect, UVec2, UVec4, Vec2}; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_reflect::prelude::*; +use bevy_transform::components::GlobalTransform; +use bevy_window::{PrimaryWindow, Window, WindowCreated, WindowResized, WindowScaleFactorChanged}; +use tracing::warn; +use wgpu::TextureFormat; + +#[derive(Default)] +pub struct CameraPlugin; + +impl Plugin for CameraPlugin { + fn build(&self, app: &mut App) { + app.register_required_components::() + .register_required_components::() + .register_required_components::() + .register_required_components::() + .add_plugins(( + ExtractResourcePlugin::::default(), + ExtractComponentPlugin::::default(), + )) + .add_systems(PostStartup, camera_system.in_set(CameraUpdateSystems)) + .add_systems( + PostUpdate, + camera_system + .in_set(CameraUpdateSystems) + .before(AssetEventSystems) + .before(visibility::update_frusta), + ); + app.world_mut() + .register_component_hooks::() + .on_add(warn_on_no_render_graph); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .add_systems(ExtractSchedule, extract_cameras) + .add_systems(Render, sort_cameras.in_set(RenderSystems::ManageViews)); + let camera_driver_node = CameraDriverNode::new(render_app.world_mut()); + let mut render_graph = render_app.world_mut().resource_mut::(); + render_graph.add_node(crate::render::graph::CameraDriverLabel, camera_driver_node); + } + } +} + +fn warn_on_no_render_graph(world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) { + if !world.entity(entity).contains::() { + warn!("{}Entity {entity} has a `Camera` component, but it doesn't have a render graph configured. Usually, adding a `Camera2d` or `Camera3d` component will work. + However, you may instead need to enable `bevy_core_pipeline`, or may want to manually add a `CameraRenderGraph` component to create a custom render graph.", caller.map(|location|format!("{location}: ")).unwrap_or_default()); + } +} + +impl ExtractResource for ClearColor { + type Source = Self; + + fn extract_resource(source: &Self::Source) -> Self { + source.clone() + } +} +impl ExtractComponent for CameraMainTextureUsages { + type QueryData = &'static Self; + type QueryFilter = (); + type Out = Self; + + fn extract_component(item: QueryItem) -> Option { + Some(*item) + } +} +impl ExtractComponent for Camera2d { + type QueryData = &'static Self; + type QueryFilter = With; + type Out = Self; + + fn extract_component(item: QueryItem) -> Option { + Some(item.clone()) + } +} +impl ExtractComponent for Camera3d { + type QueryData = &'static Self; + type QueryFilter = With; + type Out = Self; + + fn extract_component(item: QueryItem) -> Option { + Some(item.clone()) + } +} + +/// Configures the [`RenderGraph`] name assigned to be run for a given [`Camera`] entity. +#[derive(Component, Debug, Deref, DerefMut, Reflect, Clone)] +#[reflect(opaque)] +#[reflect(Component, Debug, Clone)] +pub struct CameraRenderGraph(InternedRenderSubGraph); + +impl CameraRenderGraph { + /// Creates a new [`CameraRenderGraph`] from any string-like type. + #[inline] + pub fn new(name: T) -> Self { + Self(name.intern()) + } + + /// Sets the graph name. + #[inline] + pub fn set(&mut self, name: T) { + self.0 = name.intern(); + } +} + +pub trait NormalizedRenderTargetExt { + fn get_texture_view<'a>( + &self, + windows: &'a ExtractedWindows, + images: &'a RenderAssets, + manual_texture_views: &'a ManualTextureViews, + ) -> Option<&'a TextureView>; + + /// Retrieves the [`TextureFormat`] of this render target, if it exists. + fn get_texture_format<'a>( + &self, + windows: &'a ExtractedWindows, + images: &'a RenderAssets, + manual_texture_views: &'a ManualTextureViews, + ) -> Option; + + fn get_render_target_info<'a>( + &self, + resolutions: impl IntoIterator, + images: &Assets, + manual_texture_views: &ManualTextureViews, + ) -> Result; + + // Check if this render target is contained in the given changed windows or images. + fn is_changed( + &self, + changed_window_ids: &HashSet, + changed_image_handles: &HashSet<&AssetId>, + ) -> bool; +} + +impl NormalizedRenderTargetExt for NormalizedRenderTarget { + fn get_texture_view<'a>( + &self, + windows: &'a ExtractedWindows, + images: &'a RenderAssets, + manual_texture_views: &'a ManualTextureViews, + ) -> Option<&'a TextureView> { + match self { + NormalizedRenderTarget::Window(window_ref) => windows + .get(&window_ref.entity()) + .and_then(|window| window.swap_chain_texture_view.as_ref()), + NormalizedRenderTarget::Image(image_target) => images + .get(&image_target.handle) + .map(|image| &image.texture_view), + NormalizedRenderTarget::TextureView(id) => { + manual_texture_views.get(id).map(|tex| &tex.texture_view) + } + NormalizedRenderTarget::None { .. } => None, + } + } + + /// Retrieves the [`TextureFormat`] of this render target, if it exists. + fn get_texture_format<'a>( + &self, + windows: &'a ExtractedWindows, + images: &'a RenderAssets, + manual_texture_views: &'a ManualTextureViews, + ) -> Option { + match self { + NormalizedRenderTarget::Window(window_ref) => windows + .get(&window_ref.entity()) + .and_then(|window| window.swap_chain_texture_format), + NormalizedRenderTarget::Image(image_target) => images + .get(&image_target.handle) + .map(|image| image.texture_format), + NormalizedRenderTarget::TextureView(id) => { + manual_texture_views.get(id).map(|tex| tex.format) + } + NormalizedRenderTarget::None { .. } => None, + } + } + + fn get_render_target_info<'a>( + &self, + resolutions: impl IntoIterator, + images: &Assets, + manual_texture_views: &ManualTextureViews, + ) -> Result { + match self { + NormalizedRenderTarget::Window(window_ref) => resolutions + .into_iter() + .find(|(entity, _)| *entity == window_ref.entity()) + .map(|(_, window)| RenderTargetInfo { + physical_size: window.physical_size(), + scale_factor: window.resolution.scale_factor(), + }) + .ok_or(MissingRenderTargetInfoError::Window { + window: window_ref.entity(), + }), + NormalizedRenderTarget::Image(image_target) => images + .get(&image_target.handle) + .map(|image| RenderTargetInfo { + physical_size: image.size(), + scale_factor: image_target.scale_factor.0, + }) + .ok_or(MissingRenderTargetInfoError::Image { + image: image_target.handle.id(), + }), + NormalizedRenderTarget::TextureView(id) => manual_texture_views + .get(id) + .map(|tex| RenderTargetInfo { + physical_size: tex.size, + scale_factor: 1.0, + }) + .ok_or(MissingRenderTargetInfoError::TextureView { texture_view: *id }), + NormalizedRenderTarget::None { width, height } => Ok(RenderTargetInfo { + physical_size: uvec2(*width, *height), + scale_factor: 1.0, + }), + } + } + + // Check if this render target is contained in the given changed windows or images. + fn is_changed( + &self, + changed_window_ids: &HashSet, + changed_image_handles: &HashSet<&AssetId>, + ) -> bool { + match self { + NormalizedRenderTarget::Window(window_ref) => { + changed_window_ids.contains(&window_ref.entity()) + } + NormalizedRenderTarget::Image(image_target) => { + changed_image_handles.contains(&image_target.handle.id()) + } + NormalizedRenderTarget::TextureView(_) => true, + NormalizedRenderTarget::None { .. } => false, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum MissingRenderTargetInfoError { + #[error("RenderTarget::Window missing ({window:?}): Make sure the provided entity has a Window component.")] + Window { window: Entity }, + #[error("RenderTarget::Image missing ({image:?}): Make sure the Image's usages include RenderAssetUsages::MAIN_WORLD.")] + Image { image: AssetId }, + #[error("RenderTarget::TextureView missing ({texture_view:?}): make sure the texture view handle was not removed.")] + TextureView { + texture_view: ManualTextureViewHandle, + }, +} + +/// System in charge of updating a [`Camera`] when its window or projection changes. +/// +/// The system detects window creation, resize, and scale factor change events to update the camera +/// [`Projection`] if needed. +/// +/// ## World Resources +/// +/// [`Res>`](Assets) -- For cameras that render to an image, this resource is used to +/// inspect information about the render target. This system will not access any other image assets. +/// +/// [`OrthographicProjection`]: bevy_camera::OrthographicProjection +/// [`PerspectiveProjection`]: bevy_camera::PerspectiveProjection +pub fn camera_system( + mut window_resized_reader: MessageReader, + mut window_created_reader: MessageReader, + mut window_scale_factor_changed_reader: MessageReader, + mut image_asset_event_reader: MessageReader>, + primary_window: Query>, + windows: Query<(Entity, &Window)>, + images: Res>, + manual_texture_views: Res, + mut cameras: Query<(&mut Camera, &mut Projection)>, +) -> Result<(), BevyError> { + let primary_window = primary_window.iter().next(); + + let mut changed_window_ids = >::default(); + changed_window_ids.extend(window_created_reader.read().map(|event| event.window)); + changed_window_ids.extend(window_resized_reader.read().map(|event| event.window)); + let scale_factor_changed_window_ids: HashSet<_> = window_scale_factor_changed_reader + .read() + .map(|event| event.window) + .collect(); + changed_window_ids.extend(scale_factor_changed_window_ids.clone()); + + let changed_image_handles: HashSet<&AssetId> = image_asset_event_reader + .read() + .filter_map(|event| match event { + AssetEvent::Modified { id } | AssetEvent::Added { id } => Some(id), + _ => None, + }) + .collect(); + + for (mut camera, mut camera_projection) in &mut cameras { + let mut viewport_size = camera + .viewport + .as_ref() + .map(|viewport| viewport.physical_size); + + if let Some(normalized_target) = &camera.target.normalize(primary_window) + && (normalized_target.is_changed(&changed_window_ids, &changed_image_handles) + || camera.is_added() + || camera_projection.is_changed() + || camera.computed.old_viewport_size != viewport_size + || camera.computed.old_sub_camera_view != camera.sub_camera_view) + { + let new_computed_target_info = normalized_target.get_render_target_info( + windows, + &images, + &manual_texture_views, + )?; + // Check for the scale factor changing, and resize the viewport if needed. + // This can happen when the window is moved between monitors with different DPIs. + // Without this, the viewport will take a smaller portion of the window moved to + // a higher DPI monitor. + if normalized_target.is_changed(&scale_factor_changed_window_ids, &HashSet::default()) + && let Some(old_scale_factor) = camera + .computed + .target_info + .as_ref() + .map(|info| info.scale_factor) + { + let resize_factor = new_computed_target_info.scale_factor / old_scale_factor; + if let Some(ref mut viewport) = camera.viewport { + let resize = |vec: UVec2| (vec.as_vec2() * resize_factor).as_uvec2(); + viewport.physical_position = resize(viewport.physical_position); + viewport.physical_size = resize(viewport.physical_size); + viewport_size = Some(viewport.physical_size); + } + } + // This check is needed because when changing WindowMode to Fullscreen, the viewport may have invalid + // arguments due to a sudden change on the window size to a lower value. + // If the size of the window is lower, the viewport will match that lower value. + if let Some(viewport) = &mut camera.viewport { + viewport.clamp_to_size(new_computed_target_info.physical_size); + } + camera.computed.target_info = Some(new_computed_target_info); + if let Some(size) = camera.logical_viewport_size() + && size.x != 0.0 + && size.y != 0.0 + { + camera_projection.update(size.x, size.y); + camera.computed.clip_from_view = match &camera.sub_camera_view { + Some(sub_view) => camera_projection.get_clip_from_view_for_sub(sub_view), + None => camera_projection.get_clip_from_view(), + } + } + } + + if camera.computed.old_viewport_size != viewport_size { + camera.computed.old_viewport_size = viewport_size; + } + + if camera.computed.old_sub_camera_view != camera.sub_camera_view { + camera.computed.old_sub_camera_view = camera.sub_camera_view; + } + } + Ok(()) +} + +#[derive(Component, Debug)] +pub struct ExtractedCamera { + pub target: Option, + pub physical_viewport_size: Option, + pub physical_target_size: Option, + pub viewport: Option, + pub render_graph: InternedRenderSubGraph, + pub order: isize, + pub output_mode: CameraOutputMode, + pub msaa_writeback: bool, + pub clear_color: ClearColorConfig, + pub sorted_camera_index_for_target: usize, + pub exposure: f32, + pub hdr: bool, +} + +pub fn extract_cameras( + mut commands: Commands, + query: Extract< + Query<( + Entity, + RenderEntity, + &Camera, + &CameraRenderGraph, + &GlobalTransform, + &VisibleEntities, + &Frustum, + Has, + Option<&ColorGrading>, + Option<&Exposure>, + Option<&TemporalJitter>, + Option<&MipBias>, + Option<&RenderLayers>, + Option<&Projection>, + Has, + )>, + >, + primary_window: Extract>>, + gpu_preprocessing_support: Res, + mapper: Extract>, +) { + let primary_window = primary_window.iter().next(); + type ExtractedCameraComponents = ( + ExtractedCamera, + ExtractedView, + RenderVisibleEntities, + TemporalJitter, + MipBias, + RenderLayers, + Projection, + NoIndirectDrawing, + ViewUniformOffset, + ); + for ( + main_entity, + render_entity, + camera, + camera_render_graph, + transform, + visible_entities, + frustum, + hdr, + color_grading, + exposure, + temporal_jitter, + mip_bias, + render_layers, + projection, + no_indirect_drawing, + ) in query.iter() + { + if !camera.is_active { + commands + .entity(render_entity) + .remove::(); + continue; + } + + let color_grading = color_grading.unwrap_or(&ColorGrading::default()).clone(); + + if let ( + Some(URect { + min: viewport_origin, + .. + }), + Some(viewport_size), + Some(target_size), + ) = ( + camera.physical_viewport_rect(), + camera.physical_viewport_size(), + camera.physical_target_size(), + ) { + if target_size.x == 0 || target_size.y == 0 { + commands + .entity(render_entity) + .remove::(); + continue; + } + + let render_visible_entities = RenderVisibleEntities { + entities: visible_entities + .entities + .iter() + .map(|(type_id, entities)| { + let entities = entities + .iter() + .map(|entity| { + let render_entity = mapper + .get(*entity) + .cloned() + .map(|entity| entity.id()) + .unwrap_or(Entity::PLACEHOLDER); + (render_entity, (*entity).into()) + }) + .collect(); + (*type_id, entities) + }) + .collect(), + }; + + let mut commands = commands.entity(render_entity); + commands.insert(( + ExtractedCamera { + target: camera.target.normalize(primary_window), + viewport: camera.viewport.clone(), + physical_viewport_size: Some(viewport_size), + physical_target_size: Some(target_size), + render_graph: camera_render_graph.0, + order: camera.order, + output_mode: camera.output_mode, + msaa_writeback: camera.msaa_writeback, + clear_color: camera.clear_color, + // this will be set in sort_cameras + sorted_camera_index_for_target: 0, + exposure: exposure + .map(Exposure::exposure) + .unwrap_or_else(|| Exposure::default().exposure()), + hdr, + }, + ExtractedView { + retained_view_entity: RetainedViewEntity::new(main_entity.into(), None, 0), + clip_from_view: camera.clip_from_view(), + world_from_view: *transform, + clip_from_world: None, + hdr, + viewport: UVec4::new( + viewport_origin.x, + viewport_origin.y, + viewport_size.x, + viewport_size.y, + ), + color_grading, + }, + render_visible_entities, + *frustum, + )); + + if let Some(temporal_jitter) = temporal_jitter { + commands.insert(temporal_jitter.clone()); + } else { + commands.remove::(); + } + + if let Some(mip_bias) = mip_bias { + commands.insert(mip_bias.clone()); + } else { + commands.remove::(); + } + + if let Some(render_layers) = render_layers { + commands.insert(render_layers.clone()); + } else { + commands.remove::(); + } + + if let Some(projection) = projection { + commands.insert(projection.clone()); + } else { + commands.remove::(); + } + + if no_indirect_drawing + || !matches!( + gpu_preprocessing_support.max_supported_mode, + GpuPreprocessingMode::Culling + ) + { + commands.insert(NoIndirectDrawing); + } else { + commands.remove::(); + } + }; + } +} + +/// Cameras sorted by their order field. This is updated in the [`sort_cameras`] system. +#[derive(Resource, Default)] +pub struct SortedCameras(pub Vec); + +pub struct SortedCamera { + pub entity: Entity, + pub order: isize, + pub target: Option, + pub hdr: bool, +} + +pub fn sort_cameras( + mut sorted_cameras: ResMut, + mut cameras: Query<(Entity, &mut ExtractedCamera)>, +) { + sorted_cameras.0.clear(); + for (entity, camera) in cameras.iter() { + sorted_cameras.0.push(SortedCamera { + entity, + order: camera.order, + target: camera.target.clone(), + hdr: camera.hdr, + }); + } + // sort by order and ensure within an order, RenderTargets of the same type are packed together + sorted_cameras + .0 + .sort_by(|c1, c2| (c1.order, &c1.target).cmp(&(c2.order, &c2.target))); + let mut previous_order_target = None; + let mut ambiguities = >::default(); + let mut target_counts = >::default(); + for sorted_camera in &mut sorted_cameras.0 { + let new_order_target = (sorted_camera.order, sorted_camera.target.clone()); + if let Some(previous_order_target) = previous_order_target + && previous_order_target == new_order_target + { + ambiguities.insert(new_order_target.clone()); + } + if let Some(target) = &sorted_camera.target { + let count = target_counts + .entry((target.clone(), sorted_camera.hdr)) + .or_insert(0usize); + let (_, mut camera) = cameras.get_mut(sorted_camera.entity).unwrap(); + camera.sorted_camera_index_for_target = *count; + *count += 1; + } + previous_order_target = Some(new_order_target); + } + + if !ambiguities.is_empty() { + warn!( + "Camera order ambiguities detected for active cameras with the following priorities: {:?}. \ + To fix this, ensure there is exactly one Camera entity spawned with a given order for a given RenderTarget. \ + Ambiguities should be resolved because either (1) multiple active cameras were spawned accidentally, which will \ + result in rendering multiple instances of the scene or (2) for cases where multiple active cameras is intentional, \ + ambiguities could result in unpredictable render results.", + ambiguities + ); + } +} + +/// A subpixel offset to jitter a perspective camera's frustum by. +/// +/// Useful for temporal rendering techniques. +#[derive(Component, Clone, Default, Reflect)] +#[reflect(Default, Component, Clone)] +pub struct TemporalJitter { + /// Offset is in range [-0.5, 0.5]. + pub offset: Vec2, +} + +impl TemporalJitter { + pub fn jitter_projection(&self, clip_from_view: &mut Mat4, view_size: Vec2) { + // https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/blob/d7531ae47d8b36a5d4025663e731a47a38be882f/docs/techniques/media/super-resolution-temporal/jitter-space.svg + let mut jitter = (self.offset * vec2(2.0, -2.0)) / view_size; + + // orthographic + if clip_from_view.w_axis.w == 1.0 { + jitter *= vec2(clip_from_view.x_axis.x, clip_from_view.y_axis.y) * 0.5; + } + + clip_from_view.z_axis.x += jitter.x; + clip_from_view.z_axis.y += jitter.y; + } +} + +/// Camera component specifying a mip bias to apply when sampling from material textures. +/// +/// Often used in conjunction with antialiasing post-process effects to reduce textures blurriness. +#[derive(Component, Reflect, Clone)] +#[reflect(Default, Component)] +pub struct MipBias(pub f32); + +impl Default for MipBias { + fn default() -> Self { + Self(-1.0) + } +} diff --git a/crates/libmarathon/src/render/color_operations.wgsl b/crates/libmarathon/src/render/color_operations.wgsl new file mode 100644 index 0000000..b68ad2a --- /dev/null +++ b/crates/libmarathon/src/render/color_operations.wgsl @@ -0,0 +1,47 @@ +#define_import_path bevy_render::color_operations + +#import bevy_render::maths::FRAC_PI_3 + +// Converts HSV to RGB. +// +// Input: H ∈ [0, 2π), S ∈ [0, 1], V ∈ [0, 1]. +// Output: R ∈ [0, 1], G ∈ [0, 1], B ∈ [0, 1]. +// +// +fn hsv_to_rgb(hsv: vec3) -> vec3 { + let n = vec3(5.0, 3.0, 1.0); + let k = (n + hsv.x / FRAC_PI_3) % 6.0; + return hsv.z - hsv.z * hsv.y * max(vec3(0.0), min(k, min(4.0 - k, vec3(1.0)))); +} + +// Converts RGB to HSV. +// +// Input: R ∈ [0, 1], G ∈ [0, 1], B ∈ [0, 1]. +// Output: H ∈ [0, 2π), S ∈ [0, 1], V ∈ [0, 1]. +// +// +fn rgb_to_hsv(rgb: vec3) -> vec3 { + let x_max = max(rgb.r, max(rgb.g, rgb.b)); // i.e. V + let x_min = min(rgb.r, min(rgb.g, rgb.b)); + let c = x_max - x_min; // chroma + + var swizzle = vec3(0.0); + if (x_max == rgb.r) { + swizzle = vec3(rgb.gb, 0.0); + } else if (x_max == rgb.g) { + swizzle = vec3(rgb.br, 2.0); + } else { + swizzle = vec3(rgb.rg, 4.0); + } + + let h = FRAC_PI_3 * (((swizzle.x - swizzle.y) / c + swizzle.z) % 6.0); + + // Avoid division by zero. + var s = 0.0; + if (x_max > 0.0) { + s = c / x_max; + } + + return vec3(h, s, x_max); +} + diff --git a/crates/libmarathon/src/render/core_2d/main_opaque_pass_2d_node.rs b/crates/libmarathon/src/render/core_2d/main_opaque_pass_2d_node.rs new file mode 100644 index 0000000..5e8f9f3 --- /dev/null +++ b/crates/libmarathon/src/render/core_2d/main_opaque_pass_2d_node.rs @@ -0,0 +1,106 @@ +use crate::render::core_2d::Opaque2d; +use bevy_ecs::{prelude::World, query::QueryItem}; +use crate::render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_phase::{TrackedRenderPass, ViewBinnedRenderPhases}, + render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp}, + renderer::RenderContext, + view::{ExtractedView, ViewDepthTexture, ViewTarget}, +}; +use tracing::error; +#[cfg(feature = "trace")] +use tracing::info_span; + +use super::AlphaMask2d; + +/// A [`bevy_render::render_graph::Node`] that runs the +/// [`Opaque2d`] [`ViewBinnedRenderPhases`] and [`AlphaMask2d`] [`ViewBinnedRenderPhases`] +#[derive(Default)] +pub struct MainOpaquePass2dNode; +impl ViewNode for MainOpaquePass2dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ExtractedView, + &'static ViewTarget, + &'static ViewDepthTexture, + ); + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let (Some(opaque_phases), Some(alpha_mask_phases)) = ( + world.get_resource::>(), + world.get_resource::>(), + ) else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let color_attachments = [Some(target.get_color_attachment())]; + let depth_stencil_attachment = Some(depth.get_attachment(StoreOp::Store)); + + let view_entity = graph.view_entity(); + let (Some(opaque_phase), Some(alpha_mask_phase)) = ( + opaque_phases.get(&view.retained_view_entity), + alpha_mask_phases.get(&view.retained_view_entity), + ) else { + return Ok(()); + }; + render_context.add_command_buffer_generation_task(move |render_device| { + #[cfg(feature = "trace")] + let _main_opaque_pass_2d_span = info_span!("main_opaque_pass_2d").entered(); + + // Command encoder setup + let mut command_encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("main_opaque_pass_2d_command_encoder"), + }); + + // Render pass setup + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("main_opaque_pass_2d"), + color_attachments: &color_attachments, + depth_stencil_attachment, + timestamp_writes: None, + occlusion_query_set: None, + }); + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); + let pass_span = diagnostics.pass_span(&mut render_pass, "main_opaque_pass_2d"); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + // Opaque draws + if !opaque_phase.is_empty() { + #[cfg(feature = "trace")] + let _opaque_main_pass_2d_span = info_span!("opaque_main_pass_2d").entered(); + if let Err(err) = opaque_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the 2d opaque phase {err:?}"); + } + } + + // Alpha mask draws + if !alpha_mask_phase.is_empty() { + #[cfg(feature = "trace")] + let _alpha_mask_main_pass_2d_span = info_span!("alpha_mask_main_pass_2d").entered(); + if let Err(err) = alpha_mask_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the 2d alpha mask phase {err:?}"); + } + } + + pass_span.end(&mut render_pass); + drop(render_pass); + command_encoder.finish() + }); + + Ok(()) + } +} diff --git a/crates/libmarathon/src/render/core_2d/main_transparent_pass_2d_node.rs b/crates/libmarathon/src/render/core_2d/main_transparent_pass_2d_node.rs new file mode 100644 index 0000000..7e890a7 --- /dev/null +++ b/crates/libmarathon/src/render/core_2d/main_transparent_pass_2d_node.rs @@ -0,0 +1,120 @@ +use crate::render::core_2d::Transparent2d; +use bevy_ecs::prelude::*; +use crate::render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_phase::{TrackedRenderPass, ViewSortedRenderPhases}, + render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp}, + renderer::RenderContext, + view::{ExtractedView, ViewDepthTexture, ViewTarget}, +}; +use tracing::error; +#[cfg(feature = "trace")] +use tracing::info_span; + +#[derive(Default)] +pub struct MainTransparentPass2dNode {} + +impl ViewNode for MainTransparentPass2dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ExtractedView, + &'static ViewTarget, + &'static ViewDepthTexture, + ); + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + (camera, view, target, depth): bevy_ecs::query::QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let Some(transparent_phases) = + world.get_resource::>() + else { + return Ok(()); + }; + + let view_entity = graph.view_entity(); + let Some(transparent_phase) = transparent_phases.get(&view.retained_view_entity) else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let color_attachments = [Some(target.get_color_attachment())]; + // NOTE: For the transparent pass we load the depth buffer. There should be no + // need to write to it, but store is set to `true` as a workaround for issue #3776, + // https://github.com/bevyengine/bevy/issues/3776 + // so that wgpu does not clear the depth buffer. + // As the opaque and alpha mask passes run first, opaque meshes can occlude + // transparent ones. + let depth_stencil_attachment = Some(depth.get_attachment(StoreOp::Store)); + + render_context.add_command_buffer_generation_task(move |render_device| { + // Command encoder setup + let mut command_encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("main_transparent_pass_2d_command_encoder"), + }); + + // This needs to run at least once to clear the background color, even if there are no items to render + { + #[cfg(feature = "trace")] + let _main_pass_2d = info_span!("main_transparent_pass_2d").entered(); + + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("main_transparent_pass_2d"), + color_attachments: &color_attachments, + depth_stencil_attachment, + timestamp_writes: None, + occlusion_query_set: None, + }); + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); + + let pass_span = diagnostics.pass_span(&mut render_pass, "main_transparent_pass_2d"); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + if !transparent_phase.items.is_empty() { + #[cfg(feature = "trace")] + let _transparent_main_pass_2d_span = + info_span!("transparent_main_pass_2d").entered(); + if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity) + { + error!( + "Error encountered while rendering the transparent 2D phase {err:?}" + ); + } + } + + pass_span.end(&mut render_pass); + } + + // WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't + // reset for the next render pass so add an empty render pass without a custom viewport + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + if camera.viewport.is_some() { + #[cfg(feature = "trace")] + let _reset_viewport_pass_2d = info_span!("reset_viewport_pass_2d").entered(); + let pass_descriptor = RenderPassDescriptor { + label: Some("reset_viewport_pass_2d"), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + command_encoder.begin_render_pass(&pass_descriptor); + } + + command_encoder.finish() + }); + + Ok(()) + } +} diff --git a/crates/libmarathon/src/render/core_2d/mod.rs b/crates/libmarathon/src/render/core_2d/mod.rs new file mode 100644 index 0000000..b521f87 --- /dev/null +++ b/crates/libmarathon/src/render/core_2d/mod.rs @@ -0,0 +1,508 @@ +mod main_opaque_pass_2d_node; +mod main_transparent_pass_2d_node; + +pub mod graph { + use crate::render::render_graph::{RenderLabel, RenderSubGraph}; + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderSubGraph)] + pub struct Core2d; + + pub mod input { + pub const VIEW_ENTITY: &str = "view_entity"; + } + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + pub enum Node2d { + MsaaWriteback, + StartMainPass, + MainOpaquePass, + MainTransparentPass, + EndMainPass, + Wireframe, + StartMainPassPostProcessing, + Bloom, + PostProcessing, + Tonemapping, + Fxaa, + Smaa, + Upscaling, + ContrastAdaptiveSharpening, + EndMainPassPostProcessing, + } +} + +use core::ops::Range; + +use bevy_asset::UntypedAssetId; +use bevy_camera::{Camera, Camera2d}; +use bevy_image::ToExtents; +use bevy_platform::collections::{HashMap, HashSet}; +use crate::render::{ + batching::gpu_preprocessing::GpuPreprocessingMode, + camera::CameraRenderGraph, + render_phase::PhaseItemBatchSetKey, + view::{ExtractedView, RetainedViewEntity}, +}; +pub use main_opaque_pass_2d_node::*; +pub use main_transparent_pass_2d_node::*; + +use crate::render::{ + tonemapping::{DebandDither, Tonemapping, TonemappingNode}, + upscaling::UpscalingNode, +}; +use bevy_app::{App, Plugin}; +use bevy_ecs::prelude::*; +use bevy_math::FloatOrd; +use crate::render::{ + camera::ExtractedCamera, + extract_component::ExtractComponentPlugin, + render_graph::{EmptyNode, RenderGraphExt, ViewNodeRunner}, + render_phase::{ + sort_phase_system, BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, + DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewBinnedRenderPhases, + ViewSortedRenderPhases, + }, + render_resource::{ + BindGroupId, CachedRenderPipelineId, TextureDescriptor, TextureDimension, TextureFormat, + TextureUsages, + }, + renderer::RenderDevice, + sync_world::MainEntity, + texture::TextureCache, + view::{Msaa, ViewDepthTexture}, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; + +use self::graph::{Core2d, Node2d}; + +pub const CORE_2D_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float; + +pub struct Core2dPlugin; + +impl Plugin for Core2dPlugin { + fn build(&self, app: &mut App) { + app.register_required_components::() + .register_required_components_with::(|| { + CameraRenderGraph::new(Core2d) + }) + .register_required_components_with::(|| Tonemapping::None) + .add_plugins(ExtractComponentPlugin::::default()); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .add_systems(ExtractSchedule, extract_core_2d_camera_phases) + .add_systems( + Render, + ( + sort_phase_system::.in_set(RenderSystems::PhaseSort), + prepare_core_2d_depth_textures.in_set(RenderSystems::PrepareResources), + ), + ); + + render_app + .add_render_sub_graph(Core2d) + .add_render_graph_node::(Core2d, Node2d::StartMainPass) + .add_render_graph_node::>( + Core2d, + Node2d::MainOpaquePass, + ) + .add_render_graph_node::>( + Core2d, + Node2d::MainTransparentPass, + ) + .add_render_graph_node::(Core2d, Node2d::EndMainPass) + .add_render_graph_node::(Core2d, Node2d::StartMainPassPostProcessing) + .add_render_graph_node::>(Core2d, Node2d::Tonemapping) + .add_render_graph_node::(Core2d, Node2d::EndMainPassPostProcessing) + .add_render_graph_node::>(Core2d, Node2d::Upscaling) + .add_render_graph_edges( + Core2d, + ( + Node2d::StartMainPass, + Node2d::MainOpaquePass, + Node2d::MainTransparentPass, + Node2d::EndMainPass, + Node2d::StartMainPassPostProcessing, + Node2d::Tonemapping, + Node2d::EndMainPassPostProcessing, + Node2d::Upscaling, + ), + ); + } +} + +/// Opaque 2D [`BinnedPhaseItem`]s. +pub struct Opaque2d { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: BatchSetKey2d, + /// The key, which determines which can be batched. + pub bin_key: Opaque2dBinKey, + /// An entity from which data will be fetched, including the mesh if + /// applicable. + pub representative_entity: (Entity, MainEntity), + /// The ranges of instances. + pub batch_range: Range, + /// An extra index, which is either a dynamic offset or an index in the + /// indirect parameters list. + pub extra_index: PhaseItemExtraIndex, +} + +/// Data that must be identical in order to batch phase items together. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Opaque2dBinKey { + /// The identifier of the render pipeline. + pub pipeline: CachedRenderPipelineId, + /// The function used to draw. + pub draw_function: DrawFunctionId, + /// The asset that this phase item is associated with. + /// + /// Normally, this is the ID of the mesh, but for non-mesh items it might be + /// the ID of another type of asset. + pub asset_id: UntypedAssetId, + /// The ID of a bind group specific to the material. + pub material_bind_group_id: Option, +} + +impl PhaseItem for Opaque2d { + #[inline] + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.bin_key.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl BinnedPhaseItem for Opaque2d { + // Since 2D meshes presently can't be multidrawn, the batch set key is + // irrelevant. + type BatchSetKey = BatchSetKey2d; + + type BinKey = Opaque2dBinKey; + + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Opaque2d { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +/// 2D meshes aren't currently multi-drawn together, so this batch set key only +/// stores whether the mesh is indexed. +#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +pub struct BatchSetKey2d { + /// True if the mesh is indexed. + pub indexed: bool, +} + +impl PhaseItemBatchSetKey for BatchSetKey2d { + fn indexed(&self) -> bool { + self.indexed + } +} + +impl CachedRenderPipelinePhaseItem for Opaque2d { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.bin_key.pipeline + } +} + +/// Alpha mask 2D [`BinnedPhaseItem`]s. +pub struct AlphaMask2d { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: BatchSetKey2d, + /// The key, which determines which can be batched. + pub bin_key: AlphaMask2dBinKey, + /// An entity from which data will be fetched, including the mesh if + /// applicable. + pub representative_entity: (Entity, MainEntity), + /// The ranges of instances. + pub batch_range: Range, + /// An extra index, which is either a dynamic offset or an index in the + /// indirect parameters list. + pub extra_index: PhaseItemExtraIndex, +} + +/// Data that must be identical in order to batch phase items together. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AlphaMask2dBinKey { + /// The identifier of the render pipeline. + pub pipeline: CachedRenderPipelineId, + /// The function used to draw. + pub draw_function: DrawFunctionId, + /// The asset that this phase item is associated with. + /// + /// Normally, this is the ID of the mesh, but for non-mesh items it might be + /// the ID of another type of asset. + pub asset_id: UntypedAssetId, + /// The ID of a bind group specific to the material. + pub material_bind_group_id: Option, +} + +impl PhaseItem for AlphaMask2d { + #[inline] + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + #[inline] + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.bin_key.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl BinnedPhaseItem for AlphaMask2d { + // Since 2D meshes presently can't be multidrawn, the batch set key is + // irrelevant. + type BatchSetKey = BatchSetKey2d; + + type BinKey = AlphaMask2dBinKey; + + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + AlphaMask2d { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +impl CachedRenderPipelinePhaseItem for AlphaMask2d { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.bin_key.pipeline + } +} + +/// Transparent 2D [`SortedPhaseItem`]s. +pub struct Transparent2d { + pub sort_key: FloatOrd, + pub entity: (Entity, MainEntity), + pub pipeline: CachedRenderPipelineId, + pub draw_function: DrawFunctionId, + pub batch_range: Range, + pub extracted_index: usize, + pub extra_index: PhaseItemExtraIndex, + /// Whether the mesh in question is indexed (uses an index buffer in + /// addition to its vertex buffer). + pub indexed: bool, +} + +impl PhaseItem for Transparent2d { + #[inline] + fn entity(&self) -> Entity { + self.entity.0 + } + + #[inline] + fn main_entity(&self) -> MainEntity { + self.entity.1 + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + #[inline] + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl SortedPhaseItem for Transparent2d { + type SortKey = FloatOrd; + + #[inline] + fn sort_key(&self) -> Self::SortKey { + self.sort_key + } + + #[inline] + fn sort(items: &mut [Self]) { + // radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`. + radsort::sort_by_key(items, |item| item.sort_key().0); + } + + fn indexed(&self) -> bool { + self.indexed + } +} + +impl CachedRenderPipelinePhaseItem for Transparent2d { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline + } +} + +pub fn extract_core_2d_camera_phases( + mut transparent_2d_phases: ResMut>, + mut opaque_2d_phases: ResMut>, + mut alpha_mask_2d_phases: ResMut>, + cameras_2d: Extract>>, + mut live_entities: Local>, +) { + live_entities.clear(); + + for (main_entity, camera) in &cameras_2d { + if !camera.is_active { + continue; + } + + // This is the main 2D camera, so we use the first subview index (0). + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + + transparent_2d_phases.insert_or_clear(retained_view_entity); + opaque_2d_phases.prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None); + alpha_mask_2d_phases + .prepare_for_new_frame(retained_view_entity, GpuPreprocessingMode::None); + + live_entities.insert(retained_view_entity); + } + + // Clear out all dead views. + transparent_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity)); + opaque_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity)); + alpha_mask_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity)); +} + +pub fn prepare_core_2d_depth_textures( + mut commands: Commands, + mut texture_cache: ResMut, + render_device: Res, + transparent_2d_phases: Res>, + opaque_2d_phases: Res>, + views_2d: Query<(Entity, &ExtractedCamera, &ExtractedView, &Msaa), (With,)>, +) { + let mut textures = >::default(); + for (view, camera, extracted_view, msaa) in &views_2d { + if !opaque_2d_phases.contains_key(&extracted_view.retained_view_entity) + || !transparent_2d_phases.contains_key(&extracted_view.retained_view_entity) + { + continue; + }; + + let Some(physical_target_size) = camera.physical_target_size else { + continue; + }; + + let cached_texture = textures + .entry(camera.target.clone()) + .or_insert_with(|| { + let descriptor = TextureDescriptor { + label: Some("view_depth_texture"), + // The size of the depth texture + size: physical_target_size.to_extents(), + mip_level_count: 1, + sample_count: msaa.samples(), + dimension: TextureDimension::D2, + format: CORE_2D_DEPTH_FORMAT, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }; + + texture_cache.get(&render_device, descriptor) + }) + .clone(); + + commands + .entity(view) + .insert(ViewDepthTexture::new(cached_texture, Some(0.0))); + } +} diff --git a/crates/libmarathon/src/render/core_3d/main_opaque_pass_3d_node.rs b/crates/libmarathon/src/render/core_3d/main_opaque_pass_3d_node.rs new file mode 100644 index 0000000..f910606 --- /dev/null +++ b/crates/libmarathon/src/render/core_3d/main_opaque_pass_3d_node.rs @@ -0,0 +1,142 @@ +use crate::render::{ + core_3d::Opaque3d, + skybox::{SkyboxBindGroup, SkyboxPipelineId}, +}; +use bevy_camera::{MainPassResolutionOverride, Viewport}; +use bevy_ecs::{prelude::World, query::QueryItem}; +use crate::render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_phase::{TrackedRenderPass, ViewBinnedRenderPhases}, + render_resource::{CommandEncoderDescriptor, PipelineCache, RenderPassDescriptor, StoreOp}, + renderer::RenderContext, + view::{ExtractedView, ViewDepthTexture, ViewTarget, ViewUniformOffset}, +}; +use tracing::error; +#[cfg(feature = "trace")] +use tracing::info_span; + +use super::AlphaMask3d; + +/// A [`bevy_render::render_graph::Node`] that runs the [`Opaque3d`] and [`AlphaMask3d`] +/// [`ViewBinnedRenderPhases`]s. +#[derive(Default)] +pub struct MainOpaquePass3dNode; +impl ViewNode for MainOpaquePass3dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ExtractedView, + &'static ViewTarget, + &'static ViewDepthTexture, + Option<&'static SkyboxPipelineId>, + Option<&'static SkyboxBindGroup>, + &'static ViewUniformOffset, + Option<&'static MainPassResolutionOverride>, + ); + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + ( + camera, + extracted_view, + target, + depth, + skybox_pipeline, + skybox_bind_group, + view_uniform_offset, + resolution_override, + ): QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let (Some(opaque_phases), Some(alpha_mask_phases)) = ( + world.get_resource::>(), + world.get_resource::>(), + ) else { + return Ok(()); + }; + + let (Some(opaque_phase), Some(alpha_mask_phase)) = ( + opaque_phases.get(&extracted_view.retained_view_entity), + alpha_mask_phases.get(&extracted_view.retained_view_entity), + ) else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let color_attachments = [Some(target.get_color_attachment())]; + let depth_stencil_attachment = Some(depth.get_attachment(StoreOp::Store)); + + let view_entity = graph.view_entity(); + render_context.add_command_buffer_generation_task(move |render_device| { + #[cfg(feature = "trace")] + let _main_opaque_pass_3d_span = info_span!("main_opaque_pass_3d").entered(); + + // Command encoder setup + let mut command_encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("main_opaque_pass_3d_command_encoder"), + }); + + // Render pass setup + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("main_opaque_pass_3d"), + color_attachments: &color_attachments, + depth_stencil_attachment, + timestamp_writes: None, + occlusion_query_set: None, + }); + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); + let pass_span = diagnostics.pass_span(&mut render_pass, "main_opaque_pass_3d"); + + if let Some(viewport) = + Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override) + { + render_pass.set_camera_viewport(&viewport); + } + + // Opaque draws + if !opaque_phase.is_empty() { + #[cfg(feature = "trace")] + let _opaque_main_pass_3d_span = info_span!("opaque_main_pass_3d").entered(); + if let Err(err) = opaque_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the opaque phase {err:?}"); + } + } + + // Alpha draws + if !alpha_mask_phase.is_empty() { + #[cfg(feature = "trace")] + let _alpha_mask_main_pass_3d_span = info_span!("alpha_mask_main_pass_3d").entered(); + if let Err(err) = alpha_mask_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the alpha mask phase {err:?}"); + } + } + + // Skybox draw using a fullscreen triangle + if let (Some(skybox_pipeline), Some(SkyboxBindGroup(skybox_bind_group))) = + (skybox_pipeline, skybox_bind_group) + { + let pipeline_cache = world.resource::(); + if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_pipeline.0) { + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group( + 0, + &skybox_bind_group.0, + &[view_uniform_offset.offset, skybox_bind_group.1], + ); + render_pass.draw(0..3, 0..1); + } + } + + pass_span.end(&mut render_pass); + drop(render_pass); + command_encoder.finish() + }); + + Ok(()) + } +} diff --git a/crates/libmarathon/src/render/core_3d/main_transmissive_pass_3d_node.rs b/crates/libmarathon/src/render/core_3d/main_transmissive_pass_3d_node.rs new file mode 100644 index 0000000..d67c748 --- /dev/null +++ b/crates/libmarathon/src/render/core_3d/main_transmissive_pass_3d_node.rs @@ -0,0 +1,167 @@ +use super::ViewTransmissionTexture; +use crate::render::core_3d::Transmissive3d; +use bevy_camera::{Camera3d, MainPassResolutionOverride, Viewport}; +use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_image::ToExtents; +use crate::render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_phase::ViewSortedRenderPhases, + render_resource::{RenderPassDescriptor, StoreOp}, + renderer::RenderContext, + view::{ExtractedView, ViewDepthTexture, ViewTarget}, +}; +use core::ops::Range; +use tracing::error; +#[cfg(feature = "trace")] +use tracing::info_span; + +/// A [`bevy_render::render_graph::Node`] that runs the [`Transmissive3d`] +/// [`ViewSortedRenderPhases`]. +#[derive(Default)] +pub struct MainTransmissivePass3dNode; + +impl ViewNode for MainTransmissivePass3dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ExtractedView, + &'static Camera3d, + &'static ViewTarget, + Option<&'static ViewTransmissionTexture>, + &'static ViewDepthTexture, + Option<&'static MainPassResolutionOverride>, + ); + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (camera, view, camera_3d, target, transmission, depth, resolution_override): QueryItem< + Self::ViewQuery, + >, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.view_entity(); + + let Some(transmissive_phases) = + world.get_resource::>() + else { + return Ok(()); + }; + + let Some(transmissive_phase) = transmissive_phases.get(&view.retained_view_entity) else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let physical_target_size = camera.physical_target_size.unwrap(); + + let render_pass_descriptor = RenderPassDescriptor { + label: Some("main_transmissive_pass_3d"), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)), + timestamp_writes: None, + occlusion_query_set: None, + }; + + // Run the transmissive pass, sorted back-to-front + // NOTE: Scoped to drop the mutable borrow of render_context + #[cfg(feature = "trace")] + let _main_transmissive_pass_3d_span = info_span!("main_transmissive_pass_3d").entered(); + + if !transmissive_phase.items.is_empty() { + let screen_space_specular_transmission_steps = + camera_3d.screen_space_specular_transmission_steps; + if screen_space_specular_transmission_steps > 0 { + let transmission = + transmission.expect("`ViewTransmissionTexture` should exist at this point"); + + // `transmissive_phase.items` are depth sorted, so we split them into N = `screen_space_specular_transmission_steps` + // ranges, rendering them back-to-front in multiple steps, allowing multiple levels of transparency. + // + // Note: For the sake of simplicity, we currently split items evenly among steps. In the future, we + // might want to use a more sophisticated heuristic (e.g. based on view bounds, or with an exponential + // falloff so that nearby objects have more levels of transparency available to them) + for range in split_range( + 0..transmissive_phase.items.len(), + screen_space_specular_transmission_steps, + ) { + // Copy the main texture to the transmission texture, allowing to use the color output of the + // previous step (or of the `Opaque3d` phase, for the first step) as a transmissive color input + render_context.command_encoder().copy_texture_to_texture( + target.main_texture().as_image_copy(), + transmission.texture.as_image_copy(), + physical_target_size.to_extents(), + ); + + let mut render_pass = + render_context.begin_tracked_render_pass(render_pass_descriptor.clone()); + let pass_span = + diagnostics.pass_span(&mut render_pass, "main_transmissive_pass_3d"); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + // render items in range + if let Err(err) = + transmissive_phase.render_range(&mut render_pass, world, view_entity, range) + { + error!("Error encountered while rendering the transmissive phase {err:?}"); + } + + pass_span.end(&mut render_pass); + } + } else { + let mut render_pass = + render_context.begin_tracked_render_pass(render_pass_descriptor); + let pass_span = + diagnostics.pass_span(&mut render_pass, "main_transmissive_pass_3d"); + + if let Some(viewport) = Viewport::from_viewport_and_override( + camera.viewport.as_ref(), + resolution_override, + ) { + render_pass.set_camera_viewport(&viewport); + } + + if let Err(err) = transmissive_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the transmissive phase {err:?}"); + } + + pass_span.end(&mut render_pass); + } + } + + Ok(()) + } +} + +/// Splits a [`Range`] into at most `max_num_splits` sub-ranges without overlaps +/// +/// Properly takes into account remainders of inexact divisions (by adding extra +/// elements to the initial sub-ranges as needed) +fn split_range(range: Range, max_num_splits: usize) -> impl Iterator> { + let len = range.end - range.start; + assert!(len > 0, "to be split, a range must not be empty"); + assert!(max_num_splits > 0, "max_num_splits must be at least 1"); + let num_splits = max_num_splits.min(len); + let step = len / num_splits; + let mut rem = len % num_splits; + let mut start = range.start; + + (0..num_splits).map(move |_| { + let extra = if rem > 0 { + rem -= 1; + 1 + } else { + 0 + }; + let end = (start + step + extra).min(range.end); + let result = start..end; + start = end; + result + }) +} diff --git a/crates/libmarathon/src/render/core_3d/main_transparent_pass_3d_node.rs b/crates/libmarathon/src/render/core_3d/main_transparent_pass_3d_node.rs new file mode 100644 index 0000000..58dbd89 --- /dev/null +++ b/crates/libmarathon/src/render/core_3d/main_transparent_pass_3d_node.rs @@ -0,0 +1,107 @@ +use crate::render::core_3d::Transparent3d; +use bevy_camera::{MainPassResolutionOverride, Viewport}; +use bevy_ecs::{prelude::*, query::QueryItem}; +use crate::render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_phase::ViewSortedRenderPhases, + render_resource::{RenderPassDescriptor, StoreOp}, + renderer::RenderContext, + view::{ExtractedView, ViewDepthTexture, ViewTarget}, +}; +use tracing::error; +#[cfg(feature = "trace")] +use tracing::info_span; + +/// A [`bevy_render::render_graph::Node`] that runs the [`Transparent3d`] +/// [`ViewSortedRenderPhases`]. +#[derive(Default)] +pub struct MainTransparentPass3dNode; + +impl ViewNode for MainTransparentPass3dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ExtractedView, + &'static ViewTarget, + &'static ViewDepthTexture, + Option<&'static MainPassResolutionOverride>, + ); + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (camera, view, target, depth, resolution_override): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.view_entity(); + + let Some(transparent_phases) = + world.get_resource::>() + else { + return Ok(()); + }; + + let Some(transparent_phase) = transparent_phases.get(&view.retained_view_entity) else { + return Ok(()); + }; + + if !transparent_phase.items.is_empty() { + // Run the transparent pass, sorted back-to-front + // NOTE: Scoped to drop the mutable borrow of render_context + #[cfg(feature = "trace")] + let _main_transparent_pass_3d_span = info_span!("main_transparent_pass_3d").entered(); + + let diagnostics = render_context.diagnostic_recorder(); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("main_transparent_pass_3d"), + color_attachments: &[Some(target.get_color_attachment())], + // NOTE: For the transparent pass we load the depth buffer. There should be no + // need to write to it, but store is set to `true` as a workaround for issue #3776, + // https://github.com/bevyengine/bevy/issues/3776 + // so that wgpu does not clear the depth buffer. + // As the opaque and alpha mask passes run first, opaque meshes can occlude + // transparent ones. + depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)), + timestamp_writes: None, + occlusion_query_set: None, + }); + + let pass_span = diagnostics.pass_span(&mut render_pass, "main_transparent_pass_3d"); + + if let Some(viewport) = + Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override) + { + render_pass.set_camera_viewport(&viewport); + } + + if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the transparent phase {err:?}"); + } + + pass_span.end(&mut render_pass); + } + + // WebGL2 quirk: if ending with a render pass with a custom viewport, the viewport isn't + // reset for the next render pass so add an empty render pass without a custom viewport + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + if camera.viewport.is_some() { + #[cfg(feature = "trace")] + let _reset_viewport_pass_3d = info_span!("reset_viewport_pass_3d").entered(); + let pass_descriptor = RenderPassDescriptor { + label: Some("reset_viewport_pass_3d"), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + } + + Ok(()) + } +} diff --git a/crates/libmarathon/src/render/core_3d/mod.rs b/crates/libmarathon/src/render/core_3d/mod.rs new file mode 100644 index 0000000..04c61fb --- /dev/null +++ b/crates/libmarathon/src/render/core_3d/mod.rs @@ -0,0 +1,1150 @@ +mod main_opaque_pass_3d_node; +mod main_transmissive_pass_3d_node; +mod main_transparent_pass_3d_node; + +pub mod graph { + use crate::render::render_graph::{RenderLabel, RenderSubGraph}; + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderSubGraph)] + pub struct Core3d; + + pub mod input { + pub const VIEW_ENTITY: &str = "view_entity"; + } + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + pub enum Node3d { + MsaaWriteback, + EarlyPrepass, + EarlyDownsampleDepth, + LatePrepass, + EarlyDeferredPrepass, + LateDeferredPrepass, + CopyDeferredLightingId, + EndPrepasses, + StartMainPass, + MainOpaquePass, + MainTransmissivePass, + MainTransparentPass, + EndMainPass, + Wireframe, + StartMainPassPostProcessing, + LateDownsampleDepth, + MotionBlur, + Taa, + DlssSuperResolution, + DlssRayReconstruction, + Bloom, + AutoExposure, + DepthOfField, + PostProcessing, + Tonemapping, + Fxaa, + Smaa, + Upscaling, + ContrastAdaptiveSharpening, + EndMainPassPostProcessing, + } +} + +// PERF: vulkan docs recommend using 24 bit depth for better performance +pub const CORE_3D_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float; + +/// True if multisampled depth textures are supported on this platform. +/// +/// In theory, Naga supports depth textures on WebGL 2. In practice, it doesn't, +/// because of a silly bug whereby Naga assumes that all depth textures are +/// `sampler2DShadow` and will cheerfully generate invalid GLSL that tries to +/// perform non-percentage-closer-filtering with such a sampler. Therefore we +/// disable depth of field and screen space reflections entirely on WebGL 2. +#[cfg(not(any(feature = "webgpu", not(target_arch = "wasm32"))))] +pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = false; + +/// True if multisampled depth textures are supported on this platform. +/// +/// In theory, Naga supports depth textures on WebGL 2. In practice, it doesn't, +/// because of a silly bug whereby Naga assumes that all depth textures are +/// `sampler2DShadow` and will cheerfully generate invalid GLSL that tries to +/// perform non-percentage-closer-filtering with such a sampler. Therefore we +/// disable depth of field and screen space reflections entirely on WebGL 2. +#[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))] +pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = true; + +use core::ops::Range; + +use bevy_camera::{Camera, Camera3d, Camera3dDepthLoadOp}; +use crate::render::{ + batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, + camera::CameraRenderGraph, + experimental::occlusion_culling::OcclusionCulling, + mesh::allocator::SlabId, + render_phase::PhaseItemBatchSetKey, + view::{prepare_view_targets, NoIndirectDrawing, RetainedViewEntity}, +}; +pub use main_opaque_pass_3d_node::*; +pub use main_transparent_pass_3d_node::*; + +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_asset::UntypedAssetId; +use bevy_color::LinearRgba; +use bevy_ecs::prelude::*; +use bevy_image::{BevyDefault, ToExtents}; +use bevy_math::FloatOrd; +use bevy_platform::collections::{HashMap, HashSet}; +use crate::render::{ + camera::ExtractedCamera, + extract_component::ExtractComponentPlugin, + prelude::Msaa, + render_graph::{EmptyNode, RenderGraphExt, ViewNodeRunner}, + render_phase::{ + sort_phase_system, BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, + DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewBinnedRenderPhases, + ViewSortedRenderPhases, + }, + render_resource::{ + CachedRenderPipelineId, FilterMode, Sampler, SamplerDescriptor, Texture, TextureDescriptor, + TextureDimension, TextureFormat, TextureUsages, TextureView, + }, + renderer::RenderDevice, + sync_world::{MainEntity, RenderEntity}, + texture::{ColorAttachment, TextureCache}, + view::{ExtractedView, ViewDepthTexture, ViewTarget}, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use nonmax::NonMaxU32; +use tracing::warn; + +use crate::render::{ + core_3d::main_transmissive_pass_3d_node::MainTransmissivePass3dNode, + deferred::{ + copy_lighting_id::CopyDeferredLightingIdNode, + node::{EarlyDeferredGBufferPrepassNode, LateDeferredGBufferPrepassNode}, + AlphaMask3dDeferred, Opaque3dDeferred, DEFERRED_LIGHTING_PASS_ID_FORMAT, + DEFERRED_PREPASS_FORMAT, + }, + prepass::{ + node::{EarlyPrepassNode, LatePrepassNode}, + AlphaMask3dPrepass, DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass, + Opaque3dPrepass, OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey, + ViewPrepassTextures, MOTION_VECTOR_PREPASS_FORMAT, NORMAL_PREPASS_FORMAT, + }, + skybox::SkyboxPlugin, + tonemapping::{DebandDither, Tonemapping, TonemappingNode}, + upscaling::UpscalingNode, +}; + +use self::graph::{Core3d, Node3d}; + +pub struct Core3dPlugin; + +impl Plugin for Core3dPlugin { + fn build(&self, app: &mut App) { + app.register_required_components_with::(|| DebandDither::Enabled) + .register_required_components_with::(|| { + CameraRenderGraph::new(Core3d) + }) + .register_required_components::() + .add_plugins((SkyboxPlugin, ExtractComponentPlugin::::default())) + .add_systems(PostUpdate, check_msaa); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .add_systems(ExtractSchedule, extract_core_3d_camera_phases) + .add_systems(ExtractSchedule, extract_camera_prepass_phase) + .add_systems( + Render, + ( + sort_phase_system::.in_set(RenderSystems::PhaseSort), + sort_phase_system::.in_set(RenderSystems::PhaseSort), + configure_occlusion_culling_view_targets + .after(prepare_view_targets) + .in_set(RenderSystems::ManageViews), + prepare_core_3d_depth_textures.in_set(RenderSystems::PrepareResources), + prepare_core_3d_transmission_textures.in_set(RenderSystems::PrepareResources), + prepare_prepass_textures.in_set(RenderSystems::PrepareResources), + ), + ); + + render_app + .add_render_sub_graph(Core3d) + .add_render_graph_node::>(Core3d, Node3d::EarlyPrepass) + .add_render_graph_node::>(Core3d, Node3d::LatePrepass) + .add_render_graph_node::>( + Core3d, + Node3d::EarlyDeferredPrepass, + ) + .add_render_graph_node::>( + Core3d, + Node3d::LateDeferredPrepass, + ) + .add_render_graph_node::>( + Core3d, + Node3d::CopyDeferredLightingId, + ) + .add_render_graph_node::(Core3d, Node3d::EndPrepasses) + .add_render_graph_node::(Core3d, Node3d::StartMainPass) + .add_render_graph_node::>( + Core3d, + Node3d::MainOpaquePass, + ) + .add_render_graph_node::>( + Core3d, + Node3d::MainTransmissivePass, + ) + .add_render_graph_node::>( + Core3d, + Node3d::MainTransparentPass, + ) + .add_render_graph_node::(Core3d, Node3d::EndMainPass) + .add_render_graph_node::(Core3d, Node3d::StartMainPassPostProcessing) + .add_render_graph_node::>(Core3d, Node3d::Tonemapping) + .add_render_graph_node::(Core3d, Node3d::EndMainPassPostProcessing) + .add_render_graph_node::>(Core3d, Node3d::Upscaling) + .add_render_graph_edges( + Core3d, + ( + Node3d::EarlyPrepass, + Node3d::EarlyDeferredPrepass, + Node3d::LatePrepass, + Node3d::LateDeferredPrepass, + Node3d::CopyDeferredLightingId, + Node3d::EndPrepasses, + Node3d::StartMainPass, + Node3d::MainOpaquePass, + Node3d::MainTransmissivePass, + Node3d::MainTransparentPass, + Node3d::EndMainPass, + Node3d::StartMainPassPostProcessing, + Node3d::Tonemapping, + Node3d::EndMainPassPostProcessing, + Node3d::Upscaling, + ), + ); + } +} + +/// Opaque 3D [`BinnedPhaseItem`]s. +pub struct Opaque3d { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: Opaque3dBatchSetKey, + /// The key, which determines which can be batched. + pub bin_key: Opaque3dBinKey, + /// An entity from which data will be fetched, including the mesh if + /// applicable. + pub representative_entity: (Entity, MainEntity), + /// The ranges of instances. + pub batch_range: Range, + /// An extra index, which is either a dynamic offset or an index in the + /// indirect parameters list. + pub extra_index: PhaseItemExtraIndex, +} + +/// Information that must be identical in order to place opaque meshes in the +/// same *batch set*. +/// +/// A batch set is a set of batches that can be multi-drawn together, if +/// multi-draw is in use. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Opaque3dBatchSetKey { + /// The identifier of the render pipeline. + pub pipeline: CachedRenderPipelineId, + + /// The function used to draw. + pub draw_function: DrawFunctionId, + + /// The ID of a bind group specific to the material instance. + /// + /// In the case of PBR, this is the `MaterialBindGroupIndex`. + pub material_bind_group_index: Option, + + /// The ID of the slab of GPU memory that contains vertex data. + /// + /// For non-mesh items, you can fill this with 0 if your items can be + /// multi-drawn, or with a unique value if they can't. + pub vertex_slab: SlabId, + + /// The ID of the slab of GPU memory that contains index data, if present. + /// + /// For non-mesh items, you can safely fill this with `None`. + pub index_slab: Option, + + /// Index of the slab that the lightmap resides in, if a lightmap is + /// present. + pub lightmap_slab: Option, +} + +impl PhaseItemBatchSetKey for Opaque3dBatchSetKey { + fn indexed(&self) -> bool { + self.index_slab.is_some() + } +} + +/// Data that must be identical in order to *batch* phase items together. +/// +/// Note that a *batch set* (if multi-draw is in use) contains multiple batches. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Opaque3dBinKey { + /// The asset that this phase item is associated with. + /// + /// Normally, this is the ID of the mesh, but for non-mesh items it might be + /// the ID of another type of asset. + pub asset_id: UntypedAssetId, +} + +impl PhaseItem for Opaque3d { + #[inline] + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + #[inline] + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.batch_set_key.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl BinnedPhaseItem for Opaque3d { + type BatchSetKey = Opaque3dBatchSetKey; + type BinKey = Opaque3dBinKey; + + #[inline] + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Opaque3d { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +impl CachedRenderPipelinePhaseItem for Opaque3d { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.batch_set_key.pipeline + } +} + +pub struct AlphaMask3d { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: OpaqueNoLightmap3dBatchSetKey, + /// The key, which determines which can be batched. + pub bin_key: OpaqueNoLightmap3dBinKey, + pub representative_entity: (Entity, MainEntity), + pub batch_range: Range, + pub extra_index: PhaseItemExtraIndex, +} + +impl PhaseItem for AlphaMask3d { + #[inline] + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.batch_set_key.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + #[inline] + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl BinnedPhaseItem for AlphaMask3d { + type BinKey = OpaqueNoLightmap3dBinKey; + type BatchSetKey = OpaqueNoLightmap3dBatchSetKey; + + #[inline] + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Self { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +impl CachedRenderPipelinePhaseItem for AlphaMask3d { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.batch_set_key.pipeline + } +} + +pub struct Transmissive3d { + pub distance: f32, + pub pipeline: CachedRenderPipelineId, + pub entity: (Entity, MainEntity), + pub draw_function: DrawFunctionId, + pub batch_range: Range, + pub extra_index: PhaseItemExtraIndex, + /// Whether the mesh in question is indexed (uses an index buffer in + /// addition to its vertex buffer). + pub indexed: bool, +} + +impl PhaseItem for Transmissive3d { + /// For now, automatic batching is disabled for transmissive items because their rendering is + /// split into multiple steps depending on [`Camera3d::screen_space_specular_transmission_steps`], + /// which the batching system doesn't currently know about. + /// + /// Having batching enabled would cause the same item to be drawn multiple times across different + /// steps, whenever the batching range crossed a step boundary. + /// + /// Eventually, we could add support for this by having the batching system break up the batch ranges + /// using the same logic as the transmissive pass, but for now it's simpler to just disable batching. + const AUTOMATIC_BATCHING: bool = false; + + #[inline] + fn entity(&self) -> Entity { + self.entity.0 + } + + #[inline] + fn main_entity(&self) -> MainEntity { + self.entity.1 + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + #[inline] + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl SortedPhaseItem for Transmissive3d { + // NOTE: Values increase towards the camera. Back-to-front ordering for transmissive means we need an ascending sort. + type SortKey = FloatOrd; + + #[inline] + fn sort_key(&self) -> Self::SortKey { + FloatOrd(self.distance) + } + + #[inline] + fn sort(items: &mut [Self]) { + radsort::sort_by_key(items, |item| item.distance); + } + + #[inline] + fn indexed(&self) -> bool { + self.indexed + } +} + +impl CachedRenderPipelinePhaseItem for Transmissive3d { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline + } +} + +pub struct Transparent3d { + pub distance: f32, + pub pipeline: CachedRenderPipelineId, + pub entity: (Entity, MainEntity), + pub draw_function: DrawFunctionId, + pub batch_range: Range, + pub extra_index: PhaseItemExtraIndex, + /// Whether the mesh in question is indexed (uses an index buffer in + /// addition to its vertex buffer). + pub indexed: bool, +} + +impl PhaseItem for Transparent3d { + #[inline] + fn entity(&self) -> Entity { + self.entity.0 + } + + fn main_entity(&self) -> MainEntity { + self.entity.1 + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + #[inline] + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl SortedPhaseItem for Transparent3d { + // NOTE: Values increase towards the camera. Back-to-front ordering for transparent means we need an ascending sort. + type SortKey = FloatOrd; + + #[inline] + fn sort_key(&self) -> Self::SortKey { + FloatOrd(self.distance) + } + + #[inline] + fn sort(items: &mut [Self]) { + radsort::sort_by_key(items, |item| item.distance); + } + + #[inline] + fn indexed(&self) -> bool { + self.indexed + } +} + +impl CachedRenderPipelinePhaseItem for Transparent3d { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline + } +} + +pub fn extract_core_3d_camera_phases( + mut opaque_3d_phases: ResMut>, + mut alpha_mask_3d_phases: ResMut>, + mut transmissive_3d_phases: ResMut>, + mut transparent_3d_phases: ResMut>, + cameras_3d: Extract), With>>, + mut live_entities: Local>, + gpu_preprocessing_support: Res, +) { + live_entities.clear(); + + for (main_entity, camera, no_indirect_drawing) in &cameras_3d { + if !camera.is_active { + continue; + } + + // If GPU culling is in use, use it (and indirect mode); otherwise, just + // preprocess the meshes. + let gpu_preprocessing_mode = gpu_preprocessing_support.min(if !no_indirect_drawing { + GpuPreprocessingMode::Culling + } else { + GpuPreprocessingMode::PreprocessingOnly + }); + + // This is the main 3D camera, so use the first subview index (0). + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + + opaque_3d_phases.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + alpha_mask_3d_phases.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + transmissive_3d_phases.insert_or_clear(retained_view_entity); + transparent_3d_phases.insert_or_clear(retained_view_entity); + + live_entities.insert(retained_view_entity); + } + + opaque_3d_phases.retain(|view_entity, _| live_entities.contains(view_entity)); + alpha_mask_3d_phases.retain(|view_entity, _| live_entities.contains(view_entity)); + transmissive_3d_phases.retain(|view_entity, _| live_entities.contains(view_entity)); + transparent_3d_phases.retain(|view_entity, _| live_entities.contains(view_entity)); +} + +// Extract the render phases for the prepass + +pub fn extract_camera_prepass_phase( + mut commands: Commands, + mut opaque_3d_prepass_phases: ResMut>, + mut alpha_mask_3d_prepass_phases: ResMut>, + mut opaque_3d_deferred_phases: ResMut>, + mut alpha_mask_3d_deferred_phases: ResMut>, + cameras_3d: Extract< + Query< + ( + Entity, + RenderEntity, + &Camera, + Has, + Has, + Has, + Has, + Has, + ), + With, + >, + >, + mut live_entities: Local>, + gpu_preprocessing_support: Res, +) { + live_entities.clear(); + + for ( + main_entity, + entity, + camera, + no_indirect_drawing, + depth_prepass, + normal_prepass, + motion_vector_prepass, + deferred_prepass, + ) in cameras_3d.iter() + { + if !camera.is_active { + continue; + } + + // If GPU culling is in use, use it (and indirect mode); otherwise, just + // preprocess the meshes. + let gpu_preprocessing_mode = gpu_preprocessing_support.min(if !no_indirect_drawing { + GpuPreprocessingMode::Culling + } else { + GpuPreprocessingMode::PreprocessingOnly + }); + + // This is the main 3D camera, so we use the first subview index (0). + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + + if depth_prepass || normal_prepass || motion_vector_prepass { + opaque_3d_prepass_phases + .prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + alpha_mask_3d_prepass_phases + .prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + } else { + opaque_3d_prepass_phases.remove(&retained_view_entity); + alpha_mask_3d_prepass_phases.remove(&retained_view_entity); + } + + if deferred_prepass { + opaque_3d_deferred_phases + .prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + alpha_mask_3d_deferred_phases + .prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + } else { + opaque_3d_deferred_phases.remove(&retained_view_entity); + alpha_mask_3d_deferred_phases.remove(&retained_view_entity); + } + live_entities.insert(retained_view_entity); + + // Add or remove prepasses as appropriate. + + let mut camera_commands = commands + .get_entity(entity) + .expect("Camera entity wasn't synced."); + + if depth_prepass { + camera_commands.insert(DepthPrepass); + } else { + camera_commands.remove::(); + } + + if normal_prepass { + camera_commands.insert(NormalPrepass); + } else { + camera_commands.remove::(); + } + + if motion_vector_prepass { + camera_commands.insert(MotionVectorPrepass); + } else { + camera_commands.remove::(); + } + + if deferred_prepass { + camera_commands.insert(DeferredPrepass); + } else { + camera_commands.remove::(); + } + } + + opaque_3d_prepass_phases.retain(|view_entity, _| live_entities.contains(view_entity)); + alpha_mask_3d_prepass_phases.retain(|view_entity, _| live_entities.contains(view_entity)); + opaque_3d_deferred_phases.retain(|view_entity, _| live_entities.contains(view_entity)); + alpha_mask_3d_deferred_phases.retain(|view_entity, _| live_entities.contains(view_entity)); +} + +pub fn prepare_core_3d_depth_textures( + mut commands: Commands, + mut texture_cache: ResMut, + render_device: Res, + opaque_3d_phases: Res>, + alpha_mask_3d_phases: Res>, + transmissive_3d_phases: Res>, + transparent_3d_phases: Res>, + views_3d: Query<( + Entity, + &ExtractedCamera, + &ExtractedView, + Option<&DepthPrepass>, + &Camera3d, + &Msaa, + )>, +) { + let mut render_target_usage = >::default(); + for (_, camera, extracted_view, depth_prepass, camera_3d, _msaa) in &views_3d { + if !opaque_3d_phases.contains_key(&extracted_view.retained_view_entity) + || !alpha_mask_3d_phases.contains_key(&extracted_view.retained_view_entity) + || !transmissive_3d_phases.contains_key(&extracted_view.retained_view_entity) + || !transparent_3d_phases.contains_key(&extracted_view.retained_view_entity) + { + continue; + }; + + // Default usage required to write to the depth texture + let mut usage: TextureUsages = camera_3d.depth_texture_usages.into(); + if depth_prepass.is_some() { + // Required to read the output of the prepass + usage |= TextureUsages::COPY_SRC; + } + render_target_usage + .entry(camera.target.clone()) + .and_modify(|u| *u |= usage) + .or_insert_with(|| usage); + } + + let mut textures = >::default(); + for (entity, camera, _, _, camera_3d, msaa) in &views_3d { + let Some(physical_target_size) = camera.physical_target_size else { + continue; + }; + + let cached_texture = textures + .entry((camera.target.clone(), msaa)) + .or_insert_with(|| { + let usage = *render_target_usage + .get(&camera.target.clone()) + .expect("The depth texture usage should already exist for this target"); + + let descriptor = TextureDescriptor { + label: Some("view_depth_texture"), + // The size of the depth texture + size: physical_target_size.to_extents(), + mip_level_count: 1, + sample_count: msaa.samples(), + dimension: TextureDimension::D2, + format: CORE_3D_DEPTH_FORMAT, + usage, + view_formats: &[], + }; + + texture_cache.get(&render_device, descriptor) + }) + .clone(); + + commands.entity(entity).insert(ViewDepthTexture::new( + cached_texture, + match camera_3d.depth_load_op { + Camera3dDepthLoadOp::Clear(v) => Some(v), + Camera3dDepthLoadOp::Load => None, + }, + )); + } +} + +#[derive(Component)] +pub struct ViewTransmissionTexture { + pub texture: Texture, + pub view: TextureView, + pub sampler: Sampler, +} + +pub fn prepare_core_3d_transmission_textures( + mut commands: Commands, + mut texture_cache: ResMut, + render_device: Res, + opaque_3d_phases: Res>, + alpha_mask_3d_phases: Res>, + transmissive_3d_phases: Res>, + transparent_3d_phases: Res>, + views_3d: Query<(Entity, &ExtractedCamera, &Camera3d, &ExtractedView)>, +) { + let mut textures = >::default(); + for (entity, camera, camera_3d, view) in &views_3d { + if !opaque_3d_phases.contains_key(&view.retained_view_entity) + || !alpha_mask_3d_phases.contains_key(&view.retained_view_entity) + || !transparent_3d_phases.contains_key(&view.retained_view_entity) + { + continue; + }; + + let Some(transmissive_3d_phase) = transmissive_3d_phases.get(&view.retained_view_entity) + else { + continue; + }; + + let Some(physical_target_size) = camera.physical_target_size else { + continue; + }; + + // Don't prepare a transmission texture if the number of steps is set to 0 + if camera_3d.screen_space_specular_transmission_steps == 0 { + continue; + } + + // Don't prepare a transmission texture if there are no transmissive items to render + if transmissive_3d_phase.items.is_empty() { + continue; + } + + let cached_texture = textures + .entry(camera.target.clone()) + .or_insert_with(|| { + let usage = TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST; + + let format = if view.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }; + + let descriptor = TextureDescriptor { + label: Some("view_transmission_texture"), + // The size of the transmission texture + size: physical_target_size.to_extents(), + mip_level_count: 1, + sample_count: 1, // No need for MSAA, as we'll only copy the main texture here + dimension: TextureDimension::D2, + format, + usage, + view_formats: &[], + }; + + texture_cache.get(&render_device, descriptor) + }) + .clone(); + + let sampler = render_device.create_sampler(&SamplerDescriptor { + label: Some("view_transmission_sampler"), + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..Default::default() + }); + + commands.entity(entity).insert(ViewTransmissionTexture { + texture: cached_texture.texture, + view: cached_texture.default_view, + sampler, + }); + } +} + +/// Sets the `TEXTURE_BINDING` flag on the depth texture if necessary for +/// occlusion culling. +/// +/// We need that flag to be set in order to read from the texture. +fn configure_occlusion_culling_view_targets( + mut view_targets: Query< + &mut Camera3d, + ( + With, + Without, + With, + ), + >, +) { + for mut camera_3d in &mut view_targets { + let mut depth_texture_usages = TextureUsages::from(camera_3d.depth_texture_usages); + depth_texture_usages |= TextureUsages::TEXTURE_BINDING; + camera_3d.depth_texture_usages = depth_texture_usages.into(); + } +} + +// Disable MSAA and warn if using deferred rendering +pub fn check_msaa(mut deferred_views: Query<&mut Msaa, (With, With)>) { + for mut msaa in deferred_views.iter_mut() { + match *msaa { + Msaa::Off => (), + _ => { + warn!("MSAA is incompatible with deferred rendering and has been disabled."); + *msaa = Msaa::Off; + } + }; + } +} + +// Prepares the textures used by the prepass +pub fn prepare_prepass_textures( + mut commands: Commands, + mut texture_cache: ResMut, + render_device: Res, + opaque_3d_prepass_phases: Res>, + alpha_mask_3d_prepass_phases: Res>, + opaque_3d_deferred_phases: Res>, + alpha_mask_3d_deferred_phases: Res>, + views_3d: Query<( + Entity, + &ExtractedCamera, + &ExtractedView, + &Msaa, + Has, + Has, + Has, + Has, + )>, +) { + let mut depth_textures = >::default(); + let mut normal_textures = >::default(); + let mut deferred_textures = >::default(); + let mut deferred_lighting_id_textures = >::default(); + let mut motion_vectors_textures = >::default(); + for ( + entity, + camera, + view, + msaa, + depth_prepass, + normal_prepass, + motion_vector_prepass, + deferred_prepass, + ) in &views_3d + { + if !opaque_3d_prepass_phases.contains_key(&view.retained_view_entity) + && !alpha_mask_3d_prepass_phases.contains_key(&view.retained_view_entity) + && !opaque_3d_deferred_phases.contains_key(&view.retained_view_entity) + && !alpha_mask_3d_deferred_phases.contains_key(&view.retained_view_entity) + { + commands.entity(entity).remove::(); + continue; + }; + + let Some(physical_target_size) = camera.physical_target_size else { + continue; + }; + + let size = physical_target_size.to_extents(); + + let cached_depth_texture = depth_prepass.then(|| { + depth_textures + .entry(camera.target.clone()) + .or_insert_with(|| { + let descriptor = TextureDescriptor { + label: Some("prepass_depth_texture"), + size, + mip_level_count: 1, + sample_count: msaa.samples(), + dimension: TextureDimension::D2, + format: CORE_3D_DEPTH_FORMAT, + usage: TextureUsages::COPY_DST + | TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_SRC, // TODO: Remove COPY_SRC, double buffer instead (for bevy_solari) + view_formats: &[], + }; + texture_cache.get(&render_device, descriptor) + }) + .clone() + }); + + let cached_normals_texture = normal_prepass.then(|| { + normal_textures + .entry(camera.target.clone()) + .or_insert_with(|| { + texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("prepass_normal_texture"), + size, + mip_level_count: 1, + sample_count: msaa.samples(), + dimension: TextureDimension::D2, + format: NORMAL_PREPASS_FORMAT, + usage: TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ) + }) + .clone() + }); + + let cached_motion_vectors_texture = motion_vector_prepass.then(|| { + motion_vectors_textures + .entry(camera.target.clone()) + .or_insert_with(|| { + texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("prepass_motion_vectors_textures"), + size, + mip_level_count: 1, + sample_count: msaa.samples(), + dimension: TextureDimension::D2, + format: MOTION_VECTOR_PREPASS_FORMAT, + usage: TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ) + }) + .clone() + }); + + let cached_deferred_texture = deferred_prepass.then(|| { + deferred_textures + .entry(camera.target.clone()) + .or_insert_with(|| { + texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("prepass_deferred_texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: DEFERRED_PREPASS_FORMAT, + usage: TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_SRC, // TODO: Remove COPY_SRC, double buffer instead (for bevy_solari) + view_formats: &[], + }, + ) + }) + .clone() + }); + + let cached_deferred_lighting_pass_id_texture = deferred_prepass.then(|| { + deferred_lighting_id_textures + .entry(camera.target.clone()) + .or_insert_with(|| { + texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("deferred_lighting_pass_id_texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: DEFERRED_LIGHTING_PASS_ID_FORMAT, + usage: TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ) + }) + .clone() + }); + + commands.entity(entity).insert(ViewPrepassTextures { + depth: cached_depth_texture + .map(|t| ColorAttachment::new(t, None, Some(LinearRgba::BLACK))), + normal: cached_normals_texture + .map(|t| ColorAttachment::new(t, None, Some(LinearRgba::BLACK))), + // Red and Green channels are X and Y components of the motion vectors + // Blue channel doesn't matter, but set to 0.0 for possible faster clear + // https://gpuopen.com/performance/#clears + motion_vectors: cached_motion_vectors_texture + .map(|t| ColorAttachment::new(t, None, Some(LinearRgba::BLACK))), + deferred: cached_deferred_texture + .map(|t| ColorAttachment::new(t, None, Some(LinearRgba::BLACK))), + deferred_lighting_pass_id: cached_deferred_lighting_pass_id_texture + .map(|t| ColorAttachment::new(t, None, Some(LinearRgba::BLACK))), + size, + }); + } +} diff --git a/crates/libmarathon/src/render/deferred/copy_deferred_lighting_id.wgsl b/crates/libmarathon/src/render/deferred/copy_deferred_lighting_id.wgsl new file mode 100644 index 0000000..25acf47 --- /dev/null +++ b/crates/libmarathon/src/render/deferred/copy_deferred_lighting_id.wgsl @@ -0,0 +1,18 @@ +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +@group(0) @binding(0) +var material_id_texture: texture_2d; + +struct FragmentOutput { + @builtin(frag_depth) frag_depth: f32, + +} + +@fragment +fn fragment(in: FullscreenVertexOutput) -> FragmentOutput { + var out: FragmentOutput; + // Depth is stored as unorm, so we are dividing the u8 by 255.0 here. + out.frag_depth = f32(textureLoad(material_id_texture, vec2(in.position.xy), 0).x) / 255.0; + return out; +} + diff --git a/crates/libmarathon/src/render/deferred/copy_lighting_id.rs b/crates/libmarathon/src/render/deferred/copy_lighting_id.rs new file mode 100644 index 0000000..39f0de5 --- /dev/null +++ b/crates/libmarathon/src/render/deferred/copy_lighting_id.rs @@ -0,0 +1,193 @@ +use crate::render::{ + prepass::{DeferredPrepass, ViewPrepassTextures}, + FullscreenShader, +}; +use bevy_app::prelude::*; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer}; +use bevy_ecs::prelude::*; +use bevy_image::ToExtents; +use crate::render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_resource::{binding_types::texture_2d, *}, + renderer::RenderDevice, + texture::{CachedTexture, TextureCache}, + view::ViewTarget, + Render, RenderApp, RenderStartup, RenderSystems, +}; + +use super::DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT; +use bevy_ecs::query::QueryItem; +use crate::render::{ + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + renderer::RenderContext, +}; +use bevy_utils::default; + +pub struct CopyDeferredLightingIdPlugin; + +impl Plugin for CopyDeferredLightingIdPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "copy_deferred_lighting_id.wgsl"); + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app + .add_systems(RenderStartup, init_copy_deferred_lighting_id_pipeline) + .add_systems( + Render, + (prepare_deferred_lighting_id_textures.in_set(RenderSystems::PrepareResources),), + ); + } +} + +#[derive(Default)] +pub struct CopyDeferredLightingIdNode; +impl CopyDeferredLightingIdNode { + pub const NAME: &'static str = "copy_deferred_lighting_id"; +} + +impl ViewNode for CopyDeferredLightingIdNode { + type ViewQuery = ( + &'static ViewTarget, + &'static ViewPrepassTextures, + &'static DeferredLightingIdDepthTexture, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (_view_target, view_prepass_textures, deferred_lighting_id_depth_texture): QueryItem< + Self::ViewQuery, + >, + world: &World, + ) -> Result<(), NodeRunError> { + let copy_deferred_lighting_id_pipeline = world.resource::(); + + let pipeline_cache = world.resource::(); + + let Some(pipeline) = + pipeline_cache.get_render_pipeline(copy_deferred_lighting_id_pipeline.pipeline_id) + else { + return Ok(()); + }; + let Some(deferred_lighting_pass_id_texture) = + &view_prepass_textures.deferred_lighting_pass_id + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let bind_group = render_context.render_device().create_bind_group( + "copy_deferred_lighting_id_bind_group", + ©_deferred_lighting_id_pipeline.layout, + &BindGroupEntries::single(&deferred_lighting_pass_id_texture.texture.default_view), + ); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("copy_deferred_lighting_id"), + color_attachments: &[], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &deferred_lighting_id_depth_texture.texture.default_view, + depth_ops: Some(Operations { + load: LoadOp::Clear(0.0), + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + + let pass_span = diagnostics.pass_span(&mut render_pass, "copy_deferred_lighting_id"); + + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group(0, &bind_group, &[]); + render_pass.draw(0..3, 0..1); + + pass_span.end(&mut render_pass); + + Ok(()) + } +} + +#[derive(Resource)] +struct CopyDeferredLightingIdPipeline { + layout: BindGroupLayout, + pipeline_id: CachedRenderPipelineId, +} + +pub fn init_copy_deferred_lighting_id_pipeline( + mut commands: Commands, + render_device: Res, + fullscreen_shader: Res, + asset_server: Res, + pipeline_cache: Res, +) { + let layout = render_device.create_bind_group_layout( + "copy_deferred_lighting_id_bind_group_layout", + &BindGroupLayoutEntries::single( + ShaderStages::FRAGMENT, + texture_2d(TextureSampleType::Uint), + ), + ); + + let vertex_state = fullscreen_shader.to_vertex_state(); + let shader = load_embedded_asset!(asset_server.as_ref(), "copy_deferred_lighting_id.wgsl"); + + let pipeline_id = pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("copy_deferred_lighting_id_pipeline".into()), + layout: vec![layout.clone()], + vertex: vertex_state, + fragment: Some(FragmentState { + shader, + ..default() + }), + depth_stencil: Some(DepthStencilState { + format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::Always, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + ..default() + }); + + commands.insert_resource(CopyDeferredLightingIdPipeline { + layout, + pipeline_id, + }); +} + +#[derive(Component)] +pub struct DeferredLightingIdDepthTexture { + pub texture: CachedTexture, +} + +fn prepare_deferred_lighting_id_textures( + mut commands: Commands, + mut texture_cache: ResMut, + render_device: Res, + views: Query<(Entity, &ExtractedCamera), With>, +) { + for (entity, camera) in &views { + if let Some(physical_target_size) = camera.physical_target_size { + let texture_descriptor = TextureDescriptor { + label: Some("deferred_lighting_id_depth_texture_a"), + size: physical_target_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC, + view_formats: &[], + }; + let texture = texture_cache.get(&render_device, texture_descriptor); + commands + .entity(entity) + .insert(DeferredLightingIdDepthTexture { texture }); + } + } +} diff --git a/crates/libmarathon/src/render/deferred/mod.rs b/crates/libmarathon/src/render/deferred/mod.rs new file mode 100644 index 0000000..65b76b3 --- /dev/null +++ b/crates/libmarathon/src/render/deferred/mod.rs @@ -0,0 +1,186 @@ +pub mod copy_lighting_id; +pub mod node; + +use core::ops::Range; + +use crate::render::prepass::{OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey}; +use bevy_ecs::prelude::*; +use crate::render::sync_world::MainEntity; +use crate::render::{ + render_phase::{ + BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem, + PhaseItemExtraIndex, + }, + render_resource::{CachedRenderPipelineId, TextureFormat}, +}; + +pub const DEFERRED_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgba32Uint; +pub const DEFERRED_LIGHTING_PASS_ID_FORMAT: TextureFormat = TextureFormat::R8Uint; +pub const DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth16Unorm; + +/// Opaque phase of the 3D Deferred pass. +/// +/// Sorted by pipeline, then by mesh to improve batching. +/// +/// Used to render all 3D meshes with materials that have no transparency. +#[derive(PartialEq, Eq, Hash)] +pub struct Opaque3dDeferred { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: OpaqueNoLightmap3dBatchSetKey, + /// Information that separates items into bins. + pub bin_key: OpaqueNoLightmap3dBinKey, + pub representative_entity: (Entity, MainEntity), + pub batch_range: Range, + pub extra_index: PhaseItemExtraIndex, +} + +impl PhaseItem for Opaque3dDeferred { + #[inline] + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.batch_set_key.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + #[inline] + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl BinnedPhaseItem for Opaque3dDeferred { + type BatchSetKey = OpaqueNoLightmap3dBatchSetKey; + type BinKey = OpaqueNoLightmap3dBinKey; + + #[inline] + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Self { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +impl CachedRenderPipelinePhaseItem for Opaque3dDeferred { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.batch_set_key.pipeline + } +} + +/// Alpha mask phase of the 3D Deferred pass. +/// +/// Sorted by pipeline, then by mesh to improve batching. +/// +/// Used to render all meshes with a material with an alpha mask. +pub struct AlphaMask3dDeferred { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: OpaqueNoLightmap3dBatchSetKey, + /// Information that separates items into bins. + pub bin_key: OpaqueNoLightmap3dBinKey, + pub representative_entity: (Entity, MainEntity), + pub batch_range: Range, + pub extra_index: PhaseItemExtraIndex, +} + +impl PhaseItem for AlphaMask3dDeferred { + #[inline] + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + #[inline] + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.batch_set_key.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + #[inline] + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl BinnedPhaseItem for AlphaMask3dDeferred { + type BatchSetKey = OpaqueNoLightmap3dBatchSetKey; + type BinKey = OpaqueNoLightmap3dBinKey; + + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Self { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +impl CachedRenderPipelinePhaseItem for AlphaMask3dDeferred { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.batch_set_key.pipeline + } +} diff --git a/crates/libmarathon/src/render/deferred/node.rs b/crates/libmarathon/src/render/deferred/node.rs new file mode 100644 index 0000000..79d1584 --- /dev/null +++ b/crates/libmarathon/src/render/deferred/node.rs @@ -0,0 +1,273 @@ +use bevy_camera::{MainPassResolutionOverride, Viewport}; +use bevy_ecs::{prelude::*, query::QueryItem}; +use crate::render::experimental::occlusion_culling::OcclusionCulling; +use crate::render::render_graph::ViewNode; + +use crate::render::view::{ExtractedView, NoIndirectDrawing}; +use crate::render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext}, + render_phase::{TrackedRenderPass, ViewBinnedRenderPhases}, + render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp}, + renderer::RenderContext, + view::ViewDepthTexture, +}; +use tracing::error; +#[cfg(feature = "trace")] +use tracing::info_span; + +use crate::render::prepass::ViewPrepassTextures; + +use super::{AlphaMask3dDeferred, Opaque3dDeferred}; + +/// The phase of the deferred prepass that draws meshes that were visible last +/// frame. +/// +/// If occlusion culling isn't in use, this prepass simply draws all meshes. +/// +/// Like all prepass nodes, this is inserted before the main pass in the render +/// graph. +#[derive(Default)] +pub struct EarlyDeferredGBufferPrepassNode; + +impl ViewNode for EarlyDeferredGBufferPrepassNode { + type ViewQuery = ::ViewQuery; + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + run_deferred_prepass( + graph, + render_context, + view_query, + false, + world, + "early deferred prepass", + ) + } +} + +/// The phase of the prepass that runs after occlusion culling against the +/// meshes that were visible last frame. +/// +/// If occlusion culling isn't in use, this is a no-op. +/// +/// Like all prepass nodes, this is inserted before the main pass in the render +/// graph. +#[derive(Default)] +pub struct LateDeferredGBufferPrepassNode; + +impl ViewNode for LateDeferredGBufferPrepassNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ExtractedView, + &'static ViewDepthTexture, + &'static ViewPrepassTextures, + Option<&'static MainPassResolutionOverride>, + Has, + Has, + ); + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let (.., occlusion_culling, no_indirect_drawing) = view_query; + if !occlusion_culling || no_indirect_drawing { + return Ok(()); + } + + run_deferred_prepass( + graph, + render_context, + view_query, + true, + world, + "late deferred prepass", + ) + } +} + +/// Runs the deferred prepass that draws all meshes to the depth buffer and +/// G-buffers. +/// +/// If occlusion culling isn't in use, and a prepass is enabled, then there's +/// only one prepass. If occlusion culling is in use, then any prepass is split +/// into two: an *early* prepass and a *late* prepass. The early prepass draws +/// what was visible last frame, and the last prepass performs occlusion culling +/// against a conservative hierarchical Z buffer before drawing unoccluded +/// meshes. +fn run_deferred_prepass<'w>( + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + (camera, extracted_view, view_depth_texture, view_prepass_textures, resolution_override, _, _): QueryItem< + 'w, + '_, + ::ViewQuery, + >, + is_late: bool, + world: &'w World, + label: &'static str, +) -> Result<(), NodeRunError> { + let (Some(opaque_deferred_phases), Some(alpha_mask_deferred_phases)) = ( + world.get_resource::>(), + world.get_resource::>(), + ) else { + return Ok(()); + }; + + let (Some(opaque_deferred_phase), Some(alpha_mask_deferred_phase)) = ( + opaque_deferred_phases.get(&extracted_view.retained_view_entity), + alpha_mask_deferred_phases.get(&extracted_view.retained_view_entity), + ) else { + return Ok(()); + }; + + let diagnostic = render_context.diagnostic_recorder(); + + let mut color_attachments = vec![]; + color_attachments.push( + view_prepass_textures + .normal + .as_ref() + .map(|normals_texture| normals_texture.get_attachment()), + ); + color_attachments.push( + view_prepass_textures + .motion_vectors + .as_ref() + .map(|motion_vectors_texture| motion_vectors_texture.get_attachment()), + ); + + // If we clear the deferred texture with LoadOp::Clear(Default::default()) we get these errors: + // Chrome: GL_INVALID_OPERATION: No defined conversion between clear value and attachment format. + // Firefox: WebGL warning: clearBufferu?[fi]v: This attachment is of type FLOAT, but this function is of type UINT. + // Appears to be unsupported: https://registry.khronos.org/webgl/specs/latest/2.0/#3.7.9 + // For webgl2 we fallback to manually clearing + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + if !is_late { + if let Some(deferred_texture) = &view_prepass_textures.deferred { + render_context.command_encoder().clear_texture( + &deferred_texture.texture.texture, + &bevy_render::render_resource::ImageSubresourceRange::default(), + ); + } + } + + color_attachments.push( + view_prepass_textures + .deferred + .as_ref() + .map(|deferred_texture| { + if is_late { + deferred_texture.get_attachment() + } else { + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + { + bevy_render::render_resource::RenderPassColorAttachment { + view: &deferred_texture.texture.default_view, + resolve_target: None, + ops: bevy_render::render_resource::Operations { + load: bevy_render::render_resource::LoadOp::Load, + store: StoreOp::Store, + }, + depth_slice: None, + } + } + #[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ))] + deferred_texture.get_attachment() + } + }), + ); + + color_attachments.push( + view_prepass_textures + .deferred_lighting_pass_id + .as_ref() + .map(|deferred_lighting_pass_id| deferred_lighting_pass_id.get_attachment()), + ); + + // If all color attachments are none: clear the color attachment list so that no fragment shader is required + if color_attachments.iter().all(Option::is_none) { + color_attachments.clear(); + } + + let depth_stencil_attachment = Some(view_depth_texture.get_attachment(StoreOp::Store)); + + let view_entity = graph.view_entity(); + render_context.add_command_buffer_generation_task(move |render_device| { + #[cfg(feature = "trace")] + let _deferred_span = info_span!("deferred_prepass").entered(); + + // Command encoder setup + let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("deferred_prepass_command_encoder"), + }); + + // Render pass setup + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { + label: Some(label), + color_attachments: &color_attachments, + depth_stencil_attachment, + timestamp_writes: None, + occlusion_query_set: None, + }); + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); + let pass_span = diagnostic.pass_span(&mut render_pass, label); + if let Some(viewport) = + Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override) + { + render_pass.set_camera_viewport(&viewport); + } + + // Opaque draws + if !opaque_deferred_phase.multidrawable_meshes.is_empty() + || !opaque_deferred_phase.batchable_meshes.is_empty() + || !opaque_deferred_phase.unbatchable_meshes.is_empty() + { + #[cfg(feature = "trace")] + let _opaque_prepass_span = info_span!("opaque_deferred_prepass").entered(); + if let Err(err) = opaque_deferred_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the opaque deferred phase {err:?}"); + } + } + + // Alpha masked draws + if !alpha_mask_deferred_phase.is_empty() { + #[cfg(feature = "trace")] + let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred_prepass").entered(); + if let Err(err) = alpha_mask_deferred_phase.render(&mut render_pass, world, view_entity) + { + error!("Error encountered while rendering the alpha mask deferred phase {err:?}"); + } + } + + pass_span.end(&mut render_pass); + drop(render_pass); + + // After rendering to the view depth texture, copy it to the prepass depth texture + if let Some(prepass_depth_texture) = &view_prepass_textures.depth { + command_encoder.copy_texture_to_texture( + view_depth_texture.texture.as_image_copy(), + prepass_depth_texture.texture.texture.as_image_copy(), + view_prepass_textures.size, + ); + } + + command_encoder.finish() + }); + + Ok(()) +} diff --git a/crates/libmarathon/src/render/diagnostic/internal.rs b/crates/libmarathon/src/render/diagnostic/internal.rs new file mode 100644 index 0000000..876d066 --- /dev/null +++ b/crates/libmarathon/src/render/diagnostic/internal.rs @@ -0,0 +1,709 @@ +use std::{borrow::Cow, sync::Arc}; +use core::{ + ops::{DerefMut, Range}, + sync::atomic::{AtomicBool, Ordering}, +}; +use std::thread::{self, ThreadId}; + +use bevy_diagnostic::{Diagnostic, DiagnosticMeasurement, DiagnosticPath, DiagnosticsStore}; +use bevy_ecs::resource::Resource; +use bevy_ecs::system::{Res, ResMut}; +use bevy_platform::time::Instant; +use std::sync::Mutex; +use wgpu::{ + Buffer, BufferDescriptor, BufferUsages, CommandEncoder, ComputePass, Features, MapMode, + PipelineStatisticsTypes, QuerySet, QuerySetDescriptor, QueryType, RenderPass, +}; + +use crate::render::renderer::{RenderAdapterInfo, RenderDevice, RenderQueue, WgpuWrapper}; + +use super::RecordDiagnostics; + +// buffer offset must be divisible by 256, so this constant must be divisible by 32 (=256/8) +const MAX_TIMESTAMP_QUERIES: u32 = 256; +const MAX_PIPELINE_STATISTICS: u32 = 128; + +const TIMESTAMP_SIZE: u64 = 8; +const PIPELINE_STATISTICS_SIZE: u64 = 40; + +struct DiagnosticsRecorderInternal { + timestamp_period_ns: f32, + features: Features, + current_frame: Mutex, + submitted_frames: Vec, + finished_frames: Vec, + #[cfg(feature = "tracing-tracy")] + tracy_gpu_context: tracy_client::GpuContext, +} + +/// Records diagnostics into [`QuerySet`]'s keeping track of the mapping between +/// spans and indices to the corresponding entries in the [`QuerySet`]. +#[derive(Resource)] +pub struct DiagnosticsRecorder(WgpuWrapper); + +impl DiagnosticsRecorder { + /// Creates the new `DiagnosticsRecorder`. + pub fn new( + adapter_info: &RenderAdapterInfo, + device: &RenderDevice, + queue: &RenderQueue, + ) -> DiagnosticsRecorder { + let features = device.features(); + + #[cfg(feature = "tracing-tracy")] + let tracy_gpu_context = + super::tracy_gpu::new_tracy_gpu_context(adapter_info, device, queue); + let _ = adapter_info; // Prevent unused variable warnings when tracing-tracy is not enabled + + DiagnosticsRecorder(WgpuWrapper::new(DiagnosticsRecorderInternal { + timestamp_period_ns: queue.get_timestamp_period(), + features, + current_frame: Mutex::new(FrameData::new( + device, + features, + #[cfg(feature = "tracing-tracy")] + tracy_gpu_context.clone(), + )), + submitted_frames: Vec::new(), + finished_frames: Vec::new(), + #[cfg(feature = "tracing-tracy")] + tracy_gpu_context, + })) + } + + fn current_frame_mut(&mut self) -> &mut FrameData { + self.0.current_frame.get_mut().expect("lock poisoned") + } + + fn current_frame_lock(&self) -> impl DerefMut + '_ { + self.0.current_frame.lock().expect("lock poisoned") + } + + /// Begins recording diagnostics for a new frame. + pub fn begin_frame(&mut self) { + let internal = &mut self.0; + let mut idx = 0; + while idx < internal.submitted_frames.len() { + let timestamp = internal.timestamp_period_ns; + if internal.submitted_frames[idx].run_mapped_callback(timestamp) { + let removed = internal.submitted_frames.swap_remove(idx); + internal.finished_frames.push(removed); + } else { + idx += 1; + } + } + + self.current_frame_mut().begin(); + } + + /// Copies data from [`QuerySet`]'s to a [`Buffer`], after which it can be downloaded to CPU. + /// + /// Should be called before [`DiagnosticsRecorder::finish_frame`]. + pub fn resolve(&mut self, encoder: &mut CommandEncoder) { + self.current_frame_mut().resolve(encoder); + } + + /// Finishes recording diagnostics for the current frame. + /// + /// The specified `callback` will be invoked when diagnostics become available. + /// + /// Should be called after [`DiagnosticsRecorder::resolve`], + /// and **after** all commands buffers have been queued. + pub fn finish_frame( + &mut self, + device: &RenderDevice, + callback: impl FnOnce(RenderDiagnostics) + Send + Sync + 'static, + ) { + #[cfg(feature = "tracing-tracy")] + let tracy_gpu_context = self.0.tracy_gpu_context.clone(); + + let internal = &mut self.0; + internal + .current_frame + .get_mut() + .expect("lock poisoned") + .finish(callback); + + // reuse one of the finished frames, if we can + let new_frame = match internal.finished_frames.pop() { + Some(frame) => frame, + None => FrameData::new( + device, + internal.features, + #[cfg(feature = "tracing-tracy")] + tracy_gpu_context, + ), + }; + + let old_frame = core::mem::replace( + internal.current_frame.get_mut().expect("lock poisoned"), + new_frame, + ); + internal.submitted_frames.push(old_frame); + } +} + +impl RecordDiagnostics for DiagnosticsRecorder { + fn begin_time_span(&self, encoder: &mut E, span_name: Cow<'static, str>) { + self.current_frame_lock() + .begin_time_span(encoder, span_name); + } + + fn end_time_span(&self, encoder: &mut E) { + self.current_frame_lock().end_time_span(encoder); + } + + fn begin_pass_span(&self, pass: &mut P, span_name: Cow<'static, str>) { + self.current_frame_lock().begin_pass(pass, span_name); + } + + fn end_pass_span(&self, pass: &mut P) { + self.current_frame_lock().end_pass(pass); + } +} + +struct SpanRecord { + thread_id: ThreadId, + path_range: Range, + pass_kind: Option, + begin_timestamp_index: Option, + end_timestamp_index: Option, + begin_instant: Option, + end_instant: Option, + pipeline_statistics_index: Option, +} + +struct FrameData { + timestamps_query_set: Option, + num_timestamps: u32, + supports_timestamps_inside_passes: bool, + supports_timestamps_inside_encoders: bool, + pipeline_statistics_query_set: Option, + num_pipeline_statistics: u32, + buffer_size: u64, + pipeline_statistics_buffer_offset: u64, + resolve_buffer: Option, + read_buffer: Option, + path_components: Vec>, + open_spans: Vec, + closed_spans: Vec, + is_mapped: Arc, + callback: Option>, + #[cfg(feature = "tracing-tracy")] + tracy_gpu_context: tracy_client::GpuContext, +} + +impl FrameData { + fn new( + device: &RenderDevice, + features: Features, + #[cfg(feature = "tracing-tracy")] tracy_gpu_context: tracy_client::GpuContext, + ) -> FrameData { + let wgpu_device = device.wgpu_device(); + let mut buffer_size = 0; + + let timestamps_query_set = if features.contains(Features::TIMESTAMP_QUERY) { + buffer_size += u64::from(MAX_TIMESTAMP_QUERIES) * TIMESTAMP_SIZE; + Some(wgpu_device.create_query_set(&QuerySetDescriptor { + label: Some("timestamps_query_set"), + ty: QueryType::Timestamp, + count: MAX_TIMESTAMP_QUERIES, + })) + } else { + None + }; + + let pipeline_statistics_buffer_offset = buffer_size; + + let pipeline_statistics_query_set = + if features.contains(Features::PIPELINE_STATISTICS_QUERY) { + buffer_size += u64::from(MAX_PIPELINE_STATISTICS) * PIPELINE_STATISTICS_SIZE; + Some(wgpu_device.create_query_set(&QuerySetDescriptor { + label: Some("pipeline_statistics_query_set"), + ty: QueryType::PipelineStatistics(PipelineStatisticsTypes::all()), + count: MAX_PIPELINE_STATISTICS, + })) + } else { + None + }; + + let (resolve_buffer, read_buffer) = if buffer_size > 0 { + let resolve_buffer = wgpu_device.create_buffer(&BufferDescriptor { + label: Some("render_statistics_resolve_buffer"), + size: buffer_size, + usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC, + mapped_at_creation: false, + }); + let read_buffer = wgpu_device.create_buffer(&BufferDescriptor { + label: Some("render_statistics_read_buffer"), + size: buffer_size, + usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + (Some(resolve_buffer), Some(read_buffer)) + } else { + (None, None) + }; + + FrameData { + timestamps_query_set, + num_timestamps: 0, + supports_timestamps_inside_passes: features + .contains(Features::TIMESTAMP_QUERY_INSIDE_PASSES), + supports_timestamps_inside_encoders: features + .contains(Features::TIMESTAMP_QUERY_INSIDE_ENCODERS), + pipeline_statistics_query_set, + num_pipeline_statistics: 0, + buffer_size, + pipeline_statistics_buffer_offset, + resolve_buffer, + read_buffer, + path_components: Vec::new(), + open_spans: Vec::new(), + closed_spans: Vec::new(), + is_mapped: Arc::new(AtomicBool::new(false)), + callback: None, + #[cfg(feature = "tracing-tracy")] + tracy_gpu_context, + } + } + + fn begin(&mut self) { + self.num_timestamps = 0; + self.num_pipeline_statistics = 0; + self.path_components.clear(); + self.open_spans.clear(); + self.closed_spans.clear(); + } + + fn write_timestamp( + &mut self, + encoder: &mut impl WriteTimestamp, + is_inside_pass: bool, + ) -> Option { + // `encoder.write_timestamp` is unsupported on WebGPU. + if !self.supports_timestamps_inside_encoders { + return None; + } + + if is_inside_pass && !self.supports_timestamps_inside_passes { + return None; + } + + if self.num_timestamps >= MAX_TIMESTAMP_QUERIES { + return None; + } + + let set = self.timestamps_query_set.as_ref()?; + let index = self.num_timestamps; + encoder.write_timestamp(set, index); + self.num_timestamps += 1; + Some(index) + } + + fn write_pipeline_statistics( + &mut self, + encoder: &mut impl WritePipelineStatistics, + ) -> Option { + if self.num_pipeline_statistics >= MAX_PIPELINE_STATISTICS { + return None; + } + + let set = self.pipeline_statistics_query_set.as_ref()?; + let index = self.num_pipeline_statistics; + encoder.begin_pipeline_statistics_query(set, index); + self.num_pipeline_statistics += 1; + Some(index) + } + + fn open_span( + &mut self, + pass_kind: Option, + name: Cow<'static, str>, + ) -> &mut SpanRecord { + let thread_id = thread::current().id(); + + let parent = self + .open_spans + .iter() + .filter(|v| v.thread_id == thread_id) + .next_back(); + + let path_range = match &parent { + Some(parent) if parent.path_range.end == self.path_components.len() => { + parent.path_range.start..parent.path_range.end + 1 + } + Some(parent) => { + self.path_components + .extend_from_within(parent.path_range.clone()); + self.path_components.len() - parent.path_range.len()..self.path_components.len() + 1 + } + None => self.path_components.len()..self.path_components.len() + 1, + }; + + self.path_components.push(name); + + self.open_spans.push(SpanRecord { + thread_id, + path_range, + pass_kind, + begin_timestamp_index: None, + end_timestamp_index: None, + begin_instant: None, + end_instant: None, + pipeline_statistics_index: None, + }); + + self.open_spans.last_mut().unwrap() + } + + fn close_span(&mut self) -> &mut SpanRecord { + let thread_id = thread::current().id(); + + let iter = self.open_spans.iter(); + let (index, _) = iter + .enumerate() + .filter(|(_, v)| v.thread_id == thread_id) + .next_back() + .unwrap(); + + let span = self.open_spans.swap_remove(index); + self.closed_spans.push(span); + self.closed_spans.last_mut().unwrap() + } + + fn begin_time_span(&mut self, encoder: &mut impl WriteTimestamp, name: Cow<'static, str>) { + let begin_instant = Instant::now(); + let begin_timestamp_index = self.write_timestamp(encoder, false); + + let span = self.open_span(None, name); + span.begin_instant = Some(begin_instant); + span.begin_timestamp_index = begin_timestamp_index; + } + + fn end_time_span(&mut self, encoder: &mut impl WriteTimestamp) { + let end_timestamp_index = self.write_timestamp(encoder, false); + + let span = self.close_span(); + span.end_timestamp_index = end_timestamp_index; + span.end_instant = Some(Instant::now()); + } + + fn begin_pass(&mut self, pass: &mut P, name: Cow<'static, str>) { + let begin_instant = Instant::now(); + + let begin_timestamp_index = self.write_timestamp(pass, true); + let pipeline_statistics_index = self.write_pipeline_statistics(pass); + + let span = self.open_span(Some(P::KIND), name); + span.begin_instant = Some(begin_instant); + span.begin_timestamp_index = begin_timestamp_index; + span.pipeline_statistics_index = pipeline_statistics_index; + } + + fn end_pass(&mut self, pass: &mut impl Pass) { + let end_timestamp_index = self.write_timestamp(pass, true); + + let span = self.close_span(); + span.end_timestamp_index = end_timestamp_index; + + if span.pipeline_statistics_index.is_some() { + pass.end_pipeline_statistics_query(); + } + + span.end_instant = Some(Instant::now()); + } + + fn resolve(&mut self, encoder: &mut CommandEncoder) { + let Some(resolve_buffer) = &self.resolve_buffer else { + return; + }; + + match &self.timestamps_query_set { + Some(set) if self.num_timestamps > 0 => { + encoder.resolve_query_set(set, 0..self.num_timestamps, resolve_buffer, 0); + } + _ => {} + } + + match &self.pipeline_statistics_query_set { + Some(set) if self.num_pipeline_statistics > 0 => { + encoder.resolve_query_set( + set, + 0..self.num_pipeline_statistics, + resolve_buffer, + self.pipeline_statistics_buffer_offset, + ); + } + _ => {} + } + + let Some(read_buffer) = &self.read_buffer else { + return; + }; + + encoder.copy_buffer_to_buffer(resolve_buffer, 0, read_buffer, 0, self.buffer_size); + } + + fn diagnostic_path(&self, range: &Range, field: &str) -> DiagnosticPath { + DiagnosticPath::from_components( + core::iter::once("render") + .chain(self.path_components[range.clone()].iter().map(|v| &**v)) + .chain(core::iter::once(field)), + ) + } + + fn finish(&mut self, callback: impl FnOnce(RenderDiagnostics) + Send + Sync + 'static) { + let Some(read_buffer) = &self.read_buffer else { + // we still have cpu timings, so let's use them + + let mut diagnostics = Vec::new(); + + for span in &self.closed_spans { + if let (Some(begin), Some(end)) = (span.begin_instant, span.end_instant) { + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "elapsed_cpu"), + suffix: "ms", + value: (end - begin).as_secs_f64() * 1000.0, + }); + } + } + + callback(RenderDiagnostics(diagnostics)); + return; + }; + + self.callback = Some(Box::new(callback)); + + let is_mapped = self.is_mapped.clone(); + read_buffer.slice(..).map_async(MapMode::Read, move |res| { + if let Err(e) = res { + tracing::warn!("Failed to download render statistics buffer: {e}"); + return; + } + + is_mapped.store(true, Ordering::Release); + }); + } + + // returns true if the frame is considered finished, false otherwise + fn run_mapped_callback(&mut self, timestamp_period_ns: f32) -> bool { + let Some(read_buffer) = &self.read_buffer else { + return true; + }; + if !self.is_mapped.load(Ordering::Acquire) { + // need to wait more + return false; + } + let Some(callback) = self.callback.take() else { + return true; + }; + + let data = read_buffer.slice(..).get_mapped_range(); + + let timestamps = data[..(self.num_timestamps * 8) as usize] + .chunks(8) + .map(|v| u64::from_le_bytes(v.try_into().unwrap())) + .collect::>(); + + let start = self.pipeline_statistics_buffer_offset as usize; + let len = (self.num_pipeline_statistics as usize) * 40; + let pipeline_statistics = data[start..start + len] + .chunks(8) + .map(|v| u64::from_le_bytes(v.try_into().unwrap())) + .collect::>(); + + let mut diagnostics = Vec::new(); + + for span in &self.closed_spans { + if let (Some(begin), Some(end)) = (span.begin_instant, span.end_instant) { + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "elapsed_cpu"), + suffix: "ms", + value: (end - begin).as_secs_f64() * 1000.0, + }); + } + + if let (Some(begin), Some(end)) = (span.begin_timestamp_index, span.end_timestamp_index) + { + let begin = timestamps[begin as usize] as f64; + let end = timestamps[end as usize] as f64; + let value = (end - begin) * (timestamp_period_ns as f64) / 1e6; + + #[cfg(feature = "tracing-tracy")] + { + // Calling span_alloc() and end_zone() here instead of in open_span() and close_span() means that tracy does not know where each GPU command was recorded on the CPU timeline. + // Unfortunately we must do it this way, because tracy does not play nicely with multithreaded command recording. The start/end pairs would get all mixed up. + // The GPU spans themselves are still accurate though, and it's probably safe to assume that each GPU span in frame N belongs to the corresponding CPU render node span from frame N-1. + let name = &self.path_components[span.path_range.clone()].join("/"); + let mut tracy_gpu_span = + self.tracy_gpu_context.span_alloc(name, "", "", 0).unwrap(); + tracy_gpu_span.end_zone(); + tracy_gpu_span.upload_timestamp_start(begin as i64); + tracy_gpu_span.upload_timestamp_end(end as i64); + } + + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "elapsed_gpu"), + suffix: "ms", + value, + }); + } + + if let Some(index) = span.pipeline_statistics_index { + let index = (index as usize) * 5; + + if span.pass_kind == Some(PassKind::Render) { + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "vertex_shader_invocations"), + suffix: "", + value: pipeline_statistics[index] as f64, + }); + + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "clipper_invocations"), + suffix: "", + value: pipeline_statistics[index + 1] as f64, + }); + + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "clipper_primitives_out"), + suffix: "", + value: pipeline_statistics[index + 2] as f64, + }); + + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "fragment_shader_invocations"), + suffix: "", + value: pipeline_statistics[index + 3] as f64, + }); + } + + if span.pass_kind == Some(PassKind::Compute) { + diagnostics.push(RenderDiagnostic { + path: self.diagnostic_path(&span.path_range, "compute_shader_invocations"), + suffix: "", + value: pipeline_statistics[index + 4] as f64, + }); + } + } + } + + callback(RenderDiagnostics(diagnostics)); + + drop(data); + read_buffer.unmap(); + self.is_mapped.store(false, Ordering::Release); + + true + } +} + +/// Resource which stores render diagnostics of the most recent frame. +#[derive(Debug, Default, Clone, Resource)] +pub struct RenderDiagnostics(Vec); + +/// A render diagnostic which has been recorded, but not yet stored in [`DiagnosticsStore`]. +#[derive(Debug, Clone, Resource)] +pub struct RenderDiagnostic { + pub path: DiagnosticPath, + pub suffix: &'static str, + pub value: f64, +} + +/// Stores render diagnostics before they can be synced with the main app. +/// +/// This mutex is locked twice per frame: +/// 1. in `PreUpdate`, during [`sync_diagnostics`], +/// 2. after rendering has finished and statistics have been downloaded from GPU. +#[derive(Debug, Default, Clone, Resource)] +pub struct RenderDiagnosticsMutex(pub(crate) Arc>>); + +/// Updates render diagnostics measurements. +pub fn sync_diagnostics(mutex: Res, mut store: ResMut) { + let Some(diagnostics) = mutex.0.lock().ok().and_then(|mut v| v.take()) else { + return; + }; + + let time = Instant::now(); + + for diagnostic in &diagnostics.0 { + if store.get(&diagnostic.path).is_none() { + store.add(Diagnostic::new(diagnostic.path.clone()).with_suffix(diagnostic.suffix)); + } + + store + .get_mut(&diagnostic.path) + .unwrap() + .add_measurement(DiagnosticMeasurement { + time, + value: diagnostic.value, + }); + } +} + +pub trait WriteTimestamp { + fn write_timestamp(&mut self, query_set: &QuerySet, index: u32); +} + +impl WriteTimestamp for CommandEncoder { + fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) { + CommandEncoder::write_timestamp(self, query_set, index); + } +} + +impl WriteTimestamp for RenderPass<'_> { + fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) { + RenderPass::write_timestamp(self, query_set, index); + } +} + +impl WriteTimestamp for ComputePass<'_> { + fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) { + ComputePass::write_timestamp(self, query_set, index); + } +} + +pub trait WritePipelineStatistics { + fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32); + + fn end_pipeline_statistics_query(&mut self); +} + +impl WritePipelineStatistics for RenderPass<'_> { + fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32) { + RenderPass::begin_pipeline_statistics_query(self, query_set, index); + } + + fn end_pipeline_statistics_query(&mut self) { + RenderPass::end_pipeline_statistics_query(self); + } +} + +impl WritePipelineStatistics for ComputePass<'_> { + fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32) { + ComputePass::begin_pipeline_statistics_query(self, query_set, index); + } + + fn end_pipeline_statistics_query(&mut self) { + ComputePass::end_pipeline_statistics_query(self); + } +} + +pub trait Pass: WritePipelineStatistics + WriteTimestamp { + const KIND: PassKind; +} + +impl Pass for RenderPass<'_> { + const KIND: PassKind = PassKind::Render; +} + +impl Pass for ComputePass<'_> { + const KIND: PassKind = PassKind::Compute; +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum PassKind { + Render, + Compute, +} diff --git a/crates/libmarathon/src/render/diagnostic/mod.rs b/crates/libmarathon/src/render/diagnostic/mod.rs new file mode 100644 index 0000000..6e03946 --- /dev/null +++ b/crates/libmarathon/src/render/diagnostic/mod.rs @@ -0,0 +1,188 @@ +//! Infrastructure for recording render diagnostics. +//! +//! For more info, see [`RenderDiagnosticsPlugin`]. + +pub(crate) mod internal; +#[cfg(feature = "tracing-tracy")] +mod tracy_gpu; + +use std::{borrow::Cow, sync::Arc}; +use core::marker::PhantomData; + +use bevy_app::{App, Plugin, PreUpdate}; + +use crate::render::{renderer::RenderAdapterInfo, RenderApp}; + +use self::internal::{ + sync_diagnostics, DiagnosticsRecorder, Pass, RenderDiagnosticsMutex, WriteTimestamp, +}; + +use crate::render::renderer::{RenderDevice, RenderQueue}; + +/// Enables collecting render diagnostics, such as CPU/GPU elapsed time per render pass, +/// as well as pipeline statistics (number of primitives, number of shader invocations, etc). +/// +/// To access the diagnostics, you can use the [`DiagnosticsStore`](bevy_diagnostic::DiagnosticsStore) resource, +/// add [`LogDiagnosticsPlugin`](bevy_diagnostic::LogDiagnosticsPlugin), or use [Tracy](https://github.com/bevyengine/bevy/blob/main/docs/profiling.md#tracy-renderqueue). +/// +/// To record diagnostics in your own passes: +/// 1. First, obtain the diagnostic recorder using [`RenderContext::diagnostic_recorder`](crate::renderer::RenderContext::diagnostic_recorder). +/// +/// It won't do anything unless [`RenderDiagnosticsPlugin`] is present, +/// so you're free to omit `#[cfg]` clauses. +/// ```ignore +/// let diagnostics = render_context.diagnostic_recorder(); +/// ``` +/// 2. Begin the span inside a command encoder, or a render/compute pass encoder. +/// ```ignore +/// let time_span = diagnostics.time_span(render_context.command_encoder(), "shadows"); +/// ``` +/// 3. End the span, providing the same encoder. +/// ```ignore +/// time_span.end(render_context.command_encoder()); +/// ``` +/// +/// # Supported platforms +/// Timestamp queries and pipeline statistics are currently supported only on Vulkan and DX12. +/// On other platforms (Metal, WebGPU, WebGL2) only CPU time will be recorded. +#[derive(Default)] +pub struct RenderDiagnosticsPlugin; + +impl Plugin for RenderDiagnosticsPlugin { + fn build(&self, app: &mut App) { + let render_diagnostics_mutex = RenderDiagnosticsMutex::default(); + app.insert_resource(render_diagnostics_mutex.clone()) + .add_systems(PreUpdate, sync_diagnostics); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.insert_resource(render_diagnostics_mutex); + } + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + let adapter_info = render_app.world().resource::(); + let device = render_app.world().resource::(); + let queue = render_app.world().resource::(); + render_app.insert_resource(DiagnosticsRecorder::new(adapter_info, device, queue)); + } +} + +/// Allows recording diagnostic spans. +pub trait RecordDiagnostics: Send + Sync { + /// Begin a time span, which will record elapsed CPU and GPU time. + /// + /// Returns a guard, which will panic on drop unless you end the span. + fn time_span(&self, encoder: &mut E, name: N) -> TimeSpanGuard<'_, Self, E> + where + E: WriteTimestamp, + N: Into>, + { + self.begin_time_span(encoder, name.into()); + TimeSpanGuard { + recorder: self, + marker: PhantomData, + } + } + + /// Begin a pass span, which will record elapsed CPU and GPU time, + /// as well as pipeline statistics on supported platforms. + /// + /// Returns a guard, which will panic on drop unless you end the span. + fn pass_span(&self, pass: &mut P, name: N) -> PassSpanGuard<'_, Self, P> + where + P: Pass, + N: Into>, + { + self.begin_pass_span(pass, name.into()); + PassSpanGuard { + recorder: self, + marker: PhantomData, + } + } + + #[doc(hidden)] + fn begin_time_span(&self, encoder: &mut E, name: Cow<'static, str>); + + #[doc(hidden)] + fn end_time_span(&self, encoder: &mut E); + + #[doc(hidden)] + fn begin_pass_span(&self, pass: &mut P, name: Cow<'static, str>); + + #[doc(hidden)] + fn end_pass_span(&self, pass: &mut P); +} + +/// Guard returned by [`RecordDiagnostics::time_span`]. +/// +/// Will panic on drop unless [`TimeSpanGuard::end`] is called. +pub struct TimeSpanGuard<'a, R: ?Sized, E> { + recorder: &'a R, + marker: PhantomData, +} + +impl TimeSpanGuard<'_, R, E> { + /// End the span. You have to provide the same encoder which was used to begin the span. + pub fn end(self, encoder: &mut E) { + self.recorder.end_time_span(encoder); + core::mem::forget(self); + } +} + +impl Drop for TimeSpanGuard<'_, R, E> { + fn drop(&mut self) { + panic!("TimeSpanScope::end was never called") + } +} + +/// Guard returned by [`RecordDiagnostics::pass_span`]. +/// +/// Will panic on drop unless [`PassSpanGuard::end`] is called. +pub struct PassSpanGuard<'a, R: ?Sized, P> { + recorder: &'a R, + marker: PhantomData

, +} + +impl PassSpanGuard<'_, R, P> { + /// End the span. You have to provide the same pass which was used to begin the span. + pub fn end(self, pass: &mut P) { + self.recorder.end_pass_span(pass); + core::mem::forget(self); + } +} + +impl Drop for PassSpanGuard<'_, R, P> { + fn drop(&mut self) { + panic!("PassSpanScope::end was never called") + } +} + +impl RecordDiagnostics for Option> { + fn begin_time_span(&self, encoder: &mut E, name: Cow<'static, str>) { + if let Some(recorder) = &self { + recorder.begin_time_span(encoder, name); + } + } + + fn end_time_span(&self, encoder: &mut E) { + if let Some(recorder) = &self { + recorder.end_time_span(encoder); + } + } + + fn begin_pass_span(&self, pass: &mut P, name: Cow<'static, str>) { + if let Some(recorder) = &self { + recorder.begin_pass_span(pass, name); + } + } + + fn end_pass_span(&self, pass: &mut P) { + if let Some(recorder) = &self { + recorder.end_pass_span(pass); + } + } +} diff --git a/crates/libmarathon/src/render/diagnostic/tracy_gpu.rs b/crates/libmarathon/src/render/diagnostic/tracy_gpu.rs new file mode 100644 index 0000000..2a86a15 --- /dev/null +++ b/crates/libmarathon/src/render/diagnostic/tracy_gpu.rs @@ -0,0 +1,69 @@ +use crate::render::renderer::{RenderAdapterInfo, RenderDevice, RenderQueue}; +use tracy_client::{Client, GpuContext, GpuContextType}; +use wgpu::{ + Backend, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, MapMode, PollType, + QuerySetDescriptor, QueryType, QUERY_SIZE, +}; + +pub fn new_tracy_gpu_context( + adapter_info: &RenderAdapterInfo, + device: &RenderDevice, + queue: &RenderQueue, +) -> GpuContext { + let tracy_gpu_backend = match adapter_info.backend { + Backend::Vulkan => GpuContextType::Vulkan, + Backend::Dx12 => GpuContextType::Direct3D12, + Backend::Gl => GpuContextType::OpenGL, + Backend::Metal | Backend::BrowserWebGpu | Backend::Noop => GpuContextType::Invalid, + }; + + let tracy_client = Client::running().unwrap(); + tracy_client + .new_gpu_context( + Some("RenderQueue"), + tracy_gpu_backend, + initial_timestamp(device, queue), + queue.get_timestamp_period(), + ) + .unwrap() +} + +// Code copied from https://github.com/Wumpf/wgpu-profiler/blob/f9de342a62cb75f50904a98d11dd2bbeb40ceab8/src/tracy.rs +fn initial_timestamp(device: &RenderDevice, queue: &RenderQueue) -> i64 { + let query_set = device.wgpu_device().create_query_set(&QuerySetDescriptor { + label: None, + ty: QueryType::Timestamp, + count: 1, + }); + + let resolve_buffer = device.create_buffer(&BufferDescriptor { + label: None, + size: QUERY_SIZE as _, + usage: BufferUsages::QUERY_RESOLVE | BufferUsages::COPY_SRC, + mapped_at_creation: false, + }); + + let map_buffer = device.create_buffer(&BufferDescriptor { + label: None, + size: QUERY_SIZE as _, + usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let mut timestamp_encoder = device.create_command_encoder(&CommandEncoderDescriptor::default()); + timestamp_encoder.write_timestamp(&query_set, 0); + timestamp_encoder.resolve_query_set(&query_set, 0..1, &resolve_buffer, 0); + // Workaround for https://github.com/gfx-rs/wgpu/issues/6406 + // TODO when that bug is fixed, merge these encoders together again + let mut copy_encoder = device.create_command_encoder(&CommandEncoderDescriptor::default()); + copy_encoder.copy_buffer_to_buffer(&resolve_buffer, 0, &map_buffer, 0, Some(QUERY_SIZE as _)); + queue.submit([timestamp_encoder.finish(), copy_encoder.finish()]); + + map_buffer.slice(..).map_async(MapMode::Read, |_| ()); + device + .poll(PollType::Wait) + .expect("Failed to poll device for map async"); + + let view = map_buffer.slice(..).get_mapped_range(); + i64::from_le_bytes((*view).try_into().unwrap()) +} diff --git a/crates/libmarathon/src/render/erased_render_asset.rs b/crates/libmarathon/src/render/erased_render_asset.rs new file mode 100644 index 0000000..dc30150 --- /dev/null +++ b/crates/libmarathon/src/render/erased_render_asset.rs @@ -0,0 +1,431 @@ +use crate::render::{ + render_resource::AsBindGroupError, ExtractSchedule, MainWorld, Render, RenderApp, + RenderSystems, +}; +use bevy_app::{App, Plugin, SubApp}; +use bevy_asset::RenderAssetUsages; +use bevy_asset::{Asset, AssetEvent, AssetId, Assets, UntypedAssetId}; +use bevy_ecs::{ + prelude::{Commands, IntoScheduleConfigs, MessageReader, Res, ResMut, Resource}, + schedule::{ScheduleConfigs, SystemSet}, + system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState}, + world::{FromWorld, Mut}, +}; +use bevy_platform::collections::{HashMap, HashSet}; +use crate::render::render_asset::RenderAssetBytesPerFrameLimiter; +use core::marker::PhantomData; +use thiserror::Error; +use tracing::{debug, error}; + +#[derive(Debug, Error)] +pub enum PrepareAssetError { + #[error("Failed to prepare asset")] + RetryNextUpdate(E), + #[error("Failed to build bind group: {0}")] + AsBindGroupError(AsBindGroupError), +} + +/// The system set during which we extract modified assets to the render world. +#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] +pub struct AssetExtractionSystems; + +/// Deprecated alias for [`AssetExtractionSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `AssetExtractionSystems`.")] +pub type ExtractAssetsSet = AssetExtractionSystems; + +/// Describes how an asset gets extracted and prepared for rendering. +/// +/// In the [`ExtractSchedule`] step the [`ErasedRenderAsset::SourceAsset`] is transferred +/// from the "main world" into the "render world". +/// +/// After that in the [`RenderSystems::PrepareAssets`] step the extracted asset +/// is transformed into its GPU-representation of type [`ErasedRenderAsset`]. +pub trait ErasedRenderAsset: Send + Sync + 'static { + /// The representation of the asset in the "main world". + type SourceAsset: Asset + Clone; + /// The target representation of the asset in the "render world". + type ErasedAsset: Send + Sync + 'static + Sized; + + /// Specifies all ECS data required by [`ErasedRenderAsset::prepare_asset`]. + /// + /// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`]. + type Param: SystemParam; + + /// Whether or not to unload the asset after extracting it to the render world. + #[inline] + fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages { + RenderAssetUsages::default() + } + + /// Size of the data the asset will upload to the gpu. Specifying a return value + /// will allow the asset to be throttled via [`RenderAssetBytesPerFrameLimiter`]. + #[inline] + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + fn byte_len(erased_asset: &Self::SourceAsset) -> Option { + None + } + + /// Prepares the [`ErasedRenderAsset::SourceAsset`] for the GPU by transforming it into a [`ErasedRenderAsset`]. + /// + /// ECS data may be accessed via `param`. + fn prepare_asset( + source_asset: Self::SourceAsset, + asset_id: AssetId, + param: &mut SystemParamItem, + ) -> Result>; + + /// Called whenever the [`ErasedRenderAsset::SourceAsset`] has been removed. + /// + /// You can implement this method if you need to access ECS data (via + /// `_param`) in order to perform cleanup tasks when the asset is removed. + /// + /// The default implementation does nothing. + fn unload_asset( + _source_asset: AssetId, + _param: &mut SystemParamItem, + ) { + } +} + +/// This plugin extracts the changed assets from the "app world" into the "render world" +/// and prepares them for the GPU. They can then be accessed from the [`ErasedRenderAssets`] resource. +/// +/// Therefore it sets up the [`ExtractSchedule`] and +/// [`RenderSystems::PrepareAssets`] steps for the specified [`ErasedRenderAsset`]. +/// +/// The `AFTER` generic parameter can be used to specify that `A::prepare_asset` should not be run until +/// `prepare_assets::` has completed. This allows the `prepare_asset` function to depend on another +/// prepared [`ErasedRenderAsset`], for example `Mesh::prepare_asset` relies on `ErasedRenderAssets::` for morph +/// targets, so the plugin is created as `ErasedRenderAssetPlugin::::default()`. +pub struct ErasedRenderAssetPlugin< + A: ErasedRenderAsset, + AFTER: ErasedRenderAssetDependency + 'static = (), +> { + phantom: PhantomData (A, AFTER)>, +} + +impl Default + for ErasedRenderAssetPlugin +{ + fn default() -> Self { + Self { + phantom: Default::default(), + } + } +} + +impl Plugin + for ErasedRenderAssetPlugin +{ + fn build(&self, app: &mut App) { + app.init_resource::>(); + } + + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::>() + .init_resource::>() + .init_resource::>() + .add_systems( + ExtractSchedule, + extract_erased_render_asset::.in_set(AssetExtractionSystems), + ); + AFTER::register_system( + render_app, + prepare_erased_assets::.in_set(RenderSystems::PrepareAssets), + ); + } + } +} + +// helper to allow specifying dependencies between render assets +pub trait ErasedRenderAssetDependency { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs); +} + +impl ErasedRenderAssetDependency for () { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs) { + render_app.add_systems(Render, system); + } +} + +impl ErasedRenderAssetDependency for A { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs) { + render_app.add_systems(Render, system.after(prepare_erased_assets::)); + } +} + +/// Temporarily stores the extracted and removed assets of the current frame. +#[derive(Resource)] +pub struct ExtractedAssets { + /// The assets extracted this frame. + /// + /// These are assets that were either added or modified this frame. + pub extracted: Vec<(AssetId, A::SourceAsset)>, + + /// IDs of the assets that were removed this frame. + /// + /// These assets will not be present in [`ExtractedAssets::extracted`]. + pub removed: HashSet>, + + /// IDs of the assets that were modified this frame. + pub modified: HashSet>, + + /// IDs of the assets that were added this frame. + pub added: HashSet>, +} + +impl Default for ExtractedAssets { + fn default() -> Self { + Self { + extracted: Default::default(), + removed: Default::default(), + modified: Default::default(), + added: Default::default(), + } + } +} + +/// Stores all GPU representations ([`ErasedRenderAsset`]) +/// of [`ErasedRenderAsset::SourceAsset`] as long as they exist. +#[derive(Resource)] +pub struct ErasedRenderAssets(HashMap); + +impl Default for ErasedRenderAssets { + fn default() -> Self { + Self(Default::default()) + } +} + +impl ErasedRenderAssets { + pub fn get(&self, id: impl Into) -> Option<&ERA> { + self.0.get(&id.into()) + } + + pub fn get_mut(&mut self, id: impl Into) -> Option<&mut ERA> { + self.0.get_mut(&id.into()) + } + + pub fn insert(&mut self, id: impl Into, value: ERA) -> Option { + self.0.insert(id.into(), value) + } + + pub fn remove(&mut self, id: impl Into) -> Option { + self.0.remove(&id.into()) + } + + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|(k, v)| (*k, v)) + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.0.iter_mut().map(|(k, v)| (*k, v)) + } +} + +#[derive(Resource)] +struct CachedExtractErasedRenderAssetSystemState { + state: SystemState<( + MessageReader<'static, 'static, AssetEvent>, + ResMut<'static, Assets>, + )>, +} + +impl FromWorld for CachedExtractErasedRenderAssetSystemState { + fn from_world(world: &mut bevy_ecs::world::World) -> Self { + Self { + state: SystemState::new(world), + } + } +} + +/// This system extracts all created or modified assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type +/// into the "render world". +pub(crate) fn extract_erased_render_asset( + mut commands: Commands, + mut main_world: ResMut, +) { + main_world.resource_scope( + |world, mut cached_state: Mut>| { + let (mut events, mut assets) = cached_state.state.get_mut(world); + + let mut needs_extracting = >::default(); + let mut removed = >::default(); + let mut modified = >::default(); + + for event in events.read() { + #[expect( + clippy::match_same_arms, + reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon." + )] + match event { + AssetEvent::Added { id } => { + needs_extracting.insert(*id); + } + AssetEvent::Modified { id } => { + needs_extracting.insert(*id); + modified.insert(*id); + } + AssetEvent::Removed { .. } => { + // We don't care that the asset was removed from Assets in the main world. + // An asset is only removed from ErasedRenderAssets when its last handle is dropped (AssetEvent::Unused). + } + AssetEvent::Unused { id } => { + needs_extracting.remove(id); + modified.remove(id); + removed.insert(*id); + } + AssetEvent::LoadedWithDependencies { .. } => { + // TODO: handle this + } + } + } + + let mut extracted_assets = Vec::new(); + let mut added = >::default(); + for id in needs_extracting.drain() { + if let Some(asset) = assets.get(id) { + let asset_usage = A::asset_usage(asset); + if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) { + if asset_usage == RenderAssetUsages::RENDER_WORLD { + if let Some(asset) = assets.remove(id) { + extracted_assets.push((id, asset)); + added.insert(id); + } + } else { + extracted_assets.push((id, asset.clone())); + added.insert(id); + } + } + } + } + + commands.insert_resource(ExtractedAssets:: { + extracted: extracted_assets, + removed, + modified, + added, + }); + cached_state.state.apply(world); + }, + ); +} + +// TODO: consider storing inside system? +/// All assets that should be prepared next frame. +#[derive(Resource)] +pub struct PrepareNextFrameAssets { + assets: Vec<(AssetId, A::SourceAsset)>, +} + +impl Default for PrepareNextFrameAssets { + fn default() -> Self { + Self { + assets: Default::default(), + } + } +} + +/// This system prepares all assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type +/// which where extracted this frame for the GPU. +pub fn prepare_erased_assets( + mut extracted_assets: ResMut>, + mut render_assets: ResMut>, + mut prepare_next_frame: ResMut>, + param: StaticSystemParam<::Param>, + bpf: Res, +) { + let mut wrote_asset_count = 0; + + let mut param = param.into_inner(); + let queued_assets = core::mem::take(&mut prepare_next_frame.assets); + for (id, extracted_asset) in queued_assets { + if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) { + // skip previous frame's assets that have been removed or updated + continue; + } + + let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { + // we could check if available bytes > byte_len here, but we want to make some + // forward progress even if the asset is larger than the max bytes per frame. + // this way we always write at least one (sized) asset per frame. + // in future we could also consider partial asset uploads. + if bpf.exhausted() { + prepare_next_frame.assets.push((id, extracted_asset)); + continue; + } + size + } else { + 0 + }; + + match A::prepare_asset(extracted_asset, id, &mut param) { + Ok(prepared_asset) => { + render_assets.insert(id, prepared_asset); + bpf.write_bytes(write_bytes); + wrote_asset_count += 1; + } + Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { + prepare_next_frame.assets.push((id, extracted_asset)); + } + Err(PrepareAssetError::AsBindGroupError(e)) => { + error!( + "{} Bind group construction failed: {e}", + core::any::type_name::() + ); + } + } + } + + for removed in extracted_assets.removed.drain() { + render_assets.remove(removed); + A::unload_asset(removed, &mut param); + } + + for (id, extracted_asset) in extracted_assets.extracted.drain(..) { + // we remove previous here to ensure that if we are updating the asset then + // any users will not see the old asset after a new asset is extracted, + // even if the new asset is not yet ready or we are out of bytes to write. + render_assets.remove(id); + + let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { + if bpf.exhausted() { + prepare_next_frame.assets.push((id, extracted_asset)); + continue; + } + size + } else { + 0 + }; + + match A::prepare_asset(extracted_asset, id, &mut param) { + Ok(prepared_asset) => { + render_assets.insert(id, prepared_asset); + bpf.write_bytes(write_bytes); + wrote_asset_count += 1; + } + Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { + prepare_next_frame.assets.push((id, extracted_asset)); + } + Err(PrepareAssetError::AsBindGroupError(e)) => { + error!( + "{} Bind group construction failed: {e}", + core::any::type_name::() + ); + } + } + } + + if bpf.exhausted() && !prepare_next_frame.assets.is_empty() { + debug!( + "{} write budget exhausted with {} assets remaining (wrote {})", + core::any::type_name::(), + prepare_next_frame.assets.len(), + wrote_asset_count + ); + } +} diff --git a/crates/libmarathon/src/render/experimental/mip_generation/downsample_depth.wgsl b/crates/libmarathon/src/render/experimental/mip_generation/downsample_depth.wgsl new file mode 100644 index 0000000..12a4d2b --- /dev/null +++ b/crates/libmarathon/src/render/experimental/mip_generation/downsample_depth.wgsl @@ -0,0 +1,338 @@ +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@group(0) @binding(0) var mip_0: texture_storage_2d; +#else +#ifdef MESHLET +@group(0) @binding(0) var mip_0: texture_storage_2d; +#else // MESHLET +#ifdef MULTISAMPLE +@group(0) @binding(0) var mip_0: texture_depth_multisampled_2d; +#else // MULTISAMPLE +@group(0) @binding(0) var mip_0: texture_depth_2d; +#endif // MULTISAMPLE +#endif // MESHLET +#endif // MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@group(0) @binding(1) var mip_1: texture_storage_2d; +@group(0) @binding(2) var mip_2: texture_storage_2d; +@group(0) @binding(3) var mip_3: texture_storage_2d; +@group(0) @binding(4) var mip_4: texture_storage_2d; +@group(0) @binding(5) var mip_5: texture_storage_2d; +@group(0) @binding(6) var mip_6: texture_storage_2d; +@group(0) @binding(7) var mip_7: texture_storage_2d; +@group(0) @binding(8) var mip_8: texture_storage_2d; +@group(0) @binding(9) var mip_9: texture_storage_2d; +@group(0) @binding(10) var mip_10: texture_storage_2d; +@group(0) @binding(11) var mip_11: texture_storage_2d; +@group(0) @binding(12) var mip_12: texture_storage_2d; +@group(0) @binding(13) var samplr: sampler; +struct Constants { max_mip_level: u32 } +var constants: Constants; + +/// Generates a hierarchical depth buffer. +/// Based on FidelityFX SPD v2.1 https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/blob/d7531ae47d8b36a5d4025663e731a47a38be882f/sdk/include/FidelityFX/gpu/spd/ffx_spd.h#L528 + +// TODO: +// * Subgroup support +// * True single pass downsampling + +var intermediate_memory: array, 16>; + +@compute +@workgroup_size(256, 1, 1) +fn downsample_depth_first( + @builtin(workgroup_id) workgroup_id: vec3u, + @builtin(local_invocation_index) local_invocation_index: u32, +) { + let sub_xy = remap_for_wave_reduction(local_invocation_index % 64u); + let x = sub_xy.x + 8u * ((local_invocation_index >> 6u) % 2u); + let y = sub_xy.y + 8u * (local_invocation_index >> 7u); + + downsample_mips_0_and_1(x, y, workgroup_id.xy, local_invocation_index); + + downsample_mips_2_to_5(x, y, workgroup_id.xy, local_invocation_index); +} + +@compute +@workgroup_size(256, 1, 1) +fn downsample_depth_second(@builtin(local_invocation_index) local_invocation_index: u32) { + let sub_xy = remap_for_wave_reduction(local_invocation_index % 64u); + let x = sub_xy.x + 8u * ((local_invocation_index >> 6u) % 2u); + let y = sub_xy.y + 8u * (local_invocation_index >> 7u); + + downsample_mips_6_and_7(x, y); + + downsample_mips_8_to_11(x, y, local_invocation_index); +} + +fn downsample_mips_0_and_1(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) { + var v: vec4f; + + var tex = vec2(workgroup_id * 64u) + vec2(x * 2u, y * 2u); + var pix = vec2(workgroup_id * 32u) + vec2(x, y); + v[0] = reduce_load_mip_0(tex); + textureStore(mip_1, pix, vec4(v[0])); + + tex = vec2(workgroup_id * 64u) + vec2(x * 2u + 32u, y * 2u); + pix = vec2(workgroup_id * 32u) + vec2(x + 16u, y); + v[1] = reduce_load_mip_0(tex); + textureStore(mip_1, pix, vec4(v[1])); + + tex = vec2(workgroup_id * 64u) + vec2(x * 2u, y * 2u + 32u); + pix = vec2(workgroup_id * 32u) + vec2(x, y + 16u); + v[2] = reduce_load_mip_0(tex); + textureStore(mip_1, pix, vec4(v[2])); + + tex = vec2(workgroup_id * 64u) + vec2(x * 2u + 32u, y * 2u + 32u); + pix = vec2(workgroup_id * 32u) + vec2(x + 16u, y + 16u); + v[3] = reduce_load_mip_0(tex); + textureStore(mip_1, pix, vec4(v[3])); + + if constants.max_mip_level <= 1u { return; } + + for (var i = 0u; i < 4u; i++) { + intermediate_memory[x][y] = v[i]; + workgroupBarrier(); + if local_invocation_index < 64u { + v[i] = reduce_4(vec4( + intermediate_memory[x * 2u + 0u][y * 2u + 0u], + intermediate_memory[x * 2u + 1u][y * 2u + 0u], + intermediate_memory[x * 2u + 0u][y * 2u + 1u], + intermediate_memory[x * 2u + 1u][y * 2u + 1u], + )); + pix = (workgroup_id * 16u) + vec2( + x + (i % 2u) * 8u, + y + (i / 2u) * 8u, + ); + textureStore(mip_2, pix, vec4(v[i])); + } + workgroupBarrier(); + } + + if local_invocation_index < 64u { + intermediate_memory[x + 0u][y + 0u] = v[0]; + intermediate_memory[x + 8u][y + 0u] = v[1]; + intermediate_memory[x + 0u][y + 8u] = v[2]; + intermediate_memory[x + 8u][y + 8u] = v[3]; + } +} + +fn downsample_mips_2_to_5(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) { + if constants.max_mip_level <= 2u { return; } + workgroupBarrier(); + downsample_mip_2(x, y, workgroup_id, local_invocation_index); + + if constants.max_mip_level <= 3u { return; } + workgroupBarrier(); + downsample_mip_3(x, y, workgroup_id, local_invocation_index); + + if constants.max_mip_level <= 4u { return; } + workgroupBarrier(); + downsample_mip_4(x, y, workgroup_id, local_invocation_index); + + if constants.max_mip_level <= 5u { return; } + workgroupBarrier(); + downsample_mip_5(workgroup_id, local_invocation_index); +} + +fn downsample_mip_2(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) { + if local_invocation_index < 64u { + let v = reduce_4(vec4( + intermediate_memory[x * 2u + 0u][y * 2u + 0u], + intermediate_memory[x * 2u + 1u][y * 2u + 0u], + intermediate_memory[x * 2u + 0u][y * 2u + 1u], + intermediate_memory[x * 2u + 1u][y * 2u + 1u], + )); + textureStore(mip_3, (workgroup_id * 8u) + vec2(x, y), vec4(v)); + intermediate_memory[x * 2u + y % 2u][y * 2u] = v; + } +} + +fn downsample_mip_3(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) { + if local_invocation_index < 16u { + let v = reduce_4(vec4( + intermediate_memory[x * 4u + 0u + 0u][y * 4u + 0u], + intermediate_memory[x * 4u + 2u + 0u][y * 4u + 0u], + intermediate_memory[x * 4u + 0u + 1u][y * 4u + 2u], + intermediate_memory[x * 4u + 2u + 1u][y * 4u + 2u], + )); + textureStore(mip_4, (workgroup_id * 4u) + vec2(x, y), vec4(v)); + intermediate_memory[x * 4u + y][y * 4u] = v; + } +} + +fn downsample_mip_4(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32) { + if local_invocation_index < 4u { + let v = reduce_4(vec4( + intermediate_memory[x * 8u + 0u + 0u + y * 2u][y * 8u + 0u], + intermediate_memory[x * 8u + 4u + 0u + y * 2u][y * 8u + 0u], + intermediate_memory[x * 8u + 0u + 1u + y * 2u][y * 8u + 4u], + intermediate_memory[x * 8u + 4u + 1u + y * 2u][y * 8u + 4u], + )); + textureStore(mip_5, (workgroup_id * 2u) + vec2(x, y), vec4(v)); + intermediate_memory[x + y * 2u][0u] = v; + } +} + +fn downsample_mip_5(workgroup_id: vec2u, local_invocation_index: u32) { + if local_invocation_index < 1u { + let v = reduce_4(vec4( + intermediate_memory[0u][0u], + intermediate_memory[1u][0u], + intermediate_memory[2u][0u], + intermediate_memory[3u][0u], + )); + textureStore(mip_6, workgroup_id, vec4(v)); + } +} + +fn downsample_mips_6_and_7(x: u32, y: u32) { + var v: vec4f; + + var tex = vec2(x * 4u + 0u, y * 4u + 0u); + var pix = vec2(x * 2u + 0u, y * 2u + 0u); + v[0] = reduce_load_mip_6(tex); + textureStore(mip_7, pix, vec4(v[0])); + + tex = vec2(x * 4u + 2u, y * 4u + 0u); + pix = vec2(x * 2u + 1u, y * 2u + 0u); + v[1] = reduce_load_mip_6(tex); + textureStore(mip_7, pix, vec4(v[1])); + + tex = vec2(x * 4u + 0u, y * 4u + 2u); + pix = vec2(x * 2u + 0u, y * 2u + 1u); + v[2] = reduce_load_mip_6(tex); + textureStore(mip_7, pix, vec4(v[2])); + + tex = vec2(x * 4u + 2u, y * 4u + 2u); + pix = vec2(x * 2u + 1u, y * 2u + 1u); + v[3] = reduce_load_mip_6(tex); + textureStore(mip_7, pix, vec4(v[3])); + + if constants.max_mip_level <= 7u { return; } + + let vr = reduce_4(v); + textureStore(mip_8, vec2(x, y), vec4(vr)); + intermediate_memory[x][y] = vr; +} + +fn downsample_mips_8_to_11(x: u32, y: u32, local_invocation_index: u32) { + if constants.max_mip_level <= 8u { return; } + workgroupBarrier(); + downsample_mip_8(x, y, local_invocation_index); + + if constants.max_mip_level <= 9u { return; } + workgroupBarrier(); + downsample_mip_9(x, y, local_invocation_index); + + if constants.max_mip_level <= 10u { return; } + workgroupBarrier(); + downsample_mip_10(x, y, local_invocation_index); + + if constants.max_mip_level <= 11u { return; } + workgroupBarrier(); + downsample_mip_11(local_invocation_index); +} + +fn downsample_mip_8(x: u32, y: u32, local_invocation_index: u32) { + if local_invocation_index < 64u { + let v = reduce_4(vec4( + intermediate_memory[x * 2u + 0u][y * 2u + 0u], + intermediate_memory[x * 2u + 1u][y * 2u + 0u], + intermediate_memory[x * 2u + 0u][y * 2u + 1u], + intermediate_memory[x * 2u + 1u][y * 2u + 1u], + )); + textureStore(mip_9, vec2(x, y), vec4(v)); + intermediate_memory[x * 2u + y % 2u][y * 2u] = v; + } +} + +fn downsample_mip_9(x: u32, y: u32, local_invocation_index: u32) { + if local_invocation_index < 16u { + let v = reduce_4(vec4( + intermediate_memory[x * 4u + 0u + 0u][y * 4u + 0u], + intermediate_memory[x * 4u + 2u + 0u][y * 4u + 0u], + intermediate_memory[x * 4u + 0u + 1u][y * 4u + 2u], + intermediate_memory[x * 4u + 2u + 1u][y * 4u + 2u], + )); + textureStore(mip_10, vec2(x, y), vec4(v)); + intermediate_memory[x * 4u + y][y * 4u] = v; + } +} + +fn downsample_mip_10(x: u32, y: u32, local_invocation_index: u32) { + if local_invocation_index < 4u { + let v = reduce_4(vec4( + intermediate_memory[x * 8u + 0u + 0u + y * 2u][y * 8u + 0u], + intermediate_memory[x * 8u + 4u + 0u + y * 2u][y * 8u + 0u], + intermediate_memory[x * 8u + 0u + 1u + y * 2u][y * 8u + 4u], + intermediate_memory[x * 8u + 4u + 1u + y * 2u][y * 8u + 4u], + )); + textureStore(mip_11, vec2(x, y), vec4(v)); + intermediate_memory[x + y * 2u][0u] = v; + } +} + +fn downsample_mip_11(local_invocation_index: u32) { + if local_invocation_index < 1u { + let v = reduce_4(vec4( + intermediate_memory[0u][0u], + intermediate_memory[1u][0u], + intermediate_memory[2u][0u], + intermediate_memory[3u][0u], + )); + textureStore(mip_12, vec2(0u, 0u), vec4(v)); + } +} + +fn remap_for_wave_reduction(a: u32) -> vec2u { + return vec2( + insertBits(extractBits(a, 2u, 3u), a, 0u, 1u), + insertBits(extractBits(a, 3u, 3u), extractBits(a, 1u, 2u), 0u, 2u), + ); +} + +fn reduce_load_mip_0(tex: vec2u) -> f32 { + let a = load_mip_0(tex.x, tex.y); + let b = load_mip_0(tex.x + 1u, tex.y); + let c = load_mip_0(tex.x, tex.y + 1u); + let d = load_mip_0(tex.x + 1u, tex.y + 1u); + return reduce_4(vec4(a, b, c, d)); +} + +fn reduce_load_mip_6(tex: vec2u) -> f32 { + return reduce_4(vec4( + textureLoad(mip_6, tex + vec2(0u, 0u)).r, + textureLoad(mip_6, tex + vec2(0u, 1u)).r, + textureLoad(mip_6, tex + vec2(1u, 0u)).r, + textureLoad(mip_6, tex + vec2(1u, 1u)).r, + )); +} + +fn load_mip_0(x: u32, y: u32) -> f32 { +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + let visibility = textureLoad(mip_0, vec2(x, y)).r; + return bitcast(u32(visibility >> 32u)); +#else // MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +#ifdef MESHLET + let visibility = textureLoad(mip_0, vec2(x, y)).r; + return bitcast(visibility); +#else // MESHLET + // Downsample the top level. +#ifdef MULTISAMPLE + // The top level is multisampled, so we need to loop over all the samples + // and reduce them to 1. + var result = textureLoad(mip_0, vec2(x, y), 0); + let sample_count = i32(textureNumSamples(mip_0)); + for (var sample = 1; sample < sample_count; sample += 1) { + result = min(result, textureLoad(mip_0, vec2(x, y), sample)); + } + return result; +#else // MULTISAMPLE + return textureLoad(mip_0, vec2(x, y), 0); +#endif // MULTISAMPLE +#endif // MESHLET +#endif // MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +} + +fn reduce_4(v: vec4f) -> f32 { + return min(min(v.x, v.y), min(v.z, v.w)); +} diff --git a/crates/libmarathon/src/render/experimental/mip_generation/mod.rs b/crates/libmarathon/src/render/experimental/mip_generation/mod.rs new file mode 100644 index 0000000..f773e75 --- /dev/null +++ b/crates/libmarathon/src/render/experimental/mip_generation/mod.rs @@ -0,0 +1,783 @@ +//! Downsampling of textures to produce mipmap levels. +//! +//! Currently, this module only supports generation of hierarchical Z buffers +//! for occlusion culling. It's marked experimental because the shader is +//! designed only for power-of-two texture sizes and is slightly incorrect for +//! non-power-of-two depth buffer sizes. + +use core::array; + +use crate::render::core_3d::{ + graph::{Core3d, Node3d}, + prepare_core_3d_depth_textures, +}; +use bevy_app::{App, Plugin}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + prelude::{resource_exists, Without}, + query::{Or, QueryState, With}, + resource::Resource, + schedule::IntoScheduleConfigs as _, + system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_math::{uvec2, UVec2, Vec4Swizzles as _}; +use crate::render::{batching::gpu_preprocessing::GpuPreprocessingSupport, RenderStartup}; +use crate::render::{ + experimental::occlusion_culling::{ + OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities, + }, + render_graph::{Node, NodeRunError, RenderGraphContext, RenderGraphExt}, + render_resource::{ + binding_types::{sampler, texture_2d, texture_2d_multisampled, texture_storage_2d}, + BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, + CachedComputePipelineId, ComputePassDescriptor, ComputePipeline, ComputePipelineDescriptor, + Extent3d, IntoBinding, PipelineCache, PushConstantRange, Sampler, SamplerBindingType, + SamplerDescriptor, ShaderStages, SpecializedComputePipeline, SpecializedComputePipelines, + StorageTextureAccess, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, + TextureSampleType, TextureUsages, TextureView, TextureViewDescriptor, TextureViewDimension, + }, + renderer::{RenderContext, RenderDevice}, + texture::TextureCache, + view::{ExtractedView, NoIndirectDrawing, ViewDepthTexture}, + Render, RenderApp, RenderSystems, +}; +use bevy_shader::Shader; +use bevy_utils::default; +use bitflags::bitflags; +use tracing::debug; + +/// Identifies the `downsample_depth.wgsl` shader. +#[derive(Resource, Deref)] +pub struct DownsampleDepthShader(Handle); + +/// The maximum number of mip levels that we can produce. +/// +/// 2^12 is 4096, so that's the maximum size of the depth buffer that we +/// support. +pub const DEPTH_PYRAMID_MIP_COUNT: usize = 12; + +/// A plugin that allows Bevy to repeatedly downsample textures to create +/// mipmaps. +/// +/// Currently, this is only used for hierarchical Z buffer generation for the +/// purposes of occlusion culling. +pub struct MipGenerationPlugin; + +impl Plugin for MipGenerationPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "downsample_depth.wgsl"); + + let downsample_depth_shader = load_embedded_asset!(app, "downsample_depth.wgsl"); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .insert_resource(DownsampleDepthShader(downsample_depth_shader)) + .init_resource::>() + .add_render_graph_node::(Core3d, Node3d::EarlyDownsampleDepth) + .add_render_graph_node::(Core3d, Node3d::LateDownsampleDepth) + .add_render_graph_edges( + Core3d, + ( + Node3d::EarlyPrepass, + Node3d::EarlyDeferredPrepass, + Node3d::EarlyDownsampleDepth, + Node3d::LatePrepass, + Node3d::LateDeferredPrepass, + ), + ) + .add_render_graph_edges( + Core3d, + ( + Node3d::StartMainPassPostProcessing, + Node3d::LateDownsampleDepth, + Node3d::EndMainPassPostProcessing, + ), + ) + .add_systems(RenderStartup, init_depth_pyramid_dummy_texture) + .add_systems( + Render, + create_downsample_depth_pipelines.in_set(RenderSystems::Prepare), + ) + .add_systems( + Render, + ( + prepare_view_depth_pyramids, + prepare_downsample_depth_view_bind_groups, + ) + .chain() + .in_set(RenderSystems::PrepareResources) + .run_if(resource_exists::) + .after(prepare_core_3d_depth_textures), + ); + } +} + +/// The nodes that produce a hierarchical Z-buffer, also known as a depth +/// pyramid. +/// +/// This runs the single-pass downsampling (SPD) shader with the *min* filter in +/// order to generate a series of mipmaps for the Z buffer. The resulting +/// hierarchical Z-buffer can be used for occlusion culling. +/// +/// There are two instances of this node. The *early* downsample depth pass is +/// the first hierarchical Z-buffer stage, which runs after the early prepass +/// and before the late prepass. It prepares the Z-buffer for the bounding box +/// tests that the late mesh preprocessing stage will perform. The *late* +/// downsample depth pass runs at the end of the main phase. It prepares the +/// Z-buffer for the occlusion culling that the early mesh preprocessing phase +/// of the *next* frame will perform. +/// +/// This node won't do anything if occlusion culling isn't on. +pub struct DownsampleDepthNode { + /// The query that we use to find views that need occlusion culling for + /// their Z-buffer. + main_view_query: QueryState<( + Read, + Read, + Read, + Option>, + )>, + /// The query that we use to find shadow maps that need occlusion culling. + shadow_view_query: QueryState<( + Read, + Read, + Read, + )>, +} + +impl FromWorld for DownsampleDepthNode { + fn from_world(world: &mut World) -> Self { + Self { + main_view_query: QueryState::new(world), + shadow_view_query: QueryState::new(world), + } + } +} + +impl Node for DownsampleDepthNode { + fn update(&mut self, world: &mut World) { + self.main_view_query.update_archetypes(world); + self.shadow_view_query.update_archetypes(world); + } + + fn run<'w>( + &self, + render_graph_context: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let Ok(( + view_depth_pyramid, + view_downsample_depth_bind_group, + view_depth_texture, + maybe_view_light_entities, + )) = self + .main_view_query + .get_manual(world, render_graph_context.view_entity()) + else { + return Ok(()); + }; + + // Downsample depth for the main Z-buffer. + downsample_depth( + render_graph_context, + render_context, + world, + view_depth_pyramid, + view_downsample_depth_bind_group, + uvec2( + view_depth_texture.texture.width(), + view_depth_texture.texture.height(), + ), + view_depth_texture.texture.sample_count(), + )?; + + // Downsample depth for shadow maps that have occlusion culling enabled. + if let Some(view_light_entities) = maybe_view_light_entities { + for &view_light_entity in &view_light_entities.0 { + let Ok((view_depth_pyramid, view_downsample_depth_bind_group, occlusion_culling)) = + self.shadow_view_query.get_manual(world, view_light_entity) + else { + continue; + }; + downsample_depth( + render_graph_context, + render_context, + world, + view_depth_pyramid, + view_downsample_depth_bind_group, + UVec2::splat(occlusion_culling.depth_texture_size), + 1, + )?; + } + } + + Ok(()) + } +} + +/// Produces a depth pyramid from the current depth buffer for a single view. +/// The resulting depth pyramid can be used for occlusion testing. +fn downsample_depth<'w>( + render_graph_context: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + view_depth_pyramid: &ViewDepthPyramid, + view_downsample_depth_bind_group: &ViewDownsampleDepthBindGroup, + view_size: UVec2, + sample_count: u32, +) -> Result<(), NodeRunError> { + let downsample_depth_pipelines = world.resource::(); + let pipeline_cache = world.resource::(); + + // Despite the name "single-pass downsampling", we actually need two + // passes because of the lack of `coherent` buffers in WGPU/WGSL. + // Between each pass, there's an implicit synchronization barrier. + + // Fetch the appropriate pipeline ID, depending on whether the depth + // buffer is multisampled or not. + let (Some(first_downsample_depth_pipeline_id), Some(second_downsample_depth_pipeline_id)) = + (if sample_count > 1 { + ( + downsample_depth_pipelines.first_multisample.pipeline_id, + downsample_depth_pipelines.second_multisample.pipeline_id, + ) + } else { + ( + downsample_depth_pipelines.first.pipeline_id, + downsample_depth_pipelines.second.pipeline_id, + ) + }) + else { + return Ok(()); + }; + + // Fetch the pipelines for the two passes. + let (Some(first_downsample_depth_pipeline), Some(second_downsample_depth_pipeline)) = ( + pipeline_cache.get_compute_pipeline(first_downsample_depth_pipeline_id), + pipeline_cache.get_compute_pipeline(second_downsample_depth_pipeline_id), + ) else { + return Ok(()); + }; + + // Run the depth downsampling. + view_depth_pyramid.downsample_depth( + &format!("{:?}", render_graph_context.label()), + render_context, + view_size, + view_downsample_depth_bind_group, + first_downsample_depth_pipeline, + second_downsample_depth_pipeline, + ); + Ok(()) +} + +/// A single depth downsample pipeline. +#[derive(Resource)] +pub struct DownsampleDepthPipeline { + /// The bind group layout for this pipeline. + bind_group_layout: BindGroupLayout, + /// A handle that identifies the compiled shader. + pipeline_id: Option, + /// The shader asset handle. + shader: Handle, +} + +impl DownsampleDepthPipeline { + /// Creates a new [`DownsampleDepthPipeline`] from a bind group layout and the downsample + /// shader. + /// + /// This doesn't actually specialize the pipeline; that must be done + /// afterward. + fn new(bind_group_layout: BindGroupLayout, shader: Handle) -> DownsampleDepthPipeline { + DownsampleDepthPipeline { + bind_group_layout, + pipeline_id: None, + shader, + } + } +} + +/// Stores all depth buffer downsampling pipelines. +#[derive(Resource)] +pub struct DownsampleDepthPipelines { + /// The first pass of the pipeline, when the depth buffer is *not* + /// multisampled. + first: DownsampleDepthPipeline, + /// The second pass of the pipeline, when the depth buffer is *not* + /// multisampled. + second: DownsampleDepthPipeline, + /// The first pass of the pipeline, when the depth buffer is multisampled. + first_multisample: DownsampleDepthPipeline, + /// The second pass of the pipeline, when the depth buffer is multisampled. + second_multisample: DownsampleDepthPipeline, + /// The sampler that the depth downsampling shader uses to sample the depth + /// buffer. + sampler: Sampler, +} + +/// Creates the [`DownsampleDepthPipelines`] if downsampling is supported on the +/// current platform. +fn create_downsample_depth_pipelines( + mut commands: Commands, + render_device: Res, + pipeline_cache: Res, + mut specialized_compute_pipelines: ResMut>, + gpu_preprocessing_support: Res, + downsample_depth_shader: Res, + mut has_run: Local, +) { + // Only run once. + // We can't use a `resource_exists` or similar run condition here because + // this function might fail to create downsample depth pipelines if the + // current platform doesn't support compute shaders. + if *has_run { + return; + } + *has_run = true; + + if !gpu_preprocessing_support.is_culling_supported() { + debug!("Downsample depth is not supported on this platform."); + return; + } + + // Create the bind group layouts. The bind group layouts are identical + // between the first and second passes, so the only thing we need to + // treat specially is the type of the first mip level (non-multisampled + // or multisampled). + let standard_bind_group_layout = + create_downsample_depth_bind_group_layout(&render_device, false); + let multisampled_bind_group_layout = + create_downsample_depth_bind_group_layout(&render_device, true); + + // Create the depth pyramid sampler. This is shared among all shaders. + let sampler = render_device.create_sampler(&SamplerDescriptor { + label: Some("depth pyramid sampler"), + ..SamplerDescriptor::default() + }); + + // Initialize the pipelines. + let mut downsample_depth_pipelines = DownsampleDepthPipelines { + first: DownsampleDepthPipeline::new( + standard_bind_group_layout.clone(), + downsample_depth_shader.0.clone(), + ), + second: DownsampleDepthPipeline::new( + standard_bind_group_layout.clone(), + downsample_depth_shader.0.clone(), + ), + first_multisample: DownsampleDepthPipeline::new( + multisampled_bind_group_layout.clone(), + downsample_depth_shader.0.clone(), + ), + second_multisample: DownsampleDepthPipeline::new( + multisampled_bind_group_layout.clone(), + downsample_depth_shader.0.clone(), + ), + sampler, + }; + + // Specialize each pipeline with the appropriate + // `DownsampleDepthPipelineKey`. + downsample_depth_pipelines.first.pipeline_id = Some(specialized_compute_pipelines.specialize( + &pipeline_cache, + &downsample_depth_pipelines.first, + DownsampleDepthPipelineKey::empty(), + )); + downsample_depth_pipelines.second.pipeline_id = Some(specialized_compute_pipelines.specialize( + &pipeline_cache, + &downsample_depth_pipelines.second, + DownsampleDepthPipelineKey::SECOND_PHASE, + )); + downsample_depth_pipelines.first_multisample.pipeline_id = + Some(specialized_compute_pipelines.specialize( + &pipeline_cache, + &downsample_depth_pipelines.first_multisample, + DownsampleDepthPipelineKey::MULTISAMPLE, + )); + downsample_depth_pipelines.second_multisample.pipeline_id = + Some(specialized_compute_pipelines.specialize( + &pipeline_cache, + &downsample_depth_pipelines.second_multisample, + DownsampleDepthPipelineKey::SECOND_PHASE | DownsampleDepthPipelineKey::MULTISAMPLE, + )); + + commands.insert_resource(downsample_depth_pipelines); +} + +/// Creates a single bind group layout for the downsample depth pass. +fn create_downsample_depth_bind_group_layout( + render_device: &RenderDevice, + is_multisampled: bool, +) -> BindGroupLayout { + render_device.create_bind_group_layout( + if is_multisampled { + "downsample multisample depth bind group layout" + } else { + "downsample depth bind group layout" + }, + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + // We only care about the multisample status of the depth buffer + // for the first mip level. After the first mip level is + // sampled, we drop to a single sample. + if is_multisampled { + texture_2d_multisampled(TextureSampleType::Depth) + } else { + texture_2d(TextureSampleType::Depth) + }, + // All the mip levels follow: + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::ReadWrite), + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly), + sampler(SamplerBindingType::NonFiltering), + ), + ), + ) +} + +bitflags! { + /// Uniquely identifies a configuration of the downsample depth shader. + /// + /// Note that meshlets maintain their downsample depth shaders on their own + /// and don't use this infrastructure; thus there's no flag for meshlets in + /// here, even though the shader has defines for it. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct DownsampleDepthPipelineKey: u8 { + /// True if the depth buffer is multisampled. + const MULTISAMPLE = 1; + /// True if this shader is the second phase of the downsample depth + /// process; false if this shader is the first phase. + const SECOND_PHASE = 2; + } +} + +impl SpecializedComputePipeline for DownsampleDepthPipeline { + type Key = DownsampleDepthPipelineKey; + + fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor { + let mut shader_defs = vec![]; + if key.contains(DownsampleDepthPipelineKey::MULTISAMPLE) { + shader_defs.push("MULTISAMPLE".into()); + } + + let label = format!( + "downsample depth{}{} pipeline", + if key.contains(DownsampleDepthPipelineKey::MULTISAMPLE) { + " multisample" + } else { + "" + }, + if key.contains(DownsampleDepthPipelineKey::SECOND_PHASE) { + " second phase" + } else { + " first phase" + } + ) + .into(); + + ComputePipelineDescriptor { + label: Some(label), + layout: vec![self.bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: self.shader.clone(), + shader_defs, + entry_point: Some(if key.contains(DownsampleDepthPipelineKey::SECOND_PHASE) { + "downsample_depth_second".into() + } else { + "downsample_depth_first".into() + }), + ..default() + } + } +} + +/// Stores a placeholder texture that can be bound to a depth pyramid binding if +/// no depth pyramid is needed. +#[derive(Resource, Deref, DerefMut)] +pub struct DepthPyramidDummyTexture(TextureView); + +pub fn init_depth_pyramid_dummy_texture(mut commands: Commands, render_device: Res) { + commands.insert_resource(DepthPyramidDummyTexture( + create_depth_pyramid_dummy_texture( + &render_device, + "depth pyramid dummy texture", + "depth pyramid dummy texture view", + ), + )); +} + +/// Creates a placeholder texture that can be bound to a depth pyramid binding +/// if no depth pyramid is needed. +pub fn create_depth_pyramid_dummy_texture( + render_device: &RenderDevice, + texture_label: &'static str, + texture_view_label: &'static str, +) -> TextureView { + render_device + .create_texture(&TextureDescriptor { + label: Some(texture_label), + size: Extent3d::default(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R32Float, + usage: TextureUsages::STORAGE_BINDING, + view_formats: &[], + }) + .create_view(&TextureViewDescriptor { + label: Some(texture_view_label), + format: Some(TextureFormat::R32Float), + dimension: Some(TextureViewDimension::D2), + usage: None, + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: Some(1), + base_array_layer: 0, + array_layer_count: Some(1), + }) +} + +/// Stores a hierarchical Z-buffer for a view, which is a series of mipmaps +/// useful for efficient occlusion culling. +/// +/// This will only be present on a view when occlusion culling is enabled. +#[derive(Component)] +pub struct ViewDepthPyramid { + /// A texture view containing the entire depth texture. + pub all_mips: TextureView, + /// A series of texture views containing one mip level each. + pub mips: [TextureView; DEPTH_PYRAMID_MIP_COUNT], + /// The total number of mipmap levels. + /// + /// This is the base-2 logarithm of the greatest dimension of the depth + /// buffer, rounded up. + pub mip_count: u32, +} + +impl ViewDepthPyramid { + /// Allocates a new depth pyramid for a depth buffer with the given size. + pub fn new( + render_device: &RenderDevice, + texture_cache: &mut TextureCache, + depth_pyramid_dummy_texture: &TextureView, + size: UVec2, + texture_label: &'static str, + texture_view_label: &'static str, + ) -> ViewDepthPyramid { + // Calculate the size of the depth pyramid. + let depth_pyramid_size = Extent3d { + width: size.x.div_ceil(2), + height: size.y.div_ceil(2), + depth_or_array_layers: 1, + }; + + // Calculate the number of mip levels we need. + let depth_pyramid_mip_count = depth_pyramid_size.max_mips(TextureDimension::D2); + + // Create the depth pyramid. + let depth_pyramid = texture_cache.get( + render_device, + TextureDescriptor { + label: Some(texture_label), + size: depth_pyramid_size, + mip_level_count: depth_pyramid_mip_count, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R32Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + // Create individual views for each level of the depth pyramid. + let depth_pyramid_mips = array::from_fn(|i| { + if (i as u32) < depth_pyramid_mip_count { + depth_pyramid.texture.create_view(&TextureViewDescriptor { + label: Some(texture_view_label), + format: Some(TextureFormat::R32Float), + dimension: Some(TextureViewDimension::D2), + usage: None, + aspect: TextureAspect::All, + base_mip_level: i as u32, + mip_level_count: Some(1), + base_array_layer: 0, + array_layer_count: Some(1), + }) + } else { + (*depth_pyramid_dummy_texture).clone() + } + }); + + // Create the view for the depth pyramid as a whole. + let depth_pyramid_all_mips = depth_pyramid.default_view.clone(); + + Self { + all_mips: depth_pyramid_all_mips, + mips: depth_pyramid_mips, + mip_count: depth_pyramid_mip_count, + } + } + + /// Creates a bind group that allows the depth buffer to be attached to the + /// `downsample_depth.wgsl` shader. + pub fn create_bind_group<'a, R>( + &'a self, + render_device: &RenderDevice, + label: &'static str, + bind_group_layout: &BindGroupLayout, + source_image: R, + sampler: &'a Sampler, + ) -> BindGroup + where + R: IntoBinding<'a>, + { + render_device.create_bind_group( + label, + bind_group_layout, + &BindGroupEntries::sequential(( + source_image, + &self.mips[0], + &self.mips[1], + &self.mips[2], + &self.mips[3], + &self.mips[4], + &self.mips[5], + &self.mips[6], + &self.mips[7], + &self.mips[8], + &self.mips[9], + &self.mips[10], + &self.mips[11], + sampler, + )), + ) + } + + /// Invokes the shaders to generate the hierarchical Z-buffer. + /// + /// This is intended to be invoked as part of a render node. + pub fn downsample_depth( + &self, + label: &str, + render_context: &mut RenderContext, + view_size: UVec2, + downsample_depth_bind_group: &BindGroup, + downsample_depth_first_pipeline: &ComputePipeline, + downsample_depth_second_pipeline: &ComputePipeline, + ) { + let command_encoder = render_context.command_encoder(); + let mut downsample_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some(label), + timestamp_writes: None, + }); + downsample_pass.set_pipeline(downsample_depth_first_pipeline); + // Pass the mip count as a push constant, for simplicity. + downsample_pass.set_push_constants(0, &self.mip_count.to_le_bytes()); + downsample_pass.set_bind_group(0, downsample_depth_bind_group, &[]); + downsample_pass.dispatch_workgroups(view_size.x.div_ceil(64), view_size.y.div_ceil(64), 1); + + if self.mip_count >= 7 { + downsample_pass.set_pipeline(downsample_depth_second_pipeline); + downsample_pass.dispatch_workgroups(1, 1, 1); + } + } +} + +/// Creates depth pyramids for views that have occlusion culling enabled. +pub fn prepare_view_depth_pyramids( + mut commands: Commands, + render_device: Res, + mut texture_cache: ResMut, + depth_pyramid_dummy_texture: Res, + views: Query<(Entity, &ExtractedView), (With, Without)>, +) { + for (view_entity, view) in &views { + commands.entity(view_entity).insert(ViewDepthPyramid::new( + &render_device, + &mut texture_cache, + &depth_pyramid_dummy_texture, + view.viewport.zw(), + "view depth pyramid texture", + "view depth pyramid texture view", + )); + } +} + +/// The bind group that we use to attach the depth buffer and depth pyramid for +/// a view to the `downsample_depth.wgsl` shader. +/// +/// This will only be present for a view if occlusion culling is enabled. +#[derive(Component, Deref, DerefMut)] +pub struct ViewDownsampleDepthBindGroup(BindGroup); + +/// Creates the [`ViewDownsampleDepthBindGroup`]s for all views with occlusion +/// culling enabled. +fn prepare_downsample_depth_view_bind_groups( + mut commands: Commands, + render_device: Res, + downsample_depth_pipelines: Res, + view_depth_textures: Query< + ( + Entity, + &ViewDepthPyramid, + Option<&ViewDepthTexture>, + Option<&OcclusionCullingSubview>, + ), + Or<(With, With)>, + >, +) { + for (view_entity, view_depth_pyramid, view_depth_texture, shadow_occlusion_culling) in + &view_depth_textures + { + let is_multisampled = view_depth_texture + .is_some_and(|view_depth_texture| view_depth_texture.texture.sample_count() > 1); + commands + .entity(view_entity) + .insert(ViewDownsampleDepthBindGroup( + view_depth_pyramid.create_bind_group( + &render_device, + if is_multisampled { + "downsample multisample depth bind group" + } else { + "downsample depth bind group" + }, + if is_multisampled { + &downsample_depth_pipelines + .first_multisample + .bind_group_layout + } else { + &downsample_depth_pipelines.first.bind_group_layout + }, + match (view_depth_texture, shadow_occlusion_culling) { + (Some(view_depth_texture), _) => view_depth_texture.view(), + (None, Some(shadow_occlusion_culling)) => { + &shadow_occlusion_culling.depth_texture_view + } + (None, None) => panic!("Should never happen"), + }, + &downsample_depth_pipelines.sampler, + ), + )); + } +} diff --git a/crates/libmarathon/src/render/experimental/mod.rs b/crates/libmarathon/src/render/experimental/mod.rs new file mode 100644 index 0000000..47c42bd --- /dev/null +++ b/crates/libmarathon/src/render/experimental/mod.rs @@ -0,0 +1,8 @@ +//! Experimental rendering features. +//! +//! Experimental features are features with known problems, missing features, +//! compatibility issues, low performance, and/or future breaking changes, but +//! are included nonetheless for testing purposes. + +pub mod mip_generation; +pub mod occlusion_culling; diff --git a/crates/libmarathon/src/render/experimental/occlusion_culling/mesh_preprocess_types.wgsl b/crates/libmarathon/src/render/experimental/occlusion_culling/mesh_preprocess_types.wgsl new file mode 100644 index 0000000..a597fb0 --- /dev/null +++ b/crates/libmarathon/src/render/experimental/occlusion_culling/mesh_preprocess_types.wgsl @@ -0,0 +1,69 @@ +// Types needed for GPU mesh uniform building. + +#define_import_path bevy_pbr::mesh_preprocess_types + +// Per-frame data that the CPU supplies to the GPU. +struct MeshInput { + // The model transform. + world_from_local: mat3x4, + // The lightmap UV rect, packed into 64 bits. + lightmap_uv_rect: vec2, + // Various flags. + flags: u32, + previous_input_index: u32, + first_vertex_index: u32, + first_index_index: u32, + index_count: u32, + current_skin_index: u32, + // Low 16 bits: index of the material inside the bind group data. + // High 16 bits: index of the lightmap in the binding array. + material_and_lightmap_bind_group_slot: u32, + timestamp: u32, + // User supplied index to identify the mesh instance + tag: u32, + pad: u32, +} + +// The `wgpu` indirect parameters structure. This is a union of two structures. +// For more information, see the corresponding comment in +// `gpu_preprocessing.rs`. +struct IndirectParametersIndexed { + // `vertex_count` or `index_count`. + index_count: u32, + // `instance_count` in both structures. + instance_count: u32, + // `first_vertex` or `first_index`. + first_index: u32, + // `base_vertex` or `first_instance`. + base_vertex: u32, + // A read-only copy of `instance_index`. + first_instance: u32, +} + +struct IndirectParametersNonIndexed { + vertex_count: u32, + instance_count: u32, + base_vertex: u32, + first_instance: u32, +} + +struct IndirectParametersCpuMetadata { + base_output_index: u32, + batch_set_index: u32, +} + +struct IndirectParametersGpuMetadata { + mesh_index: u32, +#ifdef WRITE_INDIRECT_PARAMETERS_METADATA + early_instance_count: atomic, + late_instance_count: atomic, +#else // WRITE_INDIRECT_PARAMETERS_METADATA + early_instance_count: u32, + late_instance_count: u32, +#endif // WRITE_INDIRECT_PARAMETERS_METADATA +} + +struct IndirectBatchSet { + indirect_parameters_count: atomic, + indirect_parameters_base: u32, +} diff --git a/crates/libmarathon/src/render/experimental/occlusion_culling/mod.rs b/crates/libmarathon/src/render/experimental/occlusion_culling/mod.rs new file mode 100644 index 0000000..0b280a9 --- /dev/null +++ b/crates/libmarathon/src/render/experimental/occlusion_culling/mod.rs @@ -0,0 +1,104 @@ +//! GPU occlusion culling. +//! +//! See [`OcclusionCulling`] for a detailed description of occlusion culling in +//! Bevy. + +use bevy_app::{App, Plugin}; +use bevy_ecs::{component::Component, entity::Entity, prelude::ReflectComponent}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_shader::load_shader_library; + +use crate::render::{extract_component::ExtractComponent, render_resource::TextureView}; + +/// Enables GPU occlusion culling. +/// +/// See [`OcclusionCulling`] for a detailed description of occlusion culling in +/// Bevy. +pub struct OcclusionCullingPlugin; + +impl Plugin for OcclusionCullingPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "mesh_preprocess_types.wgsl"); + } +} + +/// Add this component to a view in order to enable experimental GPU occlusion +/// culling. +/// +/// *Bevy's occlusion culling is currently marked as experimental.* There are +/// known issues whereby, in rare circumstances, occlusion culling can result in +/// meshes being culled that shouldn't be (i.e. meshes that turn invisible). +/// Please try it out and report issues. +/// +/// *Occlusion culling* allows Bevy to avoid rendering objects that are fully +/// behind other opaque or alpha tested objects. This is different from, and +/// complements, depth fragment rejection as the `DepthPrepass` enables. While +/// depth rejection allows Bevy to avoid rendering *pixels* that are behind +/// other objects, the GPU still has to examine those pixels to reject them, +/// which requires transforming the vertices of the objects and performing +/// skinning if the objects were skinned. Occlusion culling allows the GPU to go +/// a step further, avoiding even transforming the vertices of objects that it +/// can quickly prove to be behind other objects. +/// +/// Occlusion culling inherently has some overhead, because Bevy must examine +/// the objects' bounding boxes, and create an acceleration structure +/// (hierarchical Z-buffer) to perform the occlusion tests. Therefore, occlusion +/// culling is disabled by default. Only enable it if you measure it to be a +/// speedup on your scene. Note that, because Bevy's occlusion culling runs on +/// the GPU and is quite efficient, it's rare for occlusion culling to result in +/// a significant slowdown. +/// +/// Occlusion culling currently requires a `DepthPrepass`. If no depth prepass +/// is present on the view, the [`OcclusionCulling`] component will be ignored. +/// Additionally, occlusion culling is currently incompatible with deferred +/// shading; including both `DeferredPrepass` and [`OcclusionCulling`] results +/// in unspecified behavior. +/// +/// The algorithm that Bevy uses is known as [*two-phase occlusion culling*]. +/// When you enable occlusion culling, Bevy splits the depth prepass into two: +/// an *early* depth prepass and a *late* depth prepass. The early depth prepass +/// renders all the meshes that were visible last frame to produce a +/// conservative approximation of the depth buffer. Then, after producing an +/// acceleration structure known as a hierarchical Z-buffer or depth pyramid, +/// Bevy tests the bounding boxes of all meshes against that depth buffer. Those +/// that can be quickly proven to be behind the geometry rendered during the +/// early depth prepass are skipped entirely. The other potentially-visible +/// meshes are rendered during the late prepass, and finally all the visible +/// meshes are rendered as usual during the opaque, transparent, etc. passes. +/// +/// Unlike other occlusion culling systems you may be familiar with, Bevy's +/// occlusion culling is fully dynamic and requires no baking step. The CPU +/// overhead is minimal. Large skinned meshes and other dynamic objects can +/// occlude other objects. +/// +/// [*two-phase occlusion culling*]: +/// https://medium.com/@mil_kru/two-pass-occlusion-culling-4100edcad501 +#[derive(Component, ExtractComponent, Clone, Copy, Default, Reflect)] +#[reflect(Component, Default, Clone)] +pub struct OcclusionCulling; + +/// A render-world component that contains resources necessary to perform +/// occlusion culling on any view other than a camera. +/// +/// Bevy automatically places this component on views created for shadow +/// mapping. You don't ordinarily need to add this component yourself. +#[derive(Clone, Component)] +pub struct OcclusionCullingSubview { + /// A texture view of the Z-buffer. + pub depth_texture_view: TextureView, + /// The size of the texture along both dimensions. + /// + /// Because [`OcclusionCullingSubview`] is only currently used for shadow + /// maps, they're guaranteed to have sizes equal to a power of two, so we + /// don't have to store the two dimensions individually here. + pub depth_texture_size: u32, +} + +/// A render-world component placed on each camera that stores references to all +/// entities other than cameras that need occlusion culling. +/// +/// Bevy automatically places this component on cameras that are drawing +/// shadows, when those shadows come from lights with occlusion culling enabled. +/// You don't ordinarily need to add this component yourself. +#[derive(Clone, Component)] +pub struct OcclusionCullingSubviewEntities(pub Vec); diff --git a/crates/libmarathon/src/render/extract_component.rs b/crates/libmarathon/src/render/extract_component.rs new file mode 100644 index 0000000..47f4bb1 --- /dev/null +++ b/crates/libmarathon/src/render/extract_component.rs @@ -0,0 +1,236 @@ +use crate::render::{ + render_resource::{encase::internal::WriteInto, DynamicUniformBuffer, ShaderType}, + renderer::{RenderDevice, RenderQueue}, + sync_component::SyncComponentPlugin, + sync_world::RenderEntity, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use bevy_app::{App, Plugin}; +use bevy_camera::visibility::ViewVisibility; +use bevy_ecs::{ + bundle::NoBundleEffect, + component::Component, + prelude::*, + query::{QueryFilter, QueryItem, ReadOnlyQueryData}, +}; +use core::{marker::PhantomData, ops::Deref}; + +pub use macros::ExtractComponent; + +/// Stores the index of a uniform inside of [`ComponentUniforms`]. +#[derive(Component)] +pub struct DynamicUniformIndex { + index: u32, + marker: PhantomData, +} + +impl DynamicUniformIndex { + #[inline] + pub fn index(&self) -> u32 { + self.index + } +} + +/// Describes how a component gets extracted for rendering. +/// +/// Therefore the component is transferred from the "app world" into the "render world" +/// in the [`ExtractSchedule`] step. +pub trait ExtractComponent: Component { + /// ECS [`ReadOnlyQueryData`] to fetch the components to extract. + type QueryData: ReadOnlyQueryData; + /// Filters the entities with additional constraints. + type QueryFilter: QueryFilter; + + /// The output from extraction. + /// + /// Returning `None` based on the queried item will remove the component from the entity in + /// the render world. This can be used, for example, to conditionally extract camera settings + /// in order to disable a rendering feature on the basis of those settings, without removing + /// the component from the entity in the main world. + /// + /// The output may be different from the queried component. + /// This can be useful for example if only a subset of the fields are useful + /// in the render world. + /// + /// `Out` has a [`Bundle`] trait bound instead of a [`Component`] trait bound in order to allow use cases + /// such as tuples of components as output. + type Out: Bundle; + + // TODO: https://github.com/rust-lang/rust/issues/29661 + // type Out: Component = Self; + + /// Defines how the component is transferred into the "render world". + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option; +} + +/// This plugin prepares the components of the corresponding type for the GPU +/// by transforming them into uniforms. +/// +/// They can then be accessed from the [`ComponentUniforms`] resource. +/// For referencing the newly created uniforms a [`DynamicUniformIndex`] is inserted +/// for every processed entity. +/// +/// Therefore it sets up the [`RenderSystems::Prepare`] step +/// for the specified [`ExtractComponent`]. +pub struct UniformComponentPlugin(PhantomData C>); + +impl Default for UniformComponentPlugin { + fn default() -> Self { + Self(PhantomData) + } +} + +impl Plugin for UniformComponentPlugin { + fn build(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .insert_resource(ComponentUniforms::::default()) + .add_systems( + Render, + prepare_uniform_components::.in_set(RenderSystems::PrepareResources), + ); + } + } +} + +/// Stores all uniforms of the component type. +#[derive(Resource)] +pub struct ComponentUniforms { + uniforms: DynamicUniformBuffer, +} + +impl Deref for ComponentUniforms { + type Target = DynamicUniformBuffer; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.uniforms + } +} + +impl ComponentUniforms { + #[inline] + pub fn uniforms(&self) -> &DynamicUniformBuffer { + &self.uniforms + } +} + +impl Default for ComponentUniforms { + fn default() -> Self { + Self { + uniforms: Default::default(), + } + } +} + +/// This system prepares all components of the corresponding component type. +/// They are transformed into uniforms and stored in the [`ComponentUniforms`] resource. +fn prepare_uniform_components( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut component_uniforms: ResMut>, + components: Query<(Entity, &C)>, +) where + C: Component + ShaderType + WriteInto + Clone, +{ + let components_iter = components.iter(); + let count = components_iter.len(); + let Some(mut writer) = + component_uniforms + .uniforms + .get_writer(count, &render_device, &render_queue) + else { + return; + }; + let entities = components_iter + .map(|(entity, component)| { + ( + entity, + DynamicUniformIndex:: { + index: writer.write(component), + marker: PhantomData, + }, + ) + }) + .collect::>(); + commands.try_insert_batch(entities); +} + +/// This plugin extracts the components into the render world for synced entities. +/// +/// To do so, it sets up the [`ExtractSchedule`] step for the specified [`ExtractComponent`]. +pub struct ExtractComponentPlugin { + only_extract_visible: bool, + marker: PhantomData (C, F)>, +} + +impl Default for ExtractComponentPlugin { + fn default() -> Self { + Self { + only_extract_visible: false, + marker: PhantomData, + } + } +} + +impl ExtractComponentPlugin { + pub fn extract_visible() -> Self { + Self { + only_extract_visible: true, + marker: PhantomData, + } + } +} + +impl Plugin for ExtractComponentPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(SyncComponentPlugin::::default()); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + if self.only_extract_visible { + render_app.add_systems(ExtractSchedule, extract_visible_components::); + } else { + render_app.add_systems(ExtractSchedule, extract_components::); + } + } + } +} + +/// This system extracts all components of the corresponding [`ExtractComponent`], for entities that are synced via [`crate::sync_world::SyncToRenderWorld`]. +fn extract_components( + mut commands: Commands, + mut previous_len: Local, + query: Extract>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, query_item) in &query { + if let Some(component) = C::extract_component(query_item) { + values.push((entity, component)); + } else { + commands.entity(entity).remove::(); + } + } + *previous_len = values.len(); + commands.try_insert_batch(values); +} + +/// This system extracts all components of the corresponding [`ExtractComponent`], for entities that are visible and synced via [`crate::sync_world::SyncToRenderWorld`]. +fn extract_visible_components( + mut commands: Commands, + mut previous_len: Local, + query: Extract>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, view_visibility, query_item) in &query { + if view_visibility.get() { + if let Some(component) = C::extract_component(query_item) { + values.push((entity, component)); + } else { + commands.entity(entity).remove::(); + } + } + } + *previous_len = values.len(); + commands.try_insert_batch(values); +} diff --git a/crates/libmarathon/src/render/extract_instances.rs b/crates/libmarathon/src/render/extract_instances.rs new file mode 100644 index 0000000..d3c0b2c --- /dev/null +++ b/crates/libmarathon/src/render/extract_instances.rs @@ -0,0 +1,137 @@ +//! Convenience logic for turning components from the main world into extracted +//! instances in the render world. +//! +//! This is essentially the same as the `extract_component` module, but +//! higher-performance because it avoids the ECS overhead. + +use core::marker::PhantomData; + +use bevy_app::{App, Plugin}; +use bevy_camera::visibility::ViewVisibility; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + prelude::Entity, + query::{QueryFilter, QueryItem, ReadOnlyQueryData}, + resource::Resource, + system::{Query, ResMut}, +}; + +use crate::render::sync_world::MainEntityHashMap; +use crate::render::{Extract, ExtractSchedule, RenderApp}; + +/// Describes how to extract data needed for rendering from a component or +/// components. +/// +/// Before rendering, any applicable components will be transferred from the +/// main world to the render world in the [`ExtractSchedule`] step. +/// +/// This is essentially the same as +/// [`ExtractComponent`](crate::extract_component::ExtractComponent), but +/// higher-performance because it avoids the ECS overhead. +pub trait ExtractInstance: Send + Sync + Sized + 'static { + /// ECS [`ReadOnlyQueryData`] to fetch the components to extract. + type QueryData: ReadOnlyQueryData; + /// Filters the entities with additional constraints. + type QueryFilter: QueryFilter; + + /// Defines how the component is transferred into the "render world". + fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option; +} + +/// This plugin extracts one or more components into the "render world" as +/// extracted instances. +/// +/// Therefore it sets up the [`ExtractSchedule`] step for the specified +/// [`ExtractedInstances`]. +#[derive(Default)] +pub struct ExtractInstancesPlugin +where + EI: ExtractInstance, +{ + only_extract_visible: bool, + marker: PhantomData EI>, +} + +/// Stores all extract instances of a type in the render world. +#[derive(Resource, Deref, DerefMut)] +pub struct ExtractedInstances(MainEntityHashMap) +where + EI: ExtractInstance; + +impl Default for ExtractedInstances +where + EI: ExtractInstance, +{ + fn default() -> Self { + Self(Default::default()) + } +} + +impl ExtractInstancesPlugin +where + EI: ExtractInstance, +{ + /// Creates a new [`ExtractInstancesPlugin`] that unconditionally extracts to + /// the render world, whether the entity is visible or not. + pub fn new() -> Self { + Self { + only_extract_visible: false, + marker: PhantomData, + } + } + + /// Creates a new [`ExtractInstancesPlugin`] that extracts to the render world + /// if and only if the entity it's attached to is visible. + pub fn extract_visible() -> Self { + Self { + only_extract_visible: true, + marker: PhantomData, + } + } +} + +impl Plugin for ExtractInstancesPlugin +where + EI: ExtractInstance, +{ + fn build(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.init_resource::>(); + if self.only_extract_visible { + render_app.add_systems(ExtractSchedule, extract_visible::); + } else { + render_app.add_systems(ExtractSchedule, extract_all::); + } + } + } +} + +fn extract_all( + mut extracted_instances: ResMut>, + query: Extract>, +) where + EI: ExtractInstance, +{ + extracted_instances.clear(); + for (entity, other) in &query { + if let Some(extract_instance) = EI::extract(other) { + extracted_instances.insert(entity.into(), extract_instance); + } + } +} + +fn extract_visible( + mut extracted_instances: ResMut>, + query: Extract>, +) where + EI: ExtractInstance, +{ + extracted_instances.clear(); + for (entity, view_visibility, other) in &query { + if view_visibility.get() + && let Some(extract_instance) = EI::extract(other) + { + extracted_instances.insert(entity.into(), extract_instance); + } + } +} diff --git a/crates/libmarathon/src/render/extract_param.rs b/crates/libmarathon/src/render/extract_param.rs new file mode 100644 index 0000000..c578406 --- /dev/null +++ b/crates/libmarathon/src/render/extract_param.rs @@ -0,0 +1,177 @@ +use crate::render::MainWorld; +use bevy_ecs::{ + component::Tick, + prelude::*, + query::FilteredAccessSet, + system::{ + ReadOnlySystemParam, SystemMeta, SystemParam, SystemParamItem, SystemParamValidationError, + SystemState, + }, + world::unsafe_world_cell::UnsafeWorldCell, +}; +use core::ops::{Deref, DerefMut}; + +/// A helper for accessing [`MainWorld`] content using a system parameter. +/// +/// A [`SystemParam`] adapter which applies the contained `SystemParam` to the [`World`] +/// contained in [`MainWorld`]. This parameter only works for systems run +/// during the [`ExtractSchedule`](crate::ExtractSchedule). +/// +/// This requires that the contained [`SystemParam`] does not mutate the world, as it +/// uses a read-only reference to [`MainWorld`] internally. +/// +/// ## Context +/// +/// [`ExtractSchedule`] is used to extract (move) data from the simulation world ([`MainWorld`]) to the +/// render world. The render world drives rendering each frame (generally to a `Window`). +/// This design is used to allow performing calculations related to rendering a prior frame at the same +/// time as the next frame is simulated, which increases throughput (FPS). +/// +/// [`Extract`] is used to get data from the main world during [`ExtractSchedule`]. +/// +/// ## Examples +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// use crate::render::Extract; +/// use crate::render::sync_world::RenderEntity; +/// # #[derive(Component)] +/// // Do make sure to sync the cloud entities before extracting them. +/// # struct Cloud; +/// fn extract_clouds(mut commands: Commands, clouds: Extract>>) { +/// for cloud in &clouds { +/// commands.entity(cloud).insert(Cloud); +/// } +/// } +/// ``` +/// +/// [`ExtractSchedule`]: crate::ExtractSchedule +/// [Window]: bevy_window::Window +pub struct Extract<'w, 's, P> +where + P: ReadOnlySystemParam + 'static, +{ + item: SystemParamItem<'w, 's, P>, +} + +#[doc(hidden)] +pub struct ExtractState { + state: SystemState

, + main_world_state: as SystemParam>::State, +} + +// SAFETY: The only `World` access (`Res`) is read-only. +unsafe impl

ReadOnlySystemParam for Extract<'_, '_, P> where P: ReadOnlySystemParam {} + +// SAFETY: The only `World` access is properly registered by `Res::init_state`. +// This call will also ensure that there are no conflicts with prior params. +unsafe impl

SystemParam for Extract<'_, '_, P> +where + P: ReadOnlySystemParam, +{ + type State = ExtractState

; + type Item<'w, 's> = Extract<'w, 's, P>; + + fn init_state(world: &mut World) -> Self::State { + let mut main_world = world.resource_mut::(); + ExtractState { + state: SystemState::new(&mut main_world), + main_world_state: Res::::init_state(world), + } + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + Res::::init_access( + &state.main_world_state, + system_meta, + component_access_set, + world, + ); + } + + #[inline] + unsafe fn validate_param( + state: &mut Self::State, + _system_meta: &SystemMeta, + world: UnsafeWorldCell, + ) -> Result<(), SystemParamValidationError> { + // SAFETY: Read-only access to world data registered in `init_state`. + let result = unsafe { world.get_resource_by_id(state.main_world_state) }; + let Some(main_world) = result else { + return Err(SystemParamValidationError::invalid::( + "`MainWorld` resource does not exist", + )); + }; + // SAFETY: Type is guaranteed by `SystemState`. + let main_world: &World = unsafe { main_world.deref() }; + // SAFETY: We provide the main world on which this system state was initialized on. + unsafe { + SystemState::

::validate_param( + &mut state.state, + main_world.as_unsafe_world_cell_readonly(), + ) + } + } + + #[inline] + unsafe fn get_param<'w, 's>( + state: &'s mut Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell<'w>, + change_tick: Tick, + ) -> Self::Item<'w, 's> { + // SAFETY: + // - The caller ensures that `world` is the same one that `init_state` was called with. + // - The caller ensures that no other `SystemParam`s will conflict with the accesses we have registered. + let main_world = unsafe { + Res::::get_param( + &mut state.main_world_state, + system_meta, + world, + change_tick, + ) + }; + let item = state.state.get(main_world.into_inner()); + Extract { item } + } +} + +impl<'w, 's, P> Deref for Extract<'w, 's, P> +where + P: ReadOnlySystemParam, +{ + type Target = SystemParamItem<'w, 's, P>; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.item + } +} + +impl<'w, 's, P> DerefMut for Extract<'w, 's, P> +where + P: ReadOnlySystemParam, +{ + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.item + } +} + +impl<'a, 'w, 's, P> IntoIterator for &'a Extract<'w, 's, P> +where + P: ReadOnlySystemParam, + &'a SystemParamItem<'w, 's, P>: IntoIterator, +{ + type Item = <&'a SystemParamItem<'w, 's, P> as IntoIterator>::Item; + type IntoIter = <&'a SystemParamItem<'w, 's, P> as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + (&self.item).into_iter() + } +} diff --git a/crates/libmarathon/src/render/extract_resource.rs b/crates/libmarathon/src/render/extract_resource.rs new file mode 100644 index 0000000..d1be4a9 --- /dev/null +++ b/crates/libmarathon/src/render/extract_resource.rs @@ -0,0 +1,70 @@ +use core::marker::PhantomData; + +use bevy_app::{App, Plugin}; +use bevy_ecs::prelude::*; +pub use macros::ExtractResource; +use bevy_utils::once; + +use crate::render::{Extract, ExtractSchedule, RenderApp}; + +/// Describes how a resource gets extracted for rendering. +/// +/// Therefore the resource is transferred from the "main world" into the "render world" +/// in the [`ExtractSchedule`] step. +pub trait ExtractResource: Resource { + type Source: Resource; + + /// Defines how the resource is transferred into the "render world". + fn extract_resource(source: &Self::Source) -> Self; +} + +/// This plugin extracts the resources into the "render world". +/// +/// Therefore it sets up the[`ExtractSchedule`] step +/// for the specified [`Resource`]. +pub struct ExtractResourcePlugin(PhantomData); + +impl Default for ExtractResourcePlugin { + fn default() -> Self { + Self(PhantomData) + } +} + +impl Plugin for ExtractResourcePlugin { + fn build(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.add_systems(ExtractSchedule, extract_resource::); + } else { + once!(tracing::error!( + "Render app did not exist when trying to add `extract_resource` for <{}>.", + core::any::type_name::() + )); + } + } +} + +/// This system extracts the resource of the corresponding [`Resource`] type +pub fn extract_resource( + mut commands: Commands, + main_resource: Extract>>, + target_resource: Option>, +) { + if let Some(main_resource) = main_resource.as_ref() { + if let Some(mut target_resource) = target_resource { + if main_resource.is_changed() { + *target_resource = R::extract_resource(main_resource); + } + } else { + #[cfg(debug_assertions)] + if !main_resource.is_added() { + once!(tracing::warn!( + "Removing resource {} from render world not expected, adding using `Commands`. + This may decrease performance", + core::any::type_name::() + )); + } + + commands.insert_resource(R::extract_resource(main_resource)); + } + } +} diff --git a/crates/libmarathon/src/render/fullscreen_vertex_shader/fullscreen.wgsl b/crates/libmarathon/src/render/fullscreen_vertex_shader/fullscreen.wgsl new file mode 100644 index 0000000..04c3c49 --- /dev/null +++ b/crates/libmarathon/src/render/fullscreen_vertex_shader/fullscreen.wgsl @@ -0,0 +1,34 @@ +#define_import_path bevy_core_pipeline::fullscreen_vertex_shader + +struct FullscreenVertexOutput { + @builtin(position) + position: vec4, + @location(0) + uv: vec2, +}; + +// This vertex shader produces the following, when drawn using indices 0..3: +// +// 1 | 0-----x.....2 +// 0 | | s | . ´ +// -1 | x_____x´ +// -2 | : .´ +// -3 | 1´ +// +--------------- +// -1 0 1 2 3 +// +// The axes are clip-space x and y. The region marked s is the visible region. +// The digits in the corners of the right-angled triangle are the vertex +// indices. +// +// The top-left has UV 0,0, the bottom-left has 0,2, and the top-right has 2,0. +// This means that the UV gets interpolated to 1,1 at the bottom-right corner +// of the clip-space rectangle that is at 1,-1 in clip space. +@vertex +fn fullscreen_vertex_shader(@builtin(vertex_index) vertex_index: u32) -> FullscreenVertexOutput { + // See the explanation above for how this works + let uv = vec2(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0; + let clip_position = vec4(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0); + + return FullscreenVertexOutput(clip_position, uv); +} diff --git a/crates/libmarathon/src/render/fullscreen_vertex_shader/mod.rs b/crates/libmarathon/src/render/fullscreen_vertex_shader/mod.rs new file mode 100644 index 0000000..d3f8435 --- /dev/null +++ b/crates/libmarathon/src/render/fullscreen_vertex_shader/mod.rs @@ -0,0 +1,41 @@ +use bevy_asset::{load_embedded_asset, Handle}; +use bevy_ecs::{resource::Resource, world::FromWorld}; +use crate::render::render_resource::VertexState; +use bevy_shader::Shader; + +/// A shader that renders to the whole screen. Useful for post-processing. +#[derive(Resource, Clone)] +pub struct FullscreenShader(Handle); + +impl FromWorld for FullscreenShader { + fn from_world(world: &mut bevy_ecs::world::World) -> Self { + Self(load_embedded_asset!(world, "fullscreen.wgsl")) + } +} + +impl FullscreenShader { + /// Gets the raw shader handle. + pub fn shader(&self) -> Handle { + self.0.clone() + } + + /// Creates a [`VertexState`] that uses the [`FullscreenShader`] to output a + /// ```wgsl + /// struct FullscreenVertexOutput { + /// @builtin(position) + /// position: vec4; + /// @location(0) + /// uv: vec2; + /// }; + /// ``` + /// from the vertex shader. + /// The draw call should render one triangle: `render_pass.draw(0..3, 0..1);` + pub fn to_vertex_state(&self) -> VertexState { + VertexState { + shader: self.0.clone(), + shader_defs: Vec::new(), + entry_point: Some("fullscreen_vertex_shader".into()), + buffers: Vec::new(), + } + } +} diff --git a/crates/libmarathon/src/render/globals.rs b/crates/libmarathon/src/render/globals.rs new file mode 100644 index 0000000..1489f6f --- /dev/null +++ b/crates/libmarathon/src/render/globals.rs @@ -0,0 +1,79 @@ +use crate::render::{ + extract_resource::ExtractResource, + render_resource::{ShaderType, UniformBuffer}, + renderer::{RenderDevice, RenderQueue}, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use bevy_app::{App, Plugin}; +use bevy_diagnostic::FrameCount; +use bevy_ecs::prelude::*; +use bevy_reflect::prelude::*; +use bevy_shader::load_shader_library; +use bevy_time::Time; + +pub struct GlobalsPlugin; + +impl Plugin for GlobalsPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "globals.wgsl"); + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::

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 SE~ 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

w}~qf%R`!k`Na>wD2=CT{*liN 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

1f%{8;uc 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�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

$p`#|8I3k0(Y{vhQ#x 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%0DS15{kIAyk=(c{Z~EQ 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
  • GuoLTz^fB|mUb;w}iqwhZwN+V1S_RRk5K~yJ;C-0V{rJZpbyJU;s1(K41(IaJB4nSRT`wEjhUj?|Wayg%1wy&RzJAy@L}emQYgNcj?>H0{i%z_y zxdjk47oniOuFOUOpsP$I%+1Pzk$xXS|o6GfM zJm$*jDJbP{Z7$T{s|h+IHPYI8(}mm(L&9c#tHvFaP}uP!YZP2AvNNGW?)zCyFqOG$ z=@rfUR;z@~wO5)^UOYnA%*|YadS^Prdxu+^o<2Lqpo(vHfT1kH1b11+r3@jo$hB;J z@tpY)*e882i|D;WENNqG6q;jX9=|vEqWWZX2^F{@{??#41)sn>OBmk#DO(n%7^YCC zx~yss>M(aKe6r>9nKT4qO0sqE+z=r^=u^u5)#pHB4U>|M>dDm=8_7$$5H?Tq=mot8 z@3+Nu@k#<0@1Cp*Z9Z%b1xKo4|JEyo4jKXCoRF0ss*w85T4w|F5Uhu-`v8Pzrq6s( zQicJhKgT<9b-kb*x1HB~{Zu zxq=zB0=YQbvbaUV$HU-nXnv(9ul3i-N8Yt@P44G|XvZL$iK)1(10jYGNFrn>^d!-F zCR?EDDgHG>jYFU>y4LMvLb@#4UY@Vk=8;QwB9bVh)VH=mMG2iZj>;CjGto^a&doMDii0Q01*fZ;E zZU;f6{?>J^U2PX*+1~WOi#HfxRI<9==h_Pr98p`+*GeTDK6-e-f7&%|Lq$`oEny&}Pq z?HAg3GMyD(pza7!uQb%Fwn&}_o+S)t_Fn7g~_v}Ix12;!WrMlHT4FgK1=B$YP z0@Mw})mveQ+zGID_V7?64aS7Z<&*|Nl85Y0(aN+Db+60771W1da?8Y=^-yDG)JsLy-~iyspYuxP zb)i~gnx^ixIZ!PC35=nSv@xH<>I?flmH`vg%tTmWN9au6q`5`|Voe#z+{S;A~#pP z%R8!neGH$Y_>OH<*J^V^RNQ+y4n~liw&THvIvQkQtBPk>>sFQ*LgMYk9)%pg5+5M9 z?dYk_x zZ)hb6e6;iog5d6?!l{iJ^g&HGW3_FP@Dacyr-@Xr0R8+ip)!54yW2( z5$YV$h$o1qlrO~(A1vUT!m2gf7O!%Tv8OoGd-*}w3ave5qRssMU$bY@ z+@lVXPe^WYpJIFYf^02h!O=-w1Wab6H3&L3*@yFcU6gEPC&%_~ zwNbVhcU1rJj&mmH$bD6c{dWa)<<)CRxde8St2Y1rR9#wBmbK~nEZMi&D~^7TK#218 z;!Pv;nu?LolnQ|~x(3yCZo1~tjbkB5E~HlrM!>`jAji0!4gSE(02^zY_5{jN`Y5BD zkGLPtCXpzp?2XD<=7DFX7Jf0&;;+e-=89=m-{rFyQ$mQ0L+*Q|3>IoOfxpmWPnXeK ztR!~{}d`zN9sHGb;x>&FzM*s>@@N;dRM{tHjazA^zgi(4IA z&C8=T-5KsMN1w-NVe(9tT=Ghq zRHMwiyL9Of6{7L}prn0}HY0!49B!lX0k(uN6ij&I&cPx;sJJ9~bhHrNT{&ZmR{fHM zNIO~~y)*Zb)}4D_kcyb({xlD2Mu(9V-u4L;N;c3ci71HUamh!ubm99Ncq<2@B;R(yT;tVCLTt*G0ou4N=)*nU=#+uoSEu{>+3S8KfpMY+_Ki@kii<*rs zxjXG))`YxQkFsJOXMgtYev_}7BwomAy#f|Emhr_|S0X?fZ-{V=9*!p+@R{D!@XShF zdR&u&`HT=RXGyP}yKAI1BR2=E34so>&E0_%XcP1*nY^?4O8XZ5Bc@?YJdu_;4xFQk ztzv8t!7Z~2TDV-b7-=f)78@_V^V?2itk4T6D2QA^+*!eSAj-$`JEVzKeEpq884;d` zUy#e*c3Nc)g2?WIAK|mK1&`#awJ_^}TMiFYuxHVaycO zHpmIg(d8K|kZ)JmJ1TnLuDFJ>5h}7RkV>+=vd*2&i7s- zwCS7`0z3HK0C}S{iuC6RA2&qGF|=v21SOKq4Ut!Q;z*-t`*kAdf$Tr+5YbLEH)fh) zz+k!(IVDAs$KLRE6B`D~T;hzJ#5xHY4QnH;7l8}Mhud!&j|WOJYrP_d0nu&1%g#!{ zM%x?r{HO%M)TK18Q;=q~e;6v{J)&d!awVUO@-Bo&<^3vuc;s4(wfaj^dx-dV%~mk9 zOf4WdX0+*v3k^af!%6kPlPC(j9RBHBHoF%76_A(6?Cg^| z4-lAbvdQOE3)df*uzgw+jBU3TvsAhe^UO)WC#7S}rhGwpp6WeJ5~*BQj=1r`{~>d5 z6>LK5wT(xt|0nd7|D6nzTx+A2?YR|QC{1OGbmDRfyA3yGo>fU9*=hX-k7`anRT5$R zh>Y3u_zm_T(v~z=v-5=aGRJCwV*vTQ?{fWli&J3C=<*o#i$$i=?@%ak?JYYuiN_k~ z3cpd-W7m=ZNI=wbk~ytZY;L~xC5x4_Yi&m|{}9fW&y0VDW%T6@W-ot#NA?>-X7|hlZLgCPb%QxRLgF52pA$EiIM3FF1zSx00n zu>|zj;B&#LdphiFO9eg6-}0h6YmHM{+3clvm3bb=m)KZk?Ah8KWNVO(Rx?g()o`;4 zF_dGJb*`&D1W3NOQCodZw*}{0^6frU)=nm2&M-EdJHYVfTg$%iJ4r{yBN~3E)oU^X zGDnVb+#}h_bt5afI+S&zA*B3(ml{ku&O)wjwx}{H9T1gn?oCV(AoT0rQM+S@41uzd z7whAOJgC3BMq%5@Rx8cKiT`4oK$#`EdT+VjuwZ1xLP81zF~?==HRf9erz5FT;8BX%TOmK}uA!a=G5;8vMf-Ql;B^lPET62u( zWgo8<;e9xAt*(PIYjxaCd3>m7>&acPao9w}c64eMJ01PSsXd=CC@DRlLZT0lX_2Y6 z`$7o*kII1jm`or}?Tu)X%)?cCAl->SGS7<-1QmRIx?G|+cM|YI?nL>tPgD@!QTMy7 zLzB5^P$>)(n#4Eqq{;#TL}Q#Q&{wDc1YPLg_~6_hM+;iZooQak(V+Du(A7o^mv1R1mO(ts`GHWvLg@~zL1ZZE z_sg(NWaVdGcqD~&>Bj*n=ardw==Hk@MSZ+3F!6_zEUSig4?@;)con@6MKuPar>Bb? zCGPFfn!=1CQA1U%nZK@N4pH>TA$_uq&0#@c83u!eW}M5_v>O*z7wJrxug z8a20tmlcA3hGbVv(LV=Q$m7PRUyBPqQy}0b)e;b?P4^wP6`B;6Ib?jvlMrYcMc!JP zZsrgkvex&Kl}*Bm$TrISR}&n3e_(1E#zU8wvc7K<5iHxt_*88YFQPcIt-^_@9Q`Nx zDZ#?Itw*PTzazv zi-+*YUwj`ofu)$)xImiI6g^otlv5XsU|T{BkR z>*zX<09atm3D!-_Yw&!|0iUy49z?~lSjLxAijZAvK zf(iqL=(FHFl3DLDUdq0wPU1+>7So~GGuQna&KK}X+wX#HGqVLoUiOE>bI$#{9(dx0 zrcH1A<1-RF&8zYP+JBH2)!g#NC7)h~7J8c9$ddU81PAxL%Ys7YN>vMf;rI#zhG%gY z%*EvaI#^FC;ym8+*3cMiZ(in8=dZs;950p~-JhV0o{3M`J?S>1Ap%S-qbj)PT;E@>1l_rhK=o`Wt2PU0hHsPTQmLjzus zsfPb(40v(r0~^a;(z=R0YYBy%oTB@PEhAdnp0#sl)D>cw&?Hfj!&wx$D&sIhH&Ahq zidjNqiOb#N9>u^@V`dKz{<2zLf!in z6Tg(ozmZUz-o$}lHQtKCZLchz3c%5^fm6{^Ni)+Ch*eX;!F-}382Uua)q7ePx7n-H zAvxG;Ydi3Lc2HZsYh+LiCyj|Ek5;4kvW7x(DVR|9$N;|*eL@!DO64Z84S_aa;&R6R zI|Cv=D9CG*sPG*Tk=x6)i2WESs88oQe=5cd5+r`1aC9Zk*Oh8quB-ta9547&+r{If zB@AQco$+Msjj4Bhi3XE(>tLZu*<1)&Sm|zgt|d(zd3nXZku&X9G&HQIdBFlC0{7LVvQbIYc?72al2Ec= zuLMjXw4P|4gZ>+#6hJpU#lG|xJ`4?m7ta4gy5xb94RiQ!EXq(H0O z*LL~`5gk$bc3_Y95)ds#H)>gc-6oe+s6!#|p?_nOl<0q$Z<7odpEf3M9O zWebcQa61D2__3xjQ$BYQ#vjW2as;L(kEI* zdhOY;b%)6(OhAnWa~)}L%A$Pj7xo6xfa<0whm-9zSzOqthTucofLT_~scu1B&JNUj zbV?J9J^x;btY7O>HeCCHen*jb!HUG2Gl3!M;ws+>bP9VJlK!!=LdDX=*w~aqcGf9x zBYpBB4uXn0c16|i5*Q&fVQzek#`R(bwDLH+AJR23O8D>=Rv7ztp+vI2h)U@uH3A3D z_+(MD5(h7#{v*4wOW)=VLr-lc&Tt{qY=Ni=jro4D+F zY>GXjE7r}bC7HeDpoTMc8m+~dz|DX=Gny%kGWeBkfO4x^5i0LvvmBr2l_c$ty|^Q{ zw4NL8C|fM@{{M&!St3bZ#iJl|1rKRWmDZ^?i3VSUt&~)=p!)(Bht2yEH<~^&4JG7p zbAx8+^~QYBB$7PeMB_dPnT*7Y@yh(GPW}SWRWc$}lOwkXS7L0XTtWrn@NiCfsWVUu zsVVM{{eP!d6Aq;5s&$+2<9jWvY+qEP$db)KjUF}mpyyp7U_fS!p`|K~rswFos5#nM z+sRH|E$Z5aUd*!}i~*7DW>=e~EPoGhG>_fh0DzLs8tT z*;`1v43Yv$kMzHTV_qKseep#z63XKh5@zpU8W2Y28*zS5_XuGq*9n%?=7IZR1JubDAX~6|R@>`k=5ouq-4??F7tghHC}#WJeUF>7@8g_Zhn`P_~J+ z-2corc^lbRzR}^9BIfXESEoo+njE{kjCK|azXz=O8;c`S9B+j#Ug+YQQYt>dxoU2jlFZbtZ& z{w`c)B6cO^tF>Z?K4;uM@VY|0*f21zHh^a@dDYIVkFA3%KzObd?^gZoaE z2mPEmD38j;LicvWd3&caFeb)Wk|lSSry05Z;lB`duf-dZgU|b-gN9pDa@4;*$DG|z zT8oiYAU@LNR(*$-PaUeH;x%%e!}TOz!_ZR~ey6X7W%{hH1rYSIM-hi`xXGdX`0h38YdZIepb^>)DVm3Oeg6+)L1H!W&>J9m%$x3C{ytd2c^m zBwa18#p;hTX)Hh(!{AqyX+b?3*Dv==q>B$CAq?!yxm+LQ!a_8}wYCa5=A1M|+(?(E zm#%vY7fwgN`HLBj#+4A!KiQ$qpcIzseZ_#s<+UD)Dcc!&CjAFk@ol3KbmhJjMAESm zCTq3bW|&6hK8Sz)IT?0(vr~Pn^?+D>0?ZS*X;oD=`6PR$;aie4qAq=e!rF2H6q~Yw z5`b+I4qWTXtE-qfpAo}Ui5 z$4>Mlk3WTSmdOj@4e{87FDAw=#<~X|7J40OTlBO90>VF^o{CC8b`AYBpsu_ALZJxn z2}Txls~d*^vGR(`vP6e=gbe10x0bQr=YUZoU^f2k<@8EuC?sdcI}Rnp9KK|ueFRUL zv;fNA%33|c33U$V((y*vp|Md^F^szQ*Yv>HGYdN`pjh?p$C(F}#n@{Fh9bdOhBm`g zF2=RUIkN7ccts#(d2N!V_R(B~KJ=aKa6PRUc%7Zn88x7_AF<=M^eR>Nbm%V>q$; z?)W@4OjKN<_1~@^DpXizu`<70t}sXFySz{B8S2|rV_gGwxPb5UKYhjDjVZ2PZ&9S! zrHafeb~)Z>*1*S65*k<_lqJ@unBH%qOIj<6(m3omc(vA62;eEKGsYnoXcMBnPt;o6 zxDxdStfEFg-r8AKmEd?PO}^_FuT0!PT`&HVpt?>O7SYofx4z_EsgTOtBoR`#Q*5cc zogGP5^B0P@*P2X^CBRW>)$=`Qq?gJX3VIOmQmebR&=CK>LA5){O)LXWP@Z_i@*DX; z`%0N!x&u$p2K8cSU1Qnyuh0lryt%dkPCtS_SMZIqV`7JyZsBKn1VspiTWPBTS+O6`N#ifC~Q1q z9Qe6i5!dwUCAr>|MTnlm6BcvLR}+H};{h{u+n-V>+v!ViVAr?~<0R{bULud^3mhPkplU^&cL0q3p)?M-=o^ys-pXOz#oQ=tF0%9li2;g|^Jios z9ScuXwbV`{v16Hxra~E#m)xb*y;mY-!M(O&;FxYa6Zsl&BkVC)naZspx=PqKBVb_m z0-cJk6il0d7Gj4iwxPLxVD5kKpCgY1r`Va{N@uF^7&gSp`lC;_(2HFFv1W=+G9xwl z%HWq&)zf`VFYMQSlh582C@6qep|ta^2An|_M)9Aen=~#A5as2=1XDG786;8qU4xz! z@c!+<1xab&5j-cvW)%{#nExr;(|t1w%#i3N_^6$-&g*d^B5+CrTNi+zQR<}WmKc`!8RG(FAiHe0+YeX+*khb0xM?j04O@)w zkS2=MvtPN<@ei%okylCU@c1f_eT6XysOLxQu=Zk84Lqna@_ONTIWi!O2T;0_k2^F1 ze@(2>8$HdYGcvID{tBg*uSZ*dBk=>naycOn!dT}f|0~QiUd!O0W7Yw?Rt`4xhLMQ# z94TUgf;_c%yJ7TR>AZJEFuqK-47^J%P+!}1@&T7@+Y5M4XnbbWcJR__iH8pcK3c3b zlrER|KFA*L=v}c-33fIrD7;<=Gr4N zy?e8MvOip@r#CCA>n?!H&Nx!USZYW}%-7a;J#fBn4&58$Rb;=&!wI7|@ExX%DhwOH zcgz99#(l_D&Qgn9-wdrr56k{{a7V4}yxcZ7LHA5aH&8T%`Me>%s)I~bz36B)e&m)7 zOj_H+nzM=cBO@SaT)$XA_3P;f+luLYUOOylx;D5@!TP}vYeI>;C$~{ zK1RCbR8H(OV0vWs>*lPHY}6ZJcaBB z0J*2gj>sz13Ns+ZAq6x}&@+xZ0XLnFI6m$61Oh}`i9E?|rK1Te+4c8T1EeF3kM^$A zQzC1>$z*&T1ySanWg(k4X};RKu_+W+8F$RMmMQQih+r#c(BXQl1KI#GT5y+e?_$U1 z$4Oj_bi9|?AY4MGI1^dFd#U|SBS9icm{{h1GZSU_G+z_U=Ql6A{K&Yr?nlLL5ybnq z1PNe-#t5L0lOD>F;)M`~-f9#NhmU{2amqGC%JAmckx-K-;&Y!BaOA-cC-aiUG0;lQ zUI`?0XMx{T95lvv95Y#EU+k9|nt7I8>+Ey;Cl#%6vm+MZxg(nXiA_KP(^}CyC#Kz) zopLXLJVsId>d{Ck(A13Gyw{iyg(;(z{^qJ^j0Jj(#qi2B;Zh5m%daFka`1`~6R(Oy zgG~K=6<_M0(9j9jCmBKbl|~H%rk7cQ;!cm=Z&Z%Cv;5?ASn@$EVQMfC50xTivP1%}u748vy6xrQ^O z06aj$zu3O}_-Sib0;vJ$8aHZ^g!Pdrx8V zCnf(dyvUp}L;}z9qEbb%7=Lg1ke*cf1#fwO-nO1DJZxEoKCbPQCQT7t$ir%db}nvG z!79nBd*nZuM6n?e7Kx>5_nO8TDPI=hnMrhe3O4I4l@!r?S077^)a3%s+T0z}mE?pR z%1iIaz=eI(6Z#CwUF3cuevz79z)N}AUW(=g{-^_GThTV zbB!(C7v?Z%DJz0X*4^XBd^jwzsGvaoE{iEhVQ_C31(=r3LQo0hvT7vM^&I|Vg-+PK(0nP!bed?F+qke0J{SGuGQa14C4sW`bA#k+(MH0yu4WkJMBsbSv;f5Qv?1(GwYs~PC)(WCV0C+n-E+lAYMK<=Roe;N!Pnsc zlp9B0t;2-pKG$-Xk|Dh>{6rdIr=Vb@FpM2btFGNZa%}1?&@~V-;`T-@n;g)BuMQD+ z=_e=Oi_dx2NPp7R%USj-`xu74%|;kY+E@^zz_bpX;Kq3jB)%wjkL#zHWAEBrO4dbj z8F%2W=~80)3|bk-8`-R|kXhF&I1?5@HM-VlCPNoJpK|@l%Ml0wlW?c*V`XT+U?5`X z+R>pnTPZ00;Nn=x`EJix*p?o|3w#=9U2E|>3uhdft5;(3?bie1#;k-6P8!v?^ z`$P6JSHR8zU;W9AhmTeuxe%WzWJqPrT^tg_#9MSQUBPN|a^*lH>hN{IVXiR%;7wv6 z+e_$227!HA20_F1ERc{=EFd|l{oU^fYg*dXHYzLm)t-D;nmE6w>Goy+Yl=Rj2FWBu z<+_eDS;eiq99gzT@%4)(M5NdEK=?R};O}m6^n~>v1w~t7R+jeh5$9D{ahAh*^a>#;bY6^Gq|gpU#9zTF zsN>&PL7^_YKsU&|KUB=kOH?F$E&IZtcYIc~Ff?>A*5cs*E!uv=T2S2J=vE^o3*Qgl z^_^z|b3wieC_g-Rm4br(Ri_|Z3EsE;@joX{q;Zx&@9JltTF9JaOC`Kp)yJV73WHIR z8QS8kUk+CZbbi~EauEgP)k6=x+gMA5?z)K*T%LE%Kh`Vg4C|9d3Lx&8Xkp^R2!-Lrby4d za&pb%Q(+k3l|q1`{KEVQz7yRlUazVbU8tUX&T9=?K3S>TYy%z2v?sxM$;s%dzeS3uI()UrEm_+c>iyfa;PPL=7ph z*GQ4^)Aa`+kku_c=%LI$%vDb|ET^vBW8tBR2)GSg(`4{M@rtsarg{sjt=HVsi_Jp9nrCW54zsVMuLq#R({nwgsU;Z&ppwv|>ykBqSnyO*wK&7;3I!mW^- z{T?k%>1sPYo8k=PbJAFRU76%C$$I5PKr`I$GYw*sN~N^d>?x}}lVdIjZS*Xbo$CT3 z3nleMe*uq_{j~z@38L1 zpj~F1?A{EfV}BfqWzX3wKYaaJ0gj~F$-(6@t=`#KV;NN3DWs%y(?4z5Y_7SXqAp& z16ft~^d&_RKr3?DUc>GtBsp>4kU@d6<~*G$nk_z!n)E-K6==AnL}T!of<-0Dubeje z4?D2F{elDX194EfLrO$F4gkEVL@nzY8WfGx(dk+l24-AOl=I{{8V9~t-`$vGR!uK<7rCRHOtr;y*qOZ%5BdluWJ0F}GH$ zW$O>F4vr*D{wq0l?>lv?P5mYjny|B{z%8{H4q1J9?{LHvrq!Gp!hd%`iJe#Mj?1MQ zzbg#@^P9Jw73&ylZ@yL-IZf=YRbJbELUFu>HBzTk+^NRpZ#QTWywkbK(_J=3-RpHI zTb^+NcKhrSn^uYw1}U-5D?(mh@q56ncTY@P@$YIxNXb_AHSU|(dRb|^^;pJpu~c$F zy74oS>(WFhcdw}?hv(B2PLy&nU)Q-pB~kyyQ)TD%)w8cnggszFtBtpsp-+ehMKjlg zYd}4Uh_=KIghGe19wSGpk>c%A*%v}!70}|Zb`);5bqzqiccAKIay5Z+o=E8gB5ov*@m-)S&sjR~hXe>DvKVrvV) z*`fDK@oBiQ6ZCQ2YM0C!2M6K&QfH~qJQCJsztYbkc*5GLopeq30!)eHEp}nxKOQ~m zcat}a*a-l}!vC4wJR=Jfv2-=;bk-e$nv+*!^-#+_jYr0szw-&GRIEC;r`gUJtUCOh zmIO<4<_#*AjblSpNALz$G1tnrskY@bMgYC0eyViNH~#cOq-XdCY7c&h6CJa{#WSgk zo>GLry66{uvZ5MLxQ++3;WGz~sW{Tie0ej3C7C__$~c5oD*?HdeqX@W+65U4HF`XX zP1gW_P;3tOa5ql|i2;d%_7VWYm2byv**v=N#i>lP($@nP@K0$%jAyww8X_P6f7L~o z0#y^D^QFJBG(}(9WC9!@}m%7ib-r!&45 zd8${YhMt7)JAZN@$LId?2}mrvDHJ@7{uO_6Dh?5WA^+}bU65M>X~$Lphv)D~4qSYo zRp>wds^}=5Z?!eQZ2c39q=&3U`w;s~AE5KmTHk^Eil$iM^5(lt<(eBZN7xjaXz6tu zTynE?rAo~VvH9^JiN?!PC5NE$cYa&TD<+^myZn&2RSUACOleK5$vpUQw!DzGR61r5 zu+*WQbnttwPd7sbFN?`N_aqnstsQGHH}IVxBO{ik4l(QGS+{gklET}n^ha!9b!Qmb z$nww=bj4dnX_&^LWsi@`!5@5C7!ToVP;d1(Q+mniQiU%poQtObzbI(~Lm*%2iYJhk z&NWA;eX1sGiPQwaM)FNGgegVg+0D4^BIPrTwMj}6tSkNimETf75ahpCUd+ZH*9^V7 zi#ut}$b5!z`(G(x-x3umXY^8MH@p-G33S$!U_jBWba;BZ5=(q_j;_?lp)ltamTMCt zf5PV6LbEs|gh=^1|9hAPyun>d5kfYK0L;2`n8uLYW7=t4ZGyEv`AGZNYhpR8(8Rx< z*+YUjX65A z20v1igFbF}$RI9B-i=5VD7YM19Io+=I@=T2gqdpXsySi=$%yA%@UF)`EW;p^bT*6H z>}o0uQcuxR`*Dm|#0Z8MeV}K?r3fSs_DNrq&iWpjQY0qg)A*=-C!B(~Q~NCC57r8O ziHPkM_!!j|$VmtQK8~+_tWwC^NeUgmQnldOJ5_K2=M!Ej`Yu--d$T`u16Ug2=LDQZDsrHVCiYp{v`}Cl%kkQng~Pf`dIQ`k#hq!NHMa zCEH=21bS*uC z%5dmc_}30ElZ|o!?l>?^TA!YaTG9d)hNP(7$$n=rnXrY!5b$EVH?rM%%6BOqK^{<~ z6Bkp}V=Z{XY2bu(JoXrdK+`E$yW132ucd&S05uZ-Ig*%^$7o6lV;%FunM8XCJY1uh z#7aX0BIHns6aGsRJVdr?HXP+qLj|AcIxPv;cwuS0^5251Y^AQ``@fMGMhV`HG8l2n zU$qg%6Rr`58N|#ne*x0XS83wLJMSX8vcP{KEB(=_QBPg>2mnW#kjE84+wVTVvd?s!9>Qh!2^zIxPdrGe#wYls@mzc`a<;nmF0R(r{(?5%x6c0U&P#FJA;XaIBp?e`F zi1<>?HYG@J&q#u^vDgrVK+2Vy)Au|wEK}b++$P#|YUAXd0`6nJlIr%f zq*VL0f>L12ZvWoWyNVlJb#MloE!!8lR(AB%1`V-oSX<4rB57`_g4~=Dx=@!E$cUpI zBYhL-j%;a!+Y!|txf91{oRt9eCEQO-_$G^@z}2#}B=a(GKc3r-C(QlH@Lyn)GoCqpFPEft|{)?c=!->U8N6Yq7maa(OPs}eXbzTw5e<}!n%SY&3~>q zwb=GDro`ivR+R7j7hv$KbRgIH_9~rIw}0KQ>f#wh#6J{u`Qrl3N7qk=-|tyivdYbD zlxK4e*&@xYELuIUe!)(TL;_km;!KoK1K9*h9EbldvUZQE_pxxnHySmZqx0Ujf8Q8( zorgn4<)a@>2bzQDh)9DI*k_X#GdG;}3SKI0>k?fT?H`E(g3s2#V`m8CYp;LvIbu_v(*qe&f|`OEYV|=PV$fz^S%%Pj>L|!#Z%5Rce>^`E2(1ACR#_}O+(Dx{8_L|%!ZHIc73DKf~

    Lc*Ps)1PC)q%Z^24@Ie-SroNBtKgxXZLo3-5gf-`_OIuf@ zL9bqFD69Q`=tbH#!hfQt*8>VbMn0;>=RZ75VpA0FwZ9MZZF4lEpx=_kLSQIgO0b3WA(B*C zL9>V6daXSJY~Mo1nV^Gl7CwfD9y^rTSF5YZLJ3dn)GQRTiy0bj+4{OsQR;Y6LFE7D zJg_vmSH=GKIS>ev+EbR8mh}RFq;YpJ?Zx{^`CF4slQU)elqM|8fO||YYwbj-f2|)H zQ|_URZzSb^NN8%QTg^Z7tmoZ&k$OgM(YEO*CCwQa%4%gUiqc+<)%ULxtgQU_@k;c6} zk56NHzML=h@L6sJ!gcP#_YV~2$lHZgf)2>y`)01P@-Sv2`{+r2(Cf<#+X~1*cJb7f z7x-7XMnu%?xkY^>*dt`YwBVNtxCoM7Of5!XyM4Y-7_bF^3{i{SOxs1NZn?j|Rux02 zNXy*d{ly4L(?EU@^RjM7GR=gS60RUf75-?cKmy8OFbdQigd0ce1#K1E2(}Ju%$=xE zmPP&99x**meYatfxx~ZT3eTD7mq68LGa6B86v{ zV!K8}9LGPVdVt(ME(IH}TP&gR4i8%CSPA0^y9Y~q%*la8C%79jtbACnsF)ApFlGuMn3R2%VYdDg{eZ_YDtFB6 zpuvhWe`k3lxy!PK8UQ6sXUpVwhCB{(ilu-9q|1!N(GYt`qR^Y)L>tQX?SXi!(St6X z{}`jD(^wj+%p*5H&MT zOL)83iA1Gcq=?|v1D&xdSsc zd=)Oed6u5ji*jEIYM14v^!kVyPKT~0eg*Hz4H!W7Og4YhbR!m?>}pAGERh@CfANB4 zd>C^b_7t;BhT1nvA=(>WDF)W~_@m~3T>u-=^5EBm=NyFuquPPx!TDY{RJ(htRli1W zKB7f{l;fpgZeG_g&3(ojIf;R0b!OP31UJ`vL|LtfUh?6tD+lbX0|7(@N@I`UJg#k?gMzB>emgTFPZ|?!8(l^W=6}(NrrYI z7uCf#S2B|pqvLqT6I#Es47+({ZU1D?jnRpF_xfgiOqL?#75%bkUy43kGTe3EjOU~mu3#~Y)Jq+oxNN6rhY_2-(M2CN%z-JB zqT8AX+sw5gcu#oxQ^gtPYp^eR{cE!Y%5{af3Vav*>I@^&|8GEo$nyx8x%IsgWdKpu zi0FRaZ=MaS!JsJ3RG3aOeBC(iY0oh(%f{1Y_aBv@y%HM4GPqiXeBn)a%ksHsa3$+; zsY&zJ+hHTnH(DIX?X`WjtRhxR>Hp`ldAy>VQ*Z0$+8N`efS1OfSshvPK}EfiYHnZW zO>cj}x1D=oP=61gd9{|!;}Gnjp&zdHq7(=B#H|d}^Vc(&#OzC_zxdf9UI<#v@z1*$ zB!;nmMe@fcBB5!h?kwSeeLO`r$L?#=Vx&v};3A`ANyN>K9w@_xXc%)kk_C*`V)8}p zYDM8;*iej1~|2_{4ReY>c z)qvKjR&t0QKpLjnivrtfBSbKQmino?fiDnYp0??n;qet8X7Hm^eOQqNm?8X7sF4|A58YAh zEyS=xkv@hiY8_aY>V5&nlVk@Vv*HmuxyUmGChw#_0AKGcF00lg92QQ?*ujl885l6b zF@iywiZ$K}ybpT`n3HASDeMvCAw*k0vLDE*REOh>RQQwN5fg&qb|i0pyC4UmT;2R& z{%lAC4~*dHe*8HBKDUiTv(cwvV*w#+&(*RD%E+NH1{=T<7Q7n1-n91ESaaE8v{Z_bRg#sV~?2X1guQ8AdTsHC&J3-3KN= zfiC+0!we}`54~Ap-@hO*OH;U3^NYGX!rC21V^t5*pLu?&2T(Ru z*}b|x3Vv0D!IFqbtx&8hB!krKvf6KleBmS&=kT~Ej~V_N6kSZnZ?9GoOh3J1X)}4{ zA}1LUwz|B9iRmFV7MtQVOnrwG1Kdg01p>u?$%_6qMR%pE;6E{^^!V z9_Vg#QQVAq^Ap=kI zZ#r+%TSav*{(h_?g+@jFP$cOuiPMH!t$_;mf1-~O2Yhd0I1qvtRa$5 zJob9{pr0Xp+>h*!>lBuBd35rI_8-P6*~Z1+b<|nugGojgZrKcvR*oH$Dq@S8`P`zWZqI4UNqSj3jA`Yi{$7J{YniBO5&9RKXoJJf<_(-$_0qR- z8rTGR0c2(2lyaFg=Ul`9!w7N8T)~6Wn_VbCe3|vl!U`M^xL9r0*hmYb3~uyd%YA@x zp|~#om$-vaDii;~?lKJ1aeO?6-Nr%J$=yRqvrcFj#LUU#ad&jrz$c@S@WGZ0JBYutI9I9vFy&j}_$Bq8U4k4l3)N(iD z^wKSSXE%`pg>K@2VyWN^3x5*EorCCum0`Oz*kKqw3HA=Hj?Z;=cbsFpqu8kwxq#1eB1i@QT3qJ{I1 z_7;W~5<4{7C0H)X>P&f?3<^|G(FAAO>g@$*2;6CSJeyX*1%t+`hU{B$FE%hfKDfS7 zTpESm6(4qZ@wWn!(In*xh2Xvkf0EwkS%NogR2gZW*u%6LNWK*C(P&%rM zm)ZHKUu_SPk*c=x(MnV}r+k>A-7Q9s_x{as0Enw@_Wm)7XxUreLpJUC$`yPiHwB)i z?S^|D246_r2Cw7M);Vk>Z5~!|#`H*gJ#*%KI3!;pmJTebp1xU_>+P-!qi!5G=)0tp zgg0>@7L&WNa09-M6gUuz%4a=F$2!o}byP_EK2)5~_LGWA|5H~_cxa$(*bA3DUk!4) zfs|r{S}Ie?ItDTe!P?Q8j58uGGoj<$X!cJY56d@nGhVlDR0wP}j{WDib87;z3aa<0 zSXbpaBqvk!EBBY8jtEvR<-d(zFM)HbG5E}`s!oMTx0EYP%&3lmKG&>bzeS9L&P@~i z{ry{zas+lw#(v8DY@-G8y+V@4@>7E$w`utzue;N$(_QP1?ACV3xB#WeM*8;UL zG9$9u9-|Fe4e)|zWP(Oydc5d4lowt0n8%a5XJS{4lBlf;%16KIOJAmup^Ge7W%ly4 zYu2gNEMM%@Pago2Ievial|?2TJG?-VgXdeGDE#J!fR?&jC1vDM9Uxmpqoar{;Vdy; zGt>6V1YHCcy*QGQZOvowLf3HeRIoZc#R{@kv@Yf>oltw+!_2 zU(PLcwXgn4Qw>)-t$WB=t7Ne1@Oez%jC5~}zTqSHS1NwO@(Z6V+L&@s{=!N(1plzy z3Z}cX^{!P1+CHx;G^Tx66ur5xCoOpi(BWstSt4A0+zg(cz=$o7hU^Jm;@p&UMUV3i zWQ9!JATm~<#Y&Mm1nFGctHDLqQVqBs{DasnT(M#mNUijQ#gOHZhcQGG1Bb{@#F>tq z9Zw0{hq#YKa|Nq-w{J4e<~?s--(Qu4%j_*k9ykhVrrW9o5eGsy;)aKC0DV0BQNV@) zEIZ34eXfEaT*GDBwrM7v#-1g2LzQl^`ShZHeK07IsN>J1+#M3|Z1b_8p%V{Q{*O8k zpGA8Ap2m7m+ARSa>Gi`J*e9ede7WyhGer!#YN=b`s9Vf6(8qNIYwyhN6tKl#9#v$d z%m(tC-5H_6%De&j+50(GeDr7{#flIW4S@Jx28i0DXym^0Xoy0@2o3+V)`(y5*f<6% zuut82{Ih}!DCjG)?SDuW!o!aR?T*xeAG;j=jd;jXH>Ty$JxMB6D-qFl=kQ;08c^$% zRIEBAm3*_6@k9hcG=Gl9ouVfeQW4PLN@fR_0MT3h_k7C|NeALEQ=O0LDr;RGG~kti zFZF&T8!3ZFs$=g<0t77@_o6J1d6~PZGWIAYiNl$GT;!uF3=RvHCQ zysx;;8xm?efrGIBs>NEhC5fYf(weW{rmkW0E(JEez?fiBq_z-BNfD% zrxD!B8Uq$WXKIdD!!Y&If^j;a$U39j&6SlZW;XdazV*`GCc;dzQihQq2Y zafPmC{-1P>>cT9S4gIyr%95oGM=JQx-`d?xs~Uz^+6w1;EmsxC zW_fH+_7(gK;&O{r3GEZ2A%8bY-jt*$eBPrag_F10tCAIEM%tvSu*ZKFNnSwTGm+NW z#~xKuwSD%k`%pvQ$mdYca8(XL7Gd|-%x=y(3{4MqpZWXEWN_7McVSWZl@KUQvw|`w zb-6=4V2rn>c67cv>VE(N?pL!#^~FCU0Q@_+a;*@bw-kpNoVCs0>qj|HB!vNi@RLX+ zgvdD=0)hGMkm^G|hwmsDAI8U-uUUf-r<9DBCy0%gSL@lZO5QQMeR5Ns=Y#Q1csd@} z<(woQDynDIE4|-AEce~*M-U*^Y<5ndDxyFD5D556B$S!W~!?sJ>dyL2Xr-lhrWovI|(K zge5Cfj`B9qtna!_9f`K9amA7Blib23J1$JD&XGpYAv`#_IE4NGZg|XtY&1RZ5$~Pb z1I1)V_yM^Og>-4@S)Fn$vGmqq-7U9dmnTPS$*7LfL#!qKWKpS7jpY2u3_vc*Zsy9Q z7b*kY~ID`0D)~2z~8&yG?gK z1y8(rA;Wp+OyffmHVLC4o@n$9LCK6VcsfTwcs$FKoUU<9Pq8 z=QC+!Ik6Ji%O)Q?VTg=@p{NJf)wk>qq{ZSB8V@RSexi@-i}r5x&5m&lEx^h!Za;>w zK?Lcc_{kJnhxuX&qvDQQ2D?0xcws#d zpUme{59pq67%@q#fB;k;*z~2+Fg;4RAfE%bAZUtYhDF+D%E?)bUw{azg}f?Ce#seB zJKc&CCTI6<{j;rv7y_h&$IG&RM!P539gZMQMr-0^!cxr;%?R8p@_-eM7yg?oRPgbt z5I5|Jrnr1+UI2L3%!*@VGK7)auw*ggZc|h3h=?+W@V!*k2bfWAr>gPPAda(ZCa zcV%w$KFw%?9HP3w@TPkeBD%Ek;>o%opcHSWe&qY{P==t2-yAga-e5UkgyE#zkEjj= z9;oY+PijIypZNYBm?WSJ6jjYG0$l6*rk;$d3N>(=Xs3BgyL&Nvw6Ore%1|Upg+$wFG zo)s@R0`Fhau^&S$|1<4Kkdu?T280-u27N1yLY2s|R=>3(bXI&Vd$7Bo&pflB>;92Z zx-w}>0|xcFnJMvXQyM0}&H7OEY|IdG!O7hN4iR)Xv%2j>Pli4g&+zcJTW-0=MLHn( zp%53q#fl?0A!wY&bYCm+2Y$3_(_IM{zj4zN;7%_VTCVNF*jfrKWk0mJH}e#t>fm`E zhG+u=@M2Lg8uQE8E;;+w0|KPJw$~3&>EnCCkd+#Tje9=Kdh5J72 zM#kQEa(DxaFw}MlQf$UVfKc2$8uYfYy+LR^a}SBMb}Rskq*lo*;vH6&ZrjgGXcuFe zl0XZIna22KfdEVCW$+>H4%t+Hpf>*!rzhGoRE5&#uZU!jh`t|N-`;R>jWoXxJJ{a$ zph&J@o);RyhS+$!d{;tLW_ZdS)+&l&CVgqR2y!ThX>7vl$kI&HEBA8JKv;ZDvjfdO z%9zkdeIl&%y?yMj*`)}Z!D((w8c zyL*HCMk<}-_ir#Z72tF)E<^Fkhr%Ktdu4@T`}tI)1zV=wnl8$Hydiw5ct}clwRh_( zgmZ5EQWtgF5(67fq{drp!tTE&D5l7=bai~5>#pr=->*z*UXcd0u71*Etqi&OYjK^3 z`TZ#|; zBzq^@Y43(xJDa4vkO8#qtaAiJv}Omp_82hb4@dSuizO-rO1!Q|9Z zsno7B8E>*?7DM58YES_DD$xX4$gQzV0clEbxxj}@dPgT7l%knS;^CbvGH-^>HP8t5 z7}Tj<>>`C%GK=(>DY0G3WDqRG5Qbu&|BF~!`B1L_vglwyy5%wtcbxH7XV>qgCbaAI zG%PU&x=ftiFmh~e6Ym-$M)TPWkF;7BOptOm9ODDzNPvWsUo@+z2vJ}##yV|t7GJmoQUA(ZPBav9(x z8Z`02f=gHH8%0mX*U>y4tk--}O6;9Z5A&N(T)T{-U?bD|TD=vyF>!*Qpjq=6ygGje z3x2U@l{wi-QdQgqIgBCdBebzKMu;&07s6!mlM##siz{$*kZ_JgfUz{RYfgYK@)->ew#Q1lwcVu&a6^^<3Tbn7Z5}0Z z>yi2Shafwl5%XQqph^-|uw|bt|2=n+GpHLkzqkmjsc)6n^?E~KsSe78&F#{GNrTa+ z0c9-LPjp!KmJ^Q+4O^eZgd{%rMmU<>mU^i=O#!?&fSxGzD}PPMY?yKx&@h-=#3z}z z)TIQHRr@6-+PT+j12ZMW0GLC&>6bBuX^_)SpKtV9c-F!lZr58{_ zYUciI+k^9gBPs$zq6&cWx@~>P>Hz}a(8&+E=HoVf7sH!>mQg-jyAa;hsU~^(d~PGs z$DFtg^i#h67Cv-*%|6HXry>RiKUBWq(3~oZ0@!41${^-$X8~_|m&g+7RY5}=>n(_G zmiYTq6ML7XooZ8i5iAL$aQ~6$PU@j6S550ObAxIj8rtfmZh+pesZe-LVSv1>!wPcW zW?qWFB;*f5nY7YTb%)97f4fG$dD*xN z`byR#E>?-yB>zwFIdDEyNB=N#yW+DtL-bmR@@bX zfxu(7aU}Q>%^X-$w)6g&Ktb*zdIub*c3dA{(fFkQN!hQV+Z1k1lm9tbJN{~GgrX1_ z`0e@|fU?@W6ST&*F+HbqK!u3PRT)1UF)Q|j{`)lIe5aI?m-BxLdnA7JM~9$K@F<$z!i{u z`3wEn;(iglvB@_u@fkQMPg$kX1^XC$W!l+QJyHT^l09Uur*0S&rUSGogMzTDRa+6$ zqiBh4=B%(UIF-XBPUy1s;?7?ZVrZPYf~WxTARU;&mSuwnE~=~6lhIh&a!4$y$SaL~ z=weDoXi3Js!iJ=5IMI=BDI={u!&>ih_#W&L7i^O2sPjR(Ujy`=^I>0Y7_749CXmL~ z9C|vs4@w#P(X)9duLfIa!tu6DK}yw9{>POVtJ=at3ZZItCg)qRrLV_sJg_$I8B%(A z#&AdoY_&O}H^ywdVjG1+ZB+3kjwHdHx-gAPgBFFN} z>~f2jmq-aUqTFfK0C-iCz2)QxzjRwghGL5|TLTG^=-T-|wlO0h?BH+Ke$QtYFdcx> zf>U=d+e9*x&W=A#Yw^N$c(A}CR0|YbRl$9dJU<@sSiweJ{H}p+dI+9e;A%jN$E(eTHXinwy1qZ00+j8S5svEgn7~Ub79F7ayxi%{C#Pugaf2*%A3CHA}h+#b< zVtij?=(S5i^oQO{XCw%Ha{23*wwYTR7JmP7$(D9o5Dk?1@*EF{p;;$j)ZILQ5Rx~4 zQRaZVgA1XpRsq!c+9ZqNsUrmm#~HN4*}H5XA0D*k)7})Nn7Dk}PO&;2<*wOQtRA&H zr!Ix>M0XU|F4)rcum=Zka%S+y_EjQFgF4|q#sqv+ORz`f4$)L;gGI|f_z1WPg500W zDsN=m`UstE;T3+RNdG~cDJU?ANPf3#LANN+CG) z3uB&pGQ|3j`d_(lG9CZX6Z#_dHJ zT17ttX6^LGH+o=H1)u)vvcH*7QVH{qz0z=$IPbrHhp`6^GK8$_@OvdL^+k$_i|uY> z4@EFO-t^h6no?y#k7rr(bO}Yj>36t_1ezCWGWoE4bGwKW+%YUBt5(;gq(p%C1Uz`> zLV9k9%T&5M`y@9USV!WuoTXcb3J3c2^{$^U4PrLKOGfgD*-$#{ZmfFx;Tldc+1X51 z?Cb9WRUacP+0L3-6839=K+@2242t?}qJF}`lolYKYVwVKEc(-*5QhA9%PMyM^}QBl zbY`7x{@@S=MqGCemh|g-G<0fj48!Kq1tFfJ(AbT{PxQX3GpJmQzlz_M1+a!C5mM<0V>Lh!u;XZv6&ru0!+2J`$ ztzD-?h|K3^4q4B8YgX#duv7cAQ<8avZX~5pFaZFb^X!H=@7RO`dd~P_?{GT zoFK2%Lt)6+HxXb(-`;b_b_HkmJReL;C=IEvvv(d16Fis5ejP2;Znxuyvym|7qO)D?c=y9JF;I8;bX686Wa})H1@I}7(AP-YvlL7 zD1oytRw!if@66t#V_o07S+g_RvE~F2>x$WG=C9c5RSYW?w2Er|^hpSW(LOZ4fxoN^ zN^6IkKb&j7F&`IrxQ}!Eu)yp$B(^?l;x{wg6FbVkpt^`oKW}(PSLk1 zmY!++jiWY%>As{ssKzY{O0f*R^>K4UAf%7-B8q3QY;`X6mrhOQ8Wlg9a+R$+`_^U{ zJmw7B-Iupzgh=cA!U~9GFhLLI`Q{pkW#DWdkJJo#65vg%f)jh`=K@Vo1z8pg&Yq%n z#1|Q)HJL!o>&MHD8?TAzG?I>yC_i4=UGL?mW_akBzFj%BD1Qwya_#MAz)I}-D+{iW zZ;ZXMz7iI!WzZ?PY&ysyy7>HMzho5)AK+F!MV!kNxtGS#2#6z>P>rhQC0HlZXX?qZ z_)@zrw_`}ZS?l6}2PcQO0TS+%|lbg!kby!*rhAH@7`Rhm^ z9!>KsfY|nAqJ2I50zk<6t88Q|b-Rg96K!g2=t+kxZSzg1xS(b^(*=VF&TNr?3dEsP zC?sc%aZ>9jUj#~qb7c#t+V6JGLc3^&QPAbKwjHB{nUs51^9CGyvBR(BWA+7aQsISh zTuJg=M{a=kNP+m*qfH0?h0(mccx?Fu+yITEhIbV3erLQiuS*uqJcEe z(h($Z^%NMUhV>31khC%q+Ki|wuXWHT30})Z2yD z33H9gvsT7U5ZK=7LTrm+&%({uCDhK?fyRFb{lMCnu&sbn9pC<&Kpg#aTqAD(X*mYw z${kncr$L}6xymtKfr(rjhN{~QHV8bsV zcK-mJw9lk$dko@gc%ORi5b!Q;C{XO9hRAZ@6?K2mhl2JSgN9OqVj&;=LCdOOGfoo( zjE%QG<}-S9J2B_CuFXWq7c^GY9FTlf`@OG3uRVRli}4icM{hDr9(x#M{@sJE%iIlWJqNtxm}E=FtAWb37GLklkvv~uomT!Szo z-6EZs#|VvT=PC|3}nT!^mI3 zzglp|y%7%B%8`4*v#&gn{+Ex4+*SreX8CH9WS%)#5?awd;m)28_=qQNMu1+Rukep0 z1@*prNmk?d1Q!K=xh2w;OHFX=X0lRb18m4z*I|XJPcis))d*GRnoU~T@$JHx4j!d0 zxA*;Dgs_=6_T({G41zXjAphJi1PpV2x6!XAeFqy}Ws>&x2ND7&ExDAJ+N}=@C6pOk zbi91mGD9j0W1IOT{4tj(Dr~F%X5=a&lEYf^`{1eu#{y8vK%qB`AF>Zvn2O3x_<+h(Uum~IVZ6+#SqjvfhITKb z*!`Vy%&y%GMxVAhpA4gRJW0^YrU+X7%uZB8>EhAQENXj-P)o%S@(L=WoAbsSEBX9aRg=Waaj#t&M6IW}<%ilJ9tGNOuIs z*kyh!ePU_BT_%vvc-qtq1g)te@?o}NnXYO?3se3mdz z+@>Bx)U-1!*?^xQR?DfGM{Vus9b>`GkFkCKqc(1J&a#xNf+oJS{*Ql}Q8-39yo*AW z{fZ)p8mxD)Un!YyiapSgkb$b_b3KPl7e z%4`K$1GN?D{L_09H<}Nnwg1AoAR2cKbC{lWb3fkNFZrVVhI}{30Cd}Oze;+(e>eyP zCB}Y}+Sa~<6Djd{X^hjF+}1TVU|%zUrvJ^3KLPM6bZh3S3~cn1dUa&HPHrakj<&HQ zU|SdKjO8x*kBLJB2Ut97nAxo{D|{p$S)U)xg4hct@}!M^`|OrkP^IaKdO)E-nl zEh*99bMOcD1aXu&{U&vh?T%m+d}(Q^IZ^#`|9d)7AVMa?wWtrdtU(gdUE zgB&hmR_6|}02(mh^;4!*ju0;$pX;U)jqj?-3;OlYM1vJtT9(xzIU0S7UDT&)24_rl z5h3DoReq_`+#ee;M=`sh>8KN|KCWp0wn}A=%_yZ{`6q@lKAAwuS=9)nJrs*h`DySF zS70-0H`}ep%uFAWHLtsixgxK<+6o!(hp8OIMsp_K`8-MvR~CG4zURPKrE%vtm&D?) zFQuS7f(Dh(wFmdB@!}DO$zDph&@D$3)l$Glwd9EuZ;W;{|Ns+sE47rTRzHP;u+?`Jl4TDd zy}Ys~ztM>-KIWp19@EsAb;}l2!6djGdX3Kl4#12iDI)E*c%W6>u}n+phn#nvRJaOO$s*1OT<%GR{wNw1UA1L()9JzG+j zCZz6*&_)q<0grgWyY`OfD;0As=N&LIT9LtvZXP@x{}NWMu1Rfxz0IhJ~Ra`k+2!U+|6=Zw61Q~pK*bc<#bei*%V>Ujrm z4MOSvFCWLPP!^c3^)ol(=E*P|f1hy{f+)vcvqfjZ+LARs`+4EY=W~)klKwF=X6p}n zWz_ykC>GMQ5H(b0FV4Z41X*(99X3(zRUe1EA-f38z4fc1Ks@pezT53xSRQk95DpB( zhk0{XQ3eSVoN#eyV#fk6HtM9rX(kC~tMy>-Z*Yv(r3~76i7SvrS`Ddw#ZoSoN(=&G z%Jul!8IvD)ICezm{S^QzxtU=53jKse*E_wa_b&>FSjQBtzw!qKYcHq8l*6VKWU=v0 z)@t0B>&sF?Cfrt_qoOQsCvfgDs`14dSz2}1C+N#_q5^$#U(xYhYc{gula;JwN(P(; zhg^}E_h~M38*DH?`E|EKmYMn2aSdU9LdC^~_byLzi+2yX#rdnLmx^87?1Hc73HEJ8 zRb_v)(ZAJckO7k~Qn&p;Hin3p((8pb#t*M2hecpcI`jy#k$JExs}Z1c$*{cnHn><; zVIBW%2|Lk6R#PbKVh06V!8D7u$YwqV%$5);k%Gpe%3%V>u`$mH(!z=}G^BQ%%Kwbi zIZrrP;U-0<^ABdEE1xW%U%rmvCh34DY zDJRq|evvrVH;p42>#L$z^XD05FXFl3{T>5))wL^zbkQ^$K9{y&kiq#2RiXd5wM;z+ z+N_ZFvyT{N?T0xnAX}a(&3mJQ0{t?t7`}M$0qeTr=9YWG4K?E1Zct07g3VIx!(NWo zavs_)S1=Xn^Y>a3WcpJSM$q}5DwU+cyDk8?uTEj0&cny~P?C5haYO6H_KB(qf7*=l zEa-F-ON`XY+%^7fc}5)mSRPl?taeleD@}4%!<4B|O47r&P$8oyR$KQ#?6E?TRbDRu!O>46RjO-2g>!z0l;Kt^T=s+So z72&&o=s&?$7~4)OQLt~Qk+KoGzUXWFg|MRnA_S_M#HF;VAs$b^bRIr@bl+?*n45zf zgMaH0!icj|7OuV01jad3fuH$hX%_hzVn&S017fpxg{zokndXcJHAMa2709;bUVgyiQ#Sq4v3=GOp(G`0ix+m>m|#n&dB`3Gujxi zlUN517@=BpiFa!Q2}DF0wUDDa88`#zsKh&bW*-}*=I1Kqqk$80w8qyEQQPhWR+fAa z0SGVGTDUlp*Z<@kw3Y1|^CW5@tEV(=8|+Q%6w|cG(<>s8)F8HA#%3aS>((R~YD~SA z&ENGVXAz4`r|HbAydSI^WMa_jaakwI7gS`7{pzp_n@4->b?wgrwG@b)x$wxgSlcX! zXmVj<>`clJQHO`cBA;H{0ElmK`6q9hzAhMz&RWKeNBtqAB$~piZe%`8K`4j7?u~)S zFOWWHSvA}sn-gAs3w(wg_Xl;YXlm`Ufhi%?bR_#Z$u>qRgnW)&NwD3X z<51g1-s65g!CJg<0FSIV7?nqWB>}i{){%L}JmYz6dvXjQU|0CW@R>F3KT=554^az) z(|3Vx)pZ&~OR&FOQTw2|B@ERoxrjGQ! zSeE=kLM}g#h|%WZjXnXZOQBb+l8r0~c&RE9zKY%k--tvV6usE-)zQQjpFQ93R}L7y zPREE}9p<6pMbn}=%SHkQNq(&i!tS~jE*T_8vUf^F^Ke>B8kD}ZpGo_ZGl(6Or}h-` zia2mLNGi{#_)fhEQm26LCfsg&EGd1~_>CCB+{a8c^uZ^BU+K`hKB<&2CZdb(4)#`N zTi@ejfEQIH>88Kl)iDV>vrJb*Ij`yICyyHCBA3nd#crXt6o0%;_^Re#1VEE2P}=I# zt*ax6mP9$}u+!Wp8EY=^jrYVH1sPYHC z#Q>F)_jn{lO=pd)k)rv8|99=8#ZOUqpZZ5SD*ek|>{@6*A)Djy6{Wy|N`IQQMi3L~ zcu<&iF;hK&#+9<%)OW@q!Zgu*)98EEG`9=-J$<*X92EFY$um&v>h&#&J7&P9k(bcX zGovAcY^t3U1xk3FTlIY+AuQjctyFKD@(U)doirC>F36CIp)S_skT&<)r83uG&ZX<~ z8O@!h`~*sKTeOp4uX4cweDjd3n`n7 zD~?oadYAB~rDCcIptiPGo!UX#>wM?PrZ{; zZIt=UGAzFstT0RM@yG*k$a4rzwArexAj~ES4CvCMJM%_}hX>{-dXzut2<|esi=kqQ z=r;&sKN~WvN0emJ#sup4)w3JK}cK)J!4c&5*MwI@X{7(EOxUZ(JzuyDDQt20eYlerl@o~!-^fv(wGTnuXMEZs@ zmJ^yXTu=1rWf2p_kStih(B%m$T0z-0*oae$SiBw56GU@-%i@}{5Mf;KG);6(>{XZ2 z-_g~OJJ20!Eq;ocV1hdDDu1GqWljL84C)$^@_P_t{Com9wX=gSX^zcD&O^m;EiE3D zwvDo()H8fBQa{34=2PulERq`bW4FKG6hjkJDB}IM&VZ`~8+$_T5k8BW9aoiOwj@+& zV6*0^n1wp4qws5TD01jj(Tj7f5d=niUw1qE@2Yuo#$%K>KKSO=cx&pl<7Z@wqrX zKNZ?(i?>iq884Kj-MULKA3QkW{9A;Nt}#Yy3cMT3d0>4#L9}Yc;@Z568cQ_5gyQ0P zEO!Wq1~LoHeV{j@AYDMDvTPZyQ<@yVkG6HzqvR}9)lFbto)=*W*doy0UJr8cEK5Gv z5&;5J-$^KCqmFQwS<^)uTBrHamvb9&PS%gKtF4(iKHeW8*q+rJE;(W5o$ddXl_BUJ z(tGIRZ$JreVyfD_@$RiO}>wO2I zJ#KshrSHkrdZh>nq>R<@(V%+;H=wZa&Dcx?FadhT`)c+-J&UarH}|xi#Ya&AN|A(T zg302{0~r|SfXK;F`zIt>mQ#R7x7Y9?U%S?uPJi?8S^~WESS-gmr~|n=RV5JUn&gJj zzVfr?0#(_Ed{Xu6TsK1Sz^X5 z%-ew$oUCerCDkV}6QPeiS&g~W;gHWekuI=V|Kavvz@QJ~57ut9Fstk!`Un4k$5tyJ zYG$?gg}kuIa-IXGP(Cx-v5g~(%vcMS>X=BukWRKqMPxGva#4j;S89OUW2scE}1sSHh8qEhO0$CT$Y(+7HDm$Yp)+|({L-&BCT>W! zvfwwI0o8m~uGneNaujN~pLehv=f?y#qKsrCq~`V!C32E&Ze9G^6J>nVO+k3{8QOPQ zXM`7n7h?$4sBNPCLrta}=PGN#i}3j^eqdq5nG&MyEUcq5W3M#*^d>KKU#Yehj+4Bw(KWeZxMsVC8f2FkwLo;aM zCa)}2s(hW{=HTKnI6+-%y@0Y8o{@-0inp&8AmVLbGClR+|I-I-Dmp@T#YgKIVSS9f z98?X^ERiFwxxf?wYF%2Q6WngR07=x38nrlLG8*s4ZV{RPg#o8qt2FWnI0NhRWE{4! z4;>d`nNyK(&3WhpTytuqVdQ{@8?@6@bU}9NpU_~mp^X*TgeJ8;Z!Bx7Esc&vp8CtRC zoEVXH>|omK+!kJArk+8=v+7lx3N-~)>7Fgj+@pYWf+M7WLy-=xW-rb~&wP=U?a`@} zMtCVKF@N@tS)CRz0!uLeRWeR|x@wLl>ttkqFwishh^DmZx{@Fn*_(R!U|}rj10p`i zY8ET1;Sd37nJz3CZKxN+rL|o&BH?u{W3B64I2xIXZqdw;Doy0=(dJWmfcRzlcy%)}&#d|A?Y5*dL3glw{}KTdC_}&8WB(EAa7<7l_ga8Y@M(OR zDYLDxdL%l<&4m|s^z0=TvxbKbzWVb(g*S#xj;hn)k-uaNBRe%?sG&(zZ<6h#_->P2 zk)xmtm-@sWS(C-qGM`%7dy7ZpkiSya`MqWcq2z)4r8r#}*BZq!!fe&IUpMO9UEe-* zM%{BVWkIrgA1uy(T=@l34)NShmQ+EgtljT1{A6=+dtM6B@$8d~dJVk>g7L*`MC&XC zAK9>tGi)q3c;NZl9yS~5YUyj%Y@a{IN&z*(xmUOVk|Sub@`sa0<3qva8NM%eJIyr_ z7Q7*i^Sq-anyjDcKfw=qIh++C|L4Cz?CKCd6tUf_oQ&%vG+uBio(kzPlI^C_R2qL$ z9Qo;Fd3K@|WYcbds?-D~th{GHAxT%299m;SNWIdnKJ+;P~E@o>8=687EU z%$PaT5OOC;mwd70EOzL?`$mKf`6xh11mKHfaOc-+i2`C)SjosNzAzj!I?b5DBmLH$ zX>pZd)GYZRKn9faX7|PX87-|-w$NjvHXZM6sB!R)gKrqpZ@WR{OuVV%$W%uo`(^km z`hJRB#PI9IO3#wL924dHmt=4NaJp#YtQ#pnkDwALVBlfL#)9T|Z4j>e*1OBqnK65*XRsjG1~*qc{WP z_;slOdV*mdN7K7gEmW1#O-eJ__=31fUK;wT(eqFxM@s5?kiV~@uP|I7aIUPhw#ge{FJgv!w#w@QS0KTjLSqv29}4gO_bnJKyd#Lv z=}>`Givw$!vDPNypP*fJlKA3j z%4WHOnoI$VoYS5KEO#p0RFaZJF{@g~9fi56`5u{<#c^PBd-%__j9)IMoK8v*lzogv#dEdZ(c!@^rB3i2J+>_gInxAu?*dM>){W9V^?Xnp}8l@P_6Sc-T4Y=91e7%`i zXM4CYwvMTOD}}3+Rw;R)$$kt)57US7APUyScTiGcoez}Wxt>Rz6Ne;cx}`WbjKp$I z3TM~K>KC=9b}~EfQU6N9swAMk&pke!hLl(Rsk#Hf`QK0!($9aQF#i=>YV+4KvuN*z zEh6yaj$y3k+Dx`6h#z(xH5##;L+}J)Gp5xffHb-FaE)`-=DjxTeH}qr{DGpx9R$Ct zNBlzDFx3PtL4v!k$Ycvy&|oz_>>yL$_wY4+P`)e^-M&mb09evC&KG@~o!!EyiU^hd zKV%j)zhj32p!95)g4Z^$Dse!X!hwL1;NrmtuPdb-;XZun-cn9cl5&GEWw&V-`eCC8 zar$O`J^`jDAKS>ROM*p85KF`Ezql&|_c7l<+a`YT6YLN8d($+X&ed`>G@OBX?M(wW z;^DlCgzzg4zrKE23(wapBxsYwG_A$#U3YgPo(|5|OL4K!J{kfM>m8YDf|yGb<5ndO zfuDqv`_}PN$|VjjmHl$HeGU%=HSzAHDrxf@@Lhx=+L6S22IPU&g11R#b*tFGX(Ges zQ1Ug(<(}0){fchOxcjuoM4B3`%)8T!Y0QKSefh?@YJr6o40HcUk5=!k2y+OQ9*dXE zWCvRxZ&`&H{H3}N7eP4A3cWY{G0T0IW$H#HQr~GO2iA!6+t*V{1_Zx|t*Ynwm1Ge> zm~Ge*fyw#~Eo+c7MgIC~46-zHJP4`Yq6K5vzzoh6>vH&{3{IQx3Fb?WT@fQS`-3%} zEAdEqne>{FK{fMpKrAdgbENx2!A3_a0dIKn;BRE#KBSM`n)N>dPg49P6kFolMrO6| zFpS;c%$|Ac4%#)`p1AMHa^?evGsMFYJ>_wpQ#oUL+>*HQ4UK$p23D--x4m?lUnoF1 z_=YEJOU$Lzu_?s@<3wz?+x+!a10su2xYqg8RWYU=%yWXqcrb<_lP1`b(KX%mAFMDi zE|+gkUiZJAB&0q>$I-)8J-AxK7NFPKI8jn-Pk&eR+#8EFShPpKu)zYwMvp7zBna`` zjaHHR^AB->heKDR@X-bj4Ot@MSDmA zhIjKXv`n8Z7S+MvZ9et7W-4{ZpHgSs;}U$C*A$Mt?QWP_*kd~a%<&i`QI6*bpl8d9 zactGkJ&=Rv{ZzMHT|YXxJn~Rdo0$%S8PA0In*SEz$e;_q)i}1k%?k>=aG_4-IT9@9;k&Ncw30hPU z6(Wwv`EGfZ0oNMda9vDowXS3a1W{618nkT7@Essa{j!6a6e>7(qlByC(euw4V{6UQ z31RP{>NzZ7ksHI&ATL?7!^GZ!p9fEBqgEm@MWPRmQ%2-$YJp`&9>^k|p@e=HK(Z@f zEAYP3X4ikl1_m!W!E~da(ZnN^QyZpa$;+~iSrF7e|W_UaUmjv3(-*WFa3V8`^RXX(o*(iaQlQ5jxkPH-u}@# zJ0uacQGE599z+Ve;kb|WOGI2pkaouV<3vhB&j6P_MjzMWlWu7OxH+Hw4x5%u{T&>Z zkMM7siR;422H!tRa&^{7(Jbdt_zh60c6TYn>ey;Ol3mOe#6$L*q(TYVoMfpLToH&= zPl6|#|D`GkkasKG(3P%c`0IyDd-JQoFgg+Rl>nebj|vV~iuSh@<5Qy|m`j(8PXC(Xu0vOa zwZ*dicSMkFBkXJ8&&SbC+{O0fGiP&9GJwGP$|%s7k65w1);Y|G+H6#}BqP0p!Lzov5=eKr5>IWAW-J`yjSvW$7Vk_}h z(peod0d%IRqFkTgzU;0oK^x(E=?d3xtK9(DDq|ffAcOT8&0q&l7Xl&WLfQtQvE3RSBY6bwS z#_Tb~R)aX#|Bmx<+fp|5ZvJ52A~w{zUIuOHMQPpxU-YgAuu;eq3njSa9)tY?b! zir^YyJ-U?)c&*t+PoFuy6uy@=;09j+j{~lr`|jvbI8{oNh3>?~VUHH18emJ>W#O!P zf-2YSGUFW@RjaTxU$V?!IL%$3Lqw)|-7ZGC~4b>nrsTb}L1kT6bxF5a!USS1a)@VPuWN zc?%BiG*ga;SYr}Lk@xE>iCEo67Yp=y>i3wFk^z*b;ZobI#n zg|AO`|06;uy8Kg$6+#O#!9jw6HMU9_CPFdLi5nxr5KDmd<%$+4Ly`u~aZ0gXWUxYe9 zDCyhNu&gWk0Hb7I%*jeaH3YYH+1m6je74JkaAr!>1{JVhr!#r@P-$eiC%T%IHmxiq z-87Xh*4;lcl%z*6j_D@HXLqATC-K+ej1yiI-ymXuDVJHe5EACSO>i@=j71Fx z9ie2V6#sIOq8m4nm3;EbuD3SOXLosP*iyQG0kT@R7^vhX#{EaC|PTf~S zZ{OV9G~ABK7#j-LM^J6yh=dinw;Dk5n{SYT_EDp`)-f`8^9r&Izdm5CDexB(&(F6) z4zBe8=RoR-jkw6RG}o8zXql=-!%rN2;`WA+w`Gv<8Mm8_z%Qr0DR zCa@~dd_%cI|uauAYkHRMucqL6}S}D ztE^-cZ1W^@8vI>VctmO5QS3^0hAjumCEC=^92XTqM#hnCRvV!Rj}5}KPL5FYG}DzC zAjfTgt+qOEo!xVn59Nj9*$I={_x=~N6odi9?DOJ7GNBK@uI=~;4?@Rmo(RwWyL+1F5Ubl5~`42$J^;v7TFfyU6xO0o-9*Ee)4Vgh>y z`}N9-(LEVif!Z4WQJs8QC@gl!y`<$GD^Hj6nE5_udVOHK{$JdDs5@+Ljs~f#clTue zRgg}Pt85-G;(erNj80=6I>w+jK;A_uNdLD8ZSKUyYG0p~y*+PT-6_}6rY%dfY5hWh z9i+)-&3OG6n+E(s%8ZcsD-sOUf(El1tafvx=wmQJRJ7VPNGi| zQkVTv{nVmFk($p%@{PSnAsI*_M(U4kQFrY5)RC^o9Rh2-y+ob5yIXBmNV5TlS-4yA zeG-(`z$XRXH?fWB!CewKBz_lo>70hM6(Kh{KC#_vwi2~5e!P#IGB1jb=Ctl{g-Q`A zPm9vmUW=KHh)JI;y>$BAC5*$k31SxESb|?;{_nJmyK+hq^*WxsuV*8Fr1e9bhE&=P zeYM3Eh0u2xwd!T00+wh{!k6}|G+b9*3c7yb4(oyLT@0pg{155m+H;G96*V?i`0j8~ zw)Audki}pr*A0w6n|H&1;(s46VX#}mW7r}GDy@u@Ox3Txy|aM_aSqG5Ub$R=1t_)4 zR^1p|Ed~@OzE}A|Y;P7J*s&^YcHq27s4VpX4hqZfTaab=480e~?T45!vo^J~wgE;u zO1KToYV0IYL4Co{@2YbvB`6rrJ^cd7hhIboubAuECTrb}dBPI0Yh~dAtwI^1^wo4> z-Ci(Q<)9C0r_>o>g!#~_d zqNBd%9R1muzF3IAR^F$Qz=8}NkdSm)a~1|*(uX*Y%ulvoV9w97gGAm z?|}*vM{Jm0|B`)V{Yi^RUNu4E{BixAHJU-L*Y!eZrOd1Ebt-`rQdey54W>ckt*bs% zyz?oXx1MX18#BP2{sOB+q^5zw7V@6)0Hw8&LXzR5HsG5>M2cPb^4-e9pV+x7|Z3KPr$CU=$ z8j&!zTzX;;g1>Sny3KxTaL=lT1>Vx~dlj_$>uBeN%nSs`^obx++V>bAok;EkL3QcF zx~5V&cZ<~Cm|J4A)g|06;zRpGIAR=A2JsJgh1||?CK=2Ne^-?KnpPBkJ%ojJ>5=^| zqyQ8}cHG;)EiMO4(_ZN{$r_ykjIvCTecb2tyc`_3QdTmm{flmlp(PJh zPWL!Vp_n2$pj6e_)jDD>Gmw&{k%$fka^u=?y+|?y5G43BTv~pg<}AAn-Zk5cO?GDu$)X*58LppNFbK@iVgh%~~;B ztcyE+XZ@9mCV&fG&qB*fp8 zcu|@tP!?_A<-1UrmBbl<>z9x&F}$&3jc=j;Edj zvvPBHB|qflMtBdSRuE$2?88i0bH9vWC?*oeQb9&2`%wZ3&ntUZD)i{4pzI%*RbTgC z*M22MfyGUCVwuXjjzSxCwEq<_U+tqIlIzefTn~bkK3LG)(8MaodX7hDgTCCZ4>JDb zV2F8#E;+`P&bCl0)-6Ez-(k!5P|Wb#)CnH-!(l(VdsNop1NO6`i&MvgpTh8g<>SrL+Hu1zP-){h5j zm%UOQOeWBviyr9sN3D5)5t7uZ;zFOC4Ia7i7}_g>Zqo=bK9nNoY( zo~Sc{wwCZ%mJ9)Vgzxe5XToFG0m1X4hFXWD7t1I5PsB|slh;{&iR^UOaPllKfS(p5 zx0u`?3;6=NV?IlH)(nFRCIh#mhF#A3O%^kv@is+B`zez^KA6fdRy|(2kX`3u72}wT8fd_t1_8;n{tx zPw9|D7SRxsA7oZ#e>?mMN_7AGxW0!+Sr{!Q+Z4IILrdg^Ut~LUgvgQkzV(afLOnxG z_f@5ve?xBC0O?I142Oe1umRqf(s+|WCw{~LN|2?0cwr33ufYxWoi;NX(?f4-S%0On zNCK0MRaR_-c%~7>Y1~&`c#PscsYk#l`p7qGMI8?Qlp(tR(xyU2@1s7%!|82cu=ZV* zxz7^e zz5M0Rnq00f+71q{rcQP#-HQYzdbXx%N2R?>6FR?(SQ2*SQv*#Cmt}P6u7_K~Dfb*d zt>3k5ee3x8NLv)QT3R=dqKXd)lc?eps-Mnl6}0B1A;Hs~tpZ7KQHE#Vq{o@%T1`km z$+K~^_qKmJ6Y`1AE)#P~(rS$%m$@GZZMEIn{aY5KX!3f~#mwv|YL8sbwcQ5L@a#^2 zj{a9@KL7Kw1hLO<6kxnz-vYqEl7%3u=H0zqq+>3D&X?};IWx@VE`Kwe zT5?&1|K|$xKynF__N)&B|9#wTQaTJl%@Day%Fas`Ae&UYA=BF`aa>v08;*_aGyQ$X zlpCN<=tl1*hAyM;3zAvz4&YPM?-dms(gb%I&|uX{6O#&vY_hWOqy~>XI@w{bf7@I^ zPfN+2g`GzWHVE7~WisIFC0S+C3k`ELl|`^Lm1BChEz0@GaKEse#viP8vq|_q=Kq6q1iei2xkt*9*5_gX$lLjH-5T4 z#9MeC|HQ$`s_Ij%S~qDQ$T^6O_tO4jfjoxtE9TSZT7qk^5tdArHfUuv*!r7Ht>=Bo zPv{1}w_qGB2Cq%YGJTV>B25k% zVSww|ot?%0B!F6DIi4KL=s|bxo7Y(4d!@W+s3V$YdK9jHHY{(T%8CCGScO2caNMNu zY_fYR=P~uB^Ong}EtCN*2RD7My<r3U(*VKIlHm5Z@MAG!5tfD^|S zfCiV&ovuq+ZjSJHxkHC5ak#q!vRvkGu9R-h4eKNs;j`jQFj9G9KujRe?63=^$b`u;JaY@9I<%z%mBXPDF5DMYnA0;O?4$8LZ zp1m+}a-00C`WI1(F~p_Xf=3}YlD^I`duUz=AzazUu5Cr!`D}(fd&jaWF7{%Ag20Nb zRE%x9_=Oq#bp4(fU4DWOV2$0Vxew5quYfi_?d^Fzdt#+E%mys`^ zFuF6s>si|X-6OLxf4W%jAKIO{ZC-$@&kfkRsDE!j^^OQ1RM_`%_JR8a6*$z3SEi!v z8WdBCdc%?uvyYV`wo{=}88yz@ksun@W`d(f?)#obT|JNvqLJjMAtRcV03-{SL(A4g zvR+&sOW^U0*qnTI0Q{EuE;5Fkfjt`8^#KsT=zVuWB?65&r+J7c^&^)gb>eeeOu>1c zKu!ZUIl9}GfpjS0;Tt2Zv|3DH&A$S?NJBZW*7h<5bYv)wlKr^+u1G;%fMeQejuL{v z#_wQ+D_#oEKpD}8!4^4@bK^+clG!gqyp8lhE)*h1d)lV&Xa$lk@=sotAI&zx^W_XM zWIoo5k#?dUyOxV7$X6zK-~!Nz<*%?8~Oavj>INfg@X_~ErACIKY!+&-J9HxM~zQ?;^}#x^NVgg(Bp zt@79@av>I2;=W~we`Ep^fd4$uzRn5(mDssk9JqK&9>R}jUTfFtt{L_b)LT%a?EggV zMSoFFmKt~`gcYgf;c+zO(6gBXzIK!;zxni} z7rTNL7(~M02*5KU27r>^_Jgf4Dp_=T%<@baeD;C)1EOubjTnZe%R%9J zixNk&jDQhdYl+Etu4CBFEf}ClxX;bc`YSKIZl=l3Dv(d}XbDADys;|iEgfNZ#i!&l z->95YbcEHv6XV0@lSL6_LB^f0B99;?2r8nlX((dao*2E^>1hT*W!wiPTvKMpeM`U_ zlGEH6T%4Wcfn2rOHr){{KbM)5QCCaSSs2hI%AA`$jI>9 z84b?gS1&wD*ikt@8goFQXnHcYE%K=!X*=9|WHi3G96vnsUr#Q@z#y^^`IYDD5JAW9 zMBkFs>K+;;or&|x+QbrXdzy}${pN%nK%{<;B)Y%CflW3l?dv_VQSz2+B-xNL}>JTzrJ1gw^&PkMLtyp38-+L0v&=Xuad6BHs80jAEdzmFjKdte4KUU#C4pYU~ zW!LU1J3qUZ6bB-&*6LzZR~#|xVZ=LpNw45{m0}(Kc(YgaSga5-_!QM$rBdgv!@LK8 zKL6T0y<&I>N(k(T2gUj9brh56XD`Ob^(!aHmPQ8F^UEk4Wyn^rWpFJ3RI;1|7eRCR zm*8v<20vKG&HCoQJ@F=DBG0SWM zKd-D-&Revt-H}JyPP{;d;K{r#D)?>6I$kQ6NcYgg9U^8WNDM>oTtUy z5KlqB?SHU7>y#$u;H&o1ExFWRm6ShZ50cD^5D{2 zPJ04O4a)ZoqDD(FeZSd$&}*@A0`uUoZ(-#ICOS&b-`Hwl7XhY+f=B!*Ct^FIZjhH7 zH8t5}kf^_+KRO&?7oL{5tAt%MMRGv@G?p@o(Vre^wj!p8u$(Ia>@akQu!?8Q>_n3s zf;W3w;nIM)1)&g;=4mJ=s*02oJdmKzwe)HiC(mNFHZ-f~B5rb2bbi!-7tv|2h_6&n7Z_ zYW7_qlSPmxM>?8�)XKiyNXez0n*v_6kHnR@Lzj$s1rP!|{JfoLm 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

    %0;1j z*hKxt6*%bs8aXnVy$V_XtZk}R`Z|oXl&K2=?0%n>aPTAy2m0V*jZ;XgnlIc0p~%&V zS1d0`|H+Z$uU=Il?jf_`!k&qZ2$Y`N>3GVa_tP(B#<@YQg7jH7oAS<&pBZ(DMJ)CHDT#-~t9KCf(PxiR zV>+>iA`ds9Lfo9O?JM3XV+n)-#ztIj+9H*t(55(7P^j$2S8qqi|e>b@CT)%&{ z;hF3;V-#kImF1#B3c?j`I`fJ~TZGFnb(Owa8X|>NtiIO4C^%VDloW~Ox*mdhBAG~x zYox5iQoPlt3T7RQ^*SUtjg6H`6Ycxv zyorfxPZ8xglCtf;V-iu!;@hDW(r?dSTCeNjxWu$hN+rky%bcO3HDrh4^cK zd5t7_Ly;^yQkLe<$Gl@JKPwnazrA{P@;BE5FOAjZLbX=CEeNgj-#{VA1U2i+lrs#k zB%D7_>(ePOB<`n5q*vZp!^_ znu4ZYDlzE#Z8qdN`6ZXs^R^%(5}R_kIWxna-!}`nkMzIGg`Zo9F1sn{c~W|`va26L z9Bkx(3zod$F#)cuv+yTjKK10^(oiJ)7N1QZ&~Ad#}kH5CAE@i~aK{0m_#yZno6tN(s_lJ&y!_joHo zs7B9C`C2NTJ>Ox%E5q0FJ-rV;Cp6^U$d>L#^(t${80>M1AXl)40WsluNuMOzKqWXOlNm*{g~_s=v>ExN~{?2kRJhEmOK+Kh$Xf8tA|XTCaN-m^75)H zWL~Un&iSka#t_<9pqz?}rwWO!;a$gc^3lY`E8k3WHKU*K&03&x;~QAy?GhAqMtaVQ zXG8)W7thHZ5ogLh*hSzQ$_HAZePK@gK-Cnr-ABtdd0^}AUDF?U!u;`Uv0DDg0ANgs zo!o_pyA|Td7zd%kNDe18INay9(@X#*E71P4uZ1lG4GD1U`Wc@>n`pKR?x^Dz1$dtl zkAchSDg4a#jbE+FbTgxKLI&(XZUFIijDjTVXO|^!y-fx{zjaA9$;_#|Bsd;|Zh3&j z%o>*1eIZnNJThh5fCP(Tl| z7pS^An1k_WS}7OJFLq7xcT}2{L_BMXfxxN>xW(oqE3!p654`ylo{4v`oZj$+ zdKd#Lf{WW*`@hjl(8kITx1A%l_v;Ao#hJ zaNqy-imwwGMOK{2|9~m2U>z5Pu#B26DIx8~Y%@2_p$!m&WZ?j=T%3Mozn^T)I_*g~@CJL~PhKO1?769EGOc#56W9XM;`e=HXbq3oH}- z!D+?;R9JiVHkwZ#3f}I?+h=kcJohf`h0(mhJPB_*fn%HIkXadZE17nX#h>?bxRWQ2 zJDX~uSj+MmEfIso(6fg8I|dw$?76C$$TXdG zftT0m0!p`+V9axARSyYvMt_{Vo@BBlz)9koG?o$HW0iE!^FqG}(_=(KpWH->KIDE! zh5Y0@Qdz+YyYYD)U&9(}CJv>tUjMiOA`m-XaigyxkQ2K{-k+&#$xkt=r0*PpfLAvT z8a`m?;PLlTcQn6l@zvpCKNfYF7$N}2Wk*E~jt^D)Z0h(r5?194>6&WFUF z;`t3*h?Ne}rq6c|Lg9nVGrzA?!eN9~I}wS@M_ex-urJ2-@Vf)DTZ$5j^^l zp>Z|ekJK>I-+jrAOa!|yg>;}F)&|A!F<6W$2TN=RiBL!7zko(_khs_0n8k3+! z!j|6J6;&{(0$1jm@xaEb^b8Z6-6nY&^S@vH3wlnG`Gw+a#|$!2lLiNVY-MobwW1%1 zC-iJObo#JsdnNOM^{~A^pDgx5V4aKOev^64I7->_>z|`*Rnp3F+DNW|H&Fh((l$%| z!~zG&VMZv5YMu%Mf8VGgapYnElG6AxetK793u~bMXUbd5a7{n;=V72%(shn|{r&km zMAf=?HbVc%R*anR{4Ak{G#4&mq|C}x9@p?HOI4NjJdWSVACx?z5(J0L_?~i%BzG(` zmYsR26wYNr{X*cO!@_|+mOecEl*nWXd;seiu9a6eC#(R|uHTW3GFUqDfhSyM?m|?I z^+iiwvFI+3FU!e$cbC#RB7E!TG*hOAjsHFGXAh>}$CzsP9zZqDvd6vh@B=$5=kb`- zm`#{w<`W@)ZF*gQwagE3oQfPho5#l`U@pO|fUK&X)kY|i9`CEUW z2lSiAZ#>g_@b2hvg_Hs!w9%yrYf7^ZC`+4%l+-IC8;rUY2h)%C0vfb+O$e%y|8F{j z4Jv~r{CI{3h;X&mbaXKAL+YxZlD~2&`7OqNy8J2K3R`_>s66PYwR$p*FI@uhlfxey zR0E_xw3&b-iTgydqqPcwlJY`&ohrE?Uadr&0HRxrezl!H%2&cm|gldxxH-0YVdrQ2a-KkCeD3NNw7dCN^f! zweEfX5hM`G>j_ypsc8oHGtSkwMWiz_S#-A$`{iXaYO9#tp;!(!D28&Q+^YL3R&8ut zjO*VmoU5@^y>P`Id0@^sJOaX1ua(xUVJYC1S0-5JE!8KvG){}qqjn4yZ*??Xy|0+X z3fd;vQkShvqz|f)XaRJHY`e?0nnNG9z@ZdzV2ohsnpsviez{LlR?)g71Jt{`*) z4OJFGUhDOKy<@4om;9+4Zi$ymX9osc`@S}MVsQlvy@%&acwUq>=#%N5*h_*QB$CF~ zg{(H1By3xnujZiE2YLyuCyd^o^gtTrm6I z5NBq8rWxkE@0buz^kY5{(p#fLUo{icLW`4#%LUlhkqn30d?B>ymh*`Gd2TE5B=xd* zqc67nJv`LIc^EA6+q_7}ym}z2Af|Y|k*IhZvTXilwtkHQo1sS`wcEeZ5Q7!}5+8NX zun0&U=;nqX-pYBvW0*@Q2{lPayp*^}Br!1`D9^!^BXXd)Q&`onNggty2cc;H<@cGk ztplA1%k}?j(@^9uNxJ;%Z*3(p$17T5M(_Kg?{ZNFpUipq=0reOIZ`Sqs~z$Pr18uu z0`U*m1dZoYh8u3ttjGeqlBUuDu4l}x(_jk4bRRGPmptzNUPD{0`Mj66Aj)HZuMJAd z&EPw`sbmnPF`$Y z-kHa;`B~Q{+4yubvZHv2{Pg8c<_#OfWs?{eTCTA{GNkXhu;Dmi6nFPo5ar0S1a1>z zO3+=paNY`KIV3WDoWPAT^MR}oIJ2cs>6&6X96H(v?-}VLMzw$K1xx^Wk~ZzNPF94p z!NBHPHA_nDA*&zgvYv~b)=QZb-Y~;z8u2=9`T6d-XvIDA6hIHy(a6`B zumDLB+5W`lHg&~;>K+(uPK=A3Q4a>XuOzCDm?s0lhfjLa?CFLIG<>vc=w+CS0tf1@ zfJ9=+9NH`L{g1Uou%WBFx{w#DIJ&&Bwl96x)PxmLs6G4(mMkT$SoD|h6PAG;YSGDR zkb*RY46tz53`&2a!U3YEblg!+R{hDCjsp{0t<#;iId>Hs7c__H|3j5MF9HRc{iKL! zU}7}@_4e4UaxsYm6GwTb>+uxRe~FTQk}&=?^PjxC?j2*3Ti{4YIBMQY<-27dJ$8bB zCO9AQ1=tO@+R~w2Qf#Vtwz98&4JbLogY5{*a&&N*zSQCS&}Qb(1+!JE*B*cs8*m5 z?Q2Ia5u~t1_4S%g*Vg1U5u-2No%-c8y@2nedA)z#hYlDh(2B}N%54n<7kfgs65nBO zbr28-*UO)bV>bjufPZYF8vRCS2fM`6bdo|J5d*~F>6KD1G`xn&t-wbmOsp2en21Qx z-=Ujd?~k>={ah7$nEq3~wdpd_nlnIn>p|{?OLGf`(6(eB$+OWF#N48ndwx+S9DdY) zHJaE%PuJxyKaJ1+`!gi%*+R24?EUNfBFaa+G~IC0mu3K1s%`)5OZQ9R3ilta$iKBb za|GQYPF`u~FG~Q_5C7;{4vyb)pE-_JFZ*}W7NWrVCfVYbbZQ_t|f0%C#0doYWw?7kC^o^>&2 zS=&eIM(j;nET%Wr#Dn}HEN%m2 z%b^XXG~NVHw36CiC4ztDTlgSG0U*DO=0oWy?GVR>%v3;caQhj{M?DgQ|DXOQ=ZH-+ z-RGQYBl{<&Q||X9Y8I6^MD^6}wmHYr2upjTos1=M2)`R3!LhPgei&p8mdwi0MyJ_m z2vzh+Zl{>N!XFPP8Dz=r$gVA6Io>_!_42Z7RPVrBtaS9Ff*wu8x>7>2-5W;Z#~NK4 z*&dQv(L|?C{$R1; z@3v5_c|a`g78(psmNwVg0lTbnZ6tq+F%z+k8S@nbdprgV&ifG~nz2!4an>w0~>K zF2xIReyPnpQ_I>TG#TQvKV*yO5pr^fzn4Ko^*vX@<)nm{+Qw!b28&@wILTCR0IGw9 zi4NdWYZ&UtN3LvNq516qrZjWDfF&Gs`gh}Mj3hV!p!L)3BgT=YvS&Ih)o$_66_~c6 z3`G0UIwbC#m@b!trydL&uXA$jDCe&1SuDmyx}M6a4!Dk?Zg!Ev7V|GaqcPR8xdkT2 zd|7?HwysJLkW)Qty1q0lA%7l>M&g(08e(c)d_Kn8s75N3AX93_?PX~^OaySL)MZnIhWubp-eaie3@La35itkddKU_gGdp8dQS8&v%=Up3V(0AtA~0s!Oo zu*Y^{wgl-q#;wCjq0q`!o-@s33=^rZJjCtH(p!|+jWSgd>nu--!-)1|{=n5YEe?!^ zJHd>ZRU0rT|GbL9Gv9O>lHC8()`4jPQa{Q<`#*B01+4EYeBW+EK$c5+- zjmcRHMlaczwxFlsZTeum?(f(qar2S9rriW2aOy*-SXX{aEc}ggIPNet43@prr*xz| zp+HS1^@4(*6n;67U*?b~e^Fj-ZnvewvK#^T)qz`Si?>nl0?Jl#Rc(j@X#{#rG-{~# z`56r=HW{yp+C`;8SS?P7?fvskr$HciH~jhHJLd9=7g+p@Zi9r^h%is``3Ph{Y~f-e z-2K-k&6BlN35L0h7l3OQ`UR$VMd%L_9j&y^ct_p>G(SYQ?AoR!ZJTJx-#r67E%*_O zi)0E)-R{GOwXW1Wp+v^i$l**_L>yMr&FYC%TrF6l+9-H+bJw=+iZdoO!rD`7V)Y(L zhx%jMZ{nBM7|DlEMRuD4-?zti)%^KVtBR(XP;E*7J%l0u>BlNnZH& zc7bI0R%cJ+l@S0uy^i&VCT<^u+ZNVRXkicdpX^Gv8d@=ua!fyz+ZkOKsW*v(wjs5! zxe^GzwUATM<(dC4YCUXQ(%DV>C@YDkl(Hb6Th4V64%FOY^*`YS9|tq1W2@H?P7Pa_ z(3#?8=6oDMnbT$3#IG9`)nUjn%bi(+CDjV;=75Ect=)wKBGH>-V`t|bpaUqYxVf5? zTLUh!>`mFP&4(E%z3l`6=(nkYK5rHU3$mHk=JpYs?bnK;LjJKbqUP;)M9$?$Bq`0m zrUkRZXah91<0J@C6!;sE~62(;lVnMLD9%}k8Cz#stl#k_df zYf2{1%Pfd_xMl!2p0JLwr%W;s#LQYNnYV;20R78{9i?Egva*SR{?GSWuTi%m9{l0Ch0 z+AAXEW8qna)a#%%b^nXFEb`G3cu~?PDK*gXA+coXG4j7|bwJ;cG;g=1_=|^7IOhhm zMAJdax*e^jY z#)JU?EAoe)4(+FSv@)xiy6up3O~W>)Gn&G>1R-LdBA)!S<^){}Vm+Glw4M>`e+>{q z;w@J3J4U=$zw zuGdt2U>gbQIWQ!+#e*JI-9uzC-xHn?nABaVPE8)%kiPNqIk=*533bbJ&Ofhp*BpnE zkPo|0`d{8Vr13`*9IZ;9YkssbYt@Wk#3LfM+L-!H_`&r5n(Qh<@JoC=LajePWfQ zybd-tU-Th8n3LcPDgefv4<1*J&x!0dRU0Mnf9_bSY>NMDMGXJHfzT#?PGA$Zw1+ZD1%9!O2+C1$`v{^}pgTT-!bk?+0-0mm%vfY?`vHdJ-UL(X(_}Tfl}? zWAm^wD^ljR<4<>n=rci8Xnm3QsK!5zCa9L#)W6am8X!P&w%m1%ZsmMF2+J;`DYK9tDGM}8sdh-O;Er0vt7ovukMiSLjO zD~k$)4QS)*uySj?EC(3;b^OSnEFlv=oS|`!;}{-9hsKNKq3M1h44UjiA~tj4 z+X~0EhF5^ynL`5T$$A0R@p9fM{FF!(xXEGaO>dE{4TBfDBN?r-Kit&C;sYEAtWwRe zCOjCnQ{yY%p3iVP&7fv4$F`Y2f6oB61nkUzF+Y%;UU=-DuO9HPZxe6u}W z*}ooFx}P2~fW|U`tj=iZu9Ut)jgx6ToDON8Yd;mm@hR^6`Yi zPxd1(Y^(3afqe~aWxkGh2+Uqf8ZeOlnEdV6*vK!zW4B<~ zBJU?+2+MG#yc=*iPjcC9G=j&LGZ*(i!Wg`cTG$v+B5;$$gF1sbpnkl*9$^#}+{UyT z!4~mDXwnnq#0vm>X62K5jQjO7$#(3=nom{>a4K1}wkz*U{u5QR#r%vB>y4g4B-)LB z7W#SJQhHd#&k|Mn+gwh>(OosRXy8=4WZ+CxL@fQ3#6=-xOt3BkShQd3*3ODvY!gc| z)mrjCA4(&-E~dbLUQaR!fk3W*)w7@^j9sQ{LPqKT$d!M^RkL$4_u}Y(sj#=tJSkk# z_g`Fk##3|^kOKE0J)7XT1c>x_pgn}XTn+km;662~a&-n;R%#&oQj9PVr(w$y+`q>Q z6n9j@7V>QH6Fq&%F&bZThL@gA#6NJs1pR-aBF&xF+gns5UNxbF-HO{&$#VvD;`hBr z;{q9~RjX?}pyyD+>aHILqOv3AOUYyn; zC_a+N#(x^>8GdLr{MFYFi}ivTy*e}v!l{mtJls4s4#@6cgf%l!Xs> zgyIwFjHE{3)M3>%3X)u${POsHT@#_9vj#rNF6uAN zT!&Czl6*${U`$R9BbYX|a0{Mr*L)r#3LW?06u#)3;}D)H$;!(M!SOS~(0h#h_0@vvc`h z0;lbSwG8&!kmD+a88TB&r5xp`ifMYfF}hK-`fZXbiuq{@OYT8asJLVh=(4kXF#VTl z6YZ~lqDS&OlX?qSj*uzd_1zB&JjU#I>V0)w51C>$rkg>FljevbdbPIKUNbwl+T zJ`o;YOt1x%dApO}ROmVjZg<@SZw}B7q^wIBbywM?`dG(M6;H(6nqgdZ2zJQvUJqwBuQi4!i9*%xA z2U*50*n~Sy?8GX9z56VhJ0CdGqNMywxm%kgL*{%JO^Usi0!ey?alBy9wg3FUyjD~C zF~8E(yC3`gsxub*{8eeA_YMRTMm%QK!2;KV$xefX!4Ib>?!c?CGK&klGuwu+(VV^* zGYRaQPIXV_yC5$og(Ytr;Ir@-) zN*jp%^Nsw*E>`N}G7Wu`urMa7!`L299px2URU^wJRUsiY?<>TlQ*l_&M|){gtk3pBI`n# zO6J&Kj86p)j{)z5yXgsvZHo-u9jeBlf5rMWvJ=hOl&yFdnGQ&2n#HYM&ClCN#3@@p z-@dy0aBM+BT;bIy3x`T{POG_?ay+N<1nZ6IcxVhHf1_3N)%WWxCjMy7z`9!afr*+w;GJ5L{A^adfK;VF@6 zNri`)018{$6r{ZK|GG9SD(`^^<_5Sh1@AtHBdc>{=`+FJl#ihWZ0pF&AqjY7ci_v2-~ zYvo|#fZ?VevO{tP#u#nAIf!I>u@0RBN*Nxk2A&Pn{e`oVJRGhqor zb?e-<(k+E8+wZ6i@<*a({_Nj@Gb|CT!1mvwPi(q?!gAbEi53lGyXVtoQ8P%>1^2>` zR0~Y}GW-&d(>@6s!AIj5wx_gh2kBIq0q;Qe)J9Fx;Sxv*&*pu(u^|?=t~~!Z6Fpet z0Bdr$3%XOw*#c>qk&cb*))BmXD~S)`{fERDzYD~NX<$a83U=pZ-vhw2K%!Qk1Q{A+ zL-~=uhtO|9Sdy-7IJkoNGjBa8mCN}GNnn#I4$6=qBfedGN!1+*wVjl6E|c`;A9;) z@EU$XSnbzFK+JuvX>U;rzevoL!A3l^iKlM-MbIozIciYh>|bUIPXk%aq1${|3o^%6 zGgjn1Y=p%TB7N8cSk;NX>Wm{6j=Q1x<18451JrJZ$v1OBf7%u+z_M6gpzK~EEdFoJ zZRzDJYU`Kdas15`lTOsz$oY@^4+yu>VihMfoDOLB`~p*pp?9#2QZP9Gx{PJB)E>OS zu@8io)V3kYSTu1pzyw9T%}*5I9lsZalzYQ!|Fw0dApm@`d~-`3)9iiDX+b`zDD!EMK!?`lW+lsGuwlWWf@U-wS9s&~ z;DrJQP`NqrU&?<;6^E3Rr)j(=>Df6^Lx3N58oW+kIR>_UwO{FDab5|{vW>ssIFml1 zi}=PLeC9e^0`2;wN}1SzQ6gmZ+}(59KoxY-(Ayosl<%(ysF!+D5l&#IN2sNwFD^@3 zofakaMZwdnLV&)+*gV@kQ)it#0rO@tT|kXr+Pm2B-B24-%F8MiHg88-qZ*)jEhv527Z1Qm2gKFe0OZ zpeTIc$8QTg)N1{+Z)sGV9EU$v@dImf(u9gZ_HEWEF6`oHJ4)q~8D}C#46HrNb;hTh z_66B-qlJxYH};SiIfmBf*PTC*mQ0n#&EQ?&dR zw;jJ@H{G%QdO!k{5ycr;=dETn!Y23%-y*(j6_~8x1I}UZn{8**=>xQ(`l~sr$&DYZ zsCE7TFJ2zVYx{vRY0iuy!!7e=*RrXK6E|vIw$1Y}trFWg7b=8;h7`E8IupK~$O`0f zF_+keAQB*QJ6)g7(y9!Z=4_!6^;SMAx=m}ys^>XD09}h{C2ahYQy3G4PIt?p+e=d+ zYG8%ApX(wM?dE4$tfBT9Xu{jFcEM%@E0I0rOtlD5G>%8+m6;hy>1>K$pf$ikUHRBV z4@K1LA7hr8D^G-(;sLDNfw5)E9sj-fpBMx(yE1ktF3oeeV|<27t4ps=>5P7kE5u>5 zbu4R7>$Kun{T7WQ$M_dy;krX>Q0(Gzj8R)m1Z|egJ%FC<*gG8EQrH6(kKhwMI)-X` zx$d^yZXS4`i9`IEU1;SP8-RM^v+EaGc`ulO#}m9U&fcX^xxDHT5=$7HAc5@qC@AT& zbx^tipDGgS=#T(gq-4{5+@pgwFQY1&j`tjPk;+b{%AVp=oO#S5FCI_tIE3Bznh*@q zMk+s+{3W*oH++eM9?&2Y7LB)fyu2rA`>baRb)<#+qZ;f<4P$eYOK{$iwhf%f&a_6_ zQK<(o@z>e?Wa)kxckHQkYxue7+t381WX9U_`WDtvqD@m66eS7j;+lY*8b5;#{E1|f z$E6!@D(YGNNLzh+%4Pu0VH+;P`)wPY>K~*A!=?Q<7XKLf7K>k%PmT7NoCAa-u_mhP ztZO2H{8M!D_48B>k@AHP-*$qQI=-^5%*stncROhl(`x99I4x+^PZu`WOY8}YTXjD~Zt1TUuPhr3w{t^?sSlePEoUlQ9x zNtGd$_kP{iXA@^DCVw|R@odM zG9b{8xm`Dj5aqR2nrww^fD^-8vfA9KsYce-UiaO-y@fAp%u&hKdBus zBVZ-&v{hMCpuD6BvcXeznxIi?e7jo5R{d2pg07?_;#u?02rXN9JX+N5Wgy3j4hV;E zqT8~Z=M@c4VE!ljAMpe#N)w8I@r!g-Qpk;ZE3^USAjCsGZ(E-*We~5#Tu^ngc(e(b zNI>H1qt;EcQsR9X-145Y0N=!S7PI-Gxohi3M_2Xv5t!xsXP_+BYQbC#yPI}YW4&j9 zJW&r=Nw-)k`!dF3){@tLR_o68bcnJk6d%MdcZ`vk8!ILF*+0xf!A=9E3f?aKF_pOE zbj^AjKClCKTD04Umk07=P*IeKyN>~e%83BP=-$-|000w@=hoVw0)jG{?FuukgWd!m zfui$tn@h+q6TL0gDT|L{|GF8>3YZOW;jmdD))awnwW4z>Fk|S|k+X+qSd72V1KEFj zY$_%s{N;L~Mun987<@IRYq-DCH!!Xj{iv3R z5oJTU!`}TQsY^W*9Q&trZY!W>g5UZj0P?`EeiQPU#VLzq18>IE=0SfO*J^3ezuV}EU{gs1XSoue0j>^8irp_rVZ;iZPtq9j zPkXc(E}X|THbk_Fcz*IR0x^(RjS-mo_aVKz@zEZQVM73ez*E<$6Hq0~Ca!9#XH-{3 z<%@IMC3IR=y&$fM-}FsdRm3->E&BsFQed^fh|BR1lUQVJhWD;$C2xA0AGYfH8=*}H z6;STP>mcI*j)tW1X-6#hiwJx?gqH5W6r)3ili_ACMna!nuwNJBc~ zu!}Tpch3AedAi{bI8+?ka(`$-{$kN?S6PC%f<;o2<>nf1zV5wx!4}h3e`8~zFO*5_ zH;LQv?M@Fo04>_hz=xhY8!0z3%AuAtyN3rOhQE{1lR0MCp;+7Z)B?gS97}4>l>f3( zo4MXZj`{m5L)hmipOF0R1A>m7d9kkPX!4-`vmTp^?l#}E@GU-@dsZR=-CiFim~k3b z1*C;X=Koa^k_y9|UflrU8>Sh}xuyynBO;r6uHw%7QV~ct!uI?R4B3+34xFa!UI*c} z)+v>8!5*T1*b%qSlX;)%?QXaWzLIF4y8Uko9ZzN8!d zELv8>KD~7k0zwm}>}&LgA3Prurs{-_iXnVltN#l58LNGeqTBBYX zoP`dPgUl&jW=r*miLAAU4#NJNyixM@W(Bb3U$tD_Rd#)vSU#$-;^4h7?2;#kVQ1SM1Fog<9b#(IA_old39DSboB)dSFxQ z-O6*qMeuHvY#)qk(Qw%0!50`1a*KWa!dYk)*>liz^bmff@%~V=G}6j$9*9ZNSP_QL zpW^)|WRp6=8b##i+=c4_M1^Z%3(PIggf2y;<7Dk6?b%)%u)bfHD%#ys3AUncg+Aax zusKcHO^-oLVh30Z7Ku;XU0at%Bu+F)vilcB3gGG<`& zO11@6Sd5;tzYshdSr*Hn#ilVs1|DE%*o&++`|)_|Oyx)0UD#t`R(hR6mJp{KYOcJk z@No(dEAf1{Lr9RM<|2Mh^H?LGk>pe3`Zg;s=xv@ez^V0=3C*N6YGI8F52Bbr2m2ZR z({>JEeSR+EwEHI(dTDG!tj@fNSrvdS+P=~*tvCh9tm(nKd5{wejE~68tbtXyCUg%P zAwi>Ec=gg$_zhQFtK*=tD}_FGQs;z?gvlhR070jG~kY;DJ(cv~W9Q$WdjQ<@>0HlBW%htkgJv zU}_X#&bQxi$PiwJ?@QIy+cqxr z4Pj@l(fr&`z0mYXHR{`CPTBXzqYpE*UF)#RU!_1q=eESCQpySDM&W(lxg)zjLKG4t z>(6R3cn%L(vY4_h_{c^~N%#S6yn*unzdrn0Du!DC@=Z|JY4LCgXo!7h;uBYO7hqNJ@d92?`~ z=ifk++Yn=K!ccsc(Nq4M^Q>Zo2`-=cz46;>HyAUB*U_z>CIWm1#lOpFQ$qv$&f`og z(w_159#C%Rl%`Bh|ZP#FC@5`aV)fU>*zv9R1-%7P%rd0D0aJfvhF;4C*X2n8HAI(9RP$zlMOlIf*t^wMTCR;-dry&dq(Z~ z{mUsrN}a6_7M9GQXO~rRUK9F>tc|^t8I?cqdhA8|M;O*d@UZGeYJz#Z8qp%VW3H05 zP^JdU4uuLRJ+n~!85;U5MYqHzznO5%L*R4;L?hOG{TdzP}+7O2^sZQc@%aR zE!Cy5P5l+KAY{r|_)ZcL3C(yVOV}%hZ1i1s5nFMQ18cRM!4HRv+sZ!{+!obnBo_-_ zgk0SJne=J0dj=27jf1(|)Cdb%LNu?rgsA}9dL9TnK&~{jn$F%oi(1(cxlymrR=p>Y z9Nn;6UQX6sbTaawm5QgXjyEB|uj4vqa+b$Yj1&{=P33s?YHcGc?>OP@{GSo8u}B>! zmCkNak8|zL6rG4chtFP_BAdzV1msNSP%$-^W-aBhDDgWj0kZq3~w1naD#kDsNwQh_=6TIk2e0p5rKoKf*-J6o2 zwgu8^z;U!xc3vJqs>EC>=P+FZOS^c7R&qL;-0ra{6ftb1`{;A-6nGPrlK%g{Kx83k zgQuG*ZXU`W_B%^Mj{J4(FU?v7W zk0=Zecd`=2?^g_KudD+8YCaKeo@Z=y$E&^yvU@|7l-R!<1BAldO?A4_ z;wdh04S`bYV>d^j8kjqg{(te8k~FG8qjcg?)WiQ@t9QZmhNwyU8^i=}6kH3RY1m=jlh#>9ZP^iQYo~jk^b0(Y zyR0F$*(wEGpf&w}`qt4{fLMke*8a*|YRW>b1?BSM!!5KZ8RIsjoS@-GQAEV^^#{df zwX?`j>=gycZ<0n^L&`^yB1beKAw%t(u`8~AUpSRI((B7rQ1IP~ZShw_ncN|P9|$gAx>E7e zhARNgE3mxyip(unra85`z^@|qK__bO-Mlc0ND9eBuj-|m3HTrU93YaAiBtIi*%6EB zt~nCOZ>PTC{bezc2MEBi0ipdTTyXaKLacgI{t>>o+JR&W_>$aPPS`rLY?c-NJ z_4Au5)Vr@89}-brvfIpz2NU{bT+Xm$2&$)*3VzritQ2b(Jutt(ot!QIw?|i< z=$NG{MCnI6klxgt?S4>Ho9sSzYwT-APd55+JeqgTYGbtRB65Mq4dnG0V#XQ~5 zgMEp|b_#jg{O>iu40Rp?m9do{@UNnEEev|JP|&8e6Eg6I=pbp|m=gn-Z16nlUb`%i zm6FRW?*&IYVT0?+m60L%CRRw(rntg3kx9woMo=21+8&3849rpV3P7gMC8!?n9WRy;k9e91h~I=`%wU|1do_vH5v zzjUWnIE4G@9%qGD!v^%HTjlLddVV z`2;aj(#D`szVdq`kj6eoaO=|rg@Eo|YJm8>YMNE_0DFMGo$@tm@TCNvU#J_ZD0^_K-G!IgN~&kW|lhOgMt}Y zrSP)vhU7iyVgntrGe*in!_rDTl0GnG2ui(55wUsJSO8t z*Quf{c0L;R$F-!?I2`2}bV#~@qFW7l7x8oK%+YWQyZstbo4!08SdxKxGVV!9|hWmU3~@N7ETZ8T5Vl%W8nA3 zdq*F2>@!P#(S{YBzC{)(=A`IRFxj=If04@*Gj*%Tc%WzfDDrjheo%236Yv+;XHzq% z_@D}U{BfWkNGFe8aKx-lyAH}xPtYaQOk0#!xf-X7yhS%57;$cJ$J*cni0jPGGus(#-e05% z(-D*q;$FgVPg6Yg75J5GXPnuEfiTO$jPxqkdSw#>+^!ddK%>vWSP*a?rDdmYr#DYZ z2Jq`mnGx03p*Hxb$rh$woOP)DWfsVQHh=o21!DJAmh>e{Xw zaS0L;^*qh8^r0k%dCtM^{$g-01*f&fH_|{bxIEa}tQhW!Crz-qO+M?LoJm#k^5_u=hZTQ|nEYN=Uz=5I>BgR$$7v zzBo|wfVN||lK~{}jjRV$Q#NM^^q{yM0#Z>cgZ8_)+zEsoA~mh;qD2qf9cgEfU$pvE z&Nk<#tA{7}lqEW9@mu?FI~NaWc-NQl$2|%qE$mCe?_4Bdk1s7F^Y6~?r5XZFGi#di z)nlJc(U(^O3i#-+@z7SA2*c)BwhE%IsfJRECO96uy}S^!o>c-3x!qCuXT2|Z%rECR z^bqD~io#aCC^ky{-ICbPEr+Y*Z5KQxlTw!;%#2TcBDPHH34jxq{X(rJ_yq(z=kN-V zM&VtvL-&Lz(?!vNte2_@JJUwkV2*Rx-15;Hs^_I!z@gA5f=OQlIh4e&a52~hrU0&n zXD$%DW|lALV#wSPLpcG&x_M1ue~yb1QO4D7Xl+0Oa1ZPWfYGyhe2b(FIx+gtV6lE& z5kEQQx79U>&J8Mmrk(qB&y|`1o5pJ9cg9QtEyB>T$bU zFRQE}i^#${1@etsg#`J-UjnA6FsBRiwjFdqy<_)N^m~RPdU-*ozVnP%XvKS-9@lOz zS$n%w!!jpc!sk#}FaPAT$VZM8HU$Tpt+G{S5jGeiriRX*#c5AUOjO#Xy5(ztG!$`; zzwMt$13RWafH>Sva!=ce!=V__2jP#?H+O%Q{C7~N5jJw0!s%ex5*00AfYj=0%q$g7 zc6|GVhJJGy8>3x%_oy`~BA%9sruLr#3_-?SmtJRq1_LTm?OA6x-S=?@@v--302Yc$UxIz}k|M}$LV7+gi^!y$?gfEHXVD6z8Q}_lVrn&k)!bkVBBL*9h|2vx z5hlSCDzWQ9BV!De6pL7FjO363{uPI15y8yJTB)X675C_u?Nd!}Pz3fv_SPmmL%#${ z+|ll`QdtK74!xV_vnfY7wGZaaID&eBWM%f_9kQCkdlx{n-y1HgfahepUC6%@ow(yx zQrXPKE5IM96!tR9G!d;n+EL>3VR@j)vod$V&@)+0$Hp)OgrQg}@%}$1MjkJ8Y}(vf zIy*TPm_)qG)7ChqEqw>}SmyY4=R=+b@?)dxpLVTI2gL1b7fFZlVUA7YOBEE+*~Qdj zr&&5%%kMCzJ70Lk(3N=ra<<0(Cd1R{xSC9DHdxfzzj|=Aes4M1&fZ!yAv#USz0J^# zfe00FY|62Xbx0kKw;PW` zU^UfKO26)DO?}$sneGY8S|@{&|CiZQGaRbVPnAaapMYdKWg_kX`b|HzCF2Fjmg}Xw z^#eMph3T&@d4$9$2

    p2$SK%wJ+zF6dd@aPH!rw#F&?FtT&SmS|Tf7q5m{-o$(Ht zNoe$x=@52=!3KHLx8PP((K@aM{o>A~XP?0eXG}R8IqR8fvp-EhDZoY16slH)Rs%(` z`{)5)1_Pw(+8JcGgv3Ts?VUKH9{e^Urtd{4f8EyUC*J3kd{N`rGY=@$tc+JLu09nc zcZ8^EsVHtLx;%w}s)`rtJl3-iNJ$^+tgT=~Z;gE|PnnYqNtD1ih9ikZMEiEChnAi* zaw7FSBE+$wPm5?JGz>a4;+m7n7~RP}7TDiLm}1VTXBWf)xxU5ynVE$MVP4qxK(VT1 z6SkfFtP!(!gi^wQ^9{m2+-e7r{GJkscslSSf_e~+KAY6^Bs6OI$ajZ_yapOlk@S^Y zk)v#O4zLx)S8E9w)@430eORr1&g|8hv#k(Z8WsyXusq(qpJ+iICm1P|h>Jwnu)o6~ z9I{r+s*yoKApxIG&qszfG&dT$K+spfi8e!JIFR#_w`Z*^m|~RR5x{oPI9_#1yc9?iilUi`rcRW+2XS?&6j!trBTdAnt*3a+-PjCqK)+kUz8x zN;FU2W1~A<=2Pfkn5(l?RrNovrQ5a>yoKO^KI;`e;8NraqCoH)5U3_eUU%2*$F}?Ya$$E|R7ZbT2 zpkukL_-h1y>M|I-LtJGTB#3>h7SMxN}kNtOZx4jk6bj*kcKkRI5na zWV3cUVl>*Y!%x|6=y4sQWgdUDMx;+Bg-KXp|x>XN>E&kUgTK@29 zJotbXJ|dJ-gCqU&13Uruz%3+w_||u&4MkdYNc|r9U z$jL?n;>TxtgBvJDQoCuVOsUxP*dtwEv709!$$9|+ji5UtUGo`D zJNlN46JNeT5E-;{M4lscz3_eZU35BM5?s=74tjR7me{--rVT;oxzEqfgBI~l@+wov zF}~Da_d6jBAaP--3GXIxQT@WFwD?;rX&}D5d#o}2(M7N;YifP;B!>~ZBH>pFSnqQM zYpD5(9@H$8gV*`gq{ZSrLOo`k2DbsWX;BoQ`ibh6rc8ee)Wc9EKy#X&-DIF`6+;Om z$Y_{a`{T8aVr9 zKLeTDYWqD-Hb6H>=Ba-Pn75#%_<$LH$wLr4+x=P@VWpKw z8p5p3DLL>p@}I?Yxh-iwUJ;hZVfPhk6C=QsJ{-bIvNwwVe2}e3(^a&?$l|2oE;K+= z^(^WoLb2O}J6WCpI%C}Et|o$~0qaGC9sHkpyB51>ONaB<4)_EH4DGt2LT^+eBoyk| z%){yA4QMFunM6tXX5KC%ZH?rlqKgN`9kE4zFt`1ery9#A>QrxuTIeX_FQqh%2q6ebA}w9ym9#MpIL}U z=4g;hvfY~$$co%$mCOD3zAeBEe^*Ke{Hr~R38h!Y{ny!k4*Ck=YVM2rbA?T8*--!i z!Ovxocsp1m+orw}xBlet> zKN5mEax>Mt+N zOiW$h(m9+50fN7xhbYBokr2V5_kjI=Xs8F2kbOZ`h@_hbQJVXwYMkt43y8c#dI}&s z$Vd=&Ikg)aVFL4^Nr9||fDg(6Pfu-gy|x zcU9>O=T)uGI^K#A9@dmkl+LSf!to#OSKJtH!XN#KZOb6-X_C)}Onw=U+TrQXcbnA+a9^Cb( z9S)&Rw@0|-(^yG9IkXCwx&5{6KO3$Nm<{-8r#J}EH&P?t9!*;))$65Wa}|e7Ve7io zd^|R2b8fru3AlgKT1h+6i4j{gjb*7_fhIoVDnvTGKhBbm`t7_ZJ0+e@evtr(28awHflt}SKQ8eQe`t0z{cXe zxLz)}BhmBB%{+cVAnY-Xa@yQqE)pppw$@}#He~`}Ph;8deU>*VP0Wq?fDScG+HaU# z&=3#P=#pVcP;85<p%?kX>*qb&r(xoo`9uC1FnhQ{RNqvgX7TB zv?|%CrfgWx_~}*2SPj#%ZzNQ?Y0@y~_ANRqu6~wY)1JVL zOfrOJGWVoxkwG5iGYieq-^xRQ0 z*NvJ@w~wfT=E`!;NhXQ6F2Q@>+nPIwz2aMWPFCp>?=M@Oihv!#8gS=p;W`dluoR+L zl^C{bqv<}oapZUjBjWz&RoJJr0w0caUPCY^67Y$!bbPV$v-B~ynQu)G8(Rj?fw6t? zbDn+yGlG?XCb~+*78=;AEFUyaWQsPhp7NT}iCpd|q>}TEg-d9>4-;bYuB23nPK5e` z2EUg+L->wgLPipbIE)q{?H7`ESoS1OYTTm`Ixpr7hla&k5+&dNMr&hw3_cO^^vRHY zku?Ql`r#U?WytLWhdXD4leT^Wgr8sHT$odmkGmYuZ2SS@x{%Qy?Pkr36~_#+oBZw9 zz91DXAbfD zT2KK<2;6Sj+tPA*5-{A3&8)IRVE_=x*1q*L$|Z6{Nrfum600UgEL_6{mV7_kYvbCz#Aaxgmyxa?`QQ;Dm64#Du& z&FLtCXnY$>22u^GA7Lb@a;uWpj!j6IRFaF>bekG(V?C-a^iYM6!XDPLxs@2Ga|o+I5MKu!68T*oG>u`#%(Kr z6k{3A-1U8rJqTGy_WreWfq{UN-ZbPCYch8AgxEwH9zM5Tufx493~=mXRiU#lpaxA< zs@X4^wbM}4OLA!q?T4>%!#)xMQP6KnY+FWrefcq#zE-Vb3L+1SJ~&^4t?NE7MotE6 z!J+1$AaBD%v!{+pM`R|_In0jRyQTVvf+_3nCst ziX`sW(ze}PfW<`arC8t=QInm20N~hyDs-O8nJdHam@w3ixdLqfp)$6laU5?=w9;HHj($0R{1|43YcU$ybreE5_a&>+7p8reh44h192EYCi`$CNb zsgc9J3WTnS>ev0!m@7@Fi4m&&VelohS$ub13)+PLC41ip$PTr(Z-=|o(<_qo=NbjyoUhJWx_V;sB^Ipd81qE`YQ%3d_pa~jQF(C zK&CGZ)|d3zNuF-wvJ0t5QOZd2(N$PU$RKKu#-CMNW)U?oLg~X`Cvpj67hKSekOsT+ zwsKRILUcLoJ}^QY!$s|MIAfli2kVH;d8NGo*FSF5O_20m7e;z+%X=>n(SoepCB zH2)ilz4_FQ?9PuQ;0Xt?0E#~p?|wXgE${r z`KN%YKAsR9_wavvu4!3lzw1F|p6`}PG~3pcr;R8A4^R8-h+$?d8K2T&3f{@szRU9_ zgA&Mrav40umBA1=wMDREgO25%2`oR{E=h6JYz8Pi8^&x(a=*u<{)|UShnTP4)uOF9 zmK;WdVx_Lf?-XFru}hvSEo;S081v_lcTZvlK-zA^UpB+wHStWjK7>iH@tk7-MK@uK zuq$*u0+^kZDb{Zt?u*9 z%N6b`aTPr-QT(Y5A?R&zuh!9A2Rr4jU7K6$bpvAe>@>Lpz6*PhKWrAbD5MiMPgxTo z^qvPqk!7ef9t!7;sWh2V0UVR$A;TchL~&-W+b!J?@Fs37F}h~BKgkGB8(AEHfzkTU zp9zgsdW!6T{HYdy-)TWqPzeS&eZqhzTXush#O>0TNUJ{2H84}o>CY+(wBtphe#f~o z)eY)3s9#qe5*VJg44@nQZW}{K(=NXy*OMp~SlyXgYSM%){r`;xkVv(+kbM99I8#Cg zPj^|M!@NM-#$ zW-;e}aS07{qkyLPG}R^~BF9Q-m~9Ff=Hk$tuYNDcobmBQg+u=Xujb zXI{ORF(Uv(%|r0zsHmHTp8Hwi+PMLF68_798W+ z#WnN+#aSy#pP%j7YC2v!%3Un(?|fXG!3yx5aLg^aWzfVl0U%8Az~7SE+7dsIDccXl ze#QWgY)G!tk3AX;CzC+#^eV?(%1WIp(%ra&JmskACQp@l_YA@R{tt3|IP^8s0|OpP z(t7u4JKgc-Y~Ncf+T~yco;Y`gmug3>7NkOh(MkGtz9MIMdv8(imn9c9M3C1w;}{qh zRE#9&=lU1HeTJUXZ0}*EJ5~hP^a<7TVDkOE0g@?tUP5!8B>q! z>Y2EU#Z8CU(oLUpr@|* z?Ri3#l%a}Bj+V-k86YKA>sYO=$ZnkzPK?eF+U0$4W4cfdvFnp%WkdRSC6@NvMv@}c zP`h`@Xdo^<>5oZH>{C8&x&7h7h=?Gy32SLig#=1?n+E^uUyx<-7TnCO)C|sa;oZi5 ztRV&@Z1ukNfR}Goy%{i~_3GSdnR<^-UA5yACP#1~z+;%**6btiE2Dt5$SmXWZv#7j{a)y5v01kB^!Y(-G>&rC_mAwhf@&9!W!&*Oz+iGYoEk6N z&!JLD%scTtL?_GC5$PGB7VFkFQiLvn=z$Zn#ld4x;d)RF=p63ZD@8TXQ|FK81Rgv| zHNjAITD%Dgg<4KQm|L_ONe}UZoQ0r$1wkyE8SFsngxOxlCyFvY{taXu=`N3O2xcpm^k(j4?Luv zeZ8;uc?wfp&ev`PeY#CQ`Ahka?%^^0Ds>F-6$<;4dzOU{U&{`623Mmw2<%KiO> z3le5~e186pV-4zx_n72VDz?b+CAx3aCP}q)opq`c#0`4cd?Zy^>)R}pPoo+p`SRQ{ zj3)>;mX!9|xt~N#|91f==W9Aqq4b9%b^5VIGLhWtWEtk7dbT~;C-lLE0nF@)RA5qv zj{)e%O%HcXqoEU^d%!!ZOSG2(kuQBGZzKHpA(BF;RgIwF*Nu4W-%@oV>Ib6U@aj~| z1nwfXFrBDMBL&;0Ez0G&Q{cEK3i@IyBU_g)DmbX*+uNcWq9XY`kexJ1CRWLe8!WZP zVwY!+MitiJ)-lCPN!k4A*sU0>u*8%(zWTmi zYf{Ee96JccdstzMY(f5gh zo8y#*DacsjX`H81*Fx`2vZ>tWia&My3#$m496KR=JFC;H;fNP{qtAR?Sv{;qJO~im%oLKYzh*ftsI17UAtB67BF}!EX@Tz8KNy2#Kx%O4df161%=;!|q zZm!F^I#@N{6PJG5P%VGb00Qux-APGPKk>a2&7 zf;fsN&9{C#&5a{F(3M~sw>j$8wYJ+01~?7FX8-Vp90oR2Z+4}hWxo(TQu&v$&xZ6n zn<)E}4S_0IJcrFg(qzyuV{|?S2KQ~YEf=W`str7D4&rxZ2e}i@BXWkZY&8Q|tX@xII70iyUHploTjA7DI>d3Qq1axWQh{2$A5hTcl zvj&Z1;r!!=okwa(N&H{btZX-tWDf)~_(DoJgNx1jV*N8i>osECsY zF)E2D#~bF{3^R6o$j0SjGaeR6fpsin=y?(q8?}=z4yA)+{t17Rf&xdp`yv|p#HU%= zHvB&t`^C0q1#EY*rT4r!SJggd1z?};#uZwJj9y8v?@n1}>D6q6>Hqc7D+Ur5$z}QJ zwF3lNM}}_x-;=%+oqXzZNh$FEau<^HbUl8~q0}g&HpFIZAzXhp|F6p5NCS;tw@hWj z)Hnqr`ldccK=iZ>KwR*4dV^?mx|#>~kc2z;bq*Kd(V8)9D*pc{xHHtN*?Iij)QsXPwobi7CJ@!Y8rtp|m<@>h`VZSiiQxwpRgx@Kl z54?T1IQH7D#bDwIt?K^)L7B_j5Rb&66Hmon+A3;qn-?OvimXR?^{?$VQoiYNj=lf& zm@&I1ce&cLP@_`MfzTtHIFDLFASl-QH9>tF#3i0pC*TFMJwc|OYn=4SP1_%^g!Q-UMb-y3WVpjQC!%rQJ&JHB)c#0HJQOXVQ)>12f$Qt7W z(0Pp1fx1nRAZk~U&}OAt$3xZA&$Kq(Sn(9a+@U|{vD|nCFGILi09>xDwXjgC#NZ(@ z_n)k1Q#V~q`n|fkr&CNZh&8yOuprd8GP*Z-s(;NMW?4PZYn61q0%VF@OgAd%%siul z&t%eMQ4@u|DAa)4ZHhrpb6~|s!&-YyKOWcG;TH|5c~;iv?$LQG4M;_k4b?ZK6BL?* z<`3M8Sp$Q+Z2f?lP(|4@-C^60hq3v@3Q|YvhGlEo3XOdK>qdED=5TL03PBXqh^eiz zaSosC-|dfbc7bUM>56(G>cnztQY;2%%ugO=c83ELP`;z%IN4r%T`J`EvNdR%XhH_; zn8|!H)gs~NcElx_HKRSN)VV)EScxwD29m3f_|*O8E*X3`9N z;Hu>6KTH}0SCT;snXMvG}28(RNV_sxf z#{zUb_+IGF+%&*07z>j@mniZzC2Vz*SgqX$JPQ|?aHmto3`q}3qmEQzRbA)Tf!q`C z>At8mp5C?rTXFPJX@G7?DXXWy`jF92#P(y z*hjJ{2&TQB18vvsbpEPRc^G4mexV)VG+G|mlq@N6ruP)O13T;J>(rn-P&wzH{mNW; z7aWP;rfZ29B4VA3&8!0AGWPLwn}+t{CI}LEj4K`Z_%a%wav&cTexiyN=!P^iTV;+3 z4K?HW<(t?TSQB2F|JE9s1!Z68hNiqO`DTtp!NHty5g;_8e&i?ElXF#L(C_wis%9QW z)BXbh7r_~jLa@d`2Pv%e{dj{R&dFJ}!P<48CLUYs|KfCsM2{Cq#k1O9PY`JU>de~e zMj;ASX8V%f6k;oSGf&a>6GOK=)Pb>_gzZouLv}*<_t=+}ArchFkL>N1-faZMBJ1l# znqA2s;bY9=gY-J)*sC*E4i!E@fi=c1^Ci?yf!#^{LRwS}Lu+~ahJBO;f3%MUN#He_ zXzW%Q1yTRy?GWCO;er8*P09*{nC~wNSv=!!k+_)}*8yKc+nc?& zGinVUUap;9+f~yP3%8fxD9Y`YOqJF{*cr~=gf2X+eWmDAN9LN#M+`p{Ddh&KuNhKQnXM0B|+`WVE#rpdnjEbjv<0O$OnTfQi}O zpYsAxIayV?VC?%GLKePBr@g&uV{Yhwyv6rgZjE>1ibmc)rD7MPkroQ#HqI+kgtaCI z+-vSUx)tanWW{ptSDtpfK`(ff%ru8G>T~e8*|g zK+n~+W8^iSju;e(eL|>k`y8pf|5k!zwERURYIO1$K8;V7qo?9LO{}N!Q&~8+Z5x9O zdyYf($IA~A4YIWBtZ;^Z0!+}+xlS~tdC+5NRh3mk9t_x`x>Il0R0px~!tF;{sA3%{ z9p;P4ASb&s^a@aVY&o37L;WPOC$N{q+%$XS)RjwuvFK7!kAPQY3Ft=RZ{oZcb$r0^ z14BH7LmkhA`3K(g*H&^qKk8rKH@eJ>9iMcve^x#*)h}gwyO2~SJyp zD>kzcQ&z^BWptjxs%i}8vVS~+ zkfdkrO1lnN)v~kYBZcw!mmtd7~@)Up6Hh=F%;R6{#AyC0&T*+wmfy~>dXjL zHkUu;=<%s9hH6nh>bFHxqW8-V z=cw8}DB9CRZ3D+pR+sR_0|v&gdU2We_bmrPvVCJ8o2MZWd%>G!Tk~17ZC?A%5S#kq zNFmW`v<-a1{|tS6z@o%<4`Byl`_g{pwHStc?%84MQji0P=+SS5yAv7-5HxS1ewYRA+f`#D$eBYt{Dyi)LPkC1KC$ z|I&eA7f9AKnIe;tiq?WP-5xnB6g@$b>D#cORKXdCb>m6hMT8VsN64|btHY=_Zb;I1 z<4}Lr38K#9-2IXv*Awtnq)zugmC$3QYJYOjgS7K z2O(Axlb_{0&vB3#+V$yxMf2IKVKC3l#|GPwp$8%Gr~1d35kAR2UysZzDR~*ElzuYE z&>Ll=x{b+g|1d8Fb%BZNPyV_a8Xuiq%Y@Cu?_Wkm8P%q&l%^!W`hKijyyqosL>QqS zff>R~>M9a_<>kt~7f-XIsTRUuux^~dC>?s-`DZabSD$a6%=-TJJlHvARw}{g#*F1F zNf6HxfuMVKD(sEVB3@RQ+Fu5OPgJnC%b`#8#!qr2%C)`$<(hpeNyl%cFc5kC`*2ZRWt5@SrUNT=sM9jeb7mmJb)>i>ijk}bU|Xn15n6ZU&AH@A z4bjV~nG+~4cL(b1$)UFk7K$D04_>gNXS`R!D%$b?ISY;Tj$oO=(SdwqyGaxRDwMIn zCS$p$>oX`y-6k$~blC+vP`L*6ZTxdWP8Xal*W!$x8k9_)Igz2hq&+kYB0B18xG=e? zA~If#Un6rm>F13IqnAHwg`r(W?8rB55O%5laMrm|Pmqb$RSYHu)Ocg7>YRr9DCdzNhF0 z;uEzDa7L8IbIxb+7=#Xc!Cru1>yr|h*wXq7hk3djcVXPcq+j6z%}GRlrtBvPRFyVg zJ*E7tVqOSC4qV;K{7pS*tvud+(cMxfMuMs|euVgy6S6IvV+NHsq+z!8aO4Dl*IckrekI}U6Gu8zQ=$*NdA8%d@8=2cydw# z#}$fyT<|l?Sj`WMFv;y?+)RKZVc4;E8Ex#I9g+6fasctc795qZY^%JELI^(2gsil^ zb1h8&j>yv@RwUYKep`a?qLm@@GRqWZh`;J3t`c7@z2@rHJIY4dIlwOA03pFWhvB%N`U?$)RdWV0{MxGoqsF0gae--|q8Gq~HHI^)y(FO10nR{Mq z|AaUyY{IwBE`s{tfT1!)B5<1E>L+itZVklqR!a{I2Av@H)bng+zj@kGhn~_CRSUv} zwSLR47)2(&BW4>Dh^*~ro|eO>LuJ;iTNYLTW){%JqJ%La2Ye*ft>otvG$vNgV>esh zd;Yd~1QAWpDww$ND=;2w2gCi8>t6|hFUzvk%00O!YAbOTQ`#tqJ5&yQRQqMAi{^Qq zJPc)%N6^utg*fFDh-2Gu^V@hVMH^)X&;fREo+GU9uB9O58i_4_mQFn9_x(p|Z@V%R z^0+i=CZ+OpS21e$C1G)5%J^4_OnTz#Oe?JeUPAk zS`TRk-L=?s6%q?6UF!8bzF%_{ovr-P)LpSV&Vr-a@h*o=j}#=bO43=!DfBzcEn=Vr=jC<{;0gV>Qcb^(GV{!=ODlcubtOFM%W=jJ&#_o;Mq zQr4AX&ZgTG28k|@n&H{sbz6=@2m<2$`8>vE*&0Zhxb0bzm*|2^G34>F0`+Ud@SPdXiV#8tvLX$z*0faOQ(r-hiL32sdSD< zDLStByC06hw#;lo$4n>=m>B2U%e>+X6$en!!@vE-cqtcoWbeZ9+c-CUvcBzn#hjNF z4lf&nS!g4tGsPSUrg&@%tU9Ms0f`HaKzj-B@m_!ye=V6cjQUCDl7?>}V z20xFBNx8zVB@|$bZY?o7-S3A?bdUi)Khn~nd4(y5%|h;`+ZxiRmsVc+K(Zsm&2En` zbJaBKqTyd;Rd+Fw>+JyWCqHMmyF{!HfJ|3^uAo{uuGxepQ382ET-5QEUk_2bX4Emx zLs;i%p^yRfvx@~X!%m8)tLG?hmQCzhC3P09BfzatD<790z-!nRyuFSxW95o=c17u> z{~^tZEu;w26~qatqPu?T>h*yLa_<|^H zM8`}_cEi3f0prxVKbtg9F$3u!uu?af(x$;SA(?pRyl7|{qd;9HK<8=e+zF5g`uc8i zA3s-X7vukIpB_MxUisVnTyqpJbd2~wpKb^N32FA`Vm6R%exJN-M46GXE}JC1VU>Vf z`6#0ej#V|e;@Cni91SdxhPXJkJoVu-F`?$yVXCX4LKm<2JH6ERTv*$O27>|U2ouZx>0@T z)#!>tIw7sg!d^&w195t;1x$!TO(W>O#vWJA$8mm`YA*x=(y8WIH+4%64e*?sR{QfE z1O#!jrABC*)BNy4S^^xCo2l>&^lTs~1-5tl-_Qa_Cr@<5i`fX&gz=z0FBb}4{2`f2 zcJ^evA;VPXttnR*Jdu}F6)x=vvjlSoy?J;k2*-gYS|0w&# zoWC(Lc=4pk(l;!DtVFvSx_W++b%lck5odAR6ZaFmW3^nzf63u?Zft9P*fAQ-;`auq ztL{Lo& z)^z%SdU9vm8IsN0t|R{_f_VQ?TgG*jl{`jw?0AO(7!hh6;ZGj`^z6hX~;z`*|C z6S}716jlKyYLzEifUz-G0>T(XB7TBAWf8R)@%^6;KxVpI!-)`1zoj_vs@MX`3Q8Z& z)R>*b;d&#==J+3rgKvS6#BD)EV)#+PV7CVh%Am)>VMsZ(R?P0 z7tl4m!&q5OmMJIs#c*sI0lEs=0Z5JY5eV6Fu{1}r{wh}>#hs;HA0sDU+~18+fDkMLl--5QEUImyCDxI# zH-{~iZ`yQqJU3oZpO1Rbt_mkKg-BCa@6w){2H4d$CYZda!r|6Wh(M6zJKs?KVmpAJ zmGSt9?p8}Yz~HYk+cJVg$QqYWFvtFBC1VounsOD_Vbn;@LX$IW_JYk0v7y07rm88e*{;^TN%vO;!(; zr%U1n#*lYdhDp}!G1$P!a3A560HeFRx;a}+cd+yzq z3kOu|#RXqT(Qs`7Q8FlZ5R!4%&e-xSoSwOMzzLUoZN!%*V%~Wo zy>WV!`p?+}nfz;ok`S^8JzeddYk@v>2v!%nu}+6Als&~M(65k|B(0D97jeSJp_)>h z1#wtd9lVILxU!C8PWfrO4&yC!TCVV{)d}SyW7$2&E8rM)K8LA1bS}mTZvdOBkisM8 zppX&CHn*EcSafv<$pOyJtc?uWLC>qXym@m;J0J0y3NLH_g5#I*BRG~q^a#>)_nnhM zUu`KLto()NA(tszUC#f(KEx9?REVf?P@$gEQ_7-X8#m#GYDm?lY$B7Q zno2KtG=>PkhojugeQ;qPH7FN{yW+-Rn z^6d`TdYKyl{q4q16hTeV&CBbE`jpj<** z9|)A~i48tS)7AybtbA0`spO#_9m?emD_)+i02R=lMZZ7aBi5WADrn5JJ=zmAS8n~W z$T}P~V;oKQ;AMnCXo_RiMRG6AY;5= z5qiq(2`+59eioY3=Rh{5sZQUS-1Qbu!BYTI_Wjl-pTT`C5&x8-SoskY3T3EG{7scq zs2iTTWket9taL2W*Pp@BsT5hnNajEPKrW;Py{VBUEQF|KBsOt&qpVLYx+UcG@Kp!= z_h@?$fiJA$;>3_;63sT@1TSCDCJo@EYFClB;4Ug08!w&8gjazJUKvKkugM%%G>09M z7@mLIO3R{)IWp&qjReq)-&VbTH+w{7eudxobrnTRO@p?!2(R#FOreUaYU;|=%|}{! z9NI4I?9}o!Vl<$R`25GyEoRnUn=2=w(aUh!JBJ}c`t>Y2VJ}|!yL*E|-$0j{+ng>e zex&jR{zZbI=a8e4=pVvqZ2!WWb~O6-VzZ&rTJNkdD?^}t&@lSdl?@Qnj3At+d6WW zq%>t^_&%ykxj$A{y5#z3iQVZ0pTL(ol=^#CgzZHEDKAhAyVbz9r_w-tDAZ_hfC|hS zx8(A{1z!?da`oYIb{9!xE^_c+Aqn?5`!3vcXC)E!0^@URhnX+~&<>FgM&BiAIk%#} zrYkd%P{(6=2{5Jpb&ZBJ6HLMpOzVD?Xa6n=1m;|)z(Dd0w%no}$jIS(`6&D?3SF1n z)j2B9PdHGlQS1Ce(nQ|IRr5JnYVBkr6hoAx`bxYO%6+L-Qr94Z0vL0Fj`VIY9;>}; zW}w+i`QW<0S8r4-Ej(VCn@030&THH3$TXV6FcSsq?WLs$2f)_|6TMx(hrBAg( zk12F=h1&)z6S_hp)c&V;jqK}{Cm%eov7F5BzYb)MOJ~cRGxP-4UV4=Ga~E%5T$lB< zi9QX0S1sV*uH%%7U3=_h^G_9IL636p-jU%Ql@d^V>9wFitSD@z+D&_h&L?>ag=^8P zP0&7eBroHARDq0vE_NQ#wQ^{h!0@3b*xaLu>>)s7o0%&i+Og~2hn^E5s@CX5Yergt zX&OZH)reQX(V7=+{C=_pxDGt5uQCSY+XM_xv@%0aAN`pQY-I0XISeBuiIJtUeSQ)| zUbhgZlG!W$5eRaUbU5e>Z!9M2jfXYuqvGEKFq+#h-{jq@4Gx+q337>#fCNX&KzQRE(v7&kKI>isH1e)A%M~t%R)-NPg z;A0T#$oUHZSGb#ue!%wJ4;x1(ZeN$~;IjBzNReLz{mt^(181|&2q@gvR;}R26f6g; z)Hb;hz7k1vh153WkLUqxq`qgDh}^Wp{U4|%&ry>~6KcM18>>{)l7W;|jw?M)>{oCX z$>P)Nt2#)2o~BJ5rQEhTr&p}r?5}3zDY=czMl3HW&o9WRJ$n9Q2}b^(up}M#I+}Iy z+>IKC>~Sgu^vUIrS&|jArszpEL%Wcpssal>lv&->cV-_KLAAa6BOi#oYbOv_mVge= zb;TB8FmFvWtYq1u5{UHC0)O_E605r67*b>SF`ilej*VgeoE4);?r2h21loJeL(ZJM zjvXzH@*}{OtW_)M>BR!J?D?jduuGcK+I{#glabOiU8Irpvh>p&x<;iJ`YV#;vani5 zD`~j3YlGMKFABFe|3*T;*>h8z@eqn}wZ~0P%g4~+v#)PBMIFgq}+BYUXB>c zQ`|Y^LWQ8fPy$s&fTKH3IA#jf0MIF}q~k|nC@piu)|S&^Z4rkpX6BsIW>gNdSgI?2 zYgs!i>g*zdRg#05Yv$g5(K}lkP7r*F>*SqK7EMC$BgeRcDR|VU)dsF$^qvyTm%ksCWW%{@ZtVgwerC<$ z#vAK2PeE^ozlA82*`-`Aj^}*dRo{;}a2GpfP?l2vTE`g^iixLo{;b7*j5`|OWTSTT z77!}voip1P%Lt&o-ghu%GJLHI^sBuni%?vAZvFGtZh=2U<)6JK8>VmW!`*2qH|{Yu z)$X8WQaDIql7ic=tyNH7x6@`3F2b8CF=4?SH!YK=Boot8lNs!hdBs}^9FV)Th3FLw zICZdgZkF{&QolNCY6Ir#{)lMG#vyo=iVvax5l*i5 zG9l!eg?0xF0vQme52S}yCX|y@3tcCsH)gd3;PU-6JI9W3$*I>)gL&4|RIalLg z&k4TSt_04gitrPw5X@Sey1M>iqflHstWsodtfvG|ZEzyx){9MluNZPOndyi0UmBSx z%N*_`gFk`dpI;P~GldXj?2Xo9UJ3jmdWhh}6VW>H2&P!=k<;4U&>OnIu3IPfJF*d@ zM@cuHwB4ErrU)=Bv*~R|NUOzp9Qpp`kpQgEt5>xD&!`k%I&qZMhvAB!TJwo z;_1aIPnq;JN{Q@;aBXVwJ=j-a=&TJo%}-Zl_zhS^c=JnC;>zr*q^us0yszXcom)Q6 z--)3Ow4aF@E~B-Hm)Aq@njQi}#WlE4o20 zarFGtpGH!8s_CR6+aAefnc}c@g4?2v6_P`?W@RjP~qhw?S?Iqzbv6 zBbs{CH0B7!e9c1*pfA#1fs$IT%W&jB*XmXTcbng%v3y<$lthww#Vv2>7PU zk~6EWOetfb6k%_?eNz?T(<^abo~_rG+gvM$iJSANUhIXtY9 z?<9JguP_XoX8T9=Wq%)NyLW2q#41clr|fzQX@ke7SLF`XU$P3Omz zKBSV!(iti>J}ca8#l^B=BtCMcwm+3e#(pl(d#@8}En+RP>8*q^NH7iq*8O7k_^Mj;l&1=?fH2PxLy-w(q_raafU1%4~oe{fcvJdo$e>7Bh?iFY*#3zpLL z^qO@pPW=}q!1$|uheAPwS9b85h()q;r68iLJu9$Ayd3NjBNYvT9+bt=aKV{A9D$p9 z!rLA_w6%ym?8c|K4sFX!!?amGKpH0Eh{2k=ffJfORTEJ-E{C~~Vkxv__Wi_&lm=fP z^MCYiC0H#G9N=PLI@T`D-oJ)EF{&pQ*B!RPm1L zGK;X_4k>ulaVVo_cmEu&%%?dTIZ}yEb|ayEsS*J%qkWaIl#f2K({)%jpzvIr6U6jg zTxf*rh!EJ(^_B`vBCd9n_;8?IqBWVLBR7Y^w4xX0+c8fW#GJ|Z3f1W228y+<=0fov z`v~{)T3m^YxmAGj$t}7Y8i~@aiEf`J#U|D@PLYe~l#Wh0w#8@~lXIMeB`OX|z_r%f zi^4_?=mY#|V}UosE%|D}gtkO?9M?u=BLivvYp<#-A`bXI_zB0M(ch2%0rw8Bj`YOJ zolHk~Q>8thEx!e_3tVO{*blQttx{h&^r9nAXP%H9e`16F{O7>9MC zuh>=cQy5Yr><3eu+381cFx4w{l)*VoKoS4Jz+?cQx^^d*`|G)3l0}JW-EXJ??s$17 zvNJa1N3fB$^G1~550z=0!wx^)!q@)x`3o5M4>v#{`Whp9i`s7r)PCABD??sJ%uW1$5^?F-P)yG_7 z0W<wU2%o-!67o~< zQ~Wk;>D#*ujE26>Ku0$814AF5hPwRLB(EKgVn&_KxMVScOX?KPMf8#jgp%$j_>agyCt18DkHp!RY7PglyYdG9VrrM4{QE7`jLdtb zG1e5u9K^9q;Cm2VRCs?MLtGjX#Z2RbpoTz6q=a zt2WHCyv7oS8#>I)eA3bab3e82n)5d9CekIE=#U8k@t{!@JV2ZAd1giI37u=?vV6-| z!xI!@KF-AG1T*Zt+&FQG<~JpUcdy6MjX(5=UMPLg__%PCU?7Ow-qb@(T`Z||n4G^2 zrU3^+4&nD(r(O;opoyFY`4)Fpj(N*G_9xpqOHdcmbMuXdvQMQ2tRPi)yEzVi7r;FG z%0ehQtI87CrjB9|v%XO%5|rA)O%f6;>^i_gS1VW&<+dzNf*b0-`P;S@f2nJ%j-^ z4^aFiV{eJPa!xJ0O!4wW969*QC(y{kZ@JUGBO+HR7H+JSZE#SCwHJ+`GYr&w8kaO4I-B zhZ4o5a9Ry;!q2%$&>eG!yCq&R!NoRb*40&WRIQma@ILyXQwpX^Q8x;mOo}cv%W2sI zZA{MrSE_ZyV$4aYj_-kozU)0dKs#N zoA291x-!&O?TIslOTpMfYXOKrfely08Drus&d?wu4jPVx>6~-2jVej5L-lzw+6j zhJtI1-6&uBu*gmonxm9>?^m9p_9=>jkjB8TQ5gXxsCLcAyb2RM=CMtDV6vSb7=`OKkwLAs z(z~D888hItMy#eZ{l+&|2Y9l1 zwJ-XOsa8CYDZmzp(u50&$i+yw`v^W-UGR2_UGGIL0;W{xv4l%bk}9)KI1~AiDrn^V zurHCZ$7O?5G)3c>P$8V8xz+Q;_#j`E>leiH8GjoKSC9VHZ&%X&}ez=B;z)5O7eyt`X>CoR%Vf? zN%bk`1!dSU(M0-U&KL~mWSDL2aDSHorlYO$=T1`=-TNe5(!MBM}@gots$%%Npy+( zF4-c>6(@6cz_aVpnlPv}+-U=8e$|?)Qe{S$($Vh$dnNcAqJ;ao2GDHe&5D#lYAC$6 z(z{8BA`}D2=~Oelp~Hp;+vGGwZ;JAAo-|;C+TE7HU@%H3K)C3cHWUNZdsc8t^Ua5b zyb*+Lt>EML#;XD*uG@qq;_TEkc9k#PJ~d5Wo5{W;^8#^V!i`&y?JoII00E9ciH&C( zJ2=M~!djb4773mcte?S)&q*v;31idDMXBk}11kV8K+wN}d-x|&Ixi5B+_~>F-mL!R z2S9X7$6&%jC=M~_W@TWa7F37fwmt33VstvZ9Nk3T%e{QyE-4ttpgulHIz8v8_o2Bg zs1`8L&;H@NoKGDx4i&8CWq~?+B5G-aJr8>c%z)X}ZN~7ho~D z4ga=-O%(hHgmmeBuXeuP4J9a7A}q~L0yjUrp&MT@hn@MRmuu$YTsZ?h|43Rq$}w|`M2k@h^#*|l6)j}qeJ zdnvbSgcphGM|zW*>(dDhH(OTDxo$Ne42NSt(^%KC+m~y&H}ZJ59D%J~4<2Qh)0cfv zj87GbzsU}$EDI-@)Ad8bN%+rky<3N>zB)>c<_~FczRU^my77l^cNhVk{6$`DyP6$X zT;b3BRD$tjo89R)R%AfdWv;OO5kOx-o`P5*@}WP48B2x1 z@>KMplV}S+%J)L?!oRKt8)De*0h2W~J<6%u)MKAF4;f~^A)rQQ0UZ2Wn_qTuMHflW zG;qOjzWJ~xrZ}y$n&s%ZiVwNDL<;QKqTdt3ZOS)Utgz?v2ez$)iD`8C`Zl*(tTsR& z(ytFucwl1$MS;pZdqF=9*JSwAxu04hPi#%o$l#vHXzKGSS67ie@Tj(~`bDW2ijJeg z%jQ=ZVa<4GZjs`#J5RRaFo#=PQ{rfeUJlnWlUCUqQ(GXz8K@qH+n|0(hH+OJRgwBs zJFBYXzC5ak1a9v!2M@KBJ{p~NF9sCKV#P<1>Hnm_pbu(^kn*Z?O{L2sgnQ!4D^Yj` zj;xCg7g8Uq*>8h&xGWkG&f#gG28<$EHU zOot{umF8J{yjh?j!?YH`<7_&g+1LoQLntFOn~DgPa3AKtN3 zu?aq=mV!mdKtcp1j(JSz$l;Mb6<#aqIloRHXWNaN->f)Qs*6z^3S9mL_?xIp9vB|P zvi{Zu$&2ebxv%vQYdoVw@0ckH9y?={6ykS|cqn+5Wwq*F;ECv)=h{nczMd07iK0O% zO_xD6{&DVCkDLk7;rfY2Wu(!sT=%Q?BNy&ob(-YZydAIljr0bzE+_^W(3ZZ4{_0>H zc8;BG2gjA=V;Sff*A1)#EyAmgTA-oyLdZ5)Vzurh*&+K~0LD1y31Czj0rXPg)fkB3+xQ&aJ; z>@PwhvU+euVGCf?cnq_Xsa~9>|6>xsmXKh9*1?)$`QNEvhxn4Kujv+=Olsp+ohBPn zpro!Rs6ap%*7QrzJHx`FFc4@enQXq?RT&5Sl`uMbHW?AmMS6tQRw8~YinfgEMaH#8+uLf%*%cv1aKp?$4;z690d>q&t+b#eSs}9DaWv)r#I0! zQ9Xf6WVd_xkbhERuP-Vrt2jYW63!s_PWHcOXt?3ijw%wEhZ7{a|6mhs`&T&?vH4D) zbZ~wW4Dy51P>aUxE;50wxtpPX;##Yk7Lc!1`<|e94K�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 z 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

    5B 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

    dwsz5Ht`B-2ZY%ClyqdAEt7D_ z_x0&r#EkETaXz0ZZR@l)qmYGF2jwPW*Neds(;cF65mNw8U(f0Z)HnVp5|FO60^dzG zL`t?UDi6at*K4W>!3@Hg>a?czvVlDbRF(8 zV#)6`ql|qgE00m*^|d7*86Ae3`4Ce_DU?Nu@AjIVcqzn$sB7@!(GX+$)3ux5OCSp_ zNM6R!@xU1{^y@%DU&$Q_jr78Nc+wxjjG@;!0oZ$}6iFLIZL`9*;}~O7TRCl$so*Y^ z3{)>%yS{UT-fQ*eTnq;a>}Ju`_jnL24XKw;;OBr4L|3KR_v)#r4OJ(7;M#_3RqkAf zcA!ZMaHHBZB*jhliq;ExK5Ygh&uD*gkG3*ob>xe>PNyhH=tp^3KHx7Sd*mmXdgp#& zqG7)!e^P)i>c?=Cf2nh_M;FWBxqoua!IoN<|0Qf&dgP^YKwu3y z_gceA31cJ7uc)aXF6d|dGGEBkv`ux zqhI7I6x>MHH|2Y2BO&Z6fkvCTtFCF?Sx6S_EL~}Dxo_@y|6Gw%($1ozS;U^>LA0(q z88`cYJDnu+UaT5VTcc0U%eEP9<7cmQph9a@8}pRchh@NX2r>JPEtIfLgc3ysdG98r1_Gb~;6 zvLmwi-f@b;?5iuR`N+$C(kKGGl^RxfqB8vMCx+(p)=rb0-1BdXRID zy2>1Nf!r9-JJ!;~$_=XT9Y#7oXrOIPqNTM@Seh|7x)1rdF9d%b(*2(ZhAALHAM z{yMV+gE2aNM!k)}qJbBe=ZyLkR1&4ZG59a^i5Ym&z&h?IkZ=G*60HfO{LB3ln;M<0 zt50iZt%FR-R`k8qr9;y2;|Tf{>oAUlPPGm~aS+Y6IbuL#7Vzx>75d?ww3}#18U$Q{ zW5+ObE|CFJb6b%kU(v)D1!!iBICiVb5*8ujPNZR&TB`|gJ3RZncTc3@lQ@GG(OE(%8<9kp{XmJb`4q1!(*h^^SLUjqc%&&8q>`O9+kS+##CUwR^lRjY z$tN;=XJwGV8up8RB`2Yz?QO=U&Hy}woedYx@f@W#u|pYo*8WN0 zkjg}C43h9o6ECua(EaTbtjL>J1BcE?Yzw5p!z~s6#TS@@yX+SVEp>Np(whVmH;z)e zZ(*_LW_=s0B56yHQ-22e(M^Mfdj;3Gs1u#{`I8QYGR`+LSKwk|N};~Pf*bDmq&x#% zU(^|`nu!`Y){ThHnY=e1KF6RX#QarlF82^5vwYVbTUK1JL#2b42j`gZX+5quXMuXsOlGdIz)Fx>9N5zy1QVHt^Pak=FZaoEOj+<~`qn z993PS@hMxs`>&FIPL zS5HRGVSCePBe)&E z1bmy%imb%)h#43m*gbG;QNwqkc1Ngke-HQF-fBxREAvIM zsjG1ri%2rK^W;<}GK-3Fq4&4&g&SBjSK;T!iVH?8GAe?1p_syHML9FGOtZjbfC5)w zWfp6Qm`4G@esLT`LS|78(oMuMOK-2dY^1;F>{URB>uZSD`u}eM<#C#B*x%_j6Yy%N z7U#<~G$*D5!X;`3bI#aYy(XWfxrNbIu8n zEfX;Lt9he|_99I{-p^W^IpzD3&$0Sq2}1D4!qBIx9C#4C=Rs;Vc(Yl$AN*ejaN%+W zE|Sw36M~*I5ciZ8U2&H#b1eM2j4~8pDVk--e7y@0R$*^xe%LW2Yb+aVIgMxVxAR_f4ZQ!C^HJ-pm zXFQBKYE|Z#*F!aU1VlmhY0-K7zqKu5{hP8D=K{(tFEM}5h{}8QH2|oNjFjui!3YdP zudE;~Z?G@acy;O7#!6si+=X>(6?Xvg8GV|+Q3s=ulknk9i4itdJL=+~Kqe7u9Dv8- zr6rO6i@X?Fg~3CYFvcr;ma2>AzUWh48O4HTlF}xKFwiXu>d0&nFg=urswT^qR#ioF zxdqi9fmi~iVunWR@Jc>1*`LWK@$z_!#F{u`?NYjqpS^d`5F|(bZ<4C`+h6kwzmn{i zazLL5&LFLE4ugrbC`pRc$KtFkk=6A^d73vh&g-iD5C9i=%nQcr&&{U$DI0Bm>}?yb z-@5Qr(>IRQv}y_99Qrt$ThDp}b)~EXGg0$JDzUV`K_f=+UwoMUc1e=cpIm-0SS}yb zdhR$PL|G7PQu@mTTt%vG0iL;L9&{>(y7DM*wr7i#sqctdh(TrCW-b zecEvYTx&g1gv%EO3xZEmNoeCarvMoOl(KhZ3;fA7U4+hWhXkqb`PbjdhjT7>_=gC( zV7gGdmH(WXI1eJe)%5p(?=Mbd-<+CDPQ#f4cW9)%TJA>uhr-|s7B`#$)da|gVU7m&!r6DYE_s?L+P_a+X-nD&SkNV;5Wp$XXT# z?&%^PKiRqz92V+>vhPlPKY*|y(*i}k)Xe@>SsgIsv2?^U<`JY5^GvBC*cL{LGP&Y7 z^A4*)b0SGf)O5(&8ZHh^eVEo%{E4KBB)PHDUh#eBsmHEi`&q7$H})mRMVh}=qDt;j zZCn2!3`||`MH+>@5+4QKW$M)@rZK)ZJ)7UlR1}ay$Q{71kOV0sf|hI5hUXFINV4e7yu(qgHBo8k)eQ^SRwTedT7OP=UhekP*B z)|^7(CR;Nkk`lfr$x(4?V8!}C4BN`O76rdI(;cue?K&UIJFXTBj@Es(k@N}ZuDwDc zYp?q!hZr23lTPvN&5AKP+K5~3hs!kuQ}K`>R(t0@S}=VDQdCIYJ!&(Tsa{P1Hfr|- zg?-~@lynI$1fJQNeon!l#|v9q88ED#?1}w-F(@BD?eNvm(Fs9RK0bJ=x_FB`U_+~P zn~&Scj37g9r_~uehPR6B*E#H0$Z(cvueLCu8s0gR8&=!m6bOT-Ahxcb%a<~sC$I<| zp!0P*I$(Dnnf>nbkjxJpX|rqBD(OZ?xrN-Q{mX20Rn4jrw@_pbQHA>@_;6xwBnLX| zq!NZ&htvaxlAt0Mi2op`)X0M$Ff=}&()8C#)|*O)+_76(`5~mIrxFJ!=qIm=``0vN zahkO!;Qm-94~n}{ficHr4Q9@BIb_SryXeU{roqW~jr#=SlJ#8lqY)Aj+CO|qFx>5w z7-Hxkp6>6`Y7P8z#$dJCty=--0Aa7F`lv;Ho5TWmztG+Z9K~j=HZ4PPke_<|czGgl zN^fv3nZUBZg3t&)k(2p;6}YWnB@@AlWLn(oj!;+NT~u+ej0vl~{!MGLy>2F{x$T?u zBJ0?UNJ_(f`4!1j)b*2Qv*`zk9?Ws+%**Q#tB-Db8SP&9I{AO>z#A}Z;8`zw*tm+$ z44E~`6Ud-zNCU78;&}4RS`IAqe7Jj^S(YA%4|1)|yi{Hdn47+w zL1|&o;O$B8VRTy!TxkKRAI?0HXt8vF+CRY{-exGqXy#Fs=yIn8k)G9b9( z=azGG3_)3T+`qkTb_KN>%wevbZxum~_M3Q*ta_C}?RoqDE)PhWcJu@4ckxF^c|e*E zZT#*aB@0_H;U4>tWptw$fb*d~{yKPSVAjQwVHl^DO^5jxL$^JKRAGRq@qTBCm(A9UzRZF94cx{@i1OycCWzk~ITvTBz#k9>AtRew?67?@@fc#S>Tg>dI0?fi8)kP zWTj{Hwu4@ix}_>h0YvPMqI`ZthE>*#@d)d>zpFTd2Nd%4_6L|Lv=kZfn4*{w@Or{Q z#OT>Z7$DKm9H*F=XiFVT?+&a_oa~{d$3qO%JC;FvbhUyL*ows?S2r#UyTiMJhfD|n zE6G=Xl7y85j)V47-IKx72~dzfD=GZKuM0lEN!2cIDTkDco%dWmUaArWoYMckRC-r7 zE*-UL-GzZZVyIJgN59`!X|AzQyb!Bug=e0=$+|TES-*uS}O3vxFG1JOx2Uw%t^T&90Xd8vPeis4= z7Kt6Fp!T864{w>c0fs5S0b$GdGY3I*8B&D9aNJ(lq~?VW^RxV+-i}Qxv$O2`t_TTw z8mFh^M+wJ3mRkAnc<3MBoyA7Mt@j@ZgAtFv#3}s-x_}#MukQ@*7L;(7o6A}-X9lT@ z-;pM8i4b(gyI8#YrCDGF%h&fU;T`%#=k&R<#=t zY|=V7PU*RRT9}+Aofzo$x*ElkqJD388gh-olBB}hI5!x9QB91;gfW{6{YBfp<)a@J zibhfmuh`GDj~W$=bja*)Aq1xUFFzrSY#|grv$F%h(%Up6e5y9eB83x56OuT#UVUD^ z@ek)Ti6w1}jsFA7ZN^A`e6C;T@l&2ts$D1@wfMVf`J3&DQxIa&?e>zzjH72pIN|ZW z#0w5dF@aKSRLT6ATd(E7NuDIYGhIu*=OELzol$muFPL6&GIKn3aa36hiid4eo5H+ zGpL3HWB|}~`b>9)9*Rxox;{C|>}`2Xxxow$>f7gOt$iSd3dJngq0{I2m=)P#7egBK z=<8Z!h!<`V=F;nTGB7-D`EBt3$0)o)J9YiO>t4edaoLA2j$v3-NO<(Lzz_8&%Pwol z5B^l+K=Z^we9pBI2Wo+lZ*R_=4H&iReKVLn#8kHt4%vpMai>v#kNkrEedAoes9kJK zv_90zY&zk7^*b~ebNA7_V!d`A1we&642j41r!4V}^dd)q_kjK&c-w{9|X3g^omg1?PghWmI+(5K+gm zvHg4oM`X4RF&1sg%S&uc`Zo1t-MV3f1gGgsGj;X5V*yz3NxVzgXEi)0>OE3mZ>9rg zyUgp_dT~cL2X<(SQM5W%d)vHrI%e=ukcGTlm@QkK&bG#<8~${Y8nAqzyk5i)SP0&+ zC=!^^hWB*cn;Zl_#ZnMbe!w(K-mXAaxVk?FKSW^6Si{oI5r;VAQ|z>%h;2KK0pA?l z@tVq~wP*6HG1$`E&{_Zt*uQ=xxV|Q!D0b363Rj#$7vVw z`*$wIn+Gks*)MBRx`H(VZT9h*W~kd0{(#bR_1~MM`Rn1s>D}Un)b$XN{>uEa*R_2Jh+}T;10YXOOkrL=mz**Re zs^A$zB7BEa-R?p@Sj@setsv_Lv32;ngRM_c694_@A*4f*2g6bCbq&3#<+MJ&MvQFA zyYy*;9|dD`@aWOgRU(7AoJWrnL1d|S>USnL*8L+FtmB9UR0c|gh}74a2tF$B75}E# zFLneWT6k0gj`0;JHeOP6#I8R=w}Ecg3J)lFNdLv(v8`lhzP9_7Sau0sG^Mx7m^$4X z^R4((795amtr|yU-d(J<;A96_R%T9+uY@EBPJ8(S2&9lQ3tVBey3(>G#~@8FS zM&@q=PLAaI?^WER4~BbOWpoT!*C3=kurq$llhUhO zOl-J6aYoS|1~1M<1!|9=a)f2#@TE<`8~_9!;9iODYv!g3-86*kdmw$ifC#5r*v|* zvMJ34@;f9s9oOOWUi-YFDxp7EE_-RrxjRe(^GfXJYOV`QUC8}DE@|E{M2PhF`?5yU zgGL$oo?eTP_Bg0>92B~=y|`oRE}s>$g3^;(LO7a);!n_`C^kj8+bhfgkz?nHHpvV1 zDc-hfMG?MQ!;ye&s^$sE6X1=9k1=r)!3=n-MB8l#1AwT&48YI|1u&GV_noVlYaPXt zv?XWX@D^2q#~bMw84aMbY(i)~I*{kdh?kIiPbxLW%W%8)__i)#xsK%%=-lflGA)99 zSD%{jB4kX$;1Fbo`s&c6t~cOoy{PNZ10T0nj)FIh6iZ4aoxQ7SwHOJuNVaL0RwNl0 z+*0tEy3@}o0Ci~LnEhNOGBGix_`>Pn1#3TuXy1!_{48>2#sncl(J8y4+~rHf^-amXH*NKE3?i)hY?`~g_Lqe(6C*N3{ScK|#GR`QPMeW;bP*4*uxLl4sYq zl{~5j|{EO@K2F@Lrf6lLv){}_0WDp^Xm+fjMPQNu7g__$t!>*Hv1#A3V&U;l-Qi`6MfkzQ$`!V( zTWLVa+j0*F9oD$fpYL8zEJ_=$H=@`ag*^1gB8ghoF2Hy;6*nUJ=a*m@w%R^KOO!Z> zSBJtCL)h!9KNQ?&;S`4Pm`=FsQG9xWC_^6G$U9^=t5tMGtDlf2!=4=s3sj`YT5O5g za|tEH8{!RlRx#cf?mF(bKv(a`Tij>LbVp(S45cvLqS-n=&4iR1Lj19B6wa5IKRWBB z9tpkBv~J~Y@DCcP&yNI`lv7AKb2^#=I}qEW(3jT$Y*%ITHHLBX5NML7`Xz#dmwk~K zd<@gXJ;5(W;QYw0sfPRqv0*8$)y<+R=0V2_>CS{=uj`-xU$&DIb879U^0fHTc|%2^ z!q=7ash;e^&3w76dXcpkJoN z?4Be2PndQ%w#wCN@enQ=%bi&$1kr}pPH9;cKFHa}ZM%YYJVFrj$J=C*XSMH8@2=BM zCdDe(cHUJoVp+*SsfObpwPE&~f~C;oZQzYSm*QFx2{naIzQ7%#3QQzOd%BM^<_M@S zAT>_7XTl|w&xzd_kDS*YGSXkeTdgcm@9B;|VPpO@Nsi+D1D!kMqvLRr*Q_)Va-{~^ z0Ki(0llhfRFsF1x3DZ@ywG>4A=Z;WQVwYg586*He#p$tVX5;$_JYHYfGHEV`rcv_z z&>kNM!(gYJMxR>9B})vr1c$z7tO!@p?*|ntkXM+pD#Ka^LV%-K*bk=c@>0&qiQb=0h{76I4}lnO2I{B0G-2SEUB>B}%t`ppzJ>0Z<* zFyPU#ZR3x7e$vvAxL7n*lgPT$V2d*#fLyP6@C~1eGKJ^2A+fdj$NP41#W1B??}j+LTVk4z zsN(Z`m0foW+$^T@x^Y2<`^Ymi>(8QXmj3=FBN;4=ZLs|4Mm`W=LU+mZQ4AApCUK94 z_2$ZiT)_oPNy7c9lv}`!Jspg{;_t98Nh?kk_E6)^ZCSA_6W@|x67z(&>nh@FN;27b zg(xuGNg)KoaHHt1u16>ZD1u$dTev_F5`spN{o(5-K_P=HxL2L#G*2lvS>%_AMbSd{ zC*d_tVyTrX5je*xe{%kVNOnwzbkJ=BW0wC}ympCc6SE_u(u53oQ|1}fHKaIailC}9 zw^88lDTyp=46TLnMple*<2#jCHy905lCMO$EILG+_imFxy#2Oe2PJ61puE4g3s9|i zKg!x{juwK|Th3M>UnSLMmwI}L+WVt47%eWGkZRTpej)$%WqKO^3==1h{HTTA5OW1s zU5-1o>lr^T@DGSGY5!}IU?ta+{)9ZK6gH%rZo~0rO*Kbtuo2PPS!oz*I^?*gM<7il z#|Gn)a1R>8*QQG83NMLB+799`?>o`(6AqK`E%0lP)o#>70cO;%y32fLB}FFCboP~p zbRc7?{0!PXtjuYXHSzU%s0cWxd1W(#A`f)zP(NYXPXa$V(VWmQ-y%hlo~Bm!6TB!i z*?sEO+ht-mLYHs68RH^nz@<{;)y3Quh)IZ#z09g4=_TWO=6P2v&*V(fO1sM@PE(kS zYx~0Ksj6a--)N9C(|LF}!0tU7P2lQ_2E*m$m70W$8U}Id(Y0sZC5=jeo-kE9NPP2K z#zg&mB8kP%Wq?uiiUyd>pc920lKbBoEU0|bSF}n*E1SI_cm>kG&pm9w{p$TedW*I{uhu=q;*s9-*2segmz*uW?efJLm-XxefCU-@V=KYSj14y^xzHAL9= z4O1VkNQ_a6hlY9oWa`{1rF~Lc|K68gYgi%{9oM$xj%Zj50{qdIkC)1I3Ufx_$YY|G z<#@FcGq7H_J=R^62U?JIq&)e-iy~o8wtdx~i^ThA!`3N~C+lo=w9E zH7{o+^DgJTBS0XmMBN_0AAyc*u5q3qdD(cqV|GP)`N4P%bA9MCHSgH;eRrh~;D4hK z@NkKTwPP*s5S9pDOzYyDaMPqpJ3vTUuUoXART10&ge?$9s!qBr+3*)174M`^k19k? zLbdy3J8t{6I!T>2&a|h!4h|PA$V9u!RvQ5v&F359)xH@PQI_#D<&0|wTT;q7$ZoTC z-Leix1XwoEyZj{ZXX1Pj+b!eO%#sMLEoWwe)OPd`tl@vKwXp+O3dl14qG4#4#e8m%kfBA_9T zfJyJsO=|Ma(~TOUa@Y^u+r$yNj#DL{;?;Llj-E;kATz#e# zO&;5tFH>o`<7nD-oPGgViX-X6vpYKc-q#&c@4kMJb8-hJy*yL7i1F-W0DB@#wY>$d1V#+@{lPtd7TNn_UW)coo$RV#`^t1MPG2v zmtAB3S#fI9OoazI-(4$fgszk(sbLN*e6^o|KK4d+~clb-tJQl1Vpp5J|+6e3e za73}`6`BYnGf_If`9x7V)c|`Fwe)kGSiF{AIC@^EH8R%jGDDA4X(3?c`yN`6Ia~`f z(f8+!JYsY$b#wmyJ!4B|H?z081s6j& z_td$1UcsVp4^Bp z-{rwt3$^?*^?wGXwwp`|a;*{~J1f47nx5eh^~ z{#g1pPe_o2Nb>sa$3lsftPE>o!|0_#;|f~}WRm%_0)G*yCf$IBd`4#h^IU0fm42EE zCjxz+ZhJRLy!rd1UX8`Pxy?JxI2oy|pE$K2C*16!G?`bTTR9R85S6lb-m4565xX_z zF>{>!m``xwM45!n@)V3wFs^XTEB4*nWdwj#ssud@96v zFAns$QC6e>byWkl(?EN;;uI6OZn$DPs`9~$ypO&@b`>~zmx{XnxJC^FarZ_}a^BNo zBhmX>n|bGdt9#Hx4yRC;X1{@04yc+iJ$8Vc8}D+eH4vPigkbQL+v6)nuqbtJeGRI5 zp*l#E!)pslANT=#wT;CZ=oq#td5(ug3gC=LN|E2usjQ4Bc`UrT*{WeFUSHc-mgUuV zDazrDh{ke4ETI{z_%GBfpB9KGV`HWSli;XjW09{T1#P&QO?S!MPgopNB(K@(Q^pMv zOVM9C^jh~WH@@UNEPJP!yUxHMT+WG^Ckqh?u7%8`66k|fK>BpjKt9Qcb}gMD>)%!| zx54&0b+<OU9Nv`PRu zK*qm3j{JLOU;gp1Eo4fH#}|X-n`2L?VWxb<5D{{y1YriCodsYR;g=TFW1RxBK;n4< zxY%edhaA5l15>Hq-RpunR{&DKqPz0sT*)L>c&oNzYXjjh-vo4oXNbIE8lL;1_J=-T zdmV51k-duV9d>0Ow({blJDD$bq|U|X4!{5u6~m=m;9DsJc@Bxo>Gbb{E+C76QloRRI$@2jw826#j@ZLMw`cu1gn6L)f}@`M zEN~j!2uCk>-DqQH8eE+Wok%GAl_L7LZdnJeH{ME61Q{hVwGcoW^qnM?j z5e)&5FGAoR_ARjtSMaqwHe#Bm)YZTo6CP&rRhoUQ4*1OOTW9Etv>5X5*%j%9m~jSu z!T$KenrM7-O3(K;2vm7W?K^9PsS|bXZp#2^iB6eQU<{rV?R7_F+K*gZ03m|;9N=*12)R6D*XZX?*x zjJ!p)S*2eUHJH#81Z~?Un(3ShVi3^AZJQ|9YM%Ay`3Zvtd)}#VMmR43&DSeUtm)zl zUAn+YNTjj)*DrwF%GpW_7SwUtgF5E#0NIYroe zFg_`z4f;QvZ6B$xf&*2Sb1UYm+5SIIish!Zq};I#tR^y~nRN!*5nPjMkWjf3qm-Mj zohS`GTheexQZN?^@BS!gaB-)8-YB*|TgF84g}762^QWU#G^)e7HuKkSFwS$@hSY?F z)_DA%BuM&ne4NrBmTb^1&iPYRGO)!$lZe}Kju;afu5qLNrJ0Tf%_Ohg=9K{KCh-Kf z*;>+su04ju%laqBWl7+H--F!inSF}vkOn>>u90hqq*f6i7~_u;eTz{b2IU9e9qOan ziCZ{?Wa1~U&T}b&!M!9sh3=0PDJ-%iZh+X<-r;ScNmi4(SqH6|Z>hPAx;yfLMMxpR zjkgwlqkw<^^IaRmyP>XvnGEZ-|A%ZWR?{K3GIup$KpGUzJegz9VOJWlznrHEw^d(- zs=JZ^PKT^h89My&8>+*Wc?%5i$Tq9hyWWc=1DL;m8ZEtJuQ`>=@G=;&gqmK(2mIY= zeR^>`)G+2I4u6y0w6{9-wuEj=#J&{>rA%OXy5pZBYV^4qLaCnZ4mp7HXmnnrgaaAl z?R~d&SKR6XaG~_}qf16c(-4~wrVRhY@JhiIPG7$8=RU`W5B{>`?12 zYKlF=4(^udW)KYwHIjy!=>WV2kvMttotGFtU|R2Z#0|0;sC5iOT53PVDfGNfZ}{5T zLqXfiszlwT{ykM9yI((02+y6yP9FFr4jkf{<1B zs=U6eC??hox4Vg*9ID;p;Cn+9Ko(8aQ?b>~Krt!oD0onf_BJbx^hML;@yK;nGOEHU z@h|B}#8^gXkb(@d7?&!^94T9Bt@o@D)1)U6Z_U&oz?(=o{)RoMhJgS(~7InrO+%HWz!Qux(JR4H`sKE%7sUio0Vf&*Zr*)J45!+X6{rFB-U6^kHQ6bJ)LvlZz!}?(vx-oARHqI;FHq4 z0S_92DcG@JbIrV9AeV+-9t25rIJboB?z#$;LwIWR|6<}L>Rp^(Jge)!59v_52;q=N zcU(PhywCX=L(0pA!bYyvPAtZb!AGA6zO#(i2b?f_Iz;d5)LPn55tw!JDRF3>Ash%j z$xFAr-MHjSE_Sw5(ypB190{mgXF6)*=NbyL8bVuw!F2>5+ya`y$U3@O^TZvne4{sY zTJY_AA7t7uq#=JW6Zk&FxUUW?5?$4*c#z{j1=QWL%Ab8@+0QrwnUPp&OCedR9-L~F z5S^-*V`2ZJ&d|^WlsGMQ*`Xb=e2=`UOk4c=x&s9jrGum zM9*)fRHUNs`zf7>lN89*@p1B6Q#6*J!1NO=^{9Nal(`-f4OX{L!Xb{!6{kGIW#C2k zVGS1Z^^ntP!M(EIN39s(=4B z1UBrvVII=@pdzCENb+&f#$8I<{3dZr-7i2iJr=2>p6wZ4bYJ08m&m$*3j?O3K)w&p zE-4GMHB&^tKcmO!AOSY~%alA$3_Pd%o;*!Qj})j(vi;Kj8w4kvmpkmQam4qprUcIj8+o#CTTgO&9RkrX;wGN7R5 zRo#`*rPexfrQbFa{iA$pxna9h?`?)Y61EJwoHn5`awFH+jN`=>R#G$E{^Zt!*ioAl zxpb1^!)CA?je}t%CJ-*}sm>uzO%MY?qEz3t*;;Q2XMDB7_~io`8OC~p1hW!|5-dfR zfBmp>a_{i)iAyqiR_$udaQ`y`4uR)ouZJxLb{bFM+ms^s`ub?VggqRjvJkrkxJpYn zCC$j?pLTxf*>5V{l(L}!*VwjJ^9jxSuU7KwkEzr(G`9f$+J|1w)|e|7Tg{-Jp| zZ6Kmcb~OWIRWQq}9 zA(ojVE^4S#E~@8_5`vSeGHRarnGzFD0jwrsl?CrCaKN@SjF;&KAeHgHXlZ@oYL}bu z$vlahjDE?*+~5OnysvAm?4LVAQe%$yJuKPNIsvqlb$9CeIOH6~9SjhKuBH4DV) z#!^O*#|~tFXYHDd=@Wt~FrkvCS)4n& zvmvi8J^QVHS`#Nq8Fk3d!#y^8H-{_#qtRVSfGTGJL~QqeIfyU!F}{o_NME1~{xN&K z`f6=wc-OkNwlg78`kMTbr}Qo=UH-%IX1D#65o>BVFU~I>Bv^RLydTOe07O6~op> zt0-Jry`Gv~D5UJI0$a#TwFkI!?>MxN#1^IPqitExGa`0B*!`|Yk9nWp0@J&R0aI^Y zXDH*H@9x%Z)gNnv!nWe%X}dI3I8rC1@JjH6#8;pMlQ^?#z529q4Kn~T)F_1c?FCYZ zoX|vKoxSl24*YuelAvz17o2gJTq0@R7bXxNDt^*Y$~%sr(sf!9f!ryhj;x6UOQ-I! z(RH5l!UyRs_Pua;hpQIT2RWf#CP%Lwt~+T`FNSFTCI>@2)^yh9%^nP&5b%vY++7|djyJq~Moxp&KY|8<@Sb529%xQe+ISa2-tph_jIDP;Nq6du^C?ELh=~Sli_ELe) zfwyL72c~PmJK$TwO$zkR)h5DwG=Ka5l4K0$4yX|4b82$;=$Tst9=E$XJ%BM<9&}Zv zyQP#@T}jD(1XSOgbtVx=Y~rHRoz*OPQSA#5&t< ziWNiCjfgSxXm(tYhSk6%dHm~9(j7g0^!5RTe`pU@MZm|bzYy#lKEHyV(DVn=Ps%&y@O)EINqEpNmST#Ln)}(9ET2{eHrZl1W8PN?VFl)+ zT58UH1ZYLj=3gk|iON<4jFHYV?q|M8O#jq89=h$zVE=h}TPhTAbjcRAR?8+fu!q!K z-F!%vAeu$;c||X@>$}<=_V=y~BH(`UqCJC$&afnVvH7d~m)R%DBvmNxbssSWENiFK zVbR2>1u}SM%PrpRZz=*Pb*V7+)zUH`XhxuH)Bn-J2ZB&_!cAf@2Lu0=Td;lGH!DS6 z*s|ISrY!u7OI1;e3{`cckTRM*IUam%8uZ>FeBIjD_Litd_r%A$_}6_ z*T;NhaH0>gbLdXwbj(#5oRRV)(7@8dcrq~>&Q@*D+Tb1-A`JXx(%U$DCcpvT?%6Rz z8{`yKXxFV{H*fa$^w#QqOB009rBgUZ6LCF~jT9E~I!zpzoo5{^o-d0D#kR;Stmu;e zNn?U{yCN0@Hd%k#;ioeoIi`T+kKtwaoSSx!XaTYYd=%8zmQwd~FarvZrMzI(Q6UHd z@_JdPbLeq*2}9@Zdq2?1A#pMO=z}0QsUfzn#m~exjyFg6k=8`45UA^htUv=v>=!6&f6c0%aTuP_ypEeGI2Wa7f)|UaQ9)B-x4GqO%HIl#tfGykne9tD4>H{j6KK3NZ;^wvdu#kW_vkn>OyM|Foxvgh$;a zP`SW?GEEry&vwRlz#zF3qG7{oXw{_r5yTbDZh}=B_#w_9(94Q`7Sd0F2fD~yxpe+l z#j=|vQ#3dHDV}ZX&m(#a>lORP9X)lM%`CRit`T{Z^IM#pyS#CMIIC9r^PeRudy!QI zL)MMBBzH+~B0)n^pS1KyVwt3~coL_GDlYE+at1bLBPfp~omk^o{vo z$187+eXlx~`Ls7|HhOL(FV2N=xRp<6o7gd79)7tmCmZ-5QcOa}4lR22XNh7wy8wDN z@E9TrNn$!!UDcuJt{NjlM)zRG9Xm+pA?dQl+l`3aTd6F@{G~ zCkPbr>Jc7Xvn;tM%jT&I^|X9x1^vcDCCJo?*qms50ppN#ZuHEPY4oU6 z?d$>~TIWxIb^LKLhQS#l+Tcb~XAtBn)VFsoaLCb3torVKG42mtuk^XuF#)L#bcI!` zZmb!=o#+lv1@3R zkM|n!z|agVIj#YtY;A1q#5)IQF}&qGr`a*Cv=23Cd8*zhW)h_Pe{dWtLXaNbxdCaFAEdoc(%2$G$;m1 zz~RI%XBM~|#Fx5!R~=%tRXXWa>H8PCn{q z=ye8~MRJmDsUrDb+@YYSWeGL%nC;D&c2x@_6VrZ*%GG#YN3SK%ms-7cdi!Q{i&FEU}sEzrvT;TN;1AzC8sV4>)H6?SGx!1DXtaTP~w zK!yyNe5!9*CMEp4y->`k(+JALktv7cm%S;<^J+v)2L~d$d;OJPAQnRik&%qXO0b;* z6zSpU8rSEd5U%&V1x8|Lpc)&}_C{>OzIVjHj7-@3^G7^UnnTEuyKP2iB!)n^f$;RB zz=D)V8zJ$9p=*m(Wit!z-grN#pI1l>j^e&Iwjkv<)0u+Hn*ATr6)KH>*E#&7V^Kr* z0^b>5NXErjBkl6*>yglyJnF2&Ck)}v0&%$z1a=3N_eAclP z;DZoU5ZKI_NA#%m6*jC_d|X4p)+UIvmCk@Hmazu zVJ-2Q;f11u#UT>*ZUtsJ)j^rdu^^VN@HItf6Yq>%n$#l@3r42F-usFNC}+Mv(8Iz` zPl3TY&12|3r%Y&dM%MGy%(+HZJZ?FZdA=2~dR!c_cb> zd5ub|K)(oI6iFjKIGKm>Qv6sD_0lT)ya6C5>gXWzx30I zsDfUag6j~j;C)K>P#!Dnf-F@i&{S6aBe@k$-%WVjDKu*?B^)rr%BcDk?bOZfX+S>5l$7og|Bk9+$sRn;l@GEl%7N^I5Be06MAK}c@P1EH?*UV zarGq`k+b$f$8+Up`m*EMEuV|+5gSydx;>UPt};DCypNiAO#Z)~%IcgA< z5Fx9+=A(&sARtC~y)M>4lZ$pWJr}=C-cW+mn1B=Mu)$fR?0!!pKM#qQoPisE3*%|* z37YK9q9N;Vy?g~tj7Ri&k8#k*)0^7IXPh4n~lXehdi0dMnM`w9J^(Rt^&SB-R#&O4+sgY7d#2*C_;AY04GJIPJ*;O|8PcO+@!Fgyp4@ zG_>pkNmPJ+&S2Hv3QlCuy)rqA#x7BWE#N`MM?bx_*M|+!J*qNKi7W10mzrt|h`}NS zPrsyuXHa1_1^adSEX`s9Bn{6UY+amGf^xs|Hocj?k6N|d_*CAR?+>R2XW-LBGB3qP z#a5R`#-ST1XK@qm^PboY_qo&6OpbbpbITJF-oB{-fA-R|L9p4}P6E|ZdIGht6*CUC zI#H0se-$h%>!x#Cl=PBUssj@uJ^5FnfuD{At_>rEx+lz4<-4C9$tZ21(U< z%~X#xuHB?qBwjuF7QQjoI3!!2@f*Bj+tnrrhoqBqjw~}tvT4QCpEtOH8%JAUECa%8 zUv7Di&C0bEvv$-WU5?4h{a#q?SrHWbM%D9jK{iQt@sdNivFJjNsYNaIk=rXvrHXK7)}Cv?-Y=p{M-Kd08LV-$7SW3N4;(P3 zdCPeB+!}^FN~W`_1g8eE?&Y3MDv~`|Xqa;G0x@rk4{H4XSWE@w{FZkA0KP~z-?`59TffV^k=l8ziM;sI_P56mz;DTBb}hE@8y*# zPJ7Xv={?Otkdxx;ODoeU3RZURfTUyp8+ssaJ*7LUp59>|lg#^l#G=@G6^5v3Ei141 zLt6;iLONK%y7yha3<7O#OnuPN0?A;s89c#RsI0BvFX2_ZZy(<~HZJ0A5UqG5OY< zB}4K?T)g^tA*e_#SJy__r8+HX*SFb}*BuJiNmF~%IJ-9MDm!?2fpF!uwRm|(TT2Af z2L5wSK$Mq3R21wr)B@<0b8628DoJ!A;~a^-x>OaT;dO4N*-}Dk@ zi25pmG}MnY8%l84CT5z5-LjG=Z|v`2pHbax5w!e^0$z^pxaVab-kyv`@S+zZui_?r zCz6+<8tq!-G+PT=fjfMKoa0FL5)G1_fH4yH=?v(#!&#b7xd!$y%gBz!Hrm zF&u$EUK@@21Cr0GEpQ39YndeperQUlPrj)9YID166!5TmId@hX-Rb9sE%_UNq9SF^MmHVB0 zfgcM7ci{NG(i*q|;$%f&mDF@0L~#z`-(re{?@byp;M8YrSI0OR0^h|(&zZbx*DsiG zyR1TA+i3qj2rZ0XfC7qRlM0EJ#zrE=Fm5mICd0DV$xrYws7=6Lq^(FD3m}G8c8KYo zl`hLUkRo=jq;3)CAj7Shdar5~JX_%%fP=&8duE&-co0zri!F3TJ(ZQeDpHn+ z=rX8alNUbUN6H$hMNrX^^-s?PaViJI@ZM>NXYk7npVRjRz_9QV*RSlfezRoROIyDF z|5`RnkYl|D*rJ0Tc~3po3rbKYFW_tSH1yk zKOeuq&Z*X0h8#1C2H-Z>4BNk2kBXG@qx)juGWibPYRCIpPCXORgLwy&nZjAwXDl5a z=&N+P2#Y8kZ2#EXKR9w7Z6bCbsm_>zltv;Pt3I05u^l>0Y<(r)#b^lz`!M`pea6>n zQsK(A2EX@6ByzSMousCI|9BjJRq>0wnb~AWa6=Z1#3=p0-BQ5hlhSwi9G4?kwt?u< z&1WkHZhN-!_q<$CP)(6;^#1;HPBnIFCI2xbGvtlEGT-{0Mo24R4JAvN^?<5$4Rq7eNTy3+Q=tK%>|UWK@Q-Roz1WA92I2*H ztFK>he+}idw+FrMmxBcA^p+`EgGS8&<_&8uSdW^T6w1feJT$_v(TJNURiM0GNg+UM z!IGehiAl+48o+tg8Qh3THS@BeSn55q69KfP;^XA1l2r%BVY+?0px-aeV_I!h(&Khl zMEmwqVc+4W1?8hYI`Xe;+k&l$hcgKUu@FwolVOSfnhPDZ*lZ(F%4`tTyMcF`;1?Wau+T6yt*_67y3h2X~X z)J%#ShF9&|yA`#fD^E_mqWEje15cl5?3B$Vi|bXdU(YaI3OpFj{Kh=N#i5TG<;So< z+|&;_UW!d-XaMqyr#}_N$HEBKKg=*%PqYRcGhNd(lrDi01O_U9cwzTfu#pBqW+1R; z(hPk>=Qf2{8a{0c*zHgX+J)07?z(6qh<3{y$vc}mBO^u79~F_De9-&j#XpuBsFI#2 z$Evqid3K$U0~z<776`D!6cEmUR!kv#&tur5V>jOCwa*A;Jd|;^_rdH2pS+7iCYh>m z-A0Tn+2P<24TQahSK*c1Jbq0t>!@ctF7KSn)T%qAcau?7{}Xmsf*+k)z2XroNg89e zyCn7=cm#{RJkXSd;RRSPwlpiZFRcqu)!68&wwlk_%-<;Ln(a*aF(TdL=??z!{^pgG8` z!q}Sac?ZAC{hyMaCxx!HKSpcuFD#ndsB;*8@-+bJ_LyOsGK&_fdQmq=G^B^qRcVLn zQ5W+4mAD+3H7J6*YSqzQay2JB3h-QW1GDVX`;nfbv+Og-Pu>i?sN()h$qN~4tIHTd zVYYk1#q<_&B^D0gh(U`U@9tj+6?Vi(CSqb=ZONVlEl=`RLK?x%S6`%oms+<5FB;gK zTu}MeF&faCY@v7T1N-}CH?zZGH^yM6*`6YOe(#Udns}uU5k3L}#pL&CTm#4MBtU z9P^C&j}uLU4dUEkM;e8ot2QJ!Ho~OdIQJRpQZaFy zaqflBZcW}Uv_vJwcu`!Y^FY8B_DnUBuGt5z68~`|J3s0?h-v$b2yDCAmp6Fs%P1cK zmJTRafzf-oZty%n9bEd6Tce8TNmZ7;T&{JbD>oGG%Qu$cywhAN#iiDheN~0$vA9vS zjX({sula;BebVD86yyHX1^tN~H0GmyyN$9|(N75#>QxL{#GB0d@{JQUh0=}=KK*fr zJyBb3v($6bY$bR$eI@lY&w6yx83-iwc#oN3f$&2Ml+1+gcc;_J39LizrogA~d{_t1 zOo6@Ry`%xEA7}&Z>)r}bWTC16`JCkdC4^bZ$nhkWh!rB?%#1@=b~vN;b+$(oHIjU9 z@JXPKAOLCkll?Pf=O-F>_Z-e!bQBn8pULe$wYdIwJAR+uXbJ(umY)wD)^-UN{{L7i zg)466y~OhmrYtPPN2AW`jF<#DJlbb8i|mCFqYF>I?ScF+sv-4mV0B~0+_OpzOH%8f z-y#mom(_*(a!RfMeGk!hm+v*?(_vZ3slabp#*{bjSP2 z%4LUFT5s21#68`maV{;Nft-1@mK`JHDYB{ zg28hM`xEU9s)_Fd)7)r$PRY^Mf-kW`1UwC$>F%A)YrHBx$y$Qc^+fEN!m0 zUn6Hcg%MG~KGN{jl`m|ylNyr$G@v4vGLa8Z)xd@pGiO}kuh^aVx_CAcs*cV@z|rLa zH?HB%PNgMbg59Nv9`2FXm=pyk7cR+%+0`ck4p_e^GNx#P4$+AsIYjZKatFq$JT;H} zWL-O2+6DJ%WR6UPCgs3&7POPnHYndm!?@z0EUlov`yt!Ek-lV=>K=w~LP9fPYtHGu zajqt2cBkpv)+!wDcBvJFb7)JNVdHZR_oYT-3GPXKMvc8guSV z@n}K%RRG|hcof6L5C=s zkDnDR-?3c)9oxF_>_EX}_{ag-jOldkt~3XGP_aI!&x`yC;j3OrpZ2IZhv?J;OO0fq zt@m_oH(f7faH8L5)GOB~aa0!JMENz*K4fNoLeJo?{n})2OqW5+&)P5>l4|J*42q#k zBwnV8Z?#OuZm$MzPAee;^?mEIIYM!;5G8mO`MV34;;S|%*iU_FB3gyt0n=TLX@ER! z5I)ginK@r49Y6)R$5qo?b%1IiMOg5WUY_fsXtF6UKX!nN8u) z_ceBgjeSt(;MY5g1SdEe?UIOi6d47HS!#_6+*4^T5EWv{vW@L|EENYei{FZ~+_WVW z905kwQ07!v3CO*b*Gj00u?x$$YEv=*NlF{yrN@bZ40t7X?m|73P5&tJma=;<6>k4`mpfDp<^@x0!w_=s z>YJ`wHa4&0!Uml+wYG|HmH`ELVi<|$`wMC2_bef%t4j>4QkV=M0N z<#_3UDitmCLekceM}_4Tq#(KF^^Ls+w^(*zx5TW$NReeU>T2=+t~Fdt!qJq+2HEv& z&phpVsz~?LT2~VIPMI2&OI0Yhvyt5aDjj+o-SN03Vz5$V=}$>-tfu7{Tko(YQR?ENxQ%ApIOvYJy+qqy!DEMP)a zpZM+}RIcG)mEhp|ZQ28A7u&Vd_L_#4f(q^p#<|G6$B%ph;wJnC7+pqyG9j$Igd>XZ z!&lAGz{40tP}18RtV4v%mQTy}&8@mjY_0Sy%KP^NUqRvK0(m4Vh9Zv8_tL~%)s=>v zFVPkE8Y6mkoR7B#y3gTAY_FOR0YV&nsf$757yh&`4Nho;`vi$P;;wp!o$w9E$(_Xw zV7tL9Pxof9NICkrb(QN$cbv>pE*unNNU4UYAdwXt4U&;NR!E(99Y0uV__I=z0H0AX zKKs+gng%L8mFD;Hn|Uz|@Tc7Z{83zRw6YdWp!H}CG#{>9G5-!tJScV&#JqrWj;&G2MpT}!Q5f8omm#cB#Pp~iM{-BID^-eT< z^!@^n;p&?&+~4~SjfJs0PRz{xBCm*UoGh5YoDaKELKQoya<|rW%lK1U5mB(ex-mhkC8fK~7v zrpb`5V7NZV8I|vA&~o57vuIUx1^a11H91L@xFX|9Xc9VS+4TX8M`9;C!gcW%SbJP1 zO0n!df!H|9lQfXuiTEquoGAymb2Pv2fGer$(K`1Dr||1mZ~`2 znpws%ov88^7zXH%Onau+Ob7(2Ai=iYpG2n3p^jnuq6$UI{3AN==EfsCCwB+Oq&7@} z?}i;4wtve18Bu}I0b59n?+S0t{5DrQo0PHxO^m$P}+g&XWU}0YbooG_9!JB zxxpQfT8%?_3{Cauaw* z_AqL(ww;Z$l=yp)GGF+-B2_-ygr-XuuO(ql;hTFv3WZL##nQw|uL}cRN$Q-rFWlgR zZbaDgGzy%Jb6JYyROc`7?u#4IwzCCjsp!)c*~Q-tnwWb69hcW|2ff`^#2;|)fE9=C zU|w!ADo=OjmhG@dRg!78{_dlxw;C7{NcO*y;-G{p^7H8)?YL)MRp-&|tRlcCL!1KOALdqb*U%{fyIB8VjuhDKaJf+VN$VL;pb-$T=*PUakX@BBk{x!4E;+?PEYz-kh<$Q zRess{LIBa`jDM6PeLxV;*@y35b&u&I(38|L9UnZiNwRiz2O=6Qk}irEc{^o2C-19< ziqf0)5KPJ!R2zpy`)%c#X>u>Iy!f~62d)eMW^U3>IhMddnL zh2@rzg*0|$w{_c)0>p}C7{HVAxcSe|3JchG^Cr=KEqUn$HpZ(&oaZq=GF+wQit~&Ov-a>y zmZ<9RCDh%B6^+!H;6>NNv?MT%kO*g$u6QO;Vgi=^@|u1w zteuTKn!25XY@!*Y2hRDBpK+fK9S2qSGyYj_paL*E$G@;VwTTJj9eCW}gFIOWCWp?U zYI-WjDVl+{!H71n{+@Ukq0-%6QTDukaROmyT#AJp{RUtYWP^ltHSP(Oyzn$9CY@ZG zYPXm^FI=!>>a@3dDIju4UKohXRl&~k1n)0(L?qwt+X-w$f0rd=M!cBCr@|3XVWltP zxlvpO(z(sPU@Jc$&56Y>xQ?$~Q+~^5o-jBOwb)~bj_^#9Od`eI;#cexBoBg5qK-t( z5v!CzLv^c(sB-T~pbVkreN?fj)>Z;8<5EI=w8C~IUFb&df%2A#hyx%!@u|A9!&L1J zlSuZNUKkQ>lIUsUetv!qO9#M<*YNESdmb#?INROeM@%ju6O!Mi$XFanBB6TqY0)#k z_Od?`4!@0!FWd)ZP(;#&Y6tB!HfMjXrep%j)qZ*3$niulbf*}P1MAX5Y=Kifp~T=^ z0>^id4xr%k5vO^}zls*dE~`PO^cV;%@R;Pt{pc!0JzI{&%u7DDT>DnP$h))@dGz-g zolX*j0j&3jY<`P+q(#yB`qOEZ7ZQ8b^5oF2y;SiS)Ze`QSug>^n5rO5sN&iH)1uo1y0daH0Z5^vANYYu zlx^a|=>n~RA}tGq-8D{LoPe}W%KRz8ht)oz?)@?=4g42zA^e$6vgP{}%y_mjI;_tA zJ~utj7m?X|KZbp!=g)7`5CEx3@9EmBgbRB_bQ!$eS6re$gTu<%GTcjL5HHuG>2BH; zNi&9@noduK*yHb(IO1AZfd5*B2mm9FH-YrHKq|({rw?_P%y<=LdP6A4tg4M3t=ABt zUYL!Zu7VgazG8j;)}l9m4w`!B$tpaqGQ$}9=&kBI$Mv{p$=F%U>c)#iw`AH zmQDks!L*b#-l`UP{)9#AzF`1dLtx zFd{*j>;+;b-lsyy9PNieX!!D6c5+uO?2~p3Z2M z8HQ+OMLTT`$-k_^Pi0!npmbB5=Wo@TwTULEjqyKe?6A^2M2U(N3rl^v3*~fo4ih;_ z^KZq9yhBB%c6e0KbG&A&(leqyg(EYCEbwW(FgX6v+G^K?t_T9hR#`DtMMO3`m& z1@!9Gv$blG5l{DinV{+BW(!s(%u~vBwRTCkj=TwizJpyS;9FsT&_F9w_U*&?31q~K z6f56&$04uGmLod0vCtJblacK%dy+`42DDkkxymSN@P;LMexY0*E!sHb&*qnZFb1nZ z&F0+wHzPyDx**D!e>4Gfn%V~0aCw|C(;I3M5cKn+jhXewz8HlrghO%tA5saJ_g^+e z@gq1aiRxnTyXiem0Cw(7(uO=7(8^RyJIt3-2KH}Ft9UwL#Jjc61gE`JES}8MLp#@w zMX?UgZeE!Lg1NnY082o$zZzlk%0YbQIqXBW(JjnC{F9kYMABpN6z!MGV^qGM@Od8x zw{WQMzS)tH1do&L1oDtyf*EFeEZ)y68PxUaikZ8~#3>g+oeufepDq&5J^VjxCf3G= zZz6~RO9;)1VtEqFQSnV{i;#aGfB~-9Vaojb+af~>4oAkUV^H(>1y?y$ zNWAY?bYMR>Tv6I}!sX%zmYA*zPps$yP-}4yt6hs(S?8`5Zr_?zdQ`}i_lOxZG#@1G z(D%}hfh!L**y{LW;vRVf#(H`5G0%lD1;4k+?{7|G7Yu5H{IzXJN--EW!|juOD2}F( zc_8D1X$dmJm&c3Z7*{#d5p8wY2BgDx&<3D?ZLqtK%K5(y(hUuoCBbN#+5sM&)5unXTF52xbPALcROQ$ zv)p&BLpd+VFf|&A7KP0d+E`4HV&iEX9nHz6Mx4XUgU)3p2dk|fC-^f}SPz>ZWx?qq z8X_&qB0>q2`{a*F$%1ocR6H%?o>me;9MoHU1g$ zB}uA!{xW?ZrtKGxPlysK$UO2#@HD4rSZO8Z!}}vL`;o^L+}(3tMT))O7@B9(2FeT> zB?XkMx7`s>tAMOV^YyuswNOFo`W0QAl>q{L`{#O+J9ryot(3KLZ&Q%{C}wHnj^3Y{MKj=a)ewV%PACcD#>|p# zP(t0Km!-C?)DEHu7rf!+GC5n^6s)^PL~5xp843lt1NDZ`q4pFKnmNkFuG08c1$1(t zEHV1=0-tkNUOgw=`!uVIl>Nd6P{hd$T@v-{;_jh+bH)U)hWV^$dp0W`{`)U=8BS*x z^iO)P@r5vcRGfvd-N-XhzqFQUw4MD_N`lwQHfEK(RbXG?$rP?|)F@aC{J&xaf7Gi4 z2DBXEKDV9&F;C-~r*<_cO&QWnsePVNUdG3>=E7|OKCGqj`MRUJHG-kp z8Y?QTjXv)HLmwMudfA1eM{`)9#Bygbiv_rI9)7Im5HIbzwb_rWu8Q`p^H~n#dm@) z&aHAiNdDgLylZ0BBoQ~Xy6=#te`g$|r-JRL_)T4D?+`9mCCq5PBCf)x=<$3MjIyIu zM+W7Ozw9N`X?GiyQ=b2$)CuBDc6~7J1q>b@Y{hngN4huJ%g7xdu_)u4<{{DQ-QrxK zh$LT>>M1^8N#`{JO7&z6Q@hHcEcGIbLkQ{OolxP8K_fqQpMC_}jy6b#kLFt#vV299 z<`o#4zJb~U%W%ZlQfimmhR-ByF-`DnUT%&wC&pgWES%Zrjs^C&S4{`n4b`%TdOuh*$0)`Uwr}zFj>H+bT)!nIDi?m+T*p#P zAtv>`Uab_LTA4&iM%`fvGCAZ_SWU07VaFT&N!qa^=4z{5>>$>y{r{YFVb#MDM`*i5 z32VaXE>(w=sWFcoE0SoZpDFxf^p)ieFI__TlIAuP1zT1<>SB5v2$-s9^I+a`Qa3@# z?eiEHnKyM3ApY46%f#6kp85PDM_$<~Ze;8F2rGn_4y;d_=uYAI*I*t#hHs|#Hf>&F%%RVfzjXF+9>YYl8gP35c;11aa z)3zFC##9`b!A$t>_ zYmEsLPHFc7yc%T&783!<{=OS;6a^P5=*7lNTU!lbN1vR4$~I_Pxs~h8?0yeaaOH5c zg7|zn261TmuAyH3v$~cBQV_YXWK066TpvTV>VLaWf)Sl2wf>Z`eY7ApsLHL@p*$4) zL5i)a4>JHNU~H;;9QhI9FRakw(T{U(`@1tXS@|hnSNx?`sO7^JZQK@8N&K~1=F9e}pJr(1so=+5n1Z7uwwCQgH zpEYPaKGWK&E3Ph4WcQ2HS>;cgA-r>JX+io;_&?ekNho4xy_8}>|ARD2;=a~C#vMfx zz6o~ytIRhhDRtr5r)#Nu!whjJ&&(4r8YE7DKkC&EL(`xwI>L4kBF6YNIeM#`qX^>P zKrxB@zS@4vdK&u|-6~^i)lQE=!v$ZutO-GJ${jr6VFotFQ<+Gikd~H!s!glRY)w(xgWRa@Gkq~hj z`}|tQOXXZvDia&F^j_35C{m~RRD3&Nbde%5~4{>{%%?iY8TvGX%99 zh1pC9l`jV)^rGo(GjxUxJ@w~9wMp`2KRdAC$0AxED-3n_`@${ebh1=>xu_47Q@3q@ z^~kwl6336Z1G0hUS@gwn5pLP@3PQ)oB`YPKojMgKBf*2fxzN8!zmtY9l>Q*_lILY%Ve`*~ zPP6Y35f_NSf}U{39X~0-M```8Pdbc>#`ipapomky`J^8fIc_*JbtO$vxi?$)M`Lr1`9t| z_J)E$F$@eEbH0SujSmzdv06N2zGZp^0&$3q0jrAJ=)sj+LD1GKTG@B5rnLx#$2!7c zlUg?Y^p7XruTgAOf~`8Mny5o{+a@H4aj-3}5Q~&*$;A4~5kfljN~Ul0W`%Lm>KpEqbmVLZAtV|h%QnGPIoq|(SM#9@gtyXW6JpA znp(`xV;5AVY5I-X_BsGLXP)_Z^5pDBNKOq*PXc(PqS{@k;v^zFh{P>=+j9QGVIDBtmf7{5e_1cLxD+t zvUSj7M>!^vQqs=Nd3z06a)Yi~?OX{W#g3Mak07UQusXItOh`MW5|X`HC@kL-O%7^6 zUV`sW7LqZ36bNJdtros|;mI|NEQSK8=EAR~O}|NKeWUaNi5cC;QPU3kHqUl1yK&Y7 zDFjtLqwhB3ogE)bI(hA*SAHC;yQJVUQ;p*S3r$<&B_@E}1rx|R#Ju8o=|>|M+L1Oe z;#yfl@mxkzE7|+yrHreshz|Y}VQG9CM)Pl_Kw!{+Z7JMZ%U}hugzw93O1a1eg#50h zM8r;^vDB5RCwB01; zVsd!%v?{h(tjJE=3FT-*4?sb`^B;3s{9X$j-L$Spk9^ggEO7?}XorH*)bY4QTJJ>PBerSS5)TQWOt-S;v)S$6}py>HYL6M8Tp=-&M=HlNf9psOva6tHz z-NT!BL8;+5Sb5>oeIHrczY3Sf(PS~EBm$H1k$}ods08T|^>NjZ0j&YKr&FRj%h9H* zw(?30@1R?nb)v~N0BJq3n)l_ z$Z(67$_!9hep>}h*G&pXXhY=Ky=QkS2HP9#wCH6PGF-H=*hW1S5lJbx{(QwXve9BG zlHXybQ+rrh?uqTSeWF(d7Qb7yqPz}H-`|WAm9WA&E|ww6o4K)pAD5D26Kkc#&;^X7 zN4f|w;>ncQdUTpC6nmc9gkN>L9mWVGy@O^|#c?Z*7Xe1Kg1+(MjTKXZsoo>jAl(Y< zWg6AowtGZmK1=%B-Kd8@DhNDpf>MT;OdS$JS^o=RN=*EI{wfM`vp-i0Z?50)W{3uUjGB4<#N2uqXRm6+ z`ojcThH`Zx!#Oic=k5!AbY*B1cbodJt+~t#ru^>P0v->>GVJnOsBBlUh}NL|a#lJHL29*pb20K>+eM3P zf+(8xci<%K<06?U!t0^F1|bEnGJZSfy5JoKP8iz`)D=FXa7kLGH|cgl2sAupvr_Xh zkQCwcH`9lX$z*;ikQ=x4EF-?bt*6s(_i8m^BX{^1jj|0i{jn} zD4hOPNku{s-c{H0!mEc$A_$$zYVUuH7GdLR>OzjTupuY1yOVL$)afT$6#g7nF!7BL zs2M* z>>(uv5_2n;Qk3E@18*&p3ZH&2h9Uh7kez>84>c0ddir?WENXwm%WWa9VoV>wVAdn& zj`52IK#Q;p4sh~TvTGQ*dmTB?Rbo%L*svL+%a-Ft4~qM%EnKjkln!kO8k73cXXqfL zQ_-gI;yc54#RQ&_S~o82fJ$j<=7r?U7Y@#vMUk2lKp(kdUxAszW@}?3>Mpm+_bjMD zC(qNU5&RnQ)kRdC{urXbvEmnHF1r+mYEWSPxR{?q+YHC~Ezwg)|y zefj0Hp0`Dewslh>b?W?S8ht1>ndCe7CNk--C!4@YF1-gI}J9uj{K zkNtMrqqU9Q^w zigT`=vJ$Q+_&|_q;N`_2X!^2MayF1CvS9qXSVxH8TM2$xeSh?K+$(TFn9j_9GO&zI z{vyAflIJu3591k>)skJNA4r3rvNSSsi1TBn1dS78qp?yI;%R-lS?!g(NODt{xjgz- z>q{urFHR8Lh17lEc6%RdK2Ae+uuK5Jk6)6iz>u~JpMFvegFnUAuJA8$B^VD}WqYP} zq8#nnm)1!;Qw}@b`h9^zpdv1}u=n3U6@(`yzR<4_zBxV!rIHViTi2_@0Z3 z0hYYU{n>IKBa9B$Mciu`UMV$DOYsvo&J3*nq_X#i}mp!#$;93A7jGEBa z{zvTvP=0;~X2wu9FX$alg6X&Ej^Th0Qup;HGLzV86EMC3$FQrQVz4u}mYsf@;U-rSGifaKty*1acMx(K z{n*wmG!<>3tn|qEb)F36L{~q|$`SPkC$g-QDyl1QnGmcV(~_N_uH7L;WMc$_>GStaa*Ny_ z47>286t_F9GVGzmRvEI*!6kIJ`<6ye+-n*)_lieJ9M@zFGtPLn4aRQq3<~0N-!#T~ z8Jvr(2tu&s+!~b*UK<(Ve!~Av9(*hp1A)Oa@+eXSGA~nT(p`2)a{grqnahAbqQJw< zgmOV|60bPzfr7Cm_+^&}X_w^8>`kOFX{+GVc%i+EhQC_#Wq9?6ZRrQ*~so+|E(Jqr>bEoDY9Uv9!PbMSvD9 zaQyolHmBbpxO=~+*w3a%CS`2Bga06!2me@VC^W}x1P8!S%~!6v)aV8Ke1~E}|MP2B zx%GXQ7!84trJmqM(BEH#f#4n_1Jm7?r^3h|Sv5g@dw_BF0PXDTa=hw>s!Jtv8psZ(Kn<%(Tgz#~Z8>((E|achwyCYJ)%gW;KM@ z-axtE=I>R++IAIqyVvx5X9-)B63f-cta9u^>t~_$X(b5O;97WX`8KKzS7}9)bh^6g zVL^T~IEDA%?@~R}Z|j5QQZzL+`4qrC=}CI6nVBcGOz=ZfDJYM-q!0jNz(=@zdY8$v z{rWdP#HjrMi$8?U4|W;&IKEN&XO5hMA_-hlZ-oY*VXLXcc6)XoLVBRb$u(TT9sW?2 z3J1Yxu>*#Bg;p}S%k4laCL?*k_dK&%-F%qg~H)VvLA0UhGpVPoP`vhxw=J12NTs(%rkA-4#EL-{!oZz=*6xj{=7|M&D zi>*c>IeMABmq34lmTu&u=3Joxa^z#A9+M+r5e4RuyeUU2Irjp-r27qDHUmUAb?I$M zD~iEfc|@P`sPBxY;=BSX{Q3kVX}W0(o~o1=3Q{nLIJ+8rr>7e>o^9=(wn!{B8OJ-7 zxSmx9$rs5D#KrB=5UB-(U6KtKV}oSya%@#WV7vcMjnCy8GKI`c9z%U8)0NqK-Ww=o z{Er!HO0xnh!`yNLC$ox~_?tn30P-c45P*R5{xX3FnH83#RjnzqBAJki<5s_gVNpsp zVOrhZm|}aT6joQ&i5qsC;AzOv`JFS(D_kkbA_BX3heMv&>I4w4ot$hO#rXmq%dsZ` z8=fWHRhT2&_|+;Vo}%vju`6gB3Pa)b>mLoqUzJ^A@^wr-+dq9N!kmm0FK~>R`H(-M z_mDeers(W#Jon7e8TKA|Md`8_d{O)%V}6~YoZ=w*Uf>I``wujRV2zqc_(=s;Q}Pt+ z1nJ1A#S@93rW#S7R~a_9sZKvQrdsXmzL z;WJ0uwrYz|@8viF*S|qRQr^nvAPnG;uwdATwPcnkIOBM#C6J2$EFsnb?gY4p`%4tN zevAjtvtV>B=n&8B^13|C6u_Z`4xZDYy&LfU>vOt5ja8Z>2dcHZ@|b=$gQl=L;Mj#@ z2xA89t)!dFMkz5qUGs1Xqq$?lOw@9;AF3Zh9o*X8+cY>9YAd6e41_vYGXJmbtnIbp zHx9Nz_4)p)wGIiTK`j0AVtmcvk?gfBG*{7yI)4AlD^*uTia+~*xg9HNv+KWr!Wjw? zkB<=iE!~Rey9gmdvtg(-yK2p&5GzEm>O{N~@_i3xa>>T%i&H9F+#OmypV4C~HHaf= zEVsdYTtbWqk2Kxpc#4HS44Gi7`sSi6B?Em!?k3oq&E2wCU+6Q%MItpHx2~Zw%Tn&% z{)ic;fO}!|(d+eHY}UU3+d;W%QS66B2Q}R~%Y<_UF}1t1G}`I^{|KBxM&YO#gf$;}HGx37nR^NzliEH#^LJLxJg{3>op)F4~-FjFIZMIU} zfI10$;*j^5P(L+nXFvLAFf`{_a(BCAk@et^(kTx{w=3-5^Z-33-N6lE^6mvp!=&Rc zigdsQ!oWQvwdI79V98YuTgqT1hLdZ715TTEV%p=VeosqQpX_=yh#zhlfwhx;5;Q+! zj}@b+v%WO4w#Qd6J9?NRv7h$P5mEYczV$Qq-)gC6=**Br(8Xn=kDhUhRa$ zcAo;>3q90M{?p=6gkm0^WXz_^E+)O4yc9!aaPkR>tvLi%ZrQ}lgGxRwXyAUPDAdZw z4+A-CNqcFnZ3quv-=NgKiu9V{0MRPc)gUOX{~Qf1z@7#^bz>+JP#eH1>Q&#y)gLFY z8`(puDQ(#c%>X|Qf(-KRyiZl{X}cqlHewG?={U|wW+ad%ecAV~CU`tAHj3;D7rDB` z6OUoYIbqnU-Z&4*UCIlPH$7!=#QmoSf7h=vDdy-T2EAe1rrO)lKmvf}Fc>x7b(5FU z%P1wqy1bre$PY^gz`w|Ipuc2}U~c7?Bl>rGxXfFH2!eZTCyfsCr92G%AYR zzb8|c{-FTQo%cUzuhiF@9^0tvA@6562jt>0gLSdCU}q&Kkht{UE{!3Bv|{4ruqRs) zhd?0o*vnYAG9LO6PQ7d2z*Icp>urW+$iI0XPL(xT~X*8+rg=>}+{N zgh`VZtK4WFdZW=SReB`wy8M+val7t)k4W4k6o9wcHFQGMGa?p*}V|(g3NEqFJ%3Rw4~>wEuUH~K+2b{`CcxvJmWxX z%kcs@;g_8Qt(Tz=q5EFL9Fl-BBM0^P*%YCXR}{^c(mt)w@bYVtP2E$46<`9eri>Q7 zoW99QYY%7$p%dmF?^N5zE+J30R`rY&k4;I>XX`zrCb%qrEX%{)s7MJyVN%GFBOPrA zpo{$Cs%-?cy9{RAn zArs7D&<4AQlOO>4J8))FuRzmdnxZcqQINz~^NV3^B_O#ExvS-`$83tzJuC`w?F{Tf zI$#j6ecKR{bX^+`oYqv zF5F#RMI^Jog*n${GBU8=iE&xXL+gFJ6JHR_^5f}#V+tGv__ErRc}zi(hT-((#5Gb? ztdM9b@&~J&ssL5tnajUt+CM6dYroN>Qb4s%sWIlIVO;y^b-OX_HlMOrzA{ZzR9Ju5 z?X*4+D>M{v_+95a(H-JFMVfl&Fw--dD3gq-YPNGzIM5OOR8V?p8U%K3#J=X7 za26Iu>%HJUHrc>hL+9FQlL*t{Wkh{=>_c4$m5g5N6pY|&5^!sQ3M1}P zpcCy!u~#<<$lAO38$e7OzVP%O(Ra=o%PrcmIkh`AQ+`Fv z3T1CCP<`CCwpW>dDkJ66_<94dpLh*})RmbCSzm0ew)?u%BNnB9=pJ}o##1B8!_dO| zKM7vL+pWff$9Wd+|J=Mn{Q^xS;i0LfmC*#9Cn9gc%%W-3L9i8)MW3+ZMm1}eHL;1a zA0`TgHeH+}<{n5x`%q(EMZ56Kwb@A+J%p|Lq3Dks4jY@d^KykmPDOyB6ZG#tSjvQo zadk28031kNZ$;Tazgiqj20cpwN{k zS$^`mSatFV*@GoQFOArOD%n7F%k5)lj@`z9gPv!~ZU5+aTaq{~VY8vDzVf6aG(nA!pKsT>4?45qv9?d$W5x|PTP2>>766J&K1gfEqlB&g!78dLqLm$EQb4Vcmx~XuVA&XSh<@b_tkj$w= zGJpgS!QWs#Kl0m2Km++Q#~73WF`*vxK(Dq{139Q8+HrD|Ps=Q7DY|b&SW?>v`Epq} zfq_Lz1=+78#p(P4NJ70XjwS!H*QgZgJym0}~pD!D6hvh}nWIY!@k>~-%)Tn1N7?#B_$+Nid{yIjB5mlh&Xfzk>R;|y7 zUtTa?K>};9#9fw^bkL9LJH#5^jiPk*)KKLm^O(1W-#>GjJo$Aqnr6Xf#HSb*W$M`; zoS@gY@~fzpjzl1Bn?zgXgC~J&mYdyNLN40ZfDCh_)0HCL`kk0J)EChO-w8u4(fip> zK9b{r#Af}XBAZ!n9?>f2elCt){8sTG|DHRs8lP^1tc}qs->YL*bxipZtDv#_YOhf; zf2njE18ver7Dvwi8fQiOG6qDX@GtbEoQY}jHmEYbA&@T*M zi^#67S3V#-veo9=)qsXS67ur8EA?ANmcfL|CK2Uh>2lu4c1sA~WR|Ju`q>}IR;rU1 zqrBK=XXe9qHu6)>0^IxMM)< znAJktm)adyftdm%(o1$>os)zcVil{`_US$45qv=J#}=!I1|45 zLWgpJDnIIJmj~&GvL0Vf@Bjt--LPyI^n1+)_vX!Lkc{_38AydVXa;^WWzAOn<}Rr( zJ1Rit!3h=FKHP7g7)m$i!24lWYy|`eQ>pX6NzkP)6Sec_a==@js0U9_%qYYV=++X6 zTB#qNQD72@?bLZNtfFdF9n@c(4#+x2Q5T;Iw+N!~M<*-1#F^hQd34&CI6!lTgb$JT zv%MKS4G4E@Ld{O>w=g&=w=`E{YXxa%&bT;|qNCm<4@gR7m41~{epZzZ2rh+$mT zZ40o1Qz~G&|Y^SA|XB;@l zJLT9ji-m4r>md+|rR3yWRB6_BNJ8Z8CQ&}Wpq|+8*=X(-V~n}{uj598kEJ1g*Kaie zntiezrRaemJEVNc2lx4Zr^9bdCoA%cB|lN-@x?pFjG-)?Xr>@5^QESXrQsqSINyaX z12_P(n|b%`(u%1NPYct599H9Ajx+LZh7j{4WBmm!n>ac5CD)##jg0X4KEI&V!~tLHy?7a5|)T;k!g-K5hl(h--N&( zjG68zGi{;whIk z#*3BYw%KJfRLzV@Tpo3Svz#~DUi?)$!Q8-AM!{R9UwFe=<*VO&zeQ>e)Bk22@YI@7 zcik06So*R>qLrnQ@A&kRlvX=;iNsNo8mZ*TE+^e~qi9uJSIAXlnf1bmJ1Bs~*LaBb zBoit*u~Ogauv0Mp$sTTnwWPRU_J;yMxUz~=+vfLW!BI}3Ajf3PE#~%t%5+D z)vg;%X3he749m7$(X_MyQy!SA+k!|V5c|r{k@&kUf+;h8Y<=hRk+gT!bai9hX>AJd z@^xR%&STLWfKYj$z3VIG55A*VjY5752NkBIJp3AW77&Xt{Ikpg!!D!V)!XiUJ^ylB zRNVs%VZHWihAQ;2wXrb6AKJVT;t#qR`%JdJ`0TB;LU(11MZ`D3PK9*^u8QoS1WB-_gM5Uk&UJ= zEB1lxe~_0Q1#Bms2(VCqqwa@}UQZT)t$zXQAFJ2zqK{|L^?Zv3L_h924^lf)Ux3&| zC5qLiULlLkb7P3#vQV+V(oZ!BV*i2k)|(H)#kxb_&n6yaaEA2FG>RkSGzu_*oPH^y zCwDoe%g$@YdJGyjd(RctfXa^nc5#s-;pz)p=GcZv(Xk^bK8q$F-)Yy{tfeQOe(_uO{iUJA0i;D_JZ{&b`?v_a&ne_&LtQ*DwB#F zN&Lhx!LG2leYr*Q^u%7@s--n7D69v?e0)DIuvSV8ww9b+0&!LBHcXC$&=OIh3gV#9zdpitOVX?wOm}3 z4p-S6__u^ML}$O&nwb=s5p`nL+Wh>kn;M)Ra8t3{(Q9hyADKO)!h2Lf-fMImvW*%w zn7Fu#_jMV~TmMrAl9b2G(PSV{e-N~w)%%t=sH1T>p6#Ma9+8WVrVH&hni9Fe$@9WQ zD=&3Bo((wXZlZ0ghao`nL&=q*fP6GJ+_i}r=U}f`e*O`D z4S!peB?E@i`F*Bnk98M~3)8yp#kp`)AQt9I@-vYmGVbi=MCY8dvk5F+=1lEf<&lnP zW&N1cScxGz%l^^FA2z=zYUk9XKW2^p9bv}U=UX8SmI_=atjx?t-v-4Ggtms^u=6DR zFn`ClR3Y(h>rrMV9w~1AdasqPreo;ZGT$9ydtkf4TD)uC_PG;hkM#!zNLEg;TJm!B z3)P3tLrEZF+IIXwTdz5JR?(xOY~&xDcVd#Zz807u{v(4E{9{N6om#2C^u6CqZ$zyT zBZbp?8e?PN0f1x0PV#%!)97SEPjQ^X7_-v5MonRd0?T0CZ_!^$WO|bk#!9fFe=$)| zOZjR+PQkmSdw2LWIAjl#gTRZaJi-mT4!oySUM}UIbPM;EA47e{Txt%GxjQC~iu*CO z6>#g$#ZdHlv&$>nOnPe(C9zJ~0Pq`2V(M9@bj3*&MgYrfy`0}VezWy*{(a3Z5i#oq zehq0`c!2z42pjRWeM4Ad3uV;jcVM92<-h+nwFd-|$(b#v>+A}+TZ&6ot*Xf|Qh#5> zQNKc<1W+eor*FR=*b+S5y<>P>wMYzN=nfLEh0`Dz7s?@x^!#%f+IOcJ!x+0l#>9O69mR%oxrTV$)O4 z#6A%p;1{kiqEw=LRNR6G3rzexe*l1Aw=A^w0+$fsB>=4T$OhJ}nJfMUIwrfa{u$< zzXa4Vu@{8oLJAJNA0B)+8=nYy& zz~*n;i{J~C9JWIROZCfwO1vA1%JscXU`%>K^)$9VU7(lcm)wuD4ZmBEzY{%XuTZBo z_CZ}ZNUnlVMKIMU;U>GXjT1w}zmXS=i;3&#VsV}3VSzgliqHf*6Z$V!JK1HZNV|Y8 zH#?r>BnyV^*Y7O-SWsw{>$|ByK=7%5g}b1U z#V3;;fsbSCBrmC<hM8w<1(h z&P5GAC)PfUlEOMdI{ay_dTyd(EkOvFSc3bh|D=joZcUrzGQ}EIf;!Ms_P6>>h>kf+ zFXD+MKbFWnKQs3}=buOKqx(e&s#JsQpCF7rpq@79%a?#Z*cwG(H|~`04d5RcQc&u= ztw)3-NnJ+4X`>B4s@R?+j!xV?YM10a?{v)6j3}MEVH1_1T{i8Aw~@v<6{Jt6P`hQ# zXcITczyNP}_o!JZds$Viu+Y<3m5$4(7Kx?FGDbR z$*l1sGC6+Q!QzUu5fbN@+3vBLZD8~P_F$f|1wRXULGX=^b2-x&gA4j_YQOqrUZmiP z;WqUv1y-^jRoasM1`omi;88h)D-$WZ&r^PT_k!t^;r5j&kGxDSuVCZ_U2}NlQR{sA z0I@fKd5rruhhFbK9)_!!bo><$2CFOIs(6kHe@}HF63hKF2ZX?=PRmHJzrO1?tVCB{ z#9k9Q?`kkqb?KrTen`r^zGL*8CzIt>(39*ps0Eo7$r8BjS!-#!Dc%|8?{bomSS5&DKm4rX z*V`BePW)2B(?9s2_ZvFPV-8!do9t){%!r|gbb0{7i|`O8mcfe$_gD8qYuJ7K(s3Id z26pg~Sf+B~X%KIL$0gwVJ~R{jMt;??V+LLFV*_!-(?*Qw z<`4uUnAHtbwE3Mck1aRlb)Gwb5PCD-{i|Pjt`J~Zj9>#ezIDzj`p987ue;nZg5x0^ zSI8BtHUIENA2`6znVFhAI&chud7A-=+Ie0s^ol@Y%*AZVGl-SPR!uz#L*=*K26{ou z!G^JA<5sQ{I7GVQ7ZuU+gL*=43;~wsz$N8=#eoOFBf6v=YD7p(m%hKZA1yXZWTPlZ z2ixMiNeSMb&oCnfJ!J;;kboixIY6t)*4z3BBUHR#TGa26ils^w*)q`~(6LCyzTInT zZ|`nC)a0m)i=ULxHhc`gi^;1M_bG8?R*2Zsr1%kZSz(sGx8NN|GC<^-i+lgXSlx=- zlHeC5CXmE53DDio3x*6Un{qR1an(@l56{VKk6*)VNyzIo?JOe1@F8c;p^`FraUSBILVlbS5Q5bmQ!x zVucsjGDR%|m}=?QVva3tvX)3@fX>gl8yHryM)wjMt|Oa zl3&WEQFEBkr%fwDC*2=7|WA-BsEA&*hW(P^KSOCUGKfuf*h~m4Z!=)WwCam(f5QgB!D$gw0CK z9Az`kph({D&q*^!QK_Q0d5%&H>uT9hz8}vXWw;5)KH#LzM}S`1uruTV_A|SNB}cx_ z0Okj%d&kQmX%#hc@2(3_b9o9Gf4#ryh*($FN?kof^G?XbrYBp3p)i0v3Xj%@pRuSk zC;wn(HIIL^P*x!Lz8hj~r*tsippH@C>~wb+^vgh+q`X8!FtM}>z_IB#_d9EL4b)(A z$^Y6WT~R-IvqSB1&rPC)g(m>*&4KtVsza3UpV~YH!+NL18Fy%)QSNz7DgpP{fNakG zj_k8=C|QBC@(E#&m=G$M{ILg%<>^91#_|ysTyh{-YTJEbsUuwiHy{y%R zJD;DvP0TeBNKVMZ*IjIg5*0RZ@S{uAG~#%u0vhB>iz>uomjgVAfWTMTY?{Fm*B%0LO9g5?VBU16va$Y_(UZLP=#ZDKV+KHHVv z1tL#AzjsbD6T>dg>eNF|3{7z(|CN$bCkHllmaWsvQlh&H14M_YkmkIKMg4|ob~?8l z+1!a|FO|94AoP`nuD#6{7(vPLc%4i0OHLPN^RtNc0x4)#qKK|f+XJs!M4ejIP^@^W zEr8Q%;gYy8=;oBlU9$#ECF<6SSIOWS(-rI}xQZ1chhlPGDDeey0ow9kbDHo!wH3VH zefw7VMJGwN`YAFJ-oj~TMbupo5Y4qYMl-{ZhLuot18>(ny^Ecd05E=o=Pk0Duxce) zZPW$-DdSFajJ#~oa25pv)qU4Wl;hfOd&V|ft$zM0Lnhk%n(Ezh&xvmdmX(yj_m zk~_k&;m=lBFs&-5>Sq}ly~~CnJ&$SpN&G=^ynTIVmJQ=uuAA>|rU%tacBMh`T$WiX z`kc>;&=s;IU&nI3OZftZ6POR5`%?%K<&UP$KUHYEP7sybU&4hC1*!2UJ%ho;~A&dSF|G#?x zUIV7}*7vF_hCHoP#ok328?=lz^JzV4A!ygwhx7pJ zQC*Io&@=j<)!dOueKSfj|0Op`+?#{6^zt)ONp#%Zw{jA_MBUy1M+wgLTbi5vSW;(W z<)0f=wJLx!9yI!l@U@v}x<5?&_f;;diT60P;{3fSh#C}-M29cP&#vn?WPXMjt$n>X zG)$P(b-Qq!V~AV?G=Scme}qyme>u#=vw*k-6?A9m8L?mY1XPG4PNcqSCW?-$MHo%^ ziMCVahVmAd9S!d)zS^j9QwM%mi}Y;xYzZoIvt_q&IwJIUd^5wvrop`nJVu@%APvso zAE+$(oB}M8*kUl8>k9`yu*qFtnX(ZNG4#~%&UZC1VmiF~;JkZ6NoulK2OYyFvIN2i zm+4BX5o{{$g&i}ZhnUHy@ z2)ms=la#moHMh75asmuHZrm?XGKM?QtGXF zHBr2VziaNWR-r+~qu^nGula#gbb`p4+UL(sM)Ishj2Y!u6_9O`W>_{W8L~(Cnd9?o z$}h1==BA{c{e50G$g_?sWDy$$+GyQryiA8B2i`-HwKSSupm!+B5p`8P=1N)}U3Y)W ztYisOoNB%Y&t=^7yE&Kak={N03zn9DKM>K4m)7?sLByWjotN(L12c2$#<6PVITr*6 z^7YnvA@(E+2(aF|JtZk5#?{m_)5yvt5q)n!ytag5KBI$i8LcQ>c~#qd%3QuVoeDDf zNT)EfHsSnAU?i7mj!N{!cz5_M9An$sqLn|z50*i`^G1ty@>(@+w$3N5cWN!t&ifJ; zrcBrh8E=}bgYN+OYPU4y*oR53^v}wLzO7bfga4%3evtlm!yNu9b0FaQo}g=J6Uh*d z`ha3ay7VmCG)^6|uw1?fjftxly4Ja8H%JL^KCfidPsTkY+C`~!nh>kiOOmXD=$eL>;(F0P&-iHw+GRhH`9Jzq$ zZe*7SNF?W&;j2k+jK5VB9gp#P^;VC42R7>H&#X?uJ0Zz?=(Q*`fyWqfOMy_P{A$sm z90JMq-@u&Zt6F*I{FtnAlj)-MHz)r4d$dQ3|%j>kcEMgLF zH-S{>u0n$fR!7Gc1JBu4Dt6okeX4#%V@1k}*cBz}{pQPta@R7NlG0%SL^6??vY9cv z_+|DZi5Kf_tZT&HDsC~at5Iq|pp(wT^@;f=<|`X`pv<19X0aaMrEK#)`2&OK^Xot| z&Ls>URI}DnH>)ll6W!UYF|AIQAZxGxs4HLx%n{{xNR}lFCm@t%_Kl`%Id4L=uZOWH ztx}g8@1K64Mp03-uyn9N9kTzks4=*~6wfl{2GV($!ZPhJ@2;Y9?OVSM;pu@_Tf%#2sS0@33V?0kQ^1g|M}&6J!+lvliZx+IKu|B z&1om9T%>OYROfKdD7sb~5m%WZ`oA&T>HQT zDi3rzRO)_HnK&+p5Yv=LtJ{$oHjpY2Hp*$li4QT86P%@P@F1h=r!$ECj4xEa-ekXCOk{%!% z6lvJc;yZ|3cPkxO|Ad`fE;bCb3H*3{SkHo-H=?-R@rvgeSh#EROYGIBLS2!n@@@BZ zlSzm(zi~KYz9Y>8SrNa3ECaZLYdqI4#d7vgYgz4zuTk0K-|cyYD8;yj4(;8fyDZbY zb~zsITc$nJ2sqJxc`_9u$}Np6gF!L0b`}+%zg5*8DqS zm|#9|BGePxPO0Y4FeP?>ExxyBgHK#X*DAgz6WUI9Mc!F(07Cz* z@%NHFt;KCJFa0Wb8XNCiR~Y}-;NCm-B!6@@M=rkqrD(1tmXF1O60wJJKz~^P!Q^;< zh~#g13n9T{bd2z9z6>0JoN{(WuhCD(1WX+LcV1aK*#eH4;G7$|AH)cfpW@*L{=q&a zBNaT;{iM4$RzgAlr}>oVb#EzA=A>3m8e?*uclUJL4g7$-kCDx|f}eiIIc z{d4iS@qPy$|0X#JbT77n*nAP53uqBx#rUHQv@WY5TE5A&6LLJb7MYspWEC+;-%iky z#Z@kPr?!1Ydi|FtfXdB%iF(SGWKy^JP@O5;a0N0-TY!%?zEbQP_^J3sc%P`78w;E6 ziY!TouQ6-`+G5$h9w|p*1MW;3E&O~c9yYx<-LeGDvXbrrunMTmFQ*OfQTW*7Dy7`0 zfLAvzis`}BCXf}v5V-5KFm5QxBY%1GuudTrxfqANS`8eh6rLzzg7YX2aJlOy++1TV zTykEp5%|D)BQbkweklKM61~-SAahg52Nbe4`JH=tY2;ixTXnw3QrjX>EYJgUeCo&( zcv||oghcQyTLYH-Wu+o(K`r1osMcZV9qjsd~NxYZ2JLoFy@?}>q5CfME^JYJ3-0r1ZP!n<` zR>M!pRCK+zM;oeDt2@S#eXe2?L9IHn7-~AIo09md8I{0S)%9`-NheDX#*mWcO({8f^P^Lm62yn}W)&tWn7wp(05{BkjgddWsD}QhRyWKhL=N)aRWI$ z>JmBSq5LQ1r~m=SIdYMa%x?k-Y?j}-k!Qv?x&mgjsf%=Sjj?C&~SV|~=fI{T4j&2}hQop1JoFTb(qlg7d6HrKVr@*gVYD9AYB@wY{7=qyP(wA|*J`6_E?t(S2Y_)mnjdL3xbIfSh;x1vAf=%<9Zs*6X~;wgJ2#BB3}=jgd)ARyI2y_?Ne-7$YP^))1(@=M)|(JmV?jC`z00grF18oBXY!K_B;|RSAEai{TH{5okpmGjUkB`2Zlc2pIo4(+ zbM z(&7UghRPrtioB{Z;w;%9Roa}KUX|~86=JAi$z;Q{AcZ;JXt#Cp*7|hqDv6_oV3_HH z$8P2>o4znU2H!F74Sv$PB_p7Qz>3_>s1#)GrQ%M)nsPEv?|ISdOBfW0dOQh3hTW46 zl+^wtvMbx#wmtWTfdXdtLNmJ;eC) zCU&r^wRQQtzn>n3bV7x9?J5(jxu z^YXR9>1zW`x1t5RNcItfR}An`@YQTv$7W|yuFc@RU*5`bOgmhGv^Is3%a2ZUiM4NA zDkofar3k?>X`$3v)V0028+DR*XSVLCGr8iKP;qg z=i+-}568}==GRDp6BZlCdMIo=bySiD@rq|E4G5l+PW**To4xOtz*^H&H(tn&T?wG3 z^gGwwr+F7x1GfR~ptpS@bV zS3s#2->srf1dkQ)`2LHNpD05UTnPPR&M%z~3Qne@YGtwTDRCK}vWcGo9|B*!dd3Kn z11pukm7nzihGg6a7V?Zo5eB|o^#rchucscNeSeR?wv`GcmwL{Q*-Sc&KEqDJqnUkA zU|wwI4q9~UQ@FF>r(FvZCl;E??7l#>^ayzrxkoEN``RQ0Eo+JsEdS^@1(VJ_L};7g zTTL-a>W$G~kTozaX76~#TA8N~y~ov*bix!x{Jz+`g7Y=OK^mOp+vhaDI6x9$=jYob zWuwP~rFe_PMjOr2<^ryZ=|gJ9%V4wNS0>0=eJrA z$W%{4F6}UAjLrMgq^L~eQ=mW#3<4>zgI+M-zM+75&qY96#wJP*)9NFYUUDUgWIE;` zX^X=HI&SzFxU9R zvy+W0S6h1ThX=Fwq~<)1Rfv2g?e+#2Z2nioD8f$WgeU&CvRqu_itUJvI~x(3@sb41 zKsHnl)bNhF6opSQOq}(@7ZL;H#B;h_?=TuQ^kZ^-Y^EXW*lTV!h@tsQ-Gv*HqSSL- zw@B-O!cw26+2NlDg+QatRoX-GG7puxZxm^@^Wa^62Q=k=HEU&=| z<(S&A0EHm%_splWy9}ZM(xh<7G5Jy43r@R=CvYJj91`2pzkaO1Y(LXl^fTFIr%KMO4gAF&CXL${v8DQDS*p9t z#p4P986)k`a-}_Q^aSPSf4_P?tPXImPXwHGqUjR8iPK{e7sKL z5D_@x)lm_!4D7|wI7g)wVod!Ceu=Jkk8^4ik`kH4n(jAtT!XQt;(*yrHm@JGKenOD z*;GV1kTSBbtxkE%v`)nUbA8e(h1&7wgcSvEW3`n2^%L|h{~kFxC1n!l!>}j_9!zW; z?i3lLzG^| zE&H(lZpwe2K+VahlO0rxiYm+FbwM}vGmlK{+KGf*$csh`0r-X&*ys~9MYp_d3y zs)pDfaeRxkfw@31bhKhHe~HXZT&l{TK`h2^d?Nr@ac5xNh(b;dL=2U3qOJ^ay!O@! z7f{Hxg?>rc31fTmDkhBfxC)#|cYrSA)UYP4W?*<7`m4)nD}T%pM^d}vMtaP!LVmcK zX(1tlcl@VglE#@cfcBrb5lZxqRd(FCdo^Qfh{+BsfBDYM!84KnpfJzm@edN}>YO2i z`lbU-@>>XXxt&A>!|F3rW2m(wPIb=#%*=&iF6f6ZL-}2&ww8HOcpQsNRS4JeS-o(Z zc7P3!pszuq@}w-b32BVjmECXrs%ZsJcc?i#V*V3S{`7Qvqu@i=WH8u zo!aTC1>a{VC~R?j5ac%5!*M>v+Jy&n4r2^Ze5An{L0E(c%H>*X3?vsSqUf}RY#2Tf zC-t(`Z+?CXFc|0k!V~E#z&yFx0*4`s%Ua|F%p}YRp+I*X4s{;BLb|j%;vBnr-{?u0 z7`9Er1(VFWVJ_St{AQX%{lJ1A>QaNF(=>;ZzRV_!h*kr7G44IY94FJO9c}excvp0 zi{wWjE`9K#beP~pH60bMsuDgfF*KdIg+u5`a8Kfyti(}yeE{H7T{V%U%kPOG4Uopz z*-iZtjKqMcmD|8&HOZkbAV9KX@luHhKaP5%&8_oTQ4YAb6b7@3rLk!-T_4-MO=Gl}?sUZsfI5Q;E!k-Los034(sqqqb;Ff{QyN`_mZ4i_>49_~c;- zR*ZAI=w;_EK4K(YI_a3WeRYciM3?!7_l`w9B}Sw2VrkL2fCDPJkmzVkcMkf>-KDX& zAYGyM|6&FH{fs#c)p#vtbEyN={c~^M`(?0_hrnlJ1J4V-Jrex=Hl=*V9(6MnrWC%G3QYpONYs zJ6&?`KllRmCcA}@WT6%VqPVV8)_AKEELYes#mAtNGlv6FT8Yv^Vk6%-=2(^1>&mjP z67z#wh*XFs@2m7z8mpScV%3t|SiIpfkQnuT^lKg%+Sh*6C}$J??99iH&sPW0Ev^)e zx;+;g#E2IYzQl1?h94C%bZJsJ(NcJN2P2P*{lgb9gb>O}_|VH>Uw8!!_IksgPWeoY z1cHW?W%!fS5(=21@#FE)Zi7l0a`rdnoPdLx;Q|P%#G!uf%=7})MqWC=Cxidzx?xRR zu7fk+M3h(SzfNL4h>BY#e-qxQ>8ZJ$RmT8>|J17GHe3b(Hlh1((`Z|XKIBkaq~)$t zRRlPz4#I=hfmS0|2h|zKdzG#-c}}yTURQmoi7<DZ1|2Xij^X|dOsmQmLUi6D4Kr^dvGCP7%jD&M^nxCFGur#g ztAgLcdMCQ-?N%G}qINc%Dm*GKuZ<@~)z`up7MOE)f3}xj12mOo$+i`g!i7E!r<`lJ zD6dg6Bg5WoxM9&NYZXafUMoKJjDtTSw+ocwr3L z&CWA*&3se|PV)scdw$Sh#x2G;XFbKqhFaACEL!o89x7&=J51$o!Epan%Y}4OA^>gt zBzZ%ymX0D9^qe?Bp0UHH0N(0m#cW?5Su$_(Q15xhwk|0w;RD|;rFwh;5Pn8~w~9rN z`q=iSh>{*M1St1#kaTTIDiA>J9vd!&@oGC&&_fsxqs?YIxYtlS<{RNsQ)TSzs}fF6 z_*Vly$j}3`Ida42aZ^uXU031ty1E*Ce|sM)5qL)1m7$F|tP)8luFFhdkX-i(8h4&CR={ZC%m`53GO4PUKcXP65C?n1g`XtY8POx`^PSHVDnWTOlID z>O&kV@e>J5_~i3>Jl3MWF2Crlpm@1cJ6vpYpyHS;cK$ZBB(bV{K{)?Jo$l0+lx`J- zllt3sxJJSmc)sKOsX7aUcApt!!a=-C(qwE_+S-(6t-Bb@?C2$BAZo{)%nd+wTWWmD z)W0lN6uxiTl+f5eW(|?|yo3BE159|rVZ93Tvzq6dl1I#bRU>=clPYG;*W9HVr!i;55w`$Zh zDS0@(z4ULNdteHKqy87}S9nAZg70u8lem8k@~WdEqtDqNhsU`HrGr*V8l2mYfyZrK z5bSy6Zm#UX7=^vHnHVce%!cX2lvn=voM2SS%54lXV!^9LdHm&IAiv%X!95t2IUvZSfxX zsFWv4AG^XeKTnV6NR<(?P_cUIaP9h=G+X;ukgkZtHEUgP_zPv%{xwat&!+|Zq>)7R zq<^kFs-U4LnipWz-H=P858fK&Ie{5RxNhB+ze{^+#r8S+@sGb;->8BZgU5+y2$NzRx9Tuxk z%dhtlTRF|@xbO%L4wIsity8cAV|Leu#gGx?;rJ~mY=Q1k!-4isXq-b)8xs@B-~^_G z;#zMB{ryt&7?vA=2eSH8+6Z)AX%YMJm7hmOSvSV#_HFb`O?464QlcaO*OgyFUHUzg zLp{6do+f_KsqDVypw=^|4R7i8t_p>URYFNxGxIo`eT$C^i4oK@j+QuM2EbyGtIjEV zF&U3++6pqf!JMZyp>}EgMk_XI!6HnN`_2$m4rZjr|%`N7Ok1Yap}4lF)Orb$KaniwBbt|Jh{~mr%0?gz=Uv{B1JsK zmJd+QbU_3eDv7a=y8`MN!vR1GDLPZ``{4?E0Q?Y%h?K84VFs5tv!uqq32Fg}6pA=# zHy44cxIeuU>*N^Z(9$~<1{J<=b0D3h6c`$?k@;Yf!p`?iZQw(s0EGJb&Dkv-R zFHZCgA9!`Yq>7R;AszV{XpDYoB^?^lA+b@>Ay!A(IDob>0C4q_Iz3CfL0anIr$%r# z#znNS@IOv(Fr7=pFwp$vb3E7>}`)fz<0Z!3u{w{ zqB5Lo_bE?*!7_x0r^I3KAO+gVeK`uGZM!eeCQ%X!&}>>Y*{D8HMcY2o6*g#m&LBt5HvR%r0T5>bPbo zM(L=Bp^fiTULCLCuBwq<8m8_2`bXUXrBo!2zetvWxO;e^+yDL`RGc-I$Jouz1w$j0 zua#h&3ktA(y=-_-15{x34^lhb;q-dwDkkSenJB4)_v8t_(A6;?@1+9;X`V^-5zAO zx7Rz=W>0qe!4}MCsE(CQY6Cjk(AfmM#mgrKM5^^Su7Ggr%ri+l-IRKtO>mF;3}tVD zKQqV$?*?_ez;#i$O6U@b+hbBbYQ290-H@mTrsa)QPv&8+cF@SdUUOuS?m7{g=BOK! z##j=3va+T}>HCTjpwqP(bN&AyoKJ_YHS2zoUe7_DcM@`;AMVnfb>hY{0Dz zJ}OkZ5)gXfE2g5m@biAl7lH!mIGCm->nVWw`Ls?u6-@`>``|IolAlKz1nN(5Cy%qm z)pL>zlF2|gLdJcT$HnGBZpPn6nAy^+HhT2IKN>;u4o54_frK0JZaZUux@vzQDi%(A zm&A-kZZ`~_JA@-_o}5AWozH(vs-SdwLB{%DmJfRhLoKxSXlbaCn14E|pV8OnYPNkO znVrutE3cZ;u_nMOvnlP=y5)!Mc1%{^G{%|pdih9Zl5pF4%yhE}y}0{X9bXoW7|=2H zd|)qZ#Bc{haZ;kN$!19ezv?X;EZuEV9o%I)9;M_u1SIF!n|w%5ZZ3>jDvF5WC<=Vh8Kdx(4_!H4&Fj@?Z)=Q+7crkszRjJ z@AKYY7)Mry7C6V9a}jj3!X;XIdclof7~&3jVn0BKp#&`>!zfwVRn8?PVtb<9izg-7 zs5NPhgQ*)gKTpoJ+hLh;qdKE5CU3DZ*__i-*$%BwduT{`=5jO}o43P!aAo}ju8e`@ zXOExTMNEiN!)d2y=Twa_bQ-L^cVa;LC0?Z7dM4VjnE@=}D{oe_uMrQW!GJzYdNJmc zrpgs`VMVzx%fwRyN2ftE^X%ppbq%FPAFcTRqHR>05?4!f?30Etlm-<6`JGMHJ?m{` zT4L-qU#ryn5n^RB=YKm6{R)hM*;Q2iXY2-*-Hi;Ch>w>SR-%m+Z}rNNu}&YNif|yH z`sG(F*zvvu7YASAOP8gYbzL&73B;@TJQe=@?`FOgQbB#A4a`@!vd`HMT2AUfoR)Lt z%r*>E!fe~%tVj)*cWZU)qjLtVk(i5x%JPKW7qn7BZ@%Vls|2Y}&(YA78eNu4DM^;@9hWpP>u&xm$!+)wTMfhb-17wABS1p+N6)lSoE3<2EE< z;%WpYVA0%&>{T!jPLF9#C56Udnu~7*3dpX|8G@Y6F(c=``~T(zAt_m@V)M)01QiWq zR&&>l{}2LuFR`aFohOi5-0B!8>geHcme>~z;p6&1&brNF8~M!q6uyzpND9)N{%XWt zo)754;T#3gvFI#Qi123#?yJF5sH6;U>r6TiwI8?aW?@Jt7KLNWBKbdp7(s8cLi)E9 zPd5`P(w%(M0~QV{uRElD=@%Iy1(F@SSY1_ao%(VdyZ!uNH=O-1qj0lzplY36*-jA0 z<-!l!(PE>9dJ}q+tr%6;lhy3RPH=NZJ_64%(rTXk?#{^_+|4dJO- z6Kf@6R(-&Soe!QCb|G@j`A1lY-i0iD-1uyBt#A{W_~6uef*BM`J*0vlkiQ=S&eM}Sliz7SNMP3*D-`OiahSCGUL}eW zLC0sp`hyT@3mU=_ou;nEf}0D(#8$iB@~JtZ_>zt+U8OMx~uLGnr~+{cnE zvBmb67GoIeL&3_(M?f!OFO^@z)3+*LQ(LBhp+f3QjjtCISeM0XSs1znGy-F=wEeR^ ze9EICO;g)>B`T_qHaigLfsU`^U2rN?pK7OGTFm++Ppe1$VD3=r4hcR&$vwx~m|!Q^ zvRutb;IAuN_=3|qoQjY}oZJy>MbxlL)GM`SpIpMO6gEi6d4}*)fS6*H6 z1^+7iH!xqWc&sC|BcFsHK9#3I{8)1*drlMM)77P%f(VHNTT1-5^O{U>5KtBAnwY6J z)(}eLiRAn^Nm%7?c2Ow*Mb0=X%JvRij?fW~9@Opoj_ag>ZVN8Z@H5MemO(2nviZx= z>vA4 z3$XZ4G1fS{7lxGjo-#k!vxZJ(C@P?&sWE}f;Ku=Mqv23ZBZHtDwzlJ#vBw8%gxnbH zo0pY{!z|U*{83fEJimJHoCld`H#&Bb#{`XUT@*?2j`T4a<7kv64Cial;-o?e{Qojl znfkhT9Tf39_i4C?c2}MLk*x{tbwL87*we!v;Df^=c}Ht{Q`sgrMjE}_j2QGk%}T~o zVoUEyC@XjVXe&Mb&;)ygR@-{i;&>F1rF*r*--g!qGmjQG6j1JNtk zWP9VF^Im<0DU>wnng4*y0xvE^lj+b{y&5)7y=R4jvk;0!RR~DrT!D%#P&l*-YbeZh z)oGg)F}A@EBSs(;^OTrym1PVNJ<2TFynAL$3n~<}h&15G>V9r#PM6O1TdS^IcvU?9 zDuzZ5n0^wg`9+J-@EMe&fa80H+D=Gip9bTnX!~}}Fe&lDNuGtqm z+(V$;(n}9b`ZbX*p50AQ1HFyKHg8*CMcl5ptF3z=G1lkc1twmCDQCfW{5(u|p3oRv z5&z{>M#sgc2ykUL|M!M4bOJNi-fc{dj=+{(q&lvProinwc6^O964!C{wV>r$2^gi5 z@4Lf_2s2F{-9e=qqcKp1w`f*2*x)82SIC`5N%Eah9G}<3%2-rLAOxtT zQPi`*B?j(#b2%*at^pkUf`tleE>pmXsa%DcAn7MKM7jc0WpVnbv!{wJYBFO*DoOo5 z^hwpV#@Wd%A>R)dkg121c-4uF2vk+l%QNILoknmAPXd5HZf@(^zoY>nRm{51cW-kA z&tH{u?8A}gTR6A!@@G8K+By|wrJ|Z)hx6nirl^}_Bz;V~g3t9ajyI1Kv#ZMXI4@-= zBoCXL^>C9>MAeY-&TXb+TAT~e0Q9jLr&MXn)hGhG8O=3g!t?y zz#R-zt^a@hs9=a`P3vy$(3>@PRLmeoyZ&H#f#6FE+r9F^8pU1tD5Z$$&`V{Pa%)}6=cN@1cS=iF{f$A4Bx4CSylTu-LQ$(&C<4O|)8p z0p#uG_PUBwG6;^ayy6S&JluXx%6;J_X_po5Kqpc-zTrT4Af};t<6QtZ7+uIN!YtmH zsvn1_>f~NICZX&rJG@2a+PDsAm!k_iVuW!=cHYH77-rM+BTiQ6y)GtS4SH94(YD;I z>dXKG#`r(!S{S2Ako?EQ>P|u@aE2DBm)( zbu8n4`v^V2e7_pcL?!Y}@_jnsS$1vpwZ{);8cU=7&gZC#51NYZhJ|rZD#0;MLMGj! zZCKj=$>bSyWU7rtD*=~PM`RM~`*@sY4^5KYhvk8yP#AQSn8@we!-6bbC%3WCf>)C4!P;wv-KY#k}r_e>uzO-OXPbqL^bMxY-rdXn5Fx_M`|<^ zuDI;hNA4y2Vy>Q6A0yKb>jI`*$Cf>nMs@oT9~8c=mp-D(i2BxBTtNgaEb>yRPc}LQ zN8cis!DuD|$4Z7GAZD}bR6A;q9)LeOld9{CVwRCil%?whO}Bq zSRGl+TWNS8{b}QYMC`oLBfAZ9>VK4nnNVIBgvi0!!~g&@n`yLGL<<{M#jcA-Rs#}^ z`AA2PzN-f%ig-)wqu-L_dN%L0=#o#vn@0}qgqwTB+)pycre(ImoHme|?&qf720e~F({3GSJ$AYD*x;&6r{uDi9fC)jdwAp~LS za`pqI=)4D8)8-n;Qo;S)*N=vwJZzED&hR(gyIopx5m?3y$!ae&YT z?=5{xpXPSbM4&`4=fKT&7z)$!uUSi^V?#nr1>G zm)N)+g49xQXvyNP%`;~!iGk1z!Pt{3u~_~CL_3PobBByAF(gi1h~V4W3R*^*-3fSV za{p&uBg|3C<<%n>lTVL|$@1!z1L+gCZkm{Q3HKLVOP-G$8+ab82~&?{07l!kGIlQ5 zdIb{fwt$sy^iCRJ|9d-8c;E@|d476DqRS%R8kB;R5iPja`l{$p7Z}u{%EO-Z7E4UD zN3)C_t`y5iXmo%b80bXP6OchG4c>U}kQ|(qR1QzjM?Vs2t4I}0usonuquqWgB&Wzo zG{V`9Scv955FB=u`ob2MI9euIpZeI+5)uwv(VpPdFMnp_oQ)w_$yZT7FwWlQ%%`qO z#H-YNEfMX2EFFH&(}XYB)ZegPn6d9-fnY%|QZeHMAWXR$Pe7F5)_xCP98)0f7>SKA z&IQG#q{B3GGVoTy%ibzqF)}2z-R*TGZDgxL*dO)7@V*iXU$8d%VbY5j+9>k;K4KvkW>IM-(FxZ>%(_fxPglfpK_0IS{IMkciOv4_bFH68ym`>Z%@(=qL zZfQ!Se&)+0P*?w=d=sR%`(n6f3{5|Zm={O)uKP-qRI7E9$@k_83{+;E9iPwT;apus zy%~6Ax84thUW*b?y?pH(!(1mvjqA_5=-3>nduv*Iv|GV2+|NWVKc)FN zZj@f}yvku=+vX$z?p<&n-Ld!4B(W}NBs8t-)(Fqdcc$+W~8 zqF_TVt`(CICixH1#J?j4QC!CYFfQ}XNZAhLo7^*EV-4*WqgxzDe`FNi0m7($Dag@m z=X8XNQzP;eSqinqoNB31;I0J`xAc3_t~#CrEMD!+L~WcI${@?-U!QX6TL}y$-_4YP zHgI*aL++E0OHx@qmTj|ZHA*Ogq}JIz1jvGoVQSo(0Y;?ca6Du5Wp9h7s!1*dRK9Kh0Nm9dc{gDUPCcA&kFZ0e+hWf zLJdMmUvSaz_%;_gmc(@g(SUNiruv^MBLD7`p4p-(puy}eSs(szwL$^S30&IU@U6IA59Autz@oph zgc9}ukCn~sjHF3%K}SnN4CGG6^@-4f0UB&@OgX`f)#g6(_mWN2~akPYO)-nkyoMb?hqve+kse-QBKTRn9pY2_Tz1z8!b zpM_}eM%g$d)RBa3EQDglV52x3%@S2U(Tj!|IMP|h)w-t-s9I|iP^lm8u}q*XH$hSm zx!}k~++1ZfAslp;<@%9laaU2Liu8uzI8kgwkxRwb(I(J5VMEZ%6FrtiCP>|ql7pI+ z6mUMc=pt^Fi20Xw9q@E@VWTy%g1G_fSDQ{`mD7hvJt453U*j4+DkU|F)ffx1c3C+o z4@%ZdnxUe!Pn2NgZp4ANe_cIv<0vgi%DyOj0`7OEyuUUIe+24uyOh_e2RtC_jq}{; z`yn|R^c>$eF@un#D0CM)AzjW7)+8!n%IzHqNAeT-vwT@YZV9qPt}J__Z$t&MctX2U zx$Vylte{c?SKOs;mAI`aHaeXImoZ%E$dC-Ih^QFrGGj=lyl0{Vx~1spqiKPLPC_Gl z4~Y5Ker=cPT1XJ|l9H94mLMr0_0mkIkiDUJq2%&uBPJ9t$P%etL&qB-LRp-g??@6e zN(zM2$h&(s%WthgopQys@%TYFcHMGDNerqUVpj8u=i9>j6)6Zuxl$E(r@{Hru*g3X@ok~IX(LFg=e`|L?0y(RcXpx*c{Bse{Gvv-HE0PWM8 zp)C%WW!O&7w8$G{0wDvlE%ROTuX-W?>A#R6*OZe2aIoZxPs{l?Jj3+UIkacsBUF(3 z@e&jL$2g-(`Mh#vi;jOu(cL|^Te={87O?tR8Ci7-B{#33FG;lM+G&_s%cP_R=m_Fa zo@rN&3)mCxfZ`@PSLF{(PMAWVF13)ab7MV#sks*ONmYT$edMdujBt^6Za7NoFQ28M zNmZhf%}UWVH0q<$cLTO(ui6=9q-_qkoa*xFJkTi>C|$b9+dbnZ3#;p>>W?}(0f3ph z)$q8aZ*x^nQxT#Pb9*zn2l@~^_O9j*yaZAMEaArfL-2PR5-+>Y{!-s@F+L3<_b;JT zjF7ii4gTLCxjfc?T!QpIB~II|FN&bu5>hOS^+cr{;dN$k39R0^*4nnK2?cgk zoKNkIj2;)=&4)hEy`4NZ^O7btOn_>)s~|m1ZwC3tFnnCpei|w7>%Emch=i$FG6_|#n17BChTT+fq=SWE}D zAg=YpwlE`TVuj*uZ-Mi7RanXrBNnP0>k;Cu=t2g8j^>O=la23;OQe>oKD*w)P#AW7 z(-w;J$7dgtT(uoV^w^MM3+EVvbN14OnZeGcCIo6DshfOJ7M{HwuBy0 zWfS_oVYnK0({ylGq^9L=mszPy7 zmmgZ{=&K8>?y3V`^hMBtM_wfn8I0Eb3%D;X8wEO9gC*!ULpD|ZlyLkBXDU(2tbm3M0 zn2Z2@>#MI6SQv#AyY;S4Duj}0HG=V(V-cHa!bVK1zJOhsj_}vmK^RBG|`Xh)AOUBd03iZHjEy!iJ zy!Yc46PIC(nCa~1HYn6BJg%DY6Xcg;pE_{c^#sCL;stIWzLsWAQKkS_h|bb$WkUWf z-L1L5MoBdESh{4VGGZbh^tt_y@_q+TJ+wXWKl))*oTAbgxG2G>Y)bfk!HLqgZpmGn zQ;R`#Cw1(Jp(t<|z=XS#D&>pMr8rAGS>@PN^A53QL@eL>mgz`;VwwiYlcm>v4n3a( z!=tF)0`K4TZJ>3cEdfBtk({r-Y4e-qBw@@)u)P059|(cKRdp;7al zx78jNpQT5;M6j$GNt@~wuUOOj1Zr953*o=NNO3V6fm%=mt>7Fa3y{a^+AECL*MxE; z(1IgKSkRnptAOhqq69Q_@6XCQ0`QE5eCE3fuNZR*h&g5cm<2S34WbT$#^?EOMmE$& zcZ9-Rz7*vIYvA`c8%Jq^9i#N87MN^P|J`s_XwH#0I`zBcQkPV_t&aKL`)37bAecem z2vU-+#WHR*=rUMF(O{#5jz?KTQlrj?*esb?>vCZMn<;7ZtAxJ2UkbqSt%H+u;-VdW zv-~4=asCW6{rbK_0<<`ALCVKJXC#>}g-Z10eVnB3BPYhH^9!eSx=UjyGVR}@NoqQ7 zD995pvc}#%ys-n4KC=s}X>0${<_HLJrmc`kg{JEl1RpGDwwW!)E$&eE^?-SeRvi=x zi2lHQxp{LG69#%MnD%d4X35>9lzX{jMl`4)EC!)0^jhe_*H9XEBYj%h(6YLhLO+6I z%8JI>OyfhV8=C;;$yw+I)*zK2c`iGR4A$iK`n$!BBRUh@gHDGsOlysE+*Z6q#zbd2 z@3fqW`$x=ia=9;NHhz-?6C$UXn%#T67j^>IJm-iqJa><&k4g=N-mJ% zO@A7#I5smkZmVU_TXQzn_m&A^?PEn+wEb*+cKH(IY!;7e+~R} z(SAu?x5hXR6?O^!MV}^95hGW)=#_?QiUHcQo^sy3K|LJ3((V`9g4DbbZ}nvnSDCC! z@Ha9qMpmz`d}0~jtKtHbowMuAi2%@sEa7g)g;Iyz z?mgzd{CkTcD+VJ*pQ^aHMszuN!epGe&Ji8H z`N@@;RdptaFz4uZ^*yi*dyo6Z1FQ&$7ogFlUMe~R(igABqcReStVGSC)QE76=tfKj zS=fjhMWj2yB;DcLxs^5ry1A7M)$F^!BtHYmhG_1y!azo6+D_m=!ILw7?9deGQkWZ$ z^}976%lK^88KsXX2m0;Vtw&2Qo1V(%-KD%9)nV%~G~88EeEH58P!^Rsj*jf3Dx*t# z64)E8^T)qeQ{%`oG&mN?Cw0*MFkw#Ivs$TEQ`@hRqh~!+ST9~x*)OyQ!qM$>D| zL@@aCOiQ%&&oSL~qD_c=$*qHOFdN=FWU`mvK1dqC_KkCf@l?$g9I&2s?Dhx?C(^`; z7p#k;SykrvAh~R$}skGS0vo7ZE0E0MWSE-5itSo(9c-L%IX@8y zE;`il({wk|`jtXe; ze3C@y`uy?$Qw1-Nt+rj)kC_x!b+NX^Lc&l(d2D}0ou!9Y6}8%MSl(RWJ~I@1|Dauo z*=;tOy~OtGEnjs(L1gXr)3)3JtN?N%_VeA562=u|JI#U}K$3Q-to=+rvKx*x>~_ke zb{PYUsE^syx+Mogz=`rhdX^&w8OCyyo{iEw)F?amt=}p!YEKPq&!`g%U>?*Kn_7t! zaFg|^a-@V5b4-T~4HII(fNUqLRo#)^AyQDQZX<(#*}v9L%=T10G=(IB0M}w4 zPBokMRqyGOQiO)=F^jQFZ06Nap`0xGms+5ll-@+JXBn~=nm z-NsxdzqCnJPT6w;V+DG$mA?4ssdQ9I%5G#P!qTYefC(&XcS@c8iQ)yFSAhOZ`{c_J zS~Y>>+uIL~@PR24yrA~@*C8@+2>cX=QK$5T;h=p*SM~F8YRa}A62aOrN18pdq@x9M z{dZStiQI=EhQHWc*MH&7ce{HPhXPE@f2t1n1auB5-<6|E$Uu~w?+hidoLMRIO|(_i zVAR@ilPOmksx$zk@7`_8D9%-=Nzb$PZ+q`G(W4TCddv`Ys^OIh0WR zAK?(lV}ua0|6$1*XanA*w0QyY10yn>JN3+QKLMV2y>avzp-xRdj>!h)F^I+NCkc;# zuYZh4H$4KP%glr?{GfJBzM#>VF2Rkr0b}EvY6sg-w>ok+ZFHA6?j60Po_|i16W1A4 zYO0$rxL%ApQzDw*8qS}QTAtH~JW$+yvS1J5jMWYKM8XSHKb|Xl(Wv?tQ(S(cS;;B( z6-bi!^)JF0oiNy>^7f=wN@)yk+CNta&27wCvaXoTN8^4wgKMD=Tcg2_3xl<7f$p2c ziy5mrc1)Ai&*dA}S7vXP!@hq4yM6RN9M}Hefw47Fq9FXA>Yfs|GTCNJXw9mB9A;Z? z;yr*B|8v}mH~|Utn)DugadI``l3kJjI)u+)?Nfi3k-mxv>Mn{<+O0J6o^2uW8`#v;=P@Kn6D*83@+CSx3;R3#&1?mjFmGw}_-RqSDiVz99@ z9}0tOxY&ZYb4e6V<<3zYx&v}_?5Y-D8sLZGX82-Uf*8(qTNnHAKS2D}p zN||JH_c`{pZr3d=p3LGzNSWoJKDq3AP;dut0>D2zXQa%O)&X7$s*u9{<(UOKO_KT` z>nK79h;mu|2ATqo6LvF&>k9JBtIJN5Hs%KC93vYl(MR&(|2hq33C{-fTYg<5Rb$Ed zWQQEYlB$&(l?4Ne%~k9Q+i{;)A<)fVC|A!>mQ&27MJj8|SLp~{z^ac}mMX3RBek`0 zBS0=;j@pI9u%r-g4A%R!=DE8C(ht+Z>*Q`D8~a3jU%^3Fq|?EZWl&V0F|7MP6h_kc z9gx$?20(2-q=Ba8>H}sugHdbY!H=Xp#~yk$Ivm^o#Rpfv!OsV1Hc2(Hc*FS$I00KC z;CtHXnhQHtPS3K{;gl$JJC+GVfFV`xYUY8Ni^SAgjL6!0O?=UHQ2@r^DI1;3scE&R z%_Q&;t;vU)qj?uMNPCfIa3Zw5+rb zX3~%+Ifc}jdU;?Uy|O@veQ2y z8yup0xQU)X7NuaLJFXqqc`}!_ky@N6l~OEWn6}0n`gnXi<42?*E+AQ$kn%vjXHpvn zH>}q7L_x`bCJnCpyEl^0b4u2|_M7cIFFv?F(wWzQTQ?+AwYQh86Kfh9_2E+&RkPI~Y<_{KjDs;20v<4llk22`Ch*Nr%>nxp#j#y#^sS2g^M#^Iz z1_Hs~N+57xSwXg@yFEOHZUOwMw!PcEKBtiPECbn#OGFFBHaPG=MsPMEx$)rLSCaY1c$_Iw($6f`;Ww23Lr>9ba8Z*BC`f$kTO2b@Na^E|L2 z{2J3&#oBVr0kto;Gj=&|5_y;C5(S>Hb`^)vnYyN0`1>*pBCR8RK08NC`v=Jnf(vAh zj=3D^C;5`QTa9lP6kLhevXU~YnzI}${$y@P>_bUy%C(gzj=gamoS;nl_O4y8M3X2p zGUc%n0_=-o9G|X~`T(M?P}`54(EJ&YRMAX^wDSEVcDZZ{I6&wENK%;g$~XbgF-98a z_uTj#U_;Ghby|%DJR++9Xm1;97f%4j-bb|7vaVefq&KlF>lv~tTM8b?u=OFkAeRi0 zCnfiDyE@0Tzh;bLr-Y5V+Z;qgSW|WqwTnVaB5U8K|NBt!J-g%a7=_1>IR5Pqy_40~^=xdw4q zhaRC`o{0qCh<_YVv-*WoXPcj1%|`)tDxps5yS@kDFxh#C(&x2w(vLVN6P0g@qskN& z1w5d&l$r^MeIZ-iy!e=rz$6|FzTf1SQ57Wwd~mPFEP)jssOrLSWa zTJBXDWpwFh)o?E7F_Vjw`E-do`3+|0r~WJ7t&2RG3-@X(#t^ZL(cF)AEwM7I^!N2h zgfs~$+WMt{gjZG>EVTX`Bymeo$<5%1Xspi|CIBUZQmZB65lHUxe1^jgh7o!o{gLw( zwb9TgI_CbR2AuuP@Ef`TVWUVsKWd7})pt3l=a?Eyb2Jt!v{L zd&BNVT`_Ju4-49%VV+ZGF3ZjVZ@iEDg0i(%E{(-%LTNtY95>I!)hvZ=^-7Xre+4^n zx~hzgGC)P$`ARb|{Cv`Y2;LUjjZ0?ms``f2ceFa7XAu&;#C^ZworR zlG`4~eh6RYvEFlhInu+p zvI-2bRDIZ}Au?_YeQuXX8h6;H>S+0!m3N#O5!n88HbI{7E9OAU#k)=p6}2FG(&o+;IZ3?Zau9}&&`^m%5GDn( zI_jD-yb`Zx??$GAEG!#D(|7yPSSZ`yhfI6-SOx+{P>As@!T{I!uBkNlLh6@YY3IE_ zkn-LNVn1lVDn;Ps+Zoz`J442GVY3>_>h4NWpN>EwnEB|qkIp+;VKLS9?<(C{)K_`- zp3s{FA-1oDaLD>g00~mp2Q0ef02gpn`VU&&`36Z>j0xG`v33IHN!B%P_mv5#iOydy zLBVi2&SyMs+79a{?;(e60(nVsOtYJB$rOHyK`b~B(6;F6OXv@Y(6@xvJuDm z`iERm7K+g^e|zB$tt_cferX27O6=+*-%xc0x&dqNR{A~L*JenQB=24Y!5Emuuj-TC zi+(X5#tZ7joo*{T|74m);&8{01||l6qyA+;(-OlNf4ZkQbLy;6>M35XpCDcdKgGsx zka{2@_^A6p;itq697y}-`t96$s(WjU6G2pLZ}$F?aiBAxEm)`{=(aU&9_?{@e3vw= zr>;89WYk_Crmv-8Ji0liI5`zt(i!A^<7Gi5ysLSj22Mwo?vB5q^9j7#(mXw3ZWEae z_88IB*Ixs`M#v>{@{=M9au}mCYX3_9KZ|`S<3YsO^Kf&iw&aF3TeoPPH01NXj(ju1SKr~S>R z%QWaO+-TPCS$P_#rK&)!#C>WtSMbHPU&Qvzn7)@RXEv8rFCh~v;E8_C*T5tPpLX9H zyWYDHIStGa)##WuNJXHU1=vFP7Eir`$rp%r?j8~9gpC7O#m2h;Y?q58VUfSeko8y5 zcD}^VY)}jW>MM<(h?YziKrA(#wA^WjLr-TR#+BSyF@OEf7j{N$6yT7 zO$vze{Y|7~rtp~-hVzFsZU4>Xp5t<&Ie#My12;TaK$m4D{-b zE}FN{RSQbALJbvow795V+h#gUX2pJu6R1-p6N=H5jFZ|Y7Hn2&hLy_CR2c!YC!&#K zN1TvNK)iF@#uRR>dkbefsII)d(9<+=e*Y;CXT9{;s%m}#Xprt={5;?mMjF-Sy83LG zh7{MBA|Wo*a04#n%f%k=(wR>OzJDfmGQ&3;>fB}s6wS^k6#mAc5FFfx9|5=}cU9NF zg6AUbDvvOxmF`YQbske&s#&K=0@a|?gu}2}fC1#1>$;g4nZ$Ha%<(`dU3wSAr2FM% zE@pGvRN~va)F8~4L@V!@6mpz|H%%f@G=}(T_Li%#Pxu&q4*B6&Dk0lCnY-b{=@26f z4NXq4+R1HB`4+R&)lspj*xpL0eszRS*b@j`ZcK>u!PFjbEUX@J;=S+RJ_B{Tam;gO z5ZX~eCv5s{F>(jm;^mQ{tS(<9WEW%Ow*VX5^Ey$7@n5oD=quY37e&WRZq5GE4_3|8 z6LtLDB?waUMAbMdS6H5h)*-(f;ws9LB!S4mT4Zy(wH}51@(hfe?X3l8a@|RY(Au$C zWk~vx(3tw>K~hOa6PbJX_IX4!(F!68`48uOMPE5yt?dKbS1%qS%><=J-4`iiuI&>S zt->K*edEs!4oH1guy&`=BQ9s`6zYJ9MNI2}!gnaIN?Ir?cc!6xOoA^C&Wqf-3H6yZDgxVlZr|GnX#xpJG>0 zj${q^T}c9gVvfht1CiQlE_o|geEqmYkrg!+7WUa~7kmoBA2MO)yAe``{vjmSvG+*; zh&p=l>-4DDTu3^Sb^`W=dUD#a&b&F?D!!uf4+FY1dyZh~NfMOxQLa&*!)?zUp)?LZ z$$j3BV^w07x5^R9l zdmRGyro;j6?(@zRe72F(VXVk8YAztwy3CLEgOG-yKhFF0&Xa;ep%*PEsN)YGR>*t1 zALoi4IVSXMeS|2D0SM5*Eo(acg#d|;{L6yc#zeXyph$t1LXjfp;pim#I?{Z6y#4kJ8`4~!&f~Ul zAf7$tDB)!=B6DAgeo_4=lK`*ZI*c!}U7?ND>rLdB00fjz|A-nXq^;x|H%vYr;EBUC6Unjm|Z6orNky=lIzKenmy2f^m8ek<)kp-hsLJAsb5nXza99k z?{r(H4f<36rgW5HT^qA#lMt@Csj4>l68;hYa7(~e`h4J%EW4YP3Q|Hv-r?0Or4~oM z)WA*U391o1%)c~StqtYcod@HBdK+eB#@cN&8XG3$`>O9Z!jbOey4@cHL;O*U!kHoh9?ffWF%W3J`&gb z(9b-UVN@n~+c!5SPt^oEC5y$OF$~)}C58X5+$B(3O%QSOjC#dG`GIo|3yYc3yVim; zMjw~U2hODixc zZ#of{&d&@yP-nW&;A3h8W(~P0h*^??+WbyOAxzUNjrNg9U!51DDLSz8>xO%)Djp|* zM2d^vJp&R6z@fA07pz^a4m#|*q+w+@1Y00^ubYs?0yaWdY^M6G%AN{EsHC>t!Tmx6 z?tP$X*LN8@ci0*&K;g=Dt8DnLn{l=AK&>ACzlm=$j|;I*_@+~~0)9K#eltX1=r6Lc zs;0eVKX-;y+9`ADa}qybkC2+Kw4NunjdC`$Re&0LgZ zNCWY*qwJxn3J$-aj;`^~Wgb5gxlMujTYZuRjAXcARg6~C{)DZh3{F(OiO~O$$9LbZ zK7+gOI!sWvdOtjaw`8}mv->vahL&|izythb+>S~Xe(XQ%pfVb!z?G1sVv{Qrs?IwR zeQSd+^>=$85W6F1X}(;Afei{Izd)NJsmGTSMnR}J~feOr=G_PAU9Jj>?Z1g9}#VkD;zh_ z?#xm8HCj2d`mIch|KsjFoS{*J^XYvRe>|^$^j5V(9HcXyuB*M!yc{GB*Bt3S@^Y_C z8pW)PNu{^+w8bRrWfhq}o1n!y<0T!Hz_*Cm6#glNm5lEWZzq8aKqLIr z+JT)Gj5g+Do{NY#6q5;@!?WWPxhX4w?1EWCL`!7kuK-*tu_4jb;JRO7ZN~<)@VrW* z_i3d|12#fTv+KQ2w?BYT6F=w)(;(vvE2ul%7K~r~&k0ik!V#r+&`B?t?diR!W+$a) zzeJ?K-ux?FAeA$_s{Py9kX9bOHIpyOO{#CQbo=hW8(p$ZXEn{xbKjxMXKX`3&}paw z81_uCkgQYC77?wi9ww7yv_BHZ>l8zqs)fQb=l9PKtJc5@=DFIC!!b_R$}T=okY14z ziTmd5V9}*TFU0vZN$K6%BZ3zAKFw#%sMU15#)5_+PioKX=LPL0QI$~|ENU3rZ*3fy`A9j-8QTa80iH5ygX-g$)?JRVWpM6+#NQlDDNzivZSLSxac3;#k>VQ}SD z6O^0qF$g<-ltu8JSg4}Y46$->oo{>J=3ZRywZ#I0-=>^A^%sB>aY&Nod9Na|Qm0d9 z&B24HU%6ttU?d?7_nvR)5vE!OMoid*<>E&~Ag3_l6Zs$(S084TwbjI)M+~OYzsu;r zH(M_A!D>#Fw$~{Q5xPX8%dQr=0BfUid2sFpRdD!9ZZwOK0s>sI&BFVdsuxuS<{_2M zw7(f!65#6Yx=#gID;2?+Wj(N-)j#DhK_Gb*Hm%{yy;d@KEmxnTkEI7O%FQl$$boDz zMK8hQIYftbYau8`hRoJ*BL&2)H-sz<&zrZOoU<$e!-7wmb_67*AdmGZ#3edET-5I_p;5b`8KIL3BUi=B(xoBR##0_?N z=7NEF@gha+g~hne*>+(bkgssli5C&%>^dPQ zODlAQIE+cOVEVVIYy|&QccJZJdR=wmkUoymU!E^B1LOidG*zfyymGuWP%}HIIkIWr zHd}M`?&HvNJ1oFh-AK^qiDEsOFT=3CJUo7bOAv2595tBBqW~zjOlGT0{t9x%~;C}1nTBVGe^Ab5T?9$ZGg)#}M zP?EmQO}Ph7c2PLUjON~>SpzW!>TtsBd`oizUO2mZpQwl?D0v{&ra;2b z?+et&>f#QxkLzvHeSuQB!72(joz4{}slp<{JG`_am_pzrOwH_e87N}E3_^wYvH^v2 z#|=c!>c|(V9>^<&!}^yBniv~7u5|2U5m!*FQvrpF>9FKSQ*926Y2c{GL^EACILM2c zs)G(>$gK8Pw2$Glm+gPRqurOqO-_tdp zTegt4bQ#eXeg~^}!g2$~uYsWYNeDCJBs&90l6wdj5#TtGFo|yKe9e|u!4c$y^N#9)s ztsdEqJV)iJ6@I(Srf6hu`V40uPiA4%KCN%CS1CRtITrsx|EF9Zy@&a!aQVp z&5I_)(|=#zR-cRMfeRHD=Gyw8;<|!=NMvcyZqqrr&!f5}j92QvJpifxqAU<51? zDA9n^xPT8rEF;g~@!W@R5fne#`|h!ntl=0O^!xd&W01ce3C7xCuhZENvMg~y6YL4a zE+Iy9iUEe1{K^PLVdW6i+5E`@M;)>`7W&;g7p)4(sVDSVWFY zT>1nsso&Jp`Mb^;Pq(HD3Ww{bTYL9MA{>HiXUGifKsRYln9Op|rMiRYwjfER=U_cjdH7k{iOpqSq>}~z zaDYvjsjvA1<63*1xaxw6EA=;4C})*|auU{IDKXMgcdZ`WcTs+c{tLu0y)P43_3pl? zq=Fn!jY8!X#3{!EFRI1`PHMT4HS6pC61h%_%;>ii=`y)Ma7k>A!o%=YXr*k-OK1H`gdVnGU^mxC9aZLUVRR?WdbC3pc0hDc z;490AG#j~fPpIEI%>oKzjd@jrm&0gB1|Nh*g(Tr?!MLn-U78a0_K=vsT6JD8@zvEh{30MNkeVUUHxwUBisD3z(tOYLd}fI*hfe|Sz6%Uu1_2vjpqrXoTE zY@Jtl45f~RVl4gbKP4T`HJsv2(OIztMTs!=W_E%N6Q6u47L(GL9%AdP9ZQS4j!tsC zCnTDFD+fZ+q{;$Xi~@IfngH3TC^H1! zZLlrYs&7sK%SWC`dzkr3Hx|jmC|MKYy;~aB_z^`GWK)}?QW|{!n~;t_+3ZOC2pWdQ zhBsiD$&doWsFdIpv)^ug0D5&@Zdkc`X}oL#Hq2 z^BYBLycI=B1Kl0o%1;@@wNK$Tr78!-e9$JER7YX4Y+0T3wwZ8YK0;-@A>zE6R5JH1 zRD=CabXE^AeW;iHCq83oGnB%)OXLq_TkDrF8W~Gw+m96Q=o`#}+=qKOoI z6`r~S;W3Tf>mM z)EZ@4&=0(;-RM1_J^Klaz4$7g*8h@eh5FWUb}Zi^M=PF0w{znLQR?4J3273Kf_x{H z{+dR6UH4$P$m&0Ey)wm`l{^`rUZEH4(v}|m)Abv&YiFcY+?xr8=JLSAiS$UT7s%HH9n$l@->MUeAi&>35sg%83 zAG;;2I8T-v?O|boe|gSC5&1RF)L=kCP)fin+lLk+rHz1ai7E z9yFD;ZGbased8?pCog4)=ugs1$1r{h#j4be+>yt;T|@Q9Y(PT`W=HBVVsPzTLsShM zNsQ+K24ANP2W1Q zMrdu~k`&iTqD0E(hL}|In!Bd@IMr`)AL_wQHB=fp)xOU|C+^6;roiX-0O>Hs5~2J3 zl?nP^$qIMvZJ3?i2Fp* zdgPhU1U=qTj-~f#DhzZYQN^E#G7c65b5hNj^R&G}01k70d<2}nd6`H2qYY( zsCP{%g3P;jnsP#`U|lNRFRsaje-cvL8)MBd*kF_bYR{^8@Bb)8c7cQQ18ia5K%2GK$!jR z9am>NF2n2>|E%p+qz-UO_p`HtasvQYM&Jpca@7kfg;t^;#%7i}1{DRsC96!RqyyXs zi(budf=(rh2bHOpXJ0uRW)htykZN621c4Hz|12{N%^GqOO-~2U4XYfmrl?#WvdtR= zNrRH4ayH@K2q?UfVy6u=23?nId=8Wf!Vr{p_gy6~Z9{GSyLO~dSER0`{-hSx&wd&T zr|+ve)A{q9$bn_D7{yq4d%#vBj3oHE^b8VlaK9q`%hCx5@0r6F;Ur|nD$9oJV~gD1 z3nXN)*d0?+XH85(&>ynRJrYWUD(oE2j{a6sDP<_P${9OBqyPSVkC=Xw8w?IJ#B|L8 zxtFQ_>UU~sUrhsh@8D~!Nklpoz&4rIpC<@znsm92>pMOk&vRq4Mc#{m2E4g`L%M0E za};B2chn2D?xIFV+^kMUG_Kra9>i1Qz7K%^wH`aI+eZi4`{yhrw%95F(-qy<;w~6S zeK(xI=A2be;xT_wo~Mp$%;POY$%IBNznfXjKeuP6T0tnq*Lxg#Jm5l_@!N$jCpPXP7KSx?(<0{VvDxLEl-VJN-kD8x~>i_c>7r58o zIyBbOc7>el0X{7Ke0@=B&wKU7>R}`_DByaT8zk%#g$&x@^H5y>n3IeMMkDV%e8qzj{Iog$0noPXj@4h>p~fmoAbrslDEEM{i)gJLy6La4&S9>woJ4>yRj0cl>p5OCzkxzH+RNH0?$Asc!#Zdc+F)Gj8ura2*R&b<|=`P*D_ccW&nnT?X#PG9ZCjwqvfZ|Ls2^f zN7l@mzC{^808!pqNoL3b{NbI94C{N{7{xb9WLnKQgBn=ySc)27UUS8nY$+toMgb_= z++8$N?K56_g3tz<*N${wV1G>*D!!&?C`Z9X4+PBCYDh`S_77XYM5oJUUa z2gr!Kal4^>4;z1*$E>$T34tPfkMiFa4g}bMi0IGqC4dOUFLoH<#USu2qz>X2pV61M z3ON{cDw5U3@76D6tsC?9R?^jGV4?-4TkF%!U-k=s<(l@+n(PnlKYtmUymfALDO2UE5oLgr(JrXzg%W;-&FbW-& z_1CS8MV&tV9!f%3+xNi<5rx%@?qBsFAvC+i$*V691E?fRKJn?21|Fkygwfwo+-z1h zb>e2{M&cNr;<}4M!O@$*20PSXu0PALYXwt6eByN;B(B`xvBp5ZVCx>qEl}3aR~$yD zx#+u&%{ca(im<)K7b|~3(iV*CfR=%w0K_1@$zD)Jlrs$nuV<~d#XTg3IONvawKu7L zBnW==f5alkgDxA%?M5CDZ_f4Z^RVo>rk5tD|FT}xK6z6jtOpF>q+yDAVYp!&`knp2MHX<^y! z?XS;er!eD9{;S+gf2jN7(=B1#w~)VJof`7{(Yd!D2Q z$=U1`U(e8QYMxb80l(nR=LmP?tX|~Q>Shz?uv17blnT-Zm7;0q$4Wlsuv(Az`@U2Y z(Jc9PUZlMyHMGBmy!x(X+6B=Cq_ftrsof<99JO@)%96-Jn0kGrM}JEB*GyMI=FIkee;c?O zP9w8xr|rlKe576&P&neO%auqQAeHBuvQMpASy!{muhK=QJ76w-^f17$3zLEvmS$OW_5;}T1mL19$kkmMOqKa}lr^ z+Ctk-ps~_scKJpMfhodXfc!t!j^v`MWh3YAToI*(if}srN8KN7eSI*w`37|Z?Py~H z0oTT}R|l|}z!UT} zrsBV742&=D8-E*BTeS2jbU=wweos4iOL@eZT>Sd9p z$s8>X{~2jlDuBmu4TXql5BkfVGpN1HqZ}mq^bpUNq$84}h`|Eitmh50H+>l$`lFNr zWE$HhmWAT4Co23cs)zqgtpt1}&#eNut)f;FzBO|nu%gEcO}3%92$6Y?%H-DpN-C>X zSZw>D0t^Mil~8!>&c;0FdJQ%;?*7(_IVwCG|6TG{g!*>3S$P^KmlkW`wL&@5mPOMM>2rB2rC8tKKLsB+$%Oz8( z%keg6UQ80f<-g_=p>sD+rQb~R6XR4Zj$+}F_M7Y4jRt#`dI;|S?1n#vSa zhZGG{j^V(3W?OF$Ou&~TSs3Z#rAW)6;y(& z5EEn4zcrj6#+G|n4HJ-`Zu ze6_y=Z&MLSh000R@T3yg%%Hzs=RXW?T1;}eofsttg5+G3di6C6Sd`&$ws;X|vUkd< zdCBJPI5bFXqEAnNzw1fh+E@yGx2P=*@kFaHzErhGKj;JgRMOU{HBDTc57|GZ@X<+0 z=e}$aVLoUigO~q3AE5S{q-w#WV)G;FCx!{!l=3cHd&bp3m~6y)YaaC z8bX%2K5wL1Ifd4UR%J8BO!;-8CN6i zEDk-ZeSIu#+~S2Yiq`*MfxUDbOjpHk@l&22_bKu9;XozTUCkzL!GUU)akxkw6Vp8X zfu$wBDfX6(nf{?A7|z&Fea0 z2V&{qu(^j%0ToTH<&MP*^Z~93I986}$AQtgdQr4kH;AON8`MbT^7))==W(Q9m_wcd#>S<91P7f z+K10f9ZMa;Ch)&5kREqK`BILD^#TY}O25 zD%$x2&?l=2GjCMTYV_rmB-{SmN-WC}tue|*I}>*Gsy!L>{MekhYF}Fz(|QUf8tfD2 z597TM9#X1Av(>;sEFv8Df9RIk8NN1ok?p5J0~}k1_eQfU`_Tg9OOkgtUtAtg;o*|C&NBqB%q0 z@9e|k99%g5pv_4F@B_dAw>{6Imq2!|luf&bxukm$oBX*`Y9gCe2YjW>nC) zQitF3;utQ@N`gZW#h=G!L79>SAIZsjp`RBXtM1<}ed(}AJ*A_$e>q4}3Xk4~P-T** zH|uKRTZ+l-%BT-&vP;zR;ov{4PJ|<5e8lX(=G#~iTEMa$DEH!j&P83W0rA@V&W2n& zx6xs%Q50@&aOvN*HDXYCu-2nu{wyX1L}$9!yV*`h0~{t?`Lr>0YkpZ(o7VNy%gHT> zI?h>)YPOwYKlpS&MoM_M#~qq*NcGRvhvqyOnE;?sq=rE&4%WVw;X|4i-`J0Xm0le~ zuE6f9hp(Ii90?5^+`p)YLR|!xVV!9MOf$`YoqVia<*CBC6p5^<(5Z6p%$hh3R1(& zy`+0}RXq)!f|*#^SwaoRtu&`RlG^O2gs6@s9kWQJOx2P}Zm_}(pw!b&%TukJvrRbB zIie{X=0b#i3{wlF^NjN~@d?sNwkiyYcZf$XdKGr>J&01A^&tgR;j%?oaP*Tv4XN(u z>K8vFlwhGl@#6E2t|3bA8HPZzd;_8cSOKR%zPPE%1xpA(i)Fps=`$o8`jwn|g3BDv zNHRfdpgOJ>TQny@${pKHYgCY?v}p1Wu3;A*WKu}l`~C}Af>Ry|nu-3O^1^Wg2<4xS za+pr43F^IEWDF1k)HPrwvsh3Z-F^?H?12lc(f+3Ta^k58JhtyfWm=GBwF#sXy3+5! z&qkJItWzasv}YYg_gI`y+w5kDnp& z0&hXRKa$9$C&|lrc|(<6gbd;ESC72$sT0t^zCG{%f>(FKISq5Ew!Q5!G{}$#f`ZCq zKyeI?8M_qXjF+3#B^-#uBN&RNu}c8={&)e9U9SRe-T9^xdz?lEn(dM%yV!&003qt_ z&xn-GC}SP@FrBT~-v)ibT=<&*7v>R_8BbMxsuB8~l|)~$nD|J`b<$0Th%@DJ)1o!g z0Zn~R`GCQ&?L%Hwjj#>Z#==8Kxwf7G$H?hvV_=XQu!6+%h=JEC0;bv>gg38~z#td< z{Q^E-!;t{+ zq>wZ2_t_SirEQcF?JbEz9T%3Fs9?v%Y&qiMFDHZ)vDtjnpFbwlLYGl&?23mnn-iql zeET^6i=MeXH-M|^o10P%G0Fu^Mw4adz@tZD%5=U&F?dk1>U3#k4jx^n{(#6teiK!J z+vI|uPPg(k1s8!Tfr?}mW7EuU7YqFIZuODF5a5omej__IGc26?}v#!kS zfFMF$1BI=qX++A``G;dFgl4nFVM7hecCXF23#K4*;?Q>2Q!Ut3pjfJ#R zGwQ{-0IbAa+$x2P(%u&%Pv!#ZqUdjYT0)#wf1AndQ(hTTC3UrotsfKu zr%BnJevBWizdA%)BFIWMs=eGA;Q6x^<8il>gw1y~tYz68}xhzNz zIrzMkSMCSyT{I4U-%PJI&o8bnaQ=fKU)EIr>0DWp6B25kKI%qjEACK^RC7lX;s>q* z@{I14g*O%ThliT2wjV__tyA+ZopXv06(d``*^Rh@C1N^J?E8#F){zd9ExR+asZb`1eoYdT_@@m5Ty~SVK|kJh∨>_|HA6o3NZ|3%&(HWK80xF6M&z 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

    ~bTrcDMRPc`?F`nI`}=zL3- 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

    B*OW;Ld2Gf1wpwF@jFAP#!_0C?w4GNR{;Uge- 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�srzNcC*oLEKo%*eBusy8yyD^bZ+8tT|#ND_eH_R>E3a;+gv>C=j z=0UmTsX8foG~k=JNdCk!GK{V2Z_5y z(WoedGmC1(yz6>&T_)^bvZni@Fv|_mLBvDf^;rKBUzn?IMCQFQUzzew(5=i-xUTp# zX-ow5Ww6_K;g>xujQamf(q^QtE;vlwb9_%Y-8KAR`LOkenUT;%XsFKS0iYO|Zg_LM z?O0Pp{1UE_nJlnCyftT)znikE{OL$T3urnNgw%^_tu&!hN0;rbT`G^}sbwDNAbODF zhVwOTaKgmx}ubJ6f3JI(tVw-~R2qVVP#JGq>=qhRfnBPKomBEJLT(3#S-; zZl<6YYz4aqH9}9x*UE+_4zjjVtn1}y43V+LVEexIKxCOk3<0wn#Cn*VtkQHf#hEd$=Y#*M!hs)g4DThTQ`yp+!aAluaXbI1YIFs zu-Q{C#3PK+#Bo54Vn_o@d+O2iUlcJV0>h5nv{Q)%4iWFr_oruqA5L-o!|0F%W<`gh z>^d}Uu+KU4@y8^%Ymj3|J_f{P-J!C4-8=)*a^f$>h~*N8=ag$LeU01{(n>#XQEI@( z0$T5-+dF$Wpfu#YhLac9SZf!fhxlU4lka74opkIK6Q`tcRI7r^E!Dp@HoUwzS_Ovs zP0I`>CT!5VkAob6=3iFQF26+GED+`KuU8Cz0dM2e!UFPAdlNu&#TAJ$0)F8H+ z3?9(1;f)!@XI4J1(zkU?+_hjZLAk|s2%xiJw!7sqG^9jg@O?U1j?%N+dA%uFYHjm- z#xzOFMSmR1x!)tMUpa!}Z^>E*4FQJm!*y6^3kXz}>(_C@O%w{zg2VIqB{(lKGErdF z=YLZyMgw1E=FgvO4@DX3Hu^TRUsoJ>euDbM?;n~TQ(&!e?)nRNVQ@7H%Y|tKlfQG; z2ZKYd>nXV+-WTTU^coj(Je514%<`irAsGpCt7!i{Jb*{3vO2&S*iyJHxeFw@-z`We z+-I&OA(Cno+3k&ee^k38@aMsb9I_~02>v~rZ0Znv>eR4J+`RobDpX9oX#Jo1VcHRq zx}D$A;v+WdrZjNxA{QvJN*l~_#$6~5sL`-c2|M&b@=6mu3BAJM8KN#$tKL3k_D)$X;gZ>8VUJ`mJ?%6dOTrqQ)7BoH0xmyWE8quBf(; z>YUGb8%55OAvsO^NF0c`s9m*>m?Ls!c2>)-vqROiln!IYlld0uZN4;*d$ZjKh_lP&mf2ga04p=9pUeQ+ zvm6J6=8M1xv~9;0X-EAY8g*G=^{Vu-1zs0DZJbNtb$)4=rf$4Q_(%DXOWIm~b4 zrR{RXKpLuIx~Xbhjt$7$l>%dR8Hx>;^Up}Mk*vio|5YJtwsH|-*z{<7!k zO!03LfESV{!(Nu)rA-~K%$Zu9-tmO@$9Yy71isdM$%%k`0%_%A z_|TguFCIJiY4QMQx~!Ky+xz6zoqfVOCMvf|J0I-XSywpK(Z0q@ElpJ>8iJ zsaM(^z6+C`HQ^jG1wR^%EdGPE#;Y-QmmbKZt1&xf*5c;8>P-2pE*jHKogeZ`1c#>2 zJ9QW~T9p_Ik+^yaY<@tJ7c=gB*?4*|AkvQg6Zw|o2i{jgRL<&Dr4QvjNxQepTu}^Y zWbN=r%|?}!7wvKdB>H!xIEI3!!qDDplnK(?q%Q)ao!O8jt&FocC!maO{&zh`JW;@l~XKf)sy|>hr3t@+X8s;(IgxnHDJ4z@2C_<5XXh(l?vN* zftSP*3XUYi@_Bq~f=N7x^ZYwUFJ&*sAUsFiPM<)87sML#vtL7em zbp}R|?R>Pq{yZ4OfEyZ`jP)?R4mV>3Rw7{D98Pasue+en8*+7kZ# zmSaYCaT5vwR)9y7i1g*FtzElsyK4VUq7vkpdJZ4xL|stmlhpyMM*iU;3|!057jEqP zZ9+9o8S6-YhPsam@H*(3X}SqY7N*fd6w>~0g=qRDG?vZg8`?*M1A5lL&k$EEfdO>v z{{16D(TSvmE#2{WF!4V7flZHbSng&{*_40I8|2p-)nR_{oSs zg7i4lSF8kAaPS)aZ_)D8Xe-k%(Hrl?`#>(`Kk@|p6V3r@mOiXU+GssJPT=*lgP40N z7cLlY>V}XwaiEW~k)IS(8JEI9-sRH6l@?!fhtfeV5D3Y?sG2BOPB> zfrgRN#5l}Wrc_Xe-N9(fSfWu^wR7hm9%+^WL~O+5j~c_f3=9yOM6w9N+@IMwpAU)y z-Rzv|v1S6G@5rfjh_XW%5NIv4-KkYRnkAu@$KiB6KU~PV7cp_VstY$F zI?6JzIE)hO^$=rw)*{{Z%^O2lAQZ`b%=)-)gILd^Sy~Wmx^)o6DY9$3b34Pq&Xbgn zZ#rQc7r&dVLg&LzS_P2&X&lGzzkI^n&Ok^*<1;`r>zb7usAYe6g3w?B3(q#wQJw0+ z1YO))$0v3njP&NvLc77nUKP4P_`-&NebTEB)4U!+3EZ%u76-%$oI}DRm5VgFz%TQz2mYvP~dNrYV3!bpb*Q@ zMKZm>2f8SwTVF{Qq1r`BinZtk%P@R1ceB*`K94SE%{9_3 z!fS66ufKzvxzdPYXG&)^mgC^yIhAi$0@G!|27e|R^Bw(i!LLPJK=}GBIhIxp7$DZ1 zH>?Go!-4vSlegW`YYi!s$&)lb^42;lJrUPcWF^1$sJxhln`F%@I2-bDT)q0^}f? z8Rz}J-&HdyY9zqurPO#_YeF>3yDb-3&T#p-cvdMLMcXi_d%|cPC?)b7v0c+6n?!it z1Pr^cyOwA))6k(6Z@z$DDABC=*TR34Ks~hrh+CrKeWed&D4*QdSR%4XzJUQ=KXWhmZmAfD-1&8g8hkR5Ai;rhABUC4^3uLr$nb`P#p4-NX}e80B$y#WJV z*uQ1|W7CBsPm$q07QJ=IFw9*vw?xU!k^omet|<3e7M44t$(!)&_XrSg_E;*B{^kNG z^5PXtvA#O){TuHro~M{@QKM9!_!BM?cdW*HuDXlcrU!9G@| zmgkM)lMioJ!8bJi@0zG(=8N(_{L-3cZKhEA`Z^q-NLXO2)wExrq0BR80^sFAF|5f} z*^F-s1Dwftnv({LHt@5| z)JtO+BM)G8?KqmcBM4p95AwA>en_8dLkO3&H+x3h)HMQTpK4zO$Ka!#OFTrB49*kn z4Bdxv+-CBw;S*7ARi!1&w%H9+aMmvx_sm>B##+C{e8+Apc;`cE}VHndYFYM-$RH&xDrrB_+B7^*A%d^bXIi>JbeE)l>| ziVM@ecLY{ZW)Dp*_nV_fs1;*%Wa8%QAXG+ShXeIFmzk=Z;tP(ncrYAY`&fBr75jnb zI9TJvV@@^Uc}JV>#|%JDv3n-IgM5az%P2qLEBK1}WS}K5_^7)HYigheJul~}sIyu+ z37`Z0#Brx(q!a&8^~_;jCBu2zLD%oDIuh!tOMR{Slq3qlr+;+n`av2VfY?Rc6Z76~ zp#g8bmYNfF#I+NEMeu3sqAEOdrHP~*{92ttIp=DxZ2XFOLm{Nf_}R!1E1j>;zsfT1 z07P@VkO?d^?P2RBv6vMO0HgbGSx(Z?m|`1Vkf{e*zgA&0DU^NjL62>YR3EEwytc-h zQTqQKPnhP}^j&0<;IIWQvVD zmgrM>5uK}$KfT8lfIED0yQnsHucdQ3Su?!k6S;$M6|WXy1!SOw;{cuI?e{v@5aV?> z60y!Aktm=73oV@CLxTWp5%e>w<@zvcr29`OK9TU#w%#0raUR4-tsV*HzO#of0sS{jLa)y(;D3ZOpw;RbCa;OHL? zYZr;yyNEoo1!@RX@gp;+#F8KX2#F`(tZy49{FKNH#V%==)-4KJ3}-2hNkXvNOR+ep=I=YrX`GAMey>(^)WU?%WclLjZL7mIynfNK&b* zy7?drv7(1S-=gpjuBmQRv!c6?3wHWma<9`~dVJYXbZ5`Sa}jA2gi6U*(H&C2K?*aw zu0Ruw^YoPQ9W-=;pt$`>&3P%`cdLN+pp%8%1Tz$%6?X%dke)O}v!xd&?_-~)^pGc3 z9cQe9)V!REk`!7hLJi+dq6U&KQG5vYxyAy&L6jV;x5kP}u`>59!oq53nI2vAYqc=u z9e0+tPLokr^~Lg7YA6x}+|rKoaaMWh%zJ-nFBV9ABFzveM`M2!+#9P?-3 zHK{rYN7-W{5gl-=JLqCLQ%I|Crf;l9DfO%q2rZ=2cGy+n&|>SY~B{F%0mE@PK$rfQP#X zTmmy$s`dG;uSG|jyNCZ2mrS837>AU01J&a70B#sImLwE zbTur${xgSO*D6&bvDS*br{O|cG-rICT^(!tsvC;snor`yG-L*PlVxM%wiRLWi*rR) z^?=-J@3MhMlNQx<6iq;q@MpKyuL46-apmLnu~r;kAF1QX;hTUF8qQVdeE&?yycln1 z_TI>^0ROsdn>A57nZbQC$Cc?ZYPjnMo_)X#j+V%aM4CatB43c*wM<8gw%Jbyr03I3mz}UWc;|*|s_gzVZL+lNduiYFaiGK-7vdC1u)x`h= z#`3xw4W%w@Z-svh45i}0frvEq#(WUI|2<4^knfIPk5Y)kH$diYqTKu(&|$%T$;=kOuT!2P|YkBXQZu_ zt_}fy+S%91cA3r`mdeF%fpI-^EVUh?8AgIBzKv}KRd~eh1)&+LZ^E*GRS5Ymr*xQ? z-8nMd-O(kCQ$?qU`)srhotv1aARHS~^d}gt%#Q5sh8mfXS|&`|_kS+wK&w{m`kgEB zv1*kDq5Wq_+{W4jUx+fksNgx@52YB|K(5$qeUFk0k0_WlYEhe%RWOa`XKX2+baw>j zL`WymA%^GniZHaHz*EaaW)?m9dSD}Kr}VZfwzWhYjc40U-co30Xs$cdH|t|p7;(QU zkwE4mh41*tKA-kBf*GLYG+KGpE!<8hEUcl2bTY30MK6%zc?lxZ5{ICGfldi)7or1W z^VMGsGiyS5L(}Q2XFs@oW9}DMob7|$dtwt&puhNBpomWiS_zPFp2C(BB8=x4$UeCn zb{;8L0?8um0g?tIfPFi|WMEvu;z3?+u2xypYSbAbJ#{UvXWSfoAr+4olBxAiK|a5l z4;jjlTLC`n|B3ZA*A~oDiTulWF>_uc-#XQ6+E^S05+Iiw=E~F0E14JzZ`{X&PV3Z1 zfAHpUC#J?m6M1mWf1eHa09T5eqirnC zd)!3cXpT)Tt&;RPNFlC;KDF)O-i>Qzph19#^CH9y^IJAheBhLZSNsEipI7Ltt2Xb4 zw{=VQ0gy{{9SO=)=zzUQ$o0~92*MrcZ8#e(&4?z}G?Ucu=bs-$W5LVjb`P3b zAJ7=?viN`VY>GpfD3Bwr_1?O2qI2i#Pf&xeV%N~9A3Vk*_zBL%rt%d062=8#?1=Ko zB*i~nMy!$7>6SLE_6Kb<#J8+Xn6Lqn8@tD=W@Ti`Mh92n*enp`wXG~$WxQBA*C+5RgB)+fo9s6WD6_EIpHPfV?$9sh(o-l?fcRc70q`}O_h?Z;I}A+a5r+n zuxSsm6|S8~Q?}c3VQ3G(;Q2i+Ss0uIjk12HVerbM6KCjMo4fgaI%j9rFkb$L{S53l zTg1rWmj5hCu8s<)a$*GhD51virXiaSx6K%~i+b)&5o%oNSo#q2WGHgQnj?5%fd21f zNZdugoA-3$RtbM~$6@QE3MCEMjFz@d$MGhSCi7eJZ+ydzf)xKTLP+s^#XtCQIc{3| zisu)QDKA_q#)iXq;A2h?QuxsDG=i|V4>iU0%TKT=^tzx zIP-~wQQhN-9EFyhT*UQ@AY@tMM|yEck>+0~=AF8!1O;Iip7fobL7Qm;Ea=vKh_K!B*uWxv+aRfq$_9k)1@IFv1b$go#?5o8WSEkG% zm3gq~RZ1QHbR!oo6MfCUSdFKL`MBMgSLPa9F##W~cxIRkS37k|+VL=Xwb+#~RjL&T zpo`6_C>IY9KXLWSnolwt2SZZNu`8l=lLYU3mx$qNmRsU2lmbbdix>%XDfkz4l}{^TtI9=hRk|~jbul>Eq@Dl_mY`Sz zfZa_B6;H%&L)!gXmRF8gg-h{U$n%HRwbwu%1ATZYVnX+?+ApjGOxktK2;!c+9ps|ZGf_d`^PZ68hOZRctB(WcG zdy3{Q(WgZxvfq)cLH{p438>JeOsi`?eiX7v#kJ+u&<4Oo-aaNxF8&}kn2=@ZY4xA) zig$j(>oLz@vPt#M*hecKLp#vs?D~BWmw%Rh6#Z0lC!cf7q4~Q%E2dGJAwfp%(jK=x zV?tZXt}X#c(yl&;_G;h^?yh}dn^*)>=}gYAy{?*Z>dmKX;Xs!)T&TmbY<>Df38?o- zL#X$yyaVeh&YV3|&u)iDtG8%k8T7P}Kj~qcZ_xGh0~-lH#AM&@yjgw?iVu}LlkaZ8 z29OPwuDGZIdkTLQhK#MYer^veGwZ5#%pCp|yq8+TPcYv=Y;rA*2tyj^opWvF zV9Y#HxJb{3@nl{NGRa1^WIUMJP-s#V5n5rH@6SYrP|0Wc`zfhzzX;NY^z*Dvq6|no zy6UOA6?bxwB#GC1Rt^N+yI&j3)p->$)Gwx_%dQ{Lb)F?_i-slAgdlYrjs5(gJyeZ( zr*0qH-(@Qqv@f%ek}8=EN#4~oJzt$w?&ZN^dYC=(Mz9PAriF3 zqm-W1vNOwm~cCO0YX_R*aO8b%V0ucxJCYmuA+$(peA`C^HpMHGS* zBX!Symo7&`AG3aSN&-1fh|VMBg034u>E}ZX+s|lKQjWUD8lJMA3SYLZ)+(?Gmo{7^ zy!j@^MCO?_^3L#!CyDL8@4e6jz0k0yO6S^Qd<2bi8uwX1IM(07|PJwmQ z3ssZePXd1(VHH+2}du?I2SMC0+fo)?iTkuyiAw!l(o&4NG&@LyAL$$N#Wqv$pqd3<3 z{l-uZF!ct)Ea7%qA-eTLU32@UPAi|-1i6XM^)Nct%c~gqN5-0#R4XP^g`nSdy)BVD z|M31=NV^w{dTe9!ge*B~^a<#Z{ajuR1%s@zYOFsd|8Qbt#3)P}`^5Q;dIV>-Q;Y*1 zm5z9&q|RUwTF+bdfI+&1T?y&q?C~0ZFVazIJ%Q;Zv8X ziMhN8DL|8Ej9`Pbxok@5jq_Oi? zZT&ELwll~2ZU)Ae%>lrg1-c^uWn)N+9ZxSqp z$RhR6Sd0-DjBzjjt5{L8t_n%DzWBq+Gbk(!J;XV=G&F!K?TC4T#U6C*%)n#(JL6f0 z2TUQzmXh3eei7}R+csFrpOirs)}K05+K5FO?(r>y#TPJgKQDXk5wDX)#4i- z*+lUNxZ(2pZIf%t8i|zs>0V)hBBKp{_Vvpl$2wCJ4bS(q3DYof6|F46lP&!XxiaZI zVMD`dCO0AKqf)p*oPcbdOxV1!djF!`d_BvlY9i*`ZH`IQKH#O{(p^Lxn;iPo_>vjM z1<8@DD2UR?05-bilJNQ)4g?B=tR%yyj@E)&6gC&4zC!_NZqq&n!jP;i;?} zhTV@4dj*~@ZGyEgP7k55&%YP7(|-6iWgBiIqrMOIHuxng5lAO+=q~1hR%M)cUhD$< z$gR*N2?EeUIH2A}J{hhmXuQ{O5J7B;P7!10z7>>tst=4u{h9=iOx#+rK*qNzW9oe} zOR>od0=3*NbtjC8PzSK|`M9CDrwgq!B`2rQm2GoB3Wx*vB;K>ycj+vcppN(l zW*#DAn9Anx9Y!U81Ixq3FI%B8E*}8eqURwL#$bzTgs4K}-EGojO>kk^i!?J*1N@t% zaAf3~xJF;P!qw8iEFA|{cadz@v@{DM(R;s3^ycW^A6$v4fWKTiBGSizj!fpXG=lB) z-_aee28XgcIdy%+Y4(L-ULs2L^*<24Ueo^n_25z^4O$yllz+rcav(r7+UJ}oxyl`1A<&h!w9=dof179@xEhx8*DfV&?hk2k*`KxX42m=^dFsb3u45%3!OiTwJxm4C zlPiV2n1Ue=OIW0x-_vgkHi2^K>%^~UCZpFQ4T$~BHmet~5}_GCq4ja{22*=r}b+Cv?a8+vyFR6JTD!{R?OyZMcZ{W5|Exkc|PcCqFcSJeql1RtoubI zlpTB;e8q-$?854aE0b-;vmSJw+!|>Q*I+&~&ZJU7F6?7Ic>bb3JM8(G9d)3LnI(+} za`u{`M1%>)I40uigo^Oi)Hh^8#CSF0}6PUrLf+OVg`v%VO(6busT-# zm*rLW80^A#8DHI)*CG9-hbUFl%;1-duq$pf+2tupgQ;b&25|qG%+Y#oFz=1~%_4;J ztR)Ek0%AVxt%oBI>=|1y==h%hW?=}vL`9K#?m|nBks}e+N3+zSYd^J!m`&93u+=UY ze77UrWTQAu324oIxLvk4sbu*mn~vxmg;`ATzKLAh3(tFRf;$d&ETSOJ-8-{t>Ysd0 z?M$OrOO`Y{ zHv;xg;-~d=TpR6SVsixI2CAx(E{tUChe@b_!Z~%s<*TH(MaaUDHpSGYqE)H~eSpKk zF*oK$?;8WX{jTCX=UuKn0#)tr-_-FT3MVYx0rfxT3N1!U_Mh`XbA2GU62$N93^HJ; z@gCLbzBXi@On?r-*=R;Oph1a9<1{Hz1kk~Ulk$2}Vh^Azv?${pO17B>Gmux``^1<* zOH)7D4d#?)2&{v5{u{0rPYAYfqhySi_r3%xpo4O=G`|W8TETD#SAX;d6^e3bohkE{ zAWgTUTh#*eemOCspb#AdZpgeSe|6+&la?Vl0%V%t*=c6Rn-6`amnw}T_I(6ACFt*= zDC4pvLGpqCcB9%8D_bz3fo2TDb6bq!^*2?{_EtxK+Rh1AJ?V5c7N5`ihbrE31TQDa z^))@{X;5Z0$U}%8@6RqLR5Z6vK12N7y1k~QPUHphTWFgXzM#q#t(GbWo9=A$)8)nl zCu2>D(}u4AKC*B%aOJ1e5G=xK^POsSoZo*Ma}GRQzUce%a&-=TYTeaQGzD!OZ5 zI5BG+dl^}wfsh6qG|c1uTTs*b>;(mlvR$76%I^kqO7I;AonCyHZ16b5qj8`cqpqlY z&|)Jh58`@>UD3K5K^>%;w34P_1P&dzX{bYwzcAeb+HUbLrRL820F zpXP9VJ^93~p1c>n7h95olzbE*;K;;F!8}9Crm^+yPc3 z6^!Oa8X|2ztk6nnijw?jnTrphld>p+x}o{^dMHYhW7E^}4Gxvx9@)E=7*w<+-(iPf zm)y7_UP(pjZ-ak}0HUAiJ!pipHc-RE|N2odkG+&mL+;ZgD^|6nlJ^;#1O*ItahBR8 zy(}*|fF=hio_Kkc7}pL2PL!C#G6=^nfYaQAxI+u(h+>`Qz@akLRF4vrJrZcbbjM`u zPnm#fBn)-q_d26-ZCD5MrS;E2${Q9A>HhqKjYJv6VbCgTQFx*avx1|%S8-*zyI*cF zMp}i(Qu#0j2{q^4MvN(dtxwu3nOX9N>v4C7X90Wdu=W<;v1>nViRfeu*XyGWM9=Y~ zBw22!0B=I=hHPP%_?k+4HV1nw+xhL9ppOJoJ7)3Wuan9=FbvDr8CIwLe*`50q7|iS zifJxt;N@7@kV`UoIL!8?;nrhwUSY3m;+y>wY|xrpMy9ysJ**ic-cznz8jR12`^4C$ zl-(rN_4N(N(82;L2XcA*d{rP!%^y#p@81Ucw&29WC>1qZ={l$~w_&HrCLkQYLepi! zuwh9nJ^h-Fx;_z2uU;zHG~pU~9B=Bwvykx;hHgeF?A2{L?nASDuA3V`?V~Tg1l=Uf z%P!4azSz8pTq;~(+vT-Pus|%oiR`N1fwJP)I$_A^m^-Sr!{Z~gt+6@|ao10hrltI$u~LpbpS|R{ z8+~BNx;TtX87IkD-i{yR6sf(N^ii{9%FVUuJ6bMYi*~r?}H9OTz+2 z#qpcodY~t#+`tqpV`ip54A?n1KeWuDBssZiyoMq4@~<~XN=0Y#ivW7arI1eNwn8o& zs2O%t;a;zE3LiE>Zju-L5b6vMs!;q%&D`tCxf22Jgx~Kcc?lgW&Ca3=k|n+jg8XTx z9H^mo71YIbuRIbb236OPAYN+9lW;|kqX$1lLAXUt1Jl@dwrBHjO|<_K$q~;x)4aqt z$Gx-SKcs$)y;AOP5JMHgvx|A+u?fdKZZSE6)DjMC)A23;udKVND@pOS=Gl=K2$(dZ z6@A*uXi)6bU!EWN*9M25*d+q>w)-}Os>V$yM&8SjyIP120zykvm$#2K3^w^SJZ3TC zseh$?Pz?6^Qd*YuWH)n{A%`5rlZxgOz9bWh#^YJ;K*2(YrvA}ZIVEylk&3Ig2l|hf z9-Wm=S476mZnROrR8vmzj&`V%6GePUb9vC~+38<-YGcZy*e)V1mdeOSTVC^>SATK^ z9~-{PdS`|h06|{mSSms)xDc^~bVC#iZb!oR3o)dF(_w>J_E;#(tde_3%cu4kbXt3i z5_|{XWyw$=?fvFRq;^F>35UG&j@xndfw+1J-}q{c8RDyPGa6phw0?vcUk%M3YG8}C zs^}weM0Og_h`V#6I5ZhY$&|x{#Yh(c&+y-UL~r9UpXlD_p&B}XIq6aT;HjW8=D>^} zn#HBe6x#-Pc7&K$HB5otsd7!RL-aYR5d%O6{ll$6^+}2#M}34wqu+N7|Gl+<8N5II zjXMreN}dQwr`lx>)x%(OTaJp&SMR??LPT<^^+dnW&!JwRVEPPQt8{?}IBL!f^&70W zJ)Bjt@AQe}ao8NH*3`Ik_%X=N$0F|rxr6k(m-wGxJV zyeVPl%Ecoaz7G1C&cr%4izAtm&9y08R(OjWA`sf}uW_6n69Hu6b#6`si&NhB#YT&^ zjLr%42=8mfYa@{E3_y9kUT=VuZ1>$+iAs|qtbDiWGax2)XwIb0b8uaBcEsCUWcxL3_3BR#Dt(DtB!nD zn!}VRCEmS6G-!$36=aKpn&r&^Z9rKBzpLZ4^iYukkvo|(?nA{~&^Hj|rR6_G$J%N@ zZ$lgltM9EKxOd$nIqKy=Q%nd{GB*anEU?&7{by7whZ={e<}MkMk0UX( zg1r`+Hm)H{!p7c+oFj~1VTJ1kJRar*K4GF8?C{{PFW@_A&ink)9txcnm`F>5<@#0~ zDXmEtV()Wtw-ll_)t-7q=CT7e5mcl}!+51t9%M$IQ%R-G6C(lDZtvAiPAC7v+I!u0 z%ODB#oaIKy?4&lDq2qWxrFv~A82FXXvZj0zJ#Xp@yMok zh1k{iOn(>&k=yOv->ANCFzSc8cwU#E|1I7HY!9eev#3TR&#bA#I$Dx zOdCfTA^&l%uuoAw9Mk;#esc^VE~e{rNSj=}8j|^Zv?lq*Y8{RJi|pH%Ay;4IYnq0{ z@+VqQErE)qaN+WdoLwOtRfS@*{64wUsU0x0GFnu)x_XqT!EZB=#QTaKI7ppJ*uCmG zJD+=rCE$e$BSM~tO_=lWE+ISAA~F?^?IaR_nF%H^`Vt+n%D6lu*G{S`(UIKU*I~00 zpS*Y51lc4*3$dJSJo!0C0Nb#6v%%FCDooduQnrtk1k&=8dD1(>flZb)(?pWfj#oY% z3L~DIbuHA#j#mzjcG++7DGeP~wtmWfr$SHn#EcL!gq~uY5VPeRN~-3r0lntq9cwE- z#<%Q@dah(7N#O3~QBo34YOJ#3L^)B_w3BpM?nLRC**#t{>UtT~b?Zb((G6g4YVLnkD@Z%8^7R~#RY3v#srTi^?Anpi#QN}e9aO) z>}6E|%FryNRE{9SV0fGkA49Iruc31lLn1<_)!snRX5N5248hk?*cJ4QfXRRO5Mu^|=u%Ikc}^ImE=Bv)5ih3*OX{p-U_vtFBL zNG$KeTNs%KMH9JTw8nZpQWNLo#`y+e`|lM^Q4fonVC*&L}=#8~vwtEqxn;#WKHG z)=&f`oW(tfE+Hv@CG(X{uQB9C>?wM%NAB+-eiigBEC3*e+~4?s+=fAAuib&v2mtX( zxgRLLur(~>?5r>my@C`}6P|{b9ufk5MIXeC+H@|D{EsKO^Ev(dR&0Wo`KUp6F&Dk|xV?}idr;+LndLq4x1$_Lpj*8DSCiOjXcC@ zRY{OK(eLUqM5ca+E^Z(+)xMf{sjg0Hk^JvZ#k)Lb{r20`T`8U~@*HAoA6l zj*pW=K|wK0X8N;992jv)+WYWMYnVESKBFQdgd}cPD%7j2DH0(op~>(n(sH+nggE$E7pNnygg` zA-vDep@Y@)&Qb|O7H3hu)e^7;%wMK}qgxYN2TBF*bGH@NTwG*Tt)I%Xf*K~3+(7AL zBISjvfU+pn9QieNqGQh#{+HyOSwkKFFV&Fqd~~5c$p<5`o$cmxpksL2bS9EO@D|?L zWGNkyfZ8p$>0url`{yvAGjb!z2ttFCzuWoLBX2N}rYr(@5a1f`S{ZTbx=WS&sE$=O z&{D?DjOt;>+%`Z6Qe|ZG=|i!CL|C&5{b=O(j zDc;qTd}xvu*?qjs!|J^!9=R)s+l$u$y*0ID=*xe?SQlfaw#4)C_GYY`EM={#de&-o3Po_C+b6=8(Iv6mm`E@59Mms@qH0e2$uOY2V7W|Cx-_4X z#gCO0C&@0&vHEm4sZz~2Y}Y_#A7IztFua=IgA(*Gd@5Gye+C$X z$If%tnEN9IV5{7IY>HwC4h(DKyJaJ_|7{82SKMoM4n8Q!!)d>#a9nXH5mc4?Tr!r0 zfp;hE9x7VMr=v;<+)*ZQd^8N|3ENtJ*jndQ>_~8&4f*H)symD+vk2x4#M6mQ6Nq%) zs<)TW9u3AH%VLP1!5kUG2j;l@voLTs;0o?qBQpnu%JOVtiIFiFxOn3&6Z=X3c~O-u zv2*&|Z)BF_pvOY_!u{o5j(&&I2>1KV!#1xjyXK?Z9k5MoHDGVbsRzFdYidrPKl>NW zXRUEqIK}7r6$ZnmR1y4Q_qQj7c+&DHZ>h3+^!?Xi3*N4BW&Xa$fm{lxUMV#d*KlG%84(tMjSNn)a*f_^U2}hsQ7Me6MMvlSh z3e!`499rj2|66ZEX_uc|u=+0oXnZwPn$OH-l|kxhP&=sbh0KFoTwjDQD1+n)D^zM( zZ0iL~59;d@M`O>Uzks<20A#)A0DYoj=QB1qx;zkdvB9%Fn>t_%mx|}d9f8@wH&1U8 z_8*TJWp8t9Imo`@ZzKjyIyH zj=mKZitFE%TGF&s7aqT&V^x97C?6&l^ut1Ao|(H*mnVShI{{s$+;Tlw|3w^Cel1R6 z#iju8w9Mg%^>{`mG=9VN_N7H$5+iy>$^f_`mV{qjgTEJK5D^o!=GrV8;cL=Ygq5hE zHCQXMv?=r}A8!8QlzeBlO8HaI3F~2{o5Jdz{0p;a(?De$G5^^8J%1ORUE$Dx{4(6m zXfnghzA*z7GgHu3^v|uC)kK6T=m#V7?`EntwRJ;(`|8z3eUJ-6^3xzskk|?yX|$#J z<(hkTKav`_+y3uqXDW6(=%clgwz>mdjGr$mC#3wCn<~$|lS}sfZ4k2oju7FG48aD} z8_>?g-x}T*kbK|HVdFcpu8P6?{8QIngMN$q|22>aokHrk^`T&Igbhv8<^J1-W(}nI zZrtMZCZ>5b<&=F`s+|N1S~Fs!Y)%Gz^wNJT z!)OiAuksE9p#zJwV&xV(h{KqH=_>r^6^{B{VW81gQUJKv#IST^mFLP>YR zUBN`y6lq$d-j=FceHYk*YQZ`pc8e39H>4wQRq_{FVHH1W!ychA{_AbiCB8C8DE ze|}45|2;)Q!cxz#R!AcWU%Z?<`caK)06VR))1uK6)dI*GT^NaP@R*4@2!1E90(|PP zhw01g1};Q~WW_$#)N`e`{l!ARPN{CX>5V~RjKETVF&`46)NI9eZ7LH5WIVv>fh`smCCWC`+vt=)kzIG^ z1J)N=dF6Y7naJB0IPja^Tywp|H8;yG_CmviDD!xdJM00}<7YdqUv0%!UE+a)nZ5&6zV_j-F}_JqM)$yEf(XxN+PUP?;IywMmgX~(A-{ST3WJV}|EhI4^v=3pG~zZ} z0`AR^9}wr0vJkh?`Ck2P%|M0PjMi7cK2B!3tpQ@S+ zz5Q{dSb3(Q59s}liH+B(B~;RH3hqc{W4f|7!nuulih5&)!#6l z=J|Ut`tIFLu#lQYem)aa7ThjP zKZ8PyMJ(^dvlKO_J-g!MO&<~^CUNMr=ZZeIk{=-GU71B?yX`Q)kG7FVam=>)HaDet zdRQ*>-=L9#6;1`s%hQs{Ll_>6^0BR1oj@H1YpKw}S1cutdXfiw|9Ku>T$vxlfbwfv zQiz_eR!@M{L69nSz<}w5-DgLG)(74&Y?ohq-NX&Ud>a|-o8P@gF+RCm>ha-fSl_*P zFu^gq0%_c6SUAsYqYoAPt3pT1)CWx!^~gw8d|wOelDugxY+|<(%T)7mMm=}X4?2zK z7;|XWRSuhK)xq~N#(A#-cCUy|)=bjN!ng$7qHL%?nQl*_|CfAf#Qd~$4^FzDvoB~< zuqQO!LAX82W11eWMSzS9-Ugf_)PIR>2&@KI3EQW0a`ox-#}a=T8cn~*{uV{UsK1sb zh+Aqdvv8z%#5lp&q2e<3sel_BlC_l{m(g_ralf!=I6TLVk;!2l3t84lq9S<<;0;nf5++MzHth^sLvELIq|#UT@5CwhfCWU11*w+~sL?fFp$48&R7z+iY56|0KKoLcM=pT%iSV*5| z)R4ad+Yn_T-LiG5D4Y+5_+^JrR!)(<-r-BNH9{$Mwd0a4C0dDg2rx^SYwhoYej7Lm z3G0&jiF_DC)qbPo{&rL?xnPK>o#ilnAG0w9GT@pvKS@=90wnJ0B?~B3Uk&2zuueMg z(U@lmwEch)aos&WPtELoCTi}ELVxJqv#f_jReQX>_H--122)8nV8Oe2C0xDA5{0DO zfhb$EI+Hl^))64Xe%KJi=XnxNhxcm_DBsje3pVg?WRIlF2MZ3$+Hgbi@be9nGOS+? z^~>eh9engQPZgf3DC7g|esgg2Q$t z9=zUZbd)nuU2CiSjfpoq7DSGy@%7-OHNL5+c>Pt+C7AO{VK3ay_~q{<6VjN(rEa1U z=R0@Zu-FTY50EEl{8;@@xC654nd5Wdu5V!`Ubpu9)t{eJYH{$7IWNTgHo_jtZu8!W zZg?uRrjb?sx@|rY_|dviKD#3q9r?7uoWFY-X^wIo4Pl()BbjTDCOh2Bc@1p4%w0p} z)`+H1vdgREn_ zEKAJR_{WNnQ9lhc~ z%AoBEYvzm8Jd^(0_-`CAW=b#7iP=sBS(N9L$g7J(euBkK9@rh%REWCaygBy_sY^vQ zX*SjGe6vK^Lt9pP=P15p#waI2a_@5uv{D{#@uESx5t|$2Zzjgptua;zDkPG*wWuX! z5mewOy58kZtTL9X73HwpP2@_2^jl_dhxWxyOx^r1FX#J`y& zTH{ijmV@dhVKM>rt~Elyp3hGHmmn+~#_#8gg0GAvXz1ACm=C2-jW9(j+PkG-c|@ zj?qdrMvmrB{4nLi+1e4p3pz=Cw#Gf1$EL;p=`NAjerh>sF~tBTceHiI=t9^R=$$7! zP>0DU%gJ^gj9rx@C?LIDG?wD$C)~D3TuLob`DX13H+u2j(+DCr4*qtL^Y$HEK4sWE>pnVN*VZmE{Gs zl4c$rh~mZ;m&9=tMeGPR=<;2iJirL1rP0jFk74itORAyOGgCBRgzo1g1Qt;_XO+r? z8( zn62cbEZ1yx1*`bMl8RU`J@Q+oq&`z4X?&vhK*v^jD{aWYu|caK!wHzwl2Jf*m7Wt) zK=ezsio9$q$ByVGk8x=L&RgZ_Evp~gygVM>mUI&i_rV?=C+NHi7Q?tn2HTga0!cgL zT^GUIbR=$j_bzFNnWX%l-*aI==|7AdOZ_yrC%IRaPn?5aR{Nlg8W{V(Q(cRgnny7Z zt#7j6;7HrLpfr>+E7?HWW{R6>I~mE-Td2U~6pnQJu&2ilx*V#0rgYq48m?yayOf;x zL&k5W=`;2f_*bH&TPm%}-w3)TtwEAU5Vq@!j_qFws z0%SpkEat(ZVyE+$beorS~W}gZA zyi@^#%Y#_^`<<3`rIof~p+zs>)t@?=K2;8K{tc{rcxAKaa|3A)MN`YT4hnS6$A1^F z>bet{>GfxPV#mr1{FJOzh5g&@1KlBEgj_AW^brC`%F=9@5oj?Dfw%tnZ+dqQ&K}wh z`p~t>D5y;5ehV!OyZ(sLfcWn_HiOe8f-CPp7D26_5<7s}-^6r^se7VI1F%8!`-oR; zjt{jK_r6bsVyZ!)QNR5jZmIit9*$J32tDk1w|Y7+4BacH%glTZGLm-Wm$*tUiN#ZoD4RPdaSJUj1~kHDCp?nGhHkV7?!%2%0DkMO}BM< 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 p&ksj&>uwk% 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 NKzIMh318^MG^t}}5$FH_ literal 0 HcmV?d00001 diff --git a/crates/libmarathon/src/render/pbr/cluster.rs b/crates/libmarathon/src/render/pbr/cluster.rs new file mode 100644 index 0000000..f9365a0 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/cluster.rs @@ -0,0 +1,580 @@ +use core::num::NonZero; + +use bevy_camera::Camera; +use bevy_ecs::{entity::EntityHashMap, prelude::*}; +use bevy_light::cluster::{ClusterableObjectCounts, Clusters, GlobalClusterSettings}; +use bevy_math::{uvec4, UVec3, UVec4, Vec4}; +use crate::render::{ + render_resource::{ + BindingResource, BufferBindingType, ShaderSize, ShaderType, StorageBuffer, UniformBuffer, + }, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, + sync_world::RenderEntity, + Extract, +}; +use tracing::warn; + +use crate::render::pbr::MeshPipeline; + +// NOTE: this must be kept in sync with the same constants in +// `mesh_view_types.wgsl`. +pub const MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS: usize = 204; +// Make sure that the clusterable object buffer doesn't overflow the maximum +// size of a UBO on WebGL 2. +const _: () = + assert!(size_of::() * MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS <= 16384); + +// NOTE: Clustered-forward rendering requires 3 storage buffer bindings so check that +// at least that many are supported using this constant and SupportedBindingType::from_device() +pub const CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT: u32 = 3; + +// this must match CLUSTER_COUNT_SIZE in pbr.wgsl +// and must be large enough to contain MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS +const CLUSTER_COUNT_SIZE: u32 = 9; + +const CLUSTER_OFFSET_MASK: u32 = (1 << (32 - (CLUSTER_COUNT_SIZE * 2))) - 1; +const CLUSTER_COUNT_MASK: u32 = (1 << CLUSTER_COUNT_SIZE) - 1; + +pub(crate) fn make_global_cluster_settings(world: &World) -> GlobalClusterSettings { + let device = world.resource::(); + let adapter = world.resource::(); + let clustered_decals_are_usable = + crate::render::pbr::decal::clustered::clustered_decals_are_usable(device, adapter); + let supports_storage_buffers = matches!( + device.get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT), + BufferBindingType::Storage { .. } + ); + GlobalClusterSettings { + supports_storage_buffers, + clustered_decals_are_usable, + max_uniform_buffer_clusterable_objects: MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS, + view_cluster_bindings_max_indices: ViewClusterBindings::MAX_INDICES, + } +} + +#[derive(Copy, Clone, ShaderType, Default, Debug)] +pub struct GpuClusterableObject { + // For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3] + // For spot lights: 2 components of the direction (x,z), spot_scale and spot_offset + pub(crate) light_custom_data: Vec4, + pub(crate) color_inverse_square_range: Vec4, + pub(crate) position_radius: Vec4, + pub(crate) flags: u32, + pub(crate) shadow_depth_bias: f32, + pub(crate) shadow_normal_bias: f32, + pub(crate) spot_light_tan_angle: f32, + pub(crate) soft_shadow_size: f32, + pub(crate) shadow_map_near_z: f32, + pub(crate) decal_index: u32, + pub(crate) pad: f32, +} + +#[derive(Resource)] +pub struct GlobalClusterableObjectMeta { + pub gpu_clusterable_objects: GpuClusterableObjects, + pub entity_to_index: EntityHashMap, +} + +pub enum GpuClusterableObjects { + Uniform(UniformBuffer), + Storage(StorageBuffer), +} + +#[derive(ShaderType)] +pub struct GpuClusterableObjectsUniform { + data: Box<[GpuClusterableObject; MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS]>, +} + +#[derive(ShaderType, Default)] +pub struct GpuClusterableObjectsStorage { + #[size(runtime)] + data: Vec, +} + +#[derive(Component)] +pub struct ExtractedClusterConfig { + /// Special near value for cluster calculations + pub(crate) near: f32, + pub(crate) far: f32, + /// Number of clusters in `X` / `Y` / `Z` in the view frustum + pub(crate) dimensions: UVec3, +} + +enum ExtractedClusterableObjectElement { + ClusterHeader(ClusterableObjectCounts), + ClusterableObjectEntity(Entity), +} + +#[derive(Component)] +pub struct ExtractedClusterableObjects { + data: Vec, +} + +#[derive(ShaderType)] +struct GpuClusterOffsetsAndCountsUniform { + data: Box<[UVec4; ViewClusterBindings::MAX_UNIFORM_ITEMS]>, +} + +#[derive(ShaderType, Default)] +struct GpuClusterableObjectIndexListsStorage { + #[size(runtime)] + data: Vec, +} + +#[derive(ShaderType, Default)] +struct GpuClusterOffsetsAndCountsStorage { + /// The starting offset, followed by the number of point lights, spot + /// lights, reflection probes, and irradiance volumes in each cluster, in + /// that order. The remaining fields are filled with zeroes. + #[size(runtime)] + data: Vec<[UVec4; 2]>, +} + +enum ViewClusterBuffers { + Uniform { + // NOTE: UVec4 is because all arrays in Std140 layout have 16-byte alignment + clusterable_object_index_lists: UniformBuffer, + // NOTE: UVec4 is because all arrays in Std140 layout have 16-byte alignment + cluster_offsets_and_counts: UniformBuffer, + }, + Storage { + clusterable_object_index_lists: StorageBuffer, + cluster_offsets_and_counts: StorageBuffer, + }, +} + +#[derive(Component)] +pub struct ViewClusterBindings { + n_indices: usize, + n_offsets: usize, + buffers: ViewClusterBuffers, +} + +pub fn init_global_clusterable_object_meta( + mut commands: Commands, + render_device: Res, +) { + commands.insert_resource(GlobalClusterableObjectMeta::new( + render_device.get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT), + )); +} + +impl GlobalClusterableObjectMeta { + pub fn new(buffer_binding_type: BufferBindingType) -> Self { + Self { + gpu_clusterable_objects: GpuClusterableObjects::new(buffer_binding_type), + entity_to_index: EntityHashMap::default(), + } + } +} + +impl GpuClusterableObjects { + fn new(buffer_binding_type: BufferBindingType) -> Self { + match buffer_binding_type { + BufferBindingType::Storage { .. } => Self::storage(), + BufferBindingType::Uniform => Self::uniform(), + } + } + + fn uniform() -> Self { + Self::Uniform(UniformBuffer::default()) + } + + fn storage() -> Self { + Self::Storage(StorageBuffer::default()) + } + + pub(crate) fn set(&mut self, mut clusterable_objects: Vec) { + match self { + GpuClusterableObjects::Uniform(buffer) => { + let len = clusterable_objects + .len() + .min(MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS); + let src = &clusterable_objects[..len]; + let dst = &mut buffer.get_mut().data[..len]; + dst.copy_from_slice(src); + } + GpuClusterableObjects::Storage(buffer) => { + buffer.get_mut().data.clear(); + buffer.get_mut().data.append(&mut clusterable_objects); + } + } + } + + pub(crate) fn write_buffer( + &mut self, + render_device: &RenderDevice, + render_queue: &RenderQueue, + ) { + match self { + GpuClusterableObjects::Uniform(buffer) => { + buffer.write_buffer(render_device, render_queue); + } + GpuClusterableObjects::Storage(buffer) => { + buffer.write_buffer(render_device, render_queue); + } + } + } + + pub fn binding(&self) -> Option> { + match self { + GpuClusterableObjects::Uniform(buffer) => buffer.binding(), + GpuClusterableObjects::Storage(buffer) => buffer.binding(), + } + } + + pub fn min_size(buffer_binding_type: BufferBindingType) -> NonZero { + match buffer_binding_type { + BufferBindingType::Storage { .. } => GpuClusterableObjectsStorage::min_size(), + BufferBindingType::Uniform => GpuClusterableObjectsUniform::min_size(), + } + } +} + +impl Default for GpuClusterableObjectsUniform { + fn default() -> Self { + Self { + data: Box::new( + [GpuClusterableObject::default(); MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS], + ), + } + } +} + +/// Extracts clusters from the main world from the render world. +pub fn extract_clusters( + mut commands: Commands, + views: Extract>, + mapper: Extract>, +) { + for (entity, clusters, camera) in &views { + let mut entity_commands = commands + .get_entity(entity) + .expect("Clusters entity wasn't synced."); + if !camera.is_active { + entity_commands.remove::<(ExtractedClusterableObjects, ExtractedClusterConfig)>(); + continue; + } + + let entity_count: usize = clusters + .clusterable_objects + .iter() + .map(|l| l.entities.len()) + .sum(); + let mut data = Vec::with_capacity(clusters.clusterable_objects.len() + entity_count); + for cluster_objects in &clusters.clusterable_objects { + data.push(ExtractedClusterableObjectElement::ClusterHeader( + cluster_objects.counts, + )); + for clusterable_entity in &cluster_objects.entities { + if let Ok(entity) = mapper.get(*clusterable_entity) { + data.push(ExtractedClusterableObjectElement::ClusterableObjectEntity( + entity, + )); + } + } + } + + entity_commands.insert(( + ExtractedClusterableObjects { data }, + ExtractedClusterConfig { + near: clusters.near, + far: clusters.far, + dimensions: clusters.dimensions, + }, + )); + } +} + +pub fn prepare_clusters( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mesh_pipeline: Res, + global_clusterable_object_meta: Res, + views: Query<(Entity, &ExtractedClusterableObjects)>, +) { + let render_device = render_device.into_inner(); + let supports_storage_buffers = matches!( + mesh_pipeline.clustered_forward_buffer_binding_type, + BufferBindingType::Storage { .. } + ); + for (entity, extracted_clusters) in &views { + let mut view_clusters_bindings = + ViewClusterBindings::new(mesh_pipeline.clustered_forward_buffer_binding_type); + view_clusters_bindings.clear(); + + for record in &extracted_clusters.data { + match record { + ExtractedClusterableObjectElement::ClusterHeader(counts) => { + let offset = view_clusters_bindings.n_indices(); + view_clusters_bindings.push_offset_and_counts(offset, counts); + } + ExtractedClusterableObjectElement::ClusterableObjectEntity(entity) => { + if let Some(clusterable_object_index) = + global_clusterable_object_meta.entity_to_index.get(entity) + { + if view_clusters_bindings.n_indices() >= ViewClusterBindings::MAX_INDICES + && !supports_storage_buffers + { + warn!( + "Clusterable object index lists are full! The clusterable \ + objects in the view are present in too many clusters." + ); + break; + } + view_clusters_bindings.push_index(*clusterable_object_index); + } + } + } + } + + view_clusters_bindings.write_buffers(render_device, &render_queue); + + commands.entity(entity).insert(view_clusters_bindings); + } +} + +impl ViewClusterBindings { + pub const MAX_OFFSETS: usize = 16384 / 4; + const MAX_UNIFORM_ITEMS: usize = Self::MAX_OFFSETS / 4; + pub const MAX_INDICES: usize = 16384; + + pub fn new(buffer_binding_type: BufferBindingType) -> Self { + Self { + n_indices: 0, + n_offsets: 0, + buffers: ViewClusterBuffers::new(buffer_binding_type), + } + } + + pub fn clear(&mut self) { + match &mut self.buffers { + ViewClusterBuffers::Uniform { + clusterable_object_index_lists, + cluster_offsets_and_counts, + } => { + *clusterable_object_index_lists.get_mut().data = + [UVec4::ZERO; Self::MAX_UNIFORM_ITEMS]; + *cluster_offsets_and_counts.get_mut().data = [UVec4::ZERO; Self::MAX_UNIFORM_ITEMS]; + } + ViewClusterBuffers::Storage { + clusterable_object_index_lists, + cluster_offsets_and_counts, + .. + } => { + clusterable_object_index_lists.get_mut().data.clear(); + cluster_offsets_and_counts.get_mut().data.clear(); + } + } + } + + fn push_offset_and_counts(&mut self, offset: usize, counts: &ClusterableObjectCounts) { + match &mut self.buffers { + ViewClusterBuffers::Uniform { + cluster_offsets_and_counts, + .. + } => { + let array_index = self.n_offsets >> 2; // >> 2 is equivalent to / 4 + if array_index >= Self::MAX_UNIFORM_ITEMS { + warn!("cluster offset and count out of bounds!"); + return; + } + let component = self.n_offsets & ((1 << 2) - 1); + let packed = + pack_offset_and_counts(offset, counts.point_lights, counts.spot_lights); + + cluster_offsets_and_counts.get_mut().data[array_index][component] = packed; + } + ViewClusterBuffers::Storage { + cluster_offsets_and_counts, + .. + } => { + cluster_offsets_and_counts.get_mut().data.push([ + uvec4( + offset as u32, + counts.point_lights, + counts.spot_lights, + counts.reflection_probes, + ), + uvec4(counts.irradiance_volumes, counts.decals, 0, 0), + ]); + } + } + + self.n_offsets += 1; + } + + pub fn n_indices(&self) -> usize { + self.n_indices + } + + pub fn push_index(&mut self, index: usize) { + match &mut self.buffers { + ViewClusterBuffers::Uniform { + clusterable_object_index_lists, + .. + } => { + let array_index = self.n_indices >> 4; // >> 4 is equivalent to / 16 + let component = (self.n_indices >> 2) & ((1 << 2) - 1); + let sub_index = self.n_indices & ((1 << 2) - 1); + let index = index as u32; + + clusterable_object_index_lists.get_mut().data[array_index][component] |= + index << (8 * sub_index); + } + ViewClusterBuffers::Storage { + clusterable_object_index_lists, + .. + } => { + clusterable_object_index_lists + .get_mut() + .data + .push(index as u32); + } + } + + self.n_indices += 1; + } + + pub fn write_buffers(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + match &mut self.buffers { + ViewClusterBuffers::Uniform { + clusterable_object_index_lists, + cluster_offsets_and_counts, + } => { + clusterable_object_index_lists.write_buffer(render_device, render_queue); + cluster_offsets_and_counts.write_buffer(render_device, render_queue); + } + ViewClusterBuffers::Storage { + clusterable_object_index_lists, + cluster_offsets_and_counts, + } => { + clusterable_object_index_lists.write_buffer(render_device, render_queue); + cluster_offsets_and_counts.write_buffer(render_device, render_queue); + } + } + } + + pub fn clusterable_object_index_lists_binding(&self) -> Option> { + match &self.buffers { + ViewClusterBuffers::Uniform { + clusterable_object_index_lists, + .. + } => clusterable_object_index_lists.binding(), + ViewClusterBuffers::Storage { + clusterable_object_index_lists, + .. + } => clusterable_object_index_lists.binding(), + } + } + + pub fn offsets_and_counts_binding(&self) -> Option> { + match &self.buffers { + ViewClusterBuffers::Uniform { + cluster_offsets_and_counts, + .. + } => cluster_offsets_and_counts.binding(), + ViewClusterBuffers::Storage { + cluster_offsets_and_counts, + .. + } => cluster_offsets_and_counts.binding(), + } + } + + pub fn min_size_clusterable_object_index_lists( + buffer_binding_type: BufferBindingType, + ) -> NonZero { + match buffer_binding_type { + BufferBindingType::Storage { .. } => GpuClusterableObjectIndexListsStorage::min_size(), + BufferBindingType::Uniform => GpuClusterableObjectIndexListsUniform::min_size(), + } + } + + pub fn min_size_cluster_offsets_and_counts( + buffer_binding_type: BufferBindingType, + ) -> NonZero { + match buffer_binding_type { + BufferBindingType::Storage { .. } => GpuClusterOffsetsAndCountsStorage::min_size(), + BufferBindingType::Uniform => GpuClusterOffsetsAndCountsUniform::min_size(), + } + } +} + +impl ViewClusterBuffers { + fn new(buffer_binding_type: BufferBindingType) -> Self { + match buffer_binding_type { + BufferBindingType::Storage { .. } => Self::storage(), + BufferBindingType::Uniform => Self::uniform(), + } + } + + fn uniform() -> Self { + ViewClusterBuffers::Uniform { + clusterable_object_index_lists: UniformBuffer::default(), + cluster_offsets_and_counts: UniformBuffer::default(), + } + } + + fn storage() -> Self { + ViewClusterBuffers::Storage { + clusterable_object_index_lists: StorageBuffer::default(), + cluster_offsets_and_counts: StorageBuffer::default(), + } + } +} + +// Compresses the offset and counts of point and spot lights so that they fit in +// a UBO. +// +// This function is only used if storage buffers are unavailable on this +// platform: typically, on WebGL 2. +// +// NOTE: With uniform buffer max binding size as 16384 bytes +// that means we can fit 204 clusterable objects in one uniform +// buffer, which means the count can be at most 204 so it +// needs 9 bits. +// The array of indices can also use u8 and that means the +// offset in to the array of indices needs to be able to address +// 16384 values. log2(16384) = 14 bits. +// We use 32 bits to store the offset and counts so +// we pack the offset into the upper 14 bits of a u32, +// the point light count into bits 9-17, and the spot light count into bits 0-8. +// [ 31 .. 18 | 17 .. 9 | 8 .. 0 ] +// [ offset | point light count | spot light count ] +// +// NOTE: This assumes CPU and GPU endianness are the same which is true +// for all common and tested x86/ARM CPUs and AMD/NVIDIA/Intel/Apple/etc GPUs +// +// NOTE: On platforms that use this function, we don't cluster light probes, so +// the number of light probes is irrelevant. +fn pack_offset_and_counts(offset: usize, point_count: u32, spot_count: u32) -> u32 { + ((offset as u32 & CLUSTER_OFFSET_MASK) << (CLUSTER_COUNT_SIZE * 2)) + | ((point_count & CLUSTER_COUNT_MASK) << CLUSTER_COUNT_SIZE) + | (spot_count & CLUSTER_COUNT_MASK) +} + +#[derive(ShaderType)] +struct GpuClusterableObjectIndexListsUniform { + data: Box<[UVec4; ViewClusterBindings::MAX_UNIFORM_ITEMS]>, +} + +// NOTE: Assert at compile time that GpuClusterableObjectIndexListsUniform +// fits within the maximum uniform buffer binding size +const _: () = assert!(GpuClusterableObjectIndexListsUniform::SHADER_SIZE.get() <= 16384); + +impl Default for GpuClusterableObjectIndexListsUniform { + fn default() -> Self { + Self { + data: Box::new([UVec4::ZERO; ViewClusterBindings::MAX_UNIFORM_ITEMS]), + } + } +} + +impl Default for GpuClusterOffsetsAndCountsUniform { + fn default() -> Self { + Self { + data: Box::new([UVec4::ZERO; ViewClusterBindings::MAX_UNIFORM_ITEMS]), + } + } +} diff --git a/crates/libmarathon/src/render/pbr/components.rs b/crates/libmarathon/src/render/pbr/components.rs new file mode 100644 index 0000000..3fbfbe5 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/components.rs @@ -0,0 +1,46 @@ +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Component; +use bevy_ecs::entity::{Entity, EntityHashMap}; +use bevy_ecs::reflect::ReflectComponent; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use crate::render::sync_world::MainEntity; + +#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)] +#[reflect(Component, Debug, Default, Clone)] +pub struct RenderVisibleMeshEntities { + #[reflect(ignore, clone)] + pub entities: Vec<(Entity, MainEntity)>, +} + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Debug, Default, Clone)] +pub struct RenderCubemapVisibleEntities { + #[reflect(ignore, clone)] + pub(crate) data: [RenderVisibleMeshEntities; 6], +} + +impl RenderCubemapVisibleEntities { + pub fn get(&self, i: usize) -> &RenderVisibleMeshEntities { + &self.data[i] + } + + pub fn get_mut(&mut self, i: usize) -> &mut RenderVisibleMeshEntities { + &mut self.data[i] + } + + pub fn iter(&self) -> impl DoubleEndedIterator { + self.data.iter() + } + + pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { + self.data.iter_mut() + } +} + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Default, Clone)] +pub struct RenderCascadesVisibleEntities { + /// Map of view entity to the visible entities for each cascade frustum. + #[reflect(ignore, clone)] + pub entities: EntityHashMap>, +} diff --git a/crates/libmarathon/src/render/pbr/decal/clustered.rs b/crates/libmarathon/src/render/pbr/decal/clustered.rs new file mode 100644 index 0000000..a144cc4 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/decal/clustered.rs @@ -0,0 +1,441 @@ +//! Clustered decals, bounding regions that project textures onto surfaces. +//! +//! A *clustered decal* is a bounding box that projects a texture onto any +//! surface within its bounds along the positive Z axis. In Bevy, clustered +//! decals use the *clustered forward* rendering technique. +//! +//! Clustered decals are the highest-quality types of decals that Bevy supports, +//! but they require bindless textures. This means that they presently can't be +//! used on WebGL 2 or WebGPU. Bevy's clustered decals can be used +//! with forward or deferred rendering and don't require a prepass. +//! +//! On their own, clustered decals only project the base color of a texture. You +//! can, however, use the built-in *tag* field to customize the appearance of a +//! clustered decal arbitrarily. See the documentation in `clustered.wgsl` for +//! more information and the `clustered_decals` example for an example of use. + +use core::{num::NonZero, ops::Deref}; + +use bevy_app::{App, Plugin}; +use bevy_asset::AssetId; +use bevy_camera::visibility::ViewVisibility; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + entity::{Entity, EntityHashMap}, + query::With, + resource::Resource, + schedule::IntoScheduleConfigs as _, + system::{Commands, Local, Query, Res, ResMut}, +}; +use bevy_image::Image; +use bevy_light::{ClusteredDecal, DirectionalLightTexture, PointLightTexture, SpotLightTexture}; +use bevy_math::Mat4; +use bevy_platform::collections::HashMap; +use crate::render::{ + render_asset::RenderAssets, + render_resource::{ + binding_types, BindGroupLayoutEntryBuilder, Buffer, BufferUsages, RawBufferVec, Sampler, + SamplerBindingType, ShaderType, TextureSampleType, TextureView, + }, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, + sync_component::SyncComponentPlugin, + sync_world::RenderEntity, + texture::{FallbackImage, GpuImage}, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use bevy_shader::load_shader_library; +use bevy_transform::components::GlobalTransform; +use bytemuck::{Pod, Zeroable}; + +use crate::render::pbr::{binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta}; + +/// The maximum number of decals that can be present in a view. +/// +/// This number is currently relatively low in order to work around the lack of +/// first-class binding arrays in `wgpu`. When that feature is implemented, this +/// limit can be increased. +pub(crate) const MAX_VIEW_DECALS: usize = 8; + +/// A plugin that adds support for clustered decals. +/// +/// In environments where bindless textures aren't available, clustered decals +/// can still be added to a scene, but they won't project any decals. +pub struct ClusteredDecalPlugin; + +/// Stores information about all the clustered decals in the scene. +#[derive(Resource, Default)] +pub struct RenderClusteredDecals { + /// Maps an index in the shader binding array to the associated decal image. + /// + /// [`Self::texture_to_binding_index`] holds the inverse mapping. + binding_index_to_textures: Vec>, + /// Maps a decal image to the shader binding array. + /// + /// [`Self::binding_index_to_textures`] holds the inverse mapping. + texture_to_binding_index: HashMap, u32>, + /// The information concerning each decal that we provide to the shader. + decals: Vec, + /// Maps the [`bevy_render::sync_world::RenderEntity`] of each decal to the + /// index of that decal in the [`Self::decals`] list. + entity_to_decal_index: EntityHashMap, +} + +impl RenderClusteredDecals { + /// Clears out this [`RenderClusteredDecals`] in preparation for a new + /// frame. + fn clear(&mut self) { + self.binding_index_to_textures.clear(); + self.texture_to_binding_index.clear(); + self.decals.clear(); + self.entity_to_decal_index.clear(); + } + + pub fn insert_decal( + &mut self, + entity: Entity, + image: &AssetId, + local_from_world: Mat4, + tag: u32, + ) { + let image_index = self.get_or_insert_image(image); + let decal_index = self.decals.len(); + self.decals.push(RenderClusteredDecal { + local_from_world, + image_index, + tag, + pad_a: 0, + pad_b: 0, + }); + self.entity_to_decal_index.insert(entity, decal_index); + } + + pub fn get(&self, entity: Entity) -> Option { + self.entity_to_decal_index.get(&entity).copied() + } +} + +/// The per-view bind group entries pertaining to decals. +pub(crate) struct RenderViewClusteredDecalBindGroupEntries<'a> { + /// The list of decals, corresponding to `mesh_view_bindings::decals` in the + /// shader. + pub(crate) decals: &'a Buffer, + /// The list of textures, corresponding to + /// `mesh_view_bindings::decal_textures` in the shader. + pub(crate) texture_views: Vec<&'a ::Target>, + /// The sampler that the shader uses to sample decals, corresponding to + /// `mesh_view_bindings::decal_sampler` in the shader. + pub(crate) sampler: &'a Sampler, +} + +/// A render-world resource that holds the buffer of [`ClusteredDecal`]s ready +/// to upload to the GPU. +#[derive(Resource, Deref, DerefMut)] +pub struct DecalsBuffer(RawBufferVec); + +impl Default for DecalsBuffer { + fn default() -> Self { + DecalsBuffer(RawBufferVec::new(BufferUsages::STORAGE)) + } +} + +impl Plugin for ClusteredDecalPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "clustered.wgsl"); + + app.add_plugins(SyncComponentPlugin::::default()); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .init_resource::() + .add_systems(ExtractSchedule, (extract_decals, extract_clustered_decal)) + .add_systems( + Render, + prepare_decals + .in_set(RenderSystems::ManageViews) + .after(prepare_lights), + ) + .add_systems( + Render, + upload_decals.in_set(RenderSystems::PrepareResources), + ); + } +} + +// This is needed because of the orphan rule not allowing implementing +// foreign trait ExtractComponent on foreign type ClusteredDecal +fn extract_clustered_decal( + mut commands: Commands, + mut previous_len: Local, + query: Extract>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, query_item) in &query { + values.push((entity, query_item.clone())); + } + *previous_len = values.len(); + commands.try_insert_batch(values); +} + +/// The GPU data structure that stores information about each decal. +#[derive(Clone, Copy, Default, ShaderType, Pod, Zeroable)] +#[repr(C)] +pub struct RenderClusteredDecal { + /// The inverse of the model matrix. + /// + /// The shader uses this in order to back-transform world positions into + /// model space. + local_from_world: Mat4, + /// The index of the decal texture in the binding array. + image_index: u32, + /// A custom tag available for application-defined purposes. + tag: u32, + /// Padding. + pad_a: u32, + /// Padding. + pad_b: u32, +} + +/// Extracts decals from the main world into the render world. +pub fn extract_decals( + decals: Extract< + Query<( + RenderEntity, + &ClusteredDecal, + &GlobalTransform, + &ViewVisibility, + )>, + >, + spot_light_textures: Extract< + Query<( + RenderEntity, + &SpotLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + point_light_textures: Extract< + Query<( + RenderEntity, + &PointLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + directional_light_textures: Extract< + Query<( + RenderEntity, + &DirectionalLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + mut render_decals: ResMut, +) { + // Clear out the `RenderDecals` in preparation for a new frame. + render_decals.clear(); + + // Loop over each decal. + for (decal_entity, clustered_decal, global_transform, view_visibility) in &decals { + // If the decal is invisible, skip it. + if !view_visibility.get() { + continue; + } + + render_decals.insert_decal( + decal_entity, + &clustered_decal.image.id(), + global_transform.affine().inverse().into(), + clustered_decal.tag, + ); + } + + for (decal_entity, texture, global_transform, view_visibility) in &spot_light_textures { + // If the decal is invisible, skip it. + if !view_visibility.get() { + continue; + } + + render_decals.insert_decal( + decal_entity, + &texture.image.id(), + global_transform.affine().inverse().into(), + 0, + ); + } + + for (decal_entity, texture, global_transform, view_visibility) in &point_light_textures { + // If the decal is invisible, skip it. + if !view_visibility.get() { + continue; + } + + render_decals.insert_decal( + decal_entity, + &texture.image.id(), + global_transform.affine().inverse().into(), + texture.cubemap_layout as u32, + ); + } + + for (decal_entity, texture, global_transform, view_visibility) in &directional_light_textures { + // If the decal is invisible, skip it. + if !view_visibility.get() { + continue; + } + + render_decals.insert_decal( + decal_entity, + &texture.image.id(), + global_transform.affine().inverse().into(), + if texture.tiled { 1 } else { 0 }, + ); + } +} + +/// Adds all decals in the scene to the [`GlobalClusterableObjectMeta`] table. +fn prepare_decals( + decals: Query>, + mut global_clusterable_object_meta: ResMut, + render_decals: Res, +) { + for decal_entity in &decals { + if let Some(index) = render_decals.entity_to_decal_index.get(&decal_entity) { + global_clusterable_object_meta + .entity_to_index + .insert(decal_entity, *index); + } + } +} + +/// Returns the layout for the clustered-decal-related bind group entries for a +/// single view. +pub(crate) fn get_bind_group_layout_entries( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, +) -> Option<[BindGroupLayoutEntryBuilder; 3]> { + // If binding arrays aren't supported on the current platform, we have no + // bind group layout entries. + if !clustered_decals_are_usable(render_device, render_adapter) { + return None; + } + + Some([ + // `decals` + binding_types::storage_buffer_read_only::(false), + // `decal_textures` + binding_types::texture_2d(TextureSampleType::Float { filterable: true }) + .count(NonZero::::new(MAX_VIEW_DECALS as u32).unwrap()), + // `decal_sampler` + binding_types::sampler(SamplerBindingType::Filtering), + ]) +} + +impl<'a> RenderViewClusteredDecalBindGroupEntries<'a> { + /// Creates and returns the bind group entries for clustered decals for a + /// single view. + pub(crate) fn get( + render_decals: &RenderClusteredDecals, + decals_buffer: &'a DecalsBuffer, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, + render_device: &RenderDevice, + render_adapter: &RenderAdapter, + ) -> Option> { + // Skip the entries if decals are unsupported on the current platform. + if !clustered_decals_are_usable(render_device, render_adapter) { + return None; + } + + // We use the first sampler among all the images. This assumes that all + // images use the same sampler, which is a documented restriction. If + // there's no sampler, we just use the one from the fallback image. + let sampler = match render_decals + .binding_index_to_textures + .iter() + .filter_map(|image_id| images.get(*image_id)) + .next() + { + Some(gpu_image) => &gpu_image.sampler, + None => &fallback_image.d2.sampler, + }; + + // Gather up the decal textures. + let mut texture_views = vec![]; + for image_id in &render_decals.binding_index_to_textures { + match images.get(*image_id) { + None => texture_views.push(&*fallback_image.d2.texture_view), + Some(gpu_image) => texture_views.push(&*gpu_image.texture_view), + } + } + + // Pad out the binding array to its maximum length, which is + // required on some platforms. + while texture_views.len() < MAX_VIEW_DECALS { + texture_views.push(&*fallback_image.d2.texture_view); + } + + Some(RenderViewClusteredDecalBindGroupEntries { + decals: decals_buffer.buffer()?, + texture_views, + sampler, + }) + } +} + +impl RenderClusteredDecals { + /// Returns the index of the given image in the decal texture binding array, + /// adding it to the list if necessary. + fn get_or_insert_image(&mut self, image_id: &AssetId) -> u32 { + *self + .texture_to_binding_index + .entry(*image_id) + .or_insert_with(|| { + let index = self.binding_index_to_textures.len() as u32; + self.binding_index_to_textures.push(*image_id); + index + }) + } +} + +/// Uploads the list of decals from [`RenderClusteredDecals::decals`] to the +/// GPU. +fn upload_decals( + render_decals: Res, + mut decals_buffer: ResMut, + render_device: Res, + render_queue: Res, +) { + decals_buffer.clear(); + + for &decal in &render_decals.decals { + decals_buffer.push(decal); + } + + // Make sure the buffer is non-empty. + // Otherwise there won't be a buffer to bind. + if decals_buffer.is_empty() { + decals_buffer.push(RenderClusteredDecal::default()); + } + + decals_buffer.write_buffer(&render_device, &render_queue); +} + +/// Returns true if clustered decals are usable on the current platform or false +/// otherwise. +/// +/// Clustered decals are currently disabled on macOS and iOS due to insufficient +/// texture bindings and limited bindless support in `wgpu`. +pub fn clustered_decals_are_usable( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, +) -> bool { + // Disable binding arrays on Metal. There aren't enough texture bindings available. + // See issue #17553. + // Re-enable this when `wgpu` has first-class bindless. + binding_arrays_are_usable(render_device, render_adapter) + && cfg!(feature = "pbr_clustered_decals") +} diff --git a/crates/libmarathon/src/render/pbr/decal/clustered.wgsl b/crates/libmarathon/src/render/pbr/decal/clustered.wgsl new file mode 100644 index 0000000..874722a --- /dev/null +++ b/crates/libmarathon/src/render/pbr/decal/clustered.wgsl @@ -0,0 +1,183 @@ +// Support code for clustered decals. +// +// This module provides an iterator API, which you may wish to use in your own +// shaders if you want clustered decals to provide textures other than the base +// color. The iterator API allows you to iterate over all decals affecting the +// current fragment. Use `clustered_decal_iterator_new()` and +// `clustered_decal_iterator_next()` as follows: +// +// let view_z = get_view_z(vec4(world_position, 1.0)); +// let is_orthographic = view_is_orthographic(); +// +// let cluster_index = +// clustered_forward::fragment_cluster_index(frag_coord, view_z, is_orthographic); +// var clusterable_object_index_ranges = +// clustered_forward::unpack_clusterable_object_index_ranges(cluster_index); +// +// var iterator = clustered_decal_iterator_new(world_position, &clusterable_object_index_ranges); +// while (clustered_decal_iterator_next(&iterator)) { +// ... sample from the texture at iterator.texture_index at iterator.uv ... +// } +// +// In this way, in conjunction with a custom material, you can provide your own +// texture arrays that mirror `mesh_view_bindings::clustered_decal_textures` in +// order to support decals with normal maps, etc. +// +// Note that the order in which decals are returned is currently unpredictable, +// though generally stable from frame to frame. + +#define_import_path bevy_pbr::decal::clustered + +#import bevy_pbr::clustered_forward +#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges +#import bevy_pbr::mesh_view_bindings +#import bevy_render::maths + +// An object that allows stepping through all clustered decals that affect a +// single fragment. +struct ClusteredDecalIterator { + // Public fields follow: + // The index of the decal texture in the binding array. + texture_index: i32, + // The UV coordinates at which to sample that decal texture. + uv: vec2, + // A custom tag you can use for your own purposes. + tag: u32, + + // Private fields follow: + // The current offset of the index in the `ClusterableObjectIndexRanges` list. + decal_index_offset: i32, + // The end offset of the index in the `ClusterableObjectIndexRanges` list. + end_offset: i32, + // The world-space position of the fragment. + world_position: vec3, +} + +#ifdef CLUSTERED_DECALS_ARE_USABLE + +// Creates a new iterator over the decals at the current fragment. +// +// You can retrieve `clusterable_object_index_ranges` as follows: +// +// let view_z = get_view_z(world_position); +// let is_orthographic = view_is_orthographic(); +// +// let cluster_index = +// clustered_forward::fragment_cluster_index(frag_coord, view_z, is_orthographic); +// var clusterable_object_index_ranges = +// clustered_forward::unpack_clusterable_object_index_ranges(cluster_index); +fn clustered_decal_iterator_new( + world_position: vec3, + clusterable_object_index_ranges: ptr +) -> ClusteredDecalIterator { + return ClusteredDecalIterator( + -1, + vec2(0.0), + 0u, + // We subtract 1 because the first thing `decal_iterator_next` does is + // add 1. + i32((*clusterable_object_index_ranges).first_decal_offset) - 1, + i32((*clusterable_object_index_ranges).last_clusterable_object_index_offset), + world_position, + ); +} + +// Populates the `iterator.texture_index` and `iterator.uv` fields for the next +// decal overlapping the current world position. +// +// Returns true if another decal was found or false if no more decals were found +// for this position. +fn clustered_decal_iterator_next(iterator: ptr) -> bool { + if ((*iterator).decal_index_offset == (*iterator).end_offset) { + return false; + } + + (*iterator).decal_index_offset += 1; + + while ((*iterator).decal_index_offset < (*iterator).end_offset) { + let decal_index = i32(clustered_forward::get_clusterable_object_id( + u32((*iterator).decal_index_offset) + )); + let decal_space_vector = + (mesh_view_bindings::clustered_decals.decals[decal_index].local_from_world * + vec4((*iterator).world_position, 1.0)).xyz; + + if (all(decal_space_vector >= vec3(-0.5)) && all(decal_space_vector <= vec3(0.5))) { + (*iterator).texture_index = + i32(mesh_view_bindings::clustered_decals.decals[decal_index].image_index); + (*iterator).uv = decal_space_vector.xy * vec2(1.0, -1.0) + vec2(0.5); + (*iterator).tag = + mesh_view_bindings::clustered_decals.decals[decal_index].tag; + return true; + } + + (*iterator).decal_index_offset += 1; + } + + return false; +} + +#endif // CLUSTERED_DECALS_ARE_USABLE + +// Returns the view-space Z coordinate for the given world position. +fn get_view_z(world_position: vec3) -> f32 { + return dot(vec4( + mesh_view_bindings::view.view_from_world[0].z, + mesh_view_bindings::view.view_from_world[1].z, + mesh_view_bindings::view.view_from_world[2].z, + mesh_view_bindings::view.view_from_world[3].z + ), vec4(world_position, 1.0)); +} + +// Returns true if the current view describes an orthographic projection or +// false otherwise. +fn view_is_orthographic() -> bool { + return mesh_view_bindings::view.clip_from_view[3].w == 1.0; +} + +// Modifies the base color at the given position to account for decals. +// +// Returns the new base color with decals taken into account. If no decals +// overlap the current world position, returns the supplied base color +// unmodified. +fn apply_decal_base_color( + world_position: vec3, + frag_coord: vec2, + initial_base_color: vec4, +) -> vec4 { + var base_color = initial_base_color; + +#ifdef CLUSTERED_DECALS_ARE_USABLE + // Fetch the clusterable object index ranges for this world position. + + let view_z = get_view_z(world_position); + let is_orthographic = view_is_orthographic(); + + let cluster_index = + clustered_forward::fragment_cluster_index(frag_coord, view_z, is_orthographic); + var clusterable_object_index_ranges = + clustered_forward::unpack_clusterable_object_index_ranges(cluster_index); + + // Iterate over decals. + + var iterator = clustered_decal_iterator_new(world_position, &clusterable_object_index_ranges); + while (clustered_decal_iterator_next(&iterator)) { + // Sample the current decal. + let decal_base_color = textureSampleLevel( + mesh_view_bindings::clustered_decal_textures[iterator.texture_index], + mesh_view_bindings::clustered_decal_sampler, + iterator.uv, + 0.0 + ); + + // Blend with the accumulated fragment. + base_color = vec4( + mix(base_color.rgb, decal_base_color.rgb, decal_base_color.a), + base_color.a + decal_base_color.a + ); + } +#endif // CLUSTERED_DECALS_ARE_USABLE + + return base_color; +} + diff --git a/crates/libmarathon/src/render/pbr/decal/forward.rs b/crates/libmarathon/src/render/pbr/decal/forward.rs new file mode 100644 index 0000000..ca285d1 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/decal/forward.rs @@ -0,0 +1,165 @@ +use crate::render::pbr::{ + ExtendedMaterial, Material, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline, + MaterialPlugin, StandardMaterial, +}; +use bevy_app::{App, Plugin}; +use bevy_asset::{Asset, Assets, Handle}; +use bevy_ecs::{ + component::Component, lifecycle::HookContext, resource::Resource, world::DeferredWorld, +}; +use bevy_math::{prelude::Rectangle, Quat, Vec2, Vec3}; +use bevy_mesh::{Mesh, Mesh3d, MeshBuilder, MeshVertexBufferLayoutRef, Meshable}; +use bevy_reflect::{Reflect, TypePath}; +use crate::render::{ + alpha::AlphaMode, + render_asset::RenderAssets, + render_resource::{ + AsBindGroup, AsBindGroupShaderType, CompareFunction, RenderPipelineDescriptor, ShaderType, + SpecializedMeshPipelineError, + }, + texture::GpuImage, + RenderDebugFlags, +}; +use bevy_shader::load_shader_library; + +/// Plugin to render [`ForwardDecal`]s. +pub struct ForwardDecalPlugin; + +impl Plugin for ForwardDecalPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "forward_decal.wgsl"); + + let mesh = app.world_mut().resource_mut::>().add( + Rectangle::from_size(Vec2::ONE) + .mesh() + .build() + .rotated_by(Quat::from_rotation_arc(Vec3::Z, Vec3::Y)) + .with_generated_tangents() + .unwrap(), + ); + + app.insert_resource(ForwardDecalMesh(mesh)); + + app.add_plugins(MaterialPlugin::> { + prepass_enabled: false, + shadows_enabled: false, + debug_flags: RenderDebugFlags::default(), + ..Default::default() + }); + } +} + +/// A decal that renders via a 1x1 transparent quad mesh, smoothly alpha-blending with the underlying +/// geometry towards the edges. +/// +/// Because forward decals are meshes, you can use arbitrary materials to control their appearance. +/// +/// # Usage Notes +/// +/// * Spawn this component on an entity with a [`crate::MeshMaterial3d`] component holding a [`ForwardDecalMaterial`]. +/// * Any camera rendering a forward decal must have the [`bevy_core_pipeline::prepass::DepthPrepass`] component. +/// * Looking at forward decals at a steep angle can cause distortion. This can be mitigated by padding your decal's +/// texture with extra transparent pixels on the edges. +/// * On Wasm, requires using WebGPU and disabling `Msaa` on your camera. +#[derive(Component, Reflect)] +#[require(Mesh3d)] +#[component(on_add=forward_decal_set_mesh)] +pub struct ForwardDecal; + +/// Type alias for an extended material with a [`ForwardDecalMaterialExt`] extension. +/// +/// Make sure to register the [`MaterialPlugin`] for this material in your app setup. +/// +/// [`StandardMaterial`] comes with out of the box support for forward decals. +#[expect(type_alias_bounds, reason = "Type alias generics not yet stable")] +pub type ForwardDecalMaterial = ExtendedMaterial; + +/// Material extension for a [`ForwardDecal`]. +/// +/// In addition to wrapping your material type with this extension, your shader must use +/// the `bevy_pbr::decal::forward::get_forward_decal_info` function. +/// +/// The `FORWARD_DECAL` shader define will be made available to your shader so that you can gate +/// the forward decal code behind an ifdef. +#[derive(Asset, AsBindGroup, TypePath, Clone, Debug)] +#[uniform(200, ForwardDecalMaterialExtUniform)] +pub struct ForwardDecalMaterialExt { + /// Controls the distance threshold for decal blending with surfaces. + /// + /// This parameter determines how far away a surface can be before the decal no longer blends + /// with it and instead renders with full opacity. + /// + /// Lower values cause the decal to only blend with close surfaces, while higher values allow + /// blending with more distant surfaces. + /// + /// Units are in meters. + pub depth_fade_factor: f32, +} + +#[derive(Clone, Default, ShaderType)] +pub struct ForwardDecalMaterialExtUniform { + pub inv_depth_fade_factor: f32, +} + +impl AsBindGroupShaderType for ForwardDecalMaterialExt { + fn as_bind_group_shader_type( + &self, + _images: &RenderAssets, + ) -> ForwardDecalMaterialExtUniform { + ForwardDecalMaterialExtUniform { + inv_depth_fade_factor: 1.0 / self.depth_fade_factor.max(0.001), + } + } +} + +impl MaterialExtension for ForwardDecalMaterialExt { + fn alpha_mode() -> Option { + Some(AlphaMode::Blend) + } + + fn specialize( + _pipeline: &MaterialExtensionPipeline, + descriptor: &mut RenderPipelineDescriptor, + _layout: &MeshVertexBufferLayoutRef, + _key: MaterialExtensionKey, + ) -> Result<(), SpecializedMeshPipelineError> { + descriptor.depth_stencil.as_mut().unwrap().depth_compare = CompareFunction::Always; + + descriptor.vertex.shader_defs.push("FORWARD_DECAL".into()); + + if let Some(fragment) = &mut descriptor.fragment { + fragment.shader_defs.push("FORWARD_DECAL".into()); + } + + if let Some(label) = &mut descriptor.label { + *label = format!("forward_decal_{label}").into(); + } + + Ok(()) + } +} + +impl Default for ForwardDecalMaterialExt { + fn default() -> Self { + Self { + depth_fade_factor: 8.0, + } + } +} + +#[derive(Resource)] +struct ForwardDecalMesh(Handle); + +// Note: We need to use a hook here instead of required components since we cannot access resources +// with required components, and we can't otherwise get a handle to the asset from a required +// component constructor, since the constructor must be a function pointer, and we intentionally do +// not want to use `uuid_handle!`. +fn forward_decal_set_mesh(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { + let decal_mesh = world.resource::().0.clone(); + let mut entity = world.entity_mut(entity); + let mut entity_mesh = entity.get_mut::().unwrap(); + // Only replace the mesh handle if the mesh handle is defaulted. + if **entity_mesh == Handle::default() { + entity_mesh.0 = decal_mesh; + } +} diff --git a/crates/libmarathon/src/render/pbr/decal/forward_decal.wgsl b/crates/libmarathon/src/render/pbr/decal/forward_decal.wgsl new file mode 100644 index 0000000..ffbb5f9 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/decal/forward_decal.wgsl @@ -0,0 +1,52 @@ +#define_import_path bevy_pbr::decal::forward + +#import bevy_pbr::{ + forward_io::VertexOutput, + mesh_functions::get_world_from_local, + mesh_view_bindings::view, + pbr_functions::calculate_tbn_mikktspace, + prepass_utils::prepass_depth, + view_transformations::depth_ndc_to_view_z, +} +#import bevy_render::maths::project_onto + +@group(#{MATERIAL_BIND_GROUP}) @binding(200) +var inv_depth_fade_factor: f32; + +struct ForwardDecalInformation { + world_position: vec4, + uv: vec2, + alpha: f32, +} + +fn get_forward_decal_info(in: VertexOutput) -> ForwardDecalInformation { + let world_from_local = get_world_from_local(in.instance_index); + let scale = (world_from_local * vec4(1.0, 1.0, 1.0, 0.0)).xyz; + let scaled_tangent = vec4(in.world_tangent.xyz / scale, in.world_tangent.w); + + let V = normalize(view.world_position - in.world_position.xyz); + + // Transform V from fragment to camera in world space to tangent space. + let TBN = calculate_tbn_mikktspace(in.world_normal, scaled_tangent); + let T = TBN[0]; + let B = TBN[1]; + let N = TBN[2]; + let Vt = vec3(dot(V, T), dot(V, B), dot(V, N)); + + let frag_depth = depth_ndc_to_view_z(in.position.z); + let depth_pass_depth = depth_ndc_to_view_z(prepass_depth(in.position, 0u)); + let diff_depth = frag_depth - depth_pass_depth; + let diff_depth_abs = abs(diff_depth); + + // Apply UV parallax + let contact_on_decal = project_onto(V * diff_depth, in.world_normal); + let normal_depth = length(contact_on_decal); + let view_steepness = abs(Vt.z); + let delta_uv = normal_depth * Vt.xy * vec2(1.0, -1.0) / view_steepness; + let uv = in.uv + delta_uv; + + let world_position = vec4(in.world_position.xyz + V * diff_depth_abs, in.world_position.w); + let alpha = saturate(1.0 - (normal_depth * inv_depth_fade_factor)); + + return ForwardDecalInformation(world_position, uv, alpha); +} diff --git a/crates/libmarathon/src/render/pbr/decal/mod.rs b/crates/libmarathon/src/render/pbr/decal/mod.rs new file mode 100644 index 0000000..1b92131 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/decal/mod.rs @@ -0,0 +1,11 @@ +//! Decal rendering. +//! +//! Decals are a material that render on top of the surface that they're placed above. +//! They can be used to render signs, paint, snow, impact craters, and other effects on top of surfaces. + +// TODO: Once other decal types are added, write a paragraph comparing the different types in the module docs. + +pub mod clustered; +mod forward; + +pub use forward::*; diff --git a/crates/libmarathon/src/render/pbr/deferred/deferred_lighting.wgsl b/crates/libmarathon/src/render/pbr/deferred/deferred_lighting.wgsl new file mode 100644 index 0000000..7c14eea --- /dev/null +++ b/crates/libmarathon/src/render/pbr/deferred/deferred_lighting.wgsl @@ -0,0 +1,88 @@ +#import bevy_pbr::{ + prepass_utils, + pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT, + pbr_functions, + pbr_deferred_functions::pbr_input_from_deferred_gbuffer, + pbr_deferred_types::unpack_unorm3x4_plus_unorm_20_, + lighting, + mesh_view_bindings::deferred_prepass_texture, +} + +#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION +#import bevy_pbr::mesh_view_bindings::screen_space_ambient_occlusion_texture +#import bevy_pbr::ssao_utils::ssao_multibounce +#endif + +struct FullscreenVertexOutput { + @builtin(position) + position: vec4, + @location(0) + uv: vec2, +}; + +struct PbrDeferredLightingDepthId { + depth_id: u32, // limited to u8 +#ifdef SIXTEEN_BYTE_ALIGNMENT + // WebGL2 structs must be 16 byte aligned. + _webgl2_padding_0: f32, + _webgl2_padding_1: f32, + _webgl2_padding_2: f32, +#endif +} +@group(2) @binding(0) +var depth_id: PbrDeferredLightingDepthId; + +@vertex +fn vertex(@builtin(vertex_index) vertex_index: u32) -> FullscreenVertexOutput { + // See the full screen vertex shader for explanation above for how this works. + let uv = vec2(f32(vertex_index >> 1u), f32(vertex_index & 1u)) * 2.0; + // Depth is stored as unorm, so we are dividing the u8 depth_id by 255.0 here. + let clip_position = vec4(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), f32(depth_id.depth_id) / 255.0, 1.0); + + return FullscreenVertexOutput(clip_position, uv); +} + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + var frag_coord = vec4(in.position.xy, 0.0, 0.0); + + let deferred_data = textureLoad(deferred_prepass_texture, vec2(frag_coord.xy), 0); + +#ifdef WEBGL2 + frag_coord.z = unpack_unorm3x4_plus_unorm_20_(deferred_data.b).w; +#else +#ifdef DEPTH_PREPASS + frag_coord.z = prepass_utils::prepass_depth(in.position, 0u); +#endif +#endif + + var pbr_input = pbr_input_from_deferred_gbuffer(frag_coord, deferred_data); + var output_color = vec4(0.0); + + // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit + if ((pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) { + +#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION + let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2(in.position.xy), 0i).r; + let ssao_multibounce = ssao_multibounce(ssao, pbr_input.material.base_color.rgb); + pbr_input.diffuse_occlusion = min(pbr_input.diffuse_occlusion, ssao_multibounce); + + // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" + let NdotV = max(dot(pbr_input.N, pbr_input.V), 0.0001); + var perceptual_roughness: f32 = pbr_input.material.perceptual_roughness; + let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); + // Use SSAO to estimate the specular occlusion. + // Lagarde and Rousiers 2014, "Moving Frostbite to Physically Based Rendering" + pbr_input.specular_occlusion = saturate(pow(NdotV + ssao, exp2(-16.0 * roughness - 1.0)) - 1.0 + ssao); +#endif // SCREEN_SPACE_AMBIENT_OCCLUSION + + output_color = pbr_functions::apply_pbr_lighting(pbr_input); + } else { + output_color = pbr_input.material.base_color; + } + + output_color = pbr_functions::main_pass_post_lighting_processing(pbr_input, output_color); + + return output_color; +} + diff --git a/crates/libmarathon/src/render/pbr/deferred/mod.rs b/crates/libmarathon/src/render/pbr/deferred/mod.rs new file mode 100644 index 0000000..4604abb --- /dev/null +++ b/crates/libmarathon/src/render/pbr/deferred/mod.rs @@ -0,0 +1,570 @@ +use crate::render::pbr::{ + graph::NodePbr, MeshPipeline, MeshViewBindGroup, RenderViewLightProbes, + ScreenSpaceAmbientOcclusion, ScreenSpaceReflectionsUniform, ViewEnvironmentMapUniformOffset, + ViewLightProbesUniformOffset, ViewScreenSpaceReflectionsUniformOffset, + TONEMAPPING_LUT_SAMPLER_BINDING_INDEX, TONEMAPPING_LUT_TEXTURE_BINDING_INDEX, +}; +use crate::render::pbr::{DistanceFog, MeshPipelineKey, ViewFogUniformOffset, ViewLightsUniformOffset}; +use bevy_app::prelude::*; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; +use crate::render::{ + core_3d::graph::{Core3d, Node3d}, + deferred::{ + copy_lighting_id::DeferredLightingIdDepthTexture, DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, + }, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + tonemapping::{DebandDither, Tonemapping}, +}; +use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_image::BevyDefault as _; +use bevy_light::{EnvironmentMapLight, IrradianceVolume, ShadowFilteringMethod}; +use crate::render::RenderStartup; +use crate::render::{ + diagnostic::RecordDiagnostics, + extract_component::{ + ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, + }, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, + render_resource::{binding_types::uniform_buffer, *}, + renderer::{RenderContext, RenderDevice}, + view::{ExtractedView, ViewTarget, ViewUniformOffset}, + Render, RenderApp, RenderSystems, +}; +use bevy_shader::{Shader, ShaderDefVal}; +use bevy_utils::default; + +pub struct DeferredPbrLightingPlugin; + +pub const DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID: u8 = 1; + +/// Component with a `depth_id` for specifying which corresponding materials should be rendered by this specific PBR deferred lighting pass. +/// +/// Will be automatically added to entities with the [`DeferredPrepass`] component that don't already have a [`PbrDeferredLightingDepthId`]. +#[derive(Component, Clone, Copy, ExtractComponent, ShaderType)] +pub struct PbrDeferredLightingDepthId { + depth_id: u32, + + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_0: f32, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_1: f32, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_2: f32, +} + +impl PbrDeferredLightingDepthId { + pub fn new(value: u8) -> PbrDeferredLightingDepthId { + PbrDeferredLightingDepthId { + depth_id: value as u32, + + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_0: 0.0, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_1: 0.0, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_2: 0.0, + } + } + + pub fn set(&mut self, value: u8) { + self.depth_id = value as u32; + } + + pub fn get(&self) -> u8 { + self.depth_id as u8 + } +} + +impl Default for PbrDeferredLightingDepthId { + fn default() -> Self { + PbrDeferredLightingDepthId { + depth_id: DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID as u32, + + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_0: 0.0, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_1: 0.0, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _webgl2_padding_2: 0.0, + } + } +} + +impl Plugin for DeferredPbrLightingPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + ExtractComponentPlugin::::default(), + UniformComponentPlugin::::default(), + )) + .add_systems(PostUpdate, insert_deferred_lighting_pass_id_component); + + embedded_asset!(app, "deferred_lighting.wgsl"); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::>() + .add_systems(RenderStartup, init_deferred_lighting_layout) + .add_systems( + Render, + (prepare_deferred_lighting_pipelines.in_set(RenderSystems::Prepare),), + ) + .add_render_graph_node::>( + Core3d, + NodePbr::DeferredLightingPass, + ) + .add_render_graph_edges( + Core3d, + ( + Node3d::StartMainPass, + NodePbr::DeferredLightingPass, + Node3d::MainOpaquePass, + ), + ); + } +} + +#[derive(Default)] +pub struct DeferredOpaquePass3dPbrLightingNode; + +impl ViewNode for DeferredOpaquePass3dPbrLightingNode { + type ViewQuery = ( + &'static ViewUniformOffset, + &'static ViewLightsUniformOffset, + &'static ViewFogUniformOffset, + &'static ViewLightProbesUniformOffset, + &'static ViewScreenSpaceReflectionsUniformOffset, + &'static ViewEnvironmentMapUniformOffset, + &'static MeshViewBindGroup, + &'static ViewTarget, + &'static DeferredLightingIdDepthTexture, + &'static DeferredLightingPipeline, + ); + + fn run( + &self, + _graph_context: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + view_uniform_offset, + view_lights_offset, + view_fog_offset, + view_light_probes_offset, + view_ssr_offset, + view_environment_map_offset, + mesh_view_bind_group, + target, + deferred_lighting_id_depth_texture, + deferred_lighting_pipeline, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let deferred_lighting_layout = world.resource::(); + + let Some(pipeline) = + pipeline_cache.get_render_pipeline(deferred_lighting_pipeline.pipeline_id) + else { + return Ok(()); + }; + + let deferred_lighting_pass_id = + world.resource::>(); + let Some(deferred_lighting_pass_id_binding) = + deferred_lighting_pass_id.uniforms().binding() + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let bind_group_2 = render_context.render_device().create_bind_group( + "deferred_lighting_layout_group_2", + &deferred_lighting_layout.bind_group_layout_2, + &BindGroupEntries::single(deferred_lighting_pass_id_binding), + ); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("deferred_lighting"), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &deferred_lighting_id_depth_texture.texture.default_view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: StoreOp::Discard, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + let pass_span = diagnostics.pass_span(&mut render_pass, "deferred_lighting"); + + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group( + 0, + &mesh_view_bind_group.main, + &[ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + **view_light_probes_offset, + **view_ssr_offset, + **view_environment_map_offset, + ], + ); + render_pass.set_bind_group(1, &mesh_view_bind_group.binding_array, &[]); + render_pass.set_bind_group(2, &bind_group_2, &[]); + render_pass.draw(0..3, 0..1); + + pass_span.end(&mut render_pass); + + Ok(()) + } +} + +#[derive(Resource)] +pub struct DeferredLightingLayout { + mesh_pipeline: MeshPipeline, + bind_group_layout_2: BindGroupLayout, + deferred_lighting_shader: Handle, +} + +#[derive(Component)] +pub struct DeferredLightingPipeline { + pub pipeline_id: CachedRenderPipelineId, +} + +impl SpecializedRenderPipeline for DeferredLightingLayout { + type Key = MeshPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut shader_defs = Vec::new(); + + // Let the shader code know that it's running in a deferred pipeline. + shader_defs.push("DEFERRED_LIGHTING_PIPELINE".into()); + + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + shader_defs.push("WEBGL2".into()); + + if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) { + shader_defs.push("TONEMAP_IN_SHADER".into()); + shader_defs.push(ShaderDefVal::UInt( + "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(), + TONEMAPPING_LUT_TEXTURE_BINDING_INDEX, + )); + shader_defs.push(ShaderDefVal::UInt( + "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(), + TONEMAPPING_LUT_SAMPLER_BINDING_INDEX, + )); + + let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS); + + if method == MeshPipelineKey::TONEMAP_METHOD_NONE { + shader_defs.push("TONEMAP_METHOD_NONE".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD { + shader_defs.push("TONEMAP_METHOD_REINHARD".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE { + shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED { + shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_AGX { + shader_defs.push("TONEMAP_METHOD_AGX".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM { + shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC { + shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE { + shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into()); + } + + // Debanding is tied to tonemapping in the shader, cannot run without it. + if key.contains(MeshPipelineKey::DEBAND_DITHER) { + shader_defs.push("DEBAND_DITHER".into()); + } + } + + if key.contains(MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION) { + shader_defs.push("SCREEN_SPACE_AMBIENT_OCCLUSION".into()); + } + + if key.contains(MeshPipelineKey::ENVIRONMENT_MAP) { + shader_defs.push("ENVIRONMENT_MAP".into()); + } + + if key.contains(MeshPipelineKey::IRRADIANCE_VOLUME) { + shader_defs.push("IRRADIANCE_VOLUME".into()); + } + + if key.contains(MeshPipelineKey::NORMAL_PREPASS) { + shader_defs.push("NORMAL_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::DEPTH_PREPASS) { + shader_defs.push("DEPTH_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + shader_defs.push("MOTION_VECTOR_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::SCREEN_SPACE_REFLECTIONS) { + shader_defs.push("SCREEN_SPACE_REFLECTIONS".into()); + } + + if key.contains(MeshPipelineKey::HAS_PREVIOUS_SKIN) { + shader_defs.push("HAS_PREVIOUS_SKIN".into()); + } + + if key.contains(MeshPipelineKey::HAS_PREVIOUS_MORPH) { + shader_defs.push("HAS_PREVIOUS_MORPH".into()); + } + + if key.contains(MeshPipelineKey::DISTANCE_FOG) { + shader_defs.push("DISTANCE_FOG".into()); + } + + // Always true, since we're in the deferred lighting pipeline + shader_defs.push("DEFERRED_PREPASS".into()); + + let shadow_filter_method = + key.intersection(MeshPipelineKey::SHADOW_FILTER_METHOD_RESERVED_BITS); + if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2 { + shader_defs.push("SHADOW_FILTER_METHOD_HARDWARE_2X2".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN { + shader_defs.push("SHADOW_FILTER_METHOD_GAUSSIAN".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL { + shader_defs.push("SHADOW_FILTER_METHOD_TEMPORAL".into()); + } + if self.mesh_pipeline.binding_arrays_are_usable { + shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into()); + shader_defs.push("MULTIPLE_LIGHTMAPS_IN_ARRAY".into()); + } + + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + shader_defs.push("SIXTEEN_BYTE_ALIGNMENT".into()); + + let layout = self.mesh_pipeline.get_view_layout(key.into()); + RenderPipelineDescriptor { + label: Some("deferred_lighting_pipeline".into()), + layout: vec![ + layout.main_layout.clone(), + layout.binding_array_layout.clone(), + self.bind_group_layout_2.clone(), + ], + vertex: VertexState { + shader: self.deferred_lighting_shader.clone(), + shader_defs: shader_defs.clone(), + ..default() + }, + fragment: Some(FragmentState { + shader: self.deferred_lighting_shader.clone(), + shader_defs, + targets: vec![Some(ColorTargetState { + format: if key.contains(MeshPipelineKey::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: None, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + depth_stencil: Some(DepthStencilState { + format: DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, + depth_write_enabled: false, + depth_compare: CompareFunction::Equal, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + ..default() + } + } +} + +pub fn init_deferred_lighting_layout( + mut commands: Commands, + render_device: Res, + mesh_pipeline: Res, + asset_server: Res, +) { + let layout = render_device.create_bind_group_layout( + "deferred_lighting_layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + uniform_buffer::(false), + ), + ); + commands.insert_resource(DeferredLightingLayout { + mesh_pipeline: mesh_pipeline.clone(), + bind_group_layout_2: layout, + deferred_lighting_shader: load_embedded_asset!( + asset_server.as_ref(), + "deferred_lighting.wgsl" + ), + }); +} + +pub fn insert_deferred_lighting_pass_id_component( + mut commands: Commands, + views: Query, Without)>, +) { + for entity in views.iter() { + commands + .entity(entity) + .insert(PbrDeferredLightingDepthId::default()); + } +} + +pub fn prepare_deferred_lighting_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + deferred_lighting_layout: Res, + views: Query<( + Entity, + &ExtractedView, + Option<&Tonemapping>, + Option<&DebandDither>, + Option<&ShadowFilteringMethod>, + ( + Has, + Has, + Has, + ), + ( + Has, + Has, + Has, + Has, + ), + Has>, + Has>, + Has, + )>, +) { + for ( + entity, + view, + tonemapping, + dither, + shadow_filter_method, + (ssao, ssr, distance_fog), + (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), + has_environment_maps, + has_irradiance_volumes, + skip_deferred_lighting, + ) in &views + { + // If there is no deferred prepass or we want to skip the deferred lighting pass, + // remove the old pipeline if there was one. This handles the case in which a + // view using deferred stops using it. + if !deferred_prepass || skip_deferred_lighting { + commands.entity(entity).remove::(); + continue; + } + + let mut view_key = MeshPipelineKey::from_hdr(view.hdr); + + if normal_prepass { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + + if depth_prepass { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + + if motion_vector_prepass { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + + // Always true, since we're in the deferred lighting pipeline + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + + if !view.hdr { + if let Some(tonemapping) = tonemapping { + view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; + view_key |= match tonemapping { + Tonemapping::None => MeshPipelineKey::TONEMAP_METHOD_NONE, + Tonemapping::Reinhard => MeshPipelineKey::TONEMAP_METHOD_REINHARD, + Tonemapping::ReinhardLuminance => { + MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE + } + Tonemapping::AcesFitted => MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED, + Tonemapping::AgX => MeshPipelineKey::TONEMAP_METHOD_AGX, + Tonemapping::SomewhatBoringDisplayTransform => { + MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + } + Tonemapping::TonyMcMapface => MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE, + Tonemapping::BlenderFilmic => MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC, + }; + } + if let Some(DebandDither::Enabled) = dither { + view_key |= MeshPipelineKey::DEBAND_DITHER; + } + } + + if ssao { + view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; + } + if ssr { + view_key |= MeshPipelineKey::SCREEN_SPACE_REFLECTIONS; + } + if distance_fog { + view_key |= MeshPipelineKey::DISTANCE_FOG; + } + + // We don't need to check to see whether the environment map is loaded + // because [`gather_light_probes`] already checked that for us before + // adding the [`RenderViewEnvironmentMaps`] component. + if has_environment_maps { + view_key |= MeshPipelineKey::ENVIRONMENT_MAP; + } + + if has_irradiance_volumes { + view_key |= MeshPipelineKey::IRRADIANCE_VOLUME; + } + + match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) { + ShadowFilteringMethod::Hardware2x2 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; + } + ShadowFilteringMethod::Gaussian => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN; + } + ShadowFilteringMethod::Temporal => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL; + } + } + + let pipeline_id = + pipelines.specialize(&pipeline_cache, &deferred_lighting_layout, view_key); + + commands + .entity(entity) + .insert(DeferredLightingPipeline { pipeline_id }); + } +} + +/// Component to skip running the deferred lighting pass in [`DeferredOpaquePass3dPbrLightingNode`] for a specific view. +/// +/// This works like [`crate::PbrPlugin::add_default_deferred_lighting_plugin`], but is per-view instead of global. +/// +/// Useful for cases where you want to generate a gbuffer, but skip the built-in deferred lighting pass +/// to run your own custom lighting pass instead. +/// +/// Insert this component in the render world only. +#[derive(Component, Clone, Copy, Default)] +pub struct SkipDeferredLighting; diff --git a/crates/libmarathon/src/render/pbr/deferred/pbr_deferred_functions.wgsl b/crates/libmarathon/src/render/pbr/deferred/pbr_deferred_functions.wgsl new file mode 100644 index 0000000..e6254b1 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/deferred/pbr_deferred_functions.wgsl @@ -0,0 +1,153 @@ +#define_import_path bevy_pbr::pbr_deferred_functions + +#import bevy_pbr::{ + pbr_types::{PbrInput, pbr_input_new, STANDARD_MATERIAL_FLAGS_UNLIT_BIT}, + pbr_deferred_types as deferred_types, + pbr_functions, + rgb9e5, + mesh_view_bindings::view, + utils::{octahedral_encode, octahedral_decode}, + prepass_io::FragmentOutput, + view_transformations::{position_ndc_to_world, frag_coord_to_ndc}, +} + +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput +#else +#import bevy_pbr::prepass_io::VertexOutput +#endif + +#ifdef MOTION_VECTOR_PREPASS + #import bevy_pbr::pbr_prepass_functions::calculate_motion_vector +#endif + +// Creates the deferred gbuffer from a PbrInput. +fn deferred_gbuffer_from_pbr_input(in: PbrInput) -> vec4 { + // Only monochrome occlusion supported. May not be worth including at all. + // Some models have baked occlusion, GLTF only supports monochrome. + // Real time occlusion is applied in the deferred lighting pass. + // Deriving luminance via Rec. 709. coefficients + // https://en.wikipedia.org/wiki/Rec._709 + let rec_709_coeffs = vec3(0.2126, 0.7152, 0.0722); + let diffuse_occlusion = dot(in.diffuse_occlusion, rec_709_coeffs); + // Only monochrome specular supported. + let reflectance = dot(in.material.reflectance, rec_709_coeffs); +#ifdef WEBGL2 // More crunched for webgl so we can also fit depth. + var props = deferred_types::pack_unorm3x4_plus_unorm_20_(vec4( + reflectance, + in.material.metallic, + diffuse_occlusion, + in.frag_coord.z)); +#else + var props = deferred_types::pack_unorm4x8_(vec4( + reflectance, // could be fewer bits + in.material.metallic, // could be fewer bits + diffuse_occlusion, // is this worth including? + 0.0)); // spare +#endif // WEBGL2 + let flags = deferred_types::deferred_flags_from_mesh_material_flags(in.flags, in.material.flags); + let octahedral_normal = octahedral_encode(normalize(in.N)); + var base_color_srgb = vec3(0.0); + var emissive = in.material.emissive.rgb; + if ((in.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) != 0u) { + // Material is unlit, use emissive component of gbuffer for color data. + // Unlit materials are effectively emissive. + emissive = in.material.base_color.rgb; + } else { + base_color_srgb = pow(in.material.base_color.rgb, vec3(1.0 / 2.2)); + } + + // Utilize the emissive channel to transmit the lightmap data. To ensure + // it matches the output in forward shading, pre-multiply it with the + // calculated diffuse color. + let base_color = in.material.base_color.rgb; + let metallic = in.material.metallic; + let specular_transmission = in.material.specular_transmission; + let diffuse_transmission = in.material.diffuse_transmission; + let diffuse_color = pbr_functions::calculate_diffuse_color( + base_color, + metallic, + specular_transmission, + diffuse_transmission + ); + emissive += in.lightmap_light * diffuse_color * view.exposure; + + let deferred = vec4( + deferred_types::pack_unorm4x8_(vec4(base_color_srgb, in.material.perceptual_roughness)), + rgb9e5::vec3_to_rgb9e5_(emissive), + props, + deferred_types::pack_24bit_normal_and_flags(octahedral_normal, flags), + ); + return deferred; +} + +// Creates a PbrInput from the deferred gbuffer. +fn pbr_input_from_deferred_gbuffer(frag_coord: vec4, gbuffer: vec4) -> PbrInput { + var pbr = pbr_input_new(); + + let flags = deferred_types::unpack_flags(gbuffer.a); + let deferred_flags = deferred_types::mesh_material_flags_from_deferred_flags(flags); + pbr.flags = deferred_flags.x; + pbr.material.flags = deferred_flags.y; + + let base_rough = deferred_types::unpack_unorm4x8_(gbuffer.r); + pbr.material.perceptual_roughness = base_rough.a; + let emissive = rgb9e5::rgb9e5_to_vec3_(gbuffer.g); + if ((pbr.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) != 0u) { + pbr.material.base_color = vec4(emissive, 1.0); + pbr.material.emissive = vec4(vec3(0.0), 0.0); + } else { + pbr.material.base_color = vec4(pow(base_rough.rgb, vec3(2.2)), 1.0); + pbr.material.emissive = vec4(emissive, 0.0); + } +#ifdef WEBGL2 // More crunched for webgl so we can also fit depth. + let props = deferred_types::unpack_unorm3x4_plus_unorm_20_(gbuffer.b); + // Bias to 0.5 since that's the value for almost all materials. + pbr.material.reflectance = vec3(saturate(props.r - 0.03333333333)); +#else + let props = deferred_types::unpack_unorm4x8_(gbuffer.b); + pbr.material.reflectance = vec3(props.r); +#endif // WEBGL2 + pbr.material.metallic = props.g; + pbr.diffuse_occlusion = vec3(props.b); + let octahedral_normal = deferred_types::unpack_24bit_normal(gbuffer.a); + let N = octahedral_decode(octahedral_normal); + + let world_position = vec4(position_ndc_to_world(frag_coord_to_ndc(frag_coord)), 1.0); + let is_orthographic = view.clip_from_view[3].w == 1.0; + let V = pbr_functions::calculate_view(world_position, is_orthographic); + + pbr.frag_coord = frag_coord; + pbr.world_normal = N; + pbr.world_position = world_position; + pbr.N = N; + pbr.V = V; + pbr.is_orthographic = is_orthographic; + + return pbr; +} + +#ifdef PREPASS_PIPELINE +fn deferred_output(in: VertexOutput, pbr_input: PbrInput) -> FragmentOutput { + var out: FragmentOutput; + + // gbuffer + out.deferred = deferred_gbuffer_from_pbr_input(pbr_input); + // lighting pass id (used to determine which lighting shader to run for the fragment) + out.deferred_lighting_pass_id = pbr_input.material.deferred_lighting_pass_id; + // normal if required +#ifdef NORMAL_PREPASS + out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0); +#endif + // motion vectors if required +#ifdef MOTION_VECTOR_PREPASS +#ifdef MESHLET_MESH_MATERIAL_PASS + out.motion_vector = in.motion_vector; +#else + out.motion_vector = calculate_motion_vector(in.world_position, in.previous_world_position); +#endif +#endif + + return out; +} +#endif diff --git a/crates/libmarathon/src/render/pbr/deferred/pbr_deferred_types.wgsl b/crates/libmarathon/src/render/pbr/deferred/pbr_deferred_types.wgsl new file mode 100644 index 0000000..fb4def9 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/deferred/pbr_deferred_types.wgsl @@ -0,0 +1,89 @@ +#define_import_path bevy_pbr::pbr_deferred_types + +#import bevy_pbr::{ + mesh_types::MESH_FLAGS_SHADOW_RECEIVER_BIT, + pbr_types::{STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT, STANDARD_MATERIAL_FLAGS_UNLIT_BIT}, +} + +// Maximum of 8 bits available +const DEFERRED_FLAGS_UNLIT_BIT: u32 = 1u << 0u; +const DEFERRED_FLAGS_FOG_ENABLED_BIT: u32 = 1u << 1u; +const DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u << 2u; + +fn deferred_flags_from_mesh_material_flags(mesh_flags: u32, mat_flags: u32) -> u32 { + var flags = 0u; + flags |= u32((mesh_flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u) * DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT; + flags |= u32((mat_flags & STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) * DEFERRED_FLAGS_FOG_ENABLED_BIT; + flags |= u32((mat_flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) != 0u) * DEFERRED_FLAGS_UNLIT_BIT; + return flags; +} + +fn mesh_material_flags_from_deferred_flags(deferred_flags: u32) -> vec2 { + var mat_flags = 0u; + var mesh_flags = 0u; + mesh_flags |= u32((deferred_flags & DEFERRED_MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u) * MESH_FLAGS_SHADOW_RECEIVER_BIT; + mat_flags |= u32((deferred_flags & DEFERRED_FLAGS_FOG_ENABLED_BIT) != 0u) * STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT; + mat_flags |= u32((deferred_flags & DEFERRED_FLAGS_UNLIT_BIT) != 0u) * STANDARD_MATERIAL_FLAGS_UNLIT_BIT; + return vec2(mesh_flags, mat_flags); +} + +const U12MAXF = 4095.0; +const U16MAXF = 65535.0; +const U20MAXF = 1048575.0; + +// Storing normals as oct24. +// Flags are stored in the remaining 8 bits. +// https://jcgt.org/published/0003/02/01/paper.pdf +// Could possibly go down to oct20 if the space is needed. + +fn pack_24bit_normal_and_flags(octahedral_normal: vec2, flags: u32) -> u32 { + let unorm1 = u32(saturate(octahedral_normal.x) * U12MAXF + 0.5); + let unorm2 = u32(saturate(octahedral_normal.y) * U12MAXF + 0.5); + return (unorm1 & 0xFFFu) | ((unorm2 & 0xFFFu) << 12u) | ((flags & 0xFFu) << 24u); +} + +fn unpack_24bit_normal(packed: u32) -> vec2 { + let unorm1 = packed & 0xFFFu; + let unorm2 = (packed >> 12u) & 0xFFFu; + return vec2(f32(unorm1) / U12MAXF, f32(unorm2) / U12MAXF); +} + +fn unpack_flags(packed: u32) -> u32 { + return (packed >> 24u) & 0xFFu; +} + +// The builtin one didn't work in webgl. +// "'unpackUnorm4x8' : no matching overloaded function found" +// https://github.com/gfx-rs/naga/issues/2006 +fn unpack_unorm4x8_(v: u32) -> vec4 { + return vec4( + f32(v & 0xFFu), + f32((v >> 8u) & 0xFFu), + f32((v >> 16u) & 0xFFu), + f32((v >> 24u) & 0xFFu) + ) / 255.0; +} + +// 'packUnorm4x8' : no matching overloaded function found +// https://github.com/gfx-rs/naga/issues/2006 +fn pack_unorm4x8_(values: vec4) -> u32 { + let v = vec4(saturate(values) * 255.0 + 0.5); + return (v.w << 24u) | (v.z << 16u) | (v.y << 8u) | v.x; +} + +// Pack 3x 4bit unorm + 1x 20bit +fn pack_unorm3x4_plus_unorm_20_(v: vec4) -> u32 { + let sm = vec3(saturate(v.xyz) * 15.0 + 0.5); + let bg = u32(saturate(v.w) * U20MAXF + 0.5); + return (bg << 12u) | (sm.z << 8u) | (sm.y << 4u) | sm.x; +} + +// Unpack 3x 4bit unorm + 1x 20bit +fn unpack_unorm3x4_plus_unorm_20_(v: u32) -> vec4 { + return vec4( + f32(v & 0xfu) / 15.0, + f32((v >> 4u) & 0xFu) / 15.0, + f32((v >> 8u) & 0xFu) / 15.0, + f32((v >> 12u) & 0xFFFFFFu) / U20MAXF, + ); +} diff --git a/crates/libmarathon/src/render/pbr/extended_material.rs b/crates/libmarathon/src/render/pbr/extended_material.rs new file mode 100644 index 0000000..2aa3407 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/extended_material.rs @@ -0,0 +1,406 @@ +use std::borrow::Cow; + +use bevy_asset::Asset; +use bevy_ecs::system::SystemParamItem; +use bevy_mesh::MeshVertexBufferLayoutRef; +use bevy_platform::{collections::HashSet, hash::FixedHasher}; +use bevy_reflect::{impl_type_path, Reflect}; +use crate::render::{ + alpha::AlphaMode, + render_resource::{ + AsBindGroup, AsBindGroupError, BindGroupLayout, BindGroupLayoutEntry, BindlessDescriptor, + BindlessResourceType, BindlessSlabResourceLimit, RenderPipelineDescriptor, + SpecializedMeshPipelineError, UnpreparedBindGroup, + }, + renderer::RenderDevice, +}; +use bevy_shader::ShaderRef; + +use crate::render::pbr::{Material, MaterialPipeline, MaterialPipelineKey, MeshPipeline, MeshPipelineKey}; + +pub struct MaterialExtensionPipeline { + pub mesh_pipeline: MeshPipeline, +} + +pub struct MaterialExtensionKey { + pub mesh_key: MeshPipelineKey, + pub bind_group_data: E::Data, +} + +/// A subset of the `Material` trait for defining extensions to a base `Material`, such as the builtin `StandardMaterial`. +/// +/// A user type implementing the trait should be used as the `E` generic param in an `ExtendedMaterial` struct. +pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized { + /// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the base material mesh vertex shader + /// will be used. + fn vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the base material mesh fragment shader + /// will be used. + fn fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + // Returns this material’s AlphaMode. If None is returned, the base material alpha mode will be used. + fn alpha_mode() -> Option { + None + } + + /// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the base material prepass vertex shader + /// will be used. + fn prepass_vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material prepass fragment shader + /// will be used. + fn prepass_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's deferred vertex shader. If [`ShaderRef::Default`] is returned, the base material deferred vertex shader + /// will be used. + fn deferred_vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material deferred fragment shader + /// will be used. + fn deferred_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh fragment shader will be used. + #[cfg(feature = "meshlet")] + fn meshlet_mesh_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] prepass fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh prepass fragment shader will be used. + #[cfg(feature = "meshlet")] + fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] deferred fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh deferred fragment shader will be used. + #[cfg(feature = "meshlet")] + fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's + /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayoutRef`] as input. + /// Specialization for the base material is applied before this function is called. + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + #[inline] + fn specialize( + pipeline: &MaterialExtensionPipeline, + descriptor: &mut RenderPipelineDescriptor, + layout: &MeshVertexBufferLayoutRef, + key: MaterialExtensionKey, + ) -> Result<(), SpecializedMeshPipelineError> { + Ok(()) + } +} + +/// A material that extends a base [`Material`] with additional shaders and data. +/// +/// The data from both materials will be combined and made available to the shader +/// so that shader functions built for the base material (and referencing the base material +/// bindings) will work as expected, and custom alterations based on custom data can also be used. +/// +/// If the extension `E` returns a non-default result from `vertex_shader()` it will be used in place of the base +/// material's vertex shader. +/// +/// If the extension `E` returns a non-default result from `fragment_shader()` it will be used in place of the base +/// fragment shader. +/// +/// When used with `StandardMaterial` as the base, all the standard material fields are +/// present, so the `pbr_fragment` shader functions can be called from the extension shader (see +/// the `extended_material` example). +#[derive(Asset, Clone, Debug, Reflect)] +#[reflect(type_path = false)] +#[reflect(Clone)] +pub struct ExtendedMaterial { + pub base: B, + pub extension: E, +} + +impl Default for ExtendedMaterial +where + B: Material + Default, + E: MaterialExtension + Default, +{ + fn default() -> Self { + Self { + base: B::default(), + extension: E::default(), + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +#[repr(C, packed)] +pub struct MaterialExtensionBindGroupData { + pub base: B, + pub extension: E, +} + +// We don't use the `TypePath` derive here due to a bug where `#[reflect(type_path = false)]` +// causes the `TypePath` derive to not generate an implementation. +impl_type_path!((in bevy_pbr::extended_material) ExtendedMaterial); + +impl AsBindGroup for ExtendedMaterial { + type Data = MaterialExtensionBindGroupData; + type Param = (::Param, ::Param); + + fn bindless_slot_count() -> Option { + // We only enable bindless if both the base material and its extension + // are bindless. If we do enable bindless, we choose the smaller of the + // two slab size limits. + match (B::bindless_slot_count()?, E::bindless_slot_count()?) { + (BindlessSlabResourceLimit::Auto, BindlessSlabResourceLimit::Auto) => { + Some(BindlessSlabResourceLimit::Auto) + } + (BindlessSlabResourceLimit::Auto, BindlessSlabResourceLimit::Custom(limit)) + | (BindlessSlabResourceLimit::Custom(limit), BindlessSlabResourceLimit::Auto) => { + Some(BindlessSlabResourceLimit::Custom(limit)) + } + ( + BindlessSlabResourceLimit::Custom(base_limit), + BindlessSlabResourceLimit::Custom(extended_limit), + ) => Some(BindlessSlabResourceLimit::Custom( + base_limit.min(extended_limit), + )), + } + } + + fn bind_group_data(&self) -> Self::Data { + MaterialExtensionBindGroupData { + base: self.base.bind_group_data(), + extension: self.extension.bind_group_data(), + } + } + + fn unprepared_bind_group( + &self, + layout: &BindGroupLayout, + render_device: &RenderDevice, + (base_param, extended_param): &mut SystemParamItem<'_, '_, Self::Param>, + mut force_non_bindless: bool, + ) -> Result { + force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none(); + + // add together the bindings of the base material and the user material + let UnpreparedBindGroup { mut bindings } = B::unprepared_bind_group( + &self.base, + layout, + render_device, + base_param, + force_non_bindless, + )?; + let extended_bindgroup = E::unprepared_bind_group( + &self.extension, + layout, + render_device, + extended_param, + force_non_bindless, + )?; + + bindings.extend(extended_bindgroup.bindings.0); + + Ok(UnpreparedBindGroup { bindings }) + } + + fn bind_group_layout_entries( + render_device: &RenderDevice, + mut force_non_bindless: bool, + ) -> Vec + where + Self: Sized, + { + force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none(); + + // Add together the bindings of the standard material and the user + // material, skipping duplicate bindings. Duplicate bindings will occur + // when bindless mode is on, because of the common bindless resource + // arrays, and we need to eliminate the duplicates or `wgpu` will + // complain. + let mut entries = vec![]; + let mut seen_bindings = HashSet::<_>::with_hasher(FixedHasher); + for entry in B::bind_group_layout_entries(render_device, force_non_bindless) + .into_iter() + .chain(E::bind_group_layout_entries(render_device, force_non_bindless).into_iter()) + { + if seen_bindings.insert(entry.binding) { + entries.push(entry); + } + } + entries + } + + fn bindless_descriptor() -> Option { + // We're going to combine the two bindless descriptors. + let base_bindless_descriptor = B::bindless_descriptor()?; + let extended_bindless_descriptor = E::bindless_descriptor()?; + + // Combining the buffers and index tables is straightforward. + + let mut buffers = base_bindless_descriptor.buffers.to_vec(); + let mut index_tables = base_bindless_descriptor.index_tables.to_vec(); + + buffers.extend(extended_bindless_descriptor.buffers.iter().cloned()); + index_tables.extend(extended_bindless_descriptor.index_tables.iter().cloned()); + + // Combining the resources is a little trickier because the resource + // array is indexed by bindless index, so we have to merge the two + // arrays, not just concatenate them. + let max_bindless_index = base_bindless_descriptor + .resources + .len() + .max(extended_bindless_descriptor.resources.len()); + let mut resources = Vec::with_capacity(max_bindless_index); + for bindless_index in 0..max_bindless_index { + // In the event of a conflicting bindless index, we choose the + // base's binding. + match base_bindless_descriptor.resources.get(bindless_index) { + None | Some(&BindlessResourceType::None) => resources.push( + extended_bindless_descriptor + .resources + .get(bindless_index) + .copied() + .unwrap_or(BindlessResourceType::None), + ), + Some(&resource_type) => resources.push(resource_type), + } + } + + Some(BindlessDescriptor { + resources: Cow::Owned(resources), + buffers: Cow::Owned(buffers), + index_tables: Cow::Owned(index_tables), + }) + } +} + +impl Material for ExtendedMaterial { + fn vertex_shader() -> ShaderRef { + match E::vertex_shader() { + ShaderRef::Default => B::vertex_shader(), + specified => specified, + } + } + + fn fragment_shader() -> ShaderRef { + match E::fragment_shader() { + ShaderRef::Default => B::fragment_shader(), + specified => specified, + } + } + + fn alpha_mode(&self) -> AlphaMode { + match E::alpha_mode() { + Some(specified) => specified, + None => B::alpha_mode(&self.base), + } + } + + fn opaque_render_method(&self) -> crate::render::pbr::material::OpaqueRendererMethod { + B::opaque_render_method(&self.base) + } + + fn depth_bias(&self) -> f32 { + B::depth_bias(&self.base) + } + + fn reads_view_transmission_texture(&self) -> bool { + B::reads_view_transmission_texture(&self.base) + } + + fn prepass_vertex_shader() -> ShaderRef { + match E::prepass_vertex_shader() { + ShaderRef::Default => B::prepass_vertex_shader(), + specified => specified, + } + } + + fn prepass_fragment_shader() -> ShaderRef { + match E::prepass_fragment_shader() { + ShaderRef::Default => B::prepass_fragment_shader(), + specified => specified, + } + } + + fn deferred_vertex_shader() -> ShaderRef { + match E::deferred_vertex_shader() { + ShaderRef::Default => B::deferred_vertex_shader(), + specified => specified, + } + } + + fn deferred_fragment_shader() -> ShaderRef { + match E::deferred_fragment_shader() { + ShaderRef::Default => B::deferred_fragment_shader(), + specified => specified, + } + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_fragment_shader() -> ShaderRef { + match E::meshlet_mesh_fragment_shader() { + ShaderRef::Default => B::meshlet_mesh_fragment_shader(), + specified => specified, + } + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { + match E::meshlet_mesh_prepass_fragment_shader() { + ShaderRef::Default => B::meshlet_mesh_prepass_fragment_shader(), + specified => specified, + } + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { + match E::meshlet_mesh_deferred_fragment_shader() { + ShaderRef::Default => B::meshlet_mesh_deferred_fragment_shader(), + specified => specified, + } + } + + fn specialize( + pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + layout: &MeshVertexBufferLayoutRef, + key: MaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> { + // Call the base material's specialize function + let base_key = MaterialPipelineKey:: { + mesh_key: key.mesh_key, + bind_group_data: key.bind_group_data.base, + }; + B::specialize(pipeline, descriptor, layout, base_key)?; + + // Call the extended material's specialize function afterwards + E::specialize( + &MaterialExtensionPipeline { + mesh_pipeline: pipeline.mesh_pipeline.clone(), + }, + descriptor, + layout, + MaterialExtensionKey { + mesh_key: key.mesh_key, + bind_group_data: key.bind_group_data.extension, + }, + ) + } +} diff --git a/crates/libmarathon/src/render/pbr/fog.rs b/crates/libmarathon/src/render/pbr/fog.rs new file mode 100644 index 0000000..1cea9bc --- /dev/null +++ b/crates/libmarathon/src/render/pbr/fog.rs @@ -0,0 +1,477 @@ +use bevy_camera::Camera; +use bevy_color::{Color, ColorToComponents, LinearRgba}; +use bevy_ecs::prelude::*; +use bevy_math::{ops, Vec3}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use crate::render::extract_component::ExtractComponent; + +/// Configures the “classic” computer graphics [distance fog](https://en.wikipedia.org/wiki/Distance_fog) effect, +/// in which objects appear progressively more covered in atmospheric haze the further away they are from the camera. +/// Affects meshes rendered via the PBR [`StandardMaterial`](crate::StandardMaterial). +/// +/// ## Falloff +/// +/// The rate at which fog intensity increases with distance is controlled by the falloff mode. +/// Currently, the following fog falloff modes are supported: +/// +/// - [`FogFalloff::Linear`] +/// - [`FogFalloff::Exponential`] +/// - [`FogFalloff::ExponentialSquared`] +/// - [`FogFalloff::Atmospheric`] +/// +/// ## Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use crate::render::prelude::*; +/// # use bevy_camera::prelude::*; +/// # use bevy_pbr::prelude::*; +/// # use bevy_color::Color; +/// # fn system(mut commands: Commands) { +/// commands.spawn(( +/// // Setup your camera as usual +/// Camera3d::default(), +/// // Add fog to the same entity +/// DistanceFog { +/// color: Color::WHITE, +/// falloff: FogFalloff::Exponential { density: 1e-3 }, +/// ..Default::default() +/// }, +/// )); +/// # } +/// # bevy_ecs::system::assert_is_system(system); +/// ``` +/// +/// ## Material Override +/// +/// Once enabled for a specific camera, the fog effect can also be disabled for individual +/// [`StandardMaterial`](crate::StandardMaterial) instances via the `fog_enabled` flag. +#[derive(Debug, Clone, Component, Reflect, ExtractComponent)] +#[extract_component_filter(With)] +#[reflect(Component, Default, Debug, Clone)] +pub struct DistanceFog { + /// The color of the fog effect. + /// + /// **Tip:** The alpha channel of the color can be used to “modulate” the fog effect without + /// changing the fog falloff mode or parameters. + pub color: Color, + + /// Color used to modulate the influence of directional light colors on the + /// fog, where the view direction aligns with each directional light direction, + /// producing a “glow” or light dispersion effect. (e.g. around the sun) + /// + /// Use [`Color::NONE`] to disable the effect. + pub directional_light_color: Color, + + /// The exponent applied to the directional light alignment calculation. + /// A higher value means a more concentrated “glow”. + pub directional_light_exponent: f32, + + /// Determines which falloff mode to use, and its parameters. + pub falloff: FogFalloff, +} + +/// Allows switching between different fog falloff modes, and configuring their parameters. +/// +/// ## Convenience Methods +/// +/// When using non-linear fog modes it can be hard to determine the right parameter values +/// for a given scene. +/// +/// For easier artistic control, instead of creating the enum variants directly, you can use the +/// visibility-based convenience methods: +/// +/// - For `FogFalloff::Exponential`: +/// - [`FogFalloff::from_visibility()`] +/// - [`FogFalloff::from_visibility_contrast()`] +/// +/// - For `FogFalloff::ExponentialSquared`: +/// - [`FogFalloff::from_visibility_squared()`] +/// - [`FogFalloff::from_visibility_contrast_squared()`] +/// +/// - For `FogFalloff::Atmospheric`: +/// - [`FogFalloff::from_visibility_color()`] +/// - [`FogFalloff::from_visibility_colors()`] +/// - [`FogFalloff::from_visibility_contrast_color()`] +/// - [`FogFalloff::from_visibility_contrast_colors()`] +#[derive(Debug, Clone, Reflect)] +#[reflect(Clone)] +pub enum FogFalloff { + /// A linear fog falloff that grows in intensity between `start` and `end` distances. + /// + /// This falloff mode is simpler to control than other modes, however it can produce results that look “artificial”, depending on the scene. + /// + /// ## Formula + /// + /// The fog intensity for a given point in the scene is determined by the following formula: + /// + /// ```text + /// let fog_intensity = 1.0 - ((end - distance) / (end - start)).clamp(0.0, 1.0); + /// ``` + /// + /// + /// Plot showing how linear fog falloff behaves for start and end values of 0.8 and 2.2, respectively. + /// + /// 1 + /// 1 + /// 0 + /// 2 + /// 3 + /// distance + /// fog intensity + /// + /// + /// + /// start + /// end + /// + Linear { + /// Distance from the camera where fog is completely transparent, in world units. + start: f32, + + /// Distance from the camera where fog is completely opaque, in world units. + end: f32, + }, + + /// An exponential fog falloff with a given `density`. + /// + /// Initially gains intensity quickly with distance, then more slowly. Typically produces more natural results than [`FogFalloff::Linear`], + /// but is a bit harder to control. + /// + /// To move the fog “further away”, use lower density values. To move it “closer” use higher density values. + /// + /// ## Tips + /// + /// - Use the [`FogFalloff::from_visibility()`] convenience method to create an exponential falloff with the proper + /// density for a desired visibility distance in world units; + /// - It's not _unusual_ to have very large or very small values for the density, depending on the scene + /// scale. Typically, for scenes with objects in the scale of thousands of units, you might want density values + /// in the ballpark of `0.001`. Conversely, for really small scale scenes you might want really high values of + /// density; + /// - Combine the `density` parameter with the [`DistanceFog`] `color`'s alpha channel for easier artistic control. + /// + /// ## Formula + /// + /// The fog intensity for a given point in the scene is determined by the following formula: + /// + /// ```text + /// let fog_intensity = 1.0 - 1.0 / (distance * density).exp(); + /// ``` + /// + /// + /// Plot showing how exponential fog falloff behaves for different density values + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// 1 + /// 1 + /// 0 + /// 2 + /// 3 + /// density = 2 + /// density = 1 + /// density = 0.5 + /// distance + /// fog intensity + /// + Exponential { + /// Multiplier applied to the world distance (within the exponential fog falloff calculation). + density: f32, + }, + + /// A squared exponential fog falloff with a given `density`. + /// + /// Similar to [`FogFalloff::Exponential`], but grows more slowly in intensity for closer distances + /// before “catching up”. + /// + /// To move the fog “further away”, use lower density values. To move it “closer” use higher density values. + /// + /// ## Tips + /// + /// - Use the [`FogFalloff::from_visibility_squared()`] convenience method to create an exponential squared falloff + /// with the proper density for a desired visibility distance in world units; + /// - Combine the `density` parameter with the [`DistanceFog`] `color`'s alpha channel for easier artistic control. + /// + /// ## Formula + /// + /// The fog intensity for a given point in the scene is determined by the following formula: + /// + /// ```text + /// let fog_intensity = 1.0 - 1.0 / (distance * density).squared().exp(); + /// ``` + /// + /// + /// Plot showing how exponential squared fog falloff behaves for different density values + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// 1 + /// 1 + /// 0 + /// 2 + /// 3 + /// density = 2 + /// density = 1 + /// density = 0.5 + /// distance + /// fog intensity + /// + ExponentialSquared { + /// Multiplier applied to the world distance (within the exponential squared fog falloff calculation). + density: f32, + }, + + /// A more general form of the [`FogFalloff::Exponential`] mode. The falloff formula is separated into + /// two terms, `extinction` and `inscattering`, for a somewhat simplified atmospheric scattering model. + /// Additionally, individual color channels can have their own density values, resulting in a total of + /// six different configuration parameters. + /// + /// ## Tips + /// + /// - Use the [`FogFalloff::from_visibility_colors()`] or [`FogFalloff::from_visibility_color()`] convenience methods + /// to create an atmospheric falloff with the proper densities for a desired visibility distance in world units and + /// extinction and inscattering colors; + /// - Combine the atmospheric fog parameters with the [`DistanceFog`] `color`'s alpha channel for easier artistic control. + /// + /// ## Formula + /// + /// Unlike other modes, atmospheric falloff doesn't use a simple intensity-based blend of fog color with + /// object color. Instead, it calculates per-channel extinction and inscattering factors, which are + /// then used to calculate the final color. + /// + /// ```text + /// let extinction_factor = 1.0 - 1.0 / (distance * extinction).exp(); + /// let inscattering_factor = 1.0 - 1.0 / (distance * inscattering).exp(); + /// let result = input_color * (1.0 - extinction_factor) + fog_color * inscattering_factor; + /// ``` + /// + /// ## Equivalence to [`FogFalloff::Exponential`] + /// + /// For a density value of `D`, the following two falloff modes will produce identical visual results: + /// + /// ``` + /// # use bevy_pbr::prelude::*; + /// # use bevy_math::prelude::*; + /// # const D: f32 = 0.5; + /// # + /// let exponential = FogFalloff::Exponential { + /// density: D, + /// }; + /// + /// let atmospheric = FogFalloff::Atmospheric { + /// extinction: Vec3::new(D, D, D), + /// inscattering: Vec3::new(D, D, D), + /// }; + /// ``` + /// + /// **Note:** While the results are identical, [`FogFalloff::Atmospheric`] is computationally more expensive. + Atmospheric { + /// Controls how much light is removed due to atmospheric “extinction”, i.e. loss of light due to + /// photons being absorbed by atmospheric particles. + /// + /// Each component can be thought of as an independent per `R`/`G`/`B` channel `density` factor from + /// [`FogFalloff::Exponential`]: Multiplier applied to the world distance (within the fog + /// falloff calculation) for that specific channel. + /// + /// **Note:** + /// This value is not a `Color`, since it affects the channels exponentially in a non-intuitive way. + /// For artistic control, use the [`FogFalloff::from_visibility_colors()`] convenience method. + extinction: Vec3, + + /// Controls how much light is added due to light scattering from the sun through the atmosphere. + /// + /// Each component can be thought of as an independent per `R`/`G`/`B` channel `density` factor from + /// [`FogFalloff::Exponential`]: A multiplier applied to the world distance (within the fog + /// falloff calculation) for that specific channel. + /// + /// **Note:** + /// This value is not a `Color`, since it affects the channels exponentially in a non-intuitive way. + /// For artistic control, use the [`FogFalloff::from_visibility_colors()`] convenience method. + inscattering: Vec3, + }, +} + +impl FogFalloff { + /// Creates a [`FogFalloff::Exponential`] value from the given visibility distance in world units, + /// using the revised Koschmieder contrast threshold, [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`]. + pub fn from_visibility(visibility: f32) -> FogFalloff { + FogFalloff::from_visibility_contrast( + visibility, + FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD, + ) + } + + /// Creates a [`FogFalloff::Exponential`] value from the given visibility distance in world units, + /// and a given contrast threshold in the range of `0.0` to `1.0`. + pub fn from_visibility_contrast(visibility: f32, contrast_threshold: f32) -> FogFalloff { + FogFalloff::Exponential { + density: FogFalloff::koschmieder(visibility, contrast_threshold), + } + } + + /// Creates a [`FogFalloff::ExponentialSquared`] value from the given visibility distance in world units, + /// using the revised Koschmieder contrast threshold, [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`]. + pub fn from_visibility_squared(visibility: f32) -> FogFalloff { + FogFalloff::from_visibility_contrast_squared( + visibility, + FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD, + ) + } + + /// Creates a [`FogFalloff::ExponentialSquared`] value from the given visibility distance in world units, + /// and a given contrast threshold in the range of `0.0` to `1.0`. + pub fn from_visibility_contrast_squared( + visibility: f32, + contrast_threshold: f32, + ) -> FogFalloff { + FogFalloff::ExponentialSquared { + density: (FogFalloff::koschmieder(visibility, contrast_threshold) / visibility).sqrt(), + } + } + + /// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units, + /// and a shared color for both extinction and inscattering, using the revised Koschmieder contrast threshold, + /// [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`]. + pub fn from_visibility_color( + visibility: f32, + extinction_inscattering_color: Color, + ) -> FogFalloff { + FogFalloff::from_visibility_contrast_colors( + visibility, + FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD, + extinction_inscattering_color, + extinction_inscattering_color, + ) + } + + /// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units, + /// extinction and inscattering colors, using the revised Koschmieder contrast threshold, + /// [`FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD`]. + /// + /// ## Tips + /// - Alpha values of the provided colors can modulate the `extinction` and `inscattering` effects; + /// - Using an `extinction_color` of [`Color::WHITE`] or [`Color::NONE`] disables the extinction effect; + /// - Using an `inscattering_color` of [`Color::BLACK`] or [`Color::NONE`] disables the inscattering effect. + pub fn from_visibility_colors( + visibility: f32, + extinction_color: Color, + inscattering_color: Color, + ) -> FogFalloff { + FogFalloff::from_visibility_contrast_colors( + visibility, + FogFalloff::REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD, + extinction_color, + inscattering_color, + ) + } + + /// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units, + /// a contrast threshold in the range of `0.0` to `1.0`, and a shared color for both extinction and inscattering. + pub fn from_visibility_contrast_color( + visibility: f32, + contrast_threshold: f32, + extinction_inscattering_color: Color, + ) -> FogFalloff { + FogFalloff::from_visibility_contrast_colors( + visibility, + contrast_threshold, + extinction_inscattering_color, + extinction_inscattering_color, + ) + } + + /// Creates a [`FogFalloff::Atmospheric`] value from the given visibility distance in world units, + /// a contrast threshold in the range of `0.0` to `1.0`, extinction and inscattering colors. + /// + /// ## Tips + /// - Alpha values of the provided colors can modulate the `extinction` and `inscattering` effects; + /// - Using an `extinction_color` of [`Color::WHITE`] or [`Color::NONE`] disables the extinction effect; + /// - Using an `inscattering_color` of [`Color::BLACK`] or [`Color::NONE`] disables the inscattering effect. + pub fn from_visibility_contrast_colors( + visibility: f32, + contrast_threshold: f32, + extinction_color: Color, + inscattering_color: Color, + ) -> FogFalloff { + use core::f32::consts::E; + + let [r_e, g_e, b_e, a_e] = LinearRgba::from(extinction_color).to_f32_array(); + let [r_i, g_i, b_i, a_i] = LinearRgba::from(inscattering_color).to_f32_array(); + + FogFalloff::Atmospheric { + extinction: Vec3::new( + // Values are subtracted from 1.0 here to preserve the intuitive/artistic meaning of + // colors, since they're later subtracted. (e.g. by giving a blue extinction color, you + // get blue and _not_ yellow results) + ops::powf(1.0 - r_e, E), + ops::powf(1.0 - g_e, E), + ops::powf(1.0 - b_e, E), + ) * FogFalloff::koschmieder(visibility, contrast_threshold) + * ops::powf(a_e, E), + + inscattering: Vec3::new(ops::powf(r_i, E), ops::powf(g_i, E), ops::powf(b_i, E)) + * FogFalloff::koschmieder(visibility, contrast_threshold) + * ops::powf(a_i, E), + } + } + + /// A 2% contrast threshold was originally proposed by Koschmieder, being the + /// minimum visual contrast at which a human observer could detect an object. + /// We use a revised 5% contrast threshold, deemed more realistic for typical human observers. + pub const REVISED_KOSCHMIEDER_CONTRAST_THRESHOLD: f32 = 0.05; + + /// Calculates the extinction coefficient β, from V and Cₜ, where: + /// + /// - Cₜ is the contrast threshold, in the range of `0.0` to `1.0` + /// - V is the visibility distance in which a perfectly black object is still identifiable + /// against the horizon sky within the contrast threshold + /// + /// We start with Koschmieder's equation: + /// + /// ```text + /// -ln(Cₜ) + /// V = ───────── + /// β + /// ``` + /// + /// Multiplying both sides by β/V, that gives us: + /// + /// ```text + /// -ln(Cₜ) + /// β = ───────── + /// V + /// ``` + /// + /// See: + /// - + /// - + pub fn koschmieder(v: f32, c_t: f32) -> f32 { + -ops::ln(c_t) / v + } +} + +impl Default for DistanceFog { + fn default() -> Self { + DistanceFog { + color: Color::WHITE, + falloff: FogFalloff::Linear { + start: 0.0, + end: 100.0, + }, + directional_light_color: Color::NONE, + directional_light_exponent: 8.0, + } + } +} diff --git a/crates/libmarathon/src/render/pbr/light_probe/copy.wgsl b/crates/libmarathon/src/render/pbr/light_probe/copy.wgsl new file mode 100644 index 0000000..9140562 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/light_probe/copy.wgsl @@ -0,0 +1,21 @@ +// Copy the base mip (level 0) from a source cubemap to a destination cubemap, +// performing format conversion if needed (the destination is always rgba16float). +// The alpha channel is filled with 1.0. + +@group(0) @binding(0) var src_cubemap: texture_2d_array; +@group(0) @binding(1) var dst_cubemap: texture_storage_2d_array; + +@compute +@workgroup_size(8, 8, 1) +fn copy(@builtin(global_invocation_id) global_id: vec3u) { + let size = textureDimensions(src_cubemap).xy; + + // Bounds check + if (any(global_id.xy >= size)) { + return; + } + + let color = textureLoad(src_cubemap, vec2u(global_id.xy), global_id.z, 0); + + textureStore(dst_cubemap, vec2u(global_id.xy), global_id.z, vec4f(color.rgb, 1.0)); +} \ No newline at end of file diff --git a/crates/libmarathon/src/render/pbr/light_probe/downsample.wgsl b/crates/libmarathon/src/render/pbr/light_probe/downsample.wgsl new file mode 100644 index 0000000..e28c9b2 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/light_probe/downsample.wgsl @@ -0,0 +1,439 @@ +// Single pass downsampling shader for creating the mip chain for an array texture +// Ported from https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK/blob/c16b1d286b5b438b75da159ab51ff426bacea3d1/sdk/include/FidelityFX/gpu/spd/ffx_spd.h + +@group(0) @binding(0) var sampler_linear_clamp: sampler; +@group(0) @binding(1) var constants: Constants; +#ifdef COMBINE_BIND_GROUP +@group(0) @binding(2) var mip_0: texture_2d_array; +@group(0) @binding(3) var mip_1: texture_storage_2d_array; +@group(0) @binding(4) var mip_2: texture_storage_2d_array; +@group(0) @binding(5) var mip_3: texture_storage_2d_array; +@group(0) @binding(6) var mip_4: texture_storage_2d_array; +@group(0) @binding(7) var mip_5: texture_storage_2d_array; +@group(0) @binding(8) var mip_6: texture_storage_2d_array; +@group(0) @binding(9) var mip_7: texture_storage_2d_array; +@group(0) @binding(10) var mip_8: texture_storage_2d_array; +@group(0) @binding(11) var mip_9: texture_storage_2d_array; +@group(0) @binding(12) var mip_10: texture_storage_2d_array; +@group(0) @binding(13) var mip_11: texture_storage_2d_array; +@group(0) @binding(14) var mip_12: texture_storage_2d_array; +#endif + +#ifdef FIRST_PASS +@group(0) @binding(2) var mip_0: texture_2d_array; +@group(0) @binding(3) var mip_1: texture_storage_2d_array; +@group(0) @binding(4) var mip_2: texture_storage_2d_array; +@group(0) @binding(5) var mip_3: texture_storage_2d_array; +@group(0) @binding(6) var mip_4: texture_storage_2d_array; +@group(0) @binding(7) var mip_5: texture_storage_2d_array; +@group(0) @binding(8) var mip_6: texture_storage_2d_array; +#endif + +#ifdef SECOND_PASS +@group(0) @binding(2) var mip_6: texture_2d_array; +@group(0) @binding(3) var mip_7: texture_storage_2d_array; +@group(0) @binding(4) var mip_8: texture_storage_2d_array; +@group(0) @binding(5) var mip_9: texture_storage_2d_array; +@group(0) @binding(6) var mip_10: texture_storage_2d_array; +@group(0) @binding(7) var mip_11: texture_storage_2d_array; +@group(0) @binding(8) var mip_12: texture_storage_2d_array; +#endif + +struct Constants { mips: u32, inverse_input_size: vec2f } + +var spd_intermediate_r: array, 16>; +var spd_intermediate_g: array, 16>; +var spd_intermediate_b: array, 16>; +var spd_intermediate_a: array, 16>; + +@compute +@workgroup_size(256, 1, 1) +fn downsample_first( + @builtin(workgroup_id) workgroup_id: vec3u, + @builtin(local_invocation_index) local_invocation_index: u32 +) { + + let sub_xy = remap_for_wave_reduction(local_invocation_index % 64u); + let x = sub_xy.x + 8u * ((local_invocation_index >> 6u) % 2u); + let y = sub_xy.y + 8u * (local_invocation_index >> 7u); + + spd_downsample_mips_0_1(x, y, workgroup_id.xy, local_invocation_index, constants.mips, workgroup_id.z); + + spd_downsample_next_four(x, y, workgroup_id.xy, local_invocation_index, 2u, constants.mips, workgroup_id.z); +} + +// TODO: Once wgpu supports globallycoherent buffers, make it actually a single pass +@compute +@workgroup_size(256, 1, 1) +fn downsample_second( + @builtin(workgroup_id) workgroup_id: vec3u, + @builtin(local_invocation_index) local_invocation_index: u32, +) { + let sub_xy = remap_for_wave_reduction(local_invocation_index % 64u); + let x = sub_xy.x + 8u * ((local_invocation_index >> 6u) % 2u); + let y = sub_xy.y + 8u * (local_invocation_index >> 7u); + + spd_downsample_mips_6_7(x, y, constants.mips, workgroup_id.z); + + spd_downsample_next_four(x, y, vec2(0u), local_invocation_index, 8u, constants.mips, workgroup_id.z); +} + +fn spd_downsample_mips_0_1(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32, mips: u32, slice: u32) { + var v: array; + + var tex = (workgroup_id * 64u) + vec2(x * 2u, y * 2u); + var pix = (workgroup_id * 32u) + vec2(x, y); + v[0] = spd_reduce_load_source_image(tex, slice); + spd_store(pix, v[0], 0u, slice); + + tex = (workgroup_id * 64u) + vec2(x * 2u + 32u, y * 2u); + pix = (workgroup_id * 32u) + vec2(x + 16u, y); + v[1] = spd_reduce_load_source_image(tex, slice); + spd_store(pix, v[1], 0u, slice); + + tex = (workgroup_id * 64u) + vec2(x * 2u, y * 2u + 32u); + pix = (workgroup_id * 32u) + vec2(x, y + 16u); + v[2] = spd_reduce_load_source_image(tex, slice); + spd_store(pix, v[2], 0u, slice); + + tex = (workgroup_id * 64u) + vec2(x * 2u + 32u, y * 2u + 32u); + pix = (workgroup_id * 32u) + vec2(x + 16u, y + 16u); + v[3] = spd_reduce_load_source_image(tex, slice); + spd_store(pix, v[3], 0u, slice); + + if mips <= 1u { return; } + +#ifdef SUBGROUP_SUPPORT + v[0] = spd_reduce_quad(v[0]); + v[1] = spd_reduce_quad(v[1]); + v[2] = spd_reduce_quad(v[2]); + v[3] = spd_reduce_quad(v[3]); + + if local_invocation_index % 4u == 0u { + spd_store((workgroup_id * 16u) + vec2(x / 2u, y / 2u), v[0], 1u, slice); + spd_store_intermediate(x / 2u, y / 2u, v[0]); + + spd_store((workgroup_id * 16u) + vec2(x / 2u + 8u, y / 2u), v[1], 1u, slice); + spd_store_intermediate(x / 2u + 8u, y / 2u, v[1]); + + spd_store((workgroup_id * 16u) + vec2(x / 2u, y / 2u + 8u), v[2], 1u, slice); + spd_store_intermediate(x / 2u, y / 2u + 8u, v[2]); + + spd_store((workgroup_id * 16u) + vec2(x / 2u + 8u, y / 2u + 8u), v[3], 1u, slice); + spd_store_intermediate(x / 2u + 8u, y / 2u + 8u, v[3]); + } +#else + for (var i = 0u; i < 4u; i++) { + spd_store_intermediate(x, y, v[i]); + workgroupBarrier(); + if local_invocation_index < 64u { + v[i] = spd_reduce_intermediate( + vec2(x * 2u + 0u, y * 2u + 0u), + vec2(x * 2u + 1u, y * 2u + 0u), + vec2(x * 2u + 0u, y * 2u + 1u), + vec2(x * 2u + 1u, y * 2u + 1u), + ); + spd_store(vec2(workgroup_id * 16) + vec2(x + (i % 2u) * 8u, y + (i / 2u) * 8u), v[i], 1u, slice); + } + workgroupBarrier(); + } + + if local_invocation_index < 64u { + spd_store_intermediate(x + 0u, y + 0u, v[0]); + spd_store_intermediate(x + 8u, y + 0u, v[1]); + spd_store_intermediate(x + 0u, y + 8u, v[2]); + spd_store_intermediate(x + 8u, y + 8u, v[3]); + } +#endif +} + +fn spd_downsample_next_four(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32, base_mip: u32, mips: u32, slice: u32) { + if mips <= base_mip { return; } + workgroupBarrier(); + spd_downsample_mip_2(x, y, workgroup_id, local_invocation_index, base_mip, slice); + + if mips <= base_mip + 1u { return; } + workgroupBarrier(); + spd_downsample_mip_3(x, y, workgroup_id, local_invocation_index, base_mip + 1u, slice); + + if mips <= base_mip + 2u { return; } + workgroupBarrier(); + spd_downsample_mip_4(x, y, workgroup_id, local_invocation_index, base_mip + 2u, slice); + + if mips <= base_mip + 3u { return; } + workgroupBarrier(); + spd_downsample_mip_5(x, y, workgroup_id, local_invocation_index, base_mip + 3u, slice); +} + +fn spd_downsample_mip_2(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32, base_mip: u32, slice: u32) { +#ifdef SUBGROUP_SUPPORT + var v = spd_load_intermediate(x, y); + v = spd_reduce_quad(v); + if local_invocation_index % 4u == 0u { + spd_store((workgroup_id * 8u) + vec2(x / 2u, y / 2u), v, base_mip, slice); + spd_store_intermediate(x + (y / 2u) % 2u, y, v); + } +#else + if local_invocation_index < 64u { + let v = spd_reduce_intermediate( + vec2(x * 2u + 0u, y * 2u + 0u), + vec2(x * 2u + 1u, y * 2u + 0u), + vec2(x * 2u + 0u, y * 2u + 1u), + vec2(x * 2u + 1u, y * 2u + 1u), + ); + spd_store((workgroup_id * 8u) + vec2(x, y), v, base_mip, slice); + spd_store_intermediate(x * 2u + y % 2u, y * 2u, v); + } +#endif +} + +fn spd_downsample_mip_3(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32, base_mip: u32, slice: u32) { +#ifdef SUBGROUP_SUPPORT + if local_invocation_index < 64u { + var v = spd_load_intermediate(x * 2u + y % 2u, y * 2u); + v = spd_reduce_quad(v); + if local_invocation_index % 4u == 0u { + spd_store((workgroup_id * 4u) + vec2(x / 2u, y / 2u), v, base_mip, slice); + spd_store_intermediate(x * 2u + y / 2u, y * 2u, v); + } + } +#else + if local_invocation_index < 16u { + let v = spd_reduce_intermediate( + vec2(x * 4u + 0u + 0u, y * 4u + 0u), + vec2(x * 4u + 2u + 0u, y * 4u + 0u), + vec2(x * 4u + 0u + 1u, y * 4u + 2u), + vec2(x * 4u + 2u + 1u, y * 4u + 2u), + ); + spd_store((workgroup_id * 4u) + vec2(x, y), v, base_mip, slice); + spd_store_intermediate(x * 4u + y, y * 4u, v); + } +#endif +} + +fn spd_downsample_mip_4(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32, base_mip: u32, slice: u32) { +#ifdef SUBGROUP_SUPPORT + if local_invocation_index < 16u { + var v = spd_load_intermediate(x * 4u + y, y * 4u); + v = spd_reduce_quad(v); + if local_invocation_index % 4u == 0u { + spd_store((workgroup_id * 2u) + vec2(x / 2u, y / 2u), v, base_mip, slice); + spd_store_intermediate(x / 2u + y, 0u, v); + } + } +#else + if local_invocation_index < 4u { + let v = spd_reduce_intermediate( + vec2(x * 8u + 0u + 0u + y * 2u, y * 8u + 0u), + vec2(x * 8u + 4u + 0u + y * 2u, y * 8u + 0u), + vec2(x * 8u + 0u + 1u + y * 2u, y * 8u + 4u), + vec2(x * 8u + 4u + 1u + y * 2u, y * 8u + 4u), + ); + spd_store((workgroup_id * 2u) + vec2(x, y), v, base_mip, slice); + spd_store_intermediate(x + y * 2u, 0u, v); + } +#endif +} + +fn spd_downsample_mip_5(x: u32, y: u32, workgroup_id: vec2u, local_invocation_index: u32, base_mip: u32, slice: u32) { +#ifdef SUBGROUP_SUPPORT + if local_invocation_index < 4u { + var v = spd_load_intermediate(local_invocation_index, 0u); + v = spd_reduce_quad(v); + if local_invocation_index % 4u == 0u { + spd_store(workgroup_id, v, base_mip, slice); + } + } +#else + if local_invocation_index < 1u { + let v = spd_reduce_intermediate(vec2(0u, 0u), vec2(1u, 0u), vec2(2u, 0u), vec2(3u, 0u)); + spd_store(workgroup_id, v, base_mip, slice); + } +#endif +} + +fn spd_downsample_mips_6_7(x: u32, y: u32, mips: u32, slice: u32) { + var tex = vec2(x * 4u + 0u, y * 4u + 0u); + var pix = vec2(x * 2u + 0u, y * 2u + 0u); + let v0 = spd_reduce_load_4( + vec2(x * 4u + 0u, y * 4u + 0u), + vec2(x * 4u + 1u, y * 4u + 0u), + vec2(x * 4u + 0u, y * 4u + 1u), + vec2(x * 4u + 1u, y * 4u + 1u), + slice + ); + spd_store(pix, v0, 6u, slice); + + tex = vec2(x * 4u + 2u, y * 4u + 0u); + pix = vec2(x * 2u + 1u, y * 2u + 0u); + let v1 = spd_reduce_load_4( + vec2(x * 4u + 2u, y * 4u + 0u), + vec2(x * 4u + 3u, y * 4u + 0u), + vec2(x * 4u + 2u, y * 4u + 1u), + vec2(x * 4u + 3u, y * 4u + 1u), + slice + ); + spd_store(pix, v1, 6u, slice); + + tex = vec2(x * 4u + 0u, y * 4u + 2u); + pix = vec2(x * 2u + 0u, y * 2u + 1u); + let v2 = spd_reduce_load_4( + vec2(x * 4u + 0u, y * 4u + 2u), + vec2(x * 4u + 1u, y * 4u + 2u), + vec2(x * 4u + 0u, y * 4u + 3u), + vec2(x * 4u + 1u, y * 4u + 3u), + slice + ); + spd_store(pix, v2, 6u, slice); + + tex = vec2(x * 4u + 2u, y * 4u + 2u); + pix = vec2(x * 2u + 1u, y * 2u + 1u); + let v3 = spd_reduce_load_4( + vec2(x * 4u + 2u, y * 4u + 2u), + vec2(x * 4u + 3u, y * 4u + 2u), + vec2(x * 4u + 2u, y * 4u + 3u), + vec2(x * 4u + 3u, y * 4u + 3u), + slice + ); + spd_store(pix, v3, 6u, slice); + + if mips < 7u { return; } + + let v = spd_reduce_4(v0, v1, v2, v3); + spd_store(vec2(x, y), v, 7u, slice); + spd_store_intermediate(x, y, v); +} + +fn remap_for_wave_reduction(a: u32) -> vec2u { + // This function maps linear thread IDs to 2D coordinates in a special pattern + // to ensure that neighboring threads process neighboring pixels + // For example, this transforms linear thread IDs 0,1,2,3 into a 2×2 square + + // Extract bits to form the X and Y coordinates + let x = insertBits(extractBits(a, 2u, 3u), a, 0u, 1u); + let y = insertBits(extractBits(a, 3u, 3u), extractBits(a, 1u, 2u), 0u, 2u); + + return vec2u(x, y); +} + +fn spd_reduce_load_source_image(uv: vec2u, slice: u32) -> vec4f { + let texture_coord = (vec2f(uv) + 0.5) * constants.inverse_input_size; + + #ifdef COMBINE_BIND_GROUP + let result = textureSampleLevel(mip_0, sampler_linear_clamp, texture_coord, slice, 0.0); + #endif + #ifdef FIRST_PASS + let result = textureSampleLevel(mip_0, sampler_linear_clamp, texture_coord, slice, 0.0); + #endif + #ifdef SECOND_PASS + let result = textureSampleLevel(mip_6, sampler_linear_clamp, texture_coord, slice, 0.0); + #endif + +#ifdef SRGB_CONVERSION + return vec4( + srgb_from_linear(result.r), + srgb_from_linear(result.g), + srgb_from_linear(result.b), + result.a + ); +#else + return result; +#endif + +} + +fn spd_store(pix: vec2u, value: vec4f, mip: u32, slice: u32) { + if mip >= constants.mips { return; } + switch mip { + #ifdef COMBINE_BIND_GROUP + case 0u: { textureStore(mip_1, pix, slice, value); } + case 1u: { textureStore(mip_2, pix, slice, value); } + case 2u: { textureStore(mip_3, pix, slice, value); } + case 3u: { textureStore(mip_4, pix, slice, value); } + case 4u: { textureStore(mip_5, pix, slice, value); } + case 5u: { textureStore(mip_6, pix, slice, value); } + case 6u: { textureStore(mip_7, pix, slice, value); } + case 7u: { textureStore(mip_8, pix, slice, value); } + case 8u: { textureStore(mip_9, pix, slice, value); } + case 9u: { textureStore(mip_10, pix, slice, value); } + case 10u: { textureStore(mip_11, pix, slice, value); } + case 11u: { textureStore(mip_12, pix, slice, value); } + #endif + #ifdef FIRST_PASS + case 0u: { textureStore(mip_1, pix, slice, value); } + case 1u: { textureStore(mip_2, pix, slice, value); } + case 2u: { textureStore(mip_3, pix, slice, value); } + case 3u: { textureStore(mip_4, pix, slice, value); } + case 4u: { textureStore(mip_5, pix, slice, value); } + case 5u: { textureStore(mip_6, pix, slice, value); } + #endif + #ifdef SECOND_PASS + case 6u: { textureStore(mip_7, pix, slice, value); } + case 7u: { textureStore(mip_8, pix, slice, value); } + case 8u: { textureStore(mip_9, pix, slice, value); } + case 9u: { textureStore(mip_10, pix, slice, value); } + case 10u: { textureStore(mip_11, pix, slice, value); } + case 11u: { textureStore(mip_12, pix, slice, value); } + #endif + default: {} + } +} + +fn spd_store_intermediate(x: u32, y: u32, value: vec4f) { + spd_intermediate_r[x][y] = value.x; + spd_intermediate_g[x][y] = value.y; + spd_intermediate_b[x][y] = value.z; + spd_intermediate_a[x][y] = value.w; +} + +fn spd_load_intermediate(x: u32, y: u32) -> vec4f { + return vec4(spd_intermediate_r[x][y], spd_intermediate_g[x][y], spd_intermediate_b[x][y], spd_intermediate_a[x][y]); +} + +fn spd_reduce_intermediate(i0: vec2u, i1: vec2u, i2: vec2u, i3: vec2u) -> vec4f { + let v0 = spd_load_intermediate(i0.x, i0.y); + let v1 = spd_load_intermediate(i1.x, i1.y); + let v2 = spd_load_intermediate(i2.x, i2.y); + let v3 = spd_load_intermediate(i3.x, i3.y); + return spd_reduce_4(v0, v1, v2, v3); +} + +fn spd_reduce_load_4(i0: vec2u, i1: vec2u, i2: vec2u, i3: vec2u, slice: u32) -> vec4f { + #ifdef COMBINE_BIND_GROUP + let v0 = textureLoad(mip_6, i0, slice); + let v1 = textureLoad(mip_6, i1, slice); + let v2 = textureLoad(mip_6, i2, slice); + let v3 = textureLoad(mip_6, i3, slice); + return spd_reduce_4(v0, v1, v2, v3); + #endif + #ifdef FIRST_PASS + return vec4(0.0, 0.0, 0.0, 0.0); + #endif + #ifdef SECOND_PASS + let v0 = textureLoad(mip_6, i0, slice, 0); + let v1 = textureLoad(mip_6, i1, slice, 0); + let v2 = textureLoad(mip_6, i2, slice, 0); + let v3 = textureLoad(mip_6, i3, slice, 0); + return spd_reduce_4(v0, v1, v2, v3); + #endif +} + +fn spd_reduce_4(v0: vec4f, v1: vec4f, v2: vec4f, v3: vec4f) -> vec4f { + return (v0 + v1 + v2 + v3) * 0.25; +} + +#ifdef SUBGROUP_SUPPORT +fn spd_reduce_quad(v: vec4f) -> vec4f { + let v0 = v; + let v1 = quadSwapX(v); + let v2 = quadSwapY(v); + let v3 = quadSwapDiagonal(v); + return spd_reduce_4(v0, v1, v2, v3); +} +#endif + +fn srgb_from_linear(value: f32) -> f32 { + let j = vec3(0.0031308 * 12.92, 12.92, 1.0 / 2.4); + let k = vec2(1.055, -0.055); + return clamp(j.x, value * j.y, pow(value, j.z) * k.x + k.y); +} \ No newline at end of file diff --git a/crates/libmarathon/src/render/pbr/light_probe/environment_filter.wgsl b/crates/libmarathon/src/render/pbr/light_probe/environment_filter.wgsl new file mode 100644 index 0000000..7b24b76 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/light_probe/environment_filter.wgsl @@ -0,0 +1,185 @@ +#import bevy_render::maths::PI +#import bevy_pbr::{ + lighting, + utils::{sample_cosine_hemisphere, dir_to_cube_uv, sample_cube_dir, hammersley_2d, rand_vec2f} +} + +struct FilteringConstants { + mip_level: f32, + sample_count: u32, + roughness: f32, + noise_size_bits: vec2u, +} + +@group(0) @binding(0) var input_texture: texture_2d_array; +@group(0) @binding(1) var input_sampler: sampler; +@group(0) @binding(2) var output_texture: texture_storage_2d_array; +@group(0) @binding(3) var constants: FilteringConstants; +@group(0) @binding(4) var blue_noise_texture: texture_2d_array; + +// Sample an environment map with a specific LOD +fn sample_environment(dir: vec3f, level: f32) -> vec4f { + let cube_uv = dir_to_cube_uv(dir); + return textureSampleLevel(input_texture, input_sampler, cube_uv.uv, cube_uv.face, level); +} + +// Blue noise randomization +#ifdef HAS_BLUE_NOISE +fn sample_noise(pixel_coords: vec2u) -> vec4f { + let noise_size = vec2u(1) << constants.noise_size_bits; + let noise_size_mask = noise_size - vec2u(1u); + let noise_coords = pixel_coords & noise_size_mask; + let uv = vec2f(noise_coords) / vec2f(noise_size); + return textureSampleLevel(blue_noise_texture, input_sampler, uv, 0u, 0.0); +} +#else +// pseudo-random numbers using RNG +fn sample_noise(pixel_coords: vec2u) -> vec4f { + var rng_state: u32 = (pixel_coords.x * 3966231743u) ^ (pixel_coords.y * 3928936651u); + let rnd = rand_vec2f(&rng_state); + return vec4f(rnd, 0.0, 0.0); +} +#endif + +// Calculate LOD for environment map lookup using filtered importance sampling +fn calculate_environment_map_lod(pdf: f32, width: f32, samples: f32) -> f32 { + // Solid angle of current sample + let omega_s = 1.0 / (samples * pdf); + + // Solid angle of a texel in the environment map + let omega_p = 4.0 * PI / (6.0 * width * width); + + // Filtered importance sampling: compute the correct LOD + return 0.5 * log2(omega_s / omega_p); +} + +@compute +@workgroup_size(8, 8, 1) +fn generate_radiance_map(@builtin(global_invocation_id) global_id: vec3u) { + let size = textureDimensions(output_texture).xy; + let invSize = 1.0 / vec2f(size); + + let coords = vec2u(global_id.xy); + let face = global_id.z; + + if (any(coords >= size)) { + return; + } + + // Convert texture coordinates to direction vector + let uv = (vec2f(coords) + 0.5) * invSize; + let normal = sample_cube_dir(uv, face); + + // For radiance map, view direction = normal for perfect reflection + let view = normal; + + // Convert perceptual roughness to physical microfacet roughness + let perceptual_roughness = constants.roughness; + let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); + + // Get blue noise offset for stratification + let vector_noise = sample_noise(coords); + + var radiance = vec3f(0.0); + var total_weight = 0.0; + + // Skip sampling for mirror reflection (roughness = 0) + if (roughness < 0.01) { + radiance = sample_environment(normal, 0.0).rgb; + textureStore(output_texture, coords, face, vec4f(radiance, 1.0)); + return; + } + + // For higher roughness values, use importance sampling + let sample_count = constants.sample_count; + + for (var i = 0u; i < sample_count; i++) { + // Get sample coordinates from Hammersley sequence with blue noise offset + var xi = hammersley_2d(i, sample_count); + xi = fract(xi + vector_noise.rg); // Apply Cranley-Patterson rotation + + // Sample the GGX distribution with the spherical-cap VNDF method + let light_dir = lighting::sample_visible_ggx(xi, roughness, normal, view); + + // Calculate weight (N·L) + let NdotL = dot(normal, light_dir); + + if (NdotL > 0.0) { + // Reconstruct the microfacet half-vector from view and light and compute PDF terms + let half_vector = normalize(view + light_dir); + let NdotH = dot(normal, half_vector); + let NdotV = dot(normal, view); + + // Get the geometric shadowing term + let G = lighting::G_Smith(NdotV, NdotL, roughness); + + // PDF that matches the bounded-VNDF sampling + let pdf = lighting::ggx_vndf_pdf(view, NdotH, roughness); + + // Calculate LOD using filtered importance sampling + // This is crucial to avoid fireflies and improve quality + let width = f32(size.x); + let lod = calculate_environment_map_lod(pdf, width, f32(sample_count)); + + // Get source mip level - ensure we don't go negative + let source_mip = max(0.0, lod); + + // Sample environment map with the light direction + var sample_color = sample_environment(light_dir, source_mip).rgb; + + // Accumulate weighted sample, including geometric term + radiance += sample_color * NdotL * G; + total_weight += NdotL * G; + } + } + + // Normalize by total weight + if (total_weight > 0.0) { + radiance = radiance / total_weight; + } + + // Write result to output texture + textureStore(output_texture, coords, face, vec4f(radiance, 1.0)); +} + +@compute +@workgroup_size(8, 8, 1) +fn generate_irradiance_map(@builtin(global_invocation_id) global_id: vec3u) { + let size = textureDimensions(output_texture).xy; + let invSize = 1.0 / vec2f(size); + + let coords = vec2u(global_id.xy); + let face = global_id.z; + + if (any(coords >= size)) { + return; + } + + // Convert texture coordinates to direction vector + let uv = (vec2f(coords) + 0.5) * invSize; + let normal = sample_cube_dir(uv, face); + + var irradiance = vec3f(0.0); + + // Use uniform sampling on a hemisphere + for (var i = 0u; i < constants.sample_count; i++) { + // Build a deterministic RNG seed for this pixel / sample + // 4 randomly chosen 32-bit primes + var rng: u32 = (coords.x * 2131358057u) ^ (coords.y * 3416869721u) ^ (face * 1199786941u) ^ (i * 566200673u); + + // Sample a direction from the upper hemisphere around the normal + var sample_dir = sample_cosine_hemisphere(normal, &rng); + + // Sample environment with level 0 (no mip) + var sample_color = sample_environment(sample_dir, 0.0).rgb; + + // Accumulate the contribution + irradiance += sample_color; + } + + // Normalize by number of samples (cosine-weighted sampling already accounts for PDF) + irradiance = irradiance / f32(constants.sample_count); + + // Write result to output texture + textureStore(output_texture, coords, face, vec4f(irradiance, 1.0)); +} diff --git a/crates/libmarathon/src/render/pbr/light_probe/environment_map.rs b/crates/libmarathon/src/render/pbr/light_probe/environment_map.rs new file mode 100644 index 0000000..e99cb8b --- /dev/null +++ b/crates/libmarathon/src/render/pbr/light_probe/environment_map.rs @@ -0,0 +1,310 @@ +//! Environment maps and reflection probes. +//! +//! An *environment map* consists of a pair of diffuse and specular cubemaps +//! that together reflect the static surrounding area of a region in space. When +//! available, the PBR shader uses these to apply diffuse light and calculate +//! specular reflections. +//! +//! Environment maps come in two flavors, depending on what other components the +//! entities they're attached to have: +//! +//! 1. If attached to a view, they represent the objects located a very far +//! distance from the view, in a similar manner to a skybox. Essentially, these +//! *view environment maps* represent a higher-quality replacement for +//! [`AmbientLight`](bevy_light::AmbientLight) for outdoor scenes. The indirect light from such +//! environment maps are added to every point of the scene, including +//! interior enclosed areas. +//! +//! 2. If attached to a [`bevy_light::LightProbe`], environment maps represent the immediate +//! surroundings of a specific location in the scene. These types of +//! environment maps are known as *reflection probes*. +//! +//! Typically, environment maps are static (i.e. "baked", calculated ahead of +//! time) and so only reflect fixed static geometry. The environment maps must +//! be pre-filtered into a pair of cubemaps, one for the diffuse component and +//! one for the specular component, according to the [split-sum approximation]. +//! To pre-filter your environment map, you can use the [glTF IBL Sampler] or +//! its [artist-friendly UI]. The diffuse map uses the Lambertian distribution, +//! while the specular map uses the GGX distribution. +//! +//! The Khronos Group has [several pre-filtered environment maps] available for +//! you to use. +//! +//! Currently, reflection probes (i.e. environment maps attached to light +//! probes) use binding arrays (also known as bindless textures) and +//! consequently aren't supported on WebGL2 or WebGPU. Reflection probes are +//! also unsupported if GLSL is in use, due to `naga` limitations. Environment +//! maps attached to views are, however, supported on all platforms. +//! +//! [split-sum approximation]: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf +//! +//! [glTF IBL Sampler]: https://github.com/KhronosGroup/glTF-IBL-Sampler +//! +//! [artist-friendly UI]: https://github.com/pcwalton/gltf-ibl-sampler-egui +//! +//! [several pre-filtered environment maps]: https://github.com/KhronosGroup/glTF-Sample-Environments + +use bevy_asset::AssetId; +use bevy_ecs::{query::QueryItem, system::lifetimeless::Read}; +use bevy_image::Image; +use bevy_light::EnvironmentMapLight; +use crate::render::{ + extract_instances::ExtractInstance, + render_asset::RenderAssets, + render_resource::{ + binding_types::{self, uniform_buffer}, + BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, ShaderStages, TextureSampleType, + TextureView, + }, + renderer::{RenderAdapter, RenderDevice}, + texture::{FallbackImage, GpuImage}, +}; + +use core::{num::NonZero, ops::Deref}; + +use crate::render::pbr::{ + add_cubemap_texture_view, binding_arrays_are_usable, EnvironmentMapUniform, + MAX_VIEW_LIGHT_PROBES, +}; + +use super::{LightProbeComponent, RenderViewLightProbes}; + +/// Like [`EnvironmentMapLight`], but contains asset IDs instead of handles. +/// +/// This is for use in the render app. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct EnvironmentMapIds { + /// The blurry image that represents diffuse radiance surrounding a region. + pub(crate) diffuse: AssetId, + /// The typically-sharper, mipmapped image that represents specular radiance + /// surrounding a region. + pub(crate) specular: AssetId, +} + +/// All the bind group entries necessary for PBR shaders to access the +/// environment maps exposed to a view. +pub(crate) enum RenderViewEnvironmentMapBindGroupEntries<'a> { + /// The version used when binding arrays aren't available on the current + /// platform. + Single { + /// The texture view of the view's diffuse cubemap. + diffuse_texture_view: &'a TextureView, + + /// The texture view of the view's specular cubemap. + specular_texture_view: &'a TextureView, + + /// The sampler used to sample elements of both `diffuse_texture_views` and + /// `specular_texture_views`. + sampler: &'a Sampler, + }, + + /// The version used when binding arrays are available on the current + /// platform. + Multiple { + /// A texture view of each diffuse cubemap, in the same order that they are + /// supplied to the view (i.e. in the same order as + /// `binding_index_to_cubemap` in [`RenderViewLightProbes`]). + /// + /// This is a vector of `wgpu::TextureView`s. But we don't want to import + /// `wgpu` in this crate, so we refer to it indirectly like this. + diffuse_texture_views: Vec<&'a ::Target>, + + /// As above, but for specular cubemaps. + specular_texture_views: Vec<&'a ::Target>, + + /// The sampler used to sample elements of both `diffuse_texture_views` and + /// `specular_texture_views`. + sampler: &'a Sampler, + }, +} + +/// Information about the environment map attached to the view, if any. This is +/// a global environment map that lights everything visible in the view, as +/// opposed to a light probe which affects only a specific area. +pub struct EnvironmentMapViewLightProbeInfo { + /// The index of the diffuse and specular cubemaps in the binding arrays. + pub(crate) cubemap_index: i32, + /// The smallest mip level of the specular cubemap. + pub(crate) smallest_specular_mip_level: u32, + /// The scale factor applied to the diffuse and specular light in the + /// cubemap. This is in units of cd/m² (candela per square meter). + pub(crate) intensity: f32, + /// Whether this lightmap affects the diffuse lighting of lightmapped + /// meshes. + pub(crate) affects_lightmapped_mesh_diffuse: bool, +} + +impl ExtractInstance for EnvironmentMapIds { + type QueryData = Read; + + type QueryFilter = (); + + fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option { + Some(EnvironmentMapIds { + diffuse: item.diffuse_map.id(), + specular: item.specular_map.id(), + }) + } +} + +/// Returns the bind group layout entries for the environment map diffuse and +/// specular binding arrays respectively, in addition to the sampler. +pub(crate) fn get_bind_group_layout_entries( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, +) -> [BindGroupLayoutEntryBuilder; 4] { + let mut texture_cube_binding = + binding_types::texture_cube(TextureSampleType::Float { filterable: true }); + if binding_arrays_are_usable(render_device, render_adapter) { + texture_cube_binding = + texture_cube_binding.count(NonZero::::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); + } + + [ + texture_cube_binding, + texture_cube_binding, + binding_types::sampler(SamplerBindingType::Filtering), + uniform_buffer::(true).visibility(ShaderStages::FRAGMENT), + ] +} + +impl<'a> RenderViewEnvironmentMapBindGroupEntries<'a> { + /// Looks up and returns the bindings for the environment map diffuse and + /// specular binding arrays respectively, as well as the sampler. + pub(crate) fn get( + render_view_environment_maps: Option<&RenderViewLightProbes>, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, + render_device: &RenderDevice, + render_adapter: &RenderAdapter, + ) -> RenderViewEnvironmentMapBindGroupEntries<'a> { + if binding_arrays_are_usable(render_device, render_adapter) { + let mut diffuse_texture_views = vec![]; + let mut specular_texture_views = vec![]; + let mut sampler = None; + + if let Some(environment_maps) = render_view_environment_maps { + for &cubemap_id in &environment_maps.binding_index_to_textures { + add_cubemap_texture_view( + &mut diffuse_texture_views, + &mut sampler, + cubemap_id.diffuse, + images, + fallback_image, + ); + add_cubemap_texture_view( + &mut specular_texture_views, + &mut sampler, + cubemap_id.specular, + images, + fallback_image, + ); + } + } + + // Pad out the bindings to the size of the binding array using fallback + // textures. This is necessary on D3D12 and Metal. + diffuse_texture_views.resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.cube.texture_view); + specular_texture_views + .resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.cube.texture_view); + + return RenderViewEnvironmentMapBindGroupEntries::Multiple { + diffuse_texture_views, + specular_texture_views, + sampler: sampler.unwrap_or(&fallback_image.cube.sampler), + }; + } + + if let Some(environment_maps) = render_view_environment_maps + && let Some(cubemap) = environment_maps.binding_index_to_textures.first() + && let (Some(diffuse_image), Some(specular_image)) = + (images.get(cubemap.diffuse), images.get(cubemap.specular)) + { + return RenderViewEnvironmentMapBindGroupEntries::Single { + diffuse_texture_view: &diffuse_image.texture_view, + specular_texture_view: &specular_image.texture_view, + sampler: &diffuse_image.sampler, + }; + } + + RenderViewEnvironmentMapBindGroupEntries::Single { + diffuse_texture_view: &fallback_image.cube.texture_view, + specular_texture_view: &fallback_image.cube.texture_view, + sampler: &fallback_image.cube.sampler, + } + } +} + +impl LightProbeComponent for EnvironmentMapLight { + type AssetId = EnvironmentMapIds; + + // Information needed to render with the environment map attached to the + // view. + type ViewLightProbeInfo = EnvironmentMapViewLightProbeInfo; + + fn id(&self, image_assets: &RenderAssets) -> Option { + if image_assets.get(&self.diffuse_map).is_none() + || image_assets.get(&self.specular_map).is_none() + { + None + } else { + Some(EnvironmentMapIds { + diffuse: self.diffuse_map.id(), + specular: self.specular_map.id(), + }) + } + } + + fn intensity(&self) -> f32 { + self.intensity + } + + fn affects_lightmapped_mesh_diffuse(&self) -> bool { + self.affects_lightmapped_mesh_diffuse + } + + fn create_render_view_light_probes( + view_component: Option<&EnvironmentMapLight>, + image_assets: &RenderAssets, + ) -> RenderViewLightProbes { + let mut render_view_light_probes = RenderViewLightProbes::new(); + + // Find the index of the cubemap associated with the view, and determine + // its smallest mip level. + if let Some(EnvironmentMapLight { + diffuse_map: diffuse_map_handle, + specular_map: specular_map_handle, + intensity, + affects_lightmapped_mesh_diffuse, + .. + }) = view_component + && let (Some(_), Some(specular_map)) = ( + image_assets.get(diffuse_map_handle), + image_assets.get(specular_map_handle), + ) + { + render_view_light_probes.view_light_probe_info = EnvironmentMapViewLightProbeInfo { + cubemap_index: render_view_light_probes.get_or_insert_cubemap(&EnvironmentMapIds { + diffuse: diffuse_map_handle.id(), + specular: specular_map_handle.id(), + }) as i32, + smallest_specular_mip_level: specular_map.mip_level_count - 1, + intensity: *intensity, + affects_lightmapped_mesh_diffuse: *affects_lightmapped_mesh_diffuse, + }; + }; + + render_view_light_probes + } +} + +impl Default for EnvironmentMapViewLightProbeInfo { + fn default() -> Self { + Self { + cubemap_index: -1, + smallest_specular_mip_level: 0, + intensity: 1.0, + affects_lightmapped_mesh_diffuse: true, + } + } +} diff --git a/crates/libmarathon/src/render/pbr/light_probe/environment_map.wgsl b/crates/libmarathon/src/render/pbr/light_probe/environment_map.wgsl new file mode 100644 index 0000000..e670964 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/light_probe/environment_map.wgsl @@ -0,0 +1,279 @@ +#define_import_path bevy_pbr::environment_map + +#import bevy_pbr::light_probe::query_light_probe +#import bevy_pbr::mesh_view_bindings as bindings +#import bevy_pbr::mesh_view_bindings::light_probes +#import bevy_pbr::mesh_view_bindings::environment_map_uniform +#import bevy_pbr::lighting::{F_Schlick_vec, LightingInput, LayerLightingInput, LAYER_BASE, LAYER_CLEARCOAT} +#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges + +struct EnvironmentMapLight { + diffuse: vec3, + specular: vec3, +}; + +struct EnvironmentMapRadiances { + irradiance: vec3, + radiance: vec3, +} + +// Define two versions of this function, one for the case in which there are +// multiple light probes and one for the case in which only the view light probe +// is present. + +#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY + +fn compute_radiances( + input: LayerLightingInput, + clusterable_object_index_ranges: ptr, + world_position: vec3, + found_diffuse_indirect: bool, +) -> EnvironmentMapRadiances { + // Unpack. + let N = input.N; + let R = input.R; + let perceptual_roughness = input.perceptual_roughness; + let roughness = input.roughness; + + var radiances: EnvironmentMapRadiances; + + // Search for a reflection probe that contains the fragment. + var query_result = query_light_probe( + world_position, + /*is_irradiance_volume=*/ false, + clusterable_object_index_ranges, + ); + + // If we didn't find a reflection probe, use the view environment map if applicable. + if (query_result.texture_index < 0) { + query_result.texture_index = light_probes.view_cubemap_index; + query_result.intensity = light_probes.intensity_for_view; + query_result.affects_lightmapped_mesh_diffuse = + light_probes.view_environment_map_affects_lightmapped_mesh_diffuse != 0u; + } + + // If there's no cubemap, bail out. + if (query_result.texture_index < 0) { + radiances.irradiance = vec3(0.0); + radiances.radiance = vec3(0.0); + return radiances; + } + + // Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf + let radiance_level = perceptual_roughness * f32(textureNumLevels( + bindings::specular_environment_maps[query_result.texture_index]) - 1u); + + // If we're lightmapped, and we shouldn't accumulate diffuse light from the + // environment map, note that. + var enable_diffuse = !found_diffuse_indirect; +#ifdef LIGHTMAP + enable_diffuse = enable_diffuse && query_result.affects_lightmapped_mesh_diffuse; +#endif // LIGHTMAP + + if (enable_diffuse) { + var irradiance_sample_dir = N; + // Rotating the world space ray direction by the environment light map transform matrix, it is + // equivalent to rotating the diffuse environment cubemap itself. + irradiance_sample_dir = (environment_map_uniform.transform * vec4(irradiance_sample_dir, 1.0)).xyz; + // Cube maps are left-handed so we negate the z coordinate. + irradiance_sample_dir.z = -irradiance_sample_dir.z; + radiances.irradiance = textureSampleLevel( + bindings::diffuse_environment_maps[query_result.texture_index], + bindings::environment_map_sampler, + irradiance_sample_dir, + 0.0).rgb * query_result.intensity; + } + + var radiance_sample_dir = radiance_sample_direction(N, R, roughness); + // Rotating the world space ray direction by the environment light map transform matrix, it is + // equivalent to rotating the specular environment cubemap itself. + radiance_sample_dir = (environment_map_uniform.transform * vec4(radiance_sample_dir, 1.0)).xyz; + // Cube maps are left-handed so we negate the z coordinate. + radiance_sample_dir.z = -radiance_sample_dir.z; + radiances.radiance = textureSampleLevel( + bindings::specular_environment_maps[query_result.texture_index], + bindings::environment_map_sampler, + radiance_sample_dir, + radiance_level).rgb * query_result.intensity; + + return radiances; +} + +#else // MULTIPLE_LIGHT_PROBES_IN_ARRAY + +fn compute_radiances( + input: LayerLightingInput, + clusterable_object_index_ranges: ptr, + world_position: vec3, + found_diffuse_indirect: bool, +) -> EnvironmentMapRadiances { + // Unpack. + let N = input.N; + let R = input.R; + let perceptual_roughness = input.perceptual_roughness; + let roughness = input.roughness; + + var radiances: EnvironmentMapRadiances; + + if (light_probes.view_cubemap_index < 0) { + radiances.irradiance = vec3(0.0); + radiances.radiance = vec3(0.0); + return radiances; + } + + // Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf + // Technically we could use textureNumLevels(specular_environment_map) - 1 here, but we use a uniform + // because textureNumLevels() does not work on WebGL2 + let radiance_level = perceptual_roughness * f32(light_probes.smallest_specular_mip_level_for_view); + + let intensity = light_probes.intensity_for_view; + + // If we're lightmapped, and we shouldn't accumulate diffuse light from the + // environment map, note that. + var enable_diffuse = !found_diffuse_indirect; +#ifdef LIGHTMAP + enable_diffuse = enable_diffuse && + light_probes.view_environment_map_affects_lightmapped_mesh_diffuse; +#endif // LIGHTMAP + + if (enable_diffuse) { + var irradiance_sample_dir = N; + // Rotating the world space ray direction by the environment light map transform matrix, it is + // equivalent to rotating the diffuse environment cubemap itself. + irradiance_sample_dir = (environment_map_uniform.transform * vec4(irradiance_sample_dir, 1.0)).xyz; + // Cube maps are left-handed so we negate the z coordinate. + irradiance_sample_dir.z = -irradiance_sample_dir.z; + radiances.irradiance = textureSampleLevel( + bindings::diffuse_environment_map, + bindings::environment_map_sampler, + irradiance_sample_dir, + 0.0).rgb * intensity; + } + + var radiance_sample_dir = radiance_sample_direction(N, R, roughness); + // Rotating the world space ray direction by the environment light map transform matrix, it is + // equivalent to rotating the specular environment cubemap itself. + radiance_sample_dir = (environment_map_uniform.transform * vec4(radiance_sample_dir, 1.0)).xyz; + // Cube maps are left-handed so we negate the z coordinate. + radiance_sample_dir.z = -radiance_sample_dir.z; + radiances.radiance = textureSampleLevel( + bindings::specular_environment_map, + bindings::environment_map_sampler, + radiance_sample_dir, + radiance_level).rgb * intensity; + + return radiances; +} + +#endif // MULTIPLE_LIGHT_PROBES_IN_ARRAY + +#ifdef STANDARD_MATERIAL_CLEARCOAT + +// Adds the environment map light from the clearcoat layer to that of the base +// layer. +fn environment_map_light_clearcoat( + out: ptr, + input: ptr, + clusterable_object_index_ranges: ptr, + found_diffuse_indirect: bool, +) { + // Unpack. + let world_position = (*input).P; + let clearcoat_NdotV = (*input).layers[LAYER_CLEARCOAT].NdotV; + let clearcoat_strength = (*input).clearcoat_strength; + + // Calculate the Fresnel term `Fc` for the clearcoat layer. + // 0.04 is a hardcoded value for F0 from the Filament spec. + let clearcoat_F0 = vec3(0.04); + let Fc = F_Schlick_vec(clearcoat_F0, 1.0, clearcoat_NdotV) * clearcoat_strength; + let inv_Fc = 1.0 - Fc; + + let clearcoat_radiances = compute_radiances( + (*input).layers[LAYER_CLEARCOAT], + clusterable_object_index_ranges, + world_position, + found_diffuse_indirect, + ); + + // Composite the clearcoat layer on top of the existing one. + // These formulas are from Filament: + // + (*out).diffuse *= inv_Fc; + (*out).specular = (*out).specular * inv_Fc * inv_Fc + clearcoat_radiances.radiance * Fc; +} + +#endif // STANDARD_MATERIAL_CLEARCOAT + +fn environment_map_light( + input: ptr, + clusterable_object_index_ranges: ptr, + found_diffuse_indirect: bool, +) -> EnvironmentMapLight { + // Unpack. + let roughness = (*input).layers[LAYER_BASE].roughness; + let diffuse_color = (*input).diffuse_color; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let F_ab = (*input).F_ab; + let F0 = (*input).F0_; + let world_position = (*input).P; + + var out: EnvironmentMapLight; + + let radiances = compute_radiances( + (*input).layers[LAYER_BASE], + clusterable_object_index_ranges, + world_position, + found_diffuse_indirect, + ); + + if (all(radiances.irradiance == vec3(0.0)) && all(radiances.radiance == vec3(0.0))) { + out.diffuse = vec3(0.0); + out.specular = vec3(0.0); + return out; + } + + // No real world material has specular values under 0.02, so we use this range as a + // "pre-baked specular occlusion" that extinguishes the fresnel term, for artistic control. + // See: https://google.github.io/filament/Filament.html#specularocclusion + let specular_occlusion = saturate(dot(F0, vec3(50.0 * 0.33))); + + // Multiscattering approximation: https://www.jcgt.org/published/0008/01/03/paper.pdf + // Useful reference: https://bruop.github.io/ibl + let Fr = max(vec3(1.0 - roughness), F0) - F0; + let kS = F0 + Fr * pow(1.0 - NdotV, 5.0); + let Ess = F_ab.x + F_ab.y; + let FssEss = kS * Ess * specular_occlusion; + let Ems = 1.0 - Ess; + let Favg = F0 + (1.0 - F0) / 21.0; + let Fms = FssEss * Favg / (1.0 - Ems * Favg); + let FmsEms = Fms * Ems; + let Edss = 1.0 - (FssEss + FmsEms); + let kD = diffuse_color * Edss; + + if (!found_diffuse_indirect) { + out.diffuse = (FmsEms + kD) * radiances.irradiance; + } else { + out.diffuse = vec3(0.0); + } + + out.specular = FssEss * radiances.radiance; + +#ifdef STANDARD_MATERIAL_CLEARCOAT + environment_map_light_clearcoat( + &out, + input, + clusterable_object_index_ranges, + found_diffuse_indirect, + ); +#endif // STANDARD_MATERIAL_CLEARCOAT + + return out; +} + +// "Moving Frostbite to Physically Based Rendering 3.0", listing 22 +// https://seblagarde.wordpress.com/wp-content/uploads/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf#page=70 +fn radiance_sample_direction(N: vec3, R: vec3, roughness: f32) -> vec3 { + let smoothness = saturate(1.0 - roughness); + let lerp_factor = smoothness * (sqrt(smoothness) + roughness); + return mix(N, R, lerp_factor); +} diff --git a/crates/libmarathon/src/render/pbr/light_probe/generate.rs b/crates/libmarathon/src/render/pbr/light_probe/generate.rs new file mode 100644 index 0000000..2070389 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/light_probe/generate.rs @@ -0,0 +1,1186 @@ +//! Like [`EnvironmentMapLight`], but filtered in realtime from a cubemap. +//! +//! An environment map needs to be processed to be able to support uses beyond a simple skybox, +//! such as reflections, and ambient light contribution. +//! This process is called filtering, and can either be done ahead of time (prefiltering), or +//! in realtime, although at a reduced quality. Prefiltering is preferred, but not always possible: +//! sometimes you only gain access to an environment map at runtime, for whatever reason. +//! Typically this is from realtime reflection probes, but can also be from other sources. +//! +//! In any case, Bevy supports both modes of filtering. +//! This module provides realtime filtering via [`bevy_light::GeneratedEnvironmentMapLight`]. +//! For prefiltered environment maps, see [`bevy_light::EnvironmentMapLight`]. +//! These components are intended to be added to a camera. +use bevy_app::{App, Plugin, Update}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Assets, RenderAssetUsages}; +use crate::render::core_3d::graph::{Core3d, Node3d}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{QueryState, With, Without}, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{lifetimeless::Read, Commands, Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_image::Image; +use bevy_math::{Quat, UVec2, Vec2}; +use crate::render::{ + diagnostic::RecordDiagnostics, + render_asset::RenderAssets, + render_graph::{Node, NodeRunError, RenderGraphContext, RenderGraphExt, RenderLabel}, + render_resource::{ + binding_types::*, AddressMode, BindGroup, BindGroupEntries, BindGroupLayout, + BindGroupLayoutEntries, CachedComputePipelineId, ComputePassDescriptor, + ComputePipelineDescriptor, DownlevelFlags, Extent3d, FilterMode, PipelineCache, Sampler, + SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, StorageTextureAccess, + Texture, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, + TextureFormatFeatureFlags, TextureSampleType, TextureUsages, TextureView, + TextureViewDescriptor, TextureViewDimension, UniformBuffer, + }, + renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue}, + settings::WgpuFeatures, + sync_component::SyncComponentPlugin, + sync_world::RenderEntity, + texture::{CachedTexture, GpuImage, TextureCache}, + Extract, ExtractSchedule, Render, RenderApp, RenderStartup, RenderSystems, +}; + +// Implementation: generate diffuse and specular cubemaps required by PBR +// from a given high-res cubemap by +// +// 1. Copying the base mip (level 0) of the source cubemap into an intermediate +// storage texture. +// 2. Generating mipmaps using [single-pass down-sampling] (SPD). +// 3. Convolving the mip chain twice: +// * a [Lambertian convolution] for the 32 × 32 diffuse cubemap +// * a [GGX convolution], once per mip level, for the specular cubemap. +// +// [single-pass down-sampling]: https://gpuopen.com/fidelityfx-spd/ +// [Lambertian convolution]: https://bruop.github.io/ibl/#:~:text=Lambertian%20Diffuse%20Component +// [GGX convolution]: https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf + +use bevy_light::{EnvironmentMapLight, GeneratedEnvironmentMapLight}; +use bevy_shader::ShaderDefVal; +use core::cmp::min; +use tracing::info; + +use crate::render::pbr::Bluenoise; + +/// Labels for the environment map generation nodes +#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, RenderLabel)] +pub enum GeneratorNode { + Downsampling, + Filtering, +} + +/// Stores the bind group layouts for the environment map generation pipelines +#[derive(Resource)] +pub struct GeneratorBindGroupLayouts { + pub downsampling_first: BindGroupLayout, + pub downsampling_second: BindGroupLayout, + pub radiance: BindGroupLayout, + pub irradiance: BindGroupLayout, + pub copy: BindGroupLayout, +} + +/// Samplers for the environment map generation pipelines +#[derive(Resource)] +pub struct GeneratorSamplers { + pub linear: Sampler, +} + +/// Pipelines for the environment map generation pipelines +#[derive(Resource)] +pub struct GeneratorPipelines { + pub downsample_first: CachedComputePipelineId, + pub downsample_second: CachedComputePipelineId, + pub copy: CachedComputePipelineId, + pub radiance: CachedComputePipelineId, + pub irradiance: CachedComputePipelineId, +} + +/// Configuration for downsampling strategy based on device limits +#[derive(Resource, Clone, Copy, Debug, PartialEq, Eq)] +pub struct DownsamplingConfig { + // can bind ≥12 storage textures and use read-write storage textures + pub combine_bind_group: bool, +} + +pub struct EnvironmentMapGenerationPlugin; + +impl Plugin for EnvironmentMapGenerationPlugin { + fn build(&self, _: &mut App) {} + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + let adapter = render_app.world().resource::(); + let device = render_app.world().resource::(); + + // Cubemap SPD requires at least 6 storage textures + let limit_support = device.limits().max_storage_textures_per_shader_stage >= 6 + && device.limits().max_compute_workgroup_storage_size != 0 + && device.limits().max_compute_workgroup_size_x != 0; + + let downlevel_support = adapter + .get_downlevel_capabilities() + .flags + .contains(DownlevelFlags::COMPUTE_SHADERS); + + if !limit_support || !downlevel_support { + info!("Disabling EnvironmentMapGenerationPlugin because compute is not supported on this platform. This is safe to ignore if you are not using EnvironmentMapGenerationPlugin."); + return; + } + } else { + return; + } + + embedded_asset!(app, "environment_filter.wgsl"); + embedded_asset!(app, "downsample.wgsl"); + embedded_asset!(app, "copy.wgsl"); + + app.add_plugins(SyncComponentPlugin::::default()) + .add_systems(Update, generate_environment_map_light); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .add_render_graph_node::(Core3d, GeneratorNode::Downsampling) + .add_render_graph_node::(Core3d, GeneratorNode::Filtering) + .add_render_graph_edges( + Core3d, + ( + Node3d::EndPrepasses, + GeneratorNode::Downsampling, + GeneratorNode::Filtering, + Node3d::StartMainPass, + ), + ) + .add_systems( + ExtractSchedule, + extract_generated_environment_map_entities.after(generate_environment_map_light), + ) + .add_systems( + Render, + prepare_generated_environment_map_bind_groups + .in_set(RenderSystems::PrepareBindGroups), + ) + .add_systems( + Render, + prepare_generated_environment_map_intermediate_textures + .in_set(RenderSystems::PrepareResources), + ) + .add_systems( + RenderStartup, + initialize_generated_environment_map_resources, + ); + } +} + +// The number of storage textures required to combine the bind group +const REQUIRED_STORAGE_TEXTURES: u32 = 12; + +/// Initializes all render-world resources used by the environment-map generator once on +/// [`bevy_render::RenderStartup`]. +pub fn initialize_generated_environment_map_resources( + mut commands: Commands, + render_device: Res, + render_adapter: Res, + pipeline_cache: Res, + asset_server: Res, +) { + // Determine whether we can use a single, large bind group for all mip outputs + let storage_texture_limit = render_device.limits().max_storage_textures_per_shader_stage; + + // Determine whether we can read and write to the same rgba16f storage texture + let read_write_support = render_adapter + .get_texture_format_features(TextureFormat::Rgba16Float) + .flags + .contains(TextureFormatFeatureFlags::STORAGE_READ_WRITE); + + // Combine the bind group and use read-write storage if it is supported + let combine_bind_group = + storage_texture_limit >= REQUIRED_STORAGE_TEXTURES && read_write_support; + + // Output mips are write-only + let mips = + texture_storage_2d_array(TextureFormat::Rgba16Float, StorageTextureAccess::WriteOnly); + + // Bind group layouts + let (downsampling_first, downsampling_second) = if combine_bind_group { + // One big bind group layout containing all outputs 1–12 + let downsampling = render_device.create_bind_group_layout( + "downsampling_bind_group_layout_combined", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + sampler(SamplerBindingType::Filtering), + uniform_buffer::(false), + texture_2d_array(TextureSampleType::Float { filterable: true }), + mips, // 1 + mips, // 2 + mips, // 3 + mips, // 4 + mips, // 5 + texture_storage_2d_array( + TextureFormat::Rgba16Float, + StorageTextureAccess::ReadWrite, + ), // 6 + mips, // 7 + mips, // 8 + mips, // 9 + mips, // 10 + mips, // 11 + mips, // 12 + ), + ), + ); + + (downsampling.clone(), downsampling) + } else { + // Split layout: first pass outputs 1–6, second pass outputs 7–12 (input mip6 read-only) + + let downsampling_first = render_device.create_bind_group_layout( + "downsampling_first_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + sampler(SamplerBindingType::Filtering), + uniform_buffer::(false), + // Input mip 0 + texture_2d_array(TextureSampleType::Float { filterable: true }), + mips, // 1 + mips, // 2 + mips, // 3 + mips, // 4 + mips, // 5 + mips, // 6 + ), + ), + ); + + let downsampling_second = render_device.create_bind_group_layout( + "downsampling_second_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + sampler(SamplerBindingType::Filtering), + uniform_buffer::(false), + // Input mip 6 + texture_2d_array(TextureSampleType::Float { filterable: true }), + mips, // 7 + mips, // 8 + mips, // 9 + mips, // 10 + mips, // 11 + mips, // 12 + ), + ), + ); + + (downsampling_first, downsampling_second) + }; + let radiance = render_device.create_bind_group_layout( + "radiance_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + // Source environment cubemap + texture_2d_array(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), // Source sampler + // Output specular map + texture_storage_2d_array( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + uniform_buffer::(false), // Uniforms + texture_2d_array(TextureSampleType::Float { filterable: true }), // Blue noise texture + ), + ), + ); + + let irradiance = render_device.create_bind_group_layout( + "irradiance_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + // Source environment cubemap + texture_2d_array(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), // Source sampler + // Output irradiance map + texture_storage_2d_array( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + uniform_buffer::(false), // Uniforms + texture_2d_array(TextureSampleType::Float { filterable: true }), // Blue noise texture + ), + ), + ); + + let copy = render_device.create_bind_group_layout( + "copy_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + // Source cubemap + texture_2d_array(TextureSampleType::Float { filterable: true }), + // Destination mip0 + texture_storage_2d_array( + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + ), + ), + ); + + let layouts = GeneratorBindGroupLayouts { + downsampling_first, + downsampling_second, + radiance, + irradiance, + copy, + }; + + // Samplers + let linear = render_device.create_sampler(&SamplerDescriptor { + label: Some("generator_linear_sampler"), + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + address_mode_w: AddressMode::ClampToEdge, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Linear, + ..Default::default() + }); + + let samplers = GeneratorSamplers { linear }; + + // Pipelines + let features = render_device.features(); + let mut shader_defs = vec![]; + if features.contains(WgpuFeatures::SUBGROUP) { + shader_defs.push(ShaderDefVal::Int("SUBGROUP_SUPPORT".into(), 1)); + } + if combine_bind_group { + shader_defs.push(ShaderDefVal::Int("COMBINE_BIND_GROUP".into(), 1)); + } + #[cfg(feature = "bluenoise_texture")] + { + shader_defs.push(ShaderDefVal::Int("HAS_BLUE_NOISE".into(), 1)); + } + + let downsampling_shader = load_embedded_asset!(asset_server.as_ref(), "downsample.wgsl"); + let env_filter_shader = load_embedded_asset!(asset_server.as_ref(), "environment_filter.wgsl"); + let copy_shader = load_embedded_asset!(asset_server.as_ref(), "copy.wgsl"); + + // First pass for base mip Levels (0-5) + let downsample_first = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("downsampling_first_pipeline".into()), + layout: vec![layouts.downsampling_first.clone()], + push_constant_ranges: vec![], + shader: downsampling_shader.clone(), + shader_defs: { + let mut defs = shader_defs.clone(); + if !combine_bind_group { + defs.push(ShaderDefVal::Int("FIRST_PASS".into(), 1)); + } + defs + }, + entry_point: Some("downsample_first".into()), + zero_initialize_workgroup_memory: false, + }); + + let downsample_second = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("downsampling_second_pipeline".into()), + layout: vec![layouts.downsampling_second.clone()], + push_constant_ranges: vec![], + shader: downsampling_shader, + shader_defs: { + let mut defs = shader_defs.clone(); + if !combine_bind_group { + defs.push(ShaderDefVal::Int("SECOND_PASS".into(), 1)); + } + defs + }, + entry_point: Some("downsample_second".into()), + zero_initialize_workgroup_memory: false, + }); + + // Radiance map for specular environment maps + let radiance = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("radiance_pipeline".into()), + layout: vec![layouts.radiance.clone()], + push_constant_ranges: vec![], + shader: env_filter_shader.clone(), + shader_defs: shader_defs.clone(), + entry_point: Some("generate_radiance_map".into()), + zero_initialize_workgroup_memory: false, + }); + + // Irradiance map for diffuse environment maps + let irradiance = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("irradiance_pipeline".into()), + layout: vec![layouts.irradiance.clone()], + push_constant_ranges: vec![], + shader: env_filter_shader, + shader_defs: shader_defs.clone(), + entry_point: Some("generate_irradiance_map".into()), + zero_initialize_workgroup_memory: false, + }); + + // Copy pipeline handles format conversion and populates mip0 when formats differ + let copy_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("copy_pipeline".into()), + layout: vec![layouts.copy.clone()], + push_constant_ranges: vec![], + shader: copy_shader, + shader_defs: vec![], + entry_point: Some("copy".into()), + zero_initialize_workgroup_memory: false, + }); + + let pipelines = GeneratorPipelines { + downsample_first, + downsample_second, + radiance, + irradiance, + copy: copy_pipeline, + }; + + // Insert all resources into the render world + commands.insert_resource(layouts); + commands.insert_resource(samplers); + commands.insert_resource(pipelines); + commands.insert_resource(DownsamplingConfig { combine_bind_group }); +} + +pub fn extract_generated_environment_map_entities( + query: Extract< + Query<( + RenderEntity, + &GeneratedEnvironmentMapLight, + &EnvironmentMapLight, + )>, + >, + mut commands: Commands, + render_images: Res>, +) { + for (entity, filtered_env_map, env_map_light) in query.iter() { + let Some(env_map) = render_images.get(&filtered_env_map.environment_map) else { + continue; + }; + + let diffuse_map = render_images.get(&env_map_light.diffuse_map); + let specular_map = render_images.get(&env_map_light.specular_map); + + // continue if the diffuse map is not found + if diffuse_map.is_none() || specular_map.is_none() { + continue; + } + + let diffuse_map = diffuse_map.unwrap(); + let specular_map = specular_map.unwrap(); + + let render_filtered_env_map = RenderEnvironmentMap { + environment_map: env_map.clone(), + diffuse_map: diffuse_map.clone(), + specular_map: specular_map.clone(), + intensity: filtered_env_map.intensity, + rotation: filtered_env_map.rotation, + affects_lightmapped_mesh_diffuse: filtered_env_map.affects_lightmapped_mesh_diffuse, + }; + commands + .get_entity(entity) + .expect("Entity not synced to render world") + .insert(render_filtered_env_map); + } +} + +// A render-world specific version of FilteredEnvironmentMapLight that uses CachedTexture +#[derive(Component, Clone)] +pub struct RenderEnvironmentMap { + pub environment_map: GpuImage, + pub diffuse_map: GpuImage, + pub specular_map: GpuImage, + pub intensity: f32, + pub rotation: Quat, + pub affects_lightmapped_mesh_diffuse: bool, +} + +#[derive(Component)] +pub struct IntermediateTextures { + pub environment_map: CachedTexture, +} + +/// Returns the total number of mip levels for the provided square texture size. +/// `size` must be a power of two greater than zero. For example, `size = 512` → `9`. +#[inline] +fn compute_mip_count(size: u32) -> u32 { + debug_assert!(size.is_power_of_two()); + 32 - size.leading_zeros() +} + +/// Prepares textures needed for single pass downsampling +pub fn prepare_generated_environment_map_intermediate_textures( + light_probes: Query<(Entity, &RenderEnvironmentMap)>, + render_device: Res, + mut texture_cache: ResMut, + mut commands: Commands, +) { + for (entity, env_map_light) in &light_probes { + let base_size = env_map_light.environment_map.size.width; + let mip_level_count = compute_mip_count(base_size); + + let environment_map = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("intermediate_environment_map"), + size: Extent3d { + width: base_size, + height: base_size, + depth_or_array_layers: 6, // Cubemap faces + }, + mip_level_count, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::STORAGE_BINDING + | TextureUsages::COPY_DST, + view_formats: &[], + }, + ); + + commands + .entity(entity) + .insert(IntermediateTextures { environment_map }); + } +} + +/// Shader constants for downsampling algorithm +#[derive(Clone, Copy, ShaderType)] +#[repr(C)] +pub struct DownsamplingConstants { + mips: u32, + inverse_input_size: Vec2, + _padding: u32, +} + +/// Constants for filtering +#[derive(Clone, Copy, ShaderType)] +#[repr(C)] +pub struct FilteringConstants { + mip_level: f32, + sample_count: u32, + roughness: f32, + noise_size_bits: UVec2, +} + +/// Stores bind groups for the environment map generation pipelines +#[derive(Component)] +pub struct GeneratorBindGroups { + pub downsampling_first: BindGroup, + pub downsampling_second: BindGroup, + pub radiance: Vec, // One per mip level + pub irradiance: BindGroup, + pub copy: BindGroup, +} + +/// Prepares bind groups for environment map generation pipelines +pub fn prepare_generated_environment_map_bind_groups( + light_probes: Query< + (Entity, &IntermediateTextures, &RenderEnvironmentMap), + With, + >, + render_device: Res, + queue: Res, + layouts: Res, + samplers: Res, + render_images: Res>, + bluenoise: Res, + config: Res, + mut commands: Commands, +) { + // Skip until the blue-noise texture is available to avoid panicking. + // The system will retry next frame once the asset has loaded. + let Some(stbn_texture) = render_images.get(&bluenoise.texture) else { + return; + }; + + assert!(stbn_texture.size.width.is_power_of_two()); + assert!(stbn_texture.size.height.is_power_of_two()); + let noise_size_bits = UVec2::new( + stbn_texture.size.width.trailing_zeros(), + stbn_texture.size.height.trailing_zeros(), + ); + + for (entity, textures, env_map_light) in &light_probes { + // Determine mip chain based on input size + let base_size = env_map_light.environment_map.size.width; + let mip_count = compute_mip_count(base_size); + let last_mip = mip_count - 1; + let env_map_texture = env_map_light.environment_map.texture.clone(); + + // Create downsampling constants + let downsampling_constants = DownsamplingConstants { + mips: mip_count - 1, // Number of mips we are generating (excluding mip 0) + inverse_input_size: Vec2::new(1.0 / base_size as f32, 1.0 / base_size as f32), + _padding: 0, + }; + + let mut downsampling_constants_buffer = UniformBuffer::from(downsampling_constants); + downsampling_constants_buffer.write_buffer(&render_device, &queue); + + let input_env_map_first = env_map_texture.clone().create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }); + + // Utility closure to get a unique storage view for a given mip level. + let mip_storage = |level: u32| { + if level <= last_mip { + create_storage_view(&textures.environment_map.texture, level, &render_device) + } else { + // Return a fresh 1×1 placeholder view so each binding has its own sub-resource and cannot alias. + create_placeholder_storage_view(&render_device) + } + }; + + // Depending on device limits, build either a combined or split bind group layout + let (downsampling_first_bind_group, downsampling_second_bind_group) = + if config.combine_bind_group { + // Combined layout expects destinations 1–12 in both bind groups + let bind_group = render_device.create_bind_group( + "downsampling_bind_group_combined_first", + &layouts.downsampling_first, + &BindGroupEntries::sequential(( + &samplers.linear, + &downsampling_constants_buffer, + &input_env_map_first, + &mip_storage(1), + &mip_storage(2), + &mip_storage(3), + &mip_storage(4), + &mip_storage(5), + &mip_storage(6), + &mip_storage(7), + &mip_storage(8), + &mip_storage(9), + &mip_storage(10), + &mip_storage(11), + &mip_storage(12), + )), + ); + + (bind_group.clone(), bind_group) + } else { + // Split path requires a separate view for mip6 input + let input_env_map_second = env_map_texture.create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + base_mip_level: min(6, last_mip), + mip_level_count: Some(1), + ..Default::default() + }); + + // Split layout (current behavior) + let first = render_device.create_bind_group( + "downsampling_first_bind_group", + &layouts.downsampling_first, + &BindGroupEntries::sequential(( + &samplers.linear, + &downsampling_constants_buffer, + &input_env_map_first, + &mip_storage(1), + &mip_storage(2), + &mip_storage(3), + &mip_storage(4), + &mip_storage(5), + &mip_storage(6), + )), + ); + + let second = render_device.create_bind_group( + "downsampling_second_bind_group", + &layouts.downsampling_second, + &BindGroupEntries::sequential(( + &samplers.linear, + &downsampling_constants_buffer, + &input_env_map_second, + &mip_storage(7), + &mip_storage(8), + &mip_storage(9), + &mip_storage(10), + &mip_storage(11), + &mip_storage(12), + )), + ); + + (first, second) + }; + + // create a 2d array view of the bluenoise texture + let stbn_texture_view = stbn_texture + .texture + .clone() + .create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }); + + // Create radiance map bind groups for each mip level + let num_mips = mip_count as usize; + let mut radiance_bind_groups = Vec::with_capacity(num_mips); + + for mip in 0..num_mips { + // Calculate roughness from 0.0 (mip 0) to 0.889 (mip 8) + // We don't need roughness=1.0 as a mip level because it's handled by the separate diffuse irradiance map + let roughness = mip as f32 / (num_mips - 1) as f32; + let sample_count = 32u32 * 2u32.pow((roughness * 4.0) as u32); + + let radiance_constants = FilteringConstants { + mip_level: mip as f32, + sample_count, + roughness, + noise_size_bits, + }; + + let mut radiance_constants_buffer = UniformBuffer::from(radiance_constants); + radiance_constants_buffer.write_buffer(&render_device, &queue); + + let mip_storage_view = create_storage_view( + &env_map_light.specular_map.texture, + mip as u32, + &render_device, + ); + let bind_group = render_device.create_bind_group( + Some(format!("radiance_bind_group_mip_{mip}").as_str()), + &layouts.radiance, + &BindGroupEntries::sequential(( + &textures.environment_map.default_view, + &samplers.linear, + &mip_storage_view, + &radiance_constants_buffer, + &stbn_texture_view, + )), + ); + + radiance_bind_groups.push(bind_group); + } + + // Create irradiance bind group + let irradiance_constants = FilteringConstants { + mip_level: 0.0, + // 32 phi, 32 theta = 1024 samples total + sample_count: 1024, + roughness: 1.0, + noise_size_bits, + }; + + let mut irradiance_constants_buffer = UniformBuffer::from(irradiance_constants); + irradiance_constants_buffer.write_buffer(&render_device, &queue); + + // create a 2d array view + let irradiance_map = + env_map_light + .diffuse_map + .texture + .create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }); + + let irradiance_bind_group = render_device.create_bind_group( + "irradiance_bind_group", + &layouts.irradiance, + &BindGroupEntries::sequential(( + &textures.environment_map.default_view, + &samplers.linear, + &irradiance_map, + &irradiance_constants_buffer, + &stbn_texture_view, + )), + ); + + // Create copy bind group (source env map → destination mip0) + let src_view = env_map_light + .environment_map + .texture + .create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }); + + let dst_view = create_storage_view(&textures.environment_map.texture, 0, &render_device); + + let copy_bind_group = render_device.create_bind_group( + "copy_bind_group", + &layouts.copy, + &BindGroupEntries::with_indices(((0, &src_view), (1, &dst_view))), + ); + + commands.entity(entity).insert(GeneratorBindGroups { + downsampling_first: downsampling_first_bind_group, + downsampling_second: downsampling_second_bind_group, + radiance: radiance_bind_groups, + irradiance: irradiance_bind_group, + copy: copy_bind_group, + }); + } +} + +/// Helper function to create a storage texture view for a specific mip level +fn create_storage_view(texture: &Texture, mip: u32, _render_device: &RenderDevice) -> TextureView { + texture.create_view(&TextureViewDescriptor { + label: Some(format!("storage_view_mip_{mip}").as_str()), + format: Some(texture.format()), + dimension: Some(TextureViewDimension::D2Array), + aspect: TextureAspect::All, + base_mip_level: mip, + mip_level_count: Some(1), + base_array_layer: 0, + array_layer_count: Some(texture.depth_or_array_layers()), + usage: Some(TextureUsages::STORAGE_BINDING), + }) +} + +/// To ensure compatibility in web browsers, each call returns a unique resource so that multiple missing mip +/// bindings in the same bind-group never alias. +fn create_placeholder_storage_view(render_device: &RenderDevice) -> TextureView { + let tex = render_device.create_texture(&TextureDescriptor { + label: Some("lightprobe_placeholder"), + size: Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 6, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + + tex.create_view(&TextureViewDescriptor::default()) +} + +/// Downsampling node implementation that handles all parts of the mip chain +pub struct DownsamplingNode { + query: QueryState<( + Entity, + Read, + Read, + )>, +} + +impl FromWorld for DownsamplingNode { + fn from_world(world: &mut World) -> Self { + Self { + query: QueryState::new(world), + } + } +} + +impl Node for DownsamplingNode { + fn update(&mut self, world: &mut World) { + self.query.update_archetypes(world); + } + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let pipelines = world.resource::(); + + let Some(downsample_first_pipeline) = + pipeline_cache.get_compute_pipeline(pipelines.downsample_first) + else { + return Ok(()); + }; + + let Some(downsample_second_pipeline) = + pipeline_cache.get_compute_pipeline(pipelines.downsample_second) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + for (_, bind_groups, env_map_light) in self.query.iter_manual(world) { + // Copy base mip using compute shader with pre-built bind group + let Some(copy_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.copy) else { + return Ok(()); + }; + + { + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("lightprobe_copy"), + timestamp_writes: None, + }); + + let pass_span = diagnostics.pass_span(&mut compute_pass, "lightprobe_copy"); + + compute_pass.set_pipeline(copy_pipeline); + compute_pass.set_bind_group(0, &bind_groups.copy, &[]); + + let tex_size = env_map_light.environment_map.size; + let wg_x = tex_size.width.div_ceil(8); + let wg_y = tex_size.height.div_ceil(8); + compute_pass.dispatch_workgroups(wg_x, wg_y, 6); + + pass_span.end(&mut compute_pass); + } + + // First pass - process mips 0-5 + { + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("lightprobe_downsampling_first_pass"), + timestamp_writes: None, + }); + + let pass_span = + diagnostics.pass_span(&mut compute_pass, "lightprobe_downsampling_first_pass"); + + compute_pass.set_pipeline(downsample_first_pipeline); + compute_pass.set_bind_group(0, &bind_groups.downsampling_first, &[]); + + let tex_size = env_map_light.environment_map.size; + let wg_x = tex_size.width.div_ceil(64); + let wg_y = tex_size.height.div_ceil(64); + compute_pass.dispatch_workgroups(wg_x, wg_y, 6); // 6 faces + + pass_span.end(&mut compute_pass); + } + + // Second pass - process mips 6-12 + { + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("lightprobe_downsampling_second_pass"), + timestamp_writes: None, + }); + + let pass_span = + diagnostics.pass_span(&mut compute_pass, "lightprobe_downsampling_second_pass"); + + compute_pass.set_pipeline(downsample_second_pipeline); + compute_pass.set_bind_group(0, &bind_groups.downsampling_second, &[]); + + let tex_size = env_map_light.environment_map.size; + let wg_x = tex_size.width.div_ceil(256); + let wg_y = tex_size.height.div_ceil(256); + compute_pass.dispatch_workgroups(wg_x, wg_y, 6); + + pass_span.end(&mut compute_pass); + } + } + + Ok(()) + } +} + +/// Radiance map node for generating specular environment maps +pub struct FilteringNode { + query: QueryState<( + Entity, + Read, + Read, + )>, +} + +impl FromWorld for FilteringNode { + fn from_world(world: &mut World) -> Self { + Self { + query: QueryState::new(world), + } + } +} + +impl Node for FilteringNode { + fn update(&mut self, world: &mut World) { + self.query.update_archetypes(world); + } + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let pipelines = world.resource::(); + + let Some(radiance_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.radiance) + else { + return Ok(()); + }; + let Some(irradiance_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.irradiance) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + for (_, bind_groups, env_map_light) in self.query.iter_manual(world) { + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("lightprobe_radiance_map"), + timestamp_writes: None, + }); + + let pass_span = diagnostics.pass_span(&mut compute_pass, "lightprobe_radiance_map"); + + compute_pass.set_pipeline(radiance_pipeline); + + let base_size = env_map_light.specular_map.size.width; + + // Radiance convolution pass + // Process each mip at different roughness levels + for (mip, bind_group) in bind_groups.radiance.iter().enumerate() { + compute_pass.set_bind_group(0, bind_group, &[]); + + // Calculate dispatch size based on mip level + let mip_size = base_size >> mip; + let workgroup_count = mip_size.div_ceil(8); + + // Dispatch for all 6 faces + compute_pass.dispatch_workgroups(workgroup_count, workgroup_count, 6); + } + pass_span.end(&mut compute_pass); + // End the compute pass before starting the next one + drop(compute_pass); + + // Irradiance convolution pass + // Generate the diffuse environment map + { + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("lightprobe_irradiance_map"), + timestamp_writes: None, + }); + + let irr_span = + diagnostics.pass_span(&mut compute_pass, "lightprobe_irradiance_map"); + + compute_pass.set_pipeline(irradiance_pipeline); + compute_pass.set_bind_group(0, &bind_groups.irradiance, &[]); + + // 32×32 texture processed with 8×8 workgroups for all 6 faces + compute_pass.dispatch_workgroups(4, 4, 6); + + irr_span.end(&mut compute_pass); + } + } + + Ok(()) + } +} + +/// System that generates an `EnvironmentMapLight` component based on the `GeneratedEnvironmentMapLight` component +pub fn generate_environment_map_light( + mut commands: Commands, + mut images: ResMut>, + query: Query<(Entity, &GeneratedEnvironmentMapLight), Without>, +) { + for (entity, filtered_env_map) in &query { + // Validate and fetch the source cubemap so we can size our targets correctly + let Some(src_image) = images.get(&filtered_env_map.environment_map) else { + // Texture not ready yet – try again next frame + continue; + }; + + let base_size = src_image.texture_descriptor.size.width; + + // Sanity checks – square, power-of-two, ≤ 8192 + if src_image.texture_descriptor.size.height != base_size + || !base_size.is_power_of_two() + || base_size > 8192 + { + panic!( + "GeneratedEnvironmentMapLight source cubemap must be square power-of-two ≤ 8192, got {}×{}", + base_size, src_image.texture_descriptor.size.height + ); + } + + let mip_count = compute_mip_count(base_size); + + // Create a placeholder for the irradiance map + let mut diffuse = Image::new_fill( + Extent3d { + width: 32, + height: 32, + depth_or_array_layers: 6, + }, + TextureDimension::D2, + &[0; 8], + TextureFormat::Rgba16Float, + RenderAssetUsages::all(), + ); + + diffuse.texture_descriptor.usage = + TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING; + + diffuse.texture_view_descriptor = Some(TextureViewDescriptor { + dimension: Some(TextureViewDimension::Cube), + ..Default::default() + }); + + let diffuse_handle = images.add(diffuse); + + // Create a placeholder for the specular map. It matches the input cubemap resolution. + let mut specular = Image::new_fill( + Extent3d { + width: base_size, + height: base_size, + depth_or_array_layers: 6, + }, + TextureDimension::D2, + &[0; 8], + TextureFormat::Rgba16Float, + RenderAssetUsages::all(), + ); + + // Set up for mipmaps + specular.texture_descriptor.usage = + TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING; + specular.texture_descriptor.mip_level_count = mip_count; + + // When setting mip_level_count, we need to allocate appropriate data size + // For GPU-generated mipmaps, we can set data to None since the GPU will generate the data + specular.data = None; + + specular.texture_view_descriptor = Some(TextureViewDescriptor { + dimension: Some(TextureViewDimension::Cube), + mip_level_count: Some(mip_count), + ..Default::default() + }); + + let specular_handle = images.add(specular); + + // Add the EnvironmentMapLight component with the placeholder handles + commands.entity(entity).insert(EnvironmentMapLight { + diffuse_map: diffuse_handle, + specular_map: specular_handle, + intensity: filtered_env_map.intensity, + rotation: filtered_env_map.rotation, + affects_lightmapped_mesh_diffuse: filtered_env_map.affects_lightmapped_mesh_diffuse, + }); + } +} diff --git a/crates/libmarathon/src/render/pbr/light_probe/irradiance_volume.rs b/crates/libmarathon/src/render/pbr/light_probe/irradiance_volume.rs new file mode 100644 index 0000000..f8c5d0e --- /dev/null +++ b/crates/libmarathon/src/render/pbr/light_probe/irradiance_volume.rs @@ -0,0 +1,325 @@ +//! Irradiance volumes, also known as voxel global illumination. +//! +//! An *irradiance volume* is a cuboid voxel region consisting of +//! regularly-spaced precomputed samples of diffuse indirect light. They're +//! ideal if you have a dynamic object such as a character that can move about +//! static non-moving geometry such as a level in a game, and you want that +//! dynamic object to be affected by the light bouncing off that static +//! geometry. +//! +//! To use irradiance volumes, you need to precompute, or *bake*, the indirect +//! light in your scene. Bevy doesn't currently come with a way to do this. +//! Fortunately, [Blender] provides a [baking tool] as part of the Eevee +//! renderer, and its irradiance volumes are compatible with those used by Bevy. +//! The [`bevy-baked-gi`] project provides a tool, `export-blender-gi`, that can +//! extract the baked irradiance volumes from the Blender `.blend` file and +//! package them up into a `.ktx2` texture for use by the engine. See the +//! documentation in the `bevy-baked-gi` project for more details on this +//! workflow. +//! +//! Like all light probes in Bevy, irradiance volumes are 1×1×1 cubes, centered +//! on the origin, that can be arbitrarily scaled, rotated, and positioned in a +//! scene with the [`bevy_transform::components::Transform`] component. The 3D +//! voxel grid will be stretched to fill the interior of the cube, with linear +//! interpolation, and the illumination from the irradiance volume will apply to +//! all fragments within that bounding region. +//! +//! Bevy's irradiance volumes are based on Valve's [*ambient cubes*] as used in +//! *Half-Life 2* ([Mitchell 2006, slide 27]). These encode a single color of +//! light from the six 3D cardinal directions and blend the sides together +//! according to the surface normal. For an explanation of why ambient cubes +//! were chosen over spherical harmonics, see [Why ambient cubes?] below. +//! +//! If you wish to use a tool other than `export-blender-gi` to produce the +//! irradiance volumes, you'll need to pack the irradiance volumes in the +//! following format. The irradiance volume of resolution *(Rx, Ry, Rz)* is +//! expected to be a 3D texture of dimensions *(Rx, 2Ry, 3Rz)*. The unnormalized +//! texture coordinate *(s, t, p)* of the voxel at coordinate *(x, y, z)* with +//! side *S* ∈ *{-X, +X, -Y, +Y, -Z, +Z}* is as follows: +//! +//! ```text +//! s = x +//! +//! t = y + ⎰ 0 if S ∈ {-X, -Y, -Z} +//! ⎱ Ry if S ∈ {+X, +Y, +Z} +//! +//! ⎧ 0 if S ∈ {-X, +X} +//! p = z + ⎨ Rz if S ∈ {-Y, +Y} +//! ⎩ 2Rz if S ∈ {-Z, +Z} +//! ``` +//! +//! Visually, in a left-handed coordinate system with Y up, viewed from the +//! right, the 3D texture looks like a stacked series of voxel grids, one for +//! each cube side, in this order: +//! +//! | **+X** | **+Y** | **+Z** | +//! | ------ | ------ | ------ | +//! | **-X** | **-Y** | **-Z** | +//! +//! A terminology note: Other engines may refer to irradiance volumes as *voxel +//! global illumination*, *VXGI*, or simply as *light probes*. Sometimes *light +//! probe* refers to what Bevy calls a reflection probe. In Bevy, *light probe* +//! is a generic term that encompasses all cuboid bounding regions that capture +//! indirect illumination, whether based on voxels or not. +//! +//! Note that, if binding arrays aren't supported (e.g. on WebGPU or WebGL 2), +//! then only the closest irradiance volume to the view will be taken into +//! account during rendering. The required `wgpu` features are +//! [`bevy_render::settings::WgpuFeatures::TEXTURE_BINDING_ARRAY`] and +//! [`bevy_render::settings::WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING`]. +//! +//! ## Why ambient cubes? +//! +//! This section describes the motivation behind the decision to use ambient +//! cubes in Bevy. It's not needed to use the feature; feel free to skip it +//! unless you're interested in its internal design. +//! +//! Bevy uses *Half-Life 2*-style ambient cubes (usually abbreviated as *HL2*) +//! as the representation of irradiance for light probes instead of the +//! more-popular spherical harmonics (*SH*). This might seem to be a surprising +//! choice, but it turns out to work well for the specific case of voxel +//! sampling on the GPU. Spherical harmonics have two problems that make them +//! less ideal for this use case: +//! +//! 1. The level 1 spherical harmonic coefficients can be negative. That +//! prevents the use of the efficient [RGB9E5 texture format], which only +//! encodes unsigned floating point numbers, and forces the use of the +//! less-efficient [RGBA16F format] if hardware interpolation is desired. +//! +//! 2. As an alternative to RGBA16F, level 1 spherical harmonics can be +//! normalized and scaled to the SH0 base color, as [Frostbite] does. This +//! allows them to be packed in standard LDR RGBA8 textures. However, this +//! prevents the use of hardware trilinear filtering, as the nonuniform scale +//! factor means that hardware interpolation no longer produces correct results. +//! The 8 texture fetches needed to interpolate between voxels can be upwards of +//! twice as slow as the hardware interpolation. +//! +//! The following chart summarizes the costs and benefits of ambient cubes, +//! level 1 spherical harmonics, and level 2 spherical harmonics: +//! +//! | Technique | HW-interpolated samples | Texel fetches | Bytes per voxel | Quality | +//! | ------------------------ | ----------------------- | ------------- | --------------- | ------- | +//! | Ambient cubes | 3 | 0 | 24 | Medium | +//! | Level 1 SH, compressed | 0 | 36 | 16 | Low | +//! | Level 1 SH, uncompressed | 4 | 0 | 24 | Low | +//! | Level 2 SH, compressed | 0 | 72 | 28 | High | +//! | Level 2 SH, uncompressed | 9 | 0 | 54 | High | +//! +//! (Note that the number of bytes per voxel can be reduced using various +//! texture compression methods, but the overall ratios remain similar.) +//! +//! From these data, we can see that ambient cubes balance fast lookups (from +//! leveraging hardware interpolation) with relatively-small storage +//! requirements and acceptable quality. Hence, they were chosen for irradiance +//! volumes in Bevy. +//! +//! [*ambient cubes*]: https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf +//! +//! [spherical harmonics]: https://en.wikipedia.org/wiki/Spherical_harmonic_lighting +//! +//! [RGB9E5 texture format]: https://www.khronos.org/opengl/wiki/Small_Float_Formats#RGB9_E5 +//! +//! [RGBA16F format]: https://www.khronos.org/opengl/wiki/Small_Float_Formats#Low-bitdepth_floats +//! +//! [Frostbite]: https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/gdc2018-precomputedgiobalilluminationinfrostbite.pdf#page=53 +//! +//! [Mitchell 2006, slide 27]: https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf#page=27 +//! +//! [Blender]: http://blender.org/ +//! +//! [baking tool]: https://docs.blender.org/manual/en/latest/render/eevee/light_probes/volume.html +//! +//! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi +//! +//! [Why ambient cubes?]: #why-ambient-cubes + +use bevy_image::Image; +use bevy_light::IrradianceVolume; +use crate::render::{ + render_asset::RenderAssets, + render_resource::{ + binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, TextureSampleType, + TextureView, + }, + renderer::{RenderAdapter, RenderDevice}, + texture::{FallbackImage, GpuImage}, +}; +use core::{num::NonZero, ops::Deref}; + +use bevy_asset::AssetId; + +use crate::render::pbr::{ + add_cubemap_texture_view, binding_arrays_are_usable, RenderViewLightProbes, + MAX_VIEW_LIGHT_PROBES, +}; + +use super::LightProbeComponent; + +/// On WebGL and WebGPU, we must disable irradiance volumes, as otherwise we can +/// overflow the number of texture bindings when deferred rendering is in use +/// (see issue #11885). +pub(crate) const IRRADIANCE_VOLUMES_ARE_USABLE: bool = cfg!(not(target_arch = "wasm32")); + +/// All the bind group entries necessary for PBR shaders to access the +/// irradiance volumes exposed to a view. +pub(crate) enum RenderViewIrradianceVolumeBindGroupEntries<'a> { + /// The version used when binding arrays aren't available on the current platform. + Single { + /// The texture view of the closest light probe. + texture_view: &'a TextureView, + /// A sampler used to sample voxels of the irradiance volume. + sampler: &'a Sampler, + }, + + /// The version used when binding arrays are available on the current + /// platform. + Multiple { + /// A texture view of the voxels of each irradiance volume, in the same + /// order that they are supplied to the view (i.e. in the same order as + /// `binding_index_to_cubemap` in [`RenderViewLightProbes`]). + /// + /// This is a vector of `wgpu::TextureView`s. But we don't want to import + /// `wgpu` in this crate, so we refer to it indirectly like this. + texture_views: Vec<&'a ::Target>, + + /// A sampler used to sample voxels of the irradiance volumes. + sampler: &'a Sampler, + }, +} + +impl<'a> RenderViewIrradianceVolumeBindGroupEntries<'a> { + /// Looks up and returns the bindings for any irradiance volumes visible in + /// the view, as well as the sampler. + pub(crate) fn get( + render_view_irradiance_volumes: Option<&RenderViewLightProbes>, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, + render_device: &RenderDevice, + render_adapter: &RenderAdapter, + ) -> RenderViewIrradianceVolumeBindGroupEntries<'a> { + if binding_arrays_are_usable(render_device, render_adapter) { + RenderViewIrradianceVolumeBindGroupEntries::get_multiple( + render_view_irradiance_volumes, + images, + fallback_image, + ) + } else { + RenderViewIrradianceVolumeBindGroupEntries::single( + render_view_irradiance_volumes, + images, + fallback_image, + ) + } + } + + /// Looks up and returns the bindings for any irradiance volumes visible in + /// the view, as well as the sampler. This is the version used when binding + /// arrays are available on the current platform. + fn get_multiple( + render_view_irradiance_volumes: Option<&RenderViewLightProbes>, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, + ) -> RenderViewIrradianceVolumeBindGroupEntries<'a> { + let mut texture_views = vec![]; + let mut sampler = None; + + if let Some(irradiance_volumes) = render_view_irradiance_volumes { + for &cubemap_id in &irradiance_volumes.binding_index_to_textures { + add_cubemap_texture_view( + &mut texture_views, + &mut sampler, + cubemap_id, + images, + fallback_image, + ); + } + } + + // Pad out the bindings to the size of the binding array using fallback + // textures. This is necessary on D3D12 and Metal. + texture_views.resize(MAX_VIEW_LIGHT_PROBES, &*fallback_image.d3.texture_view); + + RenderViewIrradianceVolumeBindGroupEntries::Multiple { + texture_views, + sampler: sampler.unwrap_or(&fallback_image.d3.sampler), + } + } + + /// Looks up and returns the bindings for any irradiance volumes visible in + /// the view, as well as the sampler. This is the version used when binding + /// arrays aren't available on the current platform. + fn single( + render_view_irradiance_volumes: Option<&RenderViewLightProbes>, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, + ) -> RenderViewIrradianceVolumeBindGroupEntries<'a> { + if let Some(irradiance_volumes) = render_view_irradiance_volumes + && let Some(irradiance_volume) = irradiance_volumes.render_light_probes.first() + && irradiance_volume.texture_index >= 0 + && let Some(image_id) = irradiance_volumes + .binding_index_to_textures + .get(irradiance_volume.texture_index as usize) + && let Some(image) = images.get(*image_id) + { + return RenderViewIrradianceVolumeBindGroupEntries::Single { + texture_view: &image.texture_view, + sampler: &image.sampler, + }; + } + + RenderViewIrradianceVolumeBindGroupEntries::Single { + texture_view: &fallback_image.d3.texture_view, + sampler: &fallback_image.d3.sampler, + } + } +} + +/// Returns the bind group layout entries for the voxel texture and sampler +/// respectively. +pub(crate) fn get_bind_group_layout_entries( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, +) -> [BindGroupLayoutEntryBuilder; 2] { + let mut texture_3d_binding = + binding_types::texture_3d(TextureSampleType::Float { filterable: true }); + if binding_arrays_are_usable(render_device, render_adapter) { + texture_3d_binding = + texture_3d_binding.count(NonZero::::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); + } + + [ + texture_3d_binding, + binding_types::sampler(SamplerBindingType::Filtering), + ] +} + +impl LightProbeComponent for IrradianceVolume { + type AssetId = AssetId; + + // Irradiance volumes can't be attached to the view, so we store nothing + // here. + type ViewLightProbeInfo = (); + + fn id(&self, image_assets: &RenderAssets) -> Option { + if image_assets.get(&self.voxels).is_none() { + None + } else { + Some(self.voxels.id()) + } + } + + fn intensity(&self) -> f32 { + self.intensity + } + + fn affects_lightmapped_mesh_diffuse(&self) -> bool { + self.affects_lightmapped_meshes + } + + fn create_render_view_light_probes( + _: Option<&Self>, + _: &RenderAssets, + ) -> RenderViewLightProbes { + RenderViewLightProbes::new() + } +} diff --git a/crates/libmarathon/src/render/pbr/light_probe/irradiance_volume.wgsl b/crates/libmarathon/src/render/pbr/light_probe/irradiance_volume.wgsl new file mode 100644 index 0000000..f079bd6 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/light_probe/irradiance_volume.wgsl @@ -0,0 +1,73 @@ +#define_import_path bevy_pbr::irradiance_volume + +#import bevy_pbr::light_probe::query_light_probe +#import bevy_pbr::mesh_view_bindings::{ + irradiance_volumes, + irradiance_volume, + irradiance_volume_sampler, + light_probes, +}; +#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges + +#ifdef IRRADIANCE_VOLUMES_ARE_USABLE + +// See: +// https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf +// Slide 28, "Ambient Cube Basis" +fn irradiance_volume_light( + world_position: vec3, + N: vec3, + clusterable_object_index_ranges: ptr, +) -> vec3 { + // Search for an irradiance volume that contains the fragment. + let query_result = query_light_probe( + world_position, + /*is_irradiance_volume=*/ true, + clusterable_object_index_ranges, + ); + + // If there was no irradiance volume found, bail out. + if (query_result.texture_index < 0) { + return vec3(0.0f); + } + + // If we're lightmapped, and the irradiance volume contributes no diffuse + // light, then bail out. +#ifdef LIGHTMAP + if (!query_result.affects_lightmapped_mesh_diffuse) { + return vec3(0.0f); + } +#endif // LIGHTMAP + +#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY + let irradiance_volume_texture = irradiance_volumes[query_result.texture_index]; +#else + let irradiance_volume_texture = irradiance_volume; +#endif + + let atlas_resolution = vec3(textureDimensions(irradiance_volume_texture)); + let resolution = vec3(textureDimensions(irradiance_volume_texture) / vec3(1u, 2u, 3u)); + + // Make sure to clamp to the edges to avoid texture bleed. + var unit_pos = (query_result.light_from_world * vec4(world_position, 1.0f)).xyz; + let stp = clamp((unit_pos + 0.5) * resolution, vec3(0.5f), resolution - vec3(0.5f)); + let uvw = stp / atlas_resolution; + + // The bottom half of each cube slice is the negative part, so choose it if applicable on each + // slice. + let neg_offset = select(vec3(0.0f), vec3(0.5f), N < vec3(0.0f)); + + let uvw_x = uvw + vec3(0.0f, neg_offset.x, 0.0f); + let uvw_y = uvw + vec3(0.0f, neg_offset.y, 1.0f / 3.0f); + let uvw_z = uvw + vec3(0.0f, neg_offset.z, 2.0f / 3.0f); + + let rgb_x = textureSampleLevel(irradiance_volume_texture, irradiance_volume_sampler, uvw_x, 0.0).rgb; + let rgb_y = textureSampleLevel(irradiance_volume_texture, irradiance_volume_sampler, uvw_y, 0.0).rgb; + let rgb_z = textureSampleLevel(irradiance_volume_texture, irradiance_volume_sampler, uvw_z, 0.0).rgb; + + // Use Valve's formula to sample. + let NN = N * N; + return (rgb_x * NN.x + rgb_y * NN.y + rgb_z * NN.z) * query_result.intensity; +} + +#endif // IRRADIANCE_VOLUMES_ARE_USABLE diff --git a/crates/libmarathon/src/render/pbr/light_probe/light_probe.wgsl b/crates/libmarathon/src/render/pbr/light_probe/light_probe.wgsl new file mode 100644 index 0000000..16a2112 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/light_probe/light_probe.wgsl @@ -0,0 +1,154 @@ +#define_import_path bevy_pbr::light_probe + +#import bevy_pbr::clustered_forward +#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges +#import bevy_pbr::mesh_view_bindings::light_probes +#import bevy_pbr::mesh_view_types::LightProbe + +// The result of searching for a light probe. +struct LightProbeQueryResult { + // The index of the light probe texture or textures in the binding array or + // arrays. + texture_index: i32, + // A scale factor that's applied to the diffuse and specular light from the + // light probe. This is in units of cd/m² (candela per square meter). + intensity: f32, + // Transform from world space to the light probe model space. In light probe + // model space, the light probe is a 1×1×1 cube centered on the origin. + light_from_world: mat4x4, + // Whether this light probe contributes diffuse light to lightmapped meshes. + affects_lightmapped_mesh_diffuse: bool, +}; + +fn transpose_affine_matrix(matrix: mat3x4) -> mat4x4 { + let matrix4x4 = mat4x4( + matrix[0], + matrix[1], + matrix[2], + vec4(0.0, 0.0, 0.0, 1.0)); + return transpose(matrix4x4); +} + +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + +// Searches for a light probe that contains the fragment. +// +// This is the version that's used when storage buffers are available and +// light probes are clustered. +// +// TODO: Interpolate between multiple light probes. +fn query_light_probe( + world_position: vec3, + is_irradiance_volume: bool, + clusterable_object_index_ranges: ptr, +) -> LightProbeQueryResult { + var result: LightProbeQueryResult; + result.texture_index = -1; + + // Reflection probe indices are followed by irradiance volume indices in the + // cluster index list. Use this fact to create our bracketing range of + // indices. + var start_offset: u32; + var end_offset: u32; + if is_irradiance_volume { + start_offset = (*clusterable_object_index_ranges).first_irradiance_volume_index_offset; + end_offset = (*clusterable_object_index_ranges).first_decal_offset; + } else { + start_offset = (*clusterable_object_index_ranges).first_reflection_probe_index_offset; + end_offset = (*clusterable_object_index_ranges).first_irradiance_volume_index_offset; + } + + for (var light_probe_index_offset: u32 = start_offset; + light_probe_index_offset < end_offset && result.texture_index < 0; + light_probe_index_offset += 1u) { + let light_probe_index = i32(clustered_forward::get_clusterable_object_id( + light_probe_index_offset)); + + var light_probe: LightProbe; + if is_irradiance_volume { + light_probe = light_probes.irradiance_volumes[light_probe_index]; + } else { + light_probe = light_probes.reflection_probes[light_probe_index]; + } + + // Unpack the inverse transform. + let light_from_world = + transpose_affine_matrix(light_probe.light_from_world_transposed); + + // Check to see if the transformed point is inside the unit cube + // centered at the origin. + let probe_space_pos = (light_from_world * vec4(world_position, 1.0f)).xyz; + if (all(abs(probe_space_pos) <= vec3(0.5f))) { + result.texture_index = light_probe.cubemap_index; + result.intensity = light_probe.intensity; + result.light_from_world = light_from_world; + result.affects_lightmapped_mesh_diffuse = + light_probe.affects_lightmapped_mesh_diffuse != 0u; + break; + } + } + + return result; +} + +#else // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + +// Searches for a light probe that contains the fragment. +// +// This is the version that's used when storage buffers aren't available and +// light probes aren't clustered. It simply does a brute force search of all +// light probes. Because platforms without sufficient SSBO bindings typically +// lack bindless shaders, there will usually only be one of each type of light +// probe present anyway. +fn query_light_probe( + world_position: vec3, + is_irradiance_volume: bool, + clusterable_object_index_ranges: ptr, +) -> LightProbeQueryResult { + var result: LightProbeQueryResult; + result.texture_index = -1; + + var light_probe_count: i32; + if is_irradiance_volume { + light_probe_count = light_probes.irradiance_volume_count; + } else { + light_probe_count = light_probes.reflection_probe_count; + } + + for (var light_probe_index: i32 = 0; + light_probe_index < light_probe_count && result.texture_index < 0; + light_probe_index += 1) { + var light_probe: LightProbe; + if is_irradiance_volume { + light_probe = light_probes.irradiance_volumes[light_probe_index]; + } else { + light_probe = light_probes.reflection_probes[light_probe_index]; + } + + // Unpack the inverse transform. + let light_from_world = + transpose_affine_matrix(light_probe.light_from_world_transposed); + + // Check to see if the transformed point is inside the unit cube + // centered at the origin. + let probe_space_pos = (light_from_world * vec4(world_position, 1.0f)).xyz; + if (all(abs(probe_space_pos) <= vec3(0.5f))) { + result.texture_index = light_probe.cubemap_index; + result.intensity = light_probe.intensity; + result.light_from_world = light_from_world; + result.affects_lightmapped_mesh_diffuse = + light_probe.affects_lightmapped_mesh_diffuse != 0u; + + // TODO: Workaround for ICE in DXC https://github.com/microsoft/DirectXShaderCompiler/issues/6183 + // We can't use `break` here because of the ICE. + // So instead we rely on the fact that we set `result.texture_index` + // above and check its value in the `for` loop header before + // looping. + // break; + } + } + + return result; +} + +#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 diff --git a/crates/libmarathon/src/render/pbr/light_probe/mod.rs b/crates/libmarathon/src/render/pbr/light_probe/mod.rs new file mode 100644 index 0000000..43e359a --- /dev/null +++ b/crates/libmarathon/src/render/pbr/light_probe/mod.rs @@ -0,0 +1,731 @@ +//! Light probes for baked global illumination. + +use bevy_app::{App, Plugin}; +use bevy_asset::AssetId; +use bevy_camera::{ + primitives::{Aabb, Frustum}, + Camera3d, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{Commands, Local, Query, Res, ResMut}, +}; +use bevy_image::Image; +use bevy_light::{EnvironmentMapLight, IrradianceVolume, LightProbe}; +use bevy_math::{Affine3A, FloatOrd, Mat4, Vec3A, Vec4}; +use bevy_platform::collections::HashMap; +use crate::render::{ + extract_instances::ExtractInstancesPlugin, + render_asset::RenderAssets, + render_resource::{DynamicUniformBuffer, Sampler, ShaderType, TextureView}, + renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue, WgpuWrapper}, + settings::WgpuFeatures, + sync_world::RenderEntity, + texture::{FallbackImage, GpuImage}, + view::ExtractedView, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use bevy_shader::load_shader_library; +use bevy_transform::{components::Transform, prelude::GlobalTransform}; +use tracing::error; + +use core::{hash::Hash, ops::Deref}; + +use crate::render::pbr::{ + generate::EnvironmentMapGenerationPlugin, light_probe::environment_map::EnvironmentMapIds, +}; + +pub mod environment_map; +pub mod generate; +pub mod irradiance_volume; + +/// The maximum number of each type of light probe that each view will consider. +/// +/// Because the fragment shader does a linear search through the list for each +/// fragment, this number needs to be relatively small. +pub const MAX_VIEW_LIGHT_PROBES: usize = 8; + +/// How many texture bindings are used in the fragment shader, *not* counting +/// environment maps or irradiance volumes. +const STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS: usize = 16; + +/// Adds support for light probes: cuboid bounding regions that apply global +/// illumination to objects within them. +/// +/// This also adds support for view environment maps: diffuse and specular +/// cubemaps applied to all objects that a view renders. +pub struct LightProbePlugin; + +/// A GPU type that stores information about a light probe. +#[derive(Clone, Copy, ShaderType, Default)] +struct RenderLightProbe { + /// The transform from the world space to the model space. This is used to + /// efficiently check for bounding box intersection. + light_from_world_transposed: [Vec4; 3], + + /// The index of the texture or textures in the appropriate binding array or + /// arrays. + /// + /// For example, for reflection probes this is the index of the cubemap in + /// the diffuse and specular texture arrays. + texture_index: i32, + + /// Scale factor applied to the light generated by this light probe. + /// + /// See the comment in [`EnvironmentMapLight`] for details. + intensity: f32, + + /// Whether this light probe adds to the diffuse contribution of the + /// irradiance for meshes with lightmaps. + affects_lightmapped_mesh_diffuse: u32, +} + +/// A per-view shader uniform that specifies all the light probes that the view +/// takes into account. +#[derive(ShaderType)] +pub struct LightProbesUniform { + /// The list of applicable reflection probes, sorted from nearest to the + /// camera to the farthest away from the camera. + reflection_probes: [RenderLightProbe; MAX_VIEW_LIGHT_PROBES], + + /// The list of applicable irradiance volumes, sorted from nearest to the + /// camera to the farthest away from the camera. + irradiance_volumes: [RenderLightProbe; MAX_VIEW_LIGHT_PROBES], + + /// The number of reflection probes in the list. + reflection_probe_count: i32, + + /// The number of irradiance volumes in the list. + irradiance_volume_count: i32, + + /// The index of the diffuse and specular environment maps associated with + /// the view itself. This is used as a fallback if no reflection probe in + /// the list contains the fragment. + view_cubemap_index: i32, + + /// The smallest valid mipmap level for the specular environment cubemap + /// associated with the view. + smallest_specular_mip_level_for_view: u32, + + /// The intensity of the environment cubemap associated with the view. + /// + /// See the comment in [`EnvironmentMapLight`] for details. + intensity_for_view: f32, + + /// Whether the environment map attached to the view affects the diffuse + /// lighting for lightmapped meshes. + /// + /// This will be 1 if the map does affect lightmapped meshes or 0 otherwise. + view_environment_map_affects_lightmapped_mesh_diffuse: u32, +} + +/// A GPU buffer that stores information about all light probes. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct LightProbesBuffer(DynamicUniformBuffer); + +/// A component attached to each camera in the render world that stores the +/// index of the [`LightProbesUniform`] in the [`LightProbesBuffer`]. +#[derive(Component, Default, Deref, DerefMut)] +pub struct ViewLightProbesUniformOffset(u32); + +/// Information that [`gather_light_probes`] keeps about each light probe. +/// +/// This information is parameterized by the [`LightProbeComponent`] type. This +/// will either be [`EnvironmentMapLight`] for reflection probes or +/// [`IrradianceVolume`] for irradiance volumes. +struct LightProbeInfo +where + C: LightProbeComponent, +{ + // The transform from world space to light probe space. + // Stored as the transpose of the inverse transform to compress the structure + // on the GPU (from 4 `Vec4`s to 3 `Vec4`s). The shader will transpose it + // to recover the original inverse transform. + light_from_world: [Vec4; 3], + + // The transform from light probe space to world space. + world_from_light: Affine3A, + + // Scale factor applied to the diffuse and specular light generated by this + // reflection probe. + // + // See the comment in [`EnvironmentMapLight`] for details. + intensity: f32, + + // Whether this light probe adds to the diffuse contribution of the + // irradiance for meshes with lightmaps. + affects_lightmapped_mesh_diffuse: bool, + + // The IDs of all assets associated with this light probe. + // + // Because each type of light probe component may reference different types + // of assets (e.g. a reflection probe references two cubemap assets while an + // irradiance volume references a single 3D texture asset), this is generic. + asset_id: C::AssetId, +} + +/// A component, part of the render world, that stores the mapping from asset ID +/// or IDs to the texture index in the appropriate binding arrays. +/// +/// Cubemap textures belonging to environment maps are collected into binding +/// arrays, and the index of each texture is presented to the shader for runtime +/// lookup. 3D textures belonging to reflection probes are likewise collected +/// into binding arrays, and the shader accesses the 3D texture by index. +/// +/// This component is attached to each view in the render world, because each +/// view may have a different set of light probes that it considers and therefore +/// the texture indices are per-view. +#[derive(Component, Default)] +pub struct RenderViewLightProbes +where + C: LightProbeComponent, +{ + /// The list of environment maps presented to the shader, in order. + binding_index_to_textures: Vec, + + /// The reverse of `binding_index_to_cubemap`: a map from the texture ID to + /// the index in `binding_index_to_cubemap`. + cubemap_to_binding_index: HashMap, + + /// Information about each light probe, ready for upload to the GPU, sorted + /// in order from closest to the camera to farthest. + /// + /// Note that this is not necessarily ordered by binding index. So don't + /// write code like + /// `render_light_probes[cubemap_to_binding_index[asset_id]]`; instead + /// search for the light probe with the appropriate binding index in this + /// array. + render_light_probes: Vec, + + /// Information needed to render the light probe attached directly to the + /// view, if applicable. + /// + /// A light probe attached directly to a view represents a "global" light + /// probe that affects all objects not in the bounding region of any light + /// probe. Currently, the only light probe type that supports this is the + /// [`EnvironmentMapLight`]. + view_light_probe_info: C::ViewLightProbeInfo, +} + +/// A trait implemented by all components that represent light probes. +/// +/// Currently, the two light probe types are [`EnvironmentMapLight`] and +/// [`IrradianceVolume`], for reflection probes and irradiance volumes +/// respectively. +/// +/// Most light probe systems are written to be generic over the type of light +/// probe. This allows much of the code to be shared and enables easy addition +/// of more light probe types (e.g. real-time reflection planes) in the future. +pub trait LightProbeComponent: Send + Sync + Component + Sized { + /// Holds [`AssetId`]s of the texture or textures that this light probe + /// references. + /// + /// This can just be [`AssetId`] if the light probe only references one + /// texture. If it references multiple textures, it will be a structure + /// containing those asset IDs. + type AssetId: Send + Sync + Clone + Eq + Hash; + + /// If the light probe can be attached to the view itself (as opposed to a + /// cuboid region within the scene), this contains the information that will + /// be passed to the GPU in order to render it. Otherwise, this will be + /// `()`. + /// + /// Currently, only reflection probes (i.e. [`EnvironmentMapLight`]) can be + /// attached directly to views. + type ViewLightProbeInfo: Send + Sync + Default; + + /// Returns the asset ID or asset IDs of the texture or textures referenced + /// by this light probe. + fn id(&self, image_assets: &RenderAssets) -> Option; + + /// Returns the intensity of this light probe. + /// + /// This is a scaling factor that will be multiplied by the value or values + /// sampled from the texture. + fn intensity(&self) -> f32; + + /// Returns true if this light probe contributes diffuse lighting to meshes + /// with lightmaps or false otherwise. + fn affects_lightmapped_mesh_diffuse(&self) -> bool; + + /// Creates an instance of [`RenderViewLightProbes`] containing all the + /// information needed to render this light probe. + /// + /// This is called for every light probe in view every frame. + fn create_render_view_light_probes( + view_component: Option<&Self>, + image_assets: &RenderAssets, + ) -> RenderViewLightProbes; +} + +/// The uniform struct extracted from [`EnvironmentMapLight`]. +/// Will be available for use in the Environment Map shader. +#[derive(Component, ShaderType, Clone)] +pub struct EnvironmentMapUniform { + /// The world space transformation matrix of the sample ray for environment cubemaps. + transform: Mat4, +} + +impl Default for EnvironmentMapUniform { + fn default() -> Self { + EnvironmentMapUniform { + transform: Mat4::IDENTITY, + } + } +} + +/// A GPU buffer that stores the environment map settings for each view. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct EnvironmentMapUniformBuffer(pub DynamicUniformBuffer); + +/// A component that stores the offset within the +/// [`EnvironmentMapUniformBuffer`] for each view. +#[derive(Component, Default, Deref, DerefMut)] +pub struct ViewEnvironmentMapUniformOffset(u32); + +impl Plugin for LightProbePlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "light_probe.wgsl"); + load_shader_library!(app, "environment_map.wgsl"); + load_shader_library!(app, "irradiance_volume.wgsl"); + + app.add_plugins(( + EnvironmentMapGenerationPlugin, + ExtractInstancesPlugin::::new(), + )); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .init_resource::() + .add_systems(ExtractSchedule, gather_environment_map_uniform) + .add_systems(ExtractSchedule, gather_light_probes::) + .add_systems(ExtractSchedule, gather_light_probes::) + .add_systems( + Render, + (upload_light_probes, prepare_environment_uniform_buffer) + .in_set(RenderSystems::PrepareResources), + ); + } +} + +/// Extracts [`EnvironmentMapLight`] from views and creates [`EnvironmentMapUniform`] for them. +/// +/// Compared to the `ExtractComponentPlugin`, this implementation will create a default instance +/// if one does not already exist. +fn gather_environment_map_uniform( + view_query: Extract), With>>, + mut commands: Commands, +) { + for (view_entity, environment_map_light) in view_query.iter() { + let environment_map_uniform = if let Some(environment_map_light) = environment_map_light { + EnvironmentMapUniform { + transform: Transform::from_rotation(environment_map_light.rotation) + .to_matrix() + .inverse(), + } + } else { + EnvironmentMapUniform::default() + }; + commands + .get_entity(view_entity) + .expect("Environment map light entity wasn't synced.") + .insert(environment_map_uniform); + } +} + +/// Gathers up all light probes of a single type in the scene and assigns them +/// to views, performing frustum culling and distance sorting in the process. +fn gather_light_probes( + image_assets: Res>, + light_probe_query: Extract>>, + view_query: Extract< + Query<(RenderEntity, &GlobalTransform, &Frustum, Option<&C>), With>, + >, + mut reflection_probes: Local>>, + mut view_reflection_probes: Local>>, + mut commands: Commands, +) where + C: LightProbeComponent, +{ + // Create [`LightProbeInfo`] for every light probe in the scene. + reflection_probes.clear(); + reflection_probes.extend( + light_probe_query + .iter() + .filter_map(|query_row| LightProbeInfo::new(query_row, &image_assets)), + ); + // Build up the light probes uniform and the key table. + for (view_entity, view_transform, view_frustum, view_component) in view_query.iter() { + // Cull light probes outside the view frustum. + view_reflection_probes.clear(); + view_reflection_probes.extend( + reflection_probes + .iter() + .filter(|light_probe_info| light_probe_info.frustum_cull(view_frustum)) + .cloned(), + ); + + // Sort by distance to camera. + view_reflection_probes.sort_by_cached_key(|light_probe_info| { + light_probe_info.camera_distance_sort_key(view_transform) + }); + + // Create the light probes list. + let mut render_view_light_probes = + C::create_render_view_light_probes(view_component, &image_assets); + + // Gather up the light probes in the list. + render_view_light_probes.maybe_gather_light_probes(&view_reflection_probes); + + // Record the per-view light probes. + if render_view_light_probes.is_empty() { + commands + .get_entity(view_entity) + .expect("View entity wasn't synced.") + .remove::>(); + } else { + commands + .get_entity(view_entity) + .expect("View entity wasn't synced.") + .insert(render_view_light_probes); + } + } +} + +/// Gathers up environment map settings for each applicable view and +/// writes them into a GPU buffer. +pub fn prepare_environment_uniform_buffer( + mut commands: Commands, + views: Query<(Entity, Option<&EnvironmentMapUniform>), With>, + mut environment_uniform_buffer: ResMut, + render_device: Res, + render_queue: Res, +) { + let Some(mut writer) = + environment_uniform_buffer.get_writer(views.iter().len(), &render_device, &render_queue) + else { + return; + }; + + for (view, environment_uniform) in views.iter() { + let uniform_offset = match environment_uniform { + None => 0, + Some(environment_uniform) => writer.write(environment_uniform), + }; + commands + .entity(view) + .insert(ViewEnvironmentMapUniformOffset(uniform_offset)); + } +} + +// A system that runs after [`gather_light_probes`] and populates the GPU +// uniforms with the results. +// +// Note that, unlike [`gather_light_probes`], this system is not generic over +// the type of light probe. It collects light probes of all types together into +// a single structure, ready to be passed to the shader. +fn upload_light_probes( + mut commands: Commands, + views: Query>, + mut light_probes_buffer: ResMut, + mut view_light_probes_query: Query<( + Option<&RenderViewLightProbes>, + Option<&RenderViewLightProbes>, + )>, + render_device: Res, + render_queue: Res, +) { + // If there are no views, bail. + if views.is_empty() { + return; + } + + // Initialize the uniform buffer writer. + let mut writer = light_probes_buffer + .get_writer(views.iter().len(), &render_device, &render_queue) + .unwrap(); + + // Process each view. + for view_entity in views.iter() { + let Ok((render_view_environment_maps, render_view_irradiance_volumes)) = + view_light_probes_query.get_mut(view_entity) + else { + error!("Failed to find `RenderViewLightProbes` for the view!"); + continue; + }; + + // Initialize the uniform with only the view environment map, if there + // is one. + let mut light_probes_uniform = LightProbesUniform { + reflection_probes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES], + irradiance_volumes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES], + reflection_probe_count: render_view_environment_maps + .map(RenderViewLightProbes::len) + .unwrap_or_default() + .min(MAX_VIEW_LIGHT_PROBES) as i32, + irradiance_volume_count: render_view_irradiance_volumes + .map(RenderViewLightProbes::len) + .unwrap_or_default() + .min(MAX_VIEW_LIGHT_PROBES) as i32, + view_cubemap_index: render_view_environment_maps + .map(|maps| maps.view_light_probe_info.cubemap_index) + .unwrap_or(-1), + smallest_specular_mip_level_for_view: render_view_environment_maps + .map(|maps| maps.view_light_probe_info.smallest_specular_mip_level) + .unwrap_or(0), + intensity_for_view: render_view_environment_maps + .map(|maps| maps.view_light_probe_info.intensity) + .unwrap_or(1.0), + view_environment_map_affects_lightmapped_mesh_diffuse: render_view_environment_maps + .map(|maps| maps.view_light_probe_info.affects_lightmapped_mesh_diffuse as u32) + .unwrap_or(1), + }; + + // Add any environment maps that [`gather_light_probes`] found to the + // uniform. + if let Some(render_view_environment_maps) = render_view_environment_maps { + render_view_environment_maps.add_to_uniform( + &mut light_probes_uniform.reflection_probes, + &mut light_probes_uniform.reflection_probe_count, + ); + } + + // Add any irradiance volumes that [`gather_light_probes`] found to the + // uniform. + if let Some(render_view_irradiance_volumes) = render_view_irradiance_volumes { + render_view_irradiance_volumes.add_to_uniform( + &mut light_probes_uniform.irradiance_volumes, + &mut light_probes_uniform.irradiance_volume_count, + ); + } + + // Queue the view's uniforms to be written to the GPU. + let uniform_offset = writer.write(&light_probes_uniform); + + commands + .entity(view_entity) + .insert(ViewLightProbesUniformOffset(uniform_offset)); + } +} + +impl Default for LightProbesUniform { + fn default() -> Self { + Self { + reflection_probes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES], + irradiance_volumes: [RenderLightProbe::default(); MAX_VIEW_LIGHT_PROBES], + reflection_probe_count: 0, + irradiance_volume_count: 0, + view_cubemap_index: -1, + smallest_specular_mip_level_for_view: 0, + intensity_for_view: 1.0, + view_environment_map_affects_lightmapped_mesh_diffuse: 1, + } + } +} + +impl LightProbeInfo +where + C: LightProbeComponent, +{ + /// Given the set of light probe components, constructs and returns + /// [`LightProbeInfo`]. This is done for every light probe in the scene + /// every frame. + fn new( + (light_probe_transform, environment_map): (&GlobalTransform, &C), + image_assets: &RenderAssets, + ) -> Option> { + let light_from_world_transposed = + Mat4::from(light_probe_transform.affine().inverse()).transpose(); + environment_map.id(image_assets).map(|id| LightProbeInfo { + world_from_light: light_probe_transform.affine(), + light_from_world: [ + light_from_world_transposed.x_axis, + light_from_world_transposed.y_axis, + light_from_world_transposed.z_axis, + ], + asset_id: id, + intensity: environment_map.intensity(), + affects_lightmapped_mesh_diffuse: environment_map.affects_lightmapped_mesh_diffuse(), + }) + } + + /// Returns true if this light probe is in the viewing frustum of the camera + /// or false if it isn't. + fn frustum_cull(&self, view_frustum: &Frustum) -> bool { + view_frustum.intersects_obb( + &Aabb { + center: Vec3A::default(), + half_extents: Vec3A::splat(0.5), + }, + &self.world_from_light, + true, + false, + ) + } + + /// Returns the squared distance from this light probe to the camera, + /// suitable for distance sorting. + fn camera_distance_sort_key(&self, view_transform: &GlobalTransform) -> FloatOrd { + FloatOrd( + (self.world_from_light.translation - view_transform.translation_vec3a()) + .length_squared(), + ) + } +} + +impl RenderViewLightProbes +where + C: LightProbeComponent, +{ + /// Creates a new empty list of light probes. + fn new() -> RenderViewLightProbes { + RenderViewLightProbes { + binding_index_to_textures: vec![], + cubemap_to_binding_index: HashMap::default(), + render_light_probes: vec![], + view_light_probe_info: C::ViewLightProbeInfo::default(), + } + } + + /// Returns true if there are no light probes in the list. + pub(crate) fn is_empty(&self) -> bool { + self.binding_index_to_textures.is_empty() + } + + /// Returns the number of light probes in the list. + pub(crate) fn len(&self) -> usize { + self.binding_index_to_textures.len() + } + + /// Adds a cubemap to the list of bindings, if it wasn't there already, and + /// returns its index within that list. + pub(crate) fn get_or_insert_cubemap(&mut self, cubemap_id: &C::AssetId) -> u32 { + *self + .cubemap_to_binding_index + .entry((*cubemap_id).clone()) + .or_insert_with(|| { + let index = self.binding_index_to_textures.len() as u32; + self.binding_index_to_textures.push((*cubemap_id).clone()); + index + }) + } + + /// Adds all the light probes in this structure to the supplied array, which + /// is expected to be shipped to the GPU. + fn add_to_uniform( + &self, + render_light_probes: &mut [RenderLightProbe; MAX_VIEW_LIGHT_PROBES], + render_light_probe_count: &mut i32, + ) { + render_light_probes[0..self.render_light_probes.len()] + .copy_from_slice(&self.render_light_probes[..]); + *render_light_probe_count = self.render_light_probes.len() as i32; + } + + /// Gathers up all light probes of the given type in the scene and records + /// them in this structure. + fn maybe_gather_light_probes(&mut self, light_probes: &[LightProbeInfo]) { + for light_probe in light_probes.iter().take(MAX_VIEW_LIGHT_PROBES) { + // Determine the index of the cubemap in the binding array. + let cubemap_index = self.get_or_insert_cubemap(&light_probe.asset_id); + + // Write in the light probe data. + self.render_light_probes.push(RenderLightProbe { + light_from_world_transposed: light_probe.light_from_world, + texture_index: cubemap_index as i32, + intensity: light_probe.intensity, + affects_lightmapped_mesh_diffuse: light_probe.affects_lightmapped_mesh_diffuse + as u32, + }); + } + } +} + +impl Clone for LightProbeInfo +where + C: LightProbeComponent, +{ + fn clone(&self) -> Self { + Self { + light_from_world: self.light_from_world, + world_from_light: self.world_from_light, + intensity: self.intensity, + affects_lightmapped_mesh_diffuse: self.affects_lightmapped_mesh_diffuse, + asset_id: self.asset_id.clone(), + } + } +} + +/// Adds a diffuse or specular texture view to the `texture_views` list, and +/// populates `sampler` if this is the first such view. +pub(crate) fn add_cubemap_texture_view<'a>( + texture_views: &mut Vec<&'a ::Target>, + sampler: &mut Option<&'a Sampler>, + image_id: AssetId, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, +) { + match images.get(image_id) { + None => { + // Use the fallback image if the cubemap isn't loaded yet. + texture_views.push(&*fallback_image.cube.texture_view); + } + Some(image) => { + // If this is the first texture view, populate `sampler`. + if sampler.is_none() { + *sampler = Some(&image.sampler); + } + + texture_views.push(&*image.texture_view); + } + } +} + +/// Many things can go wrong when attempting to use texture binding arrays +/// (a.k.a. bindless textures). This function checks for these pitfalls: +/// +/// 1. If GLSL support is enabled at the feature level, then in debug mode +/// `naga_oil` will attempt to compile all shader modules under GLSL to check +/// validity of names, even if GLSL isn't actually used. This will cause a crash +/// if binding arrays are enabled, because binding arrays are currently +/// unimplemented in the GLSL backend of Naga. Therefore, we disable binding +/// arrays if the `shader_format_glsl` feature is present. +/// +/// 2. If there aren't enough texture bindings available to accommodate all the +/// binding arrays, the driver will panic. So we also bail out if there aren't +/// enough texture bindings available in the fragment shader. +/// +/// 3. If binding arrays aren't supported on the hardware, then we obviously +/// can't use them. Adreno <= 610 claims to support bindless, but seems to be +/// too buggy to be usable. +/// +/// 4. If binding arrays are supported on the hardware, but they can only be +/// accessed by uniform indices, that's not good enough, and we bail out. +/// +/// If binding arrays aren't usable, we disable reflection probes and limit the +/// number of irradiance volumes in the scene to 1. +pub(crate) fn binding_arrays_are_usable( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, +) -> bool { + let adapter_info = RenderAdapterInfo(WgpuWrapper::new(render_adapter.get_info())); + + !cfg!(feature = "shader_format_glsl") + && crate::render::get_adreno_model(&adapter_info).is_none_or(|model| model > 610) + && render_device.limits().max_storage_textures_per_shader_stage + >= (STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS + MAX_VIEW_LIGHT_PROBES) + as u32 + && render_device.features().contains( + WgpuFeatures::TEXTURE_BINDING_ARRAY + | WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, + ) +} diff --git a/crates/libmarathon/src/render/pbr/lightmap/lightmap.wgsl b/crates/libmarathon/src/render/pbr/lightmap/lightmap.wgsl new file mode 100644 index 0000000..4ba6f51 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/lightmap/lightmap.wgsl @@ -0,0 +1,99 @@ +#define_import_path bevy_pbr::lightmap + +#import bevy_pbr::mesh_bindings::mesh + +#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY +@group(2) @binding(4) var lightmaps_textures: binding_array, 4>; +@group(2) @binding(5) var lightmaps_samplers: binding_array; +#else // MULTIPLE_LIGHTMAPS_IN_ARRAY +@group(2) @binding(4) var lightmaps_texture: texture_2d; +@group(2) @binding(5) var lightmaps_sampler: sampler; +#endif // MULTIPLE_LIGHTMAPS_IN_ARRAY + +// Samples the lightmap, if any, and returns indirect illumination from it. +fn lightmap(uv: vec2, exposure: f32, instance_index: u32) -> vec3 { + let packed_uv_rect = mesh[instance_index].lightmap_uv_rect; + let uv_rect = vec4( + unpack2x16unorm(packed_uv_rect.x), + unpack2x16unorm(packed_uv_rect.y), + ); + let lightmap_uv = mix(uv_rect.xy, uv_rect.zw, uv); + let lightmap_slot = mesh[instance_index].material_and_lightmap_bind_group_slot >> 16u; + + // Bicubic 4-tap + // https://developer.nvidia.com/gpugems/gpugems2/part-iii-high-quality-rendering/chapter-20-fast-third-order-texture-filtering + // https://advances.realtimerendering.com/s2021/jpatry_advances2021/index.html#/111/0/2 +#ifdef LIGHTMAP_BICUBIC_SAMPLING + let texture_size = vec2(lightmap_size(lightmap_slot)); + let texel_size = 1.0 / texture_size; + let puv = lightmap_uv * texture_size + 0.5; + let iuv = floor(puv); + let fuv = fract(puv); + let g0x = g0(fuv.x); + let g1x = g1(fuv.x); + let h0x = h0_approx(fuv.x); + let h1x = h1_approx(fuv.x); + let h0y = h0_approx(fuv.y); + let h1y = h1_approx(fuv.y); + let p0 = (vec2(iuv.x + h0x, iuv.y + h0y) - 0.5) * texel_size; + let p1 = (vec2(iuv.x + h1x, iuv.y + h0y) - 0.5) * texel_size; + let p2 = (vec2(iuv.x + h0x, iuv.y + h1y) - 0.5) * texel_size; + let p3 = (vec2(iuv.x + h1x, iuv.y + h1y) - 0.5) * texel_size; + let color = g0(fuv.y) * (g0x * sample(p0, lightmap_slot) + g1x * sample(p1, lightmap_slot)) + g1(fuv.y) * (g0x * sample(p2, lightmap_slot) + g1x * sample(p3, lightmap_slot)); +#else + let color = sample(lightmap_uv, lightmap_slot); +#endif + + return color * exposure; +} + +fn lightmap_size(lightmap_slot: u32) -> vec2 { +#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY + return textureDimensions(lightmaps_textures[lightmap_slot]); +#else + return textureDimensions(lightmaps_texture); +#endif +} + +fn sample(uv: vec2, lightmap_slot: u32) -> vec3 { + // Mipmapping lightmaps is usually a bad idea due to leaking across UV + // islands, so there's no harm in using mip level 0 and it lets us avoid + // control flow uniformity problems. +#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY + return textureSampleLevel(lightmaps_textures[lightmap_slot], lightmaps_samplers[lightmap_slot], uv, 0.0).rgb; +#else + return textureSampleLevel(lightmaps_texture, lightmaps_sampler, uv, 0.0).rgb; +#endif +} + +fn w0(a: f32) -> f32 { + return (1.0 / 6.0) * (a * (a * (-a + 3.0) - 3.0) + 1.0); +} + +fn w1(a: f32) -> f32 { + return (1.0 / 6.0) * (a * a * (3.0 * a - 6.0) + 4.0); +} + +fn w2(a: f32) -> f32 { + return (1.0 / 6.0) * (a * (a * (-3.0 * a + 3.0) + 3.0) + 1.0); +} + +fn w3(a: f32) -> f32 { + return (1.0 / 6.0) * (a * a * a); +} + +fn g0(a: f32) -> f32 { + return w0(a) + w1(a); +} + +fn g1(a: f32) -> f32 { + return w2(a) + w3(a); +} + +fn h0_approx(a: f32) -> f32 { + return -0.2 - a * (0.24 * a - 0.44); +} + +fn h1_approx(a: f32) -> f32 { + return 1.0 + a * (0.24 * a - 0.04); +} diff --git a/crates/libmarathon/src/render/pbr/lightmap/mod.rs b/crates/libmarathon/src/render/pbr/lightmap/mod.rs new file mode 100644 index 0000000..c0a873a --- /dev/null +++ b/crates/libmarathon/src/render/pbr/lightmap/mod.rs @@ -0,0 +1,519 @@ +//! Lightmaps, baked lighting textures that can be applied at runtime to provide +//! diffuse global illumination. +//! +//! Bevy doesn't currently have any way to actually bake lightmaps, but they can +//! be baked in an external tool like [Blender](http://blender.org), for example +//! with an addon like [The Lightmapper]. The tools in the [`bevy-baked-gi`] +//! project support other lightmap baking methods. +//! +//! When a [`Lightmap`] component is added to an entity with a [`Mesh3d`] and a +//! [`MeshMaterial3d`], Bevy applies the lightmap when rendering. The brightness +//! of the lightmap may be controlled with the `lightmap_exposure` field on +//! [`StandardMaterial`]. +//! +//! During the rendering extraction phase, we extract all lightmaps into the +//! [`RenderLightmaps`] table, which lives in the render world. Mesh bindgroup +//! and mesh uniform creation consults this table to determine which lightmap to +//! supply to the shader. Essentially, the lightmap is a special type of texture +//! that is part of the mesh instance rather than part of the material (because +//! multiple meshes can share the same material, whereas sharing lightmaps is +//! nonsensical). +//! +//! Note that multiple meshes can't be drawn in a single drawcall if they use +//! different lightmap textures, unless bindless textures are in use. If you +//! want to instance a lightmapped mesh, and your platform doesn't support +//! bindless textures, combine the lightmap textures into a single atlas, and +//! set the `uv_rect` field on [`Lightmap`] appropriately. +//! +//! [The Lightmapper]: https://github.com/Naxela/The_Lightmapper +//! [`Mesh3d`]: bevy_mesh::Mesh3d +//! [`MeshMaterial3d`]: crate::StandardMaterial +//! [`StandardMaterial`]: crate::StandardMaterial +//! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi + +use bevy_app::{App, Plugin}; +use bevy_asset::{AssetId, Handle}; +use bevy_camera::visibility::ViewVisibility; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + lifecycle::RemovedComponents, + query::{Changed, Or}, + reflect::ReflectComponent, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_image::Image; +use bevy_math::{uvec2, vec4, Rect, UVec2}; +use bevy_platform::collections::HashSet; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use crate::render::{ + render_asset::RenderAssets, + render_resource::{Sampler, TextureView, WgpuSampler, WgpuTextureView}, + renderer::RenderAdapter, + sync_world::MainEntity, + texture::{FallbackImage, GpuImage}, + Extract, ExtractSchedule, RenderApp, RenderStartup, +}; +use crate::render::{renderer::RenderDevice, sync_world::MainEntityHashMap}; +use bevy_shader::load_shader_library; +use bevy_utils::default; +use fixedbitset::FixedBitSet; +use nonmax::{NonMaxU16, NonMaxU32}; +use tracing::error; + +use crate::render::pbr::{binding_arrays_are_usable, MeshExtractionSystems}; + +/// The number of lightmaps that we store in a single slab, if bindless textures +/// are in use. +/// +/// If bindless textures aren't in use, then only a single lightmap can be bound +/// at a time. +pub const LIGHTMAPS_PER_SLAB: usize = 4; + +/// A plugin that provides an implementation of lightmaps. +pub struct LightmapPlugin; + +/// A component that applies baked indirect diffuse global illumination from a +/// lightmap. +/// +/// When assigned to an entity that contains a [`Mesh3d`](bevy_mesh::Mesh3d) and a +/// [`MeshMaterial3d`](crate::StandardMaterial), if the mesh +/// has a second UV layer ([`ATTRIBUTE_UV_1`](bevy_mesh::Mesh::ATTRIBUTE_UV_1)), +/// then the lightmap will render using those UVs. +#[derive(Component, Clone, Reflect)] +#[reflect(Component, Default, Clone)] +pub struct Lightmap { + /// The lightmap texture. + pub image: Handle, + + /// The rectangle within the lightmap texture that the UVs are relative to. + /// + /// The top left coordinate is the `min` part of the rect, and the bottom + /// right coordinate is the `max` part of the rect. The rect ranges from (0, + /// 0) to (1, 1). + /// + /// This field allows lightmaps for a variety of meshes to be packed into a + /// single atlas. + pub uv_rect: Rect, + + /// Whether bicubic sampling should be used for sampling this lightmap. + /// + /// Bicubic sampling is higher quality, but slower, and may lead to light leaks. + /// + /// If true, the lightmap texture's sampler must be set to [`bevy_image::ImageSampler::linear`]. + pub bicubic_sampling: bool, +} + +/// Lightmap data stored in the render world. +/// +/// There is one of these per visible lightmapped mesh instance. +#[derive(Debug)] +pub(crate) struct RenderLightmap { + /// The rectangle within the lightmap texture that the UVs are relative to. + /// + /// The top left coordinate is the `min` part of the rect, and the bottom + /// right coordinate is the `max` part of the rect. The rect ranges from (0, + /// 0) to (1, 1). + pub(crate) uv_rect: Rect, + + /// The index of the slab (i.e. binding array) in which the lightmap is + /// located. + pub(crate) slab_index: LightmapSlabIndex, + + /// The index of the slot (i.e. element within the binding array) in which + /// the lightmap is located. + /// + /// If bindless lightmaps aren't in use, this will be 0. + pub(crate) slot_index: LightmapSlotIndex, + + // Whether or not bicubic sampling should be used for this lightmap. + pub(crate) bicubic_sampling: bool, +} + +/// Stores data for all lightmaps in the render world. +/// +/// This is cleared and repopulated each frame during the `extract_lightmaps` +/// system. +#[derive(Resource)] +pub struct RenderLightmaps { + /// The mapping from every lightmapped entity to its lightmap info. + /// + /// Entities without lightmaps, or for which the mesh or lightmap isn't + /// loaded, won't have entries in this table. + pub(crate) render_lightmaps: MainEntityHashMap, + + /// The slabs (binding arrays) containing the lightmaps. + pub(crate) slabs: Vec, + + free_slabs: FixedBitSet, + + pending_lightmaps: HashSet<(LightmapSlabIndex, LightmapSlotIndex)>, + + /// Whether bindless textures are supported on this platform. + pub(crate) bindless_supported: bool, +} + +/// A binding array that contains lightmaps. +/// +/// This will have a single binding if bindless lightmaps aren't in use. +pub struct LightmapSlab { + /// The GPU images in this slab. + lightmaps: Vec, + free_slots_bitmask: u32, +} + +struct AllocatedLightmap { + gpu_image: GpuImage, + // This will only be present if the lightmap is allocated but not loaded. + asset_id: Option>, +} + +/// The index of the slab (binding array) in which a lightmap is located. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deref, DerefMut)] +#[repr(transparent)] +pub struct LightmapSlabIndex(pub(crate) NonMaxU32); + +/// The index of the slot (element within the binding array) in the slab in +/// which a lightmap is located. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Deref, DerefMut)] +#[repr(transparent)] +pub struct LightmapSlotIndex(pub(crate) NonMaxU16); + +impl Plugin for LightmapPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "lightmap.wgsl"); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app + .add_systems(RenderStartup, init_render_lightmaps) + .add_systems( + ExtractSchedule, + extract_lightmaps.after(MeshExtractionSystems), + ); + } +} + +/// Extracts all lightmaps from the scene and populates the [`RenderLightmaps`] +/// resource. +fn extract_lightmaps( + render_lightmaps: ResMut, + changed_lightmaps_query: Extract< + Query< + (Entity, &ViewVisibility, &Lightmap), + Or<(Changed, Changed)>, + >, + >, + mut removed_lightmaps_query: Extract>, + images: Res>, + fallback_images: Res, +) { + let render_lightmaps = render_lightmaps.into_inner(); + + // Loop over each entity. + for (entity, view_visibility, lightmap) in changed_lightmaps_query.iter() { + if render_lightmaps + .render_lightmaps + .contains_key(&MainEntity::from(entity)) + { + continue; + } + + // Only process visible entities. + if !view_visibility.get() { + continue; + } + + let (slab_index, slot_index) = + render_lightmaps.allocate(&fallback_images, lightmap.image.id()); + render_lightmaps.render_lightmaps.insert( + entity.into(), + RenderLightmap::new( + lightmap.uv_rect, + slab_index, + slot_index, + lightmap.bicubic_sampling, + ), + ); + + render_lightmaps + .pending_lightmaps + .insert((slab_index, slot_index)); + } + + for entity in removed_lightmaps_query.read() { + if changed_lightmaps_query.contains(entity) { + continue; + } + + let Some(RenderLightmap { + slab_index, + slot_index, + .. + }) = render_lightmaps + .render_lightmaps + .remove(&MainEntity::from(entity)) + else { + continue; + }; + + render_lightmaps.remove(&fallback_images, slab_index, slot_index); + render_lightmaps + .pending_lightmaps + .remove(&(slab_index, slot_index)); + } + + render_lightmaps + .pending_lightmaps + .retain(|&(slab_index, slot_index)| { + let Some(asset_id) = render_lightmaps.slabs[usize::from(slab_index)].lightmaps + [usize::from(slot_index)] + .asset_id + else { + error!( + "Allocated lightmap should have been removed from `pending_lightmaps` by now" + ); + return false; + }; + + let Some(gpu_image) = images.get(asset_id) else { + return true; + }; + render_lightmaps.slabs[usize::from(slab_index)].insert(slot_index, gpu_image.clone()); + false + }); +} + +impl RenderLightmap { + /// Creates a new lightmap from a texture, a UV rect, and a slab and slot + /// index pair. + fn new( + uv_rect: Rect, + slab_index: LightmapSlabIndex, + slot_index: LightmapSlotIndex, + bicubic_sampling: bool, + ) -> Self { + Self { + uv_rect, + slab_index, + slot_index, + bicubic_sampling, + } + } +} + +/// Packs the lightmap UV rect into 64 bits (4 16-bit unsigned integers). +pub(crate) fn pack_lightmap_uv_rect(maybe_rect: Option) -> UVec2 { + match maybe_rect { + Some(rect) => { + let rect_uvec4 = (vec4(rect.min.x, rect.min.y, rect.max.x, rect.max.y) * 65535.0) + .round() + .as_uvec4(); + uvec2( + rect_uvec4.x | (rect_uvec4.y << 16), + rect_uvec4.z | (rect_uvec4.w << 16), + ) + } + None => UVec2::ZERO, + } +} + +impl Default for Lightmap { + fn default() -> Self { + Self { + image: Default::default(), + uv_rect: Rect::new(0.0, 0.0, 1.0, 1.0), + bicubic_sampling: false, + } + } +} + +pub fn init_render_lightmaps( + mut commands: Commands, + render_device: Res, + render_adapter: Res, +) { + let bindless_supported = binding_arrays_are_usable(&render_device, &render_adapter); + + commands.insert_resource(RenderLightmaps { + render_lightmaps: default(), + slabs: vec![], + free_slabs: FixedBitSet::new(), + pending_lightmaps: default(), + bindless_supported, + }); +} + +impl RenderLightmaps { + /// Creates a new slab, appends it to the end of the list, and returns its + /// slab index. + fn create_slab(&mut self, fallback_images: &FallbackImage) -> LightmapSlabIndex { + let slab_index = LightmapSlabIndex::from(self.slabs.len()); + self.free_slabs.grow_and_insert(slab_index.into()); + self.slabs + .push(LightmapSlab::new(fallback_images, self.bindless_supported)); + slab_index + } + + fn allocate( + &mut self, + fallback_images: &FallbackImage, + image_id: AssetId, + ) -> (LightmapSlabIndex, LightmapSlotIndex) { + let slab_index = match self.free_slabs.minimum() { + None => self.create_slab(fallback_images), + Some(slab_index) => slab_index.into(), + }; + + let slab = &mut self.slabs[usize::from(slab_index)]; + let slot_index = slab.allocate(image_id); + if slab.is_full() { + self.free_slabs.remove(slab_index.into()); + } + + (slab_index, slot_index) + } + + fn remove( + &mut self, + fallback_images: &FallbackImage, + slab_index: LightmapSlabIndex, + slot_index: LightmapSlotIndex, + ) { + let slab = &mut self.slabs[usize::from(slab_index)]; + slab.remove(fallback_images, slot_index); + + if !slab.is_full() { + self.free_slabs.grow_and_insert(slot_index.into()); + } + } +} + +impl LightmapSlab { + fn new(fallback_images: &FallbackImage, bindless_supported: bool) -> LightmapSlab { + let count = if bindless_supported { + LIGHTMAPS_PER_SLAB + } else { + 1 + }; + + LightmapSlab { + lightmaps: (0..count) + .map(|_| AllocatedLightmap { + gpu_image: fallback_images.d2.clone(), + asset_id: None, + }) + .collect(), + free_slots_bitmask: (1 << count) - 1, + } + } + + fn is_full(&self) -> bool { + self.free_slots_bitmask == 0 + } + + fn allocate(&mut self, image_id: AssetId) -> LightmapSlotIndex { + let index = LightmapSlotIndex::from(self.free_slots_bitmask.trailing_zeros()); + self.free_slots_bitmask &= !(1 << u32::from(index)); + self.lightmaps[usize::from(index)].asset_id = Some(image_id); + index + } + + fn insert(&mut self, index: LightmapSlotIndex, gpu_image: GpuImage) { + self.lightmaps[usize::from(index)] = AllocatedLightmap { + gpu_image, + asset_id: None, + } + } + + fn remove(&mut self, fallback_images: &FallbackImage, index: LightmapSlotIndex) { + self.lightmaps[usize::from(index)] = AllocatedLightmap { + gpu_image: fallback_images.d2.clone(), + asset_id: None, + }; + self.free_slots_bitmask |= 1 << u32::from(index); + } + + /// Returns the texture views and samplers for the lightmaps in this slab, + /// ready to be placed into a bind group. + /// + /// This is used when constructing bind groups in bindless mode. Before + /// returning, this function pads out the arrays with fallback images in + /// order to fulfill requirements of platforms that require full binding + /// arrays (e.g. DX12). + pub(crate) fn build_binding_arrays(&self) -> (Vec<&WgpuTextureView>, Vec<&WgpuSampler>) { + ( + self.lightmaps + .iter() + .map(|allocated_lightmap| &*allocated_lightmap.gpu_image.texture_view) + .collect(), + self.lightmaps + .iter() + .map(|allocated_lightmap| &*allocated_lightmap.gpu_image.sampler) + .collect(), + ) + } + + /// Returns the texture view and sampler corresponding to the first + /// lightmap, which must exist. + /// + /// This is used when constructing bind groups in non-bindless mode. + pub(crate) fn bindings_for_first_lightmap(&self) -> (&TextureView, &Sampler) { + ( + &self.lightmaps[0].gpu_image.texture_view, + &self.lightmaps[0].gpu_image.sampler, + ) + } +} + +impl From for LightmapSlabIndex { + fn from(value: u32) -> Self { + Self(NonMaxU32::new(value).unwrap()) + } +} + +impl From for LightmapSlabIndex { + fn from(value: usize) -> Self { + Self::from(value as u32) + } +} + +impl From for LightmapSlotIndex { + fn from(value: u32) -> Self { + Self(NonMaxU16::new(value as u16).unwrap()) + } +} + +impl From for LightmapSlotIndex { + fn from(value: usize) -> Self { + Self::from(value as u32) + } +} + +impl From for usize { + fn from(value: LightmapSlabIndex) -> Self { + value.0.get() as usize + } +} + +impl From for usize { + fn from(value: LightmapSlotIndex) -> Self { + value.0.get() as usize + } +} + +impl From for u16 { + fn from(value: LightmapSlotIndex) -> Self { + value.0.get() + } +} + +impl From for u32 { + fn from(value: LightmapSlotIndex) -> Self { + value.0.get() as u32 + } +} diff --git a/crates/libmarathon/src/render/pbr/material.rs b/crates/libmarathon/src/render/pbr/material.rs new file mode 100644 index 0000000..286f73b --- /dev/null +++ b/crates/libmarathon/src/render/pbr/material.rs @@ -0,0 +1,1823 @@ +use crate::render::pbr::material_bind_groups::{ + FallbackBindlessResources, MaterialBindGroupAllocator, MaterialBindingId, +}; +use crate::render::pbr::*; +use std::sync::Arc; +use bevy_asset::prelude::AssetChanged; +use bevy_asset::{Asset, AssetEventSystems, AssetId, AssetServer, UntypedAssetId}; +use bevy_camera::visibility::ViewVisibility; +use bevy_camera::ScreenSpaceTransmissionQuality; +use crate::render::deferred::{AlphaMask3dDeferred, Opaque3dDeferred}; +use crate::render::prepass::{AlphaMask3dPrepass, Opaque3dPrepass}; +use crate::render::{ + core_3d::{ + AlphaMask3d, Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, Transmissive3d, Transparent3d, + }, + prepass::{OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey}, + tonemapping::Tonemapping, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Tick; +use bevy_ecs::system::SystemChangeTick; +use bevy_ecs::{ + prelude::*, + system::{ + lifetimeless::{SRes, SResMut}, + SystemParamItem, + }, +}; +use bevy_mesh::{ + mark_3d_meshes_as_changed_if_their_assets_changed, Mesh3d, MeshVertexBufferLayoutRef, +}; +use bevy_platform::collections::hash_map::Entry; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_platform::hash::FixedHasher; +use bevy_reflect::std_traits::ReflectDefault; +use bevy_reflect::Reflect; +use crate::render::camera::extract_cameras; +use crate::render::erased_render_asset::{ + ErasedRenderAsset, ErasedRenderAssetPlugin, ErasedRenderAssets, PrepareAssetError, +}; +use crate::render::render_asset::{prepare_assets, RenderAssets}; +use crate::render::renderer::RenderQueue; +use crate::render::RenderStartup; +use crate::render::{ + batching::gpu_preprocessing::GpuPreprocessingSupport, + extract_resource::ExtractResource, + mesh::RenderMesh, + prelude::*, + render_phase::*, + render_resource::*, + renderer::RenderDevice, + sync_world::MainEntity, + view::{ExtractedView, Msaa, RenderVisibilityRanges, RetainedViewEntity}, + Extract, +}; +use crate::render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap}; +use crate::render::{texture::FallbackImage, view::RenderVisibleEntities}; +use bevy_shader::{Shader, ShaderDefVal}; +use bevy_utils::Parallel; +use core::any::{Any, TypeId}; +use core::hash::{BuildHasher, Hasher}; +use core::{hash::Hash, marker::PhantomData}; +use smallvec::SmallVec; +use tracing::error; + +pub const MATERIAL_BIND_GROUP_INDEX: usize = 3; + +/// Materials are used alongside [`MaterialPlugin`], [`Mesh3d`], and [`MeshMaterial3d`] +/// to spawn entities that are rendered with a specific [`Material`] type. They serve as an easy to use high level +/// way to render [`Mesh3d`] entities with custom shader logic. +/// +/// Materials must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders. +/// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details. +/// +/// # Example +/// +/// Here is a simple [`Material`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available, +/// check out the [`AsBindGroup`] documentation. +/// +/// ``` +/// # use bevy_pbr::{Material, MeshMaterial3d}; +/// # use bevy_ecs::prelude::*; +/// # use bevy_image::Image; +/// # use bevy_reflect::TypePath; +/// # use bevy_mesh::{Mesh, Mesh3d}; +/// # use crate::render::render_resource::AsBindGroup; +/// # use bevy_shader::ShaderRef; +/// # use bevy_color::LinearRgba; +/// # use bevy_color::palettes::basic::RED; +/// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; +/// # use bevy_math::primitives::Capsule3d; +/// # +/// #[derive(AsBindGroup, Debug, Clone, Asset, TypePath)] +/// pub struct CustomMaterial { +/// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to +/// // its shader-compatible equivalent. Most core math types already implement `ShaderType`. +/// #[uniform(0)] +/// color: LinearRgba, +/// // Images can be bound as textures in shaders. If the Image's sampler is also needed, just +/// // add the sampler attribute with a different binding index. +/// #[texture(1)] +/// #[sampler(2)] +/// color_texture: Handle, +/// } +/// +/// // All functions on `Material` have default impls. You only need to implement the +/// // functions that are relevant for your material. +/// impl Material for CustomMaterial { +/// fn fragment_shader() -> ShaderRef { +/// "shaders/custom_material.wgsl".into() +/// } +/// } +/// +/// // Spawn an entity with a mesh using `CustomMaterial`. +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// asset_server: Res +/// ) { +/// commands.spawn(( +/// Mesh3d(meshes.add(Capsule3d::default())), +/// MeshMaterial3d(materials.add(CustomMaterial { +/// color: RED.into(), +/// color_texture: asset_server.load("some_image.png"), +/// })), +/// )); +/// } +/// ``` +/// +/// In WGSL shaders, the material's binding would look like this: +/// +/// ```wgsl +/// @group(#{MATERIAL_BIND_GROUP}) @binding(0) var color: vec4; +/// @group(#{MATERIAL_BIND_GROUP}) @binding(1) var color_texture: texture_2d; +/// @group(#{MATERIAL_BIND_GROUP}) @binding(2) var color_sampler: sampler; +/// ``` +pub trait Material: Asset + AsBindGroup + Clone + Sized { + /// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the default mesh vertex shader + /// will be used. + fn vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the default mesh fragment shader + /// will be used. + fn fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`AlphaMode`]. Defaults to [`AlphaMode::Opaque`]. + #[inline] + fn alpha_mode(&self) -> AlphaMode { + AlphaMode::Opaque + } + + /// Returns if this material should be rendered by the deferred or forward renderer. + /// for `AlphaMode::Opaque` or `AlphaMode::Mask` materials. + /// If `OpaqueRendererMethod::Auto`, it will default to what is selected in the `DefaultOpaqueRendererMethod` resource. + #[inline] + fn opaque_render_method(&self) -> OpaqueRendererMethod { + OpaqueRendererMethod::Forward + } + + #[inline] + /// Add a bias to the view depth of the mesh which can be used to force a specific render order. + /// for meshes with similar depth, to avoid z-fighting. + /// The bias is in depth-texture units so large values may be needed to overcome small depth differences. + fn depth_bias(&self) -> f32 { + 0.0 + } + + #[inline] + /// Returns whether the material would like to read from [`ViewTransmissionTexture`](bevy_core_pipeline::core_3d::ViewTransmissionTexture). + /// + /// This allows taking color output from the [`Opaque3d`] pass as an input, (for screen-space transmission) but requires + /// rendering to take place in a separate [`Transmissive3d`] pass. + fn reads_view_transmission_texture(&self) -> bool { + false + } + + /// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the default prepass vertex shader + /// will be used. + /// + /// This is used for the various [prepasses](bevy_core_pipeline::prepass) as well as for generating the depth maps + /// required for shadow mapping. + fn prepass_vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the default prepass fragment shader + /// will be used. + /// + /// This is used for the various [prepasses](bevy_core_pipeline::prepass) as well as for generating the depth maps + /// required for shadow mapping. + fn prepass_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's deferred vertex shader. If [`ShaderRef::Default`] is returned, the default deferred vertex shader + /// will be used. + fn deferred_vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's deferred fragment shader. If [`ShaderRef::Default`] is returned, the default deferred fragment shader + /// will be used. + fn deferred_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh fragment shader will be used. + /// + /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. + /// + /// See [`crate::meshlet::MeshletMesh`] for limitations. + #[cfg(feature = "meshlet")] + fn meshlet_mesh_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] prepass fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh prepass fragment shader will be used. + /// + /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. + /// + /// See [`crate::meshlet::MeshletMesh`] for limitations. + #[cfg(feature = "meshlet")] + fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's [`crate::meshlet::MeshletMesh`] deferred fragment shader. If [`ShaderRef::Default`] is returned, + /// the default meshlet mesh deferred fragment shader will be used. + /// + /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. + /// + /// See [`crate::meshlet::MeshletMesh`] for limitations. + #[cfg(feature = "meshlet")] + fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's + /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayoutRef`] as input. + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + #[inline] + fn specialize( + pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + layout: &MeshVertexBufferLayoutRef, + key: MaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> { + Ok(()) + } +} + +#[derive(Default)] +pub struct MaterialsPlugin { + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl Plugin for MaterialsPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((PrepassPipelinePlugin, PrepassPlugin::new(self.debug_flags))); + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::() + .init_resource::>() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::>() + .init_resource::() + .init_resource::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_systems(RenderStartup, init_material_pipeline) + .add_systems( + Render, + ( + specialize_material_meshes + .in_set(RenderSystems::PrepareMeshes) + .after(prepare_assets::) + .after(collect_meshes_for_gpu_building) + .after(set_mesh_motion_vector_flags), + queue_material_meshes.in_set(RenderSystems::QueueMeshes), + ), + ) + .add_systems( + Render, + ( + prepare_material_bind_groups, + write_material_bind_group_buffers, + ) + .chain() + .in_set(RenderSystems::PrepareBindGroups), + ) + .add_systems( + Render, + ( + check_views_lights_need_specialization.in_set(RenderSystems::PrepareAssets), + // specialize_shadows also needs to run after prepare_assets::, + // which is fine since ManageViews is after PrepareAssets + specialize_shadows + .in_set(RenderSystems::ManageViews) + .after(prepare_lights), + queue_shadows.in_set(RenderSystems::QueueMeshes), + ), + ); + } + } +} + +/// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material`] +/// asset type. +pub struct MaterialPlugin { + /// Controls if the prepass is enabled for the Material. + /// For more information about what a prepass is, see the [`bevy_core_pipeline::prepass`] docs. + /// + /// When it is enabled, it will automatically add the [`PrepassPlugin`] + /// required to make the prepass work on this Material. + pub prepass_enabled: bool, + /// Controls if shadows are enabled for the Material. + pub shadows_enabled: bool, + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, + pub _marker: PhantomData, +} + +impl Default for MaterialPlugin { + fn default() -> Self { + Self { + prepass_enabled: true, + shadows_enabled: true, + debug_flags: RenderDebugFlags::default(), + _marker: Default::default(), + } + } +} + +impl Plugin for MaterialPlugin +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + fn build(&self, app: &mut App) { + app.init_asset::() + .register_type::>() + .init_resource::>() + .add_plugins((ErasedRenderAssetPlugin::>::default(),)) + .add_systems( + PostUpdate, + ( + mark_meshes_as_changed_if_their_materials_changed::.ambiguous_with_all(), + check_entities_needing_specialization::.after(AssetEventSystems), + ) + .after(mark_3d_meshes_as_changed_if_their_assets_changed), + ); + + if self.shadows_enabled { + app.add_systems( + PostUpdate, + check_light_entities_needing_specialization:: + .after(check_entities_needing_specialization::), + ); + } + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + if self.prepass_enabled { + render_app.init_resource::>(); + } + if self.shadows_enabled { + render_app.init_resource::>(); + } + + render_app + .add_systems(RenderStartup, add_material_bind_group_allocator::) + .add_systems( + ExtractSchedule, + ( + extract_mesh_materials::.in_set(MaterialExtractionSystems), + early_sweep_material_instances:: + .after(MaterialExtractionSystems) + .before(late_sweep_material_instances), + extract_entities_needs_specialization:: + .after(extract_cameras) + .after(MaterialExtractionSystems), + ), + ); + } + } +} + +fn add_material_bind_group_allocator( + render_device: Res, + mut bind_group_allocators: ResMut, +) { + bind_group_allocators.insert( + TypeId::of::(), + MaterialBindGroupAllocator::new( + &render_device, + M::label(), + material_uses_bindless_resources::(&render_device) + .then(|| M::bindless_descriptor()) + .flatten(), + M::bind_group_layout(&render_device), + M::bindless_slot_count(), + ), + ); +} + +/// A dummy [`AssetId`] that we use as a placeholder whenever a mesh doesn't +/// have a material. +/// +/// See the comments in [`RenderMaterialInstances::mesh_material`] for more +/// information. +pub(crate) static DUMMY_MESH_MATERIAL: AssetId = + AssetId::::invalid(); + +/// A key uniquely identifying a specialized [`MaterialPipeline`]. +pub struct MaterialPipelineKey { + pub mesh_key: MeshPipelineKey, + pub bind_group_data: M::Data, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct ErasedMaterialPipelineKey { + pub mesh_key: MeshPipelineKey, + pub material_key: ErasedMaterialKey, + pub type_id: TypeId, +} + +/// Render pipeline data for a given [`Material`]. +#[derive(Resource, Clone)] +pub struct MaterialPipeline { + pub mesh_pipeline: MeshPipeline, +} + +pub struct MaterialPipelineSpecializer { + pub(crate) pipeline: MaterialPipeline, + pub(crate) properties: Arc, +} + +impl SpecializedMeshPipeline for MaterialPipelineSpecializer { + type Key = ErasedMaterialPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + let mut descriptor = self + .pipeline + .mesh_pipeline + .specialize(key.mesh_key, layout)?; + descriptor.vertex.shader_defs.push(ShaderDefVal::UInt( + "MATERIAL_BIND_GROUP".into(), + MATERIAL_BIND_GROUP_INDEX as u32, + )); + if let Some(ref mut fragment) = descriptor.fragment { + fragment.shader_defs.push(ShaderDefVal::UInt( + "MATERIAL_BIND_GROUP".into(), + MATERIAL_BIND_GROUP_INDEX as u32, + )); + }; + if let Some(vertex_shader) = self.properties.get_shader(MaterialVertexShader) { + descriptor.vertex.shader = vertex_shader.clone(); + } + + if let Some(fragment_shader) = self.properties.get_shader(MaterialFragmentShader) { + descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); + } + + descriptor + .layout + .insert(3, self.properties.material_layout.as_ref().unwrap().clone()); + + if let Some(specialize) = self.properties.specialize { + specialize(&self.pipeline, &mut descriptor, layout, key)?; + } + + // If bindless mode is on, add a `BINDLESS` define. + if self.properties.bindless { + descriptor.vertex.shader_defs.push("BINDLESS".into()); + if let Some(ref mut fragment) = descriptor.fragment { + fragment.shader_defs.push("BINDLESS".into()); + } + } + + Ok(descriptor) + } +} + +pub fn init_material_pipeline(mut commands: Commands, mesh_pipeline: Res) { + commands.insert_resource(MaterialPipeline { + mesh_pipeline: mesh_pipeline.clone(), + }); +} + +pub type DrawMaterial = ( + SetItemPipeline, + SetMeshViewBindGroup<0>, + SetMeshViewBindingArrayBindGroup<1>, + SetMeshBindGroup<2>, + SetMaterialBindGroup, + DrawMesh, +); + +/// Sets the bind group for a given [`Material`] at the configured `I` index. +pub struct SetMaterialBindGroup; +impl RenderCommand

    for SetMaterialBindGroup { + type Param = ( + SRes>, + SRes, + SRes, + ); + type ViewQuery = (); + type ItemQuery = (); + + #[inline] + fn render<'w>( + item: &P, + _view: (), + _item_query: Option<()>, + (materials, material_instances, material_bind_group_allocator): SystemParamItem< + 'w, + '_, + Self::Param, + >, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let materials = materials.into_inner(); + let material_instances = material_instances.into_inner(); + let material_bind_group_allocators = material_bind_group_allocator.into_inner(); + + let Some(material_instance) = material_instances.instances.get(&item.main_entity()) else { + return RenderCommandResult::Skip; + }; + let Some(material_bind_group_allocator) = + material_bind_group_allocators.get(&material_instance.asset_id.type_id()) + else { + return RenderCommandResult::Skip; + }; + let Some(material) = materials.get(material_instance.asset_id) else { + return RenderCommandResult::Skip; + }; + let Some(material_bind_group) = material_bind_group_allocator.get(material.binding.group) + else { + return RenderCommandResult::Skip; + }; + let Some(bind_group) = material_bind_group.bind_group() else { + return RenderCommandResult::Skip; + }; + pass.set_bind_group(I, bind_group, &[]); + RenderCommandResult::Success + } +} + +/// Stores all extracted instances of all [`Material`]s in the render world. +#[derive(Resource, Default)] +pub struct RenderMaterialInstances { + /// Maps from each entity in the main world to the + /// [`RenderMaterialInstance`] associated with it. + pub instances: MainEntityHashMap, + /// A monotonically-increasing counter, which we use to sweep + /// [`RenderMaterialInstances::instances`] when the entities and/or required + /// components are removed. + pub current_change_tick: Tick, +} + +impl RenderMaterialInstances { + /// Returns the mesh material ID for the entity with the given mesh, or a + /// dummy mesh material ID if the mesh has no material ID. + /// + /// Meshes almost always have materials, but in very specific circumstances + /// involving custom pipelines they won't. (See the + /// `specialized_mesh_pipelines` example.) + pub(crate) fn mesh_material(&self, entity: MainEntity) -> UntypedAssetId { + match self.instances.get(&entity) { + Some(render_instance) => render_instance.asset_id, + None => DUMMY_MESH_MATERIAL.into(), + } + } +} + +/// The material associated with a single mesh instance in the main world. +/// +/// Note that this uses an [`UntypedAssetId`] and isn't generic over the +/// material type, for simplicity. +pub struct RenderMaterialInstance { + /// The material asset. + pub asset_id: UntypedAssetId, + /// The [`RenderMaterialInstances::current_change_tick`] at which this + /// material instance was last modified. + pub last_change_tick: Tick, +} + +/// A [`SystemSet`] that contains all `extract_mesh_materials` systems. +#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] +pub struct MaterialExtractionSystems; + +/// Deprecated alias for [`MaterialExtractionSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `MaterialExtractionSystems`.")] +pub type ExtractMaterialsSet = MaterialExtractionSystems; + +pub const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode, msaa: &Msaa) -> MeshPipelineKey { + match alpha_mode { + // Premultiplied and Add share the same pipeline key + // They're made distinct in the PBR shader, via `premultiply_alpha()` + AlphaMode::Premultiplied | AlphaMode::Add => MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA, + AlphaMode::Blend => MeshPipelineKey::BLEND_ALPHA, + AlphaMode::Multiply => MeshPipelineKey::BLEND_MULTIPLY, + AlphaMode::Mask(_) => MeshPipelineKey::MAY_DISCARD, + AlphaMode::AlphaToCoverage => match *msaa { + Msaa::Off => MeshPipelineKey::MAY_DISCARD, + _ => MeshPipelineKey::BLEND_ALPHA_TO_COVERAGE, + }, + _ => MeshPipelineKey::NONE, + } +} + +pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> MeshPipelineKey { + match tonemapping { + Tonemapping::None => MeshPipelineKey::TONEMAP_METHOD_NONE, + Tonemapping::Reinhard => MeshPipelineKey::TONEMAP_METHOD_REINHARD, + Tonemapping::ReinhardLuminance => MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE, + Tonemapping::AcesFitted => MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED, + Tonemapping::AgX => MeshPipelineKey::TONEMAP_METHOD_AGX, + Tonemapping::SomewhatBoringDisplayTransform => { + MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + } + Tonemapping::TonyMcMapface => MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE, + Tonemapping::BlenderFilmic => MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC, + } +} + +pub const fn screen_space_specular_transmission_pipeline_key( + screen_space_transmissive_blur_quality: ScreenSpaceTransmissionQuality, +) -> MeshPipelineKey { + match screen_space_transmissive_blur_quality { + ScreenSpaceTransmissionQuality::Low => { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW + } + ScreenSpaceTransmissionQuality::Medium => { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_MEDIUM + } + ScreenSpaceTransmissionQuality::High => { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_HIGH + } + ScreenSpaceTransmissionQuality::Ultra => { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_ULTRA + } + } +} + +/// A system that ensures that +/// [`crate::render::mesh::extract_meshes_for_gpu_building`] re-extracts meshes +/// whose materials changed. +/// +/// As [`crate::render::mesh::collect_meshes_for_gpu_building`] only considers +/// meshes that were newly extracted, and it writes information from the +/// [`RenderMaterialInstances`] into the +/// [`crate::render::mesh::MeshInputUniform`], we must tell +/// [`crate::render::mesh::extract_meshes_for_gpu_building`] to re-extract a +/// mesh if its material changed. Otherwise, the material binding information in +/// the [`crate::render::mesh::MeshInputUniform`] might not be updated properly. +/// The easiest way to ensure that +/// [`crate::render::mesh::extract_meshes_for_gpu_building`] re-extracts a mesh +/// is to mark its [`Mesh3d`] as changed, so that's what this system does. +fn mark_meshes_as_changed_if_their_materials_changed( + mut changed_meshes_query: Query< + &mut Mesh3d, + Or<(Changed>, AssetChanged>)>, + >, +) where + M: Material, +{ + for mut mesh in &mut changed_meshes_query { + mesh.set_changed(); + } +} + +/// Fills the [`RenderMaterialInstances`] resources from the meshes in the +/// scene. +fn extract_mesh_materials( + mut material_instances: ResMut, + changed_meshes_query: Extract< + Query< + (Entity, &ViewVisibility, &MeshMaterial3d), + Or<(Changed, Changed>)>, + >, + >, +) { + let last_change_tick = material_instances.current_change_tick; + + for (entity, view_visibility, material) in &changed_meshes_query { + if view_visibility.get() { + material_instances.instances.insert( + entity.into(), + RenderMaterialInstance { + asset_id: material.id().untyped(), + last_change_tick, + }, + ); + } else { + material_instances + .instances + .remove(&MainEntity::from(entity)); + } + } +} + +/// Removes mesh materials from [`RenderMaterialInstances`] when their +/// [`MeshMaterial3d`] components are removed. +/// +/// This is tricky because we have to deal with the case in which a material of +/// type A was removed and replaced with a material of type B in the same frame +/// (which is actually somewhat common of an operation). In this case, even +/// though an entry will be present in `RemovedComponents>`, +/// we must not remove the entry in `RenderMaterialInstances` which corresponds +/// to material B. To handle this case, we use change ticks to avoid removing +/// the entry if it was updated this frame. +/// +/// This is the first of two sweep phases. Because this phase runs once per +/// material type, we need a second phase in order to guarantee that we only +/// bump [`RenderMaterialInstances::current_change_tick`] once. +fn early_sweep_material_instances( + mut material_instances: ResMut, + mut removed_materials_query: Extract>>, +) where + M: Material, +{ + let last_change_tick = material_instances.current_change_tick; + + for entity in removed_materials_query.read() { + if let Entry::Occupied(occupied_entry) = material_instances.instances.entry(entity.into()) { + // Only sweep the entry if it wasn't updated this frame. + if occupied_entry.get().last_change_tick != last_change_tick { + occupied_entry.remove(); + } + } + } +} + +/// Removes mesh materials from [`RenderMaterialInstances`] when their +/// [`ViewVisibility`] components are removed. +/// +/// This runs after all invocations of [`early_sweep_material_instances`] and is +/// responsible for bumping [`RenderMaterialInstances::current_change_tick`] in +/// preparation for a new frame. +pub(crate) fn late_sweep_material_instances( + mut material_instances: ResMut, + mut removed_meshes_query: Extract>, +) { + let last_change_tick = material_instances.current_change_tick; + + for entity in removed_meshes_query.read() { + if let Entry::Occupied(occupied_entry) = material_instances.instances.entry(entity.into()) { + // Only sweep the entry if it wasn't updated this frame. It's + // possible that a `ViewVisibility` component was removed and + // re-added in the same frame. + if occupied_entry.get().last_change_tick != last_change_tick { + occupied_entry.remove(); + } + } + } + + material_instances + .current_change_tick + .set(last_change_tick.get() + 1); +} + +pub fn extract_entities_needs_specialization( + entities_needing_specialization: Extract>>, + material_instances: Res, + mut entity_specialization_ticks: ResMut, + mut removed_mesh_material_components: Extract>>, + mut specialized_material_pipeline_cache: ResMut, + mut specialized_prepass_material_pipeline_cache: Option< + ResMut, + >, + mut specialized_shadow_material_pipeline_cache: Option< + ResMut, + >, + views: Query<&ExtractedView>, + ticks: SystemChangeTick, +) where + M: Material, +{ + // Clean up any despawned entities, we do this first in case the removed material was re-added + // the same frame, thus will appear both in the removed components list and have been added to + // the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter + // + // Additionally, we need to make sure that we are careful about materials that could have changed + // type, e.g. from a `StandardMaterial` to a `CustomMaterial`, as this will also appear in the + // removed components list. As such, we make sure that this system runs after `MaterialExtractionSystems` + // so that the `RenderMaterialInstances` bookkeeping has already been done, and we can check if the entity + // still has a valid material instance. + for entity in removed_mesh_material_components.read() { + if material_instances + .instances + .contains_key(&MainEntity::from(entity)) + { + continue; + } + + entity_specialization_ticks.remove(&MainEntity::from(entity)); + for view in views { + if let Some(cache) = + specialized_material_pipeline_cache.get_mut(&view.retained_view_entity) + { + cache.remove(&MainEntity::from(entity)); + } + if let Some(cache) = specialized_prepass_material_pipeline_cache + .as_mut() + .and_then(|c| c.get_mut(&view.retained_view_entity)) + { + cache.remove(&MainEntity::from(entity)); + } + if let Some(cache) = specialized_shadow_material_pipeline_cache + .as_mut() + .and_then(|c| c.get_mut(&view.retained_view_entity)) + { + cache.remove(&MainEntity::from(entity)); + } + } + } + + for entity in entities_needing_specialization.iter() { + // Update the entity's specialization tick with this run's tick + entity_specialization_ticks.insert((*entity).into(), ticks.this_run()); + } +} + +#[derive(Resource, Deref, DerefMut, Clone, Debug)] +pub struct EntitiesNeedingSpecialization { + #[deref] + pub entities: Vec, + _marker: PhantomData, +} + +impl Default for EntitiesNeedingSpecialization { + fn default() -> Self { + Self { + entities: Default::default(), + _marker: Default::default(), + } + } +} + +#[derive(Resource, Deref, DerefMut, Default, Clone, Debug)] +pub struct EntitySpecializationTicks { + #[deref] + pub entities: MainEntityHashMap, +} + +/// Stores the [`SpecializedMaterialViewPipelineCache`] for each view. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedMaterialPipelineCache { + // view entity -> view pipeline cache + #[deref] + map: HashMap, +} + +/// Stores the cached render pipeline ID for each entity in a single view, as +/// well as the last time it was changed. +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedMaterialViewPipelineCache { + // material entity -> (tick, pipeline_id) + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, +} + +pub fn check_entities_needing_specialization( + needs_specialization: Query< + Entity, + ( + Or<( + Changed, + AssetChanged, + Changed>, + AssetChanged>, + )>, + With>, + ), + >, + mut par_local: Local>>, + mut entities_needing_specialization: ResMut>, +) where + M: Material, +{ + entities_needing_specialization.clear(); + + needs_specialization + .par_iter() + .for_each(|entity| par_local.borrow_local_mut().push(entity)); + + par_local.drain_into(&mut entities_needing_specialization); +} + +pub fn specialize_material_meshes( + render_meshes: Res>, + render_materials: Res>, + render_mesh_instances: Res, + render_material_instances: Res, + render_lightmaps: Res, + render_visibility_ranges: Res, + ( + opaque_render_phases, + alpha_mask_render_phases, + transmissive_render_phases, + transparent_render_phases, + ): ( + Res>, + Res>, + Res>, + Res>, + ), + views: Query<(&ExtractedView, &RenderVisibleEntities)>, + view_key_cache: Res, + entity_specialization_ticks: Res, + view_specialization_ticks: Res, + mut specialized_material_pipeline_cache: ResMut, + mut pipelines: ResMut>, + pipeline: Res, + pipeline_cache: Res, + ticks: SystemChangeTick, +) { + // Record the retained IDs of all shadow views so that we can expire old + // pipeline IDs. + let mut all_views: HashSet = HashSet::default(); + + for (view, visible_entities) in &views { + all_views.insert(view.retained_view_entity); + + if !transparent_render_phases.contains_key(&view.retained_view_entity) + && !opaque_render_phases.contains_key(&view.retained_view_entity) + && !alpha_mask_render_phases.contains_key(&view.retained_view_entity) + && !transmissive_render_phases.contains_key(&view.retained_view_entity) + { + continue; + } + + let Some(view_key) = view_key_cache.get(&view.retained_view_entity) else { + continue; + }; + + let view_tick = view_specialization_ticks + .get(&view.retained_view_entity) + .unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(view.retained_view_entity) + .or_default(); + + for (_, visible_entity) in visible_entities.iter::() { + let Some(material_instance) = render_material_instances.instances.get(visible_entity) + else { + continue; + }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap(); + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; + } + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + let Some(material) = render_materials.get(material_instance.asset_id) else { + continue; + }; + + let mut mesh_pipeline_key_bits = material.properties.mesh_pipeline_key_bits; + mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key( + material.properties.alpha_mode, + &Msaa::from_samples(view_key.msaa_samples()), + )); + let mut mesh_key = *view_key + | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()) + | mesh_pipeline_key_bits; + + if let Some(lightmap) = render_lightmaps.render_lightmaps.get(visible_entity) { + mesh_key |= MeshPipelineKey::LIGHTMAPPED; + + if lightmap.bicubic_sampling { + mesh_key |= MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING; + } + } + + if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) { + mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER; + } + + if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + // If the previous frame have skins or morph targets, note that. + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_SKIN; + } + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_MORPH; + } + } + + let erased_key = ErasedMaterialPipelineKey { + type_id: material_instance.asset_id.type_id(), + mesh_key, + material_key: material.properties.material_key.clone(), + }; + let material_pipeline_specializer = MaterialPipelineSpecializer { + pipeline: pipeline.clone(), + properties: material.properties.clone(), + }; + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &material_pipeline_specializer, + erased_key, + &mesh.layout, + ); + let pipeline_id = match pipeline_id { + Ok(id) => id, + Err(err) => { + error!("{}", err); + continue; + } + }; + + view_specialized_material_pipeline_cache + .insert(*visible_entity, (ticks.this_run(), pipeline_id)); + } + } + + // Delete specialized pipelines belonging to views that have expired. + specialized_material_pipeline_cache + .retain(|retained_view_entity, _| all_views.contains(retained_view_entity)); +} + +/// For each view, iterates over all the meshes visible from that view and adds +/// them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as appropriate. +pub fn queue_material_meshes( + render_materials: Res>, + render_mesh_instances: Res, + render_material_instances: Res, + mesh_allocator: Res, + gpu_preprocessing_support: Res, + mut opaque_render_phases: ResMut>, + mut alpha_mask_render_phases: ResMut>, + mut transmissive_render_phases: ResMut>, + mut transparent_render_phases: ResMut>, + views: Query<(&ExtractedView, &RenderVisibleEntities)>, + specialized_material_pipeline_cache: ResMut, +) { + for (view, visible_entities) in &views { + let ( + Some(opaque_phase), + Some(alpha_mask_phase), + Some(transmissive_phase), + Some(transparent_phase), + ) = ( + opaque_render_phases.get_mut(&view.retained_view_entity), + alpha_mask_render_phases.get_mut(&view.retained_view_entity), + transmissive_render_phases.get_mut(&view.retained_view_entity), + transparent_render_phases.get_mut(&view.retained_view_entity), + ) + else { + continue; + }; + + let Some(view_specialized_material_pipeline_cache) = + specialized_material_pipeline_cache.get(&view.retained_view_entity) + else { + continue; + }; + + let rangefinder = view.rangefinder3d(); + for (render_entity, visible_entity) in visible_entities.iter::() { + let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id)) + else { + continue; + }; + + // Skip the entity if it's cached in a bin and up to date. + if opaque_phase.validate_cached_entity(*visible_entity, current_change_tick) + || alpha_mask_phase.validate_cached_entity(*visible_entity, current_change_tick) + { + continue; + } + + let Some(material_instance) = render_material_instances.instances.get(visible_entity) + else { + continue; + }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let Some(material) = render_materials.get(material_instance.asset_id) else { + continue; + }; + + // Fetch the slabs that this mesh resides in. + let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let Some(draw_function) = material.properties.get_draw_function(MaterialDrawFunction) + else { + continue; + }; + + match material.properties.render_phase_type { + RenderPhaseType::Transmissive => { + let distance = rangefinder.distance_translation(&mesh_instance.translation) + + material.properties.depth_bias; + transmissive_phase.add(Transmissive3d { + entity: (*render_entity, *visible_entity), + draw_function, + pipeline: pipeline_id, + distance, + batch_range: 0..1, + extra_index: PhaseItemExtraIndex::None, + indexed: index_slab.is_some(), + }); + } + RenderPhaseType::Opaque => { + if material.properties.render_method == OpaqueRendererMethod::Deferred { + // Even though we aren't going to insert the entity into + // a bin, we still want to update its cache entry. That + // way, we know we don't need to re-examine it in future + // frames. + opaque_phase.update_cache(*visible_entity, None, current_change_tick); + continue; + } + let batch_set_key = Opaque3dBatchSetKey { + pipeline: pipeline_id, + draw_function, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + lightmap_slab: mesh_instance.shared.lightmap_slab_index.map(|index| *index), + }; + let bin_key = Opaque3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }; + opaque_phase.add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + current_change_tick, + ); + } + // Alpha mask + RenderPhaseType::AlphaMask => { + let batch_set_key = OpaqueNoLightmap3dBatchSetKey { + draw_function, + pipeline: pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + let bin_key = OpaqueNoLightmap3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }; + alpha_mask_phase.add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + current_change_tick, + ); + } + RenderPhaseType::Transparent => { + let distance = rangefinder.distance_translation(&mesh_instance.translation) + + material.properties.depth_bias; + transparent_phase.add(Transparent3d { + entity: (*render_entity, *visible_entity), + draw_function, + pipeline: pipeline_id, + distance, + batch_range: 0..1, + extra_index: PhaseItemExtraIndex::None, + indexed: index_slab.is_some(), + }); + } + } + } + } +} + +/// Default render method used for opaque materials. +#[derive(Default, Resource, Clone, Debug, ExtractResource, Reflect)] +#[reflect(Resource, Default, Debug, Clone)] +pub struct DefaultOpaqueRendererMethod(OpaqueRendererMethod); + +impl DefaultOpaqueRendererMethod { + pub fn forward() -> Self { + DefaultOpaqueRendererMethod(OpaqueRendererMethod::Forward) + } + + pub fn deferred() -> Self { + DefaultOpaqueRendererMethod(OpaqueRendererMethod::Deferred) + } + + pub fn set_to_forward(&mut self) { + self.0 = OpaqueRendererMethod::Forward; + } + + pub fn set_to_deferred(&mut self) { + self.0 = OpaqueRendererMethod::Deferred; + } +} + +/// Render method used for opaque materials. +/// +/// The forward rendering main pass draws each mesh entity and shades it according to its +/// corresponding material and the lights that affect it. Some render features like Screen Space +/// Ambient Occlusion require running depth and normal prepasses, that are 'deferred'-like +/// prepasses over all mesh entities to populate depth and normal textures. This means that when +/// using render features that require running prepasses, multiple passes over all visible geometry +/// are required. This can be slow if there is a lot of geometry that cannot be batched into few +/// draws. +/// +/// Deferred rendering runs a prepass to gather not only geometric information like depth and +/// normals, but also all the material properties like base color, emissive color, reflectance, +/// metalness, etc, and writes them into a deferred 'g-buffer' texture. The deferred main pass is +/// then a fullscreen pass that reads data from these textures and executes shading. This allows +/// for one pass over geometry, but is at the cost of not being able to use MSAA, and has heavier +/// bandwidth usage which can be unsuitable for low end mobile or other bandwidth-constrained devices. +/// +/// If a material indicates `OpaqueRendererMethod::Auto`, `DefaultOpaqueRendererMethod` will be used. +#[derive(Default, Clone, Copy, Debug, PartialEq, Reflect)] +#[reflect(Default, Clone, PartialEq)] +pub enum OpaqueRendererMethod { + #[default] + Forward, + Deferred, + Auto, +} + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredVertexShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletPrepassFragmentShader; + +#[derive(ShaderLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MeshletDeferredFragmentShader; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct MaterialDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct PrepassDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct DeferredDrawFunction; + +#[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] +pub struct ShadowsDrawFunction; + +#[derive(Debug)] +pub struct ErasedMaterialKey { + type_id: TypeId, + hash: u64, + value: Box, + vtable: Arc, +} + +#[derive(Debug)] +pub struct ErasedMaterialKeyVTable { + clone_fn: fn(&dyn Any) -> Box, + partial_eq_fn: fn(&dyn Any, &dyn Any) -> bool, +} + +impl ErasedMaterialKey { + pub fn new(material_key: T) -> Self + where + T: Clone + Hash + PartialEq + Send + Sync + 'static, + { + let type_id = TypeId::of::(); + let hash = FixedHasher::hash_one(&FixedHasher, &material_key); + + fn clone(any: &dyn Any) -> Box { + Box::new(any.downcast_ref::().unwrap().clone()) + } + fn partial_eq(a: &dyn Any, b: &dyn Any) -> bool { + a.downcast_ref::().unwrap() == b.downcast_ref::().unwrap() + } + + Self { + type_id, + hash, + value: Box::new(material_key), + vtable: Arc::new(ErasedMaterialKeyVTable { + clone_fn: clone::, + partial_eq_fn: partial_eq::, + }), + } + } + + pub fn to_key(&self) -> T { + debug_assert_eq!(self.type_id, TypeId::of::()); + self.value.downcast_ref::().unwrap().clone() + } +} + +impl PartialEq for ErasedMaterialKey { + fn eq(&self, other: &Self) -> bool { + self.type_id == other.type_id + && (self.vtable.partial_eq_fn)(self.value.as_ref(), other.value.as_ref()) + } +} + +impl Eq for ErasedMaterialKey {} + +impl Clone for ErasedMaterialKey { + fn clone(&self) -> Self { + Self { + type_id: self.type_id, + hash: self.hash, + value: (self.vtable.clone_fn)(self.value.as_ref()), + vtable: self.vtable.clone(), + } + } +} + +impl Hash for ErasedMaterialKey { + fn hash(&self, state: &mut H) { + self.type_id.hash(state); + self.hash.hash(state); + } +} + +impl Default for ErasedMaterialKey { + fn default() -> Self { + Self::new(()) + } +} + +/// Common [`Material`] properties, calculated for a specific material instance. +#[derive(Default)] +pub struct MaterialProperties { + /// Is this material should be rendered by the deferred renderer when. + /// [`AlphaMode::Opaque`] or [`AlphaMode::Mask`] + pub render_method: OpaqueRendererMethod, + /// The [`AlphaMode`] of this material. + pub alpha_mode: AlphaMode, + /// The bits in the [`MeshPipelineKey`] for this material. + /// + /// These are precalculated so that we can just "or" them together in + /// [`queue_material_meshes`]. + pub mesh_pipeline_key_bits: MeshPipelineKey, + /// Add a bias to the view depth of the mesh which can be used to force a specific render order + /// for meshes with equal depth, to avoid z-fighting. + /// The bias is in depth-texture units so large values may be needed to overcome small depth differences. + pub depth_bias: f32, + /// Whether the material would like to read from [`ViewTransmissionTexture`](bevy_core_pipeline::core_3d::ViewTransmissionTexture). + /// + /// This allows taking color output from the [`Opaque3d`] pass as an input, (for screen-space transmission) but requires + /// rendering to take place in a separate [`Transmissive3d`] pass. + pub reads_view_transmission_texture: bool, + pub render_phase_type: RenderPhaseType, + pub material_layout: Option, + /// Backing array is a size of 4 because the `StandardMaterial` needs 4 draw functions by default + pub draw_functions: SmallVec<[(InternedDrawFunctionLabel, DrawFunctionId); 4]>, + /// Backing array is a size of 3 because the `StandardMaterial` has 3 custom shaders (`frag`, `prepass_frag`, `deferred_frag`) which is the + /// most common use case + pub shaders: SmallVec<[(InternedShaderLabel, Handle); 3]>, + /// Whether this material *actually* uses bindless resources, taking the + /// platform support (or lack thereof) of bindless resources into account. + pub bindless: bool, + pub specialize: Option< + fn( + &MaterialPipeline, + &mut RenderPipelineDescriptor, + &MeshVertexBufferLayoutRef, + ErasedMaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError>, + >, + /// The key for this material, typically a bitfield of flags that are used to modify + /// the pipeline descriptor used for this material. + pub material_key: ErasedMaterialKey, + /// Whether shadows are enabled for this material + pub shadows_enabled: bool, + /// Whether prepass is enabled for this material + pub prepass_enabled: bool, +} + +impl MaterialProperties { + pub fn get_shader(&self, label: impl ShaderLabel) -> Option> { + self.shaders + .iter() + .find(|(inner_label, _)| inner_label == &label.intern()) + .map(|(_, shader)| shader) + .cloned() + } + + pub fn add_shader(&mut self, label: impl ShaderLabel, shader: Handle) { + self.shaders.push((label.intern(), shader)); + } + + pub fn get_draw_function(&self, label: impl DrawFunctionLabel) -> Option { + self.draw_functions + .iter() + .find(|(inner_label, _)| inner_label == &label.intern()) + .map(|(_, shader)| shader) + .cloned() + } + + pub fn add_draw_function( + &mut self, + label: impl DrawFunctionLabel, + draw_function: DrawFunctionId, + ) { + self.draw_functions.push((label.intern(), draw_function)); + } +} + +#[derive(Clone, Copy, Default)] +pub enum RenderPhaseType { + #[default] + Opaque, + AlphaMask, + Transmissive, + Transparent, +} + +/// A resource that maps each untyped material ID to its binding. +/// +/// This duplicates information in `RenderAssets`, but it doesn't have the +/// `M` type parameter, so it can be used in untyped contexts like +/// [`crate::render::mesh::collect_meshes_for_gpu_building`]. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct RenderMaterialBindings(HashMap); + +/// Data prepared for a [`Material`] instance. +pub struct PreparedMaterial { + pub binding: MaterialBindingId, + pub properties: Arc, +} + +// orphan rules T_T +impl ErasedRenderAsset for MeshMaterial3d +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + type SourceAsset = M; + type ErasedAsset = PreparedMaterial; + + type Param = ( + SRes, + SRes, + SResMut, + SResMut, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, + SRes>, + SRes, + ( + Option>>, + Option>>, + M::Param, + ), + ); + + fn prepare_asset( + material: Self::SourceAsset, + material_id: AssetId, + ( + render_device, + default_opaque_render_method, + bind_group_allocators, + render_material_bindings, + opaque_draw_functions, + alpha_mask_draw_functions, + transmissive_draw_functions, + transparent_draw_functions, + opaque_prepass_draw_functions, + alpha_mask_prepass_draw_functions, + opaque_deferred_draw_functions, + alpha_mask_deferred_draw_functions, + shadow_draw_functions, + asset_server, + (shadows_enabled, prepass_enabled, material_param), + ): &mut SystemParamItem, + ) -> Result> { + let material_layout = M::bind_group_layout(render_device); + + let shadows_enabled = shadows_enabled.is_some(); + let prepass_enabled = prepass_enabled.is_some(); + + let draw_opaque_pbr = opaque_draw_functions.read().id::(); + let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::(); + let draw_transmissive_pbr = transmissive_draw_functions.read().id::(); + let draw_transparent_pbr = transparent_draw_functions.read().id::(); + let draw_opaque_prepass = opaque_prepass_draw_functions.read().get_id::(); + let draw_alpha_mask_prepass = alpha_mask_prepass_draw_functions + .read() + .get_id::(); + let draw_opaque_deferred = opaque_deferred_draw_functions + .read() + .get_id::(); + let draw_alpha_mask_deferred = alpha_mask_deferred_draw_functions + .read() + .get_id::(); + let shadow_draw_function_id = shadow_draw_functions.read().get_id::(); + + let render_method = match material.opaque_render_method() { + OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward, + OpaqueRendererMethod::Deferred => OpaqueRendererMethod::Deferred, + OpaqueRendererMethod::Auto => default_opaque_render_method.0, + }; + + let mut mesh_pipeline_key_bits = MeshPipelineKey::empty(); + mesh_pipeline_key_bits.set( + MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE, + material.reads_view_transmission_texture(), + ); + + let reads_view_transmission_texture = + mesh_pipeline_key_bits.contains(MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE); + + let render_phase_type = match material.alpha_mode() { + AlphaMode::Blend | AlphaMode::Premultiplied | AlphaMode::Add | AlphaMode::Multiply => { + RenderPhaseType::Transparent + } + _ if reads_view_transmission_texture => RenderPhaseType::Transmissive, + AlphaMode::Opaque | AlphaMode::AlphaToCoverage => RenderPhaseType::Opaque, + AlphaMode::Mask(_) => RenderPhaseType::AlphaMask, + }; + + let draw_function_id = match render_phase_type { + RenderPhaseType::Opaque => draw_opaque_pbr, + RenderPhaseType::AlphaMask => draw_alpha_mask_pbr, + RenderPhaseType::Transmissive => draw_transmissive_pbr, + RenderPhaseType::Transparent => draw_transparent_pbr, + }; + let prepass_draw_function_id = match render_phase_type { + RenderPhaseType::Opaque => draw_opaque_prepass, + RenderPhaseType::AlphaMask => draw_alpha_mask_prepass, + _ => None, + }; + let deferred_draw_function_id = match render_phase_type { + RenderPhaseType::Opaque => draw_opaque_deferred, + RenderPhaseType::AlphaMask => draw_alpha_mask_deferred, + _ => None, + }; + + let mut draw_functions = SmallVec::new(); + draw_functions.push((MaterialDrawFunction.intern(), draw_function_id)); + if let Some(prepass_draw_function_id) = prepass_draw_function_id { + draw_functions.push((PrepassDrawFunction.intern(), prepass_draw_function_id)); + } + if let Some(deferred_draw_function_id) = deferred_draw_function_id { + draw_functions.push((DeferredDrawFunction.intern(), deferred_draw_function_id)); + } + if let Some(shadow_draw_function_id) = shadow_draw_function_id { + draw_functions.push((ShadowsDrawFunction.intern(), shadow_draw_function_id)); + } + + let mut shaders = SmallVec::new(); + let mut add_shader = |label: InternedShaderLabel, shader_ref: ShaderRef| { + let mayber_shader = match shader_ref { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }; + if let Some(shader) = mayber_shader { + shaders.push((label, shader)); + } + }; + add_shader(MaterialVertexShader.intern(), M::vertex_shader()); + add_shader(MaterialFragmentShader.intern(), M::fragment_shader()); + add_shader(PrepassVertexShader.intern(), M::prepass_vertex_shader()); + add_shader(PrepassFragmentShader.intern(), M::prepass_fragment_shader()); + add_shader(DeferredVertexShader.intern(), M::deferred_vertex_shader()); + add_shader( + DeferredFragmentShader.intern(), + M::deferred_fragment_shader(), + ); + + #[cfg(feature = "meshlet")] + { + add_shader( + MeshletFragmentShader.intern(), + M::meshlet_mesh_fragment_shader(), + ); + add_shader( + MeshletPrepassFragmentShader.intern(), + M::meshlet_mesh_prepass_fragment_shader(), + ); + add_shader( + MeshletDeferredFragmentShader.intern(), + M::meshlet_mesh_deferred_fragment_shader(), + ); + } + + let bindless = material_uses_bindless_resources::(render_device); + let bind_group_data = material.bind_group_data(); + let material_key = ErasedMaterialKey::new(bind_group_data); + fn specialize( + pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + mesh_layout: &MeshVertexBufferLayoutRef, + erased_key: ErasedMaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> + where + M::Data: Hash + Clone, + { + let material_key = erased_key.material_key.to_key(); + M::specialize( + pipeline, + descriptor, + mesh_layout, + MaterialPipelineKey { + mesh_key: erased_key.mesh_key, + bind_group_data: material_key, + }, + ) + } + + match material.unprepared_bind_group(&material_layout, render_device, material_param, false) + { + Ok(unprepared) => { + let bind_group_allocator = + bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); + // Allocate or update the material. + let binding = match render_material_bindings.entry(material_id.into()) { + Entry::Occupied(mut occupied_entry) => { + // TODO: Have a fast path that doesn't require + // recreating the bind group if only buffer contents + // change. For now, we just delete and recreate the bind + // group. + bind_group_allocator.free(*occupied_entry.get()); + let new_binding = + bind_group_allocator.allocate_unprepared(unprepared, &material_layout); + *occupied_entry.get_mut() = new_binding; + new_binding + } + Entry::Vacant(vacant_entry) => *vacant_entry.insert( + bind_group_allocator.allocate_unprepared(unprepared, &material_layout), + ), + }; + + Ok(PreparedMaterial { + binding, + properties: Arc::new(MaterialProperties { + alpha_mode: material.alpha_mode(), + depth_bias: material.depth_bias(), + reads_view_transmission_texture, + render_phase_type, + render_method, + mesh_pipeline_key_bits, + material_layout: Some(material_layout), + draw_functions, + shaders, + bindless, + specialize: Some(specialize::), + material_key, + shadows_enabled, + prepass_enabled, + }), + }) + } + + Err(AsBindGroupError::RetryNextUpdate) => { + Err(PrepareAssetError::RetryNextUpdate(material)) + } + + Err(AsBindGroupError::CreateBindGroupDirectly) => { + // This material has opted out of automatic bind group creation + // and is requesting a fully-custom bind group. Invoke + // `as_bind_group` as requested, and store the resulting bind + // group in the slot. + match material.as_bind_group(&material_layout, render_device, material_param) { + Ok(prepared_bind_group) => { + let bind_group_allocator = + bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); + // Store the resulting bind group directly in the slot. + let material_binding_id = + bind_group_allocator.allocate_prepared(prepared_bind_group); + render_material_bindings.insert(material_id.into(), material_binding_id); + + Ok(PreparedMaterial { + binding: material_binding_id, + properties: Arc::new(MaterialProperties { + alpha_mode: material.alpha_mode(), + depth_bias: material.depth_bias(), + reads_view_transmission_texture, + render_phase_type, + render_method, + mesh_pipeline_key_bits, + material_layout: Some(material_layout), + draw_functions, + shaders, + bindless, + specialize: Some(specialize::), + material_key, + shadows_enabled, + prepass_enabled, + }), + }) + } + + Err(AsBindGroupError::RetryNextUpdate) => { + Err(PrepareAssetError::RetryNextUpdate(material)) + } + + Err(other) => Err(PrepareAssetError::AsBindGroupError(other)), + } + } + + Err(other) => Err(PrepareAssetError::AsBindGroupError(other)), + } + } + + fn unload_asset( + source_asset: AssetId, + (_, _, bind_group_allocators, render_material_bindings, ..): &mut SystemParamItem< + Self::Param, + >, + ) { + let Some(material_binding_id) = render_material_bindings.remove(&source_asset.untyped()) + else { + return; + }; + let bind_group_allactor = bind_group_allocators.get_mut(&TypeId::of::()).unwrap(); + bind_group_allactor.free(material_binding_id); + } +} + +/// Creates and/or recreates any bind groups that contain materials that were +/// modified this frame. +pub fn prepare_material_bind_groups( + mut allocators: ResMut, + render_device: Res, + fallback_image: Res, + fallback_resources: Res, +) { + for (_, allocator) in allocators.iter_mut() { + allocator.prepare_bind_groups(&render_device, &fallback_resources, &fallback_image); + } +} + +/// Uploads the contents of all buffers that the [`MaterialBindGroupAllocator`] +/// manages to the GPU. +/// +/// Non-bindless allocators don't currently manage any buffers, so this method +/// only has an effect for bindless allocators. +pub fn write_material_bind_group_buffers( + mut allocators: ResMut, + render_device: Res, + render_queue: Res, +) { + for (_, allocator) in allocators.iter_mut() { + allocator.write_buffers(&render_device, &render_queue); + } +} + +/// Marker resource for whether shadows are enabled for this material type +#[derive(Resource, Debug)] +pub struct ShadowsEnabled(PhantomData); + +impl Default for ShadowsEnabled { + fn default() -> Self { + Self(PhantomData) + } +} diff --git a/crates/libmarathon/src/render/pbr/material_bind_groups.rs b/crates/libmarathon/src/render/pbr/material_bind_groups.rs new file mode 100644 index 0000000..be5fc9c --- /dev/null +++ b/crates/libmarathon/src/render/pbr/material_bind_groups.rs @@ -0,0 +1,1996 @@ +//! Material bind group management for bindless resources. +//! +//! In bindless mode, Bevy's renderer groups materials into bind groups. This +//! allocator manages each bind group, assigning slots to materials as +//! appropriate. + +use crate::render::pbr::Material; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + resource::Resource, + system::{Commands, Res}, +}; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use crate::render::render_resource::BindlessSlabResourceLimit; +use crate::render::{ + render_resource::{ + BindGroup, BindGroupEntry, BindGroupLayout, BindingNumber, BindingResource, + BindingResources, BindlessDescriptor, BindlessIndex, BindlessIndexTableDescriptor, + BindlessResourceType, Buffer, BufferBinding, BufferDescriptor, BufferId, + BufferInitDescriptor, BufferUsages, CompareFunction, FilterMode, OwnedBindingResource, + PreparedBindGroup, RawBufferVec, Sampler, SamplerDescriptor, SamplerId, TextureView, + TextureViewDimension, TextureViewId, UnpreparedBindGroup, WgpuSampler, WgpuTextureView, + }, + renderer::{RenderDevice, RenderQueue}, + settings::WgpuFeatures, + texture::FallbackImage, +}; +use bevy_utils::{default, TypeIdMap}; +use bytemuck::Pod; +use core::hash::Hash; +use core::{cmp::Ordering, iter, mem, ops::Range}; +use tracing::{error, trace}; + +#[derive(Resource, Deref, DerefMut, Default)] +pub struct MaterialBindGroupAllocators(TypeIdMap); + +/// A resource that places materials into bind groups and tracks their +/// resources. +/// +/// Internally, Bevy has separate allocators for bindless and non-bindless +/// materials. This resource provides a common interface to the specific +/// allocator in use. +pub enum MaterialBindGroupAllocator { + /// The allocator used when the material is bindless. + Bindless(Box), + /// The allocator used when the material is non-bindless. + NonBindless(Box), +} + +/// The allocator that places bindless materials into bind groups and tracks +/// their resources. +pub struct MaterialBindGroupBindlessAllocator { + /// The label of the bind group allocator to use for allocated buffers. + label: Option<&'static str>, + /// The slabs, each of which contains a bind group. + slabs: Vec, + /// The layout of the bind groups that we produce. + bind_group_layout: BindGroupLayout, + /// Information about the bindless resources in the material. + /// + /// We use this information to create and maintain bind groups. + bindless_descriptor: BindlessDescriptor, + + /// Dummy buffers that we use to fill empty slots in buffer binding arrays. + /// + /// There's one fallback buffer for each buffer in the bind group, each + /// appropriately sized. Each buffer contains one uninitialized element of + /// the applicable type. + fallback_buffers: HashMap, + + /// The maximum number of resources that can be stored in a slab. + /// + /// This corresponds to `SLAB_CAPACITY` in the `#[bindless(SLAB_CAPACITY)]` + /// attribute, when deriving `AsBindGroup`. + slab_capacity: u32, +} + +/// A single bind group and the bookkeeping necessary to allocate into it. +pub struct MaterialBindlessSlab { + /// The current bind group, if it's up to date. + /// + /// If this is `None`, then the bind group is dirty and needs to be + /// regenerated. + bind_group: Option, + + /// The GPU-accessible buffers that hold the mapping from binding index to + /// bindless slot. + /// + /// This is conventionally assigned to bind group binding 0, but it can be + /// changed using the `#[bindless(index_table(binding(B)))]` attribute on + /// `AsBindGroup`. + /// + /// Because the slab binary searches this table, the entries within must be + /// sorted by bindless index. + bindless_index_tables: Vec, + + /// The binding arrays containing samplers. + samplers: HashMap>, + /// The binding arrays containing textures. + textures: HashMap>, + /// The binding arrays containing buffers. + buffers: HashMap>, + /// The buffers that contain plain old data (i.e. the structure-level + /// `#[data]` attribute of `AsBindGroup`). + data_buffers: HashMap, + + /// A list of free slot IDs. + free_slots: Vec, + /// The total number of materials currently allocated in this slab. + live_allocation_count: u32, + /// The total number of resources currently allocated in the binding arrays. + allocated_resource_count: u32, +} + +/// A GPU-accessible buffer that holds the mapping from binding index to +/// bindless slot. +/// +/// This is conventionally assigned to bind group binding 0, but it can be +/// changed by altering the [`Self::binding_number`], which corresponds to the +/// `#[bindless(index_table(binding(B)))]` attribute in `AsBindGroup`. +struct MaterialBindlessIndexTable { + /// The buffer containing the mappings. + buffer: RetainedRawBufferVec, + /// The range of bindless indices that this bindless index table covers. + /// + /// If this range is M..N, then the field at index $i$ maps to bindless + /// index $i$ + M. The size of this table is N - M. + /// + /// This corresponds to the `#[bindless(index_table(range(M..N)))]` + /// attribute in `AsBindGroup`. + index_range: Range, + /// The binding number that this index table is assigned to in the shader. + binding_number: BindingNumber, +} + +/// A single binding array for storing bindless resources and the bookkeeping +/// necessary to allocate into it. +struct MaterialBindlessBindingArray +where + R: GetBindingResourceId, +{ + /// The number of the binding that we attach this binding array to. + binding_number: BindingNumber, + /// A mapping from bindless slot index to the resource stored in that slot, + /// if any. + bindings: Vec>>, + /// The type of resource stored in this binding array. + resource_type: BindlessResourceType, + /// Maps a resource ID to the slot in which it's stored. + /// + /// This is essentially the inverse mapping of [`Self::bindings`]. + resource_to_slot: HashMap, + /// A list of free slots in [`Self::bindings`] that contain no binding. + free_slots: Vec, + /// The number of allocated objects in this binding array. + len: u32, +} + +/// A single resource (sampler, texture, or buffer) in a binding array. +/// +/// Resources hold a reference count, which specifies the number of materials +/// currently allocated within the slab that refer to this resource. When the +/// reference count drops to zero, the resource is freed. +struct MaterialBindlessBinding +where + R: GetBindingResourceId, +{ + /// The sampler, texture, or buffer. + resource: R, + /// The number of materials currently allocated within the containing slab + /// that use this resource. + ref_count: u32, +} + +/// The allocator that stores bind groups for non-bindless materials. +pub struct MaterialBindGroupNonBindlessAllocator { + /// The label of the bind group allocator to use for allocated buffers. + label: Option<&'static str>, + /// A mapping from [`MaterialBindGroupIndex`] to the bind group allocated in + /// each slot. + bind_groups: Vec>, + /// The bind groups that are dirty and need to be prepared. + /// + /// To prepare the bind groups, call + /// [`MaterialBindGroupAllocator::prepare_bind_groups`]. + to_prepare: HashSet, + /// A list of free bind group indices. + free_indices: Vec, +} + +/// A single bind group that a [`MaterialBindGroupNonBindlessAllocator`] is +/// currently managing. +enum MaterialNonBindlessAllocatedBindGroup { + /// An unprepared bind group. + /// + /// The allocator prepares all outstanding unprepared bind groups when + /// [`MaterialBindGroupNonBindlessAllocator::prepare_bind_groups`] is + /// called. + Unprepared { + /// The unprepared bind group, including extra data. + bind_group: UnpreparedBindGroup, + /// The layout of that bind group. + layout: BindGroupLayout, + }, + /// A bind group that's already been prepared. + Prepared { + bind_group: PreparedBindGroup, + #[expect(dead_code, reason = "These buffers are only referenced by bind groups")] + uniform_buffers: Vec, + }, +} + +/// Dummy instances of various resources that we fill unused slots in binding +/// arrays with. +#[derive(Resource)] +pub struct FallbackBindlessResources { + /// A dummy filtering sampler. + filtering_sampler: Sampler, + /// A dummy non-filtering sampler. + non_filtering_sampler: Sampler, + /// A dummy comparison sampler. + comparison_sampler: Sampler, +} + +/// The `wgpu` ID of a single bindless or non-bindless resource. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +enum BindingResourceId { + /// A buffer. + Buffer(BufferId), + /// A texture view, with the given dimension. + TextureView(TextureViewDimension, TextureViewId), + /// A sampler. + Sampler(SamplerId), + /// A buffer containing plain old data. + /// + /// This corresponds to the `#[data]` structure-level attribute on + /// `AsBindGroup`. + DataBuffer, +} + +/// A temporary list of references to `wgpu` bindless resources. +/// +/// We need this because the `wgpu` bindless API takes a slice of references. +/// Thus we need to create intermediate vectors of bindless resources in order +/// to satisfy `wgpu`'s lifetime requirements. +enum BindingResourceArray<'a> { + /// A list of bindings. + Buffers(Vec>), + /// A list of texture views. + TextureViews(Vec<&'a WgpuTextureView>), + /// A list of samplers. + Samplers(Vec<&'a WgpuSampler>), +} + +/// The location of a material (either bindless or non-bindless) within the +/// slabs. +#[derive(Clone, Copy, Debug, Default, Reflect)] +#[reflect(Clone, Default)] +pub struct MaterialBindingId { + /// The index of the bind group (slab) where the GPU data is located. + pub group: MaterialBindGroupIndex, + /// The slot within that bind group. + /// + /// Non-bindless materials will always have a slot of 0. + pub slot: MaterialBindGroupSlot, +} + +/// The index of each material bind group. +/// +/// In bindless mode, each bind group contains multiple materials. In +/// non-bindless mode, each bind group contains only one material. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Reflect, Deref, DerefMut)] +#[reflect(Default, Clone, PartialEq, Hash)] +pub struct MaterialBindGroupIndex(pub u32); + +impl From for MaterialBindGroupIndex { + fn from(value: u32) -> Self { + MaterialBindGroupIndex(value) + } +} + +/// The index of the slot containing material data within each material bind +/// group. +/// +/// In bindless mode, this slot is needed to locate the material data in each +/// bind group, since multiple materials are packed into a single slab. In +/// non-bindless mode, this slot is always 0. +#[derive(Clone, Copy, Debug, Default, PartialEq, Reflect, Deref, DerefMut)] +#[reflect(Default, Clone, PartialEq)] +pub struct MaterialBindGroupSlot(pub u32); + +/// The CPU/GPU synchronization state of a buffer that we maintain. +/// +/// Currently, the only buffer that we maintain is the +/// [`MaterialBindlessIndexTable`]. +enum BufferDirtyState { + /// The buffer is currently synchronized between the CPU and GPU. + Clean, + /// The buffer hasn't been created yet. + NeedsReserve, + /// The buffer exists on both CPU and GPU, but the GPU data is out of date. + NeedsUpload, +} + +/// Information that describes a potential allocation of an +/// [`UnpreparedBindGroup`] into a slab. +struct BindlessAllocationCandidate { + /// A map that, for every resource in the [`UnpreparedBindGroup`] that + /// already existed in this slab, maps bindless index of that resource to + /// its slot in the appropriate binding array. + pre_existing_resources: HashMap, + /// Stores the number of free slots that are needed to satisfy this + /// allocation. + needed_free_slots: u32, +} + +/// A trait that allows fetching the [`BindingResourceId`] from a +/// [`BindlessResourceType`]. +/// +/// This is used when freeing bindless resources, in order to locate the IDs +/// assigned to each resource so that they can be removed from the appropriate +/// maps. +trait GetBindingResourceId { + /// Returns the [`BindingResourceId`] for this resource. + /// + /// `resource_type` specifies this resource's type. This is used for + /// textures, as a `wgpu` [`TextureView`] doesn't store enough information + /// itself to determine its dimension. + fn binding_resource_id(&self, resource_type: BindlessResourceType) -> BindingResourceId; +} + +/// The public interface to a slab, which represents a single bind group. +pub struct MaterialSlab<'a>(MaterialSlabImpl<'a>); + +/// The actual implementation of a material slab. +/// +/// This has bindless and non-bindless variants. +enum MaterialSlabImpl<'a> { + /// The implementation of the slab interface we use when the slab + /// is bindless. + Bindless(&'a MaterialBindlessSlab), + /// The implementation of the slab interface we use when the slab + /// is non-bindless. + NonBindless(MaterialNonBindlessSlab<'a>), +} + +/// A single bind group that the [`MaterialBindGroupNonBindlessAllocator`] +/// manages. +enum MaterialNonBindlessSlab<'a> { + /// A slab that has a bind group. + Prepared(&'a PreparedBindGroup), + /// A slab that doesn't yet have a bind group. + Unprepared, +} + +/// Manages an array of untyped plain old data on GPU and allocates individual +/// slots within that array. +/// +/// This supports the `#[data]` attribute of `AsBindGroup`. +struct MaterialDataBuffer { + /// The number of the binding that we attach this storage buffer to. + binding_number: BindingNumber, + /// The actual data. + /// + /// Note that this is untyped (`u8`); the actual aligned size of each + /// element is given by [`Self::aligned_element_size`]; + buffer: RetainedRawBufferVec, + /// The size of each element in the buffer, including padding and alignment + /// if any. + aligned_element_size: u32, + /// A list of free slots within the buffer. + free_slots: Vec, + /// The actual number of slots that have been allocated. + len: u32, +} + +/// A buffer containing plain old data, already packed into the appropriate GPU +/// format, and that can be updated incrementally. +/// +/// This structure exists in order to encapsulate the lazy update +/// ([`BufferDirtyState`]) logic in a single place. +#[derive(Deref, DerefMut)] +struct RetainedRawBufferVec +where + T: Pod, +{ + /// The contents of the buffer. + #[deref] + buffer: RawBufferVec, + /// Whether the contents of the buffer have been uploaded to the GPU. + dirty: BufferDirtyState, +} + +/// The size of the buffer that we assign to unused buffer slots, in bytes. +/// +/// This is essentially arbitrary, as it doesn't seem to matter to `wgpu` what +/// the size is. +const DEFAULT_BINDLESS_FALLBACK_BUFFER_SIZE: u64 = 16; + +impl From for MaterialBindGroupSlot { + fn from(value: u32) -> Self { + MaterialBindGroupSlot(value) + } +} + +impl From for u32 { + fn from(value: MaterialBindGroupSlot) -> Self { + value.0 + } +} + +impl<'a> From<&'a OwnedBindingResource> for BindingResourceId { + fn from(value: &'a OwnedBindingResource) -> Self { + match *value { + OwnedBindingResource::Buffer(ref buffer) => BindingResourceId::Buffer(buffer.id()), + OwnedBindingResource::Data(_) => BindingResourceId::DataBuffer, + OwnedBindingResource::TextureView(ref texture_view_dimension, ref texture_view) => { + BindingResourceId::TextureView(*texture_view_dimension, texture_view.id()) + } + OwnedBindingResource::Sampler(_, ref sampler) => { + BindingResourceId::Sampler(sampler.id()) + } + } + } +} + +impl GetBindingResourceId for Buffer { + fn binding_resource_id(&self, _: BindlessResourceType) -> BindingResourceId { + BindingResourceId::Buffer(self.id()) + } +} + +impl GetBindingResourceId for Sampler { + fn binding_resource_id(&self, _: BindlessResourceType) -> BindingResourceId { + BindingResourceId::Sampler(self.id()) + } +} + +impl GetBindingResourceId for TextureView { + fn binding_resource_id(&self, resource_type: BindlessResourceType) -> BindingResourceId { + let texture_view_dimension = match resource_type { + BindlessResourceType::Texture1d => TextureViewDimension::D1, + BindlessResourceType::Texture2d => TextureViewDimension::D2, + BindlessResourceType::Texture2dArray => TextureViewDimension::D2Array, + BindlessResourceType::Texture3d => TextureViewDimension::D3, + BindlessResourceType::TextureCube => TextureViewDimension::Cube, + BindlessResourceType::TextureCubeArray => TextureViewDimension::CubeArray, + _ => panic!("Resource type is not a texture"), + }; + BindingResourceId::TextureView(texture_view_dimension, self.id()) + } +} + +impl MaterialBindGroupAllocator { + /// Creates a new [`MaterialBindGroupAllocator`] managing the data for a + /// single material. + pub fn new( + render_device: &RenderDevice, + label: Option<&'static str>, + bindless_descriptor: Option, + bind_group_layout: BindGroupLayout, + slab_capacity: Option, + ) -> MaterialBindGroupAllocator { + if let Some(bindless_descriptor) = bindless_descriptor { + MaterialBindGroupAllocator::Bindless(Box::new(MaterialBindGroupBindlessAllocator::new( + render_device, + label, + bindless_descriptor, + bind_group_layout, + slab_capacity, + ))) + } else { + MaterialBindGroupAllocator::NonBindless(Box::new( + MaterialBindGroupNonBindlessAllocator::new(label), + )) + } + } + + /// Returns the slab with the given index, if one exists. + pub fn get(&self, group: MaterialBindGroupIndex) -> Option> { + match *self { + MaterialBindGroupAllocator::Bindless(ref bindless_allocator) => bindless_allocator + .get(group) + .map(|bindless_slab| MaterialSlab(MaterialSlabImpl::Bindless(bindless_slab))), + MaterialBindGroupAllocator::NonBindless(ref non_bindless_allocator) => { + non_bindless_allocator.get(group).map(|non_bindless_slab| { + MaterialSlab(MaterialSlabImpl::NonBindless(non_bindless_slab)) + }) + } + } + } + + /// Allocates an [`UnpreparedBindGroup`] and returns the resulting binding ID. + /// + /// This method should generally be preferred over + /// [`Self::allocate_prepared`], because this method supports both bindless + /// and non-bindless bind groups. Only use [`Self::allocate_prepared`] if + /// you need to prepare the bind group yourself. + pub fn allocate_unprepared( + &mut self, + unprepared_bind_group: UnpreparedBindGroup, + bind_group_layout: &BindGroupLayout, + ) -> MaterialBindingId { + match *self { + MaterialBindGroupAllocator::Bindless( + ref mut material_bind_group_bindless_allocator, + ) => material_bind_group_bindless_allocator.allocate_unprepared(unprepared_bind_group), + MaterialBindGroupAllocator::NonBindless( + ref mut material_bind_group_non_bindless_allocator, + ) => material_bind_group_non_bindless_allocator + .allocate_unprepared(unprepared_bind_group, (*bind_group_layout).clone()), + } + } + + /// Places a pre-prepared bind group into a slab. + /// + /// For bindless materials, the allocator internally manages the bind + /// groups, so calling this method will panic if this is a bindless + /// allocator. Only non-bindless allocators support this method. + /// + /// It's generally preferred to use [`Self::allocate_unprepared`], because + /// that method supports both bindless and non-bindless allocators. Only use + /// this method if you need to prepare the bind group yourself. + pub fn allocate_prepared( + &mut self, + prepared_bind_group: PreparedBindGroup, + ) -> MaterialBindingId { + match *self { + MaterialBindGroupAllocator::Bindless(_) => { + panic!( + "Bindless resources are incompatible with implementing `as_bind_group` \ + directly; implement `unprepared_bind_group` instead or disable bindless" + ) + } + MaterialBindGroupAllocator::NonBindless(ref mut non_bindless_allocator) => { + non_bindless_allocator.allocate_prepared(prepared_bind_group) + } + } + } + + /// Deallocates the material with the given binding ID. + /// + /// Any resources that are no longer referenced are removed from the slab. + pub fn free(&mut self, material_binding_id: MaterialBindingId) { + match *self { + MaterialBindGroupAllocator::Bindless( + ref mut material_bind_group_bindless_allocator, + ) => material_bind_group_bindless_allocator.free(material_binding_id), + MaterialBindGroupAllocator::NonBindless( + ref mut material_bind_group_non_bindless_allocator, + ) => material_bind_group_non_bindless_allocator.free(material_binding_id), + } + } + + /// Recreates any bind groups corresponding to slabs that have been modified + /// since last calling [`MaterialBindGroupAllocator::prepare_bind_groups`]. + pub fn prepare_bind_groups( + &mut self, + render_device: &RenderDevice, + fallback_bindless_resources: &FallbackBindlessResources, + fallback_image: &FallbackImage, + ) { + match *self { + MaterialBindGroupAllocator::Bindless( + ref mut material_bind_group_bindless_allocator, + ) => material_bind_group_bindless_allocator.prepare_bind_groups( + render_device, + fallback_bindless_resources, + fallback_image, + ), + MaterialBindGroupAllocator::NonBindless( + ref mut material_bind_group_non_bindless_allocator, + ) => material_bind_group_non_bindless_allocator.prepare_bind_groups(render_device), + } + } + + /// Uploads the contents of all buffers that this + /// [`MaterialBindGroupAllocator`] manages to the GPU. + /// + /// Non-bindless allocators don't currently manage any buffers, so this + /// method only has an effect for bindless allocators. + pub fn write_buffers(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + match *self { + MaterialBindGroupAllocator::Bindless( + ref mut material_bind_group_bindless_allocator, + ) => material_bind_group_bindless_allocator.write_buffers(render_device, render_queue), + MaterialBindGroupAllocator::NonBindless(_) => { + // Not applicable. + } + } + } +} + +impl MaterialBindlessIndexTable { + /// Creates a new [`MaterialBindlessIndexTable`] for a single slab. + fn new( + bindless_index_table_descriptor: &BindlessIndexTableDescriptor, + ) -> MaterialBindlessIndexTable { + // Preallocate space for one bindings table, so that there will always be a buffer. + let mut buffer = RetainedRawBufferVec::new(BufferUsages::STORAGE); + for _ in *bindless_index_table_descriptor.indices.start + ..*bindless_index_table_descriptor.indices.end + { + buffer.push(0); + } + + MaterialBindlessIndexTable { + buffer, + index_range: bindless_index_table_descriptor.indices.clone(), + binding_number: bindless_index_table_descriptor.binding_number, + } + } + + /// Returns the bindings in the binding index table. + /// + /// If the current [`MaterialBindlessIndexTable::index_range`] is M..N, then + /// element *i* of the returned binding index table contains the slot of the + /// bindless resource with bindless index *i* + M. + fn get(&self, slot: MaterialBindGroupSlot) -> &[u32] { + let struct_size = *self.index_range.end as usize - *self.index_range.start as usize; + let start = struct_size * slot.0 as usize; + &self.buffer.values()[start..(start + struct_size)] + } + + /// Returns a single binding from the binding index table. + fn get_binding( + &self, + slot: MaterialBindGroupSlot, + bindless_index: BindlessIndex, + ) -> Option { + if bindless_index < self.index_range.start || bindless_index >= self.index_range.end { + return None; + } + self.get(slot) + .get((*bindless_index - *self.index_range.start) as usize) + .copied() + } + + fn table_length(&self) -> u32 { + self.index_range.end.0 - self.index_range.start.0 + } + + /// Updates the binding index table for a single material. + /// + /// The `allocated_resource_slots` map contains a mapping from the + /// [`BindlessIndex`] of each resource that the material references to the + /// slot that that resource occupies in the appropriate binding array. This + /// method serializes that map into a binding index table that the shader + /// can read. + fn set( + &mut self, + slot: MaterialBindGroupSlot, + allocated_resource_slots: &HashMap, + ) { + let table_len = self.table_length() as usize; + let range = (slot.0 as usize * table_len)..((slot.0 as usize + 1) * table_len); + while self.buffer.len() < range.end { + self.buffer.push(0); + } + + for (&bindless_index, &resource_slot) in allocated_resource_slots { + if self.index_range.contains(&bindless_index) { + self.buffer.set( + *bindless_index + range.start as u32 - *self.index_range.start, + resource_slot, + ); + } + } + + // Mark the buffer as needing to be recreated, in case we grew it. + self.buffer.dirty = BufferDirtyState::NeedsReserve; + } + + /// Returns the [`BindGroupEntry`] for the index table itself. + fn bind_group_entry(&self) -> BindGroupEntry<'_> { + BindGroupEntry { + binding: *self.binding_number, + resource: self + .buffer + .buffer() + .expect("Bindings buffer must exist") + .as_entire_binding(), + } + } +} + +impl RetainedRawBufferVec +where + T: Pod, +{ + /// Creates a new empty [`RetainedRawBufferVec`] supporting the given + /// [`BufferUsages`]. + fn new(buffer_usages: BufferUsages) -> RetainedRawBufferVec { + RetainedRawBufferVec { + buffer: RawBufferVec::new(buffer_usages), + dirty: BufferDirtyState::NeedsUpload, + } + } + + /// Recreates the GPU backing buffer if needed. + fn prepare(&mut self, render_device: &RenderDevice) { + match self.dirty { + BufferDirtyState::Clean | BufferDirtyState::NeedsUpload => {} + BufferDirtyState::NeedsReserve => { + let capacity = self.buffer.len(); + self.buffer.reserve(capacity, render_device); + self.dirty = BufferDirtyState::NeedsUpload; + } + } + } + + /// Writes the current contents of the buffer to the GPU if necessary. + fn write(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + match self.dirty { + BufferDirtyState::Clean => {} + BufferDirtyState::NeedsReserve | BufferDirtyState::NeedsUpload => { + self.buffer.write_buffer(render_device, render_queue); + self.dirty = BufferDirtyState::Clean; + } + } + } +} + +impl MaterialBindGroupBindlessAllocator { + /// Creates a new [`MaterialBindGroupBindlessAllocator`] managing the data + /// for a single bindless material. + fn new( + render_device: &RenderDevice, + label: Option<&'static str>, + bindless_descriptor: BindlessDescriptor, + bind_group_layout: BindGroupLayout, + slab_capacity: Option, + ) -> MaterialBindGroupBindlessAllocator { + let fallback_buffers = bindless_descriptor + .buffers + .iter() + .map(|bindless_buffer_descriptor| { + ( + bindless_buffer_descriptor.bindless_index, + render_device.create_buffer(&BufferDescriptor { + label: Some("bindless fallback buffer"), + size: match bindless_buffer_descriptor.size { + Some(size) => size as u64, + None => DEFAULT_BINDLESS_FALLBACK_BUFFER_SIZE, + }, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + ) + }) + .collect(); + + MaterialBindGroupBindlessAllocator { + label, + slabs: vec![], + bind_group_layout, + bindless_descriptor, + fallback_buffers, + slab_capacity: slab_capacity + .expect("Non-bindless materials should use the non-bindless allocator") + .resolve(), + } + } + + /// Allocates the resources for a single material into a slab and returns + /// the resulting ID. + /// + /// The returned [`MaterialBindingId`] can later be used to fetch the slab + /// that was used. + /// + /// This function can't fail. If all slabs are full, then a new slab is + /// created, and the material is allocated into it. + fn allocate_unprepared( + &mut self, + mut unprepared_bind_group: UnpreparedBindGroup, + ) -> MaterialBindingId { + for (slab_index, slab) in self.slabs.iter_mut().enumerate() { + trace!("Trying to allocate in slab {}", slab_index); + match slab.try_allocate(unprepared_bind_group, self.slab_capacity) { + Ok(slot) => { + return MaterialBindingId { + group: MaterialBindGroupIndex(slab_index as u32), + slot, + }; + } + Err(bind_group) => unprepared_bind_group = bind_group, + } + } + + let group = MaterialBindGroupIndex(self.slabs.len() as u32); + self.slabs + .push(MaterialBindlessSlab::new(&self.bindless_descriptor)); + + // Allocate into the newly-pushed slab. + let Ok(slot) = self + .slabs + .last_mut() + .expect("We just pushed a slab") + .try_allocate(unprepared_bind_group, self.slab_capacity) + else { + panic!("An allocation into an empty slab should always succeed") + }; + + MaterialBindingId { group, slot } + } + + /// Deallocates the material with the given binding ID. + /// + /// Any resources that are no longer referenced are removed from the slab. + fn free(&mut self, material_binding_id: MaterialBindingId) { + self.slabs + .get_mut(material_binding_id.group.0 as usize) + .expect("Slab should exist") + .free(material_binding_id.slot, &self.bindless_descriptor); + } + + /// Returns the slab with the given bind group index. + /// + /// A [`MaterialBindGroupIndex`] can be fetched from a + /// [`MaterialBindingId`]. + fn get(&self, group: MaterialBindGroupIndex) -> Option<&MaterialBindlessSlab> { + self.slabs.get(group.0 as usize) + } + + /// Recreates any bind groups corresponding to slabs that have been modified + /// since last calling + /// [`MaterialBindGroupBindlessAllocator::prepare_bind_groups`]. + fn prepare_bind_groups( + &mut self, + render_device: &RenderDevice, + fallback_bindless_resources: &FallbackBindlessResources, + fallback_image: &FallbackImage, + ) { + for slab in &mut self.slabs { + slab.prepare( + render_device, + self.label, + &self.bind_group_layout, + fallback_bindless_resources, + &self.fallback_buffers, + fallback_image, + &self.bindless_descriptor, + self.slab_capacity, + ); + } + } + + /// Writes any buffers that we're managing to the GPU. + /// + /// Currently, this only consists of the bindless index tables. + fn write_buffers(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + for slab in &mut self.slabs { + slab.write_buffer(render_device, render_queue); + } + } +} + +impl MaterialBindlessSlab { + /// Attempts to allocate the given unprepared bind group in this slab. + /// + /// If the allocation succeeds, this method returns the slot that the + /// allocation was placed in. If the allocation fails because the slab was + /// full, this method returns the unprepared bind group back to the caller + /// so that it can try to allocate again. + fn try_allocate( + &mut self, + unprepared_bind_group: UnpreparedBindGroup, + slot_capacity: u32, + ) -> Result { + // Locate pre-existing resources, and determine how many free slots we need. + let Some(allocation_candidate) = self.check_allocation(&unprepared_bind_group) else { + return Err(unprepared_bind_group); + }; + + // Check to see if we have enough free space. + // + // As a special case, note that if *nothing* is allocated in this slab, + // then we always allow a material to be placed in it, regardless of the + // number of bindings the material has. This is so that, if the + // platform's maximum bindless count is set too low to hold even a + // single material, we can still place each material into a separate + // slab instead of failing outright. + if self.allocated_resource_count > 0 + && self.allocated_resource_count + allocation_candidate.needed_free_slots + > slot_capacity + { + trace!("Slab is full, can't allocate"); + return Err(unprepared_bind_group); + } + + // OK, we can allocate in this slab. Assign a slot ID. + let slot = self + .free_slots + .pop() + .unwrap_or(MaterialBindGroupSlot(self.live_allocation_count)); + + // Bump the live allocation count. + self.live_allocation_count += 1; + + // Insert the resources into the binding arrays. + let allocated_resource_slots = + self.insert_resources(unprepared_bind_group.bindings, allocation_candidate); + + // Serialize the allocated resource slots. + for bindless_index_table in &mut self.bindless_index_tables { + bindless_index_table.set(slot, &allocated_resource_slots); + } + + // Invalidate the cached bind group. + self.bind_group = None; + + Ok(slot) + } + + /// Gathers the information needed to determine whether the given unprepared + /// bind group can be allocated in this slab. + fn check_allocation( + &self, + unprepared_bind_group: &UnpreparedBindGroup, + ) -> Option { + let mut allocation_candidate = BindlessAllocationCandidate { + pre_existing_resources: HashMap::default(), + needed_free_slots: 0, + }; + + for &(bindless_index, ref owned_binding_resource) in unprepared_bind_group.bindings.iter() { + let bindless_index = BindlessIndex(bindless_index); + match *owned_binding_resource { + OwnedBindingResource::Buffer(ref buffer) => { + let Some(binding_array) = self.buffers.get(&bindless_index) else { + error!( + "Binding array wasn't present for buffer at index {:?}", + bindless_index + ); + return None; + }; + match binding_array.find(BindingResourceId::Buffer(buffer.id())) { + Some(slot) => { + allocation_candidate + .pre_existing_resources + .insert(bindless_index, slot); + } + None => allocation_candidate.needed_free_slots += 1, + } + } + + OwnedBindingResource::Data(_) => { + // The size of a data buffer is unlimited. + } + + OwnedBindingResource::TextureView(texture_view_dimension, ref texture_view) => { + let bindless_resource_type = BindlessResourceType::from(texture_view_dimension); + match self + .textures + .get(&bindless_resource_type) + .expect("Missing binding array for texture") + .find(BindingResourceId::TextureView( + texture_view_dimension, + texture_view.id(), + )) { + Some(slot) => { + allocation_candidate + .pre_existing_resources + .insert(bindless_index, slot); + } + None => { + allocation_candidate.needed_free_slots += 1; + } + } + } + + OwnedBindingResource::Sampler(sampler_binding_type, ref sampler) => { + let bindless_resource_type = BindlessResourceType::from(sampler_binding_type); + match self + .samplers + .get(&bindless_resource_type) + .expect("Missing binding array for sampler") + .find(BindingResourceId::Sampler(sampler.id())) + { + Some(slot) => { + allocation_candidate + .pre_existing_resources + .insert(bindless_index, slot); + } + None => { + allocation_candidate.needed_free_slots += 1; + } + } + } + } + } + + Some(allocation_candidate) + } + + /// Inserts the given [`BindingResources`] into this slab. + /// + /// Returns a table that maps the bindless index of each resource to its + /// slot in its binding array. + fn insert_resources( + &mut self, + mut binding_resources: BindingResources, + allocation_candidate: BindlessAllocationCandidate, + ) -> HashMap { + let mut allocated_resource_slots = HashMap::default(); + + for (bindless_index, owned_binding_resource) in binding_resources.drain(..) { + let bindless_index = BindlessIndex(bindless_index); + + let pre_existing_slot = allocation_candidate + .pre_existing_resources + .get(&bindless_index); + + // Otherwise, we need to insert it anew. + let binding_resource_id = BindingResourceId::from(&owned_binding_resource); + let increment_allocated_resource_count = match owned_binding_resource { + OwnedBindingResource::Buffer(buffer) => { + let slot = self + .buffers + .get_mut(&bindless_index) + .expect("Buffer binding array should exist") + .insert(binding_resource_id, buffer); + allocated_resource_slots.insert(bindless_index, slot); + + if let Some(pre_existing_slot) = pre_existing_slot { + assert_eq!(*pre_existing_slot, slot); + + false + } else { + true + } + } + OwnedBindingResource::Data(data) => { + if pre_existing_slot.is_some() { + panic!("Data buffers can't be deduplicated") + } + + let slot = self + .data_buffers + .get_mut(&bindless_index) + .expect("Data buffer binding array should exist") + .insert(&data); + allocated_resource_slots.insert(bindless_index, slot); + false + } + OwnedBindingResource::TextureView(texture_view_dimension, texture_view) => { + let bindless_resource_type = BindlessResourceType::from(texture_view_dimension); + let slot = self + .textures + .get_mut(&bindless_resource_type) + .expect("Texture array should exist") + .insert(binding_resource_id, texture_view); + allocated_resource_slots.insert(bindless_index, slot); + + if let Some(pre_existing_slot) = pre_existing_slot { + assert_eq!(*pre_existing_slot, slot); + + false + } else { + true + } + } + OwnedBindingResource::Sampler(sampler_binding_type, sampler) => { + let bindless_resource_type = BindlessResourceType::from(sampler_binding_type); + let slot = self + .samplers + .get_mut(&bindless_resource_type) + .expect("Sampler should exist") + .insert(binding_resource_id, sampler); + allocated_resource_slots.insert(bindless_index, slot); + + if let Some(pre_existing_slot) = pre_existing_slot { + assert_eq!(*pre_existing_slot, slot); + + false + } else { + true + } + } + }; + + // Bump the allocated resource count. + if increment_allocated_resource_count { + self.allocated_resource_count += 1; + } + } + + allocated_resource_slots + } + + /// Removes the material allocated in the given slot, with the given + /// descriptor, from this slab. + fn free(&mut self, slot: MaterialBindGroupSlot, bindless_descriptor: &BindlessDescriptor) { + // Loop through each binding. + for (bindless_index, bindless_resource_type) in + bindless_descriptor.resources.iter().enumerate() + { + let bindless_index = BindlessIndex::from(bindless_index as u32); + let Some(bindless_index_table) = self.get_bindless_index_table(bindless_index) else { + continue; + }; + let Some(bindless_binding) = bindless_index_table.get_binding(slot, bindless_index) + else { + continue; + }; + + // Free the binding. If the resource in question was anything other + // than a data buffer, then it has a reference count and + // consequently we need to decrement it. + let decrement_allocated_resource_count = match *bindless_resource_type { + BindlessResourceType::None => false, + BindlessResourceType::Buffer => self + .buffers + .get_mut(&bindless_index) + .expect("Buffer should exist with that bindless index") + .remove(bindless_binding), + BindlessResourceType::DataBuffer => { + self.data_buffers + .get_mut(&bindless_index) + .expect("Data buffer should exist with that bindless index") + .remove(bindless_binding); + false + } + BindlessResourceType::SamplerFiltering + | BindlessResourceType::SamplerNonFiltering + | BindlessResourceType::SamplerComparison => self + .samplers + .get_mut(bindless_resource_type) + .expect("Sampler array should exist") + .remove(bindless_binding), + BindlessResourceType::Texture1d + | BindlessResourceType::Texture2d + | BindlessResourceType::Texture2dArray + | BindlessResourceType::Texture3d + | BindlessResourceType::TextureCube + | BindlessResourceType::TextureCubeArray => self + .textures + .get_mut(bindless_resource_type) + .expect("Texture array should exist") + .remove(bindless_binding), + }; + + // If the slot is now free, decrement the allocated resource + // count. + if decrement_allocated_resource_count { + self.allocated_resource_count -= 1; + } + } + + // Invalidate the cached bind group. + self.bind_group = None; + + // Release the slot ID. + self.free_slots.push(slot); + self.live_allocation_count -= 1; + } + + /// Recreates the bind group and bindless index table buffer if necessary. + fn prepare( + &mut self, + render_device: &RenderDevice, + label: Option<&'static str>, + bind_group_layout: &BindGroupLayout, + fallback_bindless_resources: &FallbackBindlessResources, + fallback_buffers: &HashMap, + fallback_image: &FallbackImage, + bindless_descriptor: &BindlessDescriptor, + slab_capacity: u32, + ) { + // Create the bindless index table buffers if needed. + for bindless_index_table in &mut self.bindless_index_tables { + bindless_index_table.buffer.prepare(render_device); + } + + // Create any data buffers we were managing if necessary. + for data_buffer in self.data_buffers.values_mut() { + data_buffer.buffer.prepare(render_device); + } + + // Create the bind group if needed. + self.prepare_bind_group( + render_device, + label, + bind_group_layout, + fallback_bindless_resources, + fallback_buffers, + fallback_image, + bindless_descriptor, + slab_capacity, + ); + } + + /// Recreates the bind group if this slab has been changed since the last + /// time we created it. + fn prepare_bind_group( + &mut self, + render_device: &RenderDevice, + label: Option<&'static str>, + bind_group_layout: &BindGroupLayout, + fallback_bindless_resources: &FallbackBindlessResources, + fallback_buffers: &HashMap, + fallback_image: &FallbackImage, + bindless_descriptor: &BindlessDescriptor, + slab_capacity: u32, + ) { + // If the bind group is clean, then do nothing. + if self.bind_group.is_some() { + return; + } + + // Determine whether we need to pad out our binding arrays with dummy + // resources. + let required_binding_array_size = if render_device + .features() + .contains(WgpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY) + { + None + } else { + Some(slab_capacity) + }; + + let binding_resource_arrays = self.create_binding_resource_arrays( + fallback_bindless_resources, + fallback_buffers, + fallback_image, + bindless_descriptor, + required_binding_array_size, + ); + + let mut bind_group_entries: Vec<_> = self + .bindless_index_tables + .iter() + .map(|bindless_index_table| bindless_index_table.bind_group_entry()) + .collect(); + + for &(&binding, ref binding_resource_array) in binding_resource_arrays.iter() { + bind_group_entries.push(BindGroupEntry { + binding, + resource: match *binding_resource_array { + BindingResourceArray::Buffers(ref buffer_bindings) => { + BindingResource::BufferArray(&buffer_bindings[..]) + } + BindingResourceArray::TextureViews(ref texture_views) => { + BindingResource::TextureViewArray(&texture_views[..]) + } + BindingResourceArray::Samplers(ref samplers) => { + BindingResource::SamplerArray(&samplers[..]) + } + }, + }); + } + + // Create bind group entries for any data buffers we're managing. + for data_buffer in self.data_buffers.values() { + bind_group_entries.push(BindGroupEntry { + binding: *data_buffer.binding_number, + resource: data_buffer + .buffer + .buffer() + .expect("Backing data buffer must have been uploaded by now") + .as_entire_binding(), + }); + } + + self.bind_group = + Some(render_device.create_bind_group(label, bind_group_layout, &bind_group_entries)); + } + + /// Writes any buffers that we're managing to the GPU. + /// + /// Currently, this consists of the bindless index table plus any data + /// buffers we're managing. + fn write_buffer(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + for bindless_index_table in &mut self.bindless_index_tables { + bindless_index_table + .buffer + .write(render_device, render_queue); + } + + for data_buffer in self.data_buffers.values_mut() { + data_buffer.buffer.write(render_device, render_queue); + } + } + + /// Converts our binding arrays into binding resource arrays suitable for + /// passing to `wgpu`. + fn create_binding_resource_arrays<'a>( + &'a self, + fallback_bindless_resources: &'a FallbackBindlessResources, + fallback_buffers: &'a HashMap, + fallback_image: &'a FallbackImage, + bindless_descriptor: &'a BindlessDescriptor, + required_binding_array_size: Option, + ) -> Vec<(&'a u32, BindingResourceArray<'a>)> { + let mut binding_resource_arrays = vec![]; + + // Build sampler bindings. + self.create_sampler_binding_resource_arrays( + &mut binding_resource_arrays, + fallback_bindless_resources, + required_binding_array_size, + ); + + // Build texture bindings. + self.create_texture_binding_resource_arrays( + &mut binding_resource_arrays, + fallback_image, + required_binding_array_size, + ); + + // Build buffer bindings. + self.create_buffer_binding_resource_arrays( + &mut binding_resource_arrays, + fallback_buffers, + bindless_descriptor, + required_binding_array_size, + ); + + binding_resource_arrays + } + + /// Accumulates sampler binding arrays into binding resource arrays suitable + /// for passing to `wgpu`. + fn create_sampler_binding_resource_arrays<'a, 'b>( + &'a self, + binding_resource_arrays: &'b mut Vec<(&'a u32, BindingResourceArray<'a>)>, + fallback_bindless_resources: &'a FallbackBindlessResources, + required_binding_array_size: Option, + ) { + // We have one binding resource array per sampler type. + for (bindless_resource_type, fallback_sampler) in [ + ( + BindlessResourceType::SamplerFiltering, + &fallback_bindless_resources.filtering_sampler, + ), + ( + BindlessResourceType::SamplerNonFiltering, + &fallback_bindless_resources.non_filtering_sampler, + ), + ( + BindlessResourceType::SamplerComparison, + &fallback_bindless_resources.comparison_sampler, + ), + ] { + let mut sampler_bindings = vec![]; + + match self.samplers.get(&bindless_resource_type) { + Some(sampler_bindless_binding_array) => { + for maybe_bindless_binding in sampler_bindless_binding_array.bindings.iter() { + match *maybe_bindless_binding { + Some(ref bindless_binding) => { + sampler_bindings.push(&*bindless_binding.resource); + } + None => sampler_bindings.push(&**fallback_sampler), + } + } + } + + None => { + // Fill with a single fallback sampler. + sampler_bindings.push(&**fallback_sampler); + } + } + + if let Some(required_binding_array_size) = required_binding_array_size { + sampler_bindings.extend(iter::repeat_n( + &**fallback_sampler, + required_binding_array_size as usize - sampler_bindings.len(), + )); + } + + let binding_number = bindless_resource_type + .binding_number() + .expect("Sampler bindless resource type must have a binding number"); + + binding_resource_arrays.push(( + &**binding_number, + BindingResourceArray::Samplers(sampler_bindings), + )); + } + } + + /// Accumulates texture binding arrays into binding resource arrays suitable + /// for passing to `wgpu`. + fn create_texture_binding_resource_arrays<'a, 'b>( + &'a self, + binding_resource_arrays: &'b mut Vec<(&'a u32, BindingResourceArray<'a>)>, + fallback_image: &'a FallbackImage, + required_binding_array_size: Option, + ) { + for (bindless_resource_type, fallback_image) in [ + (BindlessResourceType::Texture1d, &fallback_image.d1), + (BindlessResourceType::Texture2d, &fallback_image.d2), + ( + BindlessResourceType::Texture2dArray, + &fallback_image.d2_array, + ), + (BindlessResourceType::Texture3d, &fallback_image.d3), + (BindlessResourceType::TextureCube, &fallback_image.cube), + ( + BindlessResourceType::TextureCubeArray, + &fallback_image.cube_array, + ), + ] { + let mut texture_bindings = vec![]; + + let binding_number = bindless_resource_type + .binding_number() + .expect("Texture bindless resource type must have a binding number"); + + match self.textures.get(&bindless_resource_type) { + Some(texture_bindless_binding_array) => { + for maybe_bindless_binding in texture_bindless_binding_array.bindings.iter() { + match *maybe_bindless_binding { + Some(ref bindless_binding) => { + texture_bindings.push(&*bindless_binding.resource); + } + None => texture_bindings.push(&*fallback_image.texture_view), + } + } + } + + None => { + // Fill with a single fallback image. + texture_bindings.push(&*fallback_image.texture_view); + } + } + + if let Some(required_binding_array_size) = required_binding_array_size { + texture_bindings.extend(iter::repeat_n( + &*fallback_image.texture_view, + required_binding_array_size as usize - texture_bindings.len(), + )); + } + + binding_resource_arrays.push(( + binding_number, + BindingResourceArray::TextureViews(texture_bindings), + )); + } + } + + /// Accumulates buffer binding arrays into binding resource arrays suitable + /// for `wgpu`. + fn create_buffer_binding_resource_arrays<'a, 'b>( + &'a self, + binding_resource_arrays: &'b mut Vec<(&'a u32, BindingResourceArray<'a>)>, + fallback_buffers: &'a HashMap, + bindless_descriptor: &'a BindlessDescriptor, + required_binding_array_size: Option, + ) { + for bindless_buffer_descriptor in bindless_descriptor.buffers.iter() { + let Some(buffer_bindless_binding_array) = + self.buffers.get(&bindless_buffer_descriptor.bindless_index) + else { + // This is OK, because index buffers are present in + // `BindlessDescriptor::buffers` but not in + // `BindlessDescriptor::resources`. + continue; + }; + + let fallback_buffer = fallback_buffers + .get(&bindless_buffer_descriptor.bindless_index) + .expect("Fallback buffer should exist"); + + let mut buffer_bindings: Vec<_> = buffer_bindless_binding_array + .bindings + .iter() + .map(|maybe_bindless_binding| { + let buffer = match *maybe_bindless_binding { + None => fallback_buffer, + Some(ref bindless_binding) => &bindless_binding.resource, + }; + BufferBinding { + buffer, + offset: 0, + size: None, + } + }) + .collect(); + + if let Some(required_binding_array_size) = required_binding_array_size { + buffer_bindings.extend(iter::repeat_n( + BufferBinding { + buffer: fallback_buffer, + offset: 0, + size: None, + }, + required_binding_array_size as usize - buffer_bindings.len(), + )); + } + + binding_resource_arrays.push(( + &*buffer_bindless_binding_array.binding_number, + BindingResourceArray::Buffers(buffer_bindings), + )); + } + } + + /// Returns the [`BindGroup`] corresponding to this slab, if it's been + /// prepared. + fn bind_group(&self) -> Option<&BindGroup> { + self.bind_group.as_ref() + } + + /// Returns the bindless index table containing the given bindless index. + fn get_bindless_index_table( + &self, + bindless_index: BindlessIndex, + ) -> Option<&MaterialBindlessIndexTable> { + let table_index = self + .bindless_index_tables + .binary_search_by(|bindless_index_table| { + if bindless_index < bindless_index_table.index_range.start { + Ordering::Less + } else if bindless_index >= bindless_index_table.index_range.end { + Ordering::Greater + } else { + Ordering::Equal + } + }) + .ok()?; + self.bindless_index_tables.get(table_index) + } +} + +impl MaterialBindlessBindingArray +where + R: GetBindingResourceId, +{ + /// Creates a new [`MaterialBindlessBindingArray`] with the given binding + /// number, managing resources of the given type. + fn new( + binding_number: BindingNumber, + resource_type: BindlessResourceType, + ) -> MaterialBindlessBindingArray { + MaterialBindlessBindingArray { + binding_number, + bindings: vec![], + resource_type, + resource_to_slot: HashMap::default(), + free_slots: vec![], + len: 0, + } + } + + /// Returns the slot corresponding to the given resource, if that resource + /// is located in this binding array. + /// + /// If the resource isn't in this binding array, this method returns `None`. + fn find(&self, binding_resource_id: BindingResourceId) -> Option { + self.resource_to_slot.get(&binding_resource_id).copied() + } + + /// Inserts a bindless resource into a binding array and returns the index + /// of the slot it was inserted into. + fn insert(&mut self, binding_resource_id: BindingResourceId, resource: R) -> u32 { + match self.resource_to_slot.entry(binding_resource_id) { + bevy_platform::collections::hash_map::Entry::Occupied(o) => { + let slot = *o.get(); + + self.bindings[slot as usize] + .as_mut() + .expect("A slot in the resource_to_slot map should have a value") + .ref_count += 1; + + slot + } + bevy_platform::collections::hash_map::Entry::Vacant(v) => { + let slot = self.free_slots.pop().unwrap_or(self.len); + v.insert(slot); + + if self.bindings.len() < slot as usize + 1 { + self.bindings.resize_with(slot as usize + 1, || None); + } + self.bindings[slot as usize] = Some(MaterialBindlessBinding::new(resource)); + + self.len += 1; + slot + } + } + } + + /// Removes a reference to an object from the slot. + /// + /// If the reference count dropped to 0 and the object was freed, this + /// method returns true. If the object was still referenced after removing + /// it, returns false. + fn remove(&mut self, slot: u32) -> bool { + let maybe_binding = &mut self.bindings[slot as usize]; + let binding = maybe_binding + .as_mut() + .expect("Attempted to free an already-freed binding"); + + binding.ref_count -= 1; + if binding.ref_count != 0 { + return false; + } + + let binding_resource_id = binding.resource.binding_resource_id(self.resource_type); + self.resource_to_slot.remove(&binding_resource_id); + + *maybe_binding = None; + self.free_slots.push(slot); + self.len -= 1; + true + } +} + +impl MaterialBindlessBinding +where + R: GetBindingResourceId, +{ + /// Creates a new [`MaterialBindlessBinding`] for a freshly-added resource. + /// + /// The reference count is initialized to 1. + fn new(resource: R) -> MaterialBindlessBinding { + MaterialBindlessBinding { + resource, + ref_count: 1, + } + } +} + +/// Returns true if the material will *actually* use bindless resources or false +/// if it won't. +/// +/// This takes the platform support (or lack thereof) for bindless resources +/// into account. +pub fn material_uses_bindless_resources(render_device: &RenderDevice) -> bool +where + M: Material, +{ + M::bindless_slot_count().is_some_and(|bindless_slot_count| { + M::bindless_supported(render_device) && bindless_slot_count.resolve() > 1 + }) +} + +impl MaterialBindlessSlab { + /// Creates a new [`MaterialBindlessSlab`] for a material with the given + /// bindless descriptor. + /// + /// We use this when no existing slab could hold a material to be allocated. + fn new(bindless_descriptor: &BindlessDescriptor) -> MaterialBindlessSlab { + let mut buffers = HashMap::default(); + let mut samplers = HashMap::default(); + let mut textures = HashMap::default(); + let mut data_buffers = HashMap::default(); + + for (bindless_index, bindless_resource_type) in + bindless_descriptor.resources.iter().enumerate() + { + let bindless_index = BindlessIndex(bindless_index as u32); + match *bindless_resource_type { + BindlessResourceType::None => {} + BindlessResourceType::Buffer => { + let binding_number = bindless_descriptor + .buffers + .iter() + .find(|bindless_buffer_descriptor| { + bindless_buffer_descriptor.bindless_index == bindless_index + }) + .expect( + "Bindless buffer descriptor matching that bindless index should be \ + present", + ) + .binding_number; + buffers.insert( + bindless_index, + MaterialBindlessBindingArray::new(binding_number, *bindless_resource_type), + ); + } + BindlessResourceType::DataBuffer => { + // Copy the data in. + let buffer_descriptor = bindless_descriptor + .buffers + .iter() + .find(|bindless_buffer_descriptor| { + bindless_buffer_descriptor.bindless_index == bindless_index + }) + .expect( + "Bindless buffer descriptor matching that bindless index should be \ + present", + ); + data_buffers.insert( + bindless_index, + MaterialDataBuffer::new( + buffer_descriptor.binding_number, + buffer_descriptor + .size + .expect("Data buffers should have a size") + as u32, + ), + ); + } + BindlessResourceType::SamplerFiltering + | BindlessResourceType::SamplerNonFiltering + | BindlessResourceType::SamplerComparison => { + samplers.insert( + *bindless_resource_type, + MaterialBindlessBindingArray::new( + *bindless_resource_type.binding_number().unwrap(), + *bindless_resource_type, + ), + ); + } + BindlessResourceType::Texture1d + | BindlessResourceType::Texture2d + | BindlessResourceType::Texture2dArray + | BindlessResourceType::Texture3d + | BindlessResourceType::TextureCube + | BindlessResourceType::TextureCubeArray => { + textures.insert( + *bindless_resource_type, + MaterialBindlessBindingArray::new( + *bindless_resource_type.binding_number().unwrap(), + *bindless_resource_type, + ), + ); + } + } + } + + let bindless_index_tables = bindless_descriptor + .index_tables + .iter() + .map(MaterialBindlessIndexTable::new) + .collect(); + + MaterialBindlessSlab { + bind_group: None, + bindless_index_tables, + samplers, + textures, + buffers, + data_buffers, + free_slots: vec![], + live_allocation_count: 0, + allocated_resource_count: 0, + } + } +} + +pub fn init_fallback_bindless_resources(mut commands: Commands, render_device: Res) { + commands.insert_resource(FallbackBindlessResources { + filtering_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("fallback filtering sampler"), + ..default() + }), + non_filtering_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("fallback non-filtering sampler"), + mag_filter: FilterMode::Nearest, + min_filter: FilterMode::Nearest, + mipmap_filter: FilterMode::Nearest, + ..default() + }), + comparison_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("fallback comparison sampler"), + compare: Some(CompareFunction::Always), + ..default() + }), + }); +} + +impl MaterialBindGroupNonBindlessAllocator { + /// Creates a new [`MaterialBindGroupNonBindlessAllocator`] managing the + /// bind groups for a single non-bindless material. + fn new(label: Option<&'static str>) -> MaterialBindGroupNonBindlessAllocator { + MaterialBindGroupNonBindlessAllocator { + label, + bind_groups: vec![], + to_prepare: HashSet::default(), + free_indices: vec![], + } + } + + /// Inserts a bind group, either unprepared or prepared, into this allocator + /// and returns a [`MaterialBindingId`]. + /// + /// The returned [`MaterialBindingId`] can later be used to fetch the bind + /// group. + fn allocate(&mut self, bind_group: MaterialNonBindlessAllocatedBindGroup) -> MaterialBindingId { + let group_id = self + .free_indices + .pop() + .unwrap_or(MaterialBindGroupIndex(self.bind_groups.len() as u32)); + if self.bind_groups.len() < *group_id as usize + 1 { + self.bind_groups + .resize_with(*group_id as usize + 1, || None); + } + + if matches!( + bind_group, + MaterialNonBindlessAllocatedBindGroup::Unprepared { .. } + ) { + self.to_prepare.insert(group_id); + } + + self.bind_groups[*group_id as usize] = Some(bind_group); + + MaterialBindingId { + group: group_id, + slot: default(), + } + } + + /// Inserts an unprepared bind group into this allocator and returns a + /// [`MaterialBindingId`]. + fn allocate_unprepared( + &mut self, + unprepared_bind_group: UnpreparedBindGroup, + bind_group_layout: BindGroupLayout, + ) -> MaterialBindingId { + self.allocate(MaterialNonBindlessAllocatedBindGroup::Unprepared { + bind_group: unprepared_bind_group, + layout: bind_group_layout, + }) + } + + /// Inserts an prepared bind group into this allocator and returns a + /// [`MaterialBindingId`]. + fn allocate_prepared(&mut self, prepared_bind_group: PreparedBindGroup) -> MaterialBindingId { + self.allocate(MaterialNonBindlessAllocatedBindGroup::Prepared { + bind_group: prepared_bind_group, + uniform_buffers: vec![], + }) + } + + /// Deallocates the bind group with the given binding ID. + fn free(&mut self, binding_id: MaterialBindingId) { + debug_assert_eq!(binding_id.slot, MaterialBindGroupSlot(0)); + debug_assert!(self.bind_groups[*binding_id.group as usize].is_some()); + self.bind_groups[*binding_id.group as usize] = None; + self.to_prepare.remove(&binding_id.group); + self.free_indices.push(binding_id.group); + } + + /// Returns a wrapper around the bind group with the given index. + fn get(&self, group: MaterialBindGroupIndex) -> Option> { + self.bind_groups[group.0 as usize] + .as_ref() + .map(|bind_group| match bind_group { + MaterialNonBindlessAllocatedBindGroup::Prepared { bind_group, .. } => { + MaterialNonBindlessSlab::Prepared(bind_group) + } + MaterialNonBindlessAllocatedBindGroup::Unprepared { .. } => { + MaterialNonBindlessSlab::Unprepared + } + }) + } + + /// Prepares any as-yet unprepared bind groups that this allocator is + /// managing. + /// + /// Unprepared bind groups can be added to this allocator with + /// [`Self::allocate_unprepared`]. Such bind groups will defer being + /// prepared until the next time this method is called. + fn prepare_bind_groups(&mut self, render_device: &RenderDevice) { + for bind_group_index in mem::take(&mut self.to_prepare) { + let Some(MaterialNonBindlessAllocatedBindGroup::Unprepared { + bind_group: unprepared_bind_group, + layout: bind_group_layout, + }) = mem::take(&mut self.bind_groups[*bind_group_index as usize]) + else { + panic!("Allocation didn't exist or was already prepared"); + }; + + // Pack any `Data` into uniform buffers. + let mut uniform_buffers = vec![]; + for (index, binding) in unprepared_bind_group.bindings.iter() { + let OwnedBindingResource::Data(ref owned_data) = *binding else { + continue; + }; + let label = format!("material uniform data {}", *index); + let uniform_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some(&label), + contents: &owned_data.0, + usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM, + }); + uniform_buffers.push(uniform_buffer); + } + + // Create bind group entries. + let mut bind_group_entries = vec![]; + let mut uniform_buffers_iter = uniform_buffers.iter(); + for (index, binding) in unprepared_bind_group.bindings.iter() { + match *binding { + OwnedBindingResource::Data(_) => { + bind_group_entries.push(BindGroupEntry { + binding: *index, + resource: uniform_buffers_iter + .next() + .expect("We should have created uniform buffers for each `Data`") + .as_entire_binding(), + }); + } + _ => bind_group_entries.push(BindGroupEntry { + binding: *index, + resource: binding.get_binding(), + }), + } + } + + // Create the bind group. + let bind_group = render_device.create_bind_group( + self.label, + &bind_group_layout, + &bind_group_entries, + ); + + self.bind_groups[*bind_group_index as usize] = + Some(MaterialNonBindlessAllocatedBindGroup::Prepared { + bind_group: PreparedBindGroup { + bindings: unprepared_bind_group.bindings, + bind_group, + }, + uniform_buffers, + }); + } + } +} + +impl<'a> MaterialSlab<'a> { + /// Returns the [`BindGroup`] corresponding to this slab, if it's been + /// prepared. + /// + /// You can prepare bind groups by calling + /// [`MaterialBindGroupAllocator::prepare_bind_groups`]. If the bind group + /// isn't ready, this method returns `None`. + pub fn bind_group(&self) -> Option<&'a BindGroup> { + match self.0 { + MaterialSlabImpl::Bindless(material_bindless_slab) => { + material_bindless_slab.bind_group() + } + MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Prepared( + prepared_bind_group, + )) => Some(&prepared_bind_group.bind_group), + MaterialSlabImpl::NonBindless(MaterialNonBindlessSlab::Unprepared) => None, + } + } +} + +impl MaterialDataBuffer { + /// Creates a new [`MaterialDataBuffer`] managing a buffer of elements of + /// size `aligned_element_size` that will be bound to the given binding + /// number. + fn new(binding_number: BindingNumber, aligned_element_size: u32) -> MaterialDataBuffer { + MaterialDataBuffer { + binding_number, + buffer: RetainedRawBufferVec::new(BufferUsages::STORAGE), + aligned_element_size, + free_slots: vec![], + len: 0, + } + } + + /// Allocates a slot for a new piece of data, copies the data into that + /// slot, and returns the slot ID. + /// + /// The size of the piece of data supplied to this method must equal the + /// [`Self::aligned_element_size`] provided to [`MaterialDataBuffer::new`]. + fn insert(&mut self, data: &[u8]) -> u32 { + // Make sure the data is of the right length. + debug_assert_eq!(data.len(), self.aligned_element_size as usize); + + // Grab a slot. + let slot = self.free_slots.pop().unwrap_or(self.len); + + // Calculate the range we're going to copy to. + let start = slot as usize * self.aligned_element_size as usize; + let end = (slot as usize + 1) * self.aligned_element_size as usize; + + // Resize the buffer if necessary. + if self.buffer.len() < end { + self.buffer.reserve_internal(end); + } + while self.buffer.values().len() < end { + self.buffer.push(0); + } + + // Copy in the data. + self.buffer.values_mut()[start..end].copy_from_slice(data); + + // Mark the buffer dirty, and finish up. + self.len += 1; + self.buffer.dirty = BufferDirtyState::NeedsReserve; + slot + } + + /// Marks the given slot as free. + fn remove(&mut self, slot: u32) { + self.free_slots.push(slot); + self.len -= 1; + } +} diff --git a/crates/libmarathon/src/render/pbr/mesh_material.rs b/crates/libmarathon/src/render/pbr/mesh_material.rs new file mode 100644 index 0000000..443a3ba --- /dev/null +++ b/crates/libmarathon/src/render/pbr/mesh_material.rs @@ -0,0 +1,75 @@ +use crate::render::pbr::Material; +use bevy_asset::{AsAssetId, AssetId, Handle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{component::Component, reflect::ReflectComponent}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use derive_more::derive::From; + +/// A [material](Material) used for rendering a [`Mesh3d`]. +/// +/// See [`Material`] for general information about 3D materials and how to implement your own materials. +/// +/// [`Mesh3d`]: bevy_mesh::Mesh3d +/// +/// # Example +/// +/// ``` +/// # use bevy_pbr::{Material, MeshMaterial3d, StandardMaterial}; +/// # use bevy_ecs::prelude::*; +/// # use bevy_mesh::{Mesh, Mesh3d}; +/// # use bevy_color::palettes::basic::RED; +/// # use bevy_asset::Assets; +/// # use bevy_math::primitives::Capsule3d; +/// # +/// // Spawn an entity with a mesh using `StandardMaterial`. +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// ) { +/// commands.spawn(( +/// Mesh3d(meshes.add(Capsule3d::default())), +/// MeshMaterial3d(materials.add(StandardMaterial { +/// base_color: RED.into(), +/// ..Default::default() +/// })), +/// )); +/// } +/// ``` +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, From)] +#[reflect(Component, Default, Clone, PartialEq)] +pub struct MeshMaterial3d(pub Handle); + +impl Default for MeshMaterial3d { + fn default() -> Self { + Self(Handle::default()) + } +} + +impl PartialEq for MeshMaterial3d { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for MeshMaterial3d {} + +impl From> for AssetId { + fn from(material: MeshMaterial3d) -> Self { + material.id() + } +} + +impl From<&MeshMaterial3d> for AssetId { + fn from(material: &MeshMaterial3d) -> Self { + material.id() + } +} + +impl AsAssetId for MeshMaterial3d { + type Asset = M; + + fn as_asset_id(&self) -> AssetId { + self.id() + } +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/asset.rs b/crates/libmarathon/src/render/pbr/meshlet/asset.rs new file mode 100644 index 0000000..6a84dcb --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/asset.rs @@ -0,0 +1,319 @@ +use std::sync::Arc; +use bevy_asset::{ + io::{Reader, Writer}, + saver::{AssetSaver, SavedAsset}, + Asset, AssetLoader, AsyncReadExt, AsyncWriteExt, LoadContext, +}; +use bevy_math::{Vec2, Vec3}; +use bevy_reflect::TypePath; +use crate::render::render_resource::ShaderType; +use bevy_tasks::block_on; +use bytemuck::{Pod, Zeroable}; +use lz4_flex::frame::{FrameDecoder, FrameEncoder}; +use std::io::{Read, Write}; +use thiserror::Error; + +/// Unique identifier for the [`MeshletMesh`] asset format. +const MESHLET_MESH_ASSET_MAGIC: u64 = 1717551717668; + +/// The current version of the [`MeshletMesh`] asset format. +pub const MESHLET_MESH_ASSET_VERSION: u64 = 2; + +/// A mesh that has been pre-processed into multiple small clusters of triangles called meshlets. +/// +/// A [`bevy_mesh::Mesh`] can be converted to a [`MeshletMesh`] using `MeshletMesh::from_mesh` when the `meshlet_processor` cargo feature is enabled. +/// The conversion step is very slow, and is meant to be ran once ahead of time, and not during runtime. This type of mesh is not suitable for +/// dynamically generated geometry. +/// +/// There are restrictions on the [`crate::Material`] functionality that can be used with this type of mesh. +/// * Materials have no control over the vertex shader or vertex attributes. +/// * Materials must be opaque. Transparent, alpha masked, and transmissive materials are not supported. +/// * Do not use normal maps baked from higher-poly geometry. Use the high-poly geometry directly and skip the normal map. +/// * If additional detail is needed, a smaller tiling normal map not baked from a mesh is ok. +/// * Material shaders must not use builtin functions that automatically calculate derivatives . +/// * Performing manual arithmetic on texture coordinates (UVs) is forbidden. Use the chain-rule version of arithmetic functions instead (TODO: not yet implemented). +/// * Limited control over [`bevy_render::render_resource::RenderPipelineDescriptor`] attributes. +/// * Materials must use the [`crate::Material::meshlet_mesh_fragment_shader`] method (and similar variants for prepass/deferred shaders) +/// which requires certain shader patterns that differ from the regular material shaders. +/// +/// See also [`super::MeshletMesh3d`] and [`super::MeshletPlugin`]. +#[derive(Asset, TypePath, Clone)] +pub struct MeshletMesh { + /// Quantized and bitstream-packed vertex positions for meshlet vertices. + pub(crate) vertex_positions: Arc<[u32]>, + /// Octahedral-encoded and 2x16snorm packed normals for meshlet vertices. + pub(crate) vertex_normals: Arc<[u32]>, + /// Uncompressed vertex texture coordinates for meshlet vertices. + pub(crate) vertex_uvs: Arc<[Vec2]>, + /// Triangle indices for meshlets. + pub(crate) indices: Arc<[u8]>, + /// The BVH8 used for culling and LOD selection of the meshlets. The root is at index 0. + pub(crate) bvh: Arc<[BvhNode]>, + /// The list of meshlets making up this mesh. + pub(crate) meshlets: Arc<[Meshlet]>, + /// Spherical bounding volumes. + pub(crate) meshlet_cull_data: Arc<[MeshletCullData]>, + /// The tight AABB of the meshlet mesh, used for frustum and occlusion culling at the instance + /// level. + pub(crate) aabb: MeshletAabb, + /// The depth of the culling BVH, used to determine the number of dispatches at runtime. + pub(crate) bvh_depth: u32, +} + +/// A single BVH8 node in the BVH used for culling and LOD selection of a [`MeshletMesh`]. +#[derive(Copy, Clone, Default, Pod, Zeroable)] +#[repr(C)] +pub struct BvhNode { + /// The tight AABBs of this node's children, used for frustum and occlusion during BVH + /// traversal. + pub aabbs: [MeshletAabbErrorOffset; 8], + /// The LOD bounding spheres of this node's children, used for LOD selection during BVH + /// traversal. + pub lod_bounds: [MeshletBoundingSphere; 8], + /// If `u8::MAX`, it indicates that the child of each children is a BVH node, otherwise it is the number of meshlets in the group. + pub child_counts: [u8; 8], + pub _padding: [u32; 2], +} + +/// A single meshlet within a [`MeshletMesh`]. +#[derive(Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct Meshlet { + /// The bit offset within the parent mesh's [`MeshletMesh::vertex_positions`] buffer where the vertex positions for this meshlet begin. + pub start_vertex_position_bit: u32, + /// The offset within the parent mesh's [`MeshletMesh::vertex_normals`] and [`MeshletMesh::vertex_uvs`] buffers + /// where non-position vertex attributes for this meshlet begin. + pub start_vertex_attribute_id: u32, + /// The offset within the parent mesh's [`MeshletMesh::indices`] buffer where the indices for this meshlet begin. + pub start_index_id: u32, + /// The amount of vertices in this meshlet. + pub vertex_count: u8, + /// The amount of triangles in this meshlet. + pub triangle_count: u8, + /// Unused. + pub padding: u16, + /// Number of bits used to store the X channel of vertex positions within this meshlet. + pub bits_per_vertex_position_channel_x: u8, + /// Number of bits used to store the Y channel of vertex positions within this meshlet. + pub bits_per_vertex_position_channel_y: u8, + /// Number of bits used to store the Z channel of vertex positions within this meshlet. + pub bits_per_vertex_position_channel_z: u8, + /// Power of 2 factor used to quantize vertex positions within this meshlet. + pub vertex_position_quantization_factor: u8, + /// Minimum quantized X channel value of vertex positions within this meshlet. + pub min_vertex_position_channel_x: f32, + /// Minimum quantized Y channel value of vertex positions within this meshlet. + pub min_vertex_position_channel_y: f32, + /// Minimum quantized Z channel value of vertex positions within this meshlet. + pub min_vertex_position_channel_z: f32, +} + +/// Bounding spheres used for culling and choosing level of detail for a [`Meshlet`]. +#[derive(Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct MeshletCullData { + /// Tight bounding box, used for frustum and occlusion culling for this meshlet. + pub aabb: MeshletAabbErrorOffset, + /// Bounding sphere used for determining if this meshlet's group is at the correct level of detail for a given view. + pub lod_group_sphere: MeshletBoundingSphere, +} + +/// An axis-aligned bounding box used for a [`Meshlet`]. +#[derive(Copy, Clone, Default, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct MeshletAabb { + pub center: Vec3, + pub half_extent: Vec3, +} + +// An axis-aligned bounding box used for a [`Meshlet`]. +#[derive(Copy, Clone, Default, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct MeshletAabbErrorOffset { + pub center: Vec3, + pub error: f32, + pub half_extent: Vec3, + pub child_offset: u32, +} + +/// A spherical bounding volume used for a [`Meshlet`]. +#[derive(Copy, Clone, Default, Pod, Zeroable)] +#[repr(C)] +pub struct MeshletBoundingSphere { + pub center: Vec3, + pub radius: f32, +} + +/// An [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets. +pub struct MeshletMeshSaver; + +impl AssetSaver for MeshletMeshSaver { + type Asset = MeshletMesh; + type Settings = (); + type OutputLoader = MeshletMeshLoader; + type Error = MeshletMeshSaveOrLoadError; + + async fn save( + &self, + writer: &mut Writer, + asset: SavedAsset<'_, MeshletMesh>, + _settings: &(), + ) -> Result<(), MeshletMeshSaveOrLoadError> { + // Write asset magic number + writer + .write_all(&MESHLET_MESH_ASSET_MAGIC.to_le_bytes()) + .await?; + + // Write asset version + writer + .write_all(&MESHLET_MESH_ASSET_VERSION.to_le_bytes()) + .await?; + + writer.write_all(bytemuck::bytes_of(&asset.aabb)).await?; + writer + .write_all(bytemuck::bytes_of(&asset.bvh_depth)) + .await?; + + // Compress and write asset data + let mut writer = FrameEncoder::new(AsyncWriteSyncAdapter(writer)); + write_slice(&asset.vertex_positions, &mut writer)?; + write_slice(&asset.vertex_normals, &mut writer)?; + write_slice(&asset.vertex_uvs, &mut writer)?; + write_slice(&asset.indices, &mut writer)?; + write_slice(&asset.bvh, &mut writer)?; + write_slice(&asset.meshlets, &mut writer)?; + write_slice(&asset.meshlet_cull_data, &mut writer)?; + // BUG: Flushing helps with an async_fs bug, but it still fails sometimes. https://github.com/smol-rs/async-fs/issues/45 + // ERROR bevy_asset::server: Failed to load asset with asset loader MeshletMeshLoader: failed to fill whole buffer + writer.flush()?; + writer.finish()?; + + Ok(()) + } +} + +/// An [`AssetLoader`] for `.meshlet_mesh` [`MeshletMesh`] assets. +pub struct MeshletMeshLoader; + +impl AssetLoader for MeshletMeshLoader { + type Asset = MeshletMesh; + type Settings = (); + type Error = MeshletMeshSaveOrLoadError; + + async fn load( + &self, + reader: &mut dyn Reader, + _settings: &(), + _load_context: &mut LoadContext<'_>, + ) -> Result { + // Load and check magic number + let magic = async_read_u64(reader).await?; + if magic != MESHLET_MESH_ASSET_MAGIC { + return Err(MeshletMeshSaveOrLoadError::WrongFileType); + } + + // Load and check asset version + let version = async_read_u64(reader).await?; + if version != MESHLET_MESH_ASSET_VERSION { + return Err(MeshletMeshSaveOrLoadError::WrongVersion { found: version }); + } + + let mut bytes = [0u8; size_of::()]; + reader.read_exact(&mut bytes).await?; + let aabb = bytemuck::cast(bytes); + let mut bytes = [0u8; size_of::()]; + reader.read_exact(&mut bytes).await?; + let bvh_depth = u32::from_le_bytes(bytes); + + // Load and decompress asset data + let reader = &mut FrameDecoder::new(AsyncReadSyncAdapter(reader)); + let vertex_positions = read_slice(reader)?; + let vertex_normals = read_slice(reader)?; + let vertex_uvs = read_slice(reader)?; + let indices = read_slice(reader)?; + let bvh = read_slice(reader)?; + let meshlets = read_slice(reader)?; + let meshlet_cull_data = read_slice(reader)?; + + Ok(MeshletMesh { + vertex_positions, + vertex_normals, + vertex_uvs, + indices, + bvh, + meshlets, + meshlet_cull_data, + aabb, + bvh_depth, + }) + } + + fn extensions(&self) -> &[&str] { + &["meshlet_mesh"] + } +} + +#[derive(Error, Debug)] +pub enum MeshletMeshSaveOrLoadError { + #[error("file was not a MeshletMesh asset")] + WrongFileType, + #[error("expected asset version {MESHLET_MESH_ASSET_VERSION} but found version {found}")] + WrongVersion { found: u64 }, + #[error("failed to compress or decompress asset data")] + CompressionOrDecompression(#[from] lz4_flex::frame::Error), + #[error(transparent)] + Io(#[from] std::io::Error), +} + +async fn async_read_u64(reader: &mut dyn Reader) -> Result { + let mut bytes = [0u8; 8]; + reader.read_exact(&mut bytes).await?; + Ok(u64::from_le_bytes(bytes)) +} + +fn read_u64(reader: &mut dyn Read) -> Result { + let mut bytes = [0u8; 8]; + reader.read_exact(&mut bytes)?; + Ok(u64::from_le_bytes(bytes)) +} + +fn write_slice( + field: &[T], + writer: &mut dyn Write, +) -> Result<(), MeshletMeshSaveOrLoadError> { + writer.write_all(&(field.len() as u64).to_le_bytes())?; + writer.write_all(bytemuck::cast_slice(field))?; + Ok(()) +} + +fn read_slice(reader: &mut dyn Read) -> Result, std::io::Error> { + let len = read_u64(reader)? as usize; + + let mut data: Arc<[T]> = core::iter::repeat_with(T::zeroed).take(len).collect(); + let slice = Arc::get_mut(&mut data).unwrap(); + reader.read_exact(bytemuck::cast_slice_mut(slice))?; + + Ok(data) +} + +// TODO: Use async for everything and get rid of this adapter +struct AsyncWriteSyncAdapter<'a>(&'a mut Writer); + +impl Write for AsyncWriteSyncAdapter<'_> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + block_on(self.0.write(buf)) + } + + fn flush(&mut self) -> std::io::Result<()> { + block_on(self.0.flush()) + } +} + +// TODO: Use async for everything and get rid of this adapter +struct AsyncReadSyncAdapter<'a>(&'a mut dyn Reader); + +impl Read for AsyncReadSyncAdapter<'_> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + block_on(self.0.read(buf)) + } +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/clear_visibility_buffer.wgsl b/crates/libmarathon/src/render/pbr/meshlet/clear_visibility_buffer.wgsl new file mode 100644 index 0000000..5956921 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/clear_visibility_buffer.wgsl @@ -0,0 +1,18 @@ +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; +#else +@group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; +#endif +var view_size: vec2; + +@compute +@workgroup_size(16, 16, 1) +fn clear_visibility_buffer(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= view_size) { return; } + +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + textureStore(meshlet_visibility_buffer, global_id.xy, vec4(0lu)); +#else + textureStore(meshlet_visibility_buffer, global_id.xy, vec4(0u)); +#endif +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/cull_bvh.wgsl b/crates/libmarathon/src/render/pbr/meshlet/cull_bvh.wgsl new file mode 100644 index 0000000..b0bbb5f --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/cull_bvh.wgsl @@ -0,0 +1,110 @@ +#import bevy_pbr::meshlet_bindings::{ + InstancedOffset, + get_aabb, + get_aabb_error, + get_aabb_child_offset, + constants, + meshlet_bvh_nodes, + meshlet_bvh_cull_count_read, + meshlet_bvh_cull_count_write, + meshlet_bvh_cull_dispatch, + meshlet_bvh_cull_queue, + meshlet_meshlet_cull_count_early, + meshlet_meshlet_cull_count_late, + meshlet_meshlet_cull_dispatch_early, + meshlet_meshlet_cull_dispatch_late, + meshlet_meshlet_cull_queue, + meshlet_second_pass_bvh_count, + meshlet_second_pass_bvh_dispatch, + meshlet_second_pass_bvh_queue, +} +#import bevy_pbr::meshlet_cull_shared::{ + lod_error_is_imperceptible, + aabb_in_frustum, + should_occlusion_cull_aabb, +} + +@compute +@workgroup_size(128, 1, 1) // 8 threads per node, 16 nodes per workgroup +fn cull_bvh(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Calculate the queue ID for this thread + let dispatch_id = global_invocation_id.x; + var node = dispatch_id >> 3u; + let subnode = dispatch_id & 7u; + if node >= meshlet_bvh_cull_count_read { return; } + + node = select(node, constants.rightmost_slot - node, constants.read_from_front == 0u); + let instanced_offset = meshlet_bvh_cull_queue[node]; + let instance_id = instanced_offset.instance_id; + let bvh_node = &meshlet_bvh_nodes[instanced_offset.offset]; + + var aabb_error_offset = (*bvh_node).aabbs[subnode]; + let aabb = get_aabb(&aabb_error_offset); + let parent_error = get_aabb_error(&aabb_error_offset); + let lod_sphere = (*bvh_node).lod_bounds[subnode]; + + let parent_is_imperceptible = lod_error_is_imperceptible(lod_sphere, parent_error, instance_id); + // Error and frustum cull, in both passes + if parent_is_imperceptible || !aabb_in_frustum(aabb, instance_id) { return; } + + let child_offset = get_aabb_child_offset(&aabb_error_offset); + let index = subnode >> 2u; + let bit_offset = subnode & 3u; + let packed_child_count = (*bvh_node).child_counts[index]; + let child_count = extractBits(packed_child_count, bit_offset * 8u, 8u); + var value = InstancedOffset(instance_id, child_offset); + + // If we pass, try occlusion culling + // If this node was occluded, push it's children to the second pass to check against this frame's HZB + if should_occlusion_cull_aabb(aabb, instance_id) { +#ifdef MESHLET_FIRST_CULLING_PASS + if child_count == 255u { + let id = atomicAdd(&meshlet_second_pass_bvh_count, 1u); + meshlet_second_pass_bvh_queue[id] = value; + if ((id & 15u) == 0u) { + atomicAdd(&meshlet_second_pass_bvh_dispatch.x, 1u); + } + } else { + let base = atomicAdd(&meshlet_meshlet_cull_count_late, child_count); + let start = constants.rightmost_slot - base; + for (var i = start; i < start - child_count; i--) { + meshlet_meshlet_cull_queue[i] = value; + value.offset += 1u; + } + let req = (base + child_count + 127u) >> 7u; + atomicMax(&meshlet_meshlet_cull_dispatch_late.x, req); + } +#endif + return; + } + + // If we pass, push the children to the next BVH cull + if child_count == 255u { + let id = atomicAdd(&meshlet_bvh_cull_count_write, 1u); + let index = select(constants.rightmost_slot - id, id, constants.read_from_front == 0u); + meshlet_bvh_cull_queue[index] = value; + if ((id & 15u) == 0u) { + atomicAdd(&meshlet_bvh_cull_dispatch.x, 1u); + } + } else { +#ifdef MESHLET_FIRST_CULLING_PASS + let base = atomicAdd(&meshlet_meshlet_cull_count_early, child_count); + let end = base + child_count; + for (var i = base; i < end; i++) { + meshlet_meshlet_cull_queue[i] = value; + value.offset += 1u; + } + let req = (end + 127u) >> 7u; + atomicMax(&meshlet_meshlet_cull_dispatch_early.x, req); +#else + let base = atomicAdd(&meshlet_meshlet_cull_count_late, child_count); + let start = constants.rightmost_slot - base; + for (var i = start; i < start - child_count; i--) { + meshlet_meshlet_cull_queue[i] = value; + value.offset += 1u; + } + let req = (base + child_count + 127u) >> 7u; + atomicMax(&meshlet_meshlet_cull_dispatch_late.x, req); +#endif + } +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/cull_clusters.wgsl b/crates/libmarathon/src/render/pbr/meshlet/cull_clusters.wgsl new file mode 100644 index 0000000..85cbc06 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/cull_clusters.wgsl @@ -0,0 +1,93 @@ +#import bevy_pbr::meshlet_bindings::{ + InstancedOffset, + get_aabb, + get_aabb_error, + constants, + view, + meshlet_instance_uniforms, + meshlet_cull_data, + meshlet_software_raster_indirect_args, + meshlet_hardware_raster_indirect_args, + meshlet_previous_raster_counts, + meshlet_raster_clusters, + meshlet_meshlet_cull_count_read, + meshlet_meshlet_cull_count_write, + meshlet_meshlet_cull_dispatch, + meshlet_meshlet_cull_queue, +} +#import bevy_pbr::meshlet_cull_shared::{ + ScreenAabb, + project_aabb, + lod_error_is_imperceptible, + aabb_in_frustum, + should_occlusion_cull_aabb, +} +#import bevy_render::maths::affine3_to_square + +@compute +@workgroup_size(128, 1, 1) // 1 cluster per thread +fn cull_clusters(@builtin(global_invocation_id) global_invocation_id: vec3) { + if global_invocation_id.x >= meshlet_meshlet_cull_count_read { return; } + +#ifdef MESHLET_FIRST_CULLING_PASS + let meshlet_id = global_invocation_id.x; +#else + let meshlet_id = constants.rightmost_slot - global_invocation_id.x; +#endif + let instanced_offset = meshlet_meshlet_cull_queue[meshlet_id]; + let instance_id = instanced_offset.instance_id; + let cull_data = &meshlet_cull_data[instanced_offset.offset]; + var aabb_error_offset = (*cull_data).aabb; + let aabb = get_aabb(&aabb_error_offset); + let error = get_aabb_error(&aabb_error_offset); + let lod_sphere = (*cull_data).lod_group_sphere; + + let is_imperceptible = lod_error_is_imperceptible(lod_sphere, error, instance_id); + // Error and frustum cull, in both passes + if !is_imperceptible || !aabb_in_frustum(aabb, instance_id) { return; } + + // If we pass, try occlusion culling + // If this node was occluded, push it's children to the second pass to check against this frame's HZB + if should_occlusion_cull_aabb(aabb, instance_id) { +#ifdef MESHLET_FIRST_CULLING_PASS + let id = atomicAdd(&meshlet_meshlet_cull_count_write, 1u); + let value = InstancedOffset(instance_id, instanced_offset.offset); + meshlet_meshlet_cull_queue[constants.rightmost_slot - id] = value; + if ((id & 127u) == 0) { + atomicAdd(&meshlet_meshlet_cull_dispatch.x, 1u); + } +#endif + return; + } + + // If we pass, rasterize the meshlet + // Check how big the cluster is in screen space + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + let clip_from_local = view.clip_from_world * world_from_local; + let projection = view.clip_from_world; + var near: f32; + if projection[3][3] == 1.0 { + near = projection[3][2] / projection[2][2]; + } else { + near = projection[3][2]; + } + var screen_aabb = ScreenAabb(vec3(0.0), vec3(0.0)); + var sw_raster = project_aabb(clip_from_local, near, aabb, &screen_aabb); + if sw_raster { + let aabb_size = (screen_aabb.max.xy - screen_aabb.min.xy) * view.viewport.zw; + sw_raster = all(aabb_size <= vec2(64.0)); + } + + var buffer_slot: u32; + if sw_raster { + // Append this cluster to the list for software rasterization + buffer_slot = atomicAdd(&meshlet_software_raster_indirect_args.x, 1u); + buffer_slot += meshlet_previous_raster_counts[0]; + } else { + // Append this cluster to the list for hardware rasterization + buffer_slot = atomicAdd(&meshlet_hardware_raster_indirect_args.instance_count, 1u); + buffer_slot += meshlet_previous_raster_counts[1]; + buffer_slot = constants.rightmost_slot - buffer_slot; + } + meshlet_raster_clusters[buffer_slot] = InstancedOffset(instance_id, instanced_offset.offset); +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/cull_instances.wgsl b/crates/libmarathon/src/render/pbr/meshlet/cull_instances.wgsl new file mode 100644 index 0000000..5d14d10 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/cull_instances.wgsl @@ -0,0 +1,76 @@ +#import bevy_pbr::meshlet_bindings::{ + InstancedOffset, + constants, + meshlet_view_instance_visibility, + meshlet_instance_aabbs, + meshlet_instance_bvh_root_nodes, + meshlet_bvh_cull_count_write, + meshlet_bvh_cull_dispatch, + meshlet_bvh_cull_queue, + meshlet_second_pass_instance_count, + meshlet_second_pass_instance_dispatch, + meshlet_second_pass_instance_candidates, +} +#import bevy_pbr::meshlet_cull_shared::{ + aabb_in_frustum, + should_occlusion_cull_aabb, +} + +fn instance_count() -> u32 { +#ifdef MESHLET_FIRST_CULLING_PASS + return constants.scene_instance_count; +#else + return meshlet_second_pass_instance_count; +#endif +} + +fn map_instance_id(id: u32) -> u32 { +#ifdef MESHLET_FIRST_CULLING_PASS + return id; +#else + return meshlet_second_pass_instance_candidates[id]; +#endif +} + +fn should_cull_instance(instance_id: u32) -> bool { + let bit_offset = instance_id >> 5u; + let packed_visibility = meshlet_view_instance_visibility[instance_id & 31u]; + return bool(extractBits(packed_visibility, bit_offset, 1u)); +} + +@compute +@workgroup_size(128, 1, 1) // 1 instance per thread +fn cull_instances(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Calculate the instance ID for this thread + let dispatch_id = global_invocation_id.x; + if dispatch_id >= instance_count() { return; } + + let instance_id = map_instance_id(dispatch_id); + let aabb = meshlet_instance_aabbs[instance_id]; + + // Visibility and frustum cull, but only in the first pass +#ifdef MESHLET_FIRST_CULLING_PASS + if should_cull_instance(instance_id) || !aabb_in_frustum(aabb, instance_id) { return; } +#endif + + // If we pass, try occlusion culling + // If this instance was occluded, push it to the second pass to check against this frame's HZB + if should_occlusion_cull_aabb(aabb, instance_id) { +#ifdef MESHLET_FIRST_CULLING_PASS + let id = atomicAdd(&meshlet_second_pass_instance_count, 1u); + meshlet_second_pass_instance_candidates[id] = instance_id; + if ((id & 127u) == 0u) { + atomicAdd(&meshlet_second_pass_instance_dispatch.x, 1u); + } +#endif + return; + } + + // If we pass, push the instance's root node to BVH cull + let root_node = meshlet_instance_bvh_root_nodes[instance_id]; + let id = atomicAdd(&meshlet_bvh_cull_count_write, 1u); + meshlet_bvh_cull_queue[id] = InstancedOffset(instance_id, root_node); + if ((id & 15u) == 0u) { + atomicAdd(&meshlet_bvh_cull_dispatch.x, 1u); + } +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/dummy_visibility_buffer_resolve.wgsl b/crates/libmarathon/src/render/pbr/meshlet/dummy_visibility_buffer_resolve.wgsl new file mode 100644 index 0000000..243a400 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/dummy_visibility_buffer_resolve.wgsl @@ -0,0 +1,4 @@ +#define_import_path bevy_pbr::meshlet_visibility_buffer_resolve + +/// Dummy shader to prevent naga_oil from complaining about missing imports when the MeshletPlugin is not loaded, +/// as naga_oil tries to resolve imports even if they're behind an #ifdef. diff --git a/crates/libmarathon/src/render/pbr/meshlet/fill_counts.wgsl b/crates/libmarathon/src/render/pbr/meshlet/fill_counts.wgsl new file mode 100644 index 0000000..f319e39 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/fill_counts.wgsl @@ -0,0 +1,35 @@ +/// Copies the counts of meshlets in the hardware and software buckets, resetting the counters in the process. + +struct DispatchIndirectArgs { + x: u32, + y: u32, + z: u32, +} + +struct DrawIndirectArgs { + vertex_count: u32, + instance_count: u32, + first_vertex: u32, + first_instance: u32, +} + +@group(0) @binding(0) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; +@group(0) @binding(1) var meshlet_hardware_raster_indirect_args: DrawIndirectArgs; +@group(0) @binding(2) var meshlet_previous_raster_counts: array; +#ifdef MESHLET_2D_DISPATCH +@group(0) @binding(3) var meshlet_software_raster_cluster_count: u32; +#endif + +@compute +@workgroup_size(1, 1, 1) +fn fill_counts() { +#ifdef MESHLET_2D_DISPATCH + meshlet_previous_raster_counts[0] += meshlet_software_raster_cluster_count; +#else + meshlet_previous_raster_counts[0] += meshlet_software_raster_indirect_args.x; +#endif + meshlet_software_raster_indirect_args.x = 0; + + meshlet_previous_raster_counts[1] += meshlet_hardware_raster_indirect_args.instance_count; + meshlet_hardware_raster_indirect_args.instance_count = 0; +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/from_mesh.rs b/crates/libmarathon/src/render/pbr/meshlet/from_mesh.rs new file mode 100644 index 0000000..10a4c99 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/from_mesh.rs @@ -0,0 +1,1109 @@ +use crate::render::pbr::meshlet::asset::{MeshletAabb, MeshletAabbErrorOffset, MeshletCullData}; + +use super::asset::{BvhNode, Meshlet, MeshletBoundingSphere, MeshletMesh}; +use std::borrow::Cow; +use bevy_math::{ + bounding::{Aabb3d, BoundingSphere, BoundingVolume}, + ops::log2, + IVec3, Isometry3d, Vec2, Vec3, Vec3A, Vec3Swizzles, +}; +use bevy_mesh::{Indices, Mesh}; +use bevy_platform::collections::HashMap; +use crate::render::render_resource::PrimitiveTopology; +use bevy_tasks::{AsyncComputeTaskPool, ParallelSlice}; +use bitvec::{order::Lsb0, vec::BitVec, view::BitView}; +use core::{f32, ops::Range}; +use itertools::Itertools; +use meshopt::{ + build_meshlets, ffi::meshopt_Meshlet, generate_vertex_remap_multi, + simplify_with_attributes_and_locks, Meshlets, SimplifyOptions, VertexDataAdapter, VertexStream, +}; +use metis::{option::Opt, Graph}; +use smallvec::SmallVec; +use thiserror::Error; +use tracing::debug_span; + +// Aim to have 8 meshlets per group +const TARGET_MESHLETS_PER_GROUP: usize = 8; +// Reject groups that keep over 60% of their original triangles. We'd much rather render a few +// extra triangles than create too many meshlets, increasing cull overhead. +const SIMPLIFICATION_FAILURE_PERCENTAGE: f32 = 0.60; + +/// Default vertex position quantization factor for use with [`MeshletMesh::from_mesh`]. +/// +/// Snaps vertices to the nearest 1/16th of a centimeter (1/2^4). +pub const MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR: u8 = 4; + +const CENTIMETERS_PER_METER: f32 = 100.0; + +impl MeshletMesh { + /// Process a [`Mesh`] to generate a [`MeshletMesh`]. + /// + /// This process is very slow, and should be done ahead of time, and not at runtime. + /// + /// # Requirements + /// + /// This function requires the `meshlet_processor` cargo feature. + /// + /// The input mesh must: + /// 1. Use [`PrimitiveTopology::TriangleList`] + /// 2. Use indices + /// 3. Have the exact following set of vertex attributes: `{POSITION, NORMAL, UV_0}` (tangents can be used in material shaders, but are calculated at runtime and are not stored in the mesh) + /// + /// # Vertex precision + /// + /// `vertex_position_quantization_factor` is the amount of precision to use when quantizing vertex positions. + /// + /// Vertices are snapped to the nearest (1/2^x)th of a centimeter, where x = `vertex_position_quantization_factor`. + /// E.g. if x = 4, then vertices are snapped to the nearest 1/2^4 = 1/16th of a centimeter. + /// + /// Use [`MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR`] as a default, adjusting lower to save memory and disk space, and higher to prevent artifacts if needed. + /// + /// To ensure that two different meshes do not have cracks between them when placed directly next to each other: + /// * Use the same quantization factor when converting each mesh to a meshlet mesh + /// * Ensure that their [`bevy_transform::components::Transform::translation`]s are a multiple of 1/2^x centimeters (note that translations are in meters) + /// * Ensure that their [`bevy_transform::components::Transform::scale`]s are the same + /// * Ensure that their [`bevy_transform::components::Transform::rotation`]s are a multiple of 90 degrees + pub fn from_mesh( + mesh: &Mesh, + vertex_position_quantization_factor: u8, + ) -> Result { + let s = debug_span!("build meshlet mesh"); + let _e = s.enter(); + + // Validate mesh format + let indices = validate_input_mesh(mesh)?; + + // Get meshlet vertices + let vertex_buffer = mesh.create_packed_vertex_buffer_data(); + let vertex_stride = mesh.get_vertex_size() as usize; + let vertices = VertexDataAdapter::new(&vertex_buffer, vertex_stride, 0).unwrap(); + let vertex_normals = bytemuck::cast_slice(&vertex_buffer[12..16]); + + // Generate a position-only vertex buffer for determining triangle/meshlet connectivity + let (position_only_vertex_count, position_only_vertex_remap) = generate_vertex_remap_multi( + vertices.vertex_count, + &[VertexStream::new_with_stride::( + vertex_buffer.as_ptr(), + vertex_stride, + )], + Some(&indices), + ); + + // Split the mesh into an initial list of meshlets (LOD 0) + let (mut meshlets, mut cull_data) = compute_meshlets( + &indices, + &vertices, + &position_only_vertex_remap, + position_only_vertex_count, + None, + ); + + let mut vertex_locks = vec![false; vertices.vertex_count]; + + // Build further LODs + let mut bvh = BvhBuilder::default(); + let mut all_groups = Vec::new(); + let mut simplification_queue: Vec<_> = (0..meshlets.len() as u32).collect(); + let mut stuck = Vec::new(); + while !simplification_queue.is_empty() { + let s = debug_span!("simplify lod", meshlets = simplification_queue.len()); + let _e = s.enter(); + + // For each meshlet build a list of connected meshlets (meshlets that share a vertex) + let connected_meshlets_per_meshlet = find_connected_meshlets( + &simplification_queue, + &meshlets, + &position_only_vertex_remap, + position_only_vertex_count, + ); + + // Group meshlets into roughly groups of size TARGET_MESHLETS_PER_GROUP, + // grouping meshlets with a high number of shared vertices + let groups = group_meshlets( + &simplification_queue, + &cull_data, + &connected_meshlets_per_meshlet, + ); + simplification_queue.clear(); + + // Lock borders between groups to prevent cracks when simplifying + lock_group_borders( + &mut vertex_locks, + &groups, + &meshlets, + &position_only_vertex_remap, + position_only_vertex_count, + ); + + let simplified = groups.par_chunk_map(AsyncComputeTaskPool::get(), 1, |_, groups| { + let mut group = groups[0].clone(); + + // If the group only has a single meshlet we can't simplify it + if group.meshlets.len() == 1 { + return Err(group); + } + + let s = debug_span!("simplify group", meshlets = group.meshlets.len()); + let _e = s.enter(); + + // Simplify the group to ~50% triangle count + let Some((simplified_group_indices, mut group_error)) = simplify_meshlet_group( + &group, + &meshlets, + &vertices, + vertex_normals, + vertex_stride, + &vertex_locks, + ) else { + // Couldn't simplify the group enough + return Err(group); + }; + + // Force the group error to be atleast as large as all of its constituent meshlet's + // individual errors. + for &id in group.meshlets.iter() { + group_error = group_error.max(cull_data[id as usize].error); + } + group.parent_error = group_error; + + // Build new meshlets using the simplified group + let new_meshlets = compute_meshlets( + &simplified_group_indices, + &vertices, + &position_only_vertex_remap, + position_only_vertex_count, + Some((group.lod_bounds, group.parent_error)), + ); + + Ok((group, new_meshlets)) + }); + + let first_group = all_groups.len() as u32; + let mut passed_tris = 0; + let mut stuck_tris = 0; + for group in simplified { + match group { + Ok((group, (new_meshlets, new_cull_data))) => { + let start = meshlets.len(); + merge_meshlets(&mut meshlets, new_meshlets); + cull_data.extend(new_cull_data); + let end = meshlets.len(); + let new_meshlet_ids = start as u32..end as u32; + + passed_tris += triangles_in_meshlets(&meshlets, new_meshlet_ids.clone()); + simplification_queue.extend(new_meshlet_ids); + all_groups.push(group); + } + Err(group) => { + stuck_tris += + triangles_in_meshlets(&meshlets, group.meshlets.iter().copied()); + stuck.push(group); + } + } + } + + // If we have enough triangles that passed, we can retry simplifying the stuck + // meshlets. + if passed_tris > stuck_tris / 3 { + simplification_queue.extend(stuck.drain(..).flat_map(|group| group.meshlets)); + } + + bvh.add_lod(first_group, &all_groups); + } + + // If there's any stuck meshlets left, add another LOD level with only them + if !stuck.is_empty() { + let first_group = all_groups.len() as u32; + all_groups.extend(stuck); + bvh.add_lod(first_group, &all_groups); + } + + let (bvh, aabb, depth) = bvh.build(&mut meshlets, all_groups, &mut cull_data); + + // Copy vertex attributes per meshlet and compress + let mut vertex_positions = BitVec::::new(); + let mut vertex_normals = Vec::new(); + let mut vertex_uvs = Vec::new(); + let mut bevy_meshlets = Vec::with_capacity(meshlets.len()); + for (i, meshlet) in meshlets.meshlets.iter().enumerate() { + build_and_compress_per_meshlet_vertex_data( + meshlet, + meshlets.get(i).vertices, + &vertex_buffer, + vertex_stride, + &mut vertex_positions, + &mut vertex_normals, + &mut vertex_uvs, + &mut bevy_meshlets, + vertex_position_quantization_factor, + ); + } + vertex_positions.set_uninitialized(false); + + Ok(Self { + vertex_positions: vertex_positions.into_vec().into(), + vertex_normals: vertex_normals.into(), + vertex_uvs: vertex_uvs.into(), + indices: meshlets.triangles.into(), + bvh: bvh.into(), + meshlets: bevy_meshlets.into(), + meshlet_cull_data: cull_data + .into_iter() + .map(|cull_data| MeshletCullData { + aabb: aabb_to_meshlet(cull_data.aabb, cull_data.error, 0), + lod_group_sphere: sphere_to_meshlet(cull_data.lod_group_sphere), + }) + .collect(), + aabb, + bvh_depth: depth, + }) + } +} + +fn validate_input_mesh(mesh: &Mesh) -> Result, MeshToMeshletMeshConversionError> { + if mesh.primitive_topology() != PrimitiveTopology::TriangleList { + return Err(MeshToMeshletMeshConversionError::WrongMeshPrimitiveTopology); + } + + if mesh.attributes().map(|(attribute, _)| attribute.id).ne([ + Mesh::ATTRIBUTE_POSITION.id, + Mesh::ATTRIBUTE_NORMAL.id, + Mesh::ATTRIBUTE_UV_0.id, + ]) { + return Err(MeshToMeshletMeshConversionError::WrongMeshVertexAttributes( + mesh.attributes() + .map(|(attribute, _)| format!("{attribute:?}")) + .collect(), + )); + } + + match mesh.indices() { + Some(Indices::U32(indices)) => Ok(Cow::Borrowed(indices.as_slice())), + Some(Indices::U16(indices)) => Ok(indices.iter().map(|i| *i as u32).collect()), + _ => Err(MeshToMeshletMeshConversionError::MeshMissingIndices), + } +} + +fn triangles_in_meshlets(meshlets: &Meshlets, ids: impl IntoIterator) -> u32 { + ids.into_iter() + .map(|id| meshlets.get(id as _).triangles.len() as u32 / 3) + .sum() +} + +fn compute_meshlets( + indices: &[u32], + vertices: &VertexDataAdapter, + position_only_vertex_remap: &[u32], + position_only_vertex_count: usize, + prev_lod_data: Option<(BoundingSphere, f32)>, +) -> (Meshlets, Vec) { + // For each vertex, build a list of all triangles that use it + let mut vertices_to_triangles = vec![Vec::new(); position_only_vertex_count]; + for (i, index) in indices.iter().enumerate() { + let vertex_id = position_only_vertex_remap[*index as usize]; + let vertex_to_triangles = &mut vertices_to_triangles[vertex_id as usize]; + vertex_to_triangles.push(i / 3); + } + + // For each triangle pair, count how many vertices they share + let mut triangle_pair_to_shared_vertex_count = >::default(); + for vertex_triangle_ids in vertices_to_triangles { + for (triangle_id1, triangle_id2) in vertex_triangle_ids.into_iter().tuple_combinations() { + let count = triangle_pair_to_shared_vertex_count + .entry(( + triangle_id1.min(triangle_id2), + triangle_id1.max(triangle_id2), + )) + .or_insert(0); + *count += 1; + } + } + + // For each triangle, gather all other triangles that share at least one vertex along with their shared vertex count + let triangle_count = indices.len() / 3; + let mut connected_triangles_per_triangle = vec![Vec::new(); triangle_count]; + for ((triangle_id1, triangle_id2), shared_vertex_count) in triangle_pair_to_shared_vertex_count + { + // We record both id1->id2 and id2->id1 as adjacency is symmetrical + connected_triangles_per_triangle[triangle_id1].push((triangle_id2, shared_vertex_count)); + connected_triangles_per_triangle[triangle_id2].push((triangle_id1, shared_vertex_count)); + } + + // The order of triangles depends on hash traversal order; to produce deterministic results, sort them + // TODO: Wouldn't it be faster to use a `BTreeMap` above instead of `HashMap` + sorting? + for list in connected_triangles_per_triangle.iter_mut() { + list.sort_unstable(); + } + + let mut xadj = Vec::with_capacity(triangle_count + 1); + let mut adjncy = Vec::new(); + let mut adjwgt = Vec::new(); + for connected_triangles in connected_triangles_per_triangle { + xadj.push(adjncy.len() as i32); + for (connected_triangle_id, shared_vertex_count) in connected_triangles { + adjncy.push(connected_triangle_id as i32); + adjwgt.push(shared_vertex_count); + // TODO: Additional weight based on triangle center spatial proximity? + } + } + xadj.push(adjncy.len() as i32); + + let mut options = [-1; metis::NOPTIONS]; + options[metis::option::Seed::INDEX] = 17; + options[metis::option::UFactor::INDEX] = 1; // Important that there's very little imbalance between partitions + + let mut meshlet_per_triangle = vec![0; triangle_count]; + let partition_count = triangle_count.div_ceil(126); // Need to undershoot to prevent METIS from going over 128 triangles per meshlet + Graph::new(1, partition_count as i32, &xadj, &adjncy) + .unwrap() + .set_options(&options) + .set_adjwgt(&adjwgt) + .part_recursive(&mut meshlet_per_triangle) + .unwrap(); + + let mut indices_per_meshlet = vec![Vec::new(); partition_count]; + for (triangle_id, meshlet) in meshlet_per_triangle.into_iter().enumerate() { + let meshlet_indices = &mut indices_per_meshlet[meshlet as usize]; + let base_index = triangle_id * 3; + meshlet_indices.extend_from_slice(&indices[base_index..(base_index + 3)]); + } + + // Use meshopt to build meshlets from the sets of triangles + let mut meshlets = Meshlets { + meshlets: Vec::new(), + vertices: Vec::new(), + triangles: Vec::new(), + }; + let mut cull_data = Vec::new(); + let get_vertex = |&v: &u32| { + *bytemuck::from_bytes::( + &vertices.reader.get_ref() + [vertices.position_offset + v as usize * vertices.vertex_stride..][..12], + ) + }; + for meshlet_indices in &indices_per_meshlet { + let meshlet = build_meshlets(meshlet_indices, vertices, 255, 128, 0.0); + for meshlet in meshlet.iter() { + let (lod_group_sphere, error) = prev_lod_data.unwrap_or_else(|| { + let bounds = meshopt::compute_meshlet_bounds(meshlet, vertices); + (BoundingSphere::new(bounds.center, bounds.radius), 0.0) + }); + + cull_data.push(TempMeshletCullData { + aabb: Aabb3d::from_point_cloud( + Isometry3d::IDENTITY, + meshlet.vertices.iter().map(get_vertex), + ), + lod_group_sphere, + error, + }); + } + merge_meshlets(&mut meshlets, meshlet); + } + (meshlets, cull_data) +} + +fn find_connected_meshlets( + simplification_queue: &[u32], + meshlets: &Meshlets, + position_only_vertex_remap: &[u32], + position_only_vertex_count: usize, +) -> Vec> { + // For each vertex, build a list of all meshlets that use it + let mut vertices_to_meshlets = vec![Vec::new(); position_only_vertex_count]; + for (id_index, &meshlet_id) in simplification_queue.iter().enumerate() { + let meshlet = meshlets.get(meshlet_id as _); + for index in meshlet.triangles { + let vertex_id = position_only_vertex_remap[meshlet.vertices[*index as usize] as usize]; + let vertex_to_meshlets = &mut vertices_to_meshlets[vertex_id as usize]; + // Meshlets are added in order, so we can just check the last element to deduplicate, + // in the case of two triangles sharing the same vertex within a single meshlet + if vertex_to_meshlets.last() != Some(&id_index) { + vertex_to_meshlets.push(id_index); + } + } + } + + // For each meshlet pair, count how many vertices they share + let mut meshlet_pair_to_shared_vertex_count = >::default(); + for vertex_meshlet_ids in vertices_to_meshlets { + for (meshlet_id1, meshlet_id2) in vertex_meshlet_ids.into_iter().tuple_combinations() { + let count = meshlet_pair_to_shared_vertex_count + .entry((meshlet_id1.min(meshlet_id2), meshlet_id1.max(meshlet_id2))) + .or_insert(0); + *count += 1; + } + } + + // For each meshlet, gather all other meshlets that share at least one vertex along with their shared vertex count + let mut connected_meshlets_per_meshlet = vec![Vec::new(); simplification_queue.len()]; + for ((meshlet_id1, meshlet_id2), shared_vertex_count) in meshlet_pair_to_shared_vertex_count { + // We record both id1->id2 and id2->id1 as adjacency is symmetrical + connected_meshlets_per_meshlet[meshlet_id1].push((meshlet_id2, shared_vertex_count)); + connected_meshlets_per_meshlet[meshlet_id2].push((meshlet_id1, shared_vertex_count)); + } + + // The order of meshlets depends on hash traversal order; to produce deterministic results, sort them + // TODO: Wouldn't it be faster to use a `BTreeMap` above instead of `HashMap` + sorting? + for list in connected_meshlets_per_meshlet.iter_mut() { + list.sort_unstable(); + } + + connected_meshlets_per_meshlet +} + +// METIS manual: https://github.com/KarypisLab/METIS/blob/e0f1b88b8efcb24ffa0ec55eabb78fbe61e58ae7/manual/manual.pdf +fn group_meshlets( + simplification_queue: &[u32], + meshlet_cull_data: &[TempMeshletCullData], + connected_meshlets_per_meshlet: &[Vec<(usize, usize)>], +) -> Vec { + let mut xadj = Vec::with_capacity(simplification_queue.len() + 1); + let mut adjncy = Vec::new(); + let mut adjwgt = Vec::new(); + for connected_meshlets in connected_meshlets_per_meshlet { + xadj.push(adjncy.len() as i32); + for (connected_meshlet_id, shared_vertex_count) in connected_meshlets { + adjncy.push(*connected_meshlet_id as i32); + adjwgt.push(*shared_vertex_count as i32); + // TODO: Additional weight based on meshlet spatial proximity + } + } + xadj.push(adjncy.len() as i32); + + let mut options = [-1; metis::NOPTIONS]; + options[metis::option::Seed::INDEX] = 17; + options[metis::option::UFactor::INDEX] = 200; + + let mut group_per_meshlet = vec![0; simplification_queue.len()]; + let partition_count = simplification_queue + .len() + .div_ceil(TARGET_MESHLETS_PER_GROUP); // TODO: Nanite uses groups of 8-32, probably based on some kind of heuristic + Graph::new(1, partition_count as i32, &xadj, &adjncy) + .unwrap() + .set_options(&options) + .set_adjwgt(&adjwgt) + .part_recursive(&mut group_per_meshlet) + .unwrap(); + + let mut groups = vec![TempMeshletGroup::default(); partition_count]; + for (i, meshlet_group) in group_per_meshlet.into_iter().enumerate() { + let group = &mut groups[meshlet_group as usize]; + let meshlet_id = simplification_queue[i]; + + group.meshlets.push(meshlet_id); + let data = &meshlet_cull_data[meshlet_id as usize]; + group.aabb = group.aabb.merge(&data.aabb); + group.lod_bounds = merge_spheres(group.lod_bounds, data.lod_group_sphere); + } + groups +} + +fn lock_group_borders( + vertex_locks: &mut [bool], + groups: &[TempMeshletGroup], + meshlets: &Meshlets, + position_only_vertex_remap: &[u32], + position_only_vertex_count: usize, +) { + let mut position_only_locks = vec![-1; position_only_vertex_count]; + + // Iterate over position-only based vertices of all meshlets in all groups + for (group_id, group) in groups.iter().enumerate() { + for &meshlet_id in group.meshlets.iter() { + let meshlet = meshlets.get(meshlet_id as usize); + for index in meshlet.triangles { + let vertex_id = + position_only_vertex_remap[meshlet.vertices[*index as usize] as usize] as usize; + + // If the vertex is not yet claimed by any group, or was already claimed by this group + if position_only_locks[vertex_id] == -1 + || position_only_locks[vertex_id] == group_id as i32 + { + position_only_locks[vertex_id] = group_id as i32; // Then claim the vertex for this group + } else { + position_only_locks[vertex_id] = -2; // Else vertex was already claimed by another group or was already locked, lock it + } + } + } + } + + // Lock vertices used by more than 1 group + for i in 0..vertex_locks.len() { + let vertex_id = position_only_vertex_remap[i] as usize; + vertex_locks[i] = position_only_locks[vertex_id] == -2; + } +} + +fn simplify_meshlet_group( + group: &TempMeshletGroup, + meshlets: &Meshlets, + vertices: &VertexDataAdapter<'_>, + vertex_normals: &[f32], + vertex_stride: usize, + vertex_locks: &[bool], +) -> Option<(Vec, f32)> { + // Build a new index buffer into the mesh vertex data by combining all meshlet data in the group + let group_indices = group + .meshlets + .iter() + .flat_map(|&meshlet_id| { + let meshlet = meshlets.get(meshlet_id as _); + meshlet + .triangles + .iter() + .map(|&meshlet_index| meshlet.vertices[meshlet_index as usize]) + }) + .collect::>(); + + // Simplify the group to ~50% triangle count + let mut error = 0.0; + let simplified_group_indices = simplify_with_attributes_and_locks( + &group_indices, + vertices, + vertex_normals, + &[0.5; 3], + vertex_stride, + vertex_locks, + group_indices.len() / 2, + f32::MAX, + SimplifyOptions::Sparse | SimplifyOptions::ErrorAbsolute, + Some(&mut error), + ); + + // Check if we were able to simplify + if simplified_group_indices.len() as f32 / group_indices.len() as f32 + > SIMPLIFICATION_FAILURE_PERCENTAGE + { + return None; + } + + Some((simplified_group_indices, error)) +} + +fn merge_meshlets(meshlets: &mut Meshlets, merge: Meshlets) { + let vertex_offset = meshlets.vertices.len() as u32; + let triangle_offset = meshlets.triangles.len() as u32; + meshlets.vertices.extend_from_slice(&merge.vertices); + meshlets.triangles.extend_from_slice(&merge.triangles); + meshlets + .meshlets + .extend(merge.meshlets.into_iter().map(|mut meshlet| { + meshlet.vertex_offset += vertex_offset; + meshlet.triangle_offset += triangle_offset; + meshlet + })); +} + +fn build_and_compress_per_meshlet_vertex_data( + meshlet: &meshopt_Meshlet, + meshlet_vertex_ids: &[u32], + vertex_buffer: &[u8], + vertex_stride: usize, + vertex_positions: &mut BitVec, + vertex_normals: &mut Vec, + vertex_uvs: &mut Vec, + meshlets: &mut Vec, + vertex_position_quantization_factor: u8, +) { + let start_vertex_position_bit = vertex_positions.len() as u32; + let start_vertex_attribute_id = vertex_normals.len() as u32; + + let quantization_factor = + (1 << vertex_position_quantization_factor) as f32 * CENTIMETERS_PER_METER; + + let mut min_quantized_position_channels = IVec3::MAX; + let mut max_quantized_position_channels = IVec3::MIN; + + // Lossy vertex compression + let mut quantized_positions = [IVec3::ZERO; 255]; + for (i, vertex_id) in meshlet_vertex_ids.iter().enumerate() { + // Load source vertex attributes + let vertex_id_byte = *vertex_id as usize * vertex_stride; + let vertex_data = &vertex_buffer[vertex_id_byte..(vertex_id_byte + vertex_stride)]; + let position = Vec3::from_slice(bytemuck::cast_slice(&vertex_data[0..12])); + let normal = Vec3::from_slice(bytemuck::cast_slice(&vertex_data[12..24])); + let uv = Vec2::from_slice(bytemuck::cast_slice(&vertex_data[24..32])); + + // Copy uncompressed UV + vertex_uvs.push(uv); + + // Compress normal + vertex_normals.push(pack2x16snorm(octahedral_encode(normal))); + + // Quantize position to a fixed-point IVec3 + let quantized_position = (position * quantization_factor + 0.5).as_ivec3(); + quantized_positions[i] = quantized_position; + + // Compute per X/Y/Z-channel quantized position min/max for this meshlet + min_quantized_position_channels = min_quantized_position_channels.min(quantized_position); + max_quantized_position_channels = max_quantized_position_channels.max(quantized_position); + } + + // Calculate bits needed to encode each quantized vertex position channel based on the range of each channel + let range = max_quantized_position_channels - min_quantized_position_channels + 1; + let bits_per_vertex_position_channel_x = log2(range.x as f32).ceil() as u8; + let bits_per_vertex_position_channel_y = log2(range.y as f32).ceil() as u8; + let bits_per_vertex_position_channel_z = log2(range.z as f32).ceil() as u8; + + // Lossless encoding of vertex positions in the minimum number of bits per channel + for quantized_position in quantized_positions.iter().take(meshlet_vertex_ids.len()) { + // Remap [range_min, range_max] IVec3 to [0, range_max - range_min] UVec3 + let position = (quantized_position - min_quantized_position_channels).as_uvec3(); + + // Store as a packed bitstream + vertex_positions.extend_from_bitslice( + &position.x.view_bits::()[..bits_per_vertex_position_channel_x as usize], + ); + vertex_positions.extend_from_bitslice( + &position.y.view_bits::()[..bits_per_vertex_position_channel_y as usize], + ); + vertex_positions.extend_from_bitslice( + &position.z.view_bits::()[..bits_per_vertex_position_channel_z as usize], + ); + } + + meshlets.push(Meshlet { + start_vertex_position_bit, + start_vertex_attribute_id, + start_index_id: meshlet.triangle_offset, + vertex_count: meshlet.vertex_count as u8, + triangle_count: meshlet.triangle_count as u8, + padding: 0, + bits_per_vertex_position_channel_x, + bits_per_vertex_position_channel_y, + bits_per_vertex_position_channel_z, + vertex_position_quantization_factor, + min_vertex_position_channel_x: min_quantized_position_channels.x as f32, + min_vertex_position_channel_y: min_quantized_position_channels.y as f32, + min_vertex_position_channel_z: min_quantized_position_channels.z as f32, + }); +} + +fn merge_spheres(a: BoundingSphere, b: BoundingSphere) -> BoundingSphere { + let sr = a.radius().min(b.radius()); + let br = a.radius().max(b.radius()); + let len = a.center.distance(b.center); + if len + sr <= br || sr == 0.0 || len == 0.0 { + if a.radius() > b.radius() { + a + } else { + b + } + } else { + let radius = (sr + br + len) / 2.0; + let center = + (a.center + b.center + (a.radius() - b.radius()) * (a.center - b.center) / len) / 2.0; + BoundingSphere::new(center, radius) + } +} + +#[derive(Copy, Clone)] +struct TempMeshletCullData { + aabb: Aabb3d, + lod_group_sphere: BoundingSphere, + error: f32, +} + +#[derive(Clone)] +struct TempMeshletGroup { + aabb: Aabb3d, + lod_bounds: BoundingSphere, + parent_error: f32, + meshlets: SmallVec<[u32; TARGET_MESHLETS_PER_GROUP]>, +} + +impl Default for TempMeshletGroup { + fn default() -> Self { + Self { + aabb: aabb_default(), // Default AABB to merge into + lod_bounds: BoundingSphere::new(Vec3A::ZERO, 0.0), + parent_error: f32::MAX, + meshlets: SmallVec::new(), + } + } +} + +// All the BVH build code was stolen from https://github.com/SparkyPotato/radiance/blob/4aa17a3a5be7a0466dc69713e249bbcee9f46057/crates/rad-renderer/src/assets/mesh/virtual_mesh.rs because it works and I'm lazy and don't want to reimplement it +struct TempBvhNode { + group: u32, + aabb: Aabb3d, + children: SmallVec<[u32; 8]>, +} + +#[derive(Default)] +struct BvhBuilder { + nodes: Vec, + lods: Vec>, +} + +impl BvhBuilder { + fn add_lod(&mut self, offset: u32, all_groups: &[TempMeshletGroup]) { + let first = self.nodes.len() as u32; + self.nodes.extend( + all_groups + .iter() + .enumerate() + .skip(offset as _) + .map(|(i, group)| TempBvhNode { + group: i as u32, + aabb: group.aabb, + children: SmallVec::new(), + }), + ); + let end = self.nodes.len() as u32; + if first != end { + self.lods.push(first..end); + } + } + + fn surface_area(&self, nodes: &[u32]) -> f32 { + nodes + .iter() + .map(|&x| self.nodes[x as usize].aabb) + .reduce(|a, b| a.merge(&b)) + .expect("cannot find surface area of zero nodes") + .visible_area() + } + + fn sort_nodes_by_sah(&self, nodes: &mut [u32], splits: [usize; 8]) { + // We use a BVH8, so just recursively binary split 3 times for near-optimal SAH + for i in 0..3 { + let parts = 1 << i; // 2^i + let nodes_per_split = 8 >> i; // 8 / 2^i + let half_count = nodes_per_split / 2; + let mut offset = 0; + for p in 0..parts { + let first = p * nodes_per_split; + let mut s0 = 0; + let mut s1 = 0; + for i in 0..half_count { + s0 += splits[first + i]; + s1 += splits[first + half_count + i]; + } + let c = s0 + s1; + let nodes = &mut nodes[offset..(offset + c)]; + offset += c; + + let mut cost = f32::MAX; + let mut axis = 0; + let key = |x, ax| self.nodes[x as usize].aabb.center()[ax]; + for ax in 0..3 { + nodes.sort_unstable_by(|&x, &y| key(x, ax).partial_cmp(&key(y, ax)).unwrap()); + let (left, right) = nodes.split_at(s0); + let c = self.surface_area(left) + self.surface_area(right); + if c < cost { + axis = ax; + cost = c; + } + } + if axis != 2 { + nodes.sort_unstable_by(|&x, &y| { + key(x, axis).partial_cmp(&key(y, axis)).unwrap() + }); + } + } + } + } + + fn build_temp_inner(&mut self, nodes: &mut [u32], optimize: bool) -> u32 { + let count = nodes.len(); + if count == 1 { + nodes[0] + } else if count <= 8 { + let i = self.nodes.len(); + self.nodes.push(TempBvhNode { + group: u32::MAX, + aabb: aabb_default(), + children: nodes.iter().copied().collect(), + }); + i as _ + } else { + // We need to split the nodes into 8 groups, with the smallest possible tree depth. + // Additionally, no child should be more than one level deeper than the others. + // At `l` levels, we can fit upto 8^l nodes. + // The `max_child_size` is the largest power of 8 <= `count` (any larger and we'd have + // unfilled nodes). + // The `min_child_size` is thus 1 level (8 times) smaller. + // After distributing `min_child_size` to all children, we have distributed + // `min_child_size * 8` nodes (== `max_child_size`). + // The remaining nodes are then distributed left to right. + let max_child_size = 1 << ((count.ilog2() / 3) * 3); + let min_child_size = max_child_size >> 3; + let max_extra_per_node = max_child_size - min_child_size; + let mut extra = count - max_child_size; // 8 * min_child_size + let splits = core::array::from_fn(|_| { + let size = extra.min(max_extra_per_node); + extra -= size; + min_child_size + size + }); + + if optimize { + self.sort_nodes_by_sah(nodes, splits); + } + + let mut offset = 0; + let children = splits + .into_iter() + .map(|size| { + let i = self.build_temp_inner(&mut nodes[offset..(offset + size)], optimize); + offset += size; + i + }) + .collect(); + + let i = self.nodes.len(); + self.nodes.push(TempBvhNode { + group: u32::MAX, + aabb: aabb_default(), + children, + }); + i as _ + } + } + + fn build_temp(&mut self) -> u32 { + let mut lods = Vec::with_capacity(self.lods.len()); + for lod in core::mem::take(&mut self.lods) { + let mut lod: Vec<_> = lod.collect(); + let root = self.build_temp_inner(&mut lod, true); + let node = &self.nodes[root as usize]; + if node.group != u32::MAX || node.children.len() == 8 { + lods.push(root); + } else { + lods.extend(node.children.iter().copied()); + } + } + self.build_temp_inner(&mut lods, false) + } + + fn build_inner( + &self, + groups: &[TempMeshletGroup], + out: &mut Vec, + max_depth: &mut u32, + node: u32, + depth: u32, + ) -> u32 { + *max_depth = depth.max(*max_depth); + let node = &self.nodes[node as usize]; + let onode = out.len(); + out.push(BvhNode::default()); + + for (i, &child_id) in node.children.iter().enumerate() { + let child = &self.nodes[child_id as usize]; + if child.group != u32::MAX { + let group = &groups[child.group as usize]; + let out = &mut out[onode]; + out.aabbs[i] = aabb_to_meshlet(group.aabb, group.parent_error, group.meshlets[0]); + out.lod_bounds[i] = sphere_to_meshlet(group.lod_bounds); + out.child_counts[i] = group.meshlets[1] as _; + } else { + let child_id = self.build_inner(groups, out, max_depth, child_id, depth + 1); + let child = &out[child_id as usize]; + let mut aabb = aabb_default(); + let mut parent_error = 0.0f32; + let mut lod_bounds = BoundingSphere::new(Vec3A::ZERO, 0.0); + for i in 0..8 { + if child.child_counts[i] == 0 { + break; + } + + aabb = aabb.merge(&Aabb3d::new( + child.aabbs[i].center, + child.aabbs[i].half_extent, + )); + lod_bounds = merge_spheres( + lod_bounds, + BoundingSphere::new(child.lod_bounds[i].center, child.lod_bounds[i].radius), + ); + parent_error = parent_error.max(child.aabbs[i].error); + } + + let out = &mut out[onode]; + out.aabbs[i] = aabb_to_meshlet(aabb, parent_error, child_id); + out.lod_bounds[i] = sphere_to_meshlet(lod_bounds); + out.child_counts[i] = u8::MAX; + } + } + + onode as _ + } + + fn build( + mut self, + meshlets: &mut Meshlets, + mut groups: Vec, + cull_data: &mut Vec, + ) -> (Vec, MeshletAabb, u32) { + // The BVH requires group meshlets to be contiguous, so remap them first. + let mut remap = Vec::with_capacity(meshlets.meshlets.len()); + let mut remapped_cull_data = Vec::with_capacity(cull_data.len()); + for group in groups.iter_mut() { + let first = remap.len() as u32; + let count = group.meshlets.len() as u32; + remap.extend( + group + .meshlets + .iter() + .map(|&m| meshlets.meshlets[m as usize]), + ); + remapped_cull_data.extend(group.meshlets.iter().map(|&m| cull_data[m as usize])); + group.meshlets.resize(2, 0); + group.meshlets[0] = first; + group.meshlets[1] = count; + } + meshlets.meshlets = remap; + *cull_data = remapped_cull_data; + + let mut out = vec![]; + let mut aabb = aabb_default(); + let mut max_depth = 0; + + if self.nodes.len() == 1 { + let mut o = BvhNode::default(); + let group = &groups[0]; + o.aabbs[0] = aabb_to_meshlet(group.aabb, group.parent_error, group.meshlets[0]); + o.lod_bounds[0] = sphere_to_meshlet(group.lod_bounds); + o.child_counts[0] = group.meshlets[1] as _; + out.push(o); + aabb = group.aabb; + max_depth = 1; + } else { + let root = self.build_temp(); + let root = self.build_inner(&groups, &mut out, &mut max_depth, root, 1); + assert_eq!(root, 0, "root must be 0"); + + let root = &out[0]; + for i in 0..8 { + if root.child_counts[i] == 0 { + break; + } + + aabb = aabb.merge(&Aabb3d::new( + root.aabbs[i].center, + root.aabbs[i].half_extent, + )); + } + } + + let mut reachable = vec![false; meshlets.meshlets.len()]; + verify_bvh(&out, cull_data, &mut reachable, 0); + assert!( + reachable.iter().all(|&x| x), + "all meshlets must be reachable" + ); + + ( + out, + MeshletAabb { + center: aabb.center().into(), + half_extent: aabb.half_size().into(), + }, + max_depth, + ) + } +} + +fn verify_bvh( + out: &[BvhNode], + cull_data: &[TempMeshletCullData], + reachable: &mut [bool], + node: u32, +) { + let node = &out[node as usize]; + for i in 0..8 { + let sphere = node.lod_bounds[i]; + let error = node.aabbs[i].error; + if node.child_counts[i] == u8::MAX { + let child = &out[node.aabbs[i].child_offset as usize]; + for i in 0..8 { + if child.child_counts[i] == 0 { + break; + } + assert!( + child.aabbs[i].error <= error, + "BVH errors are not monotonic" + ); + let sphere_error = (sphere.center - child.lod_bounds[i].center).length() + - (sphere.radius - child.lod_bounds[i].radius); + assert!( + sphere_error <= 0.0001, + "BVH lod spheres are not monotonic ({sphere_error})" + ); + } + verify_bvh(out, cull_data, reachable, node.aabbs[i].child_offset); + } else { + for m in 0..node.child_counts[i] as u32 { + let mid = (m + node.aabbs[i].child_offset) as usize; + let meshlet = &cull_data[mid]; + assert!(meshlet.error <= error, "meshlet errors are not monotonic"); + let sphere_error = (Vec3A::from(sphere.center) - meshlet.lod_group_sphere.center) + .length() + - (sphere.radius - meshlet.lod_group_sphere.radius()); + assert!( + sphere_error <= 0.0001, + "meshlet lod spheres are not monotonic: ({sphere_error})" + ); + reachable[mid] = true; + } + } + } +} + +fn aabb_default() -> Aabb3d { + Aabb3d { + min: Vec3A::INFINITY, + max: Vec3A::NEG_INFINITY, + } +} + +fn aabb_to_meshlet(aabb: Aabb3d, error: f32, child_offset: u32) -> MeshletAabbErrorOffset { + MeshletAabbErrorOffset { + center: aabb.center().into(), + error, + half_extent: aabb.half_size().into(), + child_offset, + } +} + +fn sphere_to_meshlet(sphere: BoundingSphere) -> MeshletBoundingSphere { + MeshletBoundingSphere { + center: sphere.center.into(), + radius: sphere.radius(), + } +} + +// TODO: Precise encode variant +fn octahedral_encode(v: Vec3) -> Vec2 { + let n = v / (v.x.abs() + v.y.abs() + v.z.abs()); + let octahedral_wrap = (1.0 - n.yx().abs()) + * Vec2::new( + if n.x >= 0.0 { 1.0 } else { -1.0 }, + if n.y >= 0.0 { 1.0 } else { -1.0 }, + ); + if n.z >= 0.0 { + n.xy() + } else { + octahedral_wrap + } +} + +// https://www.w3.org/TR/WGSL/#pack2x16snorm-builtin +fn pack2x16snorm(v: Vec2) -> u32 { + let v = v.clamp(Vec2::NEG_ONE, Vec2::ONE); + let v = (v * 32767.0 + 0.5).floor().as_i16vec2(); + bytemuck::cast(v) +} + +/// An error produced by [`MeshletMesh::from_mesh`]. +#[derive(Error, Debug)] +pub enum MeshToMeshletMeshConversionError { + #[error("Mesh primitive topology is not TriangleList")] + WrongMeshPrimitiveTopology, + #[error("Mesh vertex attributes are not {{POSITION, NORMAL, UV_0}}: {0:?}")] + WrongMeshVertexAttributes(Vec), + #[error("Mesh has no indices")] + MeshMissingIndices, +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/instance_manager.rs b/crates/libmarathon/src/render/pbr/meshlet/instance_manager.rs new file mode 100644 index 0000000..2035083 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/instance_manager.rs @@ -0,0 +1,295 @@ +use super::{meshlet_mesh_manager::MeshletMeshManager, MeshletMesh, MeshletMesh3d}; +use crate::render::pbr::DUMMY_MESH_MATERIAL; +use crate::render::pbr::{ + meshlet::asset::MeshletAabb, MaterialBindingId, MeshFlags, MeshTransforms, MeshUniform, + PreviousGlobalTransform, RenderMaterialBindings, RenderMaterialInstances, +}; +use bevy_asset::{AssetEvent, AssetServer, Assets, UntypedAssetId}; +use bevy_camera::visibility::RenderLayers; +use bevy_ecs::{ + entity::{Entities, Entity, EntityHashMap}, + message::MessageReader, + query::Has, + resource::Resource, + system::{Local, Query, Res, ResMut, SystemState}, +}; +use bevy_light::{NotShadowCaster, NotShadowReceiver}; +use bevy_platform::collections::{HashMap, HashSet}; +use crate::render::{render_resource::StorageBuffer, sync_world::MainEntity, MainWorld}; +use bevy_transform::components::GlobalTransform; +use core::ops::DerefMut; + +/// Manages data for each entity with a [`MeshletMesh`]. +#[derive(Resource)] +pub struct InstanceManager { + /// Amount of instances in the scene. + pub scene_instance_count: u32, + /// The max BVH depth of any instance in the scene. This is used to control the number of + /// dependent dispatches emitted for BVH traversal. + pub max_bvh_depth: u32, + + /// Per-instance [`MainEntity`], [`RenderLayers`], and [`NotShadowCaster`]. + pub instances: Vec<(MainEntity, RenderLayers, bool)>, + /// Per-instance [`MeshUniform`]. + pub instance_uniforms: StorageBuffer>, + /// Per-instance model-space AABB. + pub instance_aabbs: StorageBuffer>, + /// Per-instance material ID. + pub instance_material_ids: StorageBuffer>, + /// Per-instance index to the root node of the instance's BVH. + pub instance_bvh_root_nodes: StorageBuffer>, + /// Per-view per-instance visibility bit. Used for [`RenderLayers`] and [`NotShadowCaster`] support. + pub view_instance_visibility: EntityHashMap>>, + + /// Next material ID available. + next_material_id: u32, + /// Map of material asset to material ID. + material_id_lookup: HashMap, + /// Set of material IDs used in the scene. + material_ids_present_in_scene: HashSet, +} + +impl InstanceManager { + pub fn new() -> Self { + Self { + scene_instance_count: 0, + max_bvh_depth: 0, + + instances: Vec::new(), + instance_uniforms: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_uniforms")); + buffer + }, + instance_aabbs: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_aabbs")); + buffer + }, + instance_material_ids: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_material_ids")); + buffer + }, + instance_bvh_root_nodes: { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_instance_bvh_root_nodes")); + buffer + }, + view_instance_visibility: EntityHashMap::default(), + + next_material_id: 0, + material_id_lookup: HashMap::default(), + material_ids_present_in_scene: HashSet::default(), + } + } + + pub fn add_instance( + &mut self, + instance: MainEntity, + root_bvh_node: u32, + aabb: MeshletAabb, + bvh_depth: u32, + transform: &GlobalTransform, + previous_transform: Option<&PreviousGlobalTransform>, + render_layers: Option<&RenderLayers>, + mesh_material_ids: &RenderMaterialInstances, + render_material_bindings: &RenderMaterialBindings, + not_shadow_receiver: bool, + not_shadow_caster: bool, + ) { + // Build a MeshUniform for the instance + let transform = transform.affine(); + let previous_transform = previous_transform.map(|t| t.0).unwrap_or(transform); + let mut flags = if not_shadow_receiver { + MeshFlags::empty() + } else { + MeshFlags::SHADOW_RECEIVER + }; + if transform.matrix3.determinant().is_sign_positive() { + flags |= MeshFlags::SIGN_DETERMINANT_MODEL_3X3; + } + let transforms = MeshTransforms { + world_from_local: (&transform).into(), + previous_world_from_local: (&previous_transform).into(), + flags: flags.bits(), + }; + + let mesh_material = mesh_material_ids.mesh_material(instance); + let mesh_material_binding_id = if mesh_material != DUMMY_MESH_MATERIAL.untyped() { + render_material_bindings + .get(&mesh_material) + .cloned() + .unwrap_or_default() + } else { + // Use a dummy binding ID if the mesh has no material + MaterialBindingId::default() + }; + + let mesh_uniform = MeshUniform::new( + &transforms, + 0, + mesh_material_binding_id.slot, + None, + None, + None, + ); + + // Append instance data + self.instances.push(( + instance, + render_layers.cloned().unwrap_or(RenderLayers::default()), + not_shadow_caster, + )); + self.instance_uniforms.get_mut().push(mesh_uniform); + self.instance_aabbs.get_mut().push(aabb); + self.instance_material_ids.get_mut().push(0); + self.instance_bvh_root_nodes.get_mut().push(root_bvh_node); + + self.scene_instance_count += 1; + self.max_bvh_depth = self.max_bvh_depth.max(bvh_depth); + } + + /// Get the material ID for a [`crate::Material`]. + pub fn get_material_id(&mut self, material_asset_id: UntypedAssetId) -> u32 { + *self + .material_id_lookup + .entry(material_asset_id) + .or_insert_with(|| { + self.next_material_id += 1; + self.next_material_id + }) + } + + pub fn material_present_in_scene(&self, material_id: &u32) -> bool { + self.material_ids_present_in_scene.contains(material_id) + } + + pub fn reset(&mut self, entities: &Entities) { + self.scene_instance_count = 0; + self.max_bvh_depth = 0; + + self.instances.clear(); + self.instance_uniforms.get_mut().clear(); + self.instance_aabbs.get_mut().clear(); + self.instance_material_ids.get_mut().clear(); + self.instance_bvh_root_nodes.get_mut().clear(); + self.view_instance_visibility + .retain(|view_entity, _| entities.contains(*view_entity)); + self.view_instance_visibility + .values_mut() + .for_each(|b| b.get_mut().clear()); + + self.next_material_id = 0; + self.material_id_lookup.clear(); + self.material_ids_present_in_scene.clear(); + } +} + +pub fn extract_meshlet_mesh_entities( + mut meshlet_mesh_manager: ResMut, + mut instance_manager: ResMut, + // TODO: Replace main_world and system_state when Extract>> is possible + mut main_world: ResMut, + mesh_material_ids: Res, + render_material_bindings: Res, + mut system_state: Local< + Option< + SystemState<( + Query<( + Entity, + &MeshletMesh3d, + &GlobalTransform, + Option<&PreviousGlobalTransform>, + Option<&RenderLayers>, + Has, + Has, + )>, + Res, + ResMut>, + MessageReader>, + )>, + >, + >, + render_entities: &Entities, +) { + // Get instances query + if system_state.is_none() { + *system_state = Some(SystemState::new(&mut main_world)); + } + let system_state = system_state.as_mut().unwrap(); + let (instances_query, asset_server, mut assets, mut asset_events) = + system_state.get_mut(&mut main_world); + + // Reset per-frame data + instance_manager.reset(render_entities); + + // Free GPU buffer space for any modified or dropped MeshletMesh assets + for asset_event in asset_events.read() { + if let AssetEvent::Unused { id } | AssetEvent::Modified { id } = asset_event { + meshlet_mesh_manager.remove(id); + } + } + + // Iterate over every instance + // TODO: Switch to change events to not upload every instance every frame. + for ( + instance, + meshlet_mesh, + transform, + previous_transform, + render_layers, + not_shadow_receiver, + not_shadow_caster, + ) in &instances_query + { + // Skip instances with an unloaded MeshletMesh asset + // TODO: This is a semi-expensive check + if asset_server.is_managed(meshlet_mesh.id()) + && !asset_server.is_loaded_with_dependencies(meshlet_mesh.id()) + { + continue; + } + + // Upload the instance's MeshletMesh asset data if not done already done + let (root_bvh_node, aabb, bvh_depth) = + meshlet_mesh_manager.queue_upload_if_needed(meshlet_mesh.id(), &mut assets); + + // Add the instance's data to the instance manager + instance_manager.add_instance( + instance.into(), + root_bvh_node, + aabb, + bvh_depth, + transform, + previous_transform, + render_layers, + &mesh_material_ids, + &render_material_bindings, + not_shadow_receiver, + not_shadow_caster, + ); + } +} + +/// For each entity in the scene, record what material ID its material was assigned in the `prepare_material_meshlet_meshes` systems, +/// and note that the material is used by at least one entity in the scene. +pub fn queue_material_meshlet_meshes( + mut instance_manager: ResMut, + render_material_instances: Res, +) { + let instance_manager = instance_manager.deref_mut(); + + for (i, (instance, _, _)) in instance_manager.instances.iter().enumerate() { + if let Some(material_instance) = render_material_instances.instances.get(instance) + && let Some(material_id) = instance_manager + .material_id_lookup + .get(&material_instance.asset_id) + { + instance_manager + .material_ids_present_in_scene + .insert(*material_id); + instance_manager.instance_material_ids.get_mut()[i] = *material_id; + } + } +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/material_pipeline_prepare.rs b/crates/libmarathon/src/render/pbr/meshlet/material_pipeline_prepare.rs new file mode 100644 index 0000000..2607d0c --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/material_pipeline_prepare.rs @@ -0,0 +1,475 @@ +use super::{ + instance_manager::InstanceManager, pipelines::MeshletPipelines, + resource_manager::ResourceManager, +}; +use crate::render::pbr::*; +use bevy_camera::{Camera3d, Projection}; +use crate::render::{ + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + tonemapping::{DebandDither, Tonemapping}, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_light::{EnvironmentMapLight, IrradianceVolume, ShadowFilteringMethod}; +use bevy_mesh::VertexBufferLayout; +use bevy_mesh::{Mesh, MeshVertexBufferLayout, MeshVertexBufferLayoutRef, MeshVertexBufferLayouts}; +use bevy_platform::collections::{HashMap, HashSet}; +use crate::render::erased_render_asset::ErasedRenderAssets; +use crate::render::{camera::TemporalJitter, render_resource::*, view::ExtractedView}; +use bevy_utils::default; +use core::any::{Any, TypeId}; + +/// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`super::MeshletMainOpaquePass3dNode`]. +#[derive(Component, Deref, DerefMut, Default)] +pub struct MeshletViewMaterialsMainOpaquePass(pub Vec<(u32, CachedRenderPipelineId, BindGroup)>); + +/// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletMainOpaquePass3dNode`], +/// and register the material with [`InstanceManager`]. +pub fn prepare_material_meshlet_meshes_main_opaque_pass( + resource_manager: ResMut, + mut instance_manager: ResMut, + mut cache: Local>, + pipeline_cache: Res, + material_pipeline: Res, + mesh_pipeline: Res, + render_materials: Res>, + meshlet_pipelines: Res, + render_material_instances: Res, + material_bind_group_allocators: Res, + mut mesh_vertex_buffer_layouts: ResMut, + mut views: Query< + ( + &mut MeshletViewMaterialsMainOpaquePass, + &ExtractedView, + Option<&Tonemapping>, + Option<&DebandDither>, + Option<&ShadowFilteringMethod>, + (Has, Has), + ( + Has, + Has, + Has, + Has, + ), + Has, + Option<&Projection>, + Has>, + Has>, + ), + With, + >, +) { + let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts); + + for ( + mut materials, + view, + tonemapping, + dither, + shadow_filter_method, + (ssao, distance_fog), + (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), + temporal_jitter, + projection, + has_environment_maps, + has_irradiance_volumes, + ) in &mut views + { + let mut view_key = + MeshPipelineKey::from_msaa_samples(1) | MeshPipelineKey::from_hdr(view.hdr); + + if normal_prepass { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + if depth_prepass { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + if motion_vector_prepass { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + if deferred_prepass { + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + } + + if temporal_jitter { + view_key |= MeshPipelineKey::TEMPORAL_JITTER; + } + + if has_environment_maps { + view_key |= MeshPipelineKey::ENVIRONMENT_MAP; + } + + if has_irradiance_volumes { + view_key |= MeshPipelineKey::IRRADIANCE_VOLUME; + } + + if let Some(projection) = projection { + view_key |= match projection { + Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE, + Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC, + Projection::Custom(_) => MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD, + }; + } + + match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) { + ShadowFilteringMethod::Hardware2x2 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; + } + ShadowFilteringMethod::Gaussian => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN; + } + ShadowFilteringMethod::Temporal => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL; + } + } + + if !view.hdr { + if let Some(tonemapping) = tonemapping { + view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; + view_key |= tonemapping_pipeline_key(*tonemapping); + } + if let Some(DebandDither::Enabled) = dither { + view_key |= MeshPipelineKey::DEBAND_DITHER; + } + } + + if ssao { + view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; + } + if distance_fog { + view_key |= MeshPipelineKey::DISTANCE_FOG; + } + + view_key |= MeshPipelineKey::from_primitive_topology(PrimitiveTopology::TriangleList); + + for material_id in render_material_instances + .instances + .values() + .map(|instance| instance.asset_id) + .collect::>() + { + let Some(material) = render_materials.get(material_id) else { + continue; + }; + + if material.properties.render_method != OpaqueRendererMethod::Forward + || material.properties.alpha_mode != AlphaMode::Opaque + || material.properties.reads_view_transmission_texture + { + continue; + } + + let erased_key = ErasedMaterialPipelineKey { + mesh_key: view_key, + material_key: material.properties.material_key.clone(), + type_id: material_id.type_id(), + }; + let material_pipeline_specializer = MaterialPipelineSpecializer { + pipeline: material_pipeline.clone(), + properties: material.properties.clone(), + }; + let Ok(material_pipeline_descriptor) = + material_pipeline_specializer.specialize(erased_key, fake_vertex_buffer_layout) + else { + continue; + }; + let material_fragment = material_pipeline_descriptor.fragment.unwrap(); + + let mut shader_defs = material_fragment.shader_defs; + shader_defs.push("MESHLET_MESH_MATERIAL_PASS".into()); + + let layout = mesh_pipeline.get_view_layout(view_key.into()); + let layout = vec![ + layout.main_layout.clone(), + layout.binding_array_layout.clone(), + resource_manager.material_shade_bind_group_layout.clone(), + material + .properties + .material_layout + .as_ref() + .unwrap() + .clone(), + ]; + + let pipeline_descriptor = RenderPipelineDescriptor { + label: material_pipeline_descriptor.label, + layout, + push_constant_ranges: vec![], + vertex: VertexState { + shader: meshlet_pipelines.meshlet_mesh_material.clone(), + shader_defs: shader_defs.clone(), + entry_point: material_pipeline_descriptor.vertex.entry_point, + buffers: Vec::new(), + }, + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth16Unorm, + depth_write_enabled: false, + depth_compare: CompareFunction::Equal, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + shader: match material.properties.get_shader(MeshletFragmentShader) { + Some(shader) => shader.clone(), + None => meshlet_pipelines.meshlet_mesh_material.clone(), + }, + shader_defs, + entry_point: material_fragment.entry_point, + targets: material_fragment.targets, + }), + zero_initialize_workgroup_memory: false, + }; + let type_id = material_id.type_id(); + let Some(material_bind_group_allocator) = material_bind_group_allocators.get(&type_id) + else { + continue; + }; + let material_id = instance_manager.get_material_id(material_id); + + let pipeline_id = *cache.entry((view_key, type_id)).or_insert_with(|| { + pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) + }); + + let Some(material_bind_group) = + material_bind_group_allocator.get(material.binding.group) + else { + continue; + }; + let Some(bind_group) = material_bind_group.bind_group() else { + continue; + }; + + materials.push((material_id, pipeline_id, (*bind_group).clone())); + } + } +} + +/// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`super::MeshletPrepassNode`]. +#[derive(Component, Deref, DerefMut, Default)] +pub struct MeshletViewMaterialsPrepass(pub Vec<(u32, CachedRenderPipelineId, BindGroup)>); + +/// A list of `(Material ID, Pipeline, BindGroup)` for a view for use in [`super::MeshletDeferredGBufferPrepassNode`]. +#[derive(Component, Deref, DerefMut, Default)] +pub struct MeshletViewMaterialsDeferredGBufferPrepass( + pub Vec<(u32, CachedRenderPipelineId, BindGroup)>, +); + +/// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletPrepassNode`], +/// and [`super::MeshletDeferredGBufferPrepassNode`] and register the material with [`InstanceManager`]. +pub fn prepare_material_meshlet_meshes_prepass( + resource_manager: ResMut, + mut instance_manager: ResMut, + mut cache: Local>, + pipeline_cache: Res, + prepass_pipeline: Res, + material_bind_group_allocators: Res, + render_materials: Res>, + meshlet_pipelines: Res, + render_material_instances: Res, + mut mesh_vertex_buffer_layouts: ResMut, + mut views: Query< + ( + &mut MeshletViewMaterialsPrepass, + &mut MeshletViewMaterialsDeferredGBufferPrepass, + &ExtractedView, + AnyOf<(&NormalPrepass, &MotionVectorPrepass, &DeferredPrepass)>, + ), + With, + >, +) { + let fake_vertex_buffer_layout = &fake_vertex_buffer_layout(&mut mesh_vertex_buffer_layouts); + + for ( + mut materials, + mut deferred_materials, + view, + (normal_prepass, motion_vector_prepass, deferred_prepass), + ) in &mut views + { + let mut view_key = + MeshPipelineKey::from_msaa_samples(1) | MeshPipelineKey::from_hdr(view.hdr); + + if normal_prepass.is_some() { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + if motion_vector_prepass.is_some() { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + + view_key |= MeshPipelineKey::from_primitive_topology(PrimitiveTopology::TriangleList); + + for material_id in render_material_instances + .instances + .values() + .map(|instance| instance.asset_id) + .collect::>() + { + let Some(material) = render_materials.get(material_id) else { + continue; + }; + let Some(material_bind_group_allocator) = + material_bind_group_allocators.get(&material_id.type_id()) + else { + continue; + }; + + if material.properties.alpha_mode != AlphaMode::Opaque + || material.properties.reads_view_transmission_texture + { + continue; + } + + let material_wants_deferred = matches!( + material.properties.render_method, + OpaqueRendererMethod::Deferred + ); + if deferred_prepass.is_some() && material_wants_deferred { + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + } else if normal_prepass.is_none() && motion_vector_prepass.is_none() { + continue; + } + + let erased_key = ErasedMaterialPipelineKey { + mesh_key: view_key, + material_key: material.properties.material_key.clone(), + type_id: material_id.type_id(), + }; + let material_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; + let Ok(material_pipeline_descriptor) = + material_pipeline_specializer.specialize(erased_key, fake_vertex_buffer_layout) + else { + continue; + }; + let material_fragment = material_pipeline_descriptor.fragment.unwrap(); + + let mut shader_defs = material_fragment.shader_defs; + shader_defs.push("MESHLET_MESH_MATERIAL_PASS".into()); + + let view_layout = if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + prepass_pipeline.view_layout_motion_vectors.clone() + } else { + prepass_pipeline.view_layout_no_motion_vectors.clone() + }; + + let fragment_shader = if view_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + material + .properties + .get_shader(MeshletDeferredFragmentShader) + .unwrap_or(meshlet_pipelines.meshlet_mesh_material.clone()) + } else { + material + .properties + .get_shader(MeshletPrepassFragmentShader) + .unwrap_or(meshlet_pipelines.meshlet_mesh_material.clone()) + }; + + let entry_point = if fragment_shader == meshlet_pipelines.meshlet_mesh_material { + material_fragment.entry_point.clone() + } else { + None + }; + + let pipeline_descriptor = RenderPipelineDescriptor { + label: material_pipeline_descriptor.label, + layout: vec![ + view_layout, + prepass_pipeline.empty_layout.clone(), + resource_manager.material_shade_bind_group_layout.clone(), + material + .properties + .material_layout + .as_ref() + .unwrap() + .clone(), + ], + vertex: VertexState { + shader: meshlet_pipelines.meshlet_mesh_material.clone(), + shader_defs: shader_defs.clone(), + entry_point: material_pipeline_descriptor.vertex.entry_point, + ..default() + }, + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth16Unorm, + depth_write_enabled: false, + depth_compare: CompareFunction::Equal, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + fragment: Some(FragmentState { + shader: fragment_shader, + shader_defs, + entry_point, + targets: material_fragment.targets, + }), + ..default() + }; + + let material_id = instance_manager.get_material_id(material_id); + + let pipeline_id = *cache + .entry((view_key, material_id.type_id())) + .or_insert_with(|| { + pipeline_cache.queue_render_pipeline(pipeline_descriptor.clone()) + }); + + let Some(material_bind_group) = + material_bind_group_allocator.get(material.binding.group) + else { + continue; + }; + let Some(bind_group) = material_bind_group.bind_group() else { + continue; + }; + + let item = (material_id, pipeline_id, (*bind_group).clone()); + if view_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + deferred_materials.push(item); + } else { + materials.push(item); + } + } + } +} + +// Meshlet materials don't use a traditional vertex buffer, but the material specialization requires one. +fn fake_vertex_buffer_layout(layouts: &mut MeshVertexBufferLayouts) -> MeshVertexBufferLayoutRef { + layouts.insert(MeshVertexBufferLayout::new( + vec![ + Mesh::ATTRIBUTE_POSITION.id, + Mesh::ATTRIBUTE_NORMAL.id, + Mesh::ATTRIBUTE_UV_0.id, + Mesh::ATTRIBUTE_TANGENT.id, + ], + VertexBufferLayout { + array_stride: 48, + step_mode: VertexStepMode::Vertex, + attributes: vec![ + VertexAttribute { + format: Mesh::ATTRIBUTE_POSITION.format, + offset: 0, + shader_location: 0, + }, + VertexAttribute { + format: Mesh::ATTRIBUTE_NORMAL.format, + offset: 12, + shader_location: 1, + }, + VertexAttribute { + format: Mesh::ATTRIBUTE_UV_0.format, + offset: 24, + shader_location: 2, + }, + VertexAttribute { + format: Mesh::ATTRIBUTE_TANGENT.format, + offset: 32, + shader_location: 3, + }, + ], + }, + )) +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/material_shade_nodes.rs b/crates/libmarathon/src/render/pbr/meshlet/material_shade_nodes.rs new file mode 100644 index 0000000..b363d87 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/material_shade_nodes.rs @@ -0,0 +1,421 @@ +use super::{ + material_pipeline_prepare::{ + MeshletViewMaterialsDeferredGBufferPrepass, MeshletViewMaterialsMainOpaquePass, + MeshletViewMaterialsPrepass, + }, + resource_manager::{MeshletViewBindGroups, MeshletViewResources}, + InstanceManager, +}; +use crate::render::pbr::{ + MeshViewBindGroup, PrepassViewBindGroup, ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, + ViewLightProbesUniformOffset, ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset, +}; +use bevy_camera::MainPassResolutionOverride; +use bevy_camera::Viewport; +use crate::render::prepass::{ + MotionVectorPrepass, PreviousViewUniformOffset, ViewPrepassTextures, +}; +use bevy_ecs::{ + query::{Has, QueryItem}, + world::World, +}; +use crate::render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor, + StoreOp, + }, + renderer::RenderContext, + view::{ViewTarget, ViewUniformOffset}, +}; + +/// Fullscreen shading pass based on the visibility buffer generated from rasterizing meshlets. +#[derive(Default)] +pub struct MeshletMainOpaquePass3dNode; +impl ViewNode for MeshletMainOpaquePass3dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ViewTarget, + &'static MeshViewBindGroup, + &'static ViewUniformOffset, + &'static ViewLightsUniformOffset, + &'static ViewFogUniformOffset, + &'static ViewLightProbesUniformOffset, + &'static ViewScreenSpaceReflectionsUniformOffset, + &'static ViewEnvironmentMapUniformOffset, + Option<&'static MainPassResolutionOverride>, + &'static MeshletViewMaterialsMainOpaquePass, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + camera, + target, + mesh_view_bind_group, + view_uniform_offset, + view_lights_offset, + view_fog_offset, + view_light_probes_offset, + view_ssr_offset, + view_environment_map_offset, + resolution_override, + meshlet_view_materials, + meshlet_view_bind_groups, + meshlet_view_resources, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + if meshlet_view_materials.is_empty() { + return Ok(()); + } + + let ( + Some(instance_manager), + Some(pipeline_cache), + Some(meshlet_material_depth), + Some(meshlet_material_shade_bind_group), + ) = ( + world.get_resource::(), + world.get_resource::(), + meshlet_view_resources.material_depth.as_ref(), + meshlet_view_bind_groups.material_shade.as_ref(), + ) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("meshlet_material_opaque_3d_pass"), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &meshlet_material_depth.default_view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + let pass_span = diagnostics.pass_span(&mut render_pass, "meshlet_material_opaque_3d_pass"); + if let Some(viewport) = + Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override) + { + render_pass.set_camera_viewport(&viewport); + } + + render_pass.set_bind_group( + 0, + &mesh_view_bind_group.main, + &[ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + **view_light_probes_offset, + **view_ssr_offset, + **view_environment_map_offset, + ], + ); + render_pass.set_bind_group(1, &mesh_view_bind_group.binding_array, &[]); + render_pass.set_bind_group(2, meshlet_material_shade_bind_group, &[]); + + // 1 fullscreen triangle draw per material + for (material_id, material_pipeline_id, material_bind_group) in + meshlet_view_materials.iter() + { + if instance_manager.material_present_in_scene(material_id) + && let Some(material_pipeline) = + pipeline_cache.get_render_pipeline(*material_pipeline_id) + { + let x = *material_id * 3; + render_pass.set_render_pipeline(material_pipeline); + render_pass.set_bind_group(3, material_bind_group, &[]); + render_pass.draw(x..(x + 3), 0..1); + } + } + + pass_span.end(&mut render_pass); + + Ok(()) + } +} + +/// Fullscreen pass to generate prepass textures based on the visibility buffer generated from rasterizing meshlets. +#[derive(Default)] +pub struct MeshletPrepassNode; +impl ViewNode for MeshletPrepassNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ViewPrepassTextures, + &'static ViewUniformOffset, + &'static PreviousViewUniformOffset, + Option<&'static MainPassResolutionOverride>, + Has, + &'static MeshletViewMaterialsPrepass, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + camera, + view_prepass_textures, + view_uniform_offset, + previous_view_uniform_offset, + resolution_override, + view_has_motion_vector_prepass, + meshlet_view_materials, + meshlet_view_bind_groups, + meshlet_view_resources, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + if meshlet_view_materials.is_empty() { + return Ok(()); + } + + let ( + Some(prepass_view_bind_group), + Some(instance_manager), + Some(pipeline_cache), + Some(meshlet_material_depth), + Some(meshlet_material_shade_bind_group), + ) = ( + world.get_resource::(), + world.get_resource::(), + world.get_resource::(), + meshlet_view_resources.material_depth.as_ref(), + meshlet_view_bind_groups.material_shade.as_ref(), + ) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let color_attachments = vec![ + view_prepass_textures + .normal + .as_ref() + .map(|normals_texture| normals_texture.get_attachment()), + view_prepass_textures + .motion_vectors + .as_ref() + .map(|motion_vectors_texture| motion_vectors_texture.get_attachment()), + // Use None in place of Deferred attachments + None, + None, + ]; + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("meshlet_material_prepass"), + color_attachments: &color_attachments, + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &meshlet_material_depth.default_view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + let pass_span = diagnostics.pass_span(&mut render_pass, "meshlet_material_prepass"); + if let Some(viewport) = + Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override) + { + render_pass.set_camera_viewport(&viewport); + } + + if view_has_motion_vector_prepass { + render_pass.set_bind_group( + 0, + prepass_view_bind_group.motion_vectors.as_ref().unwrap(), + &[ + view_uniform_offset.offset, + previous_view_uniform_offset.offset, + ], + ); + } else { + render_pass.set_bind_group( + 0, + prepass_view_bind_group.no_motion_vectors.as_ref().unwrap(), + &[view_uniform_offset.offset], + ); + } + + render_pass.set_bind_group(1, &prepass_view_bind_group.empty_bind_group, &[]); + render_pass.set_bind_group(2, meshlet_material_shade_bind_group, &[]); + + // 1 fullscreen triangle draw per material + for (material_id, material_pipeline_id, material_bind_group) in + meshlet_view_materials.iter() + { + if instance_manager.material_present_in_scene(material_id) + && let Some(material_pipeline) = + pipeline_cache.get_render_pipeline(*material_pipeline_id) + { + let x = *material_id * 3; + render_pass.set_render_pipeline(material_pipeline); + render_pass.set_bind_group(2, material_bind_group, &[]); + render_pass.draw(x..(x + 3), 0..1); + } + } + + pass_span.end(&mut render_pass); + + Ok(()) + } +} + +/// Fullscreen pass to generate a gbuffer based on the visibility buffer generated from rasterizing meshlets. +#[derive(Default)] +pub struct MeshletDeferredGBufferPrepassNode; +impl ViewNode for MeshletDeferredGBufferPrepassNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ViewPrepassTextures, + &'static ViewUniformOffset, + &'static PreviousViewUniformOffset, + Option<&'static MainPassResolutionOverride>, + Has, + &'static MeshletViewMaterialsDeferredGBufferPrepass, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + camera, + view_prepass_textures, + view_uniform_offset, + previous_view_uniform_offset, + resolution_override, + view_has_motion_vector_prepass, + meshlet_view_materials, + meshlet_view_bind_groups, + meshlet_view_resources, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + if meshlet_view_materials.is_empty() { + return Ok(()); + } + + let ( + Some(prepass_view_bind_group), + Some(instance_manager), + Some(pipeline_cache), + Some(meshlet_material_depth), + Some(meshlet_material_shade_bind_group), + ) = ( + world.get_resource::(), + world.get_resource::(), + world.get_resource::(), + meshlet_view_resources.material_depth.as_ref(), + meshlet_view_bind_groups.material_shade.as_ref(), + ) + else { + return Ok(()); + }; + + let color_attachments = vec![ + view_prepass_textures + .normal + .as_ref() + .map(|normals_texture| normals_texture.get_attachment()), + view_prepass_textures + .motion_vectors + .as_ref() + .map(|motion_vectors_texture| motion_vectors_texture.get_attachment()), + view_prepass_textures + .deferred + .as_ref() + .map(|deferred_texture| deferred_texture.get_attachment()), + view_prepass_textures + .deferred_lighting_pass_id + .as_ref() + .map(|deferred_lighting_pass_id| deferred_lighting_pass_id.get_attachment()), + ]; + + let diagnostics = render_context.diagnostic_recorder(); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("meshlet_material_deferred_prepass"), + color_attachments: &color_attachments, + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &meshlet_material_depth.default_view, + depth_ops: Some(Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + let pass_span = + diagnostics.pass_span(&mut render_pass, "meshlet_material_deferred_prepass"); + if let Some(viewport) = + Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override) + { + render_pass.set_camera_viewport(&viewport); + } + + if view_has_motion_vector_prepass { + render_pass.set_bind_group( + 0, + prepass_view_bind_group.motion_vectors.as_ref().unwrap(), + &[ + view_uniform_offset.offset, + previous_view_uniform_offset.offset, + ], + ); + } else { + render_pass.set_bind_group( + 0, + prepass_view_bind_group.no_motion_vectors.as_ref().unwrap(), + &[view_uniform_offset.offset], + ); + } + + render_pass.set_bind_group(1, &prepass_view_bind_group.empty_bind_group, &[]); + render_pass.set_bind_group(2, meshlet_material_shade_bind_group, &[]); + + // 1 fullscreen triangle draw per material + for (material_id, material_pipeline_id, material_bind_group) in + meshlet_view_materials.iter() + { + if instance_manager.material_present_in_scene(material_id) + && let Some(material_pipeline) = + pipeline_cache.get_render_pipeline(*material_pipeline_id) + { + let x = *material_id * 3; + render_pass.set_render_pipeline(material_pipeline); + render_pass.set_bind_group(2, material_bind_group, &[]); + render_pass.draw(x..(x + 3), 0..1); + } + } + + pass_span.end(&mut render_pass); + + Ok(()) + } +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/meshlet_bindings.wgsl b/crates/libmarathon/src/render/pbr/meshlet/meshlet_bindings.wgsl new file mode 100644 index 0000000..4533b2b --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/meshlet_bindings.wgsl @@ -0,0 +1,306 @@ +#define_import_path bevy_pbr::meshlet_bindings + +#import bevy_pbr::mesh_types::Mesh +#import bevy_render::view::View +#import bevy_pbr::prepass_bindings::PreviousViewUniforms +#import bevy_pbr::utils::octahedral_decode_signed + +struct BvhNode { + aabbs: array, + lod_bounds: array, 8>, + child_counts: array, + _padding: vec2, +} + +struct Meshlet { + start_vertex_position_bit: u32, + start_vertex_attribute_id: u32, + start_index_id: u32, + packed_a: u32, + packed_b: u32, + min_vertex_position_channel_x: f32, + min_vertex_position_channel_y: f32, + min_vertex_position_channel_z: f32, +} + +fn get_meshlet_vertex_count(meshlet: ptr) -> u32 { + return extractBits((*meshlet).packed_a, 0u, 8u); +} + +fn get_meshlet_triangle_count(meshlet: ptr) -> u32 { + return extractBits((*meshlet).packed_a, 8u, 8u); +} + +struct MeshletCullData { + aabb: MeshletAabbErrorOffset, + lod_group_sphere: vec4, +} + +struct MeshletAabb { + center: vec3, + half_extent: vec3, +} + +struct MeshletAabbErrorOffset { + center_and_error: vec4, + half_extent_and_child_offset: vec4, +} + +fn get_aabb(aabb: ptr) -> MeshletAabb { + return MeshletAabb( + (*aabb).center_and_error.xyz, + (*aabb).half_extent_and_child_offset.xyz, + ); +} + +fn get_aabb_error(aabb: ptr) -> f32 { + return (*aabb).center_and_error.w; +} + +fn get_aabb_child_offset(aabb: ptr) -> u32 { + return bitcast((*aabb).half_extent_and_child_offset.w); +} + +struct DispatchIndirectArgs { + x: atomic, + y: u32, + z: u32, +} + +struct DrawIndirectArgs { + vertex_count: u32, + instance_count: atomic, + first_vertex: u32, + first_instance: u32, +} + +// Either a BVH node or a meshlet, along with the instance it is associated with. +// Refers to BVH nodes in `meshlet_bvh_cull_queue` and `meshlet_second_pass_bvh_queue`, where `offset` is the index into `meshlet_bvh_nodes`. +// Refers to meshlets in `meshlet_meshlet_cull_queue` and `meshlet_raster_clusters`. +// In `meshlet_meshlet_cull_queue`, `offset` is the index into `meshlet_cull_data`. +// In `meshlet_raster_clusters`, `offset` is the index into `meshlets`. +struct InstancedOffset { + instance_id: u32, + offset: u32, +} + +const CENTIMETERS_PER_METER = 100.0; + +#ifdef MESHLET_INSTANCE_CULLING_PASS +struct Constants { scene_instance_count: u32 } +var constants: Constants; + +// Cull data +@group(0) @binding(0) var depth_pyramid: texture_2d; +@group(0) @binding(1) var view: View; +@group(0) @binding(2) var previous_view: PreviousViewUniforms; + +// Per entity instance data +@group(0) @binding(3) var meshlet_instance_uniforms: array; +@group(0) @binding(4) var meshlet_view_instance_visibility: array; // 1 bit per entity instance, packed as a bitmask +@group(0) @binding(5) var meshlet_instance_aabbs: array; +@group(0) @binding(6) var meshlet_instance_bvh_root_nodes: array; + +// BVH cull queue data +@group(0) @binding(7) var meshlet_bvh_cull_count_write: atomic; +@group(0) @binding(8) var meshlet_bvh_cull_dispatch: DispatchIndirectArgs; +@group(0) @binding(9) var meshlet_bvh_cull_queue: array; + +// Second pass queue data +#ifdef MESHLET_FIRST_CULLING_PASS +@group(0) @binding(10) var meshlet_second_pass_instance_count: atomic; +@group(0) @binding(11) var meshlet_second_pass_instance_dispatch: DispatchIndirectArgs; +@group(0) @binding(12) var meshlet_second_pass_instance_candidates: array; +#else +@group(0) @binding(10) var meshlet_second_pass_instance_count: u32; +@group(0) @binding(11) var meshlet_second_pass_instance_candidates: array; +#endif +#endif + +#ifdef MESHLET_BVH_CULLING_PASS +struct Constants { read_from_front: u32, rightmost_slot: u32 } +var constants: Constants; + +// Cull data +@group(0) @binding(0) var depth_pyramid: texture_2d; // From the end of the last frame for the first culling pass, and from the first raster pass for the second culling pass +@group(0) @binding(1) var view: View; +@group(0) @binding(2) var previous_view: PreviousViewUniforms; + +// Global mesh data +@group(0) @binding(3) var meshlet_bvh_nodes: array; + +// Per entity instance data +@group(0) @binding(4) var meshlet_instance_uniforms: array; + +// BVH cull queue data +@group(0) @binding(5) var meshlet_bvh_cull_count_read: u32; +@group(0) @binding(6) var meshlet_bvh_cull_count_write: atomic; +@group(0) @binding(7) var meshlet_bvh_cull_dispatch: DispatchIndirectArgs; +@group(0) @binding(8) var meshlet_bvh_cull_queue: array; + +// Meshlet cull queue data +@group(0) @binding(9) var meshlet_meshlet_cull_count_early: atomic; +@group(0) @binding(10) var meshlet_meshlet_cull_count_late: atomic; +@group(0) @binding(11) var meshlet_meshlet_cull_dispatch_early: DispatchIndirectArgs; +@group(0) @binding(12) var meshlet_meshlet_cull_dispatch_late: DispatchIndirectArgs; +@group(0) @binding(13) var meshlet_meshlet_cull_queue: array; + +// Second pass queue data +#ifdef MESHLET_FIRST_CULLING_PASS +@group(0) @binding(14) var meshlet_second_pass_bvh_count: atomic; +@group(0) @binding(15) var meshlet_second_pass_bvh_dispatch: DispatchIndirectArgs; +@group(0) @binding(16) var meshlet_second_pass_bvh_queue: array; +#endif +#endif + +#ifdef MESHLET_CLUSTER_CULLING_PASS +struct Constants { rightmost_slot: u32 } +var constants: Constants; + +// Cull data +@group(0) @binding(0) var depth_pyramid: texture_2d; // From the end of the last frame for the first culling pass, and from the first raster pass for the second culling pass +@group(0) @binding(1) var view: View; +@group(0) @binding(2) var previous_view: PreviousViewUniforms; + +// Global mesh data +@group(0) @binding(3) var meshlet_cull_data: array; + +// Per entity instance data +@group(0) @binding(4) var meshlet_instance_uniforms: array; + +// Raster queue data +@group(0) @binding(5) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; +@group(0) @binding(6) var meshlet_hardware_raster_indirect_args: DrawIndirectArgs; +@group(0) @binding(7) var meshlet_previous_raster_counts: array; +@group(0) @binding(8) var meshlet_raster_clusters: array; + +// Meshlet cull queue data +@group(0) @binding(9) var meshlet_meshlet_cull_count_read: u32; + +// Second pass queue data +#ifdef MESHLET_FIRST_CULLING_PASS +@group(0) @binding(10) var meshlet_meshlet_cull_count_write: atomic; +@group(0) @binding(11) var meshlet_meshlet_cull_dispatch: DispatchIndirectArgs; +@group(0) @binding(12) var meshlet_meshlet_cull_queue: array; +#else +@group(0) @binding(10) var meshlet_meshlet_cull_queue: array; +#endif +#endif + +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS +@group(0) @binding(0) var meshlet_raster_clusters: array; // Per cluster +@group(0) @binding(1) var meshlets: array; // Per meshlet +@group(0) @binding(2) var meshlet_indices: array; // Many per meshlet +@group(0) @binding(3) var meshlet_vertex_positions: array; // Many per meshlet +@group(0) @binding(4) var meshlet_instance_uniforms: array; // Per entity instance +@group(0) @binding(5) var meshlet_previous_raster_counts: array; +@group(0) @binding(6) var meshlet_software_raster_cluster_count: u32; +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@group(0) @binding(7) var meshlet_visibility_buffer: texture_storage_2d; +#else +@group(0) @binding(7) var meshlet_visibility_buffer: texture_storage_2d; +#endif +@group(0) @binding(8) var view: View; + +// TODO: Load only twice, instead of 3x in cases where you load 3 indices per thread? +fn get_meshlet_vertex_id(index_id: u32) -> u32 { + let packed_index = meshlet_indices[index_id / 4u]; + let bit_offset = (index_id % 4u) * 8u; + return extractBits(packed_index, bit_offset, 8u); +} + +fn get_meshlet_vertex_position(meshlet: ptr, vertex_id: u32) -> vec3 { + // Get bitstream start for the vertex + let unpacked = unpack4xU8((*meshlet).packed_b); + let bits_per_channel = unpacked.xyz; + let bits_per_vertex = bits_per_channel.x + bits_per_channel.y + bits_per_channel.z; + var start_bit = (*meshlet).start_vertex_position_bit + (vertex_id * bits_per_vertex); + + // Read each vertex channel from the bitstream + var vertex_position_packed = vec3(0u); + for (var i = 0u; i < 3u; i++) { + let lower_word_index = start_bit / 32u; + let lower_word_bit_offset = start_bit & 31u; + var next_32_bits = meshlet_vertex_positions[lower_word_index] >> lower_word_bit_offset; + if lower_word_bit_offset + bits_per_channel[i] > 32u { + next_32_bits |= meshlet_vertex_positions[lower_word_index + 1u] << (32u - lower_word_bit_offset); + } + vertex_position_packed[i] = extractBits(next_32_bits, 0u, bits_per_channel[i]); + start_bit += bits_per_channel[i]; + } + + // Remap [0, range_max - range_min] vec3 to [range_min, range_max] vec3 + var vertex_position = vec3(vertex_position_packed) + vec3( + (*meshlet).min_vertex_position_channel_x, + (*meshlet).min_vertex_position_channel_y, + (*meshlet).min_vertex_position_channel_z, + ); + + // Reverse vertex quantization + let vertex_position_quantization_factor = unpacked.w; + vertex_position /= f32(1u << vertex_position_quantization_factor) * CENTIMETERS_PER_METER; + + return vertex_position; +} +#endif + +#ifdef MESHLET_MESH_MATERIAL_PASS +@group(2) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; +@group(2) @binding(1) var meshlet_raster_clusters: array; // Per cluster +@group(2) @binding(2) var meshlets: array; // Per meshlet +@group(2) @binding(3) var meshlet_indices: array; // Many per meshlet +@group(2) @binding(4) var meshlet_vertex_positions: array; // Many per meshlet +@group(2) @binding(5) var meshlet_vertex_normals: array; // Many per meshlet +@group(2) @binding(6) var meshlet_vertex_uvs: array>; // Many per meshlet +@group(2) @binding(7) var meshlet_instance_uniforms: array; // Per entity instance + +// TODO: Load only twice, instead of 3x in cases where you load 3 indices per thread? +fn get_meshlet_vertex_id(index_id: u32) -> u32 { + let packed_index = meshlet_indices[index_id / 4u]; + let bit_offset = (index_id % 4u) * 8u; + return extractBits(packed_index, bit_offset, 8u); +} + +fn get_meshlet_vertex_position(meshlet: ptr, vertex_id: u32) -> vec3 { + // Get bitstream start for the vertex + let unpacked = unpack4xU8((*meshlet).packed_b); + let bits_per_channel = unpacked.xyz; + let bits_per_vertex = bits_per_channel.x + bits_per_channel.y + bits_per_channel.z; + var start_bit = (*meshlet).start_vertex_position_bit + (vertex_id * bits_per_vertex); + + // Read each vertex channel from the bitstream + var vertex_position_packed = vec3(0u); + for (var i = 0u; i < 3u; i++) { + let lower_word_index = start_bit / 32u; + let lower_word_bit_offset = start_bit & 31u; + var next_32_bits = meshlet_vertex_positions[lower_word_index] >> lower_word_bit_offset; + if lower_word_bit_offset + bits_per_channel[i] > 32u { + next_32_bits |= meshlet_vertex_positions[lower_word_index + 1u] << (32u - lower_word_bit_offset); + } + vertex_position_packed[i] = extractBits(next_32_bits, 0u, bits_per_channel[i]); + start_bit += bits_per_channel[i]; + } + + // Remap [0, range_max - range_min] vec3 to [range_min, range_max] vec3 + var vertex_position = vec3(vertex_position_packed) + vec3( + (*meshlet).min_vertex_position_channel_x, + (*meshlet).min_vertex_position_channel_y, + (*meshlet).min_vertex_position_channel_z, + ); + + // Reverse vertex quantization + let vertex_position_quantization_factor = unpacked.w; + vertex_position /= f32(1u << vertex_position_quantization_factor) * CENTIMETERS_PER_METER; + + return vertex_position; +} + +fn get_meshlet_vertex_normal(meshlet: ptr, vertex_id: u32) -> vec3 { + let packed_normal = meshlet_vertex_normals[(*meshlet).start_vertex_attribute_id + vertex_id]; + return octahedral_decode_signed(unpack2x16snorm(packed_normal)); +} + +fn get_meshlet_vertex_uv(meshlet: ptr, vertex_id: u32) -> vec2 { + return meshlet_vertex_uvs[(*meshlet).start_vertex_attribute_id + vertex_id]; +} +#endif diff --git a/crates/libmarathon/src/render/pbr/meshlet/meshlet_cull_shared.wgsl b/crates/libmarathon/src/render/pbr/meshlet/meshlet_cull_shared.wgsl new file mode 100644 index 0000000..975dd74 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/meshlet_cull_shared.wgsl @@ -0,0 +1,207 @@ +#define_import_path bevy_pbr::meshlet_cull_shared + +#import bevy_pbr::meshlet_bindings::{ + MeshletAabb, + DispatchIndirectArgs, + InstancedOffset, + depth_pyramid, + view, + previous_view, + meshlet_instance_uniforms, +} +#import bevy_render::maths::affine3_to_square + +// https://github.com/zeux/meshoptimizer/blob/1e48e96c7e8059321de492865165e9ef071bffba/demo/nanite.cpp#L115 +fn lod_error_is_imperceptible(lod_sphere: vec4, simplification_error: f32, instance_id: u32) -> bool { + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + let world_scale = max(length(world_from_local[0]), max(length(world_from_local[1]), length(world_from_local[2]))); + let camera_pos = view.world_position; + + let projection = view.clip_from_view; + if projection[3][3] == 1.0 { + // Orthographic + let world_error = simplification_error * world_scale; + let proj = projection[1][1]; + let height = 2.0 / proj; + let norm_error = world_error / height; + return norm_error * view.viewport.w < 1.0; + } else { + // Perspective + var near = projection[3][2]; + let world_sphere_center = (world_from_local * vec4(lod_sphere.xyz, 1.0)).xyz; + let world_sphere_radius = lod_sphere.w * world_scale; + let d_pos = world_sphere_center - camera_pos; + let d = sqrt(dot(d_pos, d_pos)) - world_sphere_radius; + let norm_error = simplification_error / max(d, near) * projection[1][1] * 0.5; + return norm_error * view.viewport.w < 1.0; + } +} + +fn normalize_plane(p: vec4) -> vec4 { + return p / length(p.xyz); +} + +// https://fgiesen.wordpress.com/2012/08/31/frustum-planes-from-the-projection-matrix/ +// https://fgiesen.wordpress.com/2010/10/17/view-frustum-culling/ +fn aabb_in_frustum(aabb: MeshletAabb, instance_id: u32) -> bool { + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + let clip_from_local = view.clip_from_world * world_from_local; + let row_major = transpose(clip_from_local); + let planes = array( + row_major[3] + row_major[0], + row_major[3] - row_major[0], + row_major[3] + row_major[1], + row_major[3] - row_major[1], + row_major[2], + ); + + for (var i = 0; i < 5; i++) { + let plane = normalize_plane(planes[i]); + let flipped = aabb.half_extent * sign(plane.xyz); + if dot(aabb.center + flipped, plane.xyz) <= -plane.w { + return false; + } + } + return true; +} + +struct ScreenAabb { + min: vec3, + max: vec3, +} + +fn min8(a: vec3, b: vec3, c: vec3, d: vec3, e: vec3, f: vec3, g: vec3, h: vec3) -> vec3 { + return min(min(min(a, b), min(c, d)), min(min(e, f), min(g, h))); +} + +fn max8(a: vec3, b: vec3, c: vec3, d: vec3, e: vec3, f: vec3, g: vec3, h: vec3) -> vec3 { + return max(max(max(a, b), max(c, d)), max(max(e, f), max(g, h))); +} + +fn min8_4(a: vec4, b: vec4, c: vec4, d: vec4, e: vec4, f: vec4, g: vec4, h: vec4) -> vec4 { + return min(min(min(a, b), min(c, d)), min(min(e, f), min(g, h))); +} + +// https://zeux.io/2023/01/12/approximate-projected-bounds/ +fn project_aabb(clip_from_local: mat4x4, near: f32, aabb: MeshletAabb, out: ptr) -> bool { + let extent = aabb.half_extent * 2.0; + let sx = clip_from_local * vec4(extent.x, 0.0, 0.0, 0.0); + let sy = clip_from_local * vec4(0.0, extent.y, 0.0, 0.0); + let sz = clip_from_local * vec4(0.0, 0.0, extent.z, 0.0); + + let p0 = clip_from_local * vec4(aabb.center - aabb.half_extent, 1.0); + let p1 = p0 + sz; + let p2 = p0 + sy; + let p3 = p2 + sz; + let p4 = p0 + sx; + let p5 = p4 + sz; + let p6 = p4 + sy; + let p7 = p6 + sz; + + let depth = min8_4(p0, p1, p2, p3, p4, p5, p6, p7).w; + // do not occlusion cull if we are inside the aabb + if depth < near { + return false; + } + + let dp0 = p0.xyz / p0.w; + let dp1 = p1.xyz / p1.w; + let dp2 = p2.xyz / p2.w; + let dp3 = p3.xyz / p3.w; + let dp4 = p4.xyz / p4.w; + let dp5 = p5.xyz / p5.w; + let dp6 = p6.xyz / p6.w; + let dp7 = p7.xyz / p7.w; + let min = min8(dp0, dp1, dp2, dp3, dp4, dp5, dp6, dp7); + let max = max8(dp0, dp1, dp2, dp3, dp4, dp5, dp6, dp7); + var vaabb = vec4(min.xy, max.xy); + // convert ndc to texture coordinates by rescaling and flipping Y + vaabb = vaabb.xwzy * vec4(0.5, -0.5, 0.5, -0.5) + 0.5; + (*out).min = vec3(vaabb.xy, min.z); + (*out).max = vec3(vaabb.zw, max.z); + return true; +} + +fn sample_hzb(smin: vec2, smax: vec2, mip: i32) -> f32 { + let texel = vec4(0, 1, 2, 3); + let sx = min(smin.x + texel, smax.xxxx); + let sy = min(smin.y + texel, smax.yyyy); + // TODO: switch to min samplers when wgpu has them + // sampling 16 times a finer mip is worth the extra cost for better culling + let a = sample_hzb_row(sx, sy.x, mip); + let b = sample_hzb_row(sx, sy.y, mip); + let c = sample_hzb_row(sx, sy.z, mip); + let d = sample_hzb_row(sx, sy.w, mip); + return min(min(a, b), min(c, d)); +} + +fn sample_hzb_row(sx: vec4, sy: u32, mip: i32) -> f32 { + let a = textureLoad(depth_pyramid, vec2(sx.x, sy), mip).x; + let b = textureLoad(depth_pyramid, vec2(sx.y, sy), mip).x; + let c = textureLoad(depth_pyramid, vec2(sx.z, sy), mip).x; + let d = textureLoad(depth_pyramid, vec2(sx.w, sy), mip).x; + return min(min(a, b), min(c, d)); +} + +// TODO: We should probably be using a POT HZB texture? +fn occlusion_cull_screen_aabb(aabb: ScreenAabb, screen: vec2) -> bool { + let hzb_size = ceil(screen * 0.5); + let aabb_min = aabb.min.xy * hzb_size; + let aabb_max = aabb.max.xy * hzb_size; + + let min_texel = vec2(max(aabb_min, vec2(0.0))); + let max_texel = vec2(min(aabb_max, hzb_size - 1.0)); + let size = max_texel - min_texel; + let max_size = max(size.x, size.y); + + // note: add 1 before max because the unsigned overflow behavior is intentional + // it wraps around firstLeadingBit(0) = ~0 to 0 + // TODO: we actually sample a 4x4 block, so ideally this would be `max(..., 3u) - 3u`. + // However, since our HZB is not a power of two, we need to be extra-conservative to not over-cull, so we go up a mip. + var mip = max(firstLeadingBit(max_size) + 1u, 2u) - 2u; + + if any((max_texel >> vec2(mip)) > (min_texel >> vec2(mip)) + 3) { + mip += 1u; + } + + let smin = min_texel >> vec2(mip); + let smax = max_texel >> vec2(mip); + + let curr_depth = sample_hzb(smin, smax, i32(mip)); + return aabb.max.z <= curr_depth; +} + +fn occlusion_cull_projection() -> mat4x4 { +#ifdef FIRST_CULLING_PASS + return view.clip_from_world; +#else + return previous_view.clip_from_world; +#endif +} + +fn occlusion_cull_clip_from_local(instance_id: u32) -> mat4x4 { +#ifdef FIRST_CULLING_PASS + let prev_world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].previous_world_from_local); + return previous_view.clip_from_world * prev_world_from_local; +#else + let world_from_local = affine3_to_square(meshlet_instance_uniforms[instance_id].world_from_local); + return view.clip_from_world * world_from_local; +#endif +} + +fn should_occlusion_cull_aabb(aabb: MeshletAabb, instance_id: u32) -> bool { + let projection = occlusion_cull_projection(); + var near: f32; + if projection[3][3] == 1.0 { + near = projection[3][2] / projection[2][2]; + } else { + near = projection[3][2]; + } + + let clip_from_local = occlusion_cull_clip_from_local(instance_id); + var screen_aabb = ScreenAabb(vec3(0.0), vec3(0.0)); + if project_aabb(clip_from_local, near, aabb, &screen_aabb) { + return occlusion_cull_screen_aabb(screen_aabb, view.viewport.zw); + } + return false; +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/meshlet_mesh_manager.rs b/crates/libmarathon/src/render/pbr/meshlet/meshlet_mesh_manager.rs new file mode 100644 index 0000000..1af5b42 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/meshlet_mesh_manager.rs @@ -0,0 +1,161 @@ +use crate::render::pbr::meshlet::asset::{BvhNode, MeshletAabb, MeshletCullData}; + +use super::{asset::Meshlet, persistent_buffer::PersistentGpuBuffer, MeshletMesh}; +use std::sync::Arc; +use bevy_asset::{AssetId, Assets}; +use bevy_ecs::{ + resource::Resource, + system::{Commands, Res, ResMut}, +}; +use bevy_math::Vec2; +use bevy_platform::collections::HashMap; +use crate::render::{ + render_resource::BufferAddress, + renderer::{RenderDevice, RenderQueue}, +}; +use core::ops::Range; + +/// Manages uploading [`MeshletMesh`] asset data to the GPU. +#[derive(Resource)] +pub struct MeshletMeshManager { + pub vertex_positions: PersistentGpuBuffer>, + pub vertex_normals: PersistentGpuBuffer>, + pub vertex_uvs: PersistentGpuBuffer>, + pub indices: PersistentGpuBuffer>, + pub bvh_nodes: PersistentGpuBuffer>, + pub meshlets: PersistentGpuBuffer>, + pub meshlet_cull_data: PersistentGpuBuffer>, + meshlet_mesh_slices: + HashMap, ([Range; 7], MeshletAabb, u32)>, +} + +pub fn init_meshlet_mesh_manager(mut commands: Commands, render_device: Res) { + commands.insert_resource(MeshletMeshManager { + vertex_positions: PersistentGpuBuffer::new("meshlet_vertex_positions", &render_device), + vertex_normals: PersistentGpuBuffer::new("meshlet_vertex_normals", &render_device), + vertex_uvs: PersistentGpuBuffer::new("meshlet_vertex_uvs", &render_device), + indices: PersistentGpuBuffer::new("meshlet_indices", &render_device), + bvh_nodes: PersistentGpuBuffer::new("meshlet_bvh_nodes", &render_device), + meshlets: PersistentGpuBuffer::new("meshlets", &render_device), + meshlet_cull_data: PersistentGpuBuffer::new("meshlet_cull_data", &render_device), + meshlet_mesh_slices: HashMap::default(), + }); +} + +impl MeshletMeshManager { + // Returns the index of the root BVH node, as well as the depth of the BVH. + pub fn queue_upload_if_needed( + &mut self, + asset_id: AssetId, + assets: &mut Assets, + ) -> (u32, MeshletAabb, u32) { + let queue_meshlet_mesh = |asset_id: &AssetId| { + let meshlet_mesh = assets.remove_untracked(*asset_id).expect( + "MeshletMesh asset was already unloaded but is not registered with MeshletMeshManager", + ); + + let vertex_positions_slice = self + .vertex_positions + .queue_write(Arc::clone(&meshlet_mesh.vertex_positions), ()); + let vertex_normals_slice = self + .vertex_normals + .queue_write(Arc::clone(&meshlet_mesh.vertex_normals), ()); + let vertex_uvs_slice = self + .vertex_uvs + .queue_write(Arc::clone(&meshlet_mesh.vertex_uvs), ()); + let indices_slice = self + .indices + .queue_write(Arc::clone(&meshlet_mesh.indices), ()); + let meshlets_slice = self.meshlets.queue_write( + Arc::clone(&meshlet_mesh.meshlets), + ( + vertex_positions_slice.start, + vertex_normals_slice.start, + indices_slice.start, + ), + ); + let base_meshlet_index = (meshlets_slice.start / size_of::() as u64) as u32; + let bvh_node_slice = self + .bvh_nodes + .queue_write(Arc::clone(&meshlet_mesh.bvh), base_meshlet_index); + let meshlet_cull_data_slice = self + .meshlet_cull_data + .queue_write(Arc::clone(&meshlet_mesh.meshlet_cull_data), ()); + + ( + [ + vertex_positions_slice, + vertex_normals_slice, + vertex_uvs_slice, + indices_slice, + bvh_node_slice, + meshlets_slice, + meshlet_cull_data_slice, + ], + meshlet_mesh.aabb, + meshlet_mesh.bvh_depth, + ) + }; + + // If the MeshletMesh asset has not been uploaded to the GPU yet, queue it for uploading + let ([_, _, _, _, bvh_node_slice, _, _], aabb, bvh_depth) = self + .meshlet_mesh_slices + .entry(asset_id) + .or_insert_with_key(queue_meshlet_mesh) + .clone(); + + ( + (bvh_node_slice.start / size_of::() as u64) as u32, + aabb, + bvh_depth, + ) + } + + pub fn remove(&mut self, asset_id: &AssetId) { + if let Some(( + [vertex_positions_slice, vertex_normals_slice, vertex_uvs_slice, indices_slice, bvh_node_slice, meshlets_slice, meshlet_cull_data_slice], + _, + _, + )) = self.meshlet_mesh_slices.remove(asset_id) + { + self.vertex_positions + .mark_slice_unused(vertex_positions_slice); + self.vertex_normals.mark_slice_unused(vertex_normals_slice); + self.vertex_uvs.mark_slice_unused(vertex_uvs_slice); + self.indices.mark_slice_unused(indices_slice); + self.bvh_nodes.mark_slice_unused(bvh_node_slice); + self.meshlets.mark_slice_unused(meshlets_slice); + self.meshlet_cull_data + .mark_slice_unused(meshlet_cull_data_slice); + } + } +} + +/// Upload all newly queued [`MeshletMesh`] asset data to the GPU. +pub fn perform_pending_meshlet_mesh_writes( + mut meshlet_mesh_manager: ResMut, + render_queue: Res, + render_device: Res, +) { + meshlet_mesh_manager + .vertex_positions + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .vertex_normals + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .vertex_uvs + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .indices + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .bvh_nodes + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .meshlets + .perform_writes(&render_queue, &render_device); + meshlet_mesh_manager + .meshlet_cull_data + .perform_writes(&render_queue, &render_device); +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/meshlet_mesh_material.wgsl b/crates/libmarathon/src/render/pbr/meshlet/meshlet_mesh_material.wgsl new file mode 100644 index 0000000..1309c78 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/meshlet_mesh_material.wgsl @@ -0,0 +1,52 @@ +#import bevy_pbr::{ + meshlet_visibility_buffer_resolve::resolve_vertex_output, + view_transformations::uv_to_ndc, + prepass_io, + pbr_prepass_functions, + utils::rand_f, +} + +@vertex +fn vertex(@builtin(vertex_index) vertex_input: u32) -> @builtin(position) vec4 { + let vertex_index = vertex_input % 3u; + let material_id = vertex_input / 3u; + let material_depth = f32(material_id) / 65535.0; + let uv = vec2(vec2(vertex_index >> 1u, vertex_index & 1u)) * 2.0; + return vec4(uv_to_ndc(uv), material_depth, 1.0); +} + +@fragment +fn fragment(@builtin(position) frag_coord: vec4) -> @location(0) vec4 { + let vertex_output = resolve_vertex_output(frag_coord); + var rng = vertex_output.cluster_id; + let color = vec3(rand_f(&rng), rand_f(&rng), rand_f(&rng)); + return vec4(color, 1.0); +} + +#ifdef PREPASS_FRAGMENT +@fragment +fn prepass_fragment(@builtin(position) frag_coord: vec4) -> prepass_io::FragmentOutput { + let vertex_output = resolve_vertex_output(frag_coord); + + var out: prepass_io::FragmentOutput; + +#ifdef NORMAL_PREPASS + out.normal = vec4(vertex_output.world_normal * 0.5 + vec3(0.5), 1.0); +#endif + +#ifdef MOTION_VECTOR_PREPASS + out.motion_vector = vertex_output.motion_vector; +#endif + +#ifdef DEFERRED_PREPASS + // There isn't any material info available for this default prepass shader so we are just writing  + // emissive magenta out to the deferred gbuffer to be rendered by the first deferred lighting pass layer. + // This is here so if the default prepass fragment is used for deferred magenta will be rendered, and also + // as an example to show that a user could write to the deferred gbuffer if they were to start from this shader. + out.deferred = vec4(0u, bevy_pbr::rgb9e5::vec3_to_rgb9e5_(vec3(1.0, 0.0, 1.0)), 0u, 0u); + out.deferred_lighting_pass_id = 1u; +#endif + + return out; +} +#endif diff --git a/crates/libmarathon/src/render/pbr/meshlet/meshlet_preview.png b/crates/libmarathon/src/render/pbr/meshlet/meshlet_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..2c319a8987720290b4c7622f3740c9105f6af320 GIT binary patch literal 183449 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

    qsp5mr+%yVIU5tmf=?sr2T`-!atY+EslY=c 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 WgpuFeatures { + WgpuFeatures::TEXTURE_INT64_ATOMIC + | WgpuFeatures::TEXTURE_ATOMIC + | WgpuFeatures::SHADER_INT64 + | WgpuFeatures::SUBGROUP + | WgpuFeatures::DEPTH_CLIP_CONTROL + | WgpuFeatures::PUSH_CONSTANTS + } +} + +impl Plugin for MeshletPlugin { + fn build(&self, app: &mut App) { + #[cfg(target_endian = "big")] + compile_error!("MeshletPlugin is only supported on little-endian processors."); + + if self.cluster_buffer_slots > 2_u32.pow(25) { + error!("MeshletPlugin::cluster_buffer_slots must not be greater than 2^25."); + std::process::exit(1); + } + + load_shader_library!(app, "meshlet_bindings.wgsl"); + load_shader_library!(app, "visibility_buffer_resolve.wgsl"); + load_shader_library!(app, "meshlet_cull_shared.wgsl"); + embedded_asset!(app, "clear_visibility_buffer.wgsl"); + embedded_asset!(app, "cull_instances.wgsl"); + embedded_asset!(app, "cull_bvh.wgsl"); + embedded_asset!(app, "cull_clusters.wgsl"); + embedded_asset!(app, "visibility_buffer_software_raster.wgsl"); + embedded_asset!(app, "visibility_buffer_hardware_raster.wgsl"); + embedded_asset!(app, "meshlet_mesh_material.wgsl"); + embedded_asset!(app, "resolve_render_targets.wgsl"); + embedded_asset!(app, "remap_1d_to_2d_dispatch.wgsl"); + embedded_asset!(app, "fill_counts.wgsl"); + + app.init_asset::() + .register_asset_loader(MeshletMeshLoader); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + // Create a variable here so we can move-capture it. + let cluster_buffer_slots = self.cluster_buffer_slots; + let init_resource_manager_system = + move |mut commands: Commands, render_device: Res| { + commands + .insert_resource(ResourceManager::new(cluster_buffer_slots, &render_device)); + }; + + render_app + .add_render_graph_node::( + Core3d, + NodeMeshlet::VisibilityBufferRasterPass, + ) + .add_render_graph_node::>( + Core3d, + NodeMeshlet::Prepass, + ) + .add_render_graph_node::>( + Core3d, + NodeMeshlet::DeferredPrepass, + ) + .add_render_graph_node::>( + Core3d, + NodeMeshlet::MainOpaquePass, + ) + .add_render_graph_edges( + Core3d, + ( + NodeMeshlet::VisibilityBufferRasterPass, + NodePbr::EarlyShadowPass, + // + NodeMeshlet::Prepass, + // + NodeMeshlet::DeferredPrepass, + Node3d::EndPrepasses, + // + Node3d::StartMainPass, + NodeMeshlet::MainOpaquePass, + Node3d::MainOpaquePass, + Node3d::EndMainPass, + ), + ) + .insert_resource(InstanceManager::new()) + .add_systems( + RenderStartup, + ( + check_meshlet_features, + ( + (init_resource_manager_system, init_meshlet_pipelines).chain(), + init_meshlet_mesh_manager, + ), + ) + .chain(), + ) + .add_systems(ExtractSchedule, extract_meshlet_mesh_entities) + .add_systems( + Render, + ( + perform_pending_meshlet_mesh_writes.in_set(RenderSystems::PrepareAssets), + configure_meshlet_views + .after(prepare_view_targets) + .in_set(RenderSystems::ManageViews), + prepare_meshlet_per_frame_resources.in_set(RenderSystems::PrepareResources), + prepare_meshlet_view_bind_groups.in_set(RenderSystems::PrepareBindGroups), + queue_material_meshlet_meshes.in_set(RenderSystems::QueueMeshes), + prepare_material_meshlet_meshes_main_opaque_pass + .in_set(RenderSystems::QueueMeshes) + .before(queue_material_meshlet_meshes), + ), + ); + } +} + +fn check_meshlet_features(render_device: Res) { + let features = render_device.features(); + if !features.contains(MeshletPlugin::required_wgpu_features()) { + error!( + "MeshletPlugin can't be used. GPU lacks support for required features: {:?}.", + MeshletPlugin::required_wgpu_features().difference(features) + ); + std::process::exit(1); + } +} + +/// The meshlet mesh equivalent of [`bevy_mesh::Mesh3d`]. +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[reflect(Component, Default, Clone, PartialEq)] +#[require(Transform, PreviousGlobalTransform, Visibility, VisibilityClass)] +#[component(on_add = visibility::add_visibility_class::)] +pub struct MeshletMesh3d(pub Handle); + +impl From for AssetId { + fn from(mesh: MeshletMesh3d) -> Self { + mesh.id() + } +} + +impl From<&MeshletMesh3d> for AssetId { + fn from(mesh: &MeshletMesh3d) -> Self { + mesh.id() + } +} + +fn configure_meshlet_views( + mut views_3d: Query<( + Entity, + &Msaa, + Has, + Has, + Has, + )>, + mut commands: Commands, +) { + for (entity, msaa, normal_prepass, motion_vector_prepass, deferred_prepass) in &mut views_3d { + if *msaa != Msaa::Off { + error!("MeshletPlugin can't be used with MSAA. Add Msaa::Off to your camera to use this plugin."); + std::process::exit(1); + } + + if !(normal_prepass || motion_vector_prepass || deferred_prepass) { + commands + .entity(entity) + .insert(MeshletViewMaterialsMainOpaquePass::default()); + } else { + // TODO: Should we add both Prepass and DeferredGBufferPrepass materials here, and in other systems/nodes? + commands.entity(entity).insert(( + MeshletViewMaterialsMainOpaquePass::default(), + MeshletViewMaterialsPrepass::default(), + MeshletViewMaterialsDeferredGBufferPrepass::default(), + )); + } + } +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/persistent_buffer.rs b/crates/libmarathon/src/render/pbr/meshlet/persistent_buffer.rs new file mode 100644 index 0000000..216c7e7 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/persistent_buffer.rs @@ -0,0 +1,132 @@ +use crate::render::{ + render_resource::{ + BindingResource, Buffer, BufferAddress, BufferDescriptor, BufferUsages, + CommandEncoderDescriptor, COPY_BUFFER_ALIGNMENT, + }, + renderer::{RenderDevice, RenderQueue}, +}; +use core::{num::NonZero, ops::Range}; +use range_alloc::RangeAllocator; + +/// Wrapper for a GPU buffer holding a large amount of data that persists across frames. +pub struct PersistentGpuBuffer { + /// Debug label for the buffer. + label: &'static str, + /// Handle to the GPU buffer. + buffer: Buffer, + /// Tracks free slices of the buffer. + allocation_planner: RangeAllocator, + /// Queue of pending writes, and associated metadata. + write_queue: Vec<(T, T::Metadata, Range)>, +} + +impl PersistentGpuBuffer { + /// Create a new persistent buffer. + pub fn new(label: &'static str, render_device: &RenderDevice) -> Self { + Self { + label, + buffer: render_device.create_buffer(&BufferDescriptor { + label: Some(label), + size: 0, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC, + mapped_at_creation: false, + }), + allocation_planner: RangeAllocator::new(0..0), + write_queue: Vec::new(), + } + } + + /// Queue an item of type T to be added to the buffer, returning the byte range within the buffer that it will be located at. + pub fn queue_write(&mut self, data: T, metadata: T::Metadata) -> Range { + let data_size = data.size_in_bytes() as u64; + debug_assert!(data_size.is_multiple_of(COPY_BUFFER_ALIGNMENT)); + if let Ok(buffer_slice) = self.allocation_planner.allocate_range(data_size) { + self.write_queue + .push((data, metadata, buffer_slice.clone())); + return buffer_slice; + } + + let buffer_size = self.allocation_planner.initial_range(); + let double_buffer_size = (buffer_size.end - buffer_size.start) * 2; + let new_size = double_buffer_size.max(data_size); + self.allocation_planner.grow_to(buffer_size.end + new_size); + + let buffer_slice = self.allocation_planner.allocate_range(data_size).unwrap(); + self.write_queue + .push((data, metadata, buffer_slice.clone())); + buffer_slice + } + + /// Upload all pending data to the GPU buffer. + pub fn perform_writes(&mut self, render_queue: &RenderQueue, render_device: &RenderDevice) { + if self.allocation_planner.initial_range().end > self.buffer.size() { + self.expand_buffer(render_device, render_queue); + } + + let queue_count = self.write_queue.len(); + + for (data, metadata, buffer_slice) in self.write_queue.drain(..) { + let buffer_slice_size = + NonZero::::new(buffer_slice.end - buffer_slice.start).unwrap(); + let mut buffer_view = render_queue + .write_buffer_with(&self.buffer, buffer_slice.start, buffer_slice_size) + .unwrap(); + data.write_bytes_le(metadata, &mut buffer_view, buffer_slice.start); + } + + let queue_saturation = queue_count as f32 / self.write_queue.capacity() as f32; + if queue_saturation < 0.3 { + self.write_queue = Vec::new(); + } + } + + /// Mark a section of the GPU buffer as no longer needed. + pub fn mark_slice_unused(&mut self, buffer_slice: Range) { + self.allocation_planner.free_range(buffer_slice); + } + + pub fn binding(&self) -> BindingResource<'_> { + self.buffer.as_entire_binding() + } + + /// Expand the buffer by creating a new buffer and copying old data over. + fn expand_buffer(&mut self, render_device: &RenderDevice, render_queue: &RenderQueue) { + let size = self.allocation_planner.initial_range(); + let new_buffer = render_device.create_buffer(&BufferDescriptor { + label: Some(self.label), + size: size.end - size.start, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST | BufferUsages::COPY_SRC, + mapped_at_creation: false, + }); + + let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("persistent_gpu_buffer_expand"), + }); + command_encoder.copy_buffer_to_buffer(&self.buffer, 0, &new_buffer, 0, self.buffer.size()); + render_queue.submit([command_encoder.finish()]); + + self.buffer = new_buffer; + } +} + +/// A trait representing data that can be written to a [`PersistentGpuBuffer`]. +pub trait PersistentGpuBufferable { + /// Additional metadata associated with each item, made available during `write_bytes_le`. + type Metadata; + + /// The size in bytes of `self`. This will determine the size of the buffer passed into + /// `write_bytes_le`. + /// + /// All data written must be in a multiple of `wgpu::COPY_BUFFER_ALIGNMENT` bytes. Failure to do so will + /// result in a panic when using [`PersistentGpuBuffer`]. + fn size_in_bytes(&self) -> usize; + + /// Convert `self` + `metadata` into bytes (little-endian), and write to the provided buffer slice. + /// Any bytes not written to in the slice will be zeroed out when uploaded to the GPU. + fn write_bytes_le( + &self, + metadata: Self::Metadata, + buffer_slice: &mut [u8], + buffer_offset: BufferAddress, + ); +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/persistent_buffer_impls.rs b/crates/libmarathon/src/render/pbr/meshlet/persistent_buffer_impls.rs new file mode 100644 index 0000000..19ae015 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/persistent_buffer_impls.rs @@ -0,0 +1,128 @@ +use crate::render::pbr::meshlet::asset::{BvhNode, MeshletCullData}; + +use super::{asset::Meshlet, persistent_buffer::PersistentGpuBufferable}; +use std::sync::Arc; +use bevy_math::Vec2; +use crate::render::render_resource::BufferAddress; + +impl PersistentGpuBufferable for Arc<[BvhNode]> { + type Metadata = u32; + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le( + &self, + base_meshlet_index: Self::Metadata, + buffer_slice: &mut [u8], + buffer_offset: BufferAddress, + ) { + const SIZE: usize = size_of::(); + for (i, &node) in self.iter().enumerate() { + let bytes: [u8; SIZE] = + bytemuck::cast(node.offset_aabbs(base_meshlet_index, buffer_offset)); + buffer_slice[i * SIZE..(i + 1) * SIZE].copy_from_slice(&bytes); + } + } +} + +impl BvhNode { + fn offset_aabbs(mut self, base_meshlet_index: u32, buffer_offset: BufferAddress) -> Self { + let size = size_of::(); + let base_bvh_node_index = (buffer_offset / size as u64) as u32; + for i in 0..self.aabbs.len() { + self.aabbs[i].child_offset += if self.child_is_bvh_node(i) { + base_bvh_node_index + } else { + base_meshlet_index + }; + } + self + } + + fn child_is_bvh_node(&self, i: usize) -> bool { + self.child_counts[i] == u8::MAX + } +} + +impl PersistentGpuBufferable for Arc<[Meshlet]> { + type Metadata = (u64, u64, u64); + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le( + &self, + (vertex_position_offset, vertex_attribute_offset, index_offset): Self::Metadata, + buffer_slice: &mut [u8], + _: BufferAddress, + ) { + let vertex_position_offset = (vertex_position_offset * 8) as u32; + let vertex_attribute_offset = (vertex_attribute_offset as usize / size_of::()) as u32; + let index_offset = index_offset as u32; + + for (i, meshlet) in self.iter().enumerate() { + let size = size_of::(); + let i = i * size; + let bytes = bytemuck::cast::<_, [u8; size_of::()]>(Meshlet { + start_vertex_position_bit: meshlet.start_vertex_position_bit + + vertex_position_offset, + start_vertex_attribute_id: meshlet.start_vertex_attribute_id + + vertex_attribute_offset, + start_index_id: meshlet.start_index_id + index_offset, + ..*meshlet + }); + buffer_slice[i..(i + size)].clone_from_slice(&bytes); + } + } +} + +impl PersistentGpuBufferable for Arc<[MeshletCullData]> { + type Metadata = (); + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { + buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); + } +} + +impl PersistentGpuBufferable for Arc<[u8]> { + type Metadata = (); + + fn size_in_bytes(&self) -> usize { + self.len() + } + + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { + buffer_slice.clone_from_slice(self); + } +} + +impl PersistentGpuBufferable for Arc<[u32]> { + type Metadata = (); + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { + buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); + } +} + +impl PersistentGpuBufferable for Arc<[Vec2]> { + type Metadata = (); + + fn size_in_bytes(&self) -> usize { + self.len() * size_of::() + } + + fn write_bytes_le(&self, _: Self::Metadata, buffer_slice: &mut [u8], _: BufferAddress) { + buffer_slice.clone_from_slice(bytemuck::cast_slice(self)); + } +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/pipelines.rs b/crates/libmarathon/src/render/pbr/meshlet/pipelines.rs new file mode 100644 index 0000000..78a675f --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/pipelines.rs @@ -0,0 +1,580 @@ +use super::resource_manager::ResourceManager; +use bevy_asset::{load_embedded_asset, AssetServer, Handle}; +use crate::render::{ + core_3d::CORE_3D_DEPTH_FORMAT, experimental::mip_generation::DownsampleDepthShader, + FullscreenShader, +}; +use bevy_ecs::{ + resource::Resource, + system::{Commands, Res}, + world::World, +}; +use crate::render::render_resource::*; +use bevy_shader::Shader; +use bevy_utils::default; + +#[derive(Resource)] +pub struct MeshletPipelines { + clear_visibility_buffer: CachedComputePipelineId, + clear_visibility_buffer_shadow_view: CachedComputePipelineId, + first_instance_cull: CachedComputePipelineId, + second_instance_cull: CachedComputePipelineId, + first_bvh_cull: CachedComputePipelineId, + second_bvh_cull: CachedComputePipelineId, + first_meshlet_cull: CachedComputePipelineId, + second_meshlet_cull: CachedComputePipelineId, + downsample_depth_first: CachedComputePipelineId, + downsample_depth_second: CachedComputePipelineId, + downsample_depth_first_shadow_view: CachedComputePipelineId, + downsample_depth_second_shadow_view: CachedComputePipelineId, + visibility_buffer_software_raster: CachedComputePipelineId, + visibility_buffer_software_raster_shadow_view: CachedComputePipelineId, + visibility_buffer_hardware_raster: CachedRenderPipelineId, + visibility_buffer_hardware_raster_shadow_view: CachedRenderPipelineId, + visibility_buffer_hardware_raster_shadow_view_unclipped: CachedRenderPipelineId, + resolve_depth: CachedRenderPipelineId, + resolve_depth_shadow_view: CachedRenderPipelineId, + resolve_material_depth: CachedRenderPipelineId, + remap_1d_to_2d_dispatch: Option, + fill_counts: CachedComputePipelineId, + pub(crate) meshlet_mesh_material: Handle, +} + +pub fn init_meshlet_pipelines( + mut commands: Commands, + resource_manager: Res, + fullscreen_shader: Res, + downsample_depth_shader: Res, + pipeline_cache: Res, + asset_server: Res, +) { + let clear_visibility_buffer_bind_group_layout = resource_manager + .clear_visibility_buffer_bind_group_layout + .clone(); + let clear_visibility_buffer_shadow_view_bind_group_layout = resource_manager + .clear_visibility_buffer_shadow_view_bind_group_layout + .clone(); + let first_instance_cull_bind_group_layout = resource_manager + .first_instance_cull_bind_group_layout + .clone(); + let second_instance_cull_bind_group_layout = resource_manager + .second_instance_cull_bind_group_layout + .clone(); + let first_bvh_cull_bind_group_layout = + resource_manager.first_bvh_cull_bind_group_layout.clone(); + let second_bvh_cull_bind_group_layout = + resource_manager.second_bvh_cull_bind_group_layout.clone(); + let first_meshlet_cull_bind_group_layout = resource_manager + .first_meshlet_cull_bind_group_layout + .clone(); + let second_meshlet_cull_bind_group_layout = resource_manager + .second_meshlet_cull_bind_group_layout + .clone(); + let downsample_depth_layout = resource_manager.downsample_depth_bind_group_layout.clone(); + let downsample_depth_shadow_view_layout = resource_manager + .downsample_depth_shadow_view_bind_group_layout + .clone(); + let visibility_buffer_raster_layout = resource_manager + .visibility_buffer_raster_bind_group_layout + .clone(); + let visibility_buffer_raster_shadow_view_layout = resource_manager + .visibility_buffer_raster_shadow_view_bind_group_layout + .clone(); + let resolve_depth_layout = resource_manager.resolve_depth_bind_group_layout.clone(); + let resolve_depth_shadow_view_layout = resource_manager + .resolve_depth_shadow_view_bind_group_layout + .clone(); + let resolve_material_depth_layout = resource_manager + .resolve_material_depth_bind_group_layout + .clone(); + let remap_1d_to_2d_dispatch_layout = resource_manager + .remap_1d_to_2d_dispatch_bind_group_layout + .clone(); + + let downsample_depth_shader = (*downsample_depth_shader).clone(); + let vertex_state = fullscreen_shader.to_vertex_state(); + let fill_counts_layout = resource_manager.fill_counts_bind_group_layout.clone(); + + let clear_visibility_buffer = + load_embedded_asset!(asset_server.as_ref(), "clear_visibility_buffer.wgsl"); + let cull_instances = load_embedded_asset!(asset_server.as_ref(), "cull_instances.wgsl"); + let cull_bvh = load_embedded_asset!(asset_server.as_ref(), "cull_bvh.wgsl"); + let cull_clusters = load_embedded_asset!(asset_server.as_ref(), "cull_clusters.wgsl"); + let visibility_buffer_software_raster = load_embedded_asset!( + asset_server.as_ref(), + "visibility_buffer_software_raster.wgsl" + ); + let visibility_buffer_hardware_raster = load_embedded_asset!( + asset_server.as_ref(), + "visibility_buffer_hardware_raster.wgsl" + ); + let resolve_render_targets = + load_embedded_asset!(asset_server.as_ref(), "resolve_render_targets.wgsl"); + let remap_1d_to_2d_dispatch = + load_embedded_asset!(asset_server.as_ref(), "remap_1d_to_2d_dispatch.wgsl"); + let fill_counts = load_embedded_asset!(asset_server.as_ref(), "fill_counts.wgsl"); + let meshlet_mesh_material = + load_embedded_asset!(asset_server.as_ref(), "meshlet_mesh_material.wgsl"); + + commands.insert_resource(MeshletPipelines { + clear_visibility_buffer: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_clear_visibility_buffer_pipeline".into()), + layout: vec![clear_visibility_buffer_bind_group_layout], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: clear_visibility_buffer.clone(), + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], + ..default() + }), + + clear_visibility_buffer_shadow_view: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_clear_visibility_buffer_shadow_view_pipeline".into()), + layout: vec![clear_visibility_buffer_shadow_view_bind_group_layout], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: clear_visibility_buffer, + ..default() + }, + ), + + first_instance_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_first_instance_cull_pipeline".into()), + layout: vec![first_instance_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_instances.clone(), + shader_defs: vec![ + "MESHLET_INSTANCE_CULLING_PASS".into(), + "MESHLET_FIRST_CULLING_PASS".into(), + ], + ..default() + }), + + second_instance_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_second_instance_cull_pipeline".into()), + layout: vec![second_instance_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_instances, + shader_defs: vec![ + "MESHLET_INSTANCE_CULLING_PASS".into(), + "MESHLET_SECOND_CULLING_PASS".into(), + ], + ..default() + }), + + first_bvh_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_first_bvh_cull_pipeline".into()), + layout: vec![first_bvh_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: cull_bvh.clone(), + shader_defs: vec![ + "MESHLET_BVH_CULLING_PASS".into(), + "MESHLET_FIRST_CULLING_PASS".into(), + ], + ..default() + }), + + second_bvh_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_second_bvh_cull_pipeline".into()), + layout: vec![second_bvh_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: cull_bvh, + shader_defs: vec![ + "MESHLET_BVH_CULLING_PASS".into(), + "MESHLET_SECOND_CULLING_PASS".into(), + ], + ..default() + }), + + first_meshlet_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_first_meshlet_cull_pipeline".into()), + layout: vec![first_meshlet_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_clusters.clone(), + shader_defs: vec![ + "MESHLET_CLUSTER_CULLING_PASS".into(), + "MESHLET_FIRST_CULLING_PASS".into(), + ], + ..default() + }), + + second_meshlet_cull: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_second_meshlet_cull_pipeline".into()), + layout: vec![second_meshlet_cull_bind_group_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: cull_clusters, + shader_defs: vec![ + "MESHLET_CLUSTER_CULLING_PASS".into(), + "MESHLET_SECOND_CULLING_PASS".into(), + ], + ..default() + }), + + downsample_depth_first: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_downsample_depth_first_pipeline".into()), + layout: vec![downsample_depth_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: downsample_depth_shader.clone(), + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), + "MESHLET".into(), + ], + entry_point: Some("downsample_depth_first".into()), + ..default() + }), + + downsample_depth_second: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_downsample_depth_second_pipeline".into()), + layout: vec![downsample_depth_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: downsample_depth_shader.clone(), + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), + "MESHLET".into(), + ], + entry_point: Some("downsample_depth_second".into()), + ..default() + }), + + downsample_depth_first_shadow_view: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_downsample_depth_first_pipeline".into()), + layout: vec![downsample_depth_shadow_view_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: downsample_depth_shader.clone(), + shader_defs: vec!["MESHLET".into()], + entry_point: Some("downsample_depth_first".into()), + ..default() + }, + ), + + downsample_depth_second_shadow_view: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_downsample_depth_second_pipeline".into()), + layout: vec![downsample_depth_shadow_view_layout], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: downsample_depth_shader, + shader_defs: vec!["MESHLET".into()], + entry_point: Some("downsample_depth_second".into()), + zero_initialize_workgroup_memory: false, + }, + ), + + visibility_buffer_software_raster: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some("meshlet_visibility_buffer_software_raster_pipeline".into()), + layout: vec![visibility_buffer_raster_layout.clone()], + push_constant_ranges: vec![], + shader: visibility_buffer_software_raster.clone(), + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), + if remap_1d_to_2d_dispatch_layout.is_some() { + "MESHLET_2D_DISPATCH" + } else { + "" + } + .into(), + ], + ..default() + }, + ), + + visibility_buffer_software_raster_shadow_view: pipeline_cache.queue_compute_pipeline( + ComputePipelineDescriptor { + label: Some( + "meshlet_visibility_buffer_software_raster_shadow_view_pipeline".into(), + ), + layout: vec![visibility_buffer_raster_shadow_view_layout.clone()], + push_constant_ranges: vec![], + shader: visibility_buffer_software_raster, + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + if remap_1d_to_2d_dispatch_layout.is_some() { + "MESHLET_2D_DISPATCH" + } else { + "" + } + .into(), + ], + ..default() + }, + ), + + visibility_buffer_hardware_raster: pipeline_cache.queue_render_pipeline( + RenderPipelineDescriptor { + label: Some("meshlet_visibility_buffer_hardware_raster_pipeline".into()), + layout: vec![visibility_buffer_raster_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::VERTEX, + range: 0..4, + }], + vertex: VertexState { + shader: visibility_buffer_hardware_raster.clone(), + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), + ], + ..default() + }, + fragment: Some(FragmentState { + shader: visibility_buffer_hardware_raster.clone(), + shader_defs: vec![ + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into(), + "MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into(), + ], + targets: vec![Some(ColorTargetState { + format: TextureFormat::R8Uint, + blend: None, + write_mask: ColorWrites::empty(), + })], + ..default() + }), + ..default() + }, + ), + + visibility_buffer_hardware_raster_shadow_view: pipeline_cache.queue_render_pipeline( + RenderPipelineDescriptor { + label: Some( + "meshlet_visibility_buffer_hardware_raster_shadow_view_pipeline".into(), + ), + layout: vec![visibility_buffer_raster_shadow_view_layout.clone()], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::VERTEX, + range: 0..4, + }], + vertex: VertexState { + shader: visibility_buffer_hardware_raster.clone(), + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], + ..default() + }, + fragment: Some(FragmentState { + shader: visibility_buffer_hardware_raster.clone(), + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], + targets: vec![Some(ColorTargetState { + format: TextureFormat::R8Uint, + blend: None, + write_mask: ColorWrites::empty(), + })], + ..default() + }), + ..default() + }, + ), + + visibility_buffer_hardware_raster_shadow_view_unclipped: pipeline_cache + .queue_render_pipeline(RenderPipelineDescriptor { + label: Some( + "meshlet_visibility_buffer_hardware_raster_shadow_view_unclipped_pipeline" + .into(), + ), + layout: vec![visibility_buffer_raster_shadow_view_layout], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::VERTEX, + range: 0..4, + }], + vertex: VertexState { + shader: visibility_buffer_hardware_raster.clone(), + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], + ..default() + }, + fragment: Some(FragmentState { + shader: visibility_buffer_hardware_raster, + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS".into()], + targets: vec![Some(ColorTargetState { + format: TextureFormat::R8Uint, + blend: None, + write_mask: ColorWrites::empty(), + })], + ..default() + }), + ..default() + }), + + resolve_depth: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("meshlet_resolve_depth_pipeline".into()), + layout: vec![resolve_depth_layout], + vertex: vertex_state.clone(), + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::Always, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + fragment: Some(FragmentState { + shader: resolve_render_targets.clone(), + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], + entry_point: Some("resolve_depth".into()), + ..default() + }), + ..default() + }), + + resolve_depth_shadow_view: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("meshlet_resolve_depth_pipeline".into()), + layout: vec![resolve_depth_shadow_view_layout], + vertex: vertex_state.clone(), + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::Always, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + fragment: Some(FragmentState { + shader: resolve_render_targets.clone(), + entry_point: Some("resolve_depth".into()), + ..default() + }), + ..default() + }), + + resolve_material_depth: pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { + label: Some("meshlet_resolve_material_depth_pipeline".into()), + layout: vec![resolve_material_depth_layout], + vertex: vertex_state, + primitive: PrimitiveState::default(), + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth16Unorm, + depth_write_enabled: true, + depth_compare: CompareFunction::Always, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + fragment: Some(FragmentState { + shader: resolve_render_targets, + shader_defs: vec!["MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT".into()], + entry_point: Some("resolve_material_depth".into()), + targets: vec![], + }), + ..default() + }), + + fill_counts: pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_fill_counts_pipeline".into()), + layout: vec![fill_counts_layout], + shader: fill_counts, + shader_defs: vec![if remap_1d_to_2d_dispatch_layout.is_some() { + "MESHLET_2D_DISPATCH" + } else { + "" + } + .into()], + ..default() + }), + + remap_1d_to_2d_dispatch: remap_1d_to_2d_dispatch_layout.map(|layout| { + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("meshlet_remap_1d_to_2d_dispatch_pipeline".into()), + layout: vec![layout], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }], + shader: remap_1d_to_2d_dispatch, + ..default() + }) + }), + + meshlet_mesh_material, + }); +} + +impl MeshletPipelines { + pub fn get( + world: &World, + ) -> Option<( + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &ComputePipeline, + &RenderPipeline, + &RenderPipeline, + &RenderPipeline, + &RenderPipeline, + &RenderPipeline, + &RenderPipeline, + Option<&ComputePipeline>, + &ComputePipeline, + )> { + let pipeline_cache = world.get_resource::()?; + let pipeline = world.get_resource::()?; + Some(( + pipeline_cache.get_compute_pipeline(pipeline.clear_visibility_buffer)?, + pipeline_cache.get_compute_pipeline(pipeline.clear_visibility_buffer_shadow_view)?, + pipeline_cache.get_compute_pipeline(pipeline.first_instance_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.second_instance_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.first_bvh_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.second_bvh_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.first_meshlet_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.second_meshlet_cull)?, + pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_first)?, + pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_second)?, + pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_first_shadow_view)?, + pipeline_cache.get_compute_pipeline(pipeline.downsample_depth_second_shadow_view)?, + pipeline_cache.get_compute_pipeline(pipeline.visibility_buffer_software_raster)?, + pipeline_cache + .get_compute_pipeline(pipeline.visibility_buffer_software_raster_shadow_view)?, + pipeline_cache.get_render_pipeline(pipeline.visibility_buffer_hardware_raster)?, + pipeline_cache + .get_render_pipeline(pipeline.visibility_buffer_hardware_raster_shadow_view)?, + pipeline_cache.get_render_pipeline( + pipeline.visibility_buffer_hardware_raster_shadow_view_unclipped, + )?, + pipeline_cache.get_render_pipeline(pipeline.resolve_depth)?, + pipeline_cache.get_render_pipeline(pipeline.resolve_depth_shadow_view)?, + pipeline_cache.get_render_pipeline(pipeline.resolve_material_depth)?, + match pipeline.remap_1d_to_2d_dispatch { + Some(id) => Some(pipeline_cache.get_compute_pipeline(id)?), + None => None, + }, + pipeline_cache.get_compute_pipeline(pipeline.fill_counts)?, + )) + } +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/remap_1d_to_2d_dispatch.wgsl b/crates/libmarathon/src/render/pbr/meshlet/remap_1d_to_2d_dispatch.wgsl new file mode 100644 index 0000000..b9970c4 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/remap_1d_to_2d_dispatch.wgsl @@ -0,0 +1,24 @@ +/// Remaps an indirect 1d to 2d dispatch for devices with low dispatch size limit. + +struct DispatchIndirectArgs { + x: u32, + y: u32, + z: u32, +} + +@group(0) @binding(0) var meshlet_software_raster_indirect_args: DispatchIndirectArgs; +@group(0) @binding(1) var meshlet_software_raster_cluster_count: u32; +var max_compute_workgroups_per_dimension: u32; + +@compute +@workgroup_size(1, 1, 1) +fn remap_dispatch() { + let cluster_count = meshlet_software_raster_indirect_args.x; + + if cluster_count > max_compute_workgroups_per_dimension { + let n = u32(ceil(sqrt(f32(cluster_count)))); + meshlet_software_raster_indirect_args.x = n; + meshlet_software_raster_indirect_args.y = n; + meshlet_software_raster_cluster_count = cluster_count; + } +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/resolve_render_targets.wgsl b/crates/libmarathon/src/render/pbr/meshlet/resolve_render_targets.wgsl new file mode 100644 index 0000000..6fef0cc --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/resolve_render_targets.wgsl @@ -0,0 +1,41 @@ +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_pbr::meshlet_bindings::InstancedOffset + +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; +#else +@group(0) @binding(0) var meshlet_visibility_buffer: texture_storage_2d; +#endif +@group(0) @binding(1) var meshlet_raster_clusters: array; // Per cluster +@group(0) @binding(2) var meshlet_instance_material_ids: array; // Per entity instance + +/// This pass writes out the depth texture. +@fragment +fn resolve_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f32 { + let visibility = textureLoad(meshlet_visibility_buffer, vec2(in.position.xy)).r; +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + let depth = u32(visibility >> 32u); +#else + let depth = visibility; +#endif + + if depth == 0u { discard; } + + return bitcast(depth); +} + +/// This pass writes out the material depth texture. +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT +@fragment +fn resolve_material_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f32 { + let visibility = textureLoad(meshlet_visibility_buffer, vec2(in.position.xy)).r; + + let depth = visibility >> 32u; + if depth == 0lu { discard; } + + let cluster_id = u32(visibility) >> 7u; + let instance_id = meshlet_raster_clusters[cluster_id].instance_id; + let material_id = meshlet_instance_material_ids[instance_id]; + return f32(material_id) / 65535.0; +} +#endif diff --git a/crates/libmarathon/src/render/pbr/meshlet/resource_manager.rs b/crates/libmarathon/src/render/pbr/meshlet/resource_manager.rs new file mode 100644 index 0000000..9785d04 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/resource_manager.rs @@ -0,0 +1,1224 @@ +use super::{instance_manager::InstanceManager, meshlet_mesh_manager::MeshletMeshManager}; +use crate::render::pbr::ShadowView; +use bevy_camera::{visibility::RenderLayers, Camera3d}; +use crate::render::{ + experimental::mip_generation::{self, ViewDepthPyramid}, + prepass::{PreviousViewData, PreviousViewUniforms}, +}; +use bevy_ecs::{ + component::Component, + entity::{Entity, EntityHashMap}, + query::AnyOf, + resource::Resource, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_image::ToExtents; +use bevy_math::{UVec2, Vec4Swizzles}; +use crate::render::{ + render_resource::*, + renderer::{RenderDevice, RenderQueue}, + texture::{CachedTexture, TextureCache}, + view::{ExtractedView, ViewUniform, ViewUniforms}, +}; +use binding_types::*; +use core::iter; + +/// Manages per-view and per-cluster GPU resources for [`super::MeshletPlugin`]. +#[derive(Resource)] +pub struct ResourceManager { + /// Intermediate buffer of cluster IDs for use with rasterizing the visibility buffer + visibility_buffer_raster_clusters: Buffer, + /// Intermediate buffer of previous counts of clusters in rasterizer buckets + pub visibility_buffer_raster_cluster_prev_counts: Buffer, + /// Intermediate buffer of count of clusters to software rasterize + software_raster_cluster_count: Buffer, + /// BVH traversal queues + bvh_traversal_queues: [Buffer; 2], + /// Cluster cull candidate queue + cluster_cull_candidate_queue: Buffer, + /// Rightmost slot index of [`Self::visibility_buffer_raster_clusters`], [`Self::bvh_traversal_queues`], and [`Self::cluster_cull_candidate_queue`] + cull_queue_rightmost_slot: u32, + + /// Second pass instance candidates + second_pass_candidates: Option, + /// Sampler for a depth pyramid + depth_pyramid_sampler: Sampler, + /// Dummy texture view for binding depth pyramids with less than the maximum amount of mips + depth_pyramid_dummy_texture: TextureView, + + // TODO + previous_depth_pyramids: EntityHashMap, + + // Bind group layouts + pub clear_visibility_buffer_bind_group_layout: BindGroupLayout, + pub clear_visibility_buffer_shadow_view_bind_group_layout: BindGroupLayout, + pub first_instance_cull_bind_group_layout: BindGroupLayout, + pub second_instance_cull_bind_group_layout: BindGroupLayout, + pub first_bvh_cull_bind_group_layout: BindGroupLayout, + pub second_bvh_cull_bind_group_layout: BindGroupLayout, + pub first_meshlet_cull_bind_group_layout: BindGroupLayout, + pub second_meshlet_cull_bind_group_layout: BindGroupLayout, + pub visibility_buffer_raster_bind_group_layout: BindGroupLayout, + pub visibility_buffer_raster_shadow_view_bind_group_layout: BindGroupLayout, + pub downsample_depth_bind_group_layout: BindGroupLayout, + pub downsample_depth_shadow_view_bind_group_layout: BindGroupLayout, + pub resolve_depth_bind_group_layout: BindGroupLayout, + pub resolve_depth_shadow_view_bind_group_layout: BindGroupLayout, + pub resolve_material_depth_bind_group_layout: BindGroupLayout, + pub material_shade_bind_group_layout: BindGroupLayout, + pub fill_counts_bind_group_layout: BindGroupLayout, + pub remap_1d_to_2d_dispatch_bind_group_layout: Option, +} + +impl ResourceManager { + pub fn new(cluster_buffer_slots: u32, render_device: &RenderDevice) -> Self { + let needs_dispatch_remap = + cluster_buffer_slots > render_device.limits().max_compute_workgroups_per_dimension; + // The IDs are a (u32, u32) of instance and index. + let cull_queue_size = 2 * cluster_buffer_slots as u64 * size_of::() as u64; + + Self { + visibility_buffer_raster_clusters: render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_visibility_buffer_raster_clusters"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + visibility_buffer_raster_cluster_prev_counts: render_device.create_buffer( + &BufferDescriptor { + label: Some("meshlet_visibility_buffer_raster_cluster_prev_counts"), + size: size_of::() as u64 * 2, + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + mapped_at_creation: false, + }, + ), + software_raster_cluster_count: render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_software_raster_cluster_count"), + size: size_of::() as u64, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + bvh_traversal_queues: [ + render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_bvh_traversal_queue_0"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_bvh_traversal_queue_1"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + ], + cluster_cull_candidate_queue: render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_cluster_cull_candidate_queue"), + size: cull_queue_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }), + cull_queue_rightmost_slot: cluster_buffer_slots - 1, + + second_pass_candidates: None, + depth_pyramid_sampler: render_device.create_sampler(&SamplerDescriptor { + label: Some("meshlet_depth_pyramid_sampler"), + ..SamplerDescriptor::default() + }), + depth_pyramid_dummy_texture: mip_generation::create_depth_pyramid_dummy_texture( + render_device, + "meshlet_depth_pyramid_dummy_texture", + "meshlet_depth_pyramid_dummy_texture_view", + ), + + previous_depth_pyramids: EntityHashMap::default(), + + // TODO: Buffer min sizes + clear_visibility_buffer_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_clear_visibility_buffer_bind_group_layout", + &BindGroupLayoutEntries::single( + ShaderStages::COMPUTE, + texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::WriteOnly), + ), + ), + clear_visibility_buffer_shadow_view_bind_group_layout: render_device + .create_bind_group_layout( + "meshlet_clear_visibility_buffer_shadow_view_bind_group_layout", + &BindGroupLayoutEntries::single( + ShaderStages::COMPUTE, + texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly), + ), + ), + first_instance_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_first_instance_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + second_instance_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_second_instance_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + first_bvh_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_first_bvh_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + second_bvh_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_second_bvh_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + first_meshlet_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_first_meshlet_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ), + second_meshlet_cull_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_second_meshlet_culling_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + uniform_buffer::(true), + uniform_buffer::(true), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + downsample_depth_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_downsample_depth_bind_group_layout", + &BindGroupLayoutEntries::sequential(ShaderStages::COMPUTE, { + let write_only_r32float = || { + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly) + }; + ( + texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::ReadOnly), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + texture_storage_2d( + TextureFormat::R32Float, + StorageTextureAccess::ReadWrite, + ), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + sampler(SamplerBindingType::NonFiltering), + ) + }), + ), + downsample_depth_shadow_view_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_downsample_depth_shadow_view_bind_group_layout", + &BindGroupLayoutEntries::sequential(ShaderStages::COMPUTE, { + let write_only_r32float = || { + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly) + }; + ( + texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::ReadOnly), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + texture_storage_2d( + TextureFormat::R32Float, + StorageTextureAccess::ReadWrite, + ), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + write_only_r32float(), + sampler(SamplerBindingType::NonFiltering), + ) + }), + ), + visibility_buffer_raster_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_visibility_buffer_raster_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ( + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::Atomic), + uniform_buffer::(true), + ), + ), + ), + visibility_buffer_raster_shadow_view_bind_group_layout: render_device + .create_bind_group_layout( + "meshlet_visibility_buffer_raster_shadow_view_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ( + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + texture_storage_2d( + TextureFormat::R32Uint, + StorageTextureAccess::Atomic, + ), + uniform_buffer::(true), + ), + ), + ), + resolve_depth_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_resolve_depth_bind_group_layout", + &BindGroupLayoutEntries::single( + ShaderStages::FRAGMENT, + texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::ReadOnly), + ), + ), + resolve_depth_shadow_view_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_resolve_depth_shadow_view_bind_group_layout", + &BindGroupLayoutEntries::single( + ShaderStages::FRAGMENT, + texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::ReadOnly), + ), + ), + resolve_material_depth_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_resolve_material_depth_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::ReadOnly), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + material_shade_bind_group_layout: render_device.create_bind_group_layout( + "meshlet_mesh_material_shade_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_storage_2d(TextureFormat::R64Uint, StorageTextureAccess::ReadOnly), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + fill_counts_bind_group_layout: if needs_dispatch_remap { + render_device.create_bind_group_layout( + "meshlet_fill_counts_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ) + } else { + render_device.create_bind_group_layout( + "meshlet_fill_counts_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ) + }, + remap_1d_to_2d_dispatch_bind_group_layout: needs_dispatch_remap.then(|| { + render_device.create_bind_group_layout( + "meshlet_remap_1d_to_2d_dispatch_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + ), + ), + ) + }), + } + } +} + +// ------------ TODO: Everything under here needs to be rewritten and cached ------------ + +#[derive(Component)] +pub struct MeshletViewResources { + pub scene_instance_count: u32, + pub rightmost_slot: u32, + pub max_bvh_depth: u32, + instance_visibility: Buffer, + pub dummy_render_target: CachedTexture, + pub visibility_buffer: CachedTexture, + pub second_pass_count: Buffer, + pub second_pass_dispatch: Buffer, + pub second_pass_candidates: Buffer, + pub first_bvh_cull_count_front: Buffer, + pub first_bvh_cull_dispatch_front: Buffer, + pub first_bvh_cull_count_back: Buffer, + pub first_bvh_cull_dispatch_back: Buffer, + pub first_bvh_cull_queue: Buffer, + pub second_bvh_cull_count_front: Buffer, + pub second_bvh_cull_dispatch_front: Buffer, + pub second_bvh_cull_count_back: Buffer, + pub second_bvh_cull_dispatch_back: Buffer, + pub second_bvh_cull_queue: Buffer, + pub front_meshlet_cull_count: Buffer, + pub front_meshlet_cull_dispatch: Buffer, + pub back_meshlet_cull_count: Buffer, + pub back_meshlet_cull_dispatch: Buffer, + pub meshlet_cull_queue: Buffer, + pub visibility_buffer_software_raster_indirect_args: Buffer, + pub visibility_buffer_hardware_raster_indirect_args: Buffer, + pub depth_pyramid: ViewDepthPyramid, + previous_depth_pyramid: TextureView, + pub material_depth: Option, + pub view_size: UVec2, + not_shadow_view: bool, +} + +#[derive(Component)] +pub struct MeshletViewBindGroups { + pub clear_visibility_buffer: BindGroup, + pub first_instance_cull: BindGroup, + pub second_instance_cull: BindGroup, + pub first_bvh_cull_ping: BindGroup, + pub first_bvh_cull_pong: BindGroup, + pub second_bvh_cull_ping: BindGroup, + pub second_bvh_cull_pong: BindGroup, + pub first_meshlet_cull: BindGroup, + pub second_meshlet_cull: BindGroup, + pub downsample_depth: BindGroup, + pub visibility_buffer_raster: BindGroup, + pub resolve_depth: BindGroup, + pub resolve_material_depth: Option, + pub material_shade: Option, + pub remap_1d_to_2d_dispatch: Option, + pub fill_counts: BindGroup, +} + +// TODO: Cache things per-view and skip running this system / optimize this system +pub fn prepare_meshlet_per_frame_resources( + mut resource_manager: ResMut, + mut instance_manager: ResMut, + views: Query<( + Entity, + &ExtractedView, + Option<&RenderLayers>, + AnyOf<(&Camera3d, &ShadowView)>, + )>, + mut texture_cache: ResMut, + render_queue: Res, + render_device: Res, + mut commands: Commands, +) { + if instance_manager.scene_instance_count == 0 { + return; + } + + let instance_manager = instance_manager.as_mut(); + + // TODO: Move this and the submit to a separate system and remove pub from the fields + instance_manager + .instance_uniforms + .write_buffer(&render_device, &render_queue); + instance_manager + .instance_aabbs + .write_buffer(&render_device, &render_queue); + instance_manager + .instance_material_ids + .write_buffer(&render_device, &render_queue); + instance_manager + .instance_bvh_root_nodes + .write_buffer(&render_device, &render_queue); + + let needed_buffer_size = 4 * instance_manager.scene_instance_count as u64; + let second_pass_candidates = match &mut resource_manager.second_pass_candidates { + Some(buffer) if buffer.size() >= needed_buffer_size => buffer.clone(), + slot => { + let buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("meshlet_second_pass_candidates"), + size: needed_buffer_size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + *slot = Some(buffer.clone()); + buffer + } + }; + + for (view_entity, view, render_layers, (_, shadow_view)) in &views { + let not_shadow_view = shadow_view.is_none(); + + let instance_visibility = instance_manager + .view_instance_visibility + .entry(view_entity) + .or_insert_with(|| { + let mut buffer = StorageBuffer::default(); + buffer.set_label(Some("meshlet_view_instance_visibility")); + buffer + }); + for (instance_index, (_, layers, not_shadow_caster)) in + instance_manager.instances.iter().enumerate() + { + // If either the layers don't match the view's layers or this is a shadow view + // and the instance is not a shadow caster, hide the instance for this view + if !render_layers + .unwrap_or(&RenderLayers::default()) + .intersects(layers) + || (shadow_view.is_some() && *not_shadow_caster) + { + let vec = instance_visibility.get_mut(); + let index = instance_index / 32; + let bit = instance_index - index * 32; + if vec.len() <= index { + vec.extend(iter::repeat_n(0, index - vec.len() + 1)); + } + vec[index] |= 1 << bit; + } + } + instance_visibility.write_buffer(&render_device, &render_queue); + let instance_visibility = instance_visibility.buffer().unwrap().clone(); + + // TODO: Remove this once wgpu allows render passes with no attachments + let dummy_render_target = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("meshlet_dummy_render_target"), + size: view.viewport.zw().to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R8Uint, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }, + ); + + let visibility_buffer = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("meshlet_visibility_buffer"), + size: view.viewport.zw().to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: if not_shadow_view { + TextureFormat::R64Uint + } else { + TextureFormat::R32Uint + }, + usage: TextureUsages::STORAGE_ATOMIC | TextureUsages::STORAGE_BINDING, + view_formats: &[], + }, + ); + + let second_pass_count = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_pass_count"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE, + }); + let second_pass_dispatch = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_pass_dispatch"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + + let first_bvh_cull_count_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_count_front"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let first_bvh_cull_dispatch_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_dispatch_front"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + let first_bvh_cull_count_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_count_back"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let first_bvh_cull_dispatch_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_first_bvh_cull_dispatch_back"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + + let second_bvh_cull_count_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_count_front"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let second_bvh_cull_dispatch_front = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_dispatch_front"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + let second_bvh_cull_count_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_count_back"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE | BufferUsages::COPY_DST, + }); + let second_bvh_cull_dispatch_back = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_second_bvh_cull_dispatch_back"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT | BufferUsages::COPY_DST, + }); + + let front_meshlet_cull_count = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_front_meshlet_cull_count"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE, + }); + let front_meshlet_cull_dispatch = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_front_meshlet_cull_dispatch"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + let back_meshlet_cull_count = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_back_meshlet_cull_count"), + contents: bytemuck::bytes_of(&0u32), + usage: BufferUsages::STORAGE, + }); + let back_meshlet_cull_dispatch = + render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_back_meshlet_cull_dispatch"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + + let visibility_buffer_software_raster_indirect_args = render_device + .create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_visibility_buffer_software_raster_indirect_args"), + contents: DispatchIndirectArgs { x: 0, y: 1, z: 1 }.as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + + let visibility_buffer_hardware_raster_indirect_args = render_device + .create_buffer_with_data(&BufferInitDescriptor { + label: Some("meshlet_visibility_buffer_hardware_raster_indirect_args"), + contents: DrawIndirectArgs { + vertex_count: 128 * 3, + instance_count: 0, + first_vertex: 0, + first_instance: 0, + } + .as_bytes(), + usage: BufferUsages::STORAGE | BufferUsages::INDIRECT, + }); + + let depth_pyramid = ViewDepthPyramid::new( + &render_device, + &mut texture_cache, + &resource_manager.depth_pyramid_dummy_texture, + view.viewport.zw(), + "meshlet_depth_pyramid", + "meshlet_depth_pyramid_texture_view", + ); + + let previous_depth_pyramid = + match resource_manager.previous_depth_pyramids.get(&view_entity) { + Some(texture_view) => texture_view.clone(), + None => depth_pyramid.all_mips.clone(), + }; + resource_manager + .previous_depth_pyramids + .insert(view_entity, depth_pyramid.all_mips.clone()); + + let material_depth = TextureDescriptor { + label: Some("meshlet_material_depth"), + size: view.viewport.zw().to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Depth16Unorm, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }; + + commands.entity(view_entity).insert(MeshletViewResources { + scene_instance_count: instance_manager.scene_instance_count, + rightmost_slot: resource_manager.cull_queue_rightmost_slot, + max_bvh_depth: instance_manager.max_bvh_depth, + instance_visibility, + dummy_render_target, + visibility_buffer, + second_pass_count, + second_pass_dispatch, + second_pass_candidates: second_pass_candidates.clone(), + first_bvh_cull_count_front, + first_bvh_cull_dispatch_front, + first_bvh_cull_count_back, + first_bvh_cull_dispatch_back, + first_bvh_cull_queue: resource_manager.bvh_traversal_queues[0].clone(), + second_bvh_cull_count_front, + second_bvh_cull_dispatch_front, + second_bvh_cull_count_back, + second_bvh_cull_dispatch_back, + second_bvh_cull_queue: resource_manager.bvh_traversal_queues[1].clone(), + front_meshlet_cull_count, + front_meshlet_cull_dispatch, + back_meshlet_cull_count, + back_meshlet_cull_dispatch, + meshlet_cull_queue: resource_manager.cluster_cull_candidate_queue.clone(), + visibility_buffer_software_raster_indirect_args, + visibility_buffer_hardware_raster_indirect_args, + depth_pyramid, + previous_depth_pyramid, + material_depth: not_shadow_view + .then(|| texture_cache.get(&render_device, material_depth)), + view_size: view.viewport.zw(), + not_shadow_view, + }); + } +} + +pub fn prepare_meshlet_view_bind_groups( + meshlet_mesh_manager: Res, + resource_manager: Res, + instance_manager: Res, + views: Query<(Entity, &MeshletViewResources)>, + view_uniforms: Res, + previous_view_uniforms: Res, + render_device: Res, + mut commands: Commands, +) { + let (Some(view_uniforms), Some(previous_view_uniforms)) = ( + view_uniforms.uniforms.binding(), + previous_view_uniforms.uniforms.binding(), + ) else { + return; + }; + + // TODO: Some of these bind groups can be reused across multiple views + for (view_entity, view_resources) in &views { + let clear_visibility_buffer = render_device.create_bind_group( + "meshlet_clear_visibility_buffer_bind_group", + if view_resources.not_shadow_view { + &resource_manager.clear_visibility_buffer_bind_group_layout + } else { + &resource_manager.clear_visibility_buffer_shadow_view_bind_group_layout + }, + &BindGroupEntries::single(&view_resources.visibility_buffer.default_view), + ); + + let first_instance_cull = render_device.create_bind_group( + "meshlet_first_instance_cull_bind_group", + &resource_manager.first_instance_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources.instance_visibility.as_entire_binding(), + instance_manager.instance_aabbs.binding().unwrap(), + instance_manager.instance_bvh_root_nodes.binding().unwrap(), + view_resources + .first_bvh_cull_count_front + .as_entire_binding(), + view_resources + .first_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.first_bvh_cull_queue.as_entire_binding(), + view_resources.second_pass_count.as_entire_binding(), + view_resources.second_pass_dispatch.as_entire_binding(), + view_resources.second_pass_candidates.as_entire_binding(), + )), + ); + + let second_instance_cull = render_device.create_bind_group( + "meshlet_second_instance_cull_bind_group", + &resource_manager.second_instance_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources.instance_visibility.as_entire_binding(), + instance_manager.instance_aabbs.binding().unwrap(), + instance_manager.instance_bvh_root_nodes.binding().unwrap(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + view_resources.second_pass_count.as_entire_binding(), + view_resources.second_pass_candidates.as_entire_binding(), + )), + ); + + let first_bvh_cull_ping = render_device.create_bind_group( + "meshlet_first_bvh_cull_ping_bind_group", + &resource_manager.first_bvh_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .first_bvh_cull_count_front + .as_entire_binding(), + view_resources.first_bvh_cull_count_back.as_entire_binding(), + view_resources + .first_bvh_cull_dispatch_back + .as_entire_binding(), + view_resources.first_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + )), + ); + + let first_bvh_cull_pong = render_device.create_bind_group( + "meshlet_first_bvh_cull_pong_bind_group", + &resource_manager.first_bvh_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources.first_bvh_cull_count_back.as_entire_binding(), + view_resources + .first_bvh_cull_count_front + .as_entire_binding(), + view_resources + .first_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.first_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + )), + ); + + let second_bvh_cull_ping = render_device.create_bind_group( + "meshlet_second_bvh_cull_ping_bind_group", + &resource_manager.second_bvh_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_count_back + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_back + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let second_bvh_cull_pong = render_device.create_bind_group( + "meshlet_second_bvh_cull_pong_bind_group", + &resource_manager.second_bvh_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.bvh_nodes.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .second_bvh_cull_count_back + .as_entire_binding(), + view_resources + .second_bvh_cull_count_front + .as_entire_binding(), + view_resources + .second_bvh_cull_dispatch_front + .as_entire_binding(), + view_resources.second_bvh_cull_queue.as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .front_meshlet_cull_dispatch + .as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let first_meshlet_cull = render_device.create_bind_group( + "meshlet_first_meshlet_cull_bind_group", + &resource_manager.first_meshlet_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.meshlet_cull_data.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + view_resources.front_meshlet_cull_count.as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources + .back_meshlet_cull_dispatch + .as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let second_meshlet_cull = render_device.create_bind_group( + "meshlet_second_meshlet_cull_bind_group", + &resource_manager.second_meshlet_cull_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.previous_depth_pyramid, + view_uniforms.clone(), + previous_view_uniforms.clone(), + meshlet_mesh_manager.meshlet_cull_data.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + view_resources.back_meshlet_cull_count.as_entire_binding(), + view_resources.meshlet_cull_queue.as_entire_binding(), + )), + ); + + let downsample_depth = view_resources.depth_pyramid.create_bind_group( + &render_device, + "meshlet_downsample_depth_bind_group", + if view_resources.not_shadow_view { + &resource_manager.downsample_depth_bind_group_layout + } else { + &resource_manager.downsample_depth_shadow_view_bind_group_layout + }, + &view_resources.visibility_buffer.default_view, + &resource_manager.depth_pyramid_sampler, + ); + + let visibility_buffer_raster = render_device.create_bind_group( + "meshlet_visibility_raster_buffer_bind_group", + if view_resources.not_shadow_view { + &resource_manager.visibility_buffer_raster_bind_group_layout + } else { + &resource_manager.visibility_buffer_raster_shadow_view_bind_group_layout + }, + &BindGroupEntries::sequential(( + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + meshlet_mesh_manager.meshlets.binding(), + meshlet_mesh_manager.indices.binding(), + meshlet_mesh_manager.vertex_positions.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .software_raster_cluster_count + .as_entire_binding(), + &view_resources.visibility_buffer.default_view, + view_uniforms.clone(), + )), + ); + + let resolve_depth = render_device.create_bind_group( + "meshlet_resolve_depth_bind_group", + if view_resources.not_shadow_view { + &resource_manager.resolve_depth_bind_group_layout + } else { + &resource_manager.resolve_depth_shadow_view_bind_group_layout + }, + &BindGroupEntries::single(&view_resources.visibility_buffer.default_view), + ); + + let resolve_material_depth = view_resources.material_depth.as_ref().map(|_| { + render_device.create_bind_group( + "meshlet_resolve_material_depth_bind_group", + &resource_manager.resolve_material_depth_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.visibility_buffer.default_view, + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + instance_manager.instance_material_ids.binding().unwrap(), + )), + ) + }); + + let material_shade = view_resources.material_depth.as_ref().map(|_| { + render_device.create_bind_group( + "meshlet_mesh_material_shade_bind_group", + &resource_manager.material_shade_bind_group_layout, + &BindGroupEntries::sequential(( + &view_resources.visibility_buffer.default_view, + resource_manager + .visibility_buffer_raster_clusters + .as_entire_binding(), + meshlet_mesh_manager.meshlets.binding(), + meshlet_mesh_manager.indices.binding(), + meshlet_mesh_manager.vertex_positions.binding(), + meshlet_mesh_manager.vertex_normals.binding(), + meshlet_mesh_manager.vertex_uvs.binding(), + instance_manager.instance_uniforms.binding().unwrap(), + )), + ) + }); + + let remap_1d_to_2d_dispatch = resource_manager + .remap_1d_to_2d_dispatch_bind_group_layout + .as_ref() + .map(|layout| { + render_device.create_bind_group( + "meshlet_remap_1d_to_2d_dispatch_bind_group", + layout, + &BindGroupEntries::sequential(( + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + resource_manager + .software_raster_cluster_count + .as_entire_binding(), + )), + ) + }); + + let fill_counts = if resource_manager + .remap_1d_to_2d_dispatch_bind_group_layout + .is_some() + { + render_device.create_bind_group( + "meshlet_fill_counts_bind_group", + &resource_manager.fill_counts_bind_group_layout, + &BindGroupEntries::sequential(( + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + resource_manager + .software_raster_cluster_count + .as_entire_binding(), + )), + ) + } else { + render_device.create_bind_group( + "meshlet_fill_counts_bind_group", + &resource_manager.fill_counts_bind_group_layout, + &BindGroupEntries::sequential(( + view_resources + .visibility_buffer_software_raster_indirect_args + .as_entire_binding(), + view_resources + .visibility_buffer_hardware_raster_indirect_args + .as_entire_binding(), + resource_manager + .visibility_buffer_raster_cluster_prev_counts + .as_entire_binding(), + )), + ) + }; + + commands.entity(view_entity).insert(MeshletViewBindGroups { + clear_visibility_buffer, + first_instance_cull, + second_instance_cull, + first_bvh_cull_ping, + first_bvh_cull_pong, + second_bvh_cull_ping, + second_bvh_cull_pong, + first_meshlet_cull, + second_meshlet_cull, + downsample_depth, + visibility_buffer_raster, + resolve_depth, + resolve_material_depth, + material_shade, + remap_1d_to_2d_dispatch, + fill_counts, + }); + } +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_hardware_raster.wgsl b/crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_hardware_raster.wgsl new file mode 100644 index 0000000..2a25144 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_hardware_raster.wgsl @@ -0,0 +1,79 @@ +#import bevy_pbr::{ + meshlet_bindings::{ + meshlet_cluster_meshlet_ids, + meshlets, + meshlet_cluster_instance_ids, + meshlet_instance_uniforms, + meshlet_raster_clusters, + meshlet_previous_raster_counts, + meshlet_visibility_buffer, + view, + get_meshlet_triangle_count, + get_meshlet_vertex_id, + get_meshlet_vertex_position, + }, + mesh_functions::mesh_position_local_to_world, +} +#import bevy_render::maths::affine3_to_square +var meshlet_raster_cluster_rightmost_slot: u32; + +/// Vertex/fragment shader for rasterizing large clusters into a visibility buffer. + +struct VertexOutput { + @builtin(position) position: vec4, +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + @location(0) @interpolate(flat) packed_ids: u32, +#endif +} + +@vertex +fn vertex(@builtin(instance_index) instance_index: u32, @builtin(vertex_index) vertex_index: u32) -> VertexOutput { + let cluster_in_draw = meshlet_previous_raster_counts[1] + instance_index; + let cluster_id = meshlet_raster_cluster_rightmost_slot - cluster_in_draw; + let instanced_offset = meshlet_raster_clusters[cluster_id]; + var meshlet = meshlets[instanced_offset.offset]; + + let triangle_id = vertex_index / 3u; + if triangle_id >= get_meshlet_triangle_count(&meshlet) { return dummy_vertex(); } + let index_id = vertex_index; + let vertex_id = get_meshlet_vertex_id(meshlet.start_index_id + index_id); + + let instance_uniform = meshlet_instance_uniforms[instanced_offset.instance_id]; + + let vertex_position = get_meshlet_vertex_position(&meshlet, vertex_id); + let world_from_local = affine3_to_square(instance_uniform.world_from_local); + let world_position = mesh_position_local_to_world(world_from_local, vec4(vertex_position, 1.0)); + let clip_position = view.clip_from_world * vec4(world_position.xyz, 1.0); + + return VertexOutput( + clip_position, +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + (cluster_id << 7u) | triangle_id, +#endif + ); +} + +@fragment +fn fragment(vertex_output: VertexOutput) { + let depth = bitcast(vertex_output.position.z); +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + let visibility = (u64(depth) << 32u) | u64(vertex_output.packed_ids); +#else + let visibility = depth; +#endif + textureAtomicMax(meshlet_visibility_buffer, vec2(vertex_output.position.xy), visibility); +} + +fn dummy_vertex() -> VertexOutput { + return VertexOutput( + vec4(divide(0.0, 0.0)), // NaN vertex position +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + 0u, +#endif + ); +} + +// Naga doesn't allow divide by zero literals, but this lets us work around it +fn divide(a: f32, b: f32) -> f32 { + return a / b; +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_raster_node.rs b/crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_raster_node.rs new file mode 100644 index 0000000..60d8bed --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_raster_node.rs @@ -0,0 +1,706 @@ +use super::{ + pipelines::MeshletPipelines, + resource_manager::{MeshletViewBindGroups, MeshletViewResources}, +}; +use crate::render::pbr::{ + meshlet::resource_manager::ResourceManager, LightEntity, ShadowView, ViewLightEntities, +}; +use bevy_color::LinearRgba; +use crate::render::prepass::PreviousViewUniformOffset; +use bevy_ecs::{ + query::QueryState, + world::{FromWorld, World}, +}; +use bevy_math::UVec2; +use crate::render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{Node, NodeRunError, RenderGraphContext}, + render_resource::*, + renderer::RenderContext, + view::{ViewDepthTexture, ViewUniformOffset}, +}; + +/// Rasterize meshlets into a depth buffer, and optional visibility buffer + material depth buffer for shading passes. +pub struct MeshletVisibilityBufferRasterPassNode { + main_view_query: QueryState<( + &'static ExtractedCamera, + &'static ViewDepthTexture, + &'static ViewUniformOffset, + &'static PreviousViewUniformOffset, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + &'static ViewLightEntities, + )>, + view_light_query: QueryState<( + &'static ShadowView, + &'static LightEntity, + &'static ViewUniformOffset, + &'static PreviousViewUniformOffset, + &'static MeshletViewBindGroups, + &'static MeshletViewResources, + )>, +} + +impl FromWorld for MeshletVisibilityBufferRasterPassNode { + fn from_world(world: &mut World) -> Self { + Self { + main_view_query: QueryState::new(world), + view_light_query: QueryState::new(world), + } + } +} + +impl Node for MeshletVisibilityBufferRasterPassNode { + fn update(&mut self, world: &mut World) { + self.main_view_query.update_archetypes(world); + self.view_light_query.update_archetypes(world); + } + + // TODO: Reuse compute/render passes between logical passes where possible, as they're expensive + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let Ok(( + camera, + view_depth, + view_offset, + previous_view_offset, + meshlet_view_bind_groups, + meshlet_view_resources, + lights, + )) = self.main_view_query.get_manual(world, graph.view_entity()) + else { + return Ok(()); + }; + + let Some(( + clear_visibility_buffer_pipeline, + clear_visibility_buffer_shadow_view_pipeline, + first_instance_cull_pipeline, + second_instance_cull_pipeline, + first_bvh_cull_pipeline, + second_bvh_cull_pipeline, + first_meshlet_cull_pipeline, + second_meshlet_cull_pipeline, + downsample_depth_first_pipeline, + downsample_depth_second_pipeline, + downsample_depth_first_shadow_view_pipeline, + downsample_depth_second_shadow_view_pipeline, + visibility_buffer_software_raster_pipeline, + visibility_buffer_software_raster_shadow_view_pipeline, + visibility_buffer_hardware_raster_pipeline, + visibility_buffer_hardware_raster_shadow_view_pipeline, + visibility_buffer_hardware_raster_shadow_view_unclipped_pipeline, + resolve_depth_pipeline, + resolve_depth_shadow_view_pipeline, + resolve_material_depth_pipeline, + remap_1d_to_2d_dispatch_pipeline, + fill_counts_pipeline, + )) = MeshletPipelines::get(world) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + render_context + .command_encoder() + .push_debug_group("meshlet_visibility_buffer_raster"); + let time_span = diagnostics.time_span( + render_context.command_encoder(), + "meshlet_visibility_buffer_raster", + ); + + let resource_manager = world.get_resource::().unwrap(); + render_context.command_encoder().clear_buffer( + &resource_manager.visibility_buffer_raster_cluster_prev_counts, + 0, + None, + ); + + clear_visibility_buffer_pass( + render_context, + &meshlet_view_bind_groups.clear_visibility_buffer, + clear_visibility_buffer_pipeline, + meshlet_view_resources.view_size, + ); + + render_context + .command_encoder() + .push_debug_group("meshlet_first_pass"); + first_cull( + render_context, + meshlet_view_bind_groups, + meshlet_view_resources, + view_offset, + previous_view_offset, + first_instance_cull_pipeline, + first_bvh_cull_pipeline, + first_meshlet_cull_pipeline, + remap_1d_to_2d_dispatch_pipeline, + ); + raster_pass( + true, + render_context, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, + &meshlet_view_resources.dummy_render_target.default_view, + meshlet_view_bind_groups, + view_offset, + visibility_buffer_software_raster_pipeline, + visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, + Some(camera), + meshlet_view_resources.rightmost_slot, + ); + render_context.command_encoder().pop_debug_group(); + + meshlet_view_resources.depth_pyramid.downsample_depth( + "downsample_depth", + render_context, + meshlet_view_resources.view_size, + &meshlet_view_bind_groups.downsample_depth, + downsample_depth_first_pipeline, + downsample_depth_second_pipeline, + ); + + render_context + .command_encoder() + .push_debug_group("meshlet_second_pass"); + second_cull( + render_context, + meshlet_view_bind_groups, + meshlet_view_resources, + view_offset, + previous_view_offset, + second_instance_cull_pipeline, + second_bvh_cull_pipeline, + second_meshlet_cull_pipeline, + remap_1d_to_2d_dispatch_pipeline, + ); + raster_pass( + false, + render_context, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, + &meshlet_view_resources.dummy_render_target.default_view, + meshlet_view_bind_groups, + view_offset, + visibility_buffer_software_raster_pipeline, + visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, + Some(camera), + meshlet_view_resources.rightmost_slot, + ); + render_context.command_encoder().pop_debug_group(); + + resolve_depth( + render_context, + view_depth.get_attachment(StoreOp::Store), + meshlet_view_bind_groups, + resolve_depth_pipeline, + camera, + ); + resolve_material_depth( + render_context, + meshlet_view_resources, + meshlet_view_bind_groups, + resolve_material_depth_pipeline, + camera, + ); + meshlet_view_resources.depth_pyramid.downsample_depth( + "downsample_depth", + render_context, + meshlet_view_resources.view_size, + &meshlet_view_bind_groups.downsample_depth, + downsample_depth_first_pipeline, + downsample_depth_second_pipeline, + ); + render_context.command_encoder().pop_debug_group(); + + for light_entity in &lights.lights { + let Ok(( + shadow_view, + light_type, + view_offset, + previous_view_offset, + meshlet_view_bind_groups, + meshlet_view_resources, + )) = self.view_light_query.get_manual(world, *light_entity) + else { + continue; + }; + + let shadow_visibility_buffer_hardware_raster_pipeline = + if let LightEntity::Directional { .. } = light_type { + visibility_buffer_hardware_raster_shadow_view_unclipped_pipeline + } else { + visibility_buffer_hardware_raster_shadow_view_pipeline + }; + + render_context.command_encoder().push_debug_group(&format!( + "meshlet_visibility_buffer_raster: {}", + shadow_view.pass_name + )); + let time_span_shadow = diagnostics.time_span( + render_context.command_encoder(), + shadow_view.pass_name.clone(), + ); + clear_visibility_buffer_pass( + render_context, + &meshlet_view_bind_groups.clear_visibility_buffer, + clear_visibility_buffer_shadow_view_pipeline, + meshlet_view_resources.view_size, + ); + + render_context + .command_encoder() + .push_debug_group("meshlet_first_pass"); + first_cull( + render_context, + meshlet_view_bind_groups, + meshlet_view_resources, + view_offset, + previous_view_offset, + first_instance_cull_pipeline, + first_bvh_cull_pipeline, + first_meshlet_cull_pipeline, + remap_1d_to_2d_dispatch_pipeline, + ); + raster_pass( + true, + render_context, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, + &meshlet_view_resources.dummy_render_target.default_view, + meshlet_view_bind_groups, + view_offset, + visibility_buffer_software_raster_shadow_view_pipeline, + shadow_visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, + None, + meshlet_view_resources.rightmost_slot, + ); + render_context.command_encoder().pop_debug_group(); + + meshlet_view_resources.depth_pyramid.downsample_depth( + "downsample_depth", + render_context, + meshlet_view_resources.view_size, + &meshlet_view_bind_groups.downsample_depth, + downsample_depth_first_shadow_view_pipeline, + downsample_depth_second_shadow_view_pipeline, + ); + + render_context + .command_encoder() + .push_debug_group("meshlet_second_pass"); + second_cull( + render_context, + meshlet_view_bind_groups, + meshlet_view_resources, + view_offset, + previous_view_offset, + second_instance_cull_pipeline, + second_bvh_cull_pipeline, + second_meshlet_cull_pipeline, + remap_1d_to_2d_dispatch_pipeline, + ); + raster_pass( + false, + render_context, + &meshlet_view_resources.visibility_buffer_software_raster_indirect_args, + &meshlet_view_resources.visibility_buffer_hardware_raster_indirect_args, + &meshlet_view_resources.dummy_render_target.default_view, + meshlet_view_bind_groups, + view_offset, + visibility_buffer_software_raster_shadow_view_pipeline, + shadow_visibility_buffer_hardware_raster_pipeline, + fill_counts_pipeline, + None, + meshlet_view_resources.rightmost_slot, + ); + render_context.command_encoder().pop_debug_group(); + + resolve_depth( + render_context, + shadow_view.depth_attachment.get_attachment(StoreOp::Store), + meshlet_view_bind_groups, + resolve_depth_shadow_view_pipeline, + camera, + ); + meshlet_view_resources.depth_pyramid.downsample_depth( + "downsample_depth", + render_context, + meshlet_view_resources.view_size, + &meshlet_view_bind_groups.downsample_depth, + downsample_depth_first_shadow_view_pipeline, + downsample_depth_second_shadow_view_pipeline, + ); + render_context.command_encoder().pop_debug_group(); + time_span_shadow.end(render_context.command_encoder()); + } + + time_span.end(render_context.command_encoder()); + + Ok(()) + } +} + +// TODO: Replace this with vkCmdClearColorImage once wgpu supports it +fn clear_visibility_buffer_pass( + render_context: &mut RenderContext, + clear_visibility_buffer_bind_group: &BindGroup, + clear_visibility_buffer_pipeline: &ComputePipeline, + view_size: UVec2, +) { + let command_encoder = render_context.command_encoder(); + let mut clear_visibility_buffer_pass = + command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("clear_visibility_buffer"), + timestamp_writes: None, + }); + clear_visibility_buffer_pass.set_pipeline(clear_visibility_buffer_pipeline); + clear_visibility_buffer_pass.set_push_constants(0, bytemuck::bytes_of(&view_size)); + clear_visibility_buffer_pass.set_bind_group(0, clear_visibility_buffer_bind_group, &[]); + clear_visibility_buffer_pass.dispatch_workgroups( + view_size.x.div_ceil(16), + view_size.y.div_ceil(16), + 1, + ); +} + +fn first_cull( + render_context: &mut RenderContext, + meshlet_view_bind_groups: &MeshletViewBindGroups, + meshlet_view_resources: &MeshletViewResources, + view_offset: &ViewUniformOffset, + previous_view_offset: &PreviousViewUniformOffset, + first_instance_cull_pipeline: &ComputePipeline, + first_bvh_cull_pipeline: &ComputePipeline, + first_meshlet_cull_pipeline: &ComputePipeline, + remap_1d_to_2d_pipeline: Option<&ComputePipeline>, +) { + let workgroups = meshlet_view_resources.scene_instance_count.div_ceil(128); + cull_pass( + "meshlet_first_instance_cull", + render_context, + &meshlet_view_bind_groups.first_instance_cull, + view_offset, + previous_view_offset, + first_instance_cull_pipeline, + &[meshlet_view_resources.scene_instance_count], + ) + .dispatch_workgroups(workgroups, 1, 1); + + render_context + .command_encoder() + .push_debug_group("meshlet_first_bvh_cull"); + let mut ping = true; + for _ in 0..meshlet_view_resources.max_bvh_depth { + cull_pass( + "meshlet_first_bvh_cull_dispatch", + render_context, + if ping { + &meshlet_view_bind_groups.first_bvh_cull_ping + } else { + &meshlet_view_bind_groups.first_bvh_cull_pong + }, + view_offset, + previous_view_offset, + first_bvh_cull_pipeline, + &[ping as u32, meshlet_view_resources.rightmost_slot], + ) + .dispatch_workgroups_indirect( + if ping { + &meshlet_view_resources.first_bvh_cull_dispatch_front + } else { + &meshlet_view_resources.first_bvh_cull_dispatch_back + }, + 0, + ); + render_context.command_encoder().clear_buffer( + if ping { + &meshlet_view_resources.first_bvh_cull_count_front + } else { + &meshlet_view_resources.first_bvh_cull_count_back + }, + 0, + Some(4), + ); + render_context.command_encoder().clear_buffer( + if ping { + &meshlet_view_resources.first_bvh_cull_dispatch_front + } else { + &meshlet_view_resources.first_bvh_cull_dispatch_back + }, + 0, + Some(4), + ); + ping = !ping; + } + render_context.command_encoder().pop_debug_group(); + + let mut pass = cull_pass( + "meshlet_first_meshlet_cull", + render_context, + &meshlet_view_bind_groups.first_meshlet_cull, + view_offset, + previous_view_offset, + first_meshlet_cull_pipeline, + &[meshlet_view_resources.rightmost_slot], + ); + pass.dispatch_workgroups_indirect(&meshlet_view_resources.front_meshlet_cull_dispatch, 0); + remap_1d_to_2d( + pass, + remap_1d_to_2d_pipeline, + meshlet_view_bind_groups.remap_1d_to_2d_dispatch.as_ref(), + ); +} + +fn second_cull( + render_context: &mut RenderContext, + meshlet_view_bind_groups: &MeshletViewBindGroups, + meshlet_view_resources: &MeshletViewResources, + view_offset: &ViewUniformOffset, + previous_view_offset: &PreviousViewUniformOffset, + second_instance_cull_pipeline: &ComputePipeline, + second_bvh_cull_pipeline: &ComputePipeline, + second_meshlet_cull_pipeline: &ComputePipeline, + remap_1d_to_2d_pipeline: Option<&ComputePipeline>, +) { + cull_pass( + "meshlet_second_instance_cull", + render_context, + &meshlet_view_bind_groups.second_instance_cull, + view_offset, + previous_view_offset, + second_instance_cull_pipeline, + &[meshlet_view_resources.scene_instance_count], + ) + .dispatch_workgroups_indirect(&meshlet_view_resources.second_pass_dispatch, 0); + + render_context + .command_encoder() + .push_debug_group("meshlet_second_bvh_cull"); + let mut ping = true; + for _ in 0..meshlet_view_resources.max_bvh_depth { + cull_pass( + "meshlet_second_bvh_cull_dispatch", + render_context, + if ping { + &meshlet_view_bind_groups.second_bvh_cull_ping + } else { + &meshlet_view_bind_groups.second_bvh_cull_pong + }, + view_offset, + previous_view_offset, + second_bvh_cull_pipeline, + &[ping as u32, meshlet_view_resources.rightmost_slot], + ) + .dispatch_workgroups_indirect( + if ping { + &meshlet_view_resources.second_bvh_cull_dispatch_front + } else { + &meshlet_view_resources.second_bvh_cull_dispatch_back + }, + 0, + ); + ping = !ping; + } + render_context.command_encoder().pop_debug_group(); + + let mut pass = cull_pass( + "meshlet_second_meshlet_cull", + render_context, + &meshlet_view_bind_groups.second_meshlet_cull, + view_offset, + previous_view_offset, + second_meshlet_cull_pipeline, + &[meshlet_view_resources.rightmost_slot], + ); + pass.dispatch_workgroups_indirect(&meshlet_view_resources.back_meshlet_cull_dispatch, 0); + remap_1d_to_2d( + pass, + remap_1d_to_2d_pipeline, + meshlet_view_bind_groups.remap_1d_to_2d_dispatch.as_ref(), + ); +} + +fn cull_pass<'a>( + label: &'static str, + render_context: &'a mut RenderContext, + bind_group: &'a BindGroup, + view_offset: &'a ViewUniformOffset, + previous_view_offset: &'a PreviousViewUniformOffset, + pipeline: &'a ComputePipeline, + push_constants: &[u32], +) -> ComputePass<'a> { + let command_encoder = render_context.command_encoder(); + let mut pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some(label), + timestamp_writes: None, + }); + pass.set_pipeline(pipeline); + pass.set_bind_group( + 0, + bind_group, + &[view_offset.offset, previous_view_offset.offset], + ); + pass.set_push_constants(0, bytemuck::cast_slice(push_constants)); + pass +} + +fn remap_1d_to_2d( + mut pass: ComputePass, + pipeline: Option<&ComputePipeline>, + bind_group: Option<&BindGroup>, +) { + if let (Some(pipeline), Some(bind_group)) = (pipeline, bind_group) { + pass.set_pipeline(pipeline); + pass.set_bind_group(0, bind_group, &[]); + pass.dispatch_workgroups(1, 1, 1); + } +} + +fn raster_pass( + first_pass: bool, + render_context: &mut RenderContext, + visibility_buffer_software_raster_indirect_args: &Buffer, + visibility_buffer_hardware_raster_indirect_args: &Buffer, + dummy_render_target: &TextureView, + meshlet_view_bind_groups: &MeshletViewBindGroups, + view_offset: &ViewUniformOffset, + visibility_buffer_software_raster_pipeline: &ComputePipeline, + visibility_buffer_hardware_raster_pipeline: &RenderPipeline, + fill_counts_pipeline: &ComputePipeline, + camera: Option<&ExtractedCamera>, + raster_cluster_rightmost_slot: u32, +) { + let mut software_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some(if first_pass { + "raster_software_first" + } else { + "raster_software_second" + }), + timestamp_writes: None, + }); + software_pass.set_pipeline(visibility_buffer_software_raster_pipeline); + software_pass.set_bind_group( + 0, + &meshlet_view_bind_groups.visibility_buffer_raster, + &[view_offset.offset], + ); + software_pass.dispatch_workgroups_indirect(visibility_buffer_software_raster_indirect_args, 0); + drop(software_pass); + + let mut hardware_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some(if first_pass { + "raster_hardware_first" + } else { + "raster_hardware_second" + }), + color_attachments: &[Some(RenderPassColorAttachment { + view: dummy_render_target, + depth_slice: None, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(LinearRgba::BLACK.into()), + store: StoreOp::Discard, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + if let Some(viewport) = camera.and_then(|camera| camera.viewport.as_ref()) { + hardware_pass.set_camera_viewport(viewport); + } + hardware_pass.set_render_pipeline(visibility_buffer_hardware_raster_pipeline); + hardware_pass.set_push_constants( + ShaderStages::VERTEX, + 0, + &raster_cluster_rightmost_slot.to_le_bytes(), + ); + hardware_pass.set_bind_group( + 0, + &meshlet_view_bind_groups.visibility_buffer_raster, + &[view_offset.offset], + ); + hardware_pass.draw_indirect(visibility_buffer_hardware_raster_indirect_args, 0); + drop(hardware_pass); + + let mut fill_counts_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("fill_counts"), + timestamp_writes: None, + }); + fill_counts_pass.set_pipeline(fill_counts_pipeline); + fill_counts_pass.set_bind_group(0, &meshlet_view_bind_groups.fill_counts, &[]); + fill_counts_pass.dispatch_workgroups(1, 1, 1); +} + +fn resolve_depth( + render_context: &mut RenderContext, + depth_stencil_attachment: RenderPassDepthStencilAttachment, + meshlet_view_bind_groups: &MeshletViewBindGroups, + resolve_depth_pipeline: &RenderPipeline, + camera: &ExtractedCamera, +) { + let mut resolve_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("resolve_depth"), + color_attachments: &[], + depth_stencil_attachment: Some(depth_stencil_attachment), + timestamp_writes: None, + occlusion_query_set: None, + }); + if let Some(viewport) = &camera.viewport { + resolve_pass.set_camera_viewport(viewport); + } + resolve_pass.set_render_pipeline(resolve_depth_pipeline); + resolve_pass.set_bind_group(0, &meshlet_view_bind_groups.resolve_depth, &[]); + resolve_pass.draw(0..3, 0..1); +} + +fn resolve_material_depth( + render_context: &mut RenderContext, + meshlet_view_resources: &MeshletViewResources, + meshlet_view_bind_groups: &MeshletViewBindGroups, + resolve_material_depth_pipeline: &RenderPipeline, + camera: &ExtractedCamera, +) { + if let (Some(material_depth), Some(resolve_material_depth_bind_group)) = ( + meshlet_view_resources.material_depth.as_ref(), + meshlet_view_bind_groups.resolve_material_depth.as_ref(), + ) { + let mut resolve_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("resolve_material_depth"), + color_attachments: &[], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &material_depth.default_view, + depth_ops: Some(Operations { + load: LoadOp::Clear(0.0), + store: StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + if let Some(viewport) = &camera.viewport { + resolve_pass.set_camera_viewport(viewport); + } + resolve_pass.set_render_pipeline(resolve_material_depth_pipeline); + resolve_pass.set_bind_group(0, resolve_material_depth_bind_group, &[]); + resolve_pass.draw(0..3, 0..1); + } +} diff --git a/crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_resolve.wgsl b/crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_resolve.wgsl new file mode 100644 index 0000000..8d8a22b --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_resolve.wgsl @@ -0,0 +1,240 @@ +#define_import_path bevy_pbr::meshlet_visibility_buffer_resolve + +#import bevy_pbr::{ + meshlet_bindings::{ + Meshlet, + meshlet_visibility_buffer, + meshlet_raster_clusters, + meshlets, + meshlet_instance_uniforms, + get_meshlet_vertex_id, + get_meshlet_vertex_position, + get_meshlet_vertex_normal, + get_meshlet_vertex_uv, + }, + mesh_view_bindings::view, + mesh_functions::mesh_position_local_to_world, + mesh_types::Mesh, + view_transformations::{position_world_to_clip, frag_coord_to_ndc}, +} +#import bevy_render::maths::{affine3_to_square, mat2x4_f32_to_mat3x3_unpack} + +#ifdef PREPASS_FRAGMENT +#ifdef MOTION_VECTOR_PREPASS +#import bevy_pbr::{ + prepass_bindings::previous_view_uniforms, + pbr_prepass_functions::calculate_motion_vector, +} +#endif +#endif + +/// Functions to be used by materials for reading from a meshlet visibility buffer texture. + +#ifdef MESHLET_MESH_MATERIAL_PASS +struct PartialDerivatives { + barycentrics: vec3, + ddx: vec3, + ddy: vec3, +} + +// https://github.com/ConfettiFX/The-Forge/blob/9d43e69141a9cd0ce2ce2d2db5122234d3a2d5b5/Common_3/Renderer/VisibilityBuffer2/Shaders/FSL/vb_shading_utilities.h.fsl#L90-L150 +fn compute_partial_derivatives(vertex_world_positions: array, 3>, ndc_uv: vec2, half_screen_size: vec2) -> PartialDerivatives { + var result: PartialDerivatives; + + let vertex_clip_position_0 = position_world_to_clip(vertex_world_positions[0].xyz); + let vertex_clip_position_1 = position_world_to_clip(vertex_world_positions[1].xyz); + let vertex_clip_position_2 = position_world_to_clip(vertex_world_positions[2].xyz); + + let inv_w = 1.0 / vec3(vertex_clip_position_0.w, vertex_clip_position_1.w, vertex_clip_position_2.w); + let ndc_0 = vertex_clip_position_0.xy * inv_w[0]; + let ndc_1 = vertex_clip_position_1.xy * inv_w[1]; + let ndc_2 = vertex_clip_position_2.xy * inv_w[2]; + + let inv_det = 1.0 / determinant(mat2x2(ndc_2 - ndc_1, ndc_0 - ndc_1)); + result.ddx = vec3(ndc_1.y - ndc_2.y, ndc_2.y - ndc_0.y, ndc_0.y - ndc_1.y) * inv_det * inv_w; + result.ddy = vec3(ndc_2.x - ndc_1.x, ndc_0.x - ndc_2.x, ndc_1.x - ndc_0.x) * inv_det * inv_w; + + var ddx_sum = dot(result.ddx, vec3(1.0)); + var ddy_sum = dot(result.ddy, vec3(1.0)); + + let delta_v = ndc_uv - ndc_0; + let interp_inv_w = inv_w.x + delta_v.x * ddx_sum + delta_v.y * ddy_sum; + let interp_w = 1.0 / interp_inv_w; + + result.barycentrics = vec3( + interp_w * (inv_w[0] + delta_v.x * result.ddx.x + delta_v.y * result.ddy.x), + interp_w * (delta_v.x * result.ddx.y + delta_v.y * result.ddy.y), + interp_w * (delta_v.x * result.ddx.z + delta_v.y * result.ddy.z), + ); + + result.ddx *= half_screen_size.x; + result.ddy *= half_screen_size.y; + ddx_sum *= half_screen_size.x; + ddy_sum *= half_screen_size.y; + + result.ddy *= -1.0; + ddy_sum *= -1.0; + + let interp_ddx_w = 1.0 / (interp_inv_w + ddx_sum); + let interp_ddy_w = 1.0 / (interp_inv_w + ddy_sum); + + result.ddx = interp_ddx_w * (result.barycentrics * interp_inv_w + result.ddx) - result.barycentrics; + result.ddy = interp_ddy_w * (result.barycentrics * interp_inv_w + result.ddy) - result.barycentrics; + return result; +} + +struct VertexOutput { + position: vec4, + world_position: vec4, + world_normal: vec3, + uv: vec2, + ddx_uv: vec2, + ddy_uv: vec2, + world_tangent: vec4, + mesh_flags: u32, + cluster_id: u32, + material_bind_group_slot: u32, +#ifdef PREPASS_FRAGMENT +#ifdef MOTION_VECTOR_PREPASS + motion_vector: vec2, +#endif +#endif +} + +/// Load the visibility buffer texture and resolve it into a VertexOutput. +fn resolve_vertex_output(frag_coord: vec4) -> VertexOutput { + let packed_ids = u32(textureLoad(meshlet_visibility_buffer, vec2(frag_coord.xy)).r); + let cluster_id = packed_ids >> 7u; + let instanced_offset = meshlet_raster_clusters[cluster_id]; + let meshlet_id = instanced_offset.offset; + var meshlet = meshlets[meshlet_id]; + + let triangle_id = extractBits(packed_ids, 0u, 7u); + let index_ids = meshlet.start_index_id + (triangle_id * 3u) + vec3(0u, 1u, 2u); + let vertex_ids = vec3(get_meshlet_vertex_id(index_ids[0]), get_meshlet_vertex_id(index_ids[1]), get_meshlet_vertex_id(index_ids[2])); + let vertex_0 = load_vertex(&meshlet, vertex_ids[0]); + let vertex_1 = load_vertex(&meshlet, vertex_ids[1]); + let vertex_2 = load_vertex(&meshlet, vertex_ids[2]); + + let instance_id = instanced_offset.instance_id; + var instance_uniform = meshlet_instance_uniforms[instance_id]; + + let world_from_local = affine3_to_square(instance_uniform.world_from_local); + let world_position_0 = mesh_position_local_to_world(world_from_local, vec4(vertex_0.position, 1.0)); + let world_position_1 = mesh_position_local_to_world(world_from_local, vec4(vertex_1.position, 1.0)); + let world_position_2 = mesh_position_local_to_world(world_from_local, vec4(vertex_2.position, 1.0)); + + let frag_coord_ndc = frag_coord_to_ndc(frag_coord).xy; + let partial_derivatives = compute_partial_derivatives( + array(world_position_0, world_position_1, world_position_2), + frag_coord_ndc, + view.viewport.zw / 2.0, + ); + + let world_position = mat3x4(world_position_0, world_position_1, world_position_2) * partial_derivatives.barycentrics; + let world_positions_camera_relative = mat3x3( + world_position_0.xyz - view.world_position, + world_position_1.xyz - view.world_position, + world_position_2.xyz - view.world_position, + ); + let ddx_world_position = world_positions_camera_relative * partial_derivatives.ddx; + let ddy_world_position = world_positions_camera_relative * partial_derivatives.ddy; + + let world_normal = mat3x3( + normal_local_to_world(vertex_0.normal, &instance_uniform), + normal_local_to_world(vertex_1.normal, &instance_uniform), + normal_local_to_world(vertex_2.normal, &instance_uniform), + ) * partial_derivatives.barycentrics; + + let uv = mat3x2(vertex_0.uv, vertex_1.uv, vertex_2.uv) * partial_derivatives.barycentrics; + let ddx_uv = mat3x2(vertex_0.uv, vertex_1.uv, vertex_2.uv) * partial_derivatives.ddx; + let ddy_uv = mat3x2(vertex_0.uv, vertex_1.uv, vertex_2.uv) * partial_derivatives.ddy; + + let world_tangent = calculate_world_tangent(world_normal, ddx_world_position, ddy_world_position, ddx_uv, ddy_uv); + +#ifdef PREPASS_FRAGMENT +#ifdef MOTION_VECTOR_PREPASS + let previous_world_from_local = affine3_to_square(instance_uniform.previous_world_from_local); + let previous_world_position_0 = mesh_position_local_to_world(previous_world_from_local, vec4(vertex_0.position, 1.0)); + let previous_world_position_1 = mesh_position_local_to_world(previous_world_from_local, vec4(vertex_1.position, 1.0)); + let previous_world_position_2 = mesh_position_local_to_world(previous_world_from_local, vec4(vertex_2.position, 1.0)); + let previous_world_position = mat3x4(previous_world_position_0, previous_world_position_1, previous_world_position_2) * partial_derivatives.barycentrics; + let motion_vector = calculate_motion_vector(world_position, previous_world_position); +#endif +#endif + + return VertexOutput( + frag_coord, + world_position, + world_normal, + uv, + ddx_uv, + ddy_uv, + world_tangent, + instance_uniform.flags, + instance_id ^ meshlet_id, + instance_uniform.material_and_lightmap_bind_group_slot & 0xffffu, +#ifdef PREPASS_FRAGMENT +#ifdef MOTION_VECTOR_PREPASS + motion_vector, +#endif +#endif + ); +} + +struct MeshletVertex { + position: vec3, + normal: vec3, + uv: vec2, +} + +fn load_vertex(meshlet: ptr, vertex_id: u32) -> MeshletVertex { + return MeshletVertex( + get_meshlet_vertex_position(meshlet, vertex_id), + get_meshlet_vertex_normal(meshlet, vertex_id), + get_meshlet_vertex_uv(meshlet, vertex_id), + ); +} + +fn normal_local_to_world(vertex_normal: vec3, instance_uniform: ptr) -> vec3 { + if any(vertex_normal != vec3(0.0)) { + return normalize( + mat2x4_f32_to_mat3x3_unpack( + (*instance_uniform).local_from_world_transpose_a, + (*instance_uniform).local_from_world_transpose_b, + ) * vertex_normal + ); + } else { + return vertex_normal; + } +} + +// https://www.jeremyong.com/graphics/2023/12/16/surface-gradient-bump-mapping/#surface-gradient-from-a-tangent-space-normal-vector-without-an-explicit-tangent-basis +fn calculate_world_tangent( + world_normal: vec3, + ddx_world_position: vec3, + ddy_world_position: vec3, + ddx_uv: vec2, + ddy_uv: vec2, +) -> vec4 { + // Project the position gradients onto the tangent plane + let ddx_world_position_s = ddx_world_position - dot(ddx_world_position, world_normal) * world_normal; + let ddy_world_position_s = ddy_world_position - dot(ddy_world_position, world_normal) * world_normal; + + // Compute the jacobian matrix to leverage the chain rule + let jacobian_sign = sign(ddx_uv.x * ddy_uv.y - ddx_uv.y * ddy_uv.x); + + var world_tangent = jacobian_sign * (ddy_uv.y * ddx_world_position_s - ddx_uv.y * ddy_world_position_s); + + // The sign intrinsic returns 0 if the argument is 0 + if jacobian_sign != 0.0 { + world_tangent = normalize(world_tangent); + } + + // The second factor here ensures a consistent handedness between + // the tangent frame and surface basis w.r.t. screenspace. + let w = jacobian_sign * sign(dot(ddy_world_position, cross(world_normal, ddx_world_position))); + + return vec4(world_tangent, -w); // TODO: Unclear why we need to negate this to match mikktspace generated tangents +} +#endif diff --git a/crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_software_raster.wgsl b/crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_software_raster.wgsl new file mode 100644 index 0000000..0ddfff8 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/meshlet/visibility_buffer_software_raster.wgsl @@ -0,0 +1,189 @@ +#import bevy_pbr::{ + meshlet_bindings::{ + meshlet_cluster_meshlet_ids, + meshlets, + meshlet_cluster_instance_ids, + meshlet_instance_uniforms, + meshlet_raster_clusters, + meshlet_previous_raster_counts, + meshlet_software_raster_cluster_count, + meshlet_visibility_buffer, + view, + get_meshlet_vertex_count, + get_meshlet_triangle_count, + get_meshlet_vertex_id, + get_meshlet_vertex_position, + }, + mesh_functions::mesh_position_local_to_world, + view_transformations::ndc_to_uv, +} +#import bevy_render::maths::affine3_to_square + +/// Compute shader for rasterizing small clusters into a visibility buffer. + +// TODO: Fixed-point math and top-left rule + +var viewport_vertices: array; + +@compute +@workgroup_size(128, 1, 1) // 128 threads per workgroup, 1-2 vertices per thread, 1 triangle per thread, 1 cluster per workgroup +fn rasterize_cluster( + @builtin(workgroup_id) workgroup_id: vec3, + @builtin(local_invocation_index) local_invocation_index: u32, +#ifdef MESHLET_2D_DISPATCH + @builtin(num_workgroups) num_workgroups: vec3, +#endif +) { + var workgroup_id_1d = workgroup_id.x; + +#ifdef MESHLET_2D_DISPATCH + workgroup_id_1d += workgroup_id.y * num_workgroups.x; + if workgroup_id_1d >= meshlet_software_raster_cluster_count { return; } +#endif + + let cluster_id = workgroup_id_1d + meshlet_previous_raster_counts[0]; + let instanced_offset = meshlet_raster_clusters[cluster_id]; + var meshlet = meshlets[instanced_offset.offset]; + + let instance_uniform = meshlet_instance_uniforms[instanced_offset.instance_id]; + let world_from_local = affine3_to_square(instance_uniform.world_from_local); + + // Load and project 1 vertex per thread, and then again if there are more than 128 vertices in the meshlet + for (var i = 0u; i <= 128u; i += 128u) { + let vertex_id = local_invocation_index + i; + if vertex_id < get_meshlet_vertex_count(&meshlet) { + let vertex_position = get_meshlet_vertex_position(&meshlet, vertex_id); + + // Project vertex to viewport space + let world_position = mesh_position_local_to_world(world_from_local, vec4(vertex_position, 1.0)); + let clip_position = view.clip_from_world * vec4(world_position.xyz, 1.0); + let ndc_position = clip_position.xyz / clip_position.w; + let viewport_position_xy = ndc_to_uv(ndc_position.xy) * view.viewport.zw; + + // Write vertex to workgroup shared memory + viewport_vertices[vertex_id] = vec3(viewport_position_xy, ndc_position.z); + } + } + workgroupBarrier(); + + // Load 1 triangle's worth of vertex data per thread + let triangle_id = local_invocation_index; + if triangle_id >= get_meshlet_triangle_count(&meshlet) { return; } + let index_ids = meshlet.start_index_id + (triangle_id * 3u) + vec3(0u, 1u, 2u); + let vertex_ids = vec3(get_meshlet_vertex_id(index_ids[0]), get_meshlet_vertex_id(index_ids[1]), get_meshlet_vertex_id(index_ids[2])); + let vertex_0 = viewport_vertices[vertex_ids[2]]; + let vertex_1 = viewport_vertices[vertex_ids[1]]; + let vertex_2 = viewport_vertices[vertex_ids[0]]; + let packed_ids = (cluster_id << 7u) | triangle_id; + + // Backface culling + let triangle_double_area = edge_function(vertex_0.xy, vertex_1.xy, vertex_2.xy); + if triangle_double_area <= 0.0 { return; } + + // Setup triangle gradients + let w_x = vec3(vertex_1.y - vertex_2.y, vertex_2.y - vertex_0.y, vertex_0.y - vertex_1.y); + let w_y = vec3(vertex_2.x - vertex_1.x, vertex_0.x - vertex_2.x, vertex_1.x - vertex_0.x); + let vertices_z = vec3(vertex_0.z, vertex_1.z, vertex_2.z) / triangle_double_area; + let z_x = dot(vertices_z, w_x); + let z_y = dot(vertices_z, w_y); + + // Compute triangle bounding box + var min_x = floor(min3(vertex_0.x, vertex_1.x, vertex_2.x)); + var min_y = floor(min3(vertex_0.y, vertex_1.y, vertex_2.y)); + var max_x = ceil(max3(vertex_0.x, vertex_1.x, vertex_2.x)); + var max_y = ceil(max3(vertex_0.y, vertex_1.y, vertex_2.y)); + min_x = max(min_x, 0.0); + min_y = max(min_y, 0.0); + max_x = min(max_x, view.viewport.z - 1.0); + max_y = min(max_y, view.viewport.w - 1.0); + + // Setup initial triangle equations + let starting_pixel = vec2(min_x, min_y) + 0.5; + var w_row = vec3( + edge_function(vertex_1.xy, vertex_2.xy, starting_pixel), + edge_function(vertex_2.xy, vertex_0.xy, starting_pixel), + edge_function(vertex_0.xy, vertex_1.xy, starting_pixel), + ); + var z_row = dot(vertices_z, w_row); + + // Rasterize triangle + if subgroupAny(max_x - min_x > 4.0) { + // Scanline setup + let edge_012 = -w_x; + let open_edge = edge_012 < vec3(0.0); + let inverse_edge_012 = select(1.0 / edge_012, vec3(1e8), edge_012 == vec3(0.0)); + let max_x_diff = vec3(max_x - min_x); + for (var y = min_y; y <= max_y; y += 1.0) { + // Calculate start and end X interval for pixels in this row within the triangle + let cross_x = w_row * inverse_edge_012; + let min_x2 = select(vec3(0.0), cross_x, open_edge); + let max_x2 = select(cross_x, max_x_diff, open_edge); + var x0 = ceil(max3(min_x2[0], min_x2[1], min_x2[2])); + var x1 = min3(max_x2[0], max_x2[1], max_x2[2]); + + var w = w_row + w_x * x0; + var z = z_row + z_x * x0; + x0 += min_x; + x1 += min_x; + + // Iterate scanline X interval + for (var x = x0; x <= x1; x += 1.0) { + // Check if point at pixel is within triangle (TODO: this shouldn't be needed, but there's bugs without it) + if min3(w[0], w[1], w[2]) >= 0.0 { + write_visibility_buffer_pixel(x, y, z, packed_ids); + } + + // Increment triangle equations along the X-axis + w += w_x; + z += z_x; + } + + // Increment triangle equations along the Y-axis + w_row += w_y; + z_row += z_y; + } + } else { + // Iterate over every pixel in the triangle's bounding box + for (var y = min_y; y <= max_y; y += 1.0) { + var w = w_row; + var z = z_row; + + for (var x = min_x; x <= max_x; x += 1.0) { + // Check if point at pixel is within triangle + if min3(w[0], w[1], w[2]) >= 0.0 { + write_visibility_buffer_pixel(x, y, z, packed_ids); + } + + // Increment triangle equations along the X-axis + w += w_x; + z += z_x; + } + + // Increment triangle equations along the Y-axis + w_row += w_y; + z_row += z_y; + } + } +} + +fn write_visibility_buffer_pixel(x: f32, y: f32, z: f32, packed_ids: u32) { + let depth = bitcast(z); +#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT + let visibility = (u64(depth) << 32u) | u64(packed_ids); +#else + let visibility = depth; +#endif + textureAtomicMax(meshlet_visibility_buffer, vec2(u32(x), u32(y)), visibility); +} + +fn edge_function(a: vec2, b: vec2, c: vec2) -> f32 { + return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); +} + +fn min3(a: f32, b: f32, c: f32) -> f32 { + return min(a, min(b, c)); +} + +fn max3(a: f32, b: f32, c: f32) -> f32 { + return max(a, max(b, c)); +} diff --git a/crates/libmarathon/src/render/pbr/mod.rs b/crates/libmarathon/src/render/pbr/mod.rs new file mode 100644 index 0000000..a329455 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/mod.rs @@ -0,0 +1,390 @@ +#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![forbid(unsafe_code)] +// Doc attributes removed - these need to be at crate level if needed + +extern crate alloc; + +#[cfg(feature = "meshlet")] +mod meshlet; +pub mod wireframe; + +/// Experimental features that are not yet finished. Please report any issues you encounter! +/// +/// Expect bugs, missing features, compatibility issues, low performance, and/or future breaking changes. +#[cfg(feature = "meshlet")] +pub mod experimental { + /// Render high-poly 3d meshes using an efficient GPU-driven method. + /// See [`MeshletPlugin`](meshlet::MeshletPlugin) and [`MeshletMesh`](meshlet::MeshletMesh) for details. + pub mod meshlet { + pub use crate::render::pbr::meshlet::*; + } +} + +mod atmosphere; +mod cluster; +mod components; +pub mod decal; +pub mod deferred; +mod extended_material; +mod fog; +mod light_probe; +mod lightmap; +mod material; +mod material_bind_groups; +mod mesh_material; +mod parallax; +mod pbr_material; +mod prepass; +mod render; +mod ssao; +mod ssr; +mod volumetric_fog; + +use bevy_color::{Color, LinearRgba}; + +pub use atmosphere::*; +use bevy_light::{ + AmbientLight, DirectionalLight, PointLight, ShadowFilteringMethod, SimulationLightSystems, + SpotLight, +}; +use bevy_shader::{load_shader_library, ShaderRef}; +pub use cluster::*; +pub use components::*; +pub use decal::clustered::ClusteredDecalPlugin; +pub use extended_material::*; +pub use fog::*; +pub use light_probe::*; +pub use lightmap::*; +pub use material::*; +pub use material_bind_groups::*; +pub use mesh_material::*; +pub use parallax::*; +pub use pbr_material::*; +pub use prepass::*; +pub use render::*; +pub use ssao::*; +pub use ssr::*; +pub use volumetric_fog::VolumetricFogPlugin; + +/// The PBR prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. +pub mod prelude { + #[doc(hidden)] + pub use crate::render::pbr::{ + fog::{DistanceFog, FogFalloff}, + material::{Material, MaterialPlugin}, + mesh_material::MeshMaterial3d, + parallax::ParallaxMappingMethod, + pbr_material::StandardMaterial, + ssao::ScreenSpaceAmbientOcclusionPlugin, + }; +} + +pub mod graph { + use crate::render::render_graph::RenderLabel; + + /// Render graph nodes specific to 3D PBR rendering. + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + pub enum NodePbr { + /// Label for the shadow pass node that draws meshes that were visible + /// from the light last frame. + EarlyShadowPass, + /// Label for the shadow pass node that draws meshes that became visible + /// from the light this frame. + LateShadowPass, + /// Label for the screen space ambient occlusion render node. + ScreenSpaceAmbientOcclusion, + DeferredLightingPass, + /// Label for the volumetric lighting pass. + VolumetricFog, + /// Label for the shader that transforms and culls meshes that were + /// visible last frame. + EarlyGpuPreprocess, + /// Label for the shader that transforms and culls meshes that became + /// visible this frame. + LateGpuPreprocess, + /// Label for the screen space reflections pass. + ScreenSpaceReflections, + /// Label for the node that builds indirect draw parameters for meshes + /// that were visible last frame. + EarlyPrepassBuildIndirectParameters, + /// Label for the node that builds indirect draw parameters for meshes + /// that became visible this frame. + LatePrepassBuildIndirectParameters, + /// Label for the node that builds indirect draw parameters for the main + /// rendering pass, containing all meshes that are visible this frame. + MainBuildIndirectParameters, + ClearIndirectParametersMetadata, + } +} + +use crate::render::pbr::{deferred::DeferredPbrLightingPlugin, graph::NodePbr}; +use bevy_app::prelude::*; +use bevy_asset::{AssetApp, AssetPath, Assets, Handle, RenderAssetUsages}; +use crate::render::core_3d::graph::{Core3d, Node3d}; +use bevy_ecs::prelude::*; +#[cfg(feature = "bluenoise_texture")] +use bevy_image::{CompressedImageFormats, ImageType}; +use bevy_image::{Image, ImageSampler}; +use crate::render::{ + alpha::AlphaMode, + camera::sort_cameras, + extract_resource::ExtractResourcePlugin, + render_graph::RenderGraph, + render_resource::{ + Extent3d, TextureDataOrder, TextureDescriptor, TextureDimension, TextureFormat, + TextureUsages, + }, + sync_component::SyncComponentPlugin, + ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems, +}; + +use std::path::PathBuf; + +fn shader_ref(path: PathBuf) -> ShaderRef { + ShaderRef::Path(AssetPath::from_path_buf(path).with_source("embedded")) +} + +pub const TONEMAPPING_LUT_TEXTURE_BINDING_INDEX: u32 = 18; +pub const TONEMAPPING_LUT_SAMPLER_BINDING_INDEX: u32 = 19; + +/// Sets up the entire PBR infrastructure of bevy. +pub struct PbrPlugin { + /// Controls if the prepass is enabled for the [`StandardMaterial`]. + /// For more information about what a prepass is, see the [`bevy_core_pipeline::prepass`] docs. + pub prepass_enabled: bool, + /// Controls if [`DeferredPbrLightingPlugin`] is added. + pub add_default_deferred_lighting_plugin: bool, + /// Controls if GPU [`MeshUniform`] building is enabled. + /// + /// This requires compute shader support and so will be forcibly disabled if + /// the platform doesn't support those. + pub use_gpu_instance_buffer_builder: bool, + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl Default for PbrPlugin { + fn default() -> Self { + Self { + prepass_enabled: true, + add_default_deferred_lighting_plugin: true, + use_gpu_instance_buffer_builder: true, + debug_flags: RenderDebugFlags::default(), + } + } +} + +/// A resource that stores the spatio-temporal blue noise texture. +#[derive(Resource)] +pub struct Bluenoise { + /// Texture handle for spatio-temporal blue noise + pub texture: Handle, +} + +impl Plugin for PbrPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "render/pbr_types.wgsl"); + load_shader_library!(app, "render/pbr_bindings.wgsl"); + load_shader_library!(app, "render/utils.wgsl"); + load_shader_library!(app, "render/clustered_forward.wgsl"); + load_shader_library!(app, "render/pbr_lighting.wgsl"); + load_shader_library!(app, "render/pbr_transmission.wgsl"); + load_shader_library!(app, "render/shadows.wgsl"); + load_shader_library!(app, "deferred/pbr_deferred_types.wgsl"); + load_shader_library!(app, "deferred/pbr_deferred_functions.wgsl"); + load_shader_library!(app, "render/shadow_sampling.wgsl"); + load_shader_library!(app, "render/pbr_functions.wgsl"); + load_shader_library!(app, "render/rgb9e5.wgsl"); + load_shader_library!(app, "render/pbr_ambient.wgsl"); + load_shader_library!(app, "render/pbr_fragment.wgsl"); + load_shader_library!(app, "render/pbr.wgsl"); + load_shader_library!(app, "render/pbr_prepass_functions.wgsl"); + load_shader_library!(app, "render/pbr_prepass.wgsl"); + load_shader_library!(app, "render/parallax_mapping.wgsl"); + load_shader_library!(app, "render/view_transformations.wgsl"); + + // Setup dummy shaders for when MeshletPlugin is not used to prevent shader import errors. + load_shader_library!(app, "meshlet/dummy_visibility_buffer_resolve.wgsl"); + + app.register_asset_reflect::() + .init_resource::() + .add_plugins(( + MeshRenderPlugin { + use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, + debug_flags: self.debug_flags, + }, + MaterialsPlugin { + debug_flags: self.debug_flags, + }, + MaterialPlugin:: { + prepass_enabled: self.prepass_enabled, + debug_flags: self.debug_flags, + ..Default::default() + }, + ScreenSpaceAmbientOcclusionPlugin, + FogPlugin, + ExtractResourcePlugin::::default(), + SyncComponentPlugin::::default(), + LightmapPlugin, + LightProbePlugin, + GpuMeshPreprocessPlugin { + use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, + }, + VolumetricFogPlugin, + ScreenSpaceReflectionsPlugin, + ClusteredDecalPlugin, + )) + .add_plugins(( + decal::ForwardDecalPlugin, + SyncComponentPlugin::::default(), + SyncComponentPlugin::::default(), + SyncComponentPlugin::::default(), + SyncComponentPlugin::::default(), + )) + .add_plugins(AtmospherePlugin) + .configure_sets( + PostUpdate, + ( + SimulationLightSystems::AddClusters, + SimulationLightSystems::AssignLightsToClusters, + ) + .chain(), + ); + + if self.add_default_deferred_lighting_plugin { + app.add_plugins(DeferredPbrLightingPlugin); + } + + // Initialize the default material handle. + app.world_mut() + .resource_mut::>() + .insert( + &Handle::::default(), + StandardMaterial { + base_color: Color::srgb(1.0, 0.0, 0.5), + ..Default::default() + }, + ) + .unwrap(); + + let has_bluenoise = app + .get_sub_app(RenderApp) + .is_some_and(|render_app| render_app.world().is_resource_added::()); + + if !has_bluenoise { + let mut images = app.world_mut().resource_mut::>(); + #[cfg(feature = "bluenoise_texture")] + let handle = { + let image = Image::from_buffer( + include_bytes!("bluenoise/stbn.ktx2"), + ImageType::Extension("ktx2"), + CompressedImageFormats::NONE, + false, + ImageSampler::Default, + RenderAssetUsages::RENDER_WORLD, + ) + .expect("Failed to decode embedded blue-noise texture"); + images.add(image) + }; + + #[cfg(not(feature = "bluenoise_texture"))] + let handle = { images.add(stbn_placeholder()) }; + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .world_mut() + .insert_resource(Bluenoise { texture: handle }); + } + } + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + // Extract the required data from the main world + render_app + .add_systems( + RenderStartup, + ( + init_shadow_samplers, + init_global_clusterable_object_meta, + init_fallback_bindless_resources, + ), + ) + .add_systems( + ExtractSchedule, + ( + extract_clusters, + extract_lights, + extract_ambient_light_resource, + extract_ambient_light, + extract_shadow_filtering_method, + late_sweep_material_instances, + ), + ) + .add_systems( + Render, + ( + prepare_lights + .in_set(RenderSystems::ManageViews) + .after(sort_cameras), + prepare_clusters.in_set(RenderSystems::PrepareResources), + ), + ) + .init_resource::() + .init_resource::(); + + render_app.world_mut().add_observer(add_light_view_entities); + render_app + .world_mut() + .add_observer(remove_light_view_entities); + render_app.world_mut().add_observer(extracted_light_removed); + + let early_shadow_pass_node = EarlyShadowPassNode::from_world(render_app.world_mut()); + let late_shadow_pass_node = LateShadowPassNode::from_world(render_app.world_mut()); + let mut graph = render_app.world_mut().resource_mut::(); + let draw_3d_graph = graph.get_sub_graph_mut(Core3d).unwrap(); + draw_3d_graph.add_node(NodePbr::EarlyShadowPass, early_shadow_pass_node); + draw_3d_graph.add_node(NodePbr::LateShadowPass, late_shadow_pass_node); + draw_3d_graph.add_node_edges(( + NodePbr::EarlyShadowPass, + NodePbr::LateShadowPass, + Node3d::StartMainPass, + )); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + let global_cluster_settings = make_global_cluster_settings(render_app.world()); + app.insert_resource(global_cluster_settings); + } +} + +pub fn stbn_placeholder() -> Image { + let format = TextureFormat::Rgba8Unorm; + let data = vec![255, 0, 255, 255]; + Image { + data: Some(data), + data_order: TextureDataOrder::default(), + texture_descriptor: TextureDescriptor { + size: Extent3d::default(), + format, + dimension: TextureDimension::D2, + label: None, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + sampler: ImageSampler::Default, + texture_view_descriptor: None, + asset_usage: RenderAssetUsages::RENDER_WORLD, + copy_on_resize: false, + } +} diff --git a/crates/libmarathon/src/render/pbr/parallax.rs b/crates/libmarathon/src/render/pbr/parallax.rs new file mode 100644 index 0000000..be588ca --- /dev/null +++ b/crates/libmarathon/src/render/pbr/parallax.rs @@ -0,0 +1,47 @@ +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; + +/// The [parallax mapping] method to use to compute depth based on the +/// material's [`depth_map`]. +/// +/// Parallax Mapping uses a depth map texture to give the illusion of depth +/// variation on a mesh surface that is geometrically flat. +/// +/// See the `parallax_mapping.wgsl` shader code for implementation details +/// and explanation of the methods used. +/// +/// [`depth_map`]: crate::StandardMaterial::depth_map +/// [parallax mapping]: https://en.wikipedia.org/wiki/Parallax_mapping +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Reflect)] +#[reflect(Default, Clone, PartialEq)] +pub enum ParallaxMappingMethod { + /// A simple linear interpolation, using a single texture sample. + /// + /// This method is named "Parallax Occlusion Mapping". + /// + /// Unlike [`ParallaxMappingMethod::Relief`], only requires a single lookup, + /// but may skip small details and result in writhing material artifacts. + #[default] + Occlusion, + /// Discovers the best depth value based on binary search. + /// + /// Each iteration incurs a texture sample. + /// The result has fewer visual artifacts than [`ParallaxMappingMethod::Occlusion`]. + /// + /// This method is named "Relief Mapping". + Relief { + /// How many additional steps to use at most to find the depth value. + max_steps: u32, + }, +} + +impl ParallaxMappingMethod { + /// [`ParallaxMappingMethod::Relief`] with a 5 steps, a reasonable default. + pub const DEFAULT_RELIEF_MAPPING: Self = ParallaxMappingMethod::Relief { max_steps: 5 }; + + pub(crate) fn max_steps(&self) -> u32 { + match self { + ParallaxMappingMethod::Occlusion => 0, + ParallaxMappingMethod::Relief { max_steps } => *max_steps, + } + } +} diff --git a/crates/libmarathon/src/render/pbr/pbr_material.rs b/crates/libmarathon/src/render/pbr/pbr_material.rs new file mode 100644 index 0000000..c16a441 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/pbr_material.rs @@ -0,0 +1,1554 @@ +use bevy_asset::Asset; +use bevy_color::{Alpha, ColorToComponents}; +use bevy_math::{Affine2, Affine3, Mat2, Mat3, Vec2, Vec3, Vec4}; +use bevy_mesh::MeshVertexBufferLayoutRef; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use crate::render::{render_asset::RenderAssets, render_resource::*, texture::GpuImage}; +use bitflags::bitflags; + +use crate::render::pbr::{deferred::DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID, *}; + +/// An enum to define which UV attribute to use for a texture. +/// +/// It is used for every texture in the [`StandardMaterial`]. +/// It only supports two UV attributes, [`bevy_mesh::Mesh::ATTRIBUTE_UV_0`] and +/// [`bevy_mesh::Mesh::ATTRIBUTE_UV_1`]. +/// The default is [`UvChannel::Uv0`]. +#[derive(Reflect, Default, Debug, Clone, PartialEq, Eq)] +#[reflect(Default, Debug, Clone, PartialEq)] +pub enum UvChannel { + #[default] + Uv0, + Uv1, +} + +/// A material with "standard" properties used in PBR lighting. +/// Standard property values with pictures here: +/// . +/// +/// May be created directly from a [`Color`] or an [`Image`]. +#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)] +#[bind_group_data(StandardMaterialKey)] +#[data(0, StandardMaterialUniform, binding_array(10))] +#[bindless(index_table(range(0..31)))] +#[reflect(Default, Debug, Clone)] +pub struct StandardMaterial { + /// The color of the surface of the material before lighting. + /// + /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything + /// in between. If used together with a `base_color_texture`, this is factored into the final + /// base color as `base_color * base_color_texture_value`. + /// + /// Defaults to [`Color::WHITE`]. + pub base_color: Color, + + /// The UV channel to use for the [`StandardMaterial::base_color_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub base_color_channel: UvChannel, + + /// The texture component of the material's color before lighting. + /// The actual pre-lighting color is `base_color * this_texture`. + /// + /// See [`base_color`] for details. + /// + /// You should set `base_color` to [`Color::WHITE`] (the default) + /// if you want the texture to show as-is. + /// + /// Setting `base_color` to something else than white will tint + /// the texture. For example, setting `base_color` to pure red will + /// tint the texture red. + /// + /// [`base_color`]: StandardMaterial::base_color + #[texture(1)] + #[sampler(2)] + #[dependency] + pub base_color_texture: Option>, + + // Use a color for user friendliness even though we technically don't use the alpha channel + // Might be used in the future for exposure correction in HDR + /// Color the material "emits" to the camera. + /// + /// This is typically used for monitor screens or LED lights. + /// Anything that can be visible even in darkness. + /// + /// The emissive color is added to what would otherwise be the material's visible color. + /// This means that for a light emissive value, in darkness, + /// you will mostly see the emissive component. + /// + /// The default emissive color is [`LinearRgba::BLACK`], which doesn't add anything to the material color. + /// + /// Emissive strength is controlled by the value of the color channels, + /// while the hue is controlled by their relative values. + /// + /// As a result, channel values for `emissive` + /// colors can exceed `1.0`. For instance, a `base_color` of + /// `LinearRgba::rgb(1.0, 0.0, 0.0)` represents the brightest + /// red for objects that reflect light, but an emissive color + /// like `LinearRgba::rgb(1000.0, 0.0, 0.0)` can be used to create + /// intensely bright red emissive effects. + /// + /// This results in a final luminance value when multiplied + /// by the value of the greyscale emissive texture (which ranges from 0 for black to 1 for white). + /// Luminance is a measure of the amount of light emitted per unit area, + /// and can be thought of as the "brightness" of the effect. + /// In Bevy, we treat these luminance values as the physical units of cd/m², aka nits. + /// + /// Increasing the emissive strength of the color will impact visual effects + /// like bloom, but it's important to note that **an emissive material won't + /// typically light up surrounding areas like a light source**, + /// it just adds a value to the color seen on screen. + pub emissive: LinearRgba, + + /// The weight in which the camera exposure influences the emissive color. + /// A value of `0.0` means the emissive color is not affected by the camera exposure. + /// In opposition, a value of `1.0` means the emissive color is multiplied by the camera exposure. + /// + /// Defaults to `0.0` + pub emissive_exposure_weight: f32, + + /// The UV channel to use for the [`StandardMaterial::emissive_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub emissive_channel: UvChannel, + + /// The emissive map, multiplies pixels with [`emissive`] + /// to get the final "emitting" color of a surface. + /// + /// This color is multiplied by [`emissive`] to get the final emitted color. + /// Meaning that you should set [`emissive`] to [`Color::WHITE`] + /// if you want to use the full range of color of the emissive texture. + /// + /// [`emissive`]: StandardMaterial::emissive + #[texture(3)] + #[sampler(4)] + #[dependency] + pub emissive_texture: Option>, + + /// Linear perceptual roughness, clamped to `[0.089, 1.0]` in the shader. + /// + /// Defaults to `0.5`. + /// + /// Low values result in a "glossy" material with specular highlights, + /// while values close to `1` result in rough materials. + /// + /// If used together with a roughness/metallic texture, this is factored into the final base + /// color as `roughness * roughness_texture_value`. + /// + /// 0.089 is the minimum floating point value that won't be rounded down to 0 in the + /// calculations used. + // Technically for 32-bit floats, 0.045 could be used. + // See + pub perceptual_roughness: f32, + + /// How "metallic" the material appears, within `[0.0, 1.0]`. + /// + /// This should be set to 0.0 for dielectric materials or 1.0 for metallic materials. + /// For a hybrid surface such as corroded metal, you may need to use in-between values. + /// + /// Defaults to `0.00`, for dielectric. + /// + /// If used together with a roughness/metallic texture, this is factored into the final base + /// color as `metallic * metallic_texture_value`. + pub metallic: f32, + + /// The UV channel to use for the [`StandardMaterial::metallic_roughness_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub metallic_roughness_channel: UvChannel, + + /// Metallic and roughness maps, stored as a single texture. + /// + /// The blue channel contains metallic values, + /// and the green channel contains the roughness values. + /// Other channels are unused. + /// + /// Those values are multiplied by the scalar ones of the material, + /// see [`metallic`] and [`perceptual_roughness`] for details. + /// + /// Note that with the default values of [`metallic`] and [`perceptual_roughness`], + /// setting this texture has no effect. If you want to exclusively use the + /// `metallic_roughness_texture` values for your material, make sure to set [`metallic`] + /// and [`perceptual_roughness`] to `1.0`. + /// + /// [`metallic`]: StandardMaterial::metallic + /// [`perceptual_roughness`]: StandardMaterial::perceptual_roughness + #[texture(5)] + #[sampler(6)] + #[dependency] + pub metallic_roughness_texture: Option>, + + /// Specular intensity for non-metals on a linear scale of `[0.0, 1.0]`. + /// + /// Use the value as a way to control the intensity of the + /// specular highlight of the material, i.e. how reflective is the material, + /// rather than the physical property "reflectance." + /// + /// Set to `0.0`, no specular highlight is visible, the highlight is strongest + /// when `reflectance` is set to `1.0`. + /// + /// Defaults to `0.5` which is mapped to 4% reflectance in the shader. + #[doc(alias = "specular_intensity")] + pub reflectance: f32, + + /// A color with which to modulate the [`StandardMaterial::reflectance`] for + /// non-metals. + /// + /// The specular highlights and reflection are tinted with this color. Note + /// that it has no effect for non-metals. + /// + /// This feature is currently unsupported in the deferred rendering path, in + /// order to reduce the size of the geometry buffers. + /// + /// Defaults to [`Color::WHITE`]. + #[doc(alias = "specular_color")] + pub specular_tint: Color, + + /// The amount of light transmitted _diffusely_ through the material (i.e. “translucency”). + /// + /// Implemented as a second, flipped [Lambertian diffuse](https://en.wikipedia.org/wiki/Lambertian_reflectance) lobe, + /// which provides an inexpensive but plausible approximation of translucency for thin dielectric objects (e.g. paper, + /// leaves, some fabrics) or thicker volumetric materials with short scattering distances (e.g. porcelain, wax). + /// + /// For specular transmission usecases with refraction (e.g. glass) use the [`StandardMaterial::specular_transmission`] and + /// [`StandardMaterial::ior`] properties instead. + /// + /// - When set to `0.0` (the default) no diffuse light is transmitted; + /// - When set to `1.0` all diffuse light is transmitted through the material; + /// - Values higher than `0.5` will cause more diffuse light to be transmitted than reflected, resulting in a “darker” + /// appearance on the side facing the light than the opposite side. (e.g. plant leaves) + /// + /// ## Notes + /// + /// - The material's [`StandardMaterial::base_color`] also modulates the transmitted light; + /// - To receive transmitted shadows on the diffuse transmission lobe (i.e. the “backside”) of the material, + /// use the [`TransmittedShadowReceiver`](bevy_light::TransmittedShadowReceiver) component. + #[doc(alias = "translucency")] + pub diffuse_transmission: f32, + + /// The UV channel to use for the [`StandardMaterial::diffuse_transmission_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_transmission_textures")] + pub diffuse_transmission_channel: UvChannel, + + /// A map that modulates diffuse transmission via its alpha channel. Multiplied by [`StandardMaterial::diffuse_transmission`] + /// to obtain the final result. + /// + /// **Important:** The [`StandardMaterial::diffuse_transmission`] property must be set to a value higher than 0.0, + /// or this texture won't have any effect. + #[cfg_attr(feature = "pbr_transmission_textures", texture(19))] + #[cfg_attr(feature = "pbr_transmission_textures", sampler(20))] + #[cfg(feature = "pbr_transmission_textures")] + pub diffuse_transmission_texture: Option>, + + /// The amount of light transmitted _specularly_ through the material (i.e. via refraction). + /// + /// - When set to `0.0` (the default) no light is transmitted. + /// - When set to `1.0` all light is transmitted through the material. + /// + /// The material's [`StandardMaterial::base_color`] also modulates the transmitted light. + /// + /// **Note:** Typically used in conjunction with [`StandardMaterial::thickness`], [`StandardMaterial::ior`] and [`StandardMaterial::perceptual_roughness`]. + /// + /// ## Performance + /// + /// Specular transmission is implemented as a relatively expensive screen-space effect that allows occluded objects to be seen through the material, + /// with distortion and blur effects. + /// + /// - [`Camera3d::screen_space_specular_transmission_steps`](bevy_camera::Camera3d::screen_space_specular_transmission_steps) can be used to enable transmissive objects + /// to be seen through other transmissive objects, at the cost of additional draw calls and texture copies; (Use with caution!) + /// - If a simplified approximation of specular transmission using only environment map lighting is sufficient, consider setting + /// [`Camera3d::screen_space_specular_transmission_steps`](bevy_camera::Camera3d::screen_space_specular_transmission_steps) to `0`. + /// - If purely diffuse light transmission is needed, (i.e. “translucency”) consider using [`StandardMaterial::diffuse_transmission`] instead, + /// for a much less expensive effect. + /// - Specular transmission is rendered before alpha blending, so any material with [`AlphaMode::Blend`], [`AlphaMode::Premultiplied`], [`AlphaMode::Add`] or [`AlphaMode::Multiply`] + /// won't be visible through specular transmissive materials. + #[doc(alias = "refraction")] + pub specular_transmission: f32, + + /// The UV channel to use for the [`StandardMaterial::specular_transmission_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_transmission_textures")] + pub specular_transmission_channel: UvChannel, + + /// A map that modulates specular transmission via its red channel. Multiplied by [`StandardMaterial::specular_transmission`] + /// to obtain the final result. + /// + /// **Important:** The [`StandardMaterial::specular_transmission`] property must be set to a value higher than 0.0, + /// or this texture won't have any effect. + #[cfg_attr(feature = "pbr_transmission_textures", texture(15))] + #[cfg_attr(feature = "pbr_transmission_textures", sampler(16))] + #[cfg(feature = "pbr_transmission_textures")] + pub specular_transmission_texture: Option>, + + /// Thickness of the volume beneath the material surface. + /// + /// When set to `0.0` (the default) the material appears as an infinitely-thin film, + /// transmitting light without distorting it. + /// + /// When set to any other value, the material distorts light like a thick lens. + /// + /// **Note:** Typically used in conjunction with [`StandardMaterial::specular_transmission`] and [`StandardMaterial::ior`], or with + /// [`StandardMaterial::diffuse_transmission`]. + #[doc(alias = "volume")] + #[doc(alias = "thin_walled")] + pub thickness: f32, + + /// The UV channel to use for the [`StandardMaterial::thickness_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_transmission_textures")] + pub thickness_channel: UvChannel, + + /// A map that modulates thickness via its green channel. Multiplied by [`StandardMaterial::thickness`] + /// to obtain the final result. + /// + /// **Important:** The [`StandardMaterial::thickness`] property must be set to a value higher than 0.0, + /// or this texture won't have any effect. + #[cfg_attr(feature = "pbr_transmission_textures", texture(17))] + #[cfg_attr(feature = "pbr_transmission_textures", sampler(18))] + #[cfg(feature = "pbr_transmission_textures")] + pub thickness_texture: Option>, + + /// The [index of refraction](https://en.wikipedia.org/wiki/Refractive_index) of the material. + /// + /// Defaults to 1.5. + /// + /// | Material | Index of Refraction | + /// |:----------------|:---------------------| + /// | Vacuum | 1 | + /// | Air | 1.00 | + /// | Ice | 1.31 | + /// | Water | 1.33 | + /// | Eyes | 1.38 | + /// | Quartz | 1.46 | + /// | Olive Oil | 1.47 | + /// | Honey | 1.49 | + /// | Acrylic | 1.49 | + /// | Window Glass | 1.52 | + /// | Polycarbonate | 1.58 | + /// | Flint Glass | 1.69 | + /// | Ruby | 1.71 | + /// | Glycerine | 1.74 | + /// | Sapphire | 1.77 | + /// | Cubic Zirconia | 2.15 | + /// | Diamond | 2.42 | + /// | Moissanite | 2.65 | + /// + /// **Note:** Typically used in conjunction with [`StandardMaterial::specular_transmission`] and [`StandardMaterial::thickness`]. + #[doc(alias = "index_of_refraction")] + #[doc(alias = "refraction_index")] + #[doc(alias = "refractive_index")] + pub ior: f32, + + /// How far, on average, light travels through the volume beneath the material's + /// surface before being absorbed. + /// + /// Defaults to [`f32::INFINITY`], i.e. light is never absorbed. + /// + /// **Note:** To have any effect, must be used in conjunction with: + /// - [`StandardMaterial::attenuation_color`]; + /// - [`StandardMaterial::thickness`]; + /// - [`StandardMaterial::diffuse_transmission`] or [`StandardMaterial::specular_transmission`]. + #[doc(alias = "absorption_distance")] + #[doc(alias = "extinction_distance")] + pub attenuation_distance: f32, + + /// The resulting (non-absorbed) color after white light travels through the attenuation distance. + /// + /// Defaults to [`Color::WHITE`], i.e. no change. + /// + /// **Note:** To have any effect, must be used in conjunction with: + /// - [`StandardMaterial::attenuation_distance`]; + /// - [`StandardMaterial::thickness`]; + /// - [`StandardMaterial::diffuse_transmission`] or [`StandardMaterial::specular_transmission`]. + #[doc(alias = "absorption_color")] + #[doc(alias = "extinction_color")] + pub attenuation_color: Color, + + /// The UV channel to use for the [`StandardMaterial::normal_map_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub normal_map_channel: UvChannel, + + /// Used to fake the lighting of bumps and dents on a material. + /// + /// A typical usage would be faking cobblestones on a flat plane mesh in 3D. + /// + /// # Notes + /// + /// Normal mapping with `StandardMaterial` and the core bevy PBR shaders requires: + /// - A normal map texture + /// - Vertex UVs + /// - Vertex tangents + /// - Vertex normals + /// + /// Tangents do not have to be stored in your model, + /// they can be generated using the [`Mesh::generate_tangents`] or + /// [`Mesh::with_generated_tangents`] methods. + /// If your material has a normal map, but still renders as a flat surface, + /// make sure your meshes have their tangents set. + /// + /// [`Mesh::generate_tangents`]: bevy_mesh::Mesh::generate_tangents + /// [`Mesh::with_generated_tangents`]: bevy_mesh::Mesh::with_generated_tangents + /// + /// # Usage + /// + /// ``` + /// # use bevy_asset::{AssetServer, Handle}; + /// # use bevy_ecs::change_detection::Res; + /// # use bevy_image::{Image, ImageLoaderSettings}; + /// # + /// fn load_normal_map(asset_server: Res) { + /// let normal_handle: Handle = asset_server.load_with_settings( + /// "textures/parallax_example/cube_normal.png", + /// // The normal map texture is in linear color space. Lighting won't look correct + /// // if `is_srgb` is `true`, which is the default. + /// |settings: &mut ImageLoaderSettings| settings.is_srgb = false, + /// ); + /// } + /// ``` + #[texture(9)] + #[sampler(10)] + #[dependency] + pub normal_map_texture: Option>, + + /// Normal map textures authored for DirectX have their y-component flipped. Set this to flip + /// it to right-handed conventions. + pub flip_normal_map_y: bool, + + /// The UV channel to use for the [`StandardMaterial::occlusion_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + pub occlusion_channel: UvChannel, + + /// Specifies the level of exposure to ambient light. + /// + /// This is usually generated and stored automatically ("baked") by 3D-modeling software. + /// + /// Typically, steep concave parts of a model (such as the armpit of a shirt) are darker, + /// because they have little exposure to light. + /// An occlusion map specifies those parts of the model that light doesn't reach well. + /// + /// The material will be less lit in places where this texture is dark. + /// This is similar to ambient occlusion, but built into the model. + #[texture(7)] + #[sampler(8)] + #[dependency] + pub occlusion_texture: Option>, + + /// The UV channel to use for the [`StandardMaterial::specular_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_specular_textures")] + pub specular_channel: UvChannel, + + /// A map that specifies reflectance for non-metallic materials. + /// + /// Alpha values from [0.0, 1.0] in this texture are linearly mapped to + /// reflectance values of [0.0, 0.5] and multiplied by the constant + /// [`StandardMaterial::reflectance`] value. This follows the + /// `KHR_materials_specular` specification. The map will have no effect if + /// the material is fully metallic. + /// + /// When using this map, you may wish to set the + /// [`StandardMaterial::reflectance`] value to 2.0 so that this map can + /// express the full [0.0, 1.0] range of values. + /// + /// Note that, because the reflectance is stored in the alpha channel, and + /// the [`StandardMaterial::specular_tint_texture`] has no alpha value, it + /// may be desirable to pack the values together and supply the same + /// texture to both fields. + #[cfg_attr(feature = "pbr_specular_textures", texture(27))] + #[cfg_attr(feature = "pbr_specular_textures", sampler(28))] + #[cfg(feature = "pbr_specular_textures")] + pub specular_texture: Option>, + + /// The UV channel to use for the + /// [`StandardMaterial::specular_tint_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_specular_textures")] + pub specular_tint_channel: UvChannel, + + /// A map that specifies color adjustment to be applied to the specular + /// reflection for non-metallic materials. + /// + /// The RGB values of this texture modulate the + /// [`StandardMaterial::specular_tint`] value. See the documentation for + /// that field for more information. + /// + /// Like the fixed specular tint value, this texture map isn't supported in + /// the deferred renderer. + #[cfg_attr(feature = "pbr_specular_textures", texture(29))] + #[cfg_attr(feature = "pbr_specular_textures", sampler(30))] + #[cfg(feature = "pbr_specular_textures")] + pub specular_tint_texture: Option>, + + /// An extra thin translucent layer on top of the main PBR layer. This is + /// typically used for painted surfaces. + /// + /// This value specifies the strength of the layer, which affects how + /// visible the clearcoat layer will be. + /// + /// Defaults to zero, specifying no clearcoat layer. + pub clearcoat: f32, + + /// The UV channel to use for the [`StandardMaterial::clearcoat_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_channel: UvChannel, + + /// An image texture that specifies the strength of the clearcoat layer in + /// the red channel. Values sampled from this texture are multiplied by the + /// main [`StandardMaterial::clearcoat`] factor. + /// + /// As this is a non-color map, it must not be loaded as sRGB. + #[cfg_attr(feature = "pbr_multi_layer_material_textures", texture(21))] + #[cfg_attr(feature = "pbr_multi_layer_material_textures", sampler(22))] + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_texture: Option>, + + /// The roughness of the clearcoat material. This is specified in exactly + /// the same way as the [`StandardMaterial::perceptual_roughness`]. + /// + /// If the [`StandardMaterial::clearcoat`] value if zero, this has no + /// effect. + /// + /// Defaults to 0.5. + pub clearcoat_perceptual_roughness: f32, + + /// The UV channel to use for the [`StandardMaterial::clearcoat_roughness_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_roughness_channel: UvChannel, + + /// An image texture that specifies the roughness of the clearcoat level in + /// the green channel. Values from this texture are multiplied by the main + /// [`StandardMaterial::clearcoat_perceptual_roughness`] factor. + /// + /// As this is a non-color map, it must not be loaded as sRGB. + #[cfg_attr(feature = "pbr_multi_layer_material_textures", texture(23))] + #[cfg_attr(feature = "pbr_multi_layer_material_textures", sampler(24))] + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_roughness_texture: Option>, + + /// The UV channel to use for the [`StandardMaterial::clearcoat_normal_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_normal_channel: UvChannel, + + /// An image texture that specifies a normal map that is to be applied to + /// the clearcoat layer. This can be used to simulate, for example, + /// scratches on an outer layer of varnish. Normal maps are in the same + /// format as [`StandardMaterial::normal_map_texture`]. + /// + /// Note that, if a clearcoat normal map isn't specified, the main normal + /// map, if any, won't be applied to the clearcoat. If you want a normal map + /// that applies to both the main material and to the clearcoat, specify it + /// in both [`StandardMaterial::normal_map_texture`] and this field. + /// + /// As this is a non-color map, it must not be loaded as sRGB. + #[cfg_attr(feature = "pbr_multi_layer_material_textures", texture(25))] + #[cfg_attr(feature = "pbr_multi_layer_material_textures", sampler(26))] + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub clearcoat_normal_texture: Option>, + + /// Increases the roughness along a specific direction, so that the specular + /// highlight will be stretched instead of being a circular lobe. + /// + /// This value ranges from 0 (perfectly circular) to 1 (maximally + /// stretched). The default direction (corresponding to a + /// [`StandardMaterial::anisotropy_rotation`] of 0) aligns with the + /// *tangent* of the mesh; thus mesh tangents must be specified in order for + /// this parameter to have any meaning. The direction can be changed using + /// the [`StandardMaterial::anisotropy_rotation`] parameter. + /// + /// This is typically used for modeling surfaces such as brushed metal and + /// hair, in which one direction of the surface but not the other is smooth. + /// + /// See the [`KHR_materials_anisotropy` specification] for more details. + /// + /// [`KHR_materials_anisotropy` specification]: + /// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md + pub anisotropy_strength: f32, + + /// The direction of increased roughness, in radians relative to the mesh + /// tangent. + /// + /// This parameter causes the roughness to vary according to the + /// [`StandardMaterial::anisotropy_strength`]. The rotation is applied in + /// tangent-bitangent space; thus, mesh tangents must be present for this + /// parameter to have any meaning. + /// + /// This parameter has no effect if + /// [`StandardMaterial::anisotropy_strength`] is zero. Its value can + /// optionally be adjusted across the mesh with the + /// [`StandardMaterial::anisotropy_texture`]. + /// + /// See the [`KHR_materials_anisotropy` specification] for more details. + /// + /// [`KHR_materials_anisotropy` specification]: + /// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md + pub anisotropy_rotation: f32, + + /// The UV channel to use for the [`StandardMaterial::anisotropy_texture`]. + /// + /// Defaults to [`UvChannel::Uv0`]. + #[cfg(feature = "pbr_anisotropy_texture")] + pub anisotropy_channel: UvChannel, + + /// An image texture that allows the + /// [`StandardMaterial::anisotropy_strength`] and + /// [`StandardMaterial::anisotropy_rotation`] to vary across the mesh. + /// + /// The [`KHR_materials_anisotropy` specification] defines the format that + /// this texture must take. To summarize: the direction vector is encoded in + /// the red and green channels, while the strength is encoded in the blue + /// channels. For the direction vector, the red and green channels map the + /// color range [0, 1] to the vector range [-1, 1]. The direction vector + /// encoded in this texture modifies the default rotation direction in + /// tangent-bitangent space, before the + /// [`StandardMaterial::anisotropy_rotation`] parameter is applied. The + /// value in the blue channel is multiplied by the + /// [`StandardMaterial::anisotropy_strength`] value to produce the final + /// anisotropy strength. + /// + /// As the texel values don't represent colors, this texture must be in + /// linear color space, not sRGB. + /// + /// [`KHR_materials_anisotropy` specification]: + /// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md + #[cfg_attr(feature = "pbr_anisotropy_texture", texture(13))] + #[cfg_attr(feature = "pbr_anisotropy_texture", sampler(14))] + #[cfg(feature = "pbr_anisotropy_texture")] + pub anisotropy_texture: Option>, + + /// Support two-sided lighting by automatically flipping the normals for "back" faces + /// within the PBR lighting shader. + /// + /// Defaults to `false`. + /// This does not automatically configure backface culling, + /// which can be done via `cull_mode`. + pub double_sided: bool, + + /// Whether to cull the "front", "back" or neither side of a mesh. + /// If set to `None`, the two sides of the mesh are visible. + /// + /// Defaults to `Some(Face::Back)`. + /// In bevy, the order of declaration of a triangle's vertices + /// in [`Mesh`] defines the triangle's front face. + /// + /// When a triangle is in a viewport, + /// if its vertices appear counter-clockwise from the viewport's perspective, + /// then the viewport is seeing the triangle's front face. + /// Conversely, if the vertices appear clockwise, you are seeing the back face. + /// + /// In short, in bevy, front faces winds counter-clockwise. + /// + /// Your 3D editing software should manage all of that. + /// + /// [`Mesh`]: bevy_mesh::Mesh + // TODO: include this in reflection somehow (maybe via remote types like serde https://serde.rs/remote-derive.html) + #[reflect(ignore, clone)] + pub cull_mode: Option, + + /// Whether to apply only the base color to this material. + /// + /// Normals, occlusion textures, roughness, metallic, reflectance, emissive, + /// shadows, alpha mode and ambient light are ignored if this is set to `true`. + pub unlit: bool, + + /// Whether to enable fog for this material. + pub fog_enabled: bool, + + /// How to apply the alpha channel of the `base_color_texture`. + /// + /// See [`AlphaMode`] for details. Defaults to [`AlphaMode::Opaque`]. + pub alpha_mode: AlphaMode, + + /// Adjust rendered depth. + /// + /// A material with a positive depth bias will render closer to the + /// camera while negative values cause the material to render behind + /// other objects. This is independent of the viewport. + /// + /// `depth_bias` affects render ordering and depth write operations + /// using the `wgpu::DepthBiasState::Constant` field. + /// + /// [z-fighting]: https://en.wikipedia.org/wiki/Z-fighting + pub depth_bias: f32, + + /// The depth map used for [parallax mapping]. + /// + /// It is a grayscale image where white represents bottom and black the top. + /// If this field is set, bevy will apply [parallax mapping]. + /// Parallax mapping, unlike simple normal maps, will move the texture + /// coordinate according to the current perspective, + /// giving actual depth to the texture. + /// + /// The visual result is similar to a displacement map, + /// but does not require additional geometry. + /// + /// Use the [`parallax_depth_scale`] field to control the depth of the parallax. + /// + /// ## Limitations + /// + /// - It will look weird on bent/non-planar surfaces. + /// - The depth of the pixel does not reflect its visual position, resulting + /// in artifacts for depth-dependent features such as fog or SSAO. + /// - For the same reason, the geometry silhouette will always be + /// the one of the actual geometry, not the parallaxed version, resulting + /// in awkward looks on intersecting parallaxed surfaces. + /// + /// ## Performance + /// + /// Parallax mapping requires multiple texture lookups, proportional to + /// [`max_parallax_layer_count`], which might be costly. + /// + /// Use the [`parallax_mapping_method`] and [`max_parallax_layer_count`] fields + /// to tweak the shader, trading graphical quality for performance. + /// + /// To improve performance, set your `depth_map`'s [`Image::sampler`] + /// filter mode to `FilterMode::Nearest`, as [this paper] indicates, it improves + /// performance a bit. + /// + /// To reduce artifacts, avoid steep changes in depth, blurring the depth + /// map helps with this. + /// + /// Larger depth maps haves a disproportionate performance impact. + /// + /// [this paper]: https://www.diva-portal.org/smash/get/diva2:831762/FULLTEXT01.pdf + /// [parallax mapping]: https://en.wikipedia.org/wiki/Parallax_mapping + /// [`parallax_depth_scale`]: StandardMaterial::parallax_depth_scale + /// [`parallax_mapping_method`]: StandardMaterial::parallax_mapping_method + /// [`max_parallax_layer_count`]: StandardMaterial::max_parallax_layer_count + #[texture(11)] + #[sampler(12)] + #[dependency] + pub depth_map: Option>, + + /// How deep the offset introduced by the depth map should be. + /// + /// Default is `0.1`, anything over that value may look distorted. + /// Lower values lessen the effect. + /// + /// The depth is relative to texture size. This means that if your texture + /// occupies a surface of `1` world unit, and `parallax_depth_scale` is `0.1`, then + /// the in-world depth will be of `0.1` world units. + /// If the texture stretches for `10` world units, then the final depth + /// will be of `1` world unit. + pub parallax_depth_scale: f32, + + /// Which parallax mapping method to use. + /// + /// We recommend that all objects use the same [`ParallaxMappingMethod`], to avoid + /// duplicating and running two shaders. + pub parallax_mapping_method: ParallaxMappingMethod, + + /// In how many layers to split the depth maps for parallax mapping. + /// + /// If you are seeing jaggy edges, increase this value. + /// However, this incurs a performance cost. + /// + /// Dependent on the situation, switching to [`ParallaxMappingMethod::Relief`] + /// and keeping this value low might have better performance than increasing the + /// layer count while using [`ParallaxMappingMethod::Occlusion`]. + /// + /// Default is `16.0`. + pub max_parallax_layer_count: f32, + + /// The exposure (brightness) level of the lightmap, if present. + pub lightmap_exposure: f32, + + /// Render method used for opaque materials. (Where `alpha_mode` is [`AlphaMode::Opaque`] or [`AlphaMode::Mask`]) + pub opaque_render_method: OpaqueRendererMethod, + + /// Used for selecting the deferred lighting pass for deferred materials. + /// Default is [`DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID`] for default + /// PBR deferred lighting pass. Ignored in the case of forward materials. + pub deferred_lighting_pass_id: u8, + + /// The transform applied to the UVs corresponding to `ATTRIBUTE_UV_0` on the mesh before sampling. Default is identity. + pub uv_transform: Affine2, +} + +impl StandardMaterial { + /// Horizontal flipping transform + /// + /// Multiplying this with another Affine2 returns transformation with horizontally flipped texture coords + pub const FLIP_HORIZONTAL: Affine2 = Affine2 { + matrix2: Mat2::from_cols(Vec2::new(-1.0, 0.0), Vec2::Y), + translation: Vec2::X, + }; + + /// Vertical flipping transform + /// + /// Multiplying this with another Affine2 returns transformation with vertically flipped texture coords + pub const FLIP_VERTICAL: Affine2 = Affine2 { + matrix2: Mat2::from_cols(Vec2::X, Vec2::new(0.0, -1.0)), + translation: Vec2::Y, + }; + + /// Flipping X 3D transform + /// + /// Multiplying this with another Affine3 returns transformation with flipped X coords + pub const FLIP_X: Affine3 = Affine3 { + matrix3: Mat3::from_cols(Vec3::new(-1.0, 0.0, 0.0), Vec3::Y, Vec3::Z), + translation: Vec3::X, + }; + + /// Flipping Y 3D transform + /// + /// Multiplying this with another Affine3 returns transformation with flipped Y coords + pub const FLIP_Y: Affine3 = Affine3 { + matrix3: Mat3::from_cols(Vec3::X, Vec3::new(0.0, -1.0, 0.0), Vec3::Z), + translation: Vec3::Y, + }; + + /// Flipping Z 3D transform + /// + /// Multiplying this with another Affine3 returns transformation with flipped Z coords + pub const FLIP_Z: Affine3 = Affine3 { + matrix3: Mat3::from_cols(Vec3::X, Vec3::Y, Vec3::new(0.0, 0.0, -1.0)), + translation: Vec3::Z, + }; + + /// Flip the texture coordinates of the material. + pub fn flip(&mut self, horizontal: bool, vertical: bool) { + if horizontal { + // Multiplication of `Affine2` is order dependent, which is why + // we do not use the `*=` operator. + self.uv_transform = Self::FLIP_HORIZONTAL * self.uv_transform; + } + if vertical { + self.uv_transform = Self::FLIP_VERTICAL * self.uv_transform; + } + } + + /// Consumes the material and returns a material with flipped texture coordinates + pub fn flipped(mut self, horizontal: bool, vertical: bool) -> Self { + self.flip(horizontal, vertical); + self + } + + /// Creates a new material from a given color + pub fn from_color(color: impl Into) -> Self { + Self::from(color.into()) + } +} + +impl Default for StandardMaterial { + fn default() -> Self { + StandardMaterial { + // White because it gets multiplied with texture values if someone uses + // a texture. + base_color: Color::WHITE, + base_color_channel: UvChannel::Uv0, + base_color_texture: None, + emissive: LinearRgba::BLACK, + emissive_exposure_weight: 0.0, + emissive_channel: UvChannel::Uv0, + emissive_texture: None, + // Matches Blender's default roughness. + perceptual_roughness: 0.5, + // Metallic should generally be set to 0.0 or 1.0. + metallic: 0.0, + metallic_roughness_channel: UvChannel::Uv0, + metallic_roughness_texture: None, + // Minimum real-world reflectance is 2%, most materials between 2-5% + // Expressed in a linear scale and equivalent to 4% reflectance see + // + reflectance: 0.5, + diffuse_transmission: 0.0, + #[cfg(feature = "pbr_transmission_textures")] + diffuse_transmission_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_transmission_textures")] + diffuse_transmission_texture: None, + specular_transmission: 0.0, + #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_texture: None, + thickness: 0.0, + #[cfg(feature = "pbr_transmission_textures")] + thickness_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_transmission_textures")] + thickness_texture: None, + ior: 1.5, + attenuation_color: Color::WHITE, + attenuation_distance: f32::INFINITY, + occlusion_channel: UvChannel::Uv0, + occlusion_texture: None, + normal_map_channel: UvChannel::Uv0, + normal_map_texture: None, + #[cfg(feature = "pbr_specular_textures")] + specular_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_specular_textures")] + specular_texture: None, + specular_tint: Color::WHITE, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_texture: None, + clearcoat: 0.0, + clearcoat_perceptual_roughness: 0.5, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture: None, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture: None, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture: None, + anisotropy_strength: 0.0, + anisotropy_rotation: 0.0, + #[cfg(feature = "pbr_anisotropy_texture")] + anisotropy_channel: UvChannel::Uv0, + #[cfg(feature = "pbr_anisotropy_texture")] + anisotropy_texture: None, + flip_normal_map_y: false, + double_sided: false, + cull_mode: Some(Face::Back), + unlit: false, + fog_enabled: true, + alpha_mode: AlphaMode::Opaque, + depth_bias: 0.0, + depth_map: None, + parallax_depth_scale: 0.1, + max_parallax_layer_count: 16.0, + lightmap_exposure: 1.0, + parallax_mapping_method: ParallaxMappingMethod::Occlusion, + opaque_render_method: OpaqueRendererMethod::Auto, + deferred_lighting_pass_id: DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID, + uv_transform: Affine2::IDENTITY, + } + } +} + +impl From for StandardMaterial { + fn from(color: Color) -> Self { + StandardMaterial { + base_color: color, + alpha_mode: if color.alpha() < 1.0 { + AlphaMode::Blend + } else { + AlphaMode::Opaque + }, + ..Default::default() + } + } +} + +impl From> for StandardMaterial { + fn from(texture: Handle) -> Self { + StandardMaterial { + base_color_texture: Some(texture), + ..Default::default() + } + } +} + +// NOTE: These must match the bit flags in bevy_pbr/src/render/pbr_types.wgsl! +bitflags::bitflags! { + /// Bitflags info about the material a shader is currently rendering. + /// This is accessible in the shader in the [`StandardMaterialUniform`] + #[repr(transparent)] + pub struct StandardMaterialFlags: u32 { + const BASE_COLOR_TEXTURE = 1 << 0; + const EMISSIVE_TEXTURE = 1 << 1; + const METALLIC_ROUGHNESS_TEXTURE = 1 << 2; + const OCCLUSION_TEXTURE = 1 << 3; + const DOUBLE_SIDED = 1 << 4; + const UNLIT = 1 << 5; + const TWO_COMPONENT_NORMAL_MAP = 1 << 6; + const FLIP_NORMAL_MAP_Y = 1 << 7; + const FOG_ENABLED = 1 << 8; + const DEPTH_MAP = 1 << 9; // Used for parallax mapping + const SPECULAR_TRANSMISSION_TEXTURE = 1 << 10; + const THICKNESS_TEXTURE = 1 << 11; + const DIFFUSE_TRANSMISSION_TEXTURE = 1 << 12; + const ATTENUATION_ENABLED = 1 << 13; + const CLEARCOAT_TEXTURE = 1 << 14; + const CLEARCOAT_ROUGHNESS_TEXTURE = 1 << 15; + const CLEARCOAT_NORMAL_TEXTURE = 1 << 16; + const ANISOTROPY_TEXTURE = 1 << 17; + const SPECULAR_TEXTURE = 1 << 18; + const SPECULAR_TINT_TEXTURE = 1 << 19; + const ALPHA_MODE_RESERVED_BITS = Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS; // ← Bitmask reserving bits for the `AlphaMode` + const ALPHA_MODE_OPAQUE = 0 << Self::ALPHA_MODE_SHIFT_BITS; // ← Values are just sequential values bitshifted into + const ALPHA_MODE_MASK = 1 << Self::ALPHA_MODE_SHIFT_BITS; // the bitmask, and can range from 0 to 7. + const ALPHA_MODE_BLEND = 2 << Self::ALPHA_MODE_SHIFT_BITS; // + const ALPHA_MODE_PREMULTIPLIED = 3 << Self::ALPHA_MODE_SHIFT_BITS; // + const ALPHA_MODE_ADD = 4 << Self::ALPHA_MODE_SHIFT_BITS; // Right now only values 0–5 are used, which still gives + const ALPHA_MODE_MULTIPLY = 5 << Self::ALPHA_MODE_SHIFT_BITS; // ← us "room" for two more modes without adding more bits + const ALPHA_MODE_ALPHA_TO_COVERAGE = 6 << Self::ALPHA_MODE_SHIFT_BITS; + const NONE = 0; + const UNINITIALIZED = 0xFFFF; + } +} + +impl StandardMaterialFlags { + const ALPHA_MODE_MASK_BITS: u32 = 0b111; + const ALPHA_MODE_SHIFT_BITS: u32 = 32 - Self::ALPHA_MODE_MASK_BITS.count_ones(); +} + +/// The GPU representation of the uniform data of a [`StandardMaterial`]. +#[derive(Clone, Default, ShaderType)] +pub struct StandardMaterialUniform { + /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything + /// in between. + pub base_color: Vec4, + // Use a color for user-friendliness even though we technically don't use the alpha channel + // Might be used in the future for exposure correction in HDR + pub emissive: Vec4, + /// Color white light takes after traveling through the attenuation distance underneath the material surface + pub attenuation_color: Vec4, + /// The transform applied to the UVs corresponding to `ATTRIBUTE_UV_0` on the mesh before sampling. Default is identity. + pub uv_transform: Mat3, + /// Specular intensity for non-metals on a linear scale of [0.0, 1.0] + /// defaults to 0.5 which is mapped to 4% reflectance in the shader + pub reflectance: Vec3, + /// Linear perceptual roughness, clamped to [0.089, 1.0] in the shader + /// Defaults to minimum of 0.089 + pub roughness: f32, + /// From [0.0, 1.0], dielectric to pure metallic + pub metallic: f32, + /// Amount of diffuse light transmitted through the material + pub diffuse_transmission: f32, + /// Amount of specular light transmitted through the material + pub specular_transmission: f32, + /// Thickness of the volume underneath the material surface + pub thickness: f32, + /// Index of Refraction + pub ior: f32, + /// How far light travels through the volume underneath the material surface before being absorbed + pub attenuation_distance: f32, + pub clearcoat: f32, + pub clearcoat_perceptual_roughness: f32, + pub anisotropy_strength: f32, + pub anisotropy_rotation: Vec2, + /// The [`StandardMaterialFlags`] accessible in the `wgsl` shader. + pub flags: u32, + /// When the alpha mode mask flag is set, any base color alpha above this cutoff means fully opaque, + /// and any below means fully transparent. + pub alpha_cutoff: f32, + /// The depth of the [`StandardMaterial::depth_map`] to apply. + pub parallax_depth_scale: f32, + /// In how many layers to split the depth maps for Steep parallax mapping. + /// + /// If your `parallax_depth_scale` is >0.1 and you are seeing jaggy edges, + /// increase this value. However, this incurs a performance cost. + pub max_parallax_layer_count: f32, + /// The exposure (brightness) level of the lightmap, if present. + pub lightmap_exposure: f32, + /// Using [`ParallaxMappingMethod::Relief`], how many additional + /// steps to use at most to find the depth value. + pub max_relief_mapping_search_steps: u32, + /// ID for specifying which deferred lighting pass should be used for rendering this material, if any. + pub deferred_lighting_pass_id: u32, +} + +impl AsBindGroupShaderType for StandardMaterial { + fn as_bind_group_shader_type( + &self, + images: &RenderAssets, + ) -> StandardMaterialUniform { + let mut flags = StandardMaterialFlags::NONE; + if self.base_color_texture.is_some() { + flags |= StandardMaterialFlags::BASE_COLOR_TEXTURE; + } + if self.emissive_texture.is_some() { + flags |= StandardMaterialFlags::EMISSIVE_TEXTURE; + } + if self.metallic_roughness_texture.is_some() { + flags |= StandardMaterialFlags::METALLIC_ROUGHNESS_TEXTURE; + } + if self.occlusion_texture.is_some() { + flags |= StandardMaterialFlags::OCCLUSION_TEXTURE; + } + if self.double_sided { + flags |= StandardMaterialFlags::DOUBLE_SIDED; + } + if self.unlit { + flags |= StandardMaterialFlags::UNLIT; + } + if self.fog_enabled { + flags |= StandardMaterialFlags::FOG_ENABLED; + } + if self.depth_map.is_some() { + flags |= StandardMaterialFlags::DEPTH_MAP; + } + #[cfg(feature = "pbr_transmission_textures")] + { + if self.specular_transmission_texture.is_some() { + flags |= StandardMaterialFlags::SPECULAR_TRANSMISSION_TEXTURE; + } + if self.thickness_texture.is_some() { + flags |= StandardMaterialFlags::THICKNESS_TEXTURE; + } + if self.diffuse_transmission_texture.is_some() { + flags |= StandardMaterialFlags::DIFFUSE_TRANSMISSION_TEXTURE; + } + } + + #[cfg(feature = "pbr_anisotropy_texture")] + { + if self.anisotropy_texture.is_some() { + flags |= StandardMaterialFlags::ANISOTROPY_TEXTURE; + } + } + + #[cfg(feature = "pbr_specular_textures")] + { + if self.specular_texture.is_some() { + flags |= StandardMaterialFlags::SPECULAR_TEXTURE; + } + if self.specular_tint_texture.is_some() { + flags |= StandardMaterialFlags::SPECULAR_TINT_TEXTURE; + } + } + + #[cfg(feature = "pbr_multi_layer_material_textures")] + { + if self.clearcoat_texture.is_some() { + flags |= StandardMaterialFlags::CLEARCOAT_TEXTURE; + } + if self.clearcoat_roughness_texture.is_some() { + flags |= StandardMaterialFlags::CLEARCOAT_ROUGHNESS_TEXTURE; + } + if self.clearcoat_normal_texture.is_some() { + flags |= StandardMaterialFlags::CLEARCOAT_NORMAL_TEXTURE; + } + } + + let has_normal_map = self.normal_map_texture.is_some(); + if has_normal_map { + let normal_map_id = self.normal_map_texture.as_ref().map(Handle::id).unwrap(); + if let Some(texture) = images.get(normal_map_id) { + match texture.texture_format { + // All 2-component unorm formats + TextureFormat::Rg8Unorm + | TextureFormat::Rg16Unorm + | TextureFormat::Bc5RgUnorm + | TextureFormat::EacRg11Unorm => { + flags |= StandardMaterialFlags::TWO_COMPONENT_NORMAL_MAP; + } + _ => {} + } + } + if self.flip_normal_map_y { + flags |= StandardMaterialFlags::FLIP_NORMAL_MAP_Y; + } + } + // NOTE: 0.5 is from the glTF default - do we want this? + let mut alpha_cutoff = 0.5; + match self.alpha_mode { + AlphaMode::Opaque => flags |= StandardMaterialFlags::ALPHA_MODE_OPAQUE, + AlphaMode::Mask(c) => { + alpha_cutoff = c; + flags |= StandardMaterialFlags::ALPHA_MODE_MASK; + } + AlphaMode::Blend => flags |= StandardMaterialFlags::ALPHA_MODE_BLEND, + AlphaMode::Premultiplied => flags |= StandardMaterialFlags::ALPHA_MODE_PREMULTIPLIED, + AlphaMode::Add => flags |= StandardMaterialFlags::ALPHA_MODE_ADD, + AlphaMode::Multiply => flags |= StandardMaterialFlags::ALPHA_MODE_MULTIPLY, + AlphaMode::AlphaToCoverage => { + flags |= StandardMaterialFlags::ALPHA_MODE_ALPHA_TO_COVERAGE; + } + }; + + if self.attenuation_distance.is_finite() { + flags |= StandardMaterialFlags::ATTENUATION_ENABLED; + } + + let mut emissive = self.emissive.to_vec4(); + emissive[3] = self.emissive_exposure_weight; + + // Doing this up front saves having to do this repeatedly in the fragment shader. + let anisotropy_rotation = Vec2::from_angle(self.anisotropy_rotation); + + StandardMaterialUniform { + base_color: LinearRgba::from(self.base_color).to_vec4(), + emissive, + roughness: self.perceptual_roughness, + metallic: self.metallic, + reflectance: LinearRgba::from(self.specular_tint).to_vec3() * self.reflectance, + clearcoat: self.clearcoat, + clearcoat_perceptual_roughness: self.clearcoat_perceptual_roughness, + anisotropy_strength: self.anisotropy_strength, + anisotropy_rotation, + diffuse_transmission: self.diffuse_transmission, + specular_transmission: self.specular_transmission, + thickness: self.thickness, + ior: self.ior, + attenuation_distance: self.attenuation_distance, + attenuation_color: LinearRgba::from(self.attenuation_color) + .to_f32_array() + .into(), + flags: flags.bits(), + alpha_cutoff, + parallax_depth_scale: self.parallax_depth_scale, + max_parallax_layer_count: self.max_parallax_layer_count, + lightmap_exposure: self.lightmap_exposure, + max_relief_mapping_search_steps: self.parallax_mapping_method.max_steps(), + deferred_lighting_pass_id: self.deferred_lighting_pass_id as u32, + uv_transform: self.uv_transform.into(), + } + } +} + +bitflags! { + /// The pipeline key for `StandardMaterial`, packed into 64 bits. + #[repr(C)] + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct StandardMaterialKey: u64 { + const CULL_FRONT = 0x000001; + const CULL_BACK = 0x000002; + const NORMAL_MAP = 0x000004; + const RELIEF_MAPPING = 0x000008; + const DIFFUSE_TRANSMISSION = 0x000010; + const SPECULAR_TRANSMISSION = 0x000020; + const CLEARCOAT = 0x000040; + const CLEARCOAT_NORMAL_MAP = 0x000080; + const ANISOTROPY = 0x000100; + const BASE_COLOR_UV = 0x000200; + const EMISSIVE_UV = 0x000400; + const METALLIC_ROUGHNESS_UV = 0x000800; + const OCCLUSION_UV = 0x001000; + const SPECULAR_TRANSMISSION_UV = 0x002000; + const THICKNESS_UV = 0x004000; + const DIFFUSE_TRANSMISSION_UV = 0x008000; + const NORMAL_MAP_UV = 0x010000; + const ANISOTROPY_UV = 0x020000; + const CLEARCOAT_UV = 0x040000; + const CLEARCOAT_ROUGHNESS_UV = 0x080000; + const CLEARCOAT_NORMAL_UV = 0x100000; + const SPECULAR_UV = 0x200000; + const SPECULAR_TINT_UV = 0x400000; + const DEPTH_BIAS = 0xffffffff_00000000; + } +} + +const STANDARD_MATERIAL_KEY_DEPTH_BIAS_SHIFT: u64 = 32; + +impl From<&StandardMaterial> for StandardMaterialKey { + fn from(material: &StandardMaterial) -> Self { + let mut key = StandardMaterialKey::empty(); + key.set( + StandardMaterialKey::CULL_FRONT, + material.cull_mode == Some(Face::Front), + ); + key.set( + StandardMaterialKey::CULL_BACK, + material.cull_mode == Some(Face::Back), + ); + key.set( + StandardMaterialKey::NORMAL_MAP, + material.normal_map_texture.is_some(), + ); + key.set( + StandardMaterialKey::RELIEF_MAPPING, + matches!( + material.parallax_mapping_method, + ParallaxMappingMethod::Relief { .. } + ), + ); + key.set( + StandardMaterialKey::DIFFUSE_TRANSMISSION, + material.diffuse_transmission > 0.0, + ); + key.set( + StandardMaterialKey::SPECULAR_TRANSMISSION, + material.specular_transmission > 0.0, + ); + + key.set(StandardMaterialKey::CLEARCOAT, material.clearcoat > 0.0); + + #[cfg(feature = "pbr_multi_layer_material_textures")] + key.set( + StandardMaterialKey::CLEARCOAT_NORMAL_MAP, + material.clearcoat > 0.0 && material.clearcoat_normal_texture.is_some(), + ); + + key.set( + StandardMaterialKey::ANISOTROPY, + material.anisotropy_strength > 0.0, + ); + + key.set( + StandardMaterialKey::BASE_COLOR_UV, + material.base_color_channel != UvChannel::Uv0, + ); + + key.set( + StandardMaterialKey::EMISSIVE_UV, + material.emissive_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::METALLIC_ROUGHNESS_UV, + material.metallic_roughness_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::OCCLUSION_UV, + material.occlusion_channel != UvChannel::Uv0, + ); + #[cfg(feature = "pbr_transmission_textures")] + { + key.set( + StandardMaterialKey::SPECULAR_TRANSMISSION_UV, + material.specular_transmission_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::THICKNESS_UV, + material.thickness_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::DIFFUSE_TRANSMISSION_UV, + material.diffuse_transmission_channel != UvChannel::Uv0, + ); + } + + key.set( + StandardMaterialKey::NORMAL_MAP_UV, + material.normal_map_channel != UvChannel::Uv0, + ); + + #[cfg(feature = "pbr_anisotropy_texture")] + { + key.set( + StandardMaterialKey::ANISOTROPY_UV, + material.anisotropy_channel != UvChannel::Uv0, + ); + } + + #[cfg(feature = "pbr_specular_textures")] + { + key.set( + StandardMaterialKey::SPECULAR_UV, + material.specular_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::SPECULAR_TINT_UV, + material.specular_tint_channel != UvChannel::Uv0, + ); + } + + #[cfg(feature = "pbr_multi_layer_material_textures")] + { + key.set( + StandardMaterialKey::CLEARCOAT_UV, + material.clearcoat_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::CLEARCOAT_ROUGHNESS_UV, + material.clearcoat_roughness_channel != UvChannel::Uv0, + ); + key.set( + StandardMaterialKey::CLEARCOAT_NORMAL_UV, + material.clearcoat_normal_channel != UvChannel::Uv0, + ); + } + + key.insert(StandardMaterialKey::from_bits_retain( + // Casting to i32 first to ensure the full i32 range is preserved. + // (wgpu expects the depth_bias as an i32 when this is extracted in a later step) + (material.depth_bias as i32 as u64) << STANDARD_MATERIAL_KEY_DEPTH_BIAS_SHIFT, + )); + key + } +} + +impl Material for StandardMaterial { + fn fragment_shader() -> ShaderRef { + shader_ref(bevy_asset::embedded_path!("render/pbr.wgsl")) + } + + #[inline] + fn alpha_mode(&self) -> AlphaMode { + self.alpha_mode + } + + #[inline] + fn opaque_render_method(&self) -> OpaqueRendererMethod { + match self.opaque_render_method { + // For now, diffuse transmission doesn't work under deferred rendering as we don't pack + // the required data into the GBuffer. If this material is set to `Auto`, we report it as + // `Forward` so that it's rendered correctly, even when the `DefaultOpaqueRendererMethod` + // is set to `Deferred`. + // + // If the developer explicitly sets the `OpaqueRendererMethod` to `Deferred`, we assume + // they know what they're doing and don't override it. + OpaqueRendererMethod::Auto if self.diffuse_transmission > 0.0 => { + OpaqueRendererMethod::Forward + } + other => other, + } + } + + #[inline] + fn depth_bias(&self) -> f32 { + self.depth_bias + } + + #[inline] + fn reads_view_transmission_texture(&self) -> bool { + self.specular_transmission > 0.0 + } + + fn prepass_fragment_shader() -> ShaderRef { + shader_ref(bevy_asset::embedded_path!("render/pbr_prepass.wgsl")) + } + + fn deferred_fragment_shader() -> ShaderRef { + shader_ref(bevy_asset::embedded_path!("render/pbr.wgsl")) + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_fragment_shader() -> ShaderRef { + Self::fragment_shader() + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { + Self::prepass_fragment_shader() + } + + #[cfg(feature = "meshlet")] + fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { + Self::deferred_fragment_shader() + } + + fn specialize( + _pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + _layout: &MeshVertexBufferLayoutRef, + key: MaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> { + if let Some(fragment) = descriptor.fragment.as_mut() { + let shader_defs = &mut fragment.shader_defs; + + for (flags, shader_def) in [ + ( + StandardMaterialKey::NORMAL_MAP, + "STANDARD_MATERIAL_NORMAL_MAP", + ), + (StandardMaterialKey::RELIEF_MAPPING, "RELIEF_MAPPING"), + ( + StandardMaterialKey::DIFFUSE_TRANSMISSION, + "STANDARD_MATERIAL_DIFFUSE_TRANSMISSION", + ), + ( + StandardMaterialKey::SPECULAR_TRANSMISSION, + "STANDARD_MATERIAL_SPECULAR_TRANSMISSION", + ), + ( + StandardMaterialKey::DIFFUSE_TRANSMISSION + | StandardMaterialKey::SPECULAR_TRANSMISSION, + "STANDARD_MATERIAL_DIFFUSE_OR_SPECULAR_TRANSMISSION", + ), + ( + StandardMaterialKey::CLEARCOAT, + "STANDARD_MATERIAL_CLEARCOAT", + ), + ( + StandardMaterialKey::CLEARCOAT_NORMAL_MAP, + "STANDARD_MATERIAL_CLEARCOAT_NORMAL_MAP", + ), + ( + StandardMaterialKey::ANISOTROPY, + "STANDARD_MATERIAL_ANISOTROPY", + ), + ( + StandardMaterialKey::BASE_COLOR_UV, + "STANDARD_MATERIAL_BASE_COLOR_UV_B", + ), + ( + StandardMaterialKey::EMISSIVE_UV, + "STANDARD_MATERIAL_EMISSIVE_UV_B", + ), + ( + StandardMaterialKey::METALLIC_ROUGHNESS_UV, + "STANDARD_MATERIAL_METALLIC_ROUGHNESS_UV_B", + ), + ( + StandardMaterialKey::OCCLUSION_UV, + "STANDARD_MATERIAL_OCCLUSION_UV_B", + ), + ( + StandardMaterialKey::SPECULAR_TRANSMISSION_UV, + "STANDARD_MATERIAL_SPECULAR_TRANSMISSION_UV_B", + ), + ( + StandardMaterialKey::THICKNESS_UV, + "STANDARD_MATERIAL_THICKNESS_UV_B", + ), + ( + StandardMaterialKey::DIFFUSE_TRANSMISSION_UV, + "STANDARD_MATERIAL_DIFFUSE_TRANSMISSION_UV_B", + ), + ( + StandardMaterialKey::NORMAL_MAP_UV, + "STANDARD_MATERIAL_NORMAL_MAP_UV_B", + ), + ( + StandardMaterialKey::CLEARCOAT_UV, + "STANDARD_MATERIAL_CLEARCOAT_UV_B", + ), + ( + StandardMaterialKey::CLEARCOAT_ROUGHNESS_UV, + "STANDARD_MATERIAL_CLEARCOAT_ROUGHNESS_UV_B", + ), + ( + StandardMaterialKey::CLEARCOAT_NORMAL_UV, + "STANDARD_MATERIAL_CLEARCOAT_NORMAL_UV_B", + ), + ( + StandardMaterialKey::ANISOTROPY_UV, + "STANDARD_MATERIAL_ANISOTROPY_UV_B", + ), + ( + StandardMaterialKey::SPECULAR_UV, + "STANDARD_MATERIAL_SPECULAR_UV_B", + ), + ( + StandardMaterialKey::SPECULAR_TINT_UV, + "STANDARD_MATERIAL_SPECULAR_TINT_UV_B", + ), + ] { + if key.bind_group_data.intersects(flags) { + shader_defs.push(shader_def.into()); + } + } + } + + descriptor.primitive.cull_mode = if key + .bind_group_data + .contains(StandardMaterialKey::CULL_FRONT) + { + Some(Face::Front) + } else if key.bind_group_data.contains(StandardMaterialKey::CULL_BACK) { + Some(Face::Back) + } else { + None + }; + + if let Some(label) = &mut descriptor.label { + *label = format!("pbr_{}", *label).into(); + } + if let Some(depth_stencil) = descriptor.depth_stencil.as_mut() { + depth_stencil.bias.constant = + (key.bind_group_data.bits() >> STANDARD_MATERIAL_KEY_DEPTH_BIAS_SHIFT) as i32; + } + Ok(()) + } +} diff --git a/crates/libmarathon/src/render/pbr/prepass/mod.rs b/crates/libmarathon/src/render/pbr/prepass/mod.rs new file mode 100644 index 0000000..248ca34 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/prepass/mod.rs @@ -0,0 +1,1282 @@ +mod prepass_bindings; + +use crate::render::pbr::{ + alpha_mode_pipeline_key, binding_arrays_are_usable, buffer_layout, + collect_meshes_for_gpu_building, init_material_pipeline, set_mesh_motion_vector_flags, + setup_morph_and_skinning_defs, skin, DeferredDrawFunction, DeferredFragmentShader, + DeferredVertexShader, DrawMesh, EntitySpecializationTicks, ErasedMaterialPipelineKey, Material, + MaterialPipeline, MaterialProperties, MeshLayouts, MeshPipeline, MeshPipelineKey, + OpaqueRendererMethod, PreparedMaterial, PrepassDrawFunction, PrepassFragmentShader, + PrepassVertexShader, RenderLightmaps, RenderMaterialInstances, RenderMeshInstanceFlags, + RenderMeshInstances, RenderPhaseType, SetMaterialBindGroup, SetMeshBindGroup, ShadowView, +}; +use bevy_app::{App, Plugin, PreUpdate}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; +use bevy_camera::{Camera, Camera3d}; +use crate::render::{core_3d::CORE_3D_DEPTH_FORMAT, deferred::*, prepass::*}; +use bevy_ecs::{ + prelude::*, + system::{ + lifetimeless::{Read, SRes}, + SystemParamItem, + }, +}; +use bevy_math::{Affine3A, Mat4, Vec4}; +use bevy_mesh::{Mesh, Mesh3d, MeshVertexBufferLayoutRef}; +use crate::render::{ + alpha::AlphaMode, + batching::gpu_preprocessing::GpuPreprocessingSupport, + globals::{GlobalsBuffer, GlobalsUniform}, + mesh::{allocator::MeshAllocator, RenderMesh}, + render_asset::{prepare_assets, RenderAssets}, + render_phase::*, + render_resource::{binding_types::uniform_buffer, *}, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, + sync_world::RenderEntity, + view::{ + ExtractedView, Msaa, RenderVisibilityRanges, RetainedViewEntity, ViewUniform, + ViewUniformOffset, ViewUniforms, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT, + }, + Extract, ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems, +}; +use bevy_shader::{load_shader_library, Shader, ShaderDefVal}; +use bevy_transform::prelude::GlobalTransform; +pub use prepass_bindings::*; +use tracing::{error, warn}; + +#[cfg(feature = "meshlet")] +use crate::render::pbr::meshlet::{ + prepare_material_meshlet_meshes_prepass, queue_material_meshlet_meshes, InstanceManager, + MeshletMesh3d, +}; + +use std::sync::Arc; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{component::Tick, system::SystemChangeTick}; +use bevy_platform::collections::HashMap; +use crate::render::{ + erased_render_asset::ErasedRenderAssets, + sync_world::MainEntityHashMap, + view::RenderVisibleEntities, + RenderSystems::{PrepareAssets, PrepareResources}, +}; +use bevy_utils::default; +use core::marker::PhantomData; + +/// Sets up everything required to use the prepass pipeline. +/// +/// This does not add the actual prepasses, see [`PrepassPlugin`] for that. +pub struct PrepassPipelinePlugin; + +impl Plugin for PrepassPipelinePlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "prepass.wgsl"); + + load_shader_library!(app, "prepass_bindings.wgsl"); + load_shader_library!(app, "prepass_utils.wgsl"); + load_shader_library!(app, "prepass_io.wgsl"); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .add_systems( + RenderStartup, + ( + init_prepass_pipeline.after(init_material_pipeline), + init_prepass_view_bind_group, + ) + .chain(), + ) + .add_systems( + Render, + prepare_prepass_view_bind_group.in_set(RenderSystems::PrepareBindGroups), + ) + .init_resource::>(); + } +} + +/// Sets up the prepasses for a material. +/// +/// This depends on the [`PrepassPipelinePlugin`]. +pub struct PrepassPlugin { + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl PrepassPlugin { + /// Creates a new [`PrepassPlugin`] with the given debug flags. + pub fn new(debug_flags: RenderDebugFlags) -> Self { + PrepassPlugin { debug_flags } + } +} + +impl Plugin for PrepassPlugin { + fn build(&self, app: &mut App) { + let no_prepass_plugin_loaded = app + .world() + .get_resource::() + .is_none(); + + if no_prepass_plugin_loaded { + app.insert_resource(AnyPrepassPluginLoaded) + // At the start of each frame, last frame's GlobalTransforms become this frame's PreviousGlobalTransforms + // and last frame's view projection matrices become this frame's PreviousViewProjections + .add_systems( + PreUpdate, + ( + update_mesh_previous_global_transforms, + update_previous_view_data, + ), + ) + .add_plugins(( + BinnedRenderPhasePlugin::::new(self.debug_flags), + BinnedRenderPhasePlugin::::new( + self.debug_flags, + ), + )); + } + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + if no_prepass_plugin_loaded { + render_app + .add_systems(ExtractSchedule, extract_camera_previous_view_data) + .add_systems( + Render, + prepare_previous_view_uniforms.in_set(PrepareResources), + ); + } + + render_app + .init_resource::() + .init_resource::() + .init_resource::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_systems( + Render, + ( + check_prepass_views_need_specialization.in_set(PrepareAssets), + specialize_prepass_material_meshes + .in_set(RenderSystems::PrepareMeshes) + .after(prepare_assets::) + .after(collect_meshes_for_gpu_building) + .after(set_mesh_motion_vector_flags), + queue_prepass_material_meshes.in_set(RenderSystems::QueueMeshes), + ), + ); + + #[cfg(feature = "meshlet")] + render_app.add_systems( + Render, + prepare_material_meshlet_meshes_prepass + .in_set(RenderSystems::QueueMeshes) + .before(queue_material_meshlet_meshes) + .run_if(resource_exists::), + ); + } +} + +/// Marker resource for whether prepass is enabled globally for this material type +#[derive(Resource, Debug)] +pub struct PrepassEnabled(PhantomData); + +impl Default for PrepassEnabled { + fn default() -> Self { + PrepassEnabled(PhantomData) + } +} + +#[derive(Resource)] +struct AnyPrepassPluginLoaded; + +pub fn update_previous_view_data( + mut commands: Commands, + query: Query<(Entity, &Camera, &GlobalTransform), Or<(With, With)>>, +) { + for (entity, camera, camera_transform) in &query { + let world_from_view = camera_transform.affine(); + let view_from_world = Mat4::from(world_from_view.inverse()); + let view_from_clip = camera.clip_from_view().inverse(); + + commands.entity(entity).try_insert(PreviousViewData { + view_from_world, + clip_from_world: camera.clip_from_view() * view_from_world, + clip_from_view: camera.clip_from_view(), + world_from_clip: Mat4::from(world_from_view) * view_from_clip, + view_from_clip, + }); + } +} + +#[derive(Component, PartialEq, Default)] +pub struct PreviousGlobalTransform(pub Affine3A); + +#[cfg(not(feature = "meshlet"))] +type PreviousMeshFilter = With; +#[cfg(feature = "meshlet")] +type PreviousMeshFilter = Or<(With, With)>; + +pub fn update_mesh_previous_global_transforms( + mut commands: Commands, + views: Query<&Camera, Or<(With, With)>>, + new_meshes: Query< + (Entity, &GlobalTransform), + (PreviousMeshFilter, Without), + >, + mut meshes: Query<(&GlobalTransform, &mut PreviousGlobalTransform), PreviousMeshFilter>, +) { + let should_run = views.iter().any(|camera| camera.is_active); + + if should_run { + for (entity, transform) in &new_meshes { + let new_previous_transform = PreviousGlobalTransform(transform.affine()); + commands.entity(entity).try_insert(new_previous_transform); + } + meshes.par_iter_mut().for_each(|(transform, mut previous)| { + previous.set_if_neq(PreviousGlobalTransform(transform.affine())); + }); + } +} + +#[derive(Resource, Clone)] +pub struct PrepassPipeline { + pub view_layout_motion_vectors: BindGroupLayout, + pub view_layout_no_motion_vectors: BindGroupLayout, + pub mesh_layouts: MeshLayouts, + pub empty_layout: BindGroupLayout, + pub default_prepass_shader: Handle, + + /// Whether skins will use uniform buffers on account of storage buffers + /// being unavailable on this platform. + pub skins_use_uniform_buffers: bool, + + pub depth_clip_control_supported: bool, + + /// Whether binding arrays (a.k.a. bindless textures) are usable on the + /// current render device. + pub binding_arrays_are_usable: bool, + pub material_pipeline: MaterialPipeline, +} + +pub fn init_prepass_pipeline( + mut commands: Commands, + render_device: Res, + render_adapter: Res, + mesh_pipeline: Res, + material_pipeline: Res, + asset_server: Res, +) { + let visibility_ranges_buffer_binding_type = + render_device.get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); + + let view_layout_motion_vectors = render_device.create_bind_group_layout( + "prepass_view_layout_motion_vectors", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX_FRAGMENT, + ( + // View + (0, uniform_buffer::(true)), + // Globals + (1, uniform_buffer::(false)), + // PreviousViewUniforms + (2, uniform_buffer::(true)), + // VisibilityRanges + ( + 14, + buffer_layout( + visibility_ranges_buffer_binding_type, + false, + Some(Vec4::min_size()), + ) + .visibility(ShaderStages::VERTEX), + ), + ), + ), + ); + + let view_layout_no_motion_vectors = render_device.create_bind_group_layout( + "prepass_view_layout_no_motion_vectors", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX_FRAGMENT, + ( + // View + (0, uniform_buffer::(true)), + // Globals + (1, uniform_buffer::(false)), + // VisibilityRanges + ( + 14, + buffer_layout( + visibility_ranges_buffer_binding_type, + false, + Some(Vec4::min_size()), + ) + .visibility(ShaderStages::VERTEX), + ), + ), + ), + ); + + let depth_clip_control_supported = render_device + .features() + .contains(WgpuFeatures::DEPTH_CLIP_CONTROL); + commands.insert_resource(PrepassPipeline { + view_layout_motion_vectors, + view_layout_no_motion_vectors, + mesh_layouts: mesh_pipeline.mesh_layouts.clone(), + default_prepass_shader: load_embedded_asset!(asset_server.as_ref(), "prepass.wgsl"), + skins_use_uniform_buffers: skin::skins_use_uniform_buffers(&render_device), + depth_clip_control_supported, + binding_arrays_are_usable: binding_arrays_are_usable(&render_device, &render_adapter), + empty_layout: render_device.create_bind_group_layout("prepass_empty_layout", &[]), + material_pipeline: material_pipeline.clone(), + }); +} + +pub struct PrepassPipelineSpecializer { + pub(crate) pipeline: PrepassPipeline, + pub(crate) properties: Arc, +} + +impl SpecializedMeshPipeline for PrepassPipelineSpecializer { + type Key = ErasedMaterialPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + let mut shader_defs = Vec::new(); + if self.properties.bindless { + shader_defs.push("BINDLESS".into()); + } + let mut descriptor = + self.pipeline + .specialize(key.mesh_key, shader_defs, layout, &self.properties)?; + + // This is a bit risky because it's possible to change something that would + // break the prepass but be fine in the main pass. + // Since this api is pretty low-level it doesn't matter that much, but it is a potential issue. + if let Some(specialize) = self.properties.specialize { + specialize( + &self.pipeline.material_pipeline, + &mut descriptor, + layout, + key, + )?; + } + + Ok(descriptor) + } +} + +impl PrepassPipeline { + fn specialize( + &self, + mesh_key: MeshPipelineKey, + shader_defs: Vec, + layout: &MeshVertexBufferLayoutRef, + material_properties: &MaterialProperties, + ) -> Result { + let mut shader_defs = shader_defs; + let mut bind_group_layouts = vec![ + if mesh_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + self.view_layout_motion_vectors.clone() + } else { + self.view_layout_no_motion_vectors.clone() + }, + self.empty_layout.clone(), + ]; + let mut vertex_attributes = Vec::new(); + + // Let the shader code know that it's running in a prepass pipeline. + // (PBR code will use this to detect that it's running in deferred mode, + // since that's the only time it gets called from a prepass pipeline.) + shader_defs.push("PREPASS_PIPELINE".into()); + + shader_defs.push(ShaderDefVal::UInt( + "MATERIAL_BIND_GROUP".into(), + crate::render::pbr::material::MATERIAL_BIND_GROUP_INDEX as u32, + )); + // NOTE: Eventually, it would be nice to only add this when the shaders are overloaded by the Material. + // The main limitation right now is that bind group order is hardcoded in shaders. + bind_group_layouts.push( + material_properties + .material_layout + .as_ref() + .unwrap() + .clone(), + ); + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + shader_defs.push("WEBGL2".into()); + shader_defs.push("VERTEX_OUTPUT_INSTANCE_INDEX".into()); + if mesh_key.contains(MeshPipelineKey::DEPTH_PREPASS) { + shader_defs.push("DEPTH_PREPASS".into()); + } + if mesh_key.contains(MeshPipelineKey::MAY_DISCARD) { + shader_defs.push("MAY_DISCARD".into()); + } + let blend_key = mesh_key.intersection(MeshPipelineKey::BLEND_RESERVED_BITS); + if blend_key == MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA { + shader_defs.push("BLEND_PREMULTIPLIED_ALPHA".into()); + } + if blend_key == MeshPipelineKey::BLEND_ALPHA { + shader_defs.push("BLEND_ALPHA".into()); + } + if layout.0.contains(Mesh::ATTRIBUTE_POSITION) { + shader_defs.push("VERTEX_POSITIONS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); + } + // For directional light shadow map views, use unclipped depth via either the native GPU feature, + // or emulated by setting depth in the fragment shader for GPUs that don't support it natively. + let emulate_unclipped_depth = mesh_key.contains(MeshPipelineKey::UNCLIPPED_DEPTH_ORTHO) + && !self.depth_clip_control_supported; + if emulate_unclipped_depth { + shader_defs.push("UNCLIPPED_DEPTH_ORTHO_EMULATION".into()); + // PERF: This line forces the "prepass fragment shader" to always run in + // common scenarios like "directional light calculation". Doing so resolves + // a pretty nasty depth clamping bug, but it also feels a bit excessive. + // We should try to find a way to resolve this without forcing the fragment + // shader to run. + // https://github.com/bevyengine/bevy/pull/8877 + shader_defs.push("PREPASS_FRAGMENT".into()); + } + let unclipped_depth = mesh_key.contains(MeshPipelineKey::UNCLIPPED_DEPTH_ORTHO) + && self.depth_clip_control_supported; + if layout.0.contains(Mesh::ATTRIBUTE_UV_0) { + shader_defs.push("VERTEX_UVS".into()); + shader_defs.push("VERTEX_UVS_A".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(1)); + } + if layout.0.contains(Mesh::ATTRIBUTE_UV_1) { + shader_defs.push("VERTEX_UVS".into()); + shader_defs.push("VERTEX_UVS_B".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_UV_1.at_shader_location(2)); + } + if mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS) { + shader_defs.push("NORMAL_PREPASS".into()); + } + if mesh_key.intersects(MeshPipelineKey::NORMAL_PREPASS | MeshPipelineKey::DEFERRED_PREPASS) + { + shader_defs.push("NORMAL_PREPASS_OR_DEFERRED_PREPASS".into()); + if layout.0.contains(Mesh::ATTRIBUTE_NORMAL) { + shader_defs.push("VERTEX_NORMALS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(3)); + } else if mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS) { + warn!( + "The default normal prepass expects the mesh to have vertex normal attributes." + ); + } + if layout.0.contains(Mesh::ATTRIBUTE_TANGENT) { + shader_defs.push("VERTEX_TANGENTS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(4)); + } + } + if mesh_key + .intersects(MeshPipelineKey::MOTION_VECTOR_PREPASS | MeshPipelineKey::DEFERRED_PREPASS) + { + shader_defs.push("MOTION_VECTOR_PREPASS_OR_DEFERRED_PREPASS".into()); + } + if mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + shader_defs.push("DEFERRED_PREPASS".into()); + } + if mesh_key.contains(MeshPipelineKey::LIGHTMAPPED) { + shader_defs.push("LIGHTMAP".into()); + } + if mesh_key.contains(MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING) { + shader_defs.push("LIGHTMAP_BICUBIC_SAMPLING".into()); + } + if layout.0.contains(Mesh::ATTRIBUTE_COLOR) { + shader_defs.push("VERTEX_COLORS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(7)); + } + if mesh_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + shader_defs.push("MOTION_VECTOR_PREPASS".into()); + } + if mesh_key.contains(MeshPipelineKey::HAS_PREVIOUS_SKIN) { + shader_defs.push("HAS_PREVIOUS_SKIN".into()); + } + if mesh_key.contains(MeshPipelineKey::HAS_PREVIOUS_MORPH) { + shader_defs.push("HAS_PREVIOUS_MORPH".into()); + } + if self.binding_arrays_are_usable { + shader_defs.push("MULTIPLE_LIGHTMAPS_IN_ARRAY".into()); + } + if mesh_key.contains(MeshPipelineKey::VISIBILITY_RANGE_DITHER) { + shader_defs.push("VISIBILITY_RANGE_DITHER".into()); + } + if mesh_key.intersects( + MeshPipelineKey::NORMAL_PREPASS + | MeshPipelineKey::MOTION_VECTOR_PREPASS + | MeshPipelineKey::DEFERRED_PREPASS, + ) { + shader_defs.push("PREPASS_FRAGMENT".into()); + } + let bind_group = setup_morph_and_skinning_defs( + &self.mesh_layouts, + layout, + 5, + &mesh_key, + &mut shader_defs, + &mut vertex_attributes, + self.skins_use_uniform_buffers, + ); + bind_group_layouts.insert(2, bind_group); + let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?; + // Setup prepass fragment targets - normals in slot 0 (or None if not needed), motion vectors in slot 1 + let mut targets = prepass_target_descriptors( + mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS), + mesh_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS), + mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS), + ); + + if targets.iter().all(Option::is_none) { + // if no targets are required then clear the list, so that no fragment shader is required + // (though one may still be used for discarding depth buffer writes) + targets.clear(); + } + + // The fragment shader is only used when the normal prepass or motion vectors prepass + // is enabled, the material uses alpha cutoff values and doesn't rely on the standard + // prepass shader, or we are emulating unclipped depth in the fragment shader. + let fragment_required = !targets.is_empty() + || emulate_unclipped_depth + || (mesh_key.contains(MeshPipelineKey::MAY_DISCARD) + && material_properties + .get_shader(PrepassFragmentShader) + .is_some()); + + let fragment = fragment_required.then(|| { + // Use the fragment shader from the material + let frag_shader_handle = if mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + match material_properties.get_shader(DeferredFragmentShader) { + Some(frag_shader_handle) => frag_shader_handle, + None => self.default_prepass_shader.clone(), + } + } else { + match material_properties.get_shader(PrepassFragmentShader) { + Some(frag_shader_handle) => frag_shader_handle, + None => self.default_prepass_shader.clone(), + } + }; + + FragmentState { + shader: frag_shader_handle, + shader_defs: shader_defs.clone(), + targets, + ..default() + } + }); + + // Use the vertex shader from the material if present + let vert_shader_handle = if mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + if let Some(handle) = material_properties.get_shader(DeferredVertexShader) { + handle + } else { + self.default_prepass_shader.clone() + } + } else if let Some(handle) = material_properties.get_shader(PrepassVertexShader) { + handle + } else { + self.default_prepass_shader.clone() + }; + let descriptor = RenderPipelineDescriptor { + vertex: VertexState { + shader: vert_shader_handle, + shader_defs, + buffers: vec![vertex_buffer_layout], + ..default() + }, + fragment, + layout: bind_group_layouts, + primitive: PrimitiveState { + topology: mesh_key.primitive_topology(), + unclipped_depth, + ..default() + }, + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + multisample: MultisampleState { + count: mesh_key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("prepass_pipeline".into()), + ..default() + }; + Ok(descriptor) + } +} + +// Extract the render phases for the prepass +pub fn extract_camera_previous_view_data( + mut commands: Commands, + cameras_3d: Extract), With>>, +) { + for (entity, camera, maybe_previous_view_data) in cameras_3d.iter() { + let mut entity = commands + .get_entity(entity) + .expect("Camera entity wasn't synced."); + if camera.is_active { + if let Some(previous_view_data) = maybe_previous_view_data { + entity.insert(previous_view_data.clone()); + } + } else { + entity.remove::(); + } + } +} + +pub fn prepare_previous_view_uniforms( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut previous_view_uniforms: ResMut, + views: Query< + (Entity, &ExtractedView, Option<&PreviousViewData>), + Or<(With, With)>, + >, +) { + let views_iter = views.iter(); + let view_count = views_iter.len(); + let Some(mut writer) = + previous_view_uniforms + .uniforms + .get_writer(view_count, &render_device, &render_queue) + else { + return; + }; + + for (entity, camera, maybe_previous_view_uniforms) in views_iter { + let prev_view_data = match maybe_previous_view_uniforms { + Some(previous_view) => previous_view.clone(), + None => { + let world_from_view = camera.world_from_view.affine(); + let view_from_world = Mat4::from(world_from_view.inverse()); + let view_from_clip = camera.clip_from_view.inverse(); + + PreviousViewData { + view_from_world, + clip_from_world: camera.clip_from_view * view_from_world, + clip_from_view: camera.clip_from_view, + world_from_clip: Mat4::from(world_from_view) * view_from_clip, + view_from_clip, + } + } + }; + + commands.entity(entity).insert(PreviousViewUniformOffset { + offset: writer.write(&prev_view_data), + }); + } +} + +#[derive(Resource)] +pub struct PrepassViewBindGroup { + pub motion_vectors: Option, + pub no_motion_vectors: Option, + pub empty_bind_group: BindGroup, +} + +pub fn init_prepass_view_bind_group( + mut commands: Commands, + render_device: Res, + pipeline: Res, +) { + let empty_bind_group = render_device.create_bind_group( + "prepass_view_empty_bind_group", + &pipeline.empty_layout, + &[], + ); + commands.insert_resource(PrepassViewBindGroup { + motion_vectors: None, + no_motion_vectors: None, + empty_bind_group, + }); +} + +pub fn prepare_prepass_view_bind_group( + render_device: Res, + prepass_pipeline: Res, + view_uniforms: Res, + globals_buffer: Res, + previous_view_uniforms: Res, + visibility_ranges: Res, + mut prepass_view_bind_group: ResMut, +) { + if let (Some(view_binding), Some(globals_binding), Some(visibility_ranges_buffer)) = ( + view_uniforms.uniforms.binding(), + globals_buffer.buffer.binding(), + visibility_ranges.buffer().buffer(), + ) { + prepass_view_bind_group.no_motion_vectors = Some(render_device.create_bind_group( + "prepass_view_no_motion_vectors_bind_group", + &prepass_pipeline.view_layout_no_motion_vectors, + &BindGroupEntries::with_indices(( + (0, view_binding.clone()), + (1, globals_binding.clone()), + (14, visibility_ranges_buffer.as_entire_binding()), + )), + )); + + if let Some(previous_view_uniforms_binding) = previous_view_uniforms.uniforms.binding() { + prepass_view_bind_group.motion_vectors = Some(render_device.create_bind_group( + "prepass_view_motion_vectors_bind_group", + &prepass_pipeline.view_layout_motion_vectors, + &BindGroupEntries::with_indices(( + (0, view_binding), + (1, globals_binding), + (2, previous_view_uniforms_binding), + (14, visibility_ranges_buffer.as_entire_binding()), + )), + )); + } + } +} + +/// Stores the [`SpecializedPrepassMaterialViewPipelineCache`] for each view. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedPrepassMaterialPipelineCache { + // view_entity -> view pipeline cache + #[deref] + map: HashMap, +} + +/// Stores the cached render pipeline ID for each entity in a single view, as +/// well as the last time it was changed. +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedPrepassMaterialViewPipelineCache { + // material entity -> (tick, pipeline_id) + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, +} + +#[derive(Resource, Deref, DerefMut, Default, Clone)] +pub struct ViewKeyPrepassCache(HashMap); + +#[derive(Resource, Deref, DerefMut, Default, Clone)] +pub struct ViewPrepassSpecializationTicks(HashMap); + +pub fn check_prepass_views_need_specialization( + mut view_key_cache: ResMut, + mut view_specialization_ticks: ResMut, + mut views: Query<( + &ExtractedView, + &Msaa, + Option<&DepthPrepass>, + Option<&NormalPrepass>, + Option<&MotionVectorPrepass>, + )>, + ticks: SystemChangeTick, +) { + for (view, msaa, depth_prepass, normal_prepass, motion_vector_prepass) in views.iter_mut() { + let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()); + if depth_prepass.is_some() { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + if normal_prepass.is_some() { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + if motion_vector_prepass.is_some() { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + + if let Some(current_key) = view_key_cache.get_mut(&view.retained_view_entity) { + if *current_key != view_key { + view_key_cache.insert(view.retained_view_entity, view_key); + view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run()); + } + } else { + view_key_cache.insert(view.retained_view_entity, view_key); + view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run()); + } + } +} + +pub fn specialize_prepass_material_meshes( + render_meshes: Res>, + render_materials: Res>, + render_mesh_instances: Res, + render_material_instances: Res, + render_lightmaps: Res, + render_visibility_ranges: Res, + view_key_cache: Res, + views: Query<( + &ExtractedView, + &RenderVisibleEntities, + &Msaa, + Option<&MotionVectorPrepass>, + Option<&DeferredPrepass>, + )>, + ( + opaque_prepass_render_phases, + alpha_mask_prepass_render_phases, + opaque_deferred_render_phases, + alpha_mask_deferred_render_phases, + ): ( + Res>, + Res>, + Res>, + Res>, + ), + ( + mut specialized_material_pipeline_cache, + ticks, + prepass_pipeline, + mut pipelines, + pipeline_cache, + view_specialization_ticks, + entity_specialization_ticks, + ): ( + ResMut, + SystemChangeTick, + Res, + ResMut>, + Res, + Res, + Res, + ), +) { + for (extracted_view, visible_entities, msaa, motion_vector_prepass, deferred_prepass) in &views + { + if !opaque_deferred_render_phases.contains_key(&extracted_view.retained_view_entity) + && !alpha_mask_deferred_render_phases.contains_key(&extracted_view.retained_view_entity) + && !opaque_prepass_render_phases.contains_key(&extracted_view.retained_view_entity) + && !alpha_mask_prepass_render_phases.contains_key(&extracted_view.retained_view_entity) + { + continue; + } + + let Some(view_key) = view_key_cache.get(&extracted_view.retained_view_entity) else { + continue; + }; + + let view_tick = view_specialization_ticks + .get(&extracted_view.retained_view_entity) + .unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(extracted_view.retained_view_entity) + .or_default(); + + for (_, visible_entity) in visible_entities.iter::() { + let Some(material_instance) = render_material_instances.instances.get(visible_entity) + else { + continue; + }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap(); + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; + } + let Some(material) = render_materials.get(material_instance.asset_id) else { + continue; + }; + if !material.properties.prepass_enabled && !material.properties.shadows_enabled { + // If the material was previously specialized for prepass, remove it + view_specialized_material_pipeline_cache.remove(visible_entity); + continue; + } + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + + let mut mesh_key = *view_key | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()); + + let alpha_mode = material.properties.alpha_mode; + match alpha_mode { + AlphaMode::Opaque | AlphaMode::AlphaToCoverage | AlphaMode::Mask(_) => { + mesh_key |= alpha_mode_pipeline_key(alpha_mode, msaa); + } + AlphaMode::Blend + | AlphaMode::Premultiplied + | AlphaMode::Add + | AlphaMode::Multiply => { + // In case this material was previously in a valid alpha_mode, remove it to + // stop the queue system from assuming its retained cache to be valid. + view_specialized_material_pipeline_cache.remove(visible_entity); + continue; + } + } + + if material.properties.reads_view_transmission_texture { + // No-op: Materials reading from `ViewTransmissionTexture` are not rendered in the `Opaque3d` + // phase, and are therefore also excluded from the prepass much like alpha-blended materials. + view_specialized_material_pipeline_cache.remove(visible_entity); + continue; + } + + let forward = match material.properties.render_method { + OpaqueRendererMethod::Forward => true, + OpaqueRendererMethod::Deferred => false, + OpaqueRendererMethod::Auto => unreachable!(), + }; + + let deferred = deferred_prepass.is_some() && !forward; + + if deferred { + mesh_key |= MeshPipelineKey::DEFERRED_PREPASS; + } + + if let Some(lightmap) = render_lightmaps.render_lightmaps.get(visible_entity) { + // Even though we don't use the lightmap in the forward prepass, the + // `SetMeshBindGroup` render command will bind the data for it. So + // we need to include the appropriate flag in the mesh pipeline key + // to ensure that the necessary bind group layout entries are + // present. + mesh_key |= MeshPipelineKey::LIGHTMAPPED; + + if lightmap.bicubic_sampling && deferred { + mesh_key |= MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING; + } + } + + if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) { + mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER; + } + + // If the previous frame has skins or morph targets, note that. + if motion_vector_prepass.is_some() { + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_SKIN; + } + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_MORPH; + } + } + + let erased_key = ErasedMaterialPipelineKey { + mesh_key, + material_key: material.properties.material_key.clone(), + type_id: material_instance.asset_id.type_id(), + }; + let prepass_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &prepass_pipeline_specializer, + erased_key, + &mesh.layout, + ); + let pipeline_id = match pipeline_id { + Ok(id) => id, + Err(err) => { + error!("{}", err); + continue; + } + }; + + view_specialized_material_pipeline_cache + .insert(*visible_entity, (ticks.this_run(), pipeline_id)); + } + } +} + +pub fn queue_prepass_material_meshes( + render_mesh_instances: Res, + render_materials: Res>, + render_material_instances: Res, + mesh_allocator: Res, + gpu_preprocessing_support: Res, + mut opaque_prepass_render_phases: ResMut>, + mut alpha_mask_prepass_render_phases: ResMut>, + mut opaque_deferred_render_phases: ResMut>, + mut alpha_mask_deferred_render_phases: ResMut>, + views: Query<(&ExtractedView, &RenderVisibleEntities)>, + specialized_material_pipeline_cache: Res, +) { + for (extracted_view, visible_entities) in &views { + let ( + mut opaque_phase, + mut alpha_mask_phase, + mut opaque_deferred_phase, + mut alpha_mask_deferred_phase, + ) = ( + opaque_prepass_render_phases.get_mut(&extracted_view.retained_view_entity), + alpha_mask_prepass_render_phases.get_mut(&extracted_view.retained_view_entity), + opaque_deferred_render_phases.get_mut(&extracted_view.retained_view_entity), + alpha_mask_deferred_render_phases.get_mut(&extracted_view.retained_view_entity), + ); + + let Some(view_specialized_material_pipeline_cache) = + specialized_material_pipeline_cache.get(&extracted_view.retained_view_entity) + else { + continue; + }; + + // Skip if there's no place to put the mesh. + if opaque_phase.is_none() + && alpha_mask_phase.is_none() + && opaque_deferred_phase.is_none() + && alpha_mask_deferred_phase.is_none() + { + continue; + } + + for (render_entity, visible_entity) in visible_entities.iter::() { + let Some((current_change_tick, pipeline_id)) = + view_specialized_material_pipeline_cache.get(visible_entity) + else { + continue; + }; + + // Skip the entity if it's cached in a bin and up to date. + if opaque_phase.as_mut().is_some_and(|phase| { + phase.validate_cached_entity(*visible_entity, *current_change_tick) + }) || alpha_mask_phase.as_mut().is_some_and(|phase| { + phase.validate_cached_entity(*visible_entity, *current_change_tick) + }) || opaque_deferred_phase.as_mut().is_some_and(|phase| { + phase.validate_cached_entity(*visible_entity, *current_change_tick) + }) || alpha_mask_deferred_phase.as_mut().is_some_and(|phase| { + phase.validate_cached_entity(*visible_entity, *current_change_tick) + }) { + continue; + } + + let Some(material_instance) = render_material_instances.instances.get(visible_entity) + else { + continue; + }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let Some(material) = render_materials.get(material_instance.asset_id) else { + continue; + }; + let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + + let deferred = match material.properties.render_method { + OpaqueRendererMethod::Forward => false, + OpaqueRendererMethod::Deferred => true, + OpaqueRendererMethod::Auto => unreachable!(), + }; + + match material.properties.render_phase_type { + RenderPhaseType::Opaque => { + if deferred { + opaque_deferred_phase.as_mut().unwrap().add( + OpaqueNoLightmap3dBatchSetKey { + draw_function: material + .properties + .get_draw_function(DeferredDrawFunction) + .unwrap(), + pipeline: *pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }, + OpaqueNoLightmap3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + *current_change_tick, + ); + } else if let Some(opaque_phase) = opaque_phase.as_mut() { + let (vertex_slab, index_slab) = + mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + opaque_phase.add( + OpaqueNoLightmap3dBatchSetKey { + draw_function: material + .properties + .get_draw_function(PrepassDrawFunction) + .unwrap(), + pipeline: *pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }, + OpaqueNoLightmap3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + *current_change_tick, + ); + } + } + RenderPhaseType::AlphaMask => { + if deferred { + let (vertex_slab, index_slab) = + mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let batch_set_key = OpaqueNoLightmap3dBatchSetKey { + draw_function: material + .properties + .get_draw_function(DeferredDrawFunction) + .unwrap(), + pipeline: *pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + let bin_key = OpaqueNoLightmap3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }; + alpha_mask_deferred_phase.as_mut().unwrap().add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + *current_change_tick, + ); + } else if let Some(alpha_mask_phase) = alpha_mask_phase.as_mut() { + let (vertex_slab, index_slab) = + mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let batch_set_key = OpaqueNoLightmap3dBatchSetKey { + draw_function: material + .properties + .get_draw_function(PrepassDrawFunction) + .unwrap(), + pipeline: *pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + let bin_key = OpaqueNoLightmap3dBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }; + alpha_mask_phase.add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + *current_change_tick, + ); + } + } + _ => {} + } + } + } +} + +pub struct SetPrepassViewBindGroup; +impl RenderCommand

    for SetPrepassViewBindGroup { + type Param = SRes; + type ViewQuery = ( + Read, + Has, + Option>, + ); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + (view_uniform_offset, has_motion_vector_prepass, previous_view_uniform_offset): ( + &'_ ViewUniformOffset, + bool, + Option<&'_ PreviousViewUniformOffset>, + ), + _entity: Option<()>, + prepass_view_bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let prepass_view_bind_group = prepass_view_bind_group.into_inner(); + + match previous_view_uniform_offset { + Some(previous_view_uniform_offset) if has_motion_vector_prepass => { + pass.set_bind_group( + I, + prepass_view_bind_group.motion_vectors.as_ref().unwrap(), + &[ + view_uniform_offset.offset, + previous_view_uniform_offset.offset, + ], + ); + } + _ => { + pass.set_bind_group( + I, + prepass_view_bind_group.no_motion_vectors.as_ref().unwrap(), + &[view_uniform_offset.offset], + ); + } + } + RenderCommandResult::Success + } +} + +pub struct SetPrepassViewEmptyBindGroup; +impl RenderCommand

    for SetPrepassViewEmptyBindGroup { + type Param = SRes; + type ViewQuery = (); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + _view: (), + _entity: Option<()>, + prepass_view_bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let prepass_view_bind_group = prepass_view_bind_group.into_inner(); + pass.set_bind_group(I, &prepass_view_bind_group.empty_bind_group, &[]); + RenderCommandResult::Success + } +} + +pub type DrawPrepass = ( + SetItemPipeline, + SetPrepassViewBindGroup<0>, + SetPrepassViewEmptyBindGroup<1>, + SetMeshBindGroup<2>, + SetMaterialBindGroup<3>, + DrawMesh, +); diff --git a/crates/libmarathon/src/render/pbr/prepass/prepass.wgsl b/crates/libmarathon/src/render/pbr/prepass/prepass.wgsl new file mode 100644 index 0000000..52dd9bf --- /dev/null +++ b/crates/libmarathon/src/render/pbr/prepass/prepass.wgsl @@ -0,0 +1,219 @@ +#import bevy_pbr::{ + prepass_bindings, + mesh_bindings::mesh, + mesh_functions, + prepass_io::{Vertex, VertexOutput, FragmentOutput}, + skinning, + morph, + mesh_view_bindings::view, + view_transformations::position_world_to_clip, +} + +#ifdef DEFERRED_PREPASS +#import bevy_pbr::rgb9e5 +#endif + +#ifdef MORPH_TARGETS +fn morph_vertex(vertex_in: Vertex) -> Vertex { + var vertex = vertex_in; + let first_vertex = mesh[vertex.instance_index].first_vertex_index; + let vertex_index = vertex.index - first_vertex; + + let weight_count = morph::layer_count(); + for (var i: u32 = 0u; i < weight_count; i ++) { + let weight = morph::weight_at(i); + if weight == 0.0 { + continue; + } + vertex.position += weight * morph::morph(vertex_index, morph::position_offset, i); +#ifdef VERTEX_NORMALS + vertex.normal += weight * morph::morph(vertex_index, morph::normal_offset, i); +#endif +#ifdef VERTEX_TANGENTS + vertex.tangent += vec4(weight * morph::morph(vertex_index, morph::tangent_offset, i), 0.0); +#endif + } + return vertex; +} + +// Returns the morphed position of the given vertex from the previous frame. +// +// This function is used for motion vector calculation, and, as such, it doesn't +// bother morphing the normals and tangents. +fn morph_prev_vertex(vertex_in: Vertex) -> Vertex { + var vertex = vertex_in; + let weight_count = morph::layer_count(); + for (var i: u32 = 0u; i < weight_count; i ++) { + let weight = morph::prev_weight_at(i); + if weight == 0.0 { + continue; + } + vertex.position += weight * morph::morph(vertex.index, morph::position_offset, i); + // Don't bother morphing normals and tangents; we don't need them for + // motion vector calculation. + } + return vertex; +} +#endif // MORPH_TARGETS + +@vertex +fn vertex(vertex_no_morph: Vertex) -> VertexOutput { + var out: VertexOutput; + +#ifdef MORPH_TARGETS + var vertex = morph_vertex(vertex_no_morph); +#else + var vertex = vertex_no_morph; +#endif + + let mesh_world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index); + +#ifdef SKINNED + var world_from_local = skinning::skin_model( + vertex.joint_indices, + vertex.joint_weights, + vertex_no_morph.instance_index + ); +#else // SKINNED + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + var world_from_local = mesh_world_from_local; +#endif // SKINNED + + out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); + out.position = position_world_to_clip(out.world_position.xyz); +#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION + out.unclipped_depth = out.position.z; + out.position.z = min(out.position.z, 1.0); // Clamp depth to avoid clipping +#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION + +#ifdef VERTEX_UVS_A + out.uv = vertex.uv; +#endif // VERTEX_UVS_A + +#ifdef VERTEX_UVS_B + out.uv_b = vertex.uv_b; +#endif // VERTEX_UVS_B + +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS +#ifdef VERTEX_NORMALS +#ifdef SKINNED + out.world_normal = skinning::skin_normals(world_from_local, vertex.normal); +#else // SKINNED + out.world_normal = mesh_functions::mesh_normal_local_to_world( + vertex.normal, + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + vertex_no_morph.instance_index + ); +#endif // SKINNED +#endif // VERTEX_NORMALS + +#ifdef VERTEX_TANGENTS + out.world_tangent = mesh_functions::mesh_tangent_local_to_world( + world_from_local, + vertex.tangent, + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + vertex_no_morph.instance_index + ); +#endif // VERTEX_TANGENTS +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + +#ifdef VERTEX_COLORS + out.color = vertex.color; +#endif + + // Compute the motion vector for TAA among other purposes. For this we need + // to know where the vertex was last frame. +#ifdef MOTION_VECTOR_PREPASS + + // Take morph targets into account. +#ifdef MORPH_TARGETS + +#ifdef HAS_PREVIOUS_MORPH + let prev_vertex = morph_prev_vertex(vertex_no_morph); +#else // HAS_PREVIOUS_MORPH + let prev_vertex = vertex_no_morph; +#endif // HAS_PREVIOUS_MORPH + +#else // MORPH_TARGETS + let prev_vertex = vertex_no_morph; +#endif // MORPH_TARGETS + + // Take skinning into account. +#ifdef SKINNED + +#ifdef HAS_PREVIOUS_SKIN + let prev_model = skinning::skin_prev_model( + prev_vertex.joint_indices, + prev_vertex.joint_weights, + vertex_no_morph.instance_index + ); +#else // HAS_PREVIOUS_SKIN + let prev_model = mesh_functions::get_previous_world_from_local(prev_vertex.instance_index); +#endif // HAS_PREVIOUS_SKIN + +#else // SKINNED + let prev_model = mesh_functions::get_previous_world_from_local(prev_vertex.instance_index); +#endif // SKINNED + + out.previous_world_position = mesh_functions::mesh_position_local_to_world( + prev_model, + vec4(prev_vertex.position, 1.0) + ); +#endif // MOTION_VECTOR_PREPASS + +#ifdef VERTEX_OUTPUT_INSTANCE_INDEX + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + out.instance_index = vertex_no_morph.instance_index; +#endif + +#ifdef VISIBILITY_RANGE_DITHER + out.visibility_range_dither = mesh_functions::get_visibility_range_dither_level( + vertex_no_morph.instance_index, mesh_world_from_local[3]); +#endif // VISIBILITY_RANGE_DITHER + + return out; +} + +#ifdef PREPASS_FRAGMENT +@fragment +fn fragment(in: VertexOutput) -> FragmentOutput { + var out: FragmentOutput; + +#ifdef NORMAL_PREPASS + out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0); +#endif + +#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION + out.frag_depth = in.unclipped_depth; +#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION + +#ifdef MOTION_VECTOR_PREPASS + let clip_position_t = view.unjittered_clip_from_world * in.world_position; + let clip_position = clip_position_t.xy / clip_position_t.w; + let previous_clip_position_t = prepass_bindings::previous_view_uniforms.clip_from_world * in.previous_world_position; + let previous_clip_position = previous_clip_position_t.xy / previous_clip_position_t.w; + // These motion vectors are used as offsets to UV positions and are stored + // in the range -1,1 to allow offsetting from the one corner to the + // diagonally-opposite corner in UV coordinates, in either direction. + // A difference between diagonally-opposite corners of clip space is in the + // range -2,2, so this needs to be scaled by 0.5. And the V direction goes + // down where clip space y goes up, so y needs to be flipped. + out.motion_vector = (clip_position - previous_clip_position) * vec2(0.5, -0.5); +#endif // MOTION_VECTOR_PREPASS + +#ifdef DEFERRED_PREPASS + // There isn't any material info available for this default prepass shader so we are just writing  + // emissive magenta out to the deferred gbuffer to be rendered by the first deferred lighting pass layer. + // This is here so if the default prepass fragment is used for deferred magenta will be rendered, and also + // as an example to show that a user could write to the deferred gbuffer if they were to start from this shader. + out.deferred = vec4(0u, bevy_pbr::rgb9e5::vec3_to_rgb9e5_(vec3(1.0, 0.0, 1.0)), 0u, 0u); + out.deferred_lighting_pass_id = 1u; +#endif + + return out; +} +#endif // PREPASS_FRAGMENT diff --git a/crates/libmarathon/src/render/pbr/prepass/prepass_bindings.rs b/crates/libmarathon/src/render/pbr/prepass/prepass_bindings.rs new file mode 100644 index 0000000..f3b9ca4 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/prepass/prepass_bindings.rs @@ -0,0 +1,75 @@ +use crate::render::prepass::ViewPrepassTextures; +use crate::render::render_resource::{ + binding_types::{ + texture_2d, texture_2d_multisampled, texture_depth_2d, texture_depth_2d_multisampled, + }, + BindGroupLayoutEntryBuilder, TextureAspect, TextureSampleType, TextureView, + TextureViewDescriptor, +}; +use bevy_utils::default; + +use crate::render::pbr::MeshPipelineViewLayoutKey; + +pub fn get_bind_group_layout_entries( + layout_key: MeshPipelineViewLayoutKey, +) -> [Option; 4] { + let mut entries: [Option; 4] = [None; 4]; + + let multisampled = layout_key.contains(MeshPipelineViewLayoutKey::MULTISAMPLED); + + if layout_key.contains(MeshPipelineViewLayoutKey::DEPTH_PREPASS) { + // Depth texture + entries[0] = if multisampled { + Some(texture_depth_2d_multisampled()) + } else { + Some(texture_depth_2d()) + }; + } + + if layout_key.contains(MeshPipelineViewLayoutKey::NORMAL_PREPASS) { + // Normal texture + entries[1] = if multisampled { + Some(texture_2d_multisampled(TextureSampleType::Float { + filterable: false, + })) + } else { + Some(texture_2d(TextureSampleType::Float { filterable: false })) + }; + } + + if layout_key.contains(MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS) { + // Motion Vectors texture + entries[2] = if multisampled { + Some(texture_2d_multisampled(TextureSampleType::Float { + filterable: false, + })) + } else { + Some(texture_2d(TextureSampleType::Float { filterable: false })) + }; + } + + if layout_key.contains(MeshPipelineViewLayoutKey::DEFERRED_PREPASS) { + // Deferred texture + entries[3] = Some(texture_2d(TextureSampleType::Uint)); + } + + entries +} + +pub fn get_bindings(prepass_textures: Option<&ViewPrepassTextures>) -> [Option; 4] { + let depth_desc = TextureViewDescriptor { + label: Some("prepass_depth"), + aspect: TextureAspect::DepthOnly, + ..default() + }; + let depth_view = prepass_textures + .and_then(|x| x.depth.as_ref()) + .map(|texture| texture.texture.texture.create_view(&depth_desc)); + + [ + depth_view, + prepass_textures.and_then(|pt| pt.normal_view().cloned()), + prepass_textures.and_then(|pt| pt.motion_vectors_view().cloned()), + prepass_textures.and_then(|pt| pt.deferred_view().cloned()), + ] +} diff --git a/crates/libmarathon/src/render/pbr/prepass/prepass_bindings.wgsl b/crates/libmarathon/src/render/pbr/prepass/prepass_bindings.wgsl new file mode 100644 index 0000000..141f7d7 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/prepass/prepass_bindings.wgsl @@ -0,0 +1,13 @@ +#define_import_path bevy_pbr::prepass_bindings + +struct PreviousViewUniforms { + view_from_world: mat4x4, + clip_from_world: mat4x4, + clip_from_view: mat4x4, + world_from_clip: mat4x4, + view_from_clip: mat4x4, +} + +@group(0) @binding(2) var previous_view_uniforms: PreviousViewUniforms; + +// Material bindings will be in @group(2) diff --git a/crates/libmarathon/src/render/pbr/prepass/prepass_io.wgsl b/crates/libmarathon/src/render/pbr/prepass/prepass_io.wgsl new file mode 100644 index 0000000..c3c0e55 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/prepass/prepass_io.wgsl @@ -0,0 +1,100 @@ +#define_import_path bevy_pbr::prepass_io + +// Most of these attributes are not used in the default prepass fragment shader, but they are still needed so we can +// pass them to custom prepass shaders like pbr_prepass.wgsl. +struct Vertex { + @builtin(instance_index) instance_index: u32, + @location(0) position: vec3, + +#ifdef VERTEX_UVS_A + @location(1) uv: vec2, +#endif + +#ifdef VERTEX_UVS_B + @location(2) uv_b: vec2, +#endif + +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS +#ifdef VERTEX_NORMALS + @location(3) normal: vec3, +#endif +#ifdef VERTEX_TANGENTS + @location(4) tangent: vec4, +#endif +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + +#ifdef SKINNED + @location(5) joint_indices: vec4, + @location(6) joint_weights: vec4, +#endif + +#ifdef VERTEX_COLORS + @location(7) color: vec4, +#endif + +#ifdef MORPH_TARGETS + @builtin(vertex_index) index: u32, +#endif // MORPH_TARGETS +} + +struct VertexOutput { + // This is `clip position` when the struct is used as a vertex stage output + // and `frag coord` when used as a fragment stage input + @builtin(position) position: vec4, + +#ifdef VERTEX_UVS_A + @location(0) uv: vec2, +#endif + +#ifdef VERTEX_UVS_B + @location(1) uv_b: vec2, +#endif + +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS + @location(2) world_normal: vec3, +#ifdef VERTEX_TANGENTS + @location(3) world_tangent: vec4, +#endif +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + + @location(4) world_position: vec4, +#ifdef MOTION_VECTOR_PREPASS + @location(5) previous_world_position: vec4, +#endif + +#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION + @location(6) unclipped_depth: f32, +#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION +#ifdef VERTEX_OUTPUT_INSTANCE_INDEX + @location(7) instance_index: u32, +#endif + +#ifdef VERTEX_COLORS + @location(8) color: vec4, +#endif + +#ifdef VISIBILITY_RANGE_DITHER + @location(9) @interpolate(flat) visibility_range_dither: i32, +#endif // VISIBILITY_RANGE_DITHER +} + +#ifdef PREPASS_FRAGMENT +struct FragmentOutput { +#ifdef NORMAL_PREPASS + @location(0) normal: vec4, +#endif + +#ifdef MOTION_VECTOR_PREPASS + @location(1) motion_vector: vec2, +#endif + +#ifdef DEFERRED_PREPASS + @location(2) deferred: vec4, + @location(3) deferred_lighting_pass_id: u32, +#endif + +#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION + @builtin(frag_depth) frag_depth: f32, +#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION +} +#endif //PREPASS_FRAGMENT diff --git a/crates/libmarathon/src/render/pbr/prepass/prepass_utils.wgsl b/crates/libmarathon/src/render/pbr/prepass/prepass_utils.wgsl new file mode 100644 index 0000000..42f403c --- /dev/null +++ b/crates/libmarathon/src/render/pbr/prepass/prepass_utils.wgsl @@ -0,0 +1,35 @@ +#define_import_path bevy_pbr::prepass_utils + +#import bevy_pbr::mesh_view_bindings as view_bindings + +#ifdef DEPTH_PREPASS +fn prepass_depth(frag_coord: vec4, sample_index: u32) -> f32 { +#ifdef MULTISAMPLED + return textureLoad(view_bindings::depth_prepass_texture, vec2(frag_coord.xy), i32(sample_index)); +#else // MULTISAMPLED + return textureLoad(view_bindings::depth_prepass_texture, vec2(frag_coord.xy), 0); +#endif // MULTISAMPLED +} +#endif // DEPTH_PREPASS + +#ifdef NORMAL_PREPASS +fn prepass_normal(frag_coord: vec4, sample_index: u32) -> vec3 { +#ifdef MULTISAMPLED + let normal_sample = textureLoad(view_bindings::normal_prepass_texture, vec2(frag_coord.xy), i32(sample_index)); +#else + let normal_sample = textureLoad(view_bindings::normal_prepass_texture, vec2(frag_coord.xy), 0); +#endif // MULTISAMPLED + return normalize(normal_sample.xyz * 2.0 - vec3(1.0)); +} +#endif // NORMAL_PREPASS + +#ifdef MOTION_VECTOR_PREPASS +fn prepass_motion_vector(frag_coord: vec4, sample_index: u32) -> vec2 { +#ifdef MULTISAMPLED + let motion_vector_sample = textureLoad(view_bindings::motion_vector_prepass_texture, vec2(frag_coord.xy), i32(sample_index)); +#else + let motion_vector_sample = textureLoad(view_bindings::motion_vector_prepass_texture, vec2(frag_coord.xy), 0); +#endif + return motion_vector_sample.rg; +} +#endif // MOTION_VECTOR_PREPASS diff --git a/crates/libmarathon/src/render/pbr/render/build_indirect_params.wgsl b/crates/libmarathon/src/render/pbr/render/build_indirect_params.wgsl new file mode 100644 index 0000000..5ca6d4c --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/build_indirect_params.wgsl @@ -0,0 +1,142 @@ +// Builds GPU indirect draw parameters from metadata. +// +// This only runs when indirect drawing is enabled. It takes the output of +// `mesh_preprocess.wgsl` and creates indirect parameters for the GPU. +// +// This shader runs separately for indexed and non-indexed meshes. Unlike +// `mesh_preprocess.wgsl`, which runs one instance per mesh *instance*, one +// instance of this shader corresponds to a single *batch* which could contain +// arbitrarily many instances of a single mesh. + +#import bevy_pbr::mesh_preprocess_types::{ + IndirectBatchSet, + IndirectParametersIndexed, + IndirectParametersNonIndexed, + IndirectParametersCpuMetadata, + IndirectParametersGpuMetadata, + MeshInput +} + +// The data for each mesh that the CPU supplied to the GPU. +@group(0) @binding(0) var current_input: array; + +// Data that we use to generate the indirect parameters. +// +// The `mesh_preprocess.wgsl` shader emits these. +@group(0) @binding(1) var indirect_parameters_cpu_metadata: + array; + +@group(0) @binding(2) var indirect_parameters_gpu_metadata: + array; + +// Information about each batch set. +// +// A *batch set* is a set of meshes that might be multi-drawn together. +@group(0) @binding(3) var indirect_batch_sets: array; + +#ifdef INDEXED +// The buffer of indirect draw parameters that we generate, and that the GPU +// reads to issue the draws. +// +// This buffer is for indexed meshes. +@group(0) @binding(4) var indirect_parameters: + array; +#else // INDEXED +// The buffer of indirect draw parameters that we generate, and that the GPU +// reads to issue the draws. +// +// This buffer is for non-indexed meshes. +@group(0) @binding(4) var indirect_parameters: + array; +#endif // INDEXED + +@compute +@workgroup_size(64) +fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Figure out our instance index (i.e. batch index). If this thread doesn't + // correspond to any index, bail. + let instance_index = global_invocation_id.x; + if (instance_index >= arrayLength(&indirect_parameters_cpu_metadata)) { + return; + } + + // Unpack the metadata for this batch. + let base_output_index = indirect_parameters_cpu_metadata[instance_index].base_output_index; + let batch_set_index = indirect_parameters_cpu_metadata[instance_index].batch_set_index; + let mesh_index = indirect_parameters_gpu_metadata[instance_index].mesh_index; + + // If we aren't using `multi_draw_indirect_count`, we have a 1:1 fixed + // assignment of batches to slots in the indirect parameters buffer, so we + // can just use the instance index as the index of our indirect parameters. + let early_instance_count = + indirect_parameters_gpu_metadata[instance_index].early_instance_count; + let late_instance_count = indirect_parameters_gpu_metadata[instance_index].late_instance_count; + + // If in the early phase, we draw only the early meshes. If in the late + // phase, we draw only the late meshes. If in the main phase, draw all the + // meshes. +#ifdef EARLY_PHASE + let instance_count = early_instance_count; +#else // EARLY_PHASE +#ifdef LATE_PHASE + let instance_count = late_instance_count; +#else // LATE_PHASE + let instance_count = early_instance_count + late_instance_count; +#endif // LATE_PHASE +#endif // EARLY_PHASE + + var indirect_parameters_index = instance_index; + + // If the current hardware and driver support `multi_draw_indirect_count`, + // dynamically reserve an index for the indirect parameters we're to + // generate. +#ifdef MULTI_DRAW_INDIRECT_COUNT_SUPPORTED + // If this batch belongs to a batch set, then allocate space for the + // indirect commands in that batch set. + if (batch_set_index != 0xffffffffu) { + // Bail out now if there are no instances. Note that we can only bail if + // we're in a batch set. That's because only batch sets are drawn using + // `multi_draw_indirect_count`. If we aren't using + // `multi_draw_indirect_count`, then we need to continue in order to + // zero out the instance count; otherwise, it'll have garbage data in + // it. + if (instance_count == 0u) { + return; + } + + let indirect_parameters_base = + indirect_batch_sets[batch_set_index].indirect_parameters_base; + let indirect_parameters_offset = + atomicAdd(&indirect_batch_sets[batch_set_index].indirect_parameters_count, 1u); + + indirect_parameters_index = indirect_parameters_base + indirect_parameters_offset; + } +#endif // MULTI_DRAW_INDIRECT_COUNT_SUPPORTED + + // Build up the indirect parameters. The structures for indexed and + // non-indexed meshes are slightly different. + + indirect_parameters[indirect_parameters_index].instance_count = instance_count; + +#ifdef LATE_PHASE + // The late mesh instances are stored after the early mesh instances, so we + // offset the output index by the number of early mesh instances. + indirect_parameters[indirect_parameters_index].first_instance = + base_output_index + early_instance_count; +#else // LATE_PHASE + indirect_parameters[indirect_parameters_index].first_instance = base_output_index; +#endif // LATE_PHASE + + indirect_parameters[indirect_parameters_index].base_vertex = + current_input[mesh_index].first_vertex_index; + +#ifdef INDEXED + indirect_parameters[indirect_parameters_index].index_count = + current_input[mesh_index].index_count; + indirect_parameters[indirect_parameters_index].first_index = + current_input[mesh_index].first_index_index; +#else // INDEXED + indirect_parameters[indirect_parameters_index].vertex_count = + current_input[mesh_index].index_count; +#endif // INDEXED +} diff --git a/crates/libmarathon/src/render/pbr/render/clustered_forward.wgsl b/crates/libmarathon/src/render/pbr/render/clustered_forward.wgsl new file mode 100644 index 0000000..aa3fb4f --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/clustered_forward.wgsl @@ -0,0 +1,193 @@ +#define_import_path bevy_pbr::clustered_forward + +#import bevy_pbr::{ + mesh_view_bindings as bindings, + utils::rand_f, +} + +#import bevy_render::{ + color_operations::hsv_to_rgb, + maths::PI_2, +} + +// Offsets within the `cluster_offsets_and_counts` buffer for a single cluster. +// +// These offsets must be monotonically nondecreasing. That is, indices are +// always sorted into the following order: point lights, spot lights, reflection +// probes, irradiance volumes. +struct ClusterableObjectIndexRanges { + // The offset of the index of the first point light. + first_point_light_index_offset: u32, + // The offset of the index of the first spot light, which also terminates + // the list of point lights. + first_spot_light_index_offset: u32, + // The offset of the index of the first reflection probe, which also + // terminates the list of spot lights. + first_reflection_probe_index_offset: u32, + // The offset of the index of the first irradiance volumes, which also + // terminates the list of reflection probes. + first_irradiance_volume_index_offset: u32, + first_decal_offset: u32, + // One past the offset of the index of the final clusterable object for this + // cluster. + last_clusterable_object_index_offset: u32, +} + +// NOTE: Keep in sync with bevy_pbr/src/light.rs +fn view_z_to_z_slice(view_z: f32, is_orthographic: bool) -> u32 { + var z_slice: u32 = 0u; + if is_orthographic { + // NOTE: view_z is correct in the orthographic case + z_slice = u32(floor((view_z - bindings::lights.cluster_factors.z) * bindings::lights.cluster_factors.w)); + } else { + // NOTE: had to use -view_z to make it positive else log(negative) is nan + z_slice = u32(log(-view_z) * bindings::lights.cluster_factors.z - bindings::lights.cluster_factors.w + 1.0); + } + // NOTE: We use min as we may limit the far z plane used for clustering to be closer than + // the furthest thing being drawn. This means that we need to limit to the maximum cluster. + return min(z_slice, bindings::lights.cluster_dimensions.z - 1u); +} + +fn fragment_cluster_index(frag_coord: vec2, view_z: f32, is_orthographic: bool) -> u32 { + let xy = vec2(floor((frag_coord - bindings::view.viewport.xy) * bindings::lights.cluster_factors.xy)); + let z_slice = view_z_to_z_slice(view_z, is_orthographic); + // NOTE: Restricting cluster index to avoid undefined behavior when accessing uniform buffer + // arrays based on the cluster index. + return min( + (xy.y * bindings::lights.cluster_dimensions.x + xy.x) * bindings::lights.cluster_dimensions.z + z_slice, + bindings::lights.cluster_dimensions.w - 1u + ); +} + +// this must match CLUSTER_COUNT_SIZE in light.rs +const CLUSTER_COUNT_SIZE = 9u; + +// Returns the indices of clusterable objects belonging to the given cluster. +// +// Note that if fewer than 3 SSBO bindings are available (in WebGL 2, +// primarily), light probes aren't clustered, and therefore both light probe +// index ranges will be empty. +fn unpack_clusterable_object_index_ranges(cluster_index: u32) -> ClusterableObjectIndexRanges { +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + + let offset_and_counts_a = bindings::cluster_offsets_and_counts.data[cluster_index][0]; + let offset_and_counts_b = bindings::cluster_offsets_and_counts.data[cluster_index][1]; + + // Sum up the counts to produce the range brackets. + // + // We could have stored the range brackets in `cluster_offsets_and_counts` + // directly, but doing it this way makes the logic in this path more + // consistent with the WebGL 2 path below. + let point_light_offset = offset_and_counts_a.x; + let spot_light_offset = point_light_offset + offset_and_counts_a.y; + let reflection_probe_offset = spot_light_offset + offset_and_counts_a.z; + let irradiance_volume_offset = reflection_probe_offset + offset_and_counts_a.w; + let decal_offset = irradiance_volume_offset + offset_and_counts_b.x; + let last_clusterable_offset = decal_offset + offset_and_counts_b.y; + return ClusterableObjectIndexRanges( + point_light_offset, + spot_light_offset, + reflection_probe_offset, + irradiance_volume_offset, + decal_offset, + last_clusterable_offset + ); + +#else // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + + let raw_offset_and_counts = bindings::cluster_offsets_and_counts.data[cluster_index >> 2u][cluster_index & ((1u << 2u) - 1u)]; + // [ 31 .. 18 | 17 .. 9 | 8 .. 0 ] + // [ offset | point light count | spot light count ] + let offset_and_counts = vec3( + (raw_offset_and_counts >> (CLUSTER_COUNT_SIZE * 2u)) & ((1u << (32u - (CLUSTER_COUNT_SIZE * 2u))) - 1u), + (raw_offset_and_counts >> CLUSTER_COUNT_SIZE) & ((1u << CLUSTER_COUNT_SIZE) - 1u), + raw_offset_and_counts & ((1u << CLUSTER_COUNT_SIZE) - 1u), + ); + + // We don't cluster reflection probes or irradiance volumes on this + // platform, as there's no room in the UBO. Thus, those offset ranges + // (corresponding to `offset_d` and `offset_e` above) are empty and are + // simply copies of `offset_c`. + + let offset_a = offset_and_counts.x; + let offset_b = offset_a + offset_and_counts.y; + let offset_c = offset_b + offset_and_counts.z; + + return ClusterableObjectIndexRanges(offset_a, offset_b, offset_c, offset_c, offset_c, offset_c); + +#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 +} + +// Returns the index of the clusterable object at the given offset. +// +// Note that, in the case of a light probe, the index refers to an element in +// one of the two `light_probes` sublists, not the `clusterable_objects` list. +fn get_clusterable_object_id(index: u32) -> u32 { +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + return bindings::clusterable_object_index_lists.data[index]; +#else + // The index is correct but in clusterable_object_index_lists we pack 4 u8s into a u32 + // This means the index into clusterable_object_index_lists is index / 4 + let indices = bindings::clusterable_object_index_lists.data[index >> 4u][(index >> 2u) & + ((1u << 2u) - 1u)]; + // And index % 4 gives the sub-index of the u8 within the u32 so we shift by 8 * sub-index + return (indices >> (8u * (index & ((1u << 2u) - 1u)))) & ((1u << 8u) - 1u); +#endif +} + +fn cluster_debug_visualization( + input_color: vec4, + view_z: f32, + is_orthographic: bool, + clusterable_object_index_ranges: ClusterableObjectIndexRanges, + cluster_index: u32, +) -> vec4 { + var output_color = input_color; + + // Cluster allocation debug (using 'over' alpha blending) +#ifdef CLUSTERED_FORWARD_DEBUG_Z_SLICES + // NOTE: This debug mode visualizes the z-slices + let cluster_overlay_alpha = 0.1; + var z_slice: u32 = view_z_to_z_slice(view_z, is_orthographic); + // A hack to make the colors alternate a bit more + if (z_slice & 1u) == 1u { + z_slice = z_slice + bindings::lights.cluster_dimensions.z / 2u; + } + let slice_color_hsv = vec3( + f32(z_slice) / f32(bindings::lights.cluster_dimensions.z + 1u) * PI_2, + 1.0, + 0.5 + ); + let slice_color = hsv_to_rgb(slice_color_hsv); + output_color = vec4( + (1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * slice_color, + output_color.a + ); +#endif // CLUSTERED_FORWARD_DEBUG_Z_SLICES +#ifdef CLUSTERED_FORWARD_DEBUG_CLUSTER_COMPLEXITY + // NOTE: This debug mode visualizes the number of clusterable objects within + // the cluster that contains the fragment. It shows a sort of cluster + // complexity measure. + let cluster_overlay_alpha = 0.1; + let max_complexity_per_cluster = 64.0; + let object_count = clusterable_object_index_ranges.first_reflection_probe_index_offset - + clusterable_object_index_ranges.first_point_light_index_offset; + output_color.r = (1.0 - cluster_overlay_alpha) * output_color.r + cluster_overlay_alpha * + smoothstep(0.0, max_complexity_per_cluster, f32(object_count)); + output_color.g = (1.0 - cluster_overlay_alpha) * output_color.g + cluster_overlay_alpha * + (1.0 - smoothstep(0.0, max_complexity_per_cluster, f32(object_count))); +#endif // CLUSTERED_FORWARD_DEBUG_CLUSTER_COMPLEXITY +#ifdef CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY + // NOTE: Visualizes the cluster to which the fragment belongs + let cluster_overlay_alpha = 0.1; + var rng = cluster_index; + let cluster_color_hsv = vec3(rand_f(&rng) * PI_2, 1.0, 0.5); + let cluster_color = hsv_to_rgb(cluster_color_hsv); + output_color = vec4( + (1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * cluster_color, + output_color.a + ); +#endif // CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY + + return output_color; +} diff --git a/crates/libmarathon/src/render/pbr/render/fog.rs b/crates/libmarathon/src/render/pbr/render/fog.rs new file mode 100644 index 0000000..d8e31cd --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/fog.rs @@ -0,0 +1,144 @@ +use bevy_app::{App, Plugin}; +use bevy_color::{ColorToComponents, LinearRgba}; +use bevy_ecs::prelude::*; +use bevy_math::{Vec3, Vec4}; +use crate::render::{ + extract_component::ExtractComponentPlugin, + render_resource::{DynamicUniformBuffer, ShaderType}, + renderer::{RenderDevice, RenderQueue}, + view::ExtractedView, + Render, RenderApp, RenderSystems, +}; +use bevy_shader::load_shader_library; + +use crate::render::pbr::{DistanceFog, FogFalloff}; + +/// The GPU-side representation of the fog configuration that's sent as a uniform to the shader +#[derive(Copy, Clone, ShaderType, Default, Debug)] +pub struct GpuFog { + /// Fog color + base_color: Vec4, + /// The color used for the fog where the view direction aligns with directional lights + directional_light_color: Vec4, + /// Allocated differently depending on fog mode. + /// See `mesh_view_types.wgsl` for a detailed explanation + be: Vec3, + /// The exponent applied to the directional light alignment calculation + directional_light_exponent: f32, + /// Allocated differently depending on fog mode. + /// See `mesh_view_types.wgsl` for a detailed explanation + bi: Vec3, + /// Unsigned int representation of the active fog falloff mode + mode: u32, +} + +// Important: These must be kept in sync with `mesh_view_types.wgsl` +const GPU_FOG_MODE_OFF: u32 = 0; +const GPU_FOG_MODE_LINEAR: u32 = 1; +const GPU_FOG_MODE_EXPONENTIAL: u32 = 2; +const GPU_FOG_MODE_EXPONENTIAL_SQUARED: u32 = 3; +const GPU_FOG_MODE_ATMOSPHERIC: u32 = 4; + +/// Metadata for fog +#[derive(Default, Resource)] +pub struct FogMeta { + pub gpu_fogs: DynamicUniformBuffer, +} + +/// Prepares fog metadata and writes the fog-related uniform buffers to the GPU +pub fn prepare_fog( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut fog_meta: ResMut, + views: Query<(Entity, Option<&DistanceFog>), With>, +) { + let views_iter = views.iter(); + let view_count = views_iter.len(); + let Some(mut writer) = fog_meta + .gpu_fogs + .get_writer(view_count, &render_device, &render_queue) + else { + return; + }; + for (entity, fog) in views_iter { + let gpu_fog = if let Some(fog) = fog { + match &fog.falloff { + FogFalloff::Linear { start, end } => GpuFog { + mode: GPU_FOG_MODE_LINEAR, + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), + directional_light_exponent: fog.directional_light_exponent, + be: Vec3::new(*start, *end, 0.0), + ..Default::default() + }, + FogFalloff::Exponential { density } => GpuFog { + mode: GPU_FOG_MODE_EXPONENTIAL, + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), + directional_light_exponent: fog.directional_light_exponent, + be: Vec3::new(*density, 0.0, 0.0), + ..Default::default() + }, + FogFalloff::ExponentialSquared { density } => GpuFog { + mode: GPU_FOG_MODE_EXPONENTIAL_SQUARED, + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), + directional_light_exponent: fog.directional_light_exponent, + be: Vec3::new(*density, 0.0, 0.0), + ..Default::default() + }, + FogFalloff::Atmospheric { + extinction, + inscattering, + } => GpuFog { + mode: GPU_FOG_MODE_ATMOSPHERIC, + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), + directional_light_exponent: fog.directional_light_exponent, + be: *extinction, + bi: *inscattering, + }, + } + } else { + // If no fog is added to a camera, by default it's off + GpuFog { + mode: GPU_FOG_MODE_OFF, + ..Default::default() + } + }; + + // This is later read by `SetMeshViewBindGroup` + commands.entity(entity).insert(ViewFogUniformOffset { + offset: writer.write(&gpu_fog), + }); + } +} + +/// Inserted on each `Entity` with an `ExtractedView` to keep track of its offset +/// in the `gpu_fogs` `DynamicUniformBuffer` within `FogMeta` +#[derive(Component)] +pub struct ViewFogUniformOffset { + pub offset: u32, +} + +/// A plugin that consolidates fog extraction, preparation and related resources/assets +pub struct FogPlugin; + +impl Plugin for FogPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "fog.wgsl"); + + app.add_plugins(ExtractComponentPlugin::::default()); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .add_systems(Render, prepare_fog.in_set(RenderSystems::PrepareResources)); + } + } +} diff --git a/crates/libmarathon/src/render/pbr/render/fog.wgsl b/crates/libmarathon/src/render/pbr/render/fog.wgsl new file mode 100644 index 0000000..a9e28ae --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/fog.wgsl @@ -0,0 +1,79 @@ +#define_import_path bevy_pbr::fog + +#import bevy_pbr::{ + mesh_view_bindings::fog, + mesh_view_types::Fog, +} + +// Fog formulas adapted from: +// https://learn.microsoft.com/en-us/windows/win32/direct3d9/fog-formulas +// https://catlikecoding.com/unity/tutorials/rendering/part-14/ +// https://iquilezles.org/articles/fog/ (Atmospheric Fog and Scattering) + +fn scattering_adjusted_fog_color( + fog_params: Fog, + scattering: vec3, +) -> vec4 { + if (fog_params.directional_light_color.a > 0.0) { + return vec4( + fog_params.base_color.rgb + + scattering * fog_params.directional_light_color.rgb * fog_params.directional_light_color.a, + fog_params.base_color.a, + ); + } else { + return fog_params.base_color; + } +} + +fn linear_fog( + fog_params: Fog, + input_color: vec4, + distance: f32, + scattering: vec3, +) -> vec4 { + var fog_color = scattering_adjusted_fog_color(fog_params, scattering); + let start = fog_params.be.x; + let end = fog_params.be.y; + fog_color.a *= 1.0 - clamp((end - distance) / (end - start), 0.0, 1.0); + return vec4(mix(input_color.rgb, fog_color.rgb, fog_color.a), input_color.a); +} + +fn exponential_fog( + fog_params: Fog, + input_color: vec4, + distance: f32, + scattering: vec3, +) -> vec4 { + var fog_color = scattering_adjusted_fog_color(fog_params, scattering); + let density = fog_params.be.x; + fog_color.a *= 1.0 - 1.0 / exp(distance * density); + return vec4(mix(input_color.rgb, fog_color.rgb, fog_color.a), input_color.a); +} + +fn exponential_squared_fog( + fog_params: Fog, + input_color: vec4, + distance: f32, + scattering: vec3, +) -> vec4 { + var fog_color = scattering_adjusted_fog_color(fog_params, scattering); + let distance_times_density = distance * fog_params.be.x; + fog_color.a *= 1.0 - 1.0 / exp(distance_times_density * distance_times_density); + return vec4(mix(input_color.rgb, fog_color.rgb, fog_color.a), input_color.a); +} + +fn atmospheric_fog( + fog_params: Fog, + input_color: vec4, + distance: f32, + scattering: vec3, +) -> vec4 { + var fog_color = scattering_adjusted_fog_color(fog_params, scattering); + let extinction_factor = 1.0 - 1.0 / exp(distance * fog_params.be); + let inscattering_factor = 1.0 - 1.0 / exp(distance * fog_params.bi); + return vec4( + input_color.rgb * (1.0 - extinction_factor * fog_color.a) + + fog_color.rgb * inscattering_factor * fog_color.a, + input_color.a + ); +} diff --git a/crates/libmarathon/src/render/pbr/render/forward_io.wgsl b/crates/libmarathon/src/render/pbr/render/forward_io.wgsl new file mode 100644 index 0000000..99f2ecc --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/forward_io.wgsl @@ -0,0 +1,60 @@ +#define_import_path bevy_pbr::forward_io + +struct Vertex { + @builtin(instance_index) instance_index: u32, +#ifdef VERTEX_POSITIONS + @location(0) position: vec3, +#endif +#ifdef VERTEX_NORMALS + @location(1) normal: vec3, +#endif +#ifdef VERTEX_UVS_A + @location(2) uv: vec2, +#endif +#ifdef VERTEX_UVS_B + @location(3) uv_b: vec2, +#endif +#ifdef VERTEX_TANGENTS + @location(4) tangent: vec4, +#endif +#ifdef VERTEX_COLORS + @location(5) color: vec4, +#endif +#ifdef SKINNED + @location(6) joint_indices: vec4, + @location(7) joint_weights: vec4, +#endif +#ifdef MORPH_TARGETS + @builtin(vertex_index) index: u32, +#endif +}; + +struct VertexOutput { + // This is `clip position` when the struct is used as a vertex stage output + // and `frag coord` when used as a fragment stage input + @builtin(position) position: vec4, + @location(0) world_position: vec4, + @location(1) world_normal: vec3, +#ifdef VERTEX_UVS_A + @location(2) uv: vec2, +#endif +#ifdef VERTEX_UVS_B + @location(3) uv_b: vec2, +#endif +#ifdef VERTEX_TANGENTS + @location(4) world_tangent: vec4, +#endif +#ifdef VERTEX_COLORS + @location(5) color: vec4, +#endif +#ifdef VERTEX_OUTPUT_INSTANCE_INDEX + @location(6) @interpolate(flat) instance_index: u32, +#endif +#ifdef VISIBILITY_RANGE_DITHER + @location(7) @interpolate(flat) visibility_range_dither: i32, +#endif +} + +struct FragmentOutput { + @location(0) color: vec4, +} diff --git a/crates/libmarathon/src/render/pbr/render/gpu_preprocess.rs b/crates/libmarathon/src/render/pbr/render/gpu_preprocess.rs new file mode 100644 index 0000000..3cbaa4a --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/gpu_preprocess.rs @@ -0,0 +1,2704 @@ +//! GPU mesh preprocessing. +//! +//! This is an optional pass that uses a compute shader to reduce the amount of +//! data that has to be transferred from the CPU to the GPU. When enabled, +//! instead of transferring [`MeshUniform`]s to the GPU, we transfer the smaller +//! [`MeshInputUniform`]s instead and use the GPU to calculate the remaining +//! derived fields in [`MeshUniform`]. + +use core::num::{NonZero, NonZeroU64}; + +use bevy_app::{App, Plugin}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; +use crate::render::{ + core_3d::graph::{Core3d, Node3d}, + experimental::mip_generation::ViewDepthPyramid, + prepass::{DepthPrepass, PreviousViewData, PreviousViewUniformOffset, PreviousViewUniforms}, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + prelude::resource_exists, + query::{Has, Or, QueryState, With, Without}, + resource::Resource, + schedule::IntoScheduleConfigs as _, + system::{lifetimeless::Read, Commands, Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use crate::render::{ + batching::gpu_preprocessing::{ + BatchedInstanceBuffers, GpuOcclusionCullingWorkItemBuffers, GpuPreprocessingMode, + GpuPreprocessingSupport, IndirectBatchSet, IndirectParametersBuffers, + IndirectParametersCpuMetadata, IndirectParametersGpuMetadata, IndirectParametersIndexed, + IndirectParametersNonIndexed, LatePreprocessWorkItemIndirectParameters, PreprocessWorkItem, + PreprocessWorkItemBuffers, UntypedPhaseBatchedInstanceBuffers, + UntypedPhaseIndirectParametersBuffers, + }, + diagnostic::RecordDiagnostics, + experimental::occlusion_culling::OcclusionCulling, + render_graph::{Node, NodeRunError, RenderGraphContext, RenderGraphExt}, + render_resource::{ + binding_types::{storage_buffer, storage_buffer_read_only, texture_2d, uniform_buffer}, + BindGroup, BindGroupEntries, BindGroupLayout, BindingResource, Buffer, BufferBinding, + CachedComputePipelineId, ComputePassDescriptor, ComputePipelineDescriptor, + DynamicBindGroupLayoutEntries, PipelineCache, PushConstantRange, RawBufferVec, + ShaderStages, ShaderType, SpecializedComputePipeline, SpecializedComputePipelines, + TextureSampleType, UninitBufferVec, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + settings::WgpuFeatures, + view::{ExtractedView, NoIndirectDrawing, ViewUniform, ViewUniformOffset, ViewUniforms}, + Render, RenderApp, RenderSystems, +}; +use bevy_shader::Shader; +use bevy_utils::{default, TypeIdMap}; +use bitflags::bitflags; +use smallvec::{smallvec, SmallVec}; +use tracing::warn; + +use crate::render::pbr::{ + graph::NodePbr, MeshCullingData, MeshCullingDataBuffer, MeshInputUniform, MeshUniform, +}; + +use super::{ShadowView, ViewLightEntities}; + +/// The GPU workgroup size. +const WORKGROUP_SIZE: usize = 64; + +/// A plugin that builds mesh uniforms on GPU. +/// +/// This will only be added if the platform supports compute shaders (e.g. not +/// on WebGL 2). +pub struct GpuMeshPreprocessPlugin { + /// Whether we're building [`MeshUniform`]s on GPU. + /// + /// This requires compute shader support and so will be forcibly disabled if + /// the platform doesn't support those. + pub use_gpu_instance_buffer_builder: bool, +} + +/// The render node that clears out the GPU-side indirect metadata buffers. +/// +/// This is only used when indirect drawing is enabled. +#[derive(Default)] +pub struct ClearIndirectParametersMetadataNode; + +/// The render node for the first mesh preprocessing pass. +/// +/// This pass runs a compute shader to cull meshes outside the view frustum (if +/// that wasn't done by the CPU), cull meshes that weren't visible last frame +/// (if occlusion culling is on), transform them, and, if indirect drawing is +/// on, populate indirect draw parameter metadata for the subsequent +/// [`EarlyPrepassBuildIndirectParametersNode`]. +pub struct EarlyGpuPreprocessNode { + view_query: QueryState< + ( + Read, + Option>, + Option>, + Has, + Has, + ), + Without, + >, + main_view_query: QueryState>, +} + +/// The render node for the second mesh preprocessing pass. +/// +/// This pass runs a compute shader to cull meshes outside the view frustum (if +/// that wasn't done by the CPU), cull meshes that were neither visible last +/// frame nor visible this frame (if occlusion culling is on), transform them, +/// and, if indirect drawing is on, populate the indirect draw parameter +/// metadata for the subsequent [`LatePrepassBuildIndirectParametersNode`]. +pub struct LateGpuPreprocessNode { + view_query: QueryState< + ( + Read, + Read, + Read, + ), + ( + Without, + Without, + With, + With, + ), + >, +} + +/// The render node for the part of the indirect parameter building pass that +/// draws the meshes visible from the previous frame. +/// +/// This node runs a compute shader on the output of the +/// [`EarlyGpuPreprocessNode`] in order to transform the +/// [`IndirectParametersGpuMetadata`] into properly-formatted +/// [`IndirectParametersIndexed`] and [`IndirectParametersNonIndexed`]. +pub struct EarlyPrepassBuildIndirectParametersNode { + view_query: QueryState< + Read, + ( + Without, + Without, + Or<(With, With)>, + ), + >, +} + +/// The render node for the part of the indirect parameter building pass that +/// draws the meshes that are potentially visible on this frame but weren't +/// visible on the previous frame. +/// +/// This node runs a compute shader on the output of the +/// [`LateGpuPreprocessNode`] in order to transform the +/// [`IndirectParametersGpuMetadata`] into properly-formatted +/// [`IndirectParametersIndexed`] and [`IndirectParametersNonIndexed`]. +pub struct LatePrepassBuildIndirectParametersNode { + view_query: QueryState< + Read, + ( + Without, + Without, + Or<(With, With)>, + With, + ), + >, +} + +/// The render node for the part of the indirect parameter building pass that +/// draws all meshes, both those that are newly-visible on this frame and those +/// that were visible last frame. +/// +/// This node runs a compute shader on the output of the +/// [`EarlyGpuPreprocessNode`] and [`LateGpuPreprocessNode`] in order to +/// transform the [`IndirectParametersGpuMetadata`] into properly-formatted +/// [`IndirectParametersIndexed`] and [`IndirectParametersNonIndexed`]. +pub struct MainBuildIndirectParametersNode { + view_query: QueryState< + Read, + (Without, Without), + >, +} + +/// The compute shader pipelines for the GPU mesh preprocessing and indirect +/// parameter building passes. +#[derive(Resource)] +pub struct PreprocessPipelines { + /// The pipeline used for CPU culling. This pipeline doesn't populate + /// indirect parameter metadata. + pub direct_preprocess: PreprocessPipeline, + /// The pipeline used for mesh preprocessing when GPU frustum culling is in + /// use, but occlusion culling isn't. + /// + /// This pipeline populates indirect parameter metadata. + pub gpu_frustum_culling_preprocess: PreprocessPipeline, + /// The pipeline used for the first phase of occlusion culling. + /// + /// This pipeline culls, transforms meshes, and populates indirect parameter + /// metadata. + pub early_gpu_occlusion_culling_preprocess: PreprocessPipeline, + /// The pipeline used for the second phase of occlusion culling. + /// + /// This pipeline culls, transforms meshes, and populates indirect parameter + /// metadata. + pub late_gpu_occlusion_culling_preprocess: PreprocessPipeline, + /// The pipeline that builds indirect draw parameters for indexed meshes, + /// when frustum culling is enabled but occlusion culling *isn't* enabled. + pub gpu_frustum_culling_build_indexed_indirect_params: BuildIndirectParametersPipeline, + /// The pipeline that builds indirect draw parameters for non-indexed + /// meshes, when frustum culling is enabled but occlusion culling *isn't* + /// enabled. + pub gpu_frustum_culling_build_non_indexed_indirect_params: BuildIndirectParametersPipeline, + /// Compute shader pipelines for the early prepass phase that draws meshes + /// visible in the previous frame. + pub early_phase: PreprocessPhasePipelines, + /// Compute shader pipelines for the late prepass phase that draws meshes + /// that weren't visible in the previous frame, but became visible this + /// frame. + pub late_phase: PreprocessPhasePipelines, + /// Compute shader pipelines for the main color phase. + pub main_phase: PreprocessPhasePipelines, +} + +/// Compute shader pipelines for a specific phase: early, late, or main. +/// +/// The distinction between these phases is relevant for occlusion culling. +#[derive(Clone)] +pub struct PreprocessPhasePipelines { + /// The pipeline that resets the indirect draw counts used in + /// `multi_draw_indirect_count` to 0 in preparation for a new pass. + pub reset_indirect_batch_sets: ResetIndirectBatchSetsPipeline, + /// The pipeline used for indexed indirect parameter building. + /// + /// This pipeline converts indirect parameter metadata into indexed indirect + /// parameters. + pub gpu_occlusion_culling_build_indexed_indirect_params: BuildIndirectParametersPipeline, + /// The pipeline used for non-indexed indirect parameter building. + /// + /// This pipeline converts indirect parameter metadata into non-indexed + /// indirect parameters. + pub gpu_occlusion_culling_build_non_indexed_indirect_params: BuildIndirectParametersPipeline, +} + +/// The pipeline for the GPU mesh preprocessing shader. +pub struct PreprocessPipeline { + /// The bind group layout for the compute shader. + pub bind_group_layout: BindGroupLayout, + /// The shader asset handle. + pub shader: Handle, + /// The pipeline ID for the compute shader. + /// + /// This gets filled in `prepare_preprocess_pipelines`. + pub pipeline_id: Option, +} + +/// The pipeline for the batch set count reset shader. +/// +/// This shader resets the indirect batch set count to 0 for each view. It runs +/// in between every phase (early, late, and main). +#[derive(Clone)] +pub struct ResetIndirectBatchSetsPipeline { + /// The bind group layout for the compute shader. + pub bind_group_layout: BindGroupLayout, + /// The shader asset handle. + pub shader: Handle, + /// The pipeline ID for the compute shader. + /// + /// This gets filled in `prepare_preprocess_pipelines`. + pub pipeline_id: Option, +} + +/// The pipeline for the indirect parameter building shader. +#[derive(Clone)] +pub struct BuildIndirectParametersPipeline { + /// The bind group layout for the compute shader. + pub bind_group_layout: BindGroupLayout, + /// The shader asset handle. + pub shader: Handle, + /// The pipeline ID for the compute shader. + /// + /// This gets filled in `prepare_preprocess_pipelines`. + pub pipeline_id: Option, +} + +bitflags! { + /// Specifies variants of the mesh preprocessing shader. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct PreprocessPipelineKey: u8 { + /// Whether GPU frustum culling is in use. + /// + /// This `#define`'s `FRUSTUM_CULLING` in the shader. + const FRUSTUM_CULLING = 1; + /// Whether GPU two-phase occlusion culling is in use. + /// + /// This `#define`'s `OCCLUSION_CULLING` in the shader. + const OCCLUSION_CULLING = 2; + /// Whether this is the early phase of GPU two-phase occlusion culling. + /// + /// This `#define`'s `EARLY_PHASE` in the shader. + const EARLY_PHASE = 4; + } + + /// Specifies variants of the indirect parameter building shader. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct BuildIndirectParametersPipelineKey: u8 { + /// Whether the indirect parameter building shader is processing indexed + /// meshes (those that have index buffers). + /// + /// This defines `INDEXED` in the shader. + const INDEXED = 1; + /// Whether the GPU and driver supports `multi_draw_indirect_count`. + /// + /// This defines `MULTI_DRAW_INDIRECT_COUNT_SUPPORTED` in the shader. + const MULTI_DRAW_INDIRECT_COUNT_SUPPORTED = 2; + /// Whether GPU two-phase occlusion culling is in use. + /// + /// This `#define`'s `OCCLUSION_CULLING` in the shader. + const OCCLUSION_CULLING = 4; + /// Whether this is the early phase of GPU two-phase occlusion culling. + /// + /// This `#define`'s `EARLY_PHASE` in the shader. + const EARLY_PHASE = 8; + /// Whether this is the late phase of GPU two-phase occlusion culling. + /// + /// This `#define`'s `LATE_PHASE` in the shader. + const LATE_PHASE = 16; + /// Whether this is the phase that runs after the early and late phases, + /// and right before the main drawing logic, when GPU two-phase + /// occlusion culling is in use. + /// + /// This `#define`'s `MAIN_PHASE` in the shader. + const MAIN_PHASE = 32; + } +} + +/// The compute shader bind group for the mesh preprocessing pass for each +/// render phase. +/// +/// This goes on the view. It maps the [`core::any::TypeId`] of a render phase +/// (e.g. [`bevy_core_pipeline::core_3d::Opaque3d`]) to the +/// [`PhasePreprocessBindGroups`] for that phase. +#[derive(Component, Clone, Deref, DerefMut)] +pub struct PreprocessBindGroups(pub TypeIdMap); + +/// The compute shader bind group for the mesh preprocessing step for a single +/// render phase on a single view. +#[derive(Clone)] +pub enum PhasePreprocessBindGroups { + /// The bind group used for the single invocation of the compute shader when + /// indirect drawing is *not* being used. + /// + /// Because direct drawing doesn't require splitting the meshes into indexed + /// and non-indexed meshes, there's only one bind group in this case. + Direct(BindGroup), + + /// The bind groups used for the compute shader when indirect drawing is + /// being used, but occlusion culling isn't being used. + /// + /// Because indirect drawing requires splitting the meshes into indexed and + /// non-indexed meshes, there are two bind groups here. + IndirectFrustumCulling { + /// The bind group for indexed meshes. + indexed: Option, + /// The bind group for non-indexed meshes. + non_indexed: Option, + }, + + /// The bind groups used for the compute shader when indirect drawing is + /// being used, but occlusion culling isn't being used. + /// + /// Because indirect drawing requires splitting the meshes into indexed and + /// non-indexed meshes, and because occlusion culling requires splitting + /// this phase into early and late versions, there are four bind groups + /// here. + IndirectOcclusionCulling { + /// The bind group for indexed meshes during the early mesh + /// preprocessing phase. + early_indexed: Option, + /// The bind group for non-indexed meshes during the early mesh + /// preprocessing phase. + early_non_indexed: Option, + /// The bind group for indexed meshes during the late mesh preprocessing + /// phase. + late_indexed: Option, + /// The bind group for non-indexed meshes during the late mesh + /// preprocessing phase. + late_non_indexed: Option, + }, +} + +/// The bind groups for the compute shaders that reset indirect draw counts and +/// build indirect parameters. +/// +/// There's one set of bind group for each phase. Phases are keyed off their +/// [`core::any::TypeId`]. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct BuildIndirectParametersBindGroups(pub TypeIdMap); + +impl BuildIndirectParametersBindGroups { + /// Creates a new, empty [`BuildIndirectParametersBindGroups`] table. + pub fn new() -> BuildIndirectParametersBindGroups { + Self::default() + } +} + +/// The per-phase set of bind groups for the compute shaders that reset indirect +/// draw counts and build indirect parameters. +pub struct PhaseBuildIndirectParametersBindGroups { + /// The bind group for the `reset_indirect_batch_sets.wgsl` shader, for + /// indexed meshes. + reset_indexed_indirect_batch_sets: Option, + /// The bind group for the `reset_indirect_batch_sets.wgsl` shader, for + /// non-indexed meshes. + reset_non_indexed_indirect_batch_sets: Option, + /// The bind group for the `build_indirect_params.wgsl` shader, for indexed + /// meshes. + build_indexed_indirect: Option, + /// The bind group for the `build_indirect_params.wgsl` shader, for + /// non-indexed meshes. + build_non_indexed_indirect: Option, +} + +/// Stops the `GpuPreprocessNode` attempting to generate the buffer for this view +/// useful to avoid duplicating effort if the bind group is shared between views +#[derive(Component, Default)] +pub struct SkipGpuPreprocess; + +impl Plugin for GpuMeshPreprocessPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "mesh_preprocess.wgsl"); + embedded_asset!(app, "reset_indirect_batch_sets.wgsl"); + embedded_asset!(app, "build_indirect_params.wgsl"); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + // This plugin does nothing if GPU instance buffer building isn't in + // use. + let gpu_preprocessing_support = render_app.world().resource::(); + if !self.use_gpu_instance_buffer_builder || !gpu_preprocessing_support.is_available() { + return; + } + + render_app + .init_resource::() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .add_systems( + Render, + ( + prepare_preprocess_pipelines.in_set(RenderSystems::Prepare), + prepare_preprocess_bind_groups + .run_if(resource_exists::>) + .in_set(RenderSystems::PrepareBindGroups), + write_mesh_culling_data_buffer.in_set(RenderSystems::PrepareResourcesFlush), + ), + ) + .add_render_graph_node::( + Core3d, + NodePbr::ClearIndirectParametersMetadata + ) + .add_render_graph_node::(Core3d, NodePbr::EarlyGpuPreprocess) + .add_render_graph_node::(Core3d, NodePbr::LateGpuPreprocess) + .add_render_graph_node::( + Core3d, + NodePbr::EarlyPrepassBuildIndirectParameters, + ) + .add_render_graph_node::( + Core3d, + NodePbr::LatePrepassBuildIndirectParameters, + ) + .add_render_graph_node::( + Core3d, + NodePbr::MainBuildIndirectParameters, + ) + .add_render_graph_edges( + Core3d, + ( + NodePbr::ClearIndirectParametersMetadata, + NodePbr::EarlyGpuPreprocess, + NodePbr::EarlyPrepassBuildIndirectParameters, + Node3d::EarlyPrepass, + Node3d::EarlyDeferredPrepass, + Node3d::EarlyDownsampleDepth, + NodePbr::LateGpuPreprocess, + NodePbr::LatePrepassBuildIndirectParameters, + Node3d::LatePrepass, + Node3d::LateDeferredPrepass, + NodePbr::MainBuildIndirectParameters, + Node3d::StartMainPass, + ), + ).add_render_graph_edges( + Core3d, + ( + NodePbr::EarlyPrepassBuildIndirectParameters, + NodePbr::EarlyShadowPass, + Node3d::EarlyDownsampleDepth, + ) + ).add_render_graph_edges( + Core3d, + ( + NodePbr::LatePrepassBuildIndirectParameters, + NodePbr::LateShadowPass, + NodePbr::MainBuildIndirectParameters, + ) + ); + } +} + +impl Node for ClearIndirectParametersMetadataNode { + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let Some(indirect_parameters_buffers) = world.get_resource::() + else { + return Ok(()); + }; + + // Clear out each indexed and non-indexed GPU-side buffer. + for phase_indirect_parameters_buffers in indirect_parameters_buffers.values() { + if let Some(indexed_gpu_metadata_buffer) = phase_indirect_parameters_buffers + .indexed + .gpu_metadata_buffer() + { + render_context.command_encoder().clear_buffer( + indexed_gpu_metadata_buffer, + 0, + Some( + phase_indirect_parameters_buffers.indexed.batch_count() as u64 + * size_of::() as u64, + ), + ); + } + + if let Some(non_indexed_gpu_metadata_buffer) = phase_indirect_parameters_buffers + .non_indexed + .gpu_metadata_buffer() + { + render_context.command_encoder().clear_buffer( + non_indexed_gpu_metadata_buffer, + 0, + Some( + phase_indirect_parameters_buffers.non_indexed.batch_count() as u64 + * size_of::() as u64, + ), + ); + } + } + + Ok(()) + } +} + +impl FromWorld for EarlyGpuPreprocessNode { + fn from_world(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + main_view_query: QueryState::new(world), + } + } +} + +impl Node for EarlyGpuPreprocessNode { + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + self.main_view_query.update_archetypes(world); + } + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let diagnostics = render_context.diagnostic_recorder(); + + // Grab the [`BatchedInstanceBuffers`]. + let batched_instance_buffers = + world.resource::>(); + + let pipeline_cache = world.resource::(); + let preprocess_pipelines = world.resource::(); + + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("early_mesh_preprocessing"), + timestamp_writes: None, + }); + let pass_span = diagnostics.pass_span(&mut compute_pass, "early_mesh_preprocessing"); + + let mut all_views: SmallVec<[_; 8]> = SmallVec::new(); + all_views.push(graph.view_entity()); + if let Ok(shadow_cascade_views) = + self.main_view_query.get_manual(world, graph.view_entity()) + { + all_views.extend(shadow_cascade_views.lights.iter().copied()); + } + + // Run the compute passes. + + for view_entity in all_views { + let Ok(( + view, + bind_groups, + view_uniform_offset, + no_indirect_drawing, + occlusion_culling, + )) = self.view_query.get_manual(world, view_entity) + else { + continue; + }; + + let Some(bind_groups) = bind_groups else { + continue; + }; + let Some(view_uniform_offset) = view_uniform_offset else { + continue; + }; + + // Select the right pipeline, depending on whether GPU culling is in + // use. + let maybe_pipeline_id = if no_indirect_drawing { + preprocess_pipelines.direct_preprocess.pipeline_id + } else if occlusion_culling { + preprocess_pipelines + .early_gpu_occlusion_culling_preprocess + .pipeline_id + } else { + preprocess_pipelines + .gpu_frustum_culling_preprocess + .pipeline_id + }; + + // Fetch the pipeline. + let Some(preprocess_pipeline_id) = maybe_pipeline_id else { + warn!("The build mesh uniforms pipeline wasn't ready"); + continue; + }; + + let Some(preprocess_pipeline) = + pipeline_cache.get_compute_pipeline(preprocess_pipeline_id) + else { + // This will happen while the pipeline is being compiled and is fine. + continue; + }; + + compute_pass.set_pipeline(preprocess_pipeline); + + // Loop over each render phase. + for (phase_type_id, batched_phase_instance_buffers) in + &batched_instance_buffers.phase_instance_buffers + { + // Grab the work item buffers for this view. + let Some(work_item_buffers) = batched_phase_instance_buffers + .work_item_buffers + .get(&view.retained_view_entity) + else { + continue; + }; + + // Fetch the bind group for the render phase. + let Some(phase_bind_groups) = bind_groups.get(phase_type_id) else { + continue; + }; + + // Make sure the mesh preprocessing shader has access to the + // view info it needs to do culling and motion vector + // computation. + let dynamic_offsets = [view_uniform_offset.offset]; + + // Are we drawing directly or indirectly? + match *phase_bind_groups { + PhasePreprocessBindGroups::Direct(ref bind_group) => { + // Invoke the mesh preprocessing shader to transform + // meshes only, but not cull. + let PreprocessWorkItemBuffers::Direct(work_item_buffer) = work_item_buffers + else { + continue; + }; + compute_pass.set_bind_group(0, bind_group, &dynamic_offsets); + let workgroup_count = work_item_buffer.len().div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + + PhasePreprocessBindGroups::IndirectFrustumCulling { + indexed: ref maybe_indexed_bind_group, + non_indexed: ref maybe_non_indexed_bind_group, + } + | PhasePreprocessBindGroups::IndirectOcclusionCulling { + early_indexed: ref maybe_indexed_bind_group, + early_non_indexed: ref maybe_non_indexed_bind_group, + .. + } => { + // Invoke the mesh preprocessing shader to transform and + // cull the meshes. + let PreprocessWorkItemBuffers::Indirect { + indexed: indexed_buffer, + non_indexed: non_indexed_buffer, + .. + } = work_item_buffers + else { + continue; + }; + + // Transform and cull indexed meshes if there are any. + if let Some(indexed_bind_group) = maybe_indexed_bind_group { + if let PreprocessWorkItemBuffers::Indirect { + gpu_occlusion_culling: + Some(GpuOcclusionCullingWorkItemBuffers { + late_indirect_parameters_indexed_offset, + .. + }), + .. + } = *work_item_buffers + { + compute_pass.set_push_constants( + 0, + bytemuck::bytes_of(&late_indirect_parameters_indexed_offset), + ); + } + + compute_pass.set_bind_group(0, indexed_bind_group, &dynamic_offsets); + let workgroup_count = indexed_buffer.len().div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + + // Transform and cull non-indexed meshes if there are any. + if let Some(non_indexed_bind_group) = maybe_non_indexed_bind_group { + if let PreprocessWorkItemBuffers::Indirect { + gpu_occlusion_culling: + Some(GpuOcclusionCullingWorkItemBuffers { + late_indirect_parameters_non_indexed_offset, + .. + }), + .. + } = *work_item_buffers + { + compute_pass.set_push_constants( + 0, + bytemuck::bytes_of( + &late_indirect_parameters_non_indexed_offset, + ), + ); + } + + compute_pass.set_bind_group( + 0, + non_indexed_bind_group, + &dynamic_offsets, + ); + let workgroup_count = non_indexed_buffer.len().div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + } + } + } + } + + pass_span.end(&mut compute_pass); + + Ok(()) + } +} + +impl FromWorld for EarlyPrepassBuildIndirectParametersNode { + fn from_world(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + } + } +} + +impl FromWorld for LatePrepassBuildIndirectParametersNode { + fn from_world(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + } + } +} + +impl FromWorld for MainBuildIndirectParametersNode { + fn from_world(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + } + } +} + +impl FromWorld for LateGpuPreprocessNode { + fn from_world(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + } + } +} + +impl Node for LateGpuPreprocessNode { + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + } + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let diagnostics = render_context.diagnostic_recorder(); + + // Grab the [`BatchedInstanceBuffers`]. + let batched_instance_buffers = + world.resource::>(); + + let pipeline_cache = world.resource::(); + let preprocess_pipelines = world.resource::(); + + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("late_mesh_preprocessing"), + timestamp_writes: None, + }); + let pass_span = diagnostics.pass_span(&mut compute_pass, "late_mesh_preprocessing"); + + // Run the compute passes. + for (view, bind_groups, view_uniform_offset) in self.view_query.iter_manual(world) { + let maybe_pipeline_id = preprocess_pipelines + .late_gpu_occlusion_culling_preprocess + .pipeline_id; + + // Fetch the pipeline. + let Some(preprocess_pipeline_id) = maybe_pipeline_id else { + warn!("The build mesh uniforms pipeline wasn't ready"); + return Ok(()); + }; + + let Some(preprocess_pipeline) = + pipeline_cache.get_compute_pipeline(preprocess_pipeline_id) + else { + // This will happen while the pipeline is being compiled and is fine. + return Ok(()); + }; + + compute_pass.set_pipeline(preprocess_pipeline); + + // Loop over each phase. Because we built the phases in parallel, + // each phase has a separate set of instance buffers. + for (phase_type_id, batched_phase_instance_buffers) in + &batched_instance_buffers.phase_instance_buffers + { + let UntypedPhaseBatchedInstanceBuffers { + ref work_item_buffers, + ref late_indexed_indirect_parameters_buffer, + ref late_non_indexed_indirect_parameters_buffer, + .. + } = *batched_phase_instance_buffers; + + // Grab the work item buffers for this view. + let Some(phase_work_item_buffers) = + work_item_buffers.get(&view.retained_view_entity) + else { + continue; + }; + + let ( + PreprocessWorkItemBuffers::Indirect { + gpu_occlusion_culling: + Some(GpuOcclusionCullingWorkItemBuffers { + late_indirect_parameters_indexed_offset, + late_indirect_parameters_non_indexed_offset, + .. + }), + .. + }, + Some(PhasePreprocessBindGroups::IndirectOcclusionCulling { + late_indexed: maybe_late_indexed_bind_group, + late_non_indexed: maybe_late_non_indexed_bind_group, + .. + }), + Some(late_indexed_indirect_parameters_buffer), + Some(late_non_indexed_indirect_parameters_buffer), + ) = ( + phase_work_item_buffers, + bind_groups.get(phase_type_id), + late_indexed_indirect_parameters_buffer.buffer(), + late_non_indexed_indirect_parameters_buffer.buffer(), + ) + else { + continue; + }; + + let mut dynamic_offsets: SmallVec<[u32; 1]> = smallvec![]; + dynamic_offsets.push(view_uniform_offset.offset); + + // If there's no space reserved for work items, then don't + // bother doing the dispatch, as there can't possibly be any + // meshes of the given class (indexed or non-indexed) in this + // phase. + + // Transform and cull indexed meshes if there are any. + if let Some(late_indexed_bind_group) = maybe_late_indexed_bind_group { + compute_pass.set_push_constants( + 0, + bytemuck::bytes_of(late_indirect_parameters_indexed_offset), + ); + + compute_pass.set_bind_group(0, late_indexed_bind_group, &dynamic_offsets); + compute_pass.dispatch_workgroups_indirect( + late_indexed_indirect_parameters_buffer, + (*late_indirect_parameters_indexed_offset as u64) + * (size_of::() as u64), + ); + } + + // Transform and cull non-indexed meshes if there are any. + if let Some(late_non_indexed_bind_group) = maybe_late_non_indexed_bind_group { + compute_pass.set_push_constants( + 0, + bytemuck::bytes_of(late_indirect_parameters_non_indexed_offset), + ); + + compute_pass.set_bind_group(0, late_non_indexed_bind_group, &dynamic_offsets); + compute_pass.dispatch_workgroups_indirect( + late_non_indexed_indirect_parameters_buffer, + (*late_indirect_parameters_non_indexed_offset as u64) + * (size_of::() as u64), + ); + } + } + } + + pass_span.end(&mut compute_pass); + + Ok(()) + } +} + +impl Node for EarlyPrepassBuildIndirectParametersNode { + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + } + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let preprocess_pipelines = world.resource::(); + + // If there are no views with a depth prepass enabled, we don't need to + // run this. + if self.view_query.iter_manual(world).next().is_none() { + return Ok(()); + } + + run_build_indirect_parameters_node( + render_context, + world, + &preprocess_pipelines.early_phase, + "early_prepass_indirect_parameters_building", + ) + } +} + +impl Node for LatePrepassBuildIndirectParametersNode { + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + } + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let preprocess_pipelines = world.resource::(); + + // If there are no views with occlusion culling enabled, we don't need + // to run this. + if self.view_query.iter_manual(world).next().is_none() { + return Ok(()); + } + + run_build_indirect_parameters_node( + render_context, + world, + &preprocess_pipelines.late_phase, + "late_prepass_indirect_parameters_building", + ) + } +} + +impl Node for MainBuildIndirectParametersNode { + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + } + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let preprocess_pipelines = world.resource::(); + + run_build_indirect_parameters_node( + render_context, + world, + &preprocess_pipelines.main_phase, + "main_indirect_parameters_building", + ) + } +} + +fn run_build_indirect_parameters_node( + render_context: &mut RenderContext, + world: &World, + preprocess_phase_pipelines: &PreprocessPhasePipelines, + label: &'static str, +) -> Result<(), NodeRunError> { + let Some(build_indirect_params_bind_groups) = + world.get_resource::() + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let pipeline_cache = world.resource::(); + let indirect_parameters_buffers = world.resource::(); + + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some(label), + timestamp_writes: None, + }); + let pass_span = diagnostics.pass_span(&mut compute_pass, label); + + // Fetch the pipeline. + let ( + Some(reset_indirect_batch_sets_pipeline_id), + Some(build_indexed_indirect_params_pipeline_id), + Some(build_non_indexed_indirect_params_pipeline_id), + ) = ( + preprocess_phase_pipelines + .reset_indirect_batch_sets + .pipeline_id, + preprocess_phase_pipelines + .gpu_occlusion_culling_build_indexed_indirect_params + .pipeline_id, + preprocess_phase_pipelines + .gpu_occlusion_culling_build_non_indexed_indirect_params + .pipeline_id, + ) + else { + warn!("The build indirect parameters pipelines weren't ready"); + pass_span.end(&mut compute_pass); + return Ok(()); + }; + + let ( + Some(reset_indirect_batch_sets_pipeline), + Some(build_indexed_indirect_params_pipeline), + Some(build_non_indexed_indirect_params_pipeline), + ) = ( + pipeline_cache.get_compute_pipeline(reset_indirect_batch_sets_pipeline_id), + pipeline_cache.get_compute_pipeline(build_indexed_indirect_params_pipeline_id), + pipeline_cache.get_compute_pipeline(build_non_indexed_indirect_params_pipeline_id), + ) + else { + // This will happen while the pipeline is being compiled and is fine. + pass_span.end(&mut compute_pass); + return Ok(()); + }; + + // Loop over each phase. As each has as separate set of buffers, we need to + // build indirect parameters individually for each phase. + for (phase_type_id, phase_build_indirect_params_bind_groups) in + build_indirect_params_bind_groups.iter() + { + let Some(phase_indirect_parameters_buffers) = + indirect_parameters_buffers.get(phase_type_id) + else { + continue; + }; + + // Build indexed indirect parameters. + if let ( + Some(reset_indexed_indirect_batch_sets_bind_group), + Some(build_indirect_indexed_params_bind_group), + ) = ( + &phase_build_indirect_params_bind_groups.reset_indexed_indirect_batch_sets, + &phase_build_indirect_params_bind_groups.build_indexed_indirect, + ) { + compute_pass.set_pipeline(reset_indirect_batch_sets_pipeline); + compute_pass.set_bind_group(0, reset_indexed_indirect_batch_sets_bind_group, &[]); + let workgroup_count = phase_indirect_parameters_buffers + .batch_set_count(true) + .div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + + compute_pass.set_pipeline(build_indexed_indirect_params_pipeline); + compute_pass.set_bind_group(0, build_indirect_indexed_params_bind_group, &[]); + let workgroup_count = phase_indirect_parameters_buffers + .indexed + .batch_count() + .div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + + // Build non-indexed indirect parameters. + if let ( + Some(reset_non_indexed_indirect_batch_sets_bind_group), + Some(build_indirect_non_indexed_params_bind_group), + ) = ( + &phase_build_indirect_params_bind_groups.reset_non_indexed_indirect_batch_sets, + &phase_build_indirect_params_bind_groups.build_non_indexed_indirect, + ) { + compute_pass.set_pipeline(reset_indirect_batch_sets_pipeline); + compute_pass.set_bind_group(0, reset_non_indexed_indirect_batch_sets_bind_group, &[]); + let workgroup_count = phase_indirect_parameters_buffers + .batch_set_count(false) + .div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + + compute_pass.set_pipeline(build_non_indexed_indirect_params_pipeline); + compute_pass.set_bind_group(0, build_indirect_non_indexed_params_bind_group, &[]); + let workgroup_count = phase_indirect_parameters_buffers + .non_indexed + .batch_count() + .div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + } + + pass_span.end(&mut compute_pass); + + Ok(()) +} + +impl PreprocessPipelines { + /// Returns true if the preprocessing and indirect parameters pipelines have + /// been loaded or false otherwise. + pub(crate) fn pipelines_are_loaded( + &self, + pipeline_cache: &PipelineCache, + preprocessing_support: &GpuPreprocessingSupport, + ) -> bool { + match preprocessing_support.max_supported_mode { + GpuPreprocessingMode::None => false, + GpuPreprocessingMode::PreprocessingOnly => { + self.direct_preprocess.is_loaded(pipeline_cache) + && self + .gpu_frustum_culling_preprocess + .is_loaded(pipeline_cache) + } + GpuPreprocessingMode::Culling => { + self.direct_preprocess.is_loaded(pipeline_cache) + && self + .gpu_frustum_culling_preprocess + .is_loaded(pipeline_cache) + && self + .early_gpu_occlusion_culling_preprocess + .is_loaded(pipeline_cache) + && self + .late_gpu_occlusion_culling_preprocess + .is_loaded(pipeline_cache) + && self + .gpu_frustum_culling_build_indexed_indirect_params + .is_loaded(pipeline_cache) + && self + .gpu_frustum_culling_build_non_indexed_indirect_params + .is_loaded(pipeline_cache) + && self.early_phase.is_loaded(pipeline_cache) + && self.late_phase.is_loaded(pipeline_cache) + && self.main_phase.is_loaded(pipeline_cache) + } + } + } +} + +impl PreprocessPhasePipelines { + fn is_loaded(&self, pipeline_cache: &PipelineCache) -> bool { + self.reset_indirect_batch_sets.is_loaded(pipeline_cache) + && self + .gpu_occlusion_culling_build_indexed_indirect_params + .is_loaded(pipeline_cache) + && self + .gpu_occlusion_culling_build_non_indexed_indirect_params + .is_loaded(pipeline_cache) + } +} + +impl PreprocessPipeline { + fn is_loaded(&self, pipeline_cache: &PipelineCache) -> bool { + self.pipeline_id + .is_some_and(|pipeline_id| pipeline_cache.get_compute_pipeline(pipeline_id).is_some()) + } +} + +impl ResetIndirectBatchSetsPipeline { + fn is_loaded(&self, pipeline_cache: &PipelineCache) -> bool { + self.pipeline_id + .is_some_and(|pipeline_id| pipeline_cache.get_compute_pipeline(pipeline_id).is_some()) + } +} + +impl BuildIndirectParametersPipeline { + /// Returns true if this pipeline has been loaded into the pipeline cache or + /// false otherwise. + fn is_loaded(&self, pipeline_cache: &PipelineCache) -> bool { + self.pipeline_id + .is_some_and(|pipeline_id| pipeline_cache.get_compute_pipeline(pipeline_id).is_some()) + } +} + +impl SpecializedComputePipeline for PreprocessPipeline { + type Key = PreprocessPipelineKey; + + fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor { + let mut shader_defs = vec!["WRITE_INDIRECT_PARAMETERS_METADATA".into()]; + if key.contains(PreprocessPipelineKey::FRUSTUM_CULLING) { + shader_defs.push("INDIRECT".into()); + shader_defs.push("FRUSTUM_CULLING".into()); + } + if key.contains(PreprocessPipelineKey::OCCLUSION_CULLING) { + shader_defs.push("OCCLUSION_CULLING".into()); + if key.contains(PreprocessPipelineKey::EARLY_PHASE) { + shader_defs.push("EARLY_PHASE".into()); + } else { + shader_defs.push("LATE_PHASE".into()); + } + } + + ComputePipelineDescriptor { + label: Some( + format!( + "mesh preprocessing ({})", + if key.contains( + PreprocessPipelineKey::OCCLUSION_CULLING + | PreprocessPipelineKey::EARLY_PHASE + ) { + "early GPU occlusion culling" + } else if key.contains(PreprocessPipelineKey::OCCLUSION_CULLING) { + "late GPU occlusion culling" + } else if key.contains(PreprocessPipelineKey::FRUSTUM_CULLING) { + "GPU frustum culling" + } else { + "direct" + } + ) + .into(), + ), + layout: vec![self.bind_group_layout.clone()], + push_constant_ranges: if key.contains(PreprocessPipelineKey::OCCLUSION_CULLING) { + vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..4, + }] + } else { + vec![] + }, + shader: self.shader.clone(), + shader_defs, + ..default() + } + } +} + +impl FromWorld for PreprocessPipelines { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + // GPU culling bind group parameters are a superset of those in the CPU + // culling (direct) shader. + let direct_bind_group_layout_entries = preprocess_direct_bind_group_layout_entries(); + let gpu_frustum_culling_bind_group_layout_entries = gpu_culling_bind_group_layout_entries(); + let gpu_early_occlusion_culling_bind_group_layout_entries = + gpu_occlusion_culling_bind_group_layout_entries().extend_with_indices((( + 11, + storage_buffer::(/*has_dynamic_offset=*/ false), + ),)); + let gpu_late_occlusion_culling_bind_group_layout_entries = + gpu_occlusion_culling_bind_group_layout_entries(); + + let reset_indirect_batch_sets_bind_group_layout_entries = + DynamicBindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + (storage_buffer::(false),), + ); + + // Indexed and non-indexed bind group parameters share all the bind + // group layout entries except the final one. + let build_indexed_indirect_params_bind_group_layout_entries = + build_indirect_params_bind_group_layout_entries() + .extend_sequential((storage_buffer::(false),)); + let build_non_indexed_indirect_params_bind_group_layout_entries = + build_indirect_params_bind_group_layout_entries() + .extend_sequential((storage_buffer::(false),)); + + // Create the bind group layouts. + let direct_bind_group_layout = render_device.create_bind_group_layout( + "build mesh uniforms direct bind group layout", + &direct_bind_group_layout_entries, + ); + let gpu_frustum_culling_bind_group_layout = render_device.create_bind_group_layout( + "build mesh uniforms GPU frustum culling bind group layout", + &gpu_frustum_culling_bind_group_layout_entries, + ); + let gpu_early_occlusion_culling_bind_group_layout = render_device.create_bind_group_layout( + "build mesh uniforms GPU early occlusion culling bind group layout", + &gpu_early_occlusion_culling_bind_group_layout_entries, + ); + let gpu_late_occlusion_culling_bind_group_layout = render_device.create_bind_group_layout( + "build mesh uniforms GPU late occlusion culling bind group layout", + &gpu_late_occlusion_culling_bind_group_layout_entries, + ); + let reset_indirect_batch_sets_bind_group_layout = render_device.create_bind_group_layout( + "reset indirect batch sets bind group layout", + &reset_indirect_batch_sets_bind_group_layout_entries, + ); + let build_indexed_indirect_params_bind_group_layout = render_device + .create_bind_group_layout( + "build indexed indirect parameters bind group layout", + &build_indexed_indirect_params_bind_group_layout_entries, + ); + let build_non_indexed_indirect_params_bind_group_layout = render_device + .create_bind_group_layout( + "build non-indexed indirect parameters bind group layout", + &build_non_indexed_indirect_params_bind_group_layout_entries, + ); + + let preprocess_shader = load_embedded_asset!(world, "mesh_preprocess.wgsl"); + let reset_indirect_batch_sets_shader = + load_embedded_asset!(world, "reset_indirect_batch_sets.wgsl"); + let build_indirect_params_shader = + load_embedded_asset!(world, "build_indirect_params.wgsl"); + + let preprocess_phase_pipelines = PreprocessPhasePipelines { + reset_indirect_batch_sets: ResetIndirectBatchSetsPipeline { + bind_group_layout: reset_indirect_batch_sets_bind_group_layout.clone(), + shader: reset_indirect_batch_sets_shader, + pipeline_id: None, + }, + gpu_occlusion_culling_build_indexed_indirect_params: BuildIndirectParametersPipeline { + bind_group_layout: build_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader.clone(), + pipeline_id: None, + }, + gpu_occlusion_culling_build_non_indexed_indirect_params: + BuildIndirectParametersPipeline { + bind_group_layout: build_non_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader.clone(), + pipeline_id: None, + }, + }; + + PreprocessPipelines { + direct_preprocess: PreprocessPipeline { + bind_group_layout: direct_bind_group_layout, + shader: preprocess_shader.clone(), + pipeline_id: None, + }, + gpu_frustum_culling_preprocess: PreprocessPipeline { + bind_group_layout: gpu_frustum_culling_bind_group_layout, + shader: preprocess_shader.clone(), + pipeline_id: None, + }, + early_gpu_occlusion_culling_preprocess: PreprocessPipeline { + bind_group_layout: gpu_early_occlusion_culling_bind_group_layout, + shader: preprocess_shader.clone(), + pipeline_id: None, + }, + late_gpu_occlusion_culling_preprocess: PreprocessPipeline { + bind_group_layout: gpu_late_occlusion_culling_bind_group_layout, + shader: preprocess_shader, + pipeline_id: None, + }, + gpu_frustum_culling_build_indexed_indirect_params: BuildIndirectParametersPipeline { + bind_group_layout: build_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader.clone(), + pipeline_id: None, + }, + gpu_frustum_culling_build_non_indexed_indirect_params: + BuildIndirectParametersPipeline { + bind_group_layout: build_non_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader, + pipeline_id: None, + }, + early_phase: preprocess_phase_pipelines.clone(), + late_phase: preprocess_phase_pipelines.clone(), + main_phase: preprocess_phase_pipelines.clone(), + } + } +} + +fn preprocess_direct_bind_group_layout_entries() -> DynamicBindGroupLayoutEntries { + DynamicBindGroupLayoutEntries::new_with_indices( + ShaderStages::COMPUTE, + ( + // `view` + ( + 0, + uniform_buffer::(/* has_dynamic_offset= */ true), + ), + // `current_input` + (3, storage_buffer_read_only::(false)), + // `previous_input` + (4, storage_buffer_read_only::(false)), + // `indices` + (5, storage_buffer_read_only::(false)), + // `output` + (6, storage_buffer::(false)), + ), + ) +} + +// Returns the first 4 bind group layout entries shared between all invocations +// of the indirect parameters building shader. +fn build_indirect_params_bind_group_layout_entries() -> DynamicBindGroupLayoutEntries { + DynamicBindGroupLayoutEntries::new_with_indices( + ShaderStages::COMPUTE, + ( + (0, storage_buffer_read_only::(false)), + ( + 1, + storage_buffer_read_only::(false), + ), + ( + 2, + storage_buffer_read_only::(false), + ), + (3, storage_buffer::(false)), + ), + ) +} + +/// A system that specializes the `mesh_preprocess.wgsl` and +/// `build_indirect_params.wgsl` pipelines if necessary. +fn gpu_culling_bind_group_layout_entries() -> DynamicBindGroupLayoutEntries { + // GPU culling bind group parameters are a superset of those in the CPU + // culling (direct) shader. + preprocess_direct_bind_group_layout_entries().extend_with_indices(( + // `indirect_parameters_cpu_metadata` + ( + 7, + storage_buffer_read_only::( + /* has_dynamic_offset= */ false, + ), + ), + // `indirect_parameters_gpu_metadata` + ( + 8, + storage_buffer::(/* has_dynamic_offset= */ false), + ), + // `mesh_culling_data` + ( + 9, + storage_buffer_read_only::(/* has_dynamic_offset= */ false), + ), + )) +} + +fn gpu_occlusion_culling_bind_group_layout_entries() -> DynamicBindGroupLayoutEntries { + gpu_culling_bind_group_layout_entries().extend_with_indices(( + ( + 2, + uniform_buffer::(/*has_dynamic_offset=*/ false), + ), + ( + 10, + texture_2d(TextureSampleType::Float { filterable: true }), + ), + ( + 12, + storage_buffer::( + /*has_dynamic_offset=*/ false, + ), + ), + )) +} + +/// A system that specializes the `mesh_preprocess.wgsl` pipelines if necessary. +pub fn prepare_preprocess_pipelines( + pipeline_cache: Res, + render_device: Res, + mut specialized_preprocess_pipelines: ResMut>, + mut specialized_reset_indirect_batch_sets_pipelines: ResMut< + SpecializedComputePipelines, + >, + mut specialized_build_indirect_parameters_pipelines: ResMut< + SpecializedComputePipelines, + >, + preprocess_pipelines: ResMut, + gpu_preprocessing_support: Res, +) { + let preprocess_pipelines = preprocess_pipelines.into_inner(); + + preprocess_pipelines.direct_preprocess.prepare( + &pipeline_cache, + &mut specialized_preprocess_pipelines, + PreprocessPipelineKey::empty(), + ); + preprocess_pipelines.gpu_frustum_culling_preprocess.prepare( + &pipeline_cache, + &mut specialized_preprocess_pipelines, + PreprocessPipelineKey::FRUSTUM_CULLING, + ); + + if gpu_preprocessing_support.is_culling_supported() { + preprocess_pipelines + .early_gpu_occlusion_culling_preprocess + .prepare( + &pipeline_cache, + &mut specialized_preprocess_pipelines, + PreprocessPipelineKey::FRUSTUM_CULLING + | PreprocessPipelineKey::OCCLUSION_CULLING + | PreprocessPipelineKey::EARLY_PHASE, + ); + preprocess_pipelines + .late_gpu_occlusion_culling_preprocess + .prepare( + &pipeline_cache, + &mut specialized_preprocess_pipelines, + PreprocessPipelineKey::FRUSTUM_CULLING | PreprocessPipelineKey::OCCLUSION_CULLING, + ); + } + + let mut build_indirect_parameters_pipeline_key = BuildIndirectParametersPipelineKey::empty(); + + // If the GPU and driver support `multi_draw_indirect_count`, tell the + // shader that. + if render_device + .wgpu_device() + .features() + .contains(WgpuFeatures::MULTI_DRAW_INDIRECT_COUNT) + { + build_indirect_parameters_pipeline_key + .insert(BuildIndirectParametersPipelineKey::MULTI_DRAW_INDIRECT_COUNT_SUPPORTED); + } + + preprocess_pipelines + .gpu_frustum_culling_build_indexed_indirect_params + .prepare( + &pipeline_cache, + &mut specialized_build_indirect_parameters_pipelines, + build_indirect_parameters_pipeline_key | BuildIndirectParametersPipelineKey::INDEXED, + ); + preprocess_pipelines + .gpu_frustum_culling_build_non_indexed_indirect_params + .prepare( + &pipeline_cache, + &mut specialized_build_indirect_parameters_pipelines, + build_indirect_parameters_pipeline_key, + ); + + if !gpu_preprocessing_support.is_culling_supported() { + return; + } + + for (preprocess_phase_pipelines, build_indirect_parameters_phase_pipeline_key) in [ + ( + &mut preprocess_pipelines.early_phase, + BuildIndirectParametersPipelineKey::EARLY_PHASE, + ), + ( + &mut preprocess_pipelines.late_phase, + BuildIndirectParametersPipelineKey::LATE_PHASE, + ), + ( + &mut preprocess_pipelines.main_phase, + BuildIndirectParametersPipelineKey::MAIN_PHASE, + ), + ] { + preprocess_phase_pipelines + .reset_indirect_batch_sets + .prepare( + &pipeline_cache, + &mut specialized_reset_indirect_batch_sets_pipelines, + ); + preprocess_phase_pipelines + .gpu_occlusion_culling_build_indexed_indirect_params + .prepare( + &pipeline_cache, + &mut specialized_build_indirect_parameters_pipelines, + build_indirect_parameters_pipeline_key + | build_indirect_parameters_phase_pipeline_key + | BuildIndirectParametersPipelineKey::INDEXED + | BuildIndirectParametersPipelineKey::OCCLUSION_CULLING, + ); + preprocess_phase_pipelines + .gpu_occlusion_culling_build_non_indexed_indirect_params + .prepare( + &pipeline_cache, + &mut specialized_build_indirect_parameters_pipelines, + build_indirect_parameters_pipeline_key + | build_indirect_parameters_phase_pipeline_key + | BuildIndirectParametersPipelineKey::OCCLUSION_CULLING, + ); + } +} + +impl PreprocessPipeline { + fn prepare( + &mut self, + pipeline_cache: &PipelineCache, + pipelines: &mut SpecializedComputePipelines, + key: PreprocessPipelineKey, + ) { + if self.pipeline_id.is_some() { + return; + } + + let preprocess_pipeline_id = pipelines.specialize(pipeline_cache, self, key); + self.pipeline_id = Some(preprocess_pipeline_id); + } +} + +impl SpecializedComputePipeline for ResetIndirectBatchSetsPipeline { + type Key = (); + + fn specialize(&self, _: Self::Key) -> ComputePipelineDescriptor { + ComputePipelineDescriptor { + label: Some("reset indirect batch sets".into()), + layout: vec![self.bind_group_layout.clone()], + shader: self.shader.clone(), + ..default() + } + } +} + +impl SpecializedComputePipeline for BuildIndirectParametersPipeline { + type Key = BuildIndirectParametersPipelineKey; + + fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor { + let mut shader_defs = vec![]; + if key.contains(BuildIndirectParametersPipelineKey::INDEXED) { + shader_defs.push("INDEXED".into()); + } + if key.contains(BuildIndirectParametersPipelineKey::MULTI_DRAW_INDIRECT_COUNT_SUPPORTED) { + shader_defs.push("MULTI_DRAW_INDIRECT_COUNT_SUPPORTED".into()); + } + if key.contains(BuildIndirectParametersPipelineKey::OCCLUSION_CULLING) { + shader_defs.push("OCCLUSION_CULLING".into()); + } + if key.contains(BuildIndirectParametersPipelineKey::EARLY_PHASE) { + shader_defs.push("EARLY_PHASE".into()); + } + if key.contains(BuildIndirectParametersPipelineKey::LATE_PHASE) { + shader_defs.push("LATE_PHASE".into()); + } + if key.contains(BuildIndirectParametersPipelineKey::MAIN_PHASE) { + shader_defs.push("MAIN_PHASE".into()); + } + + let label = format!( + "{} build {}indexed indirect parameters", + if !key.contains(BuildIndirectParametersPipelineKey::OCCLUSION_CULLING) { + "frustum culling" + } else if key.contains(BuildIndirectParametersPipelineKey::EARLY_PHASE) { + "early occlusion culling" + } else if key.contains(BuildIndirectParametersPipelineKey::LATE_PHASE) { + "late occlusion culling" + } else { + "main occlusion culling" + }, + if key.contains(BuildIndirectParametersPipelineKey::INDEXED) { + "" + } else { + "non-" + } + ); + + ComputePipelineDescriptor { + label: Some(label.into()), + layout: vec![self.bind_group_layout.clone()], + shader: self.shader.clone(), + shader_defs, + ..default() + } + } +} + +impl ResetIndirectBatchSetsPipeline { + fn prepare( + &mut self, + pipeline_cache: &PipelineCache, + pipelines: &mut SpecializedComputePipelines, + ) { + if self.pipeline_id.is_some() { + return; + } + + let reset_indirect_batch_sets_pipeline_id = pipelines.specialize(pipeline_cache, self, ()); + self.pipeline_id = Some(reset_indirect_batch_sets_pipeline_id); + } +} + +impl BuildIndirectParametersPipeline { + fn prepare( + &mut self, + pipeline_cache: &PipelineCache, + pipelines: &mut SpecializedComputePipelines, + key: BuildIndirectParametersPipelineKey, + ) { + if self.pipeline_id.is_some() { + return; + } + + let build_indirect_parameters_pipeline_id = pipelines.specialize(pipeline_cache, self, key); + self.pipeline_id = Some(build_indirect_parameters_pipeline_id); + } +} + +/// A system that attaches the mesh uniform buffers to the bind groups for the +/// variants of the mesh preprocessing compute shader. +#[expect( + clippy::too_many_arguments, + reason = "it's a system that needs a lot of arguments" +)] +pub fn prepare_preprocess_bind_groups( + mut commands: Commands, + views: Query<(Entity, &ExtractedView)>, + view_depth_pyramids: Query<(&ViewDepthPyramid, &PreviousViewUniformOffset)>, + render_device: Res, + batched_instance_buffers: Res>, + indirect_parameters_buffers: Res, + mesh_culling_data_buffer: Res, + view_uniforms: Res, + previous_view_uniforms: Res, + pipelines: Res, +) { + // Grab the `BatchedInstanceBuffers`. + let BatchedInstanceBuffers { + current_input_buffer: current_input_buffer_vec, + previous_input_buffer: previous_input_buffer_vec, + phase_instance_buffers, + } = batched_instance_buffers.into_inner(); + + let (Some(current_input_buffer), Some(previous_input_buffer)) = ( + current_input_buffer_vec.buffer().buffer(), + previous_input_buffer_vec.buffer().buffer(), + ) else { + return; + }; + + // Record whether we have any meshes that are to be drawn indirectly. If we + // don't, then we can skip building indirect parameters. + let mut any_indirect = false; + + // Loop over each view. + for (view_entity, view) in &views { + let mut bind_groups = TypeIdMap::default(); + + // Loop over each phase. + for (phase_type_id, phase_instance_buffers) in phase_instance_buffers { + let UntypedPhaseBatchedInstanceBuffers { + data_buffer: ref data_buffer_vec, + ref work_item_buffers, + ref late_indexed_indirect_parameters_buffer, + ref late_non_indexed_indirect_parameters_buffer, + } = *phase_instance_buffers; + + let Some(data_buffer) = data_buffer_vec.buffer() else { + continue; + }; + + // Grab the indirect parameters buffers for this phase. + let Some(phase_indirect_parameters_buffers) = + indirect_parameters_buffers.get(phase_type_id) + else { + continue; + }; + + let Some(work_item_buffers) = work_item_buffers.get(&view.retained_view_entity) else { + continue; + }; + + // Create the `PreprocessBindGroupBuilder`. + let preprocess_bind_group_builder = PreprocessBindGroupBuilder { + view: view_entity, + late_indexed_indirect_parameters_buffer, + late_non_indexed_indirect_parameters_buffer, + render_device: &render_device, + phase_indirect_parameters_buffers, + mesh_culling_data_buffer: &mesh_culling_data_buffer, + view_uniforms: &view_uniforms, + previous_view_uniforms: &previous_view_uniforms, + pipelines: &pipelines, + current_input_buffer, + previous_input_buffer, + data_buffer, + }; + + // Depending on the type of work items we have, construct the + // appropriate bind groups. + let (was_indirect, bind_group) = match *work_item_buffers { + PreprocessWorkItemBuffers::Direct(ref work_item_buffer) => ( + false, + preprocess_bind_group_builder + .create_direct_preprocess_bind_groups(work_item_buffer), + ), + + PreprocessWorkItemBuffers::Indirect { + indexed: ref indexed_work_item_buffer, + non_indexed: ref non_indexed_work_item_buffer, + gpu_occlusion_culling: Some(ref gpu_occlusion_culling_work_item_buffers), + } => ( + true, + preprocess_bind_group_builder + .create_indirect_occlusion_culling_preprocess_bind_groups( + &view_depth_pyramids, + indexed_work_item_buffer, + non_indexed_work_item_buffer, + gpu_occlusion_culling_work_item_buffers, + ), + ), + + PreprocessWorkItemBuffers::Indirect { + indexed: ref indexed_work_item_buffer, + non_indexed: ref non_indexed_work_item_buffer, + gpu_occlusion_culling: None, + } => ( + true, + preprocess_bind_group_builder + .create_indirect_frustum_culling_preprocess_bind_groups( + indexed_work_item_buffer, + non_indexed_work_item_buffer, + ), + ), + }; + + // Write that bind group in. + if let Some(bind_group) = bind_group { + any_indirect = any_indirect || was_indirect; + bind_groups.insert(*phase_type_id, bind_group); + } + } + + // Save the bind groups. + commands + .entity(view_entity) + .insert(PreprocessBindGroups(bind_groups)); + } + + // Now, if there were any indirect draw commands, create the bind groups for + // the indirect parameters building shader. + if any_indirect { + create_build_indirect_parameters_bind_groups( + &mut commands, + &render_device, + &pipelines, + current_input_buffer, + &indirect_parameters_buffers, + ); + } +} + +/// A temporary structure that stores all the information needed to construct +/// bind groups for the mesh preprocessing shader. +struct PreprocessBindGroupBuilder<'a> { + /// The render-world entity corresponding to the current view. + view: Entity, + /// The indirect compute dispatch parameters buffer for indexed meshes in + /// the late prepass. + late_indexed_indirect_parameters_buffer: + &'a RawBufferVec, + /// The indirect compute dispatch parameters buffer for non-indexed meshes + /// in the late prepass. + late_non_indexed_indirect_parameters_buffer: + &'a RawBufferVec, + /// The device. + render_device: &'a RenderDevice, + /// The buffers that store indirect draw parameters. + phase_indirect_parameters_buffers: &'a UntypedPhaseIndirectParametersBuffers, + /// The GPU buffer that stores the information needed to cull each mesh. + mesh_culling_data_buffer: &'a MeshCullingDataBuffer, + /// The GPU buffer that stores information about the view. + view_uniforms: &'a ViewUniforms, + /// The GPU buffer that stores information about the view from last frame. + previous_view_uniforms: &'a PreviousViewUniforms, + /// The pipelines for the mesh preprocessing shader. + pipelines: &'a PreprocessPipelines, + /// The GPU buffer containing the list of [`MeshInputUniform`]s for the + /// current frame. + current_input_buffer: &'a Buffer, + /// The GPU buffer containing the list of [`MeshInputUniform`]s for the + /// previous frame. + previous_input_buffer: &'a Buffer, + /// The GPU buffer containing the list of [`MeshUniform`]s for the current + /// frame. + /// + /// This is the buffer containing the mesh's final transforms that the + /// shaders will write to. + data_buffer: &'a Buffer, +} + +impl<'a> PreprocessBindGroupBuilder<'a> { + /// Creates the bind groups for mesh preprocessing when GPU frustum culling + /// and GPU occlusion culling are both disabled. + fn create_direct_preprocess_bind_groups( + &self, + work_item_buffer: &RawBufferVec, + ) -> Option { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let work_item_buffer_size = NonZero::::try_from( + work_item_buffer.len() as u64 * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some(PhasePreprocessBindGroups::Direct( + self.render_device.create_bind_group( + "preprocess_direct_bind_group", + &self.pipelines.direct_preprocess.bind_group_layout, + &BindGroupEntries::with_indices(( + (0, self.view_uniforms.uniforms.binding()?), + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: work_item_buffer.buffer()?, + offset: 0, + size: work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + )), + ), + )) + } + + /// Creates the bind groups for mesh preprocessing when GPU occlusion + /// culling is enabled. + fn create_indirect_occlusion_culling_preprocess_bind_groups( + &self, + view_depth_pyramids: &Query<(&ViewDepthPyramid, &PreviousViewUniformOffset)>, + indexed_work_item_buffer: &RawBufferVec, + non_indexed_work_item_buffer: &RawBufferVec, + gpu_occlusion_culling_work_item_buffers: &GpuOcclusionCullingWorkItemBuffers, + ) -> Option { + let GpuOcclusionCullingWorkItemBuffers { + late_indexed: ref late_indexed_work_item_buffer, + late_non_indexed: ref late_non_indexed_work_item_buffer, + .. + } = *gpu_occlusion_culling_work_item_buffers; + + let (view_depth_pyramid, previous_view_uniform_offset) = + view_depth_pyramids.get(self.view).ok()?; + + Some(PhasePreprocessBindGroups::IndirectOcclusionCulling { + early_indexed: self.create_indirect_occlusion_culling_early_indexed_bind_group( + view_depth_pyramid, + previous_view_uniform_offset, + indexed_work_item_buffer, + late_indexed_work_item_buffer, + ), + + early_non_indexed: self.create_indirect_occlusion_culling_early_non_indexed_bind_group( + view_depth_pyramid, + previous_view_uniform_offset, + non_indexed_work_item_buffer, + late_non_indexed_work_item_buffer, + ), + + late_indexed: self.create_indirect_occlusion_culling_late_indexed_bind_group( + view_depth_pyramid, + previous_view_uniform_offset, + late_indexed_work_item_buffer, + ), + + late_non_indexed: self.create_indirect_occlusion_culling_late_non_indexed_bind_group( + view_depth_pyramid, + previous_view_uniform_offset, + late_non_indexed_work_item_buffer, + ), + }) + } + + /// Creates the bind group for the first phase of mesh preprocessing of + /// indexed meshes when GPU occlusion culling is enabled. + fn create_indirect_occlusion_culling_early_indexed_bind_group( + &self, + view_depth_pyramid: &ViewDepthPyramid, + previous_view_uniform_offset: &PreviousViewUniformOffset, + indexed_work_item_buffer: &RawBufferVec, + late_indexed_work_item_buffer: &UninitBufferVec, + ) -> Option { + let mesh_culling_data_buffer = self.mesh_culling_data_buffer.buffer()?; + let view_uniforms_binding = self.view_uniforms.uniforms.binding()?; + let previous_view_buffer = self.previous_view_uniforms.uniforms.buffer()?; + + match ( + self.phase_indirect_parameters_buffers + .indexed + .cpu_metadata_buffer(), + self.phase_indirect_parameters_buffers + .indexed + .gpu_metadata_buffer(), + indexed_work_item_buffer.buffer(), + late_indexed_work_item_buffer.buffer(), + self.late_indexed_indirect_parameters_buffer.buffer(), + ) { + ( + Some(indexed_cpu_metadata_buffer), + Some(indexed_gpu_metadata_buffer), + Some(indexed_work_item_gpu_buffer), + Some(late_indexed_work_item_gpu_buffer), + Some(late_indexed_indirect_parameters_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let indexed_work_item_buffer_size = NonZero::::try_from( + indexed_work_item_buffer.len() as u64 + * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some( + self.render_device.create_bind_group( + "preprocess_early_indexed_gpu_occlusion_culling_bind_group", + &self + .pipelines + .early_gpu_occlusion_culling_preprocess + .bind_group_layout, + &BindGroupEntries::with_indices(( + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: indexed_work_item_gpu_buffer, + offset: 0, + size: indexed_work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + (7, indexed_cpu_metadata_buffer.as_entire_binding()), + (8, indexed_gpu_metadata_buffer.as_entire_binding()), + (9, mesh_culling_data_buffer.as_entire_binding()), + (0, view_uniforms_binding.clone()), + (10, &view_depth_pyramid.all_mips), + ( + 2, + BufferBinding { + buffer: previous_view_buffer, + offset: previous_view_uniform_offset.offset as u64, + size: NonZeroU64::new(size_of::() as u64), + }, + ), + ( + 11, + BufferBinding { + buffer: late_indexed_work_item_gpu_buffer, + offset: 0, + size: indexed_work_item_buffer_size, + }, + ), + ( + 12, + BufferBinding { + buffer: late_indexed_indirect_parameters_buffer, + offset: 0, + size: NonZeroU64::new( + late_indexed_indirect_parameters_buffer.size(), + ), + }, + ), + )), + ), + ) + } + _ => None, + } + } + + /// Creates the bind group for the first phase of mesh preprocessing of + /// non-indexed meshes when GPU occlusion culling is enabled. + fn create_indirect_occlusion_culling_early_non_indexed_bind_group( + &self, + view_depth_pyramid: &ViewDepthPyramid, + previous_view_uniform_offset: &PreviousViewUniformOffset, + non_indexed_work_item_buffer: &RawBufferVec, + late_non_indexed_work_item_buffer: &UninitBufferVec, + ) -> Option { + let mesh_culling_data_buffer = self.mesh_culling_data_buffer.buffer()?; + let view_uniforms_binding = self.view_uniforms.uniforms.binding()?; + let previous_view_buffer = self.previous_view_uniforms.uniforms.buffer()?; + + match ( + self.phase_indirect_parameters_buffers + .non_indexed + .cpu_metadata_buffer(), + self.phase_indirect_parameters_buffers + .non_indexed + .gpu_metadata_buffer(), + non_indexed_work_item_buffer.buffer(), + late_non_indexed_work_item_buffer.buffer(), + self.late_non_indexed_indirect_parameters_buffer.buffer(), + ) { + ( + Some(non_indexed_cpu_metadata_buffer), + Some(non_indexed_gpu_metadata_buffer), + Some(non_indexed_work_item_gpu_buffer), + Some(late_non_indexed_work_item_buffer), + Some(late_non_indexed_indirect_parameters_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let non_indexed_work_item_buffer_size = NonZero::::try_from( + non_indexed_work_item_buffer.len() as u64 + * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some( + self.render_device.create_bind_group( + "preprocess_early_non_indexed_gpu_occlusion_culling_bind_group", + &self + .pipelines + .early_gpu_occlusion_culling_preprocess + .bind_group_layout, + &BindGroupEntries::with_indices(( + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: non_indexed_work_item_gpu_buffer, + offset: 0, + size: non_indexed_work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + (7, non_indexed_cpu_metadata_buffer.as_entire_binding()), + (8, non_indexed_gpu_metadata_buffer.as_entire_binding()), + (9, mesh_culling_data_buffer.as_entire_binding()), + (0, view_uniforms_binding.clone()), + (10, &view_depth_pyramid.all_mips), + ( + 2, + BufferBinding { + buffer: previous_view_buffer, + offset: previous_view_uniform_offset.offset as u64, + size: NonZeroU64::new(size_of::() as u64), + }, + ), + ( + 11, + BufferBinding { + buffer: late_non_indexed_work_item_buffer, + offset: 0, + size: non_indexed_work_item_buffer_size, + }, + ), + ( + 12, + BufferBinding { + buffer: late_non_indexed_indirect_parameters_buffer, + offset: 0, + size: NonZeroU64::new( + late_non_indexed_indirect_parameters_buffer.size(), + ), + }, + ), + )), + ), + ) + } + _ => None, + } + } + + /// Creates the bind group for the second phase of mesh preprocessing of + /// indexed meshes when GPU occlusion culling is enabled. + fn create_indirect_occlusion_culling_late_indexed_bind_group( + &self, + view_depth_pyramid: &ViewDepthPyramid, + previous_view_uniform_offset: &PreviousViewUniformOffset, + late_indexed_work_item_buffer: &UninitBufferVec, + ) -> Option { + let mesh_culling_data_buffer = self.mesh_culling_data_buffer.buffer()?; + let view_uniforms_binding = self.view_uniforms.uniforms.binding()?; + let previous_view_buffer = self.previous_view_uniforms.uniforms.buffer()?; + + match ( + self.phase_indirect_parameters_buffers + .indexed + .cpu_metadata_buffer(), + self.phase_indirect_parameters_buffers + .indexed + .gpu_metadata_buffer(), + late_indexed_work_item_buffer.buffer(), + self.late_indexed_indirect_parameters_buffer.buffer(), + ) { + ( + Some(indexed_cpu_metadata_buffer), + Some(indexed_gpu_metadata_buffer), + Some(late_indexed_work_item_gpu_buffer), + Some(late_indexed_indirect_parameters_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let late_indexed_work_item_buffer_size = NonZero::::try_from( + late_indexed_work_item_buffer.len() as u64 + * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some( + self.render_device.create_bind_group( + "preprocess_late_indexed_gpu_occlusion_culling_bind_group", + &self + .pipelines + .late_gpu_occlusion_culling_preprocess + .bind_group_layout, + &BindGroupEntries::with_indices(( + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: late_indexed_work_item_gpu_buffer, + offset: 0, + size: late_indexed_work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + (7, indexed_cpu_metadata_buffer.as_entire_binding()), + (8, indexed_gpu_metadata_buffer.as_entire_binding()), + (9, mesh_culling_data_buffer.as_entire_binding()), + (0, view_uniforms_binding.clone()), + (10, &view_depth_pyramid.all_mips), + ( + 2, + BufferBinding { + buffer: previous_view_buffer, + offset: previous_view_uniform_offset.offset as u64, + size: NonZeroU64::new(size_of::() as u64), + }, + ), + ( + 12, + BufferBinding { + buffer: late_indexed_indirect_parameters_buffer, + offset: 0, + size: NonZeroU64::new( + late_indexed_indirect_parameters_buffer.size(), + ), + }, + ), + )), + ), + ) + } + _ => None, + } + } + + /// Creates the bind group for the second phase of mesh preprocessing of + /// non-indexed meshes when GPU occlusion culling is enabled. + fn create_indirect_occlusion_culling_late_non_indexed_bind_group( + &self, + view_depth_pyramid: &ViewDepthPyramid, + previous_view_uniform_offset: &PreviousViewUniformOffset, + late_non_indexed_work_item_buffer: &UninitBufferVec, + ) -> Option { + let mesh_culling_data_buffer = self.mesh_culling_data_buffer.buffer()?; + let view_uniforms_binding = self.view_uniforms.uniforms.binding()?; + let previous_view_buffer = self.previous_view_uniforms.uniforms.buffer()?; + + match ( + self.phase_indirect_parameters_buffers + .non_indexed + .cpu_metadata_buffer(), + self.phase_indirect_parameters_buffers + .non_indexed + .gpu_metadata_buffer(), + late_non_indexed_work_item_buffer.buffer(), + self.late_non_indexed_indirect_parameters_buffer.buffer(), + ) { + ( + Some(non_indexed_cpu_metadata_buffer), + Some(non_indexed_gpu_metadata_buffer), + Some(non_indexed_work_item_gpu_buffer), + Some(late_non_indexed_indirect_parameters_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let non_indexed_work_item_buffer_size = NonZero::::try_from( + late_non_indexed_work_item_buffer.len() as u64 + * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some( + self.render_device.create_bind_group( + "preprocess_late_non_indexed_gpu_occlusion_culling_bind_group", + &self + .pipelines + .late_gpu_occlusion_culling_preprocess + .bind_group_layout, + &BindGroupEntries::with_indices(( + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: non_indexed_work_item_gpu_buffer, + offset: 0, + size: non_indexed_work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + (7, non_indexed_cpu_metadata_buffer.as_entire_binding()), + (8, non_indexed_gpu_metadata_buffer.as_entire_binding()), + (9, mesh_culling_data_buffer.as_entire_binding()), + (0, view_uniforms_binding.clone()), + (10, &view_depth_pyramid.all_mips), + ( + 2, + BufferBinding { + buffer: previous_view_buffer, + offset: previous_view_uniform_offset.offset as u64, + size: NonZeroU64::new(size_of::() as u64), + }, + ), + ( + 12, + BufferBinding { + buffer: late_non_indexed_indirect_parameters_buffer, + offset: 0, + size: NonZeroU64::new( + late_non_indexed_indirect_parameters_buffer.size(), + ), + }, + ), + )), + ), + ) + } + _ => None, + } + } + + /// Creates the bind groups for mesh preprocessing when GPU frustum culling + /// is enabled, but GPU occlusion culling is disabled. + fn create_indirect_frustum_culling_preprocess_bind_groups( + &self, + indexed_work_item_buffer: &RawBufferVec, + non_indexed_work_item_buffer: &RawBufferVec, + ) -> Option { + Some(PhasePreprocessBindGroups::IndirectFrustumCulling { + indexed: self + .create_indirect_frustum_culling_indexed_bind_group(indexed_work_item_buffer), + non_indexed: self.create_indirect_frustum_culling_non_indexed_bind_group( + non_indexed_work_item_buffer, + ), + }) + } + + /// Creates the bind group for mesh preprocessing of indexed meshes when GPU + /// frustum culling is enabled, but GPU occlusion culling is disabled. + fn create_indirect_frustum_culling_indexed_bind_group( + &self, + indexed_work_item_buffer: &RawBufferVec, + ) -> Option { + let mesh_culling_data_buffer = self.mesh_culling_data_buffer.buffer()?; + let view_uniforms_binding = self.view_uniforms.uniforms.binding()?; + + match ( + self.phase_indirect_parameters_buffers + .indexed + .cpu_metadata_buffer(), + self.phase_indirect_parameters_buffers + .indexed + .gpu_metadata_buffer(), + indexed_work_item_buffer.buffer(), + ) { + ( + Some(indexed_cpu_metadata_buffer), + Some(indexed_gpu_metadata_buffer), + Some(indexed_work_item_gpu_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let indexed_work_item_buffer_size = NonZero::::try_from( + indexed_work_item_buffer.len() as u64 + * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some( + self.render_device.create_bind_group( + "preprocess_gpu_indexed_frustum_culling_bind_group", + &self + .pipelines + .gpu_frustum_culling_preprocess + .bind_group_layout, + &BindGroupEntries::with_indices(( + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: indexed_work_item_gpu_buffer, + offset: 0, + size: indexed_work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + (7, indexed_cpu_metadata_buffer.as_entire_binding()), + (8, indexed_gpu_metadata_buffer.as_entire_binding()), + (9, mesh_culling_data_buffer.as_entire_binding()), + (0, view_uniforms_binding.clone()), + )), + ), + ) + } + _ => None, + } + } + + /// Creates the bind group for mesh preprocessing of non-indexed meshes when + /// GPU frustum culling is enabled, but GPU occlusion culling is disabled. + fn create_indirect_frustum_culling_non_indexed_bind_group( + &self, + non_indexed_work_item_buffer: &RawBufferVec, + ) -> Option { + let mesh_culling_data_buffer = self.mesh_culling_data_buffer.buffer()?; + let view_uniforms_binding = self.view_uniforms.uniforms.binding()?; + + match ( + self.phase_indirect_parameters_buffers + .non_indexed + .cpu_metadata_buffer(), + self.phase_indirect_parameters_buffers + .non_indexed + .gpu_metadata_buffer(), + non_indexed_work_item_buffer.buffer(), + ) { + ( + Some(non_indexed_cpu_metadata_buffer), + Some(non_indexed_gpu_metadata_buffer), + Some(non_indexed_work_item_gpu_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let non_indexed_work_item_buffer_size = NonZero::::try_from( + non_indexed_work_item_buffer.len() as u64 + * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + + Some( + self.render_device.create_bind_group( + "preprocess_gpu_non_indexed_frustum_culling_bind_group", + &self + .pipelines + .gpu_frustum_culling_preprocess + .bind_group_layout, + &BindGroupEntries::with_indices(( + (3, self.current_input_buffer.as_entire_binding()), + (4, self.previous_input_buffer.as_entire_binding()), + ( + 5, + BindingResource::Buffer(BufferBinding { + buffer: non_indexed_work_item_gpu_buffer, + offset: 0, + size: non_indexed_work_item_buffer_size, + }), + ), + (6, self.data_buffer.as_entire_binding()), + (7, non_indexed_cpu_metadata_buffer.as_entire_binding()), + (8, non_indexed_gpu_metadata_buffer.as_entire_binding()), + (9, mesh_culling_data_buffer.as_entire_binding()), + (0, view_uniforms_binding.clone()), + )), + ), + ) + } + _ => None, + } + } +} + +/// A system that creates bind groups from the indirect parameters metadata and +/// data buffers for the indirect batch set reset shader and the indirect +/// parameter building shader. +fn create_build_indirect_parameters_bind_groups( + commands: &mut Commands, + render_device: &RenderDevice, + pipelines: &PreprocessPipelines, + current_input_buffer: &Buffer, + indirect_parameters_buffers: &IndirectParametersBuffers, +) { + let mut build_indirect_parameters_bind_groups = BuildIndirectParametersBindGroups::new(); + + for (phase_type_id, phase_indirect_parameters_buffer) in indirect_parameters_buffers.iter() { + build_indirect_parameters_bind_groups.insert( + *phase_type_id, + PhaseBuildIndirectParametersBindGroups { + reset_indexed_indirect_batch_sets: match (phase_indirect_parameters_buffer + .indexed + .batch_sets_buffer(),) + { + (Some(indexed_batch_sets_buffer),) => Some( + render_device.create_bind_group( + "reset_indexed_indirect_batch_sets_bind_group", + // The early bind group is good for the main phase and late + // phase too. They bind the same buffers. + &pipelines + .early_phase + .reset_indirect_batch_sets + .bind_group_layout, + &BindGroupEntries::sequential(( + indexed_batch_sets_buffer.as_entire_binding(), + )), + ), + ), + _ => None, + }, + + reset_non_indexed_indirect_batch_sets: match (phase_indirect_parameters_buffer + .non_indexed + .batch_sets_buffer(),) + { + (Some(non_indexed_batch_sets_buffer),) => Some( + render_device.create_bind_group( + "reset_non_indexed_indirect_batch_sets_bind_group", + // The early bind group is good for the main phase and late + // phase too. They bind the same buffers. + &pipelines + .early_phase + .reset_indirect_batch_sets + .bind_group_layout, + &BindGroupEntries::sequential(( + non_indexed_batch_sets_buffer.as_entire_binding(), + )), + ), + ), + _ => None, + }, + + build_indexed_indirect: match ( + phase_indirect_parameters_buffer + .indexed + .cpu_metadata_buffer(), + phase_indirect_parameters_buffer + .indexed + .gpu_metadata_buffer(), + phase_indirect_parameters_buffer.indexed.data_buffer(), + phase_indirect_parameters_buffer.indexed.batch_sets_buffer(), + ) { + ( + Some(indexed_indirect_parameters_cpu_metadata_buffer), + Some(indexed_indirect_parameters_gpu_metadata_buffer), + Some(indexed_indirect_parameters_data_buffer), + Some(indexed_batch_sets_buffer), + ) => Some( + render_device.create_bind_group( + "build_indexed_indirect_parameters_bind_group", + // The frustum culling bind group is good for occlusion culling + // too. They bind the same buffers. + &pipelines + .gpu_frustum_culling_build_indexed_indirect_params + .bind_group_layout, + &BindGroupEntries::sequential(( + current_input_buffer.as_entire_binding(), + // Don't use `as_entire_binding` here; the shader reads + // the length and `RawBufferVec` overallocates. + BufferBinding { + buffer: indexed_indirect_parameters_cpu_metadata_buffer, + offset: 0, + size: NonZeroU64::new( + phase_indirect_parameters_buffer.indexed.batch_count() + as u64 + * size_of::() as u64, + ), + }, + BufferBinding { + buffer: indexed_indirect_parameters_gpu_metadata_buffer, + offset: 0, + size: NonZeroU64::new( + phase_indirect_parameters_buffer.indexed.batch_count() + as u64 + * size_of::() as u64, + ), + }, + indexed_batch_sets_buffer.as_entire_binding(), + indexed_indirect_parameters_data_buffer.as_entire_binding(), + )), + ), + ), + _ => None, + }, + + build_non_indexed_indirect: match ( + phase_indirect_parameters_buffer + .non_indexed + .cpu_metadata_buffer(), + phase_indirect_parameters_buffer + .non_indexed + .gpu_metadata_buffer(), + phase_indirect_parameters_buffer.non_indexed.data_buffer(), + phase_indirect_parameters_buffer + .non_indexed + .batch_sets_buffer(), + ) { + ( + Some(non_indexed_indirect_parameters_cpu_metadata_buffer), + Some(non_indexed_indirect_parameters_gpu_metadata_buffer), + Some(non_indexed_indirect_parameters_data_buffer), + Some(non_indexed_batch_sets_buffer), + ) => Some( + render_device.create_bind_group( + "build_non_indexed_indirect_parameters_bind_group", + // The frustum culling bind group is good for occlusion culling + // too. They bind the same buffers. + &pipelines + .gpu_frustum_culling_build_non_indexed_indirect_params + .bind_group_layout, + &BindGroupEntries::sequential(( + current_input_buffer.as_entire_binding(), + // Don't use `as_entire_binding` here; the shader reads + // the length and `RawBufferVec` overallocates. + BufferBinding { + buffer: non_indexed_indirect_parameters_cpu_metadata_buffer, + offset: 0, + size: NonZeroU64::new( + phase_indirect_parameters_buffer.non_indexed.batch_count() + as u64 + * size_of::() as u64, + ), + }, + BufferBinding { + buffer: non_indexed_indirect_parameters_gpu_metadata_buffer, + offset: 0, + size: NonZeroU64::new( + phase_indirect_parameters_buffer.non_indexed.batch_count() + as u64 + * size_of::() as u64, + ), + }, + non_indexed_batch_sets_buffer.as_entire_binding(), + non_indexed_indirect_parameters_data_buffer.as_entire_binding(), + )), + ), + ), + _ => None, + }, + }, + ); + } + + commands.insert_resource(build_indirect_parameters_bind_groups); +} + +/// Writes the information needed to do GPU mesh culling to the GPU. +pub fn write_mesh_culling_data_buffer( + render_device: Res, + render_queue: Res, + mut mesh_culling_data_buffer: ResMut, +) { + mesh_culling_data_buffer.write_buffer(&render_device, &render_queue); +} diff --git a/crates/libmarathon/src/render/pbr/render/light.rs b/crates/libmarathon/src/render/pbr/render/light.rs new file mode 100644 index 0000000..e61b402 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/light.rs @@ -0,0 +1,2357 @@ +use crate::render::pbr::*; +use bevy_asset::UntypedAssetId; +use bevy_camera::primitives::{ + face_index_to_name, CascadesFrusta, CubeMapFace, CubemapFrusta, Frustum, HalfSpace, + CUBE_MAP_FACES, +}; +use bevy_camera::visibility::{ + CascadesVisibleEntities, CubemapVisibleEntities, RenderLayers, ViewVisibility, + VisibleMeshEntities, +}; +use bevy_camera::Camera3d; +use bevy_color::ColorToComponents; +use crate::render::core_3d::CORE_3D_DEPTH_FORMAT; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Tick; +use bevy_ecs::system::SystemChangeTick; +use bevy_ecs::{ + entity::{EntityHashMap, EntityHashSet}, + prelude::*, + system::lifetimeless::Read, +}; +use bevy_light::cascade::Cascade; +use bevy_light::cluster::assign::{calculate_cluster_factors, ClusterableObjectType}; +use bevy_light::cluster::GlobalVisibleClusterableObjects; +use bevy_light::SunDisk; +use bevy_light::{ + spot_light_clip_from_view, spot_light_world_from_view, AmbientLight, CascadeShadowConfig, + Cascades, DirectionalLight, DirectionalLightShadowMap, NotShadowCaster, PointLight, + PointLightShadowMap, ShadowFilteringMethod, SpotLight, VolumetricLight, +}; +use bevy_math::{ops, Mat4, UVec4, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_platform::hash::FixedHasher; +use crate::render::erased_render_asset::ErasedRenderAssets; +use crate::render::experimental::occlusion_culling::{ + OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities, +}; +use crate::render::sync_world::MainEntityHashMap; +use crate::render::{ + batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, + camera::SortedCameras, + mesh::allocator::MeshAllocator, + view::{NoIndirectDrawing, RetainedViewEntity}, +}; +use crate::render::{ + diagnostic::RecordDiagnostics, + mesh::RenderMesh, + render_asset::RenderAssets, + render_graph::{Node, NodeRunError, RenderGraphContext}, + render_phase::*, + render_resource::*, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::*, + view::ExtractedView, + Extract, +}; +use crate::render::{ + mesh::allocator::SlabId, + sync_world::{MainEntity, RenderEntity}, +}; +use bevy_transform::{components::GlobalTransform, prelude::Transform}; +use bevy_utils::default; +use core::{hash::Hash, ops::Range}; +use decal::clustered::RenderClusteredDecals; +#[cfg(feature = "trace")] +use tracing::info_span; +use tracing::{error, warn}; + +#[derive(Component)] +pub struct ExtractedPointLight { + pub color: LinearRgba, + /// luminous intensity in lumens per steradian + pub intensity: f32, + pub range: f32, + pub radius: f32, + pub transform: GlobalTransform, + pub shadows_enabled: bool, + pub shadow_depth_bias: f32, + pub shadow_normal_bias: f32, + pub shadow_map_near_z: f32, + pub spot_light_angles: Option<(f32, f32)>, + pub volumetric: bool, + pub soft_shadows_enabled: bool, + /// whether this point light contributes diffuse light to lightmapped meshes + pub affects_lightmapped_mesh_diffuse: bool, +} + +#[derive(Component, Debug)] +pub struct ExtractedDirectionalLight { + pub color: LinearRgba, + pub illuminance: f32, + pub transform: GlobalTransform, + pub shadows_enabled: bool, + pub volumetric: bool, + /// whether this directional light contributes diffuse light to lightmapped + /// meshes + pub affects_lightmapped_mesh_diffuse: bool, + pub shadow_depth_bias: f32, + pub shadow_normal_bias: f32, + pub cascade_shadow_config: CascadeShadowConfig, + pub cascades: EntityHashMap>, + pub frusta: EntityHashMap>, + pub render_layers: RenderLayers, + pub soft_shadow_size: Option, + /// True if this light is using two-phase occlusion culling. + pub occlusion_culling: bool, + pub sun_disk_angular_size: f32, + pub sun_disk_intensity: f32, +} + +// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! +bitflags::bitflags! { + #[repr(transparent)] + struct PointLightFlags: u32 { + const SHADOWS_ENABLED = 1 << 0; + const SPOT_LIGHT_Y_NEGATIVE = 1 << 1; + const VOLUMETRIC = 1 << 2; + const AFFECTS_LIGHTMAPPED_MESH_DIFFUSE = 1 << 3; + const NONE = 0; + const UNINITIALIZED = 0xFFFF; + } +} + +#[derive(Copy, Clone, ShaderType, Default, Debug)] +pub struct GpuDirectionalCascade { + clip_from_world: Mat4, + texel_size: f32, + far_bound: f32, +} + +#[derive(Copy, Clone, ShaderType, Default, Debug)] +pub struct GpuDirectionalLight { + cascades: [GpuDirectionalCascade; MAX_CASCADES_PER_LIGHT], + color: Vec4, + dir_to_light: Vec3, + flags: u32, + soft_shadow_size: f32, + shadow_depth_bias: f32, + shadow_normal_bias: f32, + num_cascades: u32, + cascades_overlap_proportion: f32, + depth_texture_base_index: u32, + decal_index: u32, + sun_disk_angular_size: f32, + sun_disk_intensity: f32, +} + +// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! +bitflags::bitflags! { + #[repr(transparent)] + struct DirectionalLightFlags: u32 { + const SHADOWS_ENABLED = 1 << 0; + const VOLUMETRIC = 1 << 1; + const AFFECTS_LIGHTMAPPED_MESH_DIFFUSE = 1 << 2; + const NONE = 0; + const UNINITIALIZED = 0xFFFF; + } +} + +#[derive(Copy, Clone, Debug, ShaderType)] +pub struct GpuLights { + directional_lights: [GpuDirectionalLight; MAX_DIRECTIONAL_LIGHTS], + ambient_color: Vec4, + // xyz are x/y/z cluster dimensions and w is the number of clusters + cluster_dimensions: UVec4, + // xy are vec2(cluster_dimensions.xy) / vec2(view.width, view.height) + // z is cluster_dimensions.z / log(far / near) + // w is cluster_dimensions.z * log(near) / log(far / near) + cluster_factors: Vec4, + n_directional_lights: u32, + // offset from spot light's light index to spot light's shadow map index + spot_light_shadowmap_offset: i32, + ambient_light_affects_lightmapped_meshes: u32, +} + +// NOTE: When running bevy on Adreno GPU chipsets in WebGL, any value above 1 will result in a crash +// when loading the wgsl "pbr_functions.wgsl" in the function apply_fog. +#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] +pub const MAX_DIRECTIONAL_LIGHTS: usize = 1; +#[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" +))] +pub const MAX_DIRECTIONAL_LIGHTS: usize = 10; +#[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" +))] +pub const MAX_CASCADES_PER_LIGHT: usize = 4; +#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] +pub const MAX_CASCADES_PER_LIGHT: usize = 1; + +#[derive(Resource, Clone)] +pub struct ShadowSamplers { + pub point_light_comparison_sampler: Sampler, + #[cfg(feature = "experimental_pbr_pcss")] + pub point_light_linear_sampler: Sampler, + pub directional_light_comparison_sampler: Sampler, + #[cfg(feature = "experimental_pbr_pcss")] + pub directional_light_linear_sampler: Sampler, +} + +pub fn init_shadow_samplers(mut commands: Commands, render_device: Res) { + let base_sampler_descriptor = SamplerDescriptor { + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + address_mode_w: AddressMode::ClampToEdge, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Nearest, + ..default() + }; + + commands.insert_resource(ShadowSamplers { + point_light_comparison_sampler: render_device.create_sampler(&SamplerDescriptor { + compare: Some(CompareFunction::GreaterEqual), + ..base_sampler_descriptor + }), + #[cfg(feature = "experimental_pbr_pcss")] + point_light_linear_sampler: render_device.create_sampler(&base_sampler_descriptor), + directional_light_comparison_sampler: render_device.create_sampler(&SamplerDescriptor { + compare: Some(CompareFunction::GreaterEqual), + ..base_sampler_descriptor + }), + #[cfg(feature = "experimental_pbr_pcss")] + directional_light_linear_sampler: render_device.create_sampler(&base_sampler_descriptor), + }); +} + +// This is needed because of the orphan rule not allowing implementing +// foreign trait ExtractComponent on foreign type ShadowFilteringMethod +pub fn extract_shadow_filtering_method( + mut commands: Commands, + mut previous_len: Local, + query: Extract>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, query_item) in &query { + values.push((entity, *query_item)); + } + *previous_len = values.len(); + commands.try_insert_batch(values); +} + +// This is needed because of the orphan rule not allowing implementing +// foreign trait ExtractResource on foreign type AmbientLight +pub fn extract_ambient_light_resource( + mut commands: Commands, + main_resource: Extract>>, + target_resource: Option>, +) { + if let Some(main_resource) = main_resource.as_ref() { + if let Some(mut target_resource) = target_resource { + if main_resource.is_changed() { + *target_resource = (*main_resource).clone(); + } + } else { + commands.insert_resource((*main_resource).clone()); + } + } +} + +// This is needed because of the orphan rule not allowing implementing +// foreign trait ExtractComponent on foreign type AmbientLight +pub fn extract_ambient_light( + mut commands: Commands, + mut previous_len: Local, + query: Extract>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, query_item) in &query { + values.push((entity, query_item.clone())); + } + *previous_len = values.len(); + commands.try_insert_batch(values); +} + +pub fn extract_lights( + mut commands: Commands, + point_light_shadow_map: Extract>, + directional_light_shadow_map: Extract>, + global_visible_clusterable: Extract>, + previous_point_lights: Query< + Entity, + ( + With, + With, + ), + >, + previous_spot_lights: Query< + Entity, + (With, With), + >, + point_lights: Extract< + Query<( + Entity, + RenderEntity, + &PointLight, + &CubemapVisibleEntities, + &GlobalTransform, + &ViewVisibility, + &CubemapFrusta, + Option<&VolumetricLight>, + )>, + >, + spot_lights: Extract< + Query<( + Entity, + RenderEntity, + &SpotLight, + &VisibleMeshEntities, + &GlobalTransform, + &ViewVisibility, + &Frustum, + Option<&VolumetricLight>, + )>, + >, + directional_lights: Extract< + Query< + ( + Entity, + RenderEntity, + &DirectionalLight, + &CascadesVisibleEntities, + &Cascades, + &CascadeShadowConfig, + &CascadesFrusta, + &GlobalTransform, + &ViewVisibility, + Option<&RenderLayers>, + Option<&VolumetricLight>, + Has, + Option<&SunDisk>, + ), + Without, + >, + >, + mapper: Extract>, + mut previous_point_lights_len: Local, + mut previous_spot_lights_len: Local, +) { + // NOTE: These shadow map resources are extracted here as they are used here too so this avoids + // races between scheduling of ExtractResourceSystems and this system. + if point_light_shadow_map.is_changed() { + commands.insert_resource(point_light_shadow_map.clone()); + } + if directional_light_shadow_map.is_changed() { + commands.insert_resource(directional_light_shadow_map.clone()); + } + + // Clear previous visible entities for all point/spot lights as they might not be in the + // `global_visible_clusterable` list anymore. + commands.try_insert_batch( + previous_point_lights + .iter() + .map(|render_entity| (render_entity, RenderCubemapVisibleEntities::default())) + .collect::>(), + ); + commands.try_insert_batch( + previous_spot_lights + .iter() + .map(|render_entity| (render_entity, RenderVisibleMeshEntities::default())) + .collect::>(), + ); + + // This is the point light shadow map texel size for one face of the cube as a distance of 1.0 + // world unit from the light. + // point_light_texel_size = 2.0 * 1.0 * tan(PI / 4.0) / cube face width in texels + // PI / 4.0 is half the cube face fov, tan(PI / 4.0) = 1.0, so this simplifies to: + // point_light_texel_size = 2.0 / cube face width in texels + // NOTE: When using various PCF kernel sizes, this will need to be adjusted, according to: + // https://catlikecoding.com/unity/tutorials/custom-srp/point-and-spot-shadows/ + let point_light_texel_size = 2.0 / point_light_shadow_map.size as f32; + + let mut point_lights_values = Vec::with_capacity(*previous_point_lights_len); + for entity in global_visible_clusterable.iter().copied() { + let Ok(( + main_entity, + render_entity, + point_light, + cubemap_visible_entities, + transform, + view_visibility, + frusta, + volumetric_light, + )) = point_lights.get(entity) + else { + continue; + }; + if !view_visibility.get() { + continue; + } + let render_cubemap_visible_entities = RenderCubemapVisibleEntities { + data: cubemap_visible_entities + .iter() + .map(|v| create_render_visible_mesh_entities(&mapper, v)) + .collect::>() + .try_into() + .unwrap(), + }; + + let extracted_point_light = ExtractedPointLight { + color: point_light.color.into(), + // NOTE: Map from luminous power in lumens to luminous intensity in lumens per steradian + // for a point light. See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower + // for details. + intensity: point_light.intensity / (4.0 * core::f32::consts::PI), + range: point_light.range, + radius: point_light.radius, + transform: *transform, + shadows_enabled: point_light.shadows_enabled, + shadow_depth_bias: point_light.shadow_depth_bias, + // The factor of SQRT_2 is for the worst-case diagonal offset + shadow_normal_bias: point_light.shadow_normal_bias + * point_light_texel_size + * core::f32::consts::SQRT_2, + shadow_map_near_z: point_light.shadow_map_near_z, + spot_light_angles: None, + volumetric: volumetric_light.is_some(), + affects_lightmapped_mesh_diffuse: point_light.affects_lightmapped_mesh_diffuse, + #[cfg(feature = "experimental_pbr_pcss")] + soft_shadows_enabled: point_light.soft_shadows_enabled, + #[cfg(not(feature = "experimental_pbr_pcss"))] + soft_shadows_enabled: false, + }; + point_lights_values.push(( + render_entity, + ( + extracted_point_light, + render_cubemap_visible_entities, + (*frusta).clone(), + MainEntity::from(main_entity), + ), + )); + } + *previous_point_lights_len = point_lights_values.len(); + commands.try_insert_batch(point_lights_values); + + let mut spot_lights_values = Vec::with_capacity(*previous_spot_lights_len); + for entity in global_visible_clusterable.iter().copied() { + if let Ok(( + main_entity, + render_entity, + spot_light, + visible_entities, + transform, + view_visibility, + frustum, + volumetric_light, + )) = spot_lights.get(entity) + { + if !view_visibility.get() { + continue; + } + let render_visible_entities = + create_render_visible_mesh_entities(&mapper, visible_entities); + + let texel_size = + 2.0 * ops::tan(spot_light.outer_angle) / directional_light_shadow_map.size as f32; + + spot_lights_values.push(( + render_entity, + ( + ExtractedPointLight { + color: spot_light.color.into(), + // NOTE: Map from luminous power in lumens to luminous intensity in lumens per steradian + // for a point light. See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower + // for details. + // Note: Filament uses a divisor of PI for spot lights. We choose to use the same 4*PI divisor + // in both cases so that toggling between point light and spot light keeps lit areas lit equally, + // which seems least surprising for users + intensity: spot_light.intensity / (4.0 * core::f32::consts::PI), + range: spot_light.range, + radius: spot_light.radius, + transform: *transform, + shadows_enabled: spot_light.shadows_enabled, + shadow_depth_bias: spot_light.shadow_depth_bias, + // The factor of SQRT_2 is for the worst-case diagonal offset + shadow_normal_bias: spot_light.shadow_normal_bias + * texel_size + * core::f32::consts::SQRT_2, + shadow_map_near_z: spot_light.shadow_map_near_z, + spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)), + volumetric: volumetric_light.is_some(), + affects_lightmapped_mesh_diffuse: spot_light + .affects_lightmapped_mesh_diffuse, + #[cfg(feature = "experimental_pbr_pcss")] + soft_shadows_enabled: spot_light.soft_shadows_enabled, + #[cfg(not(feature = "experimental_pbr_pcss"))] + soft_shadows_enabled: false, + }, + render_visible_entities, + *frustum, + MainEntity::from(main_entity), + ), + )); + } + } + *previous_spot_lights_len = spot_lights_values.len(); + commands.try_insert_batch(spot_lights_values); + + for ( + main_entity, + entity, + directional_light, + visible_entities, + cascades, + cascade_config, + frusta, + transform, + view_visibility, + maybe_layers, + volumetric_light, + occlusion_culling, + sun_disk, + ) in &directional_lights + { + if !view_visibility.get() { + commands + .get_entity(entity) + .expect("Light entity wasn't synced.") + .remove::<(ExtractedDirectionalLight, RenderCascadesVisibleEntities)>(); + continue; + } + + // TODO: update in place instead of reinserting. + let mut extracted_cascades = EntityHashMap::default(); + let mut extracted_frusta = EntityHashMap::default(); + let mut cascade_visible_entities = EntityHashMap::default(); + for (e, v) in cascades.cascades.iter() { + if let Ok(entity) = mapper.get(*e) { + extracted_cascades.insert(entity, v.clone()); + } else { + break; + } + } + for (e, v) in frusta.frusta.iter() { + if let Ok(entity) = mapper.get(*e) { + extracted_frusta.insert(entity, v.clone()); + } else { + break; + } + } + for (e, v) in visible_entities.entities.iter() { + if let Ok(entity) = mapper.get(*e) { + cascade_visible_entities.insert( + entity, + v.iter() + .map(|v| create_render_visible_mesh_entities(&mapper, v)) + .collect(), + ); + } else { + break; + } + } + + commands + .get_entity(entity) + .expect("Light entity wasn't synced.") + .insert(( + ExtractedDirectionalLight { + color: directional_light.color.into(), + illuminance: directional_light.illuminance, + transform: *transform, + volumetric: volumetric_light.is_some(), + affects_lightmapped_mesh_diffuse: directional_light + .affects_lightmapped_mesh_diffuse, + #[cfg(feature = "experimental_pbr_pcss")] + soft_shadow_size: directional_light.soft_shadow_size, + #[cfg(not(feature = "experimental_pbr_pcss"))] + soft_shadow_size: None, + shadows_enabled: directional_light.shadows_enabled, + shadow_depth_bias: directional_light.shadow_depth_bias, + // The factor of SQRT_2 is for the worst-case diagonal offset + shadow_normal_bias: directional_light.shadow_normal_bias + * core::f32::consts::SQRT_2, + cascade_shadow_config: cascade_config.clone(), + cascades: extracted_cascades, + frusta: extracted_frusta, + render_layers: maybe_layers.unwrap_or_default().clone(), + occlusion_culling, + sun_disk_angular_size: sun_disk.unwrap_or_default().angular_size, + sun_disk_intensity: sun_disk.unwrap_or_default().intensity, + }, + RenderCascadesVisibleEntities { + entities: cascade_visible_entities, + }, + MainEntity::from(main_entity), + )); + } +} + +fn create_render_visible_mesh_entities( + mapper: &Extract>, + visible_entities: &VisibleMeshEntities, +) -> RenderVisibleMeshEntities { + RenderVisibleMeshEntities { + entities: visible_entities + .iter() + .map(|e| { + let render_entity = mapper.get(*e).unwrap_or(Entity::PLACEHOLDER); + (render_entity, MainEntity::from(*e)) + }) + .collect(), + } +} + +#[derive(Component, Default, Deref, DerefMut)] +/// Component automatically attached to a light entity to track light-view entities +/// for each view. +pub struct LightViewEntities(EntityHashMap>); + +// TODO: using required component +pub(crate) fn add_light_view_entities( + add: On, + mut commands: Commands, +) { + if let Ok(mut v) = commands.get_entity(add.entity) { + v.insert(LightViewEntities::default()); + } +} + +/// Removes [`LightViewEntities`] when light is removed. See [`add_light_view_entities`]. +pub(crate) fn extracted_light_removed( + remove: On, + mut commands: Commands, +) { + if let Ok(mut v) = commands.get_entity(remove.entity) { + v.try_remove::(); + } +} + +pub(crate) fn remove_light_view_entities( + remove: On, + query: Query<&LightViewEntities>, + mut commands: Commands, +) { + if let Ok(entities) = query.get(remove.entity) { + for v in entities.0.values() { + for e in v.iter().copied() { + if let Ok(mut v) = commands.get_entity(e) { + v.despawn(); + } + } + } + } +} + +#[derive(Component)] +pub struct ShadowView { + pub depth_attachment: DepthAttachment, + pub pass_name: String, +} + +#[derive(Component)] +pub struct ViewShadowBindings { + pub point_light_depth_texture: Texture, + pub point_light_depth_texture_view: TextureView, + pub directional_light_depth_texture: Texture, + pub directional_light_depth_texture_view: TextureView, +} + +/// A component that holds the shadow cascade views for all shadow cascades +/// associated with a camera. +/// +/// Note: Despite the name, this component actually holds the shadow cascade +/// views, not the lights themselves. +#[derive(Component)] +pub struct ViewLightEntities { + /// The shadow cascade views for all shadow cascades associated with a + /// camera. + /// + /// Note: Despite the name, this component actually holds the shadow cascade + /// views, not the lights themselves. + pub lights: Vec, +} + +#[derive(Component)] +pub struct ViewLightsUniformOffset { + pub offset: u32, +} + +#[derive(Resource, Default)] +pub struct LightMeta { + pub view_gpu_lights: DynamicUniformBuffer, +} + +#[derive(Component)] +pub enum LightEntity { + Directional { + light_entity: Entity, + cascade_index: usize, + }, + Point { + light_entity: Entity, + face_index: usize, + }, + Spot { + light_entity: Entity, + }, +} + +pub fn prepare_lights( + mut commands: Commands, + mut texture_cache: ResMut, + (render_device, render_queue): (Res, Res), + mut global_light_meta: ResMut, + mut light_meta: ResMut, + views: Query< + ( + Entity, + MainEntity, + &ExtractedView, + &ExtractedClusterConfig, + Option<&RenderLayers>, + Has, + Option<&AmbientLight>, + ), + With, + >, + ambient_light: Res, + point_light_shadow_map: Res, + directional_light_shadow_map: Res, + mut shadow_render_phases: ResMut>, + ( + mut max_directional_lights_warning_emitted, + mut max_cascades_per_light_warning_emitted, + mut live_shadow_mapping_lights, + ): (Local, Local, Local>), + point_lights: Query<( + Entity, + &MainEntity, + &ExtractedPointLight, + AnyOf<(&CubemapFrusta, &Frustum)>, + )>, + directional_lights: Query<(Entity, &MainEntity, &ExtractedDirectionalLight)>, + mut light_view_entities: Query<&mut LightViewEntities>, + sorted_cameras: Res, + (gpu_preprocessing_support, decals): ( + Res, + Option>, + ), +) { + let views_iter = views.iter(); + let views_count = views_iter.len(); + let Some(mut view_gpu_lights_writer) = + light_meta + .view_gpu_lights + .get_writer(views_count, &render_device, &render_queue) + else { + return; + }; + + // Pre-calculate for PointLights + let cube_face_rotations = CUBE_MAP_FACES + .iter() + .map(|CubeMapFace { target, up }| Transform::IDENTITY.looking_at(*target, *up)) + .collect::>(); + + global_light_meta.entity_to_index.clear(); + + let mut point_lights: Vec<_> = point_lights.iter().collect::>(); + let mut directional_lights: Vec<_> = directional_lights.iter().collect::>(); + + #[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ))] + let max_texture_array_layers = render_device.limits().max_texture_array_layers as usize; + #[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ))] + let max_texture_cubes = max_texture_array_layers / 6; + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + let max_texture_array_layers = 1; + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + let max_texture_cubes = 1; + + if !*max_directional_lights_warning_emitted && directional_lights.len() > MAX_DIRECTIONAL_LIGHTS + { + warn!( + "The amount of directional lights of {} is exceeding the supported limit of {}.", + directional_lights.len(), + MAX_DIRECTIONAL_LIGHTS + ); + *max_directional_lights_warning_emitted = true; + } + + if !*max_cascades_per_light_warning_emitted + && directional_lights + .iter() + .any(|(_, _, light)| light.cascade_shadow_config.bounds.len() > MAX_CASCADES_PER_LIGHT) + { + warn!( + "The number of cascades configured for a directional light exceeds the supported limit of {}.", + MAX_CASCADES_PER_LIGHT + ); + *max_cascades_per_light_warning_emitted = true; + } + + let point_light_count = point_lights + .iter() + .filter(|light| light.2.spot_light_angles.is_none()) + .count(); + + let point_light_volumetric_enabled_count = point_lights + .iter() + .filter(|(_, _, light, _)| light.volumetric && light.spot_light_angles.is_none()) + .count() + .min(max_texture_cubes); + + let point_light_shadow_maps_count = point_lights + .iter() + .filter(|light| light.2.shadows_enabled && light.2.spot_light_angles.is_none()) + .count() + .min(max_texture_cubes); + + let directional_volumetric_enabled_count = directional_lights + .iter() + .take(MAX_DIRECTIONAL_LIGHTS) + .filter(|(_, _, light)| light.volumetric) + .count() + .min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT); + + let directional_shadow_enabled_count = directional_lights + .iter() + .take(MAX_DIRECTIONAL_LIGHTS) + .filter(|(_, _, light)| light.shadows_enabled) + .count() + .min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT); + + let spot_light_count = point_lights + .iter() + .filter(|(_, _, light, _)| light.spot_light_angles.is_some()) + .count() + .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT); + + let spot_light_volumetric_enabled_count = point_lights + .iter() + .filter(|(_, _, light, _)| light.volumetric && light.spot_light_angles.is_some()) + .count() + .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT); + + let spot_light_shadow_maps_count = point_lights + .iter() + .filter(|(_, _, light, _)| light.shadows_enabled && light.spot_light_angles.is_some()) + .count() + .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT); + + // Sort lights by + // - point-light vs spot-light, so that we can iterate point lights and spot lights in contiguous blocks in the fragment shader, + // - then those with shadows enabled first, so that the index can be used to render at most `point_light_shadow_maps_count` + // point light shadows and `spot_light_shadow_maps_count` spot light shadow maps, + // - then by entity as a stable key to ensure that a consistent set of lights are chosen if the light count limit is exceeded. + point_lights.sort_by_cached_key(|(entity, _, light, _)| { + ( + point_or_spot_light_to_clusterable(light).ordering(), + *entity, + ) + }); + + // Sort lights by + // - those with volumetric (and shadows) enabled first, so that the + // volumetric lighting pass can quickly find the volumetric lights; + // - then those with shadows enabled second, so that the index can be used + // to render at most `directional_light_shadow_maps_count` directional light + // shadows + // - then by entity as a stable key to ensure that a consistent set of + // lights are chosen if the light count limit is exceeded. + // - because entities are unique, we can use `sort_unstable_by_key` + // and still end up with a stable order. + directional_lights.sort_unstable_by_key(|(entity, _, light)| { + (light.volumetric, light.shadows_enabled, *entity) + }); + + if global_light_meta.entity_to_index.capacity() < point_lights.len() { + global_light_meta + .entity_to_index + .reserve(point_lights.len()); + } + + let mut gpu_point_lights = Vec::new(); + for (index, &(entity, _, light, _)) in point_lights.iter().enumerate() { + let mut flags = PointLightFlags::NONE; + + // Lights are sorted, shadow enabled lights are first + if light.shadows_enabled + && (index < point_light_shadow_maps_count + || (light.spot_light_angles.is_some() + && index - point_light_count < spot_light_shadow_maps_count)) + { + flags |= PointLightFlags::SHADOWS_ENABLED; + } + + let cube_face_projection = Mat4::perspective_infinite_reverse_rh( + core::f32::consts::FRAC_PI_2, + 1.0, + light.shadow_map_near_z, + ); + if light.shadows_enabled + && light.volumetric + && (index < point_light_volumetric_enabled_count + || (light.spot_light_angles.is_some() + && index - point_light_count < spot_light_volumetric_enabled_count)) + { + flags |= PointLightFlags::VOLUMETRIC; + } + + if light.affects_lightmapped_mesh_diffuse { + flags |= PointLightFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE; + } + + let (light_custom_data, spot_light_tan_angle) = match light.spot_light_angles { + Some((inner, outer)) => { + let light_direction = light.transform.forward(); + if light_direction.y.is_sign_negative() { + flags |= PointLightFlags::SPOT_LIGHT_Y_NEGATIVE; + } + + let cos_outer = ops::cos(outer); + let spot_scale = 1.0 / f32::max(ops::cos(inner) - cos_outer, 1e-4); + let spot_offset = -cos_outer * spot_scale; + + ( + // For spot lights: the direction (x,z), spot_scale and spot_offset + light_direction.xz().extend(spot_scale).extend(spot_offset), + ops::tan(outer), + ) + } + None => { + ( + // For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3] + Vec4::new( + cube_face_projection.z_axis.z, + cube_face_projection.z_axis.w, + cube_face_projection.w_axis.z, + cube_face_projection.w_axis.w, + ), + // unused + 0.0, + ) + } + }; + + gpu_point_lights.push(GpuClusterableObject { + light_custom_data, + // premultiply color by intensity + // we don't use the alpha at all, so no reason to multiply only [0..3] + color_inverse_square_range: (Vec4::from_slice(&light.color.to_f32_array()) + * light.intensity) + .xyz() + .extend(1.0 / (light.range * light.range)), + position_radius: light.transform.translation().extend(light.radius), + flags: flags.bits(), + shadow_depth_bias: light.shadow_depth_bias, + shadow_normal_bias: light.shadow_normal_bias, + shadow_map_near_z: light.shadow_map_near_z, + spot_light_tan_angle, + decal_index: decals + .as_ref() + .and_then(|decals| decals.get(entity)) + .and_then(|index| index.try_into().ok()) + .unwrap_or(u32::MAX), + pad: 0.0, + soft_shadow_size: if light.soft_shadows_enabled { + light.radius + } else { + 0.0 + }, + }); + global_light_meta.entity_to_index.insert(entity, index); + } + + // iterate the views once to find the maximum number of cascade shadowmaps we will need + let mut num_directional_cascades_enabled = 0usize; + for ( + _entity, + _camera_main_entity, + _extracted_view, + _clusters, + maybe_layers, + _no_indirect_drawing, + _maybe_ambient_override, + ) in sorted_cameras + .0 + .iter() + .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) + { + let mut num_directional_cascades_for_this_view = 0usize; + let render_layers = maybe_layers.unwrap_or_default(); + + for (_light_entity, _, light) in directional_lights.iter() { + if light.shadows_enabled && light.render_layers.intersects(render_layers) { + num_directional_cascades_for_this_view += light + .cascade_shadow_config + .bounds + .len() + .min(MAX_CASCADES_PER_LIGHT); + } + } + + num_directional_cascades_enabled = num_directional_cascades_enabled + .max(num_directional_cascades_for_this_view) + .min(max_texture_array_layers); + } + + global_light_meta + .gpu_clusterable_objects + .set(gpu_point_lights); + global_light_meta + .gpu_clusterable_objects + .write_buffer(&render_device, &render_queue); + + live_shadow_mapping_lights.clear(); + + let mut point_light_depth_attachments = HashMap::::default(); + let mut directional_light_depth_attachments = HashMap::::default(); + + let point_light_depth_texture = texture_cache.get( + &render_device, + TextureDescriptor { + size: Extent3d { + width: point_light_shadow_map.size as u32, + height: point_light_shadow_map.size as u32, + depth_or_array_layers: point_light_shadow_maps_count.max(1) as u32 * 6, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: CORE_3D_DEPTH_FORMAT, + label: Some("point_light_shadow_map_texture"), + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let point_light_depth_texture_view = + point_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("point_light_shadow_map_array_texture_view"), + format: None, + // NOTE: iOS Simulator is missing CubeArray support so we use Cube instead. + // See https://github.com/bevyengine/bevy/pull/12052 - remove if support is added. + #[cfg(all( + not(target_abi = "sim"), + any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ) + ))] + dimension: Some(TextureViewDimension::CubeArray), + #[cfg(any( + target_abi = "sim", + all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")) + ))] + dimension: Some(TextureViewDimension::Cube), + usage: None, + aspect: TextureAspect::DepthOnly, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }); + + let directional_light_depth_texture = texture_cache.get( + &render_device, + TextureDescriptor { + size: Extent3d { + width: (directional_light_shadow_map.size as u32) + .min(render_device.limits().max_texture_dimension_2d), + height: (directional_light_shadow_map.size as u32) + .min(render_device.limits().max_texture_dimension_2d), + depth_or_array_layers: (num_directional_cascades_enabled + + spot_light_shadow_maps_count) + .max(1) as u32, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: CORE_3D_DEPTH_FORMAT, + label: Some("directional_light_shadow_map_texture"), + usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let directional_light_depth_texture_view = + directional_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("directional_light_shadow_map_array_texture_view"), + format: None, + #[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ))] + dimension: Some(TextureViewDimension::D2Array), + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + dimension: Some(TextureViewDimension::D2), + usage: None, + aspect: TextureAspect::DepthOnly, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: 0, + array_layer_count: None, + }); + + let mut live_views = EntityHashSet::with_capacity(views_count); + + // set up light data for each view + for ( + entity, + camera_main_entity, + extracted_view, + clusters, + maybe_layers, + no_indirect_drawing, + maybe_ambient_override, + ) in sorted_cameras + .0 + .iter() + .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) + { + live_views.insert(entity); + + let view_layers = maybe_layers.unwrap_or_default(); + let mut view_lights = Vec::new(); + let mut view_occlusion_culling_lights = Vec::new(); + + let gpu_preprocessing_mode = gpu_preprocessing_support.min(if !no_indirect_drawing { + GpuPreprocessingMode::Culling + } else { + GpuPreprocessingMode::PreprocessingOnly + }); + + let is_orthographic = extracted_view.clip_from_view.w_axis.w == 1.0; + let cluster_factors_zw = calculate_cluster_factors( + clusters.near, + clusters.far, + clusters.dimensions.z as f32, + is_orthographic, + ); + + let n_clusters = clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z; + let ambient_light = maybe_ambient_override.unwrap_or(&ambient_light); + + let mut gpu_directional_lights = [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS]; + let mut num_directional_cascades_enabled_for_this_view = 0usize; + let mut num_directional_lights_for_this_view = 0usize; + for (index, (light_entity, _, light)) in directional_lights + .iter() + .filter(|(_light_entity, _, light)| light.render_layers.intersects(view_layers)) + .enumerate() + .take(MAX_DIRECTIONAL_LIGHTS) + { + num_directional_lights_for_this_view += 1; + + let mut flags = DirectionalLightFlags::NONE; + + // Lights are sorted, volumetric and shadow enabled lights are first + if light.volumetric + && light.shadows_enabled + && (index < directional_volumetric_enabled_count) + { + flags |= DirectionalLightFlags::VOLUMETRIC; + } + + // Shadow enabled lights are second + let mut num_cascades = 0; + if light.shadows_enabled { + let cascades = light + .cascade_shadow_config + .bounds + .len() + .min(MAX_CASCADES_PER_LIGHT); + + if num_directional_cascades_enabled_for_this_view + cascades + <= max_texture_array_layers + { + flags |= DirectionalLightFlags::SHADOWS_ENABLED; + num_cascades += cascades; + } + } + + if light.affects_lightmapped_mesh_diffuse { + flags |= DirectionalLightFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE; + } + + gpu_directional_lights[index] = GpuDirectionalLight { + // Filled in later. + cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT], + // premultiply color by illuminance + // we don't use the alpha at all, so no reason to multiply only [0..3] + color: Vec4::from_slice(&light.color.to_f32_array()) * light.illuminance, + // direction is negated to be ready for N.L + dir_to_light: light.transform.back().into(), + flags: flags.bits(), + soft_shadow_size: light.soft_shadow_size.unwrap_or_default(), + shadow_depth_bias: light.shadow_depth_bias, + shadow_normal_bias: light.shadow_normal_bias, + num_cascades: num_cascades as u32, + cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion, + depth_texture_base_index: num_directional_cascades_enabled_for_this_view as u32, + sun_disk_angular_size: light.sun_disk_angular_size, + sun_disk_intensity: light.sun_disk_intensity, + decal_index: decals + .as_ref() + .and_then(|decals| decals.get(*light_entity)) + .and_then(|index| index.try_into().ok()) + .unwrap_or(u32::MAX), + }; + num_directional_cascades_enabled_for_this_view += num_cascades; + } + + let mut gpu_lights = GpuLights { + directional_lights: gpu_directional_lights, + ambient_color: Vec4::from_slice(&LinearRgba::from(ambient_light.color).to_f32_array()) + * ambient_light.brightness, + cluster_factors: Vec4::new( + clusters.dimensions.x as f32 / extracted_view.viewport.z as f32, + clusters.dimensions.y as f32 / extracted_view.viewport.w as f32, + cluster_factors_zw.x, + cluster_factors_zw.y, + ), + cluster_dimensions: clusters.dimensions.extend(n_clusters), + n_directional_lights: num_directional_lights_for_this_view as u32, + // spotlight shadow maps are stored in the directional light array, starting at num_directional_cascades_enabled. + // the spot lights themselves start in the light array at point_light_count. so to go from light + // index to shadow map index, we need to subtract point light count and add directional shadowmap count. + spot_light_shadowmap_offset: num_directional_cascades_enabled as i32 + - point_light_count as i32, + ambient_light_affects_lightmapped_meshes: ambient_light.affects_lightmapped_meshes + as u32, + }; + + // TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query + for &(light_entity, light_main_entity, light, (point_light_frusta, _)) in point_lights + .iter() + // Lights are sorted, shadow enabled lights are first + .take(point_light_count.min(max_texture_cubes)) + { + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + + if !light.shadows_enabled { + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } + continue; + } + + let light_index = *global_light_meta + .entity_to_index + .get(&light_entity) + .unwrap(); + // ignore scale because we don't want to effectively scale light radius and range + // by applying those as a view transform to shadow map rendering of objects + // and ignore rotation because we want the shadow map projections to align with the axes + let view_translation = GlobalTransform::from_translation(light.transform.translation()); + + // for each face of a cube and each view we spawn a light entity + let light_view_entities = light_view_entities + .entry(entity) + .or_insert_with(|| (0..6).map(|_| commands.spawn_empty().id()).collect()); + + let cube_face_projection = Mat4::perspective_infinite_reverse_rh( + core::f32::consts::FRAC_PI_2, + 1.0, + light.shadow_map_near_z, + ); + + for (face_index, ((view_rotation, frustum), view_light_entity)) in cube_face_rotations + .iter() + .zip(&point_light_frusta.unwrap().frusta) + .zip(light_view_entities.iter().copied()) + .enumerate() + { + let mut first = false; + let base_array_layer = (light_index * 6 + face_index) as u32; + + let depth_attachment = point_light_depth_attachments + .entry(base_array_layer) + .or_insert_with(|| { + first = true; + + let depth_texture_view = + point_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("point_light_shadow_map_texture_view"), + format: None, + dimension: Some(TextureViewDimension::D2), + usage: None, + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer, + array_layer_count: Some(1u32), + }); + + DepthAttachment::new(depth_texture_view, Some(0.0)) + }) + .clone(); + + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + Some(camera_main_entity.into()), + face_index as u32, + ); + + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment, + pass_name: format!( + "shadow_point_light_{}_{}", + light_index, + face_index_to_name(face_index) + ), + }, + ExtractedView { + retained_view_entity, + viewport: UVec4::new( + 0, + 0, + point_light_shadow_map.size as u32, + point_light_shadow_map.size as u32, + ), + world_from_view: view_translation * *view_rotation, + clip_from_world: None, + clip_from_view: cube_face_projection, + hdr: false, + color_grading: Default::default(), + }, + *frustum, + LightEntity::Point { + light_entity, + face_index, + }, + )); + + if !matches!(gpu_preprocessing_mode, GpuPreprocessingMode::Culling) { + commands.entity(view_light_entity).insert(NoIndirectDrawing); + } + + view_lights.push(view_light_entity); + + if first { + // Subsequent views with the same light entity will reuse the same shadow map + shadow_render_phases + .prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + live_shadow_mapping_lights.insert(retained_view_entity); + } + } + } + + // spot lights + for (light_index, &(light_entity, light_main_entity, light, (_, spot_light_frustum))) in + point_lights + .iter() + .skip(point_light_count) + .take(spot_light_count) + .enumerate() + { + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + + if !light.shadows_enabled { + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } + continue; + } + + let spot_world_from_view = spot_light_world_from_view(&light.transform); + let spot_world_from_view = spot_world_from_view.into(); + + let angle = light.spot_light_angles.expect("lights should be sorted so that \ + [point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1; + let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z); + + let mut first = false; + let base_array_layer = (num_directional_cascades_enabled + light_index) as u32; + + let depth_attachment = directional_light_depth_attachments + .entry(base_array_layer) + .or_insert_with(|| { + first = true; + + let depth_texture_view = directional_light_depth_texture.texture.create_view( + &TextureViewDescriptor { + label: Some("spot_light_shadow_map_texture_view"), + format: None, + dimension: Some(TextureViewDimension::D2), + usage: None, + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer, + array_layer_count: Some(1u32), + }, + ); + + DepthAttachment::new(depth_texture_view, Some(0.0)) + }) + .clone(); + + let light_view_entities = light_view_entities + .entry(entity) + .or_insert_with(|| vec![commands.spawn_empty().id()]); + + let view_light_entity = light_view_entities[0]; + + let retained_view_entity = + RetainedViewEntity::new(*light_main_entity, Some(camera_main_entity.into()), 0); + + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment, + pass_name: format!("shadow_spot_light_{light_index}"), + }, + ExtractedView { + retained_view_entity, + viewport: UVec4::new( + 0, + 0, + directional_light_shadow_map.size as u32, + directional_light_shadow_map.size as u32, + ), + world_from_view: spot_world_from_view, + clip_from_view: spot_projection, + clip_from_world: None, + hdr: false, + color_grading: Default::default(), + }, + *spot_light_frustum.unwrap(), + LightEntity::Spot { light_entity }, + )); + + if !matches!(gpu_preprocessing_mode, GpuPreprocessingMode::Culling) { + commands.entity(view_light_entity).insert(NoIndirectDrawing); + } + + view_lights.push(view_light_entity); + + if first { + // Subsequent views with the same light entity will reuse the same shadow map + shadow_render_phases + .prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + live_shadow_mapping_lights.insert(retained_view_entity); + } + } + + // directional lights + // clear entities for lights that don't intersect the layer + for &(light_entity, _, _) in directional_lights + .iter() + .filter(|(_, _, light)| !light.render_layers.intersects(view_layers)) + { + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } + } + + let mut directional_depth_texture_array_index = 0u32; + for (light_index, &(light_entity, light_main_entity, light)) in directional_lights + .iter() + .filter(|(_, _, light)| light.render_layers.intersects(view_layers)) + .enumerate() + .take(MAX_DIRECTIONAL_LIGHTS) + { + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + + let gpu_light = &mut gpu_lights.directional_lights[light_index]; + + // Only deal with cascades when shadows are enabled. + if (gpu_light.flags & DirectionalLightFlags::SHADOWS_ENABLED.bits()) == 0u32 { + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } + continue; + } + + let cascades = light + .cascades + .get(&entity) + .unwrap() + .iter() + .take(MAX_CASCADES_PER_LIGHT); + let frusta = light + .frusta + .get(&entity) + .unwrap() + .iter() + .take(MAX_CASCADES_PER_LIGHT); + + let iter = cascades + .zip(frusta) + .zip(&light.cascade_shadow_config.bounds); + + let light_view_entities = light_view_entities.entry(entity).or_insert_with(|| { + (0..iter.len()) + .map(|_| commands.spawn_empty().id()) + .collect() + }); + if light_view_entities.len() != iter.len() { + let entities = core::mem::take(light_view_entities); + despawn_entities(&mut commands, entities); + light_view_entities.extend((0..iter.len()).map(|_| commands.spawn_empty().id())); + } + + for (cascade_index, (((cascade, frustum), bound), view_light_entity)) in + iter.zip(light_view_entities.iter().copied()).enumerate() + { + gpu_lights.directional_lights[light_index].cascades[cascade_index] = + GpuDirectionalCascade { + clip_from_world: cascade.clip_from_world, + texel_size: cascade.texel_size, + far_bound: *bound, + }; + + let depth_texture_view = + directional_light_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("directional_light_shadow_map_array_texture_view"), + format: None, + dimension: Some(TextureViewDimension::D2), + usage: None, + aspect: TextureAspect::All, + base_mip_level: 0, + mip_level_count: None, + base_array_layer: directional_depth_texture_array_index, + array_layer_count: Some(1u32), + }); + + // NOTE: For point and spotlights, we reuse the same depth attachment for all views. + // However, for directional lights, we want a new depth attachment for each view, + // so that the view is cleared for each view. + let depth_attachment = DepthAttachment::new(depth_texture_view.clone(), Some(0.0)); + + directional_depth_texture_array_index += 1; + + let mut frustum = *frustum; + // Push the near clip plane out to infinity for directional lights + frustum.half_spaces[4] = + HalfSpace::new(frustum.half_spaces[4].normal().extend(f32::INFINITY)); + + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + Some(camera_main_entity.into()), + cascade_index as u32, + ); + + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment, + pass_name: format!( + "shadow_directional_light_{light_index}_cascade_{cascade_index}" + ), + }, + ExtractedView { + retained_view_entity, + viewport: UVec4::new( + 0, + 0, + directional_light_shadow_map.size as u32, + directional_light_shadow_map.size as u32, + ), + world_from_view: GlobalTransform::from(cascade.world_from_cascade), + clip_from_view: cascade.clip_from_cascade, + clip_from_world: Some(cascade.clip_from_world), + hdr: false, + color_grading: Default::default(), + }, + frustum, + LightEntity::Directional { + light_entity, + cascade_index, + }, + )); + + if !matches!(gpu_preprocessing_mode, GpuPreprocessingMode::Culling) { + commands.entity(view_light_entity).insert(NoIndirectDrawing); + } + + view_lights.push(view_light_entity); + + // If this light is using occlusion culling, add the appropriate components. + if light.occlusion_culling { + commands.entity(view_light_entity).insert(( + OcclusionCulling, + OcclusionCullingSubview { + depth_texture_view, + depth_texture_size: directional_light_shadow_map.size as u32, + }, + )); + view_occlusion_culling_lights.push(view_light_entity); + } + + // Subsequent views with the same light entity will **NOT** reuse the same shadow map + // (Because the cascades are unique to each view) + // TODO: Implement GPU culling for shadow passes. + shadow_render_phases + .prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + live_shadow_mapping_lights.insert(retained_view_entity); + } + } + + commands.entity(entity).insert(( + ViewShadowBindings { + point_light_depth_texture: point_light_depth_texture.texture.clone(), + point_light_depth_texture_view: point_light_depth_texture_view.clone(), + directional_light_depth_texture: directional_light_depth_texture.texture.clone(), + directional_light_depth_texture_view: directional_light_depth_texture_view.clone(), + }, + ViewLightEntities { + lights: view_lights, + }, + ViewLightsUniformOffset { + offset: view_gpu_lights_writer.write(&gpu_lights), + }, + )); + + // Make a link from the camera to all shadow cascades with occlusion + // culling enabled. + if !view_occlusion_culling_lights.is_empty() { + commands + .entity(entity) + .insert(OcclusionCullingSubviewEntities( + view_occlusion_culling_lights, + )); + } + } + + // Despawn light-view entities for views that no longer exist + for mut entities in &mut light_view_entities { + for (_, light_view_entities) in + entities.extract_if(|entity, _| !live_views.contains(entity)) + { + despawn_entities(&mut commands, light_view_entities); + } + } + + shadow_render_phases.retain(|entity, _| live_shadow_mapping_lights.contains(entity)); +} + +fn despawn_entities(commands: &mut Commands, entities: Vec) { + if entities.is_empty() { + return; + } + commands.queue(move |world: &mut World| { + for entity in entities { + world.despawn(entity); + } + }); +} + +// These will be extracted in the material extraction, which will also clear the needs_specialization +// collection. +pub fn check_light_entities_needing_specialization( + needs_specialization: Query>, Changed)>, + mut entities_needing_specialization: ResMut>, + mut removed_components: RemovedComponents, +) { + for entity in &needs_specialization { + entities_needing_specialization.push(entity); + } + + for removed in removed_components.read() { + entities_needing_specialization.entities.push(removed); + } +} + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct LightKeyCache(HashMap); + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct LightSpecializationTicks(HashMap); + +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedShadowMaterialPipelineCache { + // view light entity -> view pipeline cache + #[deref] + map: HashMap, +} + +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedShadowMaterialViewPipelineCache { + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, +} + +pub fn check_views_lights_need_specialization( + view_lights: Query<&ViewLightEntities, With>, + view_light_entities: Query<(&LightEntity, &ExtractedView)>, + shadow_render_phases: Res>, + mut light_key_cache: ResMut, + mut light_specialization_ticks: ResMut, + ticks: SystemChangeTick, +) { + for view_lights in &view_lights { + for view_light_entity in view_lights.lights.iter().copied() { + let Ok((light_entity, extracted_view_light)) = + view_light_entities.get(view_light_entity) + else { + continue; + }; + if !shadow_render_phases.contains_key(&extracted_view_light.retained_view_entity) { + continue; + } + + let is_directional_light = matches!(light_entity, LightEntity::Directional { .. }); + let mut light_key = MeshPipelineKey::DEPTH_PREPASS; + light_key.set(MeshPipelineKey::UNCLIPPED_DEPTH_ORTHO, is_directional_light); + if let Some(current_key) = + light_key_cache.get_mut(&extracted_view_light.retained_view_entity) + { + if *current_key != light_key { + light_key_cache.insert(extracted_view_light.retained_view_entity, light_key); + light_specialization_ticks + .insert(extracted_view_light.retained_view_entity, ticks.this_run()); + } + } else { + light_key_cache.insert(extracted_view_light.retained_view_entity, light_key); + light_specialization_ticks + .insert(extracted_view_light.retained_view_entity, ticks.this_run()); + } + } + } +} + +pub fn specialize_shadows( + prepass_pipeline: Res, + (render_meshes, render_mesh_instances, render_materials, render_material_instances): ( + Res>, + Res, + Res>, + Res, + ), + shadow_render_phases: Res>, + mut pipelines: ResMut>, + pipeline_cache: Res, + render_lightmaps: Res, + view_lights: Query<(Entity, &ViewLightEntities), With>, + view_light_entities: Query<(&LightEntity, &ExtractedView)>, + point_light_entities: Query<&RenderCubemapVisibleEntities, With>, + directional_light_entities: Query< + &RenderCascadesVisibleEntities, + With, + >, + spot_light_entities: Query<&RenderVisibleMeshEntities, With>, + light_key_cache: Res, + mut specialized_material_pipeline_cache: ResMut, + light_specialization_ticks: Res, + entity_specialization_ticks: Res, + ticks: SystemChangeTick, +) { + // Record the retained IDs of all shadow views so that we can expire old + // pipeline IDs. + let mut all_shadow_views: HashSet = HashSet::default(); + + for (entity, view_lights) in &view_lights { + for view_light_entity in view_lights.lights.iter().copied() { + let Ok((light_entity, extracted_view_light)) = + view_light_entities.get(view_light_entity) + else { + continue; + }; + + all_shadow_views.insert(extracted_view_light.retained_view_entity); + + if !shadow_render_phases.contains_key(&extracted_view_light.retained_view_entity) { + continue; + } + let Some(light_key) = light_key_cache.get(&extracted_view_light.retained_view_entity) + else { + continue; + }; + + let visible_entities = match light_entity { + LightEntity::Directional { + light_entity, + cascade_index, + } => directional_light_entities + .get(*light_entity) + .expect("Failed to get directional light visible entities") + .entities + .get(&entity) + .expect("Failed to get directional light visible entities for view") + .get(*cascade_index) + .expect("Failed to get directional light visible entities for cascade"), + LightEntity::Point { + light_entity, + face_index, + } => point_light_entities + .get(*light_entity) + .expect("Failed to get point light visible entities") + .get(*face_index), + LightEntity::Spot { light_entity } => spot_light_entities + .get(*light_entity) + .expect("Failed to get spot light visible entities"), + }; + + // NOTE: Lights with shadow mapping disabled will have no visible entities + // so no meshes will be queued + + let view_tick = light_specialization_ticks + .get(&extracted_view_light.retained_view_entity) + .unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(extracted_view_light.retained_view_entity) + .or_default(); + + for (_, visible_entity) in visible_entities.iter().copied() { + let Some(material_instance) = + render_material_instances.instances.get(&visible_entity) + else { + continue; + }; + + let Some(mesh_instance) = + render_mesh_instances.render_mesh_queue_data(visible_entity) + else { + continue; + }; + let entity_tick = entity_specialization_ticks.get(&visible_entity).unwrap(); + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(&visible_entity) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; + } + let Some(material) = render_materials.get(material_instance.asset_id) else { + continue; + }; + if !material.properties.shadows_enabled { + // If the material is not a shadow caster, we don't need to specialize it. + continue; + } + if !mesh_instance + .flags + .contains(RenderMeshInstanceFlags::SHADOW_CASTER) + { + continue; + } + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + + let mut mesh_key = + *light_key | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()); + + // Even though we don't use the lightmap in the shadow map, the + // `SetMeshBindGroup` render command will bind the data for it. So + // we need to include the appropriate flag in the mesh pipeline key + // to ensure that the necessary bind group layout entries are + // present. + if render_lightmaps + .render_lightmaps + .contains_key(&visible_entity) + { + mesh_key |= MeshPipelineKey::LIGHTMAPPED; + } + + mesh_key |= match material.properties.alpha_mode { + AlphaMode::Mask(_) + | AlphaMode::Blend + | AlphaMode::Premultiplied + | AlphaMode::Add + | AlphaMode::AlphaToCoverage => MeshPipelineKey::MAY_DISCARD, + _ => MeshPipelineKey::NONE, + }; + let erased_key = ErasedMaterialPipelineKey { + mesh_key, + material_key: material.properties.material_key.clone(), + type_id: material_instance.asset_id.type_id(), + }; + let material_pipeline_specializer = PrepassPipelineSpecializer { + pipeline: prepass_pipeline.clone(), + properties: material.properties.clone(), + }; + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &material_pipeline_specializer, + erased_key, + &mesh.layout, + ); + let pipeline_id = match pipeline_id { + Ok(id) => id, + Err(err) => { + error!("{}", err); + continue; + } + }; + + view_specialized_material_pipeline_cache + .insert(visible_entity, (ticks.this_run(), pipeline_id)); + } + } + } + + // Delete specialized pipelines belonging to views that have expired. + specialized_material_pipeline_cache.retain(|view, _| all_shadow_views.contains(view)); +} + +/// For each shadow cascade, iterates over all the meshes "visible" from it and +/// adds them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as +/// appropriate. +pub fn queue_shadows( + render_mesh_instances: Res, + render_materials: Res>, + render_material_instances: Res, + mut shadow_render_phases: ResMut>, + gpu_preprocessing_support: Res, + mesh_allocator: Res, + view_lights: Query<(Entity, &ViewLightEntities, Option<&RenderLayers>), With>, + view_light_entities: Query<(&LightEntity, &ExtractedView)>, + point_light_entities: Query<&RenderCubemapVisibleEntities, With>, + directional_light_entities: Query< + &RenderCascadesVisibleEntities, + With, + >, + spot_light_entities: Query<&RenderVisibleMeshEntities, With>, + specialized_material_pipeline_cache: Res, +) { + for (entity, view_lights, camera_layers) in &view_lights { + for view_light_entity in view_lights.lights.iter().copied() { + let Ok((light_entity, extracted_view_light)) = + view_light_entities.get(view_light_entity) + else { + continue; + }; + let Some(shadow_phase) = + shadow_render_phases.get_mut(&extracted_view_light.retained_view_entity) + else { + continue; + }; + + let Some(view_specialized_material_pipeline_cache) = + specialized_material_pipeline_cache.get(&extracted_view_light.retained_view_entity) + else { + continue; + }; + + let visible_entities = match light_entity { + LightEntity::Directional { + light_entity, + cascade_index, + } => directional_light_entities + .get(*light_entity) + .expect("Failed to get directional light visible entities") + .entities + .get(&entity) + .expect("Failed to get directional light visible entities for view") + .get(*cascade_index) + .expect("Failed to get directional light visible entities for cascade"), + LightEntity::Point { + light_entity, + face_index, + } => point_light_entities + .get(*light_entity) + .expect("Failed to get point light visible entities") + .get(*face_index), + LightEntity::Spot { light_entity } => spot_light_entities + .get(*light_entity) + .expect("Failed to get spot light visible entities"), + }; + + for (entity, main_entity) in visible_entities.iter().copied() { + let Some((current_change_tick, pipeline_id)) = + view_specialized_material_pipeline_cache.get(&main_entity) + else { + continue; + }; + + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(main_entity) + else { + continue; + }; + if !mesh_instance + .flags + .contains(RenderMeshInstanceFlags::SHADOW_CASTER) + { + continue; + } + + let mesh_layers = mesh_instance + .shared + .render_layers + .as_ref() + .unwrap_or_default(); + + let camera_layers = camera_layers.unwrap_or_default(); + + if !camera_layers.intersects(mesh_layers) { + continue; + } + + // Skip the entity if it's cached in a bin and up to date. + if shadow_phase.validate_cached_entity(main_entity, *current_change_tick) { + continue; + } + + let Some(material_instance) = render_material_instances.instances.get(&main_entity) + else { + continue; + }; + let Some(material) = render_materials.get(material_instance.asset_id) else { + continue; + }; + let Some(draw_function) = + material.properties.get_draw_function(ShadowsDrawFunction) + else { + continue; + }; + + let (vertex_slab, index_slab) = + mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + + let batch_set_key = ShadowBatchSetKey { + pipeline: *pipeline_id, + draw_function, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + + shadow_phase.add( + batch_set_key, + ShadowBinKey { + asset_id: mesh_instance.mesh_asset_id.into(), + }, + (entity, main_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + *current_change_tick, + ); + } + } + } +} + +pub struct Shadow { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: ShadowBatchSetKey, + /// Information that separates items into bins. + pub bin_key: ShadowBinKey, + pub representative_entity: (Entity, MainEntity), + pub batch_range: Range, + pub extra_index: PhaseItemExtraIndex, +} + +/// Information that must be identical in order to place opaque meshes in the +/// same *batch set*. +/// +/// A batch set is a set of batches that can be multi-drawn together, if +/// multi-draw is in use. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ShadowBatchSetKey { + /// The identifier of the render pipeline. + pub pipeline: CachedRenderPipelineId, + + /// The function used to draw. + pub draw_function: DrawFunctionId, + + /// The ID of a bind group specific to the material. + /// + /// In the case of PBR, this is the `MaterialBindGroupIndex`. + pub material_bind_group_index: Option, + + /// The ID of the slab of GPU memory that contains vertex data. + /// + /// For non-mesh items, you can fill this with 0 if your items can be + /// multi-drawn, or with a unique value if they can't. + pub vertex_slab: SlabId, + + /// The ID of the slab of GPU memory that contains index data, if present. + /// + /// For non-mesh items, you can safely fill this with `None`. + pub index_slab: Option, +} + +impl PhaseItemBatchSetKey for ShadowBatchSetKey { + fn indexed(&self) -> bool { + self.index_slab.is_some() + } +} + +/// Data used to bin each object in the shadow map phase. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ShadowBinKey { + /// The object. + pub asset_id: UntypedAssetId, +} + +impl PhaseItem for Shadow { + #[inline] + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.batch_set_key.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + #[inline] + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl BinnedPhaseItem for Shadow { + type BatchSetKey = ShadowBatchSetKey; + type BinKey = ShadowBinKey; + + #[inline] + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Shadow { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +impl CachedRenderPipelinePhaseItem for Shadow { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.batch_set_key.pipeline + } +} + +/// The rendering node that renders meshes that were "visible" (so to speak) +/// from a light last frame. +/// +/// If occlusion culling for a light is disabled, then this node simply renders +/// all meshes in range of the light. +#[derive(Deref, DerefMut)] +pub struct EarlyShadowPassNode(ShadowPassNode); + +/// The rendering node that renders meshes that became newly "visible" (so to +/// speak) from a light this frame. +/// +/// If occlusion culling for a light is disabled, then this node does nothing. +#[derive(Deref, DerefMut)] +pub struct LateShadowPassNode(ShadowPassNode); + +/// Encapsulates rendering logic shared between the early and late shadow pass +/// nodes. +pub struct ShadowPassNode { + /// The query that finds cameras in which shadows are visible. + main_view_query: QueryState>, + /// The query that finds shadow cascades. + view_light_query: QueryState<(Read, Read, Has)>, +} + +impl FromWorld for EarlyShadowPassNode { + fn from_world(world: &mut World) -> Self { + Self(ShadowPassNode::from_world(world)) + } +} + +impl FromWorld for LateShadowPassNode { + fn from_world(world: &mut World) -> Self { + Self(ShadowPassNode::from_world(world)) + } +} + +impl FromWorld for ShadowPassNode { + fn from_world(world: &mut World) -> Self { + Self { + main_view_query: QueryState::new(world), + view_light_query: QueryState::new(world), + } + } +} + +impl Node for EarlyShadowPassNode { + fn update(&mut self, world: &mut World) { + self.0.update(world); + } + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + self.0.run(graph, render_context, world, false) + } +} + +impl Node for LateShadowPassNode { + fn update(&mut self, world: &mut World) { + self.0.update(world); + } + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + self.0.run(graph, render_context, world, true) + } +} + +impl ShadowPassNode { + fn update(&mut self, world: &mut World) { + self.main_view_query.update_archetypes(world); + self.view_light_query.update_archetypes(world); + } + + /// Runs the node logic. + /// + /// `is_late` is true if this is the late shadow pass or false if this is + /// the early shadow pass. + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + is_late: bool, + ) -> Result<(), NodeRunError> { + let Some(shadow_render_phases) = world.get_resource::>() + else { + return Ok(()); + }; + + if let Ok(view_lights) = self.main_view_query.get_manual(world, graph.view_entity()) { + for view_light_entity in view_lights.lights.iter().copied() { + let Ok((view_light, extracted_light_view, occlusion_culling)) = + self.view_light_query.get_manual(world, view_light_entity) + else { + continue; + }; + + // There's no need for a late shadow pass if the light isn't + // using occlusion culling. + if is_late && !occlusion_culling { + continue; + } + + let Some(shadow_phase) = + shadow_render_phases.get(&extracted_light_view.retained_view_entity) + else { + continue; + }; + + let depth_stencil_attachment = + Some(view_light.depth_attachment.get_attachment(StoreOp::Store)); + + let diagnostics = render_context.diagnostic_recorder(); + render_context.add_command_buffer_generation_task(move |render_device| { + #[cfg(feature = "trace")] + let _shadow_pass_span = info_span!("", "{}", view_light.pass_name).entered(); + let mut command_encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("shadow_pass_command_encoder"), + }); + + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { + label: Some(&view_light.pass_name), + color_attachments: &[], + depth_stencil_attachment, + timestamp_writes: None, + occlusion_query_set: None, + }); + + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); + let pass_span = + diagnostics.pass_span(&mut render_pass, view_light.pass_name.clone()); + + if let Err(err) = + shadow_phase.render(&mut render_pass, world, view_light_entity) + { + error!("Error encountered while rendering the shadow phase {err:?}"); + } + + pass_span.end(&mut render_pass); + drop(render_pass); + command_encoder.finish() + }); + } + } + + Ok(()) + } +} + +/// Creates the [`ClusterableObjectType`] data for a point or spot light. +fn point_or_spot_light_to_clusterable(point_light: &ExtractedPointLight) -> ClusterableObjectType { + match point_light.spot_light_angles { + Some((_, outer_angle)) => ClusterableObjectType::SpotLight { + outer_angle, + shadows_enabled: point_light.shadows_enabled, + volumetric: point_light.volumetric, + }, + None => ClusterableObjectType::PointLight { + shadows_enabled: point_light.shadows_enabled, + volumetric: point_light.volumetric, + }, + } +} diff --git a/crates/libmarathon/src/render/pbr/render/mesh.rs b/crates/libmarathon/src/render/pbr/render/mesh.rs new file mode 100644 index 0000000..6bb451d --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/mesh.rs @@ -0,0 +1,3312 @@ +use crate::render::pbr::material_bind_groups::{MaterialBindGroupIndex, MaterialBindGroupSlot}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetId}; +use bevy_camera::{ + primitives::Aabb, + visibility::{NoFrustumCulling, RenderLayers, ViewVisibility, VisibilityRange}, + Camera, Camera3d, Projection, +}; +use crate::render::{ + core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT}, + deferred::{AlphaMask3dDeferred, Opaque3dDeferred}, + oit::{prepare_oit_buffers, OrderIndependentTransparencySettingsOffset}, + prepass::MotionVectorPrepass, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_diagnostic::FrameCount; +use bevy_ecs::{ + prelude::*, + query::{QueryData, ROQueryItem}, + system::{lifetimeless::*, SystemParamItem, SystemState}, +}; +use bevy_image::{BevyDefault, ImageSampler, TextureFormatPixelInfo}; +use bevy_light::{ + EnvironmentMapLight, IrradianceVolume, NotShadowCaster, NotShadowReceiver, + ShadowFilteringMethod, TransmittedShadowReceiver, +}; +use bevy_math::{Affine3, Rect, UVec2, Vec3, Vec4}; +use bevy_mesh::{ + skinning::SkinnedMesh, BaseMeshPipelineKey, Mesh, Mesh3d, MeshTag, MeshVertexBufferLayoutRef, + VertexAttributeDescriptor, +}; +use bevy_platform::collections::{hash_map::Entry, HashMap}; +use crate::render::{ + batching::{ + gpu_preprocessing::{ + self, GpuPreprocessingSupport, IndirectBatchSet, IndirectParametersBuffers, + IndirectParametersCpuMetadata, IndirectParametersIndexed, IndirectParametersNonIndexed, + InstanceInputUniformBuffer, UntypedPhaseIndirectParametersBuffers, + }, + no_gpu_preprocessing, GetBatchData, GetFullBatchData, NoAutomaticBatching, + }, + mesh::{allocator::MeshAllocator, RenderMesh, RenderMeshBufferInfo}, + render_asset::RenderAssets, + render_phase::{ + BinnedRenderPhasePlugin, InputUniformIndex, PhaseItem, PhaseItemExtraIndex, RenderCommand, + RenderCommandResult, SortedRenderPhasePlugin, TrackedRenderPass, + }, + render_resource::*, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, + sync_world::MainEntityHashSet, + texture::{DefaultImageSampler, GpuImage}, + view::{ + self, NoIndirectDrawing, RenderVisibilityRanges, RetainedViewEntity, ViewTarget, + ViewUniformOffset, + }, + Extract, +}; +use bevy_shader::{load_shader_library, Shader, ShaderDefVal, ShaderSettings}; +use bevy_transform::components::GlobalTransform; +use bevy_utils::{default, Parallel, TypeIdMap}; +use core::any::TypeId; +use core::mem::size_of; +use material_bind_groups::MaterialBindingId; +use tracing::{error, warn}; + +use self::irradiance_volume::IRRADIANCE_VOLUMES_ARE_USABLE; +use crate::render::pbr::{ + render::{ + morph::{ + extract_morphs, no_automatic_morph_batching, prepare_morphs, MorphIndices, + MorphUniforms, + }, + skin::no_automatic_skin_batching, + }, + *, +}; +use crate::render::oit::OrderIndependentTransparencySettings; +use crate::render::prepass::{DeferredPrepass, DepthPrepass, NormalPrepass}; +use crate::render::tonemapping::{DebandDither, Tonemapping}; +use bevy_ecs::component::Tick; +use bevy_ecs::system::SystemChangeTick; +use crate::render::camera::TemporalJitter; +use crate::render::prelude::Msaa; +use crate::render::sync_world::{MainEntity, MainEntityHashMap}; +use crate::render::view::ExtractedView; +use crate::render::RenderSystems::PrepareAssets; + +use bytemuck::{Pod, Zeroable}; +use nonmax::{NonMaxU16, NonMaxU32}; +use smallvec::{smallvec, SmallVec}; +use static_assertions::const_assert_eq; + +/// Provides support for rendering 3D meshes. +pub struct MeshRenderPlugin { + /// Whether we're building [`MeshUniform`]s on GPU. + /// + /// This requires compute shader support and so will be forcibly disabled if + /// the platform doesn't support those. + pub use_gpu_instance_buffer_builder: bool, + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl MeshRenderPlugin { + /// Creates a new [`MeshRenderPlugin`] with the given debug flags. + pub fn new(debug_flags: RenderDebugFlags) -> MeshRenderPlugin { + MeshRenderPlugin { + use_gpu_instance_buffer_builder: false, + debug_flags, + } + } +} + +/// How many textures are allowed in the view bind group layout (`@group(0)`) before +/// broader compatibility with WebGL and WebGPU is at risk, due to the minimum guaranteed +/// values for `MAX_TEXTURE_IMAGE_UNITS` (in WebGL) and `maxSampledTexturesPerShaderStage` (in WebGPU), +/// currently both at 16. +/// +/// We use 10 here because it still leaves us, in a worst case scenario, with 6 textures for the other bind groups. +/// +/// See: +#[cfg(debug_assertions)] +pub const MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES: usize = 10; + +impl Plugin for MeshRenderPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "forward_io.wgsl"); + load_shader_library!(app, "mesh_view_types.wgsl", |settings| *settings = + ShaderSettings { + shader_defs: vec![ + ShaderDefVal::UInt( + "MAX_DIRECTIONAL_LIGHTS".into(), + MAX_DIRECTIONAL_LIGHTS as u32 + ), + ShaderDefVal::UInt( + "MAX_CASCADES_PER_LIGHT".into(), + MAX_CASCADES_PER_LIGHT as u32, + ) + ] + }); + load_shader_library!(app, "mesh_view_bindings.wgsl"); + load_shader_library!(app, "mesh_types.wgsl"); + load_shader_library!(app, "mesh_functions.wgsl"); + load_shader_library!(app, "skinning.wgsl"); + load_shader_library!(app, "morph.wgsl"); + load_shader_library!(app, "occlusion_culling.wgsl"); + + embedded_asset!(app, "mesh.wgsl"); + + if app.get_sub_app(RenderApp).is_none() { + return; + } + + app.add_systems( + PostUpdate, + (no_automatic_skin_batching, no_automatic_morph_batching), + ) + .add_plugins(( + BinnedRenderPhasePlugin::::new(self.debug_flags), + BinnedRenderPhasePlugin::::new(self.debug_flags), + BinnedRenderPhasePlugin::::new(self.debug_flags), + BinnedRenderPhasePlugin::::new(self.debug_flags), + BinnedRenderPhasePlugin::::new(self.debug_flags), + SortedRenderPhasePlugin::::new(self.debug_flags), + SortedRenderPhasePlugin::::new(self.debug_flags), + )); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .configure_sets( + ExtractSchedule, + MeshExtractionSystems + .after(view::extract_visibility_ranges) + .after(late_sweep_material_instances), + ) + .add_systems( + ExtractSchedule, + ( + extract_skins, + extract_morphs, + gpu_preprocessing::clear_batched_gpu_instance_buffers:: + .before(MeshExtractionSystems), + ), + ) + .add_systems( + Render, + ( + set_mesh_motion_vector_flags.in_set(RenderSystems::PrepareMeshes), + prepare_skins.in_set(RenderSystems::PrepareResources), + prepare_morphs.in_set(RenderSystems::PrepareResources), + prepare_mesh_bind_groups.in_set(RenderSystems::PrepareBindGroups), + prepare_mesh_view_bind_groups + .in_set(RenderSystems::PrepareBindGroups) + .after(prepare_oit_buffers), + no_gpu_preprocessing::clear_batched_cpu_instance_buffers:: + .in_set(RenderSystems::Cleanup) + .after(RenderSystems::Render), + ), + ); + } + } + + fn finish(&self, app: &mut App) { + let mut mesh_bindings_shader_defs = Vec::with_capacity(1); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems( + Render, + check_views_need_specialization.in_set(PrepareAssets), + ); + + let gpu_preprocessing_support = + render_app.world().resource::(); + let use_gpu_instance_buffer_builder = + self.use_gpu_instance_buffer_builder && gpu_preprocessing_support.is_available(); + + let render_mesh_instances = RenderMeshInstances::new(use_gpu_instance_buffer_builder); + render_app.insert_resource(render_mesh_instances); + + if use_gpu_instance_buffer_builder { + render_app + .init_resource::>() + .init_resource::() + .init_resource::() + .add_systems( + ExtractSchedule, + extract_meshes_for_gpu_building.in_set(MeshExtractionSystems), + ) + .add_systems( + Render, + ( + gpu_preprocessing::write_batched_instance_buffers:: + .in_set(RenderSystems::PrepareResourcesFlush), + gpu_preprocessing::delete_old_work_item_buffers:: + .in_set(RenderSystems::PrepareResources), + collect_meshes_for_gpu_building + .in_set(RenderSystems::PrepareMeshes) + // This must be before + // `set_mesh_motion_vector_flags` so it doesn't + // overwrite those flags. + .before(set_mesh_motion_vector_flags), + ), + ); + } else { + let render_device = render_app.world().resource::(); + let cpu_batched_instance_buffer = + no_gpu_preprocessing::BatchedInstanceBuffer::::new(render_device); + render_app + .insert_resource(cpu_batched_instance_buffer) + .add_systems( + ExtractSchedule, + extract_meshes_for_cpu_building.in_set(MeshExtractionSystems), + ) + .add_systems( + Render, + no_gpu_preprocessing::write_batched_instance_buffer:: + .in_set(RenderSystems::PrepareResourcesFlush), + ); + }; + + let render_device = render_app.world().resource::(); + if let Some(per_object_buffer_batch_size) = + GpuArrayBuffer::::batch_size(render_device) + { + mesh_bindings_shader_defs.push(ShaderDefVal::UInt( + "PER_OBJECT_BUFFER_BATCH_SIZE".into(), + per_object_buffer_batch_size, + )); + } + + render_app + .init_resource::() + .init_resource::(); + } + + // Load the mesh_bindings shader module here as it depends on runtime information about + // whether storage buffers are supported, or the maximum uniform buffer binding size. + load_shader_library!(app, "mesh_bindings.wgsl", move |settings| *settings = + ShaderSettings { + shader_defs: mesh_bindings_shader_defs.clone(), + }); + } +} + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct ViewKeyCache(HashMap); + +#[derive(Resource, Deref, DerefMut, Default, Debug, Clone)] +pub struct ViewSpecializationTicks(HashMap); + +pub fn check_views_need_specialization( + mut view_key_cache: ResMut, + mut view_specialization_ticks: ResMut, + mut views: Query<( + &ExtractedView, + &Msaa, + Option<&Tonemapping>, + Option<&DebandDither>, + Option<&ShadowFilteringMethod>, + Has, + ( + Has, + Has, + Has, + Has, + ), + Option<&Camera3d>, + Has, + Option<&Projection>, + Has, + ( + Has>, + Has>, + ), + Has, + )>, + ticks: SystemChangeTick, +) { + for ( + view, + msaa, + tonemapping, + dither, + shadow_filter_method, + ssao, + (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), + camera_3d, + temporal_jitter, + projection, + distance_fog, + (has_environment_maps, has_irradiance_volumes), + has_oit, + ) in views.iter_mut() + { + let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) + | MeshPipelineKey::from_hdr(view.hdr); + + if normal_prepass { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + + if depth_prepass { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + + if motion_vector_prepass { + view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; + } + + if deferred_prepass { + view_key |= MeshPipelineKey::DEFERRED_PREPASS; + } + + if temporal_jitter { + view_key |= MeshPipelineKey::TEMPORAL_JITTER; + } + + if has_environment_maps { + view_key |= MeshPipelineKey::ENVIRONMENT_MAP; + } + + if has_irradiance_volumes { + view_key |= MeshPipelineKey::IRRADIANCE_VOLUME; + } + + if has_oit { + view_key |= MeshPipelineKey::OIT_ENABLED; + } + + if let Some(projection) = projection { + view_key |= match projection { + Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE, + Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC, + Projection::Custom(_) => MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD, + }; + } + + match shadow_filter_method.unwrap_or(&ShadowFilteringMethod::default()) { + ShadowFilteringMethod::Hardware2x2 => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2; + } + ShadowFilteringMethod::Gaussian => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN; + } + ShadowFilteringMethod::Temporal => { + view_key |= MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL; + } + } + + if !view.hdr { + if let Some(tonemapping) = tonemapping { + view_key |= MeshPipelineKey::TONEMAP_IN_SHADER; + view_key |= tonemapping_pipeline_key(*tonemapping); + } + if let Some(DebandDither::Enabled) = dither { + view_key |= MeshPipelineKey::DEBAND_DITHER; + } + } + if ssao { + view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; + } + if distance_fog { + view_key |= MeshPipelineKey::DISTANCE_FOG; + } + if let Some(camera_3d) = camera_3d { + view_key |= screen_space_specular_transmission_pipeline_key( + camera_3d.screen_space_specular_transmission_quality, + ); + } + if !view_key_cache + .get_mut(&view.retained_view_entity) + .is_some_and(|current_key| *current_key == view_key) + { + view_key_cache.insert(view.retained_view_entity, view_key); + view_specialization_ticks.insert(view.retained_view_entity, ticks.this_run()); + } + } +} + +#[derive(Component)] +pub struct MeshTransforms { + pub world_from_local: Affine3, + pub previous_world_from_local: Affine3, + pub flags: u32, +} + +#[derive(ShaderType, Clone)] +pub struct MeshUniform { + // Affine 4x3 matrices transposed to 3x4 + pub world_from_local: [Vec4; 3], + pub previous_world_from_local: [Vec4; 3], + // 3x3 matrix packed in mat2x4 and f32 as: + // [0].xyz, [1].x, + // [1].yz, [2].xy + // [2].z + pub local_from_world_transpose_a: [Vec4; 2], + pub local_from_world_transpose_b: f32, + pub flags: u32, + // Four 16-bit unsigned normalized UV values packed into a `UVec2`: + // + // <--- MSB LSB ---> + // +---- min v ----+ +---- min u ----+ + // lightmap_uv_rect.x: vvvvvvvv vvvvvvvv uuuuuuuu uuuuuuuu, + // +---- max v ----+ +---- max u ----+ + // lightmap_uv_rect.y: VVVVVVVV VVVVVVVV UUUUUUUU UUUUUUUU, + // + // (MSB: most significant bit; LSB: least significant bit.) + pub lightmap_uv_rect: UVec2, + /// The index of this mesh's first vertex in the vertex buffer. + /// + /// Multiple meshes can be packed into a single vertex buffer (see + /// [`MeshAllocator`]). This value stores the offset of the first vertex in + /// this mesh in that buffer. + pub first_vertex_index: u32, + /// The current skin index, or `u32::MAX` if there's no skin. + pub current_skin_index: u32, + /// The material and lightmap indices, packed into 32 bits. + /// + /// Low 16 bits: index of the material inside the bind group data. + /// High 16 bits: index of the lightmap in the binding array. + pub material_and_lightmap_bind_group_slot: u32, + /// User supplied tag to identify this mesh instance. + pub tag: u32, + /// Padding. + pub pad: u32, +} + +/// Information that has to be transferred from CPU to GPU in order to produce +/// the full [`MeshUniform`]. +/// +/// This is essentially a subset of the fields in [`MeshUniform`] above. +#[derive(ShaderType, Pod, Zeroable, Clone, Copy, Default, Debug)] +#[repr(C)] +pub struct MeshInputUniform { + /// Affine 4x3 matrix transposed to 3x4. + pub world_from_local: [Vec4; 3], + /// Four 16-bit unsigned normalized UV values packed into a `UVec2`: + /// + /// ```text + /// <--- MSB LSB ---> + /// +---- min v ----+ +---- min u ----+ + /// lightmap_uv_rect.x: vvvvvvvv vvvvvvvv uuuuuuuu uuuuuuuu, + /// +---- max v ----+ +---- max u ----+ + /// lightmap_uv_rect.y: VVVVVVVV VVVVVVVV UUUUUUUU UUUUUUUU, + /// + /// (MSB: most significant bit; LSB: least significant bit.) + /// ``` + pub lightmap_uv_rect: UVec2, + /// Various [`MeshFlags`]. + pub flags: u32, + /// The index of this mesh's [`MeshInputUniform`] in the previous frame's + /// buffer, if applicable. + /// + /// This is used for TAA. If not present, this will be `u32::MAX`. + pub previous_input_index: u32, + /// The index of this mesh's first vertex in the vertex buffer. + /// + /// Multiple meshes can be packed into a single vertex buffer (see + /// [`MeshAllocator`]). This value stores the offset of the first vertex in + /// this mesh in that buffer. + pub first_vertex_index: u32, + /// The index of this mesh's first index in the index buffer, if any. + /// + /// Multiple meshes can be packed into a single index buffer (see + /// [`MeshAllocator`]). This value stores the offset of the first index in + /// this mesh in that buffer. + /// + /// If this mesh isn't indexed, this value is ignored. + pub first_index_index: u32, + /// For an indexed mesh, the number of indices that make it up; for a + /// non-indexed mesh, the number of vertices in it. + pub index_count: u32, + /// The current skin index, or `u32::MAX` if there's no skin. + pub current_skin_index: u32, + /// The material and lightmap indices, packed into 32 bits. + /// + /// Low 16 bits: index of the material inside the bind group data. + /// High 16 bits: index of the lightmap in the binding array. + pub material_and_lightmap_bind_group_slot: u32, + /// The number of the frame on which this [`MeshInputUniform`] was built. + /// + /// This is used to validate the previous transform and skin. If this + /// [`MeshInputUniform`] wasn't updated on this frame, then we know that + /// neither this mesh's transform nor that of its joints have been updated + /// on this frame, and therefore the transforms of both this mesh and its + /// joints must be identical to those for the previous frame. + pub timestamp: u32, + /// User supplied tag to identify this mesh instance. + pub tag: u32, + /// Padding. + pub pad: u32, +} + +/// Information about each mesh instance needed to cull it on GPU. +/// +/// This consists of its axis-aligned bounding box (AABB). +#[derive(ShaderType, Pod, Zeroable, Clone, Copy, Default)] +#[repr(C)] +pub struct MeshCullingData { + /// The 3D center of the AABB in model space, padded with an extra unused + /// float value. + pub aabb_center: Vec4, + /// The 3D extents of the AABB in model space, divided by two, padded with + /// an extra unused float value. + pub aabb_half_extents: Vec4, +} + +/// A GPU buffer that holds the information needed to cull meshes on GPU. +/// +/// At the moment, this simply holds each mesh's AABB. +/// +/// To avoid wasting CPU time in the CPU culling case, this buffer will be empty +/// if GPU culling isn't in use. +#[derive(Resource, Deref, DerefMut)] +pub struct MeshCullingDataBuffer(RawBufferVec); + +impl MeshUniform { + pub fn new( + mesh_transforms: &MeshTransforms, + first_vertex_index: u32, + material_bind_group_slot: MaterialBindGroupSlot, + maybe_lightmap: Option<(LightmapSlotIndex, Rect)>, + current_skin_index: Option, + tag: Option, + ) -> Self { + let (local_from_world_transpose_a, local_from_world_transpose_b) = + mesh_transforms.world_from_local.inverse_transpose_3x3(); + let lightmap_bind_group_slot = match maybe_lightmap { + None => u16::MAX, + Some((slot_index, _)) => slot_index.into(), + }; + + Self { + world_from_local: mesh_transforms.world_from_local.to_transpose(), + previous_world_from_local: mesh_transforms.previous_world_from_local.to_transpose(), + lightmap_uv_rect: pack_lightmap_uv_rect(maybe_lightmap.map(|(_, uv_rect)| uv_rect)), + local_from_world_transpose_a, + local_from_world_transpose_b, + flags: mesh_transforms.flags, + first_vertex_index, + current_skin_index: current_skin_index.unwrap_or(u32::MAX), + material_and_lightmap_bind_group_slot: u32::from(material_bind_group_slot) + | ((lightmap_bind_group_slot as u32) << 16), + tag: tag.unwrap_or(0), + pad: 0, + } + } +} + +// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_types.wgsl! +bitflags::bitflags! { + /// Various flags and tightly-packed values on a mesh. + /// + /// Flags grow from the top bit down; other values grow from the bottom bit + /// up. + #[repr(transparent)] + pub struct MeshFlags: u32 { + /// Bitmask for the 16-bit index into the LOD array. + /// + /// This will be `u16::MAX` if this mesh has no LOD. + const LOD_INDEX_MASK = (1 << 16) - 1; + /// Disables frustum culling for this mesh. + /// + /// This corresponds to the + /// [`bevy_render::view::visibility::NoFrustumCulling`] component. + const NO_FRUSTUM_CULLING = 1 << 28; + const SHADOW_RECEIVER = 1 << 29; + const TRANSMITTED_SHADOW_RECEIVER = 1 << 30; + // Indicates the sign of the determinant of the 3x3 model matrix. If the sign is positive, + // then the flag should be set, else it should not be set. + const SIGN_DETERMINANT_MODEL_3X3 = 1 << 31; + const NONE = 0; + const UNINITIALIZED = 0xFFFFFFFF; + } +} + +impl MeshFlags { + fn from_components( + transform: &GlobalTransform, + lod_index: Option, + no_frustum_culling: bool, + not_shadow_receiver: bool, + transmitted_receiver: bool, + ) -> MeshFlags { + let mut mesh_flags = if not_shadow_receiver { + MeshFlags::empty() + } else { + MeshFlags::SHADOW_RECEIVER + }; + if no_frustum_culling { + mesh_flags |= MeshFlags::NO_FRUSTUM_CULLING; + } + if transmitted_receiver { + mesh_flags |= MeshFlags::TRANSMITTED_SHADOW_RECEIVER; + } + if transform.affine().matrix3.determinant().is_sign_positive() { + mesh_flags |= MeshFlags::SIGN_DETERMINANT_MODEL_3X3; + } + + let lod_index_bits = match lod_index { + None => u16::MAX, + Some(lod_index) => u16::from(lod_index), + }; + mesh_flags |= + MeshFlags::from_bits_retain((lod_index_bits as u32) << MeshFlags::LOD_INDEX_SHIFT); + + mesh_flags + } + + /// The first bit of the LOD index. + pub const LOD_INDEX_SHIFT: u32 = 0; +} + +bitflags::bitflags! { + /// Various useful flags for [`RenderMeshInstance`]s. + #[derive(Clone, Copy)] + pub struct RenderMeshInstanceFlags: u8 { + /// The mesh casts shadows. + const SHADOW_CASTER = 1 << 0; + /// The mesh can participate in automatic batching. + const AUTOMATIC_BATCHING = 1 << 1; + /// The mesh had a transform last frame and so is eligible for motion + /// vector computation. + const HAS_PREVIOUS_TRANSFORM = 1 << 2; + /// The mesh had a skin last frame and so that skin should be taken into + /// account for motion vector computation. + const HAS_PREVIOUS_SKIN = 1 << 3; + /// The mesh had morph targets last frame and so they should be taken + /// into account for motion vector computation. + const HAS_PREVIOUS_MORPH = 1 << 4; + } +} + +/// CPU data that the render world keeps for each entity, when *not* using GPU +/// mesh uniform building. +#[derive(Deref, DerefMut)] +pub struct RenderMeshInstanceCpu { + /// Data shared between both the CPU mesh uniform building and the GPU mesh + /// uniform building paths. + #[deref] + pub shared: RenderMeshInstanceShared, + /// The transform of the mesh. + /// + /// This will be written into the [`MeshUniform`] at the appropriate time. + pub transforms: MeshTransforms, +} + +/// CPU data that the render world needs to keep for each entity that contains a +/// mesh when using GPU mesh uniform building. +#[derive(Deref, DerefMut)] +pub struct RenderMeshInstanceGpu { + /// Data shared between both the CPU mesh uniform building and the GPU mesh + /// uniform building paths. + #[deref] + pub shared: RenderMeshInstanceShared, + /// The translation of the mesh. + /// + /// This is the only part of the transform that we have to keep on CPU (for + /// distance sorting). + pub translation: Vec3, + /// The index of the [`MeshInputUniform`] in the buffer. + pub current_uniform_index: NonMaxU32, +} + +/// CPU data that the render world needs to keep about each entity that contains +/// a mesh. +pub struct RenderMeshInstanceShared { + /// The [`AssetId`] of the mesh. + pub mesh_asset_id: AssetId, + /// A slot for the material bind group index. + pub material_bindings_index: MaterialBindingId, + /// Various flags. + pub flags: RenderMeshInstanceFlags, + /// Index of the slab that the lightmap resides in, if a lightmap is + /// present. + pub lightmap_slab_index: Option, + /// User supplied tag to identify this mesh instance. + pub tag: u32, + /// Render layers that this mesh instance belongs to. + pub render_layers: Option, +} + +/// Information that is gathered during the parallel portion of mesh extraction +/// when GPU mesh uniform building is enabled. +/// +/// From this, the [`MeshInputUniform`] and [`RenderMeshInstanceGpu`] are +/// prepared. +pub struct RenderMeshInstanceGpuBuilder { + /// Data that will be placed on the [`RenderMeshInstanceGpu`]. + pub shared: RenderMeshInstanceShared, + /// The current transform. + pub world_from_local: Affine3, + /// Four 16-bit unsigned normalized UV values packed into a [`UVec2`]: + /// + /// ```text + /// <--- MSB LSB ---> + /// +---- min v ----+ +---- min u ----+ + /// lightmap_uv_rect.x: vvvvvvvv vvvvvvvv uuuuuuuu uuuuuuuu, + /// +---- max v ----+ +---- max u ----+ + /// lightmap_uv_rect.y: VVVVVVVV VVVVVVVV UUUUUUUU UUUUUUUU, + /// + /// (MSB: most significant bit; LSB: least significant bit.) + /// ``` + pub lightmap_uv_rect: UVec2, + /// The index of the previous mesh input. + pub previous_input_index: Option, + /// Various flags. + pub mesh_flags: MeshFlags, +} + +/// The per-thread queues used during [`extract_meshes_for_gpu_building`]. +/// +/// There are two varieties of these: one for when culling happens on CPU and +/// one for when culling happens on GPU. Having the two varieties avoids wasting +/// space if GPU culling is disabled. +#[derive(Default)] +pub enum RenderMeshInstanceGpuQueue { + /// The default value. + /// + /// This becomes [`RenderMeshInstanceGpuQueue::CpuCulling`] or + /// [`RenderMeshInstanceGpuQueue::GpuCulling`] once extraction starts. + #[default] + None, + /// The version of [`RenderMeshInstanceGpuQueue`] that omits the + /// [`MeshCullingData`], so that we don't waste space when GPU + /// culling is disabled. + CpuCulling { + /// Stores GPU data for each entity that became visible or changed in + /// such a way that necessitates updating the [`MeshInputUniform`] (e.g. + /// changed transform). + changed: Vec<(MainEntity, RenderMeshInstanceGpuBuilder)>, + /// Stores the IDs of entities that became invisible this frame. + removed: Vec, + }, + /// The version of [`RenderMeshInstanceGpuQueue`] that contains the + /// [`MeshCullingData`], used when any view has GPU culling + /// enabled. + GpuCulling { + /// Stores GPU data for each entity that became visible or changed in + /// such a way that necessitates updating the [`MeshInputUniform`] (e.g. + /// changed transform). + changed: Vec<(MainEntity, RenderMeshInstanceGpuBuilder, MeshCullingData)>, + /// Stores the IDs of entities that became invisible this frame. + removed: Vec, + }, +} + +/// The per-thread queues containing mesh instances, populated during the +/// extract phase. +/// +/// These are filled in [`extract_meshes_for_gpu_building`] and consumed in +/// [`collect_meshes_for_gpu_building`]. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct RenderMeshInstanceGpuQueues(Parallel); + +/// Holds a list of meshes that couldn't be extracted this frame because their +/// materials weren't prepared yet. +/// +/// On subsequent frames, we try to reextract those meshes. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct MeshesToReextractNextFrame(MainEntityHashSet); + +impl RenderMeshInstanceShared { + /// A gpu builder will provide the mesh instance id + /// during [`RenderMeshInstanceGpuBuilder::update`]. + fn for_gpu_building( + previous_transform: Option<&PreviousGlobalTransform>, + mesh: &Mesh3d, + tag: Option<&MeshTag>, + not_shadow_caster: bool, + no_automatic_batching: bool, + render_layers: Option<&RenderLayers>, + ) -> Self { + Self::for_cpu_building( + previous_transform, + mesh, + tag, + default(), + not_shadow_caster, + no_automatic_batching, + render_layers, + ) + } + + /// The cpu builder does not have an equivalent [`RenderMeshInstanceGpuBuilder::update`]. + fn for_cpu_building( + previous_transform: Option<&PreviousGlobalTransform>, + mesh: &Mesh3d, + tag: Option<&MeshTag>, + material_bindings_index: MaterialBindingId, + not_shadow_caster: bool, + no_automatic_batching: bool, + render_layers: Option<&RenderLayers>, + ) -> Self { + let mut mesh_instance_flags = RenderMeshInstanceFlags::empty(); + mesh_instance_flags.set(RenderMeshInstanceFlags::SHADOW_CASTER, !not_shadow_caster); + mesh_instance_flags.set( + RenderMeshInstanceFlags::AUTOMATIC_BATCHING, + !no_automatic_batching, + ); + mesh_instance_flags.set( + RenderMeshInstanceFlags::HAS_PREVIOUS_TRANSFORM, + previous_transform.is_some(), + ); + + RenderMeshInstanceShared { + mesh_asset_id: mesh.id(), + flags: mesh_instance_flags, + material_bindings_index, + lightmap_slab_index: None, + tag: tag.map_or(0, |i| **i), + render_layers: render_layers.cloned(), + } + } + + /// Returns true if this entity is eligible to participate in automatic + /// batching. + #[inline] + pub fn should_batch(&self) -> bool { + self.flags + .contains(RenderMeshInstanceFlags::AUTOMATIC_BATCHING) + } +} + +/// Information that the render world keeps about each entity that contains a +/// mesh. +/// +/// The set of information needed is different depending on whether CPU or GPU +/// [`MeshUniform`] building is in use. +#[derive(Resource)] +pub enum RenderMeshInstances { + /// Information needed when using CPU mesh instance data building. + CpuBuilding(RenderMeshInstancesCpu), + /// Information needed when using GPU mesh instance data building. + GpuBuilding(RenderMeshInstancesGpu), +} + +/// Information that the render world keeps about each entity that contains a +/// mesh, when using CPU mesh instance data building. +#[derive(Default, Deref, DerefMut)] +pub struct RenderMeshInstancesCpu(MainEntityHashMap); + +/// Information that the render world keeps about each entity that contains a +/// mesh, when using GPU mesh instance data building. +#[derive(Default, Deref, DerefMut)] +pub struct RenderMeshInstancesGpu(MainEntityHashMap); + +impl RenderMeshInstances { + /// Creates a new [`RenderMeshInstances`] instance. + fn new(use_gpu_instance_buffer_builder: bool) -> RenderMeshInstances { + if use_gpu_instance_buffer_builder { + RenderMeshInstances::GpuBuilding(RenderMeshInstancesGpu::default()) + } else { + RenderMeshInstances::CpuBuilding(RenderMeshInstancesCpu::default()) + } + } + + /// Returns the ID of the mesh asset attached to the given entity, if any. + pub fn mesh_asset_id(&self, entity: MainEntity) -> Option> { + match *self { + RenderMeshInstances::CpuBuilding(ref instances) => instances.mesh_asset_id(entity), + RenderMeshInstances::GpuBuilding(ref instances) => instances.mesh_asset_id(entity), + } + } + + /// Constructs [`RenderMeshQueueData`] for the given entity, if it has a + /// mesh attached. + pub fn render_mesh_queue_data(&self, entity: MainEntity) -> Option> { + match *self { + RenderMeshInstances::CpuBuilding(ref instances) => { + instances.render_mesh_queue_data(entity) + } + RenderMeshInstances::GpuBuilding(ref instances) => { + instances.render_mesh_queue_data(entity) + } + } + } + + /// Inserts the given flags into the CPU or GPU render mesh instance data + /// for the given mesh as appropriate. + fn insert_mesh_instance_flags(&mut self, entity: MainEntity, flags: RenderMeshInstanceFlags) { + match *self { + RenderMeshInstances::CpuBuilding(ref mut instances) => { + instances.insert_mesh_instance_flags(entity, flags); + } + RenderMeshInstances::GpuBuilding(ref mut instances) => { + instances.insert_mesh_instance_flags(entity, flags); + } + } + } +} + +impl RenderMeshInstancesCpu { + fn mesh_asset_id(&self, entity: MainEntity) -> Option> { + self.get(&entity) + .map(|render_mesh_instance| render_mesh_instance.mesh_asset_id) + } + + fn render_mesh_queue_data(&self, entity: MainEntity) -> Option> { + self.get(&entity) + .map(|render_mesh_instance| RenderMeshQueueData { + shared: &render_mesh_instance.shared, + translation: render_mesh_instance.transforms.world_from_local.translation, + current_uniform_index: InputUniformIndex::default(), + }) + } + + /// Inserts the given flags into the render mesh instance data for the given + /// mesh. + fn insert_mesh_instance_flags(&mut self, entity: MainEntity, flags: RenderMeshInstanceFlags) { + if let Some(instance) = self.get_mut(&entity) { + instance.flags.insert(flags); + } + } +} + +impl RenderMeshInstancesGpu { + fn mesh_asset_id(&self, entity: MainEntity) -> Option> { + self.get(&entity) + .map(|render_mesh_instance| render_mesh_instance.mesh_asset_id) + } + + fn render_mesh_queue_data(&self, entity: MainEntity) -> Option> { + self.get(&entity) + .map(|render_mesh_instance| RenderMeshQueueData { + shared: &render_mesh_instance.shared, + translation: render_mesh_instance.translation, + current_uniform_index: InputUniformIndex( + render_mesh_instance.current_uniform_index.into(), + ), + }) + } + + /// Inserts the given flags into the render mesh instance data for the given + /// mesh. + fn insert_mesh_instance_flags(&mut self, entity: MainEntity, flags: RenderMeshInstanceFlags) { + if let Some(instance) = self.get_mut(&entity) { + instance.flags.insert(flags); + } + } +} + +impl RenderMeshInstanceGpuQueue { + /// Clears out a [`RenderMeshInstanceGpuQueue`], creating or recreating it + /// as necessary. + /// + /// `any_gpu_culling` should be set to true if any view has GPU culling + /// enabled. + fn init(&mut self, any_gpu_culling: bool) { + match (any_gpu_culling, &mut *self) { + (true, RenderMeshInstanceGpuQueue::GpuCulling { changed, removed }) => { + changed.clear(); + removed.clear(); + } + (true, _) => { + *self = RenderMeshInstanceGpuQueue::GpuCulling { + changed: vec![], + removed: vec![], + } + } + (false, RenderMeshInstanceGpuQueue::CpuCulling { changed, removed }) => { + changed.clear(); + removed.clear(); + } + (false, _) => { + *self = RenderMeshInstanceGpuQueue::CpuCulling { + changed: vec![], + removed: vec![], + } + } + } + } + + /// Adds a new mesh to this queue. + fn push( + &mut self, + entity: MainEntity, + instance_builder: RenderMeshInstanceGpuBuilder, + culling_data_builder: Option, + ) { + match (&mut *self, culling_data_builder) { + ( + &mut RenderMeshInstanceGpuQueue::CpuCulling { + changed: ref mut queue, + .. + }, + None, + ) => { + queue.push((entity, instance_builder)); + } + ( + &mut RenderMeshInstanceGpuQueue::GpuCulling { + changed: ref mut queue, + .. + }, + Some(culling_data_builder), + ) => { + queue.push((entity, instance_builder, culling_data_builder)); + } + (_, None) => { + *self = RenderMeshInstanceGpuQueue::CpuCulling { + changed: vec![(entity, instance_builder)], + removed: vec![], + }; + } + (_, Some(culling_data_builder)) => { + *self = RenderMeshInstanceGpuQueue::GpuCulling { + changed: vec![(entity, instance_builder, culling_data_builder)], + removed: vec![], + }; + } + } + } + + /// Adds the given entity to the `removed` list, queuing it for removal. + /// + /// The `gpu_culling` parameter specifies whether GPU culling is enabled. + fn remove(&mut self, entity: MainEntity, gpu_culling: bool) { + match (&mut *self, gpu_culling) { + (RenderMeshInstanceGpuQueue::None, false) => { + *self = RenderMeshInstanceGpuQueue::CpuCulling { + changed: vec![], + removed: vec![entity], + } + } + (RenderMeshInstanceGpuQueue::None, true) => { + *self = RenderMeshInstanceGpuQueue::GpuCulling { + changed: vec![], + removed: vec![entity], + } + } + (RenderMeshInstanceGpuQueue::CpuCulling { removed, .. }, _) + | (RenderMeshInstanceGpuQueue::GpuCulling { removed, .. }, _) => { + removed.push(entity); + } + } + } +} + +impl RenderMeshInstanceGpuBuilder { + /// Flushes this mesh instance to the [`RenderMeshInstanceGpu`] and + /// [`MeshInputUniform`] tables, replacing the existing entry if applicable. + fn update( + mut self, + entity: MainEntity, + render_mesh_instances: &mut MainEntityHashMap, + current_input_buffer: &mut InstanceInputUniformBuffer, + previous_input_buffer: &mut InstanceInputUniformBuffer, + mesh_allocator: &MeshAllocator, + mesh_material_ids: &RenderMaterialInstances, + render_material_bindings: &RenderMaterialBindings, + render_lightmaps: &RenderLightmaps, + skin_uniforms: &SkinUniforms, + timestamp: FrameCount, + meshes_to_reextract_next_frame: &mut MeshesToReextractNextFrame, + ) -> Option { + let (first_vertex_index, vertex_count) = + match mesh_allocator.mesh_vertex_slice(&self.shared.mesh_asset_id) { + Some(mesh_vertex_slice) => ( + mesh_vertex_slice.range.start, + mesh_vertex_slice.range.end - mesh_vertex_slice.range.start, + ), + None => (0, 0), + }; + let (mesh_is_indexed, first_index_index, index_count) = + match mesh_allocator.mesh_index_slice(&self.shared.mesh_asset_id) { + Some(mesh_index_slice) => ( + true, + mesh_index_slice.range.start, + mesh_index_slice.range.end - mesh_index_slice.range.start, + ), + None => (false, 0, 0), + }; + let current_skin_index = match skin_uniforms.skin_byte_offset(entity) { + Some(skin_index) => skin_index.index(), + None => u32::MAX, + }; + + // Look up the material index. If we couldn't fetch the material index, + // then the material hasn't been prepared yet, perhaps because it hasn't + // yet loaded. In that case, add the mesh to + // `meshes_to_reextract_next_frame` and bail. + let mesh_material = mesh_material_ids.mesh_material(entity); + let mesh_material_binding_id = if mesh_material != DUMMY_MESH_MATERIAL.untyped() { + match render_material_bindings.get(&mesh_material) { + Some(binding_id) => *binding_id, + None => { + meshes_to_reextract_next_frame.insert(entity); + return None; + } + } + } else { + // Use a dummy material binding ID. + MaterialBindingId::default() + }; + self.shared.material_bindings_index = mesh_material_binding_id; + + let lightmap_slot = match render_lightmaps.render_lightmaps.get(&entity) { + Some(render_lightmap) => u16::from(*render_lightmap.slot_index), + None => u16::MAX, + }; + let lightmap_slab_index = render_lightmaps + .render_lightmaps + .get(&entity) + .map(|lightmap| lightmap.slab_index); + self.shared.lightmap_slab_index = lightmap_slab_index; + + // Create the mesh input uniform. + let mut mesh_input_uniform = MeshInputUniform { + world_from_local: self.world_from_local.to_transpose(), + lightmap_uv_rect: self.lightmap_uv_rect, + flags: self.mesh_flags.bits(), + previous_input_index: u32::MAX, + timestamp: timestamp.0, + first_vertex_index, + first_index_index, + index_count: if mesh_is_indexed { + index_count + } else { + vertex_count + }, + current_skin_index, + material_and_lightmap_bind_group_slot: u32::from( + self.shared.material_bindings_index.slot, + ) | ((lightmap_slot as u32) << 16), + tag: self.shared.tag, + pad: 0, + }; + + // Did the last frame contain this entity as well? + let current_uniform_index; + match render_mesh_instances.entry(entity) { + Entry::Occupied(mut occupied_entry) => { + // Yes, it did. Replace its entry with the new one. + + // Reserve a slot. + current_uniform_index = u32::from(occupied_entry.get_mut().current_uniform_index); + + // Save the old mesh input uniform. The mesh preprocessing + // shader will need it to compute motion vectors. + let previous_mesh_input_uniform = + current_input_buffer.get_unchecked(current_uniform_index); + let previous_input_index = previous_input_buffer.add(previous_mesh_input_uniform); + mesh_input_uniform.previous_input_index = previous_input_index; + + // Write in the new mesh input uniform. + current_input_buffer.set(current_uniform_index, mesh_input_uniform); + + occupied_entry.replace_entry_with(|_, _| { + Some(RenderMeshInstanceGpu { + translation: self.world_from_local.translation, + shared: self.shared, + current_uniform_index: NonMaxU32::new(current_uniform_index) + .unwrap_or_default(), + }) + }); + } + + Entry::Vacant(vacant_entry) => { + // No, this is a new entity. Push its data on to the buffer. + current_uniform_index = current_input_buffer.add(mesh_input_uniform); + + vacant_entry.insert(RenderMeshInstanceGpu { + translation: self.world_from_local.translation, + shared: self.shared, + current_uniform_index: NonMaxU32::new(current_uniform_index) + .unwrap_or_default(), + }); + } + } + + Some(current_uniform_index) + } +} + +/// Removes a [`MeshInputUniform`] corresponding to an entity that became +/// invisible from the buffer. +fn remove_mesh_input_uniform( + entity: MainEntity, + render_mesh_instances: &mut MainEntityHashMap, + current_input_buffer: &mut InstanceInputUniformBuffer, +) -> Option { + // Remove the uniform data. + let removed_render_mesh_instance = render_mesh_instances.remove(&entity)?; + + let removed_uniform_index = removed_render_mesh_instance.current_uniform_index.get(); + current_input_buffer.remove(removed_uniform_index); + Some(removed_uniform_index) +} + +impl MeshCullingData { + /// Returns a new [`MeshCullingData`] initialized with the given AABB. + /// + /// If no AABB is provided, an infinitely-large one is conservatively + /// chosen. + fn new(aabb: Option<&Aabb>) -> Self { + match aabb { + Some(aabb) => MeshCullingData { + aabb_center: aabb.center.extend(0.0), + aabb_half_extents: aabb.half_extents.extend(0.0), + }, + None => MeshCullingData { + aabb_center: Vec3::ZERO.extend(0.0), + aabb_half_extents: Vec3::INFINITY.extend(0.0), + }, + } + } + + /// Flushes this mesh instance culling data to the + /// [`MeshCullingDataBuffer`], replacing the existing entry if applicable. + fn update( + &self, + mesh_culling_data_buffer: &mut MeshCullingDataBuffer, + instance_data_index: usize, + ) { + while mesh_culling_data_buffer.len() < instance_data_index + 1 { + mesh_culling_data_buffer.push(MeshCullingData::default()); + } + mesh_culling_data_buffer.values_mut()[instance_data_index] = *self; + } +} + +impl Default for MeshCullingDataBuffer { + #[inline] + fn default() -> Self { + Self(RawBufferVec::new(BufferUsages::STORAGE)) + } +} + +/// Data that [`crate::material::queue_material_meshes`] and similar systems +/// need in order to place entities that contain meshes in the right batch. +#[derive(Deref)] +pub struct RenderMeshQueueData<'a> { + /// General information about the mesh instance. + #[deref] + pub shared: &'a RenderMeshInstanceShared, + /// The translation of the mesh instance. + pub translation: Vec3, + /// The index of the [`MeshInputUniform`] in the GPU buffer for this mesh + /// instance. + pub current_uniform_index: InputUniformIndex, +} + +/// A [`SystemSet`] that encompasses both [`extract_meshes_for_cpu_building`] +/// and [`extract_meshes_for_gpu_building`]. +#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] +pub struct MeshExtractionSystems; + +/// Deprecated alias for [`MeshExtractionSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `MeshExtractionSystems`.")] +pub type ExtractMeshesSet = MeshExtractionSystems; + +/// Extracts meshes from the main world into the render world, populating the +/// [`RenderMeshInstances`]. +/// +/// This is the variant of the system that runs when we're *not* using GPU +/// [`MeshUniform`] building. +pub fn extract_meshes_for_cpu_building( + mut render_mesh_instances: ResMut, + mesh_material_ids: Res, + render_material_bindings: Res, + render_visibility_ranges: Res, + mut render_mesh_instance_queues: Local>>, + meshes_query: Extract< + Query<( + Entity, + &ViewVisibility, + &GlobalTransform, + Option<&PreviousGlobalTransform>, + &Mesh3d, + Option<&MeshTag>, + Has, + Has, + Has, + Has, + Has, + Has, + Option<&RenderLayers>, + )>, + >, +) { + meshes_query.par_iter().for_each_init( + || render_mesh_instance_queues.borrow_local_mut(), + |queue, + ( + entity, + view_visibility, + transform, + previous_transform, + mesh, + tag, + no_frustum_culling, + not_shadow_receiver, + transmitted_receiver, + not_shadow_caster, + no_automatic_batching, + visibility_range, + render_layers, + )| { + if !view_visibility.get() { + return; + } + + let mut lod_index = None; + if visibility_range { + lod_index = render_visibility_ranges.lod_index_for_entity(entity.into()); + } + + let mesh_flags = MeshFlags::from_components( + transform, + lod_index, + no_frustum_culling, + not_shadow_receiver, + transmitted_receiver, + ); + + let mesh_material = mesh_material_ids.mesh_material(MainEntity::from(entity)); + + let material_bindings_index = render_material_bindings + .get(&mesh_material) + .copied() + .unwrap_or_default(); + + let shared = RenderMeshInstanceShared::for_cpu_building( + previous_transform, + mesh, + tag, + material_bindings_index, + not_shadow_caster, + no_automatic_batching, + render_layers, + ); + + let world_from_local = transform.affine(); + queue.push(( + entity, + RenderMeshInstanceCpu { + transforms: MeshTransforms { + world_from_local: (&world_from_local).into(), + previous_world_from_local: (&previous_transform + .map(|t| t.0) + .unwrap_or(world_from_local)) + .into(), + flags: mesh_flags.bits(), + }, + shared, + }, + )); + }, + ); + + // Collect the render mesh instances. + let RenderMeshInstances::CpuBuilding(ref mut render_mesh_instances) = *render_mesh_instances + else { + panic!( + "`extract_meshes_for_cpu_building` should only be called if we're using CPU \ + `MeshUniform` building" + ); + }; + + render_mesh_instances.clear(); + for queue in render_mesh_instance_queues.iter_mut() { + for (entity, render_mesh_instance) in queue.drain(..) { + render_mesh_instances.insert(entity.into(), render_mesh_instance); + } + } +} + +/// All the data that we need from a mesh in the main world. +type GpuMeshExtractionQuery = ( + Entity, + Read, + Read, + Option>, + Option>, + Option>, + Read, + Option>, + Has, + Has, + Has, + Has, + Has, + Has, + Option>, +); + +/// Extracts meshes from the main world into the render world and queues +/// [`MeshInputUniform`]s to be uploaded to the GPU. +/// +/// This is optimized to only look at entities that have changed since the last +/// frame. +/// +/// This is the variant of the system that runs when we're using GPU +/// [`MeshUniform`] building. +pub fn extract_meshes_for_gpu_building( + mut render_mesh_instances: ResMut, + render_visibility_ranges: Res, + mut render_mesh_instance_queues: ResMut, + changed_meshes_query: Extract< + Query< + GpuMeshExtractionQuery, + Or<( + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + )>, + >, + >, + all_meshes_query: Extract>, + mut removed_meshes_query: Extract>, + gpu_culling_query: Extract, Without)>>, + meshes_to_reextract_next_frame: ResMut, +) { + let any_gpu_culling = !gpu_culling_query.is_empty(); + + for render_mesh_instance_queue in render_mesh_instance_queues.iter_mut() { + render_mesh_instance_queue.init(any_gpu_culling); + } + + // Collect render mesh instances. Build up the uniform buffer. + + let RenderMeshInstances::GpuBuilding(ref mut render_mesh_instances) = *render_mesh_instances + else { + panic!( + "`extract_meshes_for_gpu_building` should only be called if we're \ + using GPU `MeshUniform` building" + ); + }; + + // Find all meshes that have changed, and record information needed to + // construct the `MeshInputUniform` for them. + changed_meshes_query.par_iter().for_each_init( + || render_mesh_instance_queues.borrow_local_mut(), + |queue, query_row| { + extract_mesh_for_gpu_building( + query_row, + &render_visibility_ranges, + render_mesh_instances, + queue, + any_gpu_culling, + ); + }, + ); + + // Process materials that `collect_meshes_for_gpu_building` marked as + // needing to be reextracted. This will happen when we extracted a mesh on + // some previous frame, but its material hadn't been prepared yet, perhaps + // because the material hadn't yet been loaded. We reextract such materials + // on subsequent frames so that `collect_meshes_for_gpu_building` will check + // to see if their materials have been prepared. + let mut queue = render_mesh_instance_queues.borrow_local_mut(); + for &mesh_entity in &**meshes_to_reextract_next_frame { + if let Ok(query_row) = all_meshes_query.get(*mesh_entity) { + extract_mesh_for_gpu_building( + query_row, + &render_visibility_ranges, + render_mesh_instances, + &mut queue, + any_gpu_culling, + ); + } + } + + // Also record info about each mesh that became invisible. + for entity in removed_meshes_query.read() { + // Only queue a mesh for removal if we didn't pick it up above. + // It's possible that a necessary component was removed and re-added in + // the same frame. + let entity = MainEntity::from(entity); + if !changed_meshes_query.contains(*entity) + && !meshes_to_reextract_next_frame.contains(&entity) + { + queue.remove(entity, any_gpu_culling); + } + } +} + +fn extract_mesh_for_gpu_building( + ( + entity, + view_visibility, + transform, + previous_transform, + lightmap, + aabb, + mesh, + tag, + no_frustum_culling, + not_shadow_receiver, + transmitted_receiver, + not_shadow_caster, + no_automatic_batching, + visibility_range, + render_layers, + ): ::Item<'_, '_>, + render_visibility_ranges: &RenderVisibilityRanges, + render_mesh_instances: &RenderMeshInstancesGpu, + queue: &mut RenderMeshInstanceGpuQueue, + any_gpu_culling: bool, +) { + if !view_visibility.get() { + queue.remove(entity.into(), any_gpu_culling); + return; + } + + let mut lod_index = None; + if visibility_range { + lod_index = render_visibility_ranges.lod_index_for_entity(entity.into()); + } + + let mesh_flags = MeshFlags::from_components( + transform, + lod_index, + no_frustum_culling, + not_shadow_receiver, + transmitted_receiver, + ); + + let shared = RenderMeshInstanceShared::for_gpu_building( + previous_transform, + mesh, + tag, + not_shadow_caster, + no_automatic_batching, + render_layers, + ); + + let lightmap_uv_rect = pack_lightmap_uv_rect(lightmap.map(|lightmap| lightmap.uv_rect)); + + let gpu_mesh_culling_data = any_gpu_culling.then(|| MeshCullingData::new(aabb)); + + let previous_input_index = if shared + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_TRANSFORM) + { + render_mesh_instances + .get(&MainEntity::from(entity)) + .map(|render_mesh_instance| render_mesh_instance.current_uniform_index) + } else { + None + }; + + let gpu_mesh_instance_builder = RenderMeshInstanceGpuBuilder { + shared, + world_from_local: (&transform.affine()).into(), + lightmap_uv_rect, + mesh_flags, + previous_input_index, + }; + + queue.push( + entity.into(), + gpu_mesh_instance_builder, + gpu_mesh_culling_data, + ); +} + +/// A system that sets the [`RenderMeshInstanceFlags`] for each mesh based on +/// whether the previous frame had skins and/or morph targets. +/// +/// Ordinarily, [`RenderMeshInstanceFlags`] are set during the extraction phase. +/// However, we can't do that for the flags related to skins and morph targets +/// because the previous frame's skin and morph targets are the responsibility +/// of [`extract_skins`] and [`extract_morphs`] respectively. We want to run +/// those systems in parallel with mesh extraction for performance, so we need +/// to defer setting of these mesh instance flags to after extraction, which +/// this system does. An alternative to having skin- and morph-target-related +/// data in [`RenderMeshInstanceFlags`] would be to have +/// [`crate::material::queue_material_meshes`] check the skin and morph target +/// tables for each mesh, but that would be too slow in the hot mesh queuing +/// loop. +pub(crate) fn set_mesh_motion_vector_flags( + mut render_mesh_instances: ResMut, + skin_uniforms: Res, + morph_indices: Res, +) { + for &entity in skin_uniforms.all_skins() { + render_mesh_instances + .insert_mesh_instance_flags(entity, RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN); + } + for &entity in morph_indices.prev.keys() { + render_mesh_instances + .insert_mesh_instance_flags(entity, RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH); + } +} + +/// Creates the [`RenderMeshInstanceGpu`]s and [`MeshInputUniform`]s when GPU +/// mesh uniforms are built. +pub fn collect_meshes_for_gpu_building( + render_mesh_instances: ResMut, + batched_instance_buffers: ResMut< + gpu_preprocessing::BatchedInstanceBuffers, + >, + mut mesh_culling_data_buffer: ResMut, + mut render_mesh_instance_queues: ResMut, + mesh_allocator: Res, + mesh_material_ids: Res, + render_material_bindings: Res, + render_lightmaps: Res, + skin_uniforms: Res, + frame_count: Res, + mut meshes_to_reextract_next_frame: ResMut, +) { + let RenderMeshInstances::GpuBuilding(render_mesh_instances) = + render_mesh_instances.into_inner() + else { + return; + }; + + // We're going to rebuild `meshes_to_reextract_next_frame`. + meshes_to_reextract_next_frame.clear(); + + // Collect render mesh instances. Build up the uniform buffer. + let gpu_preprocessing::BatchedInstanceBuffers { + current_input_buffer, + previous_input_buffer, + .. + } = batched_instance_buffers.into_inner(); + + previous_input_buffer.clear(); + + // Build the [`RenderMeshInstance`]s and [`MeshInputUniform`]s. + + for queue in render_mesh_instance_queues.iter_mut() { + match *queue { + RenderMeshInstanceGpuQueue::None => { + // This can only happen if the queue is empty. + } + + RenderMeshInstanceGpuQueue::CpuCulling { + ref mut changed, + ref mut removed, + } => { + for (entity, mesh_instance_builder) in changed.drain(..) { + mesh_instance_builder.update( + entity, + &mut *render_mesh_instances, + current_input_buffer, + previous_input_buffer, + &mesh_allocator, + &mesh_material_ids, + &render_material_bindings, + &render_lightmaps, + &skin_uniforms, + *frame_count, + &mut meshes_to_reextract_next_frame, + ); + } + + for entity in removed.drain(..) { + remove_mesh_input_uniform( + entity, + &mut *render_mesh_instances, + current_input_buffer, + ); + } + } + + RenderMeshInstanceGpuQueue::GpuCulling { + ref mut changed, + ref mut removed, + } => { + for (entity, mesh_instance_builder, mesh_culling_builder) in changed.drain(..) { + let Some(instance_data_index) = mesh_instance_builder.update( + entity, + &mut *render_mesh_instances, + current_input_buffer, + previous_input_buffer, + &mesh_allocator, + &mesh_material_ids, + &render_material_bindings, + &render_lightmaps, + &skin_uniforms, + *frame_count, + &mut meshes_to_reextract_next_frame, + ) else { + continue; + }; + mesh_culling_builder + .update(&mut mesh_culling_data_buffer, instance_data_index as usize); + } + + for entity in removed.drain(..) { + remove_mesh_input_uniform( + entity, + &mut *render_mesh_instances, + current_input_buffer, + ); + } + } + } + } + + // Buffers can't be empty. Make sure there's something in the previous input buffer. + previous_input_buffer.ensure_nonempty(); +} + +/// All data needed to construct a pipeline for rendering 3D meshes. +#[derive(Resource, Clone)] +pub struct MeshPipeline { + /// A reference to all the mesh pipeline view layouts. + pub view_layouts: MeshPipelineViewLayouts, + // This dummy white texture is to be used in place of optional StandardMaterial textures + pub dummy_white_gpu_image: GpuImage, + pub clustered_forward_buffer_binding_type: BufferBindingType, + pub mesh_layouts: MeshLayouts, + /// The shader asset handle. + pub shader: Handle, + /// `MeshUniform`s are stored in arrays in buffers. If storage buffers are available, they + /// are used and this will be `None`, otherwise uniform buffers will be used with batches + /// of this many `MeshUniform`s, stored at dynamic offsets within the uniform buffer. + /// Use code like this in custom shaders: + /// ```wgsl + /// ##ifdef PER_OBJECT_BUFFER_BATCH_SIZE + /// @group(1) @binding(0) var mesh: array; + /// ##else + /// @group(1) @binding(0) var mesh: array; + /// ##endif // PER_OBJECT_BUFFER_BATCH_SIZE + /// ``` + pub per_object_buffer_batch_size: Option, + + /// Whether binding arrays (a.k.a. bindless textures) are usable on the + /// current render device. + /// + /// This affects whether reflection probes can be used. + pub binding_arrays_are_usable: bool, + + /// Whether clustered decals are usable on the current render device. + pub clustered_decals_are_usable: bool, + + /// Whether skins will use uniform buffers on account of storage buffers + /// being unavailable on this platform. + pub skins_use_uniform_buffers: bool, +} + +impl FromWorld for MeshPipeline { + fn from_world(world: &mut World) -> Self { + let shader = load_embedded_asset!(world, "mesh.wgsl"); + let mut system_state: SystemState<( + Res, + Res, + Res, + Res, + Res, + )> = SystemState::new(world); + let (render_device, render_adapter, default_sampler, render_queue, view_layouts) = + system_state.get_mut(world); + + let clustered_forward_buffer_binding_type = render_device + .get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); + + // A 1x1x1 'all 1.0' texture to use as a dummy texture to use in place of optional StandardMaterial textures + let dummy_white_gpu_image = { + let image = Image::default(); + let texture = render_device.create_texture(&image.texture_descriptor); + let sampler = match image.sampler { + ImageSampler::Default => (**default_sampler).clone(), + ImageSampler::Descriptor(ref descriptor) => { + render_device.create_sampler(&descriptor.as_wgpu()) + } + }; + + if let Ok(format_size) = image.texture_descriptor.format.pixel_size() { + render_queue.write_texture( + texture.as_image_copy(), + image.data.as_ref().expect("Image was created without data"), + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(image.width() * format_size as u32), + rows_per_image: None, + }, + image.texture_descriptor.size, + ); + } + + let texture_view = texture.create_view(&TextureViewDescriptor::default()); + GpuImage { + texture, + texture_view, + texture_format: image.texture_descriptor.format, + sampler, + size: image.texture_descriptor.size, + mip_level_count: image.texture_descriptor.mip_level_count, + } + }; + + MeshPipeline { + view_layouts: view_layouts.clone(), + clustered_forward_buffer_binding_type, + dummy_white_gpu_image, + mesh_layouts: MeshLayouts::new(&render_device, &render_adapter), + shader, + per_object_buffer_batch_size: GpuArrayBuffer::::batch_size(&render_device), + binding_arrays_are_usable: binding_arrays_are_usable(&render_device, &render_adapter), + clustered_decals_are_usable: decal::clustered::clustered_decals_are_usable( + &render_device, + &render_adapter, + ), + skins_use_uniform_buffers: skins_use_uniform_buffers(&render_device), + } + } +} + +impl MeshPipeline { + pub fn get_image_texture<'a>( + &'a self, + gpu_images: &'a RenderAssets, + handle_option: &Option>, + ) -> Option<(&'a TextureView, &'a Sampler)> { + if let Some(handle) = handle_option { + let gpu_image = gpu_images.get(handle)?; + Some((&gpu_image.texture_view, &gpu_image.sampler)) + } else { + Some(( + &self.dummy_white_gpu_image.texture_view, + &self.dummy_white_gpu_image.sampler, + )) + } + } + + pub fn get_view_layout( + &self, + layout_key: MeshPipelineViewLayoutKey, + ) -> &MeshPipelineViewLayout { + self.view_layouts.get_view_layout(layout_key) + } +} + +impl GetBatchData for MeshPipeline { + type Param = ( + SRes, + SRes, + SRes>, + SRes, + SRes, + ); + // The material bind group ID, the mesh ID, and the lightmap ID, + // respectively. + type CompareData = ( + MaterialBindGroupIndex, + AssetId, + Option, + ); + + type BufferData = MeshUniform; + + fn get_batch_data( + (mesh_instances, lightmaps, _, mesh_allocator, skin_uniforms): &SystemParamItem< + Self::Param, + >, + (_entity, main_entity): (Entity, MainEntity), + ) -> Option<(Self::BufferData, Option)> { + let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else { + error!( + "`get_batch_data` should never be called in GPU mesh uniform \ + building mode" + ); + return None; + }; + let mesh_instance = mesh_instances.get(&main_entity)?; + let first_vertex_index = + match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) { + Some(mesh_vertex_slice) => mesh_vertex_slice.range.start, + None => 0, + }; + let maybe_lightmap = lightmaps.render_lightmaps.get(&main_entity); + + let current_skin_index = skin_uniforms.skin_index(main_entity); + let material_bind_group_index = mesh_instance.material_bindings_index; + + Some(( + MeshUniform::new( + &mesh_instance.transforms, + first_vertex_index, + material_bind_group_index.slot, + maybe_lightmap.map(|lightmap| (lightmap.slot_index, lightmap.uv_rect)), + current_skin_index, + Some(mesh_instance.tag), + ), + mesh_instance.should_batch().then_some(( + material_bind_group_index.group, + mesh_instance.mesh_asset_id, + maybe_lightmap.map(|lightmap| lightmap.slab_index), + )), + )) + } +} + +impl GetFullBatchData for MeshPipeline { + type BufferInputData = MeshInputUniform; + + fn get_index_and_compare_data( + (mesh_instances, lightmaps, _, _, _): &SystemParamItem, + main_entity: MainEntity, + ) -> Option<(NonMaxU32, Option)> { + // This should only be called during GPU building. + let RenderMeshInstances::GpuBuilding(ref mesh_instances) = **mesh_instances else { + error!( + "`get_index_and_compare_data` should never be called in CPU mesh uniform building \ + mode" + ); + return None; + }; + + let mesh_instance = mesh_instances.get(&main_entity)?; + let maybe_lightmap = lightmaps.render_lightmaps.get(&main_entity); + + Some(( + mesh_instance.current_uniform_index, + mesh_instance.should_batch().then_some(( + mesh_instance.material_bindings_index.group, + mesh_instance.mesh_asset_id, + maybe_lightmap.map(|lightmap| lightmap.slab_index), + )), + )) + } + + fn get_binned_batch_data( + (mesh_instances, lightmaps, _, mesh_allocator, skin_uniforms): &SystemParamItem< + Self::Param, + >, + main_entity: MainEntity, + ) -> Option { + let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else { + error!( + "`get_binned_batch_data` should never be called in GPU mesh uniform building mode" + ); + return None; + }; + let mesh_instance = mesh_instances.get(&main_entity)?; + let first_vertex_index = + match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) { + Some(mesh_vertex_slice) => mesh_vertex_slice.range.start, + None => 0, + }; + let maybe_lightmap = lightmaps.render_lightmaps.get(&main_entity); + + let current_skin_index = skin_uniforms.skin_index(main_entity); + + Some(MeshUniform::new( + &mesh_instance.transforms, + first_vertex_index, + mesh_instance.material_bindings_index.slot, + maybe_lightmap.map(|lightmap| (lightmap.slot_index, lightmap.uv_rect)), + current_skin_index, + Some(mesh_instance.tag), + )) + } + + fn get_binned_index( + (mesh_instances, _, _, _, _): &SystemParamItem, + main_entity: MainEntity, + ) -> Option { + // This should only be called during GPU building. + let RenderMeshInstances::GpuBuilding(ref mesh_instances) = **mesh_instances else { + error!( + "`get_binned_index` should never be called in CPU mesh uniform \ + building mode" + ); + return None; + }; + + mesh_instances + .get(&main_entity) + .map(|entity| entity.current_uniform_index) + } + + fn write_batch_indirect_parameters_metadata( + indexed: bool, + base_output_index: u32, + batch_set_index: Option, + phase_indirect_parameters_buffers: &mut UntypedPhaseIndirectParametersBuffers, + indirect_parameters_offset: u32, + ) { + let indirect_parameters = IndirectParametersCpuMetadata { + base_output_index, + batch_set_index: match batch_set_index { + Some(batch_set_index) => u32::from(batch_set_index), + None => !0, + }, + }; + + if indexed { + phase_indirect_parameters_buffers + .indexed + .set(indirect_parameters_offset, indirect_parameters); + } else { + phase_indirect_parameters_buffers + .non_indexed + .set(indirect_parameters_offset, indirect_parameters); + } + } +} + +bitflags::bitflags! { + #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)] + #[repr(transparent)] + // NOTE: Apparently quadro drivers support up to 64x MSAA. + /// MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. + pub struct MeshPipelineKey: u64 { + // Nothing + const NONE = 0; + + // Inherited bits + const MORPH_TARGETS = BaseMeshPipelineKey::MORPH_TARGETS.bits(); + + // Flag bits + const HDR = 1 << 0; + const TONEMAP_IN_SHADER = 1 << 1; + const DEBAND_DITHER = 1 << 2; + const DEPTH_PREPASS = 1 << 3; + const NORMAL_PREPASS = 1 << 4; + const DEFERRED_PREPASS = 1 << 5; + const MOTION_VECTOR_PREPASS = 1 << 6; + const MAY_DISCARD = 1 << 7; // Guards shader codepaths that may discard, allowing early depth tests in most cases + // See: https://www.khronos.org/opengl/wiki/Early_Fragment_Test + const ENVIRONMENT_MAP = 1 << 8; + const SCREEN_SPACE_AMBIENT_OCCLUSION = 1 << 9; + const UNCLIPPED_DEPTH_ORTHO = 1 << 10; // Disables depth clipping for use with directional light shadow views + // Emulated via fragment shader depth on hardware that doesn't support it natively + // See: https://www.w3.org/TR/webgpu/#depth-clipping and https://therealmjp.github.io/posts/shadow-maps/#disabling-z-clipping + const TEMPORAL_JITTER = 1 << 11; + const READS_VIEW_TRANSMISSION_TEXTURE = 1 << 12; + const LIGHTMAPPED = 1 << 13; + const LIGHTMAP_BICUBIC_SAMPLING = 1 << 14; + const IRRADIANCE_VOLUME = 1 << 15; + const VISIBILITY_RANGE_DITHER = 1 << 16; + const SCREEN_SPACE_REFLECTIONS = 1 << 17; + const HAS_PREVIOUS_SKIN = 1 << 18; + const HAS_PREVIOUS_MORPH = 1 << 19; + const OIT_ENABLED = 1 << 20; + const DISTANCE_FOG = 1 << 21; + const LAST_FLAG = Self::DISTANCE_FOG.bits(); + + // Bitfields + const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; + const BLEND_RESERVED_BITS = Self::BLEND_MASK_BITS << Self::BLEND_SHIFT_BITS; // ← Bitmask reserving bits for the blend state + const BLEND_OPAQUE = 0 << Self::BLEND_SHIFT_BITS; // ← Values are just sequential within the mask + const BLEND_PREMULTIPLIED_ALPHA = 1 << Self::BLEND_SHIFT_BITS; // ← As blend states is on 3 bits, it can range from 0 to 7 + const BLEND_MULTIPLY = 2 << Self::BLEND_SHIFT_BITS; // ← See `BLEND_MASK_BITS` for the number of bits available + const BLEND_ALPHA = 3 << Self::BLEND_SHIFT_BITS; // + const BLEND_ALPHA_TO_COVERAGE = 4 << Self::BLEND_SHIFT_BITS; // ← We still have room for three more values without adding more bits + const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_NONE = 0 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_REINHARD = 1 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_REINHARD_LUMINANCE = 2 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_ACES_FITTED = 3 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_AGX = 4 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM = 5 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_TONY_MC_MAPFACE = 6 << Self::TONEMAP_METHOD_SHIFT_BITS; + const TONEMAP_METHOD_BLENDER_FILMIC = 7 << Self::TONEMAP_METHOD_SHIFT_BITS; + const SHADOW_FILTER_METHOD_RESERVED_BITS = Self::SHADOW_FILTER_METHOD_MASK_BITS << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; + const SHADOW_FILTER_METHOD_HARDWARE_2X2 = 0 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; + const SHADOW_FILTER_METHOD_GAUSSIAN = 1 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; + const SHADOW_FILTER_METHOD_TEMPORAL = 2 << Self::SHADOW_FILTER_METHOD_SHIFT_BITS; + const VIEW_PROJECTION_RESERVED_BITS = Self::VIEW_PROJECTION_MASK_BITS << Self::VIEW_PROJECTION_SHIFT_BITS; + const VIEW_PROJECTION_NONSTANDARD = 0 << Self::VIEW_PROJECTION_SHIFT_BITS; + const VIEW_PROJECTION_PERSPECTIVE = 1 << Self::VIEW_PROJECTION_SHIFT_BITS; + const VIEW_PROJECTION_ORTHOGRAPHIC = 2 << Self::VIEW_PROJECTION_SHIFT_BITS; + const VIEW_PROJECTION_RESERVED = 3 << Self::VIEW_PROJECTION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_RESERVED_BITS = Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_MASK_BITS << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW = 0 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_MEDIUM = 1 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_HIGH = 2 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_ULTRA = 3 << Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS; + const ALL_RESERVED_BITS = + Self::BLEND_RESERVED_BITS.bits() | + Self::MSAA_RESERVED_BITS.bits() | + Self::TONEMAP_METHOD_RESERVED_BITS.bits() | + Self::SHADOW_FILTER_METHOD_RESERVED_BITS.bits() | + Self::VIEW_PROJECTION_RESERVED_BITS.bits() | + Self::SCREEN_SPACE_SPECULAR_TRANSMISSION_RESERVED_BITS.bits(); + } +} + +impl MeshPipelineKey { + const MSAA_MASK_BITS: u64 = 0b111; + const MSAA_SHIFT_BITS: u64 = Self::LAST_FLAG.bits().trailing_zeros() as u64 + 1; + + const BLEND_MASK_BITS: u64 = 0b111; + const BLEND_SHIFT_BITS: u64 = Self::MSAA_MASK_BITS.count_ones() as u64 + Self::MSAA_SHIFT_BITS; + + const TONEMAP_METHOD_MASK_BITS: u64 = 0b111; + const TONEMAP_METHOD_SHIFT_BITS: u64 = + Self::BLEND_MASK_BITS.count_ones() as u64 + Self::BLEND_SHIFT_BITS; + + const SHADOW_FILTER_METHOD_MASK_BITS: u64 = 0b11; + const SHADOW_FILTER_METHOD_SHIFT_BITS: u64 = + Self::TONEMAP_METHOD_MASK_BITS.count_ones() as u64 + Self::TONEMAP_METHOD_SHIFT_BITS; + + const VIEW_PROJECTION_MASK_BITS: u64 = 0b11; + const VIEW_PROJECTION_SHIFT_BITS: u64 = Self::SHADOW_FILTER_METHOD_MASK_BITS.count_ones() + as u64 + + Self::SHADOW_FILTER_METHOD_SHIFT_BITS; + + const SCREEN_SPACE_SPECULAR_TRANSMISSION_MASK_BITS: u64 = 0b11; + const SCREEN_SPACE_SPECULAR_TRANSMISSION_SHIFT_BITS: u64 = + Self::VIEW_PROJECTION_MASK_BITS.count_ones() as u64 + Self::VIEW_PROJECTION_SHIFT_BITS; + + pub fn from_msaa_samples(msaa_samples: u32) -> Self { + let msaa_bits = + (msaa_samples.trailing_zeros() as u64 & Self::MSAA_MASK_BITS) << Self::MSAA_SHIFT_BITS; + Self::from_bits_retain(msaa_bits) + } + + pub fn from_hdr(hdr: bool) -> Self { + if hdr { + MeshPipelineKey::HDR + } else { + MeshPipelineKey::NONE + } + } + + pub fn msaa_samples(&self) -> u32 { + 1 << ((self.bits() >> Self::MSAA_SHIFT_BITS) & Self::MSAA_MASK_BITS) + } + + pub fn from_primitive_topology(primitive_topology: PrimitiveTopology) -> Self { + let primitive_topology_bits = ((primitive_topology as u64) + & BaseMeshPipelineKey::PRIMITIVE_TOPOLOGY_MASK_BITS) + << BaseMeshPipelineKey::PRIMITIVE_TOPOLOGY_SHIFT_BITS; + Self::from_bits_retain(primitive_topology_bits) + } + + pub fn primitive_topology(&self) -> PrimitiveTopology { + let primitive_topology_bits = (self.bits() + >> BaseMeshPipelineKey::PRIMITIVE_TOPOLOGY_SHIFT_BITS) + & BaseMeshPipelineKey::PRIMITIVE_TOPOLOGY_MASK_BITS; + match primitive_topology_bits { + x if x == PrimitiveTopology::PointList as u64 => PrimitiveTopology::PointList, + x if x == PrimitiveTopology::LineList as u64 => PrimitiveTopology::LineList, + x if x == PrimitiveTopology::LineStrip as u64 => PrimitiveTopology::LineStrip, + x if x == PrimitiveTopology::TriangleList as u64 => PrimitiveTopology::TriangleList, + x if x == PrimitiveTopology::TriangleStrip as u64 => PrimitiveTopology::TriangleStrip, + _ => PrimitiveTopology::default(), + } + } +} + +// Ensure that we didn't overflow the number of bits available in `MeshPipelineKey`. +const_assert_eq!( + (((MeshPipelineKey::LAST_FLAG.bits() << 1) - 1) | MeshPipelineKey::ALL_RESERVED_BITS.bits()) + & BaseMeshPipelineKey::all().bits(), + 0 +); + +// Ensure that the reserved bits don't overlap with the topology bits +const_assert_eq!( + (BaseMeshPipelineKey::PRIMITIVE_TOPOLOGY_MASK_BITS + << BaseMeshPipelineKey::PRIMITIVE_TOPOLOGY_SHIFT_BITS) + & MeshPipelineKey::ALL_RESERVED_BITS.bits(), + 0 +); + +fn is_skinned(layout: &MeshVertexBufferLayoutRef) -> bool { + layout.0.contains(Mesh::ATTRIBUTE_JOINT_INDEX) + && layout.0.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT) +} +pub fn setup_morph_and_skinning_defs( + mesh_layouts: &MeshLayouts, + layout: &MeshVertexBufferLayoutRef, + offset: u32, + key: &MeshPipelineKey, + shader_defs: &mut Vec, + vertex_attributes: &mut Vec, + skins_use_uniform_buffers: bool, +) -> BindGroupLayout { + let is_morphed = key.intersects(MeshPipelineKey::MORPH_TARGETS); + let is_lightmapped = key.intersects(MeshPipelineKey::LIGHTMAPPED); + let motion_vector_prepass = key.intersects(MeshPipelineKey::MOTION_VECTOR_PREPASS); + + if skins_use_uniform_buffers { + shader_defs.push("SKINS_USE_UNIFORM_BUFFERS".into()); + } + + let mut add_skin_data = || { + shader_defs.push("SKINNED".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_INDEX.at_shader_location(offset)); + vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_WEIGHT.at_shader_location(offset + 1)); + }; + + match ( + is_skinned(layout), + is_morphed, + is_lightmapped, + motion_vector_prepass, + ) { + (true, false, _, true) => { + add_skin_data(); + mesh_layouts.skinned_motion.clone() + } + (true, false, _, false) => { + add_skin_data(); + mesh_layouts.skinned.clone() + } + (true, true, _, true) => { + add_skin_data(); + shader_defs.push("MORPH_TARGETS".into()); + mesh_layouts.morphed_skinned_motion.clone() + } + (true, true, _, false) => { + add_skin_data(); + shader_defs.push("MORPH_TARGETS".into()); + mesh_layouts.morphed_skinned.clone() + } + (false, true, _, true) => { + shader_defs.push("MORPH_TARGETS".into()); + mesh_layouts.morphed_motion.clone() + } + (false, true, _, false) => { + shader_defs.push("MORPH_TARGETS".into()); + mesh_layouts.morphed.clone() + } + (false, false, true, _) => mesh_layouts.lightmapped.clone(), + (false, false, false, _) => mesh_layouts.model_only.clone(), + } +} + +impl SpecializedMeshPipeline for MeshPipeline { + type Key = MeshPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + let mut shader_defs = Vec::new(); + let mut vertex_attributes = Vec::new(); + + // Let the shader code know that it's running in a mesh pipeline. + shader_defs.push("MESH_PIPELINE".into()); + + shader_defs.push("VERTEX_OUTPUT_INSTANCE_INDEX".into()); + + if layout.0.contains(Mesh::ATTRIBUTE_POSITION) { + shader_defs.push("VERTEX_POSITIONS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_NORMAL) { + shader_defs.push("VERTEX_NORMALS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(1)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_UV_0) { + shader_defs.push("VERTEX_UVS".into()); + shader_defs.push("VERTEX_UVS_A".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(2)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_UV_1) { + shader_defs.push("VERTEX_UVS".into()); + shader_defs.push("VERTEX_UVS_B".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_UV_1.at_shader_location(3)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_TANGENT) { + shader_defs.push("VERTEX_TANGENTS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(4)); + } + + if layout.0.contains(Mesh::ATTRIBUTE_COLOR) { + shader_defs.push("VERTEX_COLORS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(5)); + } + + if cfg!(feature = "pbr_transmission_textures") { + shader_defs.push("PBR_TRANSMISSION_TEXTURES_SUPPORTED".into()); + } + if cfg!(feature = "pbr_multi_layer_material_textures") { + shader_defs.push("PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED".into()); + } + if cfg!(feature = "pbr_anisotropy_texture") { + shader_defs.push("PBR_ANISOTROPY_TEXTURE_SUPPORTED".into()); + } + if cfg!(feature = "pbr_specular_textures") { + shader_defs.push("PBR_SPECULAR_TEXTURES_SUPPORTED".into()); + } + + let bind_group_layout = self.get_view_layout(key.into()); + let mut bind_group_layout = vec![ + bind_group_layout.main_layout.clone(), + bind_group_layout.binding_array_layout.clone(), + ]; + + if key.msaa_samples() > 1 { + shader_defs.push("MULTISAMPLED".into()); + }; + + bind_group_layout.push(setup_morph_and_skinning_defs( + &self.mesh_layouts, + layout, + 6, + &key, + &mut shader_defs, + &mut vertex_attributes, + self.skins_use_uniform_buffers, + )); + + if key.contains(MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION) { + shader_defs.push("SCREEN_SPACE_AMBIENT_OCCLUSION".into()); + } + + let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?; + + let (label, blend, depth_write_enabled); + let pass = key.intersection(MeshPipelineKey::BLEND_RESERVED_BITS); + let (mut is_opaque, mut alpha_to_coverage_enabled) = (false, false); + if key.contains(MeshPipelineKey::OIT_ENABLED) && pass == MeshPipelineKey::BLEND_ALPHA { + label = "oit_mesh_pipeline".into(); + // TODO tail blending would need alpha blending + blend = None; + shader_defs.push("OIT_ENABLED".into()); + // TODO it should be possible to use this to combine MSAA and OIT + // alpha_to_coverage_enabled = true; + depth_write_enabled = false; + } else if pass == MeshPipelineKey::BLEND_ALPHA { + label = "alpha_blend_mesh_pipeline".into(); + blend = Some(BlendState::ALPHA_BLENDING); + // For the transparent pass, fragments that are closer will be alpha blended + // but their depth is not written to the depth buffer + depth_write_enabled = false; + } else if pass == MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA { + label = "premultiplied_alpha_mesh_pipeline".into(); + blend = Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING); + shader_defs.push("PREMULTIPLY_ALPHA".into()); + shader_defs.push("BLEND_PREMULTIPLIED_ALPHA".into()); + // For the transparent pass, fragments that are closer will be alpha blended + // but their depth is not written to the depth buffer + depth_write_enabled = false; + } else if pass == MeshPipelineKey::BLEND_MULTIPLY { + label = "multiply_mesh_pipeline".into(); + blend = Some(BlendState { + color: BlendComponent { + src_factor: BlendFactor::Dst, + dst_factor: BlendFactor::OneMinusSrcAlpha, + operation: BlendOperation::Add, + }, + alpha: BlendComponent::OVER, + }); + shader_defs.push("PREMULTIPLY_ALPHA".into()); + shader_defs.push("BLEND_MULTIPLY".into()); + // For the multiply pass, fragments that are closer will be alpha blended + // but their depth is not written to the depth buffer + depth_write_enabled = false; + } else if pass == MeshPipelineKey::BLEND_ALPHA_TO_COVERAGE { + label = "alpha_to_coverage_mesh_pipeline".into(); + // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases + blend = None; + // For the opaque and alpha mask passes, fragments that are closer will replace + // the current fragment value in the output and the depth is written to the + // depth buffer + depth_write_enabled = true; + is_opaque = !key.contains(MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE); + alpha_to_coverage_enabled = true; + shader_defs.push("ALPHA_TO_COVERAGE".into()); + } else { + label = "opaque_mesh_pipeline".into(); + // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases + blend = None; + // For the opaque and alpha mask passes, fragments that are closer will replace + // the current fragment value in the output and the depth is written to the + // depth buffer + depth_write_enabled = true; + is_opaque = !key.contains(MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE); + } + + if key.contains(MeshPipelineKey::NORMAL_PREPASS) { + shader_defs.push("NORMAL_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::DEPTH_PREPASS) { + shader_defs.push("DEPTH_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + shader_defs.push("MOTION_VECTOR_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::HAS_PREVIOUS_SKIN) { + shader_defs.push("HAS_PREVIOUS_SKIN".into()); + } + + if key.contains(MeshPipelineKey::HAS_PREVIOUS_MORPH) { + shader_defs.push("HAS_PREVIOUS_MORPH".into()); + } + + if key.contains(MeshPipelineKey::DEFERRED_PREPASS) { + shader_defs.push("DEFERRED_PREPASS".into()); + } + + if key.contains(MeshPipelineKey::NORMAL_PREPASS) && key.msaa_samples() == 1 && is_opaque { + shader_defs.push("LOAD_PREPASS_NORMALS".into()); + } + + let view_projection = key.intersection(MeshPipelineKey::VIEW_PROJECTION_RESERVED_BITS); + if view_projection == MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD { + shader_defs.push("VIEW_PROJECTION_NONSTANDARD".into()); + } else if view_projection == MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE { + shader_defs.push("VIEW_PROJECTION_PERSPECTIVE".into()); + } else if view_projection == MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC { + shader_defs.push("VIEW_PROJECTION_ORTHOGRAPHIC".into()); + } + + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + shader_defs.push("WEBGL2".into()); + + #[cfg(feature = "experimental_pbr_pcss")] + shader_defs.push("PCSS_SAMPLERS_AVAILABLE".into()); + + if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) { + shader_defs.push("TONEMAP_IN_SHADER".into()); + shader_defs.push(ShaderDefVal::UInt( + "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(), + TONEMAPPING_LUT_TEXTURE_BINDING_INDEX, + )); + shader_defs.push(ShaderDefVal::UInt( + "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(), + TONEMAPPING_LUT_SAMPLER_BINDING_INDEX, + )); + + let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS); + + if method == MeshPipelineKey::TONEMAP_METHOD_NONE { + shader_defs.push("TONEMAP_METHOD_NONE".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD { + shader_defs.push("TONEMAP_METHOD_REINHARD".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE { + shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_ACES_FITTED { + shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_AGX { + shader_defs.push("TONEMAP_METHOD_AGX".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM { + shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_BLENDER_FILMIC { + shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into()); + } else if method == MeshPipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE { + shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into()); + } + + // Debanding is tied to tonemapping in the shader, cannot run without it. + if key.contains(MeshPipelineKey::DEBAND_DITHER) { + shader_defs.push("DEBAND_DITHER".into()); + } + } + + if key.contains(MeshPipelineKey::MAY_DISCARD) { + shader_defs.push("MAY_DISCARD".into()); + } + + if key.contains(MeshPipelineKey::ENVIRONMENT_MAP) { + shader_defs.push("ENVIRONMENT_MAP".into()); + } + + if key.contains(MeshPipelineKey::IRRADIANCE_VOLUME) && IRRADIANCE_VOLUMES_ARE_USABLE { + shader_defs.push("IRRADIANCE_VOLUME".into()); + } + + if key.contains(MeshPipelineKey::LIGHTMAPPED) { + shader_defs.push("LIGHTMAP".into()); + } + if key.contains(MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING) { + shader_defs.push("LIGHTMAP_BICUBIC_SAMPLING".into()); + } + + if key.contains(MeshPipelineKey::TEMPORAL_JITTER) { + shader_defs.push("TEMPORAL_JITTER".into()); + } + + let shadow_filter_method = + key.intersection(MeshPipelineKey::SHADOW_FILTER_METHOD_RESERVED_BITS); + if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_HARDWARE_2X2 { + shader_defs.push("SHADOW_FILTER_METHOD_HARDWARE_2X2".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_GAUSSIAN { + shader_defs.push("SHADOW_FILTER_METHOD_GAUSSIAN".into()); + } else if shadow_filter_method == MeshPipelineKey::SHADOW_FILTER_METHOD_TEMPORAL { + shader_defs.push("SHADOW_FILTER_METHOD_TEMPORAL".into()); + } + + let blur_quality = + key.intersection(MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_RESERVED_BITS); + + shader_defs.push(ShaderDefVal::Int( + "SCREEN_SPACE_SPECULAR_TRANSMISSION_BLUR_TAPS".into(), + match blur_quality { + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_LOW => 4, + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_MEDIUM => 8, + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_HIGH => 16, + MeshPipelineKey::SCREEN_SPACE_SPECULAR_TRANSMISSION_ULTRA => 32, + _ => unreachable!(), // Not possible, since the mask is 2 bits, and we've covered all 4 cases + }, + )); + + if key.contains(MeshPipelineKey::VISIBILITY_RANGE_DITHER) { + shader_defs.push("VISIBILITY_RANGE_DITHER".into()); + } + + if key.contains(MeshPipelineKey::DISTANCE_FOG) { + shader_defs.push("DISTANCE_FOG".into()); + } + + if self.binding_arrays_are_usable { + shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into()); + shader_defs.push("MULTIPLE_LIGHTMAPS_IN_ARRAY".into()); + } + + if IRRADIANCE_VOLUMES_ARE_USABLE { + shader_defs.push("IRRADIANCE_VOLUMES_ARE_USABLE".into()); + } + + if self.clustered_decals_are_usable { + shader_defs.push("CLUSTERED_DECALS_ARE_USABLE".into()); + if cfg!(feature = "pbr_light_textures") { + shader_defs.push("LIGHT_TEXTURES".into()); + } + } + + let format = if key.contains(MeshPipelineKey::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }; + + // This is defined here so that custom shaders that use something other than + // the mesh binding from bevy_pbr::mesh_bindings can easily make use of this + // in their own shaders. + if let Some(per_object_buffer_batch_size) = self.per_object_buffer_batch_size { + shader_defs.push(ShaderDefVal::UInt( + "PER_OBJECT_BUFFER_BATCH_SIZE".into(), + per_object_buffer_batch_size, + )); + } + + Ok(RenderPipelineDescriptor { + vertex: VertexState { + shader: self.shader.clone(), + shader_defs: shader_defs.clone(), + buffers: vec![vertex_buffer_layout], + ..default() + }, + fragment: Some(FragmentState { + shader: self.shader.clone(), + shader_defs, + targets: vec![Some(ColorTargetState { + format, + blend, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + layout: bind_group_layout, + primitive: PrimitiveState { + cull_mode: Some(Face::Back), + unclipped_depth: false, + topology: key.primitive_topology(), + ..default() + }, + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + multisample: MultisampleState { + count: key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled, + }, + label: Some(label), + ..default() + }) + } +} + +/// The bind groups for meshes currently loaded. +/// +/// If GPU mesh preprocessing isn't in use, these are global to the scene. If +/// GPU mesh preprocessing is in use, these are specific to a single phase. +#[derive(Default)] +pub struct MeshPhaseBindGroups { + model_only: Option, + skinned: Option, + morph_targets: HashMap, MeshBindGroupPair>, + lightmaps: HashMap, +} + +pub struct MeshBindGroupPair { + motion_vectors: BindGroup, + no_motion_vectors: BindGroup, +} + +/// All bind groups for meshes currently loaded. +#[derive(Resource)] +pub enum MeshBindGroups { + /// The bind groups for the meshes for the entire scene, if GPU mesh + /// preprocessing isn't in use. + CpuPreprocessing(MeshPhaseBindGroups), + /// A mapping from the type ID of a phase (e.g. [`Opaque3d`]) to the mesh + /// bind groups for that phase. + GpuPreprocessing(TypeIdMap), +} + +impl MeshPhaseBindGroups { + pub fn reset(&mut self) { + self.model_only = None; + self.skinned = None; + self.morph_targets.clear(); + self.lightmaps.clear(); + } + /// Get the `BindGroup` for `RenderMesh` with given `handle_id` and lightmap + /// key `lightmap`. + pub fn get( + &self, + asset_id: AssetId, + lightmap: Option, + is_skinned: bool, + morph: bool, + motion_vectors: bool, + ) -> Option<&BindGroup> { + match (is_skinned, morph, lightmap) { + (_, true, _) => self + .morph_targets + .get(&asset_id) + .map(|bind_group_pair| bind_group_pair.get(motion_vectors)), + (true, false, _) => self + .skinned + .as_ref() + .map(|bind_group_pair| bind_group_pair.get(motion_vectors)), + (false, false, Some(lightmap_slab)) => self.lightmaps.get(&lightmap_slab), + (false, false, None) => self.model_only.as_ref(), + } + } +} + +impl MeshBindGroupPair { + fn get(&self, motion_vectors: bool) -> &BindGroup { + if motion_vectors { + &self.motion_vectors + } else { + &self.no_motion_vectors + } + } +} + +/// Creates the per-mesh bind groups for each type of mesh and each phase. +pub fn prepare_mesh_bind_groups( + mut commands: Commands, + meshes: Res>, + mesh_pipeline: Res, + render_device: Res, + cpu_batched_instance_buffer: Option< + Res>, + >, + gpu_batched_instance_buffers: Option< + Res>, + >, + skins_uniform: Res, + weights_uniform: Res, + mut render_lightmaps: ResMut, +) { + // CPU mesh preprocessing path. + if let Some(cpu_batched_instance_buffer) = cpu_batched_instance_buffer + && let Some(instance_data_binding) = cpu_batched_instance_buffer + .into_inner() + .instance_data_binding() + { + // In this path, we only have a single set of bind groups for all phases. + let cpu_preprocessing_mesh_bind_groups = prepare_mesh_bind_groups_for_phase( + instance_data_binding, + &meshes, + &mesh_pipeline, + &render_device, + &skins_uniform, + &weights_uniform, + &mut render_lightmaps, + ); + + commands.insert_resource(MeshBindGroups::CpuPreprocessing( + cpu_preprocessing_mesh_bind_groups, + )); + return; + } + + // GPU mesh preprocessing path. + if let Some(gpu_batched_instance_buffers) = gpu_batched_instance_buffers { + let mut gpu_preprocessing_mesh_bind_groups = TypeIdMap::default(); + + // Loop over each phase. + for (phase_type_id, batched_phase_instance_buffers) in + &gpu_batched_instance_buffers.phase_instance_buffers + { + let Some(instance_data_binding) = + batched_phase_instance_buffers.instance_data_binding() + else { + continue; + }; + + let mesh_phase_bind_groups = prepare_mesh_bind_groups_for_phase( + instance_data_binding, + &meshes, + &mesh_pipeline, + &render_device, + &skins_uniform, + &weights_uniform, + &mut render_lightmaps, + ); + + gpu_preprocessing_mesh_bind_groups.insert(*phase_type_id, mesh_phase_bind_groups); + } + + commands.insert_resource(MeshBindGroups::GpuPreprocessing( + gpu_preprocessing_mesh_bind_groups, + )); + } +} + +/// Creates the per-mesh bind groups for each type of mesh, for a single phase. +fn prepare_mesh_bind_groups_for_phase( + model: BindingResource, + meshes: &RenderAssets, + mesh_pipeline: &MeshPipeline, + render_device: &RenderDevice, + skins_uniform: &SkinUniforms, + weights_uniform: &MorphUniforms, + render_lightmaps: &mut RenderLightmaps, +) -> MeshPhaseBindGroups { + let layouts = &mesh_pipeline.mesh_layouts; + + // TODO: Reuse allocations. + let mut groups = MeshPhaseBindGroups { + model_only: Some(layouts.model_only(render_device, &model)), + ..default() + }; + + // Create the skinned mesh bind group with the current and previous buffers + // (the latter being for motion vector computation). + let (skin, prev_skin) = (&skins_uniform.current_buffer, &skins_uniform.prev_buffer); + groups.skinned = Some(MeshBindGroupPair { + motion_vectors: layouts.skinned_motion(render_device, &model, skin, prev_skin), + no_motion_vectors: layouts.skinned(render_device, &model, skin), + }); + + // Create the morphed bind groups just like we did for the skinned bind + // group. + if let Some(weights) = weights_uniform.current_buffer.buffer() { + let prev_weights = weights_uniform.prev_buffer.buffer().unwrap_or(weights); + for (id, gpu_mesh) in meshes.iter() { + if let Some(targets) = gpu_mesh.morph_targets.as_ref() { + let bind_group_pair = if is_skinned(&gpu_mesh.layout) { + let prev_skin = &skins_uniform.prev_buffer; + MeshBindGroupPair { + motion_vectors: layouts.morphed_skinned_motion( + render_device, + &model, + skin, + weights, + targets, + prev_skin, + prev_weights, + ), + no_motion_vectors: layouts.morphed_skinned( + render_device, + &model, + skin, + weights, + targets, + ), + } + } else { + MeshBindGroupPair { + motion_vectors: layouts.morphed_motion( + render_device, + &model, + weights, + targets, + prev_weights, + ), + no_motion_vectors: layouts.morphed(render_device, &model, weights, targets), + } + }; + groups.morph_targets.insert(id, bind_group_pair); + } + } + } + + // Create lightmap bindgroups. There will be one bindgroup for each slab. + let bindless_supported = render_lightmaps.bindless_supported; + for (lightmap_slab_id, lightmap_slab) in render_lightmaps.slabs.iter_mut().enumerate() { + groups.lightmaps.insert( + LightmapSlabIndex(NonMaxU32::new(lightmap_slab_id as u32).unwrap()), + layouts.lightmapped(render_device, &model, lightmap_slab, bindless_supported), + ); + } + + groups +} + +pub struct SetMeshViewBindGroup; +impl RenderCommand

    for SetMeshViewBindGroup { + type Param = (); + type ViewQuery = ( + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Option>, + ); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + ( + view_uniform, + view_lights, + view_fog, + view_light_probes, + view_ssr, + view_environment_map, + mesh_view_bind_group, + maybe_oit_layers_count_offset, + ): ROQueryItem<'w, '_, Self::ViewQuery>, + _entity: Option<()>, + _: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let mut offsets: SmallVec<[u32; 8]> = smallvec![ + view_uniform.offset, + view_lights.offset, + view_fog.offset, + **view_light_probes, + **view_ssr, + **view_environment_map, + ]; + if let Some(layers_count_offset) = maybe_oit_layers_count_offset { + offsets.push(layers_count_offset.offset); + } + pass.set_bind_group(I, &mesh_view_bind_group.main, &offsets); + + RenderCommandResult::Success + } +} + +pub struct SetMeshViewBindingArrayBindGroup; +impl RenderCommand

    for SetMeshViewBindingArrayBindGroup { + type Param = (); + type ViewQuery = (Read,); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + (mesh_view_bind_group,): ROQueryItem<'w, '_, Self::ViewQuery>, + _entity: Option<()>, + _: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_bind_group(I, &mesh_view_bind_group.binding_array, &[]); + + RenderCommandResult::Success + } +} + +pub struct SetMeshViewEmptyBindGroup; +impl RenderCommand

    for SetMeshViewEmptyBindGroup { + type Param = (); + type ViewQuery = (Read,); + type ItemQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + (mesh_view_bind_group,): ROQueryItem<'w, '_, Self::ViewQuery>, + _entity: Option<()>, + _: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_bind_group(I, &mesh_view_bind_group.empty, &[]); + + RenderCommandResult::Success + } +} + +pub struct SetMeshBindGroup; +impl RenderCommand

    for SetMeshBindGroup { + type Param = ( + SRes, + SRes, + SRes, + SRes, + SRes, + SRes, + ); + type ViewQuery = Has; + type ItemQuery = (); + + #[inline] + fn render<'w>( + item: &P, + has_motion_vector_prepass: bool, + _item_query: Option<()>, + ( + render_device, + bind_groups, + mesh_instances, + skin_uniforms, + morph_indices, + lightmaps, + ): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let bind_groups = bind_groups.into_inner(); + let mesh_instances = mesh_instances.into_inner(); + let skin_uniforms = skin_uniforms.into_inner(); + let morph_indices = morph_indices.into_inner(); + + let entity = &item.main_entity(); + + let Some(mesh_asset_id) = mesh_instances.mesh_asset_id(*entity) else { + return RenderCommandResult::Success; + }; + + let current_skin_byte_offset = skin_uniforms.skin_byte_offset(*entity); + let current_morph_index = morph_indices.current.get(entity); + let prev_morph_index = morph_indices.prev.get(entity); + + let is_skinned = current_skin_byte_offset.is_some(); + let is_morphed = current_morph_index.is_some(); + + let lightmap_slab_index = lightmaps + .render_lightmaps + .get(entity) + .map(|render_lightmap| render_lightmap.slab_index); + + let Some(mesh_phase_bind_groups) = (match *bind_groups { + MeshBindGroups::CpuPreprocessing(ref mesh_phase_bind_groups) => { + Some(mesh_phase_bind_groups) + } + MeshBindGroups::GpuPreprocessing(ref mesh_phase_bind_groups) => { + mesh_phase_bind_groups.get(&TypeId::of::

    ()) + } + }) else { + // This is harmless if e.g. we're rendering the `Shadow` phase and + // there weren't any shadows. + return RenderCommandResult::Success; + }; + + let Some(bind_group) = mesh_phase_bind_groups.get( + mesh_asset_id, + lightmap_slab_index, + is_skinned, + is_morphed, + has_motion_vector_prepass, + ) else { + return RenderCommandResult::Failure( + "The MeshBindGroups resource wasn't set in the render phase. \ + It should be set by the prepare_mesh_bind_group system.\n\ + This is a bevy bug! Please open an issue.", + ); + }; + + let mut dynamic_offsets: [u32; 5] = Default::default(); + let mut offset_count = 0; + if let PhaseItemExtraIndex::DynamicOffset(dynamic_offset) = item.extra_index() { + dynamic_offsets[offset_count] = dynamic_offset; + offset_count += 1; + } + if let Some(current_skin_index) = current_skin_byte_offset + && skins_use_uniform_buffers(&render_device) + { + dynamic_offsets[offset_count] = current_skin_index.byte_offset; + offset_count += 1; + } + if let Some(current_morph_index) = current_morph_index { + dynamic_offsets[offset_count] = current_morph_index.index; + offset_count += 1; + } + + // Attach motion vectors if needed. + if has_motion_vector_prepass { + // Attach the previous skin index for motion vector computation. + if skins_use_uniform_buffers(&render_device) + && let Some(current_skin_byte_offset) = current_skin_byte_offset + { + dynamic_offsets[offset_count] = current_skin_byte_offset.byte_offset; + offset_count += 1; + } + + // Attach the previous morph index for motion vector computation. If + // there isn't one, just use zero as the shader will ignore it. + if current_morph_index.is_some() { + match prev_morph_index { + Some(prev_morph_index) => { + dynamic_offsets[offset_count] = prev_morph_index.index; + } + None => dynamic_offsets[offset_count] = 0, + } + offset_count += 1; + } + } + + pass.set_bind_group(I, bind_group, &dynamic_offsets[0..offset_count]); + + RenderCommandResult::Success + } +} + +pub struct DrawMesh; +impl RenderCommand

    for DrawMesh { + type Param = ( + SRes>, + SRes, + SRes, + SRes, + SRes, + Option>, + SRes, + ); + type ViewQuery = Has; + type ItemQuery = (); + #[inline] + fn render<'w>( + item: &P, + has_preprocess_bind_group: ROQueryItem, + _item_query: Option<()>, + ( + meshes, + mesh_instances, + indirect_parameters_buffer, + pipeline_cache, + mesh_allocator, + preprocess_pipelines, + preprocessing_support, + ): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + // If we're using GPU preprocessing, then we're dependent on that + // compute shader having been run, which of course can only happen if + // it's compiled. Otherwise, our mesh instance data won't be present. + if let Some(preprocess_pipelines) = preprocess_pipelines + && (!has_preprocess_bind_group + || !preprocess_pipelines + .pipelines_are_loaded(&pipeline_cache, &preprocessing_support)) + { + return RenderCommandResult::Skip; + } + + let meshes = meshes.into_inner(); + let mesh_instances = mesh_instances.into_inner(); + let indirect_parameters_buffer = indirect_parameters_buffer.into_inner(); + let mesh_allocator = mesh_allocator.into_inner(); + + let Some(mesh_asset_id) = mesh_instances.mesh_asset_id(item.main_entity()) else { + return RenderCommandResult::Skip; + }; + let Some(gpu_mesh) = meshes.get(mesh_asset_id) else { + return RenderCommandResult::Skip; + }; + let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&mesh_asset_id) else { + return RenderCommandResult::Skip; + }; + + pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..)); + + let batch_range = item.batch_range(); + + // Draw either directly or indirectly, as appropriate. If we're in + // indirect mode, we can additionally multi-draw. (We can't multi-draw + // in direct mode because `wgpu` doesn't expose that functionality.) + match &gpu_mesh.buffer_info { + RenderMeshBufferInfo::Indexed { + index_format, + count, + } => { + let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&mesh_asset_id) + else { + return RenderCommandResult::Skip; + }; + + pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, *index_format); + + match item.extra_index() { + PhaseItemExtraIndex::None | PhaseItemExtraIndex::DynamicOffset(_) => { + pass.draw_indexed( + index_buffer_slice.range.start + ..(index_buffer_slice.range.start + *count), + vertex_buffer_slice.range.start as i32, + batch_range.clone(), + ); + } + PhaseItemExtraIndex::IndirectParametersIndex { + range: indirect_parameters_range, + batch_set_index, + } => { + // Look up the indirect parameters buffer, as well as + // the buffer we're going to use for + // `multi_draw_indexed_indirect_count` (if available). + let Some(phase_indirect_parameters_buffers) = + indirect_parameters_buffer.get(&TypeId::of::

    ()) + else { + warn!( + "Not rendering mesh because indexed indirect parameters buffer \ + wasn't present for this phase", + ); + return RenderCommandResult::Skip; + }; + let (Some(indirect_parameters_buffer), Some(batch_sets_buffer)) = ( + phase_indirect_parameters_buffers.indexed.data_buffer(), + phase_indirect_parameters_buffers + .indexed + .batch_sets_buffer(), + ) else { + warn!( + "Not rendering mesh because indexed indirect parameters buffer \ + wasn't present", + ); + return RenderCommandResult::Skip; + }; + + // Calculate the location of the indirect parameters + // within the buffer. + let indirect_parameters_offset = indirect_parameters_range.start as u64 + * size_of::() as u64; + let indirect_parameters_count = + indirect_parameters_range.end - indirect_parameters_range.start; + + // If we're using `multi_draw_indirect_count`, take the + // number of batches from the appropriate position in + // the batch sets buffer. Otherwise, supply the size of + // the batch set. + match batch_set_index { + Some(batch_set_index) => { + let count_offset = u32::from(batch_set_index) + * (size_of::() as u32); + pass.multi_draw_indexed_indirect_count( + indirect_parameters_buffer, + indirect_parameters_offset, + batch_sets_buffer, + count_offset as u64, + indirect_parameters_count, + ); + } + None => { + pass.multi_draw_indexed_indirect( + indirect_parameters_buffer, + indirect_parameters_offset, + indirect_parameters_count, + ); + } + } + } + } + } + + RenderMeshBufferInfo::NonIndexed => match item.extra_index() { + PhaseItemExtraIndex::None | PhaseItemExtraIndex::DynamicOffset(_) => { + pass.draw(vertex_buffer_slice.range, batch_range.clone()); + } + PhaseItemExtraIndex::IndirectParametersIndex { + range: indirect_parameters_range, + batch_set_index, + } => { + // Look up the indirect parameters buffer, as well as the + // buffer we're going to use for + // `multi_draw_indirect_count` (if available). + let Some(phase_indirect_parameters_buffers) = + indirect_parameters_buffer.get(&TypeId::of::

    ()) + else { + warn!( + "Not rendering mesh because non-indexed indirect parameters buffer \ + wasn't present for this phase", + ); + return RenderCommandResult::Skip; + }; + let (Some(indirect_parameters_buffer), Some(batch_sets_buffer)) = ( + phase_indirect_parameters_buffers.non_indexed.data_buffer(), + phase_indirect_parameters_buffers + .non_indexed + .batch_sets_buffer(), + ) else { + warn!( + "Not rendering mesh because non-indexed indirect parameters buffer \ + wasn't present" + ); + return RenderCommandResult::Skip; + }; + + // Calculate the location of the indirect parameters within + // the buffer. + let indirect_parameters_offset = indirect_parameters_range.start as u64 + * size_of::() as u64; + let indirect_parameters_count = + indirect_parameters_range.end - indirect_parameters_range.start; + + // If we're using `multi_draw_indirect_count`, take the + // number of batches from the appropriate position in the + // batch sets buffer. Otherwise, supply the size of the + // batch set. + match batch_set_index { + Some(batch_set_index) => { + let count_offset = + u32::from(batch_set_index) * (size_of::() as u32); + pass.multi_draw_indirect_count( + indirect_parameters_buffer, + indirect_parameters_offset, + batch_sets_buffer, + count_offset as u64, + indirect_parameters_count, + ); + } + None => { + pass.multi_draw_indirect( + indirect_parameters_buffer, + indirect_parameters_offset, + indirect_parameters_count, + ); + } + } + } + }, + } + RenderCommandResult::Success + } +} + +#[cfg(test)] +mod tests { + use super::MeshPipelineKey; + #[test] + fn mesh_key_msaa_samples() { + for i in [1, 2, 4, 8, 16, 32, 64, 128] { + assert_eq!(MeshPipelineKey::from_msaa_samples(i).msaa_samples(), i); + } + } +} diff --git a/crates/libmarathon/src/render/pbr/render/mesh.wgsl b/crates/libmarathon/src/render/pbr/render/mesh.wgsl new file mode 100644 index 0000000..9568468 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/mesh.wgsl @@ -0,0 +1,120 @@ +#import bevy_pbr::{ + mesh_bindings::mesh, + mesh_functions, + skinning, + morph::morph, + forward_io::{Vertex, VertexOutput}, + view_transformations::position_world_to_clip, +} + +#ifdef MORPH_TARGETS +fn morph_vertex(vertex_in: Vertex) -> Vertex { + var vertex = vertex_in; + let first_vertex = mesh[vertex.instance_index].first_vertex_index; + let vertex_index = vertex.index - first_vertex; + + let weight_count = bevy_pbr::morph::layer_count(); + for (var i: u32 = 0u; i < weight_count; i ++) { + let weight = bevy_pbr::morph::weight_at(i); + if weight == 0.0 { + continue; + } + vertex.position += weight * morph(vertex_index, bevy_pbr::morph::position_offset, i); +#ifdef VERTEX_NORMALS + vertex.normal += weight * morph(vertex_index, bevy_pbr::morph::normal_offset, i); +#endif +#ifdef VERTEX_TANGENTS + vertex.tangent += vec4(weight * morph(vertex_index, bevy_pbr::morph::tangent_offset, i), 0.0); +#endif + } + return vertex; +} +#endif + +@vertex +fn vertex(vertex_no_morph: Vertex) -> VertexOutput { + var out: VertexOutput; + +#ifdef MORPH_TARGETS + var vertex = morph_vertex(vertex_no_morph); +#else + var vertex = vertex_no_morph; +#endif + + let mesh_world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index); + +#ifdef SKINNED + var world_from_local = skinning::skin_model( + vertex.joint_indices, + vertex.joint_weights, + vertex_no_morph.instance_index + ); +#else + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 . + var world_from_local = mesh_world_from_local; +#endif + +#ifdef VERTEX_NORMALS +#ifdef SKINNED + out.world_normal = skinning::skin_normals(world_from_local, vertex.normal); +#else + out.world_normal = mesh_functions::mesh_normal_local_to_world( + vertex.normal, + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + vertex_no_morph.instance_index + ); +#endif +#endif + +#ifdef VERTEX_POSITIONS + out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); + out.position = position_world_to_clip(out.world_position.xyz); +#endif + +#ifdef VERTEX_UVS_A + out.uv = vertex.uv; +#endif +#ifdef VERTEX_UVS_B + out.uv_b = vertex.uv_b; +#endif + +#ifdef VERTEX_TANGENTS + out.world_tangent = mesh_functions::mesh_tangent_local_to_world( + world_from_local, + vertex.tangent, + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + vertex_no_morph.instance_index + ); +#endif + +#ifdef VERTEX_COLORS + out.color = vertex.color; +#endif + +#ifdef VERTEX_OUTPUT_INSTANCE_INDEX + // Use vertex_no_morph.instance_index instead of vertex.instance_index to work around a wgpu dx12 bug. + // See https://github.com/gfx-rs/naga/issues/2416 + out.instance_index = vertex_no_morph.instance_index; +#endif + +#ifdef VISIBILITY_RANGE_DITHER + out.visibility_range_dither = mesh_functions::get_visibility_range_dither_level( + vertex_no_morph.instance_index, mesh_world_from_local[3]); +#endif + + return out; +} + +@fragment +fn fragment( + mesh: VertexOutput, +) -> @location(0) vec4 { +#ifdef VERTEX_COLORS + return mesh.color; +#else + return vec4(1.0, 0.0, 1.0, 1.0); +#endif +} diff --git a/crates/libmarathon/src/render/pbr/render/mesh_bindings.rs b/crates/libmarathon/src/render/pbr/render/mesh_bindings.rs new file mode 100644 index 0000000..0acbb55 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/mesh_bindings.rs @@ -0,0 +1,551 @@ +//! Bind group layout related definitions for the mesh pipeline. + +use bevy_math::Mat4; +use bevy_mesh::morph::MAX_MORPH_WEIGHTS; +use crate::render::{ + render_resource::*, + renderer::{RenderAdapter, RenderDevice}, +}; + +use crate::render::pbr::{binding_arrays_are_usable, render::skin::MAX_JOINTS, LightmapSlab}; + +const MORPH_WEIGHT_SIZE: usize = size_of::(); + +/// This is used to allocate buffers. +/// The correctness of the value depends on the GPU/platform. +/// The current value is chosen because it is guaranteed to work everywhere. +/// To allow for bigger values, a check must be made for the limits +/// of the GPU at runtime, which would mean not using consts anymore. +pub const MORPH_BUFFER_SIZE: usize = MAX_MORPH_WEIGHTS * MORPH_WEIGHT_SIZE; + +const JOINT_SIZE: usize = size_of::(); +pub(crate) const JOINT_BUFFER_SIZE: usize = MAX_JOINTS * JOINT_SIZE; + +/// Individual layout entries. +mod layout_entry { + use core::num::NonZeroU32; + + use super::{JOINT_BUFFER_SIZE, MORPH_BUFFER_SIZE}; + use crate::render::pbr::{render::skin, MeshUniform, LIGHTMAPS_PER_SLAB}; + use crate::render::{ + render_resource::{ + binding_types::{ + sampler, storage_buffer_read_only_sized, texture_2d, texture_3d, + uniform_buffer_sized, + }, + BindGroupLayoutEntryBuilder, BufferSize, GpuArrayBuffer, SamplerBindingType, + ShaderStages, TextureSampleType, + }, + renderer::RenderDevice, + }; + + pub(super) fn model(render_device: &RenderDevice) -> BindGroupLayoutEntryBuilder { + GpuArrayBuffer::::binding_layout(render_device) + .visibility(ShaderStages::VERTEX_FRAGMENT) + } + pub(super) fn skinning(render_device: &RenderDevice) -> BindGroupLayoutEntryBuilder { + // If we can use storage buffers, do so. Otherwise, fall back to uniform + // buffers. + let size = BufferSize::new(JOINT_BUFFER_SIZE as u64); + if skin::skins_use_uniform_buffers(render_device) { + uniform_buffer_sized(true, size) + } else { + storage_buffer_read_only_sized(false, size) + } + } + pub(super) fn weights() -> BindGroupLayoutEntryBuilder { + uniform_buffer_sized(true, BufferSize::new(MORPH_BUFFER_SIZE as u64)) + } + pub(super) fn targets() -> BindGroupLayoutEntryBuilder { + texture_3d(TextureSampleType::Float { filterable: false }) + } + pub(super) fn lightmaps_texture_view() -> BindGroupLayoutEntryBuilder { + texture_2d(TextureSampleType::Float { filterable: true }).visibility(ShaderStages::FRAGMENT) + } + pub(super) fn lightmaps_sampler() -> BindGroupLayoutEntryBuilder { + sampler(SamplerBindingType::Filtering).visibility(ShaderStages::FRAGMENT) + } + pub(super) fn lightmaps_texture_view_array() -> BindGroupLayoutEntryBuilder { + texture_2d(TextureSampleType::Float { filterable: true }) + .visibility(ShaderStages::FRAGMENT) + .count(NonZeroU32::new(LIGHTMAPS_PER_SLAB as u32).unwrap()) + } + pub(super) fn lightmaps_sampler_array() -> BindGroupLayoutEntryBuilder { + sampler(SamplerBindingType::Filtering) + .visibility(ShaderStages::FRAGMENT) + .count(NonZeroU32::new(LIGHTMAPS_PER_SLAB as u32).unwrap()) + } +} + +/// Individual [`BindGroupEntry`] +/// for bind groups. +mod entry { + use crate::render::pbr::render::skin; + + use super::{JOINT_BUFFER_SIZE, MORPH_BUFFER_SIZE}; + use crate::render::{ + render_resource::{ + BindGroupEntry, BindingResource, Buffer, BufferBinding, BufferSize, Sampler, + TextureView, WgpuSampler, WgpuTextureView, + }, + renderer::RenderDevice, + }; + + fn entry(binding: u32, size: Option, buffer: &Buffer) -> BindGroupEntry<'_> { + BindGroupEntry { + binding, + resource: BindingResource::Buffer(BufferBinding { + buffer, + offset: 0, + size: size.map(|size| BufferSize::new(size).unwrap()), + }), + } + } + pub(super) fn model(binding: u32, resource: BindingResource) -> BindGroupEntry { + BindGroupEntry { binding, resource } + } + pub(super) fn skinning<'a>( + render_device: &RenderDevice, + binding: u32, + buffer: &'a Buffer, + ) -> BindGroupEntry<'a> { + let size = if skin::skins_use_uniform_buffers(render_device) { + Some(JOINT_BUFFER_SIZE as u64) + } else { + None + }; + entry(binding, size, buffer) + } + pub(super) fn weights(binding: u32, buffer: &Buffer) -> BindGroupEntry<'_> { + entry(binding, Some(MORPH_BUFFER_SIZE as u64), buffer) + } + pub(super) fn targets(binding: u32, texture: &TextureView) -> BindGroupEntry<'_> { + BindGroupEntry { + binding, + resource: BindingResource::TextureView(texture), + } + } + pub(super) fn lightmaps_texture_view( + binding: u32, + texture: &TextureView, + ) -> BindGroupEntry<'_> { + BindGroupEntry { + binding, + resource: BindingResource::TextureView(texture), + } + } + pub(super) fn lightmaps_sampler(binding: u32, sampler: &Sampler) -> BindGroupEntry<'_> { + BindGroupEntry { + binding, + resource: BindingResource::Sampler(sampler), + } + } + pub(super) fn lightmaps_texture_view_array<'a>( + binding: u32, + textures: &'a [&'a WgpuTextureView], + ) -> BindGroupEntry<'a> { + BindGroupEntry { + binding, + resource: BindingResource::TextureViewArray(textures), + } + } + pub(super) fn lightmaps_sampler_array<'a>( + binding: u32, + samplers: &'a [&'a WgpuSampler], + ) -> BindGroupEntry<'a> { + BindGroupEntry { + binding, + resource: BindingResource::SamplerArray(samplers), + } + } +} + +/// All possible [`BindGroupLayout`]s in bevy's default mesh shader (`mesh.wgsl`). +#[derive(Clone)] +pub struct MeshLayouts { + /// The mesh model uniform (transform) and nothing else. + pub model_only: BindGroupLayout, + + /// Includes the lightmap texture and uniform. + pub lightmapped: BindGroupLayout, + + /// Also includes the uniform for skinning + pub skinned: BindGroupLayout, + + /// Like [`MeshLayouts::skinned`], but includes slots for the previous + /// frame's joint matrices, so that we can compute motion vectors. + pub skinned_motion: BindGroupLayout, + + /// Also includes the uniform and [`MorphAttributes`] for morph targets. + /// + /// [`MorphAttributes`]: bevy_mesh::morph::MorphAttributes + pub morphed: BindGroupLayout, + + /// Like [`MeshLayouts::morphed`], but includes a slot for the previous + /// frame's morph weights, so that we can compute motion vectors. + pub morphed_motion: BindGroupLayout, + + /// Also includes both uniforms for skinning and morph targets, also the + /// morph target [`MorphAttributes`] binding. + /// + /// [`MorphAttributes`]: bevy_mesh::morph::MorphAttributes + pub morphed_skinned: BindGroupLayout, + + /// Like [`MeshLayouts::morphed_skinned`], but includes slots for the + /// previous frame's joint matrices and morph weights, so that we can + /// compute motion vectors. + pub morphed_skinned_motion: BindGroupLayout, +} + +impl MeshLayouts { + /// Prepare the layouts used by the default bevy [`Mesh`]. + /// + /// [`Mesh`]: bevy_mesh::Mesh + pub fn new(render_device: &RenderDevice, render_adapter: &RenderAdapter) -> Self { + MeshLayouts { + model_only: Self::model_only_layout(render_device), + lightmapped: Self::lightmapped_layout(render_device, render_adapter), + skinned: Self::skinned_layout(render_device), + skinned_motion: Self::skinned_motion_layout(render_device), + morphed: Self::morphed_layout(render_device), + morphed_motion: Self::morphed_motion_layout(render_device), + morphed_skinned: Self::morphed_skinned_layout(render_device), + morphed_skinned_motion: Self::morphed_skinned_motion_layout(render_device), + } + } + + // ---------- create individual BindGroupLayouts ---------- + + fn model_only_layout(render_device: &RenderDevice) -> BindGroupLayout { + render_device.create_bind_group_layout( + "mesh_layout", + &BindGroupLayoutEntries::single( + ShaderStages::empty(), + layout_entry::model(render_device), + ), + ) + } + + /// Creates the layout for skinned meshes. + fn skinned_layout(render_device: &RenderDevice) -> BindGroupLayout { + render_device.create_bind_group_layout( + "skinned_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(render_device)), + // The current frame's joint matrix buffer. + (1, layout_entry::skinning(render_device)), + ), + ), + ) + } + + /// Creates the layout for skinned meshes with the infrastructure to compute + /// motion vectors. + fn skinned_motion_layout(render_device: &RenderDevice) -> BindGroupLayout { + render_device.create_bind_group_layout( + "skinned_motion_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(render_device)), + // The current frame's joint matrix buffer. + (1, layout_entry::skinning(render_device)), + // The previous frame's joint matrix buffer. + (6, layout_entry::skinning(render_device)), + ), + ), + ) + } + + /// Creates the layout for meshes with morph targets. + fn morphed_layout(render_device: &RenderDevice) -> BindGroupLayout { + render_device.create_bind_group_layout( + "morphed_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(render_device)), + // The current frame's morph weight buffer. + (2, layout_entry::weights()), + (3, layout_entry::targets()), + ), + ), + ) + } + + /// Creates the layout for meshes with morph targets and the infrastructure + /// to compute motion vectors. + fn morphed_motion_layout(render_device: &RenderDevice) -> BindGroupLayout { + render_device.create_bind_group_layout( + "morphed_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(render_device)), + // The current frame's morph weight buffer. + (2, layout_entry::weights()), + (3, layout_entry::targets()), + // The previous frame's morph weight buffer. + (7, layout_entry::weights()), + ), + ), + ) + } + + /// Creates the bind group layout for meshes with both skins and morph + /// targets. + fn morphed_skinned_layout(render_device: &RenderDevice) -> BindGroupLayout { + render_device.create_bind_group_layout( + "morphed_skinned_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(render_device)), + // The current frame's joint matrix buffer. + (1, layout_entry::skinning(render_device)), + // The current frame's morph weight buffer. + (2, layout_entry::weights()), + (3, layout_entry::targets()), + ), + ), + ) + } + + /// Creates the bind group layout for meshes with both skins and morph + /// targets, in addition to the infrastructure to compute motion vectors. + fn morphed_skinned_motion_layout(render_device: &RenderDevice) -> BindGroupLayout { + render_device.create_bind_group_layout( + "morphed_skinned_motion_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(render_device)), + // The current frame's joint matrix buffer. + (1, layout_entry::skinning(render_device)), + // The current frame's morph weight buffer. + (2, layout_entry::weights()), + (3, layout_entry::targets()), + // The previous frame's joint matrix buffer. + (6, layout_entry::skinning(render_device)), + // The previous frame's morph weight buffer. + (7, layout_entry::weights()), + ), + ), + ) + } + + fn lightmapped_layout( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, + ) -> BindGroupLayout { + if binding_arrays_are_usable(render_device, render_adapter) { + render_device.create_bind_group_layout( + "lightmapped_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(render_device)), + (4, layout_entry::lightmaps_texture_view_array()), + (5, layout_entry::lightmaps_sampler_array()), + ), + ), + ) + } else { + render_device.create_bind_group_layout( + "lightmapped_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(render_device)), + (4, layout_entry::lightmaps_texture_view()), + (5, layout_entry::lightmaps_sampler()), + ), + ), + ) + } + } + + // ---------- BindGroup methods ---------- + + pub fn model_only(&self, render_device: &RenderDevice, model: &BindingResource) -> BindGroup { + render_device.create_bind_group( + "model_only_mesh_bind_group", + &self.model_only, + &[entry::model(0, model.clone())], + ) + } + + pub fn lightmapped( + &self, + render_device: &RenderDevice, + model: &BindingResource, + lightmap_slab: &LightmapSlab, + bindless_lightmaps: bool, + ) -> BindGroup { + if bindless_lightmaps { + let (texture_views, samplers) = lightmap_slab.build_binding_arrays(); + render_device.create_bind_group( + "lightmapped_mesh_bind_group", + &self.lightmapped, + &[ + entry::model(0, model.clone()), + entry::lightmaps_texture_view_array(4, &texture_views), + entry::lightmaps_sampler_array(5, &samplers), + ], + ) + } else { + let (texture_view, sampler) = lightmap_slab.bindings_for_first_lightmap(); + render_device.create_bind_group( + "lightmapped_mesh_bind_group", + &self.lightmapped, + &[ + entry::model(0, model.clone()), + entry::lightmaps_texture_view(4, texture_view), + entry::lightmaps_sampler(5, sampler), + ], + ) + } + } + + /// Creates the bind group for skinned meshes with no morph targets. + pub fn skinned( + &self, + render_device: &RenderDevice, + model: &BindingResource, + current_skin: &Buffer, + ) -> BindGroup { + render_device.create_bind_group( + "skinned_mesh_bind_group", + &self.skinned, + &[ + entry::model(0, model.clone()), + entry::skinning(render_device, 1, current_skin), + ], + ) + } + + /// Creates the bind group for skinned meshes with no morph targets, with + /// the infrastructure to compute motion vectors. + /// + /// `current_skin` is the buffer of joint matrices for this frame; + /// `prev_skin` is the buffer for the previous frame. The latter is used for + /// motion vector computation. If there is no such applicable buffer, + /// `current_skin` and `prev_skin` will reference the same buffer. + pub fn skinned_motion( + &self, + render_device: &RenderDevice, + model: &BindingResource, + current_skin: &Buffer, + prev_skin: &Buffer, + ) -> BindGroup { + render_device.create_bind_group( + "skinned_motion_mesh_bind_group", + &self.skinned_motion, + &[ + entry::model(0, model.clone()), + entry::skinning(render_device, 1, current_skin), + entry::skinning(render_device, 6, prev_skin), + ], + ) + } + + /// Creates the bind group for meshes with no skins but morph targets. + pub fn morphed( + &self, + render_device: &RenderDevice, + model: &BindingResource, + current_weights: &Buffer, + targets: &TextureView, + ) -> BindGroup { + render_device.create_bind_group( + "morphed_mesh_bind_group", + &self.morphed, + &[ + entry::model(0, model.clone()), + entry::weights(2, current_weights), + entry::targets(3, targets), + ], + ) + } + + /// Creates the bind group for meshes with no skins but morph targets, in + /// addition to the infrastructure to compute motion vectors. + /// + /// `current_weights` is the buffer of morph weights for this frame; + /// `prev_weights` is the buffer for the previous frame. The latter is used + /// for motion vector computation. If there is no such applicable buffer, + /// `current_weights` and `prev_weights` will reference the same buffer. + pub fn morphed_motion( + &self, + render_device: &RenderDevice, + model: &BindingResource, + current_weights: &Buffer, + targets: &TextureView, + prev_weights: &Buffer, + ) -> BindGroup { + render_device.create_bind_group( + "morphed_motion_mesh_bind_group", + &self.morphed_motion, + &[ + entry::model(0, model.clone()), + entry::weights(2, current_weights), + entry::targets(3, targets), + entry::weights(7, prev_weights), + ], + ) + } + + /// Creates the bind group for meshes with skins and morph targets. + pub fn morphed_skinned( + &self, + render_device: &RenderDevice, + model: &BindingResource, + current_skin: &Buffer, + current_weights: &Buffer, + targets: &TextureView, + ) -> BindGroup { + render_device.create_bind_group( + "morphed_skinned_mesh_bind_group", + &self.morphed_skinned, + &[ + entry::model(0, model.clone()), + entry::skinning(render_device, 1, current_skin), + entry::weights(2, current_weights), + entry::targets(3, targets), + ], + ) + } + + /// Creates the bind group for meshes with skins and morph targets, in + /// addition to the infrastructure to compute motion vectors. + /// + /// See the documentation for [`MeshLayouts::skinned_motion`] and + /// [`MeshLayouts::morphed_motion`] above for more information about the + /// `current_skin`, `prev_skin`, `current_weights`, and `prev_weights` + /// buffers. + pub fn morphed_skinned_motion( + &self, + render_device: &RenderDevice, + model: &BindingResource, + current_skin: &Buffer, + current_weights: &Buffer, + targets: &TextureView, + prev_skin: &Buffer, + prev_weights: &Buffer, + ) -> BindGroup { + render_device.create_bind_group( + "morphed_skinned_motion_mesh_bind_group", + &self.morphed_skinned_motion, + &[ + entry::model(0, model.clone()), + entry::skinning(render_device, 1, current_skin), + entry::weights(2, current_weights), + entry::targets(3, targets), + entry::skinning(render_device, 6, prev_skin), + entry::weights(7, prev_weights), + ], + ) + } +} diff --git a/crates/libmarathon/src/render/pbr/render/mesh_bindings.wgsl b/crates/libmarathon/src/render/pbr/render/mesh_bindings.wgsl new file mode 100644 index 0000000..6e78dc4 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/mesh_bindings.wgsl @@ -0,0 +1,11 @@ +#define_import_path bevy_pbr::mesh_bindings + +#import bevy_pbr::mesh_types::Mesh + +#ifndef MESHLET_MESH_MATERIAL_PASS +#ifdef PER_OBJECT_BUFFER_BATCH_SIZE +@group(2) @binding(0) var mesh: array; +#else +@group(2) @binding(0) var mesh: array; +#endif // PER_OBJECT_BUFFER_BATCH_SIZE +#endif // MESHLET_MESH_MATERIAL_PASS diff --git a/crates/libmarathon/src/render/pbr/render/mesh_functions.wgsl b/crates/libmarathon/src/render/pbr/render/mesh_functions.wgsl new file mode 100644 index 0000000..6d4c53a --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/mesh_functions.wgsl @@ -0,0 +1,168 @@ +#define_import_path bevy_pbr::mesh_functions + +#import bevy_pbr::{ + mesh_view_bindings::{ + view, + visibility_ranges, + VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE + }, + mesh_bindings::mesh, + mesh_types::MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT, + view_transformations::position_world_to_clip, +} +#import bevy_render::maths::{affine3_to_square, mat2x4_f32_to_mat3x3_unpack} + +#ifndef MESHLET_MESH_MATERIAL_PASS + +fn get_world_from_local(instance_index: u32) -> mat4x4 { + return affine3_to_square(mesh[instance_index].world_from_local); +} + +fn get_previous_world_from_local(instance_index: u32) -> mat4x4 { + return affine3_to_square(mesh[instance_index].previous_world_from_local); +} + +fn get_local_from_world(instance_index: u32) -> mat4x4 { + // the model matrix is translation * rotation * scale + // the inverse is then scale^-1 * rotation ^-1 * translation^-1 + // the 3x3 matrix only contains the information for the rotation and scale + let inverse_model_3x3 = transpose(mat2x4_f32_to_mat3x3_unpack( + mesh[instance_index].local_from_world_transpose_a, + mesh[instance_index].local_from_world_transpose_b, + )); + // construct scale^-1 * rotation^-1 from the 3x3 + let inverse_model_4x4_no_trans = mat4x4( + vec4(inverse_model_3x3[0], 0.0), + vec4(inverse_model_3x3[1], 0.0), + vec4(inverse_model_3x3[2], 0.0), + vec4(0.0,0.0,0.0,1.0) + ); + // we can get translation^-1 by negating the translation of the model + let model = get_world_from_local(instance_index); + let inverse_model_4x4_only_trans = mat4x4( + vec4(1.0,0.0,0.0,0.0), + vec4(0.0,1.0,0.0,0.0), + vec4(0.0,0.0,1.0,0.0), + vec4(-model[3].xyz, 1.0) + ); + + return inverse_model_4x4_no_trans * inverse_model_4x4_only_trans; +} + +#endif // MESHLET_MESH_MATERIAL_PASS + +fn mesh_position_local_to_world(world_from_local: mat4x4, vertex_position: vec4) -> vec4 { + return world_from_local * vertex_position; +} + +// NOTE: The intermediate world_position assignment is important +// for precision purposes when using the 'equals' depth comparison +// function. +fn mesh_position_local_to_clip(world_from_local: mat4x4, vertex_position: vec4) -> vec4 { + let world_position = mesh_position_local_to_world(world_from_local, vertex_position); + return position_world_to_clip(world_position.xyz); +} + +#ifndef MESHLET_MESH_MATERIAL_PASS + +fn mesh_normal_local_to_world(vertex_normal: vec3, instance_index: u32) -> vec3 { + // NOTE: The mikktspace method of normal mapping requires that the world normal is + // re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents + // and normal maps so that the exact inverse process is applied when shading. Blender, Unity, + // Unreal Engine, Godot, and more all use the mikktspace method. + // We only skip normalization for invalid normals so that they don't become NaN. + // Do not change this code unless you really know what you are doing. + // http://www.mikktspace.com/ + if any(vertex_normal != vec3(0.0)) { + return normalize( + mat2x4_f32_to_mat3x3_unpack( + mesh[instance_index].local_from_world_transpose_a, + mesh[instance_index].local_from_world_transpose_b, + ) * vertex_normal + ); + } else { + return vertex_normal; + } +} + +#endif // MESHLET_MESH_MATERIAL_PASS + +// Calculates the sign of the determinant of the 3x3 model matrix based on a +// mesh flag +fn sign_determinant_model_3x3m(mesh_flags: u32) -> f32 { + // bool(u32) is false if 0u else true + // f32(bool) is 1.0 if true else 0.0 + // * 2.0 - 1.0 remaps 0.0 or 1.0 to -1.0 or 1.0 respectively + return f32(bool(mesh_flags & MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT)) * 2.0 - 1.0; +} + +#ifndef MESHLET_MESH_MATERIAL_PASS + +fn mesh_tangent_local_to_world(world_from_local: mat4x4, vertex_tangent: vec4, instance_index: u32) -> vec4 { + // NOTE: The mikktspace method of normal mapping requires that the world tangent is + // re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents + // and normal maps so that the exact inverse process is applied when shading. Blender, Unity, + // Unreal Engine, Godot, and more all use the mikktspace method. + // We only skip normalization for invalid tangents so that they don't become NaN. + // Do not change this code unless you really know what you are doing. + // http://www.mikktspace.com/ + if any(vertex_tangent != vec4(0.0)) { + return vec4( + normalize( + mat3x3( + world_from_local[0].xyz, + world_from_local[1].xyz, + world_from_local[2].xyz, + ) * vertex_tangent.xyz + ), + // NOTE: Multiplying by the sign of the determinant of the 3x3 model matrix accounts for + // situations such as negative scaling. + vertex_tangent.w * sign_determinant_model_3x3m(mesh[instance_index].flags) + ); + } else { + return vertex_tangent; + } +} + +#endif // MESHLET_MESH_MATERIAL_PASS + +// Returns an appropriate dither level for the current mesh instance. +// +// This looks up the LOD range in the `visibility_ranges` table and compares the +// camera distance to determine the dithering level. +#ifdef VISIBILITY_RANGE_DITHER +fn get_visibility_range_dither_level(instance_index: u32, world_position: vec4) -> i32 { +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + // If we're using a storage buffer, then the length is variable. + let visibility_buffer_array_len = arrayLength(&visibility_ranges); +#else // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + // If we're using a uniform buffer, then the length is constant + let visibility_buffer_array_len = VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE; +#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + + let visibility_buffer_index = mesh[instance_index].flags & 0xffffu; + if (visibility_buffer_index > visibility_buffer_array_len) { + return -16; + } + + let lod_range = visibility_ranges[visibility_buffer_index]; + let camera_distance = length(view.world_position.xyz - world_position.xyz); + + // This encodes the following mapping: + // + // `lod_range.` x y z w camera distance + // ←───────┼────────┼────────┼────────┼────────→ + // LOD level -16 -16 0 0 16 16 LOD level + let offset = select(-16, 0, camera_distance >= lod_range.z); + let bounds = select(lod_range.xy, lod_range.zw, camera_distance >= lod_range.z); + let level = i32(round((camera_distance - bounds.x) / (bounds.y - bounds.x) * 16.0)); + return offset + clamp(level, 0, 16); +} +#endif + + +#ifndef MESHLET_MESH_MATERIAL_PASS +fn get_tag(instance_index: u32) -> u32 { + return mesh[instance_index].tag; +} +#endif diff --git a/crates/libmarathon/src/render/pbr/render/mesh_preprocess.wgsl b/crates/libmarathon/src/render/pbr/render/mesh_preprocess.wgsl new file mode 100644 index 0000000..543b328 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/mesh_preprocess.wgsl @@ -0,0 +1,373 @@ +// GPU mesh transforming and culling. +// +// This is a compute shader that expands each `MeshInputUniform` out to a full +// `MeshUniform` for each view before rendering. (Thus `MeshInputUniform` and +// `MeshUniform` are in a 1:N relationship.) It runs in parallel for all meshes +// for all views. As part of this process, the shader gathers each mesh's +// transform on the previous frame and writes it into the `MeshUniform` so that +// TAA works. It also performs frustum culling and occlusion culling, if +// requested. +// +// If occlusion culling is on, this shader runs twice: once to prepare the +// meshes that were visible last frame, and once to prepare the meshes that +// weren't visible last frame but became visible this frame. The two invocations +// are known as *early mesh preprocessing* and *late mesh preprocessing* +// respectively. + +#import bevy_pbr::mesh_preprocess_types::{ + IndirectParametersCpuMetadata, IndirectParametersGpuMetadata, MeshInput +} +#import bevy_pbr::mesh_types::{Mesh, MESH_FLAGS_NO_FRUSTUM_CULLING_BIT} +#import bevy_pbr::mesh_view_bindings::view +#import bevy_pbr::occlusion_culling +#import bevy_pbr::prepass_bindings::previous_view_uniforms +#import bevy_pbr::view_transformations::{ + position_world_to_ndc, position_world_to_view, ndc_to_uv, view_z_to_depth_ndc, + position_world_to_prev_ndc, position_world_to_prev_view, prev_view_z_to_depth_ndc +} +#import bevy_render::maths +#import bevy_render::view::View + +// Information about each mesh instance needed to cull it on GPU. +// +// At the moment, this just consists of its axis-aligned bounding box (AABB). +struct MeshCullingData { + // The 3D center of the AABB in model space, padded with an extra unused + // float value. + aabb_center: vec4, + // The 3D extents of the AABB in model space, divided by two, padded with + // an extra unused float value. + aabb_half_extents: vec4, +} + +// One invocation of this compute shader: i.e. one mesh instance in a view. +struct PreprocessWorkItem { + // The index of the `MeshInput` in the `current_input` buffer that we read + // from. + input_index: u32, + // In direct mode, the index of the `Mesh` in `output` that we write to. In + // indirect mode, the index of the `IndirectParameters` in + // `indirect_parameters` that we write to. + output_or_indirect_parameters_index: u32, +} + +// The parameters for the indirect compute dispatch for the late mesh +// preprocessing phase. +struct LatePreprocessWorkItemIndirectParameters { + // The number of workgroups we're going to dispatch. + // + // This value should always be equal to `ceil(work_item_count / 64)`. + dispatch_x: atomic, + // The number of workgroups in the Y direction; always 1. + dispatch_y: u32, + // The number of workgroups in the Z direction; always 1. + dispatch_z: u32, + // The precise number of work items. + work_item_count: atomic, + // Padding. + // + // This isn't the usual structure padding; it's needed because some hardware + // requires indirect compute dispatch parameters to be aligned on 64-byte + // boundaries. + pad: vec4, +} + +// These have to be in a structure because of Naga limitations on DX12. +struct PushConstants { + // The offset into the `late_preprocess_work_item_indirect_parameters` + // buffer. + late_preprocess_work_item_indirect_offset: u32, +} + +// The current frame's `MeshInput`. +@group(0) @binding(3) var current_input: array; +// The `MeshInput` values from the previous frame. +@group(0) @binding(4) var previous_input: array; +// Indices into the `MeshInput` buffer. +// +// There may be many indices that map to the same `MeshInput`. +@group(0) @binding(5) var work_items: array; +// The output array of `Mesh`es. +@group(0) @binding(6) var output: array; + +#ifdef INDIRECT +// The array of indirect parameters for drawcalls. +@group(0) @binding(7) var indirect_parameters_cpu_metadata: + array; + +@group(0) @binding(8) var indirect_parameters_gpu_metadata: + array; +#endif + +#ifdef FRUSTUM_CULLING +// Data needed to cull the meshes. +// +// At the moment, this consists only of AABBs. +@group(0) @binding(9) var mesh_culling_data: array; +#endif // FRUSTUM_CULLING + +#ifdef OCCLUSION_CULLING +@group(0) @binding(10) var depth_pyramid: texture_2d; + +#ifdef EARLY_PHASE +@group(0) @binding(11) var late_preprocess_work_items: + array; +#endif // EARLY_PHASE + +@group(0) @binding(12) var late_preprocess_work_item_indirect_parameters: + array; + +var push_constants: PushConstants; +#endif // OCCLUSION_CULLING + +#ifdef FRUSTUM_CULLING +// Returns true if the view frustum intersects an oriented bounding box (OBB). +// +// `aabb_center.w` should be 1.0. +fn view_frustum_intersects_obb( + world_from_local: mat4x4, + aabb_center: vec4, + aabb_half_extents: vec3, +) -> bool { + + for (var i = 0; i < 5; i += 1) { + // Calculate relative radius of the sphere associated with this plane. + let plane_normal = view.frustum[i]; + let relative_radius = dot( + abs( + vec3( + dot(plane_normal.xyz, world_from_local[0].xyz), + dot(plane_normal.xyz, world_from_local[1].xyz), + dot(plane_normal.xyz, world_from_local[2].xyz), + ) + ), + aabb_half_extents + ); + + // Check the frustum plane. + if (!maths::sphere_intersects_plane_half_space( + plane_normal, aabb_center, relative_radius)) { + return false; + } + } + + return true; +} +#endif + +@compute +@workgroup_size(64) +fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Figure out our instance index. If this thread doesn't correspond to any + // index, bail. + let instance_index = global_invocation_id.x; + +#ifdef LATE_PHASE + if (instance_index >= atomicLoad(&late_preprocess_work_item_indirect_parameters[ + push_constants.late_preprocess_work_item_indirect_offset].work_item_count)) { + return; + } +#else // LATE_PHASE + if (instance_index >= arrayLength(&work_items)) { + return; + } +#endif + + // Unpack the work item. + let input_index = work_items[instance_index].input_index; +#ifdef INDIRECT + let indirect_parameters_index = work_items[instance_index].output_or_indirect_parameters_index; + + // If we're the first mesh instance in this batch, write the index of our + // `MeshInput` into the appropriate slot so that the indirect parameters + // building shader can access it. +#ifndef LATE_PHASE + if (instance_index == 0u || work_items[instance_index - 1].output_or_indirect_parameters_index != indirect_parameters_index) { + indirect_parameters_gpu_metadata[indirect_parameters_index].mesh_index = input_index; + } +#endif // LATE_PHASE + +#else // INDIRECT + let mesh_output_index = work_items[instance_index].output_or_indirect_parameters_index; +#endif // INDIRECT + + // Unpack the input matrix. + let world_from_local_affine_transpose = current_input[input_index].world_from_local; + let world_from_local = maths::affine3_to_square(world_from_local_affine_transpose); + + // Frustum cull if necessary. +#ifdef FRUSTUM_CULLING + if ((current_input[input_index].flags & MESH_FLAGS_NO_FRUSTUM_CULLING_BIT) == 0u) { + let aabb_center = mesh_culling_data[input_index].aabb_center.xyz; + let aabb_half_extents = mesh_culling_data[input_index].aabb_half_extents.xyz; + + // Do an OBB-based frustum cull. + let model_center = world_from_local * vec4(aabb_center, 1.0); + if (!view_frustum_intersects_obb(world_from_local, model_center, aabb_half_extents)) { + return; + } + } +#endif + + // See whether the `MeshInputUniform` was updated on this frame. If it + // wasn't, then we know the transforms of this mesh must be identical to + // those on the previous frame, and therefore we don't need to access the + // `previous_input_index` (in fact, we can't; that index are only valid for + // one frame and will be invalid). + let timestamp = current_input[input_index].timestamp; + let mesh_changed_this_frame = timestamp == view.frame_count; + + // Look up the previous model matrix, if it could have been. + let previous_input_index = current_input[input_index].previous_input_index; + var previous_world_from_local_affine_transpose: mat3x4; + if (mesh_changed_this_frame && previous_input_index != 0xffffffffu) { + previous_world_from_local_affine_transpose = + previous_input[previous_input_index].world_from_local; + } else { + previous_world_from_local_affine_transpose = world_from_local_affine_transpose; + } + let previous_world_from_local = + maths::affine3_to_square(previous_world_from_local_affine_transpose); + + // Occlusion cull if necessary. This is done by calculating the screen-space + // axis-aligned bounding box (AABB) of the mesh and testing it against the + // appropriate level of the depth pyramid (a.k.a. hierarchical Z-buffer). If + // no part of the AABB is in front of the corresponding pixel quad in the + // hierarchical Z-buffer, then this mesh must be occluded, and we can skip + // rendering it. +#ifdef OCCLUSION_CULLING + let aabb_center = mesh_culling_data[input_index].aabb_center.xyz; + let aabb_half_extents = mesh_culling_data[input_index].aabb_half_extents.xyz; + + // Initialize the AABB and the maximum depth. + let infinity = bitcast(0x7f800000u); + let neg_infinity = bitcast(0xff800000u); + var aabb = vec4(infinity, infinity, neg_infinity, neg_infinity); + var max_depth_view = neg_infinity; + + // Build up the AABB by taking each corner of this mesh's OBB, transforming + // it, and updating the AABB and depth accordingly. + for (var i = 0u; i < 8u; i += 1u) { + let local_pos = aabb_center + select( + vec3(-1.0), + vec3(1.0), + vec3((i & 1) != 0, (i & 2) != 0, (i & 4) != 0) + ) * aabb_half_extents; + +#ifdef EARLY_PHASE + // If we're in the early phase, we're testing against the last frame's + // depth buffer, so we need to use the previous frame's transform. + let prev_world_pos = (previous_world_from_local * vec4(local_pos, 1.0)).xyz; + let view_pos = position_world_to_prev_view(prev_world_pos); + let ndc_pos = position_world_to_prev_ndc(prev_world_pos); +#else // EARLY_PHASE + // Otherwise, if this is the late phase, we use the current frame's + // transform. + let world_pos = (world_from_local * vec4(local_pos, 1.0)).xyz; + let view_pos = position_world_to_view(world_pos); + let ndc_pos = position_world_to_ndc(world_pos); +#endif // EARLY_PHASE + + let uv_pos = ndc_to_uv(ndc_pos.xy); + + // Update the AABB and maximum view-space depth. + aabb = vec4(min(aabb.xy, uv_pos), max(aabb.zw, uv_pos)); + max_depth_view = max(max_depth_view, view_pos.z); + } + + // Clip to the near plane to avoid the NDC depth becoming negative. +#ifdef EARLY_PHASE + max_depth_view = min(-previous_view_uniforms.clip_from_view[3][2], max_depth_view); +#else // EARLY_PHASE + max_depth_view = min(-view.clip_from_view[3][2], max_depth_view); +#endif // EARLY_PHASE + + // Figure out the depth of the occluder, and compare it to our own depth. + + let aabb_pixel_size = occlusion_culling::get_aabb_size_in_pixels(aabb, depth_pyramid); + let occluder_depth_ndc = + occlusion_culling::get_occluder_depth(aabb, aabb_pixel_size, depth_pyramid); + +#ifdef EARLY_PHASE + let max_depth_ndc = prev_view_z_to_depth_ndc(max_depth_view); +#else // EARLY_PHASE + let max_depth_ndc = view_z_to_depth_ndc(max_depth_view); +#endif + + // Are we culled out? + if (max_depth_ndc < occluder_depth_ndc) { +#ifdef EARLY_PHASE + // If this is the early phase, we need to make a note of this mesh so + // that we examine it again in the late phase, so that we handle the + // case in which a mesh that was invisible last frame became visible in + // this frame. + let output_work_item_index = atomicAdd(&late_preprocess_work_item_indirect_parameters[ + push_constants.late_preprocess_work_item_indirect_offset].work_item_count, 1u); + if (output_work_item_index % 64u == 0u) { + // Our workgroup size is 64, and the indirect parameters for the + // late mesh preprocessing phase are counted in workgroups, so if + // we're the first thread in this workgroup, bump the workgroup + // count. + atomicAdd(&late_preprocess_work_item_indirect_parameters[ + push_constants.late_preprocess_work_item_indirect_offset].dispatch_x, 1u); + } + + // Enqueue a work item for the late prepass phase. + late_preprocess_work_items[output_work_item_index].input_index = input_index; + late_preprocess_work_items[output_work_item_index].output_or_indirect_parameters_index = + indirect_parameters_index; +#endif // EARLY_PHASE + // This mesh is culled. Skip it. + return; + } +#endif // OCCLUSION_CULLING + + // Calculate inverse transpose. + let local_from_world_transpose = transpose(maths::inverse_affine3(transpose( + world_from_local_affine_transpose))); + + // Pack inverse transpose. + let local_from_world_transpose_a = mat2x4( + vec4(local_from_world_transpose[0].xyz, local_from_world_transpose[1].x), + vec4(local_from_world_transpose[1].yz, local_from_world_transpose[2].xy)); + let local_from_world_transpose_b = local_from_world_transpose[2].z; + + // Figure out the output index. In indirect mode, this involves bumping the + // instance index in the indirect parameters metadata, which + // `build_indirect_params.wgsl` will use to generate the actual indirect + // parameters. Otherwise, this index was directly supplied to us. +#ifdef INDIRECT +#ifdef LATE_PHASE + let batch_output_index = atomicLoad( + &indirect_parameters_gpu_metadata[indirect_parameters_index].early_instance_count + ) + atomicAdd( + &indirect_parameters_gpu_metadata[indirect_parameters_index].late_instance_count, + 1u + ); +#else // LATE_PHASE + let batch_output_index = atomicAdd( + &indirect_parameters_gpu_metadata[indirect_parameters_index].early_instance_count, + 1u + ); +#endif // LATE_PHASE + + let mesh_output_index = + indirect_parameters_cpu_metadata[indirect_parameters_index].base_output_index + + batch_output_index; + +#endif // INDIRECT + + // Write the output. + output[mesh_output_index].world_from_local = world_from_local_affine_transpose; + output[mesh_output_index].previous_world_from_local = + previous_world_from_local_affine_transpose; + output[mesh_output_index].local_from_world_transpose_a = local_from_world_transpose_a; + output[mesh_output_index].local_from_world_transpose_b = local_from_world_transpose_b; + output[mesh_output_index].flags = current_input[input_index].flags; + output[mesh_output_index].lightmap_uv_rect = current_input[input_index].lightmap_uv_rect; + output[mesh_output_index].first_vertex_index = current_input[input_index].first_vertex_index; + output[mesh_output_index].current_skin_index = current_input[input_index].current_skin_index; + output[mesh_output_index].material_and_lightmap_bind_group_slot = + current_input[input_index].material_and_lightmap_bind_group_slot; + output[mesh_output_index].tag = current_input[input_index].tag; +} diff --git a/crates/libmarathon/src/render/pbr/render/mesh_types.wgsl b/crates/libmarathon/src/render/pbr/render/mesh_types.wgsl new file mode 100644 index 0000000..4c85192 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/mesh_types.wgsl @@ -0,0 +1,47 @@ +#define_import_path bevy_pbr::mesh_types + +struct Mesh { + // Affine 4x3 matrices transposed to 3x4 + // Use bevy_render::maths::affine3_to_square to unpack + world_from_local: mat3x4, + previous_world_from_local: mat3x4, + // 3x3 matrix packed in mat2x4 and f32 as: + // [0].xyz, [1].x, + // [1].yz, [2].xy + // [2].z + // Use bevy_pbr::mesh_functions::mat2x4_f32_to_mat3x3_unpack to unpack + local_from_world_transpose_a: mat2x4, + local_from_world_transpose_b: f32, + // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. + flags: u32, + lightmap_uv_rect: vec2, + // The index of the mesh's first vertex in the vertex buffer. + first_vertex_index: u32, + current_skin_index: u32, + // Low 16 bits: index of the material inside the bind group data. + // High 16 bits: index of the lightmap in the binding array. + material_and_lightmap_bind_group_slot: u32, + // User supplied index to identify the mesh instance + tag: u32, + pad: u32, +}; + +#ifdef SKINNED +struct SkinnedMesh { + data: array, 256u>, +}; +#endif + +#ifdef MORPH_TARGETS +struct MorphWeights { + weights: array, 16u>, // 16 = 64 / 4 (64 = MAX_MORPH_WEIGHTS) +}; +#endif + +// [2^0, 2^16) +const MESH_FLAGS_VISIBILITY_RANGE_INDEX_BITS: u32 = (1u << 16u) - 1u; +const MESH_FLAGS_NO_FRUSTUM_CULLING_BIT: u32 = 1u << 28u; +const MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u << 29u; +const MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT: u32 = 1u << 30u; +// if the flag is set, the sign is positive, else it is negative +const MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT: u32 = 1u << 31u; diff --git a/crates/libmarathon/src/render/pbr/render/mesh_view_bindings.rs b/crates/libmarathon/src/render/pbr/render/mesh_view_bindings.rs new file mode 100644 index 0000000..e9e990f --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/mesh_view_bindings.rs @@ -0,0 +1,818 @@ +use std::sync::Arc; +use crate::render::{ + core_3d::ViewTransmissionTexture, + oit::{resolve::is_oit_supported, OitBuffers, OrderIndependentTransparencySettings}, + prepass::ViewPrepassTextures, + tonemapping::{ + get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts, + }, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::Has, + resource::Resource, + system::{Commands, Query, Res}, + world::{FromWorld, World}, +}; +use bevy_image::BevyDefault as _; +use bevy_light::{EnvironmentMapLight, IrradianceVolume}; +use bevy_math::Vec4; +use crate::render::{ + globals::{GlobalsBuffer, GlobalsUniform}, + render_asset::RenderAssets, + render_resource::{binding_types::*, *}, + renderer::{RenderAdapter, RenderDevice}, + texture::{FallbackImage, FallbackImageMsaa, FallbackImageZero, GpuImage}, + view::{ + Msaa, RenderVisibilityRanges, ViewUniform, ViewUniforms, + VISIBILITY_RANGES_STORAGE_BUFFER_COUNT, + }, +}; +use core::{array, num::NonZero}; + +use crate::render::pbr::{ + decal::{ + self, + clustered::{ + DecalsBuffer, RenderClusteredDecals, RenderViewClusteredDecalBindGroupEntries, + }, + }, + environment_map::{self, RenderViewEnvironmentMapBindGroupEntries}, + irradiance_volume::{ + self, RenderViewIrradianceVolumeBindGroupEntries, IRRADIANCE_VOLUMES_ARE_USABLE, + }, + prepass, EnvironmentMapUniformBuffer, FogMeta, GlobalClusterableObjectMeta, + GpuClusterableObjects, GpuFog, GpuLights, LightMeta, LightProbesBuffer, LightProbesUniform, + MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionResources, + ScreenSpaceReflectionsBuffer, ScreenSpaceReflectionsUniform, ShadowSamplers, + ViewClusterBindings, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, +}; + +#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] +use crate::render::render_resource::binding_types::texture_cube; + +#[cfg(debug_assertions)] +use {crate::render::pbr::MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES, bevy_utils::once, tracing::warn}; + +#[derive(Clone)] +pub struct MeshPipelineViewLayout { + pub main_layout: BindGroupLayout, + pub binding_array_layout: BindGroupLayout, + pub empty_layout: BindGroupLayout, + + #[cfg(debug_assertions)] + pub texture_count: usize, +} + +bitflags::bitflags! { + /// A key that uniquely identifies a [`MeshPipelineViewLayout`]. + /// + /// Used to generate all possible layouts for the mesh pipeline in [`generate_view_layouts`], + /// so special care must be taken to not add too many flags, as the number of possible layouts + /// will grow exponentially. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + #[repr(transparent)] + pub struct MeshPipelineViewLayoutKey: u32 { + const MULTISAMPLED = 1 << 0; + const DEPTH_PREPASS = 1 << 1; + const NORMAL_PREPASS = 1 << 2; + const MOTION_VECTOR_PREPASS = 1 << 3; + const DEFERRED_PREPASS = 1 << 4; + const OIT_ENABLED = 1 << 5; + } +} + +impl MeshPipelineViewLayoutKey { + // The number of possible layouts + pub const COUNT: usize = Self::all().bits() as usize + 1; + + /// Builds a unique label for each layout based on the flags + pub fn label(&self) -> String { + use MeshPipelineViewLayoutKey as Key; + + format!( + "mesh_view_layout{}{}{}{}{}{}", + if self.contains(Key::MULTISAMPLED) { + "_multisampled" + } else { + Default::default() + }, + if self.contains(Key::DEPTH_PREPASS) { + "_depth" + } else { + Default::default() + }, + if self.contains(Key::NORMAL_PREPASS) { + "_normal" + } else { + Default::default() + }, + if self.contains(Key::MOTION_VECTOR_PREPASS) { + "_motion" + } else { + Default::default() + }, + if self.contains(Key::DEFERRED_PREPASS) { + "_deferred" + } else { + Default::default() + }, + if self.contains(Key::OIT_ENABLED) { + "_oit" + } else { + Default::default() + }, + ) + } +} + +impl From for MeshPipelineViewLayoutKey { + fn from(value: MeshPipelineKey) -> Self { + let mut result = MeshPipelineViewLayoutKey::empty(); + + if value.msaa_samples() > 1 { + result |= MeshPipelineViewLayoutKey::MULTISAMPLED; + } + if value.contains(MeshPipelineKey::DEPTH_PREPASS) { + result |= MeshPipelineViewLayoutKey::DEPTH_PREPASS; + } + if value.contains(MeshPipelineKey::NORMAL_PREPASS) { + result |= MeshPipelineViewLayoutKey::NORMAL_PREPASS; + } + if value.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + result |= MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS; + } + if value.contains(MeshPipelineKey::DEFERRED_PREPASS) { + result |= MeshPipelineViewLayoutKey::DEFERRED_PREPASS; + } + if value.contains(MeshPipelineKey::OIT_ENABLED) { + result |= MeshPipelineViewLayoutKey::OIT_ENABLED; + } + + result + } +} + +impl From for MeshPipelineViewLayoutKey { + fn from(value: Msaa) -> Self { + let mut result = MeshPipelineViewLayoutKey::empty(); + + if value.samples() > 1 { + result |= MeshPipelineViewLayoutKey::MULTISAMPLED; + } + + result + } +} + +impl From> for MeshPipelineViewLayoutKey { + fn from(value: Option<&ViewPrepassTextures>) -> Self { + let mut result = MeshPipelineViewLayoutKey::empty(); + + if let Some(prepass_textures) = value { + if prepass_textures.depth.is_some() { + result |= MeshPipelineViewLayoutKey::DEPTH_PREPASS; + } + if prepass_textures.normal.is_some() { + result |= MeshPipelineViewLayoutKey::NORMAL_PREPASS; + } + if prepass_textures.motion_vectors.is_some() { + result |= MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS; + } + if prepass_textures.deferred.is_some() { + result |= MeshPipelineViewLayoutKey::DEFERRED_PREPASS; + } + } + + result + } +} + +pub(crate) fn buffer_layout( + buffer_binding_type: BufferBindingType, + has_dynamic_offset: bool, + min_binding_size: Option>, +) -> BindGroupLayoutEntryBuilder { + match buffer_binding_type { + BufferBindingType::Uniform => uniform_buffer_sized(has_dynamic_offset, min_binding_size), + BufferBindingType::Storage { read_only } => { + if read_only { + storage_buffer_read_only_sized(has_dynamic_offset, min_binding_size) + } else { + storage_buffer_sized(has_dynamic_offset, min_binding_size) + } + } + } +} + +/// Returns the appropriate bind group layout vec based on the parameters +fn layout_entries( + clustered_forward_buffer_binding_type: BufferBindingType, + visibility_ranges_buffer_binding_type: BufferBindingType, + layout_key: MeshPipelineViewLayoutKey, + render_device: &RenderDevice, + render_adapter: &RenderAdapter, +) -> [Vec; 2] { + // EnvironmentMapLight + let environment_map_entries = + environment_map::get_bind_group_layout_entries(render_device, render_adapter); + + let mut entries = DynamicBindGroupLayoutEntries::new_with_indices( + ShaderStages::FRAGMENT, + ( + // View + ( + 0, + uniform_buffer::(true).visibility(ShaderStages::VERTEX_FRAGMENT), + ), + // Lights + (1, uniform_buffer::(true)), + // Point Shadow Texture Cube Array + ( + 2, + #[cfg(all( + not(target_abi = "sim"), + any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ) + ))] + texture_cube_array(TextureSampleType::Depth), + #[cfg(any( + target_abi = "sim", + all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")) + ))] + texture_cube(TextureSampleType::Depth), + ), + // Point Shadow Texture Array Comparison Sampler + (3, sampler(SamplerBindingType::Comparison)), + // Point Shadow Texture Array Linear Sampler + #[cfg(feature = "experimental_pbr_pcss")] + (4, sampler(SamplerBindingType::Filtering)), + // Directional Shadow Texture Array + ( + 5, + #[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" + ))] + texture_2d_array(TextureSampleType::Depth), + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + texture_2d(TextureSampleType::Depth), + ), + // Directional Shadow Texture Array Comparison Sampler + (6, sampler(SamplerBindingType::Comparison)), + // Directional Shadow Texture Array Linear Sampler + #[cfg(feature = "experimental_pbr_pcss")] + (7, sampler(SamplerBindingType::Filtering)), + // PointLights + ( + 8, + buffer_layout( + clustered_forward_buffer_binding_type, + false, + Some(GpuClusterableObjects::min_size( + clustered_forward_buffer_binding_type, + )), + ), + ), + // ClusteredLightIndexLists + ( + 9, + buffer_layout( + clustered_forward_buffer_binding_type, + false, + Some( + ViewClusterBindings::min_size_clusterable_object_index_lists( + clustered_forward_buffer_binding_type, + ), + ), + ), + ), + // ClusterOffsetsAndCounts + ( + 10, + buffer_layout( + clustered_forward_buffer_binding_type, + false, + Some(ViewClusterBindings::min_size_cluster_offsets_and_counts( + clustered_forward_buffer_binding_type, + )), + ), + ), + // Globals + ( + 11, + uniform_buffer::(false).visibility(ShaderStages::VERTEX_FRAGMENT), + ), + // Fog + (12, uniform_buffer::(true)), + // Light probes + (13, uniform_buffer::(true)), + // Visibility ranges + ( + 14, + buffer_layout( + visibility_ranges_buffer_binding_type, + false, + Some(Vec4::min_size()), + ) + .visibility(ShaderStages::VERTEX), + ), + // Screen space reflection settings + (15, uniform_buffer::(true)), + // Screen space ambient occlusion texture + ( + 16, + texture_2d(TextureSampleType::Float { filterable: false }), + ), + (17, environment_map_entries[3]), + ), + ); + + // Tonemapping + let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); + entries = entries.extend_with_indices(( + (18, tonemapping_lut_entries[0]), + (19, tonemapping_lut_entries[1]), + )); + + // Prepass + if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32"))) + || (cfg!(all(feature = "webgl", target_arch = "wasm32")) + && !layout_key.contains(MeshPipelineViewLayoutKey::MULTISAMPLED)) + { + for (entry, binding) in prepass::get_bind_group_layout_entries(layout_key) + .iter() + .zip([20, 21, 22, 23]) + { + if let Some(entry) = entry { + entries = entries.extend_with_indices(((binding as u32, *entry),)); + } + } + } + + // View Transmission Texture + entries = entries.extend_with_indices(( + ( + 24, + texture_2d(TextureSampleType::Float { filterable: true }), + ), + (25, sampler(SamplerBindingType::Filtering)), + )); + + // OIT + if layout_key.contains(MeshPipelineViewLayoutKey::OIT_ENABLED) { + // Check if we can use OIT. This is a hack to avoid errors on webgl -- + // the OIT plugin will warn the user that OIT is not supported on their + // platform, so we don't need to do it here. + if is_oit_supported(render_adapter, render_device, false) { + entries = entries.extend_with_indices(( + // oit_layers + (26, storage_buffer_sized(false, None)), + // oit_layer_ids, + (27, storage_buffer_sized(false, None)), + // oit_layer_count + ( + 28, + uniform_buffer::(true), + ), + )); + } + } + + let mut binding_array_entries = DynamicBindGroupLayoutEntries::new(ShaderStages::FRAGMENT); + binding_array_entries = binding_array_entries.extend_with_indices(( + (0, environment_map_entries[0]), + (1, environment_map_entries[1]), + (2, environment_map_entries[2]), + )); + + // Irradiance volumes + if IRRADIANCE_VOLUMES_ARE_USABLE { + let irradiance_volume_entries = + irradiance_volume::get_bind_group_layout_entries(render_device, render_adapter); + binding_array_entries = binding_array_entries.extend_with_indices(( + (3, irradiance_volume_entries[0]), + (4, irradiance_volume_entries[1]), + )); + } + + // Clustered decals + if let Some(clustered_decal_entries) = + decal::clustered::get_bind_group_layout_entries(render_device, render_adapter) + { + binding_array_entries = binding_array_entries.extend_with_indices(( + (5, clustered_decal_entries[0]), + (6, clustered_decal_entries[1]), + (7, clustered_decal_entries[2]), + )); + } + + [entries.to_vec(), binding_array_entries.to_vec()] +} + +/// Stores the view layouts for every combination of pipeline keys. +/// +/// This is wrapped in an [`Arc`] so that it can be efficiently cloned and +/// placed inside specializable pipeline types. +#[derive(Resource, Clone, Deref, DerefMut)] +pub struct MeshPipelineViewLayouts( + pub Arc<[MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT]>, +); + +impl FromWorld for MeshPipelineViewLayouts { + fn from_world(world: &mut World) -> Self { + // Generates all possible view layouts for the mesh pipeline, based on all combinations of + // [`MeshPipelineViewLayoutKey`] flags. + + let render_device = world.resource::(); + let render_adapter = world.resource::(); + + let clustered_forward_buffer_binding_type = render_device + .get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); + let visibility_ranges_buffer_binding_type = render_device + .get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); + + Self(Arc::new(array::from_fn(|i| { + let key = MeshPipelineViewLayoutKey::from_bits_truncate(i as u32); + let entries = layout_entries( + clustered_forward_buffer_binding_type, + visibility_ranges_buffer_binding_type, + key, + render_device, + render_adapter, + ); + #[cfg(debug_assertions)] + let texture_count: usize = entries + .iter() + .flat_map(|e| { + e.iter() + .filter(|entry| matches!(entry.ty, BindingType::Texture { .. })) + }) + .count(); + + MeshPipelineViewLayout { + main_layout: render_device + .create_bind_group_layout(key.label().as_str(), &entries[0]), + binding_array_layout: render_device.create_bind_group_layout( + format!("{}_binding_array", key.label()).as_str(), + &entries[1], + ), + empty_layout: render_device + .create_bind_group_layout(format!("{}_empty", key.label()).as_str(), &[]), + #[cfg(debug_assertions)] + texture_count, + } + }))) + } +} + +impl MeshPipelineViewLayouts { + pub fn get_view_layout( + &self, + layout_key: MeshPipelineViewLayoutKey, + ) -> &MeshPipelineViewLayout { + let index = layout_key.bits() as usize; + let layout = &self[index]; + + #[cfg(debug_assertions)] + if layout.texture_count > MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES { + // Issue our own warning here because Naga's error message is a bit cryptic in this situation + once!(warn!("Too many textures in mesh pipeline view layout, this might cause us to hit `wgpu::Limits::max_sampled_textures_per_shader_stage` in some environments.")); + } + + layout + } +} + +/// Generates all possible view layouts for the mesh pipeline, based on all combinations of +/// [`MeshPipelineViewLayoutKey`] flags. +pub fn generate_view_layouts( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, + clustered_forward_buffer_binding_type: BufferBindingType, + visibility_ranges_buffer_binding_type: BufferBindingType, +) -> [MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT] { + array::from_fn(|i| { + let key = MeshPipelineViewLayoutKey::from_bits_truncate(i as u32); + let entries = layout_entries( + clustered_forward_buffer_binding_type, + visibility_ranges_buffer_binding_type, + key, + render_device, + render_adapter, + ); + + #[cfg(debug_assertions)] + let texture_count: usize = entries + .iter() + .flat_map(|e| { + e.iter() + .filter(|entry| matches!(entry.ty, BindingType::Texture { .. })) + }) + .count(); + + MeshPipelineViewLayout { + main_layout: render_device.create_bind_group_layout(key.label().as_str(), &entries[0]), + binding_array_layout: render_device.create_bind_group_layout( + format!("{}_binding_array", key.label()).as_str(), + &entries[1], + ), + empty_layout: render_device + .create_bind_group_layout(format!("{}_empty", key.label()).as_str(), &[]), + #[cfg(debug_assertions)] + texture_count, + } + }) +} + +#[derive(Component)] +pub struct MeshViewBindGroup { + pub main: BindGroup, + pub binding_array: BindGroup, + pub empty: BindGroup, +} + +pub fn prepare_mesh_view_bind_groups( + mut commands: Commands, + (render_device, render_adapter): (Res, Res), + mesh_pipeline: Res, + shadow_samplers: Res, + (light_meta, global_light_meta): (Res, Res), + fog_meta: Res, + (view_uniforms, environment_map_uniform): (Res, Res), + views: Query<( + Entity, + &ViewShadowBindings, + &ViewClusterBindings, + &Msaa, + Option<&ScreenSpaceAmbientOcclusionResources>, + Option<&ViewPrepassTextures>, + Option<&ViewTransmissionTexture>, + &Tonemapping, + Option<&RenderViewLightProbes>, + Option<&RenderViewLightProbes>, + Has, + )>, + (images, mut fallback_images, fallback_image, fallback_image_zero): ( + Res>, + FallbackImageMsaa, + Res, + Res, + ), + globals_buffer: Res, + tonemapping_luts: Res, + light_probes_buffer: Res, + visibility_ranges: Res, + ssr_buffer: Res, + oit_buffers: Res, + (decals_buffer, render_decals): (Res, Res), +) { + if let ( + Some(view_binding), + Some(light_binding), + Some(clusterable_objects_binding), + Some(globals), + Some(fog_binding), + Some(light_probes_binding), + Some(visibility_ranges_buffer), + Some(ssr_binding), + Some(environment_map_binding), + ) = ( + view_uniforms.uniforms.binding(), + light_meta.view_gpu_lights.binding(), + global_light_meta.gpu_clusterable_objects.binding(), + globals_buffer.buffer.binding(), + fog_meta.gpu_fogs.binding(), + light_probes_buffer.binding(), + visibility_ranges.buffer().buffer(), + ssr_buffer.binding(), + environment_map_uniform.binding(), + ) { + for ( + entity, + shadow_bindings, + cluster_bindings, + msaa, + ssao_resources, + prepass_textures, + transmission_texture, + tonemapping, + render_view_environment_maps, + render_view_irradiance_volumes, + has_oit, + ) in &views + { + let fallback_ssao = fallback_images + .image_for_samplecount(1, TextureFormat::bevy_default()) + .texture_view + .clone(); + let ssao_view = ssao_resources + .map(|t| &t.screen_space_ambient_occlusion_texture.default_view) + .unwrap_or(&fallback_ssao); + + let mut layout_key = MeshPipelineViewLayoutKey::from(*msaa) + | MeshPipelineViewLayoutKey::from(prepass_textures); + if has_oit { + layout_key |= MeshPipelineViewLayoutKey::OIT_ENABLED; + } + + let layout = mesh_pipeline.get_view_layout(layout_key); + + let mut entries = DynamicBindGroupEntries::new_with_indices(( + (0, view_binding.clone()), + (1, light_binding.clone()), + (2, &shadow_bindings.point_light_depth_texture_view), + (3, &shadow_samplers.point_light_comparison_sampler), + #[cfg(feature = "experimental_pbr_pcss")] + (4, &shadow_samplers.point_light_linear_sampler), + (5, &shadow_bindings.directional_light_depth_texture_view), + (6, &shadow_samplers.directional_light_comparison_sampler), + #[cfg(feature = "experimental_pbr_pcss")] + (7, &shadow_samplers.directional_light_linear_sampler), + (8, clusterable_objects_binding.clone()), + ( + 9, + cluster_bindings + .clusterable_object_index_lists_binding() + .unwrap(), + ), + (10, cluster_bindings.offsets_and_counts_binding().unwrap()), + (11, globals.clone()), + (12, fog_binding.clone()), + (13, light_probes_binding.clone()), + (14, visibility_ranges_buffer.as_entire_binding()), + (15, ssr_binding.clone()), + (16, ssao_view), + )); + + entries = entries.extend_with_indices(((17, environment_map_binding.clone()),)); + + let lut_bindings = + get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image); + entries = entries.extend_with_indices(((18, lut_bindings.0), (19, lut_bindings.1))); + + // When using WebGL, we can't have a depth texture with multisampling + let prepass_bindings; + if cfg!(any(not(feature = "webgl"), not(target_arch = "wasm32"))) || msaa.samples() == 1 + { + prepass_bindings = prepass::get_bindings(prepass_textures); + for (binding, index) in prepass_bindings + .iter() + .map(Option::as_ref) + .zip([20, 21, 22, 23]) + .flat_map(|(b, i)| b.map(|b| (b, i))) + { + entries = entries.extend_with_indices(((index, binding),)); + } + }; + + let transmission_view = transmission_texture + .map(|transmission| &transmission.view) + .unwrap_or(&fallback_image_zero.texture_view); + + let transmission_sampler = transmission_texture + .map(|transmission| &transmission.sampler) + .unwrap_or(&fallback_image_zero.sampler); + + entries = + entries.extend_with_indices(((24, transmission_view), (25, transmission_sampler))); + + if has_oit + && let ( + Some(oit_layers_binding), + Some(oit_layer_ids_binding), + Some(oit_settings_binding), + ) = ( + oit_buffers.layers.binding(), + oit_buffers.layer_ids.binding(), + oit_buffers.settings.binding(), + ) + { + entries = entries.extend_with_indices(( + (26, oit_layers_binding.clone()), + (27, oit_layer_ids_binding.clone()), + (28, oit_settings_binding.clone()), + )); + } + + let mut entries_binding_array = DynamicBindGroupEntries::new(); + + let environment_map_bind_group_entries = RenderViewEnvironmentMapBindGroupEntries::get( + render_view_environment_maps, + &images, + &fallback_image, + &render_device, + &render_adapter, + ); + match environment_map_bind_group_entries { + RenderViewEnvironmentMapBindGroupEntries::Single { + diffuse_texture_view, + specular_texture_view, + sampler, + } => { + entries_binding_array = entries_binding_array.extend_with_indices(( + (0, diffuse_texture_view), + (1, specular_texture_view), + (2, sampler), + )); + } + RenderViewEnvironmentMapBindGroupEntries::Multiple { + ref diffuse_texture_views, + ref specular_texture_views, + sampler, + } => { + entries_binding_array = entries_binding_array.extend_with_indices(( + (0, diffuse_texture_views.as_slice()), + (1, specular_texture_views.as_slice()), + (2, sampler), + )); + } + } + + let irradiance_volume_bind_group_entries = if IRRADIANCE_VOLUMES_ARE_USABLE { + Some(RenderViewIrradianceVolumeBindGroupEntries::get( + render_view_irradiance_volumes, + &images, + &fallback_image, + &render_device, + &render_adapter, + )) + } else { + None + }; + + match irradiance_volume_bind_group_entries { + Some(RenderViewIrradianceVolumeBindGroupEntries::Single { + texture_view, + sampler, + }) => { + entries_binding_array = entries_binding_array + .extend_with_indices(((3, texture_view), (4, sampler))); + } + Some(RenderViewIrradianceVolumeBindGroupEntries::Multiple { + ref texture_views, + sampler, + }) => { + entries_binding_array = entries_binding_array + .extend_with_indices(((3, texture_views.as_slice()), (4, sampler))); + } + None => {} + } + + let decal_bind_group_entries = RenderViewClusteredDecalBindGroupEntries::get( + &render_decals, + &decals_buffer, + &images, + &fallback_image, + &render_device, + &render_adapter, + ); + + // Add the decal bind group entries. + if let Some(ref render_view_decal_bind_group_entries) = decal_bind_group_entries { + entries_binding_array = entries_binding_array.extend_with_indices(( + // `clustered_decals` + ( + 5, + render_view_decal_bind_group_entries + .decals + .as_entire_binding(), + ), + // `clustered_decal_textures` + ( + 6, + render_view_decal_bind_group_entries + .texture_views + .as_slice(), + ), + // `clustered_decal_sampler` + (7, render_view_decal_bind_group_entries.sampler), + )); + } + + commands.entity(entity).insert(MeshViewBindGroup { + main: render_device.create_bind_group( + "mesh_view_bind_group", + &layout.main_layout, + &entries, + ), + binding_array: render_device.create_bind_group( + "mesh_view_bind_group_binding_array", + &layout.binding_array_layout, + &entries_binding_array, + ), + empty: render_device.create_bind_group( + "mesh_view_bind_group_empty", + &layout.empty_layout, + &[], + ), + }); + } + } +} diff --git a/crates/libmarathon/src/render/pbr/render/mesh_view_bindings.wgsl b/crates/libmarathon/src/render/pbr/render/mesh_view_bindings.wgsl new file mode 100644 index 0000000..0f650e6 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/mesh_view_bindings.wgsl @@ -0,0 +1,119 @@ +#define_import_path bevy_pbr::mesh_view_bindings + +#import bevy_pbr::mesh_view_types as types +#import bevy_render::{ + view::View, + globals::Globals, +} + +@group(0) @binding(0) var view: View; +@group(0) @binding(1) var lights: types::Lights; +#ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT +@group(0) @binding(2) var point_shadow_textures: texture_depth_cube; +#else +@group(0) @binding(2) var point_shadow_textures: texture_depth_cube_array; +#endif +@group(0) @binding(3) var point_shadow_textures_comparison_sampler: sampler_comparison; +#ifdef PCSS_SAMPLERS_AVAILABLE +@group(0) @binding(4) var point_shadow_textures_linear_sampler: sampler; +#endif // PCSS_SAMPLERS_AVAILABLE +#ifdef NO_ARRAY_TEXTURES_SUPPORT +@group(0) @binding(5) var directional_shadow_textures: texture_depth_2d; +#else +@group(0) @binding(5) var directional_shadow_textures: texture_depth_2d_array; +#endif +@group(0) @binding(6) var directional_shadow_textures_comparison_sampler: sampler_comparison; +#ifdef PCSS_SAMPLERS_AVAILABLE +@group(0) @binding(7) var directional_shadow_textures_linear_sampler: sampler; +#endif // PCSS_SAMPLERS_AVAILABLE + +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 +@group(0) @binding(8) var clusterable_objects: types::ClusterableObjects; +@group(0) @binding(9) var clusterable_object_index_lists: types::ClusterLightIndexLists; +@group(0) @binding(10) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; +#else +@group(0) @binding(8) var clusterable_objects: types::ClusterableObjects; +@group(0) @binding(9) var clusterable_object_index_lists: types::ClusterLightIndexLists; +@group(0) @binding(10) var cluster_offsets_and_counts: types::ClusterOffsetsAndCounts; +#endif + +@group(0) @binding(11) var globals: Globals; +@group(0) @binding(12) var fog: types::Fog; +@group(0) @binding(13) var light_probes: types::LightProbes; + +const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u; +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 +@group(0) @binding(14) var visibility_ranges: array>; +#else +@group(0) @binding(14) var visibility_ranges: array, VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE>; +#endif + +@group(0) @binding(15) var ssr_settings: types::ScreenSpaceReflectionsSettings; +@group(0) @binding(16) var screen_space_ambient_occlusion_texture: texture_2d; +@group(0) @binding(17) var environment_map_uniform: types::EnvironmentMapUniform; + +// NB: If you change these, make sure to update `tonemapping_shared.wgsl` too. +@group(0) @binding(18) var dt_lut_texture: texture_3d; +@group(0) @binding(19) var dt_lut_sampler: sampler; + +#ifdef MULTISAMPLED +#ifdef DEPTH_PREPASS +@group(0) @binding(20) var depth_prepass_texture: texture_depth_multisampled_2d; +#endif // DEPTH_PREPASS +#ifdef NORMAL_PREPASS +@group(0) @binding(21) var normal_prepass_texture: texture_multisampled_2d; +#endif // NORMAL_PREPASS +#ifdef MOTION_VECTOR_PREPASS +@group(0) @binding(22) var motion_vector_prepass_texture: texture_multisampled_2d; +#endif // MOTION_VECTOR_PREPASS + +#else // MULTISAMPLED + +#ifdef DEPTH_PREPASS +@group(0) @binding(20) var depth_prepass_texture: texture_depth_2d; +#endif // DEPTH_PREPASS +#ifdef NORMAL_PREPASS +@group(0) @binding(21) var normal_prepass_texture: texture_2d; +#endif // NORMAL_PREPASS +#ifdef MOTION_VECTOR_PREPASS +@group(0) @binding(22) var motion_vector_prepass_texture: texture_2d; +#endif // MOTION_VECTOR_PREPASS + +#endif // MULTISAMPLED + +#ifdef DEFERRED_PREPASS +@group(0) @binding(23) var deferred_prepass_texture: texture_2d; +#endif // DEFERRED_PREPASS + +@group(0) @binding(24) var view_transmission_texture: texture_2d; +@group(0) @binding(25) var view_transmission_sampler: sampler; + +#ifdef OIT_ENABLED +@group(0) @binding(26) var oit_layers: array>; +@group(0) @binding(27) var oit_layer_ids: array>; +@group(0) @binding(28) var oit_settings: types::OrderIndependentTransparencySettings; +#endif // OIT_ENABLED + +#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY +@group(1) @binding(0) var diffuse_environment_maps: binding_array, 8u>; +@group(1) @binding(1) var specular_environment_maps: binding_array, 8u>; +#else +@group(1) @binding(0) var diffuse_environment_map: texture_cube; +@group(1) @binding(1) var specular_environment_map: texture_cube; +#endif +@group(1) @binding(2) var environment_map_sampler: sampler; + +#ifdef IRRADIANCE_VOLUMES_ARE_USABLE +#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY +@group(1) @binding(3) var irradiance_volumes: binding_array, 8u>; +#else +@group(1) @binding(3) var irradiance_volume: texture_3d; +#endif +@group(1) @binding(4) var irradiance_volume_sampler: sampler; +#endif + +#ifdef CLUSTERED_DECALS_ARE_USABLE +@group(1) @binding(5) var clustered_decals: types::ClusteredDecals; +@group(1) @binding(6) var clustered_decal_textures: binding_array, 8u>; +@group(1) @binding(7) var clustered_decal_sampler: sampler; +#endif // CLUSTERED_DECALS_ARE_USABLE diff --git a/crates/libmarathon/src/render/pbr/render/mesh_view_types.wgsl b/crates/libmarathon/src/render/pbr/render/mesh_view_types.wgsl new file mode 100644 index 0000000..19f87b3 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/mesh_view_types.wgsl @@ -0,0 +1,187 @@ +#define_import_path bevy_pbr::mesh_view_types + +struct ClusterableObject { + // For point lights: the lower-right 2x2 values of the projection matrix [2][2] [2][3] [3][2] [3][3] + // For spot lights: the direction (x,z), spot_scale and spot_offset + light_custom_data: vec4, + color_inverse_square_range: vec4, + position_radius: vec4, + // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. + flags: u32, + shadow_depth_bias: f32, + shadow_normal_bias: f32, + spot_light_tan_angle: f32, + soft_shadow_size: f32, + shadow_map_near_z: f32, + decal_index: u32, + pad: f32, +}; + +const POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u << 0u; +const POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE: u32 = 1u << 1u; +const POINT_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 1u << 2u; +const POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 1u << 3u; + +struct DirectionalCascade { + clip_from_world: mat4x4, + texel_size: f32, + far_bound: f32, +} + +struct DirectionalLight { + cascades: array, + color: vec4, + direction_to_light: vec3, + // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. + flags: u32, + soft_shadow_size: f32, + shadow_depth_bias: f32, + shadow_normal_bias: f32, + num_cascades: u32, + cascades_overlap_proportion: f32, + depth_texture_base_index: u32, + decal_index: u32, + sun_disk_angular_size: f32, + sun_disk_intensity: f32, +}; + +const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u << 0u; +const DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT: u32 = 1u << 1u; +const DIRECTIONAL_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT: u32 = 1u << 2u; + +struct Lights { + // NOTE: this array size must be kept in sync with the constants defined in bevy_pbr/src/render/light.rs + directional_lights: array, + ambient_color: vec4, + // x/y/z dimensions and n_clusters in w + cluster_dimensions: vec4, + // xy are vec2(cluster_dimensions.xy) / vec2(view.width, view.height) + // + // For perspective projections: + // z is cluster_dimensions.z / log(far / near) + // w is cluster_dimensions.z * log(near) / log(far / near) + // + // For orthographic projections: + // NOTE: near and far are +ve but -z is infront of the camera + // z is -near + // w is cluster_dimensions.z / (-far - -near) + cluster_factors: vec4, + n_directional_lights: u32, + spot_light_shadowmap_offset: i32, + ambient_light_affects_lightmapped_meshes: u32 +}; + +struct Fog { + base_color: vec4, + directional_light_color: vec4, + // `be` and `bi` are allocated differently depending on the fog mode + // + // For Linear Fog: + // be.x = start, be.y = end + // For Exponential and ExponentialSquared Fog: + // be.x = density + // For Atmospheric Fog: + // be = per-channel extinction density + // bi = per-channel inscattering density + be: vec3, + directional_light_exponent: f32, + bi: vec3, + mode: u32, +} + +// Important: These must be kept in sync with `fog.rs` +const FOG_MODE_OFF: u32 = 0u; +const FOG_MODE_LINEAR: u32 = 1u; +const FOG_MODE_EXPONENTIAL: u32 = 2u; +const FOG_MODE_EXPONENTIAL_SQUARED: u32 = 3u; +const FOG_MODE_ATMOSPHERIC: u32 = 4u; + +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 +struct ClusterableObjects { + data: array, +}; +struct ClusterLightIndexLists { + data: array, +}; +struct ClusterOffsetsAndCounts { + data: array, 2>>, +}; +#else +struct ClusterableObjects { + data: array, +}; +struct ClusterLightIndexLists { + // each u32 contains 4 u8 indices into the ClusterableObjects array + data: array, 1024u>, +}; +struct ClusterOffsetsAndCounts { + // each u32 contains a 24-bit index into ClusterLightIndexLists in the high 24 bits + // and an 8-bit count of the number of lights in the low 8 bits + data: array, 1024u>, +}; +#endif + +struct LightProbe { + // This is stored as the transpose in order to save space in this structure. + // It'll be transposed in the `environment_map_light` function. + light_from_world_transposed: mat3x4, + cubemap_index: i32, + intensity: f32, + // Whether this light probe contributes diffuse light to lightmapped meshes. + affects_lightmapped_mesh_diffuse: u32, +}; + +struct LightProbes { + // This must match `MAX_VIEW_REFLECTION_PROBES` on the Rust side. + reflection_probes: array, + irradiance_volumes: array, + reflection_probe_count: i32, + irradiance_volume_count: i32, + // The index of the view environment map cubemap binding, or -1 if there's + // no such cubemap. + view_cubemap_index: i32, + // The smallest valid mipmap level for the specular environment cubemap + // associated with the view. + smallest_specular_mip_level_for_view: u32, + // The intensity of the environment map associated with the view. + intensity_for_view: f32, + // Whether the environment map attached to the view affects the diffuse + // lighting for lightmapped meshes. + view_environment_map_affects_lightmapped_mesh_diffuse: u32, +}; + +// Settings for screen space reflections. +// +// For more information on these settings, see the documentation for +// `bevy_pbr::ssr::ScreenSpaceReflections`. +struct ScreenSpaceReflectionsSettings { + perceptual_roughness_threshold: f32, + thickness: f32, + linear_steps: u32, + linear_march_exponent: f32, + bisection_steps: u32, + use_secant: u32, +}; + +struct EnvironmentMapUniform { + // Transformation matrix for the environment cubemaps in world space. + transform: mat4x4, +}; + +// Shader version of the order independent transparency settings component. +struct OrderIndependentTransparencySettings { + layers_count: i32, + alpha_threshold: f32, +}; + +struct ClusteredDecal { + local_from_world: mat4x4, + image_index: i32, + tag: u32, + pad_a: u32, + pad_b: u32, +} + +struct ClusteredDecals { + decals: array, +} diff --git a/crates/libmarathon/src/render/pbr/render/mod.rs b/crates/libmarathon/src/render/pbr/render/mod.rs new file mode 100644 index 0000000..6a29823 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/mod.rs @@ -0,0 +1,17 @@ +mod fog; +mod gpu_preprocess; +mod light; +pub(crate) mod mesh; +mod mesh_bindings; +mod mesh_view_bindings; +mod morph; +pub(crate) mod skin; + +pub use fog::*; +pub use gpu_preprocess::*; +pub use light::*; +pub use mesh::*; +pub use mesh_bindings::MeshLayouts; +pub use mesh_view_bindings::*; +pub use morph::*; +pub use skin::{extract_skins, prepare_skins, skins_use_uniform_buffers, SkinUniforms, MAX_JOINTS}; diff --git a/crates/libmarathon/src/render/pbr/render/morph.rs b/crates/libmarathon/src/render/pbr/render/morph.rs new file mode 100644 index 0000000..e33af5a --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/morph.rs @@ -0,0 +1,150 @@ +use core::{iter, mem}; + +use bevy_camera::visibility::ViewVisibility; +use bevy_ecs::prelude::*; +use bevy_mesh::morph::{MeshMorphWeights, MAX_MORPH_WEIGHTS}; +use crate::render::sync_world::MainEntityHashMap; +use crate::render::{ + batching::NoAutomaticBatching, + render_resource::{BufferUsages, RawBufferVec}, + renderer::{RenderDevice, RenderQueue}, + Extract, +}; +use bytemuck::NoUninit; + +#[derive(Component)] +pub struct MorphIndex { + pub index: u32, +} + +/// Maps each mesh affected by morph targets to the applicable offset within the +/// [`MorphUniforms`] buffer. +/// +/// We store both the current frame's mapping and the previous frame's mapping +/// for the purposes of motion vector calculation. +#[derive(Default, Resource)] +pub struct MorphIndices { + /// Maps each entity with a morphed mesh to the appropriate offset within + /// [`MorphUniforms::current_buffer`]. + pub current: MainEntityHashMap, + + /// Maps each entity with a morphed mesh to the appropriate offset within + /// [`MorphUniforms::prev_buffer`]. + pub prev: MainEntityHashMap, +} + +/// The GPU buffers containing morph weights for all meshes with morph targets. +/// +/// This is double-buffered: we store the weights of the previous frame in +/// addition to those of the current frame. This is for motion vector +/// calculation. Every frame, we swap buffers and reuse the morph target weight +/// buffer from two frames ago for the current frame. +#[derive(Resource)] +pub struct MorphUniforms { + /// The morph weights for the current frame. + pub current_buffer: RawBufferVec, + /// The morph weights for the previous frame. + pub prev_buffer: RawBufferVec, +} + +impl Default for MorphUniforms { + fn default() -> Self { + Self { + current_buffer: RawBufferVec::new(BufferUsages::UNIFORM), + prev_buffer: RawBufferVec::new(BufferUsages::UNIFORM), + } + } +} + +pub fn prepare_morphs( + render_device: Res, + render_queue: Res, + mut uniform: ResMut, +) { + if uniform.current_buffer.is_empty() { + return; + } + let len = uniform.current_buffer.len(); + uniform.current_buffer.reserve(len, &render_device); + uniform + .current_buffer + .write_buffer(&render_device, &render_queue); + + // We don't need to write `uniform.prev_buffer` because we already wrote it + // last frame, and the data should still be on the GPU. +} + +const fn can_align(step: usize, target: usize) -> bool { + step.is_multiple_of(target) || target.is_multiple_of(step) +} + +const WGPU_MIN_ALIGN: usize = 256; + +/// Align a [`RawBufferVec`] to `N` bytes by padding the end with `T::default()` values. +fn add_to_alignment(buffer: &mut RawBufferVec) { + let n = WGPU_MIN_ALIGN; + let t_size = size_of::(); + if !can_align(n, t_size) { + // This panic is stripped at compile time, due to n, t_size and can_align being const + panic!( + "RawBufferVec should contain only types with a size multiple or divisible by {n}, \ + {} has a size of {t_size}, which is neither multiple or divisible by {n}", + core::any::type_name::() + ); + } + + let buffer_size = buffer.len(); + let byte_size = t_size * buffer_size; + let bytes_over_n = byte_size % n; + if bytes_over_n == 0 { + return; + } + let bytes_to_add = n - bytes_over_n; + let ts_to_add = bytes_to_add / t_size; + buffer.extend(iter::repeat_with(T::default).take(ts_to_add)); +} + +// Notes on implementation: see comment on top of the extract_skins system in skin module. +// This works similarly, but for `f32` instead of `Mat4` +pub fn extract_morphs( + morph_indices: ResMut, + uniform: ResMut, + query: Extract>, +) { + // Borrow check workaround. + let (morph_indices, uniform) = (morph_indices.into_inner(), uniform.into_inner()); + + // Swap buffers. We need to keep the previous frame's buffer around for the + // purposes of motion vector computation. + mem::swap(&mut morph_indices.current, &mut morph_indices.prev); + mem::swap(&mut uniform.current_buffer, &mut uniform.prev_buffer); + morph_indices.current.clear(); + uniform.current_buffer.clear(); + + for (entity, view_visibility, morph_weights) in &query { + if !view_visibility.get() { + continue; + } + let start = uniform.current_buffer.len(); + let weights = morph_weights.weights(); + let legal_weights = weights.iter().take(MAX_MORPH_WEIGHTS).copied(); + uniform.current_buffer.extend(legal_weights); + add_to_alignment::(&mut uniform.current_buffer); + + let index = (start * size_of::()) as u32; + morph_indices + .current + .insert(entity.into(), MorphIndex { index }); + } +} + +// NOTE: Because morph targets require per-morph target texture bindings, they cannot +// currently be batched. +pub fn no_automatic_morph_batching( + mut commands: Commands, + query: Query, Without)>, +) { + for entity in &query { + commands.entity(entity).try_insert(NoAutomaticBatching); + } +} diff --git a/crates/libmarathon/src/render/pbr/render/morph.wgsl b/crates/libmarathon/src/render/pbr/render/morph.wgsl new file mode 100644 index 0000000..6689d68 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/morph.wgsl @@ -0,0 +1,52 @@ +#define_import_path bevy_pbr::morph + +#ifdef MORPH_TARGETS + +#import bevy_pbr::mesh_types::MorphWeights; + +@group(2) @binding(2) var morph_weights: MorphWeights; +@group(2) @binding(3) var morph_targets: texture_3d; +@group(2) @binding(7) var prev_morph_weights: MorphWeights; + +// NOTE: Those are the "hardcoded" values found in `MorphAttributes` struct +// in crates/bevy_render/src/mesh/morph/visitors.rs +// In an ideal world, the offsets are established dynamically and passed as #defines +// to the shader, but it's out of scope for the initial implementation of morph targets. +const position_offset: u32 = 0u; +const normal_offset: u32 = 3u; +const tangent_offset: u32 = 6u; +const total_component_count: u32 = 9u; + +fn layer_count() -> u32 { + let dimensions = textureDimensions(morph_targets); + return u32(dimensions.z); +} +fn component_texture_coord(vertex_index: u32, component_offset: u32) -> vec2 { + let width = u32(textureDimensions(morph_targets).x); + let component_index = total_component_count * vertex_index + component_offset; + return vec2(component_index % width, component_index / width); +} +fn weight_at(weight_index: u32) -> f32 { + let i = weight_index; + return morph_weights.weights[i / 4u][i % 4u]; +} +fn prev_weight_at(weight_index: u32) -> f32 { + let i = weight_index; + return prev_morph_weights.weights[i / 4u][i % 4u]; +} +fn morph_pixel(vertex: u32, component: u32, weight: u32) -> f32 { + let coord = component_texture_coord(vertex, component); + // Due to https://gpuweb.github.io/gpuweb/wgsl/#texel-formats + // While the texture stores a f32, the textureLoad returns a vec4<>, where + // only the first component is set. + return textureLoad(morph_targets, vec3(coord, weight), 0).r; +} +fn morph(vertex_index: u32, component_offset: u32, weight_index: u32) -> vec3 { + return vec3( + morph_pixel(vertex_index, component_offset, weight_index), + morph_pixel(vertex_index, component_offset + 1u, weight_index), + morph_pixel(vertex_index, component_offset + 2u, weight_index), + ); +} + +#endif // MORPH_TARGETS diff --git a/crates/libmarathon/src/render/pbr/render/occlusion_culling.wgsl b/crates/libmarathon/src/render/pbr/render/occlusion_culling.wgsl new file mode 100644 index 0000000..1be999c --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/occlusion_culling.wgsl @@ -0,0 +1,30 @@ +// Occlusion culling utility functions. + +#define_import_path bevy_pbr::occlusion_culling + +fn get_aabb_size_in_pixels(aabb: vec4, depth_pyramid: texture_2d) -> vec2 { + let depth_pyramid_size_mip_0 = vec2(textureDimensions(depth_pyramid, 0)); + let aabb_width_pixels = (aabb.z - aabb.x) * depth_pyramid_size_mip_0.x; + let aabb_height_pixels = (aabb.w - aabb.y) * depth_pyramid_size_mip_0.y; + return vec2(aabb_width_pixels, aabb_height_pixels); +} + +fn get_occluder_depth( + aabb: vec4, + aabb_pixel_size: vec2, + depth_pyramid: texture_2d +) -> f32 { + let aabb_width_pixels = aabb_pixel_size.x; + let aabb_height_pixels = aabb_pixel_size.y; + + let depth_pyramid_size_mip_0 = vec2(textureDimensions(depth_pyramid, 0)); + let depth_level = max(0, i32(ceil(log2(max(aabb_width_pixels, aabb_height_pixels))))); // TODO: Naga doesn't like this being a u32 + let depth_pyramid_size = vec2(textureDimensions(depth_pyramid, depth_level)); + let aabb_top_left = vec2(aabb.xy * depth_pyramid_size); + + let depth_quad_a = textureLoad(depth_pyramid, aabb_top_left, depth_level).x; + let depth_quad_b = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 0u), depth_level).x; + let depth_quad_c = textureLoad(depth_pyramid, aabb_top_left + vec2(0u, 1u), depth_level).x; + let depth_quad_d = textureLoad(depth_pyramid, aabb_top_left + vec2(1u, 1u), depth_level).x; + return min(min(depth_quad_a, depth_quad_b), min(depth_quad_c, depth_quad_d)); +} diff --git a/crates/libmarathon/src/render/pbr/render/parallax_mapping.wgsl b/crates/libmarathon/src/render/pbr/render/parallax_mapping.wgsl new file mode 100644 index 0000000..9005734 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/parallax_mapping.wgsl @@ -0,0 +1,139 @@ +#define_import_path bevy_pbr::parallax_mapping + +#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d} + +#import bevy_pbr::{ + pbr_bindings::{depth_map_texture, depth_map_sampler}, + mesh_bindings::mesh +} + +#ifdef BINDLESS +#import bevy_pbr::pbr_bindings::material_indices +#endif // BINDLESS + +fn sample_depth_map(uv: vec2, material_bind_group_slot: u32) -> f32 { + // We use `textureSampleLevel` over `textureSample` because the wgpu DX12 + // backend (Fxc) panics when using "gradient instructions" inside a loop. + // It results in the whole loop being unrolled by the shader compiler, + // which it can't do because the upper limit of the loop in steep parallax + // mapping is a variable set by the user. + // The "gradient instructions" comes from `textureSample` computing MIP level + // based on UV derivative. With `textureSampleLevel`, we provide ourselves + // the MIP level, so no gradient instructions are used, and we can use + // sample_depth_map in our loop. + // See https://stackoverflow.com/questions/56581141/direct3d11-gradient-instruction-used-in-a-loop-with-varying-iteration-forcing + return textureSampleLevel( +#ifdef BINDLESS + bindless_textures_2d[material_indices[material_bind_group_slot].depth_map_texture], + bindless_samplers_filtering[material_indices[material_bind_group_slot].depth_map_sampler], +#else // BINDLESS + depth_map_texture, + depth_map_sampler, +#endif // BINDLESS + uv, + 0.0 + ).r; +} + +// An implementation of parallax mapping, see https://en.wikipedia.org/wiki/Parallax_mapping +// Code derived from: https://web.archive.org/web/20150419215321/http://sunandblackcat.com/tipFullView.php?l=eng&topicid=28 +fn parallaxed_uv( + depth_scale: f32, + max_layer_count: f32, + max_steps: u32, + // The original interpolated uv + original_uv: vec2, + // The vector from the camera to the fragment at the surface in tangent space + Vt: vec3, + material_bind_group_slot: u32, +) -> vec2 { + if max_layer_count < 1.0 { + return original_uv; + } + var uv = original_uv; + + // Steep Parallax Mapping + // ====================== + // Split the depth map into `layer_count` layers. + // When Vt hits the surface of the mesh (excluding depth displacement), + // if the depth is not below or on surface including depth displacement (textureSample), then + // look forward (+= delta_uv) on depth texture according to + // Vt and distance between hit surface and depth map surface, + // repeat until below the surface. + // + // Where `layer_count` is interpolated between `1.0` and + // `max_layer_count` according to the steepness of Vt. + + let view_steepness = abs(Vt.z); + // We mix with minimum value 1.0 because otherwise, + // with 0.0, we get a division by zero in surfaces parallel to viewport, + // resulting in a singularity. + let layer_count = mix(max_layer_count, 1.0, view_steepness); + let layer_depth = 1.0 / layer_count; + var delta_uv = depth_scale * layer_depth * Vt.xy * vec2(1.0, -1.0) / view_steepness; + + var current_layer_depth = 0.0; + var texture_depth = sample_depth_map(uv, material_bind_group_slot); + + // texture_depth > current_layer_depth means the depth map depth is deeper + // than the depth the ray would be at this UV offset so the ray has not + // intersected the surface + for (var i: i32 = 0; texture_depth > current_layer_depth && i <= i32(layer_count); i++) { + current_layer_depth += layer_depth; + uv += delta_uv; + texture_depth = sample_depth_map(uv, material_bind_group_slot); + } + +#ifdef RELIEF_MAPPING + // Relief Mapping + // ============== + // "Refine" the rough result from Steep Parallax Mapping + // with a **binary search** between the layer selected by steep parallax + // and the next one to find a point closer to the depth map surface. + // This reduces the jaggy step artifacts from steep parallax mapping. + + delta_uv *= 0.5; + var delta_depth = 0.5 * layer_depth; + + uv -= delta_uv; + current_layer_depth -= delta_depth; + + for (var i: u32 = 0u; i < max_steps; i++) { + texture_depth = sample_depth_map(uv, material_bind_group_slot); + + // Halve the deltas for the next step + delta_uv *= 0.5; + delta_depth *= 0.5; + + // Step based on whether the current depth is above or below the depth map + if (texture_depth > current_layer_depth) { + uv += delta_uv; + current_layer_depth += delta_depth; + } else { + uv -= delta_uv; + current_layer_depth -= delta_depth; + } + } +#else + // Parallax Occlusion mapping + // ========================== + // "Refine" Steep Parallax Mapping by interpolating between the + // previous layer's depth and the computed layer depth. + // Only requires a single lookup, unlike Relief Mapping, but + // may skip small details and result in writhing material artifacts. + let previous_uv = uv - delta_uv; + let next_depth = texture_depth - current_layer_depth; + let previous_depth = sample_depth_map(previous_uv, material_bind_group_slot) - + current_layer_depth + layer_depth; + + let weight = next_depth / (next_depth - previous_depth); + + uv = mix(uv, previous_uv, weight); + + current_layer_depth += mix(next_depth, previous_depth, weight); +#endif + + // Note: `current_layer_depth` is not returned, but may be useful + // for light computation later on in future improvements of the pbr shader. + return uv; +} diff --git a/crates/libmarathon/src/render/pbr/render/pbr.wgsl b/crates/libmarathon/src/render/pbr/render/pbr.wgsl new file mode 100644 index 0000000..1722ab9 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/pbr.wgsl @@ -0,0 +1,107 @@ +#import bevy_pbr::{ + pbr_types, + pbr_functions::alpha_discard, + pbr_fragment::pbr_input_from_standard_material, + decal::clustered::apply_decal_base_color, +} + +#ifdef PREPASS_PIPELINE +#import bevy_pbr::{ + prepass_io::{VertexOutput, FragmentOutput}, + pbr_deferred_functions::deferred_output, +} +#else +#import bevy_pbr::{ + forward_io::{VertexOutput, FragmentOutput}, + pbr_functions, + pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing}, + pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT, +} +#endif + +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output +#endif + +#ifdef OIT_ENABLED +#import bevy_core_pipeline::oit::oit_draw +#endif // OIT_ENABLED + +#ifdef FORWARD_DECAL +#import bevy_pbr::decal::forward::get_forward_decal_info +#endif + +@fragment +fn fragment( +#ifdef MESHLET_MESH_MATERIAL_PASS + @builtin(position) frag_coord: vec4, +#else + vertex_output: VertexOutput, + @builtin(front_facing) is_front: bool, +#endif +) -> FragmentOutput { +#ifdef MESHLET_MESH_MATERIAL_PASS + let vertex_output = resolve_vertex_output(frag_coord); + let is_front = true; +#endif + + var in = vertex_output; + + // If we're in the crossfade section of a visibility range, conditionally + // discard the fragment according to the visibility pattern. +#ifdef VISIBILITY_RANGE_DITHER + pbr_functions::visibility_range_dither(in.position, in.visibility_range_dither); +#endif + +#ifdef FORWARD_DECAL + let forward_decal_info = get_forward_decal_info(in); + in.world_position = forward_decal_info.world_position; + in.uv = forward_decal_info.uv; +#endif + + // generate a PbrInput struct from the StandardMaterial bindings + var pbr_input = pbr_input_from_standard_material(in, is_front); + + // alpha discard + pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color); + + // clustered decals + pbr_input.material.base_color = apply_decal_base_color( + in.world_position.xyz, + in.position.xy, + pbr_input.material.base_color + ); + +#ifdef PREPASS_PIPELINE + // write the gbuffer, lighting pass id, and optionally normal and motion_vector textures + let out = deferred_output(in, pbr_input); +#else + // in forward mode, we calculate the lit color immediately, and then apply some post-lighting effects here. + // in deferred mode the lit color and these effects will be calculated in the deferred lighting shader + var out: FragmentOutput; + if (pbr_input.material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u { + out.color = apply_pbr_lighting(pbr_input); + } else { + out.color = pbr_input.material.base_color; + } + + // apply in-shader post processing (fog, alpha-premultiply, and also tonemapping, debanding if the camera is non-hdr) + // note this does not include fullscreen postprocessing effects like bloom. + out.color = main_pass_post_lighting_processing(pbr_input, out.color); +#endif + +#ifdef OIT_ENABLED + let alpha_mode = pbr_input.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode != pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE { + // The fragments will only be drawn during the oit resolve pass. + oit_draw(in.position, out.color); + discard; + } +#endif // OIT_ENABLED + +#ifdef FORWARD_DECAL + out.color.a = min(forward_decal_info.alpha, out.color.a); +#endif + + return out; +} diff --git a/crates/libmarathon/src/render/pbr/render/pbr_ambient.wgsl b/crates/libmarathon/src/render/pbr/render/pbr_ambient.wgsl new file mode 100644 index 0000000..7b174da --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/pbr_ambient.wgsl @@ -0,0 +1,29 @@ +#define_import_path bevy_pbr::ambient + +#import bevy_pbr::{ + lighting::{EnvBRDFApprox, F_AB}, + mesh_view_bindings::lights, +} + +// A precomputed `NdotV` is provided because it is computed regardless, +// but `world_normal` and the view vector `V` are provided separately for more advanced uses. +fn ambient_light( + world_position: vec4, + world_normal: vec3, + V: vec3, + NdotV: f32, + diffuse_color: vec3, + specular_color: vec3, + perceptual_roughness: f32, + occlusion: vec3, +) -> vec3 { + let diffuse_ambient = EnvBRDFApprox(diffuse_color, F_AB(1.0, NdotV)); + let specular_ambient = EnvBRDFApprox(specular_color, F_AB(perceptual_roughness, NdotV)); + + // No real world material has specular values under 0.02, so we use this range as a + // "pre-baked specular occlusion" that extinguishes the fresnel term, for artistic control. + // See: https://google.github.io/filament/Filament.html#specularocclusion + let specular_occlusion = saturate(dot(specular_color, vec3(50.0 * 0.33))); + + return (diffuse_ambient + specular_ambient * specular_occlusion) * lights.ambient_color.rgb * occlusion; +} diff --git a/crates/libmarathon/src/render/pbr/render/pbr_bindings.wgsl b/crates/libmarathon/src/render/pbr/render/pbr_bindings.wgsl new file mode 100644 index 0000000..6d21c81 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/pbr_bindings.wgsl @@ -0,0 +1,89 @@ +#define_import_path bevy_pbr::pbr_bindings + +#import bevy_pbr::pbr_types::StandardMaterial + +#ifdef BINDLESS +struct StandardMaterialBindings { + material: u32, // 0 + base_color_texture: u32, // 1 + base_color_sampler: u32, // 2 + emissive_texture: u32, // 3 + emissive_sampler: u32, // 4 + metallic_roughness_texture: u32, // 5 + metallic_roughness_sampler: u32, // 6 + occlusion_texture: u32, // 7 + occlusion_sampler: u32, // 8 + normal_map_texture: u32, // 9 + normal_map_sampler: u32, // 10 + depth_map_texture: u32, // 11 + depth_map_sampler: u32, // 12 + anisotropy_texture: u32, // 13 + anisotropy_sampler: u32, // 14 + specular_transmission_texture: u32, // 15 + specular_transmission_sampler: u32, // 16 + thickness_texture: u32, // 17 + thickness_sampler: u32, // 18 + diffuse_transmission_texture: u32, // 19 + diffuse_transmission_sampler: u32, // 20 + clearcoat_texture: u32, // 21 + clearcoat_sampler: u32, // 22 + clearcoat_roughness_texture: u32, // 23 + clearcoat_roughness_sampler: u32, // 24 + clearcoat_normal_texture: u32, // 25 + clearcoat_normal_sampler: u32, // 26 + specular_texture: u32, // 27 + specular_sampler: u32, // 28 + specular_tint_texture: u32, // 29 + specular_tint_sampler: u32, // 30 +} + +@group(#{MATERIAL_BIND_GROUP}) @binding(0) var material_indices: array; +@group(#{MATERIAL_BIND_GROUP}) @binding(10) var material_array: array; + +#else // BINDLESS + +@group(#{MATERIAL_BIND_GROUP}) @binding(0) var material: StandardMaterial; +@group(#{MATERIAL_BIND_GROUP}) @binding(1) var base_color_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(2) var base_color_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(3) var emissive_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(4) var emissive_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(5) var metallic_roughness_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(6) var metallic_roughness_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(7) var occlusion_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(8) var occlusion_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(9) var normal_map_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(10) var normal_map_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(11) var depth_map_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(12) var depth_map_sampler: sampler; + +#ifdef PBR_ANISOTROPY_TEXTURE_SUPPORTED +@group(#{MATERIAL_BIND_GROUP}) @binding(13) var anisotropy_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(14) var anisotropy_sampler: sampler; +#endif // PBR_ANISOTROPY_TEXTURE_SUPPORTED + +#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED +@group(#{MATERIAL_BIND_GROUP}) @binding(15) var specular_transmission_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(16) var specular_transmission_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(17) var thickness_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(18) var thickness_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(19) var diffuse_transmission_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(20) var diffuse_transmission_sampler: sampler; +#endif // PBR_TRANSMISSION_TEXTURES_SUPPORTED + +#ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED +@group(#{MATERIAL_BIND_GROUP}) @binding(21) var clearcoat_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(22) var clearcoat_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(23) var clearcoat_roughness_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(24) var clearcoat_roughness_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(25) var clearcoat_normal_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(26) var clearcoat_normal_sampler: sampler; +#endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED + +#ifdef PBR_SPECULAR_TEXTURES_SUPPORTED +@group(#{MATERIAL_BIND_GROUP}) @binding(27) var specular_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(28) var specular_sampler: sampler; +@group(#{MATERIAL_BIND_GROUP}) @binding(29) var specular_tint_texture: texture_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(30) var specular_tint_sampler: sampler; +#endif // PBR_SPECULAR_TEXTURES_SUPPORTED + +#endif // BINDLESS diff --git a/crates/libmarathon/src/render/pbr/render/pbr_fragment.wgsl b/crates/libmarathon/src/render/pbr/render/pbr_fragment.wgsl new file mode 100644 index 0000000..a78abcb --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/pbr_fragment.wgsl @@ -0,0 +1,844 @@ +#define_import_path bevy_pbr::pbr_fragment + +#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d} + +#import bevy_pbr::{ + pbr_functions, + pbr_functions::SampleBias, + pbr_bindings, + pbr_types, + prepass_utils, + lighting, + mesh_bindings::mesh, + mesh_view_bindings::view, + parallax_mapping::parallaxed_uv, + lightmap::lightmap, +} + +#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION +#import bevy_pbr::mesh_view_bindings::screen_space_ambient_occlusion_texture +#import bevy_pbr::ssao_utils::ssao_multibounce +#endif + +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput +#else ifdef PREPASS_PIPELINE +#import bevy_pbr::prepass_io::VertexOutput +#else +#import bevy_pbr::forward_io::VertexOutput +#endif + +#ifdef BINDLESS +#import bevy_pbr::pbr_bindings::material_indices +#endif // BINDLESS + +// prepare a basic PbrInput from the vertex stage output, mesh binding and view binding +fn pbr_input_from_vertex_output( + in: VertexOutput, + is_front: bool, + double_sided: bool, +) -> pbr_types::PbrInput { + var pbr_input: pbr_types::PbrInput = pbr_types::pbr_input_new(); + +#ifdef MESHLET_MESH_MATERIAL_PASS + pbr_input.flags = in.mesh_flags; +#else + pbr_input.flags = mesh[in.instance_index].flags; +#endif + + pbr_input.is_orthographic = view.clip_from_view[3].w == 1.0; + pbr_input.V = pbr_functions::calculate_view(in.world_position, pbr_input.is_orthographic); + pbr_input.frag_coord = in.position; + pbr_input.world_position = in.world_position; + +#ifdef VERTEX_COLORS + pbr_input.material.base_color = in.color; +#endif + + pbr_input.world_normal = pbr_functions::prepare_world_normal( + in.world_normal, + double_sided, + is_front, + ); + +#ifdef LOAD_PREPASS_NORMALS + pbr_input.N = prepass_utils::prepass_normal(in.position, 0u); +#else + pbr_input.N = normalize(pbr_input.world_normal); +#endif + + return pbr_input; +} + +// Prepare a full PbrInput by sampling all textures to resolve +// the material members +fn pbr_input_from_standard_material( + in: VertexOutput, + is_front: bool, +) -> pbr_types::PbrInput { +#ifdef MESHLET_MESH_MATERIAL_PASS + let slot = in.material_bind_group_slot; +#else // MESHLET_MESH_MATERIAL_PASS + let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + let flags = pbr_bindings::material_array[material_indices[slot].material].flags; + let base_color = pbr_bindings::material_array[material_indices[slot].material].base_color; + let deferred_lighting_pass_id = + pbr_bindings::material_array[material_indices[slot].material].deferred_lighting_pass_id; +#else // BINDLESS + let flags = pbr_bindings::material.flags; + let base_color = pbr_bindings::material.base_color; + let deferred_lighting_pass_id = pbr_bindings::material.deferred_lighting_pass_id; +#endif + + let double_sided = (flags & pbr_types::STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u; + + var pbr_input: pbr_types::PbrInput = pbr_input_from_vertex_output(in, is_front, double_sided); + pbr_input.material.flags = flags; + pbr_input.material.base_color *= base_color; + pbr_input.material.deferred_lighting_pass_id = deferred_lighting_pass_id; + + // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" + let NdotV = max(dot(pbr_input.N, pbr_input.V), 0.0001); + + // Fill in the sample bias so we can sample from textures. + var bias: SampleBias; +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv = in.ddx_uv; + bias.ddy_uv = in.ddy_uv; +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias = view.mip_bias; +#endif // MESHLET_MESH_MATERIAL_PASS + +// TODO: Transforming UVs mean we need to apply derivative chain rule for meshlet mesh material pass +#ifdef VERTEX_UVS + +#ifdef BINDLESS + let uv_transform = pbr_bindings::material_array[material_indices[slot].material].uv_transform; +#else // BINDLESS + let uv_transform = pbr_bindings::material.uv_transform; +#endif // BINDLESS + +pbr_input.material.uv_transform = uv_transform; + +#ifdef VERTEX_UVS_A + var uv = (uv_transform * vec3(in.uv, 1.0)).xy; +#endif + +// TODO: Transforming UVs mean we need to apply derivative chain rule for meshlet mesh material pass +#ifdef VERTEX_UVS_B + var uv_b = (uv_transform * vec3(in.uv_b, 1.0)).xy; +#else + var uv_b = uv; +#endif + +#ifdef VERTEX_TANGENTS + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT) != 0u) { + let V = pbr_input.V; + let TBN = pbr_functions::calculate_tbn_mikktspace(in.world_normal, in.world_tangent); + let T = TBN[0]; + let B = TBN[1]; + let N = TBN[2]; + // Transform V from fragment to camera in world space to tangent space. + let Vt = vec3(dot(V, T), dot(V, B), dot(V, N)); +#ifdef VERTEX_UVS_A + // TODO: Transforming UVs mean we need to apply derivative chain rule for meshlet mesh material pass + uv = parallaxed_uv( +#ifdef BINDLESS + pbr_bindings::material_array[material_indices[slot].material].parallax_depth_scale, + pbr_bindings::material_array[material_indices[slot].material].max_parallax_layer_count, + pbr_bindings::material_array[material_indices[slot].material].max_relief_mapping_search_steps, +#else // BINDLESS + pbr_bindings::material.parallax_depth_scale, + pbr_bindings::material.max_parallax_layer_count, + pbr_bindings::material.max_relief_mapping_search_steps, +#endif // BINDLESS + uv, + // Flip the direction of Vt to go toward the surface to make the + // parallax mapping algorithm easier to understand and reason + // about. + -Vt, + slot, + ); +#endif + +#ifdef VERTEX_UVS_B + // TODO: Transforming UVs mean we need to apply derivative chain rule for meshlet mesh material pass + uv_b = parallaxed_uv( +#ifdef BINDLESS + pbr_bindings::material_array[material_indices[slot].material].parallax_depth_scale, + pbr_bindings::material_array[material_indices[slot].material].max_parallax_layer_count, + pbr_bindings::material_array[material_indices[slot].material].max_relief_mapping_search_steps, +#else // BINDLESS + pbr_bindings::material.parallax_depth_scale, + pbr_bindings::material.max_parallax_layer_count, + pbr_bindings::material.max_relief_mapping_search_steps, +#endif // BINDLESS + uv_b, + // Flip the direction of Vt to go toward the surface to make the + // parallax mapping algorithm easier to understand and reason + // about. + -Vt, + slot, + ); +#else + uv_b = uv; +#endif + } +#endif // VERTEX_TANGENTS + + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u) { + pbr_input.material.base_color *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].base_color_texture], + bindless_samplers_filtering[material_indices[slot].base_color_sampler], +#else // BINDLESS + pbr_bindings::base_color_texture, + pbr_bindings::base_color_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_BASE_COLOR_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ); + +#ifdef ALPHA_TO_COVERAGE + // Sharpen alpha edges. + // + // https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f + let alpha_mode = flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE { + +#ifdef BINDLESS + let alpha_cutoff = pbr_bindings::material_array[material_indices[slot].material].alpha_cutoff; +#else // BINDLESS + let alpha_cutoff = pbr_bindings::material.alpha_cutoff; +#endif // BINDLESS + + pbr_input.material.base_color.a = (pbr_input.material.base_color.a - alpha_cutoff) / + max(fwidth(pbr_input.material.base_color.a), 0.0001) + 0.5; + } +#endif // ALPHA_TO_COVERAGE + + } +#endif // VERTEX_UVS + + pbr_input.material.flags = flags; + + // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u) { +#ifdef BINDLESS + pbr_input.material.ior = pbr_bindings::material_array[material_indices[slot].material].ior; + pbr_input.material.attenuation_color = + pbr_bindings::material_array[material_indices[slot].material].attenuation_color; + pbr_input.material.attenuation_distance = + pbr_bindings::material_array[material_indices[slot].material].attenuation_distance; + pbr_input.material.alpha_cutoff = + pbr_bindings::material_array[material_indices[slot].material].alpha_cutoff; +#else // BINDLESS + pbr_input.material.ior = pbr_bindings::material.ior; + pbr_input.material.attenuation_color = pbr_bindings::material.attenuation_color; + pbr_input.material.attenuation_distance = pbr_bindings::material.attenuation_distance; + pbr_input.material.alpha_cutoff = pbr_bindings::material.alpha_cutoff; +#endif // BINDLESS + + // reflectance +#ifdef BINDLESS + pbr_input.material.reflectance = + pbr_bindings::material_array[material_indices[slot].material].reflectance; +#else // BINDLESS + pbr_input.material.reflectance = pbr_bindings::material.reflectance; +#endif // BINDLESS + +#ifdef PBR_SPECULAR_TEXTURES_SUPPORTED +#ifdef VERTEX_UVS + + // Specular texture + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TEXTURE_BIT) != 0u) { + let specular = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].specular_texture], + bindless_samplers_filtering[material_indices[slot].specular_sampler], +#else // BINDLESS + pbr_bindings::specular_texture, + pbr_bindings::specular_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_SPECULAR_UV_B + uv_b, +#else // STANDARD_MATERIAL_SPECULAR_UV_B + uv, +#endif // STANDARD_MATERIAL_SPECULAR_UV_B +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).a; + // This 0.5 factor is from the `KHR_materials_specular` specification: + // + pbr_input.material.reflectance *= specular * 0.5; + } + + // Specular tint texture + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TINT_TEXTURE_BIT) != 0u) { + let specular_tint = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].specular_tint_texture], + bindless_samplers_filtering[material_indices[slot].specular_tint_sampler], +#else // BINDLESS + pbr_bindings::specular_tint_texture, + pbr_bindings::specular_tint_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_SPECULAR_TINT_UV_B + uv_b, +#else // STANDARD_MATERIAL_SPECULAR_TINT_UV_B + uv, +#endif // STANDARD_MATERIAL_SPECULAR_TINT_UV_B +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).rgb; + pbr_input.material.reflectance *= specular_tint; + } + +#endif // VERTEX_UVS +#endif // PBR_SPECULAR_TEXTURES_SUPPORTED + + // emissive +#ifdef BINDLESS + var emissive: vec4 = pbr_bindings::material_array[material_indices[slot].material].emissive; +#else // BINDLESS + var emissive: vec4 = pbr_bindings::material.emissive; +#endif // BINDLESS + +#ifdef VERTEX_UVS + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) { + emissive = vec4(emissive.rgb * +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].emissive_texture], + bindless_samplers_filtering[material_indices[slot].emissive_sampler], +#else // BINDLESS + pbr_bindings::emissive_texture, + pbr_bindings::emissive_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_EMISSIVE_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).rgb, + emissive.a); + } +#endif + pbr_input.material.emissive = emissive; + + // metallic and perceptual roughness +#ifdef BINDLESS + var metallic: f32 = pbr_bindings::material_array[material_indices[slot].material].metallic; + var perceptual_roughness: f32 = pbr_bindings::material_array[material_indices[slot].material].perceptual_roughness; +#else // BINDLESS + var metallic: f32 = pbr_bindings::material.metallic; + var perceptual_roughness: f32 = pbr_bindings::material.perceptual_roughness; +#endif // BINDLESS + +#ifdef VERTEX_UVS + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) { + let metallic_roughness = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].metallic_roughness_texture], + bindless_samplers_filtering[material_indices[slot].metallic_roughness_sampler], +#else // BINDLESS + pbr_bindings::metallic_roughness_texture, + pbr_bindings::metallic_roughness_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_METALLIC_ROUGHNESS_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ); + // Sampling from GLTF standard channels for now + metallic *= metallic_roughness.b; + perceptual_roughness *= metallic_roughness.g; + } +#endif + pbr_input.material.metallic = metallic; + pbr_input.material.perceptual_roughness = perceptual_roughness; + + // Clearcoat factor +#ifdef BINDLESS + pbr_input.material.clearcoat = + pbr_bindings::material_array[material_indices[slot].material].clearcoat; +#else // BINDLESS + pbr_input.material.clearcoat = pbr_bindings::material.clearcoat; +#endif // BINDLESS + +#ifdef VERTEX_UVS +#ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_CLEARCOAT_TEXTURE_BIT) != 0u) { + pbr_input.material.clearcoat *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].clearcoat_texture], + bindless_samplers_filtering[material_indices[slot].clearcoat_sampler], +#else // BINDLESS + pbr_bindings::clearcoat_texture, + pbr_bindings::clearcoat_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_CLEARCOAT_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).r; + } +#endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED +#endif // VERTEX_UVS + + // Clearcoat roughness +#ifdef BINDLESS + pbr_input.material.clearcoat_perceptual_roughness = + pbr_bindings::material_array[material_indices[slot].material].clearcoat_perceptual_roughness; +#else // BINDLESS + pbr_input.material.clearcoat_perceptual_roughness = + pbr_bindings::material.clearcoat_perceptual_roughness; +#endif // BINDLESS + +#ifdef VERTEX_UVS +#ifdef PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_CLEARCOAT_ROUGHNESS_TEXTURE_BIT) != 0u) { + pbr_input.material.clearcoat_perceptual_roughness *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].clearcoat_roughness_texture], + bindless_samplers_filtering[material_indices[slot].clearcoat_roughness_sampler], +#else // BINDLESS + pbr_bindings::clearcoat_roughness_texture, + pbr_bindings::clearcoat_roughness_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_CLEARCOAT_ROUGHNESS_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).g; + } +#endif // PBR_MULTI_LAYER_MATERIAL_TEXTURES_SUPPORTED +#endif // VERTEX_UVS + +#ifdef BINDLESS + var specular_transmission: f32 = pbr_bindings::material_array[slot].specular_transmission; +#else // BINDLESS + var specular_transmission: f32 = pbr_bindings::material.specular_transmission; +#endif // BINDLESS + +#ifdef VERTEX_UVS +#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT) != 0u) { + specular_transmission *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[ + material_indices[slot].specular_transmission_texture + ], + bindless_samplers_filtering[ + material_indices[slot].specular_transmission_sampler + ], +#else // BINDLESS + pbr_bindings::specular_transmission_texture, + pbr_bindings::specular_transmission_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_SPECULAR_TRANSMISSION_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).r; + } +#endif +#endif + pbr_input.material.specular_transmission = specular_transmission; + +#ifdef BINDLESS + var thickness: f32 = pbr_bindings::material_array[material_indices[slot].material].thickness; +#else // BINDLESS + var thickness: f32 = pbr_bindings::material.thickness; +#endif // BINDLESS + +#ifdef VERTEX_UVS +#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT) != 0u) { + thickness *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].thickness_texture], + bindless_samplers_filtering[material_indices[slot].thickness_sampler], +#else // BINDLESS + pbr_bindings::thickness_texture, + pbr_bindings::thickness_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_THICKNESS_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).g; + } +#endif +#endif + // scale thickness, accounting for non-uniform scaling (e.g. a “squished” mesh) + // TODO: Meshlet support +#ifndef MESHLET_MESH_MATERIAL_PASS + thickness *= length( + (transpose(mesh[in.instance_index].world_from_local) * vec4(pbr_input.N, 0.0)).xyz + ); +#endif + pbr_input.material.thickness = thickness; + +#ifdef BINDLESS + var diffuse_transmission = + pbr_bindings::material_array[material_indices[slot].material].diffuse_transmission; +#else // BINDLESS + var diffuse_transmission = pbr_bindings::material.diffuse_transmission; +#endif // BINDLESS + +#ifdef VERTEX_UVS +#ifdef PBR_TRANSMISSION_TEXTURES_SUPPORTED + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT) != 0u) { + diffuse_transmission *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].diffuse_transmission_texture], + bindless_samplers_filtering[material_indices[slot].diffuse_transmission_sampler], +#else // BINDLESS + pbr_bindings::diffuse_transmission_texture, + pbr_bindings::diffuse_transmission_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).a; + } +#endif +#endif + pbr_input.material.diffuse_transmission = diffuse_transmission; + + var diffuse_occlusion: vec3 = vec3(1.0); + var specular_occlusion: f32 = 1.0; +#ifdef VERTEX_UVS + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) { + diffuse_occlusion *= +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].occlusion_texture], + bindless_samplers_filtering[material_indices[slot].occlusion_sampler], +#else // BINDLESS + pbr_bindings::occlusion_texture, + pbr_bindings::occlusion_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_OCCLUSION_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).r; + } +#endif +#ifdef SCREEN_SPACE_AMBIENT_OCCLUSION + let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2(in.position.xy), 0i).r; + let ssao_multibounce = ssao_multibounce(ssao, pbr_input.material.base_color.rgb); + diffuse_occlusion = min(diffuse_occlusion, ssao_multibounce); + // Use SSAO to estimate the specular occlusion. + // Lagarde and Rousiers 2014, "Moving Frostbite to Physically Based Rendering" + let roughness = lighting::perceptualRoughnessToRoughness(pbr_input.material.perceptual_roughness); + specular_occlusion = saturate(pow(NdotV + ssao, exp2(-16.0 * roughness - 1.0)) - 1.0 + ssao); +#endif + pbr_input.diffuse_occlusion = diffuse_occlusion; + pbr_input.specular_occlusion = specular_occlusion; + + // N (normal vector) +#ifndef LOAD_PREPASS_NORMALS + + pbr_input.N = normalize(pbr_input.world_normal); + pbr_input.clearcoat_N = pbr_input.N; + +#ifdef VERTEX_UVS +#ifdef VERTEX_TANGENTS + + let TBN = pbr_functions::calculate_tbn_mikktspace(pbr_input.world_normal, in.world_tangent); + +#ifdef STANDARD_MATERIAL_NORMAL_MAP + + let Nt = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].normal_map_texture], + bindless_samplers_filtering[material_indices[slot].normal_map_sampler], +#else // BINDLESS + pbr_bindings::normal_map_texture, + pbr_bindings::normal_map_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_NORMAL_MAP_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).rgb; + + pbr_input.N = pbr_functions::apply_normal_mapping(flags, TBN, double_sided, is_front, Nt); + +#endif // STANDARD_MATERIAL_NORMAL_MAP + +#ifdef STANDARD_MATERIAL_CLEARCOAT + + // Note: `KHR_materials_clearcoat` specifies that, if there's no + // clearcoat normal map, we must set the normal to the mesh's normal, + // and not to the main layer's bumped normal. + +#ifdef STANDARD_MATERIAL_CLEARCOAT_NORMAL_MAP + + let clearcoat_Nt = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].clearcoat_normal_texture], + bindless_samplers_filtering[material_indices[slot].clearcoat_normal_sampler], +#else // BINDLESS + pbr_bindings::clearcoat_normal_texture, + pbr_bindings::clearcoat_normal_sampler, +#endif // BINDLESS +#ifdef STANDARD_MATERIAL_CLEARCOAT_NORMAL_UV_B + uv_b, +#else + uv, +#endif +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).rgb; + + pbr_input.clearcoat_N = pbr_functions::apply_normal_mapping( + flags, + TBN, + double_sided, + is_front, + clearcoat_Nt, + ); + +#endif // STANDARD_MATERIAL_CLEARCOAT_NORMAL_MAP + +#endif // STANDARD_MATERIAL_CLEARCOAT + +#endif // VERTEX_TANGENTS +#endif // VERTEX_UVS + + // Take anisotropy into account. + // + // This code comes from the `KHR_materials_anisotropy` spec: + // +#ifdef PBR_ANISOTROPY_TEXTURE_SUPPORTED +#ifdef VERTEX_TANGENTS +#ifdef STANDARD_MATERIAL_ANISOTROPY + +#ifdef BINDLESS + var anisotropy_strength = + pbr_bindings::material_array[material_indices[slot].material].anisotropy_strength; + var anisotropy_direction = + pbr_bindings::material_array[material_indices[slot].material].anisotropy_rotation; +#else // BINDLESS + var anisotropy_strength = pbr_bindings::material.anisotropy_strength; + var anisotropy_direction = pbr_bindings::material.anisotropy_rotation; +#endif // BINDLESS + + // Adjust based on the anisotropy map if there is one. + if ((flags & pbr_types::STANDARD_MATERIAL_FLAGS_ANISOTROPY_TEXTURE_BIT) != 0u) { + let anisotropy_texel = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].anisotropy_texture], + bindless_samplers_filtering[material_indices[slot].anisotropy_sampler], +#else // BINDLESS + pbr_bindings::anisotropy_texture, + pbr_bindings::anisotropy_sampler, +#endif +#ifdef STANDARD_MATERIAL_ANISOTROPY_UV_B + uv_b, +#else // STANDARD_MATERIAL_ANISOTROPY_UV_B + uv, +#endif // STANDARD_MATERIAL_ANISOTROPY_UV_B +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).rgb; + + let anisotropy_direction_from_texture = normalize(anisotropy_texel.rg * 2.0 - 1.0); + // Rotate by the anisotropy direction. + anisotropy_direction = + mat2x2(anisotropy_direction.xy, anisotropy_direction.yx * vec2(-1.0, 1.0)) * + anisotropy_direction_from_texture; + anisotropy_strength *= anisotropy_texel.b; + } + + pbr_input.anisotropy_strength = anisotropy_strength; + + let anisotropy_T = normalize(TBN * vec3(anisotropy_direction, 0.0)); + let anisotropy_B = normalize(cross(pbr_input.world_normal, anisotropy_T)); + pbr_input.anisotropy_T = anisotropy_T; + pbr_input.anisotropy_B = anisotropy_B; + +#endif // STANDARD_MATERIAL_ANISOTROPY +#endif // VERTEX_TANGENTS +#endif // PBR_ANISOTROPY_TEXTURE_SUPPORTED + +#endif // LOAD_PREPASS_NORMALS + +// TODO: Meshlet support +#ifdef LIGHTMAP + +#ifdef BINDLESS + let lightmap_exposure = + pbr_bindings::material_array[material_indices[slot].material].lightmap_exposure; +#else // BINDLESS + let lightmap_exposure = pbr_bindings::material.lightmap_exposure; +#endif // BINDLESS + + pbr_input.lightmap_light = lightmap(in.uv_b, lightmap_exposure, in.instance_index); +#endif + } + + return pbr_input; +} diff --git a/crates/libmarathon/src/render/pbr/render/pbr_functions.wgsl b/crates/libmarathon/src/render/pbr/render/pbr_functions.wgsl new file mode 100644 index 0000000..2c86295 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/pbr_functions.wgsl @@ -0,0 +1,883 @@ +#define_import_path bevy_pbr::pbr_functions + +#import bevy_pbr::{ + pbr_types, + pbr_bindings, + mesh_view_bindings as view_bindings, + mesh_view_types, + lighting, + lighting::{LAYER_BASE, LAYER_CLEARCOAT}, + transmission, + clustered_forward as clustering, + shadows, + ambient, + irradiance_volume, + mesh_types::{MESH_FLAGS_SHADOW_RECEIVER_BIT, MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT}, +} +#import bevy_render::maths::{E, powsafe} + +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput +#else ifdef PREPASS_PIPELINE +#import bevy_pbr::prepass_io::VertexOutput +#else // PREPASS_PIPELINE +#import bevy_pbr::forward_io::VertexOutput +#endif // PREPASS_PIPELINE + +#ifdef ENVIRONMENT_MAP +#import bevy_pbr::environment_map +#endif + +#ifdef TONEMAP_IN_SHADER +#import bevy_core_pipeline::tonemapping::{tone_mapping, screen_space_dither} +#endif + + +// Biasing info needed to sample from a texture. How this is done depends on +// whether we're rendering meshlets or regular meshes. +struct SampleBias { +#ifdef MESHLET_MESH_MATERIAL_PASS + ddx_uv: vec2, + ddy_uv: vec2, +#else // MESHLET_MESH_MATERIAL_PASS + mip_bias: f32, +#endif // MESHLET_MESH_MATERIAL_PASS +} + +// This is the standard 4x4 ordered dithering pattern from [1]. +// +// We can't use `array, 4>` because they can't be indexed dynamically +// due to Naga limitations. So instead we pack into a single `vec4` and extract +// individual bytes. +// +// [1]: https://en.wikipedia.org/wiki/Ordered_dithering#Threshold_map +const DITHER_THRESHOLD_MAP: vec4 = vec4( + 0x0a020800, + 0x060e040c, + 0x09010b03, + 0x050d070f +); + +// Processes a visibility range dither value and discards the fragment if +// needed. +// +// Visibility ranges, also known as HLODs, are crossfades between different +// levels of detail. +// +// The `dither` value ranges from [-16, 16]. When zooming out, positive values +// are used for meshes that are in the process of disappearing, while negative +// values are used for meshes that are in the process of appearing. In other +// words, when the camera is moving backwards, the `dither` value counts up from +// -16 to 0 when the object is fading in, stays at 0 while the object is +// visible, and then counts up to 16 while the object is fading out. +// Distinguishing between negative and positive values allows the dither +// patterns for different LOD levels of a single mesh to mesh together properly. +#ifdef VISIBILITY_RANGE_DITHER +fn visibility_range_dither(frag_coord: vec4, dither: i32) { + // If `dither` is 0, the object is visible. + if (dither == 0) { + return; + } + + // If `dither` is less than -15 or greater than 15, the object is culled. + if (dither <= -16 || dither >= 16) { + discard; + } + + // Otherwise, check the dither pattern. + let coords = vec2(floor(frag_coord.xy)) % 4u; + let threshold = i32((DITHER_THRESHOLD_MAP[coords.y] >> (coords.x * 8)) & 0xff); + if ((dither >= 0 && dither + threshold >= 16) || (dither < 0 && 1 + dither + threshold <= 0)) { + discard; + } +} +#endif + +fn alpha_discard(material: pbr_types::StandardMaterial, output_color: vec4) -> vec4 { + var color = output_color; + let alpha_mode = material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE { + // NOTE: If rendering as opaque, alpha should be ignored so set to 1.0 + color.a = 1.0; + } + +#ifdef MAY_DISCARD + // NOTE: `MAY_DISCARD` is only defined in the alpha to coverage case if MSAA + // was off. This special situation causes alpha to coverage to fall back to + // alpha mask. + else if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK || + alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE { + if color.a >= material.alpha_cutoff { + // NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque + color.a = 1.0; + } else { + // NOTE: output_color.a < in.material.alpha_cutoff should not be rendered + discard; + } + } +#endif + + return color; +} + +fn prepare_world_normal( + world_normal: vec3, + double_sided: bool, + is_front: bool, +) -> vec3 { + var output: vec3 = world_normal; +#ifndef VERTEX_TANGENTS +#ifndef STANDARD_MATERIAL_NORMAL_MAP + // NOTE: When NOT using normal-mapping, if looking at the back face of a double-sided + // material, the normal needs to be inverted. This is a branchless version of that. + output = (f32(!double_sided || is_front) * 2.0 - 1.0) * output; +#endif +#endif + return output; +} + +// Calculates the three TBN vectors according to [mikktspace]. Returns a matrix +// with T, B, N columns in that order. +// +// [mikktspace]: http://www.mikktspace.com/ +fn calculate_tbn_mikktspace(world_normal: vec3, world_tangent: vec4) -> mat3x3 { + // NOTE: The mikktspace method of normal mapping explicitly requires that the world normal NOT + // be re-normalized in the fragment shader. This is primarily to match the way mikktspace + // bakes vertex tangents and normal maps so that this is the exact inverse. Blender, Unity, + // Unreal Engine, Godot, and more all use the mikktspace method. Do not change this code + // unless you really know what you are doing. + // http://www.mikktspace.com/ + var N: vec3 = world_normal; + + // NOTE: The mikktspace method of normal mapping explicitly requires that these NOT be + // normalized nor any Gram-Schmidt applied to ensure the vertex normal is orthogonal to the + // vertex tangent! Do not change this code unless you really know what you are doing. + // http://www.mikktspace.com/ + var T: vec3 = world_tangent.xyz; + var B: vec3 = world_tangent.w * cross(N, T); + +#ifdef MESHLET_MESH_MATERIAL_PASS + // https://www.jeremyong.com/graphics/2023/12/16/surface-gradient-bump-mapping/#a-note-on-mikktspace-usage + let inverse_length_n = 1.0 / length(N); + T *= inverse_length_n; + B *= inverse_length_n; + N *= inverse_length_n; +#endif + + return mat3x3(T, B, N); +} + +fn apply_normal_mapping( + standard_material_flags: u32, + TBN: mat3x3, + double_sided: bool, + is_front: bool, + in_Nt: vec3, +) -> vec3 { + // Unpack the TBN vectors. + var T = TBN[0]; + var B = TBN[1]; + var N = TBN[2]; + + // Nt is the tangent-space normal. + var Nt = in_Nt; + if (standard_material_flags & pbr_types::STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u { + // Only use the xy components and derive z for 2-component normal maps. + Nt = vec3(Nt.rg * 2.0 - 1.0, 0.0); + Nt.z = sqrt(1.0 - Nt.x * Nt.x - Nt.y * Nt.y); + } else { + Nt = Nt * 2.0 - 1.0; + } + // Normal maps authored for DirectX require flipping the y component + if (standard_material_flags & pbr_types::STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y) != 0u { + Nt.y = -Nt.y; + } + + if double_sided && !is_front { + Nt = -Nt; + } + + // NOTE: The mikktspace method of normal mapping applies maps the tangent-space normal from + // the normal map texture in this way to be an EXACT inverse of how the normal map baker + // calculates the normal maps so there is no error introduced. Do not change this code + // unless you really know what you are doing. + // http://www.mikktspace.com/ + N = Nt.x * T + Nt.y * B + Nt.z * N; + + return normalize(N); +} + +#ifdef STANDARD_MATERIAL_ANISOTROPY + +// Modifies the normal to achieve a better approximate direction from the +// environment map when using anisotropy. +// +// This follows the suggested implementation in the `KHR_materials_anisotropy` specification: +// https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md#image-based-lighting +fn bend_normal_for_anisotropy(lighting_input: ptr) { + // Unpack. + let N = (*lighting_input).layers[LAYER_BASE].N; + let roughness = (*lighting_input).layers[LAYER_BASE].roughness; + let V = (*lighting_input).V; + let anisotropy = (*lighting_input).anisotropy; + let Ba = (*lighting_input).Ba; + + var bent_normal = normalize(cross(cross(Ba, V), Ba)); + + // The `KHR_materials_anisotropy` spec states: + // + // > This heuristic can probably be improved upon + let a = pow(2.0, pow(2.0, 1.0 - anisotropy * (1.0 - roughness))); + bent_normal = normalize(mix(bent_normal, N, a)); + + // The `KHR_materials_anisotropy` spec states: + // + // > Mixing the reflection with the normal is more accurate both with and + // > without anisotropy and keeps rough objects from gathering light from + // > behind their tangent plane. + let R = normalize(mix(reflect(-V, bent_normal), bent_normal, roughness * roughness)); + + (*lighting_input).layers[LAYER_BASE].N = bent_normal; + (*lighting_input).layers[LAYER_BASE].R = R; +} + +#endif // STANDARD_MATERIAL_ANISOTROPY + +// NOTE: Correctly calculates the view vector depending on whether +// the projection is orthographic or perspective. +fn calculate_view( + world_position: vec4, + is_orthographic: bool, +) -> vec3 { + var V: vec3; + if is_orthographic { + // Orthographic view vector + V = normalize(vec3(view_bindings::view.clip_from_world[0].z, view_bindings::view.clip_from_world[1].z, view_bindings::view.clip_from_world[2].z)); + } else { + // Only valid for a perspective projection + V = normalize(view_bindings::view.world_position.xyz - world_position.xyz); + } + return V; +} + +// Diffuse strength is inversely related to metallicity, specular and diffuse transmission +fn calculate_diffuse_color( + base_color: vec3, + metallic: f32, + specular_transmission: f32, + diffuse_transmission: f32 +) -> vec3 { + return base_color * (1.0 - metallic) * (1.0 - specular_transmission) * + (1.0 - diffuse_transmission); +} + +// Remapping [0,1] reflectance to F0 +// See https://google.github.io/filament/Filament.html#materialsystem/parameterization/remapping +fn calculate_F0(base_color: vec3, metallic: f32, reflectance: vec3) -> vec3 { + return 0.16 * reflectance * reflectance * (1.0 - metallic) + base_color * metallic; +} + +#ifndef PREPASS_FRAGMENT +fn apply_pbr_lighting( + in: pbr_types::PbrInput, +) -> vec4 { + var output_color: vec4 = in.material.base_color; + + let emissive = in.material.emissive; + + // calculate non-linear roughness from linear perceptualRoughness + let metallic = in.material.metallic; + let perceptual_roughness = in.material.perceptual_roughness; + let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); + let ior = in.material.ior; + let thickness = in.material.thickness; + let reflectance = in.material.reflectance; + let diffuse_transmission = in.material.diffuse_transmission; + let specular_transmission = in.material.specular_transmission; + + let specular_transmissive_color = specular_transmission * in.material.base_color.rgb; + + let diffuse_occlusion = in.diffuse_occlusion; + let specular_occlusion = in.specular_occlusion; + + // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" + let NdotV = max(dot(in.N, in.V), 0.0001); + let R = reflect(-in.V, in.N); + +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Do the above calculations again for the clearcoat layer. Remember that + // the clearcoat can have its own roughness and its own normal. + let clearcoat = in.material.clearcoat; + let clearcoat_perceptual_roughness = in.material.clearcoat_perceptual_roughness; + let clearcoat_roughness = lighting::perceptualRoughnessToRoughness(clearcoat_perceptual_roughness); + let clearcoat_N = in.clearcoat_N; + let clearcoat_NdotV = max(dot(clearcoat_N, in.V), 0.0001); + let clearcoat_R = reflect(-in.V, clearcoat_N); +#endif // STANDARD_MATERIAL_CLEARCOAT + + let diffuse_color = calculate_diffuse_color( + output_color.rgb, + metallic, + specular_transmission, + diffuse_transmission + ); + + // Diffuse transmissive strength is inversely related to metallicity and specular transmission, but directly related to diffuse transmission + let diffuse_transmissive_color = output_color.rgb * (1.0 - metallic) * (1.0 - specular_transmission) * diffuse_transmission; + + // Calculate the world position of the second Lambertian lobe used for diffuse transmission, by subtracting material thickness + let diffuse_transmissive_lobe_world_position = in.world_position - vec4(in.world_normal, 0.0) * thickness; + + let F0 = calculate_F0(output_color.rgb, metallic, reflectance); + let F_ab = lighting::F_AB(perceptual_roughness, NdotV); + + var direct_light: vec3 = vec3(0.0); + + // Transmitted Light (Specular and Diffuse) + var transmitted_light: vec3 = vec3(0.0); + + // Pack all the values into a structure. + var lighting_input: lighting::LightingInput; + lighting_input.layers[LAYER_BASE].NdotV = NdotV; + lighting_input.layers[LAYER_BASE].N = in.N; + lighting_input.layers[LAYER_BASE].R = R; + lighting_input.layers[LAYER_BASE].perceptual_roughness = perceptual_roughness; + lighting_input.layers[LAYER_BASE].roughness = roughness; + lighting_input.P = in.world_position.xyz; + lighting_input.V = in.V; + lighting_input.diffuse_color = diffuse_color; + lighting_input.F0_ = F0; + lighting_input.F_ab = F_ab; +#ifdef STANDARD_MATERIAL_CLEARCOAT + lighting_input.layers[LAYER_CLEARCOAT].NdotV = clearcoat_NdotV; + lighting_input.layers[LAYER_CLEARCOAT].N = clearcoat_N; + lighting_input.layers[LAYER_CLEARCOAT].R = clearcoat_R; + lighting_input.layers[LAYER_CLEARCOAT].perceptual_roughness = clearcoat_perceptual_roughness; + lighting_input.layers[LAYER_CLEARCOAT].roughness = clearcoat_roughness; + lighting_input.clearcoat_strength = clearcoat; +#endif // STANDARD_MATERIAL_CLEARCOAT +#ifdef STANDARD_MATERIAL_ANISOTROPY + lighting_input.anisotropy = in.anisotropy_strength; + lighting_input.Ta = in.anisotropy_T; + lighting_input.Ba = in.anisotropy_B; +#endif // STANDARD_MATERIAL_ANISOTROPY + + // And do the same for transmissive if we need to. +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + var transmissive_lighting_input: lighting::LightingInput; + transmissive_lighting_input.layers[LAYER_BASE].NdotV = 1.0; + transmissive_lighting_input.layers[LAYER_BASE].N = -in.N; + transmissive_lighting_input.layers[LAYER_BASE].R = vec3(0.0); + transmissive_lighting_input.layers[LAYER_BASE].perceptual_roughness = 1.0; + transmissive_lighting_input.layers[LAYER_BASE].roughness = 1.0; + transmissive_lighting_input.P = diffuse_transmissive_lobe_world_position.xyz; + transmissive_lighting_input.V = -in.V; + transmissive_lighting_input.diffuse_color = diffuse_transmissive_color; + transmissive_lighting_input.F0_ = vec3(0.0); + transmissive_lighting_input.F_ab = vec2(0.1); +#ifdef STANDARD_MATERIAL_CLEARCOAT + transmissive_lighting_input.layers[LAYER_CLEARCOAT].NdotV = 0.0; + transmissive_lighting_input.layers[LAYER_CLEARCOAT].N = vec3(0.0); + transmissive_lighting_input.layers[LAYER_CLEARCOAT].R = vec3(0.0); + transmissive_lighting_input.layers[LAYER_CLEARCOAT].perceptual_roughness = 0.0; + transmissive_lighting_input.layers[LAYER_CLEARCOAT].roughness = 0.0; + transmissive_lighting_input.clearcoat_strength = 0.0; +#endif // STANDARD_MATERIAL_CLEARCOAT +#ifdef STANDARD_MATERIAL_ANISOTROPY + transmissive_lighting_input.anisotropy = in.anisotropy_strength; + transmissive_lighting_input.Ta = in.anisotropy_T; + transmissive_lighting_input.Ba = in.anisotropy_B; +#endif // STANDARD_MATERIAL_ANISOTROPY +#endif // STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + + let view_z = dot(vec4( + view_bindings::view.view_from_world[0].z, + view_bindings::view.view_from_world[1].z, + view_bindings::view.view_from_world[2].z, + view_bindings::view.view_from_world[3].z + ), in.world_position); + let cluster_index = clustering::fragment_cluster_index(in.frag_coord.xy, view_z, in.is_orthographic); + var clusterable_object_index_ranges = + clustering::unpack_clusterable_object_index_ranges(cluster_index); + + // Point lights (direct) + for (var i: u32 = clusterable_object_index_ranges.first_point_light_index_offset; + i < clusterable_object_index_ranges.first_spot_light_index_offset; + i = i + 1u) { + let light_id = clustering::get_clusterable_object_id(i); + + // If we're lightmapped, disable diffuse contribution from the light if + // requested, to avoid double-counting light. +#ifdef LIGHTMAP + let enable_diffuse = + (view_bindings::clusterable_objects.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != 0u; +#else // LIGHTMAP + let enable_diffuse = true; +#endif // LIGHTMAP + + var shadow: f32 = 1.0; + if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (view_bindings::clusterable_objects.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = shadows::fetch_point_shadow(light_id, in.world_position, in.world_normal); + } + + let light_contrib = lighting::point_light(light_id, &lighting_input, enable_diffuse, true); + direct_light += light_contrib * shadow; + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + // NOTE: We use the diffuse transmissive color, the second Lambertian lobe's calculated + // world position, inverted normal and view vectors, and the following simplified + // values for a fully diffuse transmitted light contribution approximation: + // + // roughness = 1.0; + // NdotV = 1.0; + // R = vec3(0.0) // doesn't really matter + // F_ab = vec2(0.1) + // F0 = vec3(0.0) + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::clusterable_objects.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_point_shadow(light_id, diffuse_transmissive_lobe_world_position, -in.world_normal); + } + + let transmitted_light_contrib = + lighting::point_light(light_id, &transmissive_lighting_input, enable_diffuse, true); + transmitted_light += transmitted_light_contrib * transmitted_shadow; +#endif + } + + // Spot lights (direct) + for (var i: u32 = clusterable_object_index_ranges.first_spot_light_index_offset; + i < clusterable_object_index_ranges.first_reflection_probe_index_offset; + i = i + 1u) { + let light_id = clustering::get_clusterable_object_id(i); + + // If we're lightmapped, disable diffuse contribution from the light if + // requested, to avoid double-counting light. +#ifdef LIGHTMAP + let enable_diffuse = + (view_bindings::clusterable_objects.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != 0u; +#else // LIGHTMAP + let enable_diffuse = true; +#endif // LIGHTMAP + + var shadow: f32 = 1.0; + if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (view_bindings::clusterable_objects.data[light_id].flags & + mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = shadows::fetch_spot_shadow( + light_id, + in.world_position, + in.world_normal, + view_bindings::clusterable_objects.data[light_id].shadow_map_near_z, + ); + } + + let light_contrib = lighting::spot_light(light_id, &lighting_input, enable_diffuse); + direct_light += light_contrib * shadow; + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + // NOTE: We use the diffuse transmissive color, the second Lambertian lobe's calculated + // world position, inverted normal and view vectors, and the following simplified + // values for a fully diffuse transmitted light contribution approximation: + // + // roughness = 1.0; + // NdotV = 1.0; + // R = vec3(0.0) // doesn't really matter + // F_ab = vec2(0.1) + // F0 = vec3(0.0) + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::clusterable_objects.data[light_id].flags & mesh_view_types::POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_spot_shadow( + light_id, + diffuse_transmissive_lobe_world_position, + -in.world_normal, + view_bindings::clusterable_objects.data[light_id].shadow_map_near_z, + ); + } + + let transmitted_light_contrib = + lighting::spot_light(light_id, &transmissive_lighting_input, enable_diffuse); + transmitted_light += transmitted_light_contrib * transmitted_shadow; +#endif + } + + // directional lights (direct) + let n_directional_lights = view_bindings::lights.n_directional_lights; + for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) { + // check if this light should be skipped, which occurs if this light does not intersect with the view + // note point and spot lights aren't skippable, as the relevant lights are filtered in `assign_lights_to_clusters` + let light = &view_bindings::lights.directional_lights[i]; + + // If we're lightmapped, disable diffuse contribution from the light if + // requested, to avoid double-counting light. +#ifdef LIGHTMAP + let enable_diffuse = + ((*light).flags & + mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_AFFECTS_LIGHTMAPPED_MESH_DIFFUSE_BIT) != + 0u; +#else // LIGHTMAP + let enable_diffuse = true; +#endif // LIGHTMAP + + var shadow: f32 = 1.0; + if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u + && (view_bindings::lights.directional_lights[i].flags & mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = shadows::fetch_directional_shadow(i, in.world_position, in.world_normal, view_z); + } + + var light_contrib = lighting::directional_light(i, &lighting_input, enable_diffuse); + +#ifdef DIRECTIONAL_LIGHT_SHADOW_MAP_DEBUG_CASCADES + light_contrib = shadows::cascade_debug_visualization(light_contrib, i, view_z); +#endif + direct_light += light_contrib * shadow; + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + // NOTE: We use the diffuse transmissive color, the second Lambertian lobe's calculated + // world position, inverted normal and view vectors, and the following simplified + // values for a fully diffuse transmitted light contribution approximation: + // + // roughness = 1.0; + // NdotV = 1.0; + // R = vec3(0.0) // doesn't really matter + // F_ab = vec2(0.1) + // F0 = vec3(0.0) + var transmitted_shadow: f32 = 1.0; + if ((in.flags & (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT)) == (MESH_FLAGS_SHADOW_RECEIVER_BIT | MESH_FLAGS_TRANSMITTED_SHADOW_RECEIVER_BIT) + && (view_bindings::lights.directional_lights[i].flags & mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + transmitted_shadow = shadows::fetch_directional_shadow(i, diffuse_transmissive_lobe_world_position, -in.world_normal, view_z); + } + + let transmitted_light_contrib = + lighting::directional_light(i, &transmissive_lighting_input, enable_diffuse); + transmitted_light += transmitted_light_contrib * transmitted_shadow; +#endif + } + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + // NOTE: We use the diffuse transmissive color, the second Lambertian lobe's calculated + // world position, inverted normal and view vectors, and the following simplified + // values for a fully diffuse transmitted light contribution approximation: + // + // perceptual_roughness = 1.0; + // NdotV = 1.0; + // F0 = vec3(0.0) + // diffuse_occlusion = vec3(1.0) + transmitted_light += ambient::ambient_light(diffuse_transmissive_lobe_world_position, -in.N, -in.V, 1.0, diffuse_transmissive_color, vec3(0.0), 1.0, vec3(1.0)); +#endif + + // Diffuse indirect lighting can come from a variety of sources. The + // priority goes like this: + // + // 1. Lightmap (highest) + // 2. Irradiance volume + // 3. Environment map (lowest) + // + // When we find a source of diffuse indirect lighting, we stop accumulating + // any more diffuse indirect light. This avoids double-counting if, for + // example, both lightmaps and irradiance volumes are present. + + var indirect_light = vec3(0.0f); + var found_diffuse_indirect = false; + +#ifdef LIGHTMAP + indirect_light += in.lightmap_light * diffuse_color; + found_diffuse_indirect = true; +#endif + +#ifdef IRRADIANCE_VOLUME + // Irradiance volume light (indirect) + if (!found_diffuse_indirect) { + let irradiance_volume_light = irradiance_volume::irradiance_volume_light( + in.world_position.xyz, + in.N, + &clusterable_object_index_ranges, + ); + indirect_light += irradiance_volume_light * diffuse_color * diffuse_occlusion; + found_diffuse_indirect = true; + } +#endif + + // Environment map light (indirect) +#ifdef ENVIRONMENT_MAP + // If screen space reflections are going to be used for this material, don't + // accumulate environment map light yet. The SSR shader will do it. +#ifdef SCREEN_SPACE_REFLECTIONS + let use_ssr = perceptual_roughness <= + view_bindings::ssr_settings.perceptual_roughness_threshold; +#else // SCREEN_SPACE_REFLECTIONS + let use_ssr = false; +#endif // SCREEN_SPACE_REFLECTIONS + + if (!use_ssr) { +#ifdef STANDARD_MATERIAL_ANISOTROPY + var bent_normal_lighting_input = lighting_input; + bend_normal_for_anisotropy(&bent_normal_lighting_input); + let environment_map_lighting_input = &bent_normal_lighting_input; +#else // STANDARD_MATERIAL_ANISOTROPY + let environment_map_lighting_input = &lighting_input; +#endif // STANDARD_MATERIAL_ANISOTROPY + + let environment_light = environment_map::environment_map_light( + environment_map_lighting_input, + &clusterable_object_index_ranges, + found_diffuse_indirect, + ); + + indirect_light += environment_light.diffuse * diffuse_occlusion + + environment_light.specular * specular_occlusion; + } +#endif // ENVIRONMENT_MAP + + // Ambient light (indirect) + // If we are lightmapped, disable the ambient contribution if requested. + // This is to avoid double-counting ambient light. (It might be part of the lightmap) +#ifdef LIGHTMAP + let enable_ambient = view_bindings::lights.ambient_light_affects_lightmapped_meshes != 0u; +#else // LIGHTMAP + let enable_ambient = true; +#endif // LIGHTMAP + if (enable_ambient) { + indirect_light += ambient::ambient_light(in.world_position, in.N, in.V, NdotV, diffuse_color, F0, perceptual_roughness, diffuse_occlusion); + } + + // we'll use the specular component of the transmitted environment + // light in the call to `specular_transmissive_light()` below + var specular_transmitted_environment_light = vec3(0.0); + +#ifdef ENVIRONMENT_MAP + +#ifdef STANDARD_MATERIAL_DIFFUSE_OR_SPECULAR_TRANSMISSION + // NOTE: We use the diffuse transmissive color, inverted normal and view vectors, + // and the following simplified values for the transmitted environment light contribution + // approximation: + // + // diffuse_color = vec3(1.0) // later we use `diffuse_transmissive_color` and `specular_transmissive_color` + // NdotV = 1.0; + // R = T // see definition below + // F0 = vec3(1.0) + // diffuse_occlusion = 1.0 + // + // (This one is slightly different from the other light types above, because the environment + // map light returns both diffuse and specular components separately, and we want to use both) + + let T = -normalize( + in.V + // start with view vector at entry point + refract(in.V, -in.N, 1.0 / ior) * thickness // add refracted vector scaled by thickness, towards exit point + ); // normalize to find exit point view vector + + var transmissive_environment_light_input: lighting::LightingInput; + transmissive_environment_light_input.diffuse_color = vec3(1.0); + transmissive_environment_light_input.layers[LAYER_BASE].NdotV = 1.0; + transmissive_environment_light_input.P = in.world_position.xyz; + transmissive_environment_light_input.layers[LAYER_BASE].N = -in.N; + transmissive_environment_light_input.V = in.V; + transmissive_environment_light_input.layers[LAYER_BASE].R = T; + transmissive_environment_light_input.layers[LAYER_BASE].perceptual_roughness = perceptual_roughness; + transmissive_environment_light_input.layers[LAYER_BASE].roughness = roughness; + transmissive_environment_light_input.F0_ = vec3(1.0); + transmissive_environment_light_input.F_ab = vec2(0.1); +#ifdef STANDARD_MATERIAL_CLEARCOAT + // No clearcoat. + transmissive_environment_light_input.clearcoat_strength = 0.0; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].NdotV = 0.0; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].N = in.N; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].R = vec3(0.0); + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].perceptual_roughness = 0.0; + transmissive_environment_light_input.layers[LAYER_CLEARCOAT].roughness = 0.0; +#endif // STANDARD_MATERIAL_CLEARCOAT + + let transmitted_environment_light = environment_map::environment_map_light( + &transmissive_environment_light_input, + &clusterable_object_index_ranges, + false, + ); + +#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION + transmitted_light += transmitted_environment_light.diffuse * diffuse_transmissive_color; +#endif // STANDARD_MATERIAL_DIFFUSE_TRANSMISSION +#ifdef STANDARD_MATERIAL_SPECULAR_TRANSMISSION + specular_transmitted_environment_light = transmitted_environment_light.specular * specular_transmissive_color; +#endif // STANDARD_MATERIAL_SPECULAR_TRANSMISSION + +#endif // STANDARD_MATERIAL_SPECULAR_OR_DIFFUSE_TRANSMISSION + +#endif // ENVIRONMENT_MAP + + var emissive_light = emissive.rgb * output_color.a; + + // "The clearcoat layer is on top of emission in the layering stack. + // Consequently, the emission is darkened by the Fresnel term." + // + // +#ifdef STANDARD_MATERIAL_CLEARCOAT + emissive_light = emissive_light * (0.04 + (1.0 - 0.04) * pow(1.0 - clearcoat_NdotV, 5.0)); +#endif + + emissive_light = emissive_light * mix(1.0, view_bindings::view.exposure, emissive.a); + +#ifdef STANDARD_MATERIAL_SPECULAR_TRANSMISSION + transmitted_light += transmission::specular_transmissive_light(in.world_position, in.frag_coord.xyz, view_z, in.N, in.V, F0, ior, thickness, perceptual_roughness, specular_transmissive_color, specular_transmitted_environment_light).rgb; + + if (in.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ATTENUATION_ENABLED_BIT) != 0u { + // We reuse the `atmospheric_fog()` function here, as it's fundamentally + // equivalent to the attenuation that takes place inside the material volume, + // and will allow us to eventually hook up subsurface scattering more easily + var attenuation_fog: mesh_view_types::Fog; + attenuation_fog.base_color.a = 1.0; + attenuation_fog.be = pow(1.0 - in.material.attenuation_color.rgb, vec3(E)) / in.material.attenuation_distance; + // TODO: Add the subsurface scattering factor below + // attenuation_fog.bi = /* ... */ + transmitted_light = bevy_pbr::fog::atmospheric_fog( + attenuation_fog, vec4(transmitted_light, 1.0), thickness, + vec3(0.0) // TODO: Pass in (pre-attenuated) scattered light contribution here + ).rgb; + } +#endif + + // Total light + output_color = vec4( + (view_bindings::view.exposure * (transmitted_light + direct_light + indirect_light)) + emissive_light, + output_color.a + ); + + output_color = clustering::cluster_debug_visualization( + output_color, + view_z, + in.is_orthographic, + clusterable_object_index_ranges, + cluster_index, + ); + + return output_color; +} +#endif // PREPASS_FRAGMENT + +#ifdef DISTANCE_FOG +fn apply_fog(fog_params: mesh_view_types::Fog, input_color: vec4, fragment_world_position: vec3, view_world_position: vec3) -> vec4 { + let view_to_world = fragment_world_position.xyz - view_world_position.xyz; + + // `length()` is used here instead of just `view_to_world.z` since that produces more + // high quality results, especially for denser/smaller fogs. we get a "curved" + // fog shape that remains consistent with camera rotation, instead of a "linear" + // fog shape that looks a bit fake + let distance = length(view_to_world); + + var scattering = vec3(0.0); + if fog_params.directional_light_color.a > 0.0 { + let view_to_world_normalized = view_to_world / distance; + let n_directional_lights = view_bindings::lights.n_directional_lights; + for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) { + let light = view_bindings::lights.directional_lights[i]; + scattering += pow( + max( + dot(view_to_world_normalized, light.direction_to_light), + 0.0 + ), + fog_params.directional_light_exponent + ) * light.color.rgb * view_bindings::view.exposure; + } + } + + if fog_params.mode == mesh_view_types::FOG_MODE_LINEAR { + return bevy_pbr::fog::linear_fog(fog_params, input_color, distance, scattering); + } else if fog_params.mode == mesh_view_types::FOG_MODE_EXPONENTIAL { + return bevy_pbr::fog::exponential_fog(fog_params, input_color, distance, scattering); + } else if fog_params.mode == mesh_view_types::FOG_MODE_EXPONENTIAL_SQUARED { + return bevy_pbr::fog::exponential_squared_fog(fog_params, input_color, distance, scattering); + } else if fog_params.mode == mesh_view_types::FOG_MODE_ATMOSPHERIC { + return bevy_pbr::fog::atmospheric_fog(fog_params, input_color, distance, scattering); + } else { + return input_color; + } +} +#endif // DISTANCE_FOG + +#ifdef PREMULTIPLY_ALPHA +fn premultiply_alpha(standard_material_flags: u32, color: vec4) -> vec4 { +// `Blend`, `Premultiplied` and `Alpha` all share the same `BlendState`. Depending +// on the alpha mode, we premultiply the color channels by the alpha channel value, +// (and also optionally replace the alpha value with 0.0) so that the result produces +// the desired blend mode when sent to the blending operation. +#ifdef BLEND_PREMULTIPLIED_ALPHA + // For `BlendState::PREMULTIPLIED_ALPHA_BLENDING` the blend function is: + // + // result = 1 * src_color + (1 - src_alpha) * dst_color + let alpha_mode = standard_material_flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD { + // Here, we premultiply `src_color` by `src_alpha`, and replace `src_alpha` with 0.0: + // + // src_color *= src_alpha + // src_alpha = 0.0 + // + // We end up with: + // + // result = 1 * (src_alpha * src_color) + (1 - 0) * dst_color + // result = src_alpha * src_color + 1 * dst_color + // + // Which is the blend operation for additive blending + return vec4(color.rgb * color.a, 0.0); + } else { + // Here, we don't do anything, so that we get premultiplied alpha blending. (As expected) + return color.rgba; + } +#endif +// `Multiply` uses its own `BlendState`, but we still need to premultiply here in the +// shader so that we get correct results as we tweak the alpha channel +#ifdef BLEND_MULTIPLY + // The blend function is: + // + // result = dst_color * src_color + (1 - src_alpha) * dst_color + // + // We premultiply `src_color` by `src_alpha`: + // + // src_color *= src_alpha + // + // We end up with: + // + // result = dst_color * (src_color * src_alpha) + (1 - src_alpha) * dst_color + // result = src_alpha * (src_color * dst_color) + (1 - src_alpha) * dst_color + // + // Which is the blend operation for multiplicative blending with arbitrary mixing + // controlled by the source alpha channel + return vec4(color.rgb * color.a, color.a); +#endif +} +#endif + +// fog, alpha premultiply +// for non-hdr cameras, tonemapping and debanding +fn main_pass_post_lighting_processing( + pbr_input: pbr_types::PbrInput, + input_color: vec4, +) -> vec4 { + var output_color = input_color; + +#ifdef DISTANCE_FOG + // fog + if ((pbr_input.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT) != 0u) { + output_color = apply_fog(view_bindings::fog, output_color, pbr_input.world_position.xyz, view_bindings::view.world_position.xyz); + } +#endif // DISTANCE_FOG + +#ifdef TONEMAP_IN_SHADER + output_color = tone_mapping(output_color, view_bindings::view.color_grading); +#ifdef DEBAND_DITHER + var output_rgb = output_color.rgb; + output_rgb = powsafe(output_rgb, 1.0 / 2.2); + output_rgb += screen_space_dither(pbr_input.frag_coord.xy); + // This conversion back to linear space is required because our output texture format is + // SRGB; the GPU will assume our output is linear and will apply an SRGB conversion. + output_rgb = powsafe(output_rgb, 2.2); + output_color = vec4(output_rgb, output_color.a); +#endif +#endif +#ifdef PREMULTIPLY_ALPHA + output_color = premultiply_alpha(pbr_input.material.flags, output_color); +#endif + return output_color; +} diff --git a/crates/libmarathon/src/render/pbr/render/pbr_lighting.wgsl b/crates/libmarathon/src/render/pbr/render/pbr_lighting.wgsl new file mode 100644 index 0000000..7496dea --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/pbr_lighting.wgsl @@ -0,0 +1,856 @@ +#define_import_path bevy_pbr::lighting + +#import bevy_pbr::{ + mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, + mesh_view_bindings as view_bindings, +} +#import bevy_render::maths::PI + +const LAYER_BASE: u32 = 0; +const LAYER_CLEARCOAT: u32 = 1; + +// From the Filament design doc +// https://google.github.io/filament/Filament.html#table_symbols +// Symbol Definition +// v View unit vector +// l Incident light unit vector +// n Surface normal unit vector +// h Half unit vector between l and v +// f BRDF +// f_d Diffuse component of a BRDF +// f_r Specular component of a BRDF +// α Roughness, remapped from using input perceptualRoughness +// σ Diffuse reflectance +// Ω Spherical domain +// f0 Reflectance at normal incidence +// f90 Reflectance at grazing angle +// χ+(a) Heaviside function (1 if a>0 and 0 otherwise) +// nior Index of refraction (IOR) of an interface +// ⟨n⋅l⟩ Dot product clamped to [0..1] +// ⟨a⟩ Saturated value (clamped to [0..1]) + +// The Bidirectional Reflectance Distribution Function (BRDF) describes the surface response of a standard material +// and consists of two components, the diffuse component (f_d) and the specular component (f_r): +// f(v,l) = f_d(v,l) + f_r(v,l) +// +// The form of the microfacet model is the same for diffuse and specular +// f_r(v,l) = f_d(v,l) = 1 / { |n⋅v||n⋅l| } ∫_Ω D(m,α) G(v,l,m) f_m(v,l,m) (v⋅m) (l⋅m) dm +// +// In which: +// D, also called the Normal Distribution Function (NDF) models the distribution of the microfacets +// G models the visibility (or occlusion or shadow-masking) of the microfacets +// f_m is the microfacet BRDF and differs between specular and diffuse components +// +// The above integration needs to be approximated. + +// Input to a lighting function for a single layer (either the base layer or the +// clearcoat layer). +struct LayerLightingInput { + // The normal vector. + N: vec3, + // The reflected vector. + R: vec3, + // The normal vector ⋅ the view vector. + NdotV: f32, + + // The perceptual roughness of the layer. + perceptual_roughness: f32, + // The roughness of the layer. + roughness: f32, +} + +// Input to a lighting function (`point_light`, `spot_light`, +// `directional_light`). +struct LightingInput { +#ifdef STANDARD_MATERIAL_CLEARCOAT + layers: array, +#else // STANDARD_MATERIAL_CLEARCOAT + layers: array, +#endif // STANDARD_MATERIAL_CLEARCOAT + + // The world-space position. + P: vec3, + // The vector to the view. + V: vec3, + + // The diffuse color of the material. + diffuse_color: vec3, + + // Specular reflectance at the normal incidence angle. + // + // This should be read F₀, but due to Naga limitations we can't name it that. + F0_: vec3, + // Constants for the BRDF approximation. + // + // See `EnvBRDFApprox` in + // . + // What we call `F_ab` they call `AB`. + F_ab: vec2, + +#ifdef STANDARD_MATERIAL_CLEARCOAT + // The strength of the clearcoat layer. + clearcoat_strength: f32, +#endif // STANDARD_MATERIAL_CLEARCOAT + +#ifdef STANDARD_MATERIAL_ANISOTROPY + // The anisotropy strength, reflecting the amount of increased roughness in + // the tangent direction. + anisotropy: f32, + // The tangent direction for anisotropy: i.e. the direction in which + // roughness increases. + Ta: vec3, + // The bitangent direction, which is the cross product of the normal with + // the tangent direction. + Ba: vec3, +#endif // STANDARD_MATERIAL_ANISOTROPY +} + +// Values derived from the `LightingInput` for both diffuse and specular lights. +struct DerivedLightingInput { + // The half-vector between L, the incident light vector, and V, the view + // vector. + H: vec3, + // The normal vector ⋅ the incident light vector. + NdotL: f32, + // The normal vector ⋅ the half-vector. + NdotH: f32, + // The incident light vector ⋅ the half-vector. + LdotH: f32, +} + +// distanceAttenuation is simply the square falloff of light intensity +// combined with a smooth attenuation at the edge of the light radius +// +// light radius is a non-physical construct for efficiency purposes, +// because otherwise every light affects every fragment in the scene +fn getDistanceAttenuation(distanceSquare: f32, inverseRangeSquared: f32) -> f32 { + let factor = distanceSquare * inverseRangeSquared; + let smoothFactor = saturate(1.0 - factor * factor); + let attenuation = smoothFactor * smoothFactor; + return attenuation * 1.0 / max(distanceSquare, 0.0001); +} + +// Normal distribution function (specular D) +// Based on https://google.github.io/filament/Filament.html#citation-walter07 + +// D_GGX(h,α) = α^2 / { π ((n⋅h)^2 (α2−1) + 1)^2 } + +// Simple implementation, has precision problems when using fp16 instead of fp32 +// see https://google.github.io/filament/Filament.html#listing_speculardfp16 +fn D_GGX(roughness: f32, NdotH: f32) -> f32 { + let oneMinusNdotHSquared = 1.0 - NdotH * NdotH; + let a = NdotH * roughness; + let k = roughness / (oneMinusNdotHSquared + a * a); + let d = k * k * (1.0 / PI); + return d; +} + +// An approximation of the anisotropic GGX distribution function. +// +// 1 +// D(𝐡) = ─────────────────────────────────────────────────── +// παₜα_b((𝐡 ⋅ 𝐭)² / αₜ²) + (𝐡 ⋅ 𝐛)² / α_b² + (𝐡 ⋅ 𝐧)²)² +// +// * `T` = 𝐭 = the tangent direction = the direction of increased roughness. +// +// * `B` = 𝐛 = the bitangent direction = the direction of decreased roughness. +// +// * `at` = αₜ = the alpha-roughness in the tangent direction. +// +// * `ab` = α_b = the alpha-roughness in the bitangent direction. +// +// This is from the `KHR_materials_anisotropy` spec: +// +fn D_GGX_anisotropic(at: f32, ab: f32, NdotH: f32, TdotH: f32, BdotH: f32) -> f32 { + let a2 = at * ab; + let f = vec3(ab * TdotH, at * BdotH, a2 * NdotH); + let w2 = a2 / dot(f, f); + let d = a2 * w2 * w2 * (1.0 / PI); + return d; +} + +// Visibility function (Specular G) +// V(v,l,a) = G(v,l,α) / { 4 (n⋅v) (n⋅l) } +// such that f_r becomes +// f_r(v,l) = D(h,α) V(v,l,α) F(v,h,f0) +// where +// V(v,l,α) = 0.5 / { n⋅l sqrt((n⋅v)^2 (1−α2) + α2) + n⋅v sqrt((n⋅l)^2 (1−α2) + α2) } +// Note the two sqrt's, that may be slow on mobile, see https://google.github.io/filament/Filament.html#listing_approximatedspecularv +fn V_SmithGGXCorrelated(roughness: f32, NdotV: f32, NdotL: f32) -> f32 { + let a2 = roughness * roughness; + let lambdaV = NdotL * sqrt((NdotV - a2 * NdotV) * NdotV + a2); + let lambdaL = NdotV * sqrt((NdotL - a2 * NdotL) * NdotL + a2); + let v = 0.5 / (lambdaV + lambdaL); + return v; +} + +// The visibility function, anisotropic variant. +fn V_GGX_anisotropic( + at: f32, + ab: f32, + NdotL: f32, + NdotV: f32, + BdotV: f32, + TdotV: f32, + TdotL: f32, + BdotL: f32, +) -> f32 { + let GGX_V = NdotL * length(vec3(at * TdotV, ab * BdotV, NdotV)); + let GGX_L = NdotV * length(vec3(at * TdotL, ab * BdotL, NdotL)); + let v = 0.5 / (GGX_V + GGX_L); + return saturate(v); +} + +// Probability-density function that matches the bounded VNDF sampler +// https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf (Listing 2) +fn ggx_vndf_pdf(i: vec3, NdotH: f32, roughness: f32) -> f32 { + let ndf = D_GGX(roughness, NdotH); + + // Common terms + let ai = roughness * i.xy; + let len2 = dot(ai, ai); + let t = sqrt(len2 + i.z * i.z); + if i.z >= 0.0 { + let a = roughness; + let s = 1.0 + length(i.xy); + let a2 = a * a; + let s2 = s * s; + let k = (1.0 - a2) * s2 / (s2 + a2 * i.z * i.z); + return ndf / (2.0 * (k * i.z + t)); + } + + // Backfacing case + return ndf * (t - i.z) / (2.0 * len2); +} + +// https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf (Listing 1) +fn sample_visible_ggx( + xi: vec2, + roughness: f32, + normal: vec3, + view: vec3, +) -> vec3 { + let n = normal; + let alpha = roughness; + + // Decompose view into components parallel/perpendicular to the normal + let wi_n = dot(view, n); + let wi_z = -n * wi_n; + let wi_xy = view + wi_z; + + // Warp view vector to the unit-roughness configuration + let wi_std = -normalize(alpha * wi_xy + wi_z); + + // Compute wi_std.z once for reuse + let wi_std_z = dot(wi_std, n); + + // Bounded VNDF sampling + // Compute the bound parameter k (Eq. 5) and the scaled z–limit b (Eq. 6) + let s = 1.0 + length(wi_xy); + let a = clamp(alpha, 0.0, 1.0); + let a2 = a * a; + let s2 = s * s; + let k = (1.0 - a2) * s2 / (s2 + a2 * wi_n * wi_n); + let b = select(wi_std_z, k * wi_std_z, wi_n > 0.0); + + // Sample a spherical cap in (-b, 1] + let z = 1.0 - xi.y * (1.0 + b); + let sin_theta = sqrt(max(0.0, 1.0 - z * z)); + let phi = 2.0 * PI * xi.x - PI; + let x = sin_theta * cos(phi); + let y = sin_theta * sin(phi); + let c_std = vec3f(x, y, z); + + // Reflect the sample so that the normal aligns with +Z + let up = vec3f(0.0, 0.0, 1.0); + let wr = n + up; + let c = dot(wr, c_std) * wr / wr.z - c_std; + + // Half-vector in the standard frame + let wm_std = c + wi_std; + let wm_std_z = n * dot(n, wm_std); + let wm_std_xy = wm_std_z - wm_std; + + // Unwarp back to original roughness and compute microfacet normal + let H = normalize(alpha * wm_std_xy + wm_std_z); + + // Reflect view to obtain the outgoing (light) direction + return reflect(-view, H); +} + +// Smith geometric shadowing function +fn G_Smith(NdotV: f32, NdotL: f32, roughness: f32) -> f32 { + let k = roughness / 2.0; + let GGXL = NdotL / (NdotL * (1.0 - k) + k); + let GGXV = NdotV / (NdotV * (1.0 - k) + k); + return GGXL * GGXV; +} + +// A simpler, but nonphysical, alternative to Smith-GGX. We use this for +// clearcoat, per the Filament spec. +// +// https://google.github.io/filament/Filament.html#materialsystem/clearcoatmodel#toc4.9.1 +fn V_Kelemen(LdotH: f32) -> f32 { + return 0.25 / (LdotH * LdotH); +} + +// Fresnel function +// see https://google.github.io/filament/Filament.html#citation-schlick94 +// F_Schlick(v,h,f_0,f_90) = f_0 + (f_90 − f_0) (1 − v⋅h)^5 +fn F_Schlick_vec(f0: vec3, f90: f32, VdotH: f32) -> vec3 { + // not using mix to keep the vec3 and float versions identical + return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0); +} + +fn F_Schlick(f0: f32, f90: f32, VdotH: f32) -> f32 { + // not using mix to keep the vec3 and float versions identical + return f0 + (f90 - f0) * pow(1.0 - VdotH, 5.0); +} + +fn fresnel(f0: vec3, LdotH: f32) -> vec3 { + // f_90 suitable for ambient occlusion + // see https://google.github.io/filament/Filament.html#lighting/occlusion + let f90 = saturate(dot(f0, vec3(50.0 * 0.33))); + return F_Schlick_vec(f0, f90, LdotH); +} + +// Given distribution, visibility, and Fresnel term, calculates the final +// specular light. +// +// Multiscattering approximation: +// +fn specular_multiscatter( + D: f32, + V: f32, + F: vec3, + F0: vec3, + F_ab: vec2, + specular_intensity: f32, +) -> vec3 { + var Fr = (specular_intensity * D * V) * F; + Fr *= 1.0 + F0 * (1.0 / F_ab.x - 1.0); + return Fr; +} + +// Specular BRDF +// https://google.github.io/filament/Filament.html#materialsystem/specularbrdf + +// N, V, and L must all be normalized. +fn derive_lighting_input(N: vec3, V: vec3, L: vec3) -> DerivedLightingInput { + var input: DerivedLightingInput; + var H: vec3 = normalize(L + V); + input.H = H; + input.NdotL = saturate(dot(N, L)); + input.NdotH = saturate(dot(N, H)); + input.LdotH = saturate(dot(L, H)); + return input; +} + +// Returns L in the `xyz` components and the specular intensity in the `w` component. +fn compute_specular_layer_values_for_point_light( + input: ptr, + layer: u32, + V: vec3, + light_to_frag: vec3, + light_position_radius: f32, +) -> vec4 { + // Unpack. + let R = (*input).layers[layer].R; + let a = (*input).layers[layer].roughness; + + // Representative Point Area Lights. + // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16 + var LtFdotR = dot(light_to_frag, R); + + // HACK: the following line is an amendment to fix a discontinuity when a surface + // intersects the light sphere. See https://github.com/bevyengine/bevy/issues/13318 + // + // This sentence in the reference is crux of the problem: "We approximate finding the point with the + // smallest angle to the reflection ray by finding the point with the smallest distance to the ray." + // This approximation turns out to be completely wrong for points inside or near the sphere. + // Clamping this dot product to be positive ensures `centerToRay` lies on ray and not behind it. + // Any non-zero epsilon works here, it just has to be positive to avoid a singularity at zero. + // However, this is still far from physically accurate. Deriving an exact solution would help, + // but really we should adopt a superior solution to area lighting, such as: + // Physically Based Area Lights by Michal Drobot, or + // Polygonal-Light Shading with Linearly Transformed Cosines by Eric Heitz et al. + LtFdotR = max(0.0001, LtFdotR); + + let centerToRay = LtFdotR * R - light_to_frag; + let closestPoint = light_to_frag + centerToRay * saturate( + light_position_radius * inverseSqrt(dot(centerToRay, centerToRay))); + let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint)); + let normalizationFactor = a / saturate(a + (light_position_radius * 0.5 * LspecLengthInverse)); + let intensity = normalizationFactor * normalizationFactor; + + let L: vec3 = closestPoint * LspecLengthInverse; // normalize() equivalent? + return vec4(L, intensity); +} + +// Cook-Torrance approximation of the microfacet model integration using Fresnel law F to model f_m +// f_r(v,l) = { D(h,α) G(v,l,α) F(v,h,f0) } / { 4 (n⋅v) (n⋅l) } +fn specular( + input: ptr, + derived_input: ptr, + specular_intensity: f32, +) -> vec3 { + // Unpack. + let roughness = (*input).layers[LAYER_BASE].roughness; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let F0 = (*input).F0_; + let NdotL = (*derived_input).NdotL; + let NdotH = (*derived_input).NdotH; + let LdotH = (*derived_input).LdotH; + + // Calculate distribution. + let D = D_GGX(roughness, NdotH); + // Calculate visibility. + let V = V_SmithGGXCorrelated(roughness, NdotV, NdotL); + // Calculate the Fresnel term. + let F = fresnel(F0, LdotH); + + // Calculate the specular light. + let Fr = specular_multiscatter(D, V, F, F0, (*input).F_ab, specular_intensity); + return Fr; +} + +// Calculates the specular light for the clearcoat layer. Returns Fc, the +// Fresnel term, in the first channel, and Frc, the specular clearcoat light, in +// the second channel. +// +// +fn specular_clearcoat( + input: ptr, + derived_input: ptr, + clearcoat_strength: f32, + specular_intensity: f32, +) -> vec2 { + // Unpack. + let roughness = (*input).layers[LAYER_CLEARCOAT].roughness; + let NdotH = (*derived_input).NdotH; + let LdotH = (*derived_input).LdotH; + + // Calculate distribution. + let Dc = D_GGX(roughness, NdotH); + // Calculate visibility. + let Vc = V_Kelemen(LdotH); + // Calculate the Fresnel term. + let Fc = F_Schlick(0.04, 1.0, LdotH) * clearcoat_strength; + // Calculate the specular light. + let Frc = (specular_intensity * Dc * Vc) * Fc; + return vec2(Fc, Frc); +} + +#ifdef STANDARD_MATERIAL_ANISOTROPY + +fn specular_anisotropy( + input: ptr, + derived_input: ptr, + L: vec3, + specular_intensity: f32, +) -> vec3 { + // Unpack. + let roughness = (*input).layers[LAYER_BASE].roughness; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let V = (*input).V; + let F0 = (*input).F0_; + let anisotropy = (*input).anisotropy; + let Ta = (*input).Ta; + let Ba = (*input).Ba; + let H = (*derived_input).H; + let NdotL = (*derived_input).NdotL; + let NdotH = (*derived_input).NdotH; + let LdotH = (*derived_input).LdotH; + + let TdotL = dot(Ta, L); + let BdotL = dot(Ba, L); + let TdotH = dot(Ta, H); + let BdotH = dot(Ba, H); + let TdotV = dot(Ta, V); + let BdotV = dot(Ba, V); + + let ab = roughness * roughness; + let at = mix(ab, 1.0, anisotropy * anisotropy); + + let Da = D_GGX_anisotropic(at, ab, NdotH, TdotH, BdotH); + let Va = V_GGX_anisotropic(at, ab, NdotL, NdotV, BdotV, TdotV, TdotL, BdotL); + let Fa = fresnel(F0, LdotH); + + // Calculate the specular light. + let Fr = specular_multiscatter(Da, Va, Fa, F0, (*input).F_ab, specular_intensity); + return Fr; +} + +#endif // STANDARD_MATERIAL_ANISOTROPY + +// Diffuse BRDF +// https://google.github.io/filament/Filament.html#materialsystem/diffusebrdf +// fd(v,l) = σ/π * 1 / { |n⋅v||n⋅l| } ∫Ω D(m,α) G(v,l,m) (v⋅m) (l⋅m) dm +// +// simplest approximation +// float Fd_Lambert() { +// return 1.0 / PI; +// } +// +// vec3 Fd = diffuseColor * Fd_Lambert(); +// +// Disney approximation +// See https://google.github.io/filament/Filament.html#citation-burley12 +// minimal quality difference +fn Fd_Burley( + input: ptr, + derived_input: ptr, +) -> f32 { + // Unpack. + let roughness = (*input).layers[LAYER_BASE].roughness; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let NdotL = (*derived_input).NdotL; + let LdotH = (*derived_input).LdotH; + + let f90 = 0.5 + 2.0 * roughness * LdotH * LdotH; + let lightScatter = F_Schlick(1.0, f90, NdotL); + let viewScatter = F_Schlick(1.0, f90, NdotV); + return lightScatter * viewScatter * (1.0 / PI); +} + +// Scale/bias approximation +// https://www.unrealengine.com/en-US/blog/physically-based-shading-on-mobile +// TODO: Use a LUT (more accurate) +fn F_AB(perceptual_roughness: f32, NdotV: f32) -> vec2 { + let c0 = vec4(-1.0, -0.0275, -0.572, 0.022); + let c1 = vec4(1.0, 0.0425, 1.04, -0.04); + let r = perceptual_roughness * c0 + c1; + let a004 = min(r.x * r.x, exp2(-9.28 * NdotV)) * r.x + r.y; + return vec2(-1.04, 1.04) * a004 + r.zw; +} + +fn EnvBRDFApprox(F0: vec3, F_ab: vec2) -> vec3 { + return F0 * F_ab.x + F_ab.y; +} + +fn perceptualRoughnessToRoughness(perceptualRoughness: f32) -> f32 { + // clamp perceptual roughness to prevent precision problems + // According to Filament design 0.089 is recommended for mobile + // Filament uses 0.045 for non-mobile + let clampedPerceptualRoughness = clamp(perceptualRoughness, 0.089, 1.0); + return clampedPerceptualRoughness * clampedPerceptualRoughness; +} + +// this must align with CubemapLayout in decal/clustered.rs +const CUBEMAP_TYPE_CROSS_VERTICAL: u32 = 0; +const CUBEMAP_TYPE_CROSS_HORIZONTAL: u32 = 1; +const CUBEMAP_TYPE_SEQUENCE_VERTICAL: u32 = 2; +const CUBEMAP_TYPE_SEQUENCE_HORIZONTAL: u32 = 3; + +const X_PLUS: u32 = 0; +const X_MINUS: u32 = 1; +const Y_PLUS: u32 = 2; +const Y_MINUS: u32 = 3; +const Z_MINUS: u32 = 4; +const Z_PLUS: u32 = 5; + +fn cubemap_uv(direction: vec3, cubemap_type: u32) -> vec2 { + let abs_direction = abs(direction); + let max_axis = max(abs_direction.x, max(abs_direction.y, abs_direction.z)); + + let face_index = select( + select(X_PLUS, X_MINUS, direction.x < 0.0), + select( + select(Y_PLUS, Y_MINUS, direction.y < 0.0), + select(Z_PLUS, Z_MINUS, direction.z < 0.0), + max_axis != abs_direction.y + ), + max_axis != abs_direction.x + ); + + var face_uv: vec2; + var divisor: f32; + var corner_uv: vec2 = vec2(0, 0); + var face_size: vec2; + + switch face_index { + case X_PLUS: { face_uv = vec2(direction.z, -direction.y); divisor = direction.x; } + case X_MINUS: { face_uv = vec2(-direction.z, -direction.y); divisor = -direction.x; } + case Y_PLUS: { face_uv = vec2(direction.x, -direction.z); divisor = direction.y; } + case Y_MINUS: { face_uv = vec2(direction.x, direction.z); divisor = -direction.y; } + case Z_PLUS: { face_uv = vec2(direction.x, direction.y); divisor = direction.z; } + case Z_MINUS: { face_uv = vec2(direction.x, -direction.y); divisor = -direction.z; } + default: {} + } + face_uv = (face_uv / divisor) * 0.5 + 0.5; + + switch cubemap_type { + case CUBEMAP_TYPE_CROSS_VERTICAL: { + face_size = vec2(1.0/3.0, 1.0/4.0); + corner_uv = vec2((0x111102u >> (4 * face_index)) & 0xFu, (0x132011u >> (4 * face_index)) & 0xFu); + } + case CUBEMAP_TYPE_CROSS_HORIZONTAL: { + face_size = vec2(1.0/4.0, 1.0/3.0); + corner_uv = vec2((0x131102u >> (4 * face_index)) & 0xFu, (0x112011u >> (4 * face_index)) & 0xFu); + } + case CUBEMAP_TYPE_SEQUENCE_HORIZONTAL: { + face_size = vec2(1.0/6.0, 1.0); + corner_uv.x = face_index; + } + case CUBEMAP_TYPE_SEQUENCE_VERTICAL: { + face_size = vec2(1.0, 1.0/6.0); + corner_uv.y = face_index; + } + default: {} + } + + return (vec2(corner_uv) + face_uv) * face_size; +} + +fn point_light( + light_id: u32, + input: ptr, + enable_diffuse: bool, + enable_texture: bool, +) -> vec3 { + // Unpack. + let diffuse_color = (*input).diffuse_color; + let P = (*input).P; + let N = (*input).layers[LAYER_BASE].N; + let V = (*input).V; + + let light = &view_bindings::clusterable_objects.data[light_id]; + let light_to_frag = (*light).position_radius.xyz - P; + let L = normalize(light_to_frag); + let distance_square = dot(light_to_frag, light_to_frag); + let rangeAttenuation = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w); + + // Base layer + + let specular_L_intensity = compute_specular_layer_values_for_point_light( + input, + LAYER_BASE, + V, + light_to_frag, + (*light).position_radius.w, + ); + var specular_derived_input = derive_lighting_input(N, V, specular_L_intensity.xyz); + + let specular_intensity = specular_L_intensity.w; + +#ifdef STANDARD_MATERIAL_ANISOTROPY + let specular_light = specular_anisotropy(input, &specular_derived_input, L, specular_intensity); +#else // STANDARD_MATERIAL_ANISOTROPY + let specular_light = specular(input, &specular_derived_input, specular_intensity); +#endif // STANDARD_MATERIAL_ANISOTROPY + + // Clearcoat + +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Unpack. + let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N; + let clearcoat_strength = (*input).clearcoat_strength; + + // Perform specular input calculations again for the clearcoat layer. We + // can't reuse the above because the clearcoat normal might be different + // from the main layer normal. + let clearcoat_specular_L_intensity = compute_specular_layer_values_for_point_light( + input, + LAYER_CLEARCOAT, + V, + light_to_frag, + (*light).position_radius.w, + ); + var clearcoat_specular_derived_input = + derive_lighting_input(clearcoat_N, V, clearcoat_specular_L_intensity.xyz); + + // Calculate the specular light. + let clearcoat_specular_intensity = clearcoat_specular_L_intensity.w; + let Fc_Frc = specular_clearcoat( + input, + &clearcoat_specular_derived_input, + clearcoat_strength, + clearcoat_specular_intensity + ); + let inv_Fc = 1.0 - Fc_Frc.r; // Inverse Fresnel term. + let Frc = Fc_Frc.g; // Clearcoat light. +#endif // STANDARD_MATERIAL_CLEARCOAT + + // Diffuse. + // Comes after specular since its N⋅L is used in the lighting equation. + var derived_input = derive_lighting_input(N, V, L); + var diffuse = vec3(0.0); + if (enable_diffuse) { + diffuse = diffuse_color * Fd_Burley(input, &derived_input); + } + + // See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminanceEquation + // Lout = f(v,l) Φ / { 4 π d^2 }⟨n⋅l⟩ + // where + // f(v,l) = (f_d(v,l) + f_r(v,l)) * light_color + // Φ is luminous power in lumens + // our rangeAttenuation = 1 / d^2 multiplied with an attenuation factor for smoothing at the edge of the non-physical maximum light radius + + // For a point light, luminous intensity, I, in lumens per steradian is given by: + // I = Φ / 4 π + // The derivation of this can be seen here: https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower + + // NOTE: (*light).color.rgb is premultiplied with (*light).intensity / 4 π (which would be the luminous intensity) on the CPU + + var color: vec3; +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Account for the Fresnel term from the clearcoat darkening the main layer. + // + // + color = (diffuse + specular_light * inv_Fc) * inv_Fc + Frc; +#else // STANDARD_MATERIAL_CLEARCOAT + color = diffuse + specular_light; +#endif // STANDARD_MATERIAL_CLEARCOAT + + var texture_sample = 1f; + +#ifdef LIGHT_TEXTURES + if enable_texture && (*light).decal_index != 0xFFFFFFFFu { + let relative_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * vec4(P, 1.0)).xyz; + let cubemap_type = view_bindings::clustered_decals.decals[(*light).decal_index].tag; + let decal_uv = cubemap_uv(relative_position, cubemap_type); + let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].image_index; + + texture_sample = textureSampleLevel( + view_bindings::clustered_decal_textures[image_index], + view_bindings::clustered_decal_sampler, + decal_uv, + 0.0 + ).r; + } +#endif + + return color * (*light).color_inverse_square_range.rgb * + (rangeAttenuation * derived_input.NdotL) * texture_sample; +} + +fn spot_light( + light_id: u32, + input: ptr, + enable_diffuse: bool +) -> vec3 { + // reuse the point light calculations + let point_light = point_light(light_id, input, enable_diffuse, false); + + let light = &view_bindings::clusterable_objects.data[light_id]; + + // reconstruct spot dir from x/z and y-direction flag + var spot_dir = vec3((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y); + spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z)); + if ((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u { + spot_dir.y = -spot_dir.y; + } + let light_to_frag = (*light).position_radius.xyz - (*input).P.xyz; + + // calculate attenuation based on filament formula https://google.github.io/filament/Filament.html#listing_glslpunctuallight + // spot_scale and spot_offset have been precomputed + // note we normalize here to get "l" from the filament listing. spot_dir is already normalized + let cd = dot(-spot_dir, normalize(light_to_frag)); + let attenuation = saturate(cd * (*light).light_custom_data.z + (*light).light_custom_data.w); + let spot_attenuation = attenuation * attenuation; + + var texture_sample = 1f; + +#ifdef LIGHT_TEXTURES + if (*light).decal_index != 0xFFFFFFFFu { + let local_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * + vec4((*input).P, 1.0)).xyz; + if local_position.z < 0.0 { + let decal_uv = (local_position.xy / (local_position.z * (*light).spot_light_tan_angle)) * vec2(-0.5, 0.5) + 0.5; + let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].image_index; + + texture_sample = textureSampleLevel( + view_bindings::clustered_decal_textures[image_index], + view_bindings::clustered_decal_sampler, + decal_uv, + 0.0 + ).r; + } + } +#endif + + return point_light * spot_attenuation * texture_sample; +} + +fn directional_light( + light_id: u32, + input: ptr, + enable_diffuse: bool +) -> vec3 { + // Unpack. + let diffuse_color = (*input).diffuse_color; + let NdotV = (*input).layers[LAYER_BASE].NdotV; + let N = (*input).layers[LAYER_BASE].N; + let V = (*input).V; + let roughness = (*input).layers[LAYER_BASE].roughness; + + let light = &view_bindings::lights.directional_lights[light_id]; + + let L = (*light).direction_to_light.xyz; + var derived_input = derive_lighting_input(N, V, L); + + var diffuse = vec3(0.0); + if (enable_diffuse) { + diffuse = diffuse_color * Fd_Burley(input, &derived_input); + } + +#ifdef STANDARD_MATERIAL_ANISOTROPY + let specular_light = specular_anisotropy(input, &derived_input, L, 1.0); +#else // STANDARD_MATERIAL_ANISOTROPY + let specular_light = specular(input, &derived_input, 1.0); +#endif // STANDARD_MATERIAL_ANISOTROPY + +#ifdef STANDARD_MATERIAL_CLEARCOAT + let clearcoat_N = (*input).layers[LAYER_CLEARCOAT].N; + let clearcoat_strength = (*input).clearcoat_strength; + + // Perform specular input calculations again for the clearcoat layer. We + // can't reuse the above because the clearcoat normal might be different + // from the main layer normal. + var derived_clearcoat_input = derive_lighting_input(clearcoat_N, V, L); + + let Fc_Frc = + specular_clearcoat(input, &derived_clearcoat_input, clearcoat_strength, 1.0); + let inv_Fc = 1.0 - Fc_Frc.r; + let Frc = Fc_Frc.g; +#endif // STANDARD_MATERIAL_CLEARCOAT + + var color: vec3; +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Account for the Fresnel term from the clearcoat darkening the main layer. + // + // + color = (diffuse + specular_light * inv_Fc) * inv_Fc * derived_input.NdotL + + Frc * derived_clearcoat_input.NdotL; +#else // STANDARD_MATERIAL_CLEARCOAT + color = (diffuse + specular_light) * derived_input.NdotL; +#endif // STANDARD_MATERIAL_CLEARCOAT + + var texture_sample = 1f; + +#ifdef LIGHT_TEXTURES + if (*light).decal_index != 0xFFFFFFFFu { + let local_position = (view_bindings::clustered_decals.decals[(*light).decal_index].local_from_world * + vec4((*input).P, 1.0)).xyz; + let decal_uv = local_position.xy * vec2(-0.5, 0.5) + 0.5; + + // if tiled or within tile + if (view_bindings::clustered_decals.decals[(*light).decal_index].tag != 0u) + || all(clamp(decal_uv, vec2(0.0), vec2(1.0)) == decal_uv) + { + let image_index = view_bindings::clustered_decals.decals[(*light).decal_index].image_index; + + texture_sample = textureSampleLevel( + view_bindings::clustered_decal_textures[image_index], + view_bindings::clustered_decal_sampler, + decal_uv - floor(decal_uv), + 0.0 + ).r; + } else { + texture_sample = 0f; + } + } +#endif + + return color * (*light).color.rgb * texture_sample; +} diff --git a/crates/libmarathon/src/render/pbr/render/pbr_prepass.wgsl b/crates/libmarathon/src/render/pbr/render/pbr_prepass.wgsl new file mode 100644 index 0000000..68c3602 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/pbr_prepass.wgsl @@ -0,0 +1,151 @@ +#import bevy_pbr::{ + pbr_prepass_functions, + pbr_bindings, + pbr_bindings::material, + pbr_types, + pbr_functions, + pbr_functions::SampleBias, + prepass_io, + mesh_bindings::mesh, + mesh_view_bindings::view, +} + +#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d} + +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output +#endif + +#ifdef BINDLESS +#import bevy_pbr::pbr_bindings::material_indices +#endif // BINDLESS + +#ifdef PREPASS_FRAGMENT +@fragment +fn fragment( +#ifdef MESHLET_MESH_MATERIAL_PASS + @builtin(position) frag_coord: vec4, +#else + in: prepass_io::VertexOutput, + @builtin(front_facing) is_front: bool, +#endif +) -> prepass_io::FragmentOutput { +#ifdef MESHLET_MESH_MATERIAL_PASS + let in = resolve_vertex_output(frag_coord); + let is_front = true; +#else // MESHLET_MESH_MATERIAL_PASS + +#ifdef BINDLESS + let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; + let flags = pbr_bindings::material_array[material_indices[slot].material].flags; + let uv_transform = pbr_bindings::material_array[material_indices[slot].material].uv_transform; +#else // BINDLESS + let flags = pbr_bindings::material.flags; + let uv_transform = pbr_bindings::material.uv_transform; +#endif // BINDLESS + + // If we're in the crossfade section of a visibility range, conditionally + // discard the fragment according to the visibility pattern. +#ifdef VISIBILITY_RANGE_DITHER + pbr_functions::visibility_range_dither(in.position, in.visibility_range_dither); +#endif // VISIBILITY_RANGE_DITHER + + pbr_prepass_functions::prepass_alpha_discard(in); +#endif // MESHLET_MESH_MATERIAL_PASS + + var out: prepass_io::FragmentOutput; + +#ifdef UNCLIPPED_DEPTH_ORTHO_EMULATION + out.frag_depth = in.unclipped_depth; +#endif // UNCLIPPED_DEPTH_ORTHO_EMULATION + +#ifdef NORMAL_PREPASS + // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit + if (flags & pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u { + let double_sided = (flags & pbr_types::STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u; + + let world_normal = pbr_functions::prepare_world_normal( + in.world_normal, + double_sided, + is_front, + ); + + var normal = world_normal; + +#ifdef VERTEX_UVS +#ifdef VERTEX_TANGENTS +#ifdef STANDARD_MATERIAL_NORMAL_MAP + +// TODO: Transforming UVs mean we need to apply derivative chain rule for meshlet mesh material pass +#ifdef STANDARD_MATERIAL_NORMAL_MAP_UV_B + let uv = (uv_transform * vec3(in.uv_b, 1.0)).xy; +#else + let uv = (uv_transform * vec3(in.uv, 1.0)).xy; +#endif + + // Fill in the sample bias so we can sample from textures. + var bias: SampleBias; +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv = in.ddx_uv; + bias.ddy_uv = in.ddy_uv; +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias = view.mip_bias; +#endif // MESHLET_MESH_MATERIAL_PASS + + let Nt = +#ifdef MESHLET_MESH_MATERIAL_PASS + textureSampleGrad( +#else // MESHLET_MESH_MATERIAL_PASS + textureSampleBias( +#endif // MESHLET_MESH_MATERIAL_PASS +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].normal_map_texture], + bindless_samplers_filtering[material_indices[slot].normal_map_sampler], +#else // BINDLESS + pbr_bindings::normal_map_texture, + pbr_bindings::normal_map_sampler, +#endif // BINDLESS + uv, +#ifdef MESHLET_MESH_MATERIAL_PASS + bias.ddx_uv, + bias.ddy_uv, +#else // MESHLET_MESH_MATERIAL_PASS + bias.mip_bias, +#endif // MESHLET_MESH_MATERIAL_PASS + ).rgb; + let TBN = pbr_functions::calculate_tbn_mikktspace(normal, in.world_tangent); + + normal = pbr_functions::apply_normal_mapping( + flags, + TBN, + double_sided, + is_front, + Nt, + ); + +#endif // STANDARD_MATERIAL_NORMAL_MAP +#endif // VERTEX_TANGENTS +#endif // VERTEX_UVS + + out.normal = vec4(normal * 0.5 + vec3(0.5), 1.0); + } else { + out.normal = vec4(in.world_normal * 0.5 + vec3(0.5), 1.0); + } +#endif // NORMAL_PREPASS + +#ifdef MOTION_VECTOR_PREPASS +#ifdef MESHLET_MESH_MATERIAL_PASS + out.motion_vector = in.motion_vector; +#else + out.motion_vector = pbr_prepass_functions::calculate_motion_vector(in.world_position, in.previous_world_position); +#endif +#endif + + return out; +} +#else +@fragment +fn fragment(in: prepass_io::VertexOutput) { + pbr_prepass_functions::prepass_alpha_discard(in); +} +#endif // PREPASS_FRAGMENT diff --git a/crates/libmarathon/src/render/pbr/render/pbr_prepass_functions.wgsl b/crates/libmarathon/src/render/pbr/render/pbr_prepass_functions.wgsl new file mode 100644 index 0000000..d2d2c71 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/pbr_prepass_functions.wgsl @@ -0,0 +1,102 @@ +#define_import_path bevy_pbr::pbr_prepass_functions + +#import bevy_render::bindless::{bindless_samplers_filtering, bindless_textures_2d} + +#import bevy_pbr::{ + prepass_io::VertexOutput, + prepass_bindings::previous_view_uniforms, + mesh_bindings::mesh, + mesh_view_bindings::view, + pbr_bindings, + pbr_types, +} + +#ifdef BINDLESS +#import bevy_pbr::pbr_bindings::material_indices +#endif // BINDLESS + +// Cutoff used for the premultiplied alpha modes BLEND, ADD, and ALPHA_TO_COVERAGE. +const PREMULTIPLIED_ALPHA_CUTOFF = 0.05; + +// We can use a simplified version of alpha_discard() here since we only need to handle the alpha_cutoff +fn prepass_alpha_discard(in: VertexOutput) { + +#ifdef MAY_DISCARD +#ifdef BINDLESS + let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; + var output_color: vec4 = pbr_bindings::material_array[material_indices[slot].material].base_color; + let flags = pbr_bindings::material_array[material_indices[slot].material].flags; +#else // BINDLESS + var output_color: vec4 = pbr_bindings::material.base_color; + let flags = pbr_bindings::material.flags; +#endif // BINDLESS + +#ifdef VERTEX_UVS +#ifdef STANDARD_MATERIAL_BASE_COLOR_UV_B + var uv = in.uv_b; +#else // STANDARD_MATERIAL_BASE_COLOR_UV_B + var uv = in.uv; +#endif // STANDARD_MATERIAL_BASE_COLOR_UV_B + +#ifdef BINDLESS + let uv_transform = pbr_bindings::material_array[material_indices[slot].material].uv_transform; +#else // BINDLESS + let uv_transform = pbr_bindings::material.uv_transform; +#endif // BINDLESS + + uv = (uv_transform * vec3(uv, 1.0)).xy; + if (flags & pbr_types::STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u { + output_color = output_color * textureSampleBias( +#ifdef BINDLESS + bindless_textures_2d[material_indices[slot].base_color_texture], + bindless_samplers_filtering[material_indices[slot].base_color_sampler], +#else // BINDLESS + pbr_bindings::base_color_texture, + pbr_bindings::base_color_sampler, +#endif // BINDLESS + uv, + view.mip_bias + ); + } +#endif // VERTEX_UVS + + let alpha_mode = flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK { +#ifdef BINDLESS + let alpha_cutoff = pbr_bindings::material_array[material_indices[slot].material].alpha_cutoff; +#else // BINDLESS + let alpha_cutoff = pbr_bindings::material.alpha_cutoff; +#endif // BINDLESS + if output_color.a < alpha_cutoff { + discard; + } + } else if (alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND || + alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD || + alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE) { + if output_color.a < PREMULTIPLIED_ALPHA_CUTOFF { + discard; + } + } else if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_PREMULTIPLIED { + if all(output_color < vec4(PREMULTIPLIED_ALPHA_CUTOFF)) { + discard; + } + } + +#endif // MAY_DISCARD +} + +#ifdef MOTION_VECTOR_PREPASS +fn calculate_motion_vector(world_position: vec4, previous_world_position: vec4) -> vec2 { + let clip_position_t = view.unjittered_clip_from_world * world_position; + let clip_position = clip_position_t.xy / clip_position_t.w; + let previous_clip_position_t = previous_view_uniforms.clip_from_world * previous_world_position; + let previous_clip_position = previous_clip_position_t.xy / previous_clip_position_t.w; + // These motion vectors are used as offsets to UV positions and are stored + // in the range -1,1 to allow offsetting from the one corner to the + // diagonally-opposite corner in UV coordinates, in either direction. + // A difference between diagonally-opposite corners of clip space is in the + // range -2,2, so this needs to be scaled by 0.5. And the V direction goes + // down where clip space y goes up, so y needs to be flipped. + return (clip_position - previous_clip_position) * vec2(0.5, -0.5); +} +#endif // MOTION_VECTOR_PREPASS diff --git a/crates/libmarathon/src/render/pbr/render/pbr_transmission.wgsl b/crates/libmarathon/src/render/pbr/render/pbr_transmission.wgsl new file mode 100644 index 0000000..720a42b --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/pbr_transmission.wgsl @@ -0,0 +1,192 @@ +#define_import_path bevy_pbr::transmission + +#import bevy_pbr::{ + lighting, + prepass_utils, + utils::interleaved_gradient_noise, + utils, + mesh_view_bindings as view_bindings, +}; + +#import bevy_render::maths::PI + +#ifdef TONEMAP_IN_SHADER +#import bevy_core_pipeline::tonemapping::approximate_inverse_tone_mapping +#endif + +fn specular_transmissive_light(world_position: vec4, frag_coord: vec3, view_z: f32, N: vec3, V: vec3, F0: vec3, ior: f32, thickness: f32, perceptual_roughness: f32, specular_transmissive_color: vec3, transmitted_environment_light_specular: vec3) -> vec3 { + // Calculate the ratio between refraction indexes. Assume air/vacuum for the space outside the mesh + let eta = 1.0 / ior; + + // Calculate incidence vector (opposite to view vector) and its dot product with the mesh normal + let I = -V; + let NdotI = dot(N, I); + + // Calculate refracted direction using Snell's law + let k = 1.0 - eta * eta * (1.0 - NdotI * NdotI); + let T = eta * I - (eta * NdotI + sqrt(k)) * N; + + // Calculate the exit position of the refracted ray, by propagating refracted direction through thickness + let exit_position = world_position.xyz + T * thickness; + + // Transform exit_position into clip space + let clip_exit_position = view_bindings::view.clip_from_world * vec4(exit_position, 1.0); + + // Scale / offset position so that coordinate is in right space for sampling transmissive background texture + let offset_position = (clip_exit_position.xy / clip_exit_position.w) * vec2(0.5, -0.5) + 0.5; + + // Fetch background color + var background_color: vec4; + if perceptual_roughness == 0.0 { + // If the material has zero roughness, we can use a faster approach without the blur + background_color = fetch_transmissive_background_non_rough(offset_position, frag_coord); + } else { + background_color = fetch_transmissive_background(offset_position, frag_coord, view_z, perceptual_roughness); + } + + // Compensate for exposure, since the background color is coming from an already exposure-adjusted texture + background_color = vec4(background_color.rgb / view_bindings::view.exposure, background_color.a); + + // Dot product of the refracted direction with the exit normal (Note: We assume the exit normal is the entry normal but inverted) + let MinusNdotT = dot(-N, T); + + // Calculate 1.0 - fresnel factor (how much light is _NOT_ reflected, i.e. how much is transmitted) + let F = vec3(1.0) - lighting::fresnel(F0, MinusNdotT); + + // Calculate final color by applying fresnel multiplied specular transmissive color to a mix of background color and transmitted specular environment light + return F * specular_transmissive_color * mix(transmitted_environment_light_specular, background_color.rgb, background_color.a); +} + +fn fetch_transmissive_background_non_rough(offset_position: vec2, frag_coord: vec3) -> vec4 { + var background_color = textureSampleLevel( + view_bindings::view_transmission_texture, + view_bindings::view_transmission_sampler, + offset_position, + 0.0 + ); + +#ifdef DEPTH_PREPASS +#ifndef WEBGL2 + // Use depth prepass data to reject values that are in front of the current fragment + if prepass_utils::prepass_depth(vec4(offset_position * view_bindings::view.viewport.zw, 0.0, 0.0), 0u) > frag_coord.z { + background_color.a = 0.0; + } +#endif +#endif + +#ifdef TONEMAP_IN_SHADER + background_color = approximate_inverse_tone_mapping(background_color, view_bindings::view.color_grading); +#endif + + return background_color; +} + +fn fetch_transmissive_background(offset_position: vec2, frag_coord: vec3, view_z: f32, perceptual_roughness: f32) -> vec4 { + // Calculate view aspect ratio, used to scale offset so that it's proportionate + let aspect = view_bindings::view.viewport.z / view_bindings::view.viewport.w; + + // Calculate how “blurry” the transmission should be. + // Blur is more or less eyeballed to look approximately “right”, since the “correct” + // approach would involve projecting many scattered rays and figuring out their individual + // exit positions. IRL, light rays can be scattered when entering/exiting a material (due to + // roughness) or inside the material (due to subsurface scattering). Here, we only consider + // the first scenario. + // + // Blur intensity is: + // - proportional to the square of `perceptual_roughness` + // - proportional to the inverse of view z + let blur_intensity = (perceptual_roughness * perceptual_roughness) / view_z; + +#ifdef SCREEN_SPACE_SPECULAR_TRANSMISSION_BLUR_TAPS + let num_taps = #{SCREEN_SPACE_SPECULAR_TRANSMISSION_BLUR_TAPS}; // Controlled by the `Camera3d::screen_space_specular_transmission_quality` property +#else + let num_taps = 8; // Fallback to 8 taps, if not specified +#endif + let num_spirals = i32(ceil(f32(num_taps) / 8.0)); +#ifdef TEMPORAL_JITTER + let random_angle = interleaved_gradient_noise(frag_coord.xy, view_bindings::globals.frame_count); +#else + let random_angle = interleaved_gradient_noise(frag_coord.xy, 0u); +#endif + // Pixel checkerboard pattern (helps make the interleaved gradient noise pattern less visible) + let pixel_checkboard = ( +#ifdef TEMPORAL_JITTER + // 0 or 1 on even/odd pixels, alternates every frame + (i32(frag_coord.x) + i32(frag_coord.y) + i32(view_bindings::globals.frame_count)) % 2 +#else + // 0 or 1 on even/odd pixels + (i32(frag_coord.x) + i32(frag_coord.y)) % 2 +#endif + ); + + var result = vec4(0.0); + for (var i: i32 = 0; i < num_taps; i = i + 1) { + let current_spiral = (i >> 3u); + let angle = (random_angle + f32(current_spiral) / f32(num_spirals)) * 2.0 * PI; + let m = vec2(sin(angle), cos(angle)); + let rotation_matrix = mat2x2( + m.y, -m.x, + m.x, m.y + ); + + // Get spiral offset + var spiral_offset: vec2; + switch i & 7 { + // https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135) + // TODO: Figure out a more reasonable way of doing this, as WGSL + // seems to only allow constant indexes into constant arrays at the moment. + // The downstream shader compiler should be able to optimize this into a single + // constant when unrolling the for loop, but it's still not ideal. + case 0: { spiral_offset = utils::SPIRAL_OFFSET_0_; } // Note: We go even first and then odd, so that the lowest + case 1: { spiral_offset = utils::SPIRAL_OFFSET_2_; } // quality possible (which does 4 taps) still does a full spiral + case 2: { spiral_offset = utils::SPIRAL_OFFSET_4_; } // instead of just the first half of it + case 3: { spiral_offset = utils::SPIRAL_OFFSET_6_; } + case 4: { spiral_offset = utils::SPIRAL_OFFSET_1_; } + case 5: { spiral_offset = utils::SPIRAL_OFFSET_3_; } + case 6: { spiral_offset = utils::SPIRAL_OFFSET_5_; } + case 7: { spiral_offset = utils::SPIRAL_OFFSET_7_; } + default: {} + } + + // Make each consecutive spiral slightly smaller than the previous one + spiral_offset *= 1.0 - (0.5 * f32(current_spiral + 1) / f32(num_spirals)); + + // Rotate and correct for aspect ratio + let rotated_spiral_offset = (rotation_matrix * spiral_offset) * vec2(1.0, aspect); + + // Calculate final offset position, with blur and spiral offset + let modified_offset_position = offset_position + rotated_spiral_offset * blur_intensity * (1.0 - f32(pixel_checkboard) * 0.1); + + // Sample the view transmission texture at the offset position + noise offset, to get the background color + var sample = textureSampleLevel( + view_bindings::view_transmission_texture, + view_bindings::view_transmission_sampler, + modified_offset_position, + 0.0 + ); + +#ifdef DEPTH_PREPASS +#ifndef WEBGL2 + // Use depth prepass data to reject values that are in front of the current fragment + if prepass_utils::prepass_depth(vec4(modified_offset_position * view_bindings::view.viewport.zw, 0.0, 0.0), 0u) > frag_coord.z { + sample = vec4(0.0); + } +#endif +#endif + + // As blur intensity grows higher, gradually limit *very bright* color RGB values towards a + // maximum length of 1.0 to prevent stray “firefly” pixel artifacts. This can potentially make + // very strong emissive meshes appear much dimmer, but the artifacts are noticeable enough to + // warrant this treatment. + let normalized_rgb = normalize(sample.rgb); + result += vec4(min(sample.rgb, normalized_rgb / saturate(blur_intensity / 2.0)), sample.a); + } + + result /= f32(num_taps); + +#ifdef TONEMAP_IN_SHADER + result = approximate_inverse_tone_mapping(result, view_bindings::view.color_grading); +#endif + + return result; +} diff --git a/crates/libmarathon/src/render/pbr/render/pbr_types.wgsl b/crates/libmarathon/src/render/pbr/render/pbr_types.wgsl new file mode 100644 index 0000000..b8b51c5 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/pbr_types.wgsl @@ -0,0 +1,151 @@ +#define_import_path bevy_pbr::pbr_types + +// Since this is a hot path, try to keep the alignment and size of the struct members in mind. +// You can find the alignment and sizes at . +struct StandardMaterial { + base_color: vec4, + emissive: vec4, + attenuation_color: vec4, + uv_transform: mat3x3, + reflectance: vec3, + perceptual_roughness: f32, + metallic: f32, + diffuse_transmission: f32, + specular_transmission: f32, + thickness: f32, + ior: f32, + attenuation_distance: f32, + clearcoat: f32, + clearcoat_perceptual_roughness: f32, + anisotropy_strength: f32, + anisotropy_rotation: vec2, + // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. + flags: u32, + alpha_cutoff: f32, + parallax_depth_scale: f32, + max_parallax_layer_count: f32, + lightmap_exposure: f32, + max_relief_mapping_search_steps: u32, + /// ID for specifying which deferred lighting pass should be used for rendering this material, if any. + deferred_lighting_pass_id: u32, +}; + +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// NOTE: if these flags are updated or changed. Be sure to also update +// deferred_flags_from_mesh_material_flags and mesh_material_flags_from_deferred_flags +// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +const STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT: u32 = 1u << 0u; +const STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT: u32 = 1u << 1u; +const STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT: u32 = 1u << 2u; +const STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT: u32 = 1u << 3u; +const STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT: u32 = 1u << 4u; +const STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 1u << 5u; +const STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 1u << 6u; +const STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y: u32 = 1u << 7u; +const STANDARD_MATERIAL_FLAGS_FOG_ENABLED_BIT: u32 = 1u << 8u; +const STANDARD_MATERIAL_FLAGS_DEPTH_MAP_BIT: u32 = 1u << 9u; +const STANDARD_MATERIAL_FLAGS_SPECULAR_TRANSMISSION_TEXTURE_BIT: u32 = 1u << 10u; +const STANDARD_MATERIAL_FLAGS_THICKNESS_TEXTURE_BIT: u32 = 1u << 11u; +const STANDARD_MATERIAL_FLAGS_DIFFUSE_TRANSMISSION_TEXTURE_BIT: u32 = 1u << 12u; +const STANDARD_MATERIAL_FLAGS_ATTENUATION_ENABLED_BIT: u32 = 1u << 13u; +const STANDARD_MATERIAL_FLAGS_CLEARCOAT_TEXTURE_BIT: u32 = 1u << 14u; +const STANDARD_MATERIAL_FLAGS_CLEARCOAT_ROUGHNESS_TEXTURE_BIT: u32 = 1u << 15u; +const STANDARD_MATERIAL_FLAGS_CLEARCOAT_NORMAL_TEXTURE_BIT: u32 = 1u << 16u; +const STANDARD_MATERIAL_FLAGS_ANISOTROPY_TEXTURE_BIT: u32 = 1u << 17u; +const STANDARD_MATERIAL_FLAGS_SPECULAR_TEXTURE_BIT: u32 = 1u << 18u; +const STANDARD_MATERIAL_FLAGS_SPECULAR_TINT_TEXTURE_BIT: u32 = 1u << 19u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 7u << 29u; // (0b111u << 29u) +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 1u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 2u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_PREMULTIPLIED: u32 = 3u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD: u32 = 4u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MULTIPLY: u32 = 5u << 29u; +const STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ALPHA_TO_COVERAGE: u32 = 6u << 29u; + + +// Creates a StandardMaterial with default values +fn standard_material_new() -> StandardMaterial { + var material: StandardMaterial; + + // NOTE: Keep in-sync with src/pbr_material.rs! + material.base_color = vec4(1.0, 1.0, 1.0, 1.0); + material.emissive = vec4(0.0, 0.0, 0.0, 1.0); + material.perceptual_roughness = 0.5; + material.metallic = 0.00; + material.reflectance = vec3(0.5); + material.diffuse_transmission = 0.0; + material.specular_transmission = 0.0; + material.thickness = 0.0; + material.ior = 1.5; + material.attenuation_distance = 1.0; + material.attenuation_color = vec4(1.0, 1.0, 1.0, 1.0); + material.clearcoat = 0.0; + material.clearcoat_perceptual_roughness = 0.0; + material.flags = STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE; + material.alpha_cutoff = 0.5; + material.parallax_depth_scale = 0.1; + material.max_parallax_layer_count = 16.0; + material.max_relief_mapping_search_steps = 5u; + material.deferred_lighting_pass_id = 1u; + // scale 1, translation 0, rotation 0 + material.uv_transform = mat3x3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); + + return material; +} + +struct PbrInput { + material: StandardMaterial, + // Note: this gets monochromized upon deferred PbrInput reconstruction. + diffuse_occlusion: vec3, + // Note: this is 1.0 (entirely unoccluded) when SSAO and SSR are off. + specular_occlusion: f32, + frag_coord: vec4, + world_position: vec4, + // Normalized world normal used for shadow mapping as normal-mapping is not used for shadow + // mapping + world_normal: vec3, + // Normalized normal-mapped world normal used for lighting + N: vec3, + // Normalized view vector in world space, pointing from the fragment world position toward the + // view world position + V: vec3, + lightmap_light: vec3, + clearcoat_N: vec3, + anisotropy_strength: f32, + // These two aren't specific to anisotropy, but we only fill them in if + // we're doing anisotropy, so they're prefixed with `anisotropy_`. + anisotropy_T: vec3, + anisotropy_B: vec3, + is_orthographic: bool, + flags: u32, +}; + +// Creates a PbrInput with default values +fn pbr_input_new() -> PbrInput { + var pbr_input: PbrInput; + + pbr_input.material = standard_material_new(); + pbr_input.diffuse_occlusion = vec3(1.0); + // If SSAO is enabled, then this gets overwritten with proper specular occlusion. If its not, then we get specular environment map unoccluded (we have no data with which to occlude it with). + pbr_input.specular_occlusion = 1.0; + + pbr_input.frag_coord = vec4(0.0, 0.0, 0.0, 1.0); + pbr_input.world_position = vec4(0.0, 0.0, 0.0, 1.0); + pbr_input.world_normal = vec3(0.0, 0.0, 1.0); + + pbr_input.is_orthographic = false; + + pbr_input.N = vec3(0.0, 0.0, 1.0); + pbr_input.V = vec3(1.0, 0.0, 0.0); + + pbr_input.clearcoat_N = vec3(0.0); + pbr_input.anisotropy_T = vec3(0.0); + pbr_input.anisotropy_B = vec3(0.0); + + pbr_input.lightmap_light = vec3(0.0); + + pbr_input.flags = 0u; + + return pbr_input; +} diff --git a/crates/libmarathon/src/render/pbr/render/reset_indirect_batch_sets.wgsl b/crates/libmarathon/src/render/pbr/render/reset_indirect_batch_sets.wgsl new file mode 100644 index 0000000..9309594 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/reset_indirect_batch_sets.wgsl @@ -0,0 +1,25 @@ +// Resets the indirect draw counts to zero. +// +// This shader is needed because we reuse the same indirect batch set count +// buffer (i.e. the buffer that gets passed to `multi_draw_indirect_count` to +// determine how many objects to draw) between phases (early, late, and main). +// Before launching `build_indirect_params.wgsl`, we need to reinitialize the +// value to 0. + +#import bevy_pbr::mesh_preprocess_types::IndirectBatchSet + +@group(0) @binding(0) var indirect_batch_sets: array; + +@compute +@workgroup_size(64) +fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Figure out our instance index. If this thread doesn't correspond to any + // index, bail. + let instance_index = global_invocation_id.x; + if (instance_index >= arrayLength(&indirect_batch_sets)) { + return; + } + + // Reset the number of batch sets to 0. + atomicStore(&indirect_batch_sets[instance_index].indirect_parameters_count, 0u); +} diff --git a/crates/libmarathon/src/render/pbr/render/rgb9e5.wgsl b/crates/libmarathon/src/render/pbr/render/rgb9e5.wgsl new file mode 100644 index 0000000..c635c83 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/rgb9e5.wgsl @@ -0,0 +1,63 @@ +#define_import_path bevy_pbr::rgb9e5 + +const RGB9E5_EXPONENT_BITS = 5u; +const RGB9E5_MANTISSA_BITS = 9; +const RGB9E5_MANTISSA_BITSU = 9u; +const RGB9E5_EXP_BIAS = 15; +const RGB9E5_MAX_VALID_BIASED_EXP = 31u; + +//#define MAX_RGB9E5_EXP (RGB9E5_MAX_VALID_BIASED_EXP - RGB9E5_EXP_BIAS) +//#define RGB9E5_MANTISSA_VALUES (1< i32 { + let f = bitcast(x); + let biasedexponent = (f & 0x7F800000u) >> 23u; + return i32(biasedexponent) - 127; +} + +// https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_shared_exponent.txt +fn vec3_to_rgb9e5_(rgb_in: vec3) -> u32 { + let rgb = clamp(rgb_in, vec3(0.0), vec3(MAX_RGB9E5_)); + + let maxrgb = max(rgb.r, max(rgb.g, rgb.b)); + var exp_shared = max(-RGB9E5_EXP_BIAS - 1, floor_log2_(maxrgb)) + 1 + RGB9E5_EXP_BIAS; + var denom = exp2(f32(exp_shared - RGB9E5_EXP_BIAS - RGB9E5_MANTISSA_BITS)); + + let maxm = i32(floor(maxrgb / denom + 0.5)); + if (maxm == RGB9E5_MANTISSA_VALUES) { + denom *= 2.0; + exp_shared += 1; + } + + let n = vec3(floor(rgb / denom + 0.5)); + + return (u32(exp_shared) << 27u) | (n.b << 18u) | (n.g << 9u) | (n.r << 0u); +} + +// Builtin extractBits() is not working on WEBGL or DX12 +// DX12: HLSL: Unimplemented("write_expr_math ExtractBits") +fn extract_bits(value: u32, offset: u32, bits: u32) -> u32 { + let mask = (1u << bits) - 1u; + return (value >> offset) & mask; +} + +fn rgb9e5_to_vec3_(v: u32) -> vec3 { + let exponent = i32(extract_bits(v, 27u, RGB9E5_EXPONENT_BITS)) - RGB9E5_EXP_BIAS - RGB9E5_MANTISSA_BITS; + let scale = exp2(f32(exponent)); + + return vec3( + f32(extract_bits(v, 0u, RGB9E5_MANTISSA_BITSU)), + f32(extract_bits(v, 9u, RGB9E5_MANTISSA_BITSU)), + f32(extract_bits(v, 18u, RGB9E5_MANTISSA_BITSU)) + ) * scale; +} diff --git a/crates/libmarathon/src/render/pbr/render/shadow_sampling.wgsl b/crates/libmarathon/src/render/pbr/render/shadow_sampling.wgsl new file mode 100644 index 0000000..2b35e57 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/shadow_sampling.wgsl @@ -0,0 +1,599 @@ +#define_import_path bevy_pbr::shadow_sampling + +#import bevy_pbr::{ + mesh_view_bindings as view_bindings, + utils::interleaved_gradient_noise, + utils, +} +#import bevy_render::maths::{orthonormalize, PI} + +// Do the lookup, using HW 2x2 PCF and comparison +fn sample_shadow_map_hardware(light_local: vec2, depth: f32, array_index: i32) -> f32 { +#ifdef NO_ARRAY_TEXTURES_SUPPORT + return textureSampleCompare( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_comparison_sampler, + light_local, + depth, + ); +#else + return textureSampleCompareLevel( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_comparison_sampler, + light_local, + array_index, + depth, + ); +#endif +} + +// Does a single sample of the blocker search, a part of the PCSS algorithm. +// This is the variant used for directional lights. +fn search_for_blockers_in_shadow_map_hardware( + light_local: vec2, + depth: f32, + array_index: i32, +) -> vec2 { +#ifdef WEBGL2 + // Make sure that the WebGL 2 compiler doesn't see `sampled_depth` sampled + // with different samplers, or it'll blow up. + return vec2(0.0); +#else // WEBGL2 + +#ifdef PCSS_SAMPLERS_AVAILABLE + +#ifdef NO_ARRAY_TEXTURES_SUPPORT + let sampled_depth = textureSampleLevel( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_linear_sampler, + light_local, + 0u, + ); +#else // NO_ARRAY_TEXTURES_SUPPORT + let sampled_depth = textureSampleLevel( + view_bindings::directional_shadow_textures, + view_bindings::directional_shadow_textures_linear_sampler, + light_local, + array_index, + 0u, + ); +#endif // NO_ARRAY_TEXTURES_SUPPORT + return select(vec2(0.0), vec2(sampled_depth, 1.0), sampled_depth >= depth); + +#else // PCSS_SAMPLERS_AVAILABLE + return vec2(0.0); +#endif // PCSS_SAMPLERS_AVAILABLE + +#endif // WEBGL2 +} + +// Numbers determined by trial and error that gave nice results. +const SPOT_SHADOW_TEXEL_SIZE: f32 = 0.0134277345; +const POINT_SHADOW_SCALE: f32 = 0.003; +const POINT_SHADOW_TEMPORAL_OFFSET_SCALE: f32 = 0.5; + +// These are the standard MSAA sample point positions from D3D. They were chosen +// to get a reasonable distribution that's not too regular. +// +// https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ne-d3d11-d3d11_standard_multisample_quality_levels?redirectedfrom=MSDN +const D3D_SAMPLE_POINT_POSITIONS: array, 8> = array( + vec2( 0.125, -0.375), + vec2(-0.125, 0.375), + vec2( 0.625, 0.125), + vec2(-0.375, -0.625), + vec2(-0.625, 0.625), + vec2(-0.875, -0.125), + vec2( 0.375, 0.875), + vec2( 0.875, -0.875), +); + +// And these are the coefficients corresponding to the probability distribution +// function of a 2D Gaussian lobe with zero mean and the identity covariance +// matrix at those points. +const D3D_SAMPLE_POINT_COEFFS: array = array( + 0.157112, + 0.157112, + 0.138651, + 0.130251, + 0.114946, + 0.114946, + 0.107982, + 0.079001, +); + +// https://web.archive.org/web/20230210095515/http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1 +fn sample_shadow_map_castano_thirteen(light_local: vec2, depth: f32, array_index: i32) -> f32 { + let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); + let inv_shadow_map_size = 1.0 / shadow_map_size; + + let uv = light_local * shadow_map_size; + var base_uv = floor(uv + 0.5); + let s = (uv.x + 0.5 - base_uv.x); + let t = (uv.y + 0.5 - base_uv.y); + base_uv -= 0.5; + base_uv *= inv_shadow_map_size; + + let uw0 = (4.0 - 3.0 * s); + let uw1 = 7.0; + let uw2 = (1.0 + 3.0 * s); + + let u0 = (3.0 - 2.0 * s) / uw0 - 2.0; + let u1 = (3.0 + s) / uw1; + let u2 = s / uw2 + 2.0; + + let vw0 = (4.0 - 3.0 * t); + let vw1 = 7.0; + let vw2 = (1.0 + 3.0 * t); + + let v0 = (3.0 - 2.0 * t) / vw0 - 2.0; + let v1 = (3.0 + t) / vw1; + let v2 = t / vw2 + 2.0; + + var sum = 0.0; + + sum += uw0 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u0, v0) * inv_shadow_map_size), depth, array_index); + sum += uw1 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u1, v0) * inv_shadow_map_size), depth, array_index); + sum += uw2 * vw0 * sample_shadow_map_hardware(base_uv + (vec2(u2, v0) * inv_shadow_map_size), depth, array_index); + + sum += uw0 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u0, v1) * inv_shadow_map_size), depth, array_index); + sum += uw1 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u1, v1) * inv_shadow_map_size), depth, array_index); + sum += uw2 * vw1 * sample_shadow_map_hardware(base_uv + (vec2(u2, v1) * inv_shadow_map_size), depth, array_index); + + sum += uw0 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u0, v2) * inv_shadow_map_size), depth, array_index); + sum += uw1 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u1, v2) * inv_shadow_map_size), depth, array_index); + sum += uw2 * vw2 * sample_shadow_map_hardware(base_uv + (vec2(u2, v2) * inv_shadow_map_size), depth, array_index); + + return sum * (1.0 / 144.0); +} + +fn map(min1: f32, max1: f32, min2: f32, max2: f32, value: f32) -> f32 { + return min2 + (value - min1) * (max2 - min2) / (max1 - min1); +} + +// Creates a random rotation matrix using interleaved gradient noise. +// +// See: https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare/ +fn random_rotation_matrix(scale: vec2, temporal: bool) -> mat2x2 { + let random_angle = 2.0 * PI * interleaved_gradient_noise( + scale, select(1u, view_bindings::globals.frame_count, temporal)); + let m = vec2(sin(random_angle), cos(random_angle)); + return mat2x2( + m.y, -m.x, + m.x, m.y + ); +} + +// Calculates the distance between spiral samples for the given texel size and +// penumbra size. This is used for the Jimenez '14 (i.e. temporal) variant of +// shadow sampling. +fn calculate_uv_offset_scale_jimenez_fourteen(texel_size: f32, blur_size: f32) -> vec2 { + let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); + + // Empirically chosen fudge factor to make PCF look better across different CSM cascades + let f = map(0.00390625, 0.022949219, 0.015, 0.035, texel_size); + return f * blur_size / (texel_size * shadow_map_size); +} + +fn sample_shadow_map_jimenez_fourteen( + light_local: vec2, + depth: f32, + array_index: i32, + texel_size: f32, + blur_size: f32, + temporal: bool, +) -> f32 { + let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); + let rotation_matrix = random_rotation_matrix(light_local * shadow_map_size, temporal); + let uv_offset_scale = calculate_uv_offset_scale_jimenez_fourteen(texel_size, blur_size); + + // https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135) + let sample_offset0 = (rotation_matrix * utils::SPIRAL_OFFSET_0_) * uv_offset_scale; + let sample_offset1 = (rotation_matrix * utils::SPIRAL_OFFSET_1_) * uv_offset_scale; + let sample_offset2 = (rotation_matrix * utils::SPIRAL_OFFSET_2_) * uv_offset_scale; + let sample_offset3 = (rotation_matrix * utils::SPIRAL_OFFSET_3_) * uv_offset_scale; + let sample_offset4 = (rotation_matrix * utils::SPIRAL_OFFSET_4_) * uv_offset_scale; + let sample_offset5 = (rotation_matrix * utils::SPIRAL_OFFSET_5_) * uv_offset_scale; + let sample_offset6 = (rotation_matrix * utils::SPIRAL_OFFSET_6_) * uv_offset_scale; + let sample_offset7 = (rotation_matrix * utils::SPIRAL_OFFSET_7_) * uv_offset_scale; + + var sum = 0.0; + sum += sample_shadow_map_hardware(light_local + sample_offset0, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset1, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset2, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset3, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset4, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset5, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset6, depth, array_index); + sum += sample_shadow_map_hardware(light_local + sample_offset7, depth, array_index); + return sum / 8.0; +} + +// Performs the blocker search portion of percentage-closer soft shadows (PCSS). +// This is the variation used for directional lights. +// +// We can't use Castano '13 here because that has a hard-wired fixed size, while +// the PCSS algorithm requires a search size that varies based on the size of +// the light. So we instead use the D3D sample point positions, spaced according +// to the search size, to provide a sample pattern in a similar manner to the +// cubemap sampling approach we use for PCF. +// +// `search_size` is the size of the search region in texels. +fn search_for_blockers_in_shadow_map( + light_local: vec2, + depth: f32, + array_index: i32, + texel_size: f32, + search_size: f32, +) -> f32 { + let shadow_map_size = vec2(textureDimensions(view_bindings::directional_shadow_textures)); + let uv_offset_scale = search_size / (texel_size * shadow_map_size); + + let offset0 = D3D_SAMPLE_POINT_POSITIONS[0] * uv_offset_scale; + let offset1 = D3D_SAMPLE_POINT_POSITIONS[1] * uv_offset_scale; + let offset2 = D3D_SAMPLE_POINT_POSITIONS[2] * uv_offset_scale; + let offset3 = D3D_SAMPLE_POINT_POSITIONS[3] * uv_offset_scale; + let offset4 = D3D_SAMPLE_POINT_POSITIONS[4] * uv_offset_scale; + let offset5 = D3D_SAMPLE_POINT_POSITIONS[5] * uv_offset_scale; + let offset6 = D3D_SAMPLE_POINT_POSITIONS[6] * uv_offset_scale; + let offset7 = D3D_SAMPLE_POINT_POSITIONS[7] * uv_offset_scale; + + var sum = vec2(0.0); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset0, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset1, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset2, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset3, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset4, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset5, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset6, depth, array_index); + sum += search_for_blockers_in_shadow_map_hardware(light_local + offset7, depth, array_index); + + if (sum.y == 0.0) { + return 0.0; + } + return sum.x / sum.y; +} + +fn sample_shadow_map(light_local: vec2, depth: f32, array_index: i32, texel_size: f32) -> f32 { +#ifdef SHADOW_FILTER_METHOD_GAUSSIAN + return sample_shadow_map_castano_thirteen(light_local, depth, array_index); +#else ifdef SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_map_jimenez_fourteen( + light_local, depth, array_index, texel_size, 1.0, true); +#else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2 + return sample_shadow_map_hardware(light_local, depth, array_index); +#else + // This needs a default return value to avoid shader compilation errors if it's compiled with no SHADOW_FILTER_METHOD_* defined. + // (eg. if the normal prepass is enabled it ends up compiling this due to the normal prepass depending on pbr_functions, which depends on shadows) + // This should never actually get used, as anyone using bevy's lighting/shadows should always have a SHADOW_FILTER_METHOD defined. + // Set to 0 to make it obvious that something is wrong. + return 0.0; +#endif +} + +// Samples the shadow map for a directional light when percentage-closer soft +// shadows are being used. +// +// We first search for a *blocker*, which is the average depth value of any +// shadow map samples that are adjacent to the sample we're considering. That +// allows us to determine the penumbra size; a larger gap between the blocker +// and the depth of this sample results in a wider penumbra. Finally, we sample +// the shadow map the same way we do in PCF, using that penumbra width. +// +// A good overview of the technique: +// +fn sample_shadow_map_pcss( + light_local: vec2, + depth: f32, + array_index: i32, + texel_size: f32, + light_size: f32, +) -> f32 { + // Determine the average Z value of the closest blocker. + let z_blocker = search_for_blockers_in_shadow_map( + light_local, depth, array_index, texel_size, light_size); + + // Don't let the blur size go below 0.5, or shadows will look unacceptably aliased. + let blur_size = max((z_blocker - depth) * light_size / depth, 0.5); + + // FIXME: We can't use Castano '13 here because that has a hard-wired fixed + // size. So we instead use Jimenez '14 unconditionally. In the non-temporal + // variant this is unfortunately rather noisy. This may be improvable in the + // future by generating a mip chain of the shadow map and using that to + // provide better blurs. +#ifdef SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_map_jimenez_fourteen( + light_local, depth, array_index, texel_size, blur_size, true); +#else // SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_map_jimenez_fourteen( + light_local, depth, array_index, texel_size, blur_size, false); +#endif // SHADOW_FILTER_METHOD_TEMPORAL +} + +// NOTE: Due to the non-uniform control flow in `shadows::fetch_point_shadow`, +// we must use the Level variant of textureSampleCompare to avoid undefined +// behavior due to some of the fragments in a quad (2x2 fragments) being +// processed not being sampled, and this messing with mip-mapping functionality. +// The shadow maps have no mipmaps so Level just samples from LOD 0. +fn sample_shadow_cubemap_hardware(light_local: vec3, depth: f32, light_id: u32) -> f32 { +#ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT + return textureSampleCompare( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_comparison_sampler, + light_local, + depth + ); +#else + return textureSampleCompareLevel( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_comparison_sampler, + light_local, + i32(light_id), + depth + ); +#endif +} + +// Performs one sample of the blocker search. This variation of the blocker +// search function is for point and spot lights. +fn search_for_blockers_in_shadow_cubemap_hardware( + light_local: vec3, + depth: f32, + light_id: u32, +) -> vec2 { +#ifdef WEBGL2 + // Make sure that the WebGL 2 compiler doesn't see `sampled_depth` sampled + // with different samplers, or it'll blow up. + return vec2(0.0); +#else // WEBGL2 + +#ifdef PCSS_SAMPLERS_AVAILABLE + +#ifdef NO_CUBE_ARRAY_TEXTURES_SUPPORT + let sampled_depth = textureSample( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_linear_sampler, + light_local, + ); +#else + let sampled_depth = textureSample( + view_bindings::point_shadow_textures, + view_bindings::point_shadow_textures_linear_sampler, + light_local, + i32(light_id), + ); +#endif + + return select(vec2(0.0), vec2(sampled_depth, 1.0), sampled_depth >= depth); + +#else // PCSS_SAMPLERS_AVAILABLE + return vec2(0.0); +#endif // PCSS_SAMPLERS_AVAILABLE + +#endif // WEBGL2 +} + +fn sample_shadow_cubemap_at_offset( + position: vec2, + coeff: f32, + x_basis: vec3, + y_basis: vec3, + light_local: vec3, + depth: f32, + light_id: u32, +) -> f32 { + return sample_shadow_cubemap_hardware( + light_local + position.x * x_basis + position.y * y_basis, + depth, + light_id + ) * coeff; +} + +// Computes the search position and performs one sample of the blocker search. +// This variation of the blocker search function is for point and spot lights. +// +// `x_basis`, `y_basis`, and `light_local` form an orthonormal basis over which +// the blocker search happens. +fn search_for_blockers_in_shadow_cubemap_at_offset( + position: vec2, + x_basis: vec3, + y_basis: vec3, + light_local: vec3, + depth: f32, + light_id: u32, +) -> vec2 { + return search_for_blockers_in_shadow_cubemap_hardware( + light_local + position.x * x_basis + position.y * y_basis, + depth, + light_id + ); +} + +// This more or less does what Castano13 does, but in 3D space. Castano13 is +// essentially an optimized 2D Gaussian filter that takes advantage of the +// bilinear filtering hardware to reduce the number of samples needed. This +// trick doesn't apply to cubemaps, so we manually apply a Gaussian filter over +// the standard 8xMSAA pattern instead. +fn sample_shadow_cubemap_gaussian( + light_local: vec3, + depth: f32, + scale: f32, + distance_to_light: f32, + light_id: u32, +) -> f32 { + // Create an orthonormal basis so we can apply a 2D sampling pattern to a + // cubemap. + let basis = orthonormalize(normalize(light_local)) * scale * distance_to_light; + + var sum: f32 = 0.0; + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[0], D3D_SAMPLE_POINT_COEFFS[0], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[1], D3D_SAMPLE_POINT_COEFFS[1], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[2], D3D_SAMPLE_POINT_COEFFS[2], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[3], D3D_SAMPLE_POINT_COEFFS[3], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[4], D3D_SAMPLE_POINT_COEFFS[4], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[5], D3D_SAMPLE_POINT_COEFFS[5], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[6], D3D_SAMPLE_POINT_COEFFS[6], + basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[7], D3D_SAMPLE_POINT_COEFFS[7], + basis[0], basis[1], light_local, depth, light_id); + return sum; +} + +// This is a port of the Jimenez14 filter above to the 3D space. It jitters the +// points in the spiral pattern after first creating a 2D orthonormal basis +// along the principal light direction. +fn sample_shadow_cubemap_jittered( + light_local: vec3, + depth: f32, + scale: f32, + distance_to_light: f32, + light_id: u32, + temporal: bool, +) -> f32 { + // Create an orthonormal basis so we can apply a 2D sampling pattern to a + // cubemap. + let basis = orthonormalize(normalize(light_local)) * scale * distance_to_light; + + let rotation_matrix = random_rotation_matrix(vec2(1.0), temporal); + + let sample_offset0 = rotation_matrix * utils::SPIRAL_OFFSET_0_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset1 = rotation_matrix * utils::SPIRAL_OFFSET_1_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset2 = rotation_matrix * utils::SPIRAL_OFFSET_2_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset3 = rotation_matrix * utils::SPIRAL_OFFSET_3_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset4 = rotation_matrix * utils::SPIRAL_OFFSET_4_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset5 = rotation_matrix * utils::SPIRAL_OFFSET_5_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset6 = rotation_matrix * utils::SPIRAL_OFFSET_6_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + let sample_offset7 = rotation_matrix * utils::SPIRAL_OFFSET_7_ * + POINT_SHADOW_TEMPORAL_OFFSET_SCALE; + + var sum: f32 = 0.0; + sum += sample_shadow_cubemap_at_offset( + sample_offset0, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset1, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset2, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset3, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset4, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset5, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset6, 0.125, basis[0], basis[1], light_local, depth, light_id); + sum += sample_shadow_cubemap_at_offset( + sample_offset7, 0.125, basis[0], basis[1], light_local, depth, light_id); + return sum; +} + +fn sample_shadow_cubemap( + light_local: vec3, + distance_to_light: f32, + depth: f32, + light_id: u32, +) -> f32 { +#ifdef SHADOW_FILTER_METHOD_GAUSSIAN + return sample_shadow_cubemap_gaussian( + light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_id); +#else ifdef SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_cubemap_jittered( + light_local, depth, POINT_SHADOW_SCALE, distance_to_light, light_id, true); +#else ifdef SHADOW_FILTER_METHOD_HARDWARE_2X2 + return sample_shadow_cubemap_hardware(light_local, depth, light_id); +#else + // This needs a default return value to avoid shader compilation errors if it's compiled with no SHADOW_FILTER_METHOD_* defined. + // (eg. if the normal prepass is enabled it ends up compiling this due to the normal prepass depending on pbr_functions, which depends on shadows) + // This should never actually get used, as anyone using bevy's lighting/shadows should always have a SHADOW_FILTER_METHOD defined. + // Set to 0 to make it obvious that something is wrong. + return 0.0; +#endif +} + +// Searches for PCSS blockers in a cubemap. This is the variant of the blocker +// search used for point and spot lights. +// +// This follows the logic in `sample_shadow_cubemap_gaussian`, but uses linear +// sampling instead of percentage-closer filtering. +// +// The `scale` parameter represents the size of the light. +fn search_for_blockers_in_shadow_cubemap( + light_local: vec3, + depth: f32, + scale: f32, + distance_to_light: f32, + light_id: u32, +) -> f32 { + // Create an orthonormal basis so we can apply a 2D sampling pattern to a + // cubemap. + let basis = orthonormalize(normalize(light_local)) * scale * distance_to_light; + + var sum: vec2 = vec2(0.0); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[0], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[1], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[2], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[3], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[4], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[5], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[6], basis[0], basis[1], light_local, depth, light_id); + sum += search_for_blockers_in_shadow_cubemap_at_offset( + D3D_SAMPLE_POINT_POSITIONS[7], basis[0], basis[1], light_local, depth, light_id); + + if (sum.y == 0.0) { + return 0.0; + } + return sum.x / sum.y; +} + +// Samples the shadow map for a point or spot light when percentage-closer soft +// shadows are being used. +// +// A good overview of the technique: +// +fn sample_shadow_cubemap_pcss( + light_local: vec3, + distance_to_light: f32, + depth: f32, + light_id: u32, + light_size: f32, +) -> f32 { + let z_blocker = search_for_blockers_in_shadow_cubemap( + light_local, depth, light_size, distance_to_light, light_id); + + // Don't let the blur size go below 0.5, or shadows will look unacceptably aliased. + let blur_size = max((z_blocker - depth) * light_size / depth, 0.5); + +#ifdef SHADOW_FILTER_METHOD_TEMPORAL + return sample_shadow_cubemap_jittered( + light_local, depth, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_id, true); +#else + return sample_shadow_cubemap_jittered( + light_local, depth, POINT_SHADOW_SCALE * blur_size, distance_to_light, light_id, false); +#endif +} diff --git a/crates/libmarathon/src/render/pbr/render/shadows.wgsl b/crates/libmarathon/src/render/pbr/render/shadows.wgsl new file mode 100644 index 0000000..a3727b4 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/shadows.wgsl @@ -0,0 +1,231 @@ +#define_import_path bevy_pbr::shadows + +#import bevy_pbr::{ + mesh_view_types::POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, + mesh_view_bindings as view_bindings, + shadow_sampling::{ + SPOT_SHADOW_TEXEL_SIZE, sample_shadow_cubemap, sample_shadow_cubemap_pcss, + sample_shadow_map, sample_shadow_map_pcss, + } +} + +#import bevy_render::{ + color_operations::hsv_to_rgb, + maths::{orthonormalize, PI_2} +} + +const flip_z: vec3 = vec3(1.0, 1.0, -1.0); + +fn fetch_point_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3) -> f32 { + let light = &view_bindings::clusterable_objects.data[light_id]; + + // because the shadow maps align with the axes and the frustum planes are at 45 degrees + // we can get the worldspace depth by taking the largest absolute axis + let surface_to_light = (*light).position_radius.xyz - frag_position.xyz; + let surface_to_light_abs = abs(surface_to_light); + let distance_to_light = max(surface_to_light_abs.x, max(surface_to_light_abs.y, surface_to_light_abs.z)); + + // The normal bias here is already scaled by the texel size at 1 world unit from the light. + // The texel size increases proportionally with distance from the light so multiplying by + // distance to light scales the normal bias to the texel size at the fragment distance. + let normal_offset = (*light).shadow_normal_bias * distance_to_light * surface_normal.xyz; + let depth_offset = (*light).shadow_depth_bias * normalize(surface_to_light.xyz); + let offset_position = frag_position.xyz + normal_offset + depth_offset; + + // similar largest-absolute-axis trick as above, but now with the offset fragment position + let frag_ls = offset_position.xyz - (*light).position_radius.xyz ; + let abs_position_ls = abs(frag_ls); + let major_axis_magnitude = max(abs_position_ls.x, max(abs_position_ls.y, abs_position_ls.z)); + + // NOTE: These simplifications come from multiplying: + // projection * vec4(0, 0, -major_axis_magnitude, 1.0) + // and keeping only the terms that have any impact on the depth. + // Projection-agnostic approach: + let zw = -major_axis_magnitude * (*light).light_custom_data.xy + (*light).light_custom_data.zw; + let depth = zw.x / zw.y; + + // If soft shadows are enabled, use the PCSS path. Cubemaps assume a + // left-handed coordinate space, so we have to flip the z-axis when + // sampling. + if ((*light).soft_shadow_size > 0.0) { + return sample_shadow_cubemap_pcss( + frag_ls * flip_z, + distance_to_light, + depth, + light_id, + (*light).soft_shadow_size, + ); + } + + // Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed + // coordinate space, so we have to flip the z-axis when sampling. + return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_id); +} + +fn fetch_spot_shadow( + light_id: u32, + frag_position: vec4, + surface_normal: vec3, + near_z: f32, +) -> f32 { + let light = &view_bindings::clusterable_objects.data[light_id]; + + let surface_to_light = (*light).position_radius.xyz - frag_position.xyz; + + // construct the light view matrix + var spot_dir = vec3((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y); + // reconstruct spot dir from x/z and y-direction flag + spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z)); + if (((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u) { + spot_dir.y = -spot_dir.y; + } + + // view matrix z_axis is the reverse of transform.forward() + let fwd = -spot_dir; + let distance_to_light = dot(fwd, surface_to_light); + let offset_position = + -surface_to_light + + ((*light).shadow_depth_bias * normalize(surface_to_light)) + + (surface_normal.xyz * (*light).shadow_normal_bias) * distance_to_light; + + let light_inv_rot = orthonormalize(fwd); + + // because the matrix is a pure rotation matrix, the inverse is just the transpose, and to calculate + // the product of the transpose with a vector we can just post-multiply instead of pre-multiplying. + // this allows us to keep the matrix construction code identical between CPU and GPU. + let projected_position = offset_position * light_inv_rot; + + // divide xy by perspective matrix "f" and by -projected.z (projected.z is -projection matrix's w) + // to get ndc coordinates + let f_div_minus_z = 1.0 / ((*light).spot_light_tan_angle * -projected_position.z); + let shadow_xy_ndc = projected_position.xy * f_div_minus_z; + // convert to uv coordinates + let shadow_uv = shadow_xy_ndc * vec2(0.5, -0.5) + vec2(0.5, 0.5); + + let depth = near_z / -projected_position.z; + + // If soft shadows are enabled, use the PCSS path. + let array_index = i32(light_id) + view_bindings::lights.spot_light_shadowmap_offset; + if ((*light).soft_shadow_size > 0.0) { + return sample_shadow_map_pcss( + shadow_uv, depth, array_index, SPOT_SHADOW_TEXEL_SIZE, (*light).soft_shadow_size); + } + + return sample_shadow_map(shadow_uv, depth, array_index, SPOT_SHADOW_TEXEL_SIZE); +} + +fn get_cascade_index(light_id: u32, view_z: f32) -> u32 { + let light = &view_bindings::lights.directional_lights[light_id]; + + for (var i: u32 = 0u; i < (*light).num_cascades; i = i + 1u) { + if (-view_z < (*light).cascades[i].far_bound) { + return i; + } + } + return (*light).num_cascades; +} + +// Converts from world space to the uv position in the light's shadow map. +// +// The depth is stored in the return value's z coordinate. If the return value's +// w coordinate is 0.0, then we landed outside the shadow map entirely. +fn world_to_directional_light_local( + light_id: u32, + cascade_index: u32, + offset_position: vec4 +) -> vec4 { + let light = &view_bindings::lights.directional_lights[light_id]; + let cascade = &(*light).cascades[cascade_index]; + + let offset_position_clip = (*cascade).clip_from_world * offset_position; + if (offset_position_clip.w <= 0.0) { + return vec4(0.0); + } + let offset_position_ndc = offset_position_clip.xyz / offset_position_clip.w; + // No shadow outside the orthographic projection volume + if (any(offset_position_ndc.xy < vec2(-1.0)) || offset_position_ndc.z < 0.0 + || any(offset_position_ndc > vec3(1.0))) { + return vec4(0.0); + } + + // compute texture coordinates for shadow lookup, compensating for the Y-flip difference + // between the NDC and texture coordinates + let flip_correction = vec2(0.5, -0.5); + let light_local = offset_position_ndc.xy * flip_correction + vec2(0.5, 0.5); + + let depth = offset_position_ndc.z; + + return vec4(light_local, depth, 1.0); +} + +fn sample_directional_cascade( + light_id: u32, + cascade_index: u32, + frag_position: vec4, + surface_normal: vec3, +) -> f32 { + let light = &view_bindings::lights.directional_lights[light_id]; + let cascade = &(*light).cascades[cascade_index]; + + // The normal bias is scaled to the texel size. + let normal_offset = (*light).shadow_normal_bias * (*cascade).texel_size * surface_normal.xyz; + let depth_offset = (*light).shadow_depth_bias * (*light).direction_to_light.xyz; + let offset_position = vec4(frag_position.xyz + normal_offset + depth_offset, frag_position.w); + + let light_local = world_to_directional_light_local(light_id, cascade_index, offset_position); + if (light_local.w == 0.0) { + return 1.0; + } + + let array_index = i32((*light).depth_texture_base_index + cascade_index); + let texel_size = (*cascade).texel_size; + + // If soft shadows are enabled, use the PCSS path. + if ((*light).soft_shadow_size > 0.0) { + return sample_shadow_map_pcss( + light_local.xy, light_local.z, array_index, texel_size, (*light).soft_shadow_size); + } + + return sample_shadow_map(light_local.xy, light_local.z, array_index, texel_size); +} + +fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3, view_z: f32) -> f32 { + let light = &view_bindings::lights.directional_lights[light_id]; + let cascade_index = get_cascade_index(light_id, view_z); + + if (cascade_index >= (*light).num_cascades) { + return 1.0; + } + + var shadow = sample_directional_cascade(light_id, cascade_index, frag_position, surface_normal); + + // Blend with the next cascade, if there is one. + let next_cascade_index = cascade_index + 1u; + if (next_cascade_index < (*light).num_cascades) { + let this_far_bound = (*light).cascades[cascade_index].far_bound; + let next_near_bound = (1.0 - (*light).cascades_overlap_proportion) * this_far_bound; + if (-view_z >= next_near_bound) { + let next_shadow = sample_directional_cascade(light_id, next_cascade_index, frag_position, surface_normal); + shadow = mix(shadow, next_shadow, (-view_z - next_near_bound) / (this_far_bound - next_near_bound)); + } + } + return shadow; +} + +fn cascade_debug_visualization( + output_color: vec3, + light_id: u32, + view_z: f32, +) -> vec3 { + let overlay_alpha = 0.95; + let cascade_index = get_cascade_index(light_id, view_z); + let cascade_color_hsv = vec3( + f32(cascade_index) / f32(#{MAX_CASCADES_PER_LIGHT}u + 1u) * PI_2, + 1.0, + 0.5 + ); + let cascade_color = hsv_to_rgb(cascade_color_hsv); + return vec3( + (1.0 - overlay_alpha) * output_color.rgb + overlay_alpha * cascade_color + ); +} diff --git a/crates/libmarathon/src/render/pbr/render/skin.rs b/crates/libmarathon/src/render/pbr/render/skin.rs new file mode 100644 index 0000000..9b26f4e --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/skin.rs @@ -0,0 +1,623 @@ +use core::mem::{self, size_of}; +use std::sync::OnceLock; + +use bevy_asset::{prelude::AssetChanged, Assets}; +use bevy_camera::visibility::ViewVisibility; +use bevy_ecs::prelude::*; +use bevy_math::Mat4; +use bevy_mesh::skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}; +use bevy_platform::collections::hash_map::Entry; +use crate::render::render_resource::{Buffer, BufferDescriptor}; +use crate::render::sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet}; +use crate::render::{ + batching::NoAutomaticBatching, + render_resource::BufferUsages, + renderer::{RenderDevice, RenderQueue}, + Extract, +}; +use bevy_transform::prelude::GlobalTransform; +use offset_allocator::{Allocation, Allocator}; +use smallvec::SmallVec; +use tracing::error; + +/// Maximum number of joints supported for skinned meshes. +/// +/// It is used to allocate buffers. +/// The correctness of the value depends on the GPU/platform. +/// The current value is chosen because it is guaranteed to work everywhere. +/// To allow for bigger values, a check must be made for the limits +/// of the GPU at runtime, which would mean not using consts anymore. +pub const MAX_JOINTS: usize = 256; + +/// The total number of joints we support. +/// +/// This is 256 GiB worth of joint matrices, which we will never hit under any +/// reasonable circumstances. +const MAX_TOTAL_JOINTS: u32 = 1024 * 1024 * 1024; + +/// The number of joints that we allocate at a time. +/// +/// Some hardware requires that uniforms be allocated on 256-byte boundaries, so +/// we need to allocate 4 64-byte matrices at a time to satisfy alignment +/// requirements. +const JOINTS_PER_ALLOCATION_UNIT: u32 = (256 / size_of::()) as u32; + +/// The maximum ratio of the number of entities whose transforms changed to the +/// total number of joints before we re-extract all joints. +/// +/// We use this as a heuristic to decide whether it's worth switching over to +/// fine-grained detection to determine which skins need extraction. If the +/// number of changed entities is over this threshold, we skip change detection +/// and simply re-extract the transforms of all joints. +const JOINT_EXTRACTION_THRESHOLD_FACTOR: f64 = 0.25; + +/// The location of the first joint matrix in the skin uniform buffer. +#[derive(Clone, Copy)] +pub struct SkinByteOffset { + /// The byte offset of the first joint matrix. + pub byte_offset: u32, +} + +impl SkinByteOffset { + /// Index to be in address space based on the size of a skin uniform. + const fn from_index(index: usize) -> Self { + SkinByteOffset { + byte_offset: (index * size_of::()) as u32, + } + } + + /// Returns this skin index in elements (not bytes). + /// + /// Each element is a 4x4 matrix. + pub fn index(&self) -> u32 { + self.byte_offset / size_of::() as u32 + } +} + +/// The GPU buffers containing joint matrices for all skinned meshes. +/// +/// This is double-buffered: we store the joint matrices of each mesh for the +/// previous frame in addition to those of each mesh for the current frame. This +/// is for motion vector calculation. Every frame, we swap buffers and overwrite +/// the joint matrix buffer from two frames ago with the data for the current +/// frame. +/// +/// Notes on implementation: see comment on top of the `extract_skins` system. +#[derive(Resource)] +pub struct SkinUniforms { + /// The CPU-side buffer that stores the joint matrices for skinned meshes in + /// the current frame. + pub current_staging_buffer: Vec, + /// The GPU-side buffer that stores the joint matrices for skinned meshes in + /// the current frame. + pub current_buffer: Buffer, + /// The GPU-side buffer that stores the joint matrices for skinned meshes in + /// the previous frame. + pub prev_buffer: Buffer, + /// The offset allocator that manages the placement of the joints within the + /// [`Self::current_buffer`]. + allocator: Allocator, + /// Allocation information that we keep about each skin. + skin_uniform_info: MainEntityHashMap, + /// Maps each joint entity to the skins it's associated with. + /// + /// We use this in conjunction with change detection to only update the + /// skins that need updating each frame. + /// + /// Note that conceptually this is a hash map of sets, but we use a + /// [`SmallVec`] to avoid allocations for the vast majority of the cases in + /// which each bone belongs to exactly one skin. + joint_to_skins: MainEntityHashMap>, + /// The total number of joints in the scene. + /// + /// We use this as part of our heuristic to decide whether to use + /// fine-grained change detection. + total_joints: usize, +} + +impl FromWorld for SkinUniforms { + fn from_world(world: &mut World) -> Self { + let device = world.resource::(); + let buffer_usages = (if skins_use_uniform_buffers(device) { + BufferUsages::UNIFORM + } else { + BufferUsages::STORAGE + }) | BufferUsages::COPY_DST; + + // Create the current and previous buffer with the minimum sizes. + // + // These will be swapped every frame. + let current_buffer = device.create_buffer(&BufferDescriptor { + label: Some("skin uniform buffer"), + size: MAX_JOINTS as u64 * size_of::() as u64, + usage: buffer_usages, + mapped_at_creation: false, + }); + let prev_buffer = device.create_buffer(&BufferDescriptor { + label: Some("skin uniform buffer"), + size: MAX_JOINTS as u64 * size_of::() as u64, + usage: buffer_usages, + mapped_at_creation: false, + }); + + Self { + current_staging_buffer: vec![], + current_buffer, + prev_buffer, + allocator: Allocator::new(MAX_TOTAL_JOINTS), + skin_uniform_info: MainEntityHashMap::default(), + joint_to_skins: MainEntityHashMap::default(), + total_joints: 0, + } + } +} + +impl SkinUniforms { + /// Returns the current offset in joints of the skin in the buffer. + pub fn skin_index(&self, skin: MainEntity) -> Option { + self.skin_uniform_info + .get(&skin) + .map(SkinUniformInfo::offset) + } + + /// Returns the current offset in bytes of the skin in the buffer. + pub fn skin_byte_offset(&self, skin: MainEntity) -> Option { + self.skin_uniform_info.get(&skin).map(|skin_uniform_info| { + SkinByteOffset::from_index(skin_uniform_info.offset() as usize) + }) + } + + /// Returns an iterator over all skins in the scene. + pub fn all_skins(&self) -> impl Iterator { + self.skin_uniform_info.keys() + } +} + +/// Allocation information about each skin. +struct SkinUniformInfo { + /// The allocation of the joints within the [`SkinUniforms::current_buffer`]. + allocation: Allocation, + /// The entities that comprise the joints. + joints: Vec, +} + +impl SkinUniformInfo { + /// The offset in joints within the [`SkinUniforms::current_staging_buffer`]. + fn offset(&self) -> u32 { + self.allocation.offset * JOINTS_PER_ALLOCATION_UNIT + } +} + +/// Returns true if skinning must use uniforms (and dynamic offsets) because +/// storage buffers aren't supported on the current platform. +pub fn skins_use_uniform_buffers(render_device: &RenderDevice) -> bool { + static SKINS_USE_UNIFORM_BUFFERS: OnceLock = OnceLock::new(); + *SKINS_USE_UNIFORM_BUFFERS + .get_or_init(|| render_device.limits().max_storage_buffers_per_shader_stage == 0) +} + +/// Uploads the buffers containing the joints to the GPU. +pub fn prepare_skins( + render_device: Res, + render_queue: Res, + uniform: ResMut, +) { + let uniform = uniform.into_inner(); + + if uniform.current_staging_buffer.is_empty() { + return; + } + + // Swap current and previous buffers. + mem::swap(&mut uniform.current_buffer, &mut uniform.prev_buffer); + + // Resize the buffers if necessary. Include extra space equal to `MAX_JOINTS` + // because we need to be able to bind a full uniform buffer's worth of data + // if skins use uniform buffers on this platform. + let needed_size = (uniform.current_staging_buffer.len() as u64 + MAX_JOINTS as u64) + * size_of::() as u64; + if uniform.current_buffer.size() < needed_size { + let mut new_size = uniform.current_buffer.size(); + while new_size < needed_size { + // 1.5× growth factor. + new_size = (new_size + new_size / 2).next_multiple_of(4); + } + + // Create the new buffers. + let buffer_usages = if skins_use_uniform_buffers(&render_device) { + BufferUsages::UNIFORM + } else { + BufferUsages::STORAGE + } | BufferUsages::COPY_DST; + uniform.current_buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("skin uniform buffer"), + usage: buffer_usages, + size: new_size, + mapped_at_creation: false, + }); + uniform.prev_buffer = render_device.create_buffer(&BufferDescriptor { + label: Some("skin uniform buffer"), + usage: buffer_usages, + size: new_size, + mapped_at_creation: false, + }); + + // We've created a new `prev_buffer` but we don't have the previous joint + // data needed to fill it out correctly. Use the current joint data + // instead. + // + // TODO: This is a bug - will cause motion blur to ignore joint movement + // for one frame. + render_queue.write_buffer( + &uniform.prev_buffer, + 0, + bytemuck::must_cast_slice(&uniform.current_staging_buffer[..]), + ); + } + + // Write the data from `uniform.current_staging_buffer` into + // `uniform.current_buffer`. + render_queue.write_buffer( + &uniform.current_buffer, + 0, + bytemuck::must_cast_slice(&uniform.current_staging_buffer[..]), + ); + + // We don't need to write `uniform.prev_buffer` because we already wrote it + // last frame, and the data should still be on the GPU. +} + +// Notes on implementation: +// We define the uniform binding as an array, N> in the shader, +// where N is the maximum number of Mat4s we can fit in the uniform binding, +// which may be as little as 16kB or 64kB. But, we may not need all N. +// We may only need, for example, 10. +// +// If we used uniform buffers ‘normally’ then we would have to write a full +// binding of data for each dynamic offset binding, which is wasteful, makes +// the buffer much larger than it needs to be, and uses more memory bandwidth +// to transfer the data, which then costs frame time So @superdump came up +// with this design: just bind data at the specified offset and interpret +// the data at that offset as an array regardless of what is there. +// +// So instead of writing N Mat4s when you only need 10, you write 10, and +// then pad up to the next dynamic offset alignment. Then write the next. +// And for the last dynamic offset binding, make sure there is a full binding +// of data after it so that the buffer is of size +// `last dynamic offset` + `array>`. +// +// Then when binding the first dynamic offset, the first 10 entries in the array +// are what you expect, but if you read the 11th you’re reading ‘invalid’ data +// which could be padding or could be from the next binding. +// +// In this way, we can pack ‘variable sized arrays’ into uniform buffer bindings +// which normally only support fixed size arrays. You just have to make sure +// in the shader that you only read the values that are valid for that binding. +pub fn extract_skins( + skin_uniforms: ResMut, + skinned_meshes: Extract>, + changed_skinned_meshes: Extract< + Query< + (Entity, &ViewVisibility, &SkinnedMesh), + Or<( + Changed, + Changed, + AssetChanged, + )>, + >, + >, + skinned_mesh_inverse_bindposes: Extract>>, + changed_transforms: Extract>>, + joints: Extract>, + mut removed_skinned_meshes_query: Extract>, +) { + let skin_uniforms = skin_uniforms.into_inner(); + + // Find skins that have become visible or invisible on this frame. Allocate, + // reallocate, or free space for them as necessary. + add_or_delete_skins( + skin_uniforms, + &changed_skinned_meshes, + &skinned_mesh_inverse_bindposes, + &joints, + ); + + // Extract the transforms for all joints from the scene, and write them into + // the staging buffer at the appropriate spot. + extract_joints( + skin_uniforms, + &skinned_meshes, + &changed_skinned_meshes, + &skinned_mesh_inverse_bindposes, + &changed_transforms, + &joints, + ); + + // Delete skins that became invisible. + for skinned_mesh_entity in removed_skinned_meshes_query.read() { + // Only remove a skin if we didn't pick it up in `add_or_delete_skins`. + // It's possible that a necessary component was removed and re-added in + // the same frame. + if !changed_skinned_meshes.contains(skinned_mesh_entity) { + remove_skin(skin_uniforms, skinned_mesh_entity.into()); + } + } +} + +/// Searches for all skins that have become visible or invisible this frame and +/// allocations for them as necessary. +fn add_or_delete_skins( + skin_uniforms: &mut SkinUniforms, + changed_skinned_meshes: &Query< + (Entity, &ViewVisibility, &SkinnedMesh), + Or<( + Changed, + Changed, + AssetChanged, + )>, + >, + skinned_mesh_inverse_bindposes: &Assets, + joints: &Query<&GlobalTransform>, +) { + // Find every skinned mesh that changed one of (1) visibility; (2) joint + // entities (part of `SkinnedMesh`); (3) the associated + // `SkinnedMeshInverseBindposes` asset. + for (skinned_mesh_entity, skinned_mesh_view_visibility, skinned_mesh) in changed_skinned_meshes + { + // Remove the skin if it existed last frame. + let skinned_mesh_entity = MainEntity::from(skinned_mesh_entity); + remove_skin(skin_uniforms, skinned_mesh_entity); + + // If the skin is invisible, we're done. + if !(*skinned_mesh_view_visibility).get() { + continue; + } + + // Initialize the skin. + add_skin( + skinned_mesh_entity, + skinned_mesh, + skin_uniforms, + skinned_mesh_inverse_bindposes, + joints, + ); + } +} + +/// Extracts the global transforms of all joints and updates the staging buffer +/// as necessary. +fn extract_joints( + skin_uniforms: &mut SkinUniforms, + skinned_meshes: &Query<(Entity, &SkinnedMesh)>, + changed_skinned_meshes: &Query< + (Entity, &ViewVisibility, &SkinnedMesh), + Or<( + Changed, + Changed, + AssetChanged, + )>, + >, + skinned_mesh_inverse_bindposes: &Assets, + changed_transforms: &Query<(Entity, &GlobalTransform), Changed>, + joints: &Query<&GlobalTransform>, +) { + // If the number of entities that changed transforms exceeds a certain + // fraction (currently 25%) of the total joints in the scene, then skip + // fine-grained change detection. + // + // Note that this is a crude heuristic, for performance reasons. It doesn't + // consider the ratio of modified *joints* to total joints, only the ratio + // of modified *entities* to total joints. Thus in the worst case we might + // end up re-extracting all skins even though none of the joints changed. + // But making the heuristic finer-grained would make it slower to evaluate, + // and we don't want to lose performance. + let threshold = + (skin_uniforms.total_joints as f64 * JOINT_EXTRACTION_THRESHOLD_FACTOR).floor() as usize; + + if changed_transforms.iter().nth(threshold).is_some() { + // Go ahead and re-extract all skins in the scene. + for (skin_entity, skin) in skinned_meshes { + extract_joints_for_skin( + skin_entity.into(), + skin, + skin_uniforms, + changed_skinned_meshes, + skinned_mesh_inverse_bindposes, + joints, + ); + } + return; + } + + // Use fine-grained change detection to figure out only the skins that need + // to have their joints re-extracted. + let dirty_skins: MainEntityHashSet = changed_transforms + .iter() + .flat_map(|(joint, _)| skin_uniforms.joint_to_skins.get(&MainEntity::from(joint))) + .flat_map(|skin_joint_mappings| skin_joint_mappings.iter()) + .copied() + .collect(); + + // Re-extract the joints for only those skins. + for skin_entity in dirty_skins { + let Ok((_, skin)) = skinned_meshes.get(*skin_entity) else { + continue; + }; + extract_joints_for_skin( + skin_entity, + skin, + skin_uniforms, + changed_skinned_meshes, + skinned_mesh_inverse_bindposes, + joints, + ); + } +} + +/// Extracts all joints for a single skin and writes their transforms into the +/// CPU staging buffer. +fn extract_joints_for_skin( + skin_entity: MainEntity, + skin: &SkinnedMesh, + skin_uniforms: &mut SkinUniforms, + changed_skinned_meshes: &Query< + (Entity, &ViewVisibility, &SkinnedMesh), + Or<( + Changed, + Changed, + AssetChanged, + )>, + >, + skinned_mesh_inverse_bindposes: &Assets, + joints: &Query<&GlobalTransform>, +) { + // If we initialized the skin this frame, we already populated all + // the joints, so there's no need to populate them again. + if changed_skinned_meshes.contains(*skin_entity) { + return; + } + + // Fetch information about the skin. + let Some(skin_uniform_info) = skin_uniforms.skin_uniform_info.get(&skin_entity) else { + return; + }; + let Some(skinned_mesh_inverse_bindposes) = + skinned_mesh_inverse_bindposes.get(&skin.inverse_bindposes) + else { + return; + }; + + // Calculate and write in the new joint matrices. + for (joint_index, (&joint, skinned_mesh_inverse_bindpose)) in skin + .joints + .iter() + .zip(skinned_mesh_inverse_bindposes.iter()) + .enumerate() + { + let Ok(joint_transform) = joints.get(joint) else { + continue; + }; + + let joint_matrix = joint_transform.affine() * *skinned_mesh_inverse_bindpose; + skin_uniforms.current_staging_buffer[skin_uniform_info.offset() as usize + joint_index] = + joint_matrix; + } +} + +/// Allocates space for a new skin in the buffers, and populates its joints. +fn add_skin( + skinned_mesh_entity: MainEntity, + skinned_mesh: &SkinnedMesh, + skin_uniforms: &mut SkinUniforms, + skinned_mesh_inverse_bindposes: &Assets, + joints: &Query<&GlobalTransform>, +) { + // Allocate space for the joints. + let Some(allocation) = skin_uniforms.allocator.allocate( + skinned_mesh + .joints + .len() + .div_ceil(JOINTS_PER_ALLOCATION_UNIT as usize) as u32, + ) else { + error!( + "Out of space for skin: {:?}. Tried to allocate space for {:?} joints.", + skinned_mesh_entity, + skinned_mesh.joints.len() + ); + return; + }; + + // Store that allocation. + let skin_uniform_info = SkinUniformInfo { + allocation, + joints: skinned_mesh + .joints + .iter() + .map(|entity| MainEntity::from(*entity)) + .collect(), + }; + + let skinned_mesh_inverse_bindposes = + skinned_mesh_inverse_bindposes.get(&skinned_mesh.inverse_bindposes); + + for (joint_index, &joint) in skinned_mesh.joints.iter().enumerate() { + // Calculate the initial joint matrix. + let skinned_mesh_inverse_bindpose = + skinned_mesh_inverse_bindposes.and_then(|skinned_mesh_inverse_bindposes| { + skinned_mesh_inverse_bindposes.get(joint_index) + }); + let joint_matrix = match (skinned_mesh_inverse_bindpose, joints.get(joint)) { + (Some(skinned_mesh_inverse_bindpose), Ok(transform)) => { + transform.affine() * *skinned_mesh_inverse_bindpose + } + _ => Mat4::IDENTITY, + }; + + // Write in the new joint matrix, growing the staging buffer if + // necessary. + let buffer_index = skin_uniform_info.offset() as usize + joint_index; + if skin_uniforms.current_staging_buffer.len() < buffer_index + 1 { + skin_uniforms + .current_staging_buffer + .resize(buffer_index + 1, Mat4::IDENTITY); + } + skin_uniforms.current_staging_buffer[buffer_index] = joint_matrix; + + // Record the inverse mapping from the joint back to the skin. We use + // this in order to perform fine-grained joint extraction. + skin_uniforms + .joint_to_skins + .entry(MainEntity::from(joint)) + .or_default() + .push(skinned_mesh_entity); + } + + // Record the number of joints. + skin_uniforms.total_joints += skinned_mesh.joints.len(); + + skin_uniforms + .skin_uniform_info + .insert(skinned_mesh_entity, skin_uniform_info); +} + +/// Deallocates a skin and removes it from the [`SkinUniforms`]. +fn remove_skin(skin_uniforms: &mut SkinUniforms, skinned_mesh_entity: MainEntity) { + let Some(old_skin_uniform_info) = skin_uniforms.skin_uniform_info.remove(&skinned_mesh_entity) + else { + return; + }; + + // Free the allocation. + skin_uniforms + .allocator + .free(old_skin_uniform_info.allocation); + + // Remove the inverse mapping from each joint back to the skin. + for &joint in &old_skin_uniform_info.joints { + if let Entry::Occupied(mut entry) = skin_uniforms.joint_to_skins.entry(joint) { + entry.get_mut().retain(|skin| *skin != skinned_mesh_entity); + if entry.get_mut().is_empty() { + entry.remove(); + } + } + } + + // Update the total number of joints. + skin_uniforms.total_joints -= old_skin_uniform_info.joints.len(); +} + +// NOTE: The skinned joints uniform buffer has to be bound at a dynamic offset per +// entity and so cannot currently be batched on WebGL 2. +pub fn no_automatic_skin_batching( + mut commands: Commands, + query: Query, Without)>, + render_device: Res, +) { + if !skins_use_uniform_buffers(&render_device) { + return; + } + + for entity in &query { + commands.entity(entity).try_insert(NoAutomaticBatching); + } +} diff --git a/crates/libmarathon/src/render/pbr/render/skinning.wgsl b/crates/libmarathon/src/render/pbr/render/skinning.wgsl new file mode 100644 index 0000000..6c4da07 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/skinning.wgsl @@ -0,0 +1,95 @@ +#define_import_path bevy_pbr::skinning + +#import bevy_pbr::mesh_types::SkinnedMesh +#import bevy_pbr::mesh_bindings::mesh + +#ifdef SKINNED + +#ifdef SKINS_USE_UNIFORM_BUFFERS +@group(2) @binding(1) var joint_matrices: SkinnedMesh; +#else // SKINS_USE_UNIFORM_BUFFERS +@group(2) @binding(1) var joint_matrices: array>; +#endif // SKINS_USE_UNIFORM_BUFFERS + +// An array of matrices specifying the joint positions from the previous frame. +// +// This is used for motion vector computation. +// +// If this is the first frame, or we're otherwise prevented from using data from +// the previous frame, this is simply the same as `joint_matrices` above. +#ifdef SKINS_USE_UNIFORM_BUFFERS +@group(2) @binding(6) var prev_joint_matrices: SkinnedMesh; +#else // SKINS_USE_UNIFORM_BUFFERS +@group(2) @binding(6) var prev_joint_matrices: array>; +#endif // SKINS_USE_UNIFORM_BUFFERS + +fn skin_model( + indexes: vec4, + weights: vec4, + instance_index: u32, +) -> mat4x4 { +#ifdef SKINS_USE_UNIFORM_BUFFERS + return weights.x * joint_matrices.data[indexes.x] + + weights.y * joint_matrices.data[indexes.y] + + weights.z * joint_matrices.data[indexes.z] + + weights.w * joint_matrices.data[indexes.w]; +#else // SKINS_USE_UNIFORM_BUFFERS + var skin_index = mesh[instance_index].current_skin_index; + return weights.x * joint_matrices[skin_index + indexes.x] + + weights.y * joint_matrices[skin_index + indexes.y] + + weights.z * joint_matrices[skin_index + indexes.z] + + weights.w * joint_matrices[skin_index + indexes.w]; +#endif // SKINS_USE_UNIFORM_BUFFERS +} + +// Returns the skinned position of a vertex with the given weights from the +// previous frame. +// +// This is used for motion vector computation. +fn skin_prev_model( + indexes: vec4, + weights: vec4, + instance_index: u32, +) -> mat4x4 { +#ifdef SKINS_USE_UNIFORM_BUFFERS + return weights.x * prev_joint_matrices.data[indexes.x] + + weights.y * prev_joint_matrices.data[indexes.y] + + weights.z * prev_joint_matrices.data[indexes.z] + + weights.w * prev_joint_matrices.data[indexes.w]; +#else // SKINS_USE_UNIFORM_BUFFERS + let skin_index = mesh[instance_index].current_skin_index; + return weights.x * prev_joint_matrices[skin_index + indexes.x] + + weights.y * prev_joint_matrices[skin_index + indexes.y] + + weights.z * prev_joint_matrices[skin_index + indexes.z] + + weights.w * prev_joint_matrices[skin_index + indexes.w]; +#endif // SKINS_USE_UNIFORM_BUFFERS +} + +fn inverse_transpose_3x3m(in: mat3x3) -> mat3x3 { + let x = cross(in[1], in[2]); + let y = cross(in[2], in[0]); + let z = cross(in[0], in[1]); + let det = dot(in[2], z); + return mat3x3( + x / det, + y / det, + z / det + ); +} + +fn skin_normals( + world_from_local: mat4x4, + normal: vec3, +) -> vec3 { + return normalize( + inverse_transpose_3x3m( + mat3x3( + world_from_local[0].xyz, + world_from_local[1].xyz, + world_from_local[2].xyz + ) + ) * normal + ); +} + +#endif diff --git a/crates/libmarathon/src/render/pbr/render/utils.wgsl b/crates/libmarathon/src/render/pbr/render/utils.wgsl new file mode 100644 index 0000000..8e91aeb --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/utils.wgsl @@ -0,0 +1,205 @@ +#define_import_path bevy_pbr::utils + +#import bevy_pbr::rgb9e5 +#import bevy_render::maths::{PI, PI_2, orthonormalize} + +// Generates a random u32 in range [0, u32::MAX]. +// +// `state` is a mutable reference to a u32 used as the seed. +// +// Values are generated via "white noise", with no correlation between values. +// In shaders, you often want spatial and/or temporal correlation. Use a different RNG method for these use cases. +// +// https://www.pcg-random.org +// https://www.reedbeta.com/blog/hash-functions-for-gpu-rendering +fn rand_u(state: ptr) -> u32 { + *state = *state * 747796405u + 2891336453u; + let word = ((*state >> ((*state >> 28u) + 4u)) ^ *state) * 277803737u; + return (word >> 22u) ^ word; +} + +// Generates a random f32 in range [0, 1.0]. +fn rand_f(state: ptr) -> f32 { + *state = *state * 747796405u + 2891336453u; + let word = ((*state >> ((*state >> 28u) + 4u)) ^ *state) * 277803737u; + return f32((word >> 22u) ^ word) * bitcast(0x2f800004u); +} + +// Generates a random vec2 where each value is in range [0, 1.0]. +fn rand_vec2f(state: ptr) -> vec2 { + return vec2(rand_f(state), rand_f(state)); +} + +// Generates a random u32 in range [0, n). +fn rand_range_u(n: u32, state: ptr) -> u32 { + return rand_u(state) % n; +} + +// returns the (0-1, 0-1) position within the given viewport for the current buffer coords . +// buffer coords can be obtained from `@builtin(position).xy`. +// the view uniform struct contains the current camera viewport in `view.viewport`. +// topleft = 0,0 +fn coords_to_viewport_uv(position: vec2, viewport: vec4) -> vec2 { + return (position - viewport.xy) / viewport.zw; +} + +// https://jcgt.org/published/0003/02/01/paper.pdf + +// For encoding normals or unit direction vectors as octahedral coordinates. +fn octahedral_encode(v: vec3) -> vec2 { + var n = v / (abs(v.x) + abs(v.y) + abs(v.z)); + let octahedral_wrap = (1.0 - abs(n.yx)) * select(vec2(-1.0), vec2(1.0), n.xy > vec2f(0.0)); + let n_xy = select(octahedral_wrap, n.xy, n.z >= 0.0); + return n_xy * 0.5 + 0.5; +} + +// For decoding normals or unit direction vectors from octahedral coordinates. +fn octahedral_decode(v: vec2) -> vec3 { + let f = v * 2.0 - 1.0; + return octahedral_decode_signed(f); +} + +// Like octahedral_decode, but for input in [-1, 1] instead of [0, 1]. +fn octahedral_decode_signed(v: vec2) -> vec3 { + var n = vec3(v.xy, 1.0 - abs(v.x) - abs(v.y)); + let t = saturate(-n.z); + let w = select(vec2(t), vec2(-t), n.xy >= vec2(0.0)); + n = vec3(n.xy + w, n.z); + return normalize(n); +} + +// https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence +fn interleaved_gradient_noise(pixel_coordinates: vec2, frame: u32) -> f32 { + let xy = pixel_coordinates + 5.588238 * f32(frame % 64u); + return fract(52.9829189 * fract(0.06711056 * xy.x + 0.00583715 * xy.y)); +} + +// Hammersley sequence for quasi-random points +fn hammersley_2d(i: u32, n: u32) -> vec2f { + let inv_n = 1.0 / f32(n); + let vdc = f32(reverseBits(i)) * 2.3283064365386963e-10; // 1/2^32 + return vec2f(f32(i) * inv_n, vdc); +} + +// https://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare (slides 120-135) +// TODO: Use an array here instead of a bunch of constants, once arrays work properly under DX12. +// NOTE: The names have a final underscore to avoid the following error: +// `Composable module identifiers must not require substitution according to naga writeback rules` +const SPIRAL_OFFSET_0_ = vec2(-0.7071, 0.7071); +const SPIRAL_OFFSET_1_ = vec2(-0.0000, -0.8750); +const SPIRAL_OFFSET_2_ = vec2( 0.5303, 0.5303); +const SPIRAL_OFFSET_3_ = vec2(-0.6250, -0.0000); +const SPIRAL_OFFSET_4_ = vec2( 0.3536, -0.3536); +const SPIRAL_OFFSET_5_ = vec2(-0.0000, 0.3750); +const SPIRAL_OFFSET_6_ = vec2(-0.1768, -0.1768); +const SPIRAL_OFFSET_7_ = vec2( 0.1250, 0.0000); + +// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec28%3A303 +fn sample_cosine_hemisphere(normal: vec3, rng: ptr) -> vec3 { + let cos_theta = 1.0 - 2.0 * rand_f(rng); + let phi = PI_2 * rand_f(rng); + let sin_theta = sqrt(max(1.0 - cos_theta * cos_theta, 0.0)); + let x = normal.x + sin_theta * cos(phi); + let y = normal.y + sin_theta * sin(phi); + let z = normal.z + cos_theta; + return vec3(x, y, z); +} +// https://www.pbr-book.org/3ed-2018/Monte_Carlo_Integration/2D_Sampling_with_Multidimensional_Transformations#UniformlySamplingaHemisphere +fn sample_uniform_hemisphere(normal: vec3, rng: ptr) -> vec3 { + let cos_theta = rand_f(rng); + let phi = PI_2 * rand_f(rng); + let sin_theta = sqrt(max(1.0 - cos_theta * cos_theta, 0.0)); + let x = sin_theta * cos(phi); + let y = sin_theta * sin(phi); + let z = cos_theta; + return orthonormalize(normal) * vec3(x, y, z); +} + +fn uniform_hemisphere_inverse_pdf() -> f32 { + return PI_2; +} + +// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec19%3A294 +fn sample_disk(disk_radius: f32, rng: ptr) -> vec2 { + let ab = 2.0 * rand_vec2f(rng) - 1.0; + let a = ab.x; + var b = ab.y; + if (b == 0.0) { b = 1.0; } + + var phi: f32; + var r: f32; + if (a * a > b * b) { + r = disk_radius * a; + phi = (PI / 4.0) * (b / a); + } else { + r = disk_radius * b; + phi = (PI / 2.0) - (PI / 4.0) * (a / b); + } + + let x = r * cos(phi); + let y = r * sin(phi); + return vec2(x, y); +} + +// Convert UV and face index to direction vector +fn sample_cube_dir(uv: vec2f, face: u32) -> vec3f { + // Convert from [0,1] to [-1,1] + let uvc = 2.0 * uv - 1.0; + + // Generate direction based on the cube face + var dir: vec3f; + switch(face) { + case 0u: { dir = vec3f( 1.0, -uvc.y, -uvc.x); } // +X + case 1u: { dir = vec3f(-1.0, -uvc.y, uvc.x); } // -X + case 2u: { dir = vec3f( uvc.x, 1.0, uvc.y); } // +Y + case 3u: { dir = vec3f( uvc.x, -1.0, -uvc.y); } // -Y + case 4u: { dir = vec3f( uvc.x, -uvc.y, 1.0); } // +Z + case 5u: { dir = vec3f(-uvc.x, -uvc.y, -1.0); } // -Z + default: { dir = vec3f(0.0); } + } + return normalize(dir); +} + +// Convert direction vector to cube face UV +struct CubeUV { + uv: vec2f, + face: u32, +} +fn dir_to_cube_uv(dir: vec3f) -> CubeUV { + let abs_dir = abs(dir); + var face: u32 = 0u; + var uv: vec2f = vec2f(0.0); + + // Find the dominant axis to determine face + if (abs_dir.x >= abs_dir.y && abs_dir.x >= abs_dir.z) { + // X axis is dominant + if (dir.x > 0.0) { + face = 0u; // +X + uv = vec2f(-dir.z, -dir.y) / dir.x; + } else { + face = 1u; // -X + uv = vec2f(dir.z, -dir.y) / abs_dir.x; + } + } else if (abs_dir.y >= abs_dir.x && abs_dir.y >= abs_dir.z) { + // Y axis is dominant + if (dir.y > 0.0) { + face = 2u; // +Y + uv = vec2f(dir.x, dir.z) / dir.y; + } else { + face = 3u; // -Y + uv = vec2f(dir.x, -dir.z) / abs_dir.y; + } + } else { + // Z axis is dominant + if (dir.z > 0.0) { + face = 4u; // +Z + uv = vec2f(dir.x, -dir.y) / dir.z; + } else { + face = 5u; // -Z + uv = vec2f(-dir.x, -dir.y) / abs_dir.z; + } + } + + // Convert from [-1,1] to [0,1] + return CubeUV(uv * 0.5 + 0.5, face); +} diff --git a/crates/libmarathon/src/render/pbr/render/view_transformations.wgsl b/crates/libmarathon/src/render/pbr/render/view_transformations.wgsl new file mode 100644 index 0000000..dfb4d6e --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/view_transformations.wgsl @@ -0,0 +1,238 @@ +#define_import_path bevy_pbr::view_transformations + +#import bevy_pbr::mesh_view_bindings as view_bindings +#import bevy_pbr::prepass_bindings + +/// World space: +/// +y is up + +/// View space: +/// -z is forward, +x is right, +y is up +/// Forward is from the camera position into the scene. +/// (0.0, 0.0, -1.0) is linear distance of 1.0 in front of the camera's view relative to the camera's rotation +/// (0.0, 1.0, 0.0) is linear distance of 1.0 above the camera's view relative to the camera's rotation + +/// NDC (normalized device coordinate): +/// https://www.w3.org/TR/webgpu/#coordinate-systems +/// (-1.0, -1.0) in NDC is located at the bottom-left corner of NDC +/// (1.0, 1.0) in NDC is located at the top-right corner of NDC +/// Z is depth where: +/// 1.0 is near clipping plane +/// Perspective projection: 0.0 is inf far away +/// Orthographic projection: 0.0 is far clipping plane + +/// Clip space: +/// This is NDC before the perspective divide, still in homogenous coordinate space. +/// Dividing a clip space point by its w component yields a point in NDC space. + +/// UV space: +/// 0.0, 0.0 is the top left +/// 1.0, 1.0 is the bottom right + + +// ----------------- +// TO WORLD -------- +// ----------------- + +/// Convert a view space position to world space +fn position_view_to_world(view_pos: vec3) -> vec3 { + let world_pos = view_bindings::view.world_from_view * vec4(view_pos, 1.0); + return world_pos.xyz; +} + +/// Convert a clip space position to world space +fn position_clip_to_world(clip_pos: vec4) -> vec3 { + let world_pos = view_bindings::view.world_from_clip * clip_pos; + return world_pos.xyz; +} + +/// Convert a ndc space position to world space +fn position_ndc_to_world(ndc_pos: vec3) -> vec3 { + let world_pos = view_bindings::view.world_from_clip * vec4(ndc_pos, 1.0); + return world_pos.xyz / world_pos.w; +} + +/// Convert a view space direction to world space +fn direction_view_to_world(view_dir: vec3) -> vec3 { + let world_dir = view_bindings::view.world_from_view * vec4(view_dir, 0.0); + return world_dir.xyz; +} + +/// Convert a clip space direction to world space +fn direction_clip_to_world(clip_dir: vec4) -> vec3 { + let world_dir = view_bindings::view.world_from_clip * clip_dir; + return world_dir.xyz; +} + +// ----------------- +// TO VIEW --------- +// ----------------- + +/// Convert a world space position to view space +fn position_world_to_view(world_pos: vec3) -> vec3 { + let view_pos = view_bindings::view.view_from_world * vec4(world_pos, 1.0); + return view_pos.xyz; +} + +/// Convert a clip space position to view space +fn position_clip_to_view(clip_pos: vec4) -> vec3 { + let view_pos = view_bindings::view.view_from_clip * clip_pos; + return view_pos.xyz; +} + +/// Convert a ndc space position to view space +fn position_ndc_to_view(ndc_pos: vec3) -> vec3 { + let view_pos = view_bindings::view.view_from_clip * vec4(ndc_pos, 1.0); + return view_pos.xyz / view_pos.w; +} + +/// Convert a world space direction to view space +fn direction_world_to_view(world_dir: vec3) -> vec3 { + let view_dir = view_bindings::view.view_from_world * vec4(world_dir, 0.0); + return view_dir.xyz; +} + +/// Convert a clip space direction to view space +fn direction_clip_to_view(clip_dir: vec4) -> vec3 { + let view_dir = view_bindings::view.view_from_clip * clip_dir; + return view_dir.xyz; +} + +// ----------------- +// TO PREV. VIEW --- +// ----------------- + +fn position_world_to_prev_view(world_pos: vec3) -> vec3 { + let view_pos = prepass_bindings::previous_view_uniforms.view_from_world * + vec4(world_pos, 1.0); + return view_pos.xyz; +} + +fn position_world_to_prev_ndc(world_pos: vec3) -> vec3 { + let ndc_pos = prepass_bindings::previous_view_uniforms.clip_from_world * + vec4(world_pos, 1.0); + return ndc_pos.xyz / ndc_pos.w; +} + +// ----------------- +// TO CLIP --------- +// ----------------- + +/// Convert a world space position to clip space +fn position_world_to_clip(world_pos: vec3) -> vec4 { + let clip_pos = view_bindings::view.clip_from_world * vec4(world_pos, 1.0); + return clip_pos; +} + +/// Convert a view space position to clip space +fn position_view_to_clip(view_pos: vec3) -> vec4 { + let clip_pos = view_bindings::view.clip_from_view * vec4(view_pos, 1.0); + return clip_pos; +} + +/// Convert a world space direction to clip space +fn direction_world_to_clip(world_dir: vec3) -> vec4 { + let clip_dir = view_bindings::view.clip_from_world * vec4(world_dir, 0.0); + return clip_dir; +} + +/// Convert a view space direction to clip space +fn direction_view_to_clip(view_dir: vec3) -> vec4 { + let clip_dir = view_bindings::view.clip_from_view * vec4(view_dir, 0.0); + return clip_dir; +} + +// ----------------- +// TO NDC ---------- +// ----------------- + +/// Convert a world space position to ndc space +fn position_world_to_ndc(world_pos: vec3) -> vec3 { + let ndc_pos = view_bindings::view.clip_from_world * vec4(world_pos, 1.0); + return ndc_pos.xyz / ndc_pos.w; +} + +/// Convert a view space position to ndc space +fn position_view_to_ndc(view_pos: vec3) -> vec3 { + let ndc_pos = view_bindings::view.clip_from_view * vec4(view_pos, 1.0); + return ndc_pos.xyz / ndc_pos.w; +} + +// ----------------- +// DEPTH ----------- +// ----------------- + +/// Retrieve the perspective camera near clipping plane +fn perspective_camera_near() -> f32 { + return view_bindings::view.clip_from_view[3][2]; +} + +/// Convert ndc depth to linear view z. +/// Note: Depth values in front of the camera will be negative as -z is forward +fn depth_ndc_to_view_z(ndc_depth: f32) -> f32 { +#ifdef VIEW_PROJECTION_PERSPECTIVE + return -perspective_camera_near() / ndc_depth; +#else ifdef VIEW_PROJECTION_ORTHOGRAPHIC + return -(view_bindings::view.clip_from_view[3][2] - ndc_depth) / view_bindings::view.clip_from_view[2][2]; +#else + let view_pos = view_bindings::view.view_from_clip * vec4(0.0, 0.0, ndc_depth, 1.0); + return view_pos.z / view_pos.w; +#endif +} + +/// Convert linear view z to ndc depth. +/// Note: View z input should be negative for values in front of the camera as -z is forward +fn view_z_to_depth_ndc(view_z: f32) -> f32 { +#ifdef VIEW_PROJECTION_PERSPECTIVE + return -perspective_camera_near() / view_z; +#else ifdef VIEW_PROJECTION_ORTHOGRAPHIC + return view_bindings::view.clip_from_view[3][2] + view_z * view_bindings::view.clip_from_view[2][2]; +#else + let ndc_pos = view_bindings::view.clip_from_view * vec4(0.0, 0.0, view_z, 1.0); + return ndc_pos.z / ndc_pos.w; +#endif +} + +fn prev_view_z_to_depth_ndc(view_z: f32) -> f32 { +#ifdef VIEW_PROJECTION_PERSPECTIVE + return -perspective_camera_near() / view_z; +#else ifdef VIEW_PROJECTION_ORTHOGRAPHIC + return prepass_bindings::previous_view_uniforms.clip_from_view[3][2] + + view_z * prepass_bindings::previous_view_uniforms.clip_from_view[2][2]; +#else + let ndc_pos = prepass_bindings::previous_view_uniforms.clip_from_view * + vec4(0.0, 0.0, view_z, 1.0); + return ndc_pos.z / ndc_pos.w; +#endif +} + +// ----------------- +// UV -------------- +// ----------------- + +/// Convert ndc space xy coordinate [-1.0 .. 1.0] to uv [0.0 .. 1.0] +fn ndc_to_uv(ndc: vec2) -> vec2 { + return ndc * vec2(0.5, -0.5) + vec2(0.5); +} + +/// Convert uv [0.0 .. 1.0] coordinate to ndc space xy [-1.0 .. 1.0] +fn uv_to_ndc(uv: vec2) -> vec2 { + return uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0); +} + +/// returns the (0.0, 0.0) .. (1.0, 1.0) position within the viewport for the current render target +/// [0 .. render target viewport size] eg. [(0.0, 0.0) .. (1280.0, 720.0)] to [(0.0, 0.0) .. (1.0, 1.0)] +fn frag_coord_to_uv(frag_coord: vec2) -> vec2 { + return (frag_coord - view_bindings::view.viewport.xy) / view_bindings::view.viewport.zw; +} + +/// Convert frag coord to ndc +fn frag_coord_to_ndc(frag_coord: vec4) -> vec3 { + return vec3(uv_to_ndc(frag_coord_to_uv(frag_coord.xy)), frag_coord.z); +} + +/// Convert ndc space xy coordinate [-1.0 .. 1.0] to [0 .. render target +/// viewport size] +fn ndc_to_frag_coord(ndc: vec2) -> vec2 { + return ndc_to_uv(ndc) * view_bindings::view.viewport.zw; +} diff --git a/crates/libmarathon/src/render/pbr/render/wireframe.wgsl b/crates/libmarathon/src/render/pbr/render/wireframe.wgsl new file mode 100644 index 0000000..3873ffa --- /dev/null +++ b/crates/libmarathon/src/render/pbr/render/wireframe.wgsl @@ -0,0 +1,12 @@ +#import bevy_pbr::forward_io::VertexOutput + +struct PushConstants { + color: vec4 +} + +var push_constants: PushConstants; + +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4 { + return push_constants.color; +} diff --git a/crates/libmarathon/src/render/pbr/ssao/mod.rs b/crates/libmarathon/src/render/pbr/ssao/mod.rs new file mode 100644 index 0000000..e566f93 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/ssao/mod.rs @@ -0,0 +1,757 @@ +use crate::render::pbr::NodePbr; +use bevy_app::{App, Plugin}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; +use bevy_camera::{Camera, Camera3d}; +use crate::render::{ + core_3d::graph::{Core3d, Node3d}, + prepass::{DepthPrepass, NormalPrepass, ViewPrepassTextures}, +}; +use bevy_ecs::{ + prelude::{Component, Entity}, + query::{Has, QueryItem, With}, + reflect::ReflectComponent, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_image::ToExtents; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use crate::render::{ + camera::{ExtractedCamera, TemporalJitter}, + diagnostic::RecordDiagnostics, + extract_component::ExtractComponent, + globals::{GlobalsBuffer, GlobalsUniform}, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, + render_resource::{ + binding_types::{ + sampler, texture_2d, texture_depth_2d, texture_storage_2d, uniform_buffer, + }, + *, + }, + renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue}, + sync_component::SyncComponentPlugin, + sync_world::RenderEntity, + texture::{CachedTexture, TextureCache}, + view::{Msaa, ViewUniform, ViewUniformOffset, ViewUniforms}, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use bevy_shader::{load_shader_library, Shader, ShaderDefVal}; +use bevy_utils::prelude::default; +use core::mem; +use tracing::{error, warn}; + +/// Plugin for screen space ambient occlusion. +pub struct ScreenSpaceAmbientOcclusionPlugin; + +impl Plugin for ScreenSpaceAmbientOcclusionPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "ssao_utils.wgsl"); + + embedded_asset!(app, "preprocess_depth.wgsl"); + embedded_asset!(app, "ssao.wgsl"); + embedded_asset!(app, "spatial_denoise.wgsl"); + + app.add_plugins(SyncComponentPlugin::::default()); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + if !render_app + .world() + .resource::() + .get_texture_format_features(TextureFormat::R16Float) + .allowed_usages + .contains(TextureUsages::STORAGE_BINDING) + { + warn!("ScreenSpaceAmbientOcclusionPlugin not loaded. GPU lacks support: TextureFormat::R16Float does not support TextureUsages::STORAGE_BINDING."); + return; + } + + if render_app + .world() + .resource::() + .limits() + .max_storage_textures_per_shader_stage + < 5 + { + warn!("ScreenSpaceAmbientOcclusionPlugin not loaded. GPU lacks support: Limits::max_storage_textures_per_shader_stage is less than 5."); + return; + } + + render_app + .init_resource::() + .init_resource::>() + .add_systems(ExtractSchedule, extract_ssao_settings) + .add_systems( + Render, + ( + prepare_ssao_pipelines.in_set(RenderSystems::Prepare), + prepare_ssao_textures.in_set(RenderSystems::PrepareResources), + prepare_ssao_bind_groups.in_set(RenderSystems::PrepareBindGroups), + ), + ) + .add_render_graph_node::>( + Core3d, + NodePbr::ScreenSpaceAmbientOcclusion, + ) + .add_render_graph_edges( + Core3d, + ( + // END_PRE_PASSES -> SCREEN_SPACE_AMBIENT_OCCLUSION -> MAIN_PASS + Node3d::EndPrepasses, + NodePbr::ScreenSpaceAmbientOcclusion, + Node3d::StartMainPass, + ), + ); + } +} + +/// Component to apply screen space ambient occlusion to a 3d camera. +/// +/// Screen space ambient occlusion (SSAO) approximates small-scale, +/// local occlusion of _indirect_ diffuse light between objects, based on what's visible on-screen. +/// SSAO does not apply to direct lighting, such as point or directional lights. +/// +/// This darkens creases, e.g. on staircases, and gives nice contact shadows +/// where objects meet, giving entities a more "grounded" feel. +/// +/// # Usage Notes +/// +/// Requires that you add [`ScreenSpaceAmbientOcclusionPlugin`] to your app. +/// +/// It strongly recommended that you use SSAO in conjunction with +/// TAA (`TemporalAntiAliasing`). +/// Doing so greatly reduces SSAO noise. +/// +/// SSAO is not supported on `WebGL2`, and is not currently supported on `WebGPU`. +#[derive(Component, ExtractComponent, Reflect, PartialEq, Clone, Debug)] +#[reflect(Component, Debug, Default, PartialEq, Clone)] +#[require(DepthPrepass, NormalPrepass)] +#[doc(alias = "Ssao")] +pub struct ScreenSpaceAmbientOcclusion { + /// Quality of the SSAO effect. + pub quality_level: ScreenSpaceAmbientOcclusionQualityLevel, + /// A constant estimated thickness of objects. + /// + /// This value is used to decide how far behind an object a ray of light needs to be in order + /// to pass behind it. Any ray closer than that will be occluded. + pub constant_object_thickness: f32, +} + +impl Default for ScreenSpaceAmbientOcclusion { + fn default() -> Self { + Self { + quality_level: ScreenSpaceAmbientOcclusionQualityLevel::default(), + constant_object_thickness: 0.25, + } + } +} + +#[derive(Reflect, PartialEq, Eq, Hash, Clone, Copy, Default, Debug)] +#[reflect(PartialEq, Hash, Clone, Default)] +pub enum ScreenSpaceAmbientOcclusionQualityLevel { + Low, + Medium, + #[default] + High, + Ultra, + Custom { + /// Higher slice count means less noise, but worse performance. + slice_count: u32, + /// Samples per slice side is also tweakable, but recommended to be left at 2 or 3. + samples_per_slice_side: u32, + }, +} + +impl ScreenSpaceAmbientOcclusionQualityLevel { + fn sample_counts(&self) -> (u32, u32) { + match self { + Self::Low => (1, 2), // 4 spp (1 * (2 * 2)), plus optional temporal samples + Self::Medium => (2, 2), // 8 spp (2 * (2 * 2)), plus optional temporal samples + Self::High => (3, 3), // 18 spp (3 * (3 * 2)), plus optional temporal samples + Self::Ultra => (9, 3), // 54 spp (9 * (3 * 2)), plus optional temporal samples + Self::Custom { + slice_count: slices, + samples_per_slice_side, + } => (*slices, *samples_per_slice_side), + } + } +} + +#[derive(Default)] +struct SsaoNode {} + +impl ViewNode for SsaoNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static SsaoPipelineId, + &'static SsaoBindGroups, + &'static ViewUniformOffset, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (camera, pipeline_id, bind_groups, view_uniform_offset): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let pipelines = world.resource::(); + let pipeline_cache = world.resource::(); + let ( + Some(camera_size), + Some(preprocess_depth_pipeline), + Some(spatial_denoise_pipeline), + Some(ssao_pipeline), + ) = ( + camera.physical_viewport_size, + pipeline_cache.get_compute_pipeline(pipelines.preprocess_depth_pipeline), + pipeline_cache.get_compute_pipeline(pipelines.spatial_denoise_pipeline), + pipeline_cache.get_compute_pipeline(pipeline_id.0), + ) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let command_encoder = render_context.command_encoder(); + command_encoder.push_debug_group("ssao"); + let time_span = diagnostics.time_span(command_encoder, "ssao"); + + { + let mut preprocess_depth_pass = + command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("ssao_preprocess_depth"), + timestamp_writes: None, + }); + preprocess_depth_pass.set_pipeline(preprocess_depth_pipeline); + preprocess_depth_pass.set_bind_group(0, &bind_groups.preprocess_depth_bind_group, &[]); + preprocess_depth_pass.set_bind_group( + 1, + &bind_groups.common_bind_group, + &[view_uniform_offset.offset], + ); + preprocess_depth_pass.dispatch_workgroups( + camera_size.x.div_ceil(16), + camera_size.y.div_ceil(16), + 1, + ); + } + + { + let mut ssao_pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("ssao"), + timestamp_writes: None, + }); + ssao_pass.set_pipeline(ssao_pipeline); + ssao_pass.set_bind_group(0, &bind_groups.ssao_bind_group, &[]); + ssao_pass.set_bind_group( + 1, + &bind_groups.common_bind_group, + &[view_uniform_offset.offset], + ); + ssao_pass.dispatch_workgroups(camera_size.x.div_ceil(8), camera_size.y.div_ceil(8), 1); + } + + { + let mut spatial_denoise_pass = + command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("ssao_spatial_denoise"), + timestamp_writes: None, + }); + spatial_denoise_pass.set_pipeline(spatial_denoise_pipeline); + spatial_denoise_pass.set_bind_group(0, &bind_groups.spatial_denoise_bind_group, &[]); + spatial_denoise_pass.set_bind_group( + 1, + &bind_groups.common_bind_group, + &[view_uniform_offset.offset], + ); + spatial_denoise_pass.dispatch_workgroups( + camera_size.x.div_ceil(8), + camera_size.y.div_ceil(8), + 1, + ); + } + + time_span.end(command_encoder); + command_encoder.pop_debug_group(); + Ok(()) + } +} + +#[derive(Resource)] +struct SsaoPipelines { + preprocess_depth_pipeline: CachedComputePipelineId, + spatial_denoise_pipeline: CachedComputePipelineId, + + common_bind_group_layout: BindGroupLayout, + preprocess_depth_bind_group_layout: BindGroupLayout, + ssao_bind_group_layout: BindGroupLayout, + spatial_denoise_bind_group_layout: BindGroupLayout, + + hilbert_index_lut: TextureView, + point_clamp_sampler: Sampler, + linear_clamp_sampler: Sampler, + + shader: Handle, +} + +impl FromWorld for SsaoPipelines { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let render_queue = world.resource::(); + let pipeline_cache = world.resource::(); + + let hilbert_index_lut = render_device + .create_texture_with_data( + render_queue, + &(TextureDescriptor { + label: Some("ssao_hilbert_index_lut"), + size: Extent3d { + width: HILBERT_WIDTH as u32, + height: HILBERT_WIDTH as u32, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R16Uint, + usage: TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }), + TextureDataOrder::default(), + bytemuck::cast_slice(&generate_hilbert_index_lut()), + ) + .create_view(&TextureViewDescriptor::default()); + + let point_clamp_sampler = render_device.create_sampler(&SamplerDescriptor { + min_filter: FilterMode::Nearest, + mag_filter: FilterMode::Nearest, + mipmap_filter: FilterMode::Nearest, + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + ..Default::default() + }); + let linear_clamp_sampler = render_device.create_sampler(&SamplerDescriptor { + min_filter: FilterMode::Linear, + mag_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Nearest, + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + ..Default::default() + }); + + let common_bind_group_layout = render_device.create_bind_group_layout( + "ssao_common_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + sampler(SamplerBindingType::NonFiltering), + sampler(SamplerBindingType::Filtering), + uniform_buffer::(true), + ), + ), + ); + + let preprocess_depth_bind_group_layout = render_device.create_bind_group_layout( + "ssao_preprocess_depth_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_depth_2d(), + texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly), + ), + ), + ); + + let ssao_bind_group_layout = render_device.create_bind_group_layout( + "ssao_ssao_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: true }), + texture_2d(TextureSampleType::Float { filterable: false }), + texture_2d(TextureSampleType::Uint), + texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly), + uniform_buffer::(false), + uniform_buffer::(false), + ), + ), + ); + + let spatial_denoise_bind_group_layout = render_device.create_bind_group_layout( + "ssao_spatial_denoise_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + texture_2d(TextureSampleType::Uint), + texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly), + ), + ), + ); + + let preprocess_depth_pipeline = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("ssao_preprocess_depth_pipeline".into()), + layout: vec![ + preprocess_depth_bind_group_layout.clone(), + common_bind_group_layout.clone(), + ], + shader: load_embedded_asset!(world, "preprocess_depth.wgsl"), + ..default() + }); + + let spatial_denoise_pipeline = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("ssao_spatial_denoise_pipeline".into()), + layout: vec![ + spatial_denoise_bind_group_layout.clone(), + common_bind_group_layout.clone(), + ], + shader: load_embedded_asset!(world, "spatial_denoise.wgsl"), + ..default() + }); + + Self { + preprocess_depth_pipeline, + spatial_denoise_pipeline, + + common_bind_group_layout, + preprocess_depth_bind_group_layout, + ssao_bind_group_layout, + spatial_denoise_bind_group_layout, + + hilbert_index_lut, + point_clamp_sampler, + linear_clamp_sampler, + + shader: load_embedded_asset!(world, "ssao.wgsl"), + } + } +} + +#[derive(PartialEq, Eq, Hash, Clone)] +struct SsaoPipelineKey { + quality_level: ScreenSpaceAmbientOcclusionQualityLevel, + temporal_jitter: bool, +} + +impl SpecializedComputePipeline for SsaoPipelines { + type Key = SsaoPipelineKey; + + fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor { + let (slice_count, samples_per_slice_side) = key.quality_level.sample_counts(); + + let mut shader_defs = vec![ + ShaderDefVal::Int("SLICE_COUNT".to_string(), slice_count as i32), + ShaderDefVal::Int( + "SAMPLES_PER_SLICE_SIDE".to_string(), + samples_per_slice_side as i32, + ), + ]; + + if key.temporal_jitter { + shader_defs.push("TEMPORAL_JITTER".into()); + } + + ComputePipelineDescriptor { + label: Some("ssao_ssao_pipeline".into()), + layout: vec![ + self.ssao_bind_group_layout.clone(), + self.common_bind_group_layout.clone(), + ], + shader: self.shader.clone(), + shader_defs, + ..default() + } + } +} + +fn extract_ssao_settings( + mut commands: Commands, + cameras: Extract< + Query< + (RenderEntity, &Camera, &ScreenSpaceAmbientOcclusion, &Msaa), + (With, With, With), + >, + >, +) { + for (entity, camera, ssao_settings, msaa) in &cameras { + if *msaa != Msaa::Off { + error!( + "SSAO is being used which requires Msaa::Off, but Msaa is currently set to Msaa::{:?}", + *msaa + ); + return; + } + let mut entity_commands = commands + .get_entity(entity) + .expect("SSAO entity wasn't synced."); + if camera.is_active { + entity_commands.insert(ssao_settings.clone()); + } else { + entity_commands.remove::(); + } + } +} + +#[derive(Component)] +pub struct ScreenSpaceAmbientOcclusionResources { + preprocessed_depth_texture: CachedTexture, + ssao_noisy_texture: CachedTexture, // Pre-spatially denoised texture + pub screen_space_ambient_occlusion_texture: CachedTexture, // Spatially denoised texture + depth_differences_texture: CachedTexture, + thickness_buffer: Buffer, +} + +fn prepare_ssao_textures( + mut commands: Commands, + mut texture_cache: ResMut, + render_device: Res, + views: Query<(Entity, &ExtractedCamera, &ScreenSpaceAmbientOcclusion)>, +) { + for (entity, camera, ssao_settings) in &views { + let Some(physical_viewport_size) = camera.physical_viewport_size else { + continue; + }; + let size = physical_viewport_size.to_extents(); + + let preprocessed_depth_texture = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("ssao_preprocessed_depth_texture"), + size, + mip_level_count: 5, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R16Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let ssao_noisy_texture = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("ssao_noisy_texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R16Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let ssao_texture = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("ssao_texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R16Float, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let depth_differences_texture = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("ssao_depth_differences_texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::R32Uint, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + ); + + let thickness_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("thickness_buffer"), + contents: &ssao_settings.constant_object_thickness.to_le_bytes(), + usage: BufferUsages::UNIFORM, + }); + + commands + .entity(entity) + .insert(ScreenSpaceAmbientOcclusionResources { + preprocessed_depth_texture, + ssao_noisy_texture, + screen_space_ambient_occlusion_texture: ssao_texture, + depth_differences_texture, + thickness_buffer, + }); + } +} + +#[derive(Component)] +struct SsaoPipelineId(CachedComputePipelineId); + +fn prepare_ssao_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + pipeline: Res, + views: Query<(Entity, &ScreenSpaceAmbientOcclusion, Has)>, +) { + for (entity, ssao_settings, temporal_jitter) in &views { + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &pipeline, + SsaoPipelineKey { + quality_level: ssao_settings.quality_level, + temporal_jitter, + }, + ); + + commands.entity(entity).insert(SsaoPipelineId(pipeline_id)); + } +} + +#[derive(Component)] +struct SsaoBindGroups { + common_bind_group: BindGroup, + preprocess_depth_bind_group: BindGroup, + ssao_bind_group: BindGroup, + spatial_denoise_bind_group: BindGroup, +} + +fn prepare_ssao_bind_groups( + mut commands: Commands, + render_device: Res, + pipelines: Res, + view_uniforms: Res, + global_uniforms: Res, + views: Query<( + Entity, + &ScreenSpaceAmbientOcclusionResources, + &ViewPrepassTextures, + )>, +) { + let (Some(view_uniforms), Some(globals_uniforms)) = ( + view_uniforms.uniforms.binding(), + global_uniforms.buffer.binding(), + ) else { + return; + }; + + for (entity, ssao_resources, prepass_textures) in &views { + let common_bind_group = render_device.create_bind_group( + "ssao_common_bind_group", + &pipelines.common_bind_group_layout, + &BindGroupEntries::sequential(( + &pipelines.point_clamp_sampler, + &pipelines.linear_clamp_sampler, + view_uniforms.clone(), + )), + ); + + let create_depth_view = |mip_level| { + ssao_resources + .preprocessed_depth_texture + .texture + .create_view(&TextureViewDescriptor { + label: Some("ssao_preprocessed_depth_texture_mip_view"), + base_mip_level: mip_level, + format: Some(TextureFormat::R16Float), + dimension: Some(TextureViewDimension::D2), + mip_level_count: Some(1), + ..default() + }) + }; + + let preprocess_depth_bind_group = render_device.create_bind_group( + "ssao_preprocess_depth_bind_group", + &pipelines.preprocess_depth_bind_group_layout, + &BindGroupEntries::sequential(( + prepass_textures.depth_view().unwrap(), + &create_depth_view(0), + &create_depth_view(1), + &create_depth_view(2), + &create_depth_view(3), + &create_depth_view(4), + )), + ); + + let ssao_bind_group = render_device.create_bind_group( + "ssao_ssao_bind_group", + &pipelines.ssao_bind_group_layout, + &BindGroupEntries::sequential(( + &ssao_resources.preprocessed_depth_texture.default_view, + prepass_textures.normal_view().unwrap(), + &pipelines.hilbert_index_lut, + &ssao_resources.ssao_noisy_texture.default_view, + &ssao_resources.depth_differences_texture.default_view, + globals_uniforms.clone(), + ssao_resources.thickness_buffer.as_entire_binding(), + )), + ); + + let spatial_denoise_bind_group = render_device.create_bind_group( + "ssao_spatial_denoise_bind_group", + &pipelines.spatial_denoise_bind_group_layout, + &BindGroupEntries::sequential(( + &ssao_resources.ssao_noisy_texture.default_view, + &ssao_resources.depth_differences_texture.default_view, + &ssao_resources + .screen_space_ambient_occlusion_texture + .default_view, + )), + ); + + commands.entity(entity).insert(SsaoBindGroups { + common_bind_group, + preprocess_depth_bind_group, + ssao_bind_group, + spatial_denoise_bind_group, + }); + } +} + +fn generate_hilbert_index_lut() -> [[u16; 64]; 64] { + use core::array::from_fn; + from_fn(|x| from_fn(|y| hilbert_index(x as u16, y as u16))) +} + +// https://www.shadertoy.com/view/3tB3z3 +const HILBERT_WIDTH: u16 = 64; +fn hilbert_index(mut x: u16, mut y: u16) -> u16 { + let mut index = 0; + + let mut level: u16 = HILBERT_WIDTH / 2; + while level > 0 { + let region_x = (x & level > 0) as u16; + let region_y = (y & level > 0) as u16; + index += level * level * ((3 * region_x) ^ region_y); + + if region_y == 0 { + if region_x == 1 { + x = HILBERT_WIDTH - 1 - x; + y = HILBERT_WIDTH - 1 - y; + } + + mem::swap(&mut x, &mut y); + } + + level /= 2; + } + + index +} diff --git a/crates/libmarathon/src/render/pbr/ssao/preprocess_depth.wgsl b/crates/libmarathon/src/render/pbr/ssao/preprocess_depth.wgsl new file mode 100644 index 0000000..a386b09 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/ssao/preprocess_depth.wgsl @@ -0,0 +1,102 @@ +// Inputs a depth texture and outputs a MIP-chain of depths. +// +// Because SSAO's performance is bound by texture reads, this increases +// performance over using the full resolution depth for every sample. + +// Reference: https://research.nvidia.com/sites/default/files/pubs/2012-06_Scalable-Ambient-Obscurance/McGuire12SAO.pdf, section 2.2 + +#import bevy_render::view::View + +@group(0) @binding(0) var input_depth: texture_depth_2d; +@group(0) @binding(1) var preprocessed_depth_mip0: texture_storage_2d; +@group(0) @binding(2) var preprocessed_depth_mip1: texture_storage_2d; +@group(0) @binding(3) var preprocessed_depth_mip2: texture_storage_2d; +@group(0) @binding(4) var preprocessed_depth_mip3: texture_storage_2d; +@group(0) @binding(5) var preprocessed_depth_mip4: texture_storage_2d; +@group(1) @binding(0) var point_clamp_sampler: sampler; +@group(1) @binding(1) var linear_clamp_sampler: sampler; +@group(1) @binding(2) var view: View; + + +// Using 4 depths from the previous MIP, compute a weighted average for the depth of the current MIP +fn weighted_average(depth0: f32, depth1: f32, depth2: f32, depth3: f32) -> f32 { + let depth_range_scale_factor = 0.75; + let effect_radius = depth_range_scale_factor * 0.5 * 1.457; + let falloff_range = 0.615 * effect_radius; + let falloff_from = effect_radius * (1.0 - 0.615); + let falloff_mul = -1.0 / falloff_range; + let falloff_add = falloff_from / falloff_range + 1.0; + + let min_depth = min(min(depth0, depth1), min(depth2, depth3)); + let weight0 = saturate((depth0 - min_depth) * falloff_mul + falloff_add); + let weight1 = saturate((depth1 - min_depth) * falloff_mul + falloff_add); + let weight2 = saturate((depth2 - min_depth) * falloff_mul + falloff_add); + let weight3 = saturate((depth3 - min_depth) * falloff_mul + falloff_add); + let weight_total = weight0 + weight1 + weight2 + weight3; + + return ((weight0 * depth0) + (weight1 * depth1) + (weight2 * depth2) + (weight3 * depth3)) / weight_total; +} + +// Used to share the depths from the previous MIP level between all invocations in a workgroup +var previous_mip_depth: array, 8>; + +@compute +@workgroup_size(8, 8, 1) +fn preprocess_depth(@builtin(global_invocation_id) global_id: vec3, @builtin(local_invocation_id) local_id: vec3) { + let base_coordinates = vec2(global_id.xy); + + // MIP 0 - Copy 4 texels from the input depth (per invocation, 8x8 invocations per workgroup) + let pixel_coordinates0 = base_coordinates * 2i; + let pixel_coordinates1 = pixel_coordinates0 + vec2(1i, 0i); + let pixel_coordinates2 = pixel_coordinates0 + vec2(0i, 1i); + let pixel_coordinates3 = pixel_coordinates0 + vec2(1i, 1i); + let depths_uv = vec2(pixel_coordinates0) / view.viewport.zw; + let depths = textureGather(0, input_depth, point_clamp_sampler, depths_uv, vec2(1i, 1i)); + textureStore(preprocessed_depth_mip0, pixel_coordinates0, vec4(depths.w, 0.0, 0.0, 0.0)); + textureStore(preprocessed_depth_mip0, pixel_coordinates1, vec4(depths.z, 0.0, 0.0, 0.0)); + textureStore(preprocessed_depth_mip0, pixel_coordinates2, vec4(depths.x, 0.0, 0.0, 0.0)); + textureStore(preprocessed_depth_mip0, pixel_coordinates3, vec4(depths.y, 0.0, 0.0, 0.0)); + + // MIP 1 - Weighted average of MIP 0's depth values (per invocation, 8x8 invocations per workgroup) + let depth_mip1 = weighted_average(depths.w, depths.z, depths.x, depths.y); + textureStore(preprocessed_depth_mip1, base_coordinates, vec4(depth_mip1, 0.0, 0.0, 0.0)); + previous_mip_depth[local_id.x][local_id.y] = depth_mip1; + + workgroupBarrier(); + + // MIP 2 - Weighted average of MIP 1's depth values (per invocation, 4x4 invocations per workgroup) + if all(local_id.xy % vec2(2u) == vec2(0u)) { + let depth0 = previous_mip_depth[local_id.x + 0u][local_id.y + 0u]; + let depth1 = previous_mip_depth[local_id.x + 1u][local_id.y + 0u]; + let depth2 = previous_mip_depth[local_id.x + 0u][local_id.y + 1u]; + let depth3 = previous_mip_depth[local_id.x + 1u][local_id.y + 1u]; + let depth_mip2 = weighted_average(depth0, depth1, depth2, depth3); + textureStore(preprocessed_depth_mip2, base_coordinates / 2i, vec4(depth_mip2, 0.0, 0.0, 0.0)); + previous_mip_depth[local_id.x][local_id.y] = depth_mip2; + } + + workgroupBarrier(); + + // MIP 3 - Weighted average of MIP 2's depth values (per invocation, 2x2 invocations per workgroup) + if all(local_id.xy % vec2(4u) == vec2(0u)) { + let depth0 = previous_mip_depth[local_id.x + 0u][local_id.y + 0u]; + let depth1 = previous_mip_depth[local_id.x + 2u][local_id.y + 0u]; + let depth2 = previous_mip_depth[local_id.x + 0u][local_id.y + 2u]; + let depth3 = previous_mip_depth[local_id.x + 2u][local_id.y + 2u]; + let depth_mip3 = weighted_average(depth0, depth1, depth2, depth3); + textureStore(preprocessed_depth_mip3, base_coordinates / 4i, vec4(depth_mip3, 0.0, 0.0, 0.0)); + previous_mip_depth[local_id.x][local_id.y] = depth_mip3; + } + + workgroupBarrier(); + + // MIP 4 - Weighted average of MIP 3's depth values (per invocation, 1 invocation per workgroup) + if all(local_id.xy % vec2(8u) == vec2(0u)) { + let depth0 = previous_mip_depth[local_id.x + 0u][local_id.y + 0u]; + let depth1 = previous_mip_depth[local_id.x + 4u][local_id.y + 0u]; + let depth2 = previous_mip_depth[local_id.x + 0u][local_id.y + 4u]; + let depth3 = previous_mip_depth[local_id.x + 4u][local_id.y + 4u]; + let depth_mip4 = weighted_average(depth0, depth1, depth2, depth3); + textureStore(preprocessed_depth_mip4, base_coordinates / 8i, vec4(depth_mip4, 0.0, 0.0, 0.0)); + } +} diff --git a/crates/libmarathon/src/render/pbr/ssao/spatial_denoise.wgsl b/crates/libmarathon/src/render/pbr/ssao/spatial_denoise.wgsl new file mode 100644 index 0000000..1c04f9c --- /dev/null +++ b/crates/libmarathon/src/render/pbr/ssao/spatial_denoise.wgsl @@ -0,0 +1,85 @@ +// 3x3 bilaterial filter (edge-preserving blur) +// https://people.csail.mit.edu/sparis/bf_course/course_notes.pdf + +// Note: Does not use the Gaussian kernel part of a typical bilateral blur +// From the paper: "use the information gathered on a neighborhood of 4 × 4 using a bilateral filter for +// reconstruction, using _uniform_ convolution weights" + +// Note: The paper does a 4x4 (not quite centered) filter, offset by +/- 1 pixel every other frame +// XeGTAO does a 3x3 filter, on two pixels at a time per compute thread, applied twice +// We do a 3x3 filter, on 1 pixel per compute thread, applied once + +#import bevy_render::view::View + +@group(0) @binding(0) var ambient_occlusion_noisy: texture_2d; +@group(0) @binding(1) var depth_differences: texture_2d; +@group(0) @binding(2) var ambient_occlusion: texture_storage_2d; +@group(1) @binding(0) var point_clamp_sampler: sampler; +@group(1) @binding(1) var linear_clamp_sampler: sampler; +@group(1) @binding(2) var view: View; + +@compute +@workgroup_size(8, 8, 1) +fn spatial_denoise(@builtin(global_invocation_id) global_id: vec3) { + let pixel_coordinates = vec2(global_id.xy); + let uv = vec2(pixel_coordinates) / view.viewport.zw; + + let edges0 = textureGather(0, depth_differences, point_clamp_sampler, uv); + let edges1 = textureGather(0, depth_differences, point_clamp_sampler, uv, vec2(2i, 0i)); + let edges2 = textureGather(0, depth_differences, point_clamp_sampler, uv, vec2(1i, 2i)); + let visibility0 = textureGather(0, ambient_occlusion_noisy, point_clamp_sampler, uv); + let visibility1 = textureGather(0, ambient_occlusion_noisy, point_clamp_sampler, uv, vec2(2i, 0i)); + let visibility2 = textureGather(0, ambient_occlusion_noisy, point_clamp_sampler, uv, vec2(0i, 2i)); + let visibility3 = textureGather(0, ambient_occlusion_noisy, point_clamp_sampler, uv, vec2(2i, 2i)); + + let left_edges = unpack4x8unorm(edges0.x); + let right_edges = unpack4x8unorm(edges1.x); + let top_edges = unpack4x8unorm(edges0.z); + let bottom_edges = unpack4x8unorm(edges2.w); + var center_edges = unpack4x8unorm(edges0.y); + center_edges *= vec4(left_edges.y, right_edges.x, top_edges.w, bottom_edges.z); + + let center_weight = 1.2; + let left_weight = center_edges.x; + let right_weight = center_edges.y; + let top_weight = center_edges.z; + let bottom_weight = center_edges.w; + let top_left_weight = 0.425 * (top_weight * top_edges.x + left_weight * left_edges.z); + let top_right_weight = 0.425 * (top_weight * top_edges.y + right_weight * right_edges.z); + let bottom_left_weight = 0.425 * (bottom_weight * bottom_edges.x + left_weight * left_edges.w); + let bottom_right_weight = 0.425 * (bottom_weight * bottom_edges.y + right_weight * right_edges.w); + + let center_visibility = visibility0.y; + let left_visibility = visibility0.x; + let right_visibility = visibility0.z; + let top_visibility = visibility1.x; + let bottom_visibility = visibility2.z; + let top_left_visibility = visibility0.w; + let top_right_visibility = visibility1.w; + let bottom_left_visibility = visibility2.w; + let bottom_right_visibility = visibility3.w; + + var sum = center_visibility; + sum += left_visibility * left_weight; + sum += right_visibility * right_weight; + sum += top_visibility * top_weight; + sum += bottom_visibility * bottom_weight; + sum += top_left_visibility * top_left_weight; + sum += top_right_visibility * top_right_weight; + sum += bottom_left_visibility * bottom_left_weight; + sum += bottom_right_visibility * bottom_right_weight; + + var sum_weight = center_weight; + sum_weight += left_weight; + sum_weight += right_weight; + sum_weight += top_weight; + sum_weight += bottom_weight; + sum_weight += top_left_weight; + sum_weight += top_right_weight; + sum_weight += bottom_left_weight; + sum_weight += bottom_right_weight; + + let denoised_visibility = sum / sum_weight; + + textureStore(ambient_occlusion, pixel_coordinates, vec4(denoised_visibility, 0.0, 0.0, 0.0)); +} diff --git a/crates/libmarathon/src/render/pbr/ssao/ssao.wgsl b/crates/libmarathon/src/render/pbr/ssao/ssao.wgsl new file mode 100644 index 0000000..ac64d56 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/ssao/ssao.wgsl @@ -0,0 +1,200 @@ +// Visibility Bitmask Ambient Occlusion (VBAO) +// Paper: ttps://ar5iv.labs.arxiv.org/html/2301.11376 + +// Source code heavily based on XeGTAO v1.30 from Intel +// https://github.com/GameTechDev/XeGTAO/blob/0d177ce06bfa642f64d8af4de1197ad1bcb862d4/Source/Rendering/Shaders/XeGTAO.hlsli + +// Source code based on the existing XeGTAO implementation and +// https://cdrinmatane.github.io/posts/ssaovb-code/ + +// Source code base on SSRT3 implementation +// https://github.com/cdrinmatane/SSRT3 + +#import bevy_render::maths::fast_acos + +#import bevy_render::{ + view::View, + globals::Globals, + maths::{PI, HALF_PI}, +} + +@group(0) @binding(0) var preprocessed_depth: texture_2d; +@group(0) @binding(1) var normals: texture_2d; +@group(0) @binding(2) var hilbert_index_lut: texture_2d; +@group(0) @binding(3) var ambient_occlusion: texture_storage_2d; +@group(0) @binding(4) var depth_differences: texture_storage_2d; +@group(0) @binding(5) var globals: Globals; +@group(0) @binding(6) var thickness: f32; +@group(1) @binding(0) var point_clamp_sampler: sampler; +@group(1) @binding(1) var linear_clamp_sampler: sampler; +@group(1) @binding(2) var view: View; + +fn load_noise(pixel_coordinates: vec2) -> vec2 { + var index = textureLoad(hilbert_index_lut, pixel_coordinates % 64, 0).r; + +#ifdef TEMPORAL_JITTER + index += 288u * (globals.frame_count % 64u); +#endif + + // R2 sequence - http://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences + return fract(0.5 + f32(index) * vec2(0.75487766624669276005, 0.5698402909980532659114)); +} + +// Calculate differences in depth between neighbor pixels (later used by the spatial denoiser pass to preserve object edges) +fn calculate_neighboring_depth_differences(pixel_coordinates: vec2) -> f32 { + // Sample the pixel's depth and 4 depths around it + let uv = vec2(pixel_coordinates) / view.viewport.zw; + let depths_upper_left = textureGather(0, preprocessed_depth, point_clamp_sampler, uv); + let depths_bottom_right = textureGather(0, preprocessed_depth, point_clamp_sampler, uv, vec2(1i, 1i)); + let depth_center = depths_upper_left.y; + let depth_left = depths_upper_left.x; + let depth_top = depths_upper_left.z; + let depth_bottom = depths_bottom_right.x; + let depth_right = depths_bottom_right.z; + + // Calculate the depth differences (large differences represent object edges) + var edge_info = vec4(depth_left, depth_right, depth_top, depth_bottom) - depth_center; + let slope_left_right = (edge_info.y - edge_info.x) * 0.5; + let slope_top_bottom = (edge_info.w - edge_info.z) * 0.5; + let edge_info_slope_adjusted = edge_info + vec4(slope_left_right, -slope_left_right, slope_top_bottom, -slope_top_bottom); + edge_info = min(abs(edge_info), abs(edge_info_slope_adjusted)); + let bias = 0.25; // Using the bias and then saturating nudges the values a bit + let scale = depth_center * 0.011; // Weight the edges by their distance from the camera + edge_info = saturate((1.0 + bias) - edge_info / scale); // Apply the bias and scale, and invert edge_info so that small values become large, and vice versa + + // Pack the edge info into the texture + let edge_info_packed = vec4(pack4x8unorm(edge_info), 0u, 0u, 0u); + textureStore(depth_differences, pixel_coordinates, edge_info_packed); + + return depth_center; +} + +fn load_normal_view_space(uv: vec2) -> vec3 { + var world_normal = textureSampleLevel(normals, point_clamp_sampler, uv, 0.0).xyz; + world_normal = (world_normal * 2.0) - 1.0; + let view_from_world = mat3x3( + view.view_from_world[0].xyz, + view.view_from_world[1].xyz, + view.view_from_world[2].xyz, + ); + return view_from_world * world_normal; +} + +fn reconstruct_view_space_position(depth: f32, uv: vec2) -> vec3 { + let clip_xy = vec2(uv.x * 2.0 - 1.0, 1.0 - 2.0 * uv.y); + let t = view.view_from_clip * vec4(clip_xy, depth, 1.0); + let view_xyz = t.xyz / t.w; + return view_xyz; +} + +fn load_and_reconstruct_view_space_position(uv: vec2, sample_mip_level: f32) -> vec3 { + let depth = textureSampleLevel(preprocessed_depth, linear_clamp_sampler, uv, sample_mip_level).r; + return reconstruct_view_space_position(depth, uv); +} + +fn updateSectors( + min_horizon: f32, + max_horizon: f32, + samples_per_slice: f32, + bitmask: u32, +) -> u32 { + let start_horizon = u32(min_horizon * samples_per_slice); + let angle_horizon = u32(ceil((max_horizon - min_horizon) * samples_per_slice)); + + return insertBits(bitmask, 0xFFFFFFFFu, start_horizon, angle_horizon); +} + +fn processSample( + delta_position: vec3, + view_vec: vec3, + sampling_direction: f32, + n: vec2, + samples_per_slice: f32, + bitmask: ptr, +) { + let delta_position_back_face = delta_position - view_vec * thickness; + + var front_back_horizon = vec2( + fast_acos(dot(normalize(delta_position), view_vec)), + fast_acos(dot(normalize(delta_position_back_face), view_vec)), + ); + + front_back_horizon = saturate(fma(vec2(sampling_direction), -front_back_horizon, n)); + front_back_horizon = select(front_back_horizon.xy, front_back_horizon.yx, sampling_direction >= 0.0); + + *bitmask = updateSectors(front_back_horizon.x, front_back_horizon.y, samples_per_slice, *bitmask); +} + +@compute +@workgroup_size(8, 8, 1) +fn ssao(@builtin(global_invocation_id) global_id: vec3) { + let slice_count = f32(#SLICE_COUNT); + let samples_per_slice_side = f32(#SAMPLES_PER_SLICE_SIDE); + let effect_radius = 0.5 * 1.457; + let falloff_range = 0.615 * effect_radius; + let falloff_from = effect_radius * (1.0 - 0.615); + let falloff_mul = -1.0 / falloff_range; + let falloff_add = falloff_from / falloff_range + 1.0; + + let pixel_coordinates = vec2(global_id.xy); + let uv = (vec2(pixel_coordinates) + 0.5) / view.viewport.zw; + + var pixel_depth = calculate_neighboring_depth_differences(pixel_coordinates); + pixel_depth += 0.00001; // Avoid depth precision issues + + let pixel_position = reconstruct_view_space_position(pixel_depth, uv); + let pixel_normal = load_normal_view_space(uv); + let view_vec = normalize(-pixel_position); + + let noise = load_noise(pixel_coordinates); + let sample_scale = (-0.5 * effect_radius * view.clip_from_view[0][0]) / pixel_position.z; + + var visibility = 0.0; + var occluded_sample_count = 0u; + for (var slice_t = 0.0; slice_t < slice_count; slice_t += 1.0) { + let slice = slice_t + noise.x; + let phi = (PI / slice_count) * slice; + let omega = vec2(cos(phi), sin(phi)); + + let direction = vec3(omega.xy, 0.0); + let orthographic_direction = direction - (dot(direction, view_vec) * view_vec); + let axis = cross(direction, view_vec); + let projected_normal = pixel_normal - axis * dot(pixel_normal, axis); + let projected_normal_length = length(projected_normal); + + let sign_norm = sign(dot(orthographic_direction, projected_normal)); + let cos_norm = saturate(dot(projected_normal, view_vec) / projected_normal_length); + let n = vec2((HALF_PI - sign_norm * fast_acos(cos_norm)) * (1.0 / PI)); + + var bitmask = 0u; + + let sample_mul = vec2(omega.x, -omega.y) * sample_scale; + for (var sample_t = 0.0; sample_t < samples_per_slice_side; sample_t += 1.0) { + var sample_noise = (slice_t + sample_t * samples_per_slice_side) * 0.6180339887498948482; + sample_noise = fract(noise.y + sample_noise); + + var s = (sample_t + sample_noise) / samples_per_slice_side; + s *= s; // https://github.com/GameTechDev/XeGTAO#sample-distribution + let sample = s * sample_mul; + + // * view.viewport.zw gets us from [0, 1] to [0, viewport_size], which is needed for this to get the correct mip levels + let sample_mip_level = clamp(log2(length(sample * view.viewport.zw)) - 3.3, 0.0, 5.0); // https://github.com/GameTechDev/XeGTAO#memory-bandwidth-bottleneck + let sample_position_1 = load_and_reconstruct_view_space_position(uv + sample, sample_mip_level); + let sample_position_2 = load_and_reconstruct_view_space_position(uv - sample, sample_mip_level); + + let sample_difference_1 = sample_position_1 - pixel_position; + let sample_difference_2 = sample_position_2 - pixel_position; + + processSample(sample_difference_1, view_vec, -1.0, n, samples_per_slice_side * 2.0, &bitmask); + processSample(sample_difference_2, view_vec, 1.0, n, samples_per_slice_side * 2.0, &bitmask); + } + + occluded_sample_count += countOneBits(bitmask); + } + + visibility = 1.0 - f32(occluded_sample_count) / (slice_count * 2.0 * samples_per_slice_side); + + visibility = clamp(visibility, 0.03, 1.0); + + textureStore(ambient_occlusion, pixel_coordinates, vec4(visibility, 0.0, 0.0, 0.0)); +} diff --git a/crates/libmarathon/src/render/pbr/ssao/ssao_utils.wgsl b/crates/libmarathon/src/render/pbr/ssao/ssao_utils.wgsl new file mode 100644 index 0000000..be19fa6 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/ssao/ssao_utils.wgsl @@ -0,0 +1,13 @@ +#define_import_path bevy_pbr::ssao_utils + +#import bevy_render::maths::{PI, HALF_PI} + +// Approximates single-bounce ambient occlusion to multi-bounce ambient occlusion +// https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf#page=78 +fn ssao_multibounce(visibility: f32, base_color: vec3) -> vec3 { + let a = 2.0404 * base_color - 0.3324; + let b = -4.7951 * base_color + 0.6417; + let c = 2.7552 * base_color + 0.6903; + let x = vec3(visibility); + return max(x, ((x * a + b) * x + c) * x); +} diff --git a/crates/libmarathon/src/render/pbr/ssr/mod.rs b/crates/libmarathon/src/render/pbr/ssr/mod.rs new file mode 100644 index 0000000..93ed839 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/ssr/mod.rs @@ -0,0 +1,578 @@ +//! Screen space reflections implemented via raymarching. + +use bevy_app::{App, Plugin}; +use bevy_asset::{load_embedded_asset, AssetServer, Handle}; +use crate::render::{ + core_3d::{ + graph::{Core3d, Node3d}, + DEPTH_TEXTURE_SAMPLING_SUPPORTED, + }, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + FullscreenShader, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{Has, QueryItem, With}, + reflect::ReflectComponent, + resource::Resource, + schedule::IntoScheduleConfigs as _, + system::{lifetimeless::Read, Commands, Query, Res, ResMut}, + world::World, +}; +use bevy_image::BevyDefault as _; +use bevy_light::EnvironmentMapLight; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use crate::render::{ + diagnostic::RecordDiagnostics, + extract_component::{ExtractComponent, ExtractComponentPlugin}, + render_graph::{ + NodeRunError, RenderGraph, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner, + }, + render_resource::{ + binding_types, AddressMode, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, + CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, FilterMode, + FragmentState, Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, + RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, + ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureFormat, + TextureSampleType, + }, + renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue}, + view::{ExtractedView, Msaa, ViewTarget, ViewUniformOffset}, + Render, RenderApp, RenderStartup, RenderSystems, +}; +use bevy_shader::{load_shader_library, Shader}; +use bevy_utils::{once, prelude::default}; +use tracing::info; + +use crate::render::pbr::{ + binding_arrays_are_usable, graph::NodePbr, MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, + MeshViewBindGroup, RenderViewLightProbes, ViewEnvironmentMapUniformOffset, + ViewFogUniformOffset, ViewLightProbesUniformOffset, ViewLightsUniformOffset, +}; + +/// Enables screen-space reflections for a camera. +/// +/// Screen-space reflections are currently only supported with deferred rendering. +pub struct ScreenSpaceReflectionsPlugin; + +/// Add this component to a camera to enable *screen-space reflections* (SSR). +/// +/// Screen-space reflections currently require deferred rendering in order to +/// appear. Therefore, they also need the [`DepthPrepass`] and [`DeferredPrepass`] +/// components, which are inserted automatically. +/// +/// SSR currently performs no roughness filtering for glossy reflections, so +/// only very smooth surfaces will reflect objects in screen space. You can +/// adjust the `perceptual_roughness_threshold` in order to tune the threshold +/// below which screen-space reflections will be traced. +/// +/// As with all screen-space techniques, SSR can only reflect objects on screen. +/// When objects leave the camera, they will disappear from reflections. +/// An alternative that doesn't suffer from this problem is the combination of +/// a [`LightProbe`](bevy_light::LightProbe) and [`EnvironmentMapLight`]. The advantage of SSR is +/// that it can reflect all objects, not just static ones. +/// +/// SSR is an approximation technique and produces artifacts in some situations. +/// Hand-tuning the settings in this component will likely be useful. +/// +/// Screen-space reflections are presently unsupported on WebGL 2 because of a +/// bug whereby Naga doesn't generate correct GLSL when sampling depth buffers, +/// which is required for screen-space raymarching. +#[derive(Clone, Copy, Component, Reflect)] +#[reflect(Component, Default, Clone)] +#[require(DepthPrepass, DeferredPrepass)] +#[doc(alias = "Ssr")] +pub struct ScreenSpaceReflections { + /// The maximum PBR roughness level that will enable screen space + /// reflections. + pub perceptual_roughness_threshold: f32, + + /// When marching the depth buffer, we only have 2.5D information and don't + /// know how thick surfaces are. We shall assume that the depth buffer + /// fragments are cuboids with a constant thickness defined by this + /// parameter. + pub thickness: f32, + + /// The number of steps to be taken at regular intervals to find an initial + /// intersection. Must not be zero. + /// + /// Higher values result in higher-quality reflections, because the + /// raymarching shader is less likely to miss objects. However, they take + /// more GPU time. + pub linear_steps: u32, + + /// Exponent to be applied in the linear part of the march. + /// + /// A value of 1.0 will result in equidistant steps, and higher values will + /// compress the earlier steps, and expand the later ones. This might be + /// desirable in order to get more detail close to objects. + /// + /// For optimal performance, this should be a small unsigned integer, such + /// as 1 or 2. + pub linear_march_exponent: f32, + + /// Number of steps in a bisection (binary search) to perform once the + /// linear search has found an intersection. Helps narrow down the hit, + /// increasing the chance of the secant method finding an accurate hit + /// point. + pub bisection_steps: u32, + + /// Approximate the root position using the secant method—by solving for + /// line-line intersection between the ray approach rate and the surface + /// gradient. + pub use_secant: bool, +} + +/// A version of [`ScreenSpaceReflections`] for upload to the GPU. +/// +/// For more information on these fields, see the corresponding documentation in +/// [`ScreenSpaceReflections`]. +#[derive(Clone, Copy, Component, ShaderType)] +pub struct ScreenSpaceReflectionsUniform { + perceptual_roughness_threshold: f32, + thickness: f32, + linear_steps: u32, + linear_march_exponent: f32, + bisection_steps: u32, + /// A boolean converted to a `u32`. + use_secant: u32, +} + +/// The node in the render graph that traces screen space reflections. +#[derive(Default)] +pub struct ScreenSpaceReflectionsNode; + +/// Identifies which screen space reflections render pipeline a view needs. +#[derive(Component, Deref, DerefMut)] +pub struct ScreenSpaceReflectionsPipelineId(pub CachedRenderPipelineId); + +/// Information relating to the render pipeline for the screen space reflections +/// shader. +#[derive(Resource)] +pub struct ScreenSpaceReflectionsPipeline { + mesh_view_layouts: MeshPipelineViewLayouts, + color_sampler: Sampler, + depth_linear_sampler: Sampler, + depth_nearest_sampler: Sampler, + bind_group_layout: BindGroupLayout, + binding_arrays_are_usable: bool, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, +} + +/// A GPU buffer that stores the screen space reflection settings for each view. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct ScreenSpaceReflectionsBuffer(pub DynamicUniformBuffer); + +/// A component that stores the offset within the +/// [`ScreenSpaceReflectionsBuffer`] for each view. +#[derive(Component, Default, Deref, DerefMut)] +pub struct ViewScreenSpaceReflectionsUniformOffset(u32); + +/// Identifies a specific configuration of the SSR pipeline shader. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct ScreenSpaceReflectionsPipelineKey { + mesh_pipeline_view_key: MeshPipelineViewLayoutKey, + is_hdr: bool, + has_environment_maps: bool, +} + +impl Plugin for ScreenSpaceReflectionsPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "ssr.wgsl"); + load_shader_library!(app, "raymarch.wgsl"); + + app.add_plugins(ExtractComponentPlugin::::default()); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .init_resource::>() + .add_systems( + RenderStartup, + ( + init_screen_space_reflections_pipeline, + add_screen_space_reflections_render_graph_edges, + ), + ) + .add_systems(Render, prepare_ssr_pipelines.in_set(RenderSystems::Prepare)) + .add_systems( + Render, + prepare_ssr_settings.in_set(RenderSystems::PrepareResources), + ) + // Note: we add this node here but then we add edges in + // `add_screen_space_reflections_render_graph_edges`. + .add_render_graph_node::>( + Core3d, + NodePbr::ScreenSpaceReflections, + ); + } +} + +fn add_screen_space_reflections_render_graph_edges(mut render_graph: ResMut) { + let subgraph = render_graph.sub_graph_mut(Core3d); + + subgraph.add_node_edge(NodePbr::ScreenSpaceReflections, Node3d::MainOpaquePass); + + if subgraph + .get_node_state(NodePbr::DeferredLightingPass) + .is_ok() + { + subgraph.add_node_edge( + NodePbr::DeferredLightingPass, + NodePbr::ScreenSpaceReflections, + ); + } +} + +impl Default for ScreenSpaceReflections { + // Reasonable default values. + // + // These are from + // . + fn default() -> Self { + Self { + perceptual_roughness_threshold: 0.1, + linear_steps: 16, + bisection_steps: 4, + use_secant: true, + thickness: 0.25, + linear_march_exponent: 1.0, + } + } +} + +impl ViewNode for ScreenSpaceReflectionsNode { + type ViewQuery = ( + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + ); + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + ( + view_target, + view_uniform_offset, + view_lights_offset, + view_fog_offset, + view_light_probes_offset, + view_ssr_offset, + view_environment_map_offset, + view_bind_group, + ssr_pipeline_id, + ): QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + // Grab the render pipeline. + let pipeline_cache = world.resource::(); + let Some(render_pipeline) = pipeline_cache.get_render_pipeline(**ssr_pipeline_id) else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + // Set up a standard pair of postprocessing textures. + let postprocess = view_target.post_process_write(); + + // Create the bind group for this view. + let ssr_pipeline = world.resource::(); + let ssr_bind_group = render_context.render_device().create_bind_group( + "SSR bind group", + &ssr_pipeline.bind_group_layout, + &BindGroupEntries::sequential(( + postprocess.source, + &ssr_pipeline.color_sampler, + &ssr_pipeline.depth_linear_sampler, + &ssr_pipeline.depth_nearest_sampler, + )), + ); + + // Build the SSR render pass. + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("ssr"), + color_attachments: &[Some(RenderPassColorAttachment { + view: postprocess.destination, + depth_slice: None, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + let pass_span = diagnostics.pass_span(&mut render_pass, "ssr"); + + // Set bind groups. + render_pass.set_render_pipeline(render_pipeline); + render_pass.set_bind_group( + 0, + &view_bind_group.main, + &[ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + **view_light_probes_offset, + **view_ssr_offset, + **view_environment_map_offset, + ], + ); + render_pass.set_bind_group(1, &view_bind_group.binding_array, &[]); + + // Perform the SSR render pass. + render_pass.set_bind_group(2, &ssr_bind_group, &[]); + render_pass.draw(0..3, 0..1); + + pass_span.end(&mut render_pass); + + Ok(()) + } +} + +pub fn init_screen_space_reflections_pipeline( + mut commands: Commands, + render_device: Res, + render_adapter: Res, + mesh_view_layouts: Res, + fullscreen_shader: Res, + asset_server: Res, +) { + // Create the bind group layout. + let bind_group_layout = render_device.create_bind_group_layout( + "SSR bind group layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + binding_types::texture_2d(TextureSampleType::Float { filterable: true }), + binding_types::sampler(SamplerBindingType::Filtering), + binding_types::sampler(SamplerBindingType::Filtering), + binding_types::sampler(SamplerBindingType::NonFiltering), + ), + ), + ); + + // Create the samplers we need. + + let color_sampler = render_device.create_sampler(&SamplerDescriptor { + label: "SSR color sampler".into(), + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..default() + }); + + let depth_linear_sampler = render_device.create_sampler(&SamplerDescriptor { + label: "SSR depth linear sampler".into(), + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + mag_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + ..default() + }); + + let depth_nearest_sampler = render_device.create_sampler(&SamplerDescriptor { + label: "SSR depth nearest sampler".into(), + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + mag_filter: FilterMode::Nearest, + min_filter: FilterMode::Nearest, + ..default() + }); + + commands.insert_resource(ScreenSpaceReflectionsPipeline { + mesh_view_layouts: mesh_view_layouts.clone(), + color_sampler, + depth_linear_sampler, + depth_nearest_sampler, + bind_group_layout, + binding_arrays_are_usable: binding_arrays_are_usable(&render_device, &render_adapter), + fullscreen_shader: fullscreen_shader.clone(), + // Even though ssr was loaded using load_shader_library, we can still access it like a + // normal embedded asset (so we can use it as both a library or a kernel). + fragment_shader: load_embedded_asset!(asset_server.as_ref(), "ssr.wgsl"), + }); +} + +/// Sets up screen space reflection pipelines for each applicable view. +pub fn prepare_ssr_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + ssr_pipeline: Res, + views: Query< + ( + Entity, + &ExtractedView, + Has>, + Has, + Has, + ), + ( + With, + With, + With, + ), + >, +) { + for ( + entity, + extracted_view, + has_environment_maps, + has_normal_prepass, + has_motion_vector_prepass, + ) in &views + { + // SSR is only supported in the deferred pipeline, which has no MSAA + // support. Thus we can assume MSAA is off. + let mut mesh_pipeline_view_key = MeshPipelineViewLayoutKey::from(Msaa::Off) + | MeshPipelineViewLayoutKey::DEPTH_PREPASS + | MeshPipelineViewLayoutKey::DEFERRED_PREPASS; + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::NORMAL_PREPASS, + has_normal_prepass, + ); + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS, + has_motion_vector_prepass, + ); + + // Build the pipeline. + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &ssr_pipeline, + ScreenSpaceReflectionsPipelineKey { + mesh_pipeline_view_key, + is_hdr: extracted_view.hdr, + has_environment_maps, + }, + ); + + // Note which pipeline ID was used. + commands + .entity(entity) + .insert(ScreenSpaceReflectionsPipelineId(pipeline_id)); + } +} + +/// Gathers up screen space reflection settings for each applicable view and +/// writes them into a GPU buffer. +pub fn prepare_ssr_settings( + mut commands: Commands, + views: Query<(Entity, Option<&ScreenSpaceReflectionsUniform>), With>, + mut ssr_settings_buffer: ResMut, + render_device: Res, + render_queue: Res, +) { + let Some(mut writer) = + ssr_settings_buffer.get_writer(views.iter().len(), &render_device, &render_queue) + else { + return; + }; + + for (view, ssr_uniform) in views.iter() { + let uniform_offset = match ssr_uniform { + None => 0, + Some(ssr_uniform) => writer.write(ssr_uniform), + }; + commands + .entity(view) + .insert(ViewScreenSpaceReflectionsUniformOffset(uniform_offset)); + } +} + +impl ExtractComponent for ScreenSpaceReflections { + type QueryData = Read; + + type QueryFilter = (); + + type Out = ScreenSpaceReflectionsUniform; + + fn extract_component(settings: QueryItem<'_, '_, Self::QueryData>) -> Option { + if !DEPTH_TEXTURE_SAMPLING_SUPPORTED { + once!(info!( + "Disabling screen-space reflections on this platform because depth textures \ + aren't supported correctly" + )); + return None; + } + + Some((*settings).into()) + } +} + +impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline { + type Key = ScreenSpaceReflectionsPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let layout = self + .mesh_view_layouts + .get_view_layout(key.mesh_pipeline_view_key); + let layout = vec![ + layout.main_layout.clone(), + layout.binding_array_layout.clone(), + self.bind_group_layout.clone(), + ]; + + let mut shader_defs = vec![ + "DEPTH_PREPASS".into(), + "DEFERRED_PREPASS".into(), + "SCREEN_SPACE_REFLECTIONS".into(), + ]; + + if key.has_environment_maps { + shader_defs.push("ENVIRONMENT_MAP".into()); + } + + if self.binding_arrays_are_usable { + shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into()); + } + + RenderPipelineDescriptor { + label: Some("SSR pipeline".into()), + layout, + vertex: self.fullscreen_shader.to_vertex_state(), + fragment: Some(FragmentState { + shader: self.fragment_shader.clone(), + shader_defs, + targets: vec![Some(ColorTargetState { + format: if key.is_hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: None, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + ..default() + } + } +} + +impl From for ScreenSpaceReflectionsUniform { + fn from(settings: ScreenSpaceReflections) -> Self { + Self { + perceptual_roughness_threshold: settings.perceptual_roughness_threshold, + thickness: settings.thickness, + linear_steps: settings.linear_steps, + linear_march_exponent: settings.linear_march_exponent, + bisection_steps: settings.bisection_steps, + use_secant: settings.use_secant as u32, + } + } +} diff --git a/crates/libmarathon/src/render/pbr/ssr/raymarch.wgsl b/crates/libmarathon/src/render/pbr/ssr/raymarch.wgsl new file mode 100644 index 0000000..12140c9 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/ssr/raymarch.wgsl @@ -0,0 +1,511 @@ +// Copyright (c) 2023 Tomasz Stachowiak +// +// This contribution is dual licensed under EITHER OF +// +// Apache License, Version 2.0, (http://www.apache.org/licenses/LICENSE-2.0) +// MIT license (http://opensource.org/licenses/MIT) +// +// at your option. +// +// This is a port of the original [`raymarch.hlsl`] to WGSL. It's deliberately +// kept as close as possible so that patches to the original `raymarch.hlsl` +// have the greatest chances of applying to this version. +// +// [`raymarch.hlsl`]: +// https://gist.github.com/h3r2tic/9c8356bdaefbe80b1a22ae0aaee192db + +#define_import_path bevy_pbr::raymarch + +#import bevy_pbr::mesh_view_bindings::depth_prepass_texture +#import bevy_pbr::view_transformations::{ + direction_world_to_clip, + ndc_to_uv, + perspective_camera_near, + position_world_to_ndc, +} + +// Allows us to sample from the depth buffer with bilinear filtering. +@group(2) @binding(2) var depth_linear_sampler: sampler; + +// Allows us to sample from the depth buffer with nearest-neighbor filtering. +@group(2) @binding(3) var depth_nearest_sampler: sampler; + +// Main code + +struct HybridRootFinder { + linear_steps: u32, + bisection_steps: u32, + use_secant: bool, + linear_march_exponent: f32, + + jitter: f32, + min_t: f32, + max_t: f32, +} + +fn hybrid_root_finder_new_with_linear_steps(v: u32) -> HybridRootFinder { + var res: HybridRootFinder; + res.linear_steps = v; + res.bisection_steps = 0u; + res.use_secant = false; + res.linear_march_exponent = 1.0; + res.jitter = 1.0; + res.min_t = 0.0; + res.max_t = 1.0; + return res; +} + +fn hybrid_root_finder_find_root( + root_finder: ptr, + start: vec3, + end: vec3, + distance_fn: ptr, + hit_t: ptr, + miss_t: ptr, + hit_d: ptr, +) -> bool { + let dir = end - start; + + var min_t = (*root_finder).min_t; + var max_t = (*root_finder).max_t; + + var min_d = DistanceWithPenetration(0.0, false, 0.0); + var max_d = DistanceWithPenetration(0.0, false, 0.0); + + let step_size = (max_t - min_t) / f32((*root_finder).linear_steps); + + var intersected = false; + + // + // Ray march using linear steps + + if ((*root_finder).linear_steps > 0u) { + let candidate_t = mix( + min_t, + max_t, + pow( + (*root_finder).jitter / f32((*root_finder).linear_steps), + (*root_finder).linear_march_exponent + ) + ); + + let candidate = start + dir * candidate_t; + let candidate_d = depth_raymarch_distance_fn_evaluate(distance_fn, candidate); + intersected = candidate_d.distance < 0.0 && candidate_d.valid; + + if (intersected) { + max_t = candidate_t; + max_d = candidate_d; + // The `[min_t .. max_t]` interval contains an intersection. End the linear search. + } else { + // No intersection yet. Carry on. + min_t = candidate_t; + min_d = candidate_d; + + for (var step = 1u; step < (*root_finder).linear_steps; step += 1u) { + let candidate_t = mix( + (*root_finder).min_t, + (*root_finder).max_t, + pow( + (f32(step) + (*root_finder).jitter) / f32((*root_finder).linear_steps), + (*root_finder).linear_march_exponent + ) + ); + + let candidate = start + dir * candidate_t; + let candidate_d = depth_raymarch_distance_fn_evaluate(distance_fn, candidate); + intersected = candidate_d.distance < 0.0 && candidate_d.valid; + + if (intersected) { + max_t = candidate_t; + max_d = candidate_d; + // The `[min_t .. max_t]` interval contains an intersection. + // End the linear search. + break; + } else { + // No intersection yet. Carry on. + min_t = candidate_t; + min_d = candidate_d; + } + } + } + } + + *miss_t = min_t; + *hit_t = min_t; + + // + // Refine the hit using bisection + + if (intersected) { + for (var step = 0u; step < (*root_finder).bisection_steps; step += 1u) { + let mid_t = (min_t + max_t) * 0.5; + let candidate = start + dir * mid_t; + let candidate_d = depth_raymarch_distance_fn_evaluate(distance_fn, candidate); + + if (candidate_d.distance < 0.0 && candidate_d.valid) { + // Intersection at the mid point. Refine the first half. + max_t = mid_t; + max_d = candidate_d; + } else { + // No intersection yet at the mid point. Refine the second half. + min_t = mid_t; + min_d = candidate_d; + } + } + + if ((*root_finder).use_secant) { + // Finish with one application of the secant method + let total_d = min_d.distance + -max_d.distance; + + let mid_t = mix(min_t, max_t, min_d.distance / total_d); + let candidate = start + dir * mid_t; + let candidate_d = depth_raymarch_distance_fn_evaluate(distance_fn, candidate); + + // Only accept the result of the secant method if it improves upon + // the previous result. + // + // Technically root_finder should be `abs(candidate_d.distance) < + // min(min_d.distance, -max_d.distance) * frac`, but root_finder seems + // sufficient. + if (abs(candidate_d.distance) < min_d.distance * 0.9 && candidate_d.valid) { + *hit_t = mid_t; + *hit_d = candidate_d; + } else { + *hit_t = max_t; + *hit_d = max_d; + } + + return true; + } else { + *hit_t = max_t; + *hit_d = max_d; + return true; + } + } else { + // Mark the conservative miss distance. + *hit_t = min_t; + return false; + } +} + +struct DistanceWithPenetration { + /// Distance to the surface of which a root we're trying to find + distance: f32, + + /// Whether to consider this sample valid for intersection. + /// Mostly relevant for allowing the ray marcher to travel behind surfaces, + /// as it will mark surfaces it travels under as invalid. + valid: bool, + + /// Conservative estimate of depth to which the ray penetrates the marched surface. + penetration: f32, +} + +struct DepthRaymarchDistanceFn { + depth_tex_size: vec2, + + march_behind_surfaces: bool, + depth_thickness: f32, + + use_sloppy_march: bool, +} + +fn depth_raymarch_distance_fn_evaluate( + distance_fn: ptr, + ray_point_cs: vec3, +) -> DistanceWithPenetration { + let interp_uv = ndc_to_uv(ray_point_cs.xy); + + let ray_depth = 1.0 / ray_point_cs.z; + + // We're using both point-sampled and bilinear-filtered values from the depth buffer. + // + // That's really stupid but works like magic. For samples taken near the ray origin, + // the discrete nature of the depth buffer becomes a problem. It's not a land of continuous surfaces, + // but a bunch of stacked duplo bricks. + // + // Technically we should be taking discrete steps in distance_fn duplo land, but then we're at the mercy + // of arbitrary quantization of our directions -- and sometimes we'll take a step which would + // claim that the ray is occluded -- even though the underlying smooth surface wouldn't occlude it. + // + // If we instead take linear taps from the depth buffer, we reconstruct the linear surface. + // That fixes acne, but introduces false shadowing near object boundaries, as we now pretend + // that everything is shrink-wrapped by distance_fn continuous 2.5D surface, and our depth thickness + // heuristic ends up falling apart. + // + // The fix is to consider both the smooth and the discrete surfaces, and only claim occlusion + // when the ray descends below both. + // + // The two approaches end up fixing each other's artifacts: + // * The false occlusions due to duplo land are rejected because the ray stays above the smooth surface. + // * The shrink-wrap surface is no longer continuous, so it's possible for rays to miss it. + + let linear_depth = + 1.0 / textureSampleLevel(depth_prepass_texture, depth_linear_sampler, interp_uv, 0u); + let unfiltered_depth = + 1.0 / textureSampleLevel(depth_prepass_texture, depth_nearest_sampler, interp_uv, 0u); + + var max_depth: f32; + var min_depth: f32; + + if ((*distance_fn).use_sloppy_march) { + max_depth = unfiltered_depth; + min_depth = unfiltered_depth; + } else { + max_depth = max(linear_depth, unfiltered_depth); + min_depth = min(linear_depth, unfiltered_depth); + } + + let bias = 0.000002; + + var res: DistanceWithPenetration; + res.distance = max_depth * (1.0 + bias) - ray_depth; + + // distance_fn will be used at the end of the ray march to potentially discard the hit. + res.penetration = ray_depth - min_depth; + + if ((*distance_fn).march_behind_surfaces) { + res.valid = res.penetration < (*distance_fn).depth_thickness; + } else { + res.valid = true; + } + + return res; +} + +struct DepthRayMarchResult { + /// True if the raymarch hit something. + hit: bool, + + /// In case of a hit, the normalized distance to it. + /// + /// In case of a miss, the furthest the ray managed to travel, which could either be + /// exceeding the max range, or getting behind a surface further than the depth thickness. + /// + /// Range: `0..=1` as a lerp factor over `ray_start_cs..=ray_end_cs`. + hit_t: f32, + + /// UV corresponding to `hit_t`. + hit_uv: vec2, + + /// The distance that the hit point penetrates into the hit surface. + /// Will normally be non-zero due to limited precision of the ray march. + /// + /// In case of a miss: undefined. + hit_penetration: f32, + + /// Ditto, within the range `0..DepthRayMarch::depth_thickness_linear_z` + /// + /// In case of a miss: undefined. + hit_penetration_frac: f32, +} + +struct DepthRayMarch { + /// Number of steps to be taken at regular intervals to find an initial intersection. + /// Must not be zero. + linear_steps: u32, + + /// Exponent to be applied in the linear part of the march. + /// + /// A value of 1.0 will result in equidistant steps, and higher values will compress + /// the earlier steps, and expand the later ones. This might be desirable in order + /// to get more detail close to objects in SSR or SSGI. + /// + /// For optimal performance, this should be a small compile-time unsigned integer, + /// such as 1 or 2. + linear_march_exponent: f32, + + /// Number of steps in a bisection (binary search) to perform once the linear search + /// has found an intersection. Helps narrow down the hit, increasing the chance of + /// the secant method finding an accurate hit point. + /// + /// Useful when sampling color, e.g. SSR or SSGI, but pointless for contact shadows. + bisection_steps: u32, + + /// Approximate the root position using the secant method -- by solving for line-line + /// intersection between the ray approach rate and the surface gradient. + /// + /// Useful when sampling color, e.g. SSR or SSGI, but pointless for contact shadows. + use_secant: bool, + + /// Jitter to apply to the first step of the linear search; 0..=1 range, mapping + /// to the extent of a single linear step in the first phase of the search. + /// Use 1.0 if you don't want jitter. + jitter: f32, + + /// Clip space coordinates (w=1) of the ray. + ray_start_cs: vec3, + ray_end_cs: vec3, + + /// Should be used for contact shadows, but not for any color bounce, e.g. SSR. + /// + /// For SSR etc. this can easily create leaks, but with contact shadows it allows the rays + /// to pass over invalid occlusions (due to thickness), and find potentially valid ones ahead. + /// + /// Note that this will cause the linear search to potentially miss surfaces, + /// because when the ray overshoots and ends up penetrating a surface further than + /// `depth_thickness_linear_z`, the ray marcher will just carry on. + /// + /// For this reason, this may require a lot of samples, or high depth thickness, + /// so that `depth_thickness_linear_z >= world space ray length / linear_steps`. + march_behind_surfaces: bool, + + /// If `true`, the ray marcher only performs nearest lookups of the depth buffer, + /// resulting in aliasing and false occlusion when marching tiny detail. + /// It should work fine for longer traces with fewer rays though. + use_sloppy_march: bool, + + /// When marching the depth buffer, we only have 2.5D information, and don't know how + /// thick surfaces are. We shall assume that the depth buffer fragments are little squares + /// with a constant thickness defined by this parameter. + depth_thickness_linear_z: f32, + + /// Size of the depth buffer we're marching in, in pixels. + depth_tex_size: vec2, +} + +fn depth_ray_march_new_from_depth(depth_tex_size: vec2) -> DepthRayMarch { + var res: DepthRayMarch; + res.jitter = 1.0; + res.linear_steps = 4u; + res.bisection_steps = 0u; + res.linear_march_exponent = 1.0; + res.depth_tex_size = depth_tex_size; + res.depth_thickness_linear_z = 1.0; + res.march_behind_surfaces = false; + res.use_sloppy_march = false; + return res; +} + +fn depth_ray_march_to_cs_dir_impl( + raymarch: ptr, + dir_cs: vec4, + infinite: bool, +) { + var end_cs = vec4((*raymarch).ray_start_cs, 1.0) + dir_cs; + + // Perform perspective division, but avoid dividing by zero for rays + // heading directly towards the eye. + end_cs /= select(-1.0, 1.0, end_cs.w >= 0.0) * max(1e-10, abs(end_cs.w)); + + // Clip ray start to the view frustum + var delta_cs = end_cs.xyz - (*raymarch).ray_start_cs; + let near_edge = select(vec3(-1.0, -1.0, 0.0), vec3(1.0, 1.0, 1.0), delta_cs < vec3(0.0)); + let dist_to_near_edge = (near_edge - (*raymarch).ray_start_cs) / delta_cs; + let max_dist_to_near_edge = max(dist_to_near_edge.x, dist_to_near_edge.y); + (*raymarch).ray_start_cs += delta_cs * max(0.0, max_dist_to_near_edge); + + // Clip ray end to the view frustum + + delta_cs = end_cs.xyz - (*raymarch).ray_start_cs; + let far_edge = select(vec3(-1.0, -1.0, 0.0), vec3(1.0, 1.0, 1.0), delta_cs >= vec3(0.0)); + let dist_to_far_edge = (far_edge - (*raymarch).ray_start_cs) / delta_cs; + let min_dist_to_far_edge = min( + min(dist_to_far_edge.x, dist_to_far_edge.y), + dist_to_far_edge.z + ); + + if (infinite) { + delta_cs *= min_dist_to_far_edge; + } else { + // If unbounded, would make the ray reach the end of the frustum + delta_cs *= min(1.0, min_dist_to_far_edge); + } + + (*raymarch).ray_end_cs = (*raymarch).ray_start_cs + delta_cs; +} + +/// March from a clip-space position (w = 1) +fn depth_ray_march_from_cs(raymarch: ptr, v: vec3) { + (*raymarch).ray_start_cs = v; +} + +/// March to a clip-space position (w = 1) +/// +/// Must be called after `from_cs`, as it will clip the world-space ray to the view frustum. +fn depth_ray_march_to_cs(raymarch: ptr, end_cs: vec3) { + let dir = vec4(end_cs - (*raymarch).ray_start_cs, 0.0) * sign(end_cs.z); + depth_ray_march_to_cs_dir_impl(raymarch, dir, false); +} + +/// March towards a clip-space direction. Infinite (ray is extended to cover the whole view frustum). +/// +/// Must be called after `from_cs`, as it will clip the world-space ray to the view frustum. +fn depth_ray_march_to_cs_dir(raymarch: ptr, dir: vec4) { + depth_ray_march_to_cs_dir_impl(raymarch, dir, true); +} + +/// March to a world-space position. +/// +/// Must be called after `from_cs`, as it will clip the world-space ray to the view frustum. +fn depth_ray_march_to_ws(raymarch: ptr, end: vec3) { + depth_ray_march_to_cs(raymarch, position_world_to_ndc(end)); +} + +/// March towards a world-space direction. Infinite (ray is extended to cover the whole view frustum). +/// +/// Must be called after `from_cs`, as it will clip the world-space ray to the view frustum. +fn depth_ray_march_to_ws_dir(raymarch: ptr, dir: vec3) { + depth_ray_march_to_cs_dir_impl(raymarch, direction_world_to_clip(dir), true); +} + +/// Perform the ray march. +fn depth_ray_march_march(raymarch: ptr) -> DepthRayMarchResult { + var res = DepthRayMarchResult(false, 0.0, vec2(0.0), 0.0, 0.0); + + let ray_start_uv = ndc_to_uv((*raymarch).ray_start_cs.xy); + let ray_end_uv = ndc_to_uv((*raymarch).ray_end_cs.xy); + + let ray_uv_delta = ray_end_uv - ray_start_uv; + let ray_len_px = ray_uv_delta * (*raymarch).depth_tex_size; + + let min_px_per_step = 1u; + let step_count = max( + 2, + min(i32((*raymarch).linear_steps), i32(floor(length(ray_len_px) / f32(min_px_per_step)))) + ); + + let linear_z_to_scaled_linear_z = 1.0 / perspective_camera_near(); + let depth_thickness = (*raymarch).depth_thickness_linear_z * linear_z_to_scaled_linear_z; + + var distance_fn: DepthRaymarchDistanceFn; + distance_fn.depth_tex_size = (*raymarch).depth_tex_size; + distance_fn.march_behind_surfaces = (*raymarch).march_behind_surfaces; + distance_fn.depth_thickness = depth_thickness; + distance_fn.use_sloppy_march = (*raymarch).use_sloppy_march; + + var hit: DistanceWithPenetration; + + var hit_t = 0.0; + var miss_t = 0.0; + var root_finder = hybrid_root_finder_new_with_linear_steps(u32(step_count)); + root_finder.bisection_steps = (*raymarch).bisection_steps; + root_finder.use_secant = (*raymarch).use_secant; + root_finder.linear_march_exponent = (*raymarch).linear_march_exponent; + root_finder.jitter = (*raymarch).jitter; + let intersected = hybrid_root_finder_find_root( + &root_finder, + (*raymarch).ray_start_cs, + (*raymarch).ray_end_cs, + &distance_fn, + &hit_t, + &miss_t, + &hit + ); + + res.hit_t = hit_t; + + if (intersected && hit.penetration < depth_thickness && hit.distance < depth_thickness) { + res.hit = true; + res.hit_uv = mix(ray_start_uv, ray_end_uv, res.hit_t); + res.hit_penetration = hit.penetration / linear_z_to_scaled_linear_z; + res.hit_penetration_frac = hit.penetration / depth_thickness; + return res; + } + + res.hit_t = miss_t; + res.hit_uv = mix(ray_start_uv, ray_end_uv, res.hit_t); + + return res; +} diff --git a/crates/libmarathon/src/render/pbr/ssr/ssr.wgsl b/crates/libmarathon/src/render/pbr/ssr/ssr.wgsl new file mode 100644 index 0000000..d646ac6 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/ssr/ssr.wgsl @@ -0,0 +1,194 @@ +// A postprocessing pass that performs screen-space reflections. + +#define_import_path bevy_pbr::ssr + +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_pbr::{ + clustered_forward, + lighting, + lighting::{LAYER_BASE, LAYER_CLEARCOAT}, + mesh_view_bindings::{view, depth_prepass_texture, deferred_prepass_texture, ssr_settings}, + pbr_deferred_functions::pbr_input_from_deferred_gbuffer, + pbr_deferred_types, + pbr_functions, + prepass_utils, + raymarch::{ + depth_ray_march_from_cs, + depth_ray_march_march, + depth_ray_march_new_from_depth, + depth_ray_march_to_ws_dir, + }, + utils, + view_transformations::{ + depth_ndc_to_view_z, + frag_coord_to_ndc, + ndc_to_frag_coord, + ndc_to_uv, + position_view_to_ndc, + position_world_to_ndc, + position_world_to_view, + }, +} +#import bevy_render::view::View + +#ifdef ENVIRONMENT_MAP +#import bevy_pbr::environment_map +#endif + +// The texture representing the color framebuffer. +@group(2) @binding(0) var color_texture: texture_2d; + +// The sampler that lets us sample from the color framebuffer. +@group(2) @binding(1) var color_sampler: sampler; + +// Group 1, bindings 2 and 3 are in `raymarch.wgsl`. + +// Returns the reflected color in the RGB channel and the specular occlusion in +// the alpha channel. +// +// The general approach here is similar to [1]. We first project the reflection +// ray into screen space. Then we perform uniform steps along that screen-space +// reflected ray, converting each step to view space. +// +// The arguments are: +// +// * `R_world`: The reflection vector in world space. +// +// * `P_world`: The current position in world space. +// +// [1]: https://lettier.github.io/3d-game-shaders-for-beginners/screen-space-reflection.html +fn evaluate_ssr(R_world: vec3, P_world: vec3) -> vec4 { + let depth_size = vec2(textureDimensions(depth_prepass_texture)); + + var raymarch = depth_ray_march_new_from_depth(depth_size); + depth_ray_march_from_cs(&raymarch, position_world_to_ndc(P_world)); + depth_ray_march_to_ws_dir(&raymarch, normalize(R_world)); + raymarch.linear_steps = ssr_settings.linear_steps; + raymarch.bisection_steps = ssr_settings.bisection_steps; + raymarch.use_secant = ssr_settings.use_secant != 0u; + raymarch.depth_thickness_linear_z = ssr_settings.thickness; + raymarch.jitter = 1.0; // Disable jitter for now. + raymarch.march_behind_surfaces = false; + + let raymarch_result = depth_ray_march_march(&raymarch); + if (raymarch_result.hit) { + return vec4( + textureSampleLevel(color_texture, color_sampler, raymarch_result.hit_uv, 0.0).rgb, + 0.0 + ); + } + + return vec4(0.0, 0.0, 0.0, 1.0); +} + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + // Sample the depth. + var frag_coord = in.position; + frag_coord.z = prepass_utils::prepass_depth(in.position, 0u); + + // Load the G-buffer data. + let fragment = textureLoad(color_texture, vec2(frag_coord.xy), 0); + let gbuffer = textureLoad(deferred_prepass_texture, vec2(frag_coord.xy), 0); + let pbr_input = pbr_input_from_deferred_gbuffer(frag_coord, gbuffer); + + // Don't do anything if the surface is too rough, since we can't blur or do + // temporal accumulation yet. + let perceptual_roughness = pbr_input.material.perceptual_roughness; + if (perceptual_roughness > ssr_settings.perceptual_roughness_threshold) { + return fragment; + } + + // Unpack the PBR input. + var specular_occlusion = pbr_input.specular_occlusion; + let world_position = pbr_input.world_position.xyz; + let N = pbr_input.N; + let V = pbr_input.V; + + // Calculate the reflection vector. + let R = reflect(-V, N); + + // Do the raymarching. + let ssr_specular = evaluate_ssr(R, world_position); + var indirect_light = ssr_specular.rgb; + specular_occlusion *= ssr_specular.a; + + // Sample the environment map if necessary. + // + // This will take the specular part of the environment map into account if + // the ray missed. Otherwise, it only takes the diffuse part. + // + // TODO: Merge this with the duplicated code in `apply_pbr_lighting`. +#ifdef ENVIRONMENT_MAP + // Unpack values required for environment mapping. + let base_color = pbr_input.material.base_color.rgb; + let metallic = pbr_input.material.metallic; + let reflectance = pbr_input.material.reflectance; + let specular_transmission = pbr_input.material.specular_transmission; + let diffuse_transmission = pbr_input.material.diffuse_transmission; + let diffuse_occlusion = pbr_input.diffuse_occlusion; + +#ifdef STANDARD_MATERIAL_CLEARCOAT + // Do the above calculations again for the clearcoat layer. Remember that + // the clearcoat can have its own roughness and its own normal. + let clearcoat = pbr_input.material.clearcoat; + let clearcoat_perceptual_roughness = pbr_input.material.clearcoat_perceptual_roughness; + let clearcoat_roughness = lighting::perceptualRoughnessToRoughness(clearcoat_perceptual_roughness); + let clearcoat_N = pbr_input.clearcoat_N; + let clearcoat_NdotV = max(dot(clearcoat_N, pbr_input.V), 0.0001); + let clearcoat_R = reflect(-pbr_input.V, clearcoat_N); +#endif // STANDARD_MATERIAL_CLEARCOAT + + // Calculate various other values needed for environment mapping. + let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); + let diffuse_color = pbr_functions::calculate_diffuse_color( + base_color, + metallic, + specular_transmission, + diffuse_transmission + ); + let NdotV = max(dot(N, V), 0.0001); + let F_ab = lighting::F_AB(perceptual_roughness, NdotV); + let F0 = pbr_functions::calculate_F0(base_color, metallic, reflectance); + + // Pack all the values into a structure. + var lighting_input: lighting::LightingInput; + lighting_input.layers[LAYER_BASE].NdotV = NdotV; + lighting_input.layers[LAYER_BASE].N = N; + lighting_input.layers[LAYER_BASE].R = R; + lighting_input.layers[LAYER_BASE].perceptual_roughness = perceptual_roughness; + lighting_input.layers[LAYER_BASE].roughness = roughness; + lighting_input.P = world_position.xyz; + lighting_input.V = V; + lighting_input.diffuse_color = diffuse_color; + lighting_input.F0_ = F0; + lighting_input.F_ab = F_ab; +#ifdef STANDARD_MATERIAL_CLEARCOAT + lighting_input.layers[LAYER_CLEARCOAT].NdotV = clearcoat_NdotV; + lighting_input.layers[LAYER_CLEARCOAT].N = clearcoat_N; + lighting_input.layers[LAYER_CLEARCOAT].R = clearcoat_R; + lighting_input.layers[LAYER_CLEARCOAT].perceptual_roughness = clearcoat_perceptual_roughness; + lighting_input.layers[LAYER_CLEARCOAT].roughness = clearcoat_roughness; + lighting_input.clearcoat_strength = clearcoat; +#endif // STANDARD_MATERIAL_CLEARCOAT + + // Determine which cluster we're in. We'll need this to find the right + // reflection probe. + let cluster_index = clustered_forward::fragment_cluster_index( + frag_coord.xy, frag_coord.z, false); + var clusterable_object_index_ranges = + clustered_forward::unpack_clusterable_object_index_ranges(cluster_index); + + // Sample the environment map. + let environment_light = environment_map::environment_map_light( + &lighting_input, &clusterable_object_index_ranges, false); + + // Accumulate the environment map light. + indirect_light += view.exposure * + (environment_light.diffuse * diffuse_occlusion + + environment_light.specular * specular_occlusion); +#endif + + // Write the results. + return vec4(fragment.rgb + indirect_light, 1.0); +} diff --git a/crates/libmarathon/src/render/pbr/volumetric_fog/mod.rs b/crates/libmarathon/src/render/pbr/volumetric_fog/mod.rs new file mode 100644 index 0000000..160dc14 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/volumetric_fog/mod.rs @@ -0,0 +1,114 @@ +//! Volumetric fog and volumetric lighting, also known as light shafts or god +//! rays. +//! +//! This module implements a more physically-accurate, but slower, form of fog +//! than the [`crate::fog`] module does. Notably, this *volumetric fog* allows +//! for light beams from directional lights to shine through, creating what is +//! known as *light shafts* or *god rays*. +//! +//! To add volumetric fog to a scene, add [`bevy_light::VolumetricFog`] to the +//! camera, and add [`bevy_light::VolumetricLight`] to directional lights that you wish to +//! be volumetric. [`bevy_light::VolumetricFog`] feature numerous settings that +//! allow you to define the accuracy of the simulation, as well as the look of +//! the fog. Currently, only interaction with directional lights that have +//! shadow maps is supported. Note that the overhead of the effect scales +//! directly with the number of directional lights in use, so apply +//! [`bevy_light::VolumetricLight`] sparingly for the best results. +//! +//! The overall algorithm, which is implemented as a postprocessing effect, is a +//! combination of the techniques described in [Scratchapixel] and [this blog +//! post]. It uses raymarching in screen space, transformed into shadow map +//! space for sampling and combined with physically-based modeling of absorption +//! and scattering. Bevy employs the widely-used [Henyey-Greenstein phase +//! function] to model asymmetry; this essentially allows light shafts to fade +//! into and out of existence as the user views them. +//! +//! [Scratchapixel]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html +//! +//! [this blog post]: https://www.alexandre-pestana.com/volumetric-lights/ +//! +//! [Henyey-Greenstein phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction + +use bevy_app::{App, Plugin}; +use bevy_asset::{embedded_asset, Assets, Handle}; +use crate::render::core_3d::{ + graph::{Core3d, Node3d}, + prepare_core_3d_depth_textures, +}; +use bevy_ecs::{resource::Resource, schedule::IntoScheduleConfigs as _}; +use bevy_light::FogVolume; +use bevy_math::{ + primitives::{Cuboid, Plane3d}, + Vec2, Vec3, +}; +use bevy_mesh::{Mesh, Meshable}; +use crate::render::{ + render_graph::{RenderGraphExt, ViewNodeRunner}, + render_resource::SpecializedRenderPipelines, + sync_component::SyncComponentPlugin, + ExtractSchedule, Render, RenderApp, RenderStartup, RenderSystems, +}; +use render::{VolumetricFogNode, VolumetricFogPipeline, VolumetricFogUniformBuffer}; + +use crate::render::pbr::{graph::NodePbr, volumetric_fog::render::init_volumetric_fog_pipeline}; + +pub mod render; + +/// A plugin that implements volumetric fog. +pub struct VolumetricFogPlugin; + +#[derive(Resource)] +pub struct FogAssets { + plane_mesh: Handle, + cube_mesh: Handle, +} + +impl Plugin for VolumetricFogPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "volumetric_fog.wgsl"); + + let mut meshes = app.world_mut().resource_mut::>(); + let plane_mesh = meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE).mesh()); + let cube_mesh = meshes.add(Cuboid::new(1.0, 1.0, 1.0).mesh()); + + app.add_plugins(SyncComponentPlugin::::default()); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .insert_resource(FogAssets { + plane_mesh, + cube_mesh, + }) + .init_resource::>() + .init_resource::() + .add_systems(RenderStartup, init_volumetric_fog_pipeline) + .add_systems(ExtractSchedule, render::extract_volumetric_fog) + .add_systems( + Render, + ( + render::prepare_volumetric_fog_pipelines.in_set(RenderSystems::Prepare), + render::prepare_volumetric_fog_uniforms.in_set(RenderSystems::Prepare), + render::prepare_view_depth_textures_for_volumetric_fog + .in_set(RenderSystems::Prepare) + .before(prepare_core_3d_depth_textures), + ), + ) + .add_render_graph_node::>( + Core3d, + NodePbr::VolumetricFog, + ) + .add_render_graph_edges( + Core3d, + // Volumetric fog should run after the main pass but before bloom, so + // we order if at the start of post processing. + ( + Node3d::EndMainPass, + NodePbr::VolumetricFog, + Node3d::StartMainPassPostProcessing, + ), + ); + } +} diff --git a/crates/libmarathon/src/render/pbr/volumetric_fog/render.rs b/crates/libmarathon/src/render/pbr/volumetric_fog/render.rs new file mode 100644 index 0000000..6e9955d --- /dev/null +++ b/crates/libmarathon/src/render/pbr/volumetric_fog/render.rs @@ -0,0 +1,882 @@ +//! Rendering of fog volumes. + +use core::array; + +use bevy_asset::{load_embedded_asset, AssetId, AssetServer, Handle}; +use bevy_camera::Camera3d; +use bevy_color::ColorToComponents as _; +use crate::render::prepass::{ + DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{Has, QueryItem, With}, + resource::Resource, + system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut}, + world::World, +}; +use bevy_image::{BevyDefault, Image}; +use bevy_light::{FogVolume, VolumetricFog, VolumetricLight}; +use bevy_math::{vec4, Affine3A, Mat4, Vec3, Vec3A, Vec4}; +use bevy_mesh::{Mesh, MeshVertexBufferLayoutRef}; +use crate::render::{ + diagnostic::RecordDiagnostics, + mesh::{allocator::MeshAllocator, RenderMesh, RenderMeshBufferInfo}, + render_asset::RenderAssets, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + binding_types::{ + sampler, texture_3d, texture_depth_2d, texture_depth_2d_multisampled, uniform_buffer, + }, + BindGroupLayout, BindGroupLayoutEntries, BindingResource, BlendComponent, BlendFactor, + BlendOperation, BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites, + DynamicBindGroupEntries, DynamicUniformBuffer, Face, FragmentState, LoadOp, Operations, + PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor, + RenderPipelineDescriptor, SamplerBindingType, ShaderStages, ShaderType, + SpecializedRenderPipeline, SpecializedRenderPipelines, StoreOp, TextureFormat, + TextureSampleType, TextureUsages, VertexState, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + sync_world::RenderEntity, + texture::GpuImage, + view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniformOffset}, + Extract, +}; +use bevy_shader::Shader; +use bevy_transform::components::GlobalTransform; +use bevy_utils::prelude::default; +use bitflags::bitflags; + +use crate::render::pbr::{ + MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup, + ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, ViewLightProbesUniformOffset, + ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset, +}; + +use super::FogAssets; + +bitflags! { + /// Flags that describe the bind group layout used to render volumetric fog. + #[derive(Clone, Copy, PartialEq)] + struct VolumetricFogBindGroupLayoutKey: u8 { + /// The framebuffer is multisampled. + const MULTISAMPLED = 0x1; + /// The volumetric fog has a 3D voxel density texture. + const DENSITY_TEXTURE = 0x2; + } +} + +bitflags! { + /// Flags that describe the rasterization pipeline used to render volumetric + /// fog. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + struct VolumetricFogPipelineKeyFlags: u8 { + /// The view's color format has high dynamic range. + const HDR = 0x1; + /// The volumetric fog has a 3D voxel density texture. + const DENSITY_TEXTURE = 0x2; + } +} + +/// The total number of bind group layouts. +/// +/// This is the total number of combinations of all +/// [`VolumetricFogBindGroupLayoutKey`] flags. +const VOLUMETRIC_FOG_BIND_GROUP_LAYOUT_COUNT: usize = + VolumetricFogBindGroupLayoutKey::all().bits() as usize + 1; + +/// A matrix that converts from local 1×1×1 space to UVW 3D density texture +/// space. +static UVW_FROM_LOCAL: Mat4 = Mat4::from_cols( + vec4(1.0, 0.0, 0.0, 0.0), + vec4(0.0, 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(0.5, 0.5, 0.5, 1.0), +); + +/// The GPU pipeline for the volumetric fog postprocessing effect. +#[derive(Resource)] +pub struct VolumetricFogPipeline { + /// A reference to the shared set of mesh pipeline view layouts. + mesh_view_layouts: MeshPipelineViewLayouts, + + /// All bind group layouts. + /// + /// Since there aren't too many of these, we precompile them all. + volumetric_view_bind_group_layouts: [BindGroupLayout; VOLUMETRIC_FOG_BIND_GROUP_LAYOUT_COUNT], + + // The shader asset handle. + shader: Handle, +} + +/// The two render pipelines that we use for fog volumes: one for when a 3D +/// density texture is present and one for when it isn't. +#[derive(Component)] +pub struct ViewVolumetricFogPipelines { + /// The render pipeline that we use when no density texture is present, and + /// the density distribution is uniform. + pub textureless: CachedRenderPipelineId, + /// The render pipeline that we use when a density texture is present. + pub textured: CachedRenderPipelineId, +} + +/// The node in the render graph, part of the postprocessing stack, that +/// implements volumetric fog. +#[derive(Default)] +pub struct VolumetricFogNode; + +/// Identifies a single specialization of the volumetric fog shader. +#[derive(PartialEq, Eq, Hash, Clone)] +pub struct VolumetricFogPipelineKey { + /// The layout of the view, which is needed for the raymarching. + mesh_pipeline_view_key: MeshPipelineViewLayoutKey, + + /// The vertex buffer layout of the primitive. + /// + /// Both planes (used when the camera is inside the fog volume) and cubes + /// (used when the camera is outside the fog volume) use identical vertex + /// buffer layouts, so we only need one of them. + vertex_buffer_layout: MeshVertexBufferLayoutRef, + + /// Flags that specify features on the pipeline key. + flags: VolumetricFogPipelineKeyFlags, +} + +/// The same as [`VolumetricFog`] and [`FogVolume`], but formatted for +/// the GPU. +/// +/// See the documentation of those structures for more information on these +/// fields. +#[derive(ShaderType)] +pub struct VolumetricFogUniform { + clip_from_local: Mat4, + + /// The transform from world space to 3D density texture UVW space. + uvw_from_world: Mat4, + + /// View-space plane equations of the far faces of the fog volume cuboid. + /// + /// The vector takes the form V = (N, -N⋅Q), where N is the normal of the + /// plane and Q is any point in it, in view space. The equation of the plane + /// for homogeneous point P = (Px, Py, Pz, Pw) is V⋅P = 0. + far_planes: [Vec4; 3], + + fog_color: Vec3, + light_tint: Vec3, + ambient_color: Vec3, + ambient_intensity: f32, + step_count: u32, + + /// The radius of a sphere that bounds the fog volume in view space. + bounding_radius: f32, + + absorption: f32, + scattering: f32, + density: f32, + density_texture_offset: Vec3, + scattering_asymmetry: f32, + light_intensity: f32, + jitter_strength: f32, +} + +/// Specifies the offset within the [`VolumetricFogUniformBuffer`] of the +/// [`VolumetricFogUniform`] for a specific view. +#[derive(Component, Deref, DerefMut)] +pub struct ViewVolumetricFog(Vec); + +/// Information that the render world needs to maintain about each fog volume. +pub struct ViewFogVolume { + /// The 3D voxel density texture for this volume, if present. + density_texture: Option>, + /// The offset of this view's [`VolumetricFogUniform`] structure within the + /// [`VolumetricFogUniformBuffer`]. + uniform_buffer_offset: u32, + /// True if the camera is outside the fog volume; false if it's inside the + /// fog volume. + exterior: bool, +} + +/// The GPU buffer that stores the [`VolumetricFogUniform`] data. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct VolumetricFogUniformBuffer(pub DynamicUniformBuffer); + +pub fn init_volumetric_fog_pipeline( + mut commands: Commands, + render_device: Res, + mesh_view_layouts: Res, + asset_server: Res, +) { + // Create the bind group layout entries common to all bind group + // layouts. + let base_bind_group_layout_entries = &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + // `volumetric_fog` + uniform_buffer::(true), + ); + + // For every combination of `VolumetricFogBindGroupLayoutKey` bits, + // create a bind group layout. + let bind_group_layouts = array::from_fn(|bits| { + let flags = VolumetricFogBindGroupLayoutKey::from_bits_retain(bits as u8); + + let mut bind_group_layout_entries = base_bind_group_layout_entries.to_vec(); + + // `depth_texture` + bind_group_layout_entries.extend_from_slice(&BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + (( + 1, + if flags.contains(VolumetricFogBindGroupLayoutKey::MULTISAMPLED) { + texture_depth_2d_multisampled() + } else { + texture_depth_2d() + }, + ),), + )); + + // `density_texture` and `density_sampler` + if flags.contains(VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE) { + bind_group_layout_entries.extend_from_slice(&BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + ( + (2, texture_3d(TextureSampleType::Float { filterable: true })), + (3, sampler(SamplerBindingType::Filtering)), + ), + )); + } + + // Create the bind group layout. + let description = flags.bind_group_layout_description(); + render_device.create_bind_group_layout(&*description, &bind_group_layout_entries) + }); + + commands.insert_resource(VolumetricFogPipeline { + mesh_view_layouts: mesh_view_layouts.clone(), + volumetric_view_bind_group_layouts: bind_group_layouts, + shader: load_embedded_asset!(asset_server.as_ref(), "volumetric_fog.wgsl"), + }); +} + +/// Extracts [`VolumetricFog`], [`FogVolume`], and [`VolumetricLight`]s +/// from the main world to the render world. +pub fn extract_volumetric_fog( + mut commands: Commands, + view_targets: Extract>, + fog_volumes: Extract>, + volumetric_lights: Extract>, +) { + if volumetric_lights.is_empty() { + // TODO: needs better way to handle clean up in render world + for (entity, ..) in view_targets.iter() { + commands + .entity(entity) + .remove::<(VolumetricFog, ViewVolumetricFogPipelines, ViewVolumetricFog)>(); + } + for (entity, ..) in fog_volumes.iter() { + commands.entity(entity).remove::(); + } + return; + } + + for (entity, volumetric_fog) in view_targets.iter() { + commands + .get_entity(entity) + .expect("Volumetric fog entity wasn't synced.") + .insert(*volumetric_fog); + } + + for (entity, fog_volume, fog_transform) in fog_volumes.iter() { + commands + .get_entity(entity) + .expect("Fog volume entity wasn't synced.") + .insert((*fog_volume).clone()) + .insert(*fog_transform); + } + + for (entity, volumetric_light) in volumetric_lights.iter() { + commands + .get_entity(entity) + .expect("Volumetric light entity wasn't synced.") + .insert(*volumetric_light); + } +} + +impl ViewNode for VolumetricFogNode { + type ViewQuery = ( + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + ); + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + ( + view_target, + view_depth_texture, + view_volumetric_lighting_pipelines, + view_uniform_offset, + view_lights_offset, + view_fog_offset, + view_light_probes_offset, + view_fog_volumes, + view_bind_group, + view_ssr_offset, + msaa, + view_environment_map_offset, + ): QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let volumetric_lighting_pipeline = world.resource::(); + let volumetric_lighting_uniform_buffers = world.resource::(); + let image_assets = world.resource::>(); + let mesh_allocator = world.resource::(); + + // Fetch the uniform buffer and binding. + let ( + Some(textureless_pipeline), + Some(textured_pipeline), + Some(volumetric_lighting_uniform_buffer_binding), + ) = ( + pipeline_cache.get_render_pipeline(view_volumetric_lighting_pipelines.textureless), + pipeline_cache.get_render_pipeline(view_volumetric_lighting_pipelines.textured), + volumetric_lighting_uniform_buffers.binding(), + ) + else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + render_context + .command_encoder() + .push_debug_group("volumetric_lighting"); + let time_span = + diagnostics.time_span(render_context.command_encoder(), "volumetric_lighting"); + + let fog_assets = world.resource::(); + let render_meshes = world.resource::>(); + + for view_fog_volume in view_fog_volumes.iter() { + // If the camera is outside the fog volume, pick the cube mesh; + // otherwise, pick the plane mesh. In the latter case we'll be + // effectively rendering a full-screen quad. + let mesh_handle = if view_fog_volume.exterior { + fog_assets.cube_mesh.clone() + } else { + fog_assets.plane_mesh.clone() + }; + + let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&mesh_handle.id()) + else { + continue; + }; + + let density_image = view_fog_volume + .density_texture + .and_then(|density_texture| image_assets.get(density_texture)); + + // Pick the right pipeline, depending on whether a density texture + // is present or not. + let pipeline = if density_image.is_some() { + textured_pipeline + } else { + textureless_pipeline + }; + + // This should always succeed, but if the asset was unloaded don't + // panic. + let Some(render_mesh) = render_meshes.get(&mesh_handle) else { + return Ok(()); + }; + + // Create the bind group for the view. + // + // TODO: Cache this. + + let mut bind_group_layout_key = VolumetricFogBindGroupLayoutKey::empty(); + bind_group_layout_key.set( + VolumetricFogBindGroupLayoutKey::MULTISAMPLED, + !matches!(*msaa, Msaa::Off), + ); + + // Create the bind group entries. The ones relating to the density + // texture will only be filled in if that texture is present. + let mut bind_group_entries = DynamicBindGroupEntries::sequential(( + volumetric_lighting_uniform_buffer_binding.clone(), + BindingResource::TextureView(view_depth_texture.view()), + )); + if let Some(density_image) = density_image { + bind_group_layout_key.insert(VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE); + bind_group_entries = bind_group_entries.extend_sequential(( + BindingResource::TextureView(&density_image.texture_view), + BindingResource::Sampler(&density_image.sampler), + )); + } + + let volumetric_view_bind_group_layout = &volumetric_lighting_pipeline + .volumetric_view_bind_group_layouts[bind_group_layout_key.bits() as usize]; + + let volumetric_view_bind_group = render_context.render_device().create_bind_group( + None, + volumetric_view_bind_group_layout, + &bind_group_entries, + ); + + let render_pass_descriptor = RenderPassDescriptor { + label: Some("volumetric lighting pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: view_target.main_texture_view(), + depth_slice: None, + resolve_target: None, + ops: Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&render_pass_descriptor); + + render_pass.set_vertex_buffer(0, *vertex_buffer_slice.buffer.slice(..)); + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group( + 0, + &view_bind_group.main, + &[ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + **view_light_probes_offset, + **view_ssr_offset, + **view_environment_map_offset, + ], + ); + render_pass.set_bind_group( + 1, + &volumetric_view_bind_group, + &[view_fog_volume.uniform_buffer_offset], + ); + + // Draw elements or arrays, as appropriate. + match &render_mesh.buffer_info { + RenderMeshBufferInfo::Indexed { + index_format, + count, + } => { + let Some(index_buffer_slice) = + mesh_allocator.mesh_index_slice(&mesh_handle.id()) + else { + continue; + }; + + render_pass + .set_index_buffer(*index_buffer_slice.buffer.slice(..), *index_format); + render_pass.draw_indexed( + index_buffer_slice.range.start..(index_buffer_slice.range.start + count), + vertex_buffer_slice.range.start as i32, + 0..1, + ); + } + RenderMeshBufferInfo::NonIndexed => { + render_pass.draw(vertex_buffer_slice.range, 0..1); + } + } + } + + time_span.end(render_context.command_encoder()); + render_context.command_encoder().pop_debug_group(); + + Ok(()) + } +} + +impl SpecializedRenderPipeline for VolumetricFogPipeline { + type Key = VolumetricFogPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + // We always use hardware 2x2 filtering for sampling the shadow map; the + // more accurate versions with percentage-closer filtering aren't worth + // the overhead. + let mut shader_defs = vec!["SHADOW_FILTER_METHOD_HARDWARE_2X2".into()]; + + // We need a separate layout for MSAA and non-MSAA, as well as one for + // the presence or absence of the density texture. + let mut bind_group_layout_key = VolumetricFogBindGroupLayoutKey::empty(); + bind_group_layout_key.set( + VolumetricFogBindGroupLayoutKey::MULTISAMPLED, + key.mesh_pipeline_view_key + .contains(MeshPipelineViewLayoutKey::MULTISAMPLED), + ); + bind_group_layout_key.set( + VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE, + key.flags + .contains(VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE), + ); + + let volumetric_view_bind_group_layout = + self.volumetric_view_bind_group_layouts[bind_group_layout_key.bits() as usize].clone(); + + // Both the cube and plane have the same vertex layout, so we don't need + // to distinguish between the two. + let vertex_format = key + .vertex_buffer_layout + .0 + .get_layout(&[Mesh::ATTRIBUTE_POSITION.at_shader_location(0)]) + .expect("Failed to get vertex layout for volumetric fog hull"); + + if key + .mesh_pipeline_view_key + .contains(MeshPipelineViewLayoutKey::MULTISAMPLED) + { + shader_defs.push("MULTISAMPLED".into()); + } + + if key + .flags + .contains(VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE) + { + shader_defs.push("DENSITY_TEXTURE".into()); + } + + let layout = self + .mesh_view_layouts + .get_view_layout(key.mesh_pipeline_view_key); + let layout = vec![ + layout.main_layout.clone(), + volumetric_view_bind_group_layout.clone(), + ]; + + RenderPipelineDescriptor { + label: Some("volumetric lighting pipeline".into()), + layout, + vertex: VertexState { + shader: self.shader.clone(), + shader_defs: shader_defs.clone(), + buffers: vec![vertex_format], + ..default() + }, + primitive: PrimitiveState { + cull_mode: Some(Face::Back), + ..default() + }, + fragment: Some(FragmentState { + shader: self.shader.clone(), + shader_defs, + targets: vec![Some(ColorTargetState { + format: if key.flags.contains(VolumetricFogPipelineKeyFlags::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + // Blend on top of what's already in the framebuffer. Doing + // the alpha blending with the hardware blender allows us to + // avoid having to use intermediate render targets. + blend: Some(BlendState { + color: BlendComponent { + src_factor: BlendFactor::One, + dst_factor: BlendFactor::OneMinusSrcAlpha, + operation: BlendOperation::Add, + }, + alpha: BlendComponent { + src_factor: BlendFactor::Zero, + dst_factor: BlendFactor::One, + operation: BlendOperation::Add, + }, + }), + write_mask: ColorWrites::ALL, + })], + ..default() + }), + ..default() + } + } +} + +/// Specializes volumetric fog pipelines for all views with that effect enabled. +pub fn prepare_volumetric_fog_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + volumetric_lighting_pipeline: Res, + fog_assets: Res, + view_targets: Query< + ( + Entity, + &ExtractedView, + &Msaa, + Has, + Has, + Has, + Has, + ), + With, + >, + meshes: Res>, +) { + let Some(plane_mesh) = meshes.get(&fog_assets.plane_mesh) else { + // There's an off chance that the mesh won't be prepared yet if `RenderAssetBytesPerFrame` limiting is in use. + return; + }; + + for ( + entity, + view, + msaa, + normal_prepass, + depth_prepass, + motion_vector_prepass, + deferred_prepass, + ) in view_targets.iter() + { + // Create a mesh pipeline view layout key corresponding to the view. + let mut mesh_pipeline_view_key = MeshPipelineViewLayoutKey::from(*msaa); + mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::NORMAL_PREPASS, normal_prepass); + mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::DEPTH_PREPASS, depth_prepass); + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS, + motion_vector_prepass, + ); + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::DEFERRED_PREPASS, + deferred_prepass, + ); + + let mut textureless_flags = VolumetricFogPipelineKeyFlags::empty(); + textureless_flags.set(VolumetricFogPipelineKeyFlags::HDR, view.hdr); + + // Specialize the pipeline. + let textureless_pipeline_key = VolumetricFogPipelineKey { + mesh_pipeline_view_key, + vertex_buffer_layout: plane_mesh.layout.clone(), + flags: textureless_flags, + }; + let textureless_pipeline_id = pipelines.specialize( + &pipeline_cache, + &volumetric_lighting_pipeline, + textureless_pipeline_key.clone(), + ); + let textured_pipeline_id = pipelines.specialize( + &pipeline_cache, + &volumetric_lighting_pipeline, + VolumetricFogPipelineKey { + flags: textureless_pipeline_key.flags + | VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE, + ..textureless_pipeline_key + }, + ); + + commands.entity(entity).insert(ViewVolumetricFogPipelines { + textureless: textureless_pipeline_id, + textured: textured_pipeline_id, + }); + } +} + +/// A system that converts [`VolumetricFog`] into [`VolumetricFogUniform`]s. +pub fn prepare_volumetric_fog_uniforms( + mut commands: Commands, + mut volumetric_lighting_uniform_buffer: ResMut, + view_targets: Query<(Entity, &ExtractedView, &VolumetricFog)>, + fog_volumes: Query<(Entity, &FogVolume, &GlobalTransform)>, + render_device: Res, + render_queue: Res, + mut local_from_world_matrices: Local>, +) { + // Do this up front to avoid O(n^2) matrix inversion. + local_from_world_matrices.clear(); + for (_, _, fog_transform) in fog_volumes.iter() { + local_from_world_matrices.push(fog_transform.affine().inverse()); + } + + let uniform_count = view_targets.iter().len() * local_from_world_matrices.len(); + + let Some(mut writer) = + volumetric_lighting_uniform_buffer.get_writer(uniform_count, &render_device, &render_queue) + else { + return; + }; + + for (view_entity, extracted_view, volumetric_fog) in view_targets.iter() { + let world_from_view = extracted_view.world_from_view.affine(); + + let mut view_fog_volumes = vec![]; + + for ((_, fog_volume, _), local_from_world) in + fog_volumes.iter().zip(local_from_world_matrices.iter()) + { + // Calculate the transforms to and from 1×1×1 local space. + let local_from_view = *local_from_world * world_from_view; + let view_from_local = local_from_view.inverse(); + + // Determine whether the camera is inside or outside the volume, and + // calculate the clip space transform. + let interior = camera_is_inside_fog_volume(&local_from_view); + let hull_clip_from_local = calculate_fog_volume_clip_from_local_transforms( + interior, + &extracted_view.clip_from_view, + &view_from_local, + ); + + // Calculate the radius of the sphere that bounds the fog volume. + let bounding_radius = view_from_local + .transform_vector3a(Vec3A::splat(0.5)) + .length(); + + // Write out our uniform. + let uniform_buffer_offset = writer.write(&VolumetricFogUniform { + clip_from_local: hull_clip_from_local, + uvw_from_world: UVW_FROM_LOCAL * *local_from_world, + far_planes: get_far_planes(&view_from_local), + fog_color: fog_volume.fog_color.to_linear().to_vec3(), + light_tint: fog_volume.light_tint.to_linear().to_vec3(), + ambient_color: volumetric_fog.ambient_color.to_linear().to_vec3(), + ambient_intensity: volumetric_fog.ambient_intensity, + step_count: volumetric_fog.step_count, + bounding_radius, + absorption: fog_volume.absorption, + scattering: fog_volume.scattering, + density: fog_volume.density_factor, + density_texture_offset: fog_volume.density_texture_offset, + scattering_asymmetry: fog_volume.scattering_asymmetry, + light_intensity: fog_volume.light_intensity, + jitter_strength: volumetric_fog.jitter, + }); + + view_fog_volumes.push(ViewFogVolume { + uniform_buffer_offset, + exterior: !interior, + density_texture: fog_volume.density_texture.as_ref().map(Handle::id), + }); + } + + commands + .entity(view_entity) + .insert(ViewVolumetricFog(view_fog_volumes)); + } +} + +/// A system that marks all view depth textures as readable in shaders. +/// +/// The volumetric lighting pass needs to do this, and it doesn't happen by +/// default. +pub fn prepare_view_depth_textures_for_volumetric_fog( + mut view_targets: Query<&mut Camera3d>, + fog_volumes: Query<&VolumetricFog>, +) { + if fog_volumes.is_empty() { + return; + } + + for mut camera in view_targets.iter_mut() { + camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits(); + } +} + +fn get_far_planes(view_from_local: &Affine3A) -> [Vec4; 3] { + let (mut far_planes, mut next_index) = ([Vec4::ZERO; 3], 0); + + for &local_normal in &[ + Vec3A::X, + Vec3A::NEG_X, + Vec3A::Y, + Vec3A::NEG_Y, + Vec3A::Z, + Vec3A::NEG_Z, + ] { + let view_normal = view_from_local + .transform_vector3a(local_normal) + .normalize_or_zero(); + if view_normal.z <= 0.0 { + continue; + } + + let view_position = view_from_local.transform_point3a(-local_normal * 0.5); + let plane_coords = view_normal.extend(-view_normal.dot(view_position)); + + far_planes[next_index] = plane_coords; + next_index += 1; + if next_index == far_planes.len() { + continue; + } + } + + far_planes +} + +impl VolumetricFogBindGroupLayoutKey { + /// Creates an appropriate debug description for the bind group layout with + /// these flags. + fn bind_group_layout_description(&self) -> String { + if self.is_empty() { + return "volumetric lighting view bind group layout".to_owned(); + } + + format!( + "volumetric lighting view bind group layout ({})", + self.iter() + .filter_map(|flag| { + if flag == VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE { + Some("density texture") + } else if flag == VolumetricFogBindGroupLayoutKey::MULTISAMPLED { + Some("multisampled") + } else { + None + } + }) + .collect::>() + .join(", ") + ) + } +} + +/// Given the transform from the view to the 1×1×1 cube in local fog volume +/// space, returns true if the camera is inside the volume. +fn camera_is_inside_fog_volume(local_from_view: &Affine3A) -> bool { + local_from_view + .translation + .abs() + .cmple(Vec3A::splat(0.5)) + .all() +} + +/// Given the local transforms, returns the matrix that transforms model space +/// to clip space. +fn calculate_fog_volume_clip_from_local_transforms( + interior: bool, + clip_from_view: &Mat4, + view_from_local: &Affine3A, +) -> Mat4 { + if !interior { + return *clip_from_view * Mat4::from(*view_from_local); + } + + // If the camera is inside the fog volume, then we'll be rendering a full + // screen quad. The shader will start its raymarch at the fragment depth + // value, however, so we need to make sure that the depth of the full screen + // quad is at the near clip plane `z_near`. + let z_near = clip_from_view.w_axis[2]; + Mat4::from_cols( + vec4(z_near, 0.0, 0.0, 0.0), + vec4(0.0, z_near, 0.0, 0.0), + vec4(0.0, 0.0, 0.0, 0.0), + vec4(0.0, 0.0, z_near, z_near), + ) +} diff --git a/crates/libmarathon/src/render/pbr/volumetric_fog/volumetric_fog.wgsl b/crates/libmarathon/src/render/pbr/volumetric_fog/volumetric_fog.wgsl new file mode 100644 index 0000000..43e3fc9 --- /dev/null +++ b/crates/libmarathon/src/render/pbr/volumetric_fog/volumetric_fog.wgsl @@ -0,0 +1,486 @@ +// A postprocessing shader that implements volumetric fog via raymarching and +// sampling directional light shadow maps. +// +// The overall approach is a combination of the volumetric rendering in [1] and +// the shadow map raymarching in [2]. First, we raytrace the AABB of the fog +// volume in order to determine how long our ray is. Then we do a raymarch, with +// physically-based calculations at each step to determine how much light was +// absorbed, scattered out, and scattered in. To determine in-scattering, we +// sample the shadow map for the light to determine whether the point was in +// shadow or not. +// +// [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html +// +// [2]: http://www.alexandre-pestana.com/volumetric-lights/ + +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_pbr::mesh_functions::{get_world_from_local, mesh_position_local_to_clip} +#import bevy_pbr::mesh_view_bindings::{globals, lights, view, clusterable_objects} +#import bevy_pbr::mesh_view_types::{ + DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT, + POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT, + POINT_LIGHT_FLAGS_VOLUMETRIC_BIT, + POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE, + ClusterableObject +} +#import bevy_pbr::shadow_sampling::{ + sample_shadow_map_hardware, + sample_shadow_cubemap, + sample_shadow_map, + SPOT_SHADOW_TEXEL_SIZE +} +#import bevy_pbr::shadows::{get_cascade_index, world_to_directional_light_local} +#import bevy_pbr::utils::interleaved_gradient_noise +#import bevy_pbr::view_transformations::{ + depth_ndc_to_view_z, + frag_coord_to_ndc, + position_ndc_to_view, + position_ndc_to_world, + position_view_to_world +} +#import bevy_pbr::clustered_forward as clustering +#import bevy_pbr::lighting::getDistanceAttenuation; + +// The GPU version of [`VolumetricFog`]. See the comments in +// `volumetric_fog/mod.rs` for descriptions of the fields here. +struct VolumetricFog { + clip_from_local: mat4x4, + uvw_from_world: mat4x4, + far_planes: array, 3>, + fog_color: vec3, + light_tint: vec3, + ambient_color: vec3, + ambient_intensity: f32, + step_count: u32, + bounding_radius: f32, + absorption: f32, + scattering: f32, + density_factor: f32, + density_texture_offset: vec3, + scattering_asymmetry: f32, + light_intensity: f32, + jitter_strength: f32, +} + +@group(1) @binding(0) var volumetric_fog: VolumetricFog; + +#ifdef MULTISAMPLED +@group(1) @binding(1) var depth_texture: texture_depth_multisampled_2d; +#else +@group(1) @binding(1) var depth_texture: texture_depth_2d; +#endif + +#ifdef DENSITY_TEXTURE +@group(1) @binding(2) var density_texture: texture_3d; +@group(1) @binding(3) var density_sampler: sampler; +#endif // DENSITY_TEXTURE + +// 1 / (4π) +const FRAC_4_PI: f32 = 0.07957747154594767; + +struct Vertex { + @builtin(instance_index) instance_index: u32, + @location(0) position: vec3, +} + +@vertex +fn vertex(vertex: Vertex) -> @builtin(position) vec4 { + return volumetric_fog.clip_from_local * vec4(vertex.position, 1.0); +} + +// The common Henyey-Greenstein asymmetric phase function [1] [2]. +// +// This determines how much light goes toward the viewer as opposed to away from +// the viewer. From a visual point of view, it controls how the light shafts +// appear and disappear as the camera looks at the light source. +// +// [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/ray-marching-get-it-right.html +// +// [2]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction +fn henyey_greenstein(neg_LdotV: f32) -> f32 { + let g = volumetric_fog.scattering_asymmetry; + let denom = 1.0 + g * g - 2.0 * g * neg_LdotV; + return FRAC_4_PI * (1.0 - g * g) / (denom * sqrt(denom)); +} + +@fragment +fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { + // Unpack the `volumetric_fog` settings. + let uvw_from_world = volumetric_fog.uvw_from_world; + let fog_color = volumetric_fog.fog_color; + let ambient_color = volumetric_fog.ambient_color; + let ambient_intensity = volumetric_fog.ambient_intensity; + let step_count = volumetric_fog.step_count; + let bounding_radius = volumetric_fog.bounding_radius; + let absorption = volumetric_fog.absorption; + let scattering = volumetric_fog.scattering; + let density_factor = volumetric_fog.density_factor; + let density_texture_offset = volumetric_fog.density_texture_offset; + let light_tint = volumetric_fog.light_tint; + let light_intensity = volumetric_fog.light_intensity; + let jitter_strength = volumetric_fog.jitter_strength; + + // Unpack the view. + let exposure = view.exposure; + + // Sample the depth to put an upper bound on the length of the ray (as we + // shouldn't trace through solid objects). If this is multisample, just use + // sample 0; this is approximate but good enough. + let frag_coord = position; + let ndc_end_depth_from_buffer = textureLoad(depth_texture, vec2(frag_coord.xy), 0); + let view_end_depth_from_buffer = -position_ndc_to_view( + frag_coord_to_ndc(vec4(position.xy, ndc_end_depth_from_buffer, 1.0))).z; + + // Calculate the start position of the ray. Since we're only rendering front + // faces of the AABB, this is the current fragment's depth. + let view_start_pos = position_ndc_to_view(frag_coord_to_ndc(frag_coord)); + + // Calculate the end position of the ray. This requires us to raytrace the + // three back faces of the AABB to find the one that our ray intersects. + var end_depth_view = 0.0; + for (var plane_index = 0; plane_index < 3; plane_index += 1) { + let plane = volumetric_fog.far_planes[plane_index]; + let other_plane_a = volumetric_fog.far_planes[(plane_index + 1) % 3]; + let other_plane_b = volumetric_fog.far_planes[(plane_index + 2) % 3]; + + // Calculate the intersection of the ray and the plane. The ray must + // intersect in front of us (t > 0). + let t = -plane.w / dot(plane.xyz, view_start_pos.xyz); + if (t < 0.0) { + continue; + } + let hit_pos = view_start_pos.xyz * t; + + // The intersection point must be in front of the other backfaces. + let other_sides = vec2( + dot(vec4(hit_pos, 1.0), other_plane_a) >= 0.0, + dot(vec4(hit_pos, 1.0), other_plane_b) >= 0.0 + ); + + // If those tests pass, we found our backface. + if (all(other_sides)) { + end_depth_view = -hit_pos.z; + break; + } + } + + // Starting at the end depth, which we got above, figure out how long the + // ray we want to trace is and the length of each increment. + end_depth_view = min(end_depth_view, view_end_depth_from_buffer); + + // We assume world and view have the same scale here. + let start_depth_view = -depth_ndc_to_view_z(frag_coord.z); + let ray_length_view = abs(end_depth_view - start_depth_view); + let inv_step_count = 1.0 / f32(step_count); + let step_size_world = ray_length_view * inv_step_count; + + let directional_light_count = lights.n_directional_lights; + + // Calculate the ray origin (`Ro`) and the ray direction (`Rd`) in NDC, + // view, and world coordinates. + let Rd_ndc = vec3(frag_coord_to_ndc(position).xy, 1.0); + let Rd_view = normalize(position_ndc_to_view(Rd_ndc)); + var Ro_world = position_view_to_world(view_start_pos.xyz); + let Rd_world = normalize(position_ndc_to_world(Rd_ndc) - view.world_position); + + // Offset by jitter. + let jitter = interleaved_gradient_noise(position.xy, globals.frame_count) * jitter_strength; + Ro_world += Rd_world * jitter; + + // Use Beer's law [1] [2] to calculate the maximum amount of light that each + // directional light could contribute, and modulate that value by the light + // tint and fog color. (The actual value will in turn be modulated by the + // phase according to the Henyey-Greenstein formula.) + // + // [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html + // + // [2]: https://en.wikipedia.org/wiki/Beer%E2%80%93Lambert_law + + // Use Beer's law again to accumulate the ambient light all along the path. + var accumulated_color = exp(-ray_length_view * (absorption + scattering)) * ambient_color * + ambient_intensity; + + // This is the amount of the background that shows through. We're actually + // going to recompute this over and over again for each directional light, + // coming up with the same values each time. + var background_alpha = 1.0; + + // If we have a density texture, transform to its local space. +#ifdef DENSITY_TEXTURE + let Ro_uvw = (uvw_from_world * vec4(Ro_world, 1.0)).xyz; + let Rd_step_uvw = mat3x3(uvw_from_world[0].xyz, uvw_from_world[1].xyz, uvw_from_world[2].xyz) * + (Rd_world * step_size_world); +#endif // DENSITY_TEXTURE + + for (var light_index = 0u; light_index < directional_light_count; light_index += 1u) { + // Volumetric lights are all sorted first, so the first time we come to + // a non-volumetric light, we know we've seen them all. + let light = &lights.directional_lights[light_index]; + if (((*light).flags & DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT) == 0) { + break; + } + + // Offset the depth value by the bias. + let depth_offset = (*light).shadow_depth_bias * (*light).direction_to_light.xyz; + + // Compute phase, which determines the fraction of light that's + // scattered toward the camera instead of away from it. + let neg_LdotV = dot(normalize((*light).direction_to_light.xyz), Rd_world); + let phase = henyey_greenstein(neg_LdotV); + + // Reset `background_alpha` for a new raymarch. + background_alpha = 1.0; + + // Start raymarching. + for (var step = 0u; step < step_count; step += 1u) { + // As an optimization, break if we've gotten too dark. + if (background_alpha < 0.001) { + break; + } + + // Calculate where we are in the ray. + let P_world = Ro_world + Rd_world * f32(step) * step_size_world; + let P_view = Rd_view * f32(step) * step_size_world; + + var density = density_factor; +#ifdef DENSITY_TEXTURE + // Take the density texture into account, if there is one. + // + // The uvs should never go outside the (0, 0, 0) to (1, 1, 1) box, + // but sometimes due to floating point error they can. Handle this + // case. + let P_uvw = Ro_uvw + Rd_step_uvw * f32(step); + if (all(P_uvw >= vec3(0.0)) && all(P_uvw <= vec3(1.0))) { + density *= textureSampleLevel(density_texture, density_sampler, P_uvw + density_texture_offset, 0.0).r; + } else { + density = 0.0; + } +#endif // DENSITY_TEXTURE + + // Calculate absorption (amount of light absorbed by the fog) and + // out-scattering (amount of light the fog scattered away). + let sample_attenuation = exp(-step_size_world * density * (absorption + scattering)); + + // Process absorption and out-scattering. + background_alpha *= sample_attenuation; + + // Compute in-scattering (amount of light other fog particles + // scattered into this ray). This is where any directional light is + // scattered in. + + // Prepare to sample the shadow map. + let cascade_index = get_cascade_index(light_index, P_view.z); + let light_local = world_to_directional_light_local( + light_index, + cascade_index, + vec4(P_world + depth_offset, 1.0) + ); + + // If we're outside the shadow map entirely, local light attenuation + // is zero. + var local_light_attenuation = f32(light_local.w != 0.0); + + // Otherwise, sample the shadow map to determine whether, and by how + // much, this sample is in the light. + if (local_light_attenuation != 0.0) { + let cascade = &(*light).cascades[cascade_index]; + let array_index = i32((*light).depth_texture_base_index + cascade_index); + local_light_attenuation = + sample_shadow_map_hardware(light_local.xy, light_local.z, array_index); + } + + if (local_light_attenuation != 0.0) { + let light_attenuation = exp(-density * bounding_radius * (absorption + scattering)); + let light_factors_per_step = fog_color * light_tint * light_attenuation * + scattering * density * step_size_world * light_intensity * exposure; + + // Modulate the factor we calculated above by the phase, fog color, + // light color, light tint. + let light_color_per_step = (*light).color.rgb * phase * light_factors_per_step; + + // Accumulate the light. + accumulated_color += light_color_per_step * local_light_attenuation * + background_alpha; + } + } + } + + // Point lights and Spot lights + let view_z = view_start_pos.z; + let is_orthographic = view.clip_from_view[3].w == 1.0; + let cluster_index = clustering::fragment_cluster_index(frag_coord.xy, view_z, is_orthographic); + var clusterable_object_index_ranges = + clustering::unpack_clusterable_object_index_ranges(cluster_index); + for (var i: u32 = clusterable_object_index_ranges.first_point_light_index_offset; + i < clusterable_object_index_ranges.first_reflection_probe_index_offset; + i = i + 1u) { + let light_id = clustering::get_clusterable_object_id(i); + let light = &clusterable_objects.data[light_id]; + if (((*light).flags & POINT_LIGHT_FLAGS_VOLUMETRIC_BIT) == 0) { + continue; + } + + // Reset `background_alpha` for a new raymarch. + background_alpha = 1.0; + + // Start raymarching. + for (var step = 0u; step < step_count; step += 1u) { + // As an optimization, break if we've gotten too dark. + if (background_alpha < 0.001) { + break; + } + + // Calculate where we are in the ray. + let P_world = Ro_world + Rd_world * f32(step) * step_size_world; + let P_view = Rd_view * f32(step) * step_size_world; + + var density = density_factor; + + let light_to_frag = (*light).position_radius.xyz - P_world; + let V = Rd_world; + let L = normalize(light_to_frag); + let distance_square = dot(light_to_frag, light_to_frag); + let distance_atten = getDistanceAttenuation(distance_square, (*light).color_inverse_square_range.w); + var local_light_attenuation = distance_atten; + if (i < clusterable_object_index_ranges.first_spot_light_index_offset) { + var shadow: f32 = 1.0; + if (((*light).flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = fetch_point_shadow_without_normal(light_id, vec4(P_world, 1.0)); + } + local_light_attenuation *= shadow; + } else { + // spot light attenuation + // reconstruct spot dir from x/z and y-direction flag + var spot_dir = vec3((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y); + spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z)); + if ((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u { + spot_dir.y = -spot_dir.y; + } + let light_to_frag = (*light).position_radius.xyz - P_world; + + // calculate attenuation based on filament formula https://google.github.io/filament/Filament.html#listing_glslpunctuallight + // spot_scale and spot_offset have been precomputed + // note we normalize here to get "l" from the filament listing. spot_dir is already normalized + let cd = dot(-spot_dir, normalize(light_to_frag)); + let attenuation = saturate(cd * (*light).light_custom_data.z + (*light).light_custom_data.w); + let spot_attenuation = attenuation * attenuation; + + var shadow: f32 = 1.0; + if (((*light).flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { + shadow = fetch_spot_shadow_without_normal(light_id, vec4(P_world, 1.0)); + } + local_light_attenuation *= spot_attenuation * shadow; + } + + // Calculate absorption (amount of light absorbed by the fog) and + // out-scattering (amount of light the fog scattered away). + let sample_attenuation = exp(-step_size_world * density * (absorption + scattering)); + + // Process absorption and out-scattering. + background_alpha *= sample_attenuation; + + let light_attenuation = exp(-density * bounding_radius * (absorption + scattering)); + let light_factors_per_step = fog_color * light_tint * light_attenuation * + scattering * density * step_size_world * light_intensity * 0.1; + + // Modulate the factor we calculated above by the phase, fog color, + // light color, light tint. + let light_color_per_step = (*light).color_inverse_square_range.rgb * light_factors_per_step; + + // Accumulate the light. + accumulated_color += light_color_per_step * local_light_attenuation * + background_alpha; + } + } + + // We're done! Return the color with alpha so it can be blended onto the + // render target. + return vec4(accumulated_color, 1.0 - background_alpha); +} + +fn fetch_point_shadow_without_normal(light_id: u32, frag_position: vec4) -> f32 { + let light = &clusterable_objects.data[light_id]; + + // because the shadow maps align with the axes and the frustum planes are at 45 degrees + // we can get the worldspace depth by taking the largest absolute axis + let surface_to_light = (*light).position_radius.xyz - frag_position.xyz; + let surface_to_light_abs = abs(surface_to_light); + let distance_to_light = max(surface_to_light_abs.x, max(surface_to_light_abs.y, surface_to_light_abs.z)); + + // The normal bias here is already scaled by the texel size at 1 world unit from the light. + // The texel size increases proportionally with distance from the light so multiplying by + // distance to light scales the normal bias to the texel size at the fragment distance. + let depth_offset = (*light).shadow_depth_bias * normalize(surface_to_light.xyz); + let offset_position = frag_position.xyz + depth_offset; + + // similar largest-absolute-axis trick as above, but now with the offset fragment position + let frag_ls = offset_position.xyz - (*light).position_radius.xyz ; + let abs_position_ls = abs(frag_ls); + let major_axis_magnitude = max(abs_position_ls.x, max(abs_position_ls.y, abs_position_ls.z)); + + // NOTE: These simplifications come from multiplying: + // projection * vec4(0, 0, -major_axis_magnitude, 1.0) + // and keeping only the terms that have any impact on the depth. + // Projection-agnostic approach: + let zw = -major_axis_magnitude * (*light).light_custom_data.xy + (*light).light_custom_data.zw; + let depth = zw.x / zw.y; + + // Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed coordinate space, + // so we have to flip the z-axis when sampling. + let flip_z = vec3(1.0, 1.0, -1.0); + return sample_shadow_cubemap(frag_ls * flip_z, distance_to_light, depth, light_id); +} + +fn fetch_spot_shadow_without_normal(light_id: u32, frag_position: vec4) -> f32 { + let light = &clusterable_objects.data[light_id]; + + let surface_to_light = (*light).position_radius.xyz - frag_position.xyz; + + // construct the light view matrix + var spot_dir = vec3((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y); + // reconstruct spot dir from x/z and y-direction flag + spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z)); + if (((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u) { + spot_dir.y = -spot_dir.y; + } + + // view matrix z_axis is the reverse of transform.forward() + let fwd = -spot_dir; + let offset_position = + -surface_to_light + + ((*light).shadow_depth_bias * normalize(surface_to_light)); + + // the construction of the up and right vectors needs to precisely mirror the code + // in render/light.rs:spot_light_view_matrix + var sign = -1.0; + if (fwd.z >= 0.0) { + sign = 1.0; + } + let a = -1.0 / (fwd.z + sign); + let b = fwd.x * fwd.y * a; + let up_dir = vec3(1.0 + sign * fwd.x * fwd.x * a, sign * b, -sign * fwd.x); + let right_dir = vec3(-b, -sign - fwd.y * fwd.y * a, fwd.y); + let light_inv_rot = mat3x3(right_dir, up_dir, fwd); + + // because the matrix is a pure rotation matrix, the inverse is just the transpose, and to calculate + // the product of the transpose with a vector we can just post-multiply instead of pre-multiplying. + // this allows us to keep the matrix construction code identical between CPU and GPU. + let projected_position = offset_position * light_inv_rot; + + // divide xy by perspective matrix "f" and by -projected.z (projected.z is -projection matrix's w) + // to get ndc coordinates + let f_div_minus_z = 1.0 / ((*light).spot_light_tan_angle * -projected_position.z); + let shadow_xy_ndc = projected_position.xy * f_div_minus_z; + // convert to uv coordinates + let shadow_uv = shadow_xy_ndc * vec2(0.5, -0.5) + vec2(0.5, 0.5); + + // 0.1 must match POINT_LIGHT_NEAR_Z + let depth = 0.1 / -projected_position.z; + + return sample_shadow_map( + shadow_uv, + depth, + i32(light_id) + lights.spot_light_shadowmap_offset, + SPOT_SHADOW_TEXEL_SIZE + ); +} \ No newline at end of file diff --git a/crates/libmarathon/src/render/pbr/wireframe.rs b/crates/libmarathon/src/render/pbr/wireframe.rs new file mode 100644 index 0000000..2dbf92b --- /dev/null +++ b/crates/libmarathon/src/render/pbr/wireframe.rs @@ -0,0 +1,915 @@ +use crate::render::pbr::{ + DrawMesh, MeshPipeline, MeshPipelineKey, RenderMeshInstanceFlags, RenderMeshInstances, + SetMeshBindGroup, SetMeshViewBindGroup, SetMeshViewBindingArrayBindGroup, ViewKeyCache, + ViewSpecializationTicks, +}; +use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; +use bevy_asset::{ + embedded_asset, load_embedded_asset, prelude::AssetChanged, AsAssetId, Asset, AssetApp, + AssetEventSystems, AssetId, AssetServer, Assets, Handle, UntypedAssetId, +}; +use bevy_camera::{visibility::ViewVisibility, Camera, Camera3d}; +use bevy_color::{Color, ColorToComponents}; +use crate::render::core_3d::graph::{Core3d, Node3d}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Tick, + prelude::*, + query::QueryItem, + system::{lifetimeless::SRes, SystemChangeTick, SystemParamItem}, +}; +use bevy_mesh::{Mesh3d, MeshVertexBufferLayoutRef}; +use bevy_platform::{ + collections::{HashMap, HashSet}, + hash::FixedHasher, +}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use crate::render::{ + batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, + camera::{extract_cameras, ExtractedCamera}, + diagnostic::RecordDiagnostics, + extract_resource::ExtractResource, + mesh::{ + allocator::{MeshAllocator, SlabId}, + RenderMesh, + }, + prelude::*, + render_asset::{ + prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets, + }, + render_graph::{NodeRunError, RenderGraphContext, RenderGraphExt, ViewNode, ViewNodeRunner}, + render_phase::{ + AddRenderCommand, BinnedPhaseItem, BinnedRenderPhasePlugin, BinnedRenderPhaseType, + CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, + PhaseItemBatchSetKey, PhaseItemExtraIndex, RenderCommand, RenderCommandResult, + SetItemPipeline, TrackedRenderPass, ViewBinnedRenderPhases, + }, + render_resource::*, + renderer::{RenderContext, RenderDevice}, + sync_world::{MainEntity, MainEntityHashMap}, + view::{ + ExtractedView, NoIndirectDrawing, RenderVisibilityRanges, RenderVisibleEntities, + RetainedViewEntity, ViewDepthTexture, ViewTarget, + }, + Extract, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems, +}; +use bevy_shader::Shader; +use core::{hash::Hash, ops::Range}; +use tracing::{error, warn}; + +/// A [`Plugin`] that draws wireframes. +/// +/// Wireframes currently do not work when using webgl or webgpu. +/// Supported rendering backends: +/// - DX12 +/// - Vulkan +/// - Metal +/// +/// This is a native only feature. +#[derive(Debug, Default)] +pub struct WireframePlugin { + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, +} + +impl WireframePlugin { + /// Creates a new [`WireframePlugin`] with the given debug flags. + pub fn new(debug_flags: RenderDebugFlags) -> Self { + Self { debug_flags } + } +} + +impl Plugin for WireframePlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "render/wireframe.wgsl"); + + app.add_plugins(( + BinnedRenderPhasePlugin::::new(self.debug_flags), + RenderAssetPlugin::::default(), + )) + .init_asset::() + .init_resource::>() + .init_resource::() + .init_resource::() + .add_systems(Startup, setup_global_wireframe_material) + .add_systems( + Update, + ( + global_color_changed.run_if(resource_changed::), + wireframe_color_changed, + // Run `apply_global_wireframe_material` after `apply_wireframe_material` so that the global + // wireframe setting is applied to a mesh on the same frame its wireframe marker component is removed. + (apply_wireframe_material, apply_global_wireframe_material).chain(), + ), + ) + .add_systems( + PostUpdate, + check_wireframe_entities_needing_specialization + .after(AssetEventSystems) + .run_if(resource_exists::), + ); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + let required_features = WgpuFeatures::POLYGON_MODE_LINE | WgpuFeatures::PUSH_CONSTANTS; + let render_device = render_app.world().resource::(); + if !render_device.features().contains(required_features) { + warn!( + "WireframePlugin not loaded. GPU lacks support for required features: {:?}.", + required_features + ); + return; + } + + render_app + .init_resource::() + .init_resource::() + .init_resource::>() + .add_render_command::() + .init_resource::() + .init_resource::>() + .add_render_graph_node::>(Core3d, Node3d::Wireframe) + .add_render_graph_edges( + Core3d, + ( + Node3d::EndMainPass, + Node3d::Wireframe, + Node3d::PostProcessing, + ), + ) + .add_systems(RenderStartup, init_wireframe_3d_pipeline) + .add_systems( + ExtractSchedule, + ( + extract_wireframe_3d_camera, + extract_wireframe_entities_needing_specialization.after(extract_cameras), + extract_wireframe_materials, + ), + ) + .add_systems( + Render, + ( + specialize_wireframes + .in_set(RenderSystems::PrepareMeshes) + .after(prepare_assets::) + .after(prepare_assets::), + queue_wireframes + .in_set(RenderSystems::QueueMeshes) + .after(prepare_assets::), + ), + ); + } +} + +/// Enables wireframe rendering for any entity it is attached to. +/// It will ignore the [`WireframeConfig`] global setting. +/// +/// This requires the [`WireframePlugin`] to be enabled. +#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)] +#[reflect(Component, Default, Debug, PartialEq)] +pub struct Wireframe; + +pub struct Wireframe3d { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: Wireframe3dBatchSetKey, + /// The key, which determines which can be batched. + pub bin_key: Wireframe3dBinKey, + /// An entity from which data will be fetched, including the mesh if + /// applicable. + pub representative_entity: (Entity, MainEntity), + /// The ranges of instances. + pub batch_range: Range, + /// An extra index, which is either a dynamic offset or an index in the + /// indirect parameters list. + pub extra_index: PhaseItemExtraIndex, +} + +impl PhaseItem for Wireframe3d { + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + fn draw_function(&self) -> DrawFunctionId { + self.batch_set_key.draw_function + } + + fn batch_range(&self) -> &Range { + &self.batch_range + } + + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl CachedRenderPipelinePhaseItem for Wireframe3d { + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.batch_set_key.pipeline + } +} + +impl BinnedPhaseItem for Wireframe3d { + type BinKey = Wireframe3dBinKey; + type BatchSetKey = Wireframe3dBatchSetKey; + + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Self { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Wireframe3dBatchSetKey { + /// The identifier of the render pipeline. + pub pipeline: CachedRenderPipelineId, + + /// The wireframe material asset ID. + pub asset_id: UntypedAssetId, + + /// The function used to draw. + pub draw_function: DrawFunctionId, + /// The ID of the slab of GPU memory that contains vertex data. + /// + /// For non-mesh items, you can fill this with 0 if your items can be + /// multi-drawn, or with a unique value if they can't. + pub vertex_slab: SlabId, + + /// The ID of the slab of GPU memory that contains index data, if present. + /// + /// For non-mesh items, you can safely fill this with `None`. + pub index_slab: Option, +} + +impl PhaseItemBatchSetKey for Wireframe3dBatchSetKey { + fn indexed(&self) -> bool { + self.index_slab.is_some() + } +} + +/// Data that must be identical in order to *batch* phase items together. +/// +/// Note that a *batch set* (if multi-draw is in use) contains multiple batches. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Wireframe3dBinKey { + /// The wireframe mesh asset ID. + pub asset_id: UntypedAssetId, +} + +pub struct SetWireframe3dPushConstants; + +impl RenderCommand

    for SetWireframe3dPushConstants { + type Param = ( + SRes, + SRes>, + ); + type ViewQuery = (); + type ItemQuery = (); + + #[inline] + fn render<'w>( + item: &P, + _view: (), + _item_query: Option<()>, + (wireframe_instances, wireframe_assets): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(wireframe_material) = wireframe_instances.get(&item.main_entity()) else { + return RenderCommandResult::Failure("No wireframe material found for entity"); + }; + let Some(wireframe_material) = wireframe_assets.get(*wireframe_material) else { + return RenderCommandResult::Failure("No wireframe material found for entity"); + }; + + pass.set_push_constants( + ShaderStages::FRAGMENT, + 0, + bytemuck::bytes_of(&wireframe_material.color), + ); + RenderCommandResult::Success + } +} + +pub type DrawWireframe3d = ( + SetItemPipeline, + SetMeshViewBindGroup<0>, + SetMeshViewBindingArrayBindGroup<1>, + SetMeshBindGroup<2>, + SetWireframe3dPushConstants, + DrawMesh, +); + +#[derive(Resource, Clone)] +pub struct Wireframe3dPipeline { + mesh_pipeline: MeshPipeline, + shader: Handle, +} + +pub fn init_wireframe_3d_pipeline( + mut commands: Commands, + mesh_pipeline: Res, + asset_server: Res, +) { + commands.insert_resource(Wireframe3dPipeline { + mesh_pipeline: mesh_pipeline.clone(), + shader: load_embedded_asset!(asset_server.as_ref(), "render/wireframe.wgsl"), + }); +} + +impl SpecializedMeshPipeline for Wireframe3dPipeline { + type Key = MeshPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + let mut descriptor = self.mesh_pipeline.specialize(key, layout)?; + descriptor.label = Some("wireframe_3d_pipeline".into()); + descriptor.push_constant_ranges.push(PushConstantRange { + stages: ShaderStages::FRAGMENT, + range: 0..16, + }); + let fragment = descriptor.fragment.as_mut().unwrap(); + fragment.shader = self.shader.clone(); + descriptor.primitive.polygon_mode = PolygonMode::Line; + descriptor.depth_stencil.as_mut().unwrap().bias.slope_scale = 1.0; + Ok(descriptor) + } +} + +#[derive(Default)] +struct Wireframe3dNode; +impl ViewNode for Wireframe3dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ExtractedView, + &'static ViewTarget, + &'static ViewDepthTexture, + ); + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let Some(wireframe_phase) = world.get_resource::>() + else { + return Ok(()); + }; + + let Some(wireframe_phase) = wireframe_phase.get(&view.retained_view_entity) else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("wireframe_3d"), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)), + timestamp_writes: None, + occlusion_query_set: None, + }); + let pass_span = diagnostics.pass_span(&mut render_pass, "wireframe_3d"); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + if let Err(err) = wireframe_phase.render(&mut render_pass, world, graph.view_entity()) { + error!("Error encountered while rendering the stencil phase {err:?}"); + return Err(NodeRunError::DrawError(err)); + } + + pass_span.end(&mut render_pass); + + Ok(()) + } +} + +/// Sets the color of the [`Wireframe`] of the entity it is attached to. +/// +/// If this component is present but there's no [`Wireframe`] component, +/// it will still affect the color of the wireframe when [`WireframeConfig::global`] is set to true. +/// +/// This overrides the [`WireframeConfig::default_color`]. +#[derive(Component, Debug, Clone, Default, Reflect)] +#[reflect(Component, Default, Debug)] +pub struct WireframeColor { + pub color: Color, +} + +#[derive(Component, Debug, Clone, Default)] +pub struct ExtractedWireframeColor { + pub color: [f32; 4], +} + +/// Disables wireframe rendering for any entity it is attached to. +/// It will ignore the [`WireframeConfig`] global setting. +/// +/// This requires the [`WireframePlugin`] to be enabled. +#[derive(Component, Debug, Clone, Default, Reflect, Eq, PartialEq)] +#[reflect(Component, Default, Debug, PartialEq)] +pub struct NoWireframe; + +#[derive(Resource, Debug, Clone, Default, ExtractResource, Reflect)] +#[reflect(Resource, Debug, Default)] +pub struct WireframeConfig { + /// Whether to show wireframes for all meshes. + /// Can be overridden for individual meshes by adding a [`Wireframe`] or [`NoWireframe`] component. + pub global: bool, + /// If [`Self::global`] is set, any [`Entity`] that does not have a [`Wireframe`] component attached to it will have + /// wireframes using this color. Otherwise, this will be the fallback color for any entity that has a [`Wireframe`], + /// but no [`WireframeColor`]. + pub default_color: Color, +} + +#[derive(Asset, Reflect, Clone, Debug, Default)] +#[reflect(Clone, Default)] +pub struct WireframeMaterial { + pub color: Color, +} + +pub struct RenderWireframeMaterial { + pub color: [f32; 4], +} + +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[reflect(Component, Default, Clone, PartialEq)] +pub struct Mesh3dWireframe(pub Handle); + +impl AsAssetId for Mesh3dWireframe { + type Asset = WireframeMaterial; + + fn as_asset_id(&self) -> AssetId { + self.0.id() + } +} + +impl RenderAsset for RenderWireframeMaterial { + type SourceAsset = WireframeMaterial; + type Param = (); + + fn prepare_asset( + source_asset: Self::SourceAsset, + _asset_id: AssetId, + _param: &mut SystemParamItem, + _previous_asset: Option<&Self>, + ) -> Result> { + Ok(RenderWireframeMaterial { + color: source_asset.color.to_linear().to_f32_array(), + }) + } +} + +#[derive(Resource, Deref, DerefMut, Default)] +pub struct RenderWireframeInstances(MainEntityHashMap>); + +#[derive(Clone, Resource, Deref, DerefMut, Debug, Default)] +pub struct WireframeEntitiesNeedingSpecialization { + #[deref] + pub entities: Vec, +} + +#[derive(Resource, Deref, DerefMut, Clone, Debug, Default)] +pub struct WireframeEntitySpecializationTicks { + pub entities: MainEntityHashMap, +} + +/// Stores the [`SpecializedWireframeViewPipelineCache`] for each view. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct SpecializedWireframePipelineCache { + // view entity -> view pipeline cache + #[deref] + map: HashMap, +} + +/// Stores the cached render pipeline ID for each entity in a single view, as +/// well as the last time it was changed. +#[derive(Deref, DerefMut, Default)] +pub struct SpecializedWireframeViewPipelineCache { + // material entity -> (tick, pipeline_id) + #[deref] + map: MainEntityHashMap<(Tick, CachedRenderPipelineId)>, +} + +#[derive(Resource)] +struct GlobalWireframeMaterial { + // This handle will be reused when the global config is enabled + handle: Handle, +} + +pub fn extract_wireframe_materials( + mut material_instances: ResMut, + changed_meshes_query: Extract< + Query< + (Entity, &ViewVisibility, &Mesh3dWireframe), + Or<(Changed, Changed)>, + >, + >, + mut removed_visibilities_query: Extract>, + mut removed_materials_query: Extract>, +) { + for (entity, view_visibility, material) in &changed_meshes_query { + if view_visibility.get() { + material_instances.insert(entity.into(), material.id()); + } else { + material_instances.remove(&MainEntity::from(entity)); + } + } + + for entity in removed_visibilities_query + .read() + .chain(removed_materials_query.read()) + { + // Only queue a mesh for removal if we didn't pick it up above. + // It's possible that a necessary component was removed and re-added in + // the same frame. + if !changed_meshes_query.contains(entity) { + material_instances.remove(&MainEntity::from(entity)); + } + } +} + +fn setup_global_wireframe_material( + mut commands: Commands, + mut materials: ResMut>, + config: Res, +) { + // Create the handle used for the global material + commands.insert_resource(GlobalWireframeMaterial { + handle: materials.add(WireframeMaterial { + color: config.default_color, + }), + }); +} + +/// Updates the wireframe material of all entities without a [`WireframeColor`] or without a [`Wireframe`] component +fn global_color_changed( + config: Res, + mut materials: ResMut>, + global_material: Res, +) { + if let Some(global_material) = materials.get_mut(&global_material.handle) { + global_material.color = config.default_color; + } +} + +/// Updates the wireframe material when the color in [`WireframeColor`] changes +fn wireframe_color_changed( + mut materials: ResMut>, + mut colors_changed: Query< + (&mut Mesh3dWireframe, &WireframeColor), + (With, Changed), + >, +) { + for (mut handle, wireframe_color) in &mut colors_changed { + handle.0 = materials.add(WireframeMaterial { + color: wireframe_color.color, + }); + } +} + +/// Applies or remove the wireframe material to any mesh with a [`Wireframe`] component, and removes it +/// for any mesh with a [`NoWireframe`] component. +fn apply_wireframe_material( + mut commands: Commands, + mut materials: ResMut>, + wireframes: Query< + (Entity, Option<&WireframeColor>), + (With, Without), + >, + no_wireframes: Query, With)>, + mut removed_wireframes: RemovedComponents, + global_material: Res, +) { + for e in removed_wireframes.read().chain(no_wireframes.iter()) { + if let Ok(mut commands) = commands.get_entity(e) { + commands.remove::(); + } + } + + let mut material_to_spawn = vec![]; + for (e, maybe_color) in &wireframes { + let material = get_wireframe_material(maybe_color, &mut materials, &global_material); + material_to_spawn.push((e, Mesh3dWireframe(material))); + } + commands.try_insert_batch(material_to_spawn); +} + +type WireframeFilter = (With, Without, Without); + +/// Applies or removes a wireframe material on any mesh without a [`Wireframe`] or [`NoWireframe`] component. +fn apply_global_wireframe_material( + mut commands: Commands, + config: Res, + meshes_without_material: Query< + (Entity, Option<&WireframeColor>), + (WireframeFilter, Without), + >, + meshes_with_global_material: Query)>, + global_material: Res, + mut materials: ResMut>, +) { + if config.global { + let mut material_to_spawn = vec![]; + for (e, maybe_color) in &meshes_without_material { + let material = get_wireframe_material(maybe_color, &mut materials, &global_material); + // We only add the material handle but not the Wireframe component + // This makes it easy to detect which mesh is using the global material and which ones are user specified + material_to_spawn.push((e, Mesh3dWireframe(material))); + } + commands.try_insert_batch(material_to_spawn); + } else { + for e in &meshes_with_global_material { + commands.entity(e).remove::(); + } + } +} + +/// Gets a handle to a wireframe material with a fallback on the default material +fn get_wireframe_material( + maybe_color: Option<&WireframeColor>, + wireframe_materials: &mut Assets, + global_material: &GlobalWireframeMaterial, +) -> Handle { + if let Some(wireframe_color) = maybe_color { + wireframe_materials.add(WireframeMaterial { + color: wireframe_color.color, + }) + } else { + // If there's no color specified we can use the global material since it's already set to use the default_color + global_material.handle.clone() + } +} + +fn extract_wireframe_3d_camera( + mut wireframe_3d_phases: ResMut>, + cameras: Extract), With>>, + mut live_entities: Local>, + gpu_preprocessing_support: Res, +) { + live_entities.clear(); + for (main_entity, camera, no_indirect_drawing) in &cameras { + if !camera.is_active { + continue; + } + let gpu_preprocessing_mode = gpu_preprocessing_support.min(if !no_indirect_drawing { + GpuPreprocessingMode::Culling + } else { + GpuPreprocessingMode::PreprocessingOnly + }); + + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + wireframe_3d_phases.prepare_for_new_frame(retained_view_entity, gpu_preprocessing_mode); + live_entities.insert(retained_view_entity); + } + + // Clear out all dead views. + wireframe_3d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity)); +} + +pub fn extract_wireframe_entities_needing_specialization( + entities_needing_specialization: Extract>, + mut entity_specialization_ticks: ResMut, + views: Query<&ExtractedView>, + mut specialized_wireframe_pipeline_cache: ResMut, + mut removed_meshes_query: Extract>, + ticks: SystemChangeTick, +) { + for entity in entities_needing_specialization.iter() { + // Update the entity's specialization tick with this run's tick + entity_specialization_ticks.insert((*entity).into(), ticks.this_run()); + } + + for entity in removed_meshes_query.read() { + for view in &views { + if let Some(specialized_wireframe_pipeline_cache) = + specialized_wireframe_pipeline_cache.get_mut(&view.retained_view_entity) + { + specialized_wireframe_pipeline_cache.remove(&MainEntity::from(entity)); + } + } + } +} + +pub fn check_wireframe_entities_needing_specialization( + needs_specialization: Query< + Entity, + Or<( + Changed, + AssetChanged, + Changed, + AssetChanged, + )>, + >, + mut entities_needing_specialization: ResMut, +) { + entities_needing_specialization.clear(); + for entity in &needs_specialization { + entities_needing_specialization.push(entity); + } +} + +pub fn specialize_wireframes( + render_meshes: Res>, + render_mesh_instances: Res, + render_wireframe_instances: Res, + render_visibility_ranges: Res, + wireframe_phases: Res>, + views: Query<(&ExtractedView, &RenderVisibleEntities)>, + view_key_cache: Res, + entity_specialization_ticks: Res, + view_specialization_ticks: Res, + mut specialized_material_pipeline_cache: ResMut, + mut pipelines: ResMut>, + pipeline: Res, + pipeline_cache: Res, + ticks: SystemChangeTick, +) { + // Record the retained IDs of all views so that we can expire old + // pipeline IDs. + let mut all_views: HashSet = HashSet::default(); + + for (view, visible_entities) in &views { + all_views.insert(view.retained_view_entity); + + if !wireframe_phases.contains_key(&view.retained_view_entity) { + continue; + } + + let Some(view_key) = view_key_cache.get(&view.retained_view_entity) else { + continue; + }; + + let view_tick = view_specialization_ticks + .get(&view.retained_view_entity) + .unwrap(); + let view_specialized_material_pipeline_cache = specialized_material_pipeline_cache + .entry(view.retained_view_entity) + .or_default(); + + for (_, visible_entity) in visible_entities.iter::() { + if !render_wireframe_instances.contains_key(visible_entity) { + continue; + }; + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap(); + let last_specialized_tick = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(tick, _)| *tick); + let needs_specialization = last_specialized_tick.is_none_or(|tick| { + view_tick.is_newer_than(tick, ticks.this_run()) + || entity_tick.is_newer_than(tick, ticks.this_run()) + }); + if !needs_specialization { + continue; + } + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + + let mut mesh_key = *view_key; + mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology()); + + if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) { + mesh_key |= MeshPipelineKey::VISIBILITY_RANGE_DITHER; + } + + if view_key.contains(MeshPipelineKey::MOTION_VECTOR_PREPASS) { + // If the previous frame have skins or morph targets, note that. + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_SKIN) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_SKIN; + } + if mesh_instance + .flags + .contains(RenderMeshInstanceFlags::HAS_PREVIOUS_MORPH) + { + mesh_key |= MeshPipelineKey::HAS_PREVIOUS_MORPH; + } + } + + let pipeline_id = + pipelines.specialize(&pipeline_cache, &pipeline, mesh_key, &mesh.layout); + let pipeline_id = match pipeline_id { + Ok(id) => id, + Err(err) => { + error!("{}", err); + continue; + } + }; + + view_specialized_material_pipeline_cache + .insert(*visible_entity, (ticks.this_run(), pipeline_id)); + } + } + + // Delete specialized pipelines belonging to views that have expired. + specialized_material_pipeline_cache + .retain(|retained_view_entity, _| all_views.contains(retained_view_entity)); +} + +fn queue_wireframes( + custom_draw_functions: Res>, + render_mesh_instances: Res, + gpu_preprocessing_support: Res, + mesh_allocator: Res, + specialized_wireframe_pipeline_cache: Res, + render_wireframe_instances: Res, + mut wireframe_3d_phases: ResMut>, + mut views: Query<(&ExtractedView, &RenderVisibleEntities)>, +) { + for (view, visible_entities) in &mut views { + let Some(wireframe_phase) = wireframe_3d_phases.get_mut(&view.retained_view_entity) else { + continue; + }; + let draw_wireframe = custom_draw_functions.read().id::(); + + let Some(view_specialized_material_pipeline_cache) = + specialized_wireframe_pipeline_cache.get(&view.retained_view_entity) + else { + continue; + }; + + for (render_entity, visible_entity) in visible_entities.iter::() { + let Some(wireframe_instance) = render_wireframe_instances.get(visible_entity) else { + continue; + }; + let Some((current_change_tick, pipeline_id)) = view_specialized_material_pipeline_cache + .get(visible_entity) + .map(|(current_change_tick, pipeline_id)| (*current_change_tick, *pipeline_id)) + else { + continue; + }; + + // Skip the entity if it's cached in a bin and up to date. + if wireframe_phase.validate_cached_entity(*visible_entity, current_change_tick) { + continue; + } + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(*visible_entity) + else { + continue; + }; + let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let bin_key = Wireframe3dBinKey { + asset_id: mesh_instance.mesh_asset_id.untyped(), + }; + let batch_set_key = Wireframe3dBatchSetKey { + pipeline: pipeline_id, + asset_id: wireframe_instance.untyped(), + draw_function: draw_wireframe, + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + wireframe_phase.add( + batch_set_key, + bin_key, + (*render_entity, *visible_entity), + mesh_instance.current_uniform_index, + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), + current_change_tick, + ); + } + } +} diff --git a/crates/libmarathon/src/render/pipelined_rendering.rs b/crates/libmarathon/src/render/pipelined_rendering.rs new file mode 100644 index 0000000..35ebc0b --- /dev/null +++ b/crates/libmarathon/src/render/pipelined_rendering.rs @@ -0,0 +1,204 @@ +use async_channel::{Receiver, Sender}; + +use bevy_app::{App, AppExit, AppLabel, Plugin, SubApp}; +use bevy_ecs::{ + resource::Resource, + schedule::MainThreadExecutor, + world::{Mut, World}, +}; +use bevy_tasks::ComputeTaskPool; + +use crate::render::RenderApp; + +/// A Label for the sub app that runs the parts of pipelined rendering that need to run on the main thread. +/// +/// The Main schedule of this app can be used to run logic after the render schedule starts, but +/// before I/O processing. This can be useful for something like frame pacing. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)] +pub struct RenderExtractApp; + +/// Channels used by the main app to send and receive the render app. +#[derive(Resource)] +pub struct RenderAppChannels { + app_to_render_sender: Sender, + render_to_app_receiver: Receiver, + render_app_in_render_thread: bool, +} + +impl RenderAppChannels { + /// Create a `RenderAppChannels` from a [`async_channel::Receiver`] and [`async_channel::Sender`] + pub fn new( + app_to_render_sender: Sender, + render_to_app_receiver: Receiver, + ) -> Self { + Self { + app_to_render_sender, + render_to_app_receiver, + render_app_in_render_thread: false, + } + } + + /// Send the `render_app` to the rendering thread. + pub fn send_blocking(&mut self, render_app: SubApp) { + self.app_to_render_sender.send_blocking(render_app).unwrap(); + self.render_app_in_render_thread = true; + } + + /// Receive the `render_app` from the rendering thread. + /// Return `None` if the render thread has panicked. + pub async fn recv(&mut self) -> Option { + let render_app = self.render_to_app_receiver.recv().await.ok()?; + self.render_app_in_render_thread = false; + Some(render_app) + } +} + +impl Drop for RenderAppChannels { + fn drop(&mut self) { + if self.render_app_in_render_thread { + // Any non-send data in the render world was initialized on the main thread. + // So on dropping the main world and ending the app, we block and wait for + // the render world to return to drop it. Which allows the non-send data + // drop methods to run on the correct thread. + self.render_to_app_receiver.recv_blocking().ok(); + } + } +} + +/// The [`PipelinedRenderingPlugin`] can be added to your application to enable pipelined rendering. +/// +/// This moves rendering into a different thread, so that the Nth frame's rendering can +/// be run at the same time as the N + 1 frame's simulation. +/// +/// ```text +/// |--------------------|--------------------|--------------------|--------------------| +/// | simulation thread | frame 1 simulation | frame 2 simulation | frame 3 simulation | +/// |--------------------|--------------------|--------------------|--------------------| +/// | rendering thread | | frame 1 rendering | frame 2 rendering | +/// |--------------------|--------------------|--------------------|--------------------| +/// ``` +/// +/// The plugin is dependent on the [`RenderApp`] added by [`crate::RenderPlugin`] and so must +/// be added after that plugin. If it is not added after, the plugin will do nothing. +/// +/// A single frame of execution looks something like below +/// +/// ```text +/// |---------------------------------------------------------------------------| +/// | | | RenderExtractApp schedule | winit events | main schedule | +/// | sync | extract |----------------------------------------------------------| +/// | | | extract commands | rendering schedule | +/// |---------------------------------------------------------------------------| +/// ``` +/// +/// - `sync` is the step where the entity-entity mapping between the main and render world is updated. +/// This is run on the main app's thread. For more information checkout [`SyncWorldPlugin`]. +/// - `extract` is the step where data is copied from the main world to the render world. +/// This is run on the main app's thread. +/// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the +/// main schedule can start sooner. +/// - Then the `rendering schedule` is run. See [`RenderSystems`](crate::RenderSystems) for the standard steps in this process. +/// - In parallel to the rendering thread the [`RenderExtractApp`] schedule runs. By +/// default, this schedule is empty. But it is useful if you need something to run before I/O processing. +/// - Next all the `winit events` are processed. +/// - And finally the `main app schedule` is run. +/// - Once both the `main app schedule` and the `render schedule` are finished running, `extract` is run again. +/// +/// [`SyncWorldPlugin`]: crate::sync_world::SyncWorldPlugin +#[derive(Default)] +pub struct PipelinedRenderingPlugin; + +impl Plugin for PipelinedRenderingPlugin { + fn build(&self, app: &mut App) { + // Don't add RenderExtractApp if RenderApp isn't initialized. + if app.get_sub_app(RenderApp).is_none() { + return; + } + app.insert_resource(MainThreadExecutor::new()); + + let mut sub_app = SubApp::new(); + sub_app.set_extract(renderer_extract); + app.insert_sub_app(RenderExtractApp, sub_app); + } + + // Sets up the render thread and inserts resources into the main app used for controlling the render thread. + fn cleanup(&self, app: &mut App) { + // skip setting up when headless + if app.get_sub_app(RenderExtractApp).is_none() { + return; + } + + let (app_to_render_sender, app_to_render_receiver) = async_channel::bounded::(1); + let (render_to_app_sender, render_to_app_receiver) = async_channel::bounded::(1); + + let mut render_app = app + .remove_sub_app(RenderApp) + .expect("Unable to get RenderApp. Another plugin may have removed the RenderApp before PipelinedRenderingPlugin"); + + // clone main thread executor to render world + let executor = app.world().get_resource::().unwrap(); + render_app.world_mut().insert_resource(executor.clone()); + + render_to_app_sender.send_blocking(render_app).unwrap(); + + app.insert_resource(RenderAppChannels::new( + app_to_render_sender, + render_to_app_receiver, + )); + + std::thread::spawn(move || { + #[cfg(feature = "trace")] + let _span = tracing::info_span!("render thread").entered(); + + let compute_task_pool = ComputeTaskPool::get(); + loop { + // run a scope here to allow main world to use this thread while it's waiting for the render app + let sent_app = compute_task_pool + .scope(|s| { + s.spawn(async { app_to_render_receiver.recv().await }); + }) + .pop(); + let Some(Ok(mut render_app)) = sent_app else { + break; + }; + + { + #[cfg(feature = "trace")] + let _sub_app_span = tracing::info_span!("sub app", name = ?RenderApp).entered(); + render_app.update(); + } + + if render_to_app_sender.send_blocking(render_app).is_err() { + break; + } + } + + tracing::debug!("exiting pipelined rendering thread"); + }); + } +} + +// This function waits for the rendering world to be received, +// runs extract, and then sends the rendering world back to the render thread. +fn renderer_extract(app_world: &mut World, _world: &mut World) { + app_world.resource_scope(|world, main_thread_executor: Mut| { + world.resource_scope(|world, mut render_channels: Mut| { + // we use a scope here to run any main thread tasks that the render world still needs to run + // while we wait for the render world to be received. + if let Some(mut render_app) = ComputeTaskPool::get() + .scope_with_executor(true, Some(&*main_thread_executor.0), |s| { + s.spawn(async { render_channels.recv().await }); + }) + .pop() + .unwrap() + { + render_app.extract(world); + + render_channels.send_blocking(render_app); + } else { + // Renderer thread panicked + world.write_message(AppExit::error()); + } + }); + }); +} diff --git a/crates/libmarathon/src/render/prepass/mod.rs b/crates/libmarathon/src/render/prepass/mod.rs new file mode 100644 index 0000000..35cb1c5 --- /dev/null +++ b/crates/libmarathon/src/render/prepass/mod.rs @@ -0,0 +1,383 @@ +//! Run a prepass before the main pass to generate depth, normals, and/or motion vectors textures, sometimes called a thin g-buffer. +//! These textures are useful for various screen-space effects and reducing overdraw in the main pass. +//! +//! The prepass only runs for opaque meshes or meshes with an alpha mask. Transparent meshes are ignored. +//! +//! To enable the prepass, you need to add a prepass component to a [`bevy_camera::Camera3d`]. +//! +//! [`DepthPrepass`] +//! [`NormalPrepass`] +//! [`MotionVectorPrepass`] +//! +//! The textures are automatically added to the default mesh view bindings. You can also get the raw textures +//! by querying the [`ViewPrepassTextures`] component on any camera with a prepass component. +//! +//! The depth prepass will always run and generate the depth buffer as a side effect, but it won't copy it +//! to a separate texture unless the [`DepthPrepass`] is activated. This means that if any prepass component is present +//! it will always create a depth buffer that will be used by the main pass. +//! +//! When using the default mesh view bindings you should be able to use `prepass_depth()`, +//! `prepass_normal()`, and `prepass_motion_vector()` to load the related textures. +//! These functions are defined in `bevy_pbr::prepass_utils`. See the `shader_prepass` example that shows how to use them. +//! +//! The prepass runs for each `Material`. You can control if the prepass should run per-material by setting the `prepass_enabled` +//! flag on the `MaterialPlugin`. +//! +//! Currently only works for 3D. + +pub mod node; + +use core::ops::Range; + +use crate::render::deferred::{DEFERRED_LIGHTING_PASS_ID_FORMAT, DEFERRED_PREPASS_FORMAT}; +use bevy_asset::UntypedAssetId; +use bevy_ecs::prelude::*; +use bevy_math::Mat4; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use crate::render::mesh::allocator::SlabId; +use crate::render::render_phase::PhaseItemBatchSetKey; +use crate::render::sync_world::MainEntity; +use crate::render::{ + render_phase::{ + BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem, + PhaseItemExtraIndex, + }, + render_resource::{ + CachedRenderPipelineId, ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d, + ShaderType, TextureFormat, TextureView, + }, + texture::ColorAttachment, +}; + +pub const NORMAL_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgb10a2Unorm; +pub const MOTION_VECTOR_PREPASS_FORMAT: TextureFormat = TextureFormat::Rg16Float; + +/// If added to a [`bevy_camera::Camera3d`] then depth values will be copied to a separate texture available to the main pass. +#[derive(Component, Default, Reflect, Clone)] +#[reflect(Component, Default, Clone)] +pub struct DepthPrepass; + +/// If added to a [`bevy_camera::Camera3d`] then vertex world normals will be copied to a separate texture available to the main pass. +/// Normals will have normal map textures already applied. +#[derive(Component, Default, Reflect, Clone)] +#[reflect(Component, Default, Clone)] +pub struct NormalPrepass; + +/// If added to a [`bevy_camera::Camera3d`] then screen space motion vectors will be copied to a separate texture available to the main pass. +#[derive(Component, Default, Reflect, Clone)] +#[reflect(Component, Default, Clone)] +pub struct MotionVectorPrepass; + +/// If added to a [`bevy_camera::Camera3d`] then deferred materials will be rendered to the deferred gbuffer texture and will be available to subsequent passes. +/// Note the default deferred lighting plugin also requires `DepthPrepass` to work correctly. +#[derive(Component, Default, Reflect)] +#[reflect(Component, Default)] +pub struct DeferredPrepass; + +/// View matrices from the previous frame. +/// +/// Useful for temporal rendering techniques that need access to last frame's camera data. +#[derive(Component, ShaderType, Clone)] +pub struct PreviousViewData { + pub view_from_world: Mat4, + pub clip_from_world: Mat4, + pub clip_from_view: Mat4, + pub world_from_clip: Mat4, + pub view_from_clip: Mat4, +} + +#[derive(Resource, Default)] +pub struct PreviousViewUniforms { + pub uniforms: DynamicUniformBuffer, +} + +#[derive(Component)] +pub struct PreviousViewUniformOffset { + pub offset: u32, +} + +/// Textures that are written to by the prepass. +/// +/// This component will only be present if any of the relevant prepass components are also present. +#[derive(Component)] +pub struct ViewPrepassTextures { + /// The depth texture generated by the prepass. + /// Exists only if [`DepthPrepass`] is added to the [`ViewTarget`](bevy_render::view::ViewTarget) + pub depth: Option, + /// The normals texture generated by the prepass. + /// Exists only if [`NormalPrepass`] is added to the [`ViewTarget`](bevy_render::view::ViewTarget) + pub normal: Option, + /// The motion vectors texture generated by the prepass. + /// Exists only if [`MotionVectorPrepass`] is added to the `ViewTarget` + pub motion_vectors: Option, + /// The deferred gbuffer generated by the deferred pass. + /// Exists only if [`DeferredPrepass`] is added to the `ViewTarget` + pub deferred: Option, + /// A texture that specifies the deferred lighting pass id for a material. + /// Exists only if [`DeferredPrepass`] is added to the `ViewTarget` + pub deferred_lighting_pass_id: Option, + /// The size of the textures. + pub size: Extent3d, +} + +impl ViewPrepassTextures { + pub fn depth_view(&self) -> Option<&TextureView> { + self.depth.as_ref().map(|t| &t.texture.default_view) + } + + pub fn normal_view(&self) -> Option<&TextureView> { + self.normal.as_ref().map(|t| &t.texture.default_view) + } + + pub fn motion_vectors_view(&self) -> Option<&TextureView> { + self.motion_vectors + .as_ref() + .map(|t| &t.texture.default_view) + } + + pub fn deferred_view(&self) -> Option<&TextureView> { + self.deferred.as_ref().map(|t| &t.texture.default_view) + } +} + +/// Opaque phase of the 3D prepass. +/// +/// Sorted by pipeline, then by mesh to improve batching. +/// +/// Used to render all 3D meshes with materials that have no transparency. +pub struct Opaque3dPrepass { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: OpaqueNoLightmap3dBatchSetKey, + /// Information that separates items into bins. + pub bin_key: OpaqueNoLightmap3dBinKey, + + /// An entity from which Bevy fetches data common to all instances in this + /// batch, such as the mesh. + pub representative_entity: (Entity, MainEntity), + pub batch_range: Range, + pub extra_index: PhaseItemExtraIndex, +} + +/// Information that must be identical in order to place opaque meshes in the +/// same *batch set* in the prepass and deferred pass. +/// +/// A batch set is a set of batches that can be multi-drawn together, if +/// multi-draw is in use. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct OpaqueNoLightmap3dBatchSetKey { + /// The ID of the GPU pipeline. + pub pipeline: CachedRenderPipelineId, + + /// The function used to draw the mesh. + pub draw_function: DrawFunctionId, + + /// The ID of a bind group specific to the material. + /// + /// In the case of PBR, this is the `MaterialBindGroupIndex`. + pub material_bind_group_index: Option, + + /// The ID of the slab of GPU memory that contains vertex data. + /// + /// For non-mesh items, you can fill this with 0 if your items can be + /// multi-drawn, or with a unique value if they can't. + pub vertex_slab: SlabId, + + /// The ID of the slab of GPU memory that contains index data, if present. + /// + /// For non-mesh items, you can safely fill this with `None`. + pub index_slab: Option, +} + +impl PhaseItemBatchSetKey for OpaqueNoLightmap3dBatchSetKey { + fn indexed(&self) -> bool { + self.index_slab.is_some() + } +} + +// TODO: Try interning these. +/// The data used to bin each opaque 3D object in the prepass and deferred pass. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct OpaqueNoLightmap3dBinKey { + /// The ID of the asset. + pub asset_id: UntypedAssetId, +} + +impl PhaseItem for Opaque3dPrepass { + #[inline] + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.batch_set_key.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + #[inline] + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl BinnedPhaseItem for Opaque3dPrepass { + type BatchSetKey = OpaqueNoLightmap3dBatchSetKey; + type BinKey = OpaqueNoLightmap3dBinKey; + + #[inline] + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Opaque3dPrepass { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +impl CachedRenderPipelinePhaseItem for Opaque3dPrepass { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.batch_set_key.pipeline + } +} + +/// Alpha mask phase of the 3D prepass. +/// +/// Sorted by pipeline, then by mesh to improve batching. +/// +/// Used to render all meshes with a material with an alpha mask. +pub struct AlphaMask3dPrepass { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: OpaqueNoLightmap3dBatchSetKey, + /// Information that separates items into bins. + pub bin_key: OpaqueNoLightmap3dBinKey, + pub representative_entity: (Entity, MainEntity), + pub batch_range: Range, + pub extra_index: PhaseItemExtraIndex, +} + +impl PhaseItem for AlphaMask3dPrepass { + #[inline] + fn entity(&self) -> Entity { + self.representative_entity.0 + } + + fn main_entity(&self) -> MainEntity { + self.representative_entity.1 + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.batch_set_key.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + #[inline] + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index.clone() + } + + #[inline] + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl BinnedPhaseItem for AlphaMask3dPrepass { + type BatchSetKey = OpaqueNoLightmap3dBatchSetKey; + type BinKey = OpaqueNoLightmap3dBinKey; + + #[inline] + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Self { + batch_set_key, + bin_key, + representative_entity, + batch_range, + extra_index, + } + } +} + +impl CachedRenderPipelinePhaseItem for AlphaMask3dPrepass { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.batch_set_key.pipeline + } +} + +pub fn prepass_target_descriptors( + normal_prepass: bool, + motion_vector_prepass: bool, + deferred_prepass: bool, +) -> Vec> { + vec![ + normal_prepass.then_some(ColorTargetState { + format: NORMAL_PREPASS_FORMAT, + blend: None, + write_mask: ColorWrites::ALL, + }), + motion_vector_prepass.then_some(ColorTargetState { + format: MOTION_VECTOR_PREPASS_FORMAT, + blend: None, + write_mask: ColorWrites::ALL, + }), + deferred_prepass.then_some(ColorTargetState { + format: DEFERRED_PREPASS_FORMAT, + blend: None, + write_mask: ColorWrites::ALL, + }), + deferred_prepass.then_some(ColorTargetState { + format: DEFERRED_LIGHTING_PASS_ID_FORMAT, + blend: None, + write_mask: ColorWrites::ALL, + }), + ] +} diff --git a/crates/libmarathon/src/render/prepass/node.rs b/crates/libmarathon/src/render/prepass/node.rs new file mode 100644 index 0000000..662b271 --- /dev/null +++ b/crates/libmarathon/src/render/prepass/node.rs @@ -0,0 +1,255 @@ +use bevy_camera::{MainPassResolutionOverride, Viewport}; +use bevy_ecs::{prelude::*, query::QueryItem}; +use crate::render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + experimental::occlusion_culling::OcclusionCulling, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_phase::{TrackedRenderPass, ViewBinnedRenderPhases}, + render_resource::{CommandEncoderDescriptor, PipelineCache, RenderPassDescriptor, StoreOp}, + renderer::RenderContext, + view::{ExtractedView, NoIndirectDrawing, ViewDepthTexture, ViewUniformOffset}, +}; +use tracing::error; +#[cfg(feature = "trace")] +use tracing::info_span; + +use crate::render::skybox::prepass::{RenderSkyboxPrepassPipeline, SkyboxPrepassBindGroup}; + +use super::{ + AlphaMask3dPrepass, DeferredPrepass, Opaque3dPrepass, PreviousViewUniformOffset, + ViewPrepassTextures, +}; + +/// The phase of the prepass that draws meshes that were visible last frame. +/// +/// If occlusion culling isn't in use, this prepass simply draws all meshes. +/// +/// Like all prepass nodes, this is inserted before the main pass in the render +/// graph. +#[derive(Default)] +pub struct EarlyPrepassNode; + +impl ViewNode for EarlyPrepassNode { + type ViewQuery = ::ViewQuery; + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + run_prepass(graph, render_context, view_query, world, "early prepass") + } +} + +/// The phase of the prepass that runs after occlusion culling against the +/// meshes that were visible last frame. +/// +/// If occlusion culling isn't in use, this is a no-op. +/// +/// Like all prepass nodes, this is inserted before the main pass in the render +/// graph. +#[derive(Default)] +pub struct LatePrepassNode; + +impl ViewNode for LatePrepassNode { + type ViewQuery = ( + ( + &'static ExtractedCamera, + &'static ExtractedView, + &'static ViewDepthTexture, + &'static ViewPrepassTextures, + &'static ViewUniformOffset, + ), + ( + Option<&'static DeferredPrepass>, + Option<&'static RenderSkyboxPrepassPipeline>, + Option<&'static SkyboxPrepassBindGroup>, + Option<&'static PreviousViewUniformOffset>, + Option<&'static MainPassResolutionOverride>, + ), + ( + Has, + Has, + Has, + ), + ); + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + query: QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + // We only need a late prepass if we have occlusion culling and indirect + // drawing. + let (_, _, (occlusion_culling, no_indirect_drawing, _)) = query; + if !occlusion_culling || no_indirect_drawing { + return Ok(()); + } + + run_prepass(graph, render_context, query, world, "late prepass") + } +} + +/// Runs a prepass that draws all meshes to the depth buffer, and possibly +/// normal and motion vector buffers as well. +/// +/// If occlusion culling isn't in use, and a prepass is enabled, then there's +/// only one prepass. If occlusion culling is in use, then any prepass is split +/// into two: an *early* prepass and a *late* prepass. The early prepass draws +/// what was visible last frame, and the last prepass performs occlusion culling +/// against a conservative hierarchical Z buffer before drawing unoccluded +/// meshes. +fn run_prepass<'w>( + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + ( + (camera, extracted_view, view_depth_texture, view_prepass_textures, view_uniform_offset), + ( + deferred_prepass, + skybox_prepass_pipeline, + skybox_prepass_bind_group, + view_prev_uniform_offset, + resolution_override, + ), + (_, _, has_deferred), + ): QueryItem<'w, '_, ::ViewQuery>, + world: &'w World, + label: &'static str, +) -> Result<(), NodeRunError> { + // If we're using deferred rendering, there will be a deferred prepass + // instead of this one. Just bail out so we don't have to bother looking at + // the empty bins. + if has_deferred { + return Ok(()); + } + + let (Some(opaque_prepass_phases), Some(alpha_mask_prepass_phases)) = ( + world.get_resource::>(), + world.get_resource::>(), + ) else { + return Ok(()); + }; + + let (Some(opaque_prepass_phase), Some(alpha_mask_prepass_phase)) = ( + opaque_prepass_phases.get(&extracted_view.retained_view_entity), + alpha_mask_prepass_phases.get(&extracted_view.retained_view_entity), + ) else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let mut color_attachments = vec![ + view_prepass_textures + .normal + .as_ref() + .map(|normals_texture| normals_texture.get_attachment()), + view_prepass_textures + .motion_vectors + .as_ref() + .map(|motion_vectors_texture| motion_vectors_texture.get_attachment()), + // Use None in place of deferred attachments + None, + None, + ]; + + // If all color attachments are none: clear the color attachment list so that no fragment shader is required + if color_attachments.iter().all(Option::is_none) { + color_attachments.clear(); + } + + let depth_stencil_attachment = Some(view_depth_texture.get_attachment(StoreOp::Store)); + + let view_entity = graph.view_entity(); + render_context.add_command_buffer_generation_task(move |render_device| { + #[cfg(feature = "trace")] + let _prepass_span = info_span!("prepass").entered(); + + // Command encoder setup + let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("prepass_command_encoder"), + }); + + // Render pass setup + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { + label: Some(label), + color_attachments: &color_attachments, + depth_stencil_attachment, + timestamp_writes: None, + occlusion_query_set: None, + }); + + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); + let pass_span = diagnostics.pass_span(&mut render_pass, label); + + if let Some(viewport) = + Viewport::from_viewport_and_override(camera.viewport.as_ref(), resolution_override) + { + render_pass.set_camera_viewport(&viewport); + } + + // Opaque draws + if !opaque_prepass_phase.is_empty() { + #[cfg(feature = "trace")] + let _opaque_prepass_span = info_span!("opaque_prepass").entered(); + if let Err(err) = opaque_prepass_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the opaque prepass phase {err:?}"); + } + } + + // Alpha masked draws + if !alpha_mask_prepass_phase.is_empty() { + #[cfg(feature = "trace")] + let _alpha_mask_prepass_span = info_span!("alpha_mask_prepass").entered(); + if let Err(err) = alpha_mask_prepass_phase.render(&mut render_pass, world, view_entity) + { + error!("Error encountered while rendering the alpha mask prepass phase {err:?}"); + } + } + + // Skybox draw using a fullscreen triangle + if let ( + Some(skybox_prepass_pipeline), + Some(skybox_prepass_bind_group), + Some(view_prev_uniform_offset), + ) = ( + skybox_prepass_pipeline, + skybox_prepass_bind_group, + view_prev_uniform_offset, + ) { + let pipeline_cache = world.resource::(); + if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_prepass_pipeline.0) { + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group( + 0, + &skybox_prepass_bind_group.0, + &[view_uniform_offset.offset, view_prev_uniform_offset.offset], + ); + render_pass.draw(0..3, 0..1); + } + } + + pass_span.end(&mut render_pass); + drop(render_pass); + + // After rendering to the view depth texture, copy it to the prepass depth texture if deferred isn't going to + if deferred_prepass.is_none() + && let Some(prepass_depth_texture) = &view_prepass_textures.depth + { + command_encoder.copy_texture_to_texture( + view_depth_texture.texture.as_image_copy(), + prepass_depth_texture.texture.texture.as_image_copy(), + view_prepass_textures.size, + ); + } + + command_encoder.finish() + }); + + Ok(()) +} diff --git a/crates/libmarathon/src/render/render_asset.rs b/crates/libmarathon/src/render/render_asset.rs new file mode 100644 index 0000000..7e443f4 --- /dev/null +++ b/crates/libmarathon/src/render/render_asset.rs @@ -0,0 +1,516 @@ +use crate::render::{ + render_resource::AsBindGroupError, Extract, ExtractSchedule, MainWorld, Render, RenderApp, + RenderSystems, +}; +use bevy_app::{App, Plugin, SubApp}; +use bevy_asset::{Asset, AssetEvent, AssetId, Assets, RenderAssetUsages}; +use bevy_ecs::{ + prelude::{Commands, IntoScheduleConfigs, MessageReader, Res, ResMut, Resource}, + schedule::{ScheduleConfigs, SystemSet}, + system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState}, + world::{FromWorld, Mut}, +}; +use bevy_platform::collections::{HashMap, HashSet}; +use core::marker::PhantomData; +use core::sync::atomic::{AtomicUsize, Ordering}; +use thiserror::Error; +use tracing::{debug, error}; + +#[derive(Debug, Error)] +pub enum PrepareAssetError { + #[error("Failed to prepare asset")] + RetryNextUpdate(E), + #[error("Failed to build bind group: {0}")] + AsBindGroupError(AsBindGroupError), +} + +/// The system set during which we extract modified assets to the render world. +#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] +pub struct AssetExtractionSystems; + +/// Deprecated alias for [`AssetExtractionSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `AssetExtractionSystems`.")] +pub type ExtractAssetsSet = AssetExtractionSystems; + +/// Describes how an asset gets extracted and prepared for rendering. +/// +/// In the [`ExtractSchedule`] step the [`RenderAsset::SourceAsset`] is transferred +/// from the "main world" into the "render world". +/// +/// After that in the [`RenderSystems::PrepareAssets`] step the extracted asset +/// is transformed into its GPU-representation of type [`RenderAsset`]. +pub trait RenderAsset: Send + Sync + 'static + Sized { + /// The representation of the asset in the "main world". + type SourceAsset: Asset + Clone; + + /// Specifies all ECS data required by [`RenderAsset::prepare_asset`]. + /// + /// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`]. + type Param: SystemParam; + + /// Whether or not to unload the asset after extracting it to the render world. + #[inline] + fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages { + RenderAssetUsages::default() + } + + /// Size of the data the asset will upload to the gpu. Specifying a return value + /// will allow the asset to be throttled via [`RenderAssetBytesPerFrame`]. + #[inline] + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + fn byte_len(source_asset: &Self::SourceAsset) -> Option { + None + } + + /// Prepares the [`RenderAsset::SourceAsset`] for the GPU by transforming it into a [`RenderAsset`]. + /// + /// ECS data may be accessed via `param`. + fn prepare_asset( + source_asset: Self::SourceAsset, + asset_id: AssetId, + param: &mut SystemParamItem, + previous_asset: Option<&Self>, + ) -> Result>; + + /// Called whenever the [`RenderAsset::SourceAsset`] has been removed. + /// + /// You can implement this method if you need to access ECS data (via + /// `_param`) in order to perform cleanup tasks when the asset is removed. + /// + /// The default implementation does nothing. + fn unload_asset( + _source_asset: AssetId, + _param: &mut SystemParamItem, + ) { + } +} + +/// This plugin extracts the changed assets from the "app world" into the "render world" +/// and prepares them for the GPU. They can then be accessed from the [`RenderAssets`] resource. +/// +/// Therefore it sets up the [`ExtractSchedule`] and +/// [`RenderSystems::PrepareAssets`] steps for the specified [`RenderAsset`]. +/// +/// The `AFTER` generic parameter can be used to specify that `A::prepare_asset` should not be run until +/// `prepare_assets::` has completed. This allows the `prepare_asset` function to depend on another +/// prepared [`RenderAsset`], for example `Mesh::prepare_asset` relies on `RenderAssets::` for morph +/// targets, so the plugin is created as `RenderAssetPlugin::::default()`. +pub struct RenderAssetPlugin { + phantom: PhantomData (A, AFTER)>, +} + +impl Default + for RenderAssetPlugin +{ + fn default() -> Self { + Self { + phantom: Default::default(), + } + } +} + +impl Plugin + for RenderAssetPlugin +{ + fn build(&self, app: &mut App) { + app.init_resource::>(); + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::>() + .init_resource::>() + .init_resource::>() + .add_systems( + ExtractSchedule, + extract_render_asset::.in_set(AssetExtractionSystems), + ); + AFTER::register_system( + render_app, + prepare_assets::.in_set(RenderSystems::PrepareAssets), + ); + } + } +} + +// helper to allow specifying dependencies between render assets +pub trait RenderAssetDependency { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs); +} + +impl RenderAssetDependency for () { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs) { + render_app.add_systems(Render, system); + } +} + +impl RenderAssetDependency for A { + fn register_system(render_app: &mut SubApp, system: ScheduleConfigs) { + render_app.add_systems(Render, system.after(prepare_assets::)); + } +} + +/// Temporarily stores the extracted and removed assets of the current frame. +#[derive(Resource)] +pub struct ExtractedAssets { + /// The assets extracted this frame. + /// + /// These are assets that were either added or modified this frame. + pub extracted: Vec<(AssetId, A::SourceAsset)>, + + /// IDs of the assets that were removed this frame. + /// + /// These assets will not be present in [`ExtractedAssets::extracted`]. + pub removed: HashSet>, + + /// IDs of the assets that were modified this frame. + pub modified: HashSet>, + + /// IDs of the assets that were added this frame. + pub added: HashSet>, +} + +impl Default for ExtractedAssets { + fn default() -> Self { + Self { + extracted: Default::default(), + removed: Default::default(), + modified: Default::default(), + added: Default::default(), + } + } +} + +/// Stores all GPU representations ([`RenderAsset`]) +/// of [`RenderAsset::SourceAsset`] as long as they exist. +#[derive(Resource)] +pub struct RenderAssets(HashMap, A>); + +impl Default for RenderAssets { + fn default() -> Self { + Self(Default::default()) + } +} + +impl RenderAssets { + pub fn get(&self, id: impl Into>) -> Option<&A> { + self.0.get(&id.into()) + } + + pub fn get_mut(&mut self, id: impl Into>) -> Option<&mut A> { + self.0.get_mut(&id.into()) + } + + pub fn insert(&mut self, id: impl Into>, value: A) -> Option { + self.0.insert(id.into(), value) + } + + pub fn remove(&mut self, id: impl Into>) -> Option { + self.0.remove(&id.into()) + } + + pub fn iter(&self) -> impl Iterator, &A)> { + self.0.iter().map(|(k, v)| (*k, v)) + } + + pub fn iter_mut(&mut self) -> impl Iterator, &mut A)> { + self.0.iter_mut().map(|(k, v)| (*k, v)) + } +} + +#[derive(Resource)] +struct CachedExtractRenderAssetSystemState { + state: SystemState<( + MessageReader<'static, 'static, AssetEvent>, + ResMut<'static, Assets>, + )>, +} + +impl FromWorld for CachedExtractRenderAssetSystemState { + fn from_world(world: &mut bevy_ecs::world::World) -> Self { + Self { + state: SystemState::new(world), + } + } +} + +/// This system extracts all created or modified assets of the corresponding [`RenderAsset::SourceAsset`] type +/// into the "render world". +pub(crate) fn extract_render_asset( + mut commands: Commands, + mut main_world: ResMut, +) { + main_world.resource_scope( + |world, mut cached_state: Mut>| { + let (mut events, mut assets) = cached_state.state.get_mut(world); + + let mut needs_extracting = >::default(); + let mut removed = >::default(); + let mut modified = >::default(); + + for event in events.read() { + #[expect( + clippy::match_same_arms, + reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon." + )] + match event { + AssetEvent::Added { id } => { + needs_extracting.insert(*id); + } + AssetEvent::Modified { id } => { + needs_extracting.insert(*id); + modified.insert(*id); + } + AssetEvent::Removed { .. } => { + // We don't care that the asset was removed from Assets in the main world. + // An asset is only removed from RenderAssets when its last handle is dropped (AssetEvent::Unused). + } + AssetEvent::Unused { id } => { + needs_extracting.remove(id); + modified.remove(id); + removed.insert(*id); + } + AssetEvent::LoadedWithDependencies { .. } => { + // TODO: handle this + } + } + } + + let mut extracted_assets = Vec::new(); + let mut added = >::default(); + for id in needs_extracting.drain() { + if let Some(asset) = assets.get(id) { + let asset_usage = A::asset_usage(asset); + if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) { + if asset_usage == RenderAssetUsages::RENDER_WORLD { + if let Some(asset) = assets.remove(id) { + extracted_assets.push((id, asset)); + added.insert(id); + } + } else { + extracted_assets.push((id, asset.clone())); + added.insert(id); + } + } + } + } + + commands.insert_resource(ExtractedAssets:: { + extracted: extracted_assets, + removed, + modified, + added, + }); + cached_state.state.apply(world); + }, + ); +} + +// TODO: consider storing inside system? +/// All assets that should be prepared next frame. +#[derive(Resource)] +pub struct PrepareNextFrameAssets { + assets: Vec<(AssetId, A::SourceAsset)>, +} + +impl Default for PrepareNextFrameAssets { + fn default() -> Self { + Self { + assets: Default::default(), + } + } +} + +/// This system prepares all assets of the corresponding [`RenderAsset::SourceAsset`] type +/// which where extracted this frame for the GPU. +pub fn prepare_assets( + mut extracted_assets: ResMut>, + mut render_assets: ResMut>, + mut prepare_next_frame: ResMut>, + param: StaticSystemParam<::Param>, + bpf: Res, +) { + let mut wrote_asset_count = 0; + + let mut param = param.into_inner(); + let queued_assets = core::mem::take(&mut prepare_next_frame.assets); + for (id, extracted_asset) in queued_assets { + if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) { + // skip previous frame's assets that have been removed or updated + continue; + } + + let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { + // we could check if available bytes > byte_len here, but we want to make some + // forward progress even if the asset is larger than the max bytes per frame. + // this way we always write at least one (sized) asset per frame. + // in future we could also consider partial asset uploads. + if bpf.exhausted() { + prepare_next_frame.assets.push((id, extracted_asset)); + continue; + } + size + } else { + 0 + }; + + let previous_asset = render_assets.get(id); + match A::prepare_asset(extracted_asset, id, &mut param, previous_asset) { + Ok(prepared_asset) => { + render_assets.insert(id, prepared_asset); + bpf.write_bytes(write_bytes); + wrote_asset_count += 1; + } + Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { + prepare_next_frame.assets.push((id, extracted_asset)); + } + Err(PrepareAssetError::AsBindGroupError(e)) => { + error!( + "{} Bind group construction failed: {e}", + core::any::type_name::() + ); + } + } + } + + for removed in extracted_assets.removed.drain() { + render_assets.remove(removed); + A::unload_asset(removed, &mut param); + } + + for (id, extracted_asset) in extracted_assets.extracted.drain(..) { + // we remove previous here to ensure that if we are updating the asset then + // any users will not see the old asset after a new asset is extracted, + // even if the new asset is not yet ready or we are out of bytes to write. + let previous_asset = render_assets.remove(id); + + let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { + if bpf.exhausted() { + prepare_next_frame.assets.push((id, extracted_asset)); + continue; + } + size + } else { + 0 + }; + + match A::prepare_asset(extracted_asset, id, &mut param, previous_asset.as_ref()) { + Ok(prepared_asset) => { + render_assets.insert(id, prepared_asset); + bpf.write_bytes(write_bytes); + wrote_asset_count += 1; + } + Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { + prepare_next_frame.assets.push((id, extracted_asset)); + } + Err(PrepareAssetError::AsBindGroupError(e)) => { + error!( + "{} Bind group construction failed: {e}", + core::any::type_name::() + ); + } + } + } + + if bpf.exhausted() && !prepare_next_frame.assets.is_empty() { + debug!( + "{} write budget exhausted with {} assets remaining (wrote {})", + core::any::type_name::(), + prepare_next_frame.assets.len(), + wrote_asset_count + ); + } +} + +pub fn reset_render_asset_bytes_per_frame( + mut bpf_limiter: ResMut, +) { + bpf_limiter.reset(); +} + +pub fn extract_render_asset_bytes_per_frame( + bpf: Extract>, + mut bpf_limiter: ResMut, +) { + bpf_limiter.max_bytes = bpf.max_bytes; +} + +/// A resource that defines the amount of data allowed to be transferred from CPU to GPU +/// each frame, preventing choppy frames at the cost of waiting longer for GPU assets +/// to become available. +#[derive(Resource, Default)] +pub struct RenderAssetBytesPerFrame { + pub max_bytes: Option, +} + +impl RenderAssetBytesPerFrame { + /// `max_bytes`: the number of bytes to write per frame. + /// + /// This is a soft limit: only full assets are written currently, uploading stops + /// after the first asset that exceeds the limit. + /// + /// To participate, assets should implement [`RenderAsset::byte_len`]. If the default + /// is not overridden, the assets are assumed to be small enough to upload without restriction. + pub fn new(max_bytes: usize) -> Self { + Self { + max_bytes: Some(max_bytes), + } + } +} + +/// A render-world resource that facilitates limiting the data transferred from CPU to GPU +/// each frame, preventing choppy frames at the cost of waiting longer for GPU assets +/// to become available. +#[derive(Resource, Default)] +pub struct RenderAssetBytesPerFrameLimiter { + /// Populated by [`RenderAssetBytesPerFrame`] during extraction. + pub max_bytes: Option, + /// Bytes written this frame. + pub bytes_written: AtomicUsize, +} + +impl RenderAssetBytesPerFrameLimiter { + /// Reset the available bytes. Called once per frame during extraction by [`crate::RenderPlugin`]. + pub fn reset(&mut self) { + if self.max_bytes.is_none() { + return; + } + self.bytes_written.store(0, Ordering::Relaxed); + } + + /// Check how many bytes are available for writing. + pub fn available_bytes(&self, required_bytes: usize) -> usize { + if let Some(max_bytes) = self.max_bytes { + let total_bytes = self + .bytes_written + .fetch_add(required_bytes, Ordering::Relaxed); + + // The bytes available is the inverse of the amount we overshot max_bytes + if total_bytes >= max_bytes { + required_bytes.saturating_sub(total_bytes - max_bytes) + } else { + required_bytes + } + } else { + required_bytes + } + } + + /// Decreases the available bytes for the current frame. + pub(crate) fn write_bytes(&self, bytes: usize) { + if self.max_bytes.is_some() && bytes > 0 { + self.bytes_written.fetch_add(bytes, Ordering::Relaxed); + } + } + + /// Returns `true` if there are no remaining bytes available for writing this frame. + pub(crate) fn exhausted(&self) -> bool { + if let Some(max_bytes) = self.max_bytes { + let bytes_written = self.bytes_written.load(Ordering::Relaxed); + bytes_written >= max_bytes + } else { + false + } + } +} diff --git a/crates/libmarathon/src/render/render_graph/app.rs b/crates/libmarathon/src/render/render_graph/app.rs new file mode 100644 index 0000000..879f28f --- /dev/null +++ b/crates/libmarathon/src/render/render_graph/app.rs @@ -0,0 +1,174 @@ +use bevy_app::{App, SubApp}; +use bevy_ecs::world::{FromWorld, World}; +use tracing::warn; + +use super::{IntoRenderNodeArray, Node, RenderGraph, RenderLabel, RenderSubGraph}; + +/// Adds common [`RenderGraph`] operations to [`SubApp`] (and [`App`]). +pub trait RenderGraphExt { + // Add a sub graph to the [`RenderGraph`] + fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self; + /// Add a [`Node`] to the [`RenderGraph`]: + /// * Create the [`Node`] using the [`FromWorld`] implementation + /// * Add it to the graph + fn add_render_graph_node( + &mut self, + sub_graph: impl RenderSubGraph, + node_label: impl RenderLabel, + ) -> &mut Self; + /// Automatically add the required node edges based on the given ordering + fn add_render_graph_edges( + &mut self, + sub_graph: impl RenderSubGraph, + edges: impl IntoRenderNodeArray, + ) -> &mut Self; + + /// Add node edge to the specified graph + fn add_render_graph_edge( + &mut self, + sub_graph: impl RenderSubGraph, + output_node: impl RenderLabel, + input_node: impl RenderLabel, + ) -> &mut Self; +} + +impl RenderGraphExt for World { + fn add_render_graph_node( + &mut self, + sub_graph: impl RenderSubGraph, + node_label: impl RenderLabel, + ) -> &mut Self { + let sub_graph = sub_graph.intern(); + let node = T::from_world(self); + let mut render_graph = self.get_resource_mut::().expect( + "RenderGraph not found. Make sure you are using add_render_graph_node on the RenderApp", + ); + if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) { + graph.add_node(node_label, node); + } else { + warn!( + "Tried adding a render graph node to {sub_graph:?} but the sub graph doesn't exist" + ); + } + self + } + + #[track_caller] + fn add_render_graph_edges( + &mut self, + sub_graph: impl RenderSubGraph, + edges: impl IntoRenderNodeArray, + ) -> &mut Self { + let sub_graph = sub_graph.intern(); + let mut render_graph = self.get_resource_mut::().expect( + "RenderGraph not found. Make sure you are using add_render_graph_edges on the RenderApp", + ); + if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) { + graph.add_node_edges(edges); + } else { + warn!( + "Tried adding render graph edges to {sub_graph:?} but the sub graph doesn't exist" + ); + } + self + } + + fn add_render_graph_edge( + &mut self, + sub_graph: impl RenderSubGraph, + output_node: impl RenderLabel, + input_node: impl RenderLabel, + ) -> &mut Self { + let sub_graph = sub_graph.intern(); + let mut render_graph = self.get_resource_mut::().expect( + "RenderGraph not found. Make sure you are using add_render_graph_edge on the RenderApp", + ); + if let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) { + graph.add_node_edge(output_node, input_node); + } else { + warn!( + "Tried adding a render graph edge to {sub_graph:?} but the sub graph doesn't exist" + ); + } + self + } + + fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self { + let mut render_graph = self.get_resource_mut::().expect( + "RenderGraph not found. Make sure you are using add_render_sub_graph on the RenderApp", + ); + render_graph.add_sub_graph(sub_graph, RenderGraph::default()); + self + } +} + +impl RenderGraphExt for SubApp { + fn add_render_graph_node( + &mut self, + sub_graph: impl RenderSubGraph, + node_label: impl RenderLabel, + ) -> &mut Self { + World::add_render_graph_node::(self.world_mut(), sub_graph, node_label); + self + } + + fn add_render_graph_edge( + &mut self, + sub_graph: impl RenderSubGraph, + output_node: impl RenderLabel, + input_node: impl RenderLabel, + ) -> &mut Self { + World::add_render_graph_edge(self.world_mut(), sub_graph, output_node, input_node); + self + } + + #[track_caller] + fn add_render_graph_edges( + &mut self, + sub_graph: impl RenderSubGraph, + edges: impl IntoRenderNodeArray, + ) -> &mut Self { + World::add_render_graph_edges(self.world_mut(), sub_graph, edges); + self + } + + fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self { + World::add_render_sub_graph(self.world_mut(), sub_graph); + self + } +} + +impl RenderGraphExt for App { + fn add_render_graph_node( + &mut self, + sub_graph: impl RenderSubGraph, + node_label: impl RenderLabel, + ) -> &mut Self { + World::add_render_graph_node::(self.world_mut(), sub_graph, node_label); + self + } + + fn add_render_graph_edge( + &mut self, + sub_graph: impl RenderSubGraph, + output_node: impl RenderLabel, + input_node: impl RenderLabel, + ) -> &mut Self { + World::add_render_graph_edge(self.world_mut(), sub_graph, output_node, input_node); + self + } + + fn add_render_graph_edges( + &mut self, + sub_graph: impl RenderSubGraph, + edges: impl IntoRenderNodeArray, + ) -> &mut Self { + World::add_render_graph_edges(self.world_mut(), sub_graph, edges); + self + } + + fn add_render_sub_graph(&mut self, sub_graph: impl RenderSubGraph) -> &mut Self { + World::add_render_sub_graph(self.world_mut(), sub_graph); + self + } +} diff --git a/crates/libmarathon/src/render/render_graph/camera_driver_node.rs b/crates/libmarathon/src/render/render_graph/camera_driver_node.rs new file mode 100644 index 0000000..0f52396 --- /dev/null +++ b/crates/libmarathon/src/render/render_graph/camera_driver_node.rs @@ -0,0 +1,99 @@ +use crate::render::{ + camera::{ExtractedCamera, SortedCameras}, + render_graph::{Node, NodeRunError, RenderGraphContext}, + renderer::RenderContext, + view::ExtractedWindows, +}; +use bevy_camera::{ClearColor, NormalizedRenderTarget}; +use bevy_ecs::{entity::ContainsEntity, prelude::QueryState, world::World}; +use bevy_platform::collections::HashSet; +use wgpu::{LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor, StoreOp}; + +pub struct CameraDriverNode { + cameras: QueryState<&'static ExtractedCamera>, +} + +impl CameraDriverNode { + pub fn new(world: &mut World) -> Self { + Self { + cameras: world.query(), + } + } +} + +impl Node for CameraDriverNode { + fn update(&mut self, world: &mut World) { + self.cameras.update_archetypes(world); + } + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let sorted_cameras = world.resource::(); + let windows = world.resource::(); + let mut camera_windows = >::default(); + for sorted_camera in &sorted_cameras.0 { + let Ok(camera) = self.cameras.get_manual(world, sorted_camera.entity) else { + continue; + }; + + let mut run_graph = true; + if let Some(NormalizedRenderTarget::Window(window_ref)) = camera.target { + let window_entity = window_ref.entity(); + if windows + .windows + .get(&window_entity) + .is_some_and(|w| w.physical_width > 0 && w.physical_height > 0) + { + camera_windows.insert(window_entity); + } else { + // The window doesn't exist anymore or zero-sized so we don't need to run the graph + run_graph = false; + } + } + if run_graph { + graph.run_sub_graph(camera.render_graph, vec![], Some(sorted_camera.entity))?; + } + } + + let clear_color_global = world.resource::(); + + // wgpu (and some backends) require doing work for swap chains if you call `get_current_texture()` and `present()` + // This ensures that Bevy doesn't crash, even when there are no cameras (and therefore no work submitted). + for (id, window) in world.resource::().iter() { + if camera_windows.contains(id) && render_context.has_commands() { + continue; + } + + let Some(swap_chain_texture) = &window.swap_chain_texture_view else { + continue; + }; + + #[cfg(feature = "trace")] + let _span = tracing::info_span!("no_camera_clear_pass").entered(); + let pass_descriptor = RenderPassDescriptor { + label: Some("no_camera_clear_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: swap_chain_texture, + depth_slice: None, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(clear_color_global.to_linear().into()), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + } + + Ok(()) + } +} diff --git a/crates/libmarathon/src/render/render_graph/context.rs b/crates/libmarathon/src/render/render_graph/context.rs new file mode 100644 index 0000000..0fc508e --- /dev/null +++ b/crates/libmarathon/src/render/render_graph/context.rs @@ -0,0 +1,283 @@ +use crate::render::{ + render_graph::{NodeState, RenderGraph, SlotInfos, SlotLabel, SlotType, SlotValue}, + render_resource::{Buffer, Sampler, TextureView}, +}; +use std::borrow::Cow; +use bevy_ecs::{entity::Entity, intern::Interned}; +use thiserror::Error; + +use super::{InternedRenderSubGraph, RenderLabel, RenderSubGraph}; + +/// A command that signals the graph runner to run the sub graph corresponding to the `sub_graph` +/// with the specified `inputs` next. +pub struct RunSubGraph { + pub sub_graph: InternedRenderSubGraph, + pub inputs: Vec, + pub view_entity: Option, +} + +/// The context with all graph information required to run a [`Node`](super::Node). +/// This context is created for each node by the render graph runner. +/// +/// The slot input can be read from here and the outputs must be written back to the context for +/// passing them onto the next node. +/// +/// Sub graphs can be queued for running by adding a [`RunSubGraph`] command to the context. +/// After the node has finished running the graph runner is responsible for executing the sub graphs. +pub struct RenderGraphContext<'a> { + graph: &'a RenderGraph, + node: &'a NodeState, + inputs: &'a [SlotValue], + outputs: &'a mut [Option], + run_sub_graphs: Vec, + /// The `view_entity` associated with the render graph being executed + /// This is optional because you aren't required to have a `view_entity` for a node. + /// For example, compute shader nodes don't have one. + /// It should always be set when the [`RenderGraph`] is running on a View. + view_entity: Option, +} + +impl<'a> RenderGraphContext<'a> { + /// Creates a new render graph context for the `node`. + pub fn new( + graph: &'a RenderGraph, + node: &'a NodeState, + inputs: &'a [SlotValue], + outputs: &'a mut [Option], + ) -> Self { + Self { + graph, + node, + inputs, + outputs, + run_sub_graphs: Vec::new(), + view_entity: None, + } + } + + /// Returns the input slot values for the node. + #[inline] + pub fn inputs(&self) -> &[SlotValue] { + self.inputs + } + + /// Returns the [`SlotInfos`] of the inputs. + pub fn input_info(&self) -> &SlotInfos { + &self.node.input_slots + } + + /// Returns the [`SlotInfos`] of the outputs. + pub fn output_info(&self) -> &SlotInfos { + &self.node.output_slots + } + + /// Retrieves the input slot value referenced by the `label`. + pub fn get_input(&self, label: impl Into) -> Result<&SlotValue, InputSlotError> { + let label = label.into(); + let index = self + .input_info() + .get_slot_index(label.clone()) + .ok_or(InputSlotError::InvalidSlot(label))?; + Ok(&self.inputs[index]) + } + + // TODO: should this return an Arc or a reference? + /// Retrieves the input slot value referenced by the `label` as a [`TextureView`]. + pub fn get_input_texture( + &self, + label: impl Into, + ) -> Result<&TextureView, InputSlotError> { + let label = label.into(); + match self.get_input(label.clone())? { + SlotValue::TextureView(value) => Ok(value), + value => Err(InputSlotError::MismatchedSlotType { + label, + actual: value.slot_type(), + expected: SlotType::TextureView, + }), + } + } + + /// Retrieves the input slot value referenced by the `label` as a [`Sampler`]. + pub fn get_input_sampler( + &self, + label: impl Into, + ) -> Result<&Sampler, InputSlotError> { + let label = label.into(); + match self.get_input(label.clone())? { + SlotValue::Sampler(value) => Ok(value), + value => Err(InputSlotError::MismatchedSlotType { + label, + actual: value.slot_type(), + expected: SlotType::Sampler, + }), + } + } + + /// Retrieves the input slot value referenced by the `label` as a [`Buffer`]. + pub fn get_input_buffer(&self, label: impl Into) -> Result<&Buffer, InputSlotError> { + let label = label.into(); + match self.get_input(label.clone())? { + SlotValue::Buffer(value) => Ok(value), + value => Err(InputSlotError::MismatchedSlotType { + label, + actual: value.slot_type(), + expected: SlotType::Buffer, + }), + } + } + + /// Retrieves the input slot value referenced by the `label` as an [`Entity`]. + pub fn get_input_entity(&self, label: impl Into) -> Result { + let label = label.into(); + match self.get_input(label.clone())? { + SlotValue::Entity(value) => Ok(*value), + value => Err(InputSlotError::MismatchedSlotType { + label, + actual: value.slot_type(), + expected: SlotType::Entity, + }), + } + } + + /// Sets the output slot value referenced by the `label`. + pub fn set_output( + &mut self, + label: impl Into, + value: impl Into, + ) -> Result<(), OutputSlotError> { + let label = label.into(); + let value = value.into(); + let slot_index = self + .output_info() + .get_slot_index(label.clone()) + .ok_or_else(|| OutputSlotError::InvalidSlot(label.clone()))?; + let slot = self + .output_info() + .get_slot(slot_index) + .expect("slot is valid"); + if value.slot_type() != slot.slot_type { + return Err(OutputSlotError::MismatchedSlotType { + label, + actual: slot.slot_type, + expected: value.slot_type(), + }); + } + self.outputs[slot_index] = Some(value); + Ok(()) + } + + pub fn view_entity(&self) -> Entity { + self.view_entity.unwrap() + } + + pub fn get_view_entity(&self) -> Option { + self.view_entity + } + + pub fn set_view_entity(&mut self, view_entity: Entity) { + self.view_entity = Some(view_entity); + } + + /// Queues up a sub graph for execution after the node has finished running. + pub fn run_sub_graph( + &mut self, + name: impl RenderSubGraph, + inputs: Vec, + view_entity: Option, + ) -> Result<(), RunSubGraphError> { + let name = name.intern(); + let sub_graph = self + .graph + .get_sub_graph(name) + .ok_or(RunSubGraphError::MissingSubGraph(name))?; + if let Some(input_node) = sub_graph.get_input_node() { + for (i, input_slot) in input_node.input_slots.iter().enumerate() { + if let Some(input_value) = inputs.get(i) { + if input_slot.slot_type != input_value.slot_type() { + return Err(RunSubGraphError::MismatchedInputSlotType { + graph_name: name, + slot_index: i, + actual: input_value.slot_type(), + expected: input_slot.slot_type, + label: input_slot.name.clone().into(), + }); + } + } else { + return Err(RunSubGraphError::MissingInput { + slot_index: i, + slot_name: input_slot.name.clone(), + graph_name: name, + }); + } + } + } else if !inputs.is_empty() { + return Err(RunSubGraphError::SubGraphHasNoInputs(name)); + } + + self.run_sub_graphs.push(RunSubGraph { + sub_graph: name, + inputs, + view_entity, + }); + + Ok(()) + } + + /// Returns a human-readable label for this node, for debugging purposes. + pub fn label(&self) -> Interned { + self.node.label + } + + /// Finishes the context for this [`Node`](super::Node) by + /// returning the sub graphs to run next. + pub fn finish(self) -> Vec { + self.run_sub_graphs + } +} + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum RunSubGraphError { + #[error("attempted to run sub-graph `{0:?}`, but it does not exist")] + MissingSubGraph(InternedRenderSubGraph), + #[error("attempted to pass inputs to sub-graph `{0:?}`, which has no input slots")] + SubGraphHasNoInputs(InternedRenderSubGraph), + #[error("sub graph (name: `{graph_name:?}`) could not be run because slot `{slot_name}` at index {slot_index} has no value")] + MissingInput { + slot_index: usize, + slot_name: Cow<'static, str>, + graph_name: InternedRenderSubGraph, + }, + #[error("attempted to use the wrong type for input slot")] + MismatchedInputSlotType { + graph_name: InternedRenderSubGraph, + slot_index: usize, + label: SlotLabel, + expected: SlotType, + actual: SlotType, + }, +} + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum OutputSlotError { + #[error("output slot `{0:?}` does not exist")] + InvalidSlot(SlotLabel), + #[error("attempted to output a value of type `{actual}` to output slot `{label:?}`, which has type `{expected}`")] + MismatchedSlotType { + label: SlotLabel, + expected: SlotType, + actual: SlotType, + }, +} + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum InputSlotError { + #[error("input slot `{0:?}` does not exist")] + InvalidSlot(SlotLabel), + #[error("attempted to retrieve a value of type `{actual}` from input slot `{label:?}`, which has type `{expected}`")] + MismatchedSlotType { + label: SlotLabel, + expected: SlotType, + actual: SlotType, + }, +} diff --git a/crates/libmarathon/src/render/render_graph/edge.rs b/crates/libmarathon/src/render/render_graph/edge.rs new file mode 100644 index 0000000..199b7e8 --- /dev/null +++ b/crates/libmarathon/src/render/render_graph/edge.rs @@ -0,0 +1,57 @@ +use super::InternedRenderLabel; + +/// An edge, which connects two [`Nodes`](super::Node) in +/// a [`RenderGraph`](crate::render_graph::RenderGraph). +/// +/// They are used to describe the ordering (which node has to run first) +/// and may be of two kinds: [`NodeEdge`](Self::NodeEdge) and [`SlotEdge`](Self::SlotEdge). +/// +/// Edges are added via the [`RenderGraph::add_node_edge`] and the +/// [`RenderGraph::add_slot_edge`] methods. +/// +/// The former simply states that the `output_node` has to be run before the `input_node`, +/// while the later connects an output slot of the `output_node` +/// with an input slot of the `input_node` to pass additional data along. +/// For more information see [`SlotType`](super::SlotType). +/// +/// [`RenderGraph::add_node_edge`]: crate::render_graph::RenderGraph::add_node_edge +/// [`RenderGraph::add_slot_edge`]: crate::render_graph::RenderGraph::add_slot_edge +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Edge { + /// An edge describing to ordering of both nodes (`output_node` before `input_node`) + /// and connecting the output slot at the `output_index` of the `output_node` + /// with the slot at the `input_index` of the `input_node`. + SlotEdge { + input_node: InternedRenderLabel, + input_index: usize, + output_node: InternedRenderLabel, + output_index: usize, + }, + /// An edge describing to ordering of both nodes (`output_node` before `input_node`). + NodeEdge { + input_node: InternedRenderLabel, + output_node: InternedRenderLabel, + }, +} + +impl Edge { + /// Returns the id of the `input_node`. + pub fn get_input_node(&self) -> InternedRenderLabel { + match self { + Edge::SlotEdge { input_node, .. } | Edge::NodeEdge { input_node, .. } => *input_node, + } + } + + /// Returns the id of the `output_node`. + pub fn get_output_node(&self) -> InternedRenderLabel { + match self { + Edge::SlotEdge { output_node, .. } | Edge::NodeEdge { output_node, .. } => *output_node, + } + } +} + +#[derive(PartialEq, Eq)] +pub enum EdgeExistence { + Exists, + DoesNotExist, +} diff --git a/crates/libmarathon/src/render/render_graph/graph.rs b/crates/libmarathon/src/render/render_graph/graph.rs new file mode 100644 index 0000000..83e8288 --- /dev/null +++ b/crates/libmarathon/src/render/render_graph/graph.rs @@ -0,0 +1,918 @@ +use crate::render::{ + render_graph::{ + Edge, Node, NodeRunError, NodeState, RenderGraphContext, RenderGraphError, RenderLabel, + SlotInfo, SlotLabel, + }, + renderer::RenderContext, +}; +use bevy_ecs::{define_label, intern::Interned, prelude::World, resource::Resource}; +use bevy_platform::collections::HashMap; +use core::fmt::Debug; + +use super::{EdgeExistence, InternedRenderLabel, IntoRenderNodeArray}; + +pub use macros::RenderSubGraph; + +define_label!( + #[diagnostic::on_unimplemented( + note = "consider annotating `{Self}` with `#[derive(RenderSubGraph)]`" + )] + /// A strongly-typed class of labels used to identify a [`SubGraph`] in a render graph. + RenderSubGraph, + RENDER_SUB_GRAPH_INTERNER +); + +/// A shorthand for `Interned`. +pub type InternedRenderSubGraph = Interned; + +/// The render graph configures the modular and re-usable render logic. +/// +/// It is a retained and stateless (nodes themselves may have their own internal state) structure, +/// which can not be modified while it is executed by the graph runner. +/// +/// The render graph runner is responsible for executing the entire graph each frame. +/// It will execute each node in the graph in the correct order, based on the edges between the nodes. +/// +/// It consists of three main components: [`Nodes`](Node), [`Edges`](Edge) +/// and [`Slots`](super::SlotType). +/// +/// Nodes are responsible for generating draw calls and operating on input and output slots. +/// Edges specify the order of execution for nodes and connect input and output slots together. +/// Slots describe the render resources created or used by the nodes. +/// +/// Additionally a render graph can contain multiple sub graphs, which are run by the +/// corresponding nodes. Every render graph can have its own optional input node. +/// +/// ## Example +/// Here is a simple render graph example with two nodes connected by a node edge. +/// ```ignore +/// # TODO: Remove when #10645 is fixed +/// # use bevy_app::prelude::*; +/// # use bevy_ecs::prelude::World; +/// # use crate::render::render_graph::{RenderGraph, RenderLabel, Node, RenderGraphContext, NodeRunError}; +/// # use crate::render::renderer::RenderContext; +/// # +/// #[derive(RenderLabel)] +/// enum Labels { +/// A, +/// B, +/// } +/// +/// # struct MyNode; +/// # +/// # impl Node for MyNode { +/// # fn run(&self, graph: &mut RenderGraphContext, render_context: &mut RenderContext, world: &World) -> Result<(), NodeRunError> { +/// # unimplemented!() +/// # } +/// # } +/// # +/// let mut graph = RenderGraph::default(); +/// graph.add_node(Labels::A, MyNode); +/// graph.add_node(Labels::B, MyNode); +/// graph.add_node_edge(Labels::B, Labels::A); +/// ``` +#[derive(Resource, Default)] +pub struct RenderGraph { + nodes: HashMap, + sub_graphs: HashMap, +} + +/// The label for the input node of a graph. Used to connect other nodes to it. +#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] +pub struct GraphInput; + +impl RenderGraph { + /// Updates all nodes and sub graphs of the render graph. Should be called before executing it. + pub fn update(&mut self, world: &mut World) { + for node in self.nodes.values_mut() { + node.node.update(world); + } + + for sub_graph in self.sub_graphs.values_mut() { + sub_graph.update(world); + } + } + + /// Creates an [`GraphInputNode`] with the specified slots if not already present. + pub fn set_input(&mut self, inputs: Vec) { + assert!( + matches!( + self.get_node_state(GraphInput), + Err(RenderGraphError::InvalidNode(_)) + ), + "Graph already has an input node" + ); + + self.add_node(GraphInput, GraphInputNode { inputs }); + } + + /// Returns the [`NodeState`] of the input node of this graph. + /// + /// # See also + /// + /// - [`input_node`](Self::input_node) for an unchecked version. + #[inline] + pub fn get_input_node(&self) -> Option<&NodeState> { + self.get_node_state(GraphInput).ok() + } + + /// Returns the [`NodeState`] of the input node of this graph. + /// + /// # Panics + /// + /// Panics if there is no input node set. + /// + /// # See also + /// + /// - [`get_input_node`](Self::get_input_node) for a version which returns an [`Option`] instead. + #[inline] + pub fn input_node(&self) -> &NodeState { + self.get_input_node().unwrap() + } + + /// Adds the `node` with the `label` to the graph. + /// If the label is already present replaces it instead. + pub fn add_node(&mut self, label: impl RenderLabel, node: T) + where + T: Node, + { + let label = label.intern(); + let node_state = NodeState::new(label, node); + self.nodes.insert(label, node_state); + } + + /// Add `node_edge`s based on the order of the given `edges` array. + /// + /// Defining an edge that already exists is not considered an error with this api. + /// It simply won't create a new edge. + #[track_caller] + pub fn add_node_edges(&mut self, edges: impl IntoRenderNodeArray) { + for window in edges.into_array().windows(2) { + let [a, b] = window else { + break; + }; + if let Err(err) = self.try_add_node_edge(*a, *b) { + match err { + // Already existing edges are very easy to produce with this api + // and shouldn't cause a panic + RenderGraphError::EdgeAlreadyExists(_) => {} + _ => panic!("{err}"), + } + } + } + } + + /// Removes the `node` with the `label` from the graph. + /// If the label does not exist, nothing happens. + pub fn remove_node(&mut self, label: impl RenderLabel) -> Result<(), RenderGraphError> { + let label = label.intern(); + if let Some(node_state) = self.nodes.remove(&label) { + // Remove all edges from other nodes to this one. Note that as we're removing this + // node, we don't need to remove its input edges + for input_edge in node_state.edges.input_edges() { + match input_edge { + Edge::SlotEdge { output_node, .. } + | Edge::NodeEdge { + input_node: _, + output_node, + } => { + if let Ok(output_node) = self.get_node_state_mut(*output_node) { + output_node.edges.remove_output_edge(input_edge.clone())?; + } + } + } + } + // Remove all edges from this node to other nodes. Note that as we're removing this + // node, we don't need to remove its output edges + for output_edge in node_state.edges.output_edges() { + match output_edge { + Edge::SlotEdge { + output_node: _, + output_index: _, + input_node, + input_index: _, + } + | Edge::NodeEdge { + output_node: _, + input_node, + } => { + if let Ok(input_node) = self.get_node_state_mut(*input_node) { + input_node.edges.remove_input_edge(output_edge.clone())?; + } + } + } + } + } + + Ok(()) + } + + /// Retrieves the [`NodeState`] referenced by the `label`. + pub fn get_node_state(&self, label: impl RenderLabel) -> Result<&NodeState, RenderGraphError> { + let label = label.intern(); + self.nodes + .get(&label) + .ok_or(RenderGraphError::InvalidNode(label)) + } + + /// Retrieves the [`NodeState`] referenced by the `label` mutably. + pub fn get_node_state_mut( + &mut self, + label: impl RenderLabel, + ) -> Result<&mut NodeState, RenderGraphError> { + let label = label.intern(); + self.nodes + .get_mut(&label) + .ok_or(RenderGraphError::InvalidNode(label)) + } + + /// Retrieves the [`Node`] referenced by the `label`. + pub fn get_node(&self, label: impl RenderLabel) -> Result<&T, RenderGraphError> + where + T: Node, + { + self.get_node_state(label).and_then(|n| n.node()) + } + + /// Retrieves the [`Node`] referenced by the `label` mutably. + pub fn get_node_mut(&mut self, label: impl RenderLabel) -> Result<&mut T, RenderGraphError> + where + T: Node, + { + self.get_node_state_mut(label).and_then(|n| n.node_mut()) + } + + /// Adds the [`Edge::SlotEdge`] to the graph. This guarantees that the `output_node` + /// is run before the `input_node` and also connects the `output_slot` to the `input_slot`. + /// + /// Fails if any invalid [`RenderLabel`]s or [`SlotLabel`]s are given. + /// + /// # See also + /// + /// - [`add_slot_edge`](Self::add_slot_edge) for an infallible version. + pub fn try_add_slot_edge( + &mut self, + output_node: impl RenderLabel, + output_slot: impl Into, + input_node: impl RenderLabel, + input_slot: impl Into, + ) -> Result<(), RenderGraphError> { + let output_slot = output_slot.into(); + let input_slot = input_slot.into(); + + let output_node = output_node.intern(); + let input_node = input_node.intern(); + + let output_index = self + .get_node_state(output_node)? + .output_slots + .get_slot_index(output_slot.clone()) + .ok_or(RenderGraphError::InvalidOutputNodeSlot(output_slot))?; + let input_index = self + .get_node_state(input_node)? + .input_slots + .get_slot_index(input_slot.clone()) + .ok_or(RenderGraphError::InvalidInputNodeSlot(input_slot))?; + + let edge = Edge::SlotEdge { + output_node, + output_index, + input_node, + input_index, + }; + + self.validate_edge(&edge, EdgeExistence::DoesNotExist)?; + + { + let output_node = self.get_node_state_mut(output_node)?; + output_node.edges.add_output_edge(edge.clone())?; + } + let input_node = self.get_node_state_mut(input_node)?; + input_node.edges.add_input_edge(edge)?; + + Ok(()) + } + + /// Adds the [`Edge::SlotEdge`] to the graph. This guarantees that the `output_node` + /// is run before the `input_node` and also connects the `output_slot` to the `input_slot`. + /// + /// # Panics + /// + /// Any invalid [`RenderLabel`]s or [`SlotLabel`]s are given. + /// + /// # See also + /// + /// - [`try_add_slot_edge`](Self::try_add_slot_edge) for a fallible version. + pub fn add_slot_edge( + &mut self, + output_node: impl RenderLabel, + output_slot: impl Into, + input_node: impl RenderLabel, + input_slot: impl Into, + ) { + self.try_add_slot_edge(output_node, output_slot, input_node, input_slot) + .unwrap(); + } + + /// Removes the [`Edge::SlotEdge`] from the graph. If any nodes or slots do not exist then + /// nothing happens. + pub fn remove_slot_edge( + &mut self, + output_node: impl RenderLabel, + output_slot: impl Into, + input_node: impl RenderLabel, + input_slot: impl Into, + ) -> Result<(), RenderGraphError> { + let output_slot = output_slot.into(); + let input_slot = input_slot.into(); + + let output_node = output_node.intern(); + let input_node = input_node.intern(); + + let output_index = self + .get_node_state(output_node)? + .output_slots + .get_slot_index(output_slot.clone()) + .ok_or(RenderGraphError::InvalidOutputNodeSlot(output_slot))?; + let input_index = self + .get_node_state(input_node)? + .input_slots + .get_slot_index(input_slot.clone()) + .ok_or(RenderGraphError::InvalidInputNodeSlot(input_slot))?; + + let edge = Edge::SlotEdge { + output_node, + output_index, + input_node, + input_index, + }; + + self.validate_edge(&edge, EdgeExistence::Exists)?; + + { + let output_node = self.get_node_state_mut(output_node)?; + output_node.edges.remove_output_edge(edge.clone())?; + } + let input_node = self.get_node_state_mut(input_node)?; + input_node.edges.remove_input_edge(edge)?; + + Ok(()) + } + + /// Adds the [`Edge::NodeEdge`] to the graph. This guarantees that the `output_node` + /// is run before the `input_node`. + /// + /// Fails if any invalid [`RenderLabel`] is given. + /// + /// # See also + /// + /// - [`add_node_edge`](Self::add_node_edge) for an infallible version. + pub fn try_add_node_edge( + &mut self, + output_node: impl RenderLabel, + input_node: impl RenderLabel, + ) -> Result<(), RenderGraphError> { + let output_node = output_node.intern(); + let input_node = input_node.intern(); + + let edge = Edge::NodeEdge { + output_node, + input_node, + }; + + self.validate_edge(&edge, EdgeExistence::DoesNotExist)?; + + { + let output_node = self.get_node_state_mut(output_node)?; + output_node.edges.add_output_edge(edge.clone())?; + } + let input_node = self.get_node_state_mut(input_node)?; + input_node.edges.add_input_edge(edge)?; + + Ok(()) + } + + /// Adds the [`Edge::NodeEdge`] to the graph. This guarantees that the `output_node` + /// is run before the `input_node`. + /// + /// # Panics + /// + /// Panics if any invalid [`RenderLabel`] is given. + /// + /// # See also + /// + /// - [`try_add_node_edge`](Self::try_add_node_edge) for a fallible version. + pub fn add_node_edge(&mut self, output_node: impl RenderLabel, input_node: impl RenderLabel) { + self.try_add_node_edge(output_node, input_node).unwrap(); + } + + /// Removes the [`Edge::NodeEdge`] from the graph. If either node does not exist then nothing + /// happens. + pub fn remove_node_edge( + &mut self, + output_node: impl RenderLabel, + input_node: impl RenderLabel, + ) -> Result<(), RenderGraphError> { + let output_node = output_node.intern(); + let input_node = input_node.intern(); + + let edge = Edge::NodeEdge { + output_node, + input_node, + }; + + self.validate_edge(&edge, EdgeExistence::Exists)?; + + { + let output_node = self.get_node_state_mut(output_node)?; + output_node.edges.remove_output_edge(edge.clone())?; + } + let input_node = self.get_node_state_mut(input_node)?; + input_node.edges.remove_input_edge(edge)?; + + Ok(()) + } + + /// Verifies that the edge existence is as expected and + /// checks that slot edges are connected correctly. + pub fn validate_edge( + &mut self, + edge: &Edge, + should_exist: EdgeExistence, + ) -> Result<(), RenderGraphError> { + if should_exist == EdgeExistence::Exists && !self.has_edge(edge) { + return Err(RenderGraphError::EdgeDoesNotExist(edge.clone())); + } else if should_exist == EdgeExistence::DoesNotExist && self.has_edge(edge) { + return Err(RenderGraphError::EdgeAlreadyExists(edge.clone())); + } + + match *edge { + Edge::SlotEdge { + output_node, + output_index, + input_node, + input_index, + } => { + let output_node_state = self.get_node_state(output_node)?; + let input_node_state = self.get_node_state(input_node)?; + + let output_slot = output_node_state + .output_slots + .get_slot(output_index) + .ok_or(RenderGraphError::InvalidOutputNodeSlot(SlotLabel::Index( + output_index, + )))?; + let input_slot = input_node_state.input_slots.get_slot(input_index).ok_or( + RenderGraphError::InvalidInputNodeSlot(SlotLabel::Index(input_index)), + )?; + + if let Some(Edge::SlotEdge { + output_node: current_output_node, + .. + }) = input_node_state.edges.input_edges().iter().find(|e| { + if let Edge::SlotEdge { + input_index: current_input_index, + .. + } = e + { + input_index == *current_input_index + } else { + false + } + }) && should_exist == EdgeExistence::DoesNotExist + { + return Err(RenderGraphError::NodeInputSlotAlreadyOccupied { + node: input_node, + input_slot: input_index, + occupied_by_node: *current_output_node, + }); + } + + if output_slot.slot_type != input_slot.slot_type { + return Err(RenderGraphError::MismatchedNodeSlots { + output_node, + output_slot: output_index, + input_node, + input_slot: input_index, + }); + } + } + Edge::NodeEdge { .. } => { /* nothing to validate here */ } + } + + Ok(()) + } + + /// Checks whether the `edge` already exists in the graph. + pub fn has_edge(&self, edge: &Edge) -> bool { + let output_node_state = self.get_node_state(edge.get_output_node()); + let input_node_state = self.get_node_state(edge.get_input_node()); + if let Ok(output_node_state) = output_node_state + && output_node_state.edges.output_edges().contains(edge) + && let Ok(input_node_state) = input_node_state + && input_node_state.edges.input_edges().contains(edge) + { + return true; + } + + false + } + + /// Returns an iterator over the [`NodeStates`](NodeState). + pub fn iter_nodes(&self) -> impl Iterator { + self.nodes.values() + } + + /// Returns an iterator over the [`NodeStates`](NodeState), that allows modifying each value. + pub fn iter_nodes_mut(&mut self) -> impl Iterator { + self.nodes.values_mut() + } + + /// Returns an iterator over the sub graphs. + pub fn iter_sub_graphs(&self) -> impl Iterator { + self.sub_graphs.iter().map(|(name, graph)| (*name, graph)) + } + + /// Returns an iterator over the sub graphs, that allows modifying each value. + pub fn iter_sub_graphs_mut( + &mut self, + ) -> impl Iterator { + self.sub_graphs + .iter_mut() + .map(|(name, graph)| (*name, graph)) + } + + /// Returns an iterator over a tuple of the input edges and the corresponding output nodes + /// for the node referenced by the label. + pub fn iter_node_inputs( + &self, + label: impl RenderLabel, + ) -> Result, RenderGraphError> { + let node = self.get_node_state(label)?; + Ok(node + .edges + .input_edges() + .iter() + .map(|edge| (edge, edge.get_output_node())) + .map(move |(edge, output_node)| (edge, self.get_node_state(output_node).unwrap()))) + } + + /// Returns an iterator over a tuple of the output edges and the corresponding input nodes + /// for the node referenced by the label. + pub fn iter_node_outputs( + &self, + label: impl RenderLabel, + ) -> Result, RenderGraphError> { + let node = self.get_node_state(label)?; + Ok(node + .edges + .output_edges() + .iter() + .map(|edge| (edge, edge.get_input_node())) + .map(move |(edge, input_node)| (edge, self.get_node_state(input_node).unwrap()))) + } + + /// Adds the `sub_graph` with the `label` to the graph. + /// If the label is already present replaces it instead. + pub fn add_sub_graph(&mut self, label: impl RenderSubGraph, sub_graph: RenderGraph) { + self.sub_graphs.insert(label.intern(), sub_graph); + } + + /// Removes the `sub_graph` with the `label` from the graph. + /// If the label does not exist then nothing happens. + pub fn remove_sub_graph(&mut self, label: impl RenderSubGraph) { + self.sub_graphs.remove(&label.intern()); + } + + /// Retrieves the sub graph corresponding to the `label`. + pub fn get_sub_graph(&self, label: impl RenderSubGraph) -> Option<&RenderGraph> { + self.sub_graphs.get(&label.intern()) + } + + /// Retrieves the sub graph corresponding to the `label` mutably. + pub fn get_sub_graph_mut(&mut self, label: impl RenderSubGraph) -> Option<&mut RenderGraph> { + self.sub_graphs.get_mut(&label.intern()) + } + + /// Retrieves the sub graph corresponding to the `label`. + /// + /// # Panics + /// + /// Panics if any invalid subgraph label is given. + /// + /// # See also + /// + /// - [`get_sub_graph`](Self::get_sub_graph) for a fallible version. + pub fn sub_graph(&self, label: impl RenderSubGraph) -> &RenderGraph { + let label = label.intern(); + self.sub_graphs + .get(&label) + .unwrap_or_else(|| panic!("Subgraph {label:?} not found")) + } + + /// Retrieves the sub graph corresponding to the `label` mutably. + /// + /// # Panics + /// + /// Panics if any invalid subgraph label is given. + /// + /// # See also + /// + /// - [`get_sub_graph_mut`](Self::get_sub_graph_mut) for a fallible version. + pub fn sub_graph_mut(&mut self, label: impl RenderSubGraph) -> &mut RenderGraph { + let label = label.intern(); + self.sub_graphs + .get_mut(&label) + .unwrap_or_else(|| panic!("Subgraph {label:?} not found")) + } +} + +impl Debug for RenderGraph { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + for node in self.iter_nodes() { + writeln!(f, "{:?}", node.label)?; + writeln!(f, " in: {:?}", node.input_slots)?; + writeln!(f, " out: {:?}", node.output_slots)?; + } + + Ok(()) + } +} + +/// A [`Node`] which acts as an entry point for a [`RenderGraph`] with custom inputs. +/// It has the same input and output slots and simply copies them over when run. +pub struct GraphInputNode { + inputs: Vec, +} + +impl Node for GraphInputNode { + fn input(&self) -> Vec { + self.inputs.clone() + } + + fn output(&self) -> Vec { + self.inputs.clone() + } + + fn run( + &self, + graph: &mut RenderGraphContext, + _render_context: &mut RenderContext, + _world: &World, + ) -> Result<(), NodeRunError> { + for i in 0..graph.inputs().len() { + let input = graph.inputs()[i].clone(); + graph.set_output(i, input)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::render::{ + render_graph::{ + node::IntoRenderNodeArray, Edge, InternedRenderLabel, Node, NodeRunError, RenderGraph, + RenderGraphContext, RenderGraphError, RenderLabel, SlotInfo, SlotType, + }, + renderer::RenderContext, + }; + use bevy_ecs::world::{FromWorld, World}; + use bevy_platform::collections::HashSet; + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + enum TestLabel { + A, + B, + C, + D, + } + + #[derive(Debug)] + struct TestNode { + inputs: Vec, + outputs: Vec, + } + + impl TestNode { + pub fn new(inputs: usize, outputs: usize) -> Self { + TestNode { + inputs: (0..inputs) + .map(|i| SlotInfo::new(format!("in_{i}"), SlotType::TextureView)) + .collect(), + outputs: (0..outputs) + .map(|i| SlotInfo::new(format!("out_{i}"), SlotType::TextureView)) + .collect(), + } + } + } + + impl Node for TestNode { + fn input(&self) -> Vec { + self.inputs.clone() + } + + fn output(&self) -> Vec { + self.outputs.clone() + } + + fn run( + &self, + _: &mut RenderGraphContext, + _: &mut RenderContext, + _: &World, + ) -> Result<(), NodeRunError> { + Ok(()) + } + } + + fn input_nodes(label: impl RenderLabel, graph: &RenderGraph) -> HashSet { + graph + .iter_node_inputs(label) + .unwrap() + .map(|(_edge, node)| node.label) + .collect::>() + } + + fn output_nodes(label: impl RenderLabel, graph: &RenderGraph) -> HashSet { + graph + .iter_node_outputs(label) + .unwrap() + .map(|(_edge, node)| node.label) + .collect::>() + } + + #[test] + fn test_graph_edges() { + let mut graph = RenderGraph::default(); + graph.add_node(TestLabel::A, TestNode::new(0, 1)); + graph.add_node(TestLabel::B, TestNode::new(0, 1)); + graph.add_node(TestLabel::C, TestNode::new(1, 1)); + graph.add_node(TestLabel::D, TestNode::new(1, 0)); + + graph.add_slot_edge(TestLabel::A, "out_0", TestLabel::C, "in_0"); + graph.add_node_edge(TestLabel::B, TestLabel::C); + graph.add_slot_edge(TestLabel::C, 0, TestLabel::D, 0); + + assert!( + input_nodes(TestLabel::A, &graph).is_empty(), + "A has no inputs" + ); + assert_eq!( + output_nodes(TestLabel::A, &graph), + HashSet::from_iter((TestLabel::C,).into_array()), + "A outputs to C" + ); + + assert!( + input_nodes(TestLabel::B, &graph).is_empty(), + "B has no inputs" + ); + assert_eq!( + output_nodes(TestLabel::B, &graph), + HashSet::from_iter((TestLabel::C,).into_array()), + "B outputs to C" + ); + + assert_eq!( + input_nodes(TestLabel::C, &graph), + HashSet::from_iter((TestLabel::A, TestLabel::B).into_array()), + "A and B input to C" + ); + assert_eq!( + output_nodes(TestLabel::C, &graph), + HashSet::from_iter((TestLabel::D,).into_array()), + "C outputs to D" + ); + + assert_eq!( + input_nodes(TestLabel::D, &graph), + HashSet::from_iter((TestLabel::C,).into_array()), + "C inputs to D" + ); + assert!( + output_nodes(TestLabel::D, &graph).is_empty(), + "D has no outputs" + ); + } + + #[test] + fn test_get_node_typed() { + struct MyNode { + value: usize, + } + + impl Node for MyNode { + fn run( + &self, + _: &mut RenderGraphContext, + _: &mut RenderContext, + _: &World, + ) -> Result<(), NodeRunError> { + Ok(()) + } + } + + let mut graph = RenderGraph::default(); + + graph.add_node(TestLabel::A, MyNode { value: 42 }); + + let node: &MyNode = graph.get_node(TestLabel::A).unwrap(); + assert_eq!(node.value, 42, "node value matches"); + + let result: Result<&TestNode, RenderGraphError> = graph.get_node(TestLabel::A); + assert_eq!( + result.unwrap_err(), + RenderGraphError::WrongNodeType, + "expect a wrong node type error" + ); + } + + #[test] + fn test_slot_already_occupied() { + let mut graph = RenderGraph::default(); + + graph.add_node(TestLabel::A, TestNode::new(0, 1)); + graph.add_node(TestLabel::B, TestNode::new(0, 1)); + graph.add_node(TestLabel::C, TestNode::new(1, 1)); + + graph.add_slot_edge(TestLabel::A, 0, TestLabel::C, 0); + assert_eq!( + graph.try_add_slot_edge(TestLabel::B, 0, TestLabel::C, 0), + Err(RenderGraphError::NodeInputSlotAlreadyOccupied { + node: TestLabel::C.intern(), + input_slot: 0, + occupied_by_node: TestLabel::A.intern(), + }), + "Adding to a slot that is already occupied should return an error" + ); + } + + #[test] + fn test_edge_already_exists() { + let mut graph = RenderGraph::default(); + + graph.add_node(TestLabel::A, TestNode::new(0, 1)); + graph.add_node(TestLabel::B, TestNode::new(1, 0)); + + graph.add_slot_edge(TestLabel::A, 0, TestLabel::B, 0); + assert_eq!( + graph.try_add_slot_edge(TestLabel::A, 0, TestLabel::B, 0), + Err(RenderGraphError::EdgeAlreadyExists(Edge::SlotEdge { + output_node: TestLabel::A.intern(), + output_index: 0, + input_node: TestLabel::B.intern(), + input_index: 0, + })), + "Adding to a duplicate edge should return an error" + ); + } + + #[test] + fn test_add_node_edges() { + struct SimpleNode; + impl Node for SimpleNode { + fn run( + &self, + _graph: &mut RenderGraphContext, + _render_context: &mut RenderContext, + _world: &World, + ) -> Result<(), NodeRunError> { + Ok(()) + } + } + impl FromWorld for SimpleNode { + fn from_world(_world: &mut World) -> Self { + Self + } + } + + let mut graph = RenderGraph::default(); + graph.add_node(TestLabel::A, SimpleNode); + graph.add_node(TestLabel::B, SimpleNode); + graph.add_node(TestLabel::C, SimpleNode); + + graph.add_node_edges((TestLabel::A, TestLabel::B, TestLabel::C)); + + assert_eq!( + output_nodes(TestLabel::A, &graph), + HashSet::from_iter((TestLabel::B,).into_array()), + "A -> B" + ); + assert_eq!( + input_nodes(TestLabel::B, &graph), + HashSet::from_iter((TestLabel::A,).into_array()), + "A -> B" + ); + assert_eq!( + output_nodes(TestLabel::B, &graph), + HashSet::from_iter((TestLabel::C,).into_array()), + "B -> C" + ); + assert_eq!( + input_nodes(TestLabel::C, &graph), + HashSet::from_iter((TestLabel::B,).into_array()), + "B -> C" + ); + } +} diff --git a/crates/libmarathon/src/render/render_graph/mod.rs b/crates/libmarathon/src/render/render_graph/mod.rs new file mode 100644 index 0000000..6f98a30 --- /dev/null +++ b/crates/libmarathon/src/render/render_graph/mod.rs @@ -0,0 +1,56 @@ +mod app; +mod camera_driver_node; +mod context; +mod edge; +mod graph; +mod node; +mod node_slot; + +pub use app::*; +pub use camera_driver_node::*; +pub use context::*; +pub use edge::*; +pub use graph::*; +pub use node::*; +pub use node_slot::*; + +use thiserror::Error; + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum RenderGraphError { + #[error("node {0:?} does not exist")] + InvalidNode(InternedRenderLabel), + #[error("output node slot does not exist")] + InvalidOutputNodeSlot(SlotLabel), + #[error("input node slot does not exist")] + InvalidInputNodeSlot(SlotLabel), + #[error("node does not match the given type")] + WrongNodeType, + #[error("attempted to connect output slot {output_slot} from node {output_node:?} to incompatible input slot {input_slot} from node {input_node:?}")] + MismatchedNodeSlots { + output_node: InternedRenderLabel, + output_slot: usize, + input_node: InternedRenderLabel, + input_slot: usize, + }, + #[error("attempted to add an edge that already exists")] + EdgeAlreadyExists(Edge), + #[error("attempted to remove an edge that does not exist")] + EdgeDoesNotExist(Edge), + #[error("node {node:?} has an unconnected input slot {input_slot}")] + UnconnectedNodeInputSlot { + node: InternedRenderLabel, + input_slot: usize, + }, + #[error("node {node:?} has an unconnected output slot {output_slot}")] + UnconnectedNodeOutputSlot { + node: InternedRenderLabel, + output_slot: usize, + }, + #[error("node {node:?} input slot {input_slot} already occupied by {occupied_by_node:?}")] + NodeInputSlotAlreadyOccupied { + node: InternedRenderLabel, + input_slot: usize, + occupied_by_node: InternedRenderLabel, + }, +} diff --git a/crates/libmarathon/src/render/render_graph/node.rs b/crates/libmarathon/src/render/render_graph/node.rs new file mode 100644 index 0000000..df3ee5d --- /dev/null +++ b/crates/libmarathon/src/render/render_graph/node.rs @@ -0,0 +1,420 @@ +use crate::render::{ + render_graph::{ + Edge, InputSlotError, OutputSlotError, RenderGraphContext, RenderGraphError, + RunSubGraphError, SlotInfo, SlotInfos, + }, + render_phase::DrawError, + renderer::RenderContext, +}; +pub use bevy_ecs::label::DynEq; +use bevy_ecs::{ + define_label, + intern::Interned, + query::{QueryItem, QueryState, ReadOnlyQueryData}, + world::{FromWorld, World}, +}; +use core::fmt::Debug; +use downcast_rs::{impl_downcast, Downcast}; +use thiserror::Error; +use variadics_please::all_tuples_with_size; + +pub use macros::RenderLabel; + +use super::{InternedRenderSubGraph, RenderSubGraph}; + +define_label!( + #[diagnostic::on_unimplemented( + note = "consider annotating `{Self}` with `#[derive(RenderLabel)]`" + )] + /// A strongly-typed class of labels used to identify a [`Node`] in a render graph. + RenderLabel, + RENDER_LABEL_INTERNER +); + +/// A shorthand for `Interned`. +pub type InternedRenderLabel = Interned; + +pub trait IntoRenderNodeArray { + fn into_array(self) -> [InternedRenderLabel; N]; +} + +macro_rules! impl_render_label_tuples { + ($N: expr, $(#[$meta:meta])* $(($T: ident, $I: ident)),*) => { + $(#[$meta])* + impl<$($T: RenderLabel),*> IntoRenderNodeArray<$N> for ($($T,)*) { + #[inline] + fn into_array(self) -> [InternedRenderLabel; $N] { + let ($($I,)*) = self; + [$($I.intern(), )*] + } + } + } +} + +all_tuples_with_size!( + #[doc(fake_variadic)] + impl_render_label_tuples, + 1, + 32, + T, + l +); + +/// A render node that can be added to a [`RenderGraph`](super::RenderGraph). +/// +/// Nodes are the fundamental part of the graph and used to extend its functionality, by +/// generating draw calls and/or running subgraphs. +/// They are added via the `render_graph::add_node(my_node)` method. +/// +/// To determine their position in the graph and ensure that all required dependencies (inputs) +/// are already executed, [`Edges`](Edge) are used. +/// +/// A node can produce outputs used as dependencies by other nodes. +/// Those inputs and outputs are called slots and are the default way of passing render data +/// inside the graph. For more information see [`SlotType`](super::SlotType). +pub trait Node: Downcast + Send + Sync + 'static { + /// Specifies the required input slots for this node. + /// They will then be available during the run method inside the [`RenderGraphContext`]. + fn input(&self) -> Vec { + Vec::new() + } + + /// Specifies the produced output slots for this node. + /// They can then be passed one inside [`RenderGraphContext`] during the run method. + fn output(&self) -> Vec { + Vec::new() + } + + /// Updates internal node state using the current render [`World`] prior to the run method. + fn update(&mut self, _world: &mut World) {} + + /// Runs the graph node logic, issues draw calls, updates the output slots and + /// optionally queues up subgraphs for execution. The graph data, input and output values are + /// passed via the [`RenderGraphContext`]. + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError>; +} + +impl_downcast!(Node); + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum NodeRunError { + #[error("encountered an input slot error")] + InputSlotError(#[from] InputSlotError), + #[error("encountered an output slot error")] + OutputSlotError(#[from] OutputSlotError), + #[error("encountered an error when running a sub-graph")] + RunSubGraphError(#[from] RunSubGraphError), + #[error("encountered an error when executing draw command")] + DrawError(#[from] DrawError), +} + +/// A collection of input and output [`Edges`](Edge) for a [`Node`]. +#[derive(Debug)] +pub struct Edges { + label: InternedRenderLabel, + input_edges: Vec, + output_edges: Vec, +} + +impl Edges { + /// Returns all "input edges" (edges going "in") for this node . + #[inline] + pub fn input_edges(&self) -> &[Edge] { + &self.input_edges + } + + /// Returns all "output edges" (edges going "out") for this node . + #[inline] + pub fn output_edges(&self) -> &[Edge] { + &self.output_edges + } + + /// Returns this node's label. + #[inline] + pub fn label(&self) -> InternedRenderLabel { + self.label + } + + /// Adds an edge to the `input_edges` if it does not already exist. + pub(crate) fn add_input_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> { + if self.has_input_edge(&edge) { + return Err(RenderGraphError::EdgeAlreadyExists(edge)); + } + self.input_edges.push(edge); + Ok(()) + } + + /// Removes an edge from the `input_edges` if it exists. + pub(crate) fn remove_input_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> { + if let Some(index) = self.input_edges.iter().position(|e| *e == edge) { + self.input_edges.swap_remove(index); + Ok(()) + } else { + Err(RenderGraphError::EdgeDoesNotExist(edge)) + } + } + + /// Adds an edge to the `output_edges` if it does not already exist. + pub(crate) fn add_output_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> { + if self.has_output_edge(&edge) { + return Err(RenderGraphError::EdgeAlreadyExists(edge)); + } + self.output_edges.push(edge); + Ok(()) + } + + /// Removes an edge from the `output_edges` if it exists. + pub(crate) fn remove_output_edge(&mut self, edge: Edge) -> Result<(), RenderGraphError> { + if let Some(index) = self.output_edges.iter().position(|e| *e == edge) { + self.output_edges.swap_remove(index); + Ok(()) + } else { + Err(RenderGraphError::EdgeDoesNotExist(edge)) + } + } + + /// Checks whether the input edge already exists. + pub fn has_input_edge(&self, edge: &Edge) -> bool { + self.input_edges.contains(edge) + } + + /// Checks whether the output edge already exists. + pub fn has_output_edge(&self, edge: &Edge) -> bool { + self.output_edges.contains(edge) + } + + /// Searches the `input_edges` for a [`Edge::SlotEdge`], + /// which `input_index` matches the `index`; + pub fn get_input_slot_edge(&self, index: usize) -> Result<&Edge, RenderGraphError> { + self.input_edges + .iter() + .find(|e| { + if let Edge::SlotEdge { input_index, .. } = e { + *input_index == index + } else { + false + } + }) + .ok_or(RenderGraphError::UnconnectedNodeInputSlot { + input_slot: index, + node: self.label, + }) + } + + /// Searches the `output_edges` for a [`Edge::SlotEdge`], + /// which `output_index` matches the `index`; + pub fn get_output_slot_edge(&self, index: usize) -> Result<&Edge, RenderGraphError> { + self.output_edges + .iter() + .find(|e| { + if let Edge::SlotEdge { output_index, .. } = e { + *output_index == index + } else { + false + } + }) + .ok_or(RenderGraphError::UnconnectedNodeOutputSlot { + output_slot: index, + node: self.label, + }) + } +} + +/// The internal representation of a [`Node`], with all data required +/// by the [`RenderGraph`](super::RenderGraph). +/// +/// The `input_slots` and `output_slots` are provided by the `node`. +pub struct NodeState { + pub label: InternedRenderLabel, + /// The name of the type that implements [`Node`]. + pub type_name: &'static str, + pub node: Box, + pub input_slots: SlotInfos, + pub output_slots: SlotInfos, + pub edges: Edges, +} + +impl Debug for NodeState { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + writeln!(f, "{:?} ({})", self.label, self.type_name) + } +} + +impl NodeState { + /// Creates an [`NodeState`] without edges, but the `input_slots` and `output_slots` + /// are provided by the `node`. + pub fn new(label: InternedRenderLabel, node: T) -> Self + where + T: Node, + { + NodeState { + label, + input_slots: node.input().into(), + output_slots: node.output().into(), + node: Box::new(node), + type_name: core::any::type_name::(), + edges: Edges { + label, + input_edges: Vec::new(), + output_edges: Vec::new(), + }, + } + } + + /// Retrieves the [`Node`]. + pub fn node(&self) -> Result<&T, RenderGraphError> + where + T: Node, + { + self.node + .downcast_ref::() + .ok_or(RenderGraphError::WrongNodeType) + } + + /// Retrieves the [`Node`] mutably. + pub fn node_mut(&mut self) -> Result<&mut T, RenderGraphError> + where + T: Node, + { + self.node + .downcast_mut::() + .ok_or(RenderGraphError::WrongNodeType) + } + + /// Validates that each input slot corresponds to an input edge. + pub fn validate_input_slots(&self) -> Result<(), RenderGraphError> { + for i in 0..self.input_slots.len() { + self.edges.get_input_slot_edge(i)?; + } + + Ok(()) + } + + /// Validates that each output slot corresponds to an output edge. + pub fn validate_output_slots(&self) -> Result<(), RenderGraphError> { + for i in 0..self.output_slots.len() { + self.edges.get_output_slot_edge(i)?; + } + + Ok(()) + } +} + +/// A [`Node`] without any inputs, outputs and subgraphs, which does nothing when run. +/// Used (as a label) to bundle multiple dependencies into one inside +/// the [`RenderGraph`](super::RenderGraph). +#[derive(Default)] +pub struct EmptyNode; + +impl Node for EmptyNode { + fn run( + &self, + _graph: &mut RenderGraphContext, + _render_context: &mut RenderContext, + _world: &World, + ) -> Result<(), NodeRunError> { + Ok(()) + } +} + +/// A [`RenderGraph`](super::RenderGraph) [`Node`] that runs the configured subgraph once. +/// This makes it easier to insert sub-graph runs into a graph. +pub struct RunGraphOnViewNode { + sub_graph: InternedRenderSubGraph, +} + +impl RunGraphOnViewNode { + pub fn new(sub_graph: T) -> Self { + Self { + sub_graph: sub_graph.intern(), + } + } +} + +impl Node for RunGraphOnViewNode { + fn run( + &self, + graph: &mut RenderGraphContext, + _render_context: &mut RenderContext, + _world: &World, + ) -> Result<(), NodeRunError> { + graph.run_sub_graph(self.sub_graph, vec![], Some(graph.view_entity()))?; + Ok(()) + } +} + +/// This trait should be used instead of the [`Node`] trait when making a render node that runs on a view. +/// +/// It is intended to be used with [`ViewNodeRunner`] +pub trait ViewNode { + /// The query that will be used on the view entity. + /// It is guaranteed to run on the view entity, so there's no need for a filter + type ViewQuery: ReadOnlyQueryData; + + /// Updates internal node state using the current render [`World`] prior to the run method. + fn update(&mut self, _world: &mut World) {} + + /// Runs the graph node logic, issues draw calls, updates the output slots and + /// optionally queues up subgraphs for execution. The graph data, input and output values are + /// passed via the [`RenderGraphContext`]. + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError>; +} + +/// This [`Node`] can be used to run any [`ViewNode`]. +/// It will take care of updating the view query in `update()` and running the query in `run()`. +/// +/// This [`Node`] exists to help reduce boilerplate when making a render node that runs on a view. +pub struct ViewNodeRunner { + view_query: QueryState, + node: N, +} + +impl ViewNodeRunner { + pub fn new(node: N, world: &mut World) -> Self { + Self { + view_query: world.query_filtered(), + node, + } + } +} + +impl FromWorld for ViewNodeRunner { + fn from_world(world: &mut World) -> Self { + Self::new(N::from_world(world), world) + } +} + +impl Node for ViewNodeRunner +where + T: ViewNode + Send + Sync + 'static, +{ + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + self.node.update(world); + } + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let Ok(view) = self.view_query.get_manual(world, graph.view_entity()) else { + return Ok(()); + }; + + ViewNode::run(&self.node, graph, render_context, view, world)?; + Ok(()) + } +} diff --git a/crates/libmarathon/src/render/render_graph/node_slot.rs b/crates/libmarathon/src/render/render_graph/node_slot.rs new file mode 100644 index 0000000..8f0bc6b --- /dev/null +++ b/crates/libmarathon/src/render/render_graph/node_slot.rs @@ -0,0 +1,165 @@ +use std::borrow::Cow; +use bevy_ecs::entity::Entity; +use core::fmt; +use derive_more::derive::From; + +use crate::render::render_resource::{Buffer, Sampler, TextureView}; + +/// A value passed between render [`Nodes`](super::Node). +/// Corresponds to the [`SlotType`] specified in the [`RenderGraph`](super::RenderGraph). +/// +/// Slots can have four different types of values: +/// [`Buffer`], [`TextureView`], [`Sampler`] and [`Entity`]. +/// +/// These values do not contain the actual render data, but only the ids to retrieve them. +#[derive(Debug, Clone, From)] +pub enum SlotValue { + /// A GPU-accessible [`Buffer`]. + Buffer(Buffer), + /// A [`TextureView`] describes a texture used in a pipeline. + TextureView(TextureView), + /// A texture [`Sampler`] defines how a pipeline will sample from a [`TextureView`]. + Sampler(Sampler), + /// An entity from the ECS. + Entity(Entity), +} + +impl SlotValue { + /// Returns the [`SlotType`] of this value. + pub fn slot_type(&self) -> SlotType { + match self { + SlotValue::Buffer(_) => SlotType::Buffer, + SlotValue::TextureView(_) => SlotType::TextureView, + SlotValue::Sampler(_) => SlotType::Sampler, + SlotValue::Entity(_) => SlotType::Entity, + } + } +} + +/// Describes the render resources created (output) or used (input) by +/// the render [`Nodes`](super::Node). +/// +/// This should not be confused with [`SlotValue`], which actually contains the passed data. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum SlotType { + /// A GPU-accessible [`Buffer`]. + Buffer, + /// A [`TextureView`] describes a texture used in a pipeline. + TextureView, + /// A texture [`Sampler`] defines how a pipeline will sample from a [`TextureView`]. + Sampler, + /// An entity from the ECS. + Entity, +} + +impl fmt::Display for SlotType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + SlotType::Buffer => "Buffer", + SlotType::TextureView => "TextureView", + SlotType::Sampler => "Sampler", + SlotType::Entity => "Entity", + }; + + f.write_str(s) + } +} + +/// A [`SlotLabel`] is used to reference a slot by either its name or index +/// inside the [`RenderGraph`](super::RenderGraph). +#[derive(Debug, Clone, Eq, PartialEq, From)] +pub enum SlotLabel { + Index(usize), + Name(Cow<'static, str>), +} + +impl From<&SlotLabel> for SlotLabel { + fn from(value: &SlotLabel) -> Self { + value.clone() + } +} + +impl From for SlotLabel { + fn from(value: String) -> Self { + SlotLabel::Name(value.into()) + } +} + +impl From<&'static str> for SlotLabel { + fn from(value: &'static str) -> Self { + SlotLabel::Name(value.into()) + } +} + +/// The internal representation of a slot, which specifies its [`SlotType`] and name. +#[derive(Clone, Debug)] +pub struct SlotInfo { + pub name: Cow<'static, str>, + pub slot_type: SlotType, +} + +impl SlotInfo { + pub fn new(name: impl Into>, slot_type: SlotType) -> Self { + SlotInfo { + name: name.into(), + slot_type, + } + } +} + +/// A collection of input or output [`SlotInfos`](SlotInfo) for +/// a [`NodeState`](super::NodeState). +#[derive(Default, Debug)] +pub struct SlotInfos { + slots: Vec, +} + +impl> From for SlotInfos { + fn from(slots: T) -> Self { + SlotInfos { + slots: slots.into_iter().collect(), + } + } +} + +impl SlotInfos { + /// Returns the count of slots. + #[inline] + pub fn len(&self) -> usize { + self.slots.len() + } + + /// Returns true if there are no slots. + #[inline] + pub fn is_empty(&self) -> bool { + self.slots.is_empty() + } + + /// Retrieves the [`SlotInfo`] for the provided label. + pub fn get_slot(&self, label: impl Into) -> Option<&SlotInfo> { + let label = label.into(); + let index = self.get_slot_index(label)?; + self.slots.get(index) + } + + /// Retrieves the [`SlotInfo`] for the provided label mutably. + pub fn get_slot_mut(&mut self, label: impl Into) -> Option<&mut SlotInfo> { + let label = label.into(); + let index = self.get_slot_index(label)?; + self.slots.get_mut(index) + } + + /// Retrieves the index (inside input or output slots) of the slot for the provided label. + pub fn get_slot_index(&self, label: impl Into) -> Option { + let label = label.into(); + match label { + SlotLabel::Index(index) => Some(index), + SlotLabel::Name(ref name) => self.slots.iter().position(|s| s.name == *name), + } + } + + /// Returns an iterator over the slot infos. + pub fn iter(&self) -> impl Iterator { + self.slots.iter() + } +} diff --git a/crates/libmarathon/src/render/render_phase/draw.rs b/crates/libmarathon/src/render/render_phase/draw.rs new file mode 100644 index 0000000..5136c72 --- /dev/null +++ b/crates/libmarathon/src/render/render_phase/draw.rs @@ -0,0 +1,398 @@ +use crate::render::render_phase::{PhaseItem, TrackedRenderPass}; +use bevy_app::{App, SubApp}; +use bevy_ecs::{ + entity::Entity, + query::{QueryEntityError, QueryState, ROQueryItem, ReadOnlyQueryData}, + resource::Resource, + system::{ReadOnlySystemParam, SystemParam, SystemParamItem, SystemState}, + world::World, +}; +use bevy_utils::TypeIdMap; +use core::{any::TypeId, fmt::Debug, hash::Hash}; +use std::sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use thiserror::Error; +use variadics_please::all_tuples; + +/// A draw function used to draw [`PhaseItem`]s. +/// +/// The draw function can retrieve and query the required ECS data from the render world. +/// +/// This trait can either be implemented directly or implicitly composed out of multiple modular +/// [`RenderCommand`]s. For more details and an example see the [`RenderCommand`] documentation. +pub trait Draw: Send + Sync + 'static { + /// Prepares the draw function to be used. This is called once and only once before the phase + /// begins. There may be zero or more [`draw`](Draw::draw) calls following a call to this function. + /// Implementing this is optional. + #[expect( + unused_variables, + reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." + )] + fn prepare(&mut self, world: &'_ World) {} + + /// Draws a [`PhaseItem`] by issuing zero or more `draw` calls via the [`TrackedRenderPass`]. + fn draw<'w>( + &mut self, + world: &'w World, + pass: &mut TrackedRenderPass<'w>, + view: Entity, + item: &P, + ) -> Result<(), DrawError>; +} + +#[derive(Error, Debug, PartialEq, Eq)] +pub enum DrawError { + #[error("Failed to execute render command {0:?}")] + RenderCommandFailure(&'static str), + #[error("Failed to get execute view query")] + InvalidViewQuery, + #[error("View entity not found")] + ViewEntityNotFound, +} + +// TODO: make this generic? +/// An identifier for a [`Draw`] function stored in [`DrawFunctions`]. +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] +pub struct DrawFunctionId(u32); + +/// Stores all [`Draw`] functions for the [`PhaseItem`] type. +/// +/// For retrieval, the [`Draw`] functions are mapped to their respective [`TypeId`]s. +pub struct DrawFunctionsInternal { + pub draw_functions: Vec>>, + pub indices: TypeIdMap, +} + +impl DrawFunctionsInternal

    { + /// Prepares all draw function. This is called once and only once before the phase begins. + pub fn prepare(&mut self, world: &World) { + for function in &mut self.draw_functions { + function.prepare(world); + } + } + + /// Adds the [`Draw`] function and maps it to its own type. + pub fn add>(&mut self, draw_function: T) -> DrawFunctionId { + self.add_with::(draw_function) + } + + /// Adds the [`Draw`] function and maps it to the type `T` + pub fn add_with>(&mut self, draw_function: D) -> DrawFunctionId { + let id = DrawFunctionId(self.draw_functions.len().try_into().unwrap()); + self.draw_functions.push(Box::new(draw_function)); + self.indices.insert(TypeId::of::(), id); + id + } + + /// Retrieves the [`Draw`] function corresponding to the `id` mutably. + pub fn get_mut(&mut self, id: DrawFunctionId) -> Option<&mut dyn Draw

    > { + self.draw_functions.get_mut(id.0 as usize).map(|f| &mut **f) + } + + /// Retrieves the id of the [`Draw`] function corresponding to their associated type `T`. + pub fn get_id(&self) -> Option { + self.indices.get(&TypeId::of::()).copied() + } + + /// Retrieves the id of the [`Draw`] function corresponding to their associated type `T`. + /// + /// Fallible wrapper for [`Self::get_id()`] + /// + /// ## Panics + /// If the id doesn't exist, this function will panic. + pub fn id(&self) -> DrawFunctionId { + self.get_id::().unwrap_or_else(|| { + panic!( + "Draw function {} not found for {}", + core::any::type_name::(), + core::any::type_name::

    () + ) + }) + } +} + +/// Stores all draw functions for the [`PhaseItem`] type hidden behind a reader-writer lock. +/// +/// To access them the [`DrawFunctions::read`] and [`DrawFunctions::write`] methods are used. +#[derive(Resource)] +pub struct DrawFunctions { + internal: RwLock>, +} + +impl Default for DrawFunctions

    { + fn default() -> Self { + Self { + internal: RwLock::new(DrawFunctionsInternal { + draw_functions: Vec::new(), + indices: Default::default(), + }), + } + } +} + +impl DrawFunctions

    { + /// Accesses the draw functions in read mode. + pub fn read(&self) -> RwLockReadGuard<'_, DrawFunctionsInternal

    > { + self.internal.read().unwrap_or_else(PoisonError::into_inner) + } + + /// Accesses the draw functions in write mode. + pub fn write(&self) -> RwLockWriteGuard<'_, DrawFunctionsInternal

    > { + self.internal + .write() + .unwrap_or_else(PoisonError::into_inner) + } +} + +/// [`RenderCommand`]s are modular standardized pieces of render logic that can be composed into +/// [`Draw`] functions. +/// +/// To turn a stateless render command into a usable draw function it has to be wrapped by a +/// [`RenderCommandState`]. +/// This is done automatically when registering a render command as a [`Draw`] function via the +/// [`AddRenderCommand::add_render_command`] method. +/// +/// Compared to the draw function the required ECS data is fetched automatically +/// (by the [`RenderCommandState`]) from the render world. +/// Therefore the three types [`Param`](RenderCommand::Param), +/// [`ViewQuery`](RenderCommand::ViewQuery) and +/// [`ItemQuery`](RenderCommand::ItemQuery) are used. +/// They specify which information is required to execute the render command. +/// +/// Multiple render commands can be combined together by wrapping them in a tuple. +/// +/// # Example +/// +/// The `DrawMaterial` draw function is created from the following render command +/// tuple. Const generics are used to set specific bind group locations: +/// +/// ``` +/// # use crate::render::render_phase::SetItemPipeline; +/// # struct SetMeshViewBindGroup; +/// # struct SetMeshViewBindingArrayBindGroup; +/// # struct SetMeshBindGroup; +/// # struct SetMaterialBindGroup(std::marker::PhantomData); +/// # struct DrawMesh; +/// pub type DrawMaterial = ( +/// SetItemPipeline, +/// SetMeshViewBindGroup<0>, +/// SetMeshViewBindingArrayBindGroup<1>, +/// SetMeshBindGroup<2>, +/// SetMaterialBindGroup, +/// DrawMesh, +/// ); +/// ``` +pub trait RenderCommand { + /// Specifies the general ECS data (e.g. resources) required by [`RenderCommand::render`]. + /// + /// When fetching resources, note that, due to lifetime limitations of the `Deref` trait, + /// [`SRes::into_inner`] must be called on each [`SRes`] reference in the + /// [`RenderCommand::render`] method, instead of being automatically dereferenced as is the + /// case in normal `systems`. + /// + /// All parameters have to be read only. + /// + /// [`SRes`]: bevy_ecs::system::lifetimeless::SRes + /// [`SRes::into_inner`]: bevy_ecs::system::lifetimeless::SRes::into_inner + type Param: SystemParam + 'static; + /// Specifies the ECS data of the view entity required by [`RenderCommand::render`]. + /// + /// The view entity refers to the camera, or shadow-casting light, etc. from which the phase + /// item will be rendered from. + /// All components have to be accessed read only. + type ViewQuery: ReadOnlyQueryData; + /// Specifies the ECS data of the item entity required by [`RenderCommand::render`]. + /// + /// The item is the entity that will be rendered for the corresponding view. + /// All components have to be accessed read only. + /// + /// For efficiency reasons, Bevy doesn't always extract entities to the + /// render world; for instance, entities that simply consist of meshes are + /// often not extracted. If the entity doesn't exist in the render world, + /// the supplied query data will be `None`. + type ItemQuery: ReadOnlyQueryData; + + /// Renders a [`PhaseItem`] by recording commands (e.g. setting pipelines, binding bind groups, + /// issuing draw calls, etc.) via the [`TrackedRenderPass`]. + fn render<'w>( + item: &P, + view: ROQueryItem<'w, '_, Self::ViewQuery>, + entity: Option>, + param: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult; +} + +/// The result of a [`RenderCommand`]. +#[derive(Debug)] +pub enum RenderCommandResult { + Success, + Skip, + Failure(&'static str), +} + +macro_rules! render_command_tuple_impl { + ($(#[$meta:meta])* $(($name: ident, $view: ident, $entity: ident)),*) => { + $(#[$meta])* + impl),*> RenderCommand

    for ($($name,)*) { + type Param = ($($name::Param,)*); + type ViewQuery = ($($name::ViewQuery,)*); + type ItemQuery = ($($name::ItemQuery,)*); + + #[expect( + clippy::allow_attributes, + reason = "We are in a macro; as such, `non_snake_case` may not always lint." + )] + #[allow( + non_snake_case, + reason = "Parameter and variable names are provided by the macro invocation, not by us." + )] + fn render<'w>( + _item: &P, + ($($view,)*): ROQueryItem<'w, '_, Self::ViewQuery>, + maybe_entities: Option>, + ($($name,)*): SystemParamItem<'w, '_, Self::Param>, + _pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + match maybe_entities { + None => { + $( + match $name::render(_item, $view, None, $name, _pass) { + RenderCommandResult::Skip => return RenderCommandResult::Skip, + RenderCommandResult::Failure(reason) => return RenderCommandResult::Failure(reason), + _ => {}, + } + )* + } + Some(($($entity,)*)) => { + $( + match $name::render(_item, $view, Some($entity), $name, _pass) { + RenderCommandResult::Skip => return RenderCommandResult::Skip, + RenderCommandResult::Failure(reason) => return RenderCommandResult::Failure(reason), + _ => {}, + } + )* + } + } + RenderCommandResult::Success + } + } + }; +} + +all_tuples!( + #[doc(fake_variadic)] + render_command_tuple_impl, + 0, + 15, + C, + V, + E +); + +/// Wraps a [`RenderCommand`] into a state so that it can be used as a [`Draw`] function. +/// +/// The [`RenderCommand::Param`], [`RenderCommand::ViewQuery`] and +/// [`RenderCommand::ItemQuery`] are fetched from the ECS and passed to the command. +pub struct RenderCommandState> { + state: SystemState, + view: QueryState, + entity: QueryState, +} + +impl> RenderCommandState { + /// Creates a new [`RenderCommandState`] for the [`RenderCommand`]. + pub fn new(world: &mut World) -> Self { + Self { + state: SystemState::new(world), + view: world.query(), + entity: world.query(), + } + } +} + +impl + Send + Sync + 'static> Draw

    for RenderCommandState +where + C::Param: ReadOnlySystemParam, +{ + /// Prepares the render command to be used. This is called once and only once before the phase + /// begins. There may be zero or more [`draw`](RenderCommandState::draw) calls following a call to this function. + fn prepare(&mut self, world: &'_ World) { + self.view.update_archetypes(world); + self.entity.update_archetypes(world); + } + + /// Fetches the ECS parameters for the wrapped [`RenderCommand`] and then renders it. + fn draw<'w>( + &mut self, + world: &'w World, + pass: &mut TrackedRenderPass<'w>, + view: Entity, + item: &P, + ) -> Result<(), DrawError> { + let param = self.state.get(world); + let view = match self.view.get_manual(world, view) { + Ok(view) => view, + Err(err) => match err { + QueryEntityError::EntityDoesNotExist(_) => { + return Err(DrawError::ViewEntityNotFound) + } + QueryEntityError::QueryDoesNotMatch(_, _) + | QueryEntityError::AliasedMutability(_) => { + return Err(DrawError::InvalidViewQuery) + } + }, + }; + + let entity = self.entity.get_manual(world, item.entity()).ok(); + match C::render(item, view, entity, param, pass) { + RenderCommandResult::Success | RenderCommandResult::Skip => Ok(()), + RenderCommandResult::Failure(reason) => Err(DrawError::RenderCommandFailure(reason)), + } + } +} + +/// Registers a [`RenderCommand`] as a [`Draw`] function. +/// They are stored inside the [`DrawFunctions`] resource of the app. +pub trait AddRenderCommand { + /// Adds the [`RenderCommand`] for the specified render phase to the app. + fn add_render_command + Send + Sync + 'static>( + &mut self, + ) -> &mut Self + where + C::Param: ReadOnlySystemParam; +} + +impl AddRenderCommand for SubApp { + fn add_render_command + Send + Sync + 'static>( + &mut self, + ) -> &mut Self + where + C::Param: ReadOnlySystemParam, + { + let draw_function = RenderCommandState::::new(self.world_mut()); + let draw_functions = self + .world() + .get_resource::>() + .unwrap_or_else(|| { + panic!( + "DrawFunctions<{}> must be added to the world as a resource \ + before adding render commands to it", + core::any::type_name::

    (), + ); + }); + draw_functions.write().add_with::(draw_function); + self + } +} + +impl AddRenderCommand for App { + fn add_render_command + Send + Sync + 'static>( + &mut self, + ) -> &mut Self + where + C::Param: ReadOnlySystemParam, + { + SubApp::add_render_command::(self.main_mut()); + self + } +} diff --git a/crates/libmarathon/src/render/render_phase/draw_state.rs b/crates/libmarathon/src/render/render_phase/draw_state.rs new file mode 100644 index 0000000..96919db --- /dev/null +++ b/crates/libmarathon/src/render/render_phase/draw_state.rs @@ -0,0 +1,682 @@ +use crate::render::{ + diagnostic::internal::{Pass, PassKind, WritePipelineStatistics, WriteTimestamp}, + render_resource::{ + BindGroup, BindGroupId, Buffer, BufferId, BufferSlice, RenderPipeline, RenderPipelineId, + ShaderStages, + }, + renderer::RenderDevice, +}; +use bevy_camera::Viewport; +use bevy_color::LinearRgba; +use bevy_utils::default; +use core::ops::Range; +use wgpu::{IndexFormat, QuerySet, RenderPass}; + +#[cfg(feature = "detailed_trace")] +use tracing::trace; + +/// Tracks the state of a [`TrackedRenderPass`]. +/// +/// This is used to skip redundant operations on the [`TrackedRenderPass`] (e.g. setting an already +/// set pipeline, binding an already bound bind group). These operations can otherwise be fairly +/// costly due to IO to the GPU, so deduplicating these calls results in a speedup. +#[derive(Debug, Default)] +struct DrawState { + pipeline: Option, + bind_groups: Vec<(Option, Vec)>, + /// List of vertex buffers by [`BufferId`], offset, and size. See [`DrawState::buffer_slice_key`] + vertex_buffers: Vec>, + index_buffer: Option<(BufferId, u64, IndexFormat)>, + + /// Stores whether this state is populated or empty for quick state invalidation + stores_state: bool, +} + +impl DrawState { + /// Marks the `pipeline` as bound. + fn set_pipeline(&mut self, pipeline: RenderPipelineId) { + // TODO: do these need to be cleared? + // self.bind_groups.clear(); + // self.vertex_buffers.clear(); + // self.index_buffer = None; + self.pipeline = Some(pipeline); + self.stores_state = true; + } + + /// Checks, whether the `pipeline` is already bound. + fn is_pipeline_set(&self, pipeline: RenderPipelineId) -> bool { + self.pipeline == Some(pipeline) + } + + /// Marks the `bind_group` as bound to the `index`. + fn set_bind_group(&mut self, index: usize, bind_group: BindGroupId, dynamic_indices: &[u32]) { + let group = &mut self.bind_groups[index]; + group.0 = Some(bind_group); + group.1.clear(); + group.1.extend(dynamic_indices); + self.stores_state = true; + } + + /// Checks, whether the `bind_group` is already bound to the `index`. + fn is_bind_group_set( + &self, + index: usize, + bind_group: BindGroupId, + dynamic_indices: &[u32], + ) -> bool { + if let Some(current_bind_group) = self.bind_groups.get(index) { + current_bind_group.0 == Some(bind_group) && dynamic_indices == current_bind_group.1 + } else { + false + } + } + + /// Marks the vertex `buffer` as bound to the `index`. + fn set_vertex_buffer(&mut self, index: usize, buffer_slice: BufferSlice) { + self.vertex_buffers[index] = Some(self.buffer_slice_key(&buffer_slice)); + self.stores_state = true; + } + + /// Checks, whether the vertex `buffer` is already bound to the `index`. + fn is_vertex_buffer_set(&self, index: usize, buffer_slice: &BufferSlice) -> bool { + if let Some(current) = self.vertex_buffers.get(index) { + *current == Some(self.buffer_slice_key(buffer_slice)) + } else { + false + } + } + + /// Returns the value used for checking whether `BufferSlice`s are equivalent. + fn buffer_slice_key(&self, buffer_slice: &BufferSlice) -> (BufferId, u64, u64) { + ( + buffer_slice.id(), + buffer_slice.offset(), + buffer_slice.size(), + ) + } + + /// Marks the index `buffer` as bound. + fn set_index_buffer(&mut self, buffer: BufferId, offset: u64, index_format: IndexFormat) { + self.index_buffer = Some((buffer, offset, index_format)); + self.stores_state = true; + } + + /// Checks, whether the index `buffer` is already bound. + fn is_index_buffer_set( + &self, + buffer: BufferId, + offset: u64, + index_format: IndexFormat, + ) -> bool { + self.index_buffer == Some((buffer, offset, index_format)) + } + + /// Resets tracking state + pub fn reset_tracking(&mut self) { + if !self.stores_state { + return; + } + self.pipeline = None; + self.bind_groups.iter_mut().for_each(|val| { + val.0 = None; + val.1.clear(); + }); + self.vertex_buffers.iter_mut().for_each(|val| { + *val = None; + }); + self.index_buffer = None; + self.stores_state = false; + } +} + +/// A [`RenderPass`], which tracks the current pipeline state to skip redundant operations. +/// +/// It is used to set the current [`RenderPipeline`], [`BindGroup`]s and [`Buffer`]s. +/// After all requirements are specified, draw calls can be issued. +pub struct TrackedRenderPass<'a> { + pass: RenderPass<'a>, + state: DrawState, +} + +impl<'a> TrackedRenderPass<'a> { + /// Tracks the supplied render pass. + pub fn new(device: &RenderDevice, pass: RenderPass<'a>) -> Self { + let limits = device.limits(); + let max_bind_groups = limits.max_bind_groups as usize; + let max_vertex_buffers = limits.max_vertex_buffers as usize; + Self { + state: DrawState { + bind_groups: vec![(None, Vec::new()); max_bind_groups], + vertex_buffers: vec![None; max_vertex_buffers], + ..default() + }, + pass, + } + } + + /// Returns the wgpu [`RenderPass`]. + /// + /// Function invalidates internal tracking state, + /// some redundant pipeline operations may not be skipped. + pub fn wgpu_pass(&mut self) -> &mut RenderPass<'a> { + self.state.reset_tracking(); + &mut self.pass + } + + /// Sets the active [`RenderPipeline`]. + /// + /// Subsequent draw calls will exhibit the behavior defined by the `pipeline`. + pub fn set_render_pipeline(&mut self, pipeline: &'a RenderPipeline) { + #[cfg(feature = "detailed_trace")] + trace!("set pipeline: {:?}", pipeline); + if self.state.is_pipeline_set(pipeline.id()) { + return; + } + self.pass.set_pipeline(pipeline); + self.state.set_pipeline(pipeline.id()); + } + + /// Sets the active bind group for a given bind group index. The bind group layout + /// in the active pipeline when any `draw()` function is called must match the layout of + /// this bind group. + /// + /// If the bind group have dynamic offsets, provide them in binding order. + /// These offsets have to be aligned to [`WgpuLimits::min_uniform_buffer_offset_alignment`](crate::settings::WgpuLimits::min_uniform_buffer_offset_alignment) + /// or [`WgpuLimits::min_storage_buffer_offset_alignment`](crate::settings::WgpuLimits::min_storage_buffer_offset_alignment) appropriately. + pub fn set_bind_group( + &mut self, + index: usize, + bind_group: &'a BindGroup, + dynamic_uniform_indices: &[u32], + ) { + if self + .state + .is_bind_group_set(index, bind_group.id(), dynamic_uniform_indices) + { + #[cfg(feature = "detailed_trace")] + trace!( + "set bind_group {} (already set): {:?} ({:?})", + index, + bind_group, + dynamic_uniform_indices + ); + return; + } + #[cfg(feature = "detailed_trace")] + trace!( + "set bind_group {}: {:?} ({:?})", + index, + bind_group, + dynamic_uniform_indices + ); + + self.pass + .set_bind_group(index as u32, bind_group, dynamic_uniform_indices); + self.state + .set_bind_group(index, bind_group.id(), dynamic_uniform_indices); + } + + /// Assign a vertex buffer to a slot. + /// + /// Subsequent calls to [`draw`] and [`draw_indexed`] on this + /// [`TrackedRenderPass`] will use `buffer` as one of the source vertex buffers. + /// + /// The `slot_index` refers to the index of the matching descriptor in + /// [`VertexState::buffers`](crate::render_resource::VertexState::buffers). + /// + /// [`draw`]: TrackedRenderPass::draw + /// [`draw_indexed`]: TrackedRenderPass::draw_indexed + pub fn set_vertex_buffer(&mut self, slot_index: usize, buffer_slice: BufferSlice<'a>) { + if self.state.is_vertex_buffer_set(slot_index, &buffer_slice) { + #[cfg(feature = "detailed_trace")] + trace!( + "set vertex buffer {} (already set): {:?} (offset = {}, size = {})", + slot_index, + buffer_slice.id(), + buffer_slice.offset(), + buffer_slice.size(), + ); + return; + } + #[cfg(feature = "detailed_trace")] + trace!( + "set vertex buffer {}: {:?} (offset = {}, size = {})", + slot_index, + buffer_slice.id(), + buffer_slice.offset(), + buffer_slice.size(), + ); + + self.pass + .set_vertex_buffer(slot_index as u32, *buffer_slice); + self.state.set_vertex_buffer(slot_index, buffer_slice); + } + + /// Sets the active index buffer. + /// + /// Subsequent calls to [`TrackedRenderPass::draw_indexed`] will use the buffer referenced by + /// `buffer_slice` as the source index buffer. + pub fn set_index_buffer( + &mut self, + buffer_slice: BufferSlice<'a>, + offset: u64, + index_format: IndexFormat, + ) { + if self + .state + .is_index_buffer_set(buffer_slice.id(), offset, index_format) + { + #[cfg(feature = "detailed_trace")] + trace!( + "set index buffer (already set): {:?} ({})", + buffer_slice.id(), + offset + ); + return; + } + #[cfg(feature = "detailed_trace")] + trace!("set index buffer: {:?} ({})", buffer_slice.id(), offset); + self.pass.set_index_buffer(*buffer_slice, index_format); + self.state + .set_index_buffer(buffer_slice.id(), offset, index_format); + } + + /// Draws primitives from the active vertex buffer(s). + /// + /// The active vertex buffer(s) can be set with [`TrackedRenderPass::set_vertex_buffer`]. + pub fn draw(&mut self, vertices: Range, instances: Range) { + #[cfg(feature = "detailed_trace")] + trace!("draw: {:?} {:?}", vertices, instances); + self.pass.draw(vertices, instances); + } + + /// Draws indexed primitives using the active index buffer and the active vertex buffer(s). + /// + /// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the + /// active vertex buffer(s) can be set with [`TrackedRenderPass::set_vertex_buffer`]. + pub fn draw_indexed(&mut self, indices: Range, base_vertex: i32, instances: Range) { + #[cfg(feature = "detailed_trace")] + trace!( + "draw indexed: {:?} {} {:?}", + indices, + base_vertex, + instances + ); + self.pass.draw_indexed(indices, base_vertex, instances); + } + + /// Draws primitives from the active vertex buffer(s) based on the contents of the + /// `indirect_buffer`. + /// + /// The active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. + /// + /// The structure expected in `indirect_buffer` is the following: + /// + /// ``` + /// #[repr(C)] + /// struct DrawIndirect { + /// vertex_count: u32, // The number of vertices to draw. + /// instance_count: u32, // The number of instances to draw. + /// first_vertex: u32, // The Index of the first vertex to draw. + /// first_instance: u32, // The instance ID of the first instance to draw. + /// // has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`] is enabled. + /// } + /// ``` + pub fn draw_indirect(&mut self, indirect_buffer: &'a Buffer, indirect_offset: u64) { + #[cfg(feature = "detailed_trace")] + trace!("draw indirect: {:?} {}", indirect_buffer, indirect_offset); + self.pass.draw_indirect(indirect_buffer, indirect_offset); + } + + /// Draws indexed primitives using the active index buffer and the active vertex buffers, + /// based on the contents of the `indirect_buffer`. + /// + /// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the + /// active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. + /// + /// The structure expected in `indirect_buffer` is the following: + /// + /// ``` + /// #[repr(C)] + /// struct DrawIndexedIndirect { + /// vertex_count: u32, // The number of vertices to draw. + /// instance_count: u32, // The number of instances to draw. + /// first_index: u32, // The base index within the index buffer. + /// vertex_offset: i32, // The value added to the vertex index before indexing into the vertex buffer. + /// first_instance: u32, // The instance ID of the first instance to draw. + /// // has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`] is enabled. + /// } + /// ``` + pub fn draw_indexed_indirect(&mut self, indirect_buffer: &'a Buffer, indirect_offset: u64) { + #[cfg(feature = "detailed_trace")] + trace!( + "draw indexed indirect: {:?} {}", + indirect_buffer, + indirect_offset + ); + self.pass + .draw_indexed_indirect(indirect_buffer, indirect_offset); + } + + /// Dispatches multiple draw calls from the active vertex buffer(s) based on the contents of the + /// `indirect_buffer`.`count` draw calls are issued. + /// + /// The active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. + /// + /// `indirect_buffer` should contain `count` tightly packed elements of the following structure: + /// + /// ``` + /// #[repr(C)] + /// struct DrawIndirect { + /// vertex_count: u32, // The number of vertices to draw. + /// instance_count: u32, // The number of instances to draw. + /// first_vertex: u32, // The Index of the first vertex to draw. + /// first_instance: u32, // The instance ID of the first instance to draw. + /// // has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`] is enabled. + /// } + /// ``` + pub fn multi_draw_indirect( + &mut self, + indirect_buffer: &'a Buffer, + indirect_offset: u64, + count: u32, + ) { + #[cfg(feature = "detailed_trace")] + trace!( + "multi draw indirect: {:?} {}, {}x", + indirect_buffer, + indirect_offset, + count + ); + self.pass + .multi_draw_indirect(indirect_buffer, indirect_offset, count); + } + + /// Dispatches multiple draw calls from the active vertex buffer(s) based on the contents of + /// the `indirect_buffer`. + /// The count buffer is read to determine how many draws to issue. + /// + /// The indirect buffer must be long enough to account for `max_count` draws, however only + /// `count` elements will be read, where `count` is the value read from `count_buffer` capped + /// at `max_count`. + /// + /// The active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. + /// + /// `indirect_buffer` should contain `count` tightly packed elements of the following structure: + /// + /// ``` + /// #[repr(C)] + /// struct DrawIndirect { + /// vertex_count: u32, // The number of vertices to draw. + /// instance_count: u32, // The number of instances to draw. + /// first_vertex: u32, // The Index of the first vertex to draw. + /// first_instance: u32, // The instance ID of the first instance to draw. + /// // has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`] is enabled. + /// } + /// ``` + pub fn multi_draw_indirect_count( + &mut self, + indirect_buffer: &'a Buffer, + indirect_offset: u64, + count_buffer: &'a Buffer, + count_offset: u64, + max_count: u32, + ) { + #[cfg(feature = "detailed_trace")] + trace!( + "multi draw indirect count: {:?} {}, ({:?} {})x, max {}x", + indirect_buffer, + indirect_offset, + count_buffer, + count_offset, + max_count + ); + self.pass.multi_draw_indirect_count( + indirect_buffer, + indirect_offset, + count_buffer, + count_offset, + max_count, + ); + } + + /// Dispatches multiple draw calls from the active index buffer and the active vertex buffers, + /// based on the contents of the `indirect_buffer`. `count` draw calls are issued. + /// + /// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the + /// active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. + /// + /// `indirect_buffer` should contain `count` tightly packed elements of the following structure: + /// + /// ``` + /// #[repr(C)] + /// struct DrawIndexedIndirect { + /// vertex_count: u32, // The number of vertices to draw. + /// instance_count: u32, // The number of instances to draw. + /// first_index: u32, // The base index within the index buffer. + /// vertex_offset: i32, // The value added to the vertex index before indexing into the vertex buffer. + /// first_instance: u32, // The instance ID of the first instance to draw. + /// // has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`] is enabled. + /// } + /// ``` + pub fn multi_draw_indexed_indirect( + &mut self, + indirect_buffer: &'a Buffer, + indirect_offset: u64, + count: u32, + ) { + #[cfg(feature = "detailed_trace")] + trace!( + "multi draw indexed indirect: {:?} {}, {}x", + indirect_buffer, + indirect_offset, + count + ); + self.pass + .multi_draw_indexed_indirect(indirect_buffer, indirect_offset, count); + } + + /// Dispatches multiple draw calls from the active index buffer and the active vertex buffers, + /// based on the contents of the `indirect_buffer`. + /// The count buffer is read to determine how many draws to issue. + /// + /// The indirect buffer must be long enough to account for `max_count` draws, however only + /// `count` elements will be read, where `count` is the value read from `count_buffer` capped + /// at `max_count`. + /// + /// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the + /// active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. + /// + /// `indirect_buffer` should contain `count` tightly packed elements of the following structure: + /// + /// ``` + /// #[repr(C)] + /// struct DrawIndexedIndirect { + /// vertex_count: u32, // The number of vertices to draw. + /// instance_count: u32, // The number of instances to draw. + /// first_index: u32, // The base index within the index buffer. + /// vertex_offset: i32, // The value added to the vertex index before indexing into the vertex buffer. + /// first_instance: u32, // The instance ID of the first instance to draw. + /// // has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`] is enabled. + /// } + /// ``` + pub fn multi_draw_indexed_indirect_count( + &mut self, + indirect_buffer: &'a Buffer, + indirect_offset: u64, + count_buffer: &'a Buffer, + count_offset: u64, + max_count: u32, + ) { + #[cfg(feature = "detailed_trace")] + trace!( + "multi draw indexed indirect count: {:?} {}, ({:?} {})x, max {}x", + indirect_buffer, + indirect_offset, + count_buffer, + count_offset, + max_count + ); + self.pass.multi_draw_indexed_indirect_count( + indirect_buffer, + indirect_offset, + count_buffer, + count_offset, + max_count, + ); + } + + /// Sets the stencil reference. + /// + /// Subsequent stencil tests will test against this value. + pub fn set_stencil_reference(&mut self, reference: u32) { + #[cfg(feature = "detailed_trace")] + trace!("set stencil reference: {}", reference); + self.pass.set_stencil_reference(reference); + } + + /// Sets the scissor region. + /// + /// Subsequent draw calls will discard any fragments that fall outside this region. + pub fn set_scissor_rect(&mut self, x: u32, y: u32, width: u32, height: u32) { + #[cfg(feature = "detailed_trace")] + trace!("set_scissor_rect: {} {} {} {}", x, y, width, height); + self.pass.set_scissor_rect(x, y, width, height); + } + + /// Set push constant data. + /// + /// `Features::PUSH_CONSTANTS` must be enabled on the device in order to call these functions. + pub fn set_push_constants(&mut self, stages: ShaderStages, offset: u32, data: &[u8]) { + #[cfg(feature = "detailed_trace")] + trace!( + "set push constants: {:?} offset: {} data.len: {}", + stages, + offset, + data.len() + ); + self.pass.set_push_constants(stages, offset, data); + } + + /// Set the rendering viewport. + /// + /// Subsequent draw calls will be projected into that viewport. + pub fn set_viewport( + &mut self, + x: f32, + y: f32, + width: f32, + height: f32, + min_depth: f32, + max_depth: f32, + ) { + #[cfg(feature = "detailed_trace")] + trace!( + "set viewport: {} {} {} {} {} {}", + x, + y, + width, + height, + min_depth, + max_depth + ); + self.pass + .set_viewport(x, y, width, height, min_depth, max_depth); + } + + /// Set the rendering viewport to the given camera [`Viewport`]. + /// + /// Subsequent draw calls will be projected into that viewport. + pub fn set_camera_viewport(&mut self, viewport: &Viewport) { + self.set_viewport( + viewport.physical_position.x as f32, + viewport.physical_position.y as f32, + viewport.physical_size.x as f32, + viewport.physical_size.y as f32, + viewport.depth.start, + viewport.depth.end, + ); + } + + /// Insert a single debug marker. + /// + /// This is a GPU debugging feature. This has no effect on the rendering itself. + pub fn insert_debug_marker(&mut self, label: &str) { + #[cfg(feature = "detailed_trace")] + trace!("insert debug marker: {}", label); + self.pass.insert_debug_marker(label); + } + + /// Start a new debug group. + /// + /// Push a new debug group over the internal stack. Subsequent render commands and debug + /// markers are grouped into this new group, until [`pop_debug_group`] is called. + /// + /// ``` + /// # fn example(mut pass: bevy_render::render_phase::TrackedRenderPass<'static>) { + /// pass.push_debug_group("Render the car"); + /// // [setup pipeline etc...] + /// pass.draw(0..64, 0..1); + /// pass.pop_debug_group(); + /// # } + /// ``` + /// + /// Note that [`push_debug_group`] and [`pop_debug_group`] must always be called in pairs. + /// + /// This is a GPU debugging feature. This has no effect on the rendering itself. + /// + /// [`push_debug_group`]: TrackedRenderPass::push_debug_group + /// [`pop_debug_group`]: TrackedRenderPass::pop_debug_group + pub fn push_debug_group(&mut self, label: &str) { + #[cfg(feature = "detailed_trace")] + trace!("push_debug_group marker: {}", label); + self.pass.push_debug_group(label); + } + + /// End the current debug group. + /// + /// Subsequent render commands and debug markers are not grouped anymore in + /// this group, but in the previous one (if any) or the default top-level one + /// if the debug group was the last one on the stack. + /// + /// Note that [`push_debug_group`] and [`pop_debug_group`] must always be called in pairs. + /// + /// This is a GPU debugging feature. This has no effect on the rendering itself. + /// + /// [`push_debug_group`]: TrackedRenderPass::push_debug_group + /// [`pop_debug_group`]: TrackedRenderPass::pop_debug_group + pub fn pop_debug_group(&mut self) { + #[cfg(feature = "detailed_trace")] + trace!("pop_debug_group"); + self.pass.pop_debug_group(); + } + + /// Sets the blend color as used by some of the blending modes. + /// + /// Subsequent blending tests will test against this value. + pub fn set_blend_constant(&mut self, color: LinearRgba) { + #[cfg(feature = "detailed_trace")] + trace!("set blend constant: {:?}", color); + self.pass.set_blend_constant(wgpu::Color::from(color)); + } +} + +impl WriteTimestamp for TrackedRenderPass<'_> { + fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) { + self.pass.write_timestamp(query_set, index); + } +} + +impl WritePipelineStatistics for TrackedRenderPass<'_> { + fn begin_pipeline_statistics_query(&mut self, query_set: &QuerySet, index: u32) { + self.pass.begin_pipeline_statistics_query(query_set, index); + } + + fn end_pipeline_statistics_query(&mut self) { + self.pass.end_pipeline_statistics_query(); + } +} + +impl Pass for TrackedRenderPass<'_> { + const KIND: PassKind = PassKind::Render; +} diff --git a/crates/libmarathon/src/render/render_phase/mod.rs b/crates/libmarathon/src/render/render_phase/mod.rs new file mode 100644 index 0000000..9a89fa9 --- /dev/null +++ b/crates/libmarathon/src/render/render_phase/mod.rs @@ -0,0 +1,1911 @@ +//! The modular rendering abstraction responsible for queuing, preparing, sorting and drawing +//! entities as part of separate render phases. +//! +//! In Bevy each view (camera, or shadow-casting light, etc.) has one or multiple render phases +//! (e.g. opaque, transparent, shadow, etc). +//! They are used to queue entities for rendering. +//! Multiple phases might be required due to different sorting/batching behaviors +//! (e.g. opaque: front to back, transparent: back to front) or because one phase depends on +//! the rendered texture of the previous phase (e.g. for screen-space reflections). +//! +//! To draw an entity, a corresponding [`PhaseItem`] has to be added to one or multiple of these +//! render phases for each view that it is visible in. +//! This must be done in the [`RenderSystems::Queue`]. +//! After that the render phase sorts them in the [`RenderSystems::PhaseSort`]. +//! Finally the items are rendered using a single [`TrackedRenderPass`], during +//! the [`RenderSystems::Render`]. +//! +//! Therefore each phase item is assigned a [`Draw`] function. +//! These set up the state of the [`TrackedRenderPass`] (i.e. select the +//! [`RenderPipeline`](crate::render_resource::RenderPipeline), configure the +//! [`BindGroup`](crate::render_resource::BindGroup)s, etc.) and then issue a draw call, +//! for the corresponding item. +//! +//! The [`Draw`] function trait can either be implemented directly or such a function can be +//! created by composing multiple [`RenderCommand`]s. + +mod draw; +mod draw_state; +mod rangefinder; + +use bevy_app::{App, Plugin}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Tick; +use bevy_ecs::entity::EntityHash; +use bevy_platform::collections::{hash_map::Entry, HashMap}; +use bevy_utils::default; +pub use draw::*; +pub use draw_state::*; +use encase::{internal::WriteInto, ShaderSize}; +use fixedbitset::{Block, FixedBitSet}; +use indexmap::IndexMap; +use nonmax::NonMaxU32; +pub use rangefinder::*; +use wgpu::Features; + +use crate::render::batching::gpu_preprocessing::{ + GpuPreprocessingMode, GpuPreprocessingSupport, PhaseBatchedInstanceBuffers, + PhaseIndirectParametersBuffers, +}; +use crate::render::renderer::RenderDevice; +use crate::render::sync_world::{MainEntity, MainEntityHashMap}; +use crate::render::view::RetainedViewEntity; +use crate::render::RenderDebugFlags; +use crate::render::{ + batching::{ + self, + gpu_preprocessing::{self, BatchedInstanceBuffers}, + no_gpu_preprocessing::{self, BatchedInstanceBuffer}, + GetFullBatchData, + }, + render_resource::{CachedRenderPipelineId, GpuArrayBufferIndex, PipelineCache}, + Render, RenderApp, RenderSystems, +}; +use bevy_ecs::intern::Interned; +use bevy_ecs::{ + define_label, + prelude::*, + system::{lifetimeless::SRes, SystemParamItem}, +}; +use crate::render::renderer::RenderAdapterInfo; +pub use macros::ShaderLabel; +use core::{fmt::Debug, hash::Hash, iter, marker::PhantomData, ops::Range, slice::SliceIndex}; +use smallvec::SmallVec; +use tracing::warn; + +define_label!( + #[diagnostic::on_unimplemented( + note = "consider annotating `{Self}` with `#[derive(ShaderLabel)]`" + )] + /// Labels used to uniquely identify types of material shaders + ShaderLabel, + SHADER_LABEL_INTERNER +); + +/// A shorthand for `Interned`. +pub type InternedShaderLabel = Interned; + +pub use macros::DrawFunctionLabel; + +define_label!( + #[diagnostic::on_unimplemented( + note = "consider annotating `{Self}` with `#[derive(DrawFunctionLabel)]`" + )] + /// Labels used to uniquely identify types of material shaders + DrawFunctionLabel, + DRAW_FUNCTION_LABEL_INTERNER +); + +pub type InternedDrawFunctionLabel = Interned; + +/// Stores the rendering instructions for a single phase that uses bins in all +/// views. +/// +/// They're cleared out every frame, but storing them in a resource like this +/// allows us to reuse allocations. +#[derive(Resource, Deref, DerefMut)] +pub struct ViewBinnedRenderPhases(pub HashMap>) +where + BPI: BinnedPhaseItem; + +/// A collection of all rendering instructions, that will be executed by the GPU, for a +/// single render phase for a single view. +/// +/// Each view (camera, or shadow-casting light, etc.) can have one or multiple render phases. +/// They are used to queue entities for rendering. +/// Multiple phases might be required due to different sorting/batching behaviors +/// (e.g. opaque: front to back, transparent: back to front) or because one phase depends on +/// the rendered texture of the previous phase (e.g. for screen-space reflections). +/// All [`PhaseItem`]s are then rendered using a single [`TrackedRenderPass`]. +/// The render pass might be reused for multiple phases to reduce GPU overhead. +/// +/// This flavor of render phase is used for phases in which the ordering is less +/// critical: for example, `Opaque3d`. It's generally faster than the +/// alternative [`SortedRenderPhase`]. +pub struct BinnedRenderPhase +where + BPI: BinnedPhaseItem, +{ + /// The multidrawable bins. + /// + /// Each batch set key maps to a *batch set*, which in this case is a set of + /// meshes that can be drawn together in one multidraw call. Each batch set + /// is subdivided into *bins*, each of which represents a particular mesh. + /// Each bin contains the entity IDs of instances of that mesh. + /// + /// So, for example, if there are two cubes and a sphere present in the + /// scene, we would generally have one batch set containing two bins, + /// assuming that the cubes and sphere meshes are allocated together and use + /// the same pipeline. The first bin, corresponding to the cubes, will have + /// two entities in it. The second bin, corresponding to the sphere, will + /// have one entity in it. + pub multidrawable_meshes: IndexMap>, + + /// The bins corresponding to batchable items that aren't multidrawable. + /// + /// For multidrawable entities, use `multidrawable_meshes`; for + /// unbatchable entities, use `unbatchable_values`. + pub batchable_meshes: IndexMap<(BPI::BatchSetKey, BPI::BinKey), RenderBin>, + + /// The unbatchable bins. + /// + /// Each entity here is rendered in a separate drawcall. + pub unbatchable_meshes: IndexMap<(BPI::BatchSetKey, BPI::BinKey), UnbatchableBinnedEntities>, + + /// Items in the bin that aren't meshes at all. + /// + /// Bevy itself doesn't place anything in this list, but plugins or your app + /// can in order to execute custom drawing commands. Draw functions for each + /// entity are simply called in order at rendering time. + /// + /// See the `custom_phase_item` example for an example of how to use this. + pub non_mesh_items: IndexMap<(BPI::BatchSetKey, BPI::BinKey), NonMeshEntities>, + + /// Information on each batch set. + /// + /// A *batch set* is a set of entities that will be batched together unless + /// we're on a platform that doesn't support storage buffers (e.g. WebGL 2) + /// and differing dynamic uniform indices force us to break batches. On + /// platforms that support storage buffers, a batch set always consists of + /// at most one batch. + /// + /// Multidrawable entities come first, then batchable entities, then + /// unbatchable entities. + pub(crate) batch_sets: BinnedRenderPhaseBatchSets, + + /// The batch and bin key for each entity. + /// + /// We retain these so that, when the entity changes, + /// [`Self::sweep_old_entities`] can quickly find the bin it was located in + /// and remove it. + cached_entity_bin_keys: IndexMap, EntityHash>, + + /// The set of indices in [`Self::cached_entity_bin_keys`] that are + /// confirmed to be up to date. + /// + /// Note that each bit in this bit set refers to an *index* in the + /// [`IndexMap`] (i.e. a bucket in the hash table). They aren't entity IDs. + valid_cached_entity_bin_keys: FixedBitSet, + + /// The set of entities that changed bins this frame. + /// + /// An entity will only be present in this list if it was in one bin on the + /// previous frame and is in a new bin on this frame. Each list entry + /// specifies the bin the entity used to be in. We use this in order to + /// remove the entity from the old bin during + /// [`BinnedRenderPhase::sweep_old_entities`]. + entities_that_changed_bins: Vec>, + /// The gpu preprocessing mode configured for the view this phase is associated + /// with. + gpu_preprocessing_mode: GpuPreprocessingMode, +} + +/// All entities that share a mesh and a material and can be batched as part of +/// a [`BinnedRenderPhase`]. +#[derive(Default)] +pub struct RenderBin { + /// A list of the entities in each bin, along with their cached + /// [`InputUniformIndex`]. + entities: IndexMap, +} + +/// Information that we track about an entity that was in one bin on the +/// previous frame and is in a different bin this frame. +struct EntityThatChangedBins +where + BPI: BinnedPhaseItem, +{ + /// The entity. + main_entity: MainEntity, + /// The key that identifies the bin that this entity used to be in. + old_cached_binned_entity: CachedBinnedEntity, +} + +/// Information that we keep about an entity currently within a bin. +pub struct CachedBinnedEntity +where + BPI: BinnedPhaseItem, +{ + /// Information that we use to identify a cached entity in a bin. + pub cached_bin_key: Option>, + /// The last modified tick of the entity. + /// + /// We use this to detect when the entity needs to be invalidated. + pub change_tick: Tick, +} + +/// Information that we use to identify a cached entity in a bin. +pub struct CachedBinKey +where + BPI: BinnedPhaseItem, +{ + /// The key of the batch set containing the entity. + pub batch_set_key: BPI::BatchSetKey, + /// The key of the bin containing the entity. + pub bin_key: BPI::BinKey, + /// The type of render phase that we use to render the entity: multidraw, + /// plain batch, etc. + pub phase_type: BinnedRenderPhaseType, +} + +impl Clone for CachedBinnedEntity +where + BPI: BinnedPhaseItem, +{ + fn clone(&self) -> Self { + CachedBinnedEntity { + cached_bin_key: self.cached_bin_key.clone(), + change_tick: self.change_tick, + } + } +} + +impl Clone for CachedBinKey +where + BPI: BinnedPhaseItem, +{ + fn clone(&self) -> Self { + CachedBinKey { + batch_set_key: self.batch_set_key.clone(), + bin_key: self.bin_key.clone(), + phase_type: self.phase_type, + } + } +} + +impl PartialEq for CachedBinKey +where + BPI: BinnedPhaseItem, +{ + fn eq(&self, other: &Self) -> bool { + self.batch_set_key == other.batch_set_key + && self.bin_key == other.bin_key + && self.phase_type == other.phase_type + } +} + +/// How we store and render the batch sets. +/// +/// Each one of these corresponds to a [`GpuPreprocessingMode`]. +pub enum BinnedRenderPhaseBatchSets { + /// Batches are grouped into batch sets based on dynamic uniforms. + /// + /// This corresponds to [`GpuPreprocessingMode::None`]. + DynamicUniforms(Vec>), + + /// Batches are never grouped into batch sets. + /// + /// This corresponds to [`GpuPreprocessingMode::PreprocessingOnly`]. + Direct(Vec), + + /// Batches are grouped together into batch sets based on their ability to + /// be multi-drawn together. + /// + /// This corresponds to [`GpuPreprocessingMode::Culling`]. + MultidrawIndirect(Vec>), +} + +/// A group of entities that will be batched together into a single multi-draw +/// call. +pub struct BinnedRenderPhaseBatchSet { + /// The first batch in this batch set. + pub(crate) first_batch: BinnedRenderPhaseBatch, + /// The key of the bin that the first batch corresponds to. + pub(crate) bin_key: BK, + /// The number of batches. + pub(crate) batch_count: u32, + /// The index of the batch set in the GPU buffer. + pub(crate) index: u32, +} + +impl BinnedRenderPhaseBatchSets { + fn clear(&mut self) { + match *self { + BinnedRenderPhaseBatchSets::DynamicUniforms(ref mut vec) => vec.clear(), + BinnedRenderPhaseBatchSets::Direct(ref mut vec) => vec.clear(), + BinnedRenderPhaseBatchSets::MultidrawIndirect(ref mut vec) => vec.clear(), + } + } +} + +/// Information about a single batch of entities rendered using binned phase +/// items. +#[derive(Debug)] +pub struct BinnedRenderPhaseBatch { + /// An entity that's *representative* of this batch. + /// + /// Bevy uses this to fetch the mesh. It can be any entity in the batch. + pub representative_entity: (Entity, MainEntity), + /// The range of instance indices in this batch. + pub instance_range: Range, + + /// The dynamic offset of the batch. + /// + /// Note that dynamic offsets are only used on platforms that don't support + /// storage buffers. + pub extra_index: PhaseItemExtraIndex, +} + +/// Information about the unbatchable entities in a bin. +pub struct UnbatchableBinnedEntities { + /// The entities. + pub entities: MainEntityHashMap, + + /// The GPU array buffer indices of each unbatchable binned entity. + pub(crate) buffer_indices: UnbatchableBinnedEntityIndexSet, +} + +/// Information about [`BinnedRenderPhaseType::NonMesh`] entities. +pub struct NonMeshEntities { + /// The entities. + pub entities: MainEntityHashMap, +} + +/// Stores instance indices and dynamic offsets for unbatchable entities in a +/// binned render phase. +/// +/// This is conceptually `Vec`, but it +/// avoids the overhead of storing dynamic offsets on platforms that support +/// them. In other words, this allows a fast path that avoids allocation on +/// platforms that aren't WebGL 2. +#[derive(Default)] + +pub(crate) enum UnbatchableBinnedEntityIndexSet { + /// There are no unbatchable entities in this bin (yet). + #[default] + NoEntities, + + /// The instances for all unbatchable entities in this bin are contiguous, + /// and there are no dynamic uniforms. + /// + /// This is the typical case on platforms other than WebGL 2. We special + /// case this to avoid allocation on those platforms. + Sparse { + /// The range of indices. + instance_range: Range, + /// The index of the first indirect instance parameters. + /// + /// The other indices immediately follow these. + first_indirect_parameters_index: Option, + }, + + /// Dynamic uniforms are present for unbatchable entities in this bin. + /// + /// We fall back to this on WebGL 2. + Dense(Vec), +} + +/// The instance index and dynamic offset (if present) for an unbatchable entity. +/// +/// This is only useful on platforms that don't support storage buffers. +#[derive(Clone)] +pub(crate) struct UnbatchableBinnedEntityIndices { + /// The instance index. + pub(crate) instance_index: u32, + /// The [`PhaseItemExtraIndex`], if present. + pub(crate) extra_index: PhaseItemExtraIndex, +} + +/// Identifies the list within [`BinnedRenderPhase`] that a phase item is to be +/// placed in. +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum BinnedRenderPhaseType { + /// The item is a mesh that's eligible for multi-draw indirect rendering and + /// can be batched with other meshes of the same type. + MultidrawableMesh, + + /// The item is a mesh that can be batched with other meshes of the same type and + /// drawn in a single draw call. + BatchableMesh, + + /// The item is a mesh that's eligible for indirect rendering, but can't be + /// batched with other meshes of the same type. + UnbatchableMesh, + + /// The item isn't a mesh at all. + /// + /// Bevy will simply invoke the drawing commands for such items one after + /// another, with no further processing. + /// + /// The engine itself doesn't enqueue any items of this type, but it's + /// available for use in your application and/or plugins. + NonMesh, +} + +impl From> for UnbatchableBinnedEntityIndices +where + T: Clone + ShaderSize + WriteInto, +{ + fn from(value: GpuArrayBufferIndex) -> Self { + UnbatchableBinnedEntityIndices { + instance_index: value.index, + extra_index: PhaseItemExtraIndex::maybe_dynamic_offset(value.dynamic_offset), + } + } +} + +impl Default for ViewBinnedRenderPhases +where + BPI: BinnedPhaseItem, +{ + fn default() -> Self { + Self(default()) + } +} + +impl ViewBinnedRenderPhases +where + BPI: BinnedPhaseItem, +{ + pub fn prepare_for_new_frame( + &mut self, + retained_view_entity: RetainedViewEntity, + gpu_preprocessing: GpuPreprocessingMode, + ) { + match self.entry(retained_view_entity) { + Entry::Occupied(mut entry) => entry.get_mut().prepare_for_new_frame(), + Entry::Vacant(entry) => { + entry.insert(BinnedRenderPhase::::new(gpu_preprocessing)); + } + } + } +} + +/// The index of the uniform describing this object in the GPU buffer, when GPU +/// preprocessing is enabled. +/// +/// For example, for 3D meshes, this is the index of the `MeshInputUniform` in +/// the buffer. +/// +/// This field is ignored if GPU preprocessing isn't in use, such as (currently) +/// in the case of 2D meshes. In that case, it can be safely set to +/// [`core::default::Default::default`]. +#[derive(Clone, Copy, PartialEq, Default, Deref, DerefMut)] +#[repr(transparent)] +pub struct InputUniformIndex(pub u32); + +impl BinnedRenderPhase +where + BPI: BinnedPhaseItem, +{ + /// Bins a new entity. + /// + /// The `phase_type` parameter specifies whether the entity is a + /// preprocessable mesh and whether it can be binned with meshes of the same + /// type. + pub fn add( + &mut self, + batch_set_key: BPI::BatchSetKey, + bin_key: BPI::BinKey, + (entity, main_entity): (Entity, MainEntity), + input_uniform_index: InputUniformIndex, + mut phase_type: BinnedRenderPhaseType, + change_tick: Tick, + ) { + // If the user has overridden indirect drawing for this view, we need to + // force the phase type to be batchable instead. + if self.gpu_preprocessing_mode == GpuPreprocessingMode::PreprocessingOnly + && phase_type == BinnedRenderPhaseType::MultidrawableMesh + { + phase_type = BinnedRenderPhaseType::BatchableMesh; + } + + match phase_type { + BinnedRenderPhaseType::MultidrawableMesh => { + match self.multidrawable_meshes.entry(batch_set_key.clone()) { + indexmap::map::Entry::Occupied(mut entry) => { + entry + .get_mut() + .entry(bin_key.clone()) + .or_default() + .insert(main_entity, input_uniform_index); + } + indexmap::map::Entry::Vacant(entry) => { + let mut new_batch_set = IndexMap::default(); + new_batch_set.insert( + bin_key.clone(), + RenderBin::from_entity(main_entity, input_uniform_index), + ); + entry.insert(new_batch_set); + } + } + } + + BinnedRenderPhaseType::BatchableMesh => { + match self + .batchable_meshes + .entry((batch_set_key.clone(), bin_key.clone()).clone()) + { + indexmap::map::Entry::Occupied(mut entry) => { + entry.get_mut().insert(main_entity, input_uniform_index); + } + indexmap::map::Entry::Vacant(entry) => { + entry.insert(RenderBin::from_entity(main_entity, input_uniform_index)); + } + } + } + + BinnedRenderPhaseType::UnbatchableMesh => { + match self + .unbatchable_meshes + .entry((batch_set_key.clone(), bin_key.clone())) + { + indexmap::map::Entry::Occupied(mut entry) => { + entry.get_mut().entities.insert(main_entity, entity); + } + indexmap::map::Entry::Vacant(entry) => { + let mut entities = MainEntityHashMap::default(); + entities.insert(main_entity, entity); + entry.insert(UnbatchableBinnedEntities { + entities, + buffer_indices: default(), + }); + } + } + } + + BinnedRenderPhaseType::NonMesh => { + // We don't process these items further. + match self + .non_mesh_items + .entry((batch_set_key.clone(), bin_key.clone()).clone()) + { + indexmap::map::Entry::Occupied(mut entry) => { + entry.get_mut().entities.insert(main_entity, entity); + } + indexmap::map::Entry::Vacant(entry) => { + let mut entities = MainEntityHashMap::default(); + entities.insert(main_entity, entity); + entry.insert(NonMeshEntities { entities }); + } + } + } + } + + // Update the cache. + self.update_cache( + main_entity, + Some(CachedBinKey { + batch_set_key, + bin_key, + phase_type, + }), + change_tick, + ); + } + + /// Inserts an entity into the cache with the given change tick. + pub fn update_cache( + &mut self, + main_entity: MainEntity, + cached_bin_key: Option>, + change_tick: Tick, + ) { + let new_cached_binned_entity = CachedBinnedEntity { + cached_bin_key, + change_tick, + }; + + let (index, old_cached_binned_entity) = self + .cached_entity_bin_keys + .insert_full(main_entity, new_cached_binned_entity.clone()); + + // If the entity changed bins, record its old bin so that we can remove + // the entity from it. + if let Some(old_cached_binned_entity) = old_cached_binned_entity + && old_cached_binned_entity.cached_bin_key != new_cached_binned_entity.cached_bin_key + { + self.entities_that_changed_bins.push(EntityThatChangedBins { + main_entity, + old_cached_binned_entity, + }); + } + + // Mark the entity as valid. + self.valid_cached_entity_bin_keys.grow_and_insert(index); + } + + /// Encodes the GPU commands needed to render all entities in this phase. + pub fn render<'w>( + &self, + render_pass: &mut TrackedRenderPass<'w>, + world: &'w World, + view: Entity, + ) -> Result<(), DrawError> { + { + let draw_functions = world.resource::>(); + let mut draw_functions = draw_functions.write(); + draw_functions.prepare(world); + // Make sure to drop the reader-writer lock here to avoid recursive + // locks. + } + + self.render_batchable_meshes(render_pass, world, view)?; + self.render_unbatchable_meshes(render_pass, world, view)?; + self.render_non_meshes(render_pass, world, view)?; + + Ok(()) + } + + /// Renders all batchable meshes queued in this phase. + fn render_batchable_meshes<'w>( + &self, + render_pass: &mut TrackedRenderPass<'w>, + world: &'w World, + view: Entity, + ) -> Result<(), DrawError> { + let draw_functions = world.resource::>(); + let mut draw_functions = draw_functions.write(); + + let render_device = world.resource::(); + let render_adapter_info = world.resource::(); + let multi_draw_indirect_count_supported = render_device + .features() + .contains(Features::MULTI_DRAW_INDIRECT_COUNT) + // TODO: https://github.com/gfx-rs/wgpu/issues/7974 + && !matches!(render_adapter_info.backend, wgpu::Backend::Dx12); + + match self.batch_sets { + BinnedRenderPhaseBatchSets::DynamicUniforms(ref batch_sets) => { + debug_assert_eq!(self.batchable_meshes.len(), batch_sets.len()); + + for ((batch_set_key, bin_key), batch_set) in + self.batchable_meshes.keys().zip(batch_sets.iter()) + { + for batch in batch_set { + let binned_phase_item = BPI::new( + batch_set_key.clone(), + bin_key.clone(), + batch.representative_entity, + batch.instance_range.clone(), + batch.extra_index.clone(), + ); + + // Fetch the draw function. + let Some(draw_function) = + draw_functions.get_mut(binned_phase_item.draw_function()) + else { + continue; + }; + + draw_function.draw(world, render_pass, view, &binned_phase_item)?; + } + } + } + + BinnedRenderPhaseBatchSets::Direct(ref batch_set) => { + for (batch, (batch_set_key, bin_key)) in + batch_set.iter().zip(self.batchable_meshes.keys()) + { + let binned_phase_item = BPI::new( + batch_set_key.clone(), + bin_key.clone(), + batch.representative_entity, + batch.instance_range.clone(), + batch.extra_index.clone(), + ); + + // Fetch the draw function. + let Some(draw_function) = + draw_functions.get_mut(binned_phase_item.draw_function()) + else { + continue; + }; + + draw_function.draw(world, render_pass, view, &binned_phase_item)?; + } + } + + BinnedRenderPhaseBatchSets::MultidrawIndirect(ref batch_sets) => { + for (batch_set_key, batch_set) in self + .multidrawable_meshes + .keys() + .chain( + self.batchable_meshes + .keys() + .map(|(batch_set_key, _)| batch_set_key), + ) + .zip(batch_sets.iter()) + { + let batch = &batch_set.first_batch; + + let batch_set_index = if multi_draw_indirect_count_supported { + NonMaxU32::new(batch_set.index) + } else { + None + }; + + let binned_phase_item = BPI::new( + batch_set_key.clone(), + batch_set.bin_key.clone(), + batch.representative_entity, + batch.instance_range.clone(), + match batch.extra_index { + PhaseItemExtraIndex::None => PhaseItemExtraIndex::None, + PhaseItemExtraIndex::DynamicOffset(ref dynamic_offset) => { + PhaseItemExtraIndex::DynamicOffset(*dynamic_offset) + } + PhaseItemExtraIndex::IndirectParametersIndex { ref range, .. } => { + PhaseItemExtraIndex::IndirectParametersIndex { + range: range.start..(range.start + batch_set.batch_count), + batch_set_index, + } + } + }, + ); + + // Fetch the draw function. + let Some(draw_function) = + draw_functions.get_mut(binned_phase_item.draw_function()) + else { + continue; + }; + + draw_function.draw(world, render_pass, view, &binned_phase_item)?; + } + } + } + + Ok(()) + } + + /// Renders all unbatchable meshes queued in this phase. + fn render_unbatchable_meshes<'w>( + &self, + render_pass: &mut TrackedRenderPass<'w>, + world: &'w World, + view: Entity, + ) -> Result<(), DrawError> { + let draw_functions = world.resource::>(); + let mut draw_functions = draw_functions.write(); + + for (batch_set_key, bin_key) in self.unbatchable_meshes.keys() { + let unbatchable_entities = + &self.unbatchable_meshes[&(batch_set_key.clone(), bin_key.clone())]; + for (entity_index, entity) in unbatchable_entities.entities.iter().enumerate() { + let unbatchable_dynamic_offset = match &unbatchable_entities.buffer_indices { + UnbatchableBinnedEntityIndexSet::NoEntities => { + // Shouldn't happen… + continue; + } + UnbatchableBinnedEntityIndexSet::Sparse { + instance_range, + first_indirect_parameters_index, + } => UnbatchableBinnedEntityIndices { + instance_index: instance_range.start + entity_index as u32, + extra_index: match first_indirect_parameters_index { + None => PhaseItemExtraIndex::None, + Some(first_indirect_parameters_index) => { + let first_indirect_parameters_index_for_entity = + u32::from(*first_indirect_parameters_index) + + entity_index as u32; + PhaseItemExtraIndex::IndirectParametersIndex { + range: first_indirect_parameters_index_for_entity + ..(first_indirect_parameters_index_for_entity + 1), + batch_set_index: None, + } + } + }, + }, + UnbatchableBinnedEntityIndexSet::Dense(dynamic_offsets) => { + dynamic_offsets[entity_index].clone() + } + }; + + let binned_phase_item = BPI::new( + batch_set_key.clone(), + bin_key.clone(), + (*entity.1, *entity.0), + unbatchable_dynamic_offset.instance_index + ..(unbatchable_dynamic_offset.instance_index + 1), + unbatchable_dynamic_offset.extra_index, + ); + + // Fetch the draw function. + let Some(draw_function) = draw_functions.get_mut(binned_phase_item.draw_function()) + else { + continue; + }; + + draw_function.draw(world, render_pass, view, &binned_phase_item)?; + } + } + Ok(()) + } + + /// Renders all objects of type [`BinnedRenderPhaseType::NonMesh`]. + /// + /// These will have been added by plugins or the application. + fn render_non_meshes<'w>( + &self, + render_pass: &mut TrackedRenderPass<'w>, + world: &'w World, + view: Entity, + ) -> Result<(), DrawError> { + let draw_functions = world.resource::>(); + let mut draw_functions = draw_functions.write(); + + for ((batch_set_key, bin_key), non_mesh_entities) in &self.non_mesh_items { + for (main_entity, entity) in non_mesh_entities.entities.iter() { + // Come up with a fake batch range and extra index. The draw + // function is expected to manage any sort of batching logic itself. + let binned_phase_item = BPI::new( + batch_set_key.clone(), + bin_key.clone(), + (*entity, *main_entity), + 0..1, + PhaseItemExtraIndex::None, + ); + + let Some(draw_function) = draw_functions.get_mut(binned_phase_item.draw_function()) + else { + continue; + }; + + draw_function.draw(world, render_pass, view, &binned_phase_item)?; + } + } + + Ok(()) + } + + pub fn is_empty(&self) -> bool { + self.multidrawable_meshes.is_empty() + && self.batchable_meshes.is_empty() + && self.unbatchable_meshes.is_empty() + && self.non_mesh_items.is_empty() + } + + pub fn prepare_for_new_frame(&mut self) { + self.batch_sets.clear(); + + self.valid_cached_entity_bin_keys.clear(); + self.valid_cached_entity_bin_keys + .grow(self.cached_entity_bin_keys.len()); + self.valid_cached_entity_bin_keys + .set_range(self.cached_entity_bin_keys.len().., true); + + self.entities_that_changed_bins.clear(); + + for unbatchable_bin in self.unbatchable_meshes.values_mut() { + unbatchable_bin.buffer_indices.clear(); + } + } + + /// Checks to see whether the entity is in a bin and returns true if it's + /// both in a bin and up to date. + /// + /// If this function returns true, we also add the entry to the + /// `valid_cached_entity_bin_keys` list. + pub fn validate_cached_entity( + &mut self, + visible_entity: MainEntity, + current_change_tick: Tick, + ) -> bool { + if let indexmap::map::Entry::Occupied(entry) = + self.cached_entity_bin_keys.entry(visible_entity) + && entry.get().change_tick == current_change_tick + { + self.valid_cached_entity_bin_keys.insert(entry.index()); + return true; + } + + false + } + + /// Removes all entities not marked as clean from the bins. + /// + /// During `queue_material_meshes`, we process all visible entities and mark + /// each as clean as we come to it. Then, in [`sweep_old_entities`], we call + /// this method, which removes entities that aren't marked as clean from the + /// bins. + pub fn sweep_old_entities(&mut self) { + // Search for entities not marked as valid. We have to do this in + // reverse order because `swap_remove_index` will potentially invalidate + // all indices after the one we remove. + for index in ReverseFixedBitSetZeroesIterator::new(&self.valid_cached_entity_bin_keys) { + let Some((entity, cached_binned_entity)) = + self.cached_entity_bin_keys.swap_remove_index(index) + else { + continue; + }; + + if let Some(ref cached_bin_key) = cached_binned_entity.cached_bin_key { + remove_entity_from_bin( + entity, + cached_bin_key, + &mut self.multidrawable_meshes, + &mut self.batchable_meshes, + &mut self.unbatchable_meshes, + &mut self.non_mesh_items, + ); + } + } + + // If an entity changed bins, we need to remove it from its old bin. + for entity_that_changed_bins in self.entities_that_changed_bins.drain(..) { + let Some(ref old_cached_bin_key) = entity_that_changed_bins + .old_cached_binned_entity + .cached_bin_key + else { + continue; + }; + remove_entity_from_bin( + entity_that_changed_bins.main_entity, + old_cached_bin_key, + &mut self.multidrawable_meshes, + &mut self.batchable_meshes, + &mut self.unbatchable_meshes, + &mut self.non_mesh_items, + ); + } + } +} + +/// Removes an entity from a bin. +/// +/// If this makes the bin empty, this function removes the bin as well. +/// +/// This is a standalone function instead of a method on [`BinnedRenderPhase`] +/// for borrow check reasons. +fn remove_entity_from_bin( + entity: MainEntity, + entity_bin_key: &CachedBinKey, + multidrawable_meshes: &mut IndexMap>, + batchable_meshes: &mut IndexMap<(BPI::BatchSetKey, BPI::BinKey), RenderBin>, + unbatchable_meshes: &mut IndexMap<(BPI::BatchSetKey, BPI::BinKey), UnbatchableBinnedEntities>, + non_mesh_items: &mut IndexMap<(BPI::BatchSetKey, BPI::BinKey), NonMeshEntities>, +) where + BPI: BinnedPhaseItem, +{ + match entity_bin_key.phase_type { + BinnedRenderPhaseType::MultidrawableMesh => { + if let indexmap::map::Entry::Occupied(mut batch_set_entry) = + multidrawable_meshes.entry(entity_bin_key.batch_set_key.clone()) + { + if let indexmap::map::Entry::Occupied(mut bin_entry) = batch_set_entry + .get_mut() + .entry(entity_bin_key.bin_key.clone()) + { + bin_entry.get_mut().remove(entity); + + // If the bin is now empty, remove the bin. + if bin_entry.get_mut().is_empty() { + bin_entry.swap_remove(); + } + } + + // If the batch set is now empty, remove it. This will perturb + // the order, but that's OK because we're going to sort the bin + // afterwards. + if batch_set_entry.get_mut().is_empty() { + batch_set_entry.swap_remove(); + } + } + } + + BinnedRenderPhaseType::BatchableMesh => { + if let indexmap::map::Entry::Occupied(mut bin_entry) = batchable_meshes.entry(( + entity_bin_key.batch_set_key.clone(), + entity_bin_key.bin_key.clone(), + )) { + bin_entry.get_mut().remove(entity); + + // If the bin is now empty, remove the bin. + if bin_entry.get_mut().is_empty() { + bin_entry.swap_remove(); + } + } + } + + BinnedRenderPhaseType::UnbatchableMesh => { + if let indexmap::map::Entry::Occupied(mut bin_entry) = unbatchable_meshes.entry(( + entity_bin_key.batch_set_key.clone(), + entity_bin_key.bin_key.clone(), + )) { + bin_entry.get_mut().entities.remove(&entity); + + // If the bin is now empty, remove the bin. + if bin_entry.get_mut().entities.is_empty() { + bin_entry.swap_remove(); + } + } + } + + BinnedRenderPhaseType::NonMesh => { + if let indexmap::map::Entry::Occupied(mut bin_entry) = non_mesh_items.entry(( + entity_bin_key.batch_set_key.clone(), + entity_bin_key.bin_key.clone(), + )) { + bin_entry.get_mut().entities.remove(&entity); + + // If the bin is now empty, remove the bin. + if bin_entry.get_mut().entities.is_empty() { + bin_entry.swap_remove(); + } + } + } + } +} + +impl BinnedRenderPhase +where + BPI: BinnedPhaseItem, +{ + fn new(gpu_preprocessing: GpuPreprocessingMode) -> Self { + Self { + multidrawable_meshes: IndexMap::default(), + batchable_meshes: IndexMap::default(), + unbatchable_meshes: IndexMap::default(), + non_mesh_items: IndexMap::default(), + batch_sets: match gpu_preprocessing { + GpuPreprocessingMode::Culling => { + BinnedRenderPhaseBatchSets::MultidrawIndirect(vec![]) + } + GpuPreprocessingMode::PreprocessingOnly => { + BinnedRenderPhaseBatchSets::Direct(vec![]) + } + GpuPreprocessingMode::None => BinnedRenderPhaseBatchSets::DynamicUniforms(vec![]), + }, + cached_entity_bin_keys: IndexMap::default(), + valid_cached_entity_bin_keys: FixedBitSet::new(), + entities_that_changed_bins: vec![], + gpu_preprocessing_mode: gpu_preprocessing, + } + } +} + +impl UnbatchableBinnedEntityIndexSet { + /// Returns the [`UnbatchableBinnedEntityIndices`] for the given entity. + fn indices_for_entity_index( + &self, + entity_index: u32, + ) -> Option { + match self { + UnbatchableBinnedEntityIndexSet::NoEntities => None, + UnbatchableBinnedEntityIndexSet::Sparse { instance_range, .. } + if entity_index >= instance_range.len() as u32 => + { + None + } + UnbatchableBinnedEntityIndexSet::Sparse { + instance_range, + first_indirect_parameters_index: None, + } => Some(UnbatchableBinnedEntityIndices { + instance_index: instance_range.start + entity_index, + extra_index: PhaseItemExtraIndex::None, + }), + UnbatchableBinnedEntityIndexSet::Sparse { + instance_range, + first_indirect_parameters_index: Some(first_indirect_parameters_index), + } => { + let first_indirect_parameters_index_for_this_batch = + u32::from(*first_indirect_parameters_index) + entity_index; + Some(UnbatchableBinnedEntityIndices { + instance_index: instance_range.start + entity_index, + extra_index: PhaseItemExtraIndex::IndirectParametersIndex { + range: first_indirect_parameters_index_for_this_batch + ..(first_indirect_parameters_index_for_this_batch + 1), + batch_set_index: None, + }, + }) + } + UnbatchableBinnedEntityIndexSet::Dense(indices) => { + indices.get(entity_index as usize).cloned() + } + } + } +} + +/// A convenient abstraction for adding all the systems necessary for a binned +/// render phase to the render app. +/// +/// This is the version used when the pipeline supports GPU preprocessing: e.g. +/// 3D PBR meshes. +pub struct BinnedRenderPhasePlugin +where + BPI: BinnedPhaseItem, + GFBD: GetFullBatchData, +{ + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, + phantom: PhantomData<(BPI, GFBD)>, +} + +impl BinnedRenderPhasePlugin +where + BPI: BinnedPhaseItem, + GFBD: GetFullBatchData, +{ + pub fn new(debug_flags: RenderDebugFlags) -> Self { + Self { + debug_flags, + phantom: PhantomData, + } + } +} + +impl Plugin for BinnedRenderPhasePlugin +where + BPI: BinnedPhaseItem, + GFBD: GetFullBatchData + Sync + Send + 'static, +{ + fn build(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::>() + .init_resource::>() + .insert_resource(PhaseIndirectParametersBuffers::::new( + self.debug_flags + .contains(RenderDebugFlags::ALLOW_COPIES_FROM_INDIRECT_PARAMETERS), + )) + .add_systems( + Render, + ( + batching::sort_binned_render_phase::.in_set(RenderSystems::PhaseSort), + ( + no_gpu_preprocessing::batch_and_prepare_binned_render_phase:: + .run_if(resource_exists::>), + gpu_preprocessing::batch_and_prepare_binned_render_phase:: + .run_if( + resource_exists::< + BatchedInstanceBuffers, + >, + ), + ) + .in_set(RenderSystems::PrepareResources), + sweep_old_entities::.in_set(RenderSystems::QueueSweep), + gpu_preprocessing::collect_buffers_for_phase:: + .run_if( + resource_exists::< + BatchedInstanceBuffers, + >, + ) + .in_set(RenderSystems::PrepareResourcesCollectPhaseBuffers), + ), + ); + } +} + +/// Stores the rendering instructions for a single phase that sorts items in all +/// views. +/// +/// They're cleared out every frame, but storing them in a resource like this +/// allows us to reuse allocations. +#[derive(Resource, Deref, DerefMut)] +pub struct ViewSortedRenderPhases(pub HashMap>) +where + SPI: SortedPhaseItem; + +impl Default for ViewSortedRenderPhases +where + SPI: SortedPhaseItem, +{ + fn default() -> Self { + Self(default()) + } +} + +impl ViewSortedRenderPhases +where + SPI: SortedPhaseItem, +{ + pub fn insert_or_clear(&mut self, retained_view_entity: RetainedViewEntity) { + match self.entry(retained_view_entity) { + Entry::Occupied(mut entry) => entry.get_mut().clear(), + Entry::Vacant(entry) => { + entry.insert(default()); + } + } + } +} + +/// A convenient abstraction for adding all the systems necessary for a sorted +/// render phase to the render app. +/// +/// This is the version used when the pipeline supports GPU preprocessing: e.g. +/// 3D PBR meshes. +pub struct SortedRenderPhasePlugin +where + SPI: SortedPhaseItem, + GFBD: GetFullBatchData, +{ + /// Debugging flags that can optionally be set when constructing the renderer. + pub debug_flags: RenderDebugFlags, + phantom: PhantomData<(SPI, GFBD)>, +} + +impl SortedRenderPhasePlugin +where + SPI: SortedPhaseItem, + GFBD: GetFullBatchData, +{ + pub fn new(debug_flags: RenderDebugFlags) -> Self { + Self { + debug_flags, + phantom: PhantomData, + } + } +} + +impl Plugin for SortedRenderPhasePlugin +where + SPI: SortedPhaseItem + CachedRenderPipelinePhaseItem, + GFBD: GetFullBatchData + Sync + Send + 'static, +{ + fn build(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::>() + .init_resource::>() + .insert_resource(PhaseIndirectParametersBuffers::::new( + self.debug_flags + .contains(RenderDebugFlags::ALLOW_COPIES_FROM_INDIRECT_PARAMETERS), + )) + .add_systems( + Render, + ( + ( + no_gpu_preprocessing::batch_and_prepare_sorted_render_phase:: + .run_if(resource_exists::>), + gpu_preprocessing::batch_and_prepare_sorted_render_phase:: + .run_if( + resource_exists::< + BatchedInstanceBuffers, + >, + ), + ) + .in_set(RenderSystems::PrepareResources), + gpu_preprocessing::collect_buffers_for_phase:: + .run_if( + resource_exists::< + BatchedInstanceBuffers, + >, + ) + .in_set(RenderSystems::PrepareResourcesCollectPhaseBuffers), + ), + ); + } +} + +impl UnbatchableBinnedEntityIndexSet { + /// Adds a new entity to the list of unbatchable binned entities. + pub fn add(&mut self, indices: UnbatchableBinnedEntityIndices) { + match self { + UnbatchableBinnedEntityIndexSet::NoEntities => { + match indices.extra_index { + PhaseItemExtraIndex::DynamicOffset(_) => { + // This is the first entity we've seen, and we don't have + // compute shaders. Initialize an array. + *self = UnbatchableBinnedEntityIndexSet::Dense(vec![indices]); + } + PhaseItemExtraIndex::None => { + // This is the first entity we've seen, and we have compute + // shaders. Initialize the fast path. + *self = UnbatchableBinnedEntityIndexSet::Sparse { + instance_range: indices.instance_index..indices.instance_index + 1, + first_indirect_parameters_index: None, + } + } + PhaseItemExtraIndex::IndirectParametersIndex { + range: ref indirect_parameters_index, + .. + } => { + // This is the first entity we've seen, and we have compute + // shaders. Initialize the fast path. + *self = UnbatchableBinnedEntityIndexSet::Sparse { + instance_range: indices.instance_index..indices.instance_index + 1, + first_indirect_parameters_index: NonMaxU32::new( + indirect_parameters_index.start, + ), + } + } + } + } + + UnbatchableBinnedEntityIndexSet::Sparse { + instance_range, + first_indirect_parameters_index, + } if instance_range.end == indices.instance_index + && ((first_indirect_parameters_index.is_none() + && indices.extra_index == PhaseItemExtraIndex::None) + || first_indirect_parameters_index.is_some_and( + |first_indirect_parameters_index| match indices.extra_index { + PhaseItemExtraIndex::IndirectParametersIndex { + range: ref this_range, + .. + } => { + u32::from(first_indirect_parameters_index) + instance_range.end + - instance_range.start + == this_range.start + } + PhaseItemExtraIndex::DynamicOffset(_) | PhaseItemExtraIndex::None => { + false + } + }, + )) => + { + // This is the normal case on non-WebGL 2. + instance_range.end += 1; + } + + UnbatchableBinnedEntityIndexSet::Sparse { instance_range, .. } => { + // We thought we were in non-WebGL 2 mode, but we got a dynamic + // offset or non-contiguous index anyway. This shouldn't happen, + // but let's go ahead and do the sensible thing anyhow: demote + // the compressed `NoDynamicOffsets` field to the full + // `DynamicOffsets` array. + warn!( + "Unbatchable binned entity index set was demoted from sparse to dense. \ + This is a bug in the renderer. Please report it.", + ); + let new_dynamic_offsets = (0..instance_range.len() as u32) + .flat_map(|entity_index| self.indices_for_entity_index(entity_index)) + .chain(iter::once(indices)) + .collect(); + *self = UnbatchableBinnedEntityIndexSet::Dense(new_dynamic_offsets); + } + + UnbatchableBinnedEntityIndexSet::Dense(dense_indices) => { + dense_indices.push(indices); + } + } + } + + /// Clears the unbatchable binned entity index set. + fn clear(&mut self) { + match self { + UnbatchableBinnedEntityIndexSet::Dense(dense_indices) => dense_indices.clear(), + UnbatchableBinnedEntityIndexSet::Sparse { .. } => { + *self = UnbatchableBinnedEntityIndexSet::NoEntities; + } + _ => {} + } + } +} + +/// A collection of all items to be rendered that will be encoded to GPU +/// commands for a single render phase for a single view. +/// +/// Each view (camera, or shadow-casting light, etc.) can have one or multiple render phases. +/// They are used to queue entities for rendering. +/// Multiple phases might be required due to different sorting/batching behaviors +/// (e.g. opaque: front to back, transparent: back to front) or because one phase depends on +/// the rendered texture of the previous phase (e.g. for screen-space reflections). +/// All [`PhaseItem`]s are then rendered using a single [`TrackedRenderPass`]. +/// The render pass might be reused for multiple phases to reduce GPU overhead. +/// +/// This flavor of render phase is used only for meshes that need to be sorted +/// back-to-front, such as transparent meshes. For items that don't need strict +/// sorting, [`BinnedRenderPhase`] is preferred, for performance. +pub struct SortedRenderPhase +where + I: SortedPhaseItem, +{ + /// The items within this [`SortedRenderPhase`]. + pub items: Vec, +} + +impl Default for SortedRenderPhase +where + I: SortedPhaseItem, +{ + fn default() -> Self { + Self { items: Vec::new() } + } +} + +impl SortedRenderPhase +where + I: SortedPhaseItem, +{ + /// Adds a [`PhaseItem`] to this render phase. + #[inline] + pub fn add(&mut self, item: I) { + self.items.push(item); + } + + /// Removes all [`PhaseItem`]s from this render phase. + #[inline] + pub fn clear(&mut self) { + self.items.clear(); + } + + /// Sorts all of its [`PhaseItem`]s. + pub fn sort(&mut self) { + I::sort(&mut self.items); + } + + /// An [`Iterator`] through the associated [`Entity`] for each [`PhaseItem`] in order. + #[inline] + pub fn iter_entities(&'_ self) -> impl Iterator + '_ { + self.items.iter().map(PhaseItem::entity) + } + + /// Renders all of its [`PhaseItem`]s using their corresponding draw functions. + pub fn render<'w>( + &self, + render_pass: &mut TrackedRenderPass<'w>, + world: &'w World, + view: Entity, + ) -> Result<(), DrawError> { + self.render_range(render_pass, world, view, ..) + } + + /// Renders all [`PhaseItem`]s in the provided `range` (based on their index in `self.items`) using their corresponding draw functions. + pub fn render_range<'w>( + &self, + render_pass: &mut TrackedRenderPass<'w>, + world: &'w World, + view: Entity, + range: impl SliceIndex<[I], Output = [I]>, + ) -> Result<(), DrawError> { + let items = self + .items + .get(range) + .expect("`Range` provided to `render_range()` is out of bounds"); + + let draw_functions = world.resource::>(); + let mut draw_functions = draw_functions.write(); + draw_functions.prepare(world); + + let mut index = 0; + while index < items.len() { + let item = &items[index]; + let batch_range = item.batch_range(); + if batch_range.is_empty() { + index += 1; + } else { + let draw_function = draw_functions.get_mut(item.draw_function()).unwrap(); + draw_function.draw(world, render_pass, view, item)?; + index += batch_range.len(); + } + } + Ok(()) + } +} + +/// An item (entity of the render world) which will be drawn to a texture or the screen, +/// as part of a render phase. +/// +/// The data required for rendering an entity is extracted from the main world in the +/// [`ExtractSchedule`](crate::ExtractSchedule). +/// Then it has to be queued up for rendering during the [`RenderSystems::Queue`], +/// by adding a corresponding phase item to a render phase. +/// Afterwards it will be possibly sorted and rendered automatically in the +/// [`RenderSystems::PhaseSort`] and [`RenderSystems::Render`], respectively. +/// +/// `PhaseItem`s come in two flavors: [`BinnedPhaseItem`]s and +/// [`SortedPhaseItem`]s. +/// +/// * Binned phase items have a `BinKey` which specifies what bin they're to be +/// placed in. All items in the same bin are eligible to be batched together. +/// The `BinKey`s are sorted, but the individual bin items aren't. Binned phase +/// items are good for opaque meshes, in which the order of rendering isn't +/// important. Generally, binned phase items are faster than sorted phase items. +/// +/// * Sorted phase items, on the other hand, are placed into one large buffer +/// and then sorted all at once. This is needed for transparent meshes, which +/// have to be sorted back-to-front to render with the painter's algorithm. +/// These types of phase items are generally slower than binned phase items. +pub trait PhaseItem: Sized + Send + Sync + 'static { + /// Whether or not this `PhaseItem` should be subjected to automatic batching. (Default: `true`) + const AUTOMATIC_BATCHING: bool = true; + + /// The corresponding entity that will be drawn. + /// + /// This is used to fetch the render data of the entity, required by the draw function, + /// from the render world . + fn entity(&self) -> Entity; + + /// The main world entity represented by this `PhaseItem`. + fn main_entity(&self) -> MainEntity; + + /// Specifies the [`Draw`] function used to render the item. + fn draw_function(&self) -> DrawFunctionId; + + /// The range of instances that the batch covers. After doing a batched draw, batch range + /// length phase items will be skipped. This design is to avoid having to restructure the + /// render phase unnecessarily. + fn batch_range(&self) -> &Range; + fn batch_range_mut(&mut self) -> &mut Range; + + /// Returns the [`PhaseItemExtraIndex`]. + /// + /// If present, this is either a dynamic offset or an indirect parameters + /// index. + fn extra_index(&self) -> PhaseItemExtraIndex; + + /// Returns a pair of mutable references to both the batch range and extra + /// index. + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex); +} + +/// The "extra index" associated with some [`PhaseItem`]s, alongside the +/// indirect instance index. +/// +/// Sometimes phase items require another index in addition to the range of +/// instances they already have. These can be: +/// +/// * The *dynamic offset*: a `wgpu` dynamic offset into the uniform buffer of +/// instance data. This is used on platforms that don't support storage +/// buffers, to work around uniform buffer size limitations. +/// +/// * The *indirect parameters index*: an index into the buffer that specifies +/// the indirect parameters for this [`PhaseItem`]'s drawcall. This is used when +/// indirect mode is on (as used for GPU culling). +/// +/// Note that our indirect draw functionality requires storage buffers, so it's +/// impossible to have both a dynamic offset and an indirect parameters index. +/// This convenient fact allows us to pack both indices into a single `u32`. +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub enum PhaseItemExtraIndex { + /// No extra index is present. + None, + /// A `wgpu` dynamic offset into the uniform buffer of instance data. This + /// is used on platforms that don't support storage buffers, to work around + /// uniform buffer size limitations. + DynamicOffset(u32), + /// An index into the buffer that specifies the indirect parameters for this + /// [`PhaseItem`]'s drawcall. This is used when indirect mode is on (as used + /// for GPU culling). + IndirectParametersIndex { + /// The range of indirect parameters within the indirect parameters array. + /// + /// If we're using `multi_draw_indirect_count`, this specifies the + /// maximum range of indirect parameters within that array. If batches + /// are ultimately culled out on the GPU, the actual number of draw + /// commands might be lower than the length of this range. + range: Range, + /// If `multi_draw_indirect_count` is in use, and this phase item is + /// part of a batch set, specifies the index of the batch set that this + /// phase item is a part of. + /// + /// If `multi_draw_indirect_count` isn't in use, or this phase item + /// isn't part of a batch set, this is `None`. + batch_set_index: Option, + }, +} + +impl PhaseItemExtraIndex { + /// Returns either an indirect parameters index or + /// [`PhaseItemExtraIndex::None`], as appropriate. + pub fn maybe_indirect_parameters_index( + indirect_parameters_index: Option, + ) -> PhaseItemExtraIndex { + match indirect_parameters_index { + Some(indirect_parameters_index) => PhaseItemExtraIndex::IndirectParametersIndex { + range: u32::from(indirect_parameters_index) + ..(u32::from(indirect_parameters_index) + 1), + batch_set_index: None, + }, + None => PhaseItemExtraIndex::None, + } + } + + /// Returns either a dynamic offset index or [`PhaseItemExtraIndex::None`], + /// as appropriate. + pub fn maybe_dynamic_offset(dynamic_offset: Option) -> PhaseItemExtraIndex { + match dynamic_offset { + Some(dynamic_offset) => PhaseItemExtraIndex::DynamicOffset(dynamic_offset.into()), + None => PhaseItemExtraIndex::None, + } + } +} + +/// Represents phase items that are placed into bins. The `BinKey` specifies +/// which bin they're to be placed in. Bin keys are sorted, and items within the +/// same bin are eligible to be batched together. The elements within the bins +/// aren't themselves sorted. +/// +/// An example of a binned phase item is `Opaque3d`, for which the rendering +/// order isn't critical. +pub trait BinnedPhaseItem: PhaseItem { + /// The key used for binning [`PhaseItem`]s into bins. Order the members of + /// [`BinnedPhaseItem::BinKey`] by the order of binding for best + /// performance. For example, pipeline id, draw function id, mesh asset id, + /// lowest variable bind group id such as the material bind group id, and + /// its dynamic offsets if any, next bind group and offsets, etc. This + /// reduces the need for rebinding between bins and improves performance. + type BinKey: Clone + Send + Sync + PartialEq + Eq + Ord + Hash; + + /// The key used to combine batches into batch sets. + /// + /// A *batch set* is a set of meshes that can potentially be multi-drawn + /// together. + type BatchSetKey: PhaseItemBatchSetKey; + + /// Creates a new binned phase item from the key and per-entity data. + /// + /// Unlike [`SortedPhaseItem`]s, this is generally called "just in time" + /// before rendering. The resulting phase item isn't stored in any data + /// structures, resulting in significant memory savings. + fn new( + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, + representative_entity: (Entity, MainEntity), + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self; +} + +/// A key used to combine batches into batch sets. +/// +/// A *batch set* is a set of meshes that can potentially be multi-drawn +/// together. +pub trait PhaseItemBatchSetKey: Clone + Send + Sync + PartialEq + Eq + Ord + Hash { + /// Returns true if this batch set key describes indexed meshes or false if + /// it describes non-indexed meshes. + /// + /// Bevy uses this in order to determine which kind of indirect draw + /// parameters to use, if indirect drawing is enabled. + fn indexed(&self) -> bool; +} + +/// Represents phase items that must be sorted. The `SortKey` specifies the +/// order that these items are drawn in. These are placed into a single array, +/// and the array as a whole is then sorted. +/// +/// An example of a sorted phase item is `Transparent3d`, which must be sorted +/// back to front in order to correctly render with the painter's algorithm. +pub trait SortedPhaseItem: PhaseItem { + /// The type used for ordering the items. The smallest values are drawn first. + /// This order can be calculated using the [`ViewRangefinder3d`], + /// based on the view-space `Z` value of the corresponding view matrix. + type SortKey: Ord; + + /// Determines the order in which the items are drawn. + fn sort_key(&self) -> Self::SortKey; + + /// Sorts a slice of phase items into render order. Generally if the same type + /// is batched this should use a stable sort like [`slice::sort_by_key`]. + /// In almost all other cases, this should not be altered from the default, + /// which uses an unstable sort, as this provides the best balance of CPU and GPU + /// performance. + /// + /// Implementers can optionally not sort the list at all. This is generally advisable if and + /// only if the renderer supports a depth prepass, which is by default not supported by + /// the rest of Bevy's first party rendering crates. Even then, this may have a negative + /// impact on GPU-side performance due to overdraw. + /// + /// It's advised to always profile for performance changes when changing this implementation. + #[inline] + fn sort(items: &mut [Self]) { + items.sort_unstable_by_key(Self::sort_key); + } + + /// Whether this phase item targets indexed meshes (those with both vertex + /// and index buffers as opposed to just vertex buffers). + /// + /// Bevy needs this information in order to properly group phase items + /// together for multi-draw indirect, because the GPU layout of indirect + /// commands differs between indexed and non-indexed meshes. + /// + /// If you're implementing a custom phase item that doesn't describe a mesh, + /// you can safely return false here. + fn indexed(&self) -> bool; +} + +/// A [`PhaseItem`] item, that automatically sets the appropriate render pipeline, +/// cached in the [`PipelineCache`]. +/// +/// You can use the [`SetItemPipeline`] render command to set the pipeline for this item. +pub trait CachedRenderPipelinePhaseItem: PhaseItem { + /// The id of the render pipeline, cached in the [`PipelineCache`], that will be used to draw + /// this phase item. + fn cached_pipeline(&self) -> CachedRenderPipelineId; +} + +/// A [`RenderCommand`] that sets the pipeline for the [`CachedRenderPipelinePhaseItem`]. +pub struct SetItemPipeline; + +impl RenderCommand

    for SetItemPipeline { + type Param = SRes; + type ViewQuery = (); + type ItemQuery = (); + #[inline] + fn render<'w>( + item: &P, + _view: (), + _entity: Option<()>, + pipeline_cache: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + if let Some(pipeline) = pipeline_cache + .into_inner() + .get_render_pipeline(item.cached_pipeline()) + { + pass.set_render_pipeline(pipeline); + RenderCommandResult::Success + } else { + RenderCommandResult::Skip + } + } +} + +/// This system sorts the [`PhaseItem`]s of all [`SortedRenderPhase`]s of this +/// type. +pub fn sort_phase_system(mut render_phases: ResMut>) +where + I: SortedPhaseItem, +{ + for phase in render_phases.values_mut() { + phase.sort(); + } +} + +/// Removes entities that became invisible or changed phases from the bins. +/// +/// This must run after queuing. +pub fn sweep_old_entities(mut render_phases: ResMut>) +where + BPI: BinnedPhaseItem, +{ + for phase in render_phases.0.values_mut() { + phase.sweep_old_entities(); + } +} + +impl BinnedRenderPhaseType { + pub fn mesh( + batchable: bool, + gpu_preprocessing_support: &GpuPreprocessingSupport, + ) -> BinnedRenderPhaseType { + match (batchable, gpu_preprocessing_support.max_supported_mode) { + (true, GpuPreprocessingMode::Culling) => BinnedRenderPhaseType::MultidrawableMesh, + (true, _) => BinnedRenderPhaseType::BatchableMesh, + (false, _) => BinnedRenderPhaseType::UnbatchableMesh, + } + } +} + +impl RenderBin { + /// Creates a [`RenderBin`] containing a single entity. + fn from_entity(entity: MainEntity, uniform_index: InputUniformIndex) -> RenderBin { + let mut entities = IndexMap::default(); + entities.insert(entity, uniform_index); + RenderBin { entities } + } + + /// Inserts an entity into the bin. + fn insert(&mut self, entity: MainEntity, uniform_index: InputUniformIndex) { + self.entities.insert(entity, uniform_index); + } + + /// Removes an entity from the bin. + fn remove(&mut self, entity_to_remove: MainEntity) { + self.entities.swap_remove(&entity_to_remove); + } + + /// Returns true if the bin contains no entities. + fn is_empty(&self) -> bool { + self.entities.is_empty() + } + + /// Returns the [`IndexMap`] containing all the entities in the bin, along + /// with the cached [`InputUniformIndex`] of each. + #[inline] + pub fn entities(&self) -> &IndexMap { + &self.entities + } +} + +/// An iterator that efficiently finds the indices of all zero bits in a +/// [`FixedBitSet`] and returns them in reverse order. +/// +/// [`FixedBitSet`] doesn't natively offer this functionality, so we have to +/// implement it ourselves. +#[derive(Debug)] +struct ReverseFixedBitSetZeroesIterator<'a> { + /// The bit set. + bitset: &'a FixedBitSet, + /// The next bit index we're going to scan when [`Iterator::next`] is + /// called. + bit_index: isize, +} + +impl<'a> ReverseFixedBitSetZeroesIterator<'a> { + fn new(bitset: &'a FixedBitSet) -> ReverseFixedBitSetZeroesIterator<'a> { + ReverseFixedBitSetZeroesIterator { + bitset, + bit_index: (bitset.len() as isize) - 1, + } + } +} + +impl<'a> Iterator for ReverseFixedBitSetZeroesIterator<'a> { + type Item = usize; + + fn next(&mut self) -> Option { + while self.bit_index >= 0 { + // Unpack the bit index into block and bit. + let block_index = self.bit_index / (Block::BITS as isize); + let bit_pos = self.bit_index % (Block::BITS as isize); + + // Grab the block. Mask off all bits above the one we're scanning + // from by setting them all to 1. + let mut block = self.bitset.as_slice()[block_index as usize]; + if bit_pos + 1 < (Block::BITS as isize) { + block |= (!0) << (bit_pos + 1); + } + + // Search for the next unset bit. Note that the `leading_ones` + // function counts from the MSB to the LSB, so we need to flip it to + // get the bit number. + let pos = (Block::BITS as isize) - (block.leading_ones() as isize) - 1; + + // If we found an unset bit, return it. + if pos != -1 { + let result = block_index * (Block::BITS as isize) + pos; + self.bit_index = result - 1; + return Some(result as usize); + } + + // Otherwise, go to the previous block. + self.bit_index = block_index * (Block::BITS as isize) - 1; + } + + None + } +} + +#[cfg(test)] +mod test { + use super::ReverseFixedBitSetZeroesIterator; + use fixedbitset::FixedBitSet; + use proptest::{collection::vec, prop_assert_eq, proptest}; + + proptest! { + #[test] + fn reverse_fixed_bit_set_zeroes_iterator( + bits in vec(0usize..1024usize, 0usize..1024usize), + size in 0usize..1024usize, + ) { + // Build a random bit set. + let mut bitset = FixedBitSet::new(); + bitset.grow(size); + for bit in bits { + if bit < size { + bitset.set(bit, true); + } + } + + // Iterate over the bit set backwards in a naive way, and check that + // that iteration sequence corresponds to the optimized one. + let mut iter = ReverseFixedBitSetZeroesIterator::new(&bitset); + for bit_index in (0..size).rev() { + if !bitset.contains(bit_index) { + prop_assert_eq!(iter.next(), Some(bit_index)); + } + } + + prop_assert_eq!(iter.next(), None); + } + } +} diff --git a/crates/libmarathon/src/render/render_phase/rangefinder.rs b/crates/libmarathon/src/render/render_phase/rangefinder.rs new file mode 100644 index 0000000..0a93651 --- /dev/null +++ b/crates/libmarathon/src/render/render_phase/rangefinder.rs @@ -0,0 +1,50 @@ +use bevy_math::{Affine3A, Mat4, Vec3, Vec4}; + +/// A distance calculator for the draw order of [`PhaseItem`](crate::render_phase::PhaseItem)s. +pub struct ViewRangefinder3d { + view_from_world_row_2: Vec4, +} + +impl ViewRangefinder3d { + /// Creates a 3D rangefinder for a view matrix. + pub fn from_world_from_view(world_from_view: &Affine3A) -> ViewRangefinder3d { + let view_from_world = world_from_view.inverse(); + + ViewRangefinder3d { + view_from_world_row_2: Mat4::from(view_from_world).row(2), + } + } + + /// Calculates the distance, or view-space `Z` value, for the given `translation`. + #[inline] + pub fn distance_translation(&self, translation: &Vec3) -> f32 { + // NOTE: row 2 of the inverse view matrix dotted with the translation from the model matrix + // gives the z component of translation of the mesh in view-space + self.view_from_world_row_2.dot(translation.extend(1.0)) + } + + /// Calculates the distance, or view-space `Z` value, for the given `transform`. + #[inline] + pub fn distance(&self, transform: &Mat4) -> f32 { + // NOTE: row 2 of the inverse view matrix dotted with column 3 of the model matrix + // gives the z component of translation of the mesh in view-space + self.view_from_world_row_2.dot(transform.col(3)) + } +} + +#[cfg(test)] +mod tests { + use super::ViewRangefinder3d; + use bevy_math::{Affine3A, Mat4, Vec3}; + + #[test] + fn distance() { + let view_matrix = Affine3A::from_translation(Vec3::new(0.0, 0.0, -1.0)); + let rangefinder = ViewRangefinder3d::from_world_from_view(&view_matrix); + assert_eq!(rangefinder.distance(&Mat4::IDENTITY), 1.0); + assert_eq!( + rangefinder.distance(&Mat4::from_translation(Vec3::new(0.0, 0.0, 1.0))), + 2.0 + ); + } +} diff --git a/crates/libmarathon/src/render/render_resource/batched_uniform_buffer.rs b/crates/libmarathon/src/render/render_resource/batched_uniform_buffer.rs new file mode 100644 index 0000000..a644a8b --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/batched_uniform_buffer.rs @@ -0,0 +1,157 @@ +use super::{GpuArrayBufferIndex, GpuArrayBufferable}; +use crate::render::{ + render_resource::DynamicUniformBuffer, + renderer::{RenderDevice, RenderQueue}, +}; +use core::{marker::PhantomData, num::NonZero}; +use encase::{ + private::{ArrayMetadata, BufferMut, Metadata, RuntimeSizedArray, WriteInto, Writer}, + ShaderType, +}; +use nonmax::NonMaxU32; +use wgpu::{BindingResource, Limits}; + +// 1MB else we will make really large arrays on macOS which reports very large +// `max_uniform_buffer_binding_size`. On macOS this ends up being the minimum +// size of the uniform buffer as well as the size of each chunk of data at a +// dynamic offset. +#[cfg(any( + not(feature = "webgl"), + not(target_arch = "wasm32"), + feature = "webgpu" +))] +const MAX_REASONABLE_UNIFORM_BUFFER_BINDING_SIZE: u32 = 1 << 20; + +// WebGL2 quirk: using uniform buffers larger than 4KB will cause extremely +// long shader compilation times, so the limit needs to be lower on WebGL2. +// This is due to older shader compilers/GPUs that don't support dynamically +// indexing uniform buffers, and instead emulate it with large switch statements +// over buffer indices that take a long time to compile. +#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] +const MAX_REASONABLE_UNIFORM_BUFFER_BINDING_SIZE: u32 = 1 << 12; + +/// Similar to [`DynamicUniformBuffer`], except every N elements (depending on size) +/// are grouped into a batch as an `array` in WGSL. +/// +/// This reduces the number of rebindings required due to having to pass dynamic +/// offsets to bind group commands, and if indices into the array can be passed +/// in via other means, it enables batching of draw commands. +pub struct BatchedUniformBuffer { + // Batches of fixed-size arrays of T are written to this buffer so that + // each batch in a fixed-size array can be bound at a dynamic offset. + uniforms: DynamicUniformBuffer>>, + // A batch of T are gathered into this `MaxCapacityArray` until it is full, + // then it is written into the `DynamicUniformBuffer`, cleared, and new T + // are gathered here, and so on for each batch. + temp: MaxCapacityArray>, + current_offset: u32, + dynamic_offset_alignment: u32, +} + +impl BatchedUniformBuffer { + pub fn batch_size(limits: &Limits) -> usize { + (limits + .max_uniform_buffer_binding_size + .min(MAX_REASONABLE_UNIFORM_BUFFER_BINDING_SIZE) as u64 + / T::min_size().get()) as usize + } + + pub fn new(limits: &Limits) -> Self { + let capacity = Self::batch_size(limits); + let alignment = limits.min_uniform_buffer_offset_alignment; + + Self { + uniforms: DynamicUniformBuffer::new_with_alignment(alignment as u64), + temp: MaxCapacityArray(Vec::with_capacity(capacity), capacity), + current_offset: 0, + dynamic_offset_alignment: alignment, + } + } + + #[inline] + pub fn size(&self) -> NonZero { + self.temp.size() + } + + pub fn clear(&mut self) { + self.uniforms.clear(); + self.current_offset = 0; + self.temp.0.clear(); + } + + pub fn push(&mut self, component: T) -> GpuArrayBufferIndex { + let result = GpuArrayBufferIndex { + index: self.temp.0.len() as u32, + dynamic_offset: NonMaxU32::new(self.current_offset), + element_type: PhantomData, + }; + self.temp.0.push(component); + if self.temp.0.len() == self.temp.1 { + self.flush(); + } + result + } + + pub fn flush(&mut self) { + self.uniforms.push(&self.temp); + + self.current_offset += + align_to_next(self.temp.size().get(), self.dynamic_offset_alignment as u64) as u32; + + self.temp.0.clear(); + } + + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + if !self.temp.0.is_empty() { + self.flush(); + } + self.uniforms.write_buffer(device, queue); + } + + #[inline] + pub fn binding(&self) -> Option> { + let mut binding = self.uniforms.binding(); + if let Some(BindingResource::Buffer(binding)) = &mut binding { + // MaxCapacityArray is runtime-sized so can't use T::min_size() + binding.size = Some(self.size()); + } + binding + } +} + +#[inline] +fn align_to_next(value: u64, alignment: u64) -> u64 { + debug_assert!(alignment.is_power_of_two()); + ((value - 1) | (alignment - 1)) + 1 +} + +// ---------------------------------------------------------------------------- +// MaxCapacityArray was implemented by Teodor Tanasoaia for encase. It was +// copied here as it was not yet included in an encase release and it is +// unclear if it is the correct long-term solution for encase. + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +struct MaxCapacityArray(T, usize); + +impl ShaderType for MaxCapacityArray +where + T: ShaderType, +{ + type ExtraMetadata = ArrayMetadata; + + const METADATA: Metadata = T::METADATA; + + fn size(&self) -> NonZero { + Self::METADATA.stride().mul(self.1.max(1) as u64).0 + } +} + +impl WriteInto for MaxCapacityArray +where + T: WriteInto + RuntimeSizedArray, +{ + fn write_into(&self, writer: &mut Writer) { + debug_assert!(self.0.len() <= self.1); + self.0.write_into(writer); + } +} diff --git a/crates/libmarathon/src/render/render_resource/bind_group.rs b/crates/libmarathon/src/render/render_resource/bind_group.rs new file mode 100644 index 0000000..c7b6f7e --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/bind_group.rs @@ -0,0 +1,725 @@ +use crate::render::{ + define_atomic_id, + render_asset::RenderAssets, + render_resource::{BindGroupLayout, Buffer, Sampler, TextureView}, + renderer::{RenderDevice, WgpuWrapper}, + texture::GpuImage, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::system::{SystemParam, SystemParamItem}; +pub use macros::AsBindGroup; +use core::ops::Deref; +use encase::ShaderType; +use thiserror::Error; +use wgpu::{ + BindGroupEntry, BindGroupLayoutEntry, BindingResource, SamplerBindingType, TextureViewDimension, +}; + +use super::{BindlessDescriptor, BindlessSlabResourceLimit}; + +define_atomic_id!(BindGroupId); + +/// Bind groups are responsible for binding render resources (e.g. buffers, textures, samplers) +/// to a [`TrackedRenderPass`](crate::render_phase::TrackedRenderPass). +/// This makes them accessible in the pipeline (shaders) as uniforms. +/// +/// This is a lightweight thread-safe wrapper around wgpu's own [`BindGroup`](wgpu::BindGroup), +/// which can be cloned as needed to workaround lifetime management issues. It may be converted +/// from and dereferences to wgpu's [`BindGroup`](wgpu::BindGroup). +/// +/// Can be created via [`RenderDevice::create_bind_group`](RenderDevice::create_bind_group). +#[derive(Clone, Debug)] +pub struct BindGroup { + id: BindGroupId, + value: WgpuWrapper, +} + +impl BindGroup { + /// Returns the [`BindGroupId`] representing the unique ID of the bind group. + #[inline] + pub fn id(&self) -> BindGroupId { + self.id + } +} + +impl PartialEq for BindGroup { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for BindGroup {} + +impl core::hash::Hash for BindGroup { + fn hash(&self, state: &mut H) { + self.id.0.hash(state); + } +} + +impl From for BindGroup { + fn from(value: wgpu::BindGroup) -> Self { + BindGroup { + id: BindGroupId::new(), + value: WgpuWrapper::new(value), + } + } +} + +impl<'a> From<&'a BindGroup> for Option<&'a wgpu::BindGroup> { + fn from(value: &'a BindGroup) -> Self { + Some(value.deref()) + } +} + +impl<'a> From<&'a mut BindGroup> for Option<&'a wgpu::BindGroup> { + fn from(value: &'a mut BindGroup) -> Self { + Some(&*value) + } +} + +impl Deref for BindGroup { + type Target = wgpu::BindGroup; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +/// Converts a value to a [`BindGroup`] with a given [`BindGroupLayout`], which can then be used in Bevy shaders. +/// This trait can be derived (and generally should be). Read on for details and examples. +/// +/// This is an opinionated trait that is intended to make it easy to generically +/// convert a type into a [`BindGroup`]. It provides access to specific render resources, +/// such as [`RenderAssets`] and [`crate::texture::FallbackImage`]. If a type has a [`Handle`](bevy_asset::Handle), +/// these can be used to retrieve the corresponding [`Texture`](crate::render_resource::Texture) resource. +/// +/// [`AsBindGroup::as_bind_group`] is intended to be called once, then the result cached somewhere. It is generally +/// ok to do "expensive" work here, such as creating a [`Buffer`] for a uniform. +/// +/// If for some reason a [`BindGroup`] cannot be created yet (for example, the [`Texture`](crate::render_resource::Texture) +/// for an [`Image`](bevy_image::Image) hasn't loaded yet), just return [`AsBindGroupError::RetryNextUpdate`], which signals that the caller +/// should retry again later. +/// +/// # Deriving +/// +/// This trait can be derived. Field attributes like `uniform` and `texture` are used to define which fields should be bindings, +/// what their binding type is, and what index they should be bound at: +/// +/// ``` +/// # use crate::render::render_resource::*; +/// # use bevy_image::Image; +/// # use bevy_color::LinearRgba; +/// # use bevy_asset::Handle; +/// # use crate::render::storage::ShaderStorageBuffer; +/// +/// #[derive(AsBindGroup)] +/// struct CoolMaterial { +/// #[uniform(0)] +/// color: LinearRgba, +/// #[texture(1)] +/// #[sampler(2)] +/// color_texture: Handle, +/// #[storage(3, read_only)] +/// storage_buffer: Handle, +/// #[storage(4, read_only, buffer)] +/// raw_buffer: Buffer, +/// #[storage_texture(5)] +/// storage_texture: Handle, +/// } +/// ``` +/// +/// In WGSL shaders, the binding would look like this: +/// +/// ```wgsl +/// @group(#{MATERIAL_BIND_GROUP}) @binding(0) var color: vec4; +/// @group(#{MATERIAL_BIND_GROUP}) @binding(1) var color_texture: texture_2d; +/// @group(#{MATERIAL_BIND_GROUP}) @binding(2) var color_sampler: sampler; +/// @group(#{MATERIAL_BIND_GROUP}) @binding(3) var storage_buffer: array; +/// @group(#{MATERIAL_BIND_GROUP}) @binding(4) var raw_buffer: array; +/// @group(#{MATERIAL_BIND_GROUP}) @binding(5) var storage_texture: texture_storage_2d; +/// ``` +/// Note that the "group" index is determined by the usage context. It is not defined in [`AsBindGroup`]. For example, in Bevy material bind groups +/// are generally bound to group 2. +/// +/// The following field-level attributes are supported: +/// +/// ## `uniform(BINDING_INDEX)` +/// +/// * The field will be converted to a shader-compatible type using the [`ShaderType`] trait, written to a [`Buffer`], and bound as a uniform. +/// [`ShaderType`] is implemented for most math types already, such as [`f32`], [`Vec4`](bevy_math::Vec4), and +/// [`LinearRgba`](bevy_color::LinearRgba). It can also be derived for custom structs. +/// +/// ## `texture(BINDING_INDEX, arguments)` +/// +/// * This field's [`Handle`](bevy_asset::Handle) will be used to look up the matching [`Texture`](crate::render_resource::Texture) +/// GPU resource, which will be bound as a texture in shaders. The field will be assumed to implement [`Into>>`]. In practice, +/// most fields should be a [`Handle`](bevy_asset::Handle) or [`Option>`]. If the value of an [`Option>`] is +/// [`None`], the [`crate::texture::FallbackImage`] resource will be used instead. This attribute can be used in conjunction with a `sampler` binding attribute +/// (with a different binding index) if a binding of the sampler for the [`Image`](bevy_image::Image) is also required. +/// +/// | Arguments | Values | Default | +/// |-----------------------|-------------------------------------------------------------------------|----------------------| +/// | `dimension` = "..." | `"1d"`, `"2d"`, `"2d_array"`, `"3d"`, `"cube"`, `"cube_array"` | `"2d"` | +/// | `sample_type` = "..." | `"float"`, `"depth"`, `"s_int"` or `"u_int"` | `"float"` | +/// | `filterable` = ... | `true`, `false` | `true` | +/// | `multisampled` = ... | `true`, `false` | `false` | +/// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `vertex`, `fragment` | +/// +/// ## `storage_texture(BINDING_INDEX, arguments)` +/// +/// * This field's [`Handle`](bevy_asset::Handle) will be used to look up the matching [`Texture`](crate::render_resource::Texture) +/// GPU resource, which will be bound as a storage texture in shaders. The field will be assumed to implement [`Into>>`]. In practice, +/// most fields should be a [`Handle`](bevy_asset::Handle) or [`Option>`]. If the value of an [`Option>`] is +/// [`None`], the [`crate::texture::FallbackImage`] resource will be used instead. +/// +/// | Arguments | Values | Default | +/// |------------------------|--------------------------------------------------------------------------------------------|---------------| +/// | `dimension` = "..." | `"1d"`, `"2d"`, `"2d_array"`, `"3d"`, `"cube"`, `"cube_array"` | `"2d"` | +/// | `image_format` = ... | any member of [`TextureFormat`](crate::render_resource::TextureFormat) | `Rgba8Unorm` | +/// | `access` = ... | any member of [`StorageTextureAccess`](crate::render_resource::StorageTextureAccess) | `ReadWrite` | +/// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `compute` | +/// +/// ## `sampler(BINDING_INDEX, arguments)` +/// +/// * This field's [`Handle`](bevy_asset::Handle) will be used to look up the matching [`Sampler`] GPU +/// resource, which will be bound as a sampler in shaders. The field will be assumed to implement [`Into>>`]. In practice, +/// most fields should be a [`Handle`](bevy_asset::Handle) or [`Option>`]. If the value of an [`Option>`] is +/// [`None`], the [`crate::texture::FallbackImage`] resource will be used instead. This attribute can be used in conjunction with a `texture` binding attribute +/// (with a different binding index) if a binding of the texture for the [`Image`](bevy_image::Image) is also required. +/// +/// | Arguments | Values | Default | +/// |------------------------|-------------------------------------------------------------------------|------------------------| +/// | `sampler_type` = "..." | `"filtering"`, `"non_filtering"`, `"comparison"`. | `"filtering"` | +/// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `vertex`, `fragment` | +/// +/// ## `storage(BINDING_INDEX, arguments)` +/// +/// * The field's [`Handle`](bevy_asset::Handle) will be used to look +/// up the matching [`Buffer`] GPU resource, which will be bound as a storage +/// buffer in shaders. If the `storage` attribute is used, the field is expected +/// a raw buffer, and the buffer will be bound as a storage buffer in shaders. +/// In bindless mode, `binding_array()` argument that specifies the binding +/// number of the resulting storage buffer binding array must be present. +/// +/// | Arguments | Values | Default | +/// |------------------------|-------------------------------------------------------------------------|------------------------| +/// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `vertex`, `fragment` | +/// | `read_only` | if present then value is true, otherwise false | `false` | +/// | `buffer` | if present then the field will be assumed to be a raw wgpu buffer | | +/// | `binding_array(...)` | the binding number of the binding array, for bindless mode | bindless mode disabled | +/// +/// Note that fields without field-level binding attributes will be ignored. +/// ``` +/// # use crate::render::{render_resource::AsBindGroup}; +/// # use bevy_color::LinearRgba; +/// # use bevy_asset::Handle; +/// #[derive(AsBindGroup)] +/// struct CoolMaterial { +/// #[uniform(0)] +/// color: LinearRgba, +/// this_field_is_ignored: String, +/// } +/// ``` +/// +/// As mentioned above, [`Option>`] is also supported: +/// ``` +/// # use bevy_asset::Handle; +/// # use bevy_color::LinearRgba; +/// # use bevy_image::Image; +/// # use crate::render::render_resource::AsBindGroup; +/// #[derive(AsBindGroup)] +/// struct CoolMaterial { +/// #[uniform(0)] +/// color: LinearRgba, +/// #[texture(1)] +/// #[sampler(2)] +/// color_texture: Option>, +/// } +/// ``` +/// This is useful if you want a texture to be optional. When the value is [`None`], the [`crate::texture::FallbackImage`] will be used for the binding instead, which defaults +/// to "pure white". +/// +/// Field uniforms with the same index will be combined into a single binding: +/// ``` +/// # use crate::render::{render_resource::AsBindGroup}; +/// # use bevy_color::LinearRgba; +/// #[derive(AsBindGroup)] +/// struct CoolMaterial { +/// #[uniform(0)] +/// color: LinearRgba, +/// #[uniform(0)] +/// roughness: f32, +/// } +/// ``` +/// +/// In WGSL shaders, the binding would look like this: +/// ```wgsl +/// struct CoolMaterial { +/// color: vec4, +/// roughness: f32, +/// }; +/// +/// @group(#{MATERIAL_BIND_GROUP}) @binding(0) var material: CoolMaterial; +/// ``` +/// +/// Some less common scenarios will require "struct-level" attributes. These are the currently supported struct-level attributes: +/// ## `uniform(BINDING_INDEX, ConvertedShaderType)` +/// +/// * This also creates a [`Buffer`] using [`ShaderType`] and binds it as a +/// uniform, much like the field-level `uniform` attribute. The difference is +/// that the entire [`AsBindGroup`] value is converted to `ConvertedShaderType`, +/// which must implement [`ShaderType`], instead of a specific field +/// implementing [`ShaderType`]. This is useful if more complicated conversion +/// logic is required, or when using bindless mode (see below). The conversion +/// is done using the [`AsBindGroupShaderType`] trait, +/// which is automatically implemented if `&Self` implements +/// [`Into`]. Outside of bindless mode, only use +/// [`AsBindGroupShaderType`] if access to resources like +/// [`RenderAssets`] is required. +/// +/// * In bindless mode (see `bindless(COUNT)`), this attribute becomes +/// `uniform(BINDLESS_INDEX, ConvertedShaderType, +/// binding_array(BINDING_INDEX))`. The resulting uniform buffers will be +/// available in the shader as a binding array at the given `BINDING_INDEX`. The +/// `BINDLESS_INDEX` specifies the offset of the buffer in the bindless index +/// table. +/// +/// For example, suppose that the material slot is stored in a variable named +/// `slot`, the bindless index table is named `material_indices`, and that the +/// first field (index 0) of the bindless index table type is named +/// `material`. Then specifying `#[uniform(0, StandardMaterialUniform, +/// binding_array(10)]` will create a binding array buffer declared in the +/// shader as `var material_array: +/// binding_array` and accessible as +/// `material_array[material_indices[slot].material]`. +/// +/// ## `data(BINDING_INDEX, ConvertedShaderType, binding_array(BINDING_INDEX))` +/// +/// * This is very similar to `uniform(BINDING_INDEX, ConvertedShaderType, +/// binding_array(BINDING_INDEX)` and in fact is identical if bindless mode +/// isn't being used. The difference is that, in bindless mode, the `data` +/// attribute produces a single buffer containing an array, not an array of +/// buffers. For example, suppose you had the following declaration: +/// +/// ```ignore +/// #[uniform(0, StandardMaterialUniform, binding_array(10))] +/// struct StandardMaterial { ... } +/// ``` +/// +/// In bindless mode, this will produce a binding matching the following WGSL +/// declaration: +/// +/// ```wgsl +/// @group(#{MATERIAL_BIND_GROUP}) @binding(10) var material_array: binding_array; +/// ``` +/// +/// On the other hand, if you write this declaration: +/// +/// ```ignore +/// #[data(0, StandardMaterialUniform, binding_array(10))] +/// struct StandardMaterial { ... } +/// ``` +/// +/// Then Bevy produces a binding that matches this WGSL declaration instead: +/// +/// ```wgsl +/// @group(#{MATERIAL_BIND_GROUP}) @binding(10) var material_array: array; +/// ``` +/// +/// * Just as with the structure-level `uniform` attribute, Bevy converts the +/// entire [`AsBindGroup`] to `ConvertedShaderType`, using the +/// [`AsBindGroupShaderType`] trait. +/// +/// * In non-bindless mode, the structure-level `data` attribute is the same as +/// the structure-level `uniform` attribute and produces a single uniform buffer +/// in the shader. The above example would result in a binding that looks like +/// this in WGSL in non-bindless mode: +/// +/// ```wgsl +/// @group(#{MATERIAL_BIND_GROUP}) @binding(0) var material: StandardMaterial; +/// ``` +/// +/// * For efficiency reasons, `data` is generally preferred over `uniform` +/// unless you need to place your data in individual buffers. +/// +/// ## `bind_group_data(DataType)` +/// +/// * The [`AsBindGroup`] type will be converted to some `DataType` using [`Into`] and stored +/// as [`AsBindGroup::Data`] as part of the [`AsBindGroup::as_bind_group`] call. This is useful if data needs to be stored alongside +/// the generated bind group, such as a unique identifier for a material's bind group. The most common use case for this attribute +/// is "shader pipeline specialization". See [`SpecializedRenderPipeline`](crate::render_resource::SpecializedRenderPipeline). +/// +/// ## `bindless` +/// +/// * This switch enables *bindless resources*, which changes the way Bevy +/// supplies resources (textures, and samplers) to the shader. When bindless +/// resources are enabled, and the current platform supports them, Bevy will +/// allocate textures, and samplers into *binding arrays*, separated based on +/// type and will supply your shader with indices into those arrays. +/// * Bindless textures and samplers are placed into the appropriate global +/// array defined in `bevy_render::bindless` (`bindless.wgsl`). +/// * Bevy doesn't currently support bindless buffers, except for those created +/// with the `uniform(BINDLESS_INDEX, ConvertedShaderType, +/// binding_array(BINDING_INDEX))` attribute. If you need to include a buffer in +/// your object, and you can't create the data in that buffer with the `uniform` +/// attribute, consider a non-bindless object instead. +/// * If bindless mode is enabled, the `BINDLESS` definition will be +/// available. Because not all platforms support bindless resources, you +/// should check for the presence of this definition via `#ifdef` and fall +/// back to standard bindings if it isn't present. +/// * By default, in bindless mode, binding 0 becomes the *bindless index +/// table*, which is an array of structures, each of which contains as many +/// fields of type `u32` as the highest binding number in the structure +/// annotated with `#[derive(AsBindGroup)]`. Again by default, the *i*th field +/// of the bindless index table contains the index of the resource with binding +/// *i* within the appropriate binding array. +/// * In the case of materials, the index of the applicable table within the +/// bindless index table list corresponding to the mesh currently being drawn +/// can be retrieved with +/// `mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu`. +/// * You can limit the size of the bindless slabs to N resources with the +/// `limit(N)` declaration. For example, `#[bindless(limit(16))]` ensures that +/// each slab will have no more than 16 total resources in it. If you don't +/// specify a limit, Bevy automatically picks a reasonable one for the current +/// platform. +/// * The `index_table(range(M..N), binding(B))` declaration allows you to +/// customize the layout of the bindless index table. This is useful for +/// materials that are composed of multiple bind groups, such as +/// `ExtendedMaterial`. In such cases, there will be multiple bindless index +/// tables, so they can't both be assigned to binding 0 or their bindings will +/// conflict. +/// - The `binding(B)` attribute of the `index_table` attribute allows you to +/// customize the binding (`@binding(B)`, in the shader) at which the index +/// table will be bound. +/// - The `range(M, N)` attribute of the `index_table` attribute allows you to +/// change the mapping from the field index in the bindless index table to the +/// bindless index. Instead of the field at index $i$ being mapped to the +/// bindless index $i$, with the `range(M, N)` attribute the field at index +/// $i$ in the bindless index table is mapped to the bindless index $i$ + M. +/// The size of the index table will be set to N - M. Note that this may +/// result in the table being too small to contain all the bindless bindings. +/// * The purpose of bindless mode is to improve performance by reducing +/// state changes. By grouping resources together into binding arrays, Bevy +/// doesn't have to modify GPU state as often, decreasing API and driver +/// overhead. +/// * See the `shaders/shader_material_bindless` example for an example of how +/// to use bindless mode. See the `shaders/extended_material_bindless` example +/// for a more exotic example of bindless mode that demonstrates the +/// `index_table` attribute. +/// * The following diagram illustrates how bindless mode works using a subset +/// of `StandardMaterial`: +/// +/// ```text +/// Shader Bindings Sampler Binding Array +/// +----+-----------------------------+ +-----------+-----------+-----+ +/// +---| 0 | material_indices | +->| sampler 0 | sampler 1 | ... | +/// | +----+-----------------------------+ | +-----------+-----------+-----+ +/// | | 1 | bindless_samplers_filtering +--+ ^ +/// | +----+-----------------------------+ +-------------------------------+ +/// | | .. | ... | | +/// | +----+-----------------------------+ Texture Binding Array | +/// | | 5 | bindless_textures_2d +--+ +-----------+-----------+-----+ | +/// | +----+-----------------------------+ +->| texture 0 | texture 1 | ... | | +/// | | .. | ... | +-----------+-----------+-----+ | +/// | +----+-----------------------------+ ^ | +/// | + 10 | material_array +--+ +---------------------------+ | +/// | +----+-----------------------------+ | | | +/// | | Buffer Binding Array | | +/// | | +----------+----------+-----+ | | +/// | +->| buffer 0 | buffer 1 | ... | | | +/// | Material Bindless Indices +----------+----------+-----+ | | +/// | +----+-----------------------------+ ^ | | +/// +-->| 0 | material +----------+ | | +/// +----+-----------------------------+ | | +/// | 1 | base_color_texture +---------------------------------------+ | +/// +----+-----------------------------+ | +/// | 2 | base_color_sampler +-------------------------------------------+ +/// +----+-----------------------------+ +/// | .. | ... | +/// +----+-----------------------------+ +/// ``` +/// +/// The previous `CoolMaterial` example illustrating "combining multiple field-level uniform attributes with the same binding index" can +/// also be equivalently represented with a single struct-level uniform attribute: +/// ``` +/// # use crate::render::{render_resource::{AsBindGroup, ShaderType}}; +/// # use bevy_color::LinearRgba; +/// #[derive(AsBindGroup)] +/// #[uniform(0, CoolMaterialUniform)] +/// struct CoolMaterial { +/// color: LinearRgba, +/// roughness: f32, +/// } +/// +/// #[derive(ShaderType)] +/// struct CoolMaterialUniform { +/// color: LinearRgba, +/// roughness: f32, +/// } +/// +/// impl From<&CoolMaterial> for CoolMaterialUniform { +/// fn from(material: &CoolMaterial) -> CoolMaterialUniform { +/// CoolMaterialUniform { +/// color: material.color, +/// roughness: material.roughness, +/// } +/// } +/// } +/// ``` +/// +/// Setting `bind_group_data` looks like this: +/// ``` +/// # use crate::render::{render_resource::AsBindGroup}; +/// # use bevy_color::LinearRgba; +/// #[derive(AsBindGroup)] +/// #[bind_group_data(CoolMaterialKey)] +/// struct CoolMaterial { +/// #[uniform(0)] +/// color: LinearRgba, +/// is_shaded: bool, +/// } +/// +/// // Materials keys are intended to be small, cheap to hash, and +/// // uniquely identify a specific material permutation. +/// #[repr(C)] +/// #[derive(Copy, Clone, Hash, Eq, PartialEq)] +/// struct CoolMaterialKey { +/// is_shaded: bool, +/// } +/// +/// impl From<&CoolMaterial> for CoolMaterialKey { +/// fn from(material: &CoolMaterial) -> CoolMaterialKey { +/// CoolMaterialKey { +/// is_shaded: material.is_shaded, +/// } +/// } +/// } +/// ``` +pub trait AsBindGroup { + /// Data that will be stored alongside the "prepared" bind group. + type Data: Send + Sync; + + type Param: SystemParam + 'static; + + /// The number of slots per bind group, if bindless mode is enabled. + /// + /// If this bind group doesn't use bindless, then this will be `None`. + /// + /// Note that the *actual* slot count may be different from this value, due + /// to platform limitations. For example, if bindless resources aren't + /// supported on this platform, the actual slot count will be 1. + fn bindless_slot_count() -> Option { + None + } + + /// True if the hardware *actually* supports bindless textures for this + /// type, taking the device and driver capabilities into account. + /// + /// If this type doesn't use bindless textures, then the return value from + /// this function is meaningless. + fn bindless_supported(_: &RenderDevice) -> bool { + true + } + + /// label + fn label() -> Option<&'static str> { + None + } + + /// Creates a bind group for `self` matching the layout defined in [`AsBindGroup::bind_group_layout`]. + fn as_bind_group( + &self, + layout: &BindGroupLayout, + render_device: &RenderDevice, + param: &mut SystemParamItem<'_, '_, Self::Param>, + ) -> Result { + let UnpreparedBindGroup { bindings } = + Self::unprepared_bind_group(self, layout, render_device, param, false)?; + + let entries = bindings + .iter() + .map(|(index, binding)| BindGroupEntry { + binding: *index, + resource: binding.get_binding(), + }) + .collect::>(); + + let bind_group = render_device.create_bind_group(Self::label(), layout, &entries); + + Ok(PreparedBindGroup { + bindings, + bind_group, + }) + } + + fn bind_group_data(&self) -> Self::Data; + + /// Returns a vec of (binding index, `OwnedBindingResource`). + /// + /// In cases where `OwnedBindingResource` is not available (as for bindless + /// texture arrays currently), an implementor may return + /// `AsBindGroupError::CreateBindGroupDirectly` from this function and + /// instead define `as_bind_group` directly. This may prevent certain + /// features, such as bindless mode, from working correctly. + /// + /// Set `force_no_bindless` to true to require that bindless textures *not* + /// be used. `ExtendedMaterial` uses this in order to ensure that the base + /// material doesn't use bindless mode if the extension doesn't. + fn unprepared_bind_group( + &self, + layout: &BindGroupLayout, + render_device: &RenderDevice, + param: &mut SystemParamItem<'_, '_, Self::Param>, + force_no_bindless: bool, + ) -> Result; + + /// Creates the bind group layout matching all bind groups returned by + /// [`AsBindGroup::as_bind_group`] + fn bind_group_layout(render_device: &RenderDevice) -> BindGroupLayout + where + Self: Sized, + { + render_device.create_bind_group_layout( + Self::label(), + &Self::bind_group_layout_entries(render_device, false), + ) + } + + /// Returns a vec of bind group layout entries. + /// + /// Set `force_no_bindless` to true to require that bindless textures *not* + /// be used. `ExtendedMaterial` uses this in order to ensure that the base + /// material doesn't use bindless mode if the extension doesn't. + fn bind_group_layout_entries( + render_device: &RenderDevice, + force_no_bindless: bool, + ) -> Vec + where + Self: Sized; + + fn bindless_descriptor() -> Option { + None + } +} + +/// An error that occurs during [`AsBindGroup::as_bind_group`] calls. +#[derive(Debug, Error)] +pub enum AsBindGroupError { + /// The bind group could not be generated. Try again next frame. + #[error("The bind group could not be generated")] + RetryNextUpdate, + #[error("Create the bind group via `as_bind_group()` instead")] + CreateBindGroupDirectly, + #[error("At binding index {0}, the provided image sampler `{1}` does not match the required sampler type(s) `{2}`.")] + InvalidSamplerType(u32, String, String), +} + +/// A prepared bind group returned as a result of [`AsBindGroup::as_bind_group`]. +pub struct PreparedBindGroup { + pub bindings: BindingResources, + pub bind_group: BindGroup, +} + +/// a map containing `OwnedBindingResource`s, keyed by the target binding index +pub struct UnpreparedBindGroup { + pub bindings: BindingResources, +} + +/// A pair of binding index and binding resource, used as part of +/// [`PreparedBindGroup`] and [`UnpreparedBindGroup`]. +#[derive(Deref, DerefMut)] +pub struct BindingResources(pub Vec<(u32, OwnedBindingResource)>); + +/// An owned binding resource of any type (ex: a [`Buffer`], [`TextureView`], etc). +/// This is used by types like [`PreparedBindGroup`] to hold a single list of all +/// render resources used by bindings. +#[derive(Debug)] +pub enum OwnedBindingResource { + Buffer(Buffer), + TextureView(TextureViewDimension, TextureView), + Sampler(SamplerBindingType, Sampler), + Data(OwnedData), +} + +/// Data that will be copied into a GPU buffer. +/// +/// This corresponds to the `#[data]` attribute in `AsBindGroup`. +#[derive(Debug, Deref, DerefMut)] +pub struct OwnedData(pub Vec); + +impl OwnedBindingResource { + /// Creates a [`BindingResource`] reference to this + /// [`OwnedBindingResource`]. + /// + /// Note that this operation panics if passed a + /// [`OwnedBindingResource::Data`], because [`OwnedData`] doesn't itself + /// correspond to any binding and instead requires the + /// `MaterialBindGroupAllocator` to pack it into a buffer. + pub fn get_binding(&self) -> BindingResource<'_> { + match self { + OwnedBindingResource::Buffer(buffer) => buffer.as_entire_binding(), + OwnedBindingResource::TextureView(_, view) => BindingResource::TextureView(view), + OwnedBindingResource::Sampler(_, sampler) => BindingResource::Sampler(sampler), + OwnedBindingResource::Data(_) => panic!("`OwnedData` has no binding resource"), + } + } +} + +/// Converts a value to a [`ShaderType`] for use in a bind group. +/// +/// This is automatically implemented for references that implement [`Into`]. +/// Generally normal [`Into`] / [`From`] impls should be preferred, but +/// sometimes additional runtime metadata is required. +/// This exists largely to make some [`AsBindGroup`] use cases easier. +pub trait AsBindGroupShaderType { + /// Return the `T` [`ShaderType`] for `self`. When used in [`AsBindGroup`] + /// derives, it is safe to assume that all images in `self` exist. + fn as_bind_group_shader_type(&self, images: &RenderAssets) -> T; +} + +impl AsBindGroupShaderType for T +where + for<'a> &'a T: Into, +{ + #[inline] + fn as_bind_group_shader_type(&self, _images: &RenderAssets) -> U { + self.into() + } +} + +#[cfg(test)] +mod test { + use super::*; + use bevy_asset::Handle; + use bevy_image::Image; + + #[test] + fn texture_visibility() { + #[expect( + dead_code, + reason = "This is a derive macro compilation test. It will not be constructed." + )] + #[derive(AsBindGroup)] + pub struct TextureVisibilityTest { + #[texture(0, visibility(all))] + pub all: Handle, + #[texture(1, visibility(none))] + pub none: Handle, + #[texture(2, visibility(fragment))] + pub fragment: Handle, + #[texture(3, visibility(vertex))] + pub vertex: Handle, + #[texture(4, visibility(compute))] + pub compute: Handle, + #[texture(5, visibility(vertex, fragment))] + pub vertex_fragment: Handle, + #[texture(6, visibility(vertex, compute))] + pub vertex_compute: Handle, + #[texture(7, visibility(fragment, compute))] + pub fragment_compute: Handle, + #[texture(8, visibility(vertex, fragment, compute))] + pub vertex_fragment_compute: Handle, + } + } +} diff --git a/crates/libmarathon/src/render/render_resource/bind_group_entries.rs b/crates/libmarathon/src/render/render_resource/bind_group_entries.rs new file mode 100644 index 0000000..274aa11 --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/bind_group_entries.rs @@ -0,0 +1,322 @@ +use variadics_please::all_tuples_with_size; +use wgpu::{BindGroupEntry, BindingResource}; + +use super::{Sampler, TextureView}; + +/// Helper for constructing bindgroups. +/// +/// Allows constructing the descriptor's entries as: +/// ```ignore (render_device cannot be easily accessed) +/// render_device.create_bind_group( +/// "my_bind_group", +/// &my_layout, +/// &BindGroupEntries::with_indices(( +/// (2, &my_sampler), +/// (3, my_uniform), +/// )), +/// ); +/// ``` +/// +/// instead of +/// +/// ```ignore (render_device cannot be easily accessed) +/// render_device.create_bind_group( +/// "my_bind_group", +/// &my_layout, +/// &[ +/// BindGroupEntry { +/// binding: 2, +/// resource: BindingResource::Sampler(&my_sampler), +/// }, +/// BindGroupEntry { +/// binding: 3, +/// resource: my_uniform, +/// }, +/// ], +/// ); +/// ``` +/// +/// or +/// +/// ```ignore (render_device cannot be easily accessed) +/// render_device.create_bind_group( +/// "my_bind_group", +/// &my_layout, +/// &BindGroupEntries::sequential(( +/// &my_sampler, +/// my_uniform, +/// )), +/// ); +/// ``` +/// +/// instead of +/// +/// ```ignore (render_device cannot be easily accessed) +/// render_device.create_bind_group( +/// "my_bind_group", +/// &my_layout, +/// &[ +/// BindGroupEntry { +/// binding: 0, +/// resource: BindingResource::Sampler(&my_sampler), +/// }, +/// BindGroupEntry { +/// binding: 1, +/// resource: my_uniform, +/// }, +/// ], +/// ); +/// ``` +/// +/// or +/// +/// ```ignore (render_device cannot be easily accessed) +/// render_device.create_bind_group( +/// "my_bind_group", +/// &my_layout, +/// &BindGroupEntries::single(my_uniform), +/// ); +/// ``` +/// +/// instead of +/// +/// ```ignore (render_device cannot be easily accessed) +/// render_device.create_bind_group( +/// "my_bind_group", +/// &my_layout, +/// &[ +/// BindGroupEntry { +/// binding: 0, +/// resource: my_uniform, +/// }, +/// ], +/// ); +/// ``` +pub struct BindGroupEntries<'b, const N: usize = 1> { + entries: [BindGroupEntry<'b>; N], +} + +impl<'b, const N: usize> BindGroupEntries<'b, N> { + #[inline] + pub fn sequential(resources: impl IntoBindingArray<'b, N>) -> Self { + let mut i = 0; + Self { + entries: resources.into_array().map(|resource| { + let binding = i; + i += 1; + BindGroupEntry { binding, resource } + }), + } + } + + #[inline] + pub fn with_indices(indexed_resources: impl IntoIndexedBindingArray<'b, N>) -> Self { + Self { + entries: indexed_resources + .into_array() + .map(|(binding, resource)| BindGroupEntry { binding, resource }), + } + } +} + +impl<'b> BindGroupEntries<'b, 1> { + pub fn single(resource: impl IntoBinding<'b>) -> [BindGroupEntry<'b>; 1] { + [BindGroupEntry { + binding: 0, + resource: resource.into_binding(), + }] + } +} + +impl<'b, const N: usize> core::ops::Deref for BindGroupEntries<'b, N> { + type Target = [BindGroupEntry<'b>]; + + fn deref(&self) -> &[BindGroupEntry<'b>] { + &self.entries + } +} + +pub trait IntoBinding<'a> { + fn into_binding(self) -> BindingResource<'a>; +} + +impl<'a> IntoBinding<'a> for &'a TextureView { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureView(self) + } +} + +impl<'a> IntoBinding<'a> for &'a wgpu::TextureView { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureView(self) + } +} + +impl<'a> IntoBinding<'a> for &'a [&'a wgpu::TextureView] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureViewArray(self) + } +} + +impl<'a> IntoBinding<'a> for &'a Sampler { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::Sampler(self) + } +} + +impl<'a> IntoBinding<'a> for &'a [&'a wgpu::Sampler] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::SamplerArray(self) + } +} + +impl<'a> IntoBinding<'a> for BindingResource<'a> { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + self + } +} + +impl<'a> IntoBinding<'a> for wgpu::BufferBinding<'a> { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::Buffer(self) + } +} + +impl<'a> IntoBinding<'a> for &'a [wgpu::BufferBinding<'a>] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::BufferArray(self) + } +} + +pub trait IntoBindingArray<'b, const N: usize> { + fn into_array(self) -> [BindingResource<'b>; N]; +} + +macro_rules! impl_to_binding_slice { + ($N: expr, $(#[$meta:meta])* $(($T: ident, $I: ident)),*) => { + $(#[$meta])* + impl<'b, $($T: IntoBinding<'b>),*> IntoBindingArray<'b, $N> for ($($T,)*) { + #[inline] + fn into_array(self) -> [BindingResource<'b>; $N] { + let ($($I,)*) = self; + [$($I.into_binding(), )*] + } + } + } +} + +all_tuples_with_size!( + #[doc(fake_variadic)] + impl_to_binding_slice, + 1, + 32, + T, + s +); + +pub trait IntoIndexedBindingArray<'b, const N: usize> { + fn into_array(self) -> [(u32, BindingResource<'b>); N]; +} + +macro_rules! impl_to_indexed_binding_slice { + ($N: expr, $(($T: ident, $S: ident, $I: ident)),*) => { + impl<'b, $($T: IntoBinding<'b>),*> IntoIndexedBindingArray<'b, $N> for ($((u32, $T),)*) { + #[inline] + fn into_array(self) -> [(u32, BindingResource<'b>); $N] { + let ($(($S, $I),)*) = self; + [$(($S, $I.into_binding())), *] + } + } + } +} + +all_tuples_with_size!(impl_to_indexed_binding_slice, 1, 32, T, n, s); + +pub struct DynamicBindGroupEntries<'b> { + entries: Vec>, +} + +impl<'b> Default for DynamicBindGroupEntries<'b> { + fn default() -> Self { + Self::new() + } +} + +impl<'b> DynamicBindGroupEntries<'b> { + pub fn sequential(entries: impl IntoBindingArray<'b, N>) -> Self { + Self { + entries: entries + .into_array() + .into_iter() + .enumerate() + .map(|(ix, resource)| BindGroupEntry { + binding: ix as u32, + resource, + }) + .collect(), + } + } + + pub fn extend_sequential( + mut self, + entries: impl IntoBindingArray<'b, N>, + ) -> Self { + let start = self.entries.last().unwrap().binding + 1; + self.entries.extend( + entries + .into_array() + .into_iter() + .enumerate() + .map(|(ix, resource)| BindGroupEntry { + binding: start + ix as u32, + resource, + }), + ); + self + } + + pub fn new_with_indices(entries: impl IntoIndexedBindingArray<'b, N>) -> Self { + Self { + entries: entries + .into_array() + .into_iter() + .map(|(binding, resource)| BindGroupEntry { binding, resource }) + .collect(), + } + } + + pub fn new() -> Self { + Self { + entries: Vec::new(), + } + } + + pub fn extend_with_indices( + mut self, + entries: impl IntoIndexedBindingArray<'b, N>, + ) -> Self { + self.entries.extend( + entries + .into_array() + .into_iter() + .map(|(binding, resource)| BindGroupEntry { binding, resource }), + ); + self + } +} + +impl<'b> core::ops::Deref for DynamicBindGroupEntries<'b> { + type Target = [BindGroupEntry<'b>]; + + fn deref(&self) -> &[BindGroupEntry<'b>] { + &self.entries + } +} diff --git a/crates/libmarathon/src/render/render_resource/bind_group_layout.rs b/crates/libmarathon/src/render/render_resource/bind_group_layout.rs new file mode 100644 index 0000000..dfc2b0a --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/bind_group_layout.rs @@ -0,0 +1,81 @@ +use crate::render::{define_atomic_id, renderer::RenderDevice, renderer::WgpuWrapper}; +use bevy_ecs::system::Res; +use bevy_platform::sync::OnceLock; +use core::ops::Deref; + +define_atomic_id!(BindGroupLayoutId); + +/// Bind group layouts define the interface of resources (e.g. buffers, textures, samplers) +/// for a shader. The actual resource binding is done via a [`BindGroup`](super::BindGroup). +/// +/// This is a lightweight thread-safe wrapper around wgpu's own [`BindGroupLayout`](wgpu::BindGroupLayout), +/// which can be cloned as needed to workaround lifetime management issues. It may be converted +/// from and dereferences to wgpu's [`BindGroupLayout`](wgpu::BindGroupLayout). +/// +/// Can be created via [`RenderDevice::create_bind_group_layout`](crate::renderer::RenderDevice::create_bind_group_layout). +#[derive(Clone, Debug)] +pub struct BindGroupLayout { + id: BindGroupLayoutId, + value: WgpuWrapper, +} + +impl PartialEq for BindGroupLayout { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for BindGroupLayout {} + +impl core::hash::Hash for BindGroupLayout { + fn hash(&self, state: &mut H) { + self.id.0.hash(state); + } +} + +impl BindGroupLayout { + /// Returns the [`BindGroupLayoutId`] representing the unique ID of the bind group layout. + #[inline] + pub fn id(&self) -> BindGroupLayoutId { + self.id + } + + #[inline] + pub fn value(&self) -> &wgpu::BindGroupLayout { + &self.value + } +} + +impl From for BindGroupLayout { + fn from(value: wgpu::BindGroupLayout) -> Self { + BindGroupLayout { + id: BindGroupLayoutId::new(), + value: WgpuWrapper::new(value), + } + } +} + +impl Deref for BindGroupLayout { + type Target = wgpu::BindGroupLayout; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +static EMPTY_BIND_GROUP_LAYOUT: OnceLock = OnceLock::new(); + +pub(crate) fn init_empty_bind_group_layout(render_device: Res) { + let layout = render_device.create_bind_group_layout(Some("empty_bind_group_layout"), &[]); + EMPTY_BIND_GROUP_LAYOUT + .set(layout) + .expect("init_empty_bind_group_layout was called more than once"); +} + +pub fn empty_bind_group_layout() -> BindGroupLayout { + EMPTY_BIND_GROUP_LAYOUT + .get() + .expect("init_empty_bind_group_layout was not called") + .clone() +} diff --git a/crates/libmarathon/src/render/render_resource/bind_group_layout_entries.rs b/crates/libmarathon/src/render/render_resource/bind_group_layout_entries.rs new file mode 100644 index 0000000..99f2662 --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/bind_group_layout_entries.rs @@ -0,0 +1,592 @@ +use core::num::NonZero; +use variadics_please::all_tuples_with_size; +use wgpu::{BindGroupLayoutEntry, BindingType, ShaderStages}; + +/// Helper for constructing bind group layouts. +/// +/// Allows constructing the layout's entries as: +/// ```ignore (render_device cannot be easily accessed) +/// let layout = render_device.create_bind_group_layout( +/// "my_bind_group_layout", +/// &BindGroupLayoutEntries::with_indices( +/// // The layout entries will only be visible in the fragment stage +/// ShaderStages::FRAGMENT, +/// ( +/// // Screen texture +/// (2, texture_2d(TextureSampleType::Float { filterable: true })), +/// // Sampler +/// (3, sampler(SamplerBindingType::Filtering)), +/// ), +/// ), +/// ); +/// ``` +/// +/// instead of +/// +/// ```ignore (render_device cannot be easily accessed) +/// let layout = render_device.create_bind_group_layout( +/// "my_bind_group_layout", +/// &[ +/// // Screen texture +/// BindGroupLayoutEntry { +/// binding: 2, +/// visibility: ShaderStages::FRAGMENT, +/// ty: BindingType::Texture { +/// sample_type: TextureSampleType::Float { filterable: true }, +/// view_dimension: TextureViewDimension::D2, +/// multisampled: false, +/// }, +/// count: None, +/// }, +/// // Sampler +/// BindGroupLayoutEntry { +/// binding: 3, +/// visibility: ShaderStages::FRAGMENT, +/// ty: BindingType::Sampler(SamplerBindingType::Filtering), +/// count: None, +/// }, +/// ], +/// ); +/// ``` +/// +/// or +/// +/// ```ignore (render_device cannot be easily accessed) +/// render_device.create_bind_group_layout( +/// "my_bind_group_layout", +/// &BindGroupLayoutEntries::sequential( +/// ShaderStages::FRAGMENT, +/// ( +/// // Screen texture +/// texture_2d(TextureSampleType::Float { filterable: true }), +/// // Sampler +/// sampler(SamplerBindingType::Filtering), +/// ), +/// ), +/// ); +/// ``` +/// +/// instead of +/// +/// ```ignore (render_device cannot be easily accessed) +/// let layout = render_device.create_bind_group_layout( +/// "my_bind_group_layout", +/// &[ +/// // Screen texture +/// BindGroupLayoutEntry { +/// binding: 0, +/// visibility: ShaderStages::FRAGMENT, +/// ty: BindingType::Texture { +/// sample_type: TextureSampleType::Float { filterable: true }, +/// view_dimension: TextureViewDimension::D2, +/// multisampled: false, +/// }, +/// count: None, +/// }, +/// // Sampler +/// BindGroupLayoutEntry { +/// binding: 1, +/// visibility: ShaderStages::FRAGMENT, +/// ty: BindingType::Sampler(SamplerBindingType::Filtering), +/// count: None, +/// }, +/// ], +/// ); +/// ``` +/// +/// or +/// +/// ```ignore (render_device cannot be easily accessed) +/// render_device.create_bind_group_layout( +/// "my_bind_group_layout", +/// &BindGroupLayoutEntries::single( +/// ShaderStages::FRAGMENT, +/// texture_2d(TextureSampleType::Float { filterable: true }), +/// ), +/// ); +/// ``` +/// +/// instead of +/// +/// ```ignore (render_device cannot be easily accessed) +/// let layout = render_device.create_bind_group_layout( +/// "my_bind_group_layout", +/// &[ +/// BindGroupLayoutEntry { +/// binding: 0, +/// visibility: ShaderStages::FRAGMENT, +/// ty: BindingType::Texture { +/// sample_type: TextureSampleType::Float { filterable: true }, +/// view_dimension: TextureViewDimension::D2, +/// multisampled: false, +/// }, +/// count: None, +/// }, +/// ], +/// ); +/// ``` + +#[derive(Clone, Copy)] +pub struct BindGroupLayoutEntryBuilder { + ty: BindingType, + visibility: Option, + count: Option>, +} + +impl BindGroupLayoutEntryBuilder { + pub fn visibility(mut self, visibility: ShaderStages) -> Self { + self.visibility = Some(visibility); + self + } + + pub fn count(mut self, count: NonZero) -> Self { + self.count = Some(count); + self + } + + pub fn build(&self, binding: u32, default_visibility: ShaderStages) -> BindGroupLayoutEntry { + BindGroupLayoutEntry { + binding, + ty: self.ty, + visibility: self.visibility.unwrap_or(default_visibility), + count: self.count, + } + } +} + +pub struct BindGroupLayoutEntries { + entries: [BindGroupLayoutEntry; N], +} + +impl BindGroupLayoutEntries { + #[inline] + pub fn sequential( + default_visibility: ShaderStages, + entries_ext: impl IntoBindGroupLayoutEntryBuilderArray, + ) -> Self { + let mut i = 0; + Self { + entries: entries_ext.into_array().map(|entry| { + let binding = i; + i += 1; + entry.build(binding, default_visibility) + }), + } + } + + #[inline] + pub fn with_indices( + default_visibility: ShaderStages, + indexed_entries: impl IntoIndexedBindGroupLayoutEntryBuilderArray, + ) -> Self { + Self { + entries: indexed_entries + .into_array() + .map(|(binding, entry)| entry.build(binding, default_visibility)), + } + } +} + +impl BindGroupLayoutEntries<1> { + pub fn single( + visibility: ShaderStages, + resource: impl IntoBindGroupLayoutEntryBuilder, + ) -> [BindGroupLayoutEntry; 1] { + [resource + .into_bind_group_layout_entry_builder() + .build(0, visibility)] + } +} + +impl core::ops::Deref for BindGroupLayoutEntries { + type Target = [BindGroupLayoutEntry]; + fn deref(&self) -> &[BindGroupLayoutEntry] { + &self.entries + } +} + +pub trait IntoBindGroupLayoutEntryBuilder { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder; +} + +impl IntoBindGroupLayoutEntryBuilder for BindingType { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + BindGroupLayoutEntryBuilder { + ty: self, + visibility: None, + count: None, + } + } +} + +impl IntoBindGroupLayoutEntryBuilder for BindGroupLayoutEntry { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + if self.binding != u32::MAX { + tracing::warn!("The BindGroupLayoutEntries api ignores the binding index when converting a raw wgpu::BindGroupLayoutEntry. You can ignore this warning by setting it to u32::MAX."); + } + BindGroupLayoutEntryBuilder { + ty: self.ty, + visibility: Some(self.visibility), + count: self.count, + } + } +} + +impl IntoBindGroupLayoutEntryBuilder for BindGroupLayoutEntryBuilder { + fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { + self + } +} + +pub trait IntoBindGroupLayoutEntryBuilderArray { + fn into_array(self) -> [BindGroupLayoutEntryBuilder; N]; +} +macro_rules! impl_to_binding_type_slice { + ($N: expr, $(#[$meta:meta])* $(($T: ident, $I: ident)),*) => { + $(#[$meta])* + impl<$($T: IntoBindGroupLayoutEntryBuilder),*> IntoBindGroupLayoutEntryBuilderArray<$N> for ($($T,)*) { + #[inline] + fn into_array(self) -> [BindGroupLayoutEntryBuilder; $N] { + let ($($I,)*) = self; + [$($I.into_bind_group_layout_entry_builder(), )*] + } + } + } +} +all_tuples_with_size!( + #[doc(fake_variadic)] + impl_to_binding_type_slice, + 1, + 32, + T, + s +); + +pub trait IntoIndexedBindGroupLayoutEntryBuilderArray { + fn into_array(self) -> [(u32, BindGroupLayoutEntryBuilder); N]; +} +macro_rules! impl_to_indexed_binding_type_slice { + ($N: expr, $(($T: ident, $S: ident, $I: ident)),*) => { + impl<$($T: IntoBindGroupLayoutEntryBuilder),*> IntoIndexedBindGroupLayoutEntryBuilderArray<$N> for ($((u32, $T),)*) { + #[inline] + fn into_array(self) -> [(u32, BindGroupLayoutEntryBuilder); $N] { + let ($(($S, $I),)*) = self; + [$(($S, $I.into_bind_group_layout_entry_builder())), *] + } + } + } +} +all_tuples_with_size!(impl_to_indexed_binding_type_slice, 1, 32, T, n, s); + +impl IntoBindGroupLayoutEntryBuilderArray for [BindGroupLayoutEntry; N] { + fn into_array(self) -> [BindGroupLayoutEntryBuilder; N] { + self.map(IntoBindGroupLayoutEntryBuilder::into_bind_group_layout_entry_builder) + } +} + +pub struct DynamicBindGroupLayoutEntries { + default_visibility: ShaderStages, + entries: Vec, +} + +impl DynamicBindGroupLayoutEntries { + pub fn sequential( + default_visibility: ShaderStages, + entries: impl IntoBindGroupLayoutEntryBuilderArray, + ) -> Self { + Self { + default_visibility, + entries: entries + .into_array() + .into_iter() + .enumerate() + .map(|(ix, resource)| resource.build(ix as u32, default_visibility)) + .collect(), + } + } + + pub fn extend_sequential( + mut self, + entries: impl IntoBindGroupLayoutEntryBuilderArray, + ) -> Self { + let start = self.entries.last().unwrap().binding + 1; + self.entries.extend( + entries + .into_array() + .into_iter() + .enumerate() + .map(|(ix, resource)| resource.build(start + ix as u32, self.default_visibility)), + ); + self + } + + pub fn new_with_indices( + default_visibility: ShaderStages, + entries: impl IntoIndexedBindGroupLayoutEntryBuilderArray, + ) -> Self { + Self { + default_visibility, + entries: entries + .into_array() + .into_iter() + .map(|(binding, resource)| resource.build(binding, default_visibility)) + .collect(), + } + } + + pub fn new(default_visibility: ShaderStages) -> Self { + Self { + default_visibility, + entries: Vec::new(), + } + } + + pub fn extend_with_indices( + mut self, + entries: impl IntoIndexedBindGroupLayoutEntryBuilderArray, + ) -> Self { + self.entries.extend( + entries + .into_array() + .into_iter() + .map(|(binding, resource)| resource.build(binding, self.default_visibility)), + ); + self + } +} + +impl core::ops::Deref for DynamicBindGroupLayoutEntries { + type Target = [BindGroupLayoutEntry]; + + fn deref(&self) -> &[BindGroupLayoutEntry] { + &self.entries + } +} + +pub mod binding_types { + use crate::render::render_resource::{ + BufferBindingType, SamplerBindingType, TextureSampleType, TextureViewDimension, + }; + use core::num::NonZero; + use encase::ShaderType; + use wgpu::{StorageTextureAccess, TextureFormat}; + + use super::*; + + pub fn storage_buffer(has_dynamic_offset: bool) -> BindGroupLayoutEntryBuilder { + storage_buffer_sized(has_dynamic_offset, Some(T::min_size())) + } + + pub fn storage_buffer_sized( + has_dynamic_offset: bool, + min_binding_size: Option>, + ) -> BindGroupLayoutEntryBuilder { + BindingType::Buffer { + ty: BufferBindingType::Storage { read_only: false }, + has_dynamic_offset, + min_binding_size, + } + .into_bind_group_layout_entry_builder() + } + + pub fn storage_buffer_read_only( + has_dynamic_offset: bool, + ) -> BindGroupLayoutEntryBuilder { + storage_buffer_read_only_sized(has_dynamic_offset, Some(T::min_size())) + } + + pub fn storage_buffer_read_only_sized( + has_dynamic_offset: bool, + min_binding_size: Option>, + ) -> BindGroupLayoutEntryBuilder { + BindingType::Buffer { + ty: BufferBindingType::Storage { read_only: true }, + has_dynamic_offset, + min_binding_size, + } + .into_bind_group_layout_entry_builder() + } + + pub fn uniform_buffer(has_dynamic_offset: bool) -> BindGroupLayoutEntryBuilder { + uniform_buffer_sized(has_dynamic_offset, Some(T::min_size())) + } + + pub fn uniform_buffer_sized( + has_dynamic_offset: bool, + min_binding_size: Option>, + ) -> BindGroupLayoutEntryBuilder { + BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset, + min_binding_size, + } + .into_bind_group_layout_entry_builder() + } + + pub fn texture_1d(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder { + BindingType::Texture { + sample_type, + view_dimension: TextureViewDimension::D1, + multisampled: false, + } + .into_bind_group_layout_entry_builder() + } + + pub fn texture_2d(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder { + BindingType::Texture { + sample_type, + view_dimension: TextureViewDimension::D2, + multisampled: false, + } + .into_bind_group_layout_entry_builder() + } + + pub fn texture_2d_multisampled(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder { + BindingType::Texture { + sample_type, + view_dimension: TextureViewDimension::D2, + multisampled: true, + } + .into_bind_group_layout_entry_builder() + } + + pub fn texture_2d_array(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder { + BindingType::Texture { + sample_type, + view_dimension: TextureViewDimension::D2Array, + multisampled: false, + } + .into_bind_group_layout_entry_builder() + } + + pub fn texture_2d_array_multisampled( + sample_type: TextureSampleType, + ) -> BindGroupLayoutEntryBuilder { + BindingType::Texture { + sample_type, + view_dimension: TextureViewDimension::D2Array, + multisampled: true, + } + .into_bind_group_layout_entry_builder() + } + + pub fn texture_depth_2d() -> BindGroupLayoutEntryBuilder { + texture_2d(TextureSampleType::Depth).into_bind_group_layout_entry_builder() + } + + pub fn texture_depth_2d_multisampled() -> BindGroupLayoutEntryBuilder { + texture_2d_multisampled(TextureSampleType::Depth).into_bind_group_layout_entry_builder() + } + + pub fn texture_cube(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder { + BindingType::Texture { + sample_type, + view_dimension: TextureViewDimension::Cube, + multisampled: false, + } + .into_bind_group_layout_entry_builder() + } + + pub fn texture_cube_multisampled( + sample_type: TextureSampleType, + ) -> BindGroupLayoutEntryBuilder { + BindingType::Texture { + sample_type, + view_dimension: TextureViewDimension::Cube, + multisampled: true, + } + .into_bind_group_layout_entry_builder() + } + + pub fn texture_cube_array(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder { + BindingType::Texture { + sample_type, + view_dimension: TextureViewDimension::CubeArray, + multisampled: false, + } + .into_bind_group_layout_entry_builder() + } + + pub fn texture_cube_array_multisampled( + sample_type: TextureSampleType, + ) -> BindGroupLayoutEntryBuilder { + BindingType::Texture { + sample_type, + view_dimension: TextureViewDimension::CubeArray, + multisampled: true, + } + .into_bind_group_layout_entry_builder() + } + + pub fn texture_3d(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder { + BindingType::Texture { + sample_type, + view_dimension: TextureViewDimension::D3, + multisampled: false, + } + .into_bind_group_layout_entry_builder() + } + + pub fn texture_3d_multisampled(sample_type: TextureSampleType) -> BindGroupLayoutEntryBuilder { + BindingType::Texture { + sample_type, + view_dimension: TextureViewDimension::D3, + multisampled: true, + } + .into_bind_group_layout_entry_builder() + } + + pub fn sampler(sampler_binding_type: SamplerBindingType) -> BindGroupLayoutEntryBuilder { + BindingType::Sampler(sampler_binding_type).into_bind_group_layout_entry_builder() + } + + pub fn texture_storage_2d( + format: TextureFormat, + access: StorageTextureAccess, + ) -> BindGroupLayoutEntryBuilder { + BindingType::StorageTexture { + access, + format, + view_dimension: TextureViewDimension::D2, + } + .into_bind_group_layout_entry_builder() + } + + pub fn texture_storage_2d_array( + format: TextureFormat, + access: StorageTextureAccess, + ) -> BindGroupLayoutEntryBuilder { + BindingType::StorageTexture { + access, + format, + view_dimension: TextureViewDimension::D2Array, + } + .into_bind_group_layout_entry_builder() + } + + pub fn texture_storage_3d( + format: TextureFormat, + access: StorageTextureAccess, + ) -> BindGroupLayoutEntryBuilder { + BindingType::StorageTexture { + access, + format, + view_dimension: TextureViewDimension::D3, + } + .into_bind_group_layout_entry_builder() + } + + pub fn acceleration_structure() -> BindGroupLayoutEntryBuilder { + BindingType::AccelerationStructure { + vertex_return: false, + } + .into_bind_group_layout_entry_builder() + } + + pub fn acceleration_structure_vertex_return() -> BindGroupLayoutEntryBuilder { + BindingType::AccelerationStructure { + vertex_return: true, + } + .into_bind_group_layout_entry_builder() + } +} diff --git a/crates/libmarathon/src/render/render_resource/bindless.rs b/crates/libmarathon/src/render/render_resource/bindless.rs new file mode 100644 index 0000000..0d819dd --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/bindless.rs @@ -0,0 +1,374 @@ +//! Types and functions relating to bindless resources. + +use std::borrow::Cow; +use core::{ + num::{NonZeroU32, NonZeroU64}, + ops::Range, +}; + +use bevy_derive::{Deref, DerefMut}; +use wgpu::{ + BindGroupLayoutEntry, SamplerBindingType, ShaderStages, TextureSampleType, TextureViewDimension, +}; + +use crate::render::render_resource::binding_types::storage_buffer_read_only_sized; + +use super::binding_types::{ + sampler, texture_1d, texture_2d, texture_2d_array, texture_3d, texture_cube, texture_cube_array, +}; + +/// The default value for the number of resources that can be stored in a slab +/// on this platform. +/// +/// See the documentation for [`BindlessSlabResourceLimit`] for more +/// information. +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub const AUTO_BINDLESS_SLAB_RESOURCE_LIMIT: u32 = 64; +/// The default value for the number of resources that can be stored in a slab +/// on this platform. +/// +/// See the documentation for [`BindlessSlabResourceLimit`] for more +/// information. +#[cfg(not(any(target_os = "macos", target_os = "ios")))] +pub const AUTO_BINDLESS_SLAB_RESOURCE_LIMIT: u32 = 2048; + +/// The binding numbers for the built-in binding arrays of each bindless +/// resource type. +/// +/// In the case of materials, the material allocator manages these binding +/// arrays. +/// +/// `bindless.wgsl` contains declarations of these arrays for use in your +/// shaders. If you change these, make sure to update that file as well. +pub static BINDING_NUMBERS: [(BindlessResourceType, BindingNumber); 9] = [ + (BindlessResourceType::SamplerFiltering, BindingNumber(1)), + (BindlessResourceType::SamplerNonFiltering, BindingNumber(2)), + (BindlessResourceType::SamplerComparison, BindingNumber(3)), + (BindlessResourceType::Texture1d, BindingNumber(4)), + (BindlessResourceType::Texture2d, BindingNumber(5)), + (BindlessResourceType::Texture2dArray, BindingNumber(6)), + (BindlessResourceType::Texture3d, BindingNumber(7)), + (BindlessResourceType::TextureCube, BindingNumber(8)), + (BindlessResourceType::TextureCubeArray, BindingNumber(9)), +]; + +/// The maximum number of resources that can be stored in a slab. +/// +/// This limit primarily exists in order to work around `wgpu` performance +/// problems involving large numbers of bindless resources. Also, some +/// platforms, such as Metal, currently enforce limits on the number of +/// resources in use. +/// +/// This corresponds to `LIMIT` in the `#[bindless(LIMIT)]` attribute when +/// deriving [`crate::render_resource::AsBindGroup`]. +#[derive(Clone, Copy, Default, PartialEq, Debug)] +pub enum BindlessSlabResourceLimit { + /// Allows the renderer to choose a reasonable value for the resource limit + /// based on the platform. + /// + /// This value has been tuned, so you should default to this value unless + /// you have special platform-specific considerations that prevent you from + /// using it. + #[default] + Auto, + + /// A custom value for the resource limit. + /// + /// Bevy will allocate no more than this number of resources in a slab, + /// unless exceeding this value is necessary in order to allocate at all + /// (i.e. unless the number of bindless resources in your bind group exceeds + /// this value), in which case Bevy can exceed it. + Custom(u32), +} + +/// Information about the bindless resources in this object. +/// +/// The material bind group allocator uses this descriptor in order to create +/// and maintain bind groups. The fields within this bindless descriptor are +/// [`Cow`]s in order to support both the common case in which the fields are +/// simply `static` constants and the more unusual case in which the fields are +/// dynamically generated efficiently. An example of the latter case is +/// `ExtendedMaterial`, which needs to assemble a bindless descriptor from those +/// of the base material and the material extension at runtime. +/// +/// This structure will only be present if this object is bindless. +pub struct BindlessDescriptor { + /// The bindless resource types that this object uses, in order of bindless + /// index. + /// + /// The resource assigned to binding index 0 will be at index 0, the + /// resource assigned to binding index will be at index 1 in this array, and + /// so on. Unused binding indices are set to [`BindlessResourceType::None`]. + pub resources: Cow<'static, [BindlessResourceType]>, + /// The [`BindlessBufferDescriptor`] for each bindless buffer that this + /// object uses. + /// + /// The order of this array is irrelevant. + pub buffers: Cow<'static, [BindlessBufferDescriptor]>, + /// The [`BindlessIndexTableDescriptor`]s describing each bindless index + /// table. + /// + /// This list must be sorted by the first bindless index. + pub index_tables: Cow<'static, [BindlessIndexTableDescriptor]>, +} + +/// The type of potentially-bindless resource. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum BindlessResourceType { + /// No bindless resource. + /// + /// This is used as a placeholder to fill holes in the + /// [`BindlessDescriptor::resources`] list. + None, + /// A storage buffer. + Buffer, + /// A filtering sampler. + SamplerFiltering, + /// A non-filtering sampler (nearest neighbor). + SamplerNonFiltering, + /// A comparison sampler (typically used for shadow maps). + SamplerComparison, + /// A 1D texture. + Texture1d, + /// A 2D texture. + Texture2d, + /// A 2D texture array. + /// + /// Note that this differs from a binding array. 2D texture arrays must all + /// have the same size and format. + Texture2dArray, + /// A 3D texture. + Texture3d, + /// A cubemap texture. + TextureCube, + /// A cubemap texture array. + /// + /// Note that this differs from a binding array. Cubemap texture arrays must + /// all have the same size and format. + TextureCubeArray, + /// Multiple instances of plain old data concatenated into a single buffer. + /// + /// This corresponds to the `#[data]` declaration in + /// [`crate::render_resource::AsBindGroup`]. + /// + /// Note that this resource doesn't itself map to a GPU-level binding + /// resource and instead depends on the `MaterialBindGroupAllocator` to + /// create a binding resource for it. + DataBuffer, +} + +/// Describes a bindless buffer. +/// +/// Unlike samplers and textures, each buffer in a bind group gets its own +/// unique bind group entry. That is, there isn't any `bindless_buffers` binding +/// array to go along with `bindless_textures_2d`, +/// `bindless_samplers_filtering`, etc. Therefore, this descriptor contains two +/// indices: the *binding number* and the *bindless index*. The binding number +/// is the `@binding` number used in the shader, while the bindless index is the +/// index of the buffer in the bindless index table (which is itself +/// conventionally bound to binding number 0). +/// +/// When declaring the buffer in a derived implementation +/// [`crate::render_resource::AsBindGroup`] with syntax like +/// `#[uniform(BINDLESS_INDEX, StandardMaterialUniform, +/// bindless(BINDING_NUMBER)]`, the bindless index is `BINDLESS_INDEX`, and the +/// binding number is `BINDING_NUMBER`. Note the order. +#[derive(Clone, Copy, Debug)] +pub struct BindlessBufferDescriptor { + /// The actual binding number of the buffer. + /// + /// This is declared with `@binding` in WGSL. When deriving + /// [`crate::render_resource::AsBindGroup`], this is the `BINDING_NUMBER` in + /// `#[uniform(BINDLESS_INDEX, StandardMaterialUniform, + /// bindless(BINDING_NUMBER)]`. + pub binding_number: BindingNumber, + /// The index of the buffer in the bindless index table. + /// + /// In the shader, this is the index into the table bound to binding 0. When + /// deriving [`crate::render_resource::AsBindGroup`], this is the + /// `BINDLESS_INDEX` in `#[uniform(BINDLESS_INDEX, StandardMaterialUniform, + /// bindless(BINDING_NUMBER)]`. + pub bindless_index: BindlessIndex, + /// The size of the buffer in bytes, if known. + pub size: Option, +} + +/// Describes the layout of the bindless index table, which maps bindless +/// indices to indices within the binding arrays. +#[derive(Clone)] +pub struct BindlessIndexTableDescriptor { + /// The range of bindless indices that this descriptor covers. + pub indices: Range, + /// The binding at which the index table itself will be bound. + /// + /// By default, this is binding 0, but it can be changed with the + /// `#[bindless(index_table(binding(B)))]` attribute. + pub binding_number: BindingNumber, +} + +/// The index of the actual binding in the bind group. +/// +/// This is the value specified in WGSL as `@binding`. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Deref, DerefMut)] +pub struct BindingNumber(pub u32); + +/// The index in the bindless index table. +/// +/// This table is conventionally bound to binding number 0. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Hash, Debug, Deref, DerefMut)] +pub struct BindlessIndex(pub u32); + +/// Creates the bind group layout entries common to all shaders that use +/// bindless bind groups. +/// +/// `bindless_resource_count` specifies the total number of bindless resources. +/// `bindless_slab_resource_limit` specifies the resolved +/// [`BindlessSlabResourceLimit`] value. +pub fn create_bindless_bind_group_layout_entries( + bindless_index_table_length: u32, + bindless_slab_resource_limit: u32, + bindless_index_table_binding_number: BindingNumber, +) -> Vec { + let bindless_slab_resource_limit = + NonZeroU32::new(bindless_slab_resource_limit).expect("Bindless slot count must be nonzero"); + + // The maximum size of a binding array is the + // `bindless_slab_resource_limit`, which would occur if all of the bindless + // resources were of the same type. So we create our binding arrays with + // that size. + + vec![ + // Start with the bindless index table, bound to binding number 0. + storage_buffer_read_only_sized( + false, + NonZeroU64::new(bindless_index_table_length as u64 * size_of::() as u64), + ) + .build( + *bindless_index_table_binding_number, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), + // Continue with the common bindless resource arrays. + sampler(SamplerBindingType::Filtering) + .count(bindless_slab_resource_limit) + .build( + 1, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), + sampler(SamplerBindingType::NonFiltering) + .count(bindless_slab_resource_limit) + .build( + 2, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), + sampler(SamplerBindingType::Comparison) + .count(bindless_slab_resource_limit) + .build( + 3, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), + texture_1d(TextureSampleType::Float { filterable: true }) + .count(bindless_slab_resource_limit) + .build( + 4, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), + texture_2d(TextureSampleType::Float { filterable: true }) + .count(bindless_slab_resource_limit) + .build( + 5, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), + texture_2d_array(TextureSampleType::Float { filterable: true }) + .count(bindless_slab_resource_limit) + .build( + 6, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), + texture_3d(TextureSampleType::Float { filterable: true }) + .count(bindless_slab_resource_limit) + .build( + 7, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), + texture_cube(TextureSampleType::Float { filterable: true }) + .count(bindless_slab_resource_limit) + .build( + 8, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), + texture_cube_array(TextureSampleType::Float { filterable: true }) + .count(bindless_slab_resource_limit) + .build( + 9, + ShaderStages::FRAGMENT | ShaderStages::VERTEX | ShaderStages::COMPUTE, + ), + ] +} + +impl BindlessSlabResourceLimit { + /// Determines the actual bindless slab resource limit on this platform. + pub fn resolve(&self) -> u32 { + match *self { + BindlessSlabResourceLimit::Auto => AUTO_BINDLESS_SLAB_RESOURCE_LIMIT, + BindlessSlabResourceLimit::Custom(limit) => limit, + } + } +} + +impl BindlessResourceType { + /// Returns the binding number for the common array of this resource type. + /// + /// For example, if you pass `BindlessResourceType::Texture2d`, this will + /// return 5, in order to match the `@group(2) @binding(5) var + /// bindless_textures_2d: binding_array>` declaration in + /// `bindless.wgsl`. + /// + /// Not all resource types have fixed binding numbers. If you call + /// [`Self::binding_number`] on such a resource type, it returns `None`. + /// + /// Note that this returns a static reference to the binding number, not the + /// binding number itself. This is to conform to an idiosyncratic API in + /// `wgpu` whereby binding numbers for binding arrays are taken by `&u32` + /// *reference*, not by `u32` value. + pub fn binding_number(&self) -> Option<&'static BindingNumber> { + match BINDING_NUMBERS.binary_search_by_key(self, |(key, _)| *key) { + Ok(binding_number) => Some(&BINDING_NUMBERS[binding_number].1), + Err(_) => None, + } + } +} + +impl From for BindlessResourceType { + fn from(texture_view_dimension: TextureViewDimension) -> Self { + match texture_view_dimension { + TextureViewDimension::D1 => BindlessResourceType::Texture1d, + TextureViewDimension::D2 => BindlessResourceType::Texture2d, + TextureViewDimension::D2Array => BindlessResourceType::Texture2dArray, + TextureViewDimension::Cube => BindlessResourceType::TextureCube, + TextureViewDimension::CubeArray => BindlessResourceType::TextureCubeArray, + TextureViewDimension::D3 => BindlessResourceType::Texture3d, + } + } +} + +impl From for BindlessResourceType { + fn from(sampler_binding_type: SamplerBindingType) -> Self { + match sampler_binding_type { + SamplerBindingType::Filtering => BindlessResourceType::SamplerFiltering, + SamplerBindingType::NonFiltering => BindlessResourceType::SamplerNonFiltering, + SamplerBindingType::Comparison => BindlessResourceType::SamplerComparison, + } + } +} + +impl From for BindlessIndex { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for BindingNumber { + fn from(value: u32) -> Self { + Self(value) + } +} diff --git a/crates/libmarathon/src/render/render_resource/buffer.rs b/crates/libmarathon/src/render/render_resource/buffer.rs new file mode 100644 index 0000000..734bbc2 --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/buffer.rs @@ -0,0 +1,95 @@ +use crate::render::define_atomic_id; +use crate::render::renderer::WgpuWrapper; +use core::ops::{Bound, Deref, RangeBounds}; + +define_atomic_id!(BufferId); + +#[derive(Clone, Debug)] +pub struct Buffer { + id: BufferId, + value: WgpuWrapper, +} + +impl Buffer { + #[inline] + pub fn id(&self) -> BufferId { + self.id + } + + pub fn slice(&self, bounds: impl RangeBounds) -> BufferSlice<'_> { + // need to compute and store this manually because wgpu doesn't export offset and size on wgpu::BufferSlice + let offset = match bounds.start_bound() { + Bound::Included(&bound) => bound, + Bound::Excluded(&bound) => bound + 1, + Bound::Unbounded => 0, + }; + let size = match bounds.end_bound() { + Bound::Included(&bound) => bound + 1, + Bound::Excluded(&bound) => bound, + Bound::Unbounded => self.value.size(), + } - offset; + BufferSlice { + id: self.id, + offset, + size, + value: self.value.slice(bounds), + } + } + + #[inline] + pub fn unmap(&self) { + self.value.unmap(); + } +} + +impl From for Buffer { + fn from(value: wgpu::Buffer) -> Self { + Buffer { + id: BufferId::new(), + value: WgpuWrapper::new(value), + } + } +} + +impl Deref for Buffer { + type Target = wgpu::Buffer; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +#[derive(Clone, Debug)] +pub struct BufferSlice<'a> { + id: BufferId, + offset: wgpu::BufferAddress, + value: wgpu::BufferSlice<'a>, + size: wgpu::BufferAddress, +} + +impl<'a> BufferSlice<'a> { + #[inline] + pub fn id(&self) -> BufferId { + self.id + } + + #[inline] + pub fn offset(&self) -> wgpu::BufferAddress { + self.offset + } + + #[inline] + pub fn size(&self) -> wgpu::BufferAddress { + self.size + } +} + +impl<'a> Deref for BufferSlice<'a> { + type Target = wgpu::BufferSlice<'a>; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} diff --git a/crates/libmarathon/src/render/render_resource/buffer_vec.rs b/crates/libmarathon/src/render/render_resource/buffer_vec.rs new file mode 100644 index 0000000..99811c0 --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/buffer_vec.rs @@ -0,0 +1,587 @@ +use core::{iter, marker::PhantomData}; + +use crate::render::{ + render_resource::Buffer, + renderer::{RenderDevice, RenderQueue}, +}; +use bytemuck::{must_cast_slice, NoUninit}; +use encase::{ + internal::{WriteInto, Writer}, + ShaderType, +}; +use thiserror::Error; +use wgpu::{BindingResource, BufferAddress, BufferUsages}; + +use super::GpuArrayBufferable; + +/// A structure for storing raw bytes that have already been properly formatted +/// for use by the GPU. +/// +/// "Properly formatted" means that item data already meets the alignment and padding +/// requirements for how it will be used on the GPU. The item type must implement [`NoUninit`] +/// for its data representation to be directly copyable. +/// +/// Index, vertex, and instance-rate vertex buffers have no alignment nor padding requirements and +/// so this helper type is a good choice for them. +/// +/// The contained data is stored in system RAM. Calling [`reserve`](RawBufferVec::reserve) +/// allocates VRAM from the [`RenderDevice`]. +/// [`write_buffer`](RawBufferVec::write_buffer) queues copying of the data +/// from system RAM to VRAM. +/// +/// Other options for storing GPU-accessible data are: +/// * [`BufferVec`] +/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer) +/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer) +/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer) +/// * [`StorageBuffer`](crate::render_resource::StorageBuffer) +/// * [`Texture`](crate::render_resource::Texture) +/// * [`UniformBuffer`](crate::render_resource::UniformBuffer) +pub struct RawBufferVec { + values: Vec, + buffer: Option, + capacity: usize, + item_size: usize, + buffer_usage: BufferUsages, + label: Option, + changed: bool, +} + +impl RawBufferVec { + /// Creates a new [`RawBufferVec`] with the given [`BufferUsages`]. + pub const fn new(buffer_usage: BufferUsages) -> Self { + Self { + values: Vec::new(), + buffer: None, + capacity: 0, + item_size: size_of::(), + buffer_usage, + label: None, + changed: false, + } + } + + /// Returns a handle to the buffer, if the data has been uploaded. + #[inline] + pub fn buffer(&self) -> Option<&Buffer> { + self.buffer.as_ref() + } + + /// Returns the binding for the buffer if the data has been uploaded. + #[inline] + pub fn binding(&self) -> Option> { + Some(BindingResource::Buffer( + self.buffer()?.as_entire_buffer_binding(), + )) + } + + /// Returns the amount of space that the GPU will use before reallocating. + #[inline] + pub fn capacity(&self) -> usize { + self.capacity + } + + /// Returns the number of items that have been pushed to this buffer. + #[inline] + pub fn len(&self) -> usize { + self.values.len() + } + + /// Returns true if the buffer is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + /// Adds a new value and returns its index. + pub fn push(&mut self, value: T) -> usize { + let index = self.values.len(); + self.values.push(value); + index + } + + pub fn append(&mut self, other: &mut RawBufferVec) { + self.values.append(&mut other.values); + } + + /// Returns the value at the given index. + pub fn get(&self, index: u32) -> Option<&T> { + self.values.get(index as usize) + } + + /// Sets the value at the given index. + /// + /// The index must be less than [`RawBufferVec::len`]. + pub fn set(&mut self, index: u32, value: T) { + self.values[index as usize] = value; + } + + /// Preallocates space for `count` elements in the internal CPU-side buffer. + /// + /// Unlike [`RawBufferVec::reserve`], this doesn't have any effect on the GPU buffer. + pub fn reserve_internal(&mut self, count: usize) { + self.values.reserve(count); + } + + /// Changes the debugging label of the buffer. + /// + /// The next time the buffer is updated (via [`reserve`](Self::reserve)), Bevy will inform + /// the driver of the new label. + pub fn set_label(&mut self, label: Option<&str>) { + let label = label.map(str::to_string); + + if label != self.label { + self.changed = true; + } + + self.label = label; + } + + /// Returns the label + pub fn get_label(&self) -> Option<&str> { + self.label.as_deref() + } + + /// Creates a [`Buffer`] on the [`RenderDevice`] with size + /// at least `size_of::() * capacity`, unless a such a buffer already exists. + /// + /// If a [`Buffer`] exists, but is too small, references to it will be discarded, + /// and a new [`Buffer`] will be created. Any previously created [`Buffer`]s + /// that are no longer referenced will be deleted by the [`RenderDevice`] + /// once it is done using them (typically 1-2 frames). + /// + /// In addition to any [`BufferUsages`] provided when + /// the `RawBufferVec` was created, the buffer on the [`RenderDevice`] + /// is marked as [`BufferUsages::COPY_DST`](BufferUsages). + pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) { + let size = self.item_size * capacity; + if capacity > self.capacity || (self.changed && size > 0) { + self.capacity = capacity; + self.buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { + label: self.label.as_deref(), + size: size as BufferAddress, + usage: BufferUsages::COPY_DST | self.buffer_usage, + mapped_at_creation: false, + })); + self.changed = false; + } + } + + /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`] + /// and the provided [`RenderQueue`]. + /// + /// Before queuing the write, a [`reserve`](RawBufferVec::reserve) operation + /// is executed. + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + if self.values.is_empty() { + return; + } + self.reserve(self.values.len(), device); + if let Some(buffer) = &self.buffer { + let range = 0..self.item_size * self.values.len(); + let bytes: &[u8] = must_cast_slice(&self.values); + queue.write_buffer(buffer, 0, &bytes[range]); + } + } + + /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`] + /// and the provided [`RenderQueue`]. + /// + /// If the buffer is not initialized on the GPU or the range is bigger than the capacity it will + /// return an error. You'll need to either reserve a new buffer which will lose data on the GPU + /// or create a new buffer and copy the old data to it. + /// + /// This will only write the data contained in the given range. It is useful if you only want + /// to update a part of the buffer. + pub fn write_buffer_range( + &mut self, + render_queue: &RenderQueue, + range: core::ops::Range, + ) -> Result<(), WriteBufferRangeError> { + if self.values.is_empty() { + return Err(WriteBufferRangeError::NoValuesToUpload); + } + if range.end > self.item_size * self.capacity { + return Err(WriteBufferRangeError::RangeBiggerThanBuffer); + } + if let Some(buffer) = &self.buffer { + // Cast only the bytes we need to write + let bytes: &[u8] = must_cast_slice(&self.values[range.start..range.end]); + render_queue.write_buffer(buffer, (range.start * self.item_size) as u64, bytes); + Ok(()) + } else { + Err(WriteBufferRangeError::BufferNotInitialized) + } + } + + /// Reduces the length of the buffer. + pub fn truncate(&mut self, len: usize) { + self.values.truncate(len); + } + + /// Removes all elements from the buffer. + pub fn clear(&mut self) { + self.values.clear(); + } + + /// Removes and returns the last element in the buffer. + pub fn pop(&mut self) -> Option { + self.values.pop() + } + + pub fn values(&self) -> &Vec { + &self.values + } + + pub fn values_mut(&mut self) -> &mut Vec { + &mut self.values + } +} + +impl RawBufferVec +where + T: NoUninit + Default, +{ + pub fn grow_set(&mut self, index: u32, value: T) { + while index as usize + 1 > self.len() { + self.values.push(T::default()); + } + self.values[index as usize] = value; + } +} + +impl Extend for RawBufferVec { + #[inline] + fn extend>(&mut self, iter: I) { + self.values.extend(iter); + } +} + +/// Like [`RawBufferVec`], but doesn't require that the data type `T` be +/// [`NoUninit`]. +/// +/// This is a high-performance data structure that you should use whenever +/// possible if your data is more complex than is suitable for [`RawBufferVec`]. +/// The [`ShaderType`] trait from the `encase` library is used to ensure that +/// the data is correctly aligned for use by the GPU. +/// +/// For performance reasons, unlike [`RawBufferVec`], this type doesn't allow +/// CPU access to the data after it's been added via [`BufferVec::push`]. If you +/// need CPU access to the data, consider another type, such as +/// [`StorageBuffer`][super::StorageBuffer]. +/// +/// Other options for storing GPU-accessible data are: +/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer) +/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer) +/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer) +/// * [`RawBufferVec`] +/// * [`StorageBuffer`](crate::render_resource::StorageBuffer) +/// * [`Texture`](crate::render_resource::Texture) +/// * [`UniformBuffer`](crate::render_resource::UniformBuffer) +pub struct BufferVec +where + T: ShaderType + WriteInto, +{ + data: Vec, + buffer: Option, + capacity: usize, + buffer_usage: BufferUsages, + label: Option, + label_changed: bool, + phantom: PhantomData, +} + +impl BufferVec +where + T: ShaderType + WriteInto, +{ + /// Creates a new [`BufferVec`] with the given [`BufferUsages`]. + pub const fn new(buffer_usage: BufferUsages) -> Self { + Self { + data: vec![], + buffer: None, + capacity: 0, + buffer_usage, + label: None, + label_changed: false, + phantom: PhantomData, + } + } + + /// Returns a handle to the buffer, if the data has been uploaded. + #[inline] + pub fn buffer(&self) -> Option<&Buffer> { + self.buffer.as_ref() + } + + /// Returns the binding for the buffer if the data has been uploaded. + #[inline] + pub fn binding(&self) -> Option> { + Some(BindingResource::Buffer( + self.buffer()?.as_entire_buffer_binding(), + )) + } + + /// Returns the amount of space that the GPU will use before reallocating. + #[inline] + pub fn capacity(&self) -> usize { + self.capacity + } + + /// Returns the number of items that have been pushed to this buffer. + #[inline] + pub fn len(&self) -> usize { + self.data.len() / u64::from(T::min_size()) as usize + } + + /// Returns true if the buffer is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + /// Adds a new value and returns its index. + pub fn push(&mut self, value: T) -> usize { + let element_size = u64::from(T::min_size()) as usize; + let offset = self.data.len(); + + // TODO: Consider using unsafe code to push uninitialized, to prevent + // the zeroing. It shows up in profiles. + self.data.extend(iter::repeat_n(0, element_size)); + + // Take a slice of the new data for `write_into` to use. This is + // important: it hoists the bounds check up here so that the compiler + // can eliminate all the bounds checks that `write_into` will emit. + let mut dest = &mut self.data[offset..(offset + element_size)]; + value.write_into(&mut Writer::new(&value, &mut dest, 0).unwrap()); + + offset / u64::from(T::min_size()) as usize + } + + /// Changes the debugging label of the buffer. + /// + /// The next time the buffer is updated (via [`Self::reserve`]), Bevy will inform + /// the driver of the new label. + pub fn set_label(&mut self, label: Option<&str>) { + let label = label.map(str::to_string); + + if label != self.label { + self.label_changed = true; + } + + self.label = label; + } + + /// Returns the label + pub fn get_label(&self) -> Option<&str> { + self.label.as_deref() + } + + /// Creates a [`Buffer`] on the [`RenderDevice`] with size + /// at least `size_of::() * capacity`, unless such a buffer already exists. + /// + /// If a [`Buffer`] exists, but is too small, references to it will be discarded, + /// and a new [`Buffer`] will be created. Any previously created [`Buffer`]s + /// that are no longer referenced will be deleted by the [`RenderDevice`] + /// once it is done using them (typically 1-2 frames). + /// + /// In addition to any [`BufferUsages`] provided when + /// the `BufferVec` was created, the buffer on the [`RenderDevice`] + /// is marked as [`BufferUsages::COPY_DST`](BufferUsages). + pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) { + if capacity <= self.capacity && !self.label_changed { + return; + } + + self.capacity = capacity; + let size = u64::from(T::min_size()) as usize * capacity; + self.buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { + label: self.label.as_deref(), + size: size as BufferAddress, + usage: BufferUsages::COPY_DST | self.buffer_usage, + mapped_at_creation: false, + })); + self.label_changed = false; + } + + /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`] + /// and the provided [`RenderQueue`]. + /// + /// Before queuing the write, a [`reserve`](BufferVec::reserve) operation is + /// executed. + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + if self.data.is_empty() { + return; + } + + self.reserve(self.data.len() / u64::from(T::min_size()) as usize, device); + + let Some(buffer) = &self.buffer else { return }; + queue.write_buffer(buffer, 0, &self.data); + } + + /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`] + /// and the provided [`RenderQueue`]. + /// + /// If the buffer is not initialized on the GPU or the range is bigger than the capacity it will + /// return an error. You'll need to either reserve a new buffer which will lose data on the GPU + /// or create a new buffer and copy the old data to it. + /// + /// This will only write the data contained in the given range. It is useful if you only want + /// to update a part of the buffer. + pub fn write_buffer_range( + &mut self, + render_queue: &RenderQueue, + range: core::ops::Range, + ) -> Result<(), WriteBufferRangeError> { + if self.data.is_empty() { + return Err(WriteBufferRangeError::NoValuesToUpload); + } + let item_size = u64::from(T::min_size()) as usize; + if range.end > item_size * self.capacity { + return Err(WriteBufferRangeError::RangeBiggerThanBuffer); + } + if let Some(buffer) = &self.buffer { + let bytes = &self.data[range.start..range.end]; + render_queue.write_buffer(buffer, (range.start * item_size) as u64, bytes); + Ok(()) + } else { + Err(WriteBufferRangeError::BufferNotInitialized) + } + } + + /// Reduces the length of the buffer. + pub fn truncate(&mut self, len: usize) { + self.data.truncate(u64::from(T::min_size()) as usize * len); + } + + /// Removes all elements from the buffer. + pub fn clear(&mut self) { + self.data.clear(); + } +} + +/// Like a [`BufferVec`], but only reserves space on the GPU for elements +/// instead of initializing them CPU-side. +/// +/// This type is useful when you're accumulating "output slots" for a GPU +/// compute shader to write into. +/// +/// The type `T` need not be [`NoUninit`], unlike [`RawBufferVec`]; it only has to +/// be [`GpuArrayBufferable`]. +pub struct UninitBufferVec +where + T: GpuArrayBufferable, +{ + buffer: Option, + len: usize, + capacity: usize, + item_size: usize, + buffer_usage: BufferUsages, + label: Option, + label_changed: bool, + phantom: PhantomData, +} + +impl UninitBufferVec +where + T: GpuArrayBufferable, +{ + /// Creates a new [`UninitBufferVec`] with the given [`BufferUsages`]. + pub const fn new(buffer_usage: BufferUsages) -> Self { + Self { + len: 0, + buffer: None, + capacity: 0, + item_size: size_of::(), + buffer_usage, + label: None, + label_changed: false, + phantom: PhantomData, + } + } + + /// Returns the buffer, if allocated. + #[inline] + pub fn buffer(&self) -> Option<&Buffer> { + self.buffer.as_ref() + } + + /// Returns the binding for the buffer if the data has been uploaded. + #[inline] + pub fn binding(&self) -> Option> { + Some(BindingResource::Buffer( + self.buffer()?.as_entire_buffer_binding(), + )) + } + + /// Reserves space for one more element in the buffer and returns its index. + pub fn add(&mut self) -> usize { + self.add_multiple(1) + } + + /// Reserves space for the given number of elements in the buffer and + /// returns the index of the first one. + pub fn add_multiple(&mut self, count: usize) -> usize { + let index = self.len; + self.len += count; + index + } + + /// Returns true if no elements have been added to this [`UninitBufferVec`]. + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// Removes all elements from the buffer. + pub fn clear(&mut self) { + self.len = 0; + } + + /// Returns the length of the buffer. + pub fn len(&self) -> usize { + self.len + } + + /// Materializes the buffer on the GPU with space for `capacity` elements. + /// + /// If the buffer is already big enough, this function doesn't reallocate + /// the buffer. + pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) { + if capacity <= self.capacity && !self.label_changed { + return; + } + + self.capacity = capacity; + let size = self.item_size * capacity; + self.buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { + label: self.label.as_deref(), + size: size as BufferAddress, + usage: BufferUsages::COPY_DST | self.buffer_usage, + mapped_at_creation: false, + })); + + self.label_changed = false; + } + + /// Materializes the buffer on the GPU, with an appropriate size for the + /// elements that have been pushed so far. + pub fn write_buffer(&mut self, device: &RenderDevice) { + if !self.is_empty() { + self.reserve(self.len, device); + } + } +} + +/// Error returned when `write_buffer_range` fails +/// +/// See [`RawBufferVec::write_buffer_range`] [`BufferVec::write_buffer_range`] +#[derive(Debug, Eq, PartialEq, Copy, Clone, Error)] +pub enum WriteBufferRangeError { + #[error("the range is bigger than the capacity of the buffer")] + RangeBiggerThanBuffer, + #[error("the gpu buffer is not initialized")] + BufferNotInitialized, + #[error("there are no values to upload")] + NoValuesToUpload, +} diff --git a/crates/libmarathon/src/render/render_resource/gpu_array_buffer.rs b/crates/libmarathon/src/render/render_resource/gpu_array_buffer.rs new file mode 100644 index 0000000..59fb3c6 --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/gpu_array_buffer.rs @@ -0,0 +1,118 @@ +use super::{ + binding_types::{storage_buffer_read_only, uniform_buffer_sized}, + BindGroupLayoutEntryBuilder, BufferVec, +}; +use crate::render::{ + render_resource::batched_uniform_buffer::BatchedUniformBuffer, + renderer::{RenderDevice, RenderQueue}, +}; +use bevy_ecs::{prelude::Component, resource::Resource}; +use core::marker::PhantomData; +use encase::{private::WriteInto, ShaderSize, ShaderType}; +use nonmax::NonMaxU32; +use wgpu::{BindingResource, BufferUsages}; + +/// Trait for types able to go in a [`GpuArrayBuffer`]. +pub trait GpuArrayBufferable: ShaderType + ShaderSize + WriteInto + Clone {} + +impl GpuArrayBufferable for T {} + +/// Stores an array of elements to be transferred to the GPU and made accessible to shaders as a read-only array. +/// +/// On platforms that support storage buffers, this is equivalent to +/// [`BufferVec`]. Otherwise, this falls back to a dynamic offset +/// uniform buffer with the largest array of T that fits within a uniform buffer +/// binding (within reasonable limits). +/// +/// Other options for storing GPU-accessible data are: +/// * [`BufferVec`] +/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer) +/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer) +/// * [`RawBufferVec`](crate::render_resource::RawBufferVec) +/// * [`StorageBuffer`](crate::render_resource::StorageBuffer) +/// * [`Texture`](crate::render_resource::Texture) +/// * [`UniformBuffer`](crate::render_resource::UniformBuffer) +#[derive(Resource)] +pub enum GpuArrayBuffer { + Uniform(BatchedUniformBuffer), + Storage(BufferVec), +} + +impl GpuArrayBuffer { + pub fn new(device: &RenderDevice) -> Self { + let limits = device.limits(); + if limits.max_storage_buffers_per_shader_stage == 0 { + GpuArrayBuffer::Uniform(BatchedUniformBuffer::new(&limits)) + } else { + GpuArrayBuffer::Storage(BufferVec::new(BufferUsages::STORAGE)) + } + } + + pub fn clear(&mut self) { + match self { + GpuArrayBuffer::Uniform(buffer) => buffer.clear(), + GpuArrayBuffer::Storage(buffer) => buffer.clear(), + } + } + + pub fn push(&mut self, value: T) -> GpuArrayBufferIndex { + match self { + GpuArrayBuffer::Uniform(buffer) => buffer.push(value), + GpuArrayBuffer::Storage(buffer) => { + let index = buffer.push(value) as u32; + GpuArrayBufferIndex { + index, + dynamic_offset: None, + element_type: PhantomData, + } + } + } + } + + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + match self { + GpuArrayBuffer::Uniform(buffer) => buffer.write_buffer(device, queue), + GpuArrayBuffer::Storage(buffer) => buffer.write_buffer(device, queue), + } + } + + pub fn binding_layout(device: &RenderDevice) -> BindGroupLayoutEntryBuilder { + if device.limits().max_storage_buffers_per_shader_stage == 0 { + uniform_buffer_sized( + true, + // BatchedUniformBuffer uses a MaxCapacityArray that is runtime-sized, so we use + // None here and let wgpu figure out the size. + None, + ) + } else { + storage_buffer_read_only::(false) + } + } + + pub fn binding(&self) -> Option> { + match self { + GpuArrayBuffer::Uniform(buffer) => buffer.binding(), + GpuArrayBuffer::Storage(buffer) => buffer.binding(), + } + } + + pub fn batch_size(device: &RenderDevice) -> Option { + let limits = device.limits(); + if limits.max_storage_buffers_per_shader_stage == 0 { + Some(BatchedUniformBuffer::::batch_size(&limits) as u32) + } else { + None + } + } +} + +/// An index into a [`GpuArrayBuffer`] for a given element. +#[derive(Component, Clone)] +pub struct GpuArrayBufferIndex { + /// The index to use in a shader into the array. + pub index: u32, + /// The dynamic offset to use when setting the bind group in a pass. + /// Only used on platforms that don't support storage buffers. + pub dynamic_offset: Option, + pub element_type: PhantomData, +} diff --git a/crates/libmarathon/src/render/render_resource/mod.rs b/crates/libmarathon/src/render/render_resource/mod.rs new file mode 100644 index 0000000..0a41dfd --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/mod.rs @@ -0,0 +1,75 @@ +mod batched_uniform_buffer; +mod bind_group; +mod bind_group_entries; +mod bind_group_layout; +mod bind_group_layout_entries; +mod bindless; +mod buffer; +mod buffer_vec; +mod gpu_array_buffer; +mod pipeline; +mod pipeline_cache; +mod pipeline_specializer; +pub mod resource_macros; +mod specializer; +mod storage_buffer; +mod texture; +mod uniform_buffer; + +pub use bind_group::*; +pub use bind_group_entries::*; +pub use bind_group_layout::*; +pub use bind_group_layout_entries::*; +pub use bindless::*; +pub use buffer::*; +pub use buffer_vec::*; +pub use gpu_array_buffer::*; +pub use pipeline::*; +pub use pipeline_cache::*; +pub use pipeline_specializer::*; +pub use specializer::*; +pub use storage_buffer::*; +pub use texture::*; +pub use uniform_buffer::*; + +// TODO: decide where re-exports should go +pub use wgpu::{ + util::{ + BufferInitDescriptor, DispatchIndirectArgs, DrawIndexedIndirectArgs, DrawIndirectArgs, + TextureDataOrder, + }, + AccelerationStructureFlags, AccelerationStructureGeometryFlags, + AccelerationStructureUpdateMode, AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, + AstcChannel, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingResource, BindingType, Blas, BlasBuildEntry, BlasGeometries, + BlasGeometrySizeDescriptors, BlasTriangleGeometry, BlasTriangleGeometrySizeDescriptor, + BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferAsyncError, + BufferBinding, BufferBindingType, BufferDescriptor, BufferSize, BufferUsages, ColorTargetState, + ColorWrites, CommandEncoder, CommandEncoderDescriptor, CompareFunction, ComputePass, + ComputePassDescriptor, ComputePipelineDescriptor as RawComputePipelineDescriptor, + CreateBlasDescriptor, CreateTlasDescriptor, DepthBiasState, DepthStencilState, DownlevelFlags, + Extent3d, Face, Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState, + FrontFace, ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, MapMode, + MultisampleState, Operations, Origin3d, PipelineCompilationOptions, PipelineLayout, + PipelineLayoutDescriptor, PollType, PolygonMode, PrimitiveState, PrimitiveTopology, + PushConstantRange, RenderPassColorAttachment, RenderPassDepthStencilAttachment, + RenderPassDescriptor, RenderPipelineDescriptor as RawRenderPipelineDescriptor, + Sampler as WgpuSampler, SamplerBindingType, SamplerBindingType as WgpuSamplerBindingType, + SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, + StencilFaceState, StencilOperation, StencilState, StorageTextureAccess, StoreOp, + TexelCopyBufferInfo, TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, + TextureDescriptor, TextureDimension, TextureFormat, TextureFormatFeatureFlags, + TextureFormatFeatures, TextureSampleType, TextureUsages, TextureView as WgpuTextureView, + TextureViewDescriptor, TextureViewDimension, Tlas, TlasInstance, VertexAttribute, + VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState, + VertexStepMode, COPY_BUFFER_ALIGNMENT, +}; + +pub mod encase { + pub use bevy_encase_derive::ShaderType; + pub use encase::*; +} + +pub use self::encase::{ShaderSize, ShaderType}; + +pub use naga::ShaderStage; diff --git a/crates/libmarathon/src/render/render_resource/pipeline.rs b/crates/libmarathon/src/render/render_resource/pipeline.rs new file mode 100644 index 0000000..6ff2226 --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/pipeline.rs @@ -0,0 +1,183 @@ +use super::empty_bind_group_layout; +use crate::render::renderer::WgpuWrapper; +use crate::render::{define_atomic_id, render_resource::BindGroupLayout}; +use std::borrow::Cow; +use bevy_asset::Handle; +use bevy_mesh::VertexBufferLayout; +use bevy_shader::{Shader, ShaderDefVal}; +use core::iter; +use core::ops::Deref; +use thiserror::Error; +use wgpu::{ + ColorTargetState, DepthStencilState, MultisampleState, PrimitiveState, PushConstantRange, +}; + +define_atomic_id!(RenderPipelineId); + +/// A [`RenderPipeline`] represents a graphics pipeline and its stages (shaders), bindings and vertex buffers. +/// +/// May be converted from and dereferences to a wgpu [`RenderPipeline`](wgpu::RenderPipeline). +/// Can be created via [`RenderDevice::create_render_pipeline`](crate::renderer::RenderDevice::create_render_pipeline). +#[derive(Clone, Debug)] +pub struct RenderPipeline { + id: RenderPipelineId, + value: WgpuWrapper, +} + +impl RenderPipeline { + #[inline] + pub fn id(&self) -> RenderPipelineId { + self.id + } +} + +impl From for RenderPipeline { + fn from(value: wgpu::RenderPipeline) -> Self { + RenderPipeline { + id: RenderPipelineId::new(), + value: WgpuWrapper::new(value), + } + } +} + +impl Deref for RenderPipeline { + type Target = wgpu::RenderPipeline; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +define_atomic_id!(ComputePipelineId); + +/// A [`ComputePipeline`] represents a compute pipeline and its single shader stage. +/// +/// May be converted from and dereferences to a wgpu [`ComputePipeline`](wgpu::ComputePipeline). +/// Can be created via [`RenderDevice::create_compute_pipeline`](crate::renderer::RenderDevice::create_compute_pipeline). +#[derive(Clone, Debug)] +pub struct ComputePipeline { + id: ComputePipelineId, + value: WgpuWrapper, +} + +impl ComputePipeline { + /// Returns the [`ComputePipelineId`]. + #[inline] + pub fn id(&self) -> ComputePipelineId { + self.id + } +} + +impl From for ComputePipeline { + fn from(value: wgpu::ComputePipeline) -> Self { + ComputePipeline { + id: ComputePipelineId::new(), + value: WgpuWrapper::new(value), + } + } +} + +impl Deref for ComputePipeline { + type Target = wgpu::ComputePipeline; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +/// Describes a render (graphics) pipeline. +#[derive(Clone, Debug, PartialEq, Default)] +pub struct RenderPipelineDescriptor { + /// Debug label of the pipeline. This will show up in graphics debuggers for easy identification. + pub label: Option>, + /// The layout of bind groups for this pipeline. + pub layout: Vec, + /// The push constant ranges for this pipeline. + /// Supply an empty vector if the pipeline doesn't use push constants. + pub push_constant_ranges: Vec, + /// The compiled vertex stage, its entry point, and the input buffers layout. + pub vertex: VertexState, + /// The properties of the pipeline at the primitive assembly and rasterization level. + pub primitive: PrimitiveState, + /// The effect of draw calls on the depth and stencil aspects of the output target, if any. + pub depth_stencil: Option, + /// The multi-sampling properties of the pipeline. + pub multisample: MultisampleState, + /// The compiled fragment stage, its entry point, and the color targets. + pub fragment: Option, + /// Whether to zero-initialize workgroup memory by default. If you're not sure, set this to true. + /// If this is false, reading from workgroup variables before writing to them will result in garbage values. + pub zero_initialize_workgroup_memory: bool, +} + +#[derive(Copy, Clone, Debug, Error)] +#[error("RenderPipelineDescriptor has no FragmentState configured")] +pub struct NoFragmentStateError; + +impl RenderPipelineDescriptor { + pub fn fragment_mut(&mut self) -> Result<&mut FragmentState, NoFragmentStateError> { + self.fragment.as_mut().ok_or(NoFragmentStateError) + } + + pub fn set_layout(&mut self, index: usize, layout: BindGroupLayout) { + filling_set_at(&mut self.layout, index, empty_bind_group_layout(), layout); + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Default)] +pub struct VertexState { + /// The compiled shader module for this stage. + pub shader: Handle, + pub shader_defs: Vec, + /// The name of the entry point in the compiled shader, or `None` if the default entry point + /// is used. + pub entry_point: Option>, + /// The format of any vertex buffers used with this pipeline. + pub buffers: Vec, +} + +/// Describes the fragment process in a render pipeline. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct FragmentState { + /// The compiled shader module for this stage. + pub shader: Handle, + pub shader_defs: Vec, + /// The name of the entry point in the compiled shader, or `None` if the default entry point + /// is used. + pub entry_point: Option>, + /// The color state of the render targets. + pub targets: Vec>, +} + +impl FragmentState { + pub fn set_target(&mut self, index: usize, target: ColorTargetState) { + filling_set_at(&mut self.targets, index, None, Some(target)); + } +} + +/// Describes a compute pipeline. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct ComputePipelineDescriptor { + pub label: Option>, + pub layout: Vec, + pub push_constant_ranges: Vec, + /// The compiled shader module for this stage. + pub shader: Handle, + pub shader_defs: Vec, + /// The name of the entry point in the compiled shader, or `None` if the default entry point + /// is used. + pub entry_point: Option>, + /// Whether to zero-initialize workgroup memory by default. If you're not sure, set this to true. + /// If this is false, reading from workgroup variables before writing to them will result in garbage values. + pub zero_initialize_workgroup_memory: bool, +} + +// utility function to set a value at the specified index, extending with +// a filler value if the index is out of bounds. +fn filling_set_at(vec: &mut Vec, index: usize, filler: T, value: T) { + let num_to_fill = (index + 1).saturating_sub(vec.len()); + vec.extend(iter::repeat_n(filler, num_to_fill)); + vec[index] = value; +} diff --git a/crates/libmarathon/src/render/render_resource/pipeline_cache.rs b/crates/libmarathon/src/render/render_resource/pipeline_cache.rs new file mode 100644 index 0000000..33399b6 --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/pipeline_cache.rs @@ -0,0 +1,831 @@ +use crate::render::{ + render_resource::*, + renderer::{RenderAdapter, RenderDevice, WgpuWrapper}, + Extract, +}; +use std::{borrow::Cow, sync::Arc}; +use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; +use bevy_ecs::{ + message::MessageReader, + resource::Resource, + system::{Res, ResMut}, +}; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_shader::{ + CachedPipelineId, PipelineCacheError, Shader, ShaderCache, ShaderCacheSource, ShaderDefVal, + ValidateShader, +}; +use bevy_tasks::Task; +use bevy_utils::default; +use core::{future::Future, hash::Hash, mem}; +use std::sync::{Mutex, PoisonError}; +use tracing::error; +use wgpu::{PipelineCompilationOptions, VertexBufferLayout as RawVertexBufferLayout}; + +/// A descriptor for a [`Pipeline`]. +/// +/// Used to store a heterogenous collection of render and compute pipeline descriptors together. +#[derive(Debug)] +pub enum PipelineDescriptor { + RenderPipelineDescriptor(Box), + ComputePipelineDescriptor(Box), +} + +/// A pipeline defining the data layout and shader logic for a specific GPU task. +/// +/// Used to store a heterogenous collection of render and compute pipelines together. +#[derive(Debug)] +pub enum Pipeline { + RenderPipeline(RenderPipeline), + ComputePipeline(ComputePipeline), +} + +/// Index of a cached render pipeline in a [`PipelineCache`]. +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] +pub struct CachedRenderPipelineId(CachedPipelineId); + +impl CachedRenderPipelineId { + /// An invalid cached render pipeline index, often used to initialize a variable. + pub const INVALID: Self = CachedRenderPipelineId(usize::MAX); + + #[inline] + pub fn id(&self) -> usize { + self.0 + } +} + +/// Index of a cached compute pipeline in a [`PipelineCache`]. +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub struct CachedComputePipelineId(CachedPipelineId); + +impl CachedComputePipelineId { + /// An invalid cached compute pipeline index, often used to initialize a variable. + pub const INVALID: Self = CachedComputePipelineId(usize::MAX); + + #[inline] + pub fn id(&self) -> usize { + self.0 + } +} + +pub struct CachedPipeline { + pub descriptor: PipelineDescriptor, + pub state: CachedPipelineState, +} + +/// State of a cached pipeline inserted into a [`PipelineCache`]. +#[cfg_attr( + not(target_arch = "wasm32"), + expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" + ) +)] +#[derive(Debug)] +pub enum CachedPipelineState { + /// The pipeline GPU object is queued for creation. + Queued, + /// The pipeline GPU object is being created. + Creating(Task>), + /// The pipeline GPU object was created successfully and is available (allocated on the GPU). + Ok(Pipeline), + /// An error occurred while trying to create the pipeline GPU object. + Err(PipelineCacheError), +} + +impl CachedPipelineState { + /// Convenience method to "unwrap" a pipeline state into its underlying GPU object. + /// + /// # Returns + /// + /// The method returns the allocated pipeline GPU object. + /// + /// # Panics + /// + /// This method panics if the pipeline GPU object is not available, either because it is + /// pending creation or because an error occurred while attempting to create GPU object. + pub fn unwrap(&self) -> &Pipeline { + match self { + CachedPipelineState::Ok(pipeline) => pipeline, + CachedPipelineState::Queued => { + panic!("Pipeline has not been compiled yet. It is still in the 'Queued' state.") + } + CachedPipelineState::Creating(..) => { + panic!("Pipeline has not been compiled yet. It is still in the 'Creating' state.") + } + CachedPipelineState::Err(err) => panic!("{}", err), + } + } +} + +type LayoutCacheKey = (Vec, Vec); +#[derive(Default)] +struct LayoutCache { + layouts: HashMap>>, +} + +impl LayoutCache { + fn get( + &mut self, + render_device: &RenderDevice, + bind_group_layouts: &[BindGroupLayout], + push_constant_ranges: Vec, + ) -> Arc> { + let bind_group_ids = bind_group_layouts.iter().map(BindGroupLayout::id).collect(); + self.layouts + .entry((bind_group_ids, push_constant_ranges)) + .or_insert_with_key(|(_, push_constant_ranges)| { + let bind_group_layouts = bind_group_layouts + .iter() + .map(BindGroupLayout::value) + .collect::>(); + Arc::new(WgpuWrapper::new(render_device.create_pipeline_layout( + &PipelineLayoutDescriptor { + bind_group_layouts: &bind_group_layouts, + push_constant_ranges, + ..default() + }, + ))) + }) + .clone() + } +} + +#[expect( + clippy::result_large_err, + reason = "See https://github.com/bevyengine/bevy/issues/19220" +)] +fn load_module( + render_device: &RenderDevice, + shader_source: ShaderCacheSource, + validate_shader: &ValidateShader, +) -> Result, PipelineCacheError> { + let shader_source = match shader_source { + #[cfg(feature = "shader_format_spirv")] + ShaderCacheSource::SpirV(data) => wgpu::util::make_spirv(data), + #[cfg(not(feature = "shader_format_spirv"))] + ShaderCacheSource::SpirV(_) => { + unimplemented!("Enable feature \"shader_format_spirv\" to use SPIR-V shaders") + } + ShaderCacheSource::Wgsl(src) => ShaderSource::Wgsl(Cow::Owned(src)), + #[cfg(not(feature = "decoupled_naga"))] + ShaderCacheSource::Naga(src) => ShaderSource::Naga(Cow::Owned(src)), + }; + let module_descriptor = ShaderModuleDescriptor { + label: None, + source: shader_source, + }; + + render_device + .wgpu_device() + .push_error_scope(wgpu::ErrorFilter::Validation); + + let shader_module = WgpuWrapper::new(match validate_shader { + ValidateShader::Enabled => { + render_device.create_and_validate_shader_module(module_descriptor) + } + // SAFETY: we are interfacing with shader code, which may contain undefined behavior, + // such as indexing out of bounds. + // The checks required are prohibitively expensive and a poor default for game engines. + ValidateShader::Disabled => unsafe { + render_device.create_shader_module(module_descriptor) + }, + }); + + let error = render_device.wgpu_device().pop_error_scope(); + + // `now_or_never` will return Some if the future is ready and None otherwise. + // On native platforms, wgpu will yield the error immediately while on wasm it may take longer since the browser APIs are asynchronous. + // So to keep the complexity of the ShaderCache low, we will only catch this error early on native platforms, + // and on wasm the error will be handled by wgpu and crash the application. + if let Some(Some(wgpu::Error::Validation { description, .. })) = + bevy_tasks::futures::now_or_never(error) + { + return Err(PipelineCacheError::CreateShaderModule(description)); + } + + Ok(shader_module) +} + +/// Cache for render and compute pipelines. +/// +/// The cache stores existing render and compute pipelines allocated on the GPU, as well as +/// pending creation. Pipelines inserted into the cache are identified by a unique ID, which +/// can be used to retrieve the actual GPU object once it's ready. The creation of the GPU +/// pipeline object is deferred to the [`RenderSystems::Render`] step, just before the render +/// graph starts being processed, as this requires access to the GPU. +/// +/// Note that the cache does not perform automatic deduplication of identical pipelines. It is +/// up to the user not to insert the same pipeline twice to avoid wasting GPU resources. +/// +/// [`RenderSystems::Render`]: crate::RenderSystems::Render +#[derive(Resource)] +pub struct PipelineCache { + layout_cache: Arc>, + shader_cache: Arc, RenderDevice>>>, + device: RenderDevice, + pipelines: Vec, + waiting_pipelines: HashSet, + new_pipelines: Mutex>, + global_shader_defs: Vec, + /// If `true`, disables asynchronous pipeline compilation. + /// This has no effect on macOS, wasm, or without the `multi_threaded` feature. + synchronous_pipeline_compilation: bool, +} + +impl PipelineCache { + /// Returns an iterator over the pipelines in the pipeline cache. + pub fn pipelines(&self) -> impl Iterator { + self.pipelines.iter() + } + + /// Returns a iterator of the IDs of all currently waiting pipelines. + pub fn waiting_pipelines(&self) -> impl Iterator + '_ { + self.waiting_pipelines.iter().copied() + } + + /// Create a new pipeline cache associated with the given render device. + pub fn new( + device: RenderDevice, + render_adapter: RenderAdapter, + synchronous_pipeline_compilation: bool, + ) -> Self { + let mut global_shader_defs = Vec::new(); + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + { + global_shader_defs.push("NO_ARRAY_TEXTURES_SUPPORT".into()); + global_shader_defs.push("NO_CUBE_ARRAY_TEXTURES_SUPPORT".into()); + global_shader_defs.push("SIXTEEN_BYTE_ALIGNMENT".into()); + } + + if cfg!(target_abi = "sim") { + global_shader_defs.push("NO_CUBE_ARRAY_TEXTURES_SUPPORT".into()); + } + + global_shader_defs.push(ShaderDefVal::UInt( + String::from("AVAILABLE_STORAGE_BUFFER_BINDINGS"), + device.limits().max_storage_buffers_per_shader_stage, + )); + + Self { + shader_cache: Arc::new(Mutex::new(ShaderCache::new( + device.features(), + render_adapter.get_downlevel_capabilities().flags, + load_module, + ))), + device, + layout_cache: default(), + waiting_pipelines: default(), + new_pipelines: default(), + pipelines: default(), + global_shader_defs, + synchronous_pipeline_compilation, + } + } + + /// Get the state of a cached render pipeline. + /// + /// See [`PipelineCache::queue_render_pipeline()`]. + #[inline] + pub fn get_render_pipeline_state(&self, id: CachedRenderPipelineId) -> &CachedPipelineState { + // If the pipeline id isn't in `pipelines`, it's queued in `new_pipelines` + self.pipelines + .get(id.0) + .map_or(&CachedPipelineState::Queued, |pipeline| &pipeline.state) + } + + /// Get the state of a cached compute pipeline. + /// + /// See [`PipelineCache::queue_compute_pipeline()`]. + #[inline] + pub fn get_compute_pipeline_state(&self, id: CachedComputePipelineId) -> &CachedPipelineState { + // If the pipeline id isn't in `pipelines`, it's queued in `new_pipelines` + self.pipelines + .get(id.0) + .map_or(&CachedPipelineState::Queued, |pipeline| &pipeline.state) + } + + /// Get the render pipeline descriptor a cached render pipeline was inserted from. + /// + /// See [`PipelineCache::queue_render_pipeline()`]. + /// + /// **Note**: Be careful calling this method. It will panic if called with a pipeline that + /// has been queued but has not yet been processed by [`PipelineCache::process_queue()`]. + #[inline] + pub fn get_render_pipeline_descriptor( + &self, + id: CachedRenderPipelineId, + ) -> &RenderPipelineDescriptor { + match &self.pipelines[id.0].descriptor { + PipelineDescriptor::RenderPipelineDescriptor(descriptor) => descriptor, + PipelineDescriptor::ComputePipelineDescriptor(_) => unreachable!(), + } + } + + /// Get the compute pipeline descriptor a cached render pipeline was inserted from. + /// + /// See [`PipelineCache::queue_compute_pipeline()`]. + /// + /// **Note**: Be careful calling this method. It will panic if called with a pipeline that + /// has been queued but has not yet been processed by [`PipelineCache::process_queue()`]. + #[inline] + pub fn get_compute_pipeline_descriptor( + &self, + id: CachedComputePipelineId, + ) -> &ComputePipelineDescriptor { + match &self.pipelines[id.0].descriptor { + PipelineDescriptor::RenderPipelineDescriptor(_) => unreachable!(), + PipelineDescriptor::ComputePipelineDescriptor(descriptor) => descriptor, + } + } + + /// Try to retrieve a render pipeline GPU object from a cached ID. + /// + /// # Returns + /// + /// This method returns a successfully created render pipeline if any, or `None` if the pipeline + /// was not created yet or if there was an error during creation. You can check the actual creation + /// state with [`PipelineCache::get_render_pipeline_state()`]. + #[inline] + pub fn get_render_pipeline(&self, id: CachedRenderPipelineId) -> Option<&RenderPipeline> { + if let CachedPipelineState::Ok(Pipeline::RenderPipeline(pipeline)) = + &self.pipelines.get(id.0)?.state + { + Some(pipeline) + } else { + None + } + } + + /// Wait for a render pipeline to finish compiling. + #[inline] + pub fn block_on_render_pipeline(&mut self, id: CachedRenderPipelineId) { + if self.pipelines.len() <= id.0 { + self.process_queue(); + } + + let state = &mut self.pipelines[id.0].state; + if let CachedPipelineState::Creating(task) = state { + *state = match bevy_tasks::block_on(task) { + Ok(p) => CachedPipelineState::Ok(p), + Err(e) => CachedPipelineState::Err(e), + }; + } + } + + /// Try to retrieve a compute pipeline GPU object from a cached ID. + /// + /// # Returns + /// + /// This method returns a successfully created compute pipeline if any, or `None` if the pipeline + /// was not created yet or if there was an error during creation. You can check the actual creation + /// state with [`PipelineCache::get_compute_pipeline_state()`]. + #[inline] + pub fn get_compute_pipeline(&self, id: CachedComputePipelineId) -> Option<&ComputePipeline> { + if let CachedPipelineState::Ok(Pipeline::ComputePipeline(pipeline)) = + &self.pipelines.get(id.0)?.state + { + Some(pipeline) + } else { + None + } + } + + /// Insert a render pipeline into the cache, and queue its creation. + /// + /// The pipeline is always inserted and queued for creation. There is no attempt to deduplicate it with + /// an already cached pipeline. + /// + /// # Returns + /// + /// This method returns the unique render shader ID of the cached pipeline, which can be used to query + /// the caching state with [`get_render_pipeline_state()`] and to retrieve the created GPU pipeline once + /// it's ready with [`get_render_pipeline()`]. + /// + /// [`get_render_pipeline_state()`]: PipelineCache::get_render_pipeline_state + /// [`get_render_pipeline()`]: PipelineCache::get_render_pipeline + pub fn queue_render_pipeline( + &self, + descriptor: RenderPipelineDescriptor, + ) -> CachedRenderPipelineId { + let mut new_pipelines = self + .new_pipelines + .lock() + .unwrap_or_else(PoisonError::into_inner); + let id = CachedRenderPipelineId(self.pipelines.len() + new_pipelines.len()); + new_pipelines.push(CachedPipeline { + descriptor: PipelineDescriptor::RenderPipelineDescriptor(Box::new(descriptor)), + state: CachedPipelineState::Queued, + }); + id + } + + /// Insert a compute pipeline into the cache, and queue its creation. + /// + /// The pipeline is always inserted and queued for creation. There is no attempt to deduplicate it with + /// an already cached pipeline. + /// + /// # Returns + /// + /// This method returns the unique compute shader ID of the cached pipeline, which can be used to query + /// the caching state with [`get_compute_pipeline_state()`] and to retrieve the created GPU pipeline once + /// it's ready with [`get_compute_pipeline()`]. + /// + /// [`get_compute_pipeline_state()`]: PipelineCache::get_compute_pipeline_state + /// [`get_compute_pipeline()`]: PipelineCache::get_compute_pipeline + pub fn queue_compute_pipeline( + &self, + descriptor: ComputePipelineDescriptor, + ) -> CachedComputePipelineId { + let mut new_pipelines = self + .new_pipelines + .lock() + .unwrap_or_else(PoisonError::into_inner); + let id = CachedComputePipelineId(self.pipelines.len() + new_pipelines.len()); + new_pipelines.push(CachedPipeline { + descriptor: PipelineDescriptor::ComputePipelineDescriptor(Box::new(descriptor)), + state: CachedPipelineState::Queued, + }); + id + } + + fn set_shader(&mut self, id: AssetId, shader: Shader) { + let mut shader_cache = self.shader_cache.lock().unwrap(); + let pipelines_to_queue = shader_cache.set_shader(id, shader); + for cached_pipeline in pipelines_to_queue { + self.pipelines[cached_pipeline].state = CachedPipelineState::Queued; + self.waiting_pipelines.insert(cached_pipeline); + } + } + + fn remove_shader(&mut self, shader: AssetId) { + let mut shader_cache = self.shader_cache.lock().unwrap(); + let pipelines_to_queue = shader_cache.remove(shader); + for cached_pipeline in pipelines_to_queue { + self.pipelines[cached_pipeline].state = CachedPipelineState::Queued; + self.waiting_pipelines.insert(cached_pipeline); + } + } + + fn start_create_render_pipeline( + &mut self, + id: CachedPipelineId, + descriptor: RenderPipelineDescriptor, + ) -> CachedPipelineState { + let device = self.device.clone(); + let shader_cache = self.shader_cache.clone(); + let layout_cache = self.layout_cache.clone(); + + create_pipeline_task( + async move { + let mut shader_cache = shader_cache.lock().unwrap(); + let mut layout_cache = layout_cache.lock().unwrap(); + + let vertex_module = match shader_cache.get( + &device, + id, + descriptor.vertex.shader.id(), + &descriptor.vertex.shader_defs, + ) { + Ok(module) => module, + Err(err) => return Err(err), + }; + + let fragment_module = match &descriptor.fragment { + Some(fragment) => { + match shader_cache.get( + &device, + id, + fragment.shader.id(), + &fragment.shader_defs, + ) { + Ok(module) => Some(module), + Err(err) => return Err(err), + } + } + None => None, + }; + + let layout = + if descriptor.layout.is_empty() && descriptor.push_constant_ranges.is_empty() { + None + } else { + Some(layout_cache.get( + &device, + &descriptor.layout, + descriptor.push_constant_ranges.to_vec(), + )) + }; + + drop((shader_cache, layout_cache)); + + let vertex_buffer_layouts = descriptor + .vertex + .buffers + .iter() + .map(|layout| RawVertexBufferLayout { + array_stride: layout.array_stride, + attributes: &layout.attributes, + step_mode: layout.step_mode, + }) + .collect::>(); + + let fragment_data = descriptor.fragment.as_ref().map(|fragment| { + ( + fragment_module.unwrap(), + fragment.entry_point.as_deref(), + fragment.targets.as_slice(), + ) + }); + + // TODO: Expose the rest of this somehow + let compilation_options = PipelineCompilationOptions { + constants: &[], + zero_initialize_workgroup_memory: descriptor.zero_initialize_workgroup_memory, + }; + + let descriptor = RawRenderPipelineDescriptor { + multiview: None, + depth_stencil: descriptor.depth_stencil.clone(), + label: descriptor.label.as_deref(), + layout: layout.as_ref().map(|layout| -> &PipelineLayout { layout }), + multisample: descriptor.multisample, + primitive: descriptor.primitive, + vertex: RawVertexState { + buffers: &vertex_buffer_layouts, + entry_point: descriptor.vertex.entry_point.as_deref(), + module: &vertex_module, + // TODO: Should this be the same as the fragment compilation options? + compilation_options: compilation_options.clone(), + }, + fragment: fragment_data + .as_ref() + .map(|(module, entry_point, targets)| RawFragmentState { + entry_point: entry_point.as_deref(), + module, + targets, + // TODO: Should this be the same as the vertex compilation options? + compilation_options, + }), + cache: None, + }; + + Ok(Pipeline::RenderPipeline( + device.create_render_pipeline(&descriptor), + )) + }, + self.synchronous_pipeline_compilation, + ) + } + + fn start_create_compute_pipeline( + &mut self, + id: CachedPipelineId, + descriptor: ComputePipelineDescriptor, + ) -> CachedPipelineState { + let device = self.device.clone(); + let shader_cache = self.shader_cache.clone(); + let layout_cache = self.layout_cache.clone(); + + create_pipeline_task( + async move { + let mut shader_cache = shader_cache.lock().unwrap(); + let mut layout_cache = layout_cache.lock().unwrap(); + + let compute_module = match shader_cache.get( + &device, + id, + descriptor.shader.id(), + &descriptor.shader_defs, + ) { + Ok(module) => module, + Err(err) => return Err(err), + }; + + let layout = + if descriptor.layout.is_empty() && descriptor.push_constant_ranges.is_empty() { + None + } else { + Some(layout_cache.get( + &device, + &descriptor.layout, + descriptor.push_constant_ranges.to_vec(), + )) + }; + + drop((shader_cache, layout_cache)); + + let descriptor = RawComputePipelineDescriptor { + label: descriptor.label.as_deref(), + layout: layout.as_ref().map(|layout| -> &PipelineLayout { layout }), + module: &compute_module, + entry_point: descriptor.entry_point.as_deref(), + // TODO: Expose the rest of this somehow + compilation_options: PipelineCompilationOptions { + constants: &[], + zero_initialize_workgroup_memory: descriptor + .zero_initialize_workgroup_memory, + }, + cache: None, + }; + + Ok(Pipeline::ComputePipeline( + device.create_compute_pipeline(&descriptor), + )) + }, + self.synchronous_pipeline_compilation, + ) + } + + /// Process the pipeline queue and create all pending pipelines if possible. + /// + /// This is generally called automatically during the [`RenderSystems::Render`] step, but can + /// be called manually to force creation at a different time. + /// + /// [`RenderSystems::Render`]: crate::RenderSystems::Render + pub fn process_queue(&mut self) { + let mut waiting_pipelines = mem::take(&mut self.waiting_pipelines); + let mut pipelines = mem::take(&mut self.pipelines); + + { + let mut new_pipelines = self + .new_pipelines + .lock() + .unwrap_or_else(PoisonError::into_inner); + for new_pipeline in new_pipelines.drain(..) { + let id = pipelines.len(); + pipelines.push(new_pipeline); + waiting_pipelines.insert(id); + } + } + + for id in waiting_pipelines { + self.process_pipeline(&mut pipelines[id], id); + } + + self.pipelines = pipelines; + } + + fn process_pipeline(&mut self, cached_pipeline: &mut CachedPipeline, id: usize) { + match &mut cached_pipeline.state { + CachedPipelineState::Queued => { + cached_pipeline.state = match &cached_pipeline.descriptor { + PipelineDescriptor::RenderPipelineDescriptor(descriptor) => { + self.start_create_render_pipeline(id, *descriptor.clone()) + } + PipelineDescriptor::ComputePipelineDescriptor(descriptor) => { + self.start_create_compute_pipeline(id, *descriptor.clone()) + } + }; + } + + CachedPipelineState::Creating(task) => match bevy_tasks::futures::check_ready(task) { + Some(Ok(pipeline)) => { + cached_pipeline.state = CachedPipelineState::Ok(pipeline); + return; + } + Some(Err(err)) => cached_pipeline.state = CachedPipelineState::Err(err), + _ => (), + }, + + CachedPipelineState::Err(err) => match err { + // Retry + PipelineCacheError::ShaderNotLoaded(_) + | PipelineCacheError::ShaderImportNotYetAvailable => { + cached_pipeline.state = CachedPipelineState::Queued; + } + + // Shader could not be processed ... retrying won't help + PipelineCacheError::ProcessShaderError(err) => { + let error_detail = + err.emit_to_string(&self.shader_cache.lock().unwrap().composer); + if std::env::var("VERBOSE_SHADER_ERROR") + .is_ok_and(|v| !(v.is_empty() || v == "0" || v == "false")) + { + error!("{}", pipeline_error_context(cached_pipeline)); + } + error!("failed to process shader error:\n{}", error_detail); + return; + } + PipelineCacheError::CreateShaderModule(description) => { + error!("failed to create shader module: {}", description); + return; + } + }, + + CachedPipelineState::Ok(_) => return, + } + + // Retry + self.waiting_pipelines.insert(id); + } + + pub(crate) fn process_pipeline_queue_system(mut cache: ResMut) { + cache.process_queue(); + } + + pub(crate) fn extract_shaders( + mut cache: ResMut, + shaders: Extract>>, + mut events: Extract>>, + ) { + for event in events.read() { + #[expect( + clippy::match_same_arms, + reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon." + )] + match event { + // PERF: Instead of blocking waiting for the shader cache lock, try again next frame if the lock is currently held + AssetEvent::Added { id } | AssetEvent::Modified { id } => { + if let Some(shader) = shaders.get(*id) { + let mut shader = shader.clone(); + shader.shader_defs.extend(cache.global_shader_defs.clone()); + + cache.set_shader(*id, shader); + } + } + AssetEvent::Removed { id } => cache.remove_shader(*id), + AssetEvent::Unused { .. } => {} + AssetEvent::LoadedWithDependencies { .. } => { + // TODO: handle this + } + } + } + } +} + +fn pipeline_error_context(cached_pipeline: &CachedPipeline) -> String { + fn format( + shader: &Handle, + entry: &Option>, + shader_defs: &[ShaderDefVal], + ) -> String { + let source = match shader.path() { + Some(path) => path.path().to_string_lossy().to_string(), + None => String::new(), + }; + let entry = match entry { + Some(entry) => entry.to_string(), + None => String::new(), + }; + let shader_defs = shader_defs + .iter() + .flat_map(|def| match def { + ShaderDefVal::Bool(k, v) if *v => Some(k.to_string()), + ShaderDefVal::Int(k, v) => Some(format!("{k} = {v}")), + ShaderDefVal::UInt(k, v) => Some(format!("{k} = {v}")), + _ => None, + }) + .collect::>() + .join(", "); + format!("{source}:{entry}\nshader defs: {shader_defs}") + } + match &cached_pipeline.descriptor { + PipelineDescriptor::RenderPipelineDescriptor(desc) => { + let vert = &desc.vertex; + let vert_str = format(&vert.shader, &vert.entry_point, &vert.shader_defs); + let Some(frag) = desc.fragment.as_ref() else { + return vert_str; + }; + let frag_str = format(&frag.shader, &frag.entry_point, &frag.shader_defs); + format!("vertex {vert_str}\nfragment {frag_str}") + } + PipelineDescriptor::ComputePipelineDescriptor(desc) => { + format(&desc.shader, &desc.entry_point, &desc.shader_defs) + } + } +} + +#[cfg(all( + not(target_arch = "wasm32"), + not(target_os = "macos"), + feature = "multi_threaded" +))] +fn create_pipeline_task( + task: impl Future> + Send + 'static, + sync: bool, +) -> CachedPipelineState { + if !sync { + return CachedPipelineState::Creating(bevy_tasks::AsyncComputeTaskPool::get().spawn(task)); + } + + match bevy_tasks::block_on(task) { + Ok(pipeline) => CachedPipelineState::Ok(pipeline), + Err(err) => CachedPipelineState::Err(err), + } +} + +#[cfg(any( + target_arch = "wasm32", + target_os = "macos", + not(feature = "multi_threaded") +))] +fn create_pipeline_task( + task: impl Future> + Send + 'static, + _sync: bool, +) -> CachedPipelineState { + match bevy_tasks::block_on(task) { + Ok(pipeline) => CachedPipelineState::Ok(pipeline), + Err(err) => CachedPipelineState::Err(err), + } +} diff --git a/crates/libmarathon/src/render/render_resource/pipeline_specializer.rs b/crates/libmarathon/src/render/render_resource/pipeline_specializer.rs new file mode 100644 index 0000000..137ab95 --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/pipeline_specializer.rs @@ -0,0 +1,259 @@ +use crate::render::render_resource::{ + CachedComputePipelineId, CachedRenderPipelineId, ComputePipelineDescriptor, PipelineCache, + RenderPipelineDescriptor, +}; +use bevy_ecs::resource::Resource; +use bevy_mesh::{MeshVertexBufferLayoutRef, MissingVertexAttributeError, VertexBufferLayout}; +use bevy_platform::{ + collections::{ + hash_map::{Entry, RawEntryMut, VacantEntry}, + HashMap, + }, + hash::FixedHasher, +}; +use bevy_utils::default; +use core::{fmt::Debug, hash::Hash}; +use thiserror::Error; +use tracing::error; + +/// A trait that allows constructing different variants of a render pipeline from a key. +/// +/// Note: This is intended for modifying your pipeline descriptor on the basis of a key. If your key +/// contains no data then you don't need to specialize. For example, if you are using the +/// [`AsBindGroup`](crate::render_resource::AsBindGroup) without the `#[bind_group_data]` attribute, +/// you don't need to specialize. Instead, create the pipeline directly from [`PipelineCache`] and +/// store its ID. +/// +/// See [`SpecializedRenderPipelines`] for more info. +pub trait SpecializedRenderPipeline { + /// The key that defines each "variant" of the render pipeline. + type Key: Clone + Hash + PartialEq + Eq; + + /// Construct a new render pipeline based on the provided key. + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor; +} + +/// A convenience cache for creating different variants of a render pipeline based on some key. +/// +/// Some render pipelines may need to be configured differently depending on the exact situation. +/// This cache allows constructing different render pipelines for each situation based on a key, +/// making it easy to A) construct the necessary pipelines, and B) reuse already constructed +/// pipelines. +/// +/// Note: This is intended for modifying your pipeline descriptor on the basis of a key. If your key +/// contains no data then you don't need to specialize. For example, if you are using the +/// [`AsBindGroup`](crate::render_resource::AsBindGroup) without the `#[bind_group_data]` attribute, +/// you don't need to specialize. Instead, create the pipeline directly from [`PipelineCache`] and +/// store its ID. +#[derive(Resource)] +pub struct SpecializedRenderPipelines { + cache: HashMap, +} + +impl Default for SpecializedRenderPipelines { + fn default() -> Self { + Self { cache: default() } + } +} + +impl SpecializedRenderPipelines { + /// Get or create a specialized instance of the pipeline corresponding to `key`. + pub fn specialize( + &mut self, + cache: &PipelineCache, + pipeline_specializer: &S, + key: S::Key, + ) -> CachedRenderPipelineId { + *self.cache.entry(key.clone()).or_insert_with(|| { + let descriptor = pipeline_specializer.specialize(key); + cache.queue_render_pipeline(descriptor) + }) + } +} + +/// A trait that allows constructing different variants of a compute pipeline from a key. +/// +/// Note: This is intended for modifying your pipeline descriptor on the basis of a key. If your key +/// contains no data then you don't need to specialize. For example, if you are using the +/// [`AsBindGroup`](crate::render_resource::AsBindGroup) without the `#[bind_group_data]` attribute, +/// you don't need to specialize. Instead, create the pipeline directly from [`PipelineCache`] and +/// store its ID. +/// +/// See [`SpecializedComputePipelines`] for more info. +pub trait SpecializedComputePipeline { + /// The key that defines each "variant" of the compute pipeline. + type Key: Clone + Hash + PartialEq + Eq; + + /// Construct a new compute pipeline based on the provided key. + fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor; +} + +/// A convenience cache for creating different variants of a compute pipeline based on some key. +/// +/// Some compute pipelines may need to be configured differently depending on the exact situation. +/// This cache allows constructing different compute pipelines for each situation based on a key, +/// making it easy to A) construct the necessary pipelines, and B) reuse already constructed +/// pipelines. +/// +/// Note: This is intended for modifying your pipeline descriptor on the basis of a key. If your key +/// contains no data then you don't need to specialize. For example, if you are using the +/// [`AsBindGroup`](crate::render_resource::AsBindGroup) without the `#[bind_group_data]` attribute, +/// you don't need to specialize. Instead, create the pipeline directly from [`PipelineCache`] and +/// store its ID. +#[derive(Resource)] +pub struct SpecializedComputePipelines { + cache: HashMap, +} + +impl Default for SpecializedComputePipelines { + fn default() -> Self { + Self { cache: default() } + } +} + +impl SpecializedComputePipelines { + /// Get or create a specialized instance of the pipeline corresponding to `key`. + pub fn specialize( + &mut self, + cache: &PipelineCache, + specialize_pipeline: &S, + key: S::Key, + ) -> CachedComputePipelineId { + *self.cache.entry(key.clone()).or_insert_with(|| { + let descriptor = specialize_pipeline.specialize(key); + cache.queue_compute_pipeline(descriptor) + }) + } +} + +/// A trait that allows constructing different variants of a render pipeline from a key and the +/// particular mesh's vertex buffer layout. +/// +/// See [`SpecializedMeshPipelines`] for more info. +pub trait SpecializedMeshPipeline { + /// The key that defines each "variant" of the render pipeline. + type Key: Clone + Hash + PartialEq + Eq; + + /// Construct a new render pipeline based on the provided key and vertex layout. + /// + /// The returned pipeline descriptor should have a single vertex buffer, which is derived from + /// `layout`. + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result; +} + +/// A cache of different variants of a render pipeline based on a key and the particular mesh's +/// vertex buffer layout. +#[derive(Resource)] +pub struct SpecializedMeshPipelines { + mesh_layout_cache: HashMap<(MeshVertexBufferLayoutRef, S::Key), CachedRenderPipelineId>, + vertex_layout_cache: VertexLayoutCache, +} + +type VertexLayoutCache = HashMap< + VertexBufferLayout, + HashMap<::Key, CachedRenderPipelineId>, +>; + +impl Default for SpecializedMeshPipelines { + fn default() -> Self { + Self { + mesh_layout_cache: Default::default(), + vertex_layout_cache: Default::default(), + } + } +} + +impl SpecializedMeshPipelines { + /// Construct a new render pipeline based on the provided key and the mesh's vertex buffer + /// layout. + #[inline] + pub fn specialize( + &mut self, + cache: &PipelineCache, + pipeline_specializer: &S, + key: S::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + return match self.mesh_layout_cache.entry((layout.clone(), key.clone())) { + Entry::Occupied(entry) => Ok(*entry.into_mut()), + Entry::Vacant(entry) => specialize_slow( + &mut self.vertex_layout_cache, + cache, + pipeline_specializer, + key, + layout, + entry, + ), + }; + + #[cold] + fn specialize_slow( + vertex_layout_cache: &mut VertexLayoutCache, + cache: &PipelineCache, + specialize_pipeline: &S, + key: S::Key, + layout: &MeshVertexBufferLayoutRef, + entry: VacantEntry< + (MeshVertexBufferLayoutRef, S::Key), + CachedRenderPipelineId, + FixedHasher, + >, + ) -> Result + where + S: SpecializedMeshPipeline, + { + let descriptor = specialize_pipeline + .specialize(key.clone(), layout) + .map_err(|mut err| { + { + let SpecializedMeshPipelineError::MissingVertexAttribute(err) = &mut err; + err.pipeline_type = Some(core::any::type_name::()); + } + err + })?; + // Different MeshVertexBufferLayouts can produce the same final VertexBufferLayout + // We want compatible vertex buffer layouts to use the same pipelines, so we must "deduplicate" them + let layout_map = match vertex_layout_cache + .raw_entry_mut() + .from_key(&descriptor.vertex.buffers[0]) + { + RawEntryMut::Occupied(entry) => entry.into_mut(), + RawEntryMut::Vacant(entry) => { + entry + .insert(descriptor.vertex.buffers[0].clone(), Default::default()) + .1 + } + }; + Ok(*entry.insert(match layout_map.entry(key) { + Entry::Occupied(entry) => { + if cfg!(debug_assertions) { + let stored_descriptor = cache.get_render_pipeline_descriptor(*entry.get()); + if stored_descriptor != &descriptor { + error!( + "The cached pipeline descriptor for {} is not \ + equal to the generated descriptor for the given key. \ + This means the SpecializePipeline implementation uses \ + unused' MeshVertexBufferLayout information to specialize \ + the pipeline. This is not allowed because it would invalidate \ + the pipeline cache.", + core::any::type_name::() + ); + } + } + *entry.into_mut() + } + Entry::Vacant(entry) => *entry.insert(cache.queue_render_pipeline(descriptor)), + })) + } + } +} + +#[derive(Error, Debug)] +pub enum SpecializedMeshPipelineError { + #[error(transparent)] + MissingVertexAttribute(#[from] MissingVertexAttributeError), +} diff --git a/crates/libmarathon/src/render/render_resource/resource_macros.rs b/crates/libmarathon/src/render/render_resource/resource_macros.rs new file mode 100644 index 0000000..6cdf3b6 --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/resource_macros.rs @@ -0,0 +1,39 @@ +#[macro_export] +macro_rules! define_atomic_id { + ($atomic_id_type:ident) => { + #[derive(Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)] + pub struct $atomic_id_type(core::num::NonZero); + + impl $atomic_id_type { + #[expect( + clippy::new_without_default, + reason = "Implementing the `Default` trait on atomic IDs would imply that two `::default()` equal each other. By only implementing `new()`, we indicate that each atomic ID created will be unique." + )] + pub fn new() -> Self { + use core::sync::atomic::{AtomicU32, Ordering}; + + static COUNTER: AtomicU32 = AtomicU32::new(1); + + let counter = COUNTER.fetch_add(1, Ordering::Relaxed); + Self(core::num::NonZero::::new(counter).unwrap_or_else(|| { + panic!( + "The system ran out of unique `{}`s.", + stringify!($atomic_id_type) + ); + })) + } + } + + impl From<$atomic_id_type> for core::num::NonZero { + fn from(value: $atomic_id_type) -> Self { + value.0 + } + } + + impl From> for $atomic_id_type { + fn from(value: core::num::NonZero) -> Self { + Self(value) + } + } + }; +} diff --git a/crates/libmarathon/src/render/render_resource/specializer.rs b/crates/libmarathon/src/render/render_resource/specializer.rs new file mode 100644 index 0000000..31edf62 --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/specializer.rs @@ -0,0 +1,353 @@ +use super::{ + CachedComputePipelineId, CachedRenderPipelineId, ComputePipeline, ComputePipelineDescriptor, + PipelineCache, RenderPipeline, RenderPipelineDescriptor, +}; +use bevy_ecs::error::BevyError; +use bevy_platform::{ + collections::{ + hash_map::{Entry, VacantEntry}, + HashMap, + }, + hash::FixedHasher, +}; +use core::{hash::Hash, marker::PhantomData}; +use tracing::error; +use variadics_please::all_tuples; + +pub use macros::{Specializer, SpecializerKey}; + +/// Defines a type that is able to be "specialized" and cached by creating and transforming +/// its descriptor type. This is implemented for [`RenderPipeline`] and [`ComputePipeline`], and +/// likely will not have much utility for other types. +/// +/// See docs on [`Specializer`] for more info. +pub trait Specializable { + type Descriptor: PartialEq + Clone + Send + Sync; + type CachedId: Clone + Send + Sync; + fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId; + fn get_descriptor(pipeline_cache: &PipelineCache, id: Self::CachedId) -> &Self::Descriptor; +} + +impl Specializable for RenderPipeline { + type Descriptor = RenderPipelineDescriptor; + type CachedId = CachedRenderPipelineId; + + fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId { + pipeline_cache.queue_render_pipeline(descriptor) + } + + fn get_descriptor( + pipeline_cache: &PipelineCache, + id: CachedRenderPipelineId, + ) -> &Self::Descriptor { + pipeline_cache.get_render_pipeline_descriptor(id) + } +} + +impl Specializable for ComputePipeline { + type Descriptor = ComputePipelineDescriptor; + + type CachedId = CachedComputePipelineId; + + fn queue(pipeline_cache: &PipelineCache, descriptor: Self::Descriptor) -> Self::CachedId { + pipeline_cache.queue_compute_pipeline(descriptor) + } + + fn get_descriptor( + pipeline_cache: &PipelineCache, + id: CachedComputePipelineId, + ) -> &Self::Descriptor { + pipeline_cache.get_compute_pipeline_descriptor(id) + } +} + +/// Defines a type capable of "specializing" values of a type T. +/// +/// Specialization is the process of generating variants of a type T +/// from small hashable keys, and specializers themselves can be +/// thought of as [pure functions] from the key type to `T`, that +/// [memoize] their results based on the key. +/// +///

    +/// +/// Since compiling render and compute pipelines can be so slow, +/// specialization allows a Bevy app to detect when it would compile +/// a duplicate pipeline and reuse what's already in the cache. While +/// pipelines could all be memoized hashing each whole descriptor, this +/// would be much slower and could still create duplicates. In contrast, +/// memoizing groups of *related* pipelines based on a small hashable +/// key is much faster. See the docs on [`SpecializerKey`] for more info. +/// +/// ## Composing Specializers +/// +/// This trait can be derived with `#[derive(Specializer)]` for structs whose +/// fields all implement [`Specializer`]. This allows for composing multiple +/// specializers together, and makes encapsulation and separating concerns +/// between specializers much nicer. One could make individual specializers +/// for common operations and place them in entirely separate modules, then +/// compose them together with a single `#[derive]` +/// +/// ```rust +/// # use bevy_ecs::error::BevyError; +/// # use crate::render::render_resource::Specializer; +/// # use crate::render::render_resource::SpecializerKey; +/// # use crate::render::render_resource::RenderPipeline; +/// # use crate::render::render_resource::RenderPipelineDescriptor; +/// struct A; +/// struct B; +/// #[derive(Copy, Clone, PartialEq, Eq, Hash, SpecializerKey)] +/// struct BKey { contrived_number: u32 }; +/// +/// impl Specializer for A { +/// type Key = (); +/// +/// fn specialize( +/// &self, +/// key: (), +/// descriptor: &mut RenderPipelineDescriptor +/// ) -> Result<(), BevyError> { +/// # let _ = descriptor; +/// // mutate the descriptor here +/// Ok(key) +/// } +/// } +/// +/// impl Specializer for B { +/// type Key = BKey; +/// +/// fn specialize( +/// &self, +/// key: BKey, +/// descriptor: &mut RenderPipelineDescriptor +/// ) -> Result { +/// # let _ = descriptor; +/// // mutate the descriptor here +/// Ok(key) +/// } +/// } +/// +/// #[derive(Specializer)] +/// #[specialize(RenderPipeline)] +/// struct C { +/// #[key(default)] +/// a: A, +/// b: B, +/// } +/// +/// /* +/// The generated implementation: +/// impl Specializer for C { +/// type Key = BKey; +/// fn specialize( +/// &self, +/// key: Self::Key, +/// descriptor: &mut RenderPipelineDescriptor +/// ) -> Result, BevyError> { +/// let _ = self.a.specialize((), descriptor); +/// let key = self.b.specialize(key, descriptor); +/// Ok(key) +/// } +/// } +/// */ +/// ``` +/// +/// The key type for a composed specializer will be a tuple of the keys +/// of each field, and their specialization logic will be applied in field +/// order. Since derive macros can't have generic parameters, the derive macro +/// requires an additional `#[specialize(..targets)]` attribute to specify a +/// list of types to target for the implementation. `#[specialize(all)]` is +/// also allowed, and will generate a fully generic implementation at the cost +/// of slightly worse error messages. +/// +/// Additionally, each field can optionally take a `#[key]` attribute to +/// specify a "key override". This will hide that field's key from being +/// exposed by the wrapper, and always use the value given by the attribute. +/// Values for this attribute may either be `default` which will use the key's +/// [`Default`] implementation, or a valid rust expression of the key type. +/// +/// [pure functions]: https://en.wikipedia.org/wiki/Pure_function +/// [memoize]: https://en.wikipedia.org/wiki/Memoization +pub trait Specializer: Send + Sync + 'static { + type Key: SpecializerKey; + fn specialize( + &self, + key: Self::Key, + descriptor: &mut T::Descriptor, + ) -> Result, BevyError>; +} + +// TODO: update docs for `SpecializerKey` with a more concrete example +// once we've migrated mesh layout specialization + +/// Defines a type that is able to be used as a key for [`Specializer`]s +/// +///
    +/// Most types should implement this trait with the included derive macro.
    +/// This generates a "canonical" key type, with IS_CANONICAL = true, and Canonical = Self +///
    +/// +/// ## What's a "canonical" key? +/// +/// The specialization API memoizes pipelines based on the hash of each key, but this +/// can still produce duplicates. For example, if one used a list of vertex attributes +/// as a key, even if all the same attributes were present they could be in any order. +/// In each case, though the keys would be "different" they would produce the same +/// pipeline. +/// +/// To address this, during specialization keys are processed into a [canonical] +/// (or "standard") form that represents the actual descriptor that was produced. +/// In the previous example, that would be the final `VertexBufferLayout` contained +/// by the pipeline descriptor. This new key is used by [`Variants`] to +/// perform additional checks for duplicates, but only if required. If a key is +/// canonical from the start, then there's no need. +/// +/// For implementors: the main property of a canonical key is that if two keys hash +/// differently, they should nearly always produce different descriptors. +/// +/// [canonical]: https://en.wikipedia.org/wiki/Canonicalization +pub trait SpecializerKey: Clone + Hash + Eq { + /// Denotes whether this key is canonical or not. This should only be `true` + /// if and only if `Canonical = Self`. + const IS_CANONICAL: bool; + + /// The canonical key type to convert this into during specialization. + type Canonical: Hash + Eq; +} + +pub type Canonical = ::Canonical; + +impl Specializer for () { + type Key = (); + + fn specialize( + &self, + _key: Self::Key, + _descriptor: &mut T::Descriptor, + ) -> Result<(), BevyError> { + Ok(()) + } +} + +impl Specializer for PhantomData { + type Key = (); + + fn specialize( + &self, + _key: Self::Key, + _descriptor: &mut T::Descriptor, + ) -> Result<(), BevyError> { + Ok(()) + } +} + +macro_rules! impl_specialization_key_tuple { + ($(#[$meta:meta])* $($T:ident),*) => { + $(#[$meta])* + impl <$($T: SpecializerKey),*> SpecializerKey for ($($T,)*) { + const IS_CANONICAL: bool = true $(&& <$T as SpecializerKey>::IS_CANONICAL)*; + type Canonical = ($(Canonical<$T>,)*); + } + }; +} + +all_tuples!( + #[doc(fake_variadic)] + impl_specialization_key_tuple, + 0, + 12, + T +); + +/// A cache for variants of a resource type created by a specializer. +/// At most one resource will be created for each key. +pub struct Variants> { + specializer: S, + base_descriptor: T::Descriptor, + primary_cache: HashMap, + secondary_cache: HashMap, T::CachedId>, +} + +impl> Variants { + /// Creates a new [`Variants`] from a [`Specializer`] and a base descriptor. + #[inline] + pub fn new(specializer: S, base_descriptor: T::Descriptor) -> Self { + Self { + specializer, + base_descriptor, + primary_cache: Default::default(), + secondary_cache: Default::default(), + } + } + + /// Specializes a resource given the [`Specializer`]'s key type. + #[inline] + pub fn specialize( + &mut self, + pipeline_cache: &PipelineCache, + key: S::Key, + ) -> Result { + let entry = self.primary_cache.entry(key.clone()); + match entry { + Entry::Occupied(entry) => Ok(entry.get().clone()), + Entry::Vacant(entry) => Self::specialize_slow( + &self.specializer, + self.base_descriptor.clone(), + pipeline_cache, + key, + entry, + &mut self.secondary_cache, + ), + } + } + + #[cold] + fn specialize_slow( + specializer: &S, + base_descriptor: T::Descriptor, + pipeline_cache: &PipelineCache, + key: S::Key, + primary_entry: VacantEntry, + secondary_cache: &mut HashMap, T::CachedId>, + ) -> Result { + let mut descriptor = base_descriptor.clone(); + let canonical_key = specializer.specialize(key.clone(), &mut descriptor)?; + + // if the whole key is canonical, the secondary cache isn't needed. + if ::IS_CANONICAL { + return Ok(primary_entry + .insert(::queue(pipeline_cache, descriptor)) + .clone()); + } + + let id = match secondary_cache.entry(canonical_key) { + Entry::Occupied(entry) => { + if cfg!(debug_assertions) { + let stored_descriptor = + ::get_descriptor(pipeline_cache, entry.get().clone()); + if &descriptor != stored_descriptor { + error!( + "Invalid Specializer<{}> impl for {}: the cached descriptor \ + is not equal to the generated descriptor for the given key. \ + This means the Specializer implementation uses unused information \ + from the key to specialize the pipeline. This is not allowed \ + because it would invalidate the cache.", + core::any::type_name::(), + core::any::type_name::() + ); + } + } + entry.into_mut().clone() + } + Entry::Vacant(entry) => entry + .insert(::queue(pipeline_cache, descriptor)) + .clone(), + }; + + primary_entry.insert(id.clone()); + Ok(id) + } +} diff --git a/crates/libmarathon/src/render/render_resource/storage_buffer.rs b/crates/libmarathon/src/render/render_resource/storage_buffer.rs new file mode 100644 index 0000000..b1eeeb9 --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/storage_buffer.rs @@ -0,0 +1,285 @@ +use core::marker::PhantomData; + +use super::Buffer; +use crate::render::renderer::{RenderDevice, RenderQueue}; +use encase::{ + internal::WriteInto, DynamicStorageBuffer as DynamicStorageBufferWrapper, ShaderType, + StorageBuffer as StorageBufferWrapper, +}; +use wgpu::{util::BufferInitDescriptor, BindingResource, BufferBinding, BufferSize, BufferUsages}; + +use super::IntoBinding; + +/// Stores data to be transferred to the GPU and made accessible to shaders as a storage buffer. +/// +/// Storage buffers can be made available to shaders in some combination of read/write mode, and can store large amounts of data. +/// Note however that WebGL2 does not support storage buffers, so consider alternative options in this case. +/// +/// Storage buffers can store runtime-sized arrays, but only if they are the last field in a structure. +/// +/// The contained data is stored in system RAM. [`write_buffer`](StorageBuffer::write_buffer) queues +/// copying of the data from system RAM to VRAM. Storage buffers must conform to [std430 alignment/padding requirements], which +/// is automatically enforced by this structure. +/// +/// Other options for storing GPU-accessible data are: +/// * [`BufferVec`](crate::render_resource::BufferVec) +/// * [`DynamicStorageBuffer`] +/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer) +/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer) +/// * [`RawBufferVec`](crate::render_resource::RawBufferVec) +/// * [`Texture`](crate::render_resource::Texture) +/// * [`UniformBuffer`](crate::render_resource::UniformBuffer) +/// +/// [std430 alignment/padding requirements]: https://www.w3.org/TR/WGSL/#address-spaces-storage +pub struct StorageBuffer { + value: T, + scratch: StorageBufferWrapper>, + buffer: Option, + label: Option, + changed: bool, + buffer_usage: BufferUsages, + last_written_size: Option, +} + +impl From for StorageBuffer { + fn from(value: T) -> Self { + Self { + value, + scratch: StorageBufferWrapper::new(Vec::new()), + buffer: None, + label: None, + changed: false, + buffer_usage: BufferUsages::COPY_DST | BufferUsages::STORAGE, + last_written_size: None, + } + } +} + +impl Default for StorageBuffer { + fn default() -> Self { + Self { + value: T::default(), + scratch: StorageBufferWrapper::new(Vec::new()), + buffer: None, + label: None, + changed: false, + buffer_usage: BufferUsages::COPY_DST | BufferUsages::STORAGE, + last_written_size: None, + } + } +} + +impl StorageBuffer { + #[inline] + pub fn buffer(&self) -> Option<&Buffer> { + self.buffer.as_ref() + } + + #[inline] + pub fn binding(&self) -> Option> { + Some(BindingResource::Buffer(BufferBinding { + buffer: self.buffer()?, + offset: 0, + size: self.last_written_size, + })) + } + + pub fn set(&mut self, value: T) { + self.value = value; + } + + pub fn get(&self) -> &T { + &self.value + } + + pub fn get_mut(&mut self) -> &mut T { + &mut self.value + } + + pub fn set_label(&mut self, label: Option<&str>) { + let label = label.map(str::to_string); + + if label != self.label { + self.changed = true; + } + + self.label = label; + } + + pub fn get_label(&self) -> Option<&str> { + self.label.as_deref() + } + + /// Add more [`BufferUsages`] to the buffer. + /// + /// This method only allows addition of flags to the default usage flags. + /// + /// The default values for buffer usage are `BufferUsages::COPY_DST` and `BufferUsages::STORAGE`. + pub fn add_usages(&mut self, usage: BufferUsages) { + self.buffer_usage |= usage; + self.changed = true; + } + + /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`] + /// and the provided [`RenderQueue`]. + /// + /// If there is no GPU-side buffer allocated to hold the data currently stored, or if a GPU-side buffer previously + /// allocated does not have enough capacity, a new GPU-side buffer is created. + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + self.scratch.write(&self.value).unwrap(); + + let capacity = self.buffer.as_deref().map(wgpu::Buffer::size).unwrap_or(0); + let size = self.scratch.as_ref().len() as u64; + + if capacity < size || self.changed { + self.buffer = Some(device.create_buffer_with_data(&BufferInitDescriptor { + label: self.label.as_deref(), + usage: self.buffer_usage, + contents: self.scratch.as_ref(), + })); + self.changed = false; + } else if let Some(buffer) = &self.buffer { + queue.write_buffer(buffer, 0, self.scratch.as_ref()); + } + + self.last_written_size = BufferSize::new(size); + } +} + +impl<'a, T: ShaderType + WriteInto> IntoBinding<'a> for &'a StorageBuffer { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + self.binding().expect("Failed to get buffer") + } +} + +/// Stores data to be transferred to the GPU and made accessible to shaders as a dynamic storage buffer. +/// +/// This is just a [`StorageBuffer`], but also allows you to set dynamic offsets. +/// +/// Dynamic storage buffers can be made available to shaders in some combination of read/write mode, and can store large amounts +/// of data. Note however that WebGL2 does not support storage buffers, so consider alternative options in this case. Dynamic +/// storage buffers support multiple separate bindings at dynamic byte offsets and so have a +/// [`push`](DynamicStorageBuffer::push) method. +/// +/// The contained data is stored in system RAM. [`write_buffer`](DynamicStorageBuffer::write_buffer) +/// queues copying of the data from system RAM to VRAM. The data within a storage buffer binding must conform to +/// [std430 alignment/padding requirements]. `DynamicStorageBuffer` takes care of serializing the inner type to conform to +/// these requirements. Each item [`push`](DynamicStorageBuffer::push)ed into this structure +/// will additionally be aligned to meet dynamic offset alignment requirements. +/// +/// Other options for storing GPU-accessible data are: +/// * [`BufferVec`](crate::render_resource::BufferVec) +/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer) +/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer) +/// * [`RawBufferVec`](crate::render_resource::RawBufferVec) +/// * [`StorageBuffer`] +/// * [`Texture`](crate::render_resource::Texture) +/// * [`UniformBuffer`](crate::render_resource::UniformBuffer) +/// +/// [std430 alignment/padding requirements]: https://www.w3.org/TR/WGSL/#address-spaces-storage +pub struct DynamicStorageBuffer { + scratch: DynamicStorageBufferWrapper>, + buffer: Option, + label: Option, + changed: bool, + buffer_usage: BufferUsages, + last_written_size: Option, + _marker: PhantomData T>, +} + +impl Default for DynamicStorageBuffer { + fn default() -> Self { + Self { + scratch: DynamicStorageBufferWrapper::new(Vec::new()), + buffer: None, + label: None, + changed: false, + buffer_usage: BufferUsages::COPY_DST | BufferUsages::STORAGE, + last_written_size: None, + _marker: PhantomData, + } + } +} + +impl DynamicStorageBuffer { + #[inline] + pub fn buffer(&self) -> Option<&Buffer> { + self.buffer.as_ref() + } + + #[inline] + pub fn binding(&self) -> Option> { + Some(BindingResource::Buffer(BufferBinding { + buffer: self.buffer()?, + offset: 0, + size: self.last_written_size, + })) + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.scratch.as_ref().is_empty() + } + + #[inline] + pub fn push(&mut self, value: T) -> u32 { + self.scratch.write(&value).unwrap() as u32 + } + + pub fn set_label(&mut self, label: Option<&str>) { + let label = label.map(str::to_string); + + if label != self.label { + self.changed = true; + } + + self.label = label; + } + + pub fn get_label(&self) -> Option<&str> { + self.label.as_deref() + } + + /// Add more [`BufferUsages`] to the buffer. + /// + /// This method only allows addition of flags to the default usage flags. + /// + /// The default values for buffer usage are `BufferUsages::COPY_DST` and `BufferUsages::STORAGE`. + pub fn add_usages(&mut self, usage: BufferUsages) { + self.buffer_usage |= usage; + self.changed = true; + } + + #[inline] + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + let capacity = self.buffer.as_deref().map(wgpu::Buffer::size).unwrap_or(0); + let size = self.scratch.as_ref().len() as u64; + + if capacity < size || (self.changed && size > 0) { + self.buffer = Some(device.create_buffer_with_data(&BufferInitDescriptor { + label: self.label.as_deref(), + usage: self.buffer_usage, + contents: self.scratch.as_ref(), + })); + self.changed = false; + } else if let Some(buffer) = &self.buffer { + queue.write_buffer(buffer, 0, self.scratch.as_ref()); + } + + self.last_written_size = BufferSize::new(size); + } + + #[inline] + pub fn clear(&mut self) { + self.scratch.as_mut().clear(); + self.scratch.set_offset(0); + } +} + +impl<'a, T: ShaderType + WriteInto> IntoBinding<'a> for &'a DynamicStorageBuffer { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + self.binding().expect("Failed to get buffer") + } +} diff --git a/crates/libmarathon/src/render/render_resource/texture.rs b/crates/libmarathon/src/render/render_resource/texture.rs new file mode 100644 index 0000000..25aad67 --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/texture.rs @@ -0,0 +1,166 @@ +use crate::render::define_atomic_id; +use crate::render::renderer::WgpuWrapper; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::resource::Resource; +use core::ops::Deref; + +define_atomic_id!(TextureId); + +/// A GPU-accessible texture. +/// +/// May be converted from and dereferences to a wgpu [`Texture`](wgpu::Texture). +/// Can be created via [`RenderDevice::create_texture`](crate::renderer::RenderDevice::create_texture). +/// +/// Other options for storing GPU-accessible data are: +/// * [`BufferVec`](crate::render_resource::BufferVec) +/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer) +/// * [`DynamicUniformBuffer`](crate::render_resource::DynamicUniformBuffer) +/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer) +/// * [`RawBufferVec`](crate::render_resource::RawBufferVec) +/// * [`StorageBuffer`](crate::render_resource::StorageBuffer) +/// * [`UniformBuffer`](crate::render_resource::UniformBuffer) +#[derive(Clone, Debug)] +pub struct Texture { + id: TextureId, + value: WgpuWrapper, +} + +impl Texture { + /// Returns the [`TextureId`]. + #[inline] + pub fn id(&self) -> TextureId { + self.id + } + + /// Creates a view of this texture. + pub fn create_view(&self, desc: &wgpu::TextureViewDescriptor) -> TextureView { + TextureView::from(self.value.create_view(desc)) + } +} + +impl From for Texture { + fn from(value: wgpu::Texture) -> Self { + Texture { + id: TextureId::new(), + value: WgpuWrapper::new(value), + } + } +} + +impl Deref for Texture { + type Target = wgpu::Texture; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +define_atomic_id!(TextureViewId); + +/// Describes a [`Texture`] with its associated metadata required by a pipeline or [`BindGroup`](super::BindGroup). +#[derive(Clone, Debug)] +pub struct TextureView { + id: TextureViewId, + value: WgpuWrapper, +} + +pub struct SurfaceTexture { + value: WgpuWrapper, +} + +impl SurfaceTexture { + pub fn present(self) { + self.value.into_inner().present(); + } +} + +impl TextureView { + /// Returns the [`TextureViewId`]. + #[inline] + pub fn id(&self) -> TextureViewId { + self.id + } +} + +impl From for TextureView { + fn from(value: wgpu::TextureView) -> Self { + TextureView { + id: TextureViewId::new(), + value: WgpuWrapper::new(value), + } + } +} + +impl From for SurfaceTexture { + fn from(value: wgpu::SurfaceTexture) -> Self { + SurfaceTexture { + value: WgpuWrapper::new(value), + } + } +} + +impl Deref for TextureView { + type Target = wgpu::TextureView; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl Deref for SurfaceTexture { + type Target = wgpu::SurfaceTexture; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +define_atomic_id!(SamplerId); + +/// A Sampler defines how a pipeline will sample from a [`TextureView`]. +/// They define image filters (including anisotropy) and address (wrapping) modes, among other things. +/// +/// May be converted from and dereferences to a wgpu [`Sampler`](wgpu::Sampler). +/// Can be created via [`RenderDevice::create_sampler`](crate::renderer::RenderDevice::create_sampler). +#[derive(Clone, Debug)] +pub struct Sampler { + id: SamplerId, + value: WgpuWrapper, +} + +impl Sampler { + /// Returns the [`SamplerId`]. + #[inline] + pub fn id(&self) -> SamplerId { + self.id + } +} + +impl From for Sampler { + fn from(value: wgpu::Sampler) -> Self { + Sampler { + id: SamplerId::new(), + value: WgpuWrapper::new(value), + } + } +} + +impl Deref for Sampler { + type Target = wgpu::Sampler; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +/// A rendering resource for the default image sampler which is set during renderer +/// initialization. +/// +/// The [`ImagePlugin`](bevy_image::ImagePlugin) can be set during app initialization to change the default +/// image sampler. +#[derive(Resource, Debug, Clone, Deref, DerefMut)] +pub struct DefaultImageSampler(pub(crate) Sampler); diff --git a/crates/libmarathon/src/render/render_resource/uniform_buffer.rs b/crates/libmarathon/src/render/render_resource/uniform_buffer.rs new file mode 100644 index 0000000..41efeef --- /dev/null +++ b/crates/libmarathon/src/render/render_resource/uniform_buffer.rs @@ -0,0 +1,402 @@ +use core::{marker::PhantomData, num::NonZero}; + +use crate::render::{ + render_resource::Buffer, + renderer::{RenderDevice, RenderQueue}, +}; +use encase::{ + internal::{AlignmentValue, BufferMut, WriteInto}, + DynamicUniformBuffer as DynamicUniformBufferWrapper, ShaderType, + UniformBuffer as UniformBufferWrapper, +}; +use wgpu::{ + util::BufferInitDescriptor, BindingResource, BufferBinding, BufferDescriptor, BufferUsages, +}; + +use super::IntoBinding; + +/// Stores data to be transferred to the GPU and made accessible to shaders as a uniform buffer. +/// +/// Uniform buffers are available to shaders on a read-only basis. Uniform buffers are commonly used to make available to shaders +/// parameters that are constant during shader execution, and are best used for data that is relatively small in size as they are +/// only guaranteed to support up to 16kB per binding. +/// +/// The contained data is stored in system RAM. [`write_buffer`](UniformBuffer::write_buffer) queues +/// copying of the data from system RAM to VRAM. Data in uniform buffers must follow [std140 alignment/padding requirements], +/// which is automatically enforced by this structure. Per the WGPU spec, uniform buffers cannot store runtime-sized array +/// (vectors), or structures with fields that are vectors. +/// +/// Other options for storing GPU-accessible data are: +/// * [`BufferVec`](crate::render_resource::BufferVec) +/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer) +/// * [`DynamicUniformBuffer`] +/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer) +/// * [`RawBufferVec`](crate::render_resource::RawBufferVec) +/// * [`StorageBuffer`](crate::render_resource::StorageBuffer) +/// * [`Texture`](crate::render_resource::Texture) +/// +/// [std140 alignment/padding requirements]: https://www.w3.org/TR/WGSL/#address-spaces-uniform +pub struct UniformBuffer { + value: T, + scratch: UniformBufferWrapper>, + buffer: Option, + label: Option, + changed: bool, + buffer_usage: BufferUsages, +} + +impl From for UniformBuffer { + fn from(value: T) -> Self { + Self { + value, + scratch: UniformBufferWrapper::new(Vec::new()), + buffer: None, + label: None, + changed: false, + buffer_usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM, + } + } +} + +impl Default for UniformBuffer { + fn default() -> Self { + Self { + value: T::default(), + scratch: UniformBufferWrapper::new(Vec::new()), + buffer: None, + label: None, + changed: false, + buffer_usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM, + } + } +} + +impl UniformBuffer { + #[inline] + pub fn buffer(&self) -> Option<&Buffer> { + self.buffer.as_ref() + } + + #[inline] + pub fn binding(&self) -> Option> { + Some(BindingResource::Buffer( + self.buffer()?.as_entire_buffer_binding(), + )) + } + + /// Set the data the buffer stores. + pub fn set(&mut self, value: T) { + self.value = value; + } + + pub fn get(&self) -> &T { + &self.value + } + + pub fn get_mut(&mut self) -> &mut T { + &mut self.value + } + + pub fn set_label(&mut self, label: Option<&str>) { + let label = label.map(str::to_string); + + if label != self.label { + self.changed = true; + } + + self.label = label; + } + + pub fn get_label(&self) -> Option<&str> { + self.label.as_deref() + } + + /// Add more [`BufferUsages`] to the buffer. + /// + /// This method only allows addition of flags to the default usage flags. + /// + /// The default values for buffer usage are `BufferUsages::COPY_DST` and `BufferUsages::UNIFORM`. + pub fn add_usages(&mut self, usage: BufferUsages) { + self.buffer_usage |= usage; + self.changed = true; + } + + /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`] + /// and the provided [`RenderQueue`], if a GPU-side backing buffer already exists. + /// + /// If a GPU-side buffer does not already exist for this data, such a buffer is initialized with currently + /// available data. + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + self.scratch.write(&self.value).unwrap(); + + if self.changed || self.buffer.is_none() { + self.buffer = Some(device.create_buffer_with_data(&BufferInitDescriptor { + label: self.label.as_deref(), + usage: self.buffer_usage, + contents: self.scratch.as_ref(), + })); + self.changed = false; + } else if let Some(buffer) = &self.buffer { + queue.write_buffer(buffer, 0, self.scratch.as_ref()); + } + } +} + +impl<'a, T: ShaderType + WriteInto> IntoBinding<'a> for &'a UniformBuffer { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + self.buffer() + .expect("Failed to get buffer") + .as_entire_buffer_binding() + .into_binding() + } +} + +/// Stores data to be transferred to the GPU and made accessible to shaders as a dynamic uniform buffer. +/// +/// Dynamic uniform buffers are available to shaders on a read-only basis. Dynamic uniform buffers are commonly used to make +/// available to shaders runtime-sized arrays of parameters that are otherwise constant during shader execution, and are best +/// suited to data that is relatively small in size as they are only guaranteed to support up to 16kB per binding. +/// +/// The contained data is stored in system RAM. [`write_buffer`](DynamicUniformBuffer::write_buffer) queues +/// copying of the data from system RAM to VRAM. Data in uniform buffers must follow [std140 alignment/padding requirements], +/// which is automatically enforced by this structure. Per the WGPU spec, uniform buffers cannot store runtime-sized array +/// (vectors), or structures with fields that are vectors. +/// +/// Other options for storing GPU-accessible data are: +/// * [`BufferVec`](crate::render_resource::BufferVec) +/// * [`DynamicStorageBuffer`](crate::render_resource::DynamicStorageBuffer) +/// * [`GpuArrayBuffer`](crate::render_resource::GpuArrayBuffer) +/// * [`RawBufferVec`](crate::render_resource::RawBufferVec) +/// * [`StorageBuffer`](crate::render_resource::StorageBuffer) +/// * [`Texture`](crate::render_resource::Texture) +/// * [`UniformBuffer`] +/// +/// [std140 alignment/padding requirements]: https://www.w3.org/TR/WGSL/#address-spaces-uniform +pub struct DynamicUniformBuffer { + scratch: DynamicUniformBufferWrapper>, + buffer: Option, + label: Option, + changed: bool, + buffer_usage: BufferUsages, + _marker: PhantomData T>, +} + +impl Default for DynamicUniformBuffer { + fn default() -> Self { + Self { + scratch: DynamicUniformBufferWrapper::new(Vec::new()), + buffer: None, + label: None, + changed: false, + buffer_usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM, + _marker: PhantomData, + } + } +} + +impl DynamicUniformBuffer { + pub fn new_with_alignment(alignment: u64) -> Self { + Self { + scratch: DynamicUniformBufferWrapper::new_with_alignment(Vec::new(), alignment), + buffer: None, + label: None, + changed: false, + buffer_usage: BufferUsages::COPY_DST | BufferUsages::UNIFORM, + _marker: PhantomData, + } + } + + #[inline] + pub fn buffer(&self) -> Option<&Buffer> { + self.buffer.as_ref() + } + + #[inline] + pub fn binding(&self) -> Option> { + Some(BindingResource::Buffer(BufferBinding { + buffer: self.buffer()?, + offset: 0, + size: Some(T::min_size()), + })) + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.scratch.as_ref().is_empty() + } + + /// Push data into the `DynamicUniformBuffer`'s internal vector (residing on system RAM). + #[inline] + pub fn push(&mut self, value: &T) -> u32 { + self.scratch.write(value).unwrap() as u32 + } + + pub fn set_label(&mut self, label: Option<&str>) { + let label = label.map(str::to_string); + + if label != self.label { + self.changed = true; + } + + self.label = label; + } + + pub fn get_label(&self) -> Option<&str> { + self.label.as_deref() + } + + /// Add more [`BufferUsages`] to the buffer. + /// + /// This method only allows addition of flags to the default usage flags. + /// + /// The default values for buffer usage are `BufferUsages::COPY_DST` and `BufferUsages::UNIFORM`. + pub fn add_usages(&mut self, usage: BufferUsages) { + self.buffer_usage |= usage; + self.changed = true; + } + + /// Creates a writer that can be used to directly write elements into the target buffer. + /// + /// This method uses less memory and performs fewer memory copies using over [`push`] and [`write_buffer`]. + /// + /// `max_count` *must* be greater than or equal to the number of elements that are to be written to the buffer, or + /// the writer will panic while writing. Dropping the writer will schedule the buffer write into the provided + /// [`RenderQueue`]. + /// + /// If there is no GPU-side buffer allocated to hold the data currently stored, or if a GPU-side buffer previously + /// allocated does not have enough capacity to hold `max_count` elements, a new GPU-side buffer is created. + /// + /// Returns `None` if there is no allocated GPU-side buffer, and `max_count` is 0. + /// + /// [`push`]: Self::push + /// [`write_buffer`]: Self::write_buffer + #[inline] + pub fn get_writer<'a>( + &'a mut self, + max_count: usize, + device: &RenderDevice, + queue: &'a RenderQueue, + ) -> Option> { + let alignment = if cfg!(target_abi = "sim") { + // On iOS simulator on silicon macs, metal validation check that the host OS alignment + // is respected, but the device reports the correct value for iOS, which is smaller. + // Use the larger value. + // See https://github.com/gfx-rs/wgpu/issues/7057 - remove if it's not needed anymore. + AlignmentValue::new(256) + } else { + AlignmentValue::new(device.limits().min_uniform_buffer_offset_alignment as u64) + }; + + let mut capacity = self.buffer.as_deref().map(wgpu::Buffer::size).unwrap_or(0); + let size = alignment + .round_up(T::min_size().get()) + .checked_mul(max_count as u64) + .unwrap(); + + if capacity < size || (self.changed && size > 0) { + let buffer = device.create_buffer(&BufferDescriptor { + label: self.label.as_deref(), + usage: self.buffer_usage, + size, + mapped_at_creation: false, + }); + capacity = buffer.size(); + self.buffer = Some(buffer); + self.changed = false; + } + + if let Some(buffer) = self.buffer.as_deref() { + let buffer_view = queue + .write_buffer_with(buffer, 0, NonZero::::new(buffer.size())?) + .unwrap(); + Some(DynamicUniformBufferWriter { + buffer: encase::DynamicUniformBuffer::new_with_alignment( + QueueWriteBufferViewWrapper { + capacity: capacity as usize, + buffer_view, + }, + alignment.get(), + ), + _marker: PhantomData, + }) + } else { + None + } + } + + /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`] + /// and the provided [`RenderQueue`]. + /// + /// If there is no GPU-side buffer allocated to hold the data currently stored, or if a GPU-side buffer previously + /// allocated does not have enough capacity, a new GPU-side buffer is created. + #[inline] + pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { + let capacity = self.buffer.as_deref().map(wgpu::Buffer::size).unwrap_or(0); + let size = self.scratch.as_ref().len() as u64; + + if capacity < size || (self.changed && size > 0) { + self.buffer = Some(device.create_buffer_with_data(&BufferInitDescriptor { + label: self.label.as_deref(), + usage: self.buffer_usage, + contents: self.scratch.as_ref(), + })); + self.changed = false; + } else if let Some(buffer) = &self.buffer { + queue.write_buffer(buffer, 0, self.scratch.as_ref()); + } + } + + #[inline] + pub fn clear(&mut self) { + self.scratch.as_mut().clear(); + self.scratch.set_offset(0); + } +} + +/// A writer that can be used to directly write elements into the target buffer. +/// +/// For more information, see [`DynamicUniformBuffer::get_writer`]. +pub struct DynamicUniformBufferWriter<'a, T> { + buffer: encase::DynamicUniformBuffer>, + _marker: PhantomData T>, +} + +impl<'a, T: ShaderType + WriteInto> DynamicUniformBufferWriter<'a, T> { + pub fn write(&mut self, value: &T) -> u32 { + self.buffer.write(value).unwrap() as u32 + } +} + +/// A wrapper to work around the orphan rule so that [`wgpu::QueueWriteBufferView`] can implement +/// [`BufferMut`]. +struct QueueWriteBufferViewWrapper<'a> { + buffer_view: wgpu::QueueWriteBufferView<'a>, + // Must be kept separately and cannot be retrieved from buffer_view, as the read-only access will + // invoke a panic. + capacity: usize, +} + +impl<'a> BufferMut for QueueWriteBufferViewWrapper<'a> { + #[inline] + fn capacity(&self) -> usize { + self.capacity + } + + #[inline] + fn write(&mut self, offset: usize, val: &[u8; N]) { + self.buffer_view.write(offset, val); + } + + #[inline] + fn write_slice(&mut self, offset: usize, val: &[u8]) { + self.buffer_view.write_slice(offset, val); + } +} + +impl<'a, T: ShaderType + WriteInto> IntoBinding<'a> for &'a DynamicUniformBuffer { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + self.binding().unwrap() + } +} diff --git a/crates/libmarathon/src/render/renderer/graph_runner.rs b/crates/libmarathon/src/render/renderer/graph_runner.rs new file mode 100644 index 0000000..3c4ffda --- /dev/null +++ b/crates/libmarathon/src/render/renderer/graph_runner.rs @@ -0,0 +1,267 @@ +use bevy_ecs::{prelude::Entity, world::World}; +use bevy_platform::collections::HashMap; +#[cfg(feature = "trace")] +use tracing::info_span; + +use std::{borrow::Cow, collections::VecDeque}; +use smallvec::{smallvec, SmallVec}; +use thiserror::Error; + +use crate::render::{ + diagnostic::internal::{DiagnosticsRecorder, RenderDiagnosticsMutex}, + render_graph::{ + Edge, InternedRenderLabel, InternedRenderSubGraph, NodeRunError, NodeState, RenderGraph, + RenderGraphContext, SlotLabel, SlotType, SlotValue, + }, + renderer::{RenderContext, RenderDevice}, +}; + +/// The [`RenderGraphRunner`] is responsible for executing a [`RenderGraph`]. +/// +/// It will run all nodes in the graph sequentially in the correct order (defined by the edges). +/// Each [`Node`](crate::render_graph::Node) can run any arbitrary code, but will generally +/// either send directly a [`CommandBuffer`] or a task that will asynchronously generate a [`CommandBuffer`] +/// +/// After running the graph, the [`RenderGraphRunner`] will execute in parallel all the tasks to get +/// an ordered list of [`CommandBuffer`]s to execute. These [`CommandBuffer`] will be submitted to the GPU +/// sequentially in the order that the tasks were submitted. (which is the order of the [`RenderGraph`]) +/// +/// [`CommandBuffer`]: wgpu::CommandBuffer +pub(crate) struct RenderGraphRunner; + +#[derive(Error, Debug)] +pub enum RenderGraphRunnerError { + #[error(transparent)] + NodeRunError(#[from] NodeRunError), + #[error("node output slot not set (index {slot_index}, name {slot_name})")] + EmptyNodeOutputSlot { + type_name: &'static str, + slot_index: usize, + slot_name: Cow<'static, str>, + }, + #[error("graph '{sub_graph:?}' could not be run because slot '{slot_name}' at index {slot_index} has no value")] + MissingInput { + slot_index: usize, + slot_name: Cow<'static, str>, + sub_graph: Option, + }, + #[error("attempted to use the wrong type for input slot")] + MismatchedInputSlotType { + slot_index: usize, + label: SlotLabel, + expected: SlotType, + actual: SlotType, + }, + #[error( + "node (name: '{node_name:?}') has {slot_count} input slots, but was provided {value_count} values" + )] + MismatchedInputCount { + node_name: InternedRenderLabel, + slot_count: usize, + value_count: usize, + }, +} + +impl RenderGraphRunner { + pub fn run( + graph: &RenderGraph, + render_device: RenderDevice, + mut diagnostics_recorder: Option, + queue: &wgpu::Queue, + world: &World, + finalizer: impl FnOnce(&mut wgpu::CommandEncoder), + ) -> Result, RenderGraphRunnerError> { + if let Some(recorder) = &mut diagnostics_recorder { + recorder.begin_frame(); + } + + let mut render_context = RenderContext::new(render_device, diagnostics_recorder); + Self::run_graph(graph, None, &mut render_context, world, &[], None)?; + finalizer(render_context.command_encoder()); + + let (render_device, mut diagnostics_recorder) = { + let (commands, render_device, diagnostics_recorder) = render_context.finish(); + + #[cfg(feature = "trace")] + let _span = info_span!("submit_graph_commands").entered(); + queue.submit(commands); + + (render_device, diagnostics_recorder) + }; + + if let Some(recorder) = &mut diagnostics_recorder { + let render_diagnostics_mutex = world.resource::().0.clone(); + recorder.finish_frame(&render_device, move |diagnostics| { + *render_diagnostics_mutex.lock().expect("lock poisoned") = Some(diagnostics); + }); + } + + Ok(diagnostics_recorder) + } + + /// Runs the [`RenderGraph`] and all its sub-graphs sequentially, making sure that all nodes are + /// run in the correct order. (a node only runs when all its dependencies have finished running) + fn run_graph<'w>( + graph: &RenderGraph, + sub_graph: Option, + render_context: &mut RenderContext<'w>, + world: &'w World, + inputs: &[SlotValue], + view_entity: Option, + ) -> Result<(), RenderGraphRunnerError> { + let mut node_outputs: HashMap> = + HashMap::default(); + #[cfg(feature = "trace")] + let span = if let Some(label) = &sub_graph { + info_span!("run_graph", name = format!("{label:?}")) + } else { + info_span!("run_graph", name = "main_graph") + }; + #[cfg(feature = "trace")] + let _guard = span.enter(); + + // Queue up nodes without inputs, which can be run immediately + let mut node_queue: VecDeque<&NodeState> = graph + .iter_nodes() + .filter(|node| node.input_slots.is_empty()) + .collect(); + + // pass inputs into the graph + if let Some(input_node) = graph.get_input_node() { + let mut input_values: SmallVec<[SlotValue; 4]> = SmallVec::new(); + for (i, input_slot) in input_node.input_slots.iter().enumerate() { + if let Some(input_value) = inputs.get(i) { + if input_slot.slot_type != input_value.slot_type() { + return Err(RenderGraphRunnerError::MismatchedInputSlotType { + slot_index: i, + actual: input_value.slot_type(), + expected: input_slot.slot_type, + label: input_slot.name.clone().into(), + }); + } + input_values.push(input_value.clone()); + } else { + return Err(RenderGraphRunnerError::MissingInput { + slot_index: i, + slot_name: input_slot.name.clone(), + sub_graph, + }); + } + } + + node_outputs.insert(input_node.label, input_values); + + for (_, node_state) in graph + .iter_node_outputs(input_node.label) + .expect("node exists") + { + node_queue.push_front(node_state); + } + } + + 'handle_node: while let Some(node_state) = node_queue.pop_back() { + // skip nodes that are already processed + if node_outputs.contains_key(&node_state.label) { + continue; + } + + let mut slot_indices_and_inputs: SmallVec<[(usize, SlotValue); 4]> = SmallVec::new(); + // check if all dependencies have finished running + for (edge, input_node) in graph + .iter_node_inputs(node_state.label) + .expect("node is in graph") + { + match edge { + Edge::SlotEdge { + output_index, + input_index, + .. + } => { + if let Some(outputs) = node_outputs.get(&input_node.label) { + slot_indices_and_inputs + .push((*input_index, outputs[*output_index].clone())); + } else { + node_queue.push_front(node_state); + continue 'handle_node; + } + } + Edge::NodeEdge { .. } => { + if !node_outputs.contains_key(&input_node.label) { + node_queue.push_front(node_state); + continue 'handle_node; + } + } + } + } + + // construct final sorted input list + slot_indices_and_inputs.sort_by_key(|(index, _)| *index); + let inputs: SmallVec<[SlotValue; 4]> = slot_indices_and_inputs + .into_iter() + .map(|(_, value)| value) + .collect(); + + if inputs.len() != node_state.input_slots.len() { + return Err(RenderGraphRunnerError::MismatchedInputCount { + node_name: node_state.label, + slot_count: node_state.input_slots.len(), + value_count: inputs.len(), + }); + } + + let mut outputs: SmallVec<[Option; 4]> = + smallvec![None; node_state.output_slots.len()]; + { + let mut context = RenderGraphContext::new(graph, node_state, &inputs, &mut outputs); + if let Some(view_entity) = view_entity { + context.set_view_entity(view_entity); + } + + { + #[cfg(feature = "trace")] + let _span = info_span!("node", name = node_state.type_name).entered(); + + node_state.node.run(&mut context, render_context, world)?; + } + + for run_sub_graph in context.finish() { + let sub_graph = graph + .get_sub_graph(run_sub_graph.sub_graph) + .expect("sub graph exists because it was validated when queued."); + Self::run_graph( + sub_graph, + Some(run_sub_graph.sub_graph), + render_context, + world, + &run_sub_graph.inputs, + run_sub_graph.view_entity, + )?; + } + } + + let mut values: SmallVec<[SlotValue; 4]> = SmallVec::new(); + for (i, output) in outputs.into_iter().enumerate() { + if let Some(value) = output { + values.push(value); + } else { + let empty_slot = node_state.output_slots.get_slot(i).unwrap(); + return Err(RenderGraphRunnerError::EmptyNodeOutputSlot { + type_name: node_state.type_name, + slot_index: i, + slot_name: empty_slot.name.clone(), + }); + } + } + node_outputs.insert(node_state.label, values); + + for (_, node_state) in graph + .iter_node_outputs(node_state.label) + .expect("node exists") + { + node_queue.push_front(node_state); + } + } + + Ok(()) + } +} diff --git a/crates/libmarathon/src/render/renderer/mod.rs b/crates/libmarathon/src/render/renderer/mod.rs new file mode 100644 index 0000000..b003c64 --- /dev/null +++ b/crates/libmarathon/src/render/renderer/mod.rs @@ -0,0 +1,662 @@ +mod graph_runner; +#[cfg(feature = "raw_vulkan_init")] +pub mod raw_vulkan_init; +mod render_device; +mod wgpu_wrapper; + +pub use graph_runner::*; +pub use render_device::*; +pub use wgpu_wrapper::WgpuWrapper; + +use crate::render::{ + diagnostic::{internal::DiagnosticsRecorder, RecordDiagnostics}, + render_graph::RenderGraph, + render_phase::TrackedRenderPass, + render_resource::RenderPassDescriptor, + settings::{RenderResources, WgpuSettings, WgpuSettingsPriority}, + view::{ExtractedWindows, ViewTarget}, +}; +use std::sync::Arc; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{prelude::*, system::SystemState}; +use bevy_platform::time::Instant; +use bevy_time::TimeSender; +use bevy_window::RawHandleWrapperHolder; +use tracing::{debug, error, info, info_span, warn}; +use wgpu::{ + Adapter, AdapterInfo, Backends, CommandBuffer, CommandEncoder, DeviceType, Instance, Queue, + RequestAdapterOptions, Trace, +}; + +/// Updates the [`RenderGraph`] with all of its nodes and then runs it to render the entire frame. +pub fn render_system(world: &mut World, state: &mut SystemState>>) { + world.resource_scope(|world, mut graph: Mut| { + graph.update(world); + }); + + let diagnostics_recorder = world.remove_resource::(); + + let graph = world.resource::(); + let render_device = world.resource::(); + let render_queue = world.resource::(); + + let res = RenderGraphRunner::run( + graph, + render_device.clone(), // TODO: is this clone really necessary? + diagnostics_recorder, + &render_queue.0, + world, + |encoder| { + crate::render::view::screenshot::submit_screenshot_commands(world, encoder); + crate::render::gpu_readback::submit_readback_commands(world, encoder); + }, + ); + + match res { + Ok(Some(diagnostics_recorder)) => { + world.insert_resource(diagnostics_recorder); + } + Ok(None) => {} + Err(e) => { + error!("Error running render graph:"); + { + let mut src: &dyn core::error::Error = &e; + loop { + error!("> {}", src); + match src.source() { + Some(s) => src = s, + None => break, + } + } + } + + panic!("Error running render graph: {e}"); + } + } + + { + let _span = info_span!("present_frames").entered(); + + // Remove ViewTarget components to ensure swap chain TextureViews are dropped. + // If all TextureViews aren't dropped before present, acquiring the next swap chain texture will fail. + let view_entities = state.get(world).iter().collect::>(); + for view_entity in view_entities { + world.entity_mut(view_entity).remove::(); + } + + let mut windows = world.resource_mut::(); + for window in windows.values_mut() { + if let Some(surface_texture) = window.swap_chain_texture.take() { + // TODO(clean): winit docs recommends calling pre_present_notify before this. + // though `present()` doesn't present the frame, it schedules it to be presented + // by wgpu. + // https://docs.rs/winit/0.29.9/wasm32-unknown-unknown/winit/window/struct.Window.html#method.pre_present_notify + surface_texture.present(); + } + } + + #[cfg(feature = "tracing-tracy")] + tracing::event!( + tracing::Level::INFO, + message = "finished frame", + tracy.frame_mark = true + ); + } + + crate::render::view::screenshot::collect_screenshots(world); + + // update the time and send it to the app world + let time_sender = world.resource::(); + if let Err(error) = time_sender.0.try_send(Instant::now()) { + match error { + bevy_time::TrySendError::Full(_) => { + panic!("The TimeSender channel should always be empty during render. You might need to add the bevy::core::time_system to your app.",); + } + bevy_time::TrySendError::Disconnected(_) => { + // ignore disconnected errors, the main world probably just got dropped during shutdown + } + } + } +} + +/// This queue is used to enqueue tasks for the GPU to execute asynchronously. +#[derive(Resource, Clone, Deref, DerefMut)] +pub struct RenderQueue(pub Arc>); + +/// The handle to the physical device being used for rendering. +/// See [`Adapter`] for more info. +#[derive(Resource, Clone, Debug, Deref, DerefMut)] +pub struct RenderAdapter(pub Arc>); + +/// The GPU instance is used to initialize the [`RenderQueue`] and [`RenderDevice`], +/// as well as to create [`WindowSurfaces`](crate::view::window::WindowSurfaces). +#[derive(Resource, Clone, Deref, DerefMut)] +pub struct RenderInstance(pub Arc>); + +/// The [`AdapterInfo`] of the adapter in use by the renderer. +#[derive(Resource, Clone, Deref, DerefMut)] +pub struct RenderAdapterInfo(pub WgpuWrapper); + +const GPU_NOT_FOUND_ERROR_MESSAGE: &str = if cfg!(target_os = "linux") { + "Unable to find a GPU! Make sure you have installed required drivers! For extra information, see: https://github.com/bevyengine/bevy/blob/latest/docs/linux_dependencies.md" +} else { + "Unable to find a GPU! Make sure you have installed required drivers!" +}; + +#[cfg(not(target_family = "wasm"))] +fn find_adapter_by_name( + instance: &Instance, + options: &WgpuSettings, + compatible_surface: Option<&wgpu::Surface<'_>>, + adapter_name: &str, +) -> Option { + for adapter in + instance.enumerate_adapters(options.backends.expect( + "The `backends` field of `WgpuSettings` must be set to use a specific adapter.", + )) + { + tracing::trace!("Checking adapter: {:?}", adapter.get_info()); + let info = adapter.get_info(); + if let Some(surface) = compatible_surface + && !adapter.is_surface_supported(surface) + { + continue; + } + + if info.name.eq_ignore_ascii_case(adapter_name) { + return Some(adapter); + } + } + None +} + +/// Initializes the renderer by retrieving and preparing the GPU instance, device and queue +/// for the specified backend. +pub async fn initialize_renderer( + backends: Backends, + primary_window: Option, + options: &WgpuSettings, + #[cfg(feature = "raw_vulkan_init")] + raw_vulkan_init_settings: raw_vulkan_init::RawVulkanInitSettings, +) -> RenderResources { + let instance_descriptor = wgpu::InstanceDescriptor { + backends, + flags: options.instance_flags, + memory_budget_thresholds: options.instance_memory_budget_thresholds, + backend_options: wgpu::BackendOptions { + gl: wgpu::GlBackendOptions { + gles_minor_version: options.gles3_minor_version, + fence_behavior: wgpu::GlFenceBehavior::Normal, + }, + dx12: wgpu::Dx12BackendOptions { + shader_compiler: options.dx12_shader_compiler.clone(), + }, + noop: wgpu::NoopBackendOptions { enable: false }, + }, + }; + + #[cfg(not(feature = "raw_vulkan_init"))] + let instance = Instance::new(&instance_descriptor); + #[cfg(feature = "raw_vulkan_init")] + let mut additional_vulkan_features = raw_vulkan_init::AdditionalVulkanFeatures::default(); + #[cfg(feature = "raw_vulkan_init")] + let instance = raw_vulkan_init::create_raw_vulkan_instance( + &instance_descriptor, + &raw_vulkan_init_settings, + &mut additional_vulkan_features, + ); + + let surface = primary_window.and_then(|wrapper| { + let maybe_handle = wrapper + .0 + .lock() + .expect("Couldn't get the window handle in time for renderer initialization"); + if let Some(wrapper) = maybe_handle.as_ref() { + // SAFETY: Plugins should be set up on the main thread. + let handle = unsafe { wrapper.get_handle() }; + Some( + instance + .create_surface(handle) + .expect("Failed to create wgpu surface"), + ) + } else { + None + } + }); + + let force_fallback_adapter = std::env::var("WGPU_FORCE_FALLBACK_ADAPTER") + .map_or(options.force_fallback_adapter, |v| { + !(v.is_empty() || v == "0" || v == "false") + }); + + let desired_adapter_name = std::env::var("WGPU_ADAPTER_NAME") + .as_deref() + .map_or(options.adapter_name.clone(), |x| Some(x.to_lowercase())); + + let request_adapter_options = RequestAdapterOptions { + power_preference: options.power_preference, + compatible_surface: surface.as_ref(), + force_fallback_adapter, + }; + + #[cfg(not(target_family = "wasm"))] + let mut selected_adapter = desired_adapter_name.and_then(|adapter_name| { + find_adapter_by_name( + &instance, + options, + request_adapter_options.compatible_surface, + &adapter_name, + ) + }); + #[cfg(target_family = "wasm")] + let mut selected_adapter = None; + + #[cfg(target_family = "wasm")] + if desired_adapter_name.is_some() { + warn!("Choosing an adapter is not supported on wasm."); + } + + if selected_adapter.is_none() { + debug!( + "Searching for adapter with options: {:?}", + request_adapter_options + ); + selected_adapter = instance + .request_adapter(&request_adapter_options) + .await + .ok(); + } + + let adapter = selected_adapter.expect(GPU_NOT_FOUND_ERROR_MESSAGE); + let adapter_info = adapter.get_info(); + info!("{:?}", adapter_info); + + if adapter_info.device_type == DeviceType::Cpu { + warn!( + "The selected adapter is using a driver that only supports software rendering. \ + This is likely to be very slow. See https://bevy.org/learn/errors/b0006/" + ); + } + + // Maybe get features and limits based on what is supported by the adapter/backend + let mut features = wgpu::Features::empty(); + let mut limits = options.limits.clone(); + if matches!(options.priority, WgpuSettingsPriority::Functionality) { + features = adapter.features(); + if adapter_info.device_type == DeviceType::DiscreteGpu { + // `MAPPABLE_PRIMARY_BUFFERS` can have a significant, negative performance impact for + // discrete GPUs due to having to transfer data across the PCI-E bus and so it + // should not be automatically enabled in this case. It is however beneficial for + // integrated GPUs. + features.remove(wgpu::Features::MAPPABLE_PRIMARY_BUFFERS); + } + + limits = adapter.limits(); + } + + // Enforce the disabled features + if let Some(disabled_features) = options.disabled_features { + features.remove(disabled_features); + } + // NOTE: |= is used here to ensure that any explicitly-enabled features are respected. + features |= options.features; + + // Enforce the limit constraints + if let Some(constrained_limits) = options.constrained_limits.as_ref() { + // NOTE: Respect the configured limits as an 'upper bound'. This means for 'max' limits, we + // take the minimum of the calculated limits according to the adapter/backend and the + // specified max_limits. For 'min' limits, take the maximum instead. This is intended to + // err on the side of being conservative. We can't claim 'higher' limits that are supported + // but we can constrain to 'lower' limits. + limits = wgpu::Limits { + max_texture_dimension_1d: limits + .max_texture_dimension_1d + .min(constrained_limits.max_texture_dimension_1d), + max_texture_dimension_2d: limits + .max_texture_dimension_2d + .min(constrained_limits.max_texture_dimension_2d), + max_texture_dimension_3d: limits + .max_texture_dimension_3d + .min(constrained_limits.max_texture_dimension_3d), + max_texture_array_layers: limits + .max_texture_array_layers + .min(constrained_limits.max_texture_array_layers), + max_bind_groups: limits + .max_bind_groups + .min(constrained_limits.max_bind_groups), + max_dynamic_uniform_buffers_per_pipeline_layout: limits + .max_dynamic_uniform_buffers_per_pipeline_layout + .min(constrained_limits.max_dynamic_uniform_buffers_per_pipeline_layout), + max_dynamic_storage_buffers_per_pipeline_layout: limits + .max_dynamic_storage_buffers_per_pipeline_layout + .min(constrained_limits.max_dynamic_storage_buffers_per_pipeline_layout), + max_sampled_textures_per_shader_stage: limits + .max_sampled_textures_per_shader_stage + .min(constrained_limits.max_sampled_textures_per_shader_stage), + max_samplers_per_shader_stage: limits + .max_samplers_per_shader_stage + .min(constrained_limits.max_samplers_per_shader_stage), + max_storage_buffers_per_shader_stage: limits + .max_storage_buffers_per_shader_stage + .min(constrained_limits.max_storage_buffers_per_shader_stage), + max_storage_textures_per_shader_stage: limits + .max_storage_textures_per_shader_stage + .min(constrained_limits.max_storage_textures_per_shader_stage), + max_uniform_buffers_per_shader_stage: limits + .max_uniform_buffers_per_shader_stage + .min(constrained_limits.max_uniform_buffers_per_shader_stage), + max_binding_array_elements_per_shader_stage: limits + .max_binding_array_elements_per_shader_stage + .min(constrained_limits.max_binding_array_elements_per_shader_stage), + max_binding_array_sampler_elements_per_shader_stage: limits + .max_binding_array_sampler_elements_per_shader_stage + .min(constrained_limits.max_binding_array_sampler_elements_per_shader_stage), + max_uniform_buffer_binding_size: limits + .max_uniform_buffer_binding_size + .min(constrained_limits.max_uniform_buffer_binding_size), + max_storage_buffer_binding_size: limits + .max_storage_buffer_binding_size + .min(constrained_limits.max_storage_buffer_binding_size), + max_vertex_buffers: limits + .max_vertex_buffers + .min(constrained_limits.max_vertex_buffers), + max_vertex_attributes: limits + .max_vertex_attributes + .min(constrained_limits.max_vertex_attributes), + max_vertex_buffer_array_stride: limits + .max_vertex_buffer_array_stride + .min(constrained_limits.max_vertex_buffer_array_stride), + max_push_constant_size: limits + .max_push_constant_size + .min(constrained_limits.max_push_constant_size), + min_uniform_buffer_offset_alignment: limits + .min_uniform_buffer_offset_alignment + .max(constrained_limits.min_uniform_buffer_offset_alignment), + min_storage_buffer_offset_alignment: limits + .min_storage_buffer_offset_alignment + .max(constrained_limits.min_storage_buffer_offset_alignment), + max_inter_stage_shader_components: limits + .max_inter_stage_shader_components + .min(constrained_limits.max_inter_stage_shader_components), + max_compute_workgroup_storage_size: limits + .max_compute_workgroup_storage_size + .min(constrained_limits.max_compute_workgroup_storage_size), + max_compute_invocations_per_workgroup: limits + .max_compute_invocations_per_workgroup + .min(constrained_limits.max_compute_invocations_per_workgroup), + max_compute_workgroup_size_x: limits + .max_compute_workgroup_size_x + .min(constrained_limits.max_compute_workgroup_size_x), + max_compute_workgroup_size_y: limits + .max_compute_workgroup_size_y + .min(constrained_limits.max_compute_workgroup_size_y), + max_compute_workgroup_size_z: limits + .max_compute_workgroup_size_z + .min(constrained_limits.max_compute_workgroup_size_z), + max_compute_workgroups_per_dimension: limits + .max_compute_workgroups_per_dimension + .min(constrained_limits.max_compute_workgroups_per_dimension), + max_buffer_size: limits + .max_buffer_size + .min(constrained_limits.max_buffer_size), + max_bindings_per_bind_group: limits + .max_bindings_per_bind_group + .min(constrained_limits.max_bindings_per_bind_group), + max_non_sampler_bindings: limits + .max_non_sampler_bindings + .min(constrained_limits.max_non_sampler_bindings), + max_blas_primitive_count: limits + .max_blas_primitive_count + .min(constrained_limits.max_blas_primitive_count), + max_blas_geometry_count: limits + .max_blas_geometry_count + .min(constrained_limits.max_blas_geometry_count), + max_tlas_instance_count: limits + .max_tlas_instance_count + .min(constrained_limits.max_tlas_instance_count), + max_color_attachments: limits + .max_color_attachments + .min(constrained_limits.max_color_attachments), + max_color_attachment_bytes_per_sample: limits + .max_color_attachment_bytes_per_sample + .min(constrained_limits.max_color_attachment_bytes_per_sample), + min_subgroup_size: limits + .min_subgroup_size + .max(constrained_limits.min_subgroup_size), + max_subgroup_size: limits + .max_subgroup_size + .min(constrained_limits.max_subgroup_size), + max_acceleration_structures_per_shader_stage: 0, + }; + } + + let device_descriptor = wgpu::DeviceDescriptor { + label: options.device_label.as_ref().map(AsRef::as_ref), + required_features: features, + required_limits: limits, + memory_hints: options.memory_hints.clone(), + // See https://github.com/gfx-rs/wgpu/issues/5974 + trace: Trace::Off, + }; + + #[cfg(not(feature = "raw_vulkan_init"))] + let (device, queue) = adapter.request_device(&device_descriptor).await.unwrap(); + + #[cfg(feature = "raw_vulkan_init")] + let (device, queue) = raw_vulkan_init::create_raw_device( + &adapter, + &device_descriptor, + &raw_vulkan_init_settings, + &mut additional_vulkan_features, + ) + .await + .unwrap(); + + debug!("Configured wgpu adapter Limits: {:#?}", device.limits()); + debug!("Configured wgpu adapter Features: {:#?}", device.features()); + + RenderResources( + RenderDevice::from(device), + RenderQueue(Arc::new(WgpuWrapper::new(queue))), + RenderAdapterInfo(WgpuWrapper::new(adapter_info)), + RenderAdapter(Arc::new(WgpuWrapper::new(adapter))), + RenderInstance(Arc::new(WgpuWrapper::new(instance))), + #[cfg(feature = "raw_vulkan_init")] + additional_vulkan_features, + ) +} + +/// The context with all information required to interact with the GPU. +/// +/// The [`RenderDevice`] is used to create render resources and the +/// the [`CommandEncoder`] is used to record a series of GPU operations. +pub struct RenderContext<'w> { + render_device: RenderDevice, + command_encoder: Option, + command_buffer_queue: Vec>, + diagnostics_recorder: Option>, +} + +impl<'w> RenderContext<'w> { + /// Creates a new [`RenderContext`] from a [`RenderDevice`]. + pub fn new( + render_device: RenderDevice, + diagnostics_recorder: Option, + ) -> Self { + Self { + render_device, + command_encoder: None, + command_buffer_queue: Vec::new(), + diagnostics_recorder: diagnostics_recorder.map(Arc::new), + } + } + + /// Gets the underlying [`RenderDevice`]. + pub fn render_device(&self) -> &RenderDevice { + &self.render_device + } + + /// Gets the diagnostics recorder, used to track elapsed time and pipeline statistics + /// of various render and compute passes. + pub fn diagnostic_recorder(&self) -> impl RecordDiagnostics + use<> { + self.diagnostics_recorder.clone() + } + + /// Gets the current [`CommandEncoder`]. + pub fn command_encoder(&mut self) -> &mut CommandEncoder { + self.command_encoder.get_or_insert_with(|| { + self.render_device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()) + }) + } + + pub(crate) fn has_commands(&mut self) -> bool { + self.command_encoder.is_some() || !self.command_buffer_queue.is_empty() + } + + /// Creates a new [`TrackedRenderPass`] for the context, + /// configured using the provided `descriptor`. + pub fn begin_tracked_render_pass<'a>( + &'a mut self, + descriptor: RenderPassDescriptor<'_>, + ) -> TrackedRenderPass<'a> { + // Cannot use command_encoder() as we need to split the borrow on self + let command_encoder = self.command_encoder.get_or_insert_with(|| { + self.render_device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()) + }); + + let render_pass = command_encoder.begin_render_pass(&descriptor); + TrackedRenderPass::new(&self.render_device, render_pass) + } + + /// Append a [`CommandBuffer`] to the command buffer queue. + /// + /// If present, this will flush the currently unflushed [`CommandEncoder`] + /// into a [`CommandBuffer`] into the queue before appending the provided + /// buffer. + pub fn add_command_buffer(&mut self, command_buffer: CommandBuffer) { + self.flush_encoder(); + + self.command_buffer_queue + .push(QueuedCommandBuffer::Ready(command_buffer)); + } + + /// Append a function that will generate a [`CommandBuffer`] to the + /// command buffer queue, to be ran later. + /// + /// If present, this will flush the currently unflushed [`CommandEncoder`] + /// into a [`CommandBuffer`] into the queue before appending the provided + /// buffer. + pub fn add_command_buffer_generation_task( + &mut self, + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + task: impl FnOnce(RenderDevice) -> CommandBuffer + 'w + Send, + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + task: impl FnOnce(RenderDevice) -> CommandBuffer + 'w, + ) { + self.flush_encoder(); + + self.command_buffer_queue + .push(QueuedCommandBuffer::Task(Box::new(task))); + } + + /// Finalizes and returns the queue of [`CommandBuffer`]s. + /// + /// This function will wait until all command buffer generation tasks are complete + /// by running them in parallel (where supported). + /// + /// The [`CommandBuffer`]s will be returned in the order that they were added. + pub fn finish( + mut self, + ) -> ( + Vec, + RenderDevice, + Option, + ) { + self.flush_encoder(); + + let mut command_buffers = Vec::with_capacity(self.command_buffer_queue.len()); + + #[cfg(feature = "trace")] + let _command_buffer_generation_tasks_span = + info_span!("command_buffer_generation_tasks").entered(); + + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + { + let mut task_based_command_buffers = + bevy_tasks::ComputeTaskPool::get().scope(|task_pool| { + for (i, queued_command_buffer) in + self.command_buffer_queue.into_iter().enumerate() + { + match queued_command_buffer { + QueuedCommandBuffer::Ready(command_buffer) => { + command_buffers.push((i, command_buffer)); + } + QueuedCommandBuffer::Task(command_buffer_generation_task) => { + let render_device = self.render_device.clone(); + task_pool.spawn(async move { + (i, command_buffer_generation_task(render_device)) + }); + } + } + } + }); + command_buffers.append(&mut task_based_command_buffers); + } + + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + for (i, queued_command_buffer) in self.command_buffer_queue.into_iter().enumerate() { + match queued_command_buffer { + QueuedCommandBuffer::Ready(command_buffer) => { + command_buffers.push((i, command_buffer)); + } + QueuedCommandBuffer::Task(command_buffer_generation_task) => { + let render_device = self.render_device.clone(); + command_buffers.push((i, command_buffer_generation_task(render_device))); + } + } + } + + #[cfg(feature = "trace")] + drop(_command_buffer_generation_tasks_span); + + command_buffers.sort_unstable_by_key(|(i, _)| *i); + + let mut command_buffers = command_buffers + .into_iter() + .map(|(_, cb)| cb) + .collect::>(); + + let mut diagnostics_recorder = self.diagnostics_recorder.take().map(|v| { + Arc::try_unwrap(v) + .ok() + .expect("diagnostic recorder shouldn't be held longer than necessary") + }); + + if let Some(recorder) = &mut diagnostics_recorder { + let mut command_encoder = self + .render_device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); + recorder.resolve(&mut command_encoder); + command_buffers.push(command_encoder.finish()); + } + + (command_buffers, self.render_device, diagnostics_recorder) + } + + fn flush_encoder(&mut self) { + if let Some(encoder) = self.command_encoder.take() { + self.command_buffer_queue + .push(QueuedCommandBuffer::Ready(encoder.finish())); + } + } +} + +enum QueuedCommandBuffer<'w> { + Ready(CommandBuffer), + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + Task(Box CommandBuffer + 'w + Send>), + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + Task(Box CommandBuffer + 'w>), +} diff --git a/crates/libmarathon/src/render/renderer/raw_vulkan_init.rs b/crates/libmarathon/src/render/renderer/raw_vulkan_init.rs new file mode 100644 index 0000000..660caff --- /dev/null +++ b/crates/libmarathon/src/render/renderer/raw_vulkan_init.rs @@ -0,0 +1,148 @@ +use std::sync::Arc; +use bevy_ecs::resource::Resource; +use bevy_platform::collections::HashSet; +use core::any::{Any, TypeId}; +use thiserror::Error; +use wgpu::{ + hal::api::Vulkan, Adapter, Device, DeviceDescriptor, Instance, InstanceDescriptor, Queue, +}; + +/// When the `raw_vulkan_init` feature is enabled, these settings will be used to configure the raw vulkan instance. +#[derive(Resource, Default, Clone)] +pub struct RawVulkanInitSettings { + // SAFETY: this must remain private to ensure that registering callbacks is unsafe + create_instance_callbacks: Vec< + Arc< + dyn Fn( + &mut wgpu::hal::vulkan::CreateInstanceCallbackArgs, + &mut AdditionalVulkanFeatures, + ) + Send + + Sync, + >, + >, + // SAFETY: this must remain private to ensure that registering callbacks is unsafe + create_device_callbacks: Vec< + Arc< + dyn Fn( + &mut wgpu::hal::vulkan::CreateDeviceCallbackArgs, + &wgpu::hal::vulkan::Adapter, + &mut AdditionalVulkanFeatures, + ) + Send + + Sync, + >, + >, +} + +impl RawVulkanInitSettings { + /// Adds a new Vulkan create instance callback. See [`wgpu::hal::vulkan::Instance::init_with_callback`] for details. + /// + /// # Safety + /// - Callback must not remove features. + /// - Callback must not change anything to what the instance does not support. + pub unsafe fn add_create_instance_callback( + &mut self, + callback: impl Fn(&mut wgpu::hal::vulkan::CreateInstanceCallbackArgs, &mut AdditionalVulkanFeatures) + + Send + + Sync + + 'static, + ) { + self.create_instance_callbacks.push(Arc::new(callback)); + } + + /// Adds a new Vulkan create device callback. See [`wgpu::hal::vulkan::Adapter::open_with_callback`] for details. + /// + /// # Safety + /// - Callback must not remove features. + /// - Callback must not change anything to what the device does not support. + pub unsafe fn add_create_device_callback( + &mut self, + callback: impl Fn( + &mut wgpu::hal::vulkan::CreateDeviceCallbackArgs, + &wgpu::hal::vulkan::Adapter, + &mut AdditionalVulkanFeatures, + ) + Send + + Sync + + 'static, + ) { + self.create_device_callbacks.push(Arc::new(callback)); + } +} + +pub(crate) fn create_raw_vulkan_instance( + instance_descriptor: &InstanceDescriptor, + settings: &RawVulkanInitSettings, + additional_features: &mut AdditionalVulkanFeatures, +) -> Instance { + // SAFETY: Registering callbacks is unsafe. Callback authors promise not to remove features + // or change the instance to something it does not support + unsafe { + wgpu::hal::vulkan::Instance::init_with_callback( + &wgpu::hal::InstanceDescriptor { + name: "wgpu", + flags: instance_descriptor.flags, + memory_budget_thresholds: instance_descriptor.memory_budget_thresholds, + backend_options: instance_descriptor.backend_options.clone(), + }, + Some(Box::new(|mut args| { + for callback in &settings.create_instance_callbacks { + (callback)(&mut args, additional_features); + } + })), + ) + .map(|raw_instance| Instance::from_hal::(raw_instance)) + .unwrap_or_else(|_| Instance::new(instance_descriptor)) + } +} + +pub(crate) async fn create_raw_device( + adapter: &Adapter, + device_descriptor: &DeviceDescriptor<'_>, + settings: &RawVulkanInitSettings, + additional_features: &mut AdditionalVulkanFeatures, +) -> Result<(Device, Queue), CreateRawVulkanDeviceError> { + // SAFETY: Registering callbacks is unsafe. Callback authors promise not to remove features + // or change the adapter to something it does not support + unsafe { + let Some(raw_adapter) = adapter.as_hal::() else { + return Ok(adapter.request_device(device_descriptor).await?); + }; + let open_device = raw_adapter.open_with_callback( + device_descriptor.required_features, + &device_descriptor.memory_hints, + Some(Box::new(|mut args| { + for callback in &settings.create_device_callbacks { + (callback)(&mut args, &raw_adapter, additional_features); + } + })), + )?; + + Ok(adapter.create_device_from_hal::(open_device, device_descriptor)?) + } +} + +#[derive(Error, Debug)] +pub(crate) enum CreateRawVulkanDeviceError { + #[error(transparent)] + RequestDeviceError(#[from] wgpu::RequestDeviceError), + #[error(transparent)] + DeviceError(#[from] wgpu::hal::DeviceError), +} + +/// A list of additional Vulkan features that are supported by the current wgpu instance / adapter. This is populated +/// by callbacks defined in [`RawVulkanInitSettings`] +#[derive(Resource, Default, Clone)] +pub struct AdditionalVulkanFeatures(HashSet); + +impl AdditionalVulkanFeatures { + pub fn insert(&mut self) { + self.0.insert(TypeId::of::()); + } + + pub fn has(&self) -> bool { + self.0.contains(&TypeId::of::()) + } + + pub fn remove(&mut self) { + self.0.remove(&TypeId::of::()); + } +} diff --git a/crates/libmarathon/src/render/renderer/render_device.rs b/crates/libmarathon/src/render/renderer/render_device.rs new file mode 100644 index 0000000..c56755d --- /dev/null +++ b/crates/libmarathon/src/render/renderer/render_device.rs @@ -0,0 +1,311 @@ +use super::RenderQueue; +use crate::render::render_resource::{ + BindGroup, BindGroupLayout, Buffer, ComputePipeline, RawRenderPipelineDescriptor, + RenderPipeline, Sampler, Texture, +}; +use crate::render::renderer::WgpuWrapper; +use bevy_ecs::resource::Resource; +use wgpu::{ + util::DeviceExt, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BufferAsyncError, BufferBindingType, PollError, PollStatus, +}; + +/// This GPU device is responsible for the creation of most rendering and compute resources. +#[derive(Resource, Clone)] +pub struct RenderDevice { + device: WgpuWrapper, +} + +impl From for RenderDevice { + fn from(device: wgpu::Device) -> Self { + Self::new(WgpuWrapper::new(device)) + } +} + +impl RenderDevice { + pub fn new(device: WgpuWrapper) -> Self { + Self { device } + } + + /// List all [`Features`](wgpu::Features) that may be used with this device. + /// + /// Functions may panic if you use unsupported features. + #[inline] + pub fn features(&self) -> wgpu::Features { + self.device.features() + } + + /// List all [`Limits`](wgpu::Limits) that were requested of this device. + /// + /// If any of these limits are exceeded, functions may panic. + #[inline] + pub fn limits(&self) -> wgpu::Limits { + self.device.limits() + } + + /// Creates a [`ShaderModule`](wgpu::ShaderModule) from either SPIR-V or WGSL source code. + /// + /// # Safety + /// + /// Creates a shader module with user-customizable runtime checks which allows shaders to + /// perform operations which can lead to undefined behavior like indexing out of bounds, + /// To avoid UB, ensure any unchecked shaders are sound! + /// This method should never be called for user-supplied shaders. + #[inline] + pub unsafe fn create_shader_module( + &self, + desc: wgpu::ShaderModuleDescriptor, + ) -> wgpu::ShaderModule { + #[cfg(feature = "spirv_shader_passthrough")] + match &desc.source { + wgpu::ShaderSource::SpirV(source) + if self + .features() + .contains(wgpu::Features::SPIRV_SHADER_PASSTHROUGH) => + { + // SAFETY: + // This call passes binary data to the backend as-is and can potentially result in a driver crash or bogus behavior. + // No attempt is made to ensure that data is valid SPIR-V. + unsafe { + self.device.create_shader_module_passthrough( + wgpu::ShaderModuleDescriptorPassthrough::SpirV( + wgpu::ShaderModuleDescriptorSpirV { + label: desc.label, + source: source.clone(), + }, + ), + ) + } + } + // SAFETY: + // + // This call passes binary data to the backend as-is and can potentially result in a driver crash or bogus behavior. + // No attempt is made to ensure that data is valid SPIR-V. + _ => unsafe { + self.device + .create_shader_module_trusted(desc, wgpu::ShaderRuntimeChecks::unchecked()) + }, + } + #[cfg(not(feature = "spirv_shader_passthrough"))] + // SAFETY: the caller is responsible for upholding the safety requirements + unsafe { + self.device + .create_shader_module_trusted(desc, wgpu::ShaderRuntimeChecks::unchecked()) + } + } + + /// Creates and validates a [`ShaderModule`](wgpu::ShaderModule) from either SPIR-V or WGSL source code. + /// + /// See [`ValidateShader`](bevy_shader::ValidateShader) for more information on the tradeoffs involved with shader validation. + #[inline] + pub fn create_and_validate_shader_module( + &self, + desc: wgpu::ShaderModuleDescriptor, + ) -> wgpu::ShaderModule { + #[cfg(feature = "spirv_shader_passthrough")] + match &desc.source { + wgpu::ShaderSource::SpirV(_source) => panic!("no safety checks are performed for spirv shaders. use `create_shader_module` instead"), + _ => self.device.create_shader_module(desc), + } + #[cfg(not(feature = "spirv_shader_passthrough"))] + self.device.create_shader_module(desc) + } + + /// Check for resource cleanups and mapping callbacks. + /// + /// Return `true` if the queue is empty, or `false` if there are more queue + /// submissions still in flight. (Note that, unless access to the [`wgpu::Queue`] is + /// coordinated somehow, this information could be out of date by the time + /// the caller receives it. `Queue`s can be shared between threads, so + /// other threads could submit new work at any time.) + /// + /// no-op on the web, device is automatically polled. + #[inline] + pub fn poll(&self, maintain: wgpu::PollType) -> Result { + self.device.poll(maintain) + } + + /// Creates an empty [`CommandEncoder`](wgpu::CommandEncoder). + #[inline] + pub fn create_command_encoder( + &self, + desc: &wgpu::CommandEncoderDescriptor, + ) -> wgpu::CommandEncoder { + self.device.create_command_encoder(desc) + } + + /// Creates an empty [`RenderBundleEncoder`](wgpu::RenderBundleEncoder). + #[inline] + pub fn create_render_bundle_encoder( + &self, + desc: &wgpu::RenderBundleEncoderDescriptor, + ) -> wgpu::RenderBundleEncoder<'_> { + self.device.create_render_bundle_encoder(desc) + } + + /// Creates a new [`BindGroup`](wgpu::BindGroup). + #[inline] + pub fn create_bind_group<'a>( + &self, + label: impl Into>, + layout: &'a BindGroupLayout, + entries: &'a [BindGroupEntry<'a>], + ) -> BindGroup { + let wgpu_bind_group = self.device.create_bind_group(&BindGroupDescriptor { + label: label.into(), + layout, + entries, + }); + BindGroup::from(wgpu_bind_group) + } + + /// Creates a [`BindGroupLayout`](wgpu::BindGroupLayout). + #[inline] + pub fn create_bind_group_layout<'a>( + &self, + label: impl Into>, + entries: &'a [BindGroupLayoutEntry], + ) -> BindGroupLayout { + BindGroupLayout::from( + self.device + .create_bind_group_layout(&BindGroupLayoutDescriptor { + label: label.into(), + entries, + }), + ) + } + + /// Creates a [`PipelineLayout`](wgpu::PipelineLayout). + #[inline] + pub fn create_pipeline_layout( + &self, + desc: &wgpu::PipelineLayoutDescriptor, + ) -> wgpu::PipelineLayout { + self.device.create_pipeline_layout(desc) + } + + /// Creates a [`RenderPipeline`]. + #[inline] + pub fn create_render_pipeline(&self, desc: &RawRenderPipelineDescriptor) -> RenderPipeline { + let wgpu_render_pipeline = self.device.create_render_pipeline(desc); + RenderPipeline::from(wgpu_render_pipeline) + } + + /// Creates a [`ComputePipeline`]. + #[inline] + pub fn create_compute_pipeline( + &self, + desc: &wgpu::ComputePipelineDescriptor, + ) -> ComputePipeline { + let wgpu_compute_pipeline = self.device.create_compute_pipeline(desc); + ComputePipeline::from(wgpu_compute_pipeline) + } + + /// Creates a [`Buffer`]. + pub fn create_buffer(&self, desc: &wgpu::BufferDescriptor) -> Buffer { + let wgpu_buffer = self.device.create_buffer(desc); + Buffer::from(wgpu_buffer) + } + + /// Creates a [`Buffer`] and initializes it with the specified data. + pub fn create_buffer_with_data(&self, desc: &wgpu::util::BufferInitDescriptor) -> Buffer { + let wgpu_buffer = self.device.create_buffer_init(desc); + Buffer::from(wgpu_buffer) + } + + /// Creates a new [`Texture`] and initializes it with the specified data. + /// + /// `desc` specifies the general format of the texture. + /// `data` is the raw data. + pub fn create_texture_with_data( + &self, + render_queue: &RenderQueue, + desc: &wgpu::TextureDescriptor, + order: wgpu::util::TextureDataOrder, + data: &[u8], + ) -> Texture { + let wgpu_texture = + self.device + .create_texture_with_data(render_queue.as_ref(), desc, order, data); + Texture::from(wgpu_texture) + } + + /// Creates a new [`Texture`]. + /// + /// `desc` specifies the general format of the texture. + pub fn create_texture(&self, desc: &wgpu::TextureDescriptor) -> Texture { + let wgpu_texture = self.device.create_texture(desc); + Texture::from(wgpu_texture) + } + + /// Creates a new [`Sampler`]. + /// + /// `desc` specifies the behavior of the sampler. + pub fn create_sampler(&self, desc: &wgpu::SamplerDescriptor) -> Sampler { + let wgpu_sampler = self.device.create_sampler(desc); + Sampler::from(wgpu_sampler) + } + + /// Initializes [`Surface`](wgpu::Surface) for presentation. + /// + /// # Panics + /// + /// - A old [`SurfaceTexture`](wgpu::SurfaceTexture) is still alive referencing an old surface. + /// - Texture format requested is unsupported on the surface. + pub fn configure_surface(&self, surface: &wgpu::Surface, config: &wgpu::SurfaceConfiguration) { + surface.configure(&self.device, config); + } + + /// Returns the wgpu [`Device`](wgpu::Device). + pub fn wgpu_device(&self) -> &wgpu::Device { + &self.device + } + + pub fn map_buffer( + &self, + buffer: &wgpu::BufferSlice, + map_mode: wgpu::MapMode, + callback: impl FnOnce(Result<(), BufferAsyncError>) + Send + 'static, + ) { + buffer.map_async(map_mode, callback); + } + + // Rounds up `row_bytes` to be a multiple of [`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT`]. + pub const fn align_copy_bytes_per_row(row_bytes: usize) -> usize { + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; + + // If row_bytes is aligned calculate a value just under the next aligned value. + // Otherwise calculate a value greater than the next aligned value. + let over_aligned = row_bytes + align - 1; + + // Round the number *down* to the nearest aligned value. + (over_aligned / align) * align + } + + pub fn get_supported_read_only_binding_type( + &self, + buffers_per_shader_stage: u32, + ) -> BufferBindingType { + if self.limits().max_storage_buffers_per_shader_stage >= buffers_per_shader_stage { + BufferBindingType::Storage { read_only: true } + } else { + BufferBindingType::Uniform + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn align_copy_bytes_per_row() { + // Test for https://github.com/bevyengine/bevy/issues/16992 + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; + + assert_eq!(RenderDevice::align_copy_bytes_per_row(0), 0); + assert_eq!(RenderDevice::align_copy_bytes_per_row(1), align); + assert_eq!(RenderDevice::align_copy_bytes_per_row(align + 1), align * 2); + assert_eq!(RenderDevice::align_copy_bytes_per_row(align), align); + } +} diff --git a/crates/libmarathon/src/render/renderer/wgpu_wrapper.rs b/crates/libmarathon/src/render/renderer/wgpu_wrapper.rs new file mode 100644 index 0000000..272d0dd --- /dev/null +++ b/crates/libmarathon/src/render/renderer/wgpu_wrapper.rs @@ -0,0 +1,50 @@ +/// A wrapper to safely make `wgpu` types Send / Sync on web with atomics enabled. +/// +/// On web with `atomics` enabled the inner value can only be accessed +/// or dropped on the `wgpu` thread or else a panic will occur. +/// On other platforms the wrapper simply contains the wrapped value. +#[derive(Debug, Clone)] +pub struct WgpuWrapper( + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] T, + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] send_wrapper::SendWrapper, +); + +// SAFETY: SendWrapper is always Send + Sync. +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +#[expect(unsafe_code, reason = "Blanket-impl Send requires unsafe.")] +unsafe impl Send for WgpuWrapper {} +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +#[expect(unsafe_code, reason = "Blanket-impl Sync requires unsafe.")] +unsafe impl Sync for WgpuWrapper {} + +impl WgpuWrapper { + /// Constructs a new instance of `WgpuWrapper` which will wrap the specified value. + pub fn new(t: T) -> Self { + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + return Self(t); + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + return Self(send_wrapper::SendWrapper::new(t)); + } + + /// Unwraps the value. + pub fn into_inner(self) -> T { + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + return self.0; + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + return self.0.take(); + } +} + +impl core::ops::Deref for WgpuWrapper { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl core::ops::DerefMut for WgpuWrapper { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/crates/libmarathon/src/render/settings.rs b/crates/libmarathon/src/render/settings.rs new file mode 100644 index 0000000..266c1da --- /dev/null +++ b/crates/libmarathon/src/render/settings.rs @@ -0,0 +1,226 @@ +use crate::render::renderer::{ + RenderAdapter, RenderAdapterInfo, RenderDevice, RenderInstance, RenderQueue, +}; +use std::borrow::Cow; + +pub use wgpu::{ + Backends, Dx12Compiler, Features as WgpuFeatures, Gles3MinorVersion, InstanceFlags, + Limits as WgpuLimits, MemoryHints, PowerPreference, +}; +use wgpu::{DxcShaderModel, MemoryBudgetThresholds}; + +/// Configures the priority used when automatically configuring the features/limits of `wgpu`. +#[derive(Clone)] +pub enum WgpuSettingsPriority { + /// WebGPU default features and limits + Compatibility, + /// The maximum supported features and limits of the adapter and backend + Functionality, + /// WebGPU default limits plus additional constraints in order to be compatible with WebGL2 + WebGL2, +} + +/// Provides configuration for renderer initialization. Use [`RenderDevice::features`](RenderDevice::features), +/// [`RenderDevice::limits`](RenderDevice::limits), and the [`RenderAdapterInfo`] +/// resource to get runtime information about the actual adapter, backend, features, and limits. +/// NOTE: [`Backends::DX12`](Backends::DX12), [`Backends::METAL`](Backends::METAL), and +/// [`Backends::VULKAN`](Backends::VULKAN) are enabled by default for non-web and the best choice +/// is automatically selected. Web using the `webgl` feature uses [`Backends::GL`](Backends::GL). +/// NOTE: If you want to use [`Backends::GL`](Backends::GL) in a native app on `Windows` and/or `macOS`, you must +/// use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle) and enable the `gles` feature. This is +/// because wgpu requires EGL to create a GL context without a window and only ANGLE supports that. +#[derive(Clone)] +pub struct WgpuSettings { + pub device_label: Option>, + pub backends: Option, + pub power_preference: PowerPreference, + pub priority: WgpuSettingsPriority, + /// The features to ensure are enabled regardless of what the adapter/backend supports. + /// Setting these explicitly may cause renderer initialization to fail. + pub features: WgpuFeatures, + /// The features to ensure are disabled regardless of what the adapter/backend supports + pub disabled_features: Option, + /// The imposed limits. + pub limits: WgpuLimits, + /// The constraints on limits allowed regardless of what the adapter/backend supports + pub constrained_limits: Option, + /// The shader compiler to use for the DX12 backend. + pub dx12_shader_compiler: Dx12Compiler, + /// Allows you to choose which minor version of GLES3 to use (3.0, 3.1, 3.2, or automatic) + /// This only applies when using ANGLE and the GL backend. + pub gles3_minor_version: Gles3MinorVersion, + /// These are for controlling WGPU's debug information to eg. enable validation and shader debug info in release builds. + pub instance_flags: InstanceFlags, + /// This hints to the WGPU device about the preferred memory allocation strategy. + pub memory_hints: MemoryHints, + /// The thresholds for device memory budget. + pub instance_memory_budget_thresholds: MemoryBudgetThresholds, + /// If true, will force wgpu to use a software renderer, if available. + pub force_fallback_adapter: bool, + /// The name of the adapter to use. + pub adapter_name: Option, +} + +impl Default for WgpuSettings { + fn default() -> Self { + let default_backends = if cfg!(all( + feature = "webgl", + target_arch = "wasm32", + not(feature = "webgpu") + )) { + Backends::GL + } else if cfg!(all(feature = "webgpu", target_arch = "wasm32")) { + Backends::BROWSER_WEBGPU + } else { + Backends::all() + }; + + let backends = Some(Backends::from_env().unwrap_or(default_backends)); + + let power_preference = + PowerPreference::from_env().unwrap_or(PowerPreference::HighPerformance); + + let priority = settings_priority_from_env().unwrap_or(WgpuSettingsPriority::Functionality); + + let limits = if cfg!(all( + feature = "webgl", + target_arch = "wasm32", + not(feature = "webgpu") + )) || matches!(priority, WgpuSettingsPriority::WebGL2) + { + wgpu::Limits::downlevel_webgl2_defaults() + } else { + #[expect(clippy::allow_attributes, reason = "`unused_mut` is not always linted")] + #[allow( + unused_mut, + reason = "This variable needs to be mutable if the `ci_limits` feature is enabled" + )] + let mut limits = wgpu::Limits::default(); + #[cfg(feature = "ci_limits")] + { + limits.max_storage_textures_per_shader_stage = 4; + limits.max_texture_dimension_3d = 1024; + } + limits + }; + + let dx12_shader_compiler = + Dx12Compiler::from_env().unwrap_or(if cfg!(feature = "statically-linked-dxc") { + Dx12Compiler::StaticDxc + } else { + let dxc = "dxcompiler.dll"; + + if cfg!(target_os = "windows") && std::fs::metadata(dxc).is_ok() { + Dx12Compiler::DynamicDxc { + dxc_path: String::from(dxc), + max_shader_model: DxcShaderModel::V6_7, + } + } else { + Dx12Compiler::Fxc + } + }); + + let gles3_minor_version = Gles3MinorVersion::from_env().unwrap_or_default(); + + let instance_flags = InstanceFlags::default().with_env(); + + Self { + device_label: Default::default(), + backends, + power_preference, + priority, + features: wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES, + disabled_features: None, + limits, + constrained_limits: None, + dx12_shader_compiler, + gles3_minor_version, + instance_flags, + memory_hints: MemoryHints::default(), + instance_memory_budget_thresholds: MemoryBudgetThresholds::default(), + force_fallback_adapter: false, + adapter_name: None, + } + } +} + +#[derive(Clone)] +pub struct RenderResources( + pub RenderDevice, + pub RenderQueue, + pub RenderAdapterInfo, + pub RenderAdapter, + pub RenderInstance, + #[cfg(feature = "raw_vulkan_init")] + pub crate::renderer::raw_vulkan_init::AdditionalVulkanFeatures, +); + +/// An enum describing how the renderer will initialize resources. This is used when creating the [`RenderPlugin`](crate::RenderPlugin). +#[expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" +)] +pub enum RenderCreation { + /// Allows renderer resource initialization to happen outside of the rendering plugin. + Manual(RenderResources), + /// Lets the rendering plugin create resources itself. + Automatic(WgpuSettings), +} + +impl RenderCreation { + /// Function to create a [`RenderCreation::Manual`] variant. + pub fn manual( + device: RenderDevice, + queue: RenderQueue, + adapter_info: RenderAdapterInfo, + adapter: RenderAdapter, + instance: RenderInstance, + #[cfg(feature = "raw_vulkan_init")] + additional_vulkan_features: crate::renderer::raw_vulkan_init::AdditionalVulkanFeatures, + ) -> Self { + RenderResources( + device, + queue, + adapter_info, + adapter, + instance, + #[cfg(feature = "raw_vulkan_init")] + additional_vulkan_features, + ) + .into() + } +} + +impl From for RenderCreation { + fn from(value: RenderResources) -> Self { + Self::Manual(value) + } +} + +impl Default for RenderCreation { + fn default() -> Self { + Self::Automatic(Default::default()) + } +} + +impl From for RenderCreation { + fn from(value: WgpuSettings) -> Self { + Self::Automatic(value) + } +} + +/// Get a features/limits priority from the environment variable `WGPU_SETTINGS_PRIO` +pub fn settings_priority_from_env() -> Option { + Some( + match std::env::var("WGPU_SETTINGS_PRIO") + .as_deref() + .map(str::to_lowercase) + .as_deref() + { + Ok("compatibility") => WgpuSettingsPriority::Compatibility, + Ok("functionality") => WgpuSettingsPriority::Functionality, + Ok("webgl2") => WgpuSettingsPriority::WebGL2, + _ => return None, + }, + ) +} diff --git a/crates/libmarathon/src/render/skybox/mod.rs b/crates/libmarathon/src/render/skybox/mod.rs new file mode 100644 index 0000000..35598bb --- /dev/null +++ b/crates/libmarathon/src/render/skybox/mod.rs @@ -0,0 +1,305 @@ +use bevy_app::{App, Plugin}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; +use bevy_camera::Exposure; +use bevy_ecs::{ + prelude::{Component, Entity}, + query::{QueryItem, With}, + reflect::ReflectComponent, + resource::Resource, + schedule::IntoScheduleConfigs, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_image::{BevyDefault, Image}; +use bevy_math::{Mat4, Quat}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use crate::render::{ + extract_component::{ + ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin, + UniformComponentPlugin, + }, + render_asset::RenderAssets, + render_resource::{ + binding_types::{sampler, texture_cube, uniform_buffer}, + *, + }, + renderer::RenderDevice, + texture::GpuImage, + view::{ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniforms}, + Render, RenderApp, RenderStartup, RenderSystems, +}; +use bevy_shader::Shader; +use bevy_transform::components::Transform; +use bevy_utils::default; +use prepass::SkyboxPrepassPipeline; + +use crate::render::{ + core_3d::CORE_3D_DEPTH_FORMAT, prepass::PreviousViewUniforms, + skybox::prepass::init_skybox_prepass_pipeline, +}; + +pub mod prepass; + +pub struct SkyboxPlugin; + +impl Plugin for SkyboxPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "skybox.wgsl"); + embedded_asset!(app, "skybox_prepass.wgsl"); + + app.add_plugins(( + ExtractComponentPlugin::::default(), + UniformComponentPlugin::::default(), + )); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app + .init_resource::>() + .init_resource::>() + .init_resource::() + .add_systems( + RenderStartup, + (init_skybox_pipeline, init_skybox_prepass_pipeline), + ) + .add_systems( + Render, + ( + prepare_skybox_pipelines.in_set(RenderSystems::Prepare), + prepass::prepare_skybox_prepass_pipelines.in_set(RenderSystems::Prepare), + prepare_skybox_bind_groups.in_set(RenderSystems::PrepareBindGroups), + prepass::prepare_skybox_prepass_bind_groups + .in_set(RenderSystems::PrepareBindGroups), + ), + ); + } +} + +/// Adds a skybox to a 3D camera, based on a cubemap texture. +/// +/// Note that this component does not (currently) affect the scene's lighting. +/// To do so, use `EnvironmentMapLight` alongside this component. +/// +/// See also . +#[derive(Component, Clone, Reflect)] +#[reflect(Component, Default, Clone)] +pub struct Skybox { + pub image: Handle, + /// Scale factor applied to the skybox image. + /// After applying this multiplier to the image samples, the resulting values should + /// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre). + pub brightness: f32, + + /// View space rotation applied to the skybox cubemap. + /// This is useful for users who require a different axis, such as the Z-axis, to serve + /// as the vertical axis. + pub rotation: Quat, +} + +impl Default for Skybox { + fn default() -> Self { + Skybox { + image: Handle::default(), + brightness: 0.0, + rotation: Quat::IDENTITY, + } + } +} + +impl ExtractComponent for Skybox { + type QueryData = (&'static Self, Option<&'static Exposure>); + type QueryFilter = (); + type Out = (Self, SkyboxUniforms); + + fn extract_component( + (skybox, exposure): QueryItem<'_, '_, Self::QueryData>, + ) -> Option { + let exposure = exposure + .map(Exposure::exposure) + .unwrap_or_else(|| Exposure::default().exposure()); + + Some(( + skybox.clone(), + SkyboxUniforms { + brightness: skybox.brightness * exposure, + transform: Transform::from_rotation(skybox.rotation.inverse()).to_matrix(), + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _wasm_padding_8b: 0, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _wasm_padding_12b: 0, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _wasm_padding_16b: 0, + }, + )) + } +} + +// TODO: Replace with a push constant once WebGPU gets support for that +#[derive(Component, ShaderType, Clone)] +pub struct SkyboxUniforms { + brightness: f32, + transform: Mat4, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _wasm_padding_8b: u32, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _wasm_padding_12b: u32, + #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] + _wasm_padding_16b: u32, +} + +#[derive(Resource)] +struct SkyboxPipeline { + bind_group_layout: BindGroupLayout, + shader: Handle, +} + +impl SkyboxPipeline { + fn new(render_device: &RenderDevice, shader: Handle) -> Self { + Self { + bind_group_layout: render_device.create_bind_group_layout( + "skybox_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_cube(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), + uniform_buffer::(true) + .visibility(ShaderStages::VERTEX_FRAGMENT), + uniform_buffer::(true), + ), + ), + ), + shader, + } + } +} + +fn init_skybox_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, +) { + let shader = load_embedded_asset!(asset_server.as_ref(), "skybox.wgsl"); + commands.insert_resource(SkyboxPipeline::new(&render_device, shader)); +} + +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +struct SkyboxPipelineKey { + hdr: bool, + samples: u32, + depth_format: TextureFormat, +} + +impl SpecializedRenderPipeline for SkyboxPipeline { + type Key = SkyboxPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + RenderPipelineDescriptor { + label: Some("skybox_pipeline".into()), + layout: vec![self.bind_group_layout.clone()], + vertex: VertexState { + shader: self.shader.clone(), + ..default() + }, + depth_stencil: Some(DepthStencilState { + format: key.depth_format, + depth_write_enabled: false, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + multisample: MultisampleState { + count: key.samples, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(FragmentState { + shader: self.shader.clone(), + targets: vec![Some(ColorTargetState { + format: if key.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + // BlendState::REPLACE is not needed here, and None will be potentially much faster in some cases. + blend: None, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + ..default() + } + } +} + +#[derive(Component)] +pub struct SkyboxPipelineId(pub CachedRenderPipelineId); + +fn prepare_skybox_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + pipeline: Res, + views: Query<(Entity, &ExtractedView, &Msaa), With>, +) { + for (entity, view, msaa) in &views { + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &pipeline, + SkyboxPipelineKey { + hdr: view.hdr, + samples: msaa.samples(), + depth_format: CORE_3D_DEPTH_FORMAT, + }, + ); + + commands + .entity(entity) + .insert(SkyboxPipelineId(pipeline_id)); + } +} + +#[derive(Component)] +pub struct SkyboxBindGroup(pub (BindGroup, u32)); + +fn prepare_skybox_bind_groups( + mut commands: Commands, + pipeline: Res, + view_uniforms: Res, + skybox_uniforms: Res>, + images: Res>, + render_device: Res, + views: Query<(Entity, &Skybox, &DynamicUniformIndex)>, +) { + for (entity, skybox, skybox_uniform_index) in &views { + if let (Some(skybox), Some(view_uniforms), Some(skybox_uniforms)) = ( + images.get(&skybox.image), + view_uniforms.uniforms.binding(), + skybox_uniforms.binding(), + ) { + let bind_group = render_device.create_bind_group( + "skybox_bind_group", + &pipeline.bind_group_layout, + &BindGroupEntries::sequential(( + &skybox.texture_view, + &skybox.sampler, + view_uniforms, + skybox_uniforms, + )), + ); + + commands + .entity(entity) + .insert(SkyboxBindGroup((bind_group, skybox_uniform_index.index()))); + } + } +} diff --git a/crates/libmarathon/src/render/skybox/prepass.rs b/crates/libmarathon/src/render/skybox/prepass.rs new file mode 100644 index 0000000..2e15f94 --- /dev/null +++ b/crates/libmarathon/src/render/skybox/prepass.rs @@ -0,0 +1,164 @@ +//! Adds motion vector support to skyboxes. See [`SkyboxPrepassPipeline`] for details. + +use bevy_asset::{load_embedded_asset, AssetServer, Handle}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{Has, With}, + resource::Resource, + system::{Commands, Query, Res, ResMut}, +}; +use crate::render::{ + render_resource::{ + binding_types::uniform_buffer, BindGroup, BindGroupEntries, BindGroupLayout, + BindGroupLayoutEntries, CachedRenderPipelineId, CompareFunction, DepthStencilState, + FragmentState, MultisampleState, PipelineCache, RenderPipelineDescriptor, ShaderStages, + SpecializedRenderPipeline, SpecializedRenderPipelines, + }, + renderer::RenderDevice, + view::{Msaa, ViewUniform, ViewUniforms}, +}; +use bevy_shader::Shader; +use bevy_utils::prelude::default; + +use crate::render::{ + core_3d::CORE_3D_DEPTH_FORMAT, + prepass::{ + prepass_target_descriptors, MotionVectorPrepass, NormalPrepass, PreviousViewData, + PreviousViewUniforms, + }, + FullscreenShader, Skybox, +}; + +/// This pipeline writes motion vectors to the prepass for all [`Skybox`]es. +/// +/// This allows features like motion blur and TAA to work correctly on the skybox. Without this, for +/// example, motion blur would not be applied to the skybox when the camera is rotated and motion +/// blur is enabled. +#[derive(Resource)] +pub struct SkyboxPrepassPipeline { + bind_group_layout: BindGroupLayout, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, +} + +/// Used to specialize the [`SkyboxPrepassPipeline`]. +#[derive(PartialEq, Eq, Hash, Clone, Copy)] +pub struct SkyboxPrepassPipelineKey { + samples: u32, + normal_prepass: bool, +} + +/// Stores the ID for a camera's specialized pipeline, so it can be retrieved from the +/// [`PipelineCache`]. +#[derive(Component)] +pub struct RenderSkyboxPrepassPipeline(pub CachedRenderPipelineId); + +/// Stores the [`SkyboxPrepassPipeline`] bind group for a camera. This is later used by the prepass +/// render graph node to add this binding to the prepass's render pass. +#[derive(Component)] +pub struct SkyboxPrepassBindGroup(pub BindGroup); + +pub fn init_skybox_prepass_pipeline( + mut commands: Commands, + render_device: Res, + fullscreen_shader: Res, + asset_server: Res, +) { + commands.insert_resource(SkyboxPrepassPipeline { + bind_group_layout: render_device.create_bind_group_layout( + "skybox_prepass_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + uniform_buffer::(true), + uniform_buffer::(true), + ), + ), + ), + fullscreen_shader: fullscreen_shader.clone(), + fragment_shader: load_embedded_asset!(asset_server.as_ref(), "skybox_prepass.wgsl"), + }); +} + +impl SpecializedRenderPipeline for SkyboxPrepassPipeline { + type Key = SkyboxPrepassPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + RenderPipelineDescriptor { + label: Some("skybox_prepass_pipeline".into()), + layout: vec![self.bind_group_layout.clone()], + vertex: self.fullscreen_shader.to_vertex_state(), + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: false, + depth_compare: CompareFunction::GreaterEqual, + stencil: default(), + bias: default(), + }), + multisample: MultisampleState { + count: key.samples, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(FragmentState { + shader: self.fragment_shader.clone(), + targets: prepass_target_descriptors(key.normal_prepass, true, false), + ..default() + }), + ..default() + } + } +} + +/// Specialize and cache the [`SkyboxPrepassPipeline`] for each camera with a [`Skybox`]. +pub fn prepare_skybox_prepass_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + pipeline: Res, + views: Query<(Entity, Has, &Msaa), (With, With)>, +) { + for (entity, normal_prepass, msaa) in &views { + let pipeline_key = SkyboxPrepassPipelineKey { + samples: msaa.samples(), + normal_prepass, + }; + + let render_skybox_prepass_pipeline = + pipelines.specialize(&pipeline_cache, &pipeline, pipeline_key); + commands + .entity(entity) + .insert(RenderSkyboxPrepassPipeline(render_skybox_prepass_pipeline)); + } +} + +/// Creates the required bind groups for the [`SkyboxPrepassPipeline`]. This binds the view uniforms +/// from the CPU for access in the prepass shader on the GPU, allowing us to compute camera motion +/// between frames. This is then stored in the [`SkyboxPrepassBindGroup`] component on the camera. +pub fn prepare_skybox_prepass_bind_groups( + mut commands: Commands, + pipeline: Res, + view_uniforms: Res, + prev_view_uniforms: Res, + render_device: Res, + views: Query, With)>, +) { + for entity in &views { + let (Some(view_uniforms), Some(prev_view_uniforms)) = ( + view_uniforms.uniforms.binding(), + prev_view_uniforms.uniforms.binding(), + ) else { + continue; + }; + let bind_group = render_device.create_bind_group( + "skybox_prepass_bind_group", + &pipeline.bind_group_layout, + &BindGroupEntries::sequential((view_uniforms, prev_view_uniforms)), + ); + + commands + .entity(entity) + .insert(SkyboxPrepassBindGroup(bind_group)); + } +} diff --git a/crates/libmarathon/src/render/skybox/skybox.wgsl b/crates/libmarathon/src/render/skybox/skybox.wgsl new file mode 100644 index 0000000..7982370 --- /dev/null +++ b/crates/libmarathon/src/render/skybox/skybox.wgsl @@ -0,0 +1,81 @@ +#import bevy_render::view::View +#import bevy_pbr::utils::coords_to_viewport_uv + +struct SkyboxUniforms { + brightness: f32, + transform: mat4x4, +#ifdef SIXTEEN_BYTE_ALIGNMENT + _wasm_padding_8b: u32, + _wasm_padding_12b: u32, + _wasm_padding_16b: u32, +#endif +} + +@group(0) @binding(0) var skybox: texture_cube; +@group(0) @binding(1) var skybox_sampler: sampler; +@group(0) @binding(2) var view: View; +@group(0) @binding(3) var uniforms: SkyboxUniforms; + +fn coords_to_ray_direction(position: vec2, viewport: vec4) -> vec3 { + // Using world positions of the fragment and camera to calculate a ray direction + // breaks down at large translations. This code only needs to know the ray direction. + // The ray direction is along the direction from the camera to the fragment position. + // In view space, the camera is at the origin, so the view space ray direction is + // along the direction of the fragment position - (0,0,0) which is just the + // fragment position. + // Use the position on the near clipping plane to avoid -inf world position + // because the far plane of an infinite reverse projection is at infinity. + let view_position_homogeneous = view.view_from_clip * vec4( + coords_to_viewport_uv(position, viewport) * vec2(2.0, -2.0) + vec2(-1.0, 1.0), + 1.0, + 1.0, + ); + + // Transforming the view space ray direction by the skybox transform matrix, it is + // equivalent to rotating the skybox itself. + var view_ray_direction = view_position_homogeneous.xyz / view_position_homogeneous.w; + view_ray_direction = (view.world_from_view * vec4(view_ray_direction, 0.0)).xyz; + + // Transforming the view space ray direction by the view matrix, transforms the + // direction to world space. Note that the w element is set to 0.0, as this is a + // vector direction, not a position, That causes the matrix multiplication to ignore + // the translations from the view matrix. + let ray_direction = (uniforms.transform * vec4(view_ray_direction, 0.0)).xyz; + + return normalize(ray_direction); +} + +struct VertexOutput { + @builtin(position) position: vec4, +}; + +// 3 | 2. +// 2 | : `. +// 1 | x-----x. +// 0 | | s | `. +// -1 | 0-----x.....1 +// +--------------- +// -1 0 1 2 3 +// +// The axes are clip-space x and y. The region marked s is the visible region. +// The digits in the corners of the right-angled triangle are the vertex +// indices. +@vertex +fn skybox_vertex(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + // See the explanation above for how this works. + let clip_position = vec2( + f32(vertex_index & 1u), + f32((vertex_index >> 1u) & 1u), + ) * 4.0 - vec2(1.0); + + return VertexOutput(vec4(clip_position, 0.0, 1.0)); +} + +@fragment +fn skybox_fragment(in: VertexOutput) -> @location(0) vec4 { + let ray_direction = coords_to_ray_direction(in.position.xy, view.viewport); + + // Cube maps are left-handed so we negate the z coordinate. + let out = textureSample(skybox, skybox_sampler, ray_direction * vec3(1.0, 1.0, -1.0)); + return vec4(out.rgb * uniforms.brightness, out.a); +} diff --git a/crates/libmarathon/src/render/skybox/skybox_prepass.wgsl b/crates/libmarathon/src/render/skybox/skybox_prepass.wgsl new file mode 100644 index 0000000..e4ecb47 --- /dev/null +++ b/crates/libmarathon/src/render/skybox/skybox_prepass.wgsl @@ -0,0 +1,24 @@ +#import bevy_render::view::View +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_pbr::view_transformations::uv_to_ndc + +struct PreviousViewUniforms { + view_from_world: mat4x4, + clip_from_world: mat4x4, + clip_from_view: mat4x4, + world_from_clip: mat4x4, + view_from_clip: mat4x4, +} + +@group(0) @binding(0) var view: View; +@group(0) @binding(1) var previous_view: PreviousViewUniforms; + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(1) vec4 { + let clip_pos = uv_to_ndc(in.uv); // Convert from uv to clip space + let world_pos = view.world_from_clip * vec4(clip_pos, 0.0, 1.0); + let prev_clip_pos = (previous_view.clip_from_world * world_pos).xy; + let velocity = (clip_pos - prev_clip_pos) * vec2(0.5, -0.5); // Copied from mesh motion vectors + + return vec4(velocity.x, velocity.y, 0.0, 1.0); +} diff --git a/crates/libmarathon/src/render/storage.rs b/crates/libmarathon/src/render/storage.rs new file mode 100644 index 0000000..f2e7fa2 --- /dev/null +++ b/crates/libmarathon/src/render/storage.rs @@ -0,0 +1,135 @@ +use crate::render::{ + render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin}, + render_resource::{Buffer, BufferUsages}, + renderer::RenderDevice, +}; +use bevy_app::{App, Plugin}; +use bevy_asset::{Asset, AssetApp, AssetId, RenderAssetUsages}; +use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_utils::default; +use encase::{internal::WriteInto, ShaderType}; +use wgpu::util::BufferInitDescriptor; + +/// Adds [`ShaderStorageBuffer`] as an asset that is extracted and uploaded to the GPU. +#[derive(Default)] +pub struct StoragePlugin; + +impl Plugin for StoragePlugin { + fn build(&self, app: &mut App) { + app.add_plugins(RenderAssetPlugin::::default()) + .init_asset::() + .register_asset_reflect::(); + } +} + +/// A storage buffer that is prepared as a [`RenderAsset`] and uploaded to the GPU. +#[derive(Asset, Reflect, Debug, Clone)] +#[reflect(opaque)] +#[reflect(Default, Debug, Clone)] +pub struct ShaderStorageBuffer { + /// Optional data used to initialize the buffer. + pub data: Option>, + /// The buffer description used to create the buffer. + pub buffer_description: wgpu::BufferDescriptor<'static>, + /// The asset usage of the storage buffer. + pub asset_usage: RenderAssetUsages, +} + +impl Default for ShaderStorageBuffer { + fn default() -> Self { + Self { + data: None, + buffer_description: wgpu::BufferDescriptor { + label: None, + size: 0, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }, + asset_usage: RenderAssetUsages::default(), + } + } +} + +impl ShaderStorageBuffer { + /// Creates a new storage buffer with the given data and asset usage. + pub fn new(data: &[u8], asset_usage: RenderAssetUsages) -> Self { + let mut storage = ShaderStorageBuffer { + data: Some(data.to_vec()), + ..default() + }; + storage.asset_usage = asset_usage; + storage + } + + /// Creates a new storage buffer with the given size and asset usage. + pub fn with_size(size: usize, asset_usage: RenderAssetUsages) -> Self { + let mut storage = ShaderStorageBuffer { + data: None, + ..default() + }; + storage.buffer_description.size = size as u64; + storage.buffer_description.mapped_at_creation = false; + storage.asset_usage = asset_usage; + storage + } + + /// Sets the data of the storage buffer to the given [`ShaderType`]. + pub fn set_data(&mut self, value: T) + where + T: ShaderType + WriteInto, + { + let size = value.size().get() as usize; + let mut wrapper = encase::StorageBuffer::>::new(Vec::with_capacity(size)); + wrapper.write(&value).unwrap(); + self.data = Some(wrapper.into_inner()); + } +} + +impl From for ShaderStorageBuffer +where + T: ShaderType + WriteInto, +{ + fn from(value: T) -> Self { + let size = value.size().get() as usize; + let mut wrapper = encase::StorageBuffer::>::new(Vec::with_capacity(size)); + wrapper.write(&value).unwrap(); + Self::new(wrapper.as_ref(), RenderAssetUsages::default()) + } +} + +/// A storage buffer that is prepared as a [`RenderAsset`] and uploaded to the GPU. +pub struct GpuShaderStorageBuffer { + pub buffer: Buffer, +} + +impl RenderAsset for GpuShaderStorageBuffer { + type SourceAsset = ShaderStorageBuffer; + type Param = SRes; + + fn asset_usage(source_asset: &Self::SourceAsset) -> RenderAssetUsages { + source_asset.asset_usage + } + + fn prepare_asset( + source_asset: Self::SourceAsset, + _: AssetId, + render_device: &mut SystemParamItem, + _: Option<&Self>, + ) -> Result> { + match source_asset.data { + Some(data) => { + let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: source_asset.buffer_description.label, + contents: &data, + usage: source_asset.buffer_description.usage, + }); + Ok(GpuShaderStorageBuffer { buffer }) + } + None => { + let buffer = render_device.create_buffer(&source_asset.buffer_description); + Ok(GpuShaderStorageBuffer { buffer }) + } + } + } +} diff --git a/crates/libmarathon/src/render/sync_component.rs b/crates/libmarathon/src/render/sync_component.rs new file mode 100644 index 0000000..462fa75 --- /dev/null +++ b/crates/libmarathon/src/render/sync_component.rs @@ -0,0 +1,42 @@ +use core::marker::PhantomData; + +use bevy_app::{App, Plugin}; +use bevy_ecs::component::Component; + +use crate::render::sync_world::{EntityRecord, PendingSyncEntity, SyncToRenderWorld}; + +/// Plugin that registers a component for automatic sync to the render world. See [`SyncWorldPlugin`] for more information. +/// +/// This plugin is automatically added by [`ExtractComponentPlugin`], and only needs to be added for manual extraction implementations. +/// +/// # Implementation details +/// +/// It adds [`SyncToRenderWorld`] as a required component to make the [`SyncWorldPlugin`] aware of the component, and +/// handles cleanup of the component in the render world when it is removed from an entity. +/// +/// # Warning +/// When the component is removed from the main world entity, all components are removed from the entity in the render world. +/// This is done in order to handle components with custom extraction logic and derived state. +/// +/// [`ExtractComponentPlugin`]: crate::extract_component::ExtractComponentPlugin +/// [`SyncWorldPlugin`]: crate::sync_world::SyncWorldPlugin +pub struct SyncComponentPlugin(PhantomData); + +impl Default for SyncComponentPlugin { + fn default() -> Self { + Self(PhantomData) + } +} + +impl Plugin for SyncComponentPlugin { + fn build(&self, app: &mut App) { + app.register_required_components::(); + + app.world_mut() + .register_component_hooks::() + .on_remove(|mut world, context| { + let mut pending = world.resource_mut::(); + pending.push(EntityRecord::ComponentRemoved(context.entity)); + }); + } +} diff --git a/crates/libmarathon/src/render/sync_world.rs b/crates/libmarathon/src/render/sync_world.rs new file mode 100644 index 0000000..d9a8a43 --- /dev/null +++ b/crates/libmarathon/src/render/sync_world.rs @@ -0,0 +1,580 @@ +use bevy_app::Plugin; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::{ContainsEntity, Entity, EntityEquivalent, EntityHash}, + lifecycle::{Add, Remove}, + observer::On, + query::With, + reflect::ReflectComponent, + resource::Resource, + system::{Local, Query, ResMut, SystemState}, + world::{Mut, World}, +}; +use bevy_platform::collections::{HashMap, HashSet}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; + +/// A plugin that synchronizes entities with [`SyncToRenderWorld`] between the main world and the render world. +/// +/// All entities with the [`SyncToRenderWorld`] component are kept in sync. It +/// is automatically added as a required component by [`ExtractComponentPlugin`] +/// and [`SyncComponentPlugin`], so it doesn't need to be added manually when +/// spawning or as a required component when either of these plugins are used. +/// +/// # Implementation +/// +/// Bevy's renderer is architected independently from the main app. +/// It operates in its own separate ECS [`World`], so the renderer logic can run in parallel with the main world logic. +/// This is called "Pipelined Rendering", see [`PipelinedRenderingPlugin`] for more information. +/// +/// [`SyncWorldPlugin`] is the first thing that runs every frame and it maintains an entity-to-entity mapping +/// between the main world and the render world. +/// It does so by spawning and despawning entities in the render world, to match spawned and despawned entities in the main world. +/// The link between synced entities is maintained by the [`RenderEntity`] and [`MainEntity`] components. +/// +/// The [`RenderEntity`] contains the corresponding render world entity of a main world entity, while [`MainEntity`] contains +/// the corresponding main world entity of a render world entity. +/// For convenience, [`QueryData`](bevy_ecs::query::QueryData) implementations are provided for both components: +/// adding [`MainEntity`] to a query (without a `&`) will return the corresponding main world [`Entity`], +/// and adding [`RenderEntity`] will return the corresponding render world [`Entity`]. +/// If you have access to the component itself, the underlying entities can be accessed by calling `.id()`. +/// +/// Synchronization is necessary preparation for extraction ([`ExtractSchedule`](crate::ExtractSchedule)), which copies over component data from the main +/// to the render world for these entities. +/// +/// ```text +/// |--------------------------------------------------------------------| +/// | | | Main world update | +/// | sync | extract |---------------------------------------------------| +/// | | | Render world update | +/// |--------------------------------------------------------------------| +/// ``` +/// +/// An example for synchronized main entities 1v1 and 18v1 +/// +/// ```text +/// |---------------------------Main World------------------------------| +/// | Entity | Component | +/// |-------------------------------------------------------------------| +/// | ID: 1v1 | PointLight | RenderEntity(ID: 3V1) | SyncToRenderWorld | +/// | ID: 18v1 | PointLight | RenderEntity(ID: 5V1) | SyncToRenderWorld | +/// |-------------------------------------------------------------------| +/// +/// |----------Render World-----------| +/// | Entity | Component | +/// |---------------------------------| +/// | ID: 3v1 | MainEntity(ID: 1V1) | +/// | ID: 5v1 | MainEntity(ID: 18V1) | +/// |---------------------------------| +/// +/// ``` +/// +/// Note that this effectively establishes a link between the main world entity and the render world entity. +/// Not every entity needs to be synchronized, however; only entities with the [`SyncToRenderWorld`] component are synced. +/// Adding [`SyncToRenderWorld`] to a main world component will establish such a link. +/// Once a synchronized main entity is despawned, its corresponding render entity will be automatically +/// despawned in the next `sync`. +/// +/// The sync step does not copy any of component data between worlds, since its often not necessary to transfer over all +/// the components of a main world entity. +/// The render world probably cares about a `Position` component, but not a `Velocity` component. +/// The extraction happens in its own step, independently from, and after synchronization. +/// +/// Moreover, [`SyncWorldPlugin`] only synchronizes *entities*. [`RenderAsset`](crate::render_asset::RenderAsset)s like meshes and textures are handled +/// differently. +/// +/// [`PipelinedRenderingPlugin`]: crate::pipelined_rendering::PipelinedRenderingPlugin +/// [`ExtractComponentPlugin`]: crate::extract_component::ExtractComponentPlugin +/// [`SyncComponentPlugin`]: crate::sync_component::SyncComponentPlugin +#[derive(Default)] +pub struct SyncWorldPlugin; + +impl Plugin for SyncWorldPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.init_resource::(); + app.add_observer( + |add: On, mut pending: ResMut| { + pending.push(EntityRecord::Added(add.entity)); + }, + ); + app.add_observer( + |remove: On, + mut pending: ResMut, + query: Query<&RenderEntity>| { + if let Ok(e) = query.get(remove.entity) { + pending.push(EntityRecord::Removed(*e)); + }; + }, + ); + } +} +/// Marker component that indicates that its entity needs to be synchronized to the render world. +/// +/// This component is automatically added as a required component by [`ExtractComponentPlugin`] and [`SyncComponentPlugin`]. +/// For more information see [`SyncWorldPlugin`]. +/// +/// NOTE: This component should persist throughout the entity's entire lifecycle. +/// If this component is removed from its entity, the entity will be despawned. +/// +/// [`ExtractComponentPlugin`]: crate::extract_component::ExtractComponentPlugin +/// [`SyncComponentPlugin`]: crate::sync_component::SyncComponentPlugin +#[derive(Component, Copy, Clone, Debug, Default, Reflect)] +#[reflect[Component, Default, Clone]] +#[component(storage = "SparseSet")] +pub struct SyncToRenderWorld; + +/// Component added on the main world entities that are synced to the Render World in order to keep track of the corresponding render world entity. +/// +/// Can also be used as a newtype wrapper for render world entities. +#[derive(Component, Deref, Copy, Clone, Debug, Eq, Hash, PartialEq, Reflect)] +#[component(clone_behavior = Ignore)] +#[reflect(Component, Clone)] +pub struct RenderEntity(Entity); +impl RenderEntity { + #[inline] + pub fn id(&self) -> Entity { + self.0 + } +} + +impl From for RenderEntity { + fn from(entity: Entity) -> Self { + RenderEntity(entity) + } +} + +impl ContainsEntity for RenderEntity { + fn entity(&self) -> Entity { + self.id() + } +} + +// SAFETY: RenderEntity is a newtype around Entity that derives its comparison traits. +unsafe impl EntityEquivalent for RenderEntity {} + +/// Component added on the render world entities to keep track of the corresponding main world entity. +/// +/// Can also be used as a newtype wrapper for main world entities. +#[derive(Component, Deref, Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Reflect)] +#[reflect(Component, Clone)] +pub struct MainEntity(Entity); +impl MainEntity { + #[inline] + pub fn id(&self) -> Entity { + self.0 + } +} + +impl From for MainEntity { + fn from(entity: Entity) -> Self { + MainEntity(entity) + } +} + +impl ContainsEntity for MainEntity { + fn entity(&self) -> Entity { + self.id() + } +} + +// SAFETY: RenderEntity is a newtype around Entity that derives its comparison traits. +unsafe impl EntityEquivalent for MainEntity {} + +/// A [`HashMap`] pre-configured to use [`EntityHash`] hashing with a [`MainEntity`]. +pub type MainEntityHashMap = HashMap; + +/// A [`HashSet`] pre-configured to use [`EntityHash`] hashing with a [`MainEntity`].. +pub type MainEntityHashSet = HashSet; + +/// Marker component that indicates that its entity needs to be despawned at the end of the frame. +#[derive(Component, Copy, Clone, Debug, Default, Reflect)] +#[reflect(Component, Default, Clone)] +pub struct TemporaryRenderEntity; + +/// A record enum to what entities with [`SyncToRenderWorld`] have been added or removed. +#[derive(Debug)] +pub(crate) enum EntityRecord { + /// When an entity is spawned on the main world, notify the render world so that it can spawn a corresponding + /// entity. This contains the main world entity. + Added(Entity), + /// When an entity is despawned on the main world, notify the render world so that the corresponding entity can be + /// despawned. This contains the render world entity. + Removed(RenderEntity), + /// When a component is removed from an entity, notify the render world so that the corresponding component can be + /// removed. This contains the main world entity. + ComponentRemoved(Entity), +} + +// Entity Record in MainWorld pending to Sync +#[derive(Resource, Default, Deref, DerefMut)] +pub(crate) struct PendingSyncEntity { + records: Vec, +} + +pub(crate) fn entity_sync_system(main_world: &mut World, render_world: &mut World) { + main_world.resource_scope(|world, mut pending: Mut| { + // TODO : batching record + for record in pending.drain(..) { + match record { + EntityRecord::Added(e) => { + if let Ok(mut main_entity) = world.get_entity_mut(e) { + match main_entity.entry::() { + bevy_ecs::world::ComponentEntry::Occupied(_) => { + panic!("Attempting to synchronize an entity that has already been synchronized!"); + } + bevy_ecs::world::ComponentEntry::Vacant(entry) => { + let id = render_world.spawn(MainEntity(e)).id(); + + entry.insert(RenderEntity(id)); + } + }; + } + } + EntityRecord::Removed(render_entity) => { + if let Ok(ec) = render_world.get_entity_mut(render_entity.id()) { + ec.despawn(); + }; + } + EntityRecord::ComponentRemoved(main_entity) => { + let Some(mut render_entity) = world.get_mut::(main_entity) else { + continue; + }; + if let Ok(render_world_entity) = render_world.get_entity_mut(render_entity.id()) { + // In order to handle components that extract to derived components, we clear the entity + // and let the extraction system re-add the components. + render_world_entity.despawn(); + + let id = render_world.spawn(MainEntity(main_entity)).id(); + render_entity.0 = id; + } + }, + } + } + }); +} + +pub(crate) fn despawn_temporary_render_entities( + world: &mut World, + state: &mut SystemState>>, + mut local: Local>, +) { + let query = state.get(world); + + local.extend(query.iter()); + + // Ensure next frame allocation keeps order + local.sort_unstable_by_key(|e| e.index()); + for e in local.drain(..).rev() { + world.despawn(e); + } +} + +/// This module exists to keep the complex unsafe code out of the main module. +/// +/// The implementations for both [`MainEntity`] and [`RenderEntity`] should stay in sync, +/// and are based off of the `&T` implementation in `bevy_ecs`. +mod render_entities_world_query_impls { + use super::{MainEntity, RenderEntity}; + + use bevy_ecs::{ + archetype::Archetype, + component::{ComponentId, Components, Tick}, + entity::Entity, + query::{FilteredAccess, QueryData, ReadOnlyQueryData, ReleaseStateQueryData, WorldQuery}, + storage::{Table, TableRow}, + world::{unsafe_world_cell::UnsafeWorldCell, World}, + }; + + /// SAFETY: defers completely to `&RenderEntity` implementation, + /// and then only modifies the output safely. + unsafe impl WorldQuery for RenderEntity { + type Fetch<'w> = <&'static RenderEntity as WorldQuery>::Fetch<'w>; + type State = <&'static RenderEntity as WorldQuery>::State; + + fn shrink_fetch<'wlong: 'wshort, 'wshort>( + fetch: Self::Fetch<'wlong>, + ) -> Self::Fetch<'wshort> { + fetch + } + + #[inline] + unsafe fn init_fetch<'w, 's>( + world: UnsafeWorldCell<'w>, + component_id: &'s ComponentId, + last_run: Tick, + this_run: Tick, + ) -> Self::Fetch<'w> { + // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. + unsafe { + <&RenderEntity as WorldQuery>::init_fetch(world, component_id, last_run, this_run) + } + } + + const IS_DENSE: bool = <&'static RenderEntity as WorldQuery>::IS_DENSE; + + #[inline] + unsafe fn set_archetype<'w, 's>( + fetch: &mut Self::Fetch<'w>, + component_id: &'s ComponentId, + archetype: &'w Archetype, + table: &'w Table, + ) { + // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. + unsafe { + <&RenderEntity as WorldQuery>::set_archetype(fetch, component_id, archetype, table); + } + } + + #[inline] + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w>, + &component_id: &'s ComponentId, + table: &'w Table, + ) { + // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. + unsafe { <&RenderEntity as WorldQuery>::set_table(fetch, &component_id, table) } + } + + fn update_component_access(&component_id: &ComponentId, access: &mut FilteredAccess) { + <&RenderEntity as WorldQuery>::update_component_access(&component_id, access); + } + + fn init_state(world: &mut World) -> ComponentId { + <&RenderEntity as WorldQuery>::init_state(world) + } + + fn get_state(components: &Components) -> Option { + <&RenderEntity as WorldQuery>::get_state(components) + } + + fn matches_component_set( + &state: &ComponentId, + set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + <&RenderEntity as WorldQuery>::matches_component_set(&state, set_contains_id) + } + } + + // SAFETY: Component access of Self::ReadOnly is a subset of Self. + // Self::ReadOnly matches exactly the same archetypes/tables as Self. + unsafe impl QueryData for RenderEntity { + const IS_READ_ONLY: bool = true; + type ReadOnly = RenderEntity; + type Item<'w, 's> = Entity; + + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + item + } + + #[inline(always)] + unsafe fn fetch<'w, 's>( + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, + entity: Entity, + table_row: TableRow, + ) -> Self::Item<'w, 's> { + // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. + let component = + unsafe { <&RenderEntity as QueryData>::fetch(state, fetch, entity, table_row) }; + component.id() + } + } + + // SAFETY: the underlying `Entity` is copied, and no mutable access is provided. + unsafe impl ReadOnlyQueryData for RenderEntity {} + + impl ReleaseStateQueryData for RenderEntity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } + } + + /// SAFETY: defers completely to `&RenderEntity` implementation, + /// and then only modifies the output safely. + unsafe impl WorldQuery for MainEntity { + type Fetch<'w> = <&'static MainEntity as WorldQuery>::Fetch<'w>; + type State = <&'static MainEntity as WorldQuery>::State; + + fn shrink_fetch<'wlong: 'wshort, 'wshort>( + fetch: Self::Fetch<'wlong>, + ) -> Self::Fetch<'wshort> { + fetch + } + + #[inline] + unsafe fn init_fetch<'w, 's>( + world: UnsafeWorldCell<'w>, + component_id: &'s ComponentId, + last_run: Tick, + this_run: Tick, + ) -> Self::Fetch<'w> { + // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. + unsafe { + <&MainEntity as WorldQuery>::init_fetch(world, component_id, last_run, this_run) + } + } + + const IS_DENSE: bool = <&'static MainEntity as WorldQuery>::IS_DENSE; + + #[inline] + unsafe fn set_archetype<'w, 's>( + fetch: &mut Self::Fetch<'w>, + component_id: &ComponentId, + archetype: &'w Archetype, + table: &'w Table, + ) { + // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. + unsafe { + <&MainEntity as WorldQuery>::set_archetype(fetch, component_id, archetype, table); + } + } + + #[inline] + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w>, + &component_id: &'s ComponentId, + table: &'w Table, + ) { + // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. + unsafe { <&MainEntity as WorldQuery>::set_table(fetch, &component_id, table) } + } + + fn update_component_access(&component_id: &ComponentId, access: &mut FilteredAccess) { + <&MainEntity as WorldQuery>::update_component_access(&component_id, access); + } + + fn init_state(world: &mut World) -> ComponentId { + <&MainEntity as WorldQuery>::init_state(world) + } + + fn get_state(components: &Components) -> Option { + <&MainEntity as WorldQuery>::get_state(components) + } + + fn matches_component_set( + &state: &ComponentId, + set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + <&MainEntity as WorldQuery>::matches_component_set(&state, set_contains_id) + } + } + + // SAFETY: Component access of Self::ReadOnly is a subset of Self. + // Self::ReadOnly matches exactly the same archetypes/tables as Self. + unsafe impl QueryData for MainEntity { + const IS_READ_ONLY: bool = true; + type ReadOnly = MainEntity; + type Item<'w, 's> = Entity; + + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + item + } + + #[inline(always)] + unsafe fn fetch<'w, 's>( + state: &'s Self::State, + fetch: &mut Self::Fetch<'w>, + entity: Entity, + table_row: TableRow, + ) -> Self::Item<'w, 's> { + // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. + let component = + unsafe { <&MainEntity as QueryData>::fetch(state, fetch, entity, table_row) }; + component.id() + } + } + + // SAFETY: the underlying `Entity` is copied, and no mutable access is provided. + unsafe impl ReadOnlyQueryData for MainEntity {} + + impl ReleaseStateQueryData for MainEntity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } + } +} + +#[cfg(test)] +mod tests { + use bevy_ecs::{ + component::Component, + entity::Entity, + lifecycle::{Add, Remove}, + observer::On, + query::With, + system::{Query, ResMut}, + world::World, + }; + + use super::{ + entity_sync_system, EntityRecord, MainEntity, PendingSyncEntity, RenderEntity, + SyncToRenderWorld, + }; + + #[derive(Component)] + struct RenderDataComponent; + + #[test] + fn sync_world() { + let mut main_world = World::new(); + let mut render_world = World::new(); + main_world.init_resource::(); + + main_world.add_observer( + |add: On, mut pending: ResMut| { + pending.push(EntityRecord::Added(add.entity)); + }, + ); + main_world.add_observer( + |remove: On, + mut pending: ResMut, + query: Query<&RenderEntity>| { + if let Ok(e) = query.get(remove.entity) { + pending.push(EntityRecord::Removed(*e)); + }; + }, + ); + + // spawn some empty entities for test + for _ in 0..99 { + main_world.spawn_empty(); + } + + // spawn + let main_entity = main_world + .spawn(RenderDataComponent) + // indicates that its entity needs to be synchronized to the render world + .insert(SyncToRenderWorld) + .id(); + + entity_sync_system(&mut main_world, &mut render_world); + + let mut q = render_world.query_filtered::>(); + + // Only one synchronized entity + assert!(q.iter(&render_world).count() == 1); + + let render_entity = q.single(&render_world).unwrap(); + let render_entity_component = main_world.get::(main_entity).unwrap(); + + assert!(render_entity_component.id() == render_entity); + + let main_entity_component = render_world + .get::(render_entity_component.id()) + .unwrap(); + + assert!(main_entity_component.id() == main_entity); + + // despawn + main_world.despawn(main_entity); + + entity_sync_system(&mut main_world, &mut render_world); + + // Only one synchronized entity + assert!(q.iter(&render_world).count() == 0); + } +} diff --git a/crates/libmarathon/src/render/texture/fallback_image.rs b/crates/libmarathon/src/render/texture/fallback_image.rs new file mode 100644 index 0000000..bea52ed --- /dev/null +++ b/crates/libmarathon/src/render/texture/fallback_image.rs @@ -0,0 +1,272 @@ +use crate::render::{ + render_resource::*, + renderer::{RenderDevice, RenderQueue}, + texture::{DefaultImageSampler, GpuImage}, +}; +use bevy_asset::RenderAssetUsages; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + prelude::{FromWorld, Res, ResMut}, + resource::Resource, + system::SystemParam, +}; +use bevy_image::{BevyDefault, Image, ImageSampler, TextureFormatPixelInfo}; +use bevy_platform::collections::HashMap; + +/// A [`RenderApp`](crate::RenderApp) resource that contains the default "fallback image", +/// which can be used in situations where an image was not explicitly defined. The most common +/// use case is [`AsBindGroup`] implementations (such as materials) that support optional textures. +/// +/// Defaults to a 1x1 fully opaque white texture, (1.0, 1.0, 1.0, 1.0) which makes multiplying +/// it with other colors a no-op. +#[derive(Resource)] +pub struct FallbackImage { + /// Fallback image for [`TextureViewDimension::D1`]. + pub d1: GpuImage, + /// Fallback image for [`TextureViewDimension::D2`]. + pub d2: GpuImage, + /// Fallback image for [`TextureViewDimension::D2Array`]. + pub d2_array: GpuImage, + /// Fallback image for [`TextureViewDimension::Cube`]. + pub cube: GpuImage, + /// Fallback image for [`TextureViewDimension::CubeArray`]. + pub cube_array: GpuImage, + /// Fallback image for [`TextureViewDimension::D3`]. + pub d3: GpuImage, +} + +impl FallbackImage { + /// Returns the appropriate fallback image for the given texture dimension. + pub fn get(&self, texture_dimension: TextureViewDimension) -> &GpuImage { + match texture_dimension { + TextureViewDimension::D1 => &self.d1, + TextureViewDimension::D2 => &self.d2, + TextureViewDimension::D2Array => &self.d2_array, + TextureViewDimension::Cube => &self.cube, + TextureViewDimension::CubeArray => &self.cube_array, + TextureViewDimension::D3 => &self.d3, + } + } +} + +/// A [`RenderApp`](crate::RenderApp) resource that contains a _zero-filled_ "fallback image", +/// which can be used in place of [`FallbackImage`], when a fully transparent or black fallback +/// is required instead of fully opaque white. +/// +/// Defaults to a 1x1 fully transparent black texture, (0.0, 0.0, 0.0, 0.0) which makes adding +/// or alpha-blending it to other colors a no-op. +#[derive(Resource, Deref)] +pub struct FallbackImageZero(GpuImage); + +/// A [`RenderApp`](crate::RenderApp) resource that contains a "cubemap fallback image", +/// which can be used in situations where an image was not explicitly defined. The most common +/// use case is [`AsBindGroup`] implementations (such as materials) that support optional textures. +#[derive(Resource, Deref)] +pub struct FallbackImageCubemap(GpuImage); + +fn fallback_image_new( + render_device: &RenderDevice, + render_queue: &RenderQueue, + default_sampler: &DefaultImageSampler, + format: TextureFormat, + dimension: TextureViewDimension, + samples: u32, + value: u8, +) -> GpuImage { + // TODO make this configurable per channel + + let extents = Extent3d { + width: 1, + height: 1, + depth_or_array_layers: match dimension { + TextureViewDimension::Cube | TextureViewDimension::CubeArray => 6, + _ => 1, + }, + }; + + // We can't create textures with data when it's a depth texture or when using multiple samples + let create_texture_with_data = !format.is_depth_stencil_format() && samples == 1; + + let image_dimension = dimension.compatible_texture_dimension(); + let mut image = if create_texture_with_data { + let data = vec![value; format.pixel_size().unwrap_or(0)]; + Image::new_fill( + extents, + image_dimension, + &data, + format, + RenderAssetUsages::RENDER_WORLD, + ) + } else { + let mut image = Image::default_uninit(); + image.texture_descriptor.dimension = TextureDimension::D2; + image.texture_descriptor.size = extents; + image.texture_descriptor.format = format; + image + }; + image.texture_descriptor.sample_count = samples; + if image_dimension == TextureDimension::D2 { + image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT; + } + + let texture = if create_texture_with_data { + render_device.create_texture_with_data( + render_queue, + &image.texture_descriptor, + TextureDataOrder::default(), + &image.data.expect("Image has no data"), + ) + } else { + render_device.create_texture(&image.texture_descriptor) + }; + + let texture_view = texture.create_view(&TextureViewDescriptor { + dimension: Some(dimension), + array_layer_count: Some(extents.depth_or_array_layers), + ..TextureViewDescriptor::default() + }); + let sampler = match image.sampler { + ImageSampler::Default => (**default_sampler).clone(), + ImageSampler::Descriptor(ref descriptor) => { + render_device.create_sampler(&descriptor.as_wgpu()) + } + }; + GpuImage { + texture, + texture_view, + texture_format: image.texture_descriptor.format, + sampler, + size: image.texture_descriptor.size, + mip_level_count: image.texture_descriptor.mip_level_count, + } +} + +impl FromWorld for FallbackImage { + fn from_world(world: &mut bevy_ecs::prelude::World) -> Self { + let render_device = world.resource::(); + let render_queue = world.resource::(); + let default_sampler = world.resource::(); + Self { + d1: fallback_image_new( + render_device, + render_queue, + default_sampler, + TextureFormat::bevy_default(), + TextureViewDimension::D1, + 1, + 255, + ), + d2: fallback_image_new( + render_device, + render_queue, + default_sampler, + TextureFormat::bevy_default(), + TextureViewDimension::D2, + 1, + 255, + ), + d2_array: fallback_image_new( + render_device, + render_queue, + default_sampler, + TextureFormat::bevy_default(), + TextureViewDimension::D2Array, + 1, + 255, + ), + cube: fallback_image_new( + render_device, + render_queue, + default_sampler, + TextureFormat::bevy_default(), + TextureViewDimension::Cube, + 1, + 255, + ), + cube_array: fallback_image_new( + render_device, + render_queue, + default_sampler, + TextureFormat::bevy_default(), + TextureViewDimension::CubeArray, + 1, + 255, + ), + d3: fallback_image_new( + render_device, + render_queue, + default_sampler, + TextureFormat::bevy_default(), + TextureViewDimension::D3, + 1, + 255, + ), + } + } +} + +impl FromWorld for FallbackImageZero { + fn from_world(world: &mut bevy_ecs::prelude::World) -> Self { + let render_device = world.resource::(); + let render_queue = world.resource::(); + let default_sampler = world.resource::(); + Self(fallback_image_new( + render_device, + render_queue, + default_sampler, + TextureFormat::bevy_default(), + TextureViewDimension::D2, + 1, + 0, + )) + } +} + +impl FromWorld for FallbackImageCubemap { + fn from_world(world: &mut bevy_ecs::prelude::World) -> Self { + let render_device = world.resource::(); + let render_queue = world.resource::(); + let default_sampler = world.resource::(); + Self(fallback_image_new( + render_device, + render_queue, + default_sampler, + TextureFormat::bevy_default(), + TextureViewDimension::Cube, + 1, + 255, + )) + } +} + +/// A Cache of fallback textures that uses the sample count and `TextureFormat` as a key +/// +/// # WARNING +/// Images using MSAA with sample count > 1 are not initialized with data, therefore, +/// you shouldn't sample them before writing data to them first. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct FallbackImageFormatMsaaCache(HashMap<(u32, TextureFormat), GpuImage>); + +#[derive(SystemParam)] +pub struct FallbackImageMsaa<'w> { + cache: ResMut<'w, FallbackImageFormatMsaaCache>, + render_device: Res<'w, RenderDevice>, + render_queue: Res<'w, RenderQueue>, + default_sampler: Res<'w, DefaultImageSampler>, +} + +impl<'w> FallbackImageMsaa<'w> { + pub fn image_for_samplecount(&mut self, sample_count: u32, format: TextureFormat) -> &GpuImage { + self.cache.entry((sample_count, format)).or_insert_with(|| { + fallback_image_new( + &self.render_device, + &self.render_queue, + &self.default_sampler, + format, + TextureViewDimension::D2, + sample_count, + 255, + ) + }) + } +} diff --git a/crates/libmarathon/src/render/texture/gpu_image.rs b/crates/libmarathon/src/render/texture/gpu_image.rs new file mode 100644 index 0000000..f72ad79 --- /dev/null +++ b/crates/libmarathon/src/render/texture/gpu_image.rs @@ -0,0 +1,131 @@ +use crate::render::{ + render_asset::{PrepareAssetError, RenderAsset}, + render_resource::{DefaultImageSampler, Sampler, Texture, TextureView}, + renderer::{RenderDevice, RenderQueue}, +}; +use bevy_asset::{AssetId, RenderAssetUsages}; +use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; +use bevy_image::{Image, ImageSampler}; +use bevy_math::{AspectRatio, UVec2}; +use tracing::warn; +use wgpu::{Extent3d, TextureFormat, TextureViewDescriptor}; + +/// The GPU-representation of an [`Image`]. +/// Consists of the [`Texture`], its [`TextureView`] and the corresponding [`Sampler`], and the texture's size. +#[derive(Debug, Clone)] +pub struct GpuImage { + pub texture: Texture, + pub texture_view: TextureView, + pub texture_format: TextureFormat, + pub sampler: Sampler, + pub size: Extent3d, + pub mip_level_count: u32, +} + +impl RenderAsset for GpuImage { + type SourceAsset = Image; + type Param = ( + SRes, + SRes, + SRes, + ); + + #[inline] + fn asset_usage(image: &Self::SourceAsset) -> RenderAssetUsages { + image.asset_usage + } + + #[inline] + fn byte_len(image: &Self::SourceAsset) -> Option { + image.data.as_ref().map(Vec::len) + } + + /// Converts the extracted image into a [`GpuImage`]. + fn prepare_asset( + image: Self::SourceAsset, + _: AssetId, + (render_device, render_queue, default_sampler): &mut SystemParamItem, + previous_asset: Option<&Self>, + ) -> Result> { + let texture = if let Some(ref data) = image.data { + render_device.create_texture_with_data( + render_queue, + &image.texture_descriptor, + image.data_order, + data, + ) + } else { + let new_texture = render_device.create_texture(&image.texture_descriptor); + if image.copy_on_resize { + if let Some(previous) = previous_asset { + let mut command_encoder = + render_device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("copy_image_on_resize"), + }); + let copy_size = Extent3d { + width: image.texture_descriptor.size.width.min(previous.size.width), + height: image + .texture_descriptor + .size + .height + .min(previous.size.height), + depth_or_array_layers: image + .texture_descriptor + .size + .depth_or_array_layers + .min(previous.size.depth_or_array_layers), + }; + + command_encoder.copy_texture_to_texture( + previous.texture.as_image_copy(), + new_texture.as_image_copy(), + copy_size, + ); + render_queue.submit([command_encoder.finish()]); + } else { + warn!("No previous asset to copy from for image: {:?}", image); + } + } + new_texture + }; + + let texture_view = texture.create_view( + image + .texture_view_descriptor + .or_else(|| Some(TextureViewDescriptor::default())) + .as_ref() + .unwrap(), + ); + let sampler = match image.sampler { + ImageSampler::Default => (***default_sampler).clone(), + ImageSampler::Descriptor(descriptor) => { + render_device.create_sampler(&descriptor.as_wgpu()) + } + }; + + Ok(GpuImage { + texture, + texture_view, + texture_format: image.texture_descriptor.format, + sampler, + size: image.texture_descriptor.size, + mip_level_count: image.texture_descriptor.mip_level_count, + }) + } +} + +impl GpuImage { + /// Returns the aspect ratio (width / height) of a 2D image. + #[inline] + pub fn aspect_ratio(&self) -> AspectRatio { + AspectRatio::try_from_pixels(self.size.width, self.size.height).expect( + "Failed to calculate aspect ratio: Image dimensions must be positive, non-zero values", + ) + } + + /// Returns the size of a 2D image. + #[inline] + pub fn size_2d(&self) -> UVec2 { + UVec2::new(self.size.width, self.size.height) + } +} diff --git a/crates/libmarathon/src/render/texture/manual_texture_view.rs b/crates/libmarathon/src/render/texture/manual_texture_view.rs new file mode 100644 index 0000000..898d041 --- /dev/null +++ b/crates/libmarathon/src/render/texture/manual_texture_view.rs @@ -0,0 +1,68 @@ +use bevy_camera::ManualTextureViewHandle; +use bevy_ecs::{prelude::Component, resource::Resource}; +use bevy_image::BevyDefault; +use bevy_math::UVec2; +use bevy_platform::collections::HashMap; +use macros::ExtractResource; +use wgpu::TextureFormat; + +use crate::render::render_resource::TextureView; + +/// A manually managed [`TextureView`] for use as a [`bevy_camera::RenderTarget`]. +#[derive(Debug, Clone, Component)] +pub struct ManualTextureView { + pub texture_view: TextureView, + pub size: UVec2, + pub format: TextureFormat, +} + +impl ManualTextureView { + pub fn with_default_format(texture_view: TextureView, size: UVec2) -> Self { + Self { + texture_view, + size, + format: TextureFormat::bevy_default(), + } + } +} + +/// Resource that stores manually managed [`ManualTextureView`]s for use as a [`RenderTarget`](bevy_camera::RenderTarget). +/// This type dereferences to a `HashMap`. +/// To add a new texture view, pick a new [`ManualTextureViewHandle`] and insert it into the map. +/// Then, to render to the view, set a [`Camera`](bevy_camera::Camera)s `target` to `RenderTarget::TextureView(handle)`. +/// ```ignore +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # world.insert_resource(ManualTextureViews::default()); +/// # let texture_view = todo!(); +/// let manual_views = world.resource_mut::(); +/// let manual_view = ManualTextureView::with_default_format(texture_view, UVec2::new(1024, 1024)); +/// +/// // Choose an unused handle value; it's likely only you are inserting manual views. +/// const MANUAL_VIEW_HANDLE: ManualTextureViewHandle = ManualTextureViewHandle::new(42); +/// manual_views.insert(MANUAL_VIEW_HANDLE, manual_view); +/// +/// // Now you can spawn a Cemera that renders to the manual view: +/// # use bevy_camera::{Camera, RenderTarget}; +/// world.spawn(Camera { +/// target: RenderTarget::TextureView(MANUAL_VIEW_HANDLE), +/// ..Default::default() +/// }); +/// ``` +/// Bevy will then use the `ManualTextureViews` resource to find your texture view and render to it. +#[derive(Default, Clone, Resource, ExtractResource)] +pub struct ManualTextureViews(HashMap); + +impl core::ops::Deref for ManualTextureViews { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl core::ops::DerefMut for ManualTextureViews { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/crates/libmarathon/src/render/texture/mod.rs b/crates/libmarathon/src/render/texture/mod.rs new file mode 100644 index 0000000..13e042d --- /dev/null +++ b/crates/libmarathon/src/render/texture/mod.rs @@ -0,0 +1,73 @@ +mod fallback_image; +mod gpu_image; +mod manual_texture_view; +mod texture_attachment; +mod texture_cache; + +pub use crate::render::render_resource::DefaultImageSampler; +use bevy_image::{CompressedImageFormatSupport, CompressedImageFormats, ImageLoader, ImagePlugin}; +pub use fallback_image::*; +pub use gpu_image::*; +pub use manual_texture_view::*; +pub use texture_attachment::*; +pub use texture_cache::*; + +use crate::render::{ + extract_resource::ExtractResourcePlugin, render_asset::RenderAssetPlugin, + renderer::RenderDevice, Render, RenderApp, RenderSystems, +}; +use bevy_app::{App, Plugin}; +use bevy_asset::AssetApp; +use bevy_ecs::prelude::*; +use tracing::warn; + +#[derive(Default)] +pub struct TexturePlugin; + +impl Plugin for TexturePlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + RenderAssetPlugin::::default(), + ExtractResourcePlugin::::default(), + )) + .init_resource::(); + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.init_resource::().add_systems( + Render, + update_texture_cache_system.in_set(RenderSystems::Cleanup), + ); + } + } + + fn finish(&self, app: &mut App) { + if !ImageLoader::SUPPORTED_FORMATS.is_empty() { + let supported_compressed_formats = if let Some(resource) = + app.world().get_resource::() + { + resource.0 + } else { + warn!("CompressedImageFormatSupport resource not found. It should either be initialized in finish() of \ + RenderPlugin, or manually if not using the RenderPlugin or the WGPU backend."); + CompressedImageFormats::NONE + }; + + app.register_asset_loader(ImageLoader::new(supported_compressed_formats)); + } + let default_sampler = app.get_added_plugins::()[0] + .default_sampler + .clone(); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + let default_sampler = { + let device = render_app.world().resource::(); + device.create_sampler(&default_sampler.as_wgpu()) + }; + render_app + .insert_resource(DefaultImageSampler(default_sampler)) + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::(); + } + } +} diff --git a/crates/libmarathon/src/render/texture/texture_attachment.rs b/crates/libmarathon/src/render/texture/texture_attachment.rs new file mode 100644 index 0000000..fb5a3cf --- /dev/null +++ b/crates/libmarathon/src/render/texture/texture_attachment.rs @@ -0,0 +1,162 @@ +use super::CachedTexture; +use crate::render::render_resource::{TextureFormat, TextureView}; +use std::sync::Arc; +use bevy_color::LinearRgba; +use core::sync::atomic::{AtomicBool, Ordering}; +use wgpu::{ + LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp, +}; + +/// A wrapper for a [`CachedTexture`] that is used as a [`RenderPassColorAttachment`]. +#[derive(Clone)] +pub struct ColorAttachment { + pub texture: CachedTexture, + pub resolve_target: Option, + clear_color: Option, + is_first_call: Arc, +} + +impl ColorAttachment { + pub fn new( + texture: CachedTexture, + resolve_target: Option, + clear_color: Option, + ) -> Self { + Self { + texture, + resolve_target, + clear_color, + is_first_call: Arc::new(AtomicBool::new(true)), + } + } + + /// Get this texture view as an attachment. The attachment will be cleared with a value of + /// `clear_color` if this is the first time calling this function, otherwise it will be loaded. + /// + /// The returned attachment will always have writing enabled (`store: StoreOp::Load`). + pub fn get_attachment(&self) -> RenderPassColorAttachment<'_> { + if let Some(resolve_target) = self.resolve_target.as_ref() { + let first_call = self.is_first_call.fetch_and(false, Ordering::SeqCst); + + RenderPassColorAttachment { + view: &resolve_target.default_view, + depth_slice: None, + resolve_target: Some(&self.texture.default_view), + ops: Operations { + load: match (self.clear_color, first_call) { + (Some(clear_color), true) => LoadOp::Clear(clear_color.into()), + (None, _) | (Some(_), false) => LoadOp::Load, + }, + store: StoreOp::Store, + }, + } + } else { + self.get_unsampled_attachment() + } + } + + /// Get this texture view as an attachment, without the resolve target. The attachment will be cleared with + /// a value of `clear_color` if this is the first time calling this function, otherwise it will be loaded. + /// + /// The returned attachment will always have writing enabled (`store: StoreOp::Load`). + pub fn get_unsampled_attachment(&self) -> RenderPassColorAttachment<'_> { + let first_call = self.is_first_call.fetch_and(false, Ordering::SeqCst); + + RenderPassColorAttachment { + view: &self.texture.default_view, + depth_slice: None, + resolve_target: None, + ops: Operations { + load: match (self.clear_color, first_call) { + (Some(clear_color), true) => LoadOp::Clear(clear_color.into()), + (None, _) | (Some(_), false) => LoadOp::Load, + }, + store: StoreOp::Store, + }, + } + } + + pub(crate) fn mark_as_cleared(&self) { + self.is_first_call.store(false, Ordering::SeqCst); + } +} + +/// A wrapper for a [`TextureView`] that is used as a depth-only [`RenderPassDepthStencilAttachment`]. +#[derive(Clone)] +pub struct DepthAttachment { + pub view: TextureView, + clear_value: Option, + is_first_call: Arc, +} + +impl DepthAttachment { + pub fn new(view: TextureView, clear_value: Option) -> Self { + Self { + view, + clear_value, + is_first_call: Arc::new(AtomicBool::new(clear_value.is_some())), + } + } + + /// Get this texture view as an attachment. The attachment will be cleared with a value of + /// `clear_value` if this is the first time calling this function with `store` == [`StoreOp::Store`], + /// and a clear value was provided, otherwise it will be loaded. + pub fn get_attachment(&self, store: StoreOp) -> RenderPassDepthStencilAttachment<'_> { + let first_call = self + .is_first_call + .fetch_and(store != StoreOp::Store, Ordering::SeqCst); + + RenderPassDepthStencilAttachment { + view: &self.view, + depth_ops: Some(Operations { + load: if first_call { + // If first_call is true, then a clear value will always have been provided in the constructor + LoadOp::Clear(self.clear_value.unwrap()) + } else { + LoadOp::Load + }, + store, + }), + stencil_ops: None, + } + } +} + +/// A wrapper for a [`TextureView`] that is used as a [`RenderPassColorAttachment`] for a view +/// target's final output texture. +#[derive(Clone)] +pub struct OutputColorAttachment { + pub view: TextureView, + pub format: TextureFormat, + is_first_call: Arc, +} + +impl OutputColorAttachment { + pub fn new(view: TextureView, format: TextureFormat) -> Self { + Self { + view, + format, + is_first_call: Arc::new(AtomicBool::new(true)), + } + } + + /// Get this texture view as an attachment. The attachment will be cleared with a value of + /// the provided `clear_color` if this is the first time calling this function, otherwise it + /// will be loaded. + pub fn get_attachment(&self, clear_color: Option) -> RenderPassColorAttachment<'_> { + let first_call = self.is_first_call.fetch_and(false, Ordering::SeqCst); + + RenderPassColorAttachment { + view: &self.view, + depth_slice: None, + resolve_target: None, + ops: Operations { + load: match (clear_color, first_call) { + (Some(clear_color), true) => LoadOp::Clear(clear_color.into()), + (None, _) | (Some(_), false) => LoadOp::Load, + }, + store: StoreOp::Store, + }, + } + } +} diff --git a/crates/libmarathon/src/render/texture/texture_cache.rs b/crates/libmarathon/src/render/texture/texture_cache.rs new file mode 100644 index 0000000..6d79d4b --- /dev/null +++ b/crates/libmarathon/src/render/texture/texture_cache.rs @@ -0,0 +1,108 @@ +use crate::render::{ + render_resource::{Texture, TextureView}, + renderer::RenderDevice, +}; +use bevy_ecs::{prelude::ResMut, resource::Resource}; +use bevy_platform::collections::{hash_map::Entry, HashMap}; +use wgpu::{TextureDescriptor, TextureViewDescriptor}; + +/// The internal representation of a [`CachedTexture`] used to track whether it was recently used +/// and is currently taken. +struct CachedTextureMeta { + texture: Texture, + default_view: TextureView, + taken: bool, + frames_since_last_use: usize, +} + +/// A cached GPU [`Texture`] with corresponding [`TextureView`]. +/// +/// This is useful for textures that are created repeatedly (each frame) in the rendering process +/// to reduce the amount of GPU memory allocations. +#[derive(Clone)] +pub struct CachedTexture { + pub texture: Texture, + pub default_view: TextureView, +} + +/// This resource caches textures that are created repeatedly in the rendering process and +/// are only required for one frame. +#[derive(Resource, Default)] +pub struct TextureCache { + textures: HashMap, Vec>, +} + +impl TextureCache { + /// Retrieves a texture that matches the `descriptor`. If no matching one is found a new + /// [`CachedTexture`] is created. + pub fn get( + &mut self, + render_device: &RenderDevice, + descriptor: TextureDescriptor<'static>, + ) -> CachedTexture { + match self.textures.entry(descriptor) { + Entry::Occupied(mut entry) => { + for texture in entry.get_mut().iter_mut() { + if !texture.taken { + texture.frames_since_last_use = 0; + texture.taken = true; + return CachedTexture { + texture: texture.texture.clone(), + default_view: texture.default_view.clone(), + }; + } + } + + let texture = render_device.create_texture(&entry.key().clone()); + let default_view = texture.create_view(&TextureViewDescriptor::default()); + entry.get_mut().push(CachedTextureMeta { + texture: texture.clone(), + default_view: default_view.clone(), + frames_since_last_use: 0, + taken: true, + }); + CachedTexture { + texture, + default_view, + } + } + Entry::Vacant(entry) => { + let texture = render_device.create_texture(entry.key()); + let default_view = texture.create_view(&TextureViewDescriptor::default()); + entry.insert(vec![CachedTextureMeta { + texture: texture.clone(), + default_view: default_view.clone(), + taken: true, + frames_since_last_use: 0, + }]); + CachedTexture { + texture, + default_view, + } + } + } + } + + /// Returns `true` if the texture cache contains no textures. + pub fn is_empty(&self) -> bool { + self.textures.is_empty() + } + + /// Updates the cache and only retains recently used textures. + pub fn update(&mut self) { + self.textures.retain(|_, textures| { + for texture in textures.iter_mut() { + texture.frames_since_last_use += 1; + texture.taken = false; + } + + textures.retain(|texture| texture.frames_since_last_use < 3); + !textures.is_empty() + }); + } +} + +/// Updates the [`TextureCache`] to only retains recently used textures. +pub fn update_texture_cache_system(mut texture_cache: ResMut) { + texture_cache.update(); +} diff --git a/crates/libmarathon/src/render/tonemapping/lut_bindings.wgsl b/crates/libmarathon/src/render/tonemapping/lut_bindings.wgsl new file mode 100644 index 0000000..997f9ef --- /dev/null +++ b/crates/libmarathon/src/render/tonemapping/lut_bindings.wgsl @@ -0,0 +1,5 @@ +#define_import_path bevy_core_pipeline::tonemapping_lut_bindings + +@group(0) @binding(#TONEMAPPING_LUT_TEXTURE_BINDING_INDEX) var dt_lut_texture: texture_3d; +@group(0) @binding(#TONEMAPPING_LUT_SAMPLER_BINDING_INDEX) var dt_lut_sampler: sampler; + diff --git a/crates/libmarathon/src/render/tonemapping/luts/AgX-default_contrast.ktx2 b/crates/libmarathon/src/render/tonemapping/luts/AgX-default_contrast.ktx2 new file mode 100644 index 0000000000000000000000000000000000000000..040fb1def23833a582f2d91fa5fdaa47ca80ebb2 GIT binary patch literal 17842 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 NBN*2(-79Yn{XcVgQr-Xn literal 0 HcmV?d00001 diff --git a/crates/libmarathon/src/render/tonemapping/luts/Blender_-11_12.ktx2 b/crates/libmarathon/src/render/tonemapping/luts/Blender_-11_12.ktx2 new file mode 100644 index 0000000000000000000000000000000000000000..db07c847a1879aabe14cbcc1a4d00ec402bc9f1e GIT binary patch literal 308905 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

    D$025jEGodL26#* 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ⓈkjO;BTm_qYH}B_+(1p^=rc5NgMYH2+&NR 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≠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�` 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

    w35K2GRWDc8!ncXCZ}m&D>v@^XT1M!k?G*=X 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

    al%5%Tu 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 literal 0 HcmV?d00001 diff --git a/crates/libmarathon/src/render/tonemapping/mod.rs b/crates/libmarathon/src/render/tonemapping/mod.rs new file mode 100644 index 0000000..272dcaa --- /dev/null +++ b/crates/libmarathon/src/render/tonemapping/mod.rs @@ -0,0 +1,456 @@ +use bevy_app::prelude::*; +use bevy_asset::{ + embedded_asset, load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages, +}; +use bevy_camera::Camera; +use bevy_ecs::prelude::*; +use bevy_image::{CompressedImageFormats, Image, ImageSampler, ImageType}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use crate::render::{ + extract_component::{ExtractComponent, ExtractComponentPlugin}, + extract_resource::{ExtractResource, ExtractResourcePlugin}, + render_asset::RenderAssets, + render_resource::{ + binding_types::{sampler, texture_2d, texture_3d, uniform_buffer}, + *, + }, + renderer::RenderDevice, + texture::{FallbackImage, GpuImage}, + view::{ExtractedView, ViewTarget, ViewUniform}, + Render, RenderApp, RenderStartup, RenderSystems, +}; +use bevy_shader::{load_shader_library, Shader, ShaderDefVal}; +use bitflags::bitflags; +#[cfg(not(feature = "tonemapping_luts"))] +use tracing::error; + +mod node; + +use bevy_utils::default; +pub use node::TonemappingNode; + +use crate::render::FullscreenShader; + +/// 3D LUT (look up table) textures used for tonemapping +#[derive(Resource, Clone, ExtractResource)] +pub struct TonemappingLuts { + pub blender_filmic: Handle, + pub agx: Handle, + pub tony_mc_mapface: Handle, +} + +pub struct TonemappingPlugin; + +impl Plugin for TonemappingPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "tonemapping_shared.wgsl"); + load_shader_library!(app, "lut_bindings.wgsl"); + + embedded_asset!(app, "tonemapping.wgsl"); + + if !app.world().is_resource_added::() { + let mut images = app.world_mut().resource_mut::>(); + + #[cfg(feature = "tonemapping_luts")] + let tonemapping_luts = { + TonemappingLuts { + blender_filmic: images.add(setup_tonemapping_lut_image( + include_bytes!("luts/Blender_-11_12.ktx2"), + ImageType::Extension("ktx2"), + )), + agx: images.add(setup_tonemapping_lut_image( + include_bytes!("luts/AgX-default_contrast.ktx2"), + ImageType::Extension("ktx2"), + )), + tony_mc_mapface: images.add(setup_tonemapping_lut_image( + include_bytes!("luts/tony_mc_mapface.ktx2"), + ImageType::Extension("ktx2"), + )), + } + }; + + #[cfg(not(feature = "tonemapping_luts"))] + let tonemapping_luts = { + let placeholder = images.add(lut_placeholder()); + TonemappingLuts { + blender_filmic: placeholder.clone(), + agx: placeholder.clone(), + tony_mc_mapface: placeholder, + } + }; + + app.insert_resource(tonemapping_luts); + } + + app.add_plugins(ExtractResourcePlugin::::default()); + + app.add_plugins(( + ExtractComponentPlugin::::default(), + ExtractComponentPlugin::::default(), + )); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app + .init_resource::>() + .add_systems(RenderStartup, init_tonemapping_pipeline) + .add_systems( + Render, + prepare_view_tonemapping_pipelines.in_set(RenderSystems::Prepare), + ); + } +} + +#[derive(Resource)] +pub struct TonemappingPipeline { + texture_bind_group: BindGroupLayout, + sampler: Sampler, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, +} + +/// Optionally enables a tonemapping shader that attempts to map linear input stimulus into a perceptually uniform image for a given [`Camera`] entity. +#[derive( + Component, Debug, Hash, Clone, Copy, Reflect, Default, ExtractComponent, PartialEq, Eq, +)] +#[extract_component_filter(With)] +#[reflect(Component, Debug, Hash, Default, PartialEq)] +pub enum Tonemapping { + /// Bypass tonemapping. + None, + /// Suffers from lots hue shifting, brights don't desaturate naturally. + /// Bright primaries and secondaries don't desaturate at all. + Reinhard, + /// Suffers from hue shifting. Brights don't desaturate much at all across the spectrum. + ReinhardLuminance, + /// Same base implementation that Godot 4.0 uses for Tonemap ACES. + /// + /// Not neutral, has a very specific aesthetic, intentional and dramatic hue shifting. + /// Bright greens and reds turn orange. Bright blues turn magenta. + /// Significantly increased contrast. Brights desaturate across the spectrum. + AcesFitted, + /// By Troy Sobotka + /// + /// Very neutral. Image is somewhat desaturated when compared to other tonemappers. + /// Little to no hue shifting. Subtle [Abney shifting](https://en.wikipedia.org/wiki/Abney_effect). + /// NOTE: Requires the `tonemapping_luts` cargo feature. + AgX, + /// By Tomasz Stachowiak + /// Has little hue shifting in the darks and mids, but lots in the brights. Brights desaturate across the spectrum. + /// Is sort of between Reinhard and `ReinhardLuminance`. Conceptually similar to reinhard-jodie. + /// Designed as a compromise if you want e.g. decent skin tones in low light, but can't afford to re-do your + /// VFX to look good without hue shifting. + SomewhatBoringDisplayTransform, + /// Current Bevy default. + /// By Tomasz Stachowiak + /// + /// Very neutral. Subtle but intentional hue shifting. Brights desaturate across the spectrum. + /// Comment from author: + /// Tony is a display transform intended for real-time applications such as games. + /// It is intentionally boring, does not increase contrast or saturation, and stays close to the + /// input stimulus where compression isn't necessary. + /// Brightness-equivalent luminance of the input stimulus is compressed. The non-linearity resembles Reinhard. + /// Color hues are preserved during compression, except for a deliberate [Bezold–Brücke shift](https://en.wikipedia.org/wiki/Bezold%E2%80%93Br%C3%BCcke_shift). + /// To avoid posterization, selective desaturation is employed, with care to avoid the [Abney effect](https://en.wikipedia.org/wiki/Abney_effect). + /// NOTE: Requires the `tonemapping_luts` cargo feature. + #[default] + TonyMcMapface, + /// Default Filmic Display Transform from blender. + /// Somewhat neutral. Suffers from hue shifting. Brights desaturate across the spectrum. + /// NOTE: Requires the `tonemapping_luts` cargo feature. + BlenderFilmic, +} + +impl Tonemapping { + pub fn is_enabled(&self) -> bool { + *self != Tonemapping::None + } +} + +bitflags! { + /// Various flags describing what tonemapping needs to do. + /// + /// This allows the shader to skip unneeded steps. + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] + pub struct TonemappingPipelineKeyFlags: u8 { + /// The hue needs to be changed. + const HUE_ROTATE = 0x01; + /// The white balance needs to be adjusted. + const WHITE_BALANCE = 0x02; + /// Saturation/contrast/gamma/gain/lift for one or more sections + /// (shadows, midtones, highlights) need to be adjusted. + const SECTIONAL_COLOR_GRADING = 0x04; + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct TonemappingPipelineKey { + deband_dither: DebandDither, + tonemapping: Tonemapping, + flags: TonemappingPipelineKeyFlags, +} + +impl SpecializedRenderPipeline for TonemappingPipeline { + type Key = TonemappingPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mut shader_defs = Vec::new(); + + shader_defs.push(ShaderDefVal::UInt( + "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(), + 3, + )); + shader_defs.push(ShaderDefVal::UInt( + "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(), + 4, + )); + + if let DebandDither::Enabled = key.deband_dither { + shader_defs.push("DEBAND_DITHER".into()); + } + + // Define shader flags depending on the color grading options in use. + if key.flags.contains(TonemappingPipelineKeyFlags::HUE_ROTATE) { + shader_defs.push("HUE_ROTATE".into()); + } + if key + .flags + .contains(TonemappingPipelineKeyFlags::WHITE_BALANCE) + { + shader_defs.push("WHITE_BALANCE".into()); + } + if key + .flags + .contains(TonemappingPipelineKeyFlags::SECTIONAL_COLOR_GRADING) + { + shader_defs.push("SECTIONAL_COLOR_GRADING".into()); + } + + match key.tonemapping { + Tonemapping::None => shader_defs.push("TONEMAP_METHOD_NONE".into()), + Tonemapping::Reinhard => shader_defs.push("TONEMAP_METHOD_REINHARD".into()), + Tonemapping::ReinhardLuminance => { + shader_defs.push("TONEMAP_METHOD_REINHARD_LUMINANCE".into()); + } + Tonemapping::AcesFitted => shader_defs.push("TONEMAP_METHOD_ACES_FITTED".into()), + Tonemapping::AgX => { + #[cfg(not(feature = "tonemapping_luts"))] + error!( + "AgX tonemapping requires the `tonemapping_luts` feature. + Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended), + or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`." + ); + shader_defs.push("TONEMAP_METHOD_AGX".into()); + } + Tonemapping::SomewhatBoringDisplayTransform => { + shader_defs.push("TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM".into()); + } + Tonemapping::TonyMcMapface => { + #[cfg(not(feature = "tonemapping_luts"))] + error!( + "TonyMcMapFace tonemapping requires the `tonemapping_luts` feature. + Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended), + or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`." + ); + shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into()); + } + Tonemapping::BlenderFilmic => { + #[cfg(not(feature = "tonemapping_luts"))] + error!( + "BlenderFilmic tonemapping requires the `tonemapping_luts` feature. + Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended), + or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`." + ); + shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into()); + } + } + RenderPipelineDescriptor { + label: Some("tonemapping pipeline".into()), + layout: vec![self.texture_bind_group.clone()], + vertex: self.fullscreen_shader.to_vertex_state(), + fragment: Some(FragmentState { + shader: self.fragment_shader.clone(), + shader_defs, + targets: vec![Some(ColorTargetState { + format: ViewTarget::TEXTURE_FORMAT_HDR, + blend: None, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + ..default() + } + } +} + +pub fn init_tonemapping_pipeline( + mut commands: Commands, + render_device: Res, + fullscreen_shader: Res, + asset_server: Res, +) { + let mut entries = DynamicBindGroupLayoutEntries::new_with_indices( + ShaderStages::FRAGMENT, + ( + (0, uniform_buffer::(true)), + ( + 1, + texture_2d(TextureSampleType::Float { filterable: false }), + ), + (2, sampler(SamplerBindingType::NonFiltering)), + ), + ); + let lut_layout_entries = get_lut_bind_group_layout_entries(); + entries = entries.extend_with_indices(((3, lut_layout_entries[0]), (4, lut_layout_entries[1]))); + + let tonemap_texture_bind_group = render_device + .create_bind_group_layout("tonemapping_hdr_texture_bind_group_layout", &entries); + + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); + + commands.insert_resource(TonemappingPipeline { + texture_bind_group: tonemap_texture_bind_group, + sampler, + fullscreen_shader: fullscreen_shader.clone(), + fragment_shader: load_embedded_asset!(asset_server.as_ref(), "tonemapping.wgsl"), + }); +} + +#[derive(Component)] +pub struct ViewTonemappingPipeline(CachedRenderPipelineId); + +pub fn prepare_view_tonemapping_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + upscaling_pipeline: Res, + view_targets: Query< + ( + Entity, + &ExtractedView, + Option<&Tonemapping>, + Option<&DebandDither>, + ), + With, + >, +) { + for (entity, view, tonemapping, dither) in view_targets.iter() { + // As an optimization, we omit parts of the shader that are unneeded. + let mut flags = TonemappingPipelineKeyFlags::empty(); + flags.set( + TonemappingPipelineKeyFlags::HUE_ROTATE, + view.color_grading.global.hue != 0.0, + ); + flags.set( + TonemappingPipelineKeyFlags::WHITE_BALANCE, + view.color_grading.global.temperature != 0.0 || view.color_grading.global.tint != 0.0, + ); + flags.set( + TonemappingPipelineKeyFlags::SECTIONAL_COLOR_GRADING, + view.color_grading + .all_sections() + .any(|section| *section != default()), + ); + + let key = TonemappingPipelineKey { + deband_dither: *dither.unwrap_or(&DebandDither::Disabled), + tonemapping: *tonemapping.unwrap_or(&Tonemapping::None), + flags, + }; + let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key); + + commands + .entity(entity) + .insert(ViewTonemappingPipeline(pipeline)); + } +} +/// Enables a debanding shader that applies dithering to mitigate color banding in the final image for a given [`Camera`] entity. +#[derive( + Component, Debug, Hash, Clone, Copy, Reflect, Default, ExtractComponent, PartialEq, Eq, +)] +#[extract_component_filter(With)] +#[reflect(Component, Debug, Hash, Default, PartialEq)] +pub enum DebandDither { + #[default] + Disabled, + Enabled, +} + +pub fn get_lut_bindings<'a>( + images: &'a RenderAssets, + tonemapping_luts: &'a TonemappingLuts, + tonemapping: &Tonemapping, + fallback_image: &'a FallbackImage, +) -> (&'a TextureView, &'a Sampler) { + let image = match tonemapping { + // AgX lut texture used when tonemapping doesn't need a texture since it's very small (32x32x32) + Tonemapping::None + | Tonemapping::Reinhard + | Tonemapping::ReinhardLuminance + | Tonemapping::AcesFitted + | Tonemapping::AgX + | Tonemapping::SomewhatBoringDisplayTransform => &tonemapping_luts.agx, + Tonemapping::TonyMcMapface => &tonemapping_luts.tony_mc_mapface, + Tonemapping::BlenderFilmic => &tonemapping_luts.blender_filmic, + }; + let lut_image = images.get(image).unwrap_or(&fallback_image.d3); + (&lut_image.texture_view, &lut_image.sampler) +} + +pub fn get_lut_bind_group_layout_entries() -> [BindGroupLayoutEntryBuilder; 2] { + [ + texture_3d(TextureSampleType::Float { filterable: true }), + sampler(SamplerBindingType::Filtering), + ] +} + +#[expect(clippy::allow_attributes, reason = "`dead_code` is not always linted.")] +#[allow( + dead_code, + reason = "There is unused code when the `tonemapping_luts` feature is disabled." +)] +fn setup_tonemapping_lut_image(bytes: &[u8], image_type: ImageType) -> Image { + let image_sampler = ImageSampler::Descriptor(bevy_image::ImageSamplerDescriptor { + label: Some("Tonemapping LUT sampler".to_string()), + address_mode_u: bevy_image::ImageAddressMode::ClampToEdge, + address_mode_v: bevy_image::ImageAddressMode::ClampToEdge, + address_mode_w: bevy_image::ImageAddressMode::ClampToEdge, + mag_filter: bevy_image::ImageFilterMode::Linear, + min_filter: bevy_image::ImageFilterMode::Linear, + mipmap_filter: bevy_image::ImageFilterMode::Linear, + ..default() + }); + Image::from_buffer( + bytes, + image_type, + CompressedImageFormats::NONE, + false, + image_sampler, + RenderAssetUsages::RENDER_WORLD, + ) + .unwrap() +} + +pub fn lut_placeholder() -> Image { + let format = TextureFormat::Rgba8Unorm; + let data = vec![255, 0, 255, 255]; + Image { + data: Some(data), + data_order: TextureDataOrder::default(), + texture_descriptor: TextureDescriptor { + size: Extent3d::default(), + format, + dimension: TextureDimension::D3, + label: None, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], + }, + sampler: ImageSampler::Default, + texture_view_descriptor: None, + asset_usage: RenderAssetUsages::RENDER_WORLD, + copy_on_resize: false, + } +} diff --git a/crates/libmarathon/src/render/tonemapping/node.rs b/crates/libmarathon/src/render/tonemapping/node.rs new file mode 100644 index 0000000..02c5f96 --- /dev/null +++ b/crates/libmarathon/src/render/tonemapping/node.rs @@ -0,0 +1,148 @@ +use std::sync::Mutex; + +use crate::render::tonemapping::{TonemappingLuts, TonemappingPipeline, ViewTonemappingPipeline}; + +use bevy_ecs::{prelude::*, query::QueryItem}; +use crate::render::{ + diagnostic::RecordDiagnostics, + render_asset::RenderAssets, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + BindGroup, BindGroupEntries, BufferId, LoadOp, Operations, PipelineCache, + RenderPassColorAttachment, RenderPassDescriptor, StoreOp, TextureViewId, + }, + renderer::RenderContext, + texture::{FallbackImage, GpuImage}, + view::{ViewTarget, ViewUniformOffset, ViewUniforms}, +}; + +use super::{get_lut_bindings, Tonemapping}; + +#[derive(Default)] +pub struct TonemappingNode { + cached_bind_group: Mutex>, + last_tonemapping: Mutex>, +} + +impl ViewNode for TonemappingNode { + type ViewQuery = ( + &'static ViewUniformOffset, + &'static ViewTarget, + &'static ViewTonemappingPipeline, + &'static Tonemapping, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (view_uniform_offset, target, view_tonemapping_pipeline, tonemapping): QueryItem< + Self::ViewQuery, + >, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let tonemapping_pipeline = world.resource::(); + let gpu_images = world.get_resource::>().unwrap(); + let fallback_image = world.resource::(); + let view_uniforms_resource = world.resource::(); + let view_uniforms = &view_uniforms_resource.uniforms; + let view_uniforms_id = view_uniforms.buffer().unwrap().id(); + + if *tonemapping == Tonemapping::None { + return Ok(()); + } + + if !target.is_hdr() { + return Ok(()); + } + + let Some(pipeline) = pipeline_cache.get_render_pipeline(view_tonemapping_pipeline.0) else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let post_process = target.post_process_write(); + let source = post_process.source; + let destination = post_process.destination; + + let mut last_tonemapping = self.last_tonemapping.lock().unwrap(); + + let tonemapping_changed = if let Some(last_tonemapping) = &*last_tonemapping { + tonemapping != last_tonemapping + } else { + true + }; + if tonemapping_changed { + *last_tonemapping = Some(*tonemapping); + } + + let mut cached_bind_group = self.cached_bind_group.lock().unwrap(); + let bind_group = match &mut *cached_bind_group { + Some((buffer_id, texture_id, lut_id, bind_group)) + if view_uniforms_id == *buffer_id + && source.id() == *texture_id + && *lut_id != fallback_image.d3.texture_view.id() + && !tonemapping_changed => + { + bind_group + } + cached_bind_group => { + let tonemapping_luts = world.resource::(); + + let lut_bindings = + get_lut_bindings(gpu_images, tonemapping_luts, tonemapping, fallback_image); + + let bind_group = render_context.render_device().create_bind_group( + None, + &tonemapping_pipeline.texture_bind_group, + &BindGroupEntries::sequential(( + view_uniforms, + source, + &tonemapping_pipeline.sampler, + lut_bindings.0, + lut_bindings.1, + )), + ); + + let (_, _, _, bind_group) = cached_bind_group.insert(( + view_uniforms_id, + source.id(), + lut_bindings.0.id(), + bind_group, + )); + bind_group + } + }; + + let pass_descriptor = RenderPassDescriptor { + label: Some("tonemapping"), + color_attachments: &[Some(RenderPassColorAttachment { + view: destination, + depth_slice: None, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(Default::default()), // TODO shouldn't need to be cleared + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + let pass_span = diagnostics.pass_span(&mut render_pass, "tonemapping"); + + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group(0, bind_group, &[view_uniform_offset.offset]); + render_pass.draw(0..3, 0..1); + + pass_span.end(&mut render_pass); + + Ok(()) + } +} diff --git a/crates/libmarathon/src/render/tonemapping/tonemapping.wgsl b/crates/libmarathon/src/render/tonemapping/tonemapping.wgsl new file mode 100644 index 0000000..015cd48 --- /dev/null +++ b/crates/libmarathon/src/render/tonemapping/tonemapping.wgsl @@ -0,0 +1,34 @@ +#define TONEMAPPING_PASS + +#import bevy_render::{ + view::View, + maths::powsafe, +} +#import bevy_core_pipeline::{ + fullscreen_vertex_shader::FullscreenVertexOutput, + tonemapping::{tone_mapping, screen_space_dither}, +} + +@group(0) @binding(0) var view: View; + +@group(0) @binding(1) var hdr_texture: texture_2d; +@group(0) @binding(2) var hdr_sampler: sampler; +@group(0) @binding(3) var dt_lut_texture: texture_3d; +@group(0) @binding(4) var dt_lut_sampler: sampler; + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + let hdr_color = textureSample(hdr_texture, hdr_sampler, in.uv); + + var output_rgb = tone_mapping(hdr_color, view.color_grading).rgb; + +#ifdef DEBAND_DITHER + output_rgb = powsafe(output_rgb.rgb, 1.0 / 2.2); + output_rgb = output_rgb + screen_space_dither(in.position.xy); + // This conversion back to linear space is required because our output texture format is + // SRGB; the GPU will assume our output is linear and will apply an SRGB conversion. + output_rgb = powsafe(output_rgb.rgb, 2.2); +#endif + + return vec4(output_rgb, hdr_color.a); +} diff --git a/crates/libmarathon/src/render/tonemapping/tonemapping_shared.wgsl b/crates/libmarathon/src/render/tonemapping/tonemapping_shared.wgsl new file mode 100644 index 0000000..52d1ddc --- /dev/null +++ b/crates/libmarathon/src/render/tonemapping/tonemapping_shared.wgsl @@ -0,0 +1,405 @@ +#define_import_path bevy_core_pipeline::tonemapping + +#import bevy_render::{ + view::ColorGrading, + color_operations::{hsv_to_rgb, rgb_to_hsv}, + maths::{PI_2, powsafe}, +} + +#import bevy_core_pipeline::tonemapping_lut_bindings::{ + dt_lut_texture, + dt_lut_sampler, +} + +// Half the size of the crossfade region between shadows and midtones and +// between midtones and highlights. This value, 0.1, corresponds to 10% of the +// gamut on either side of the cutoff point. +const LEVEL_MARGIN: f32 = 0.1; + +// The inverse reciprocal of twice the above, used when scaling the midtone +// region. +const LEVEL_MARGIN_DIV: f32 = 0.5 / LEVEL_MARGIN; + +fn sample_current_lut(p: vec3) -> vec3 { + // Don't include code that will try to sample from LUTs if tonemap method doesn't require it + // Allows this file to be imported without necessarily needing the lut texture bindings +#ifdef TONEMAP_METHOD_AGX + return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb; +#else ifdef TONEMAP_METHOD_TONY_MC_MAPFACE + return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb; +#else ifdef TONEMAP_METHOD_BLENDER_FILMIC + return textureSampleLevel(dt_lut_texture, dt_lut_sampler, p, 0.0).rgb; +#else + return vec3(1.0, 0.0, 1.0); + #endif +} + +// -------------------------------------- +// --- SomewhatBoringDisplayTransform --- +// -------------------------------------- +// By Tomasz Stachowiak + +fn rgb_to_ycbcr(col: vec3) -> vec3 { + let m = mat3x3( + 0.2126, 0.7152, 0.0722, + -0.1146, -0.3854, 0.5, + 0.5, -0.4542, -0.0458 + ); + return col * m; +} + +fn ycbcr_to_rgb(col: vec3) -> vec3 { + let m = mat3x3( + 1.0, 0.0, 1.5748, + 1.0, -0.1873, -0.4681, + 1.0, 1.8556, 0.0 + ); + return max(vec3(0.0), col * m); +} + +fn tonemap_curve(v: f32) -> f32 { +#ifdef 0 + // Large linear part in the lows, but compresses highs. + float c = v + v * v + 0.5 * v * v * v; + return c / (1.0 + c); +#else + return 1.0 - exp(-v); +#endif +} + +fn tonemap_curve3_(v: vec3) -> vec3 { + return vec3(tonemap_curve(v.r), tonemap_curve(v.g), tonemap_curve(v.b)); +} + +fn somewhat_boring_display_transform(col: vec3) -> vec3 { + var boring_color = col; + let ycbcr = rgb_to_ycbcr(boring_color); + + let bt = tonemap_curve(length(ycbcr.yz) * 2.4); + var desat = max((bt - 0.7) * 0.8, 0.0); + desat *= desat; + + let desat_col = mix(boring_color.rgb, ycbcr.xxx, desat); + + let tm_luma = tonemap_curve(ycbcr.x); + let tm0 = boring_color.rgb * max(0.0, tm_luma / max(1e-5, tonemapping_luminance(boring_color.rgb))); + let final_mult = 0.97; + let tm1 = tonemap_curve3_(desat_col); + + boring_color = mix(tm0, tm1, bt * bt); + + return boring_color * final_mult; +} + +// ------------------------------------------ +// ------------- Tony McMapface ------------- +// ------------------------------------------ +// By Tomasz Stachowiak +// https://github.com/h3r2tic/tony-mc-mapface + +const TONY_MC_MAPFACE_LUT_DIMS: f32 = 48.0; + +fn sample_tony_mc_mapface_lut(stimulus: vec3) -> vec3 { + var uv = (stimulus / (stimulus + 1.0)) * (f32(TONY_MC_MAPFACE_LUT_DIMS - 1.0) / f32(TONY_MC_MAPFACE_LUT_DIMS)) + 0.5 / f32(TONY_MC_MAPFACE_LUT_DIMS); + return sample_current_lut(saturate(uv)).rgb; +} + +// --------------------------------- +// ---------- ACES Fitted ---------- +// --------------------------------- + +// Same base implementation that Godot 4.0 uses for Tonemap ACES. + +// https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl + +// The code in this file was originally written by Stephen Hill (@self_shadow), who deserves all +// credit for coming up with this fit and implementing it. Buy him a beer next time you see him. :) + +fn RRTAndODTFit(v: vec3) -> vec3 { + let a = v * (v + 0.0245786) - 0.000090537; + let b = v * (0.983729 * v + 0.4329510) + 0.238081; + return a / b; +} + +fn ACESFitted(color: vec3) -> vec3 { + var fitted_color = color; + + // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT + let rgb_to_rrt = mat3x3( + vec3(0.59719, 0.35458, 0.04823), + vec3(0.07600, 0.90834, 0.01566), + vec3(0.02840, 0.13383, 0.83777) + ); + + // ODT_SAT => XYZ => D60_2_D65 => sRGB + let odt_to_rgb = mat3x3( + vec3(1.60475, -0.53108, -0.07367), + vec3(-0.10208, 1.10813, -0.00605), + vec3(-0.00327, -0.07276, 1.07602) + ); + + fitted_color *= rgb_to_rrt; + + // Apply RRT and ODT + fitted_color = RRTAndODTFit(fitted_color); + + fitted_color *= odt_to_rgb; + + // Clamp to [0, 1] + fitted_color = saturate(fitted_color); + + return fitted_color; +} + +// ------------------------------- +// ------------- AgX ------------- +// ------------------------------- +// By Troy Sobotka +// https://github.com/MrLixm/AgXc +// https://github.com/sobotka/AgX + +/* + Increase color saturation of the given color data. + :param color: expected sRGB primaries input + :param saturationAmount: expected 0-1 range with 1=neutral, 0=no saturation. + -- ref[2] [4] +*/ +fn saturation(color: vec3, saturationAmount: f32) -> vec3 { + let luma = tonemapping_luminance(color); + return mix(vec3(luma), color, vec3(saturationAmount)); +} + +/* + Output log domain encoded data. + Similar to OCIO lg2 AllocationTransform. + ref[0] +*/ +fn convertOpenDomainToNormalizedLog2_(color: vec3, minimum_ev: f32, maximum_ev: f32) -> vec3 { + let in_midgray = 0.18; + + // remove negative before log transform + var normalized_color = max(vec3(0.0), color); + // avoid infinite issue with log -- ref[1] + normalized_color = select(normalized_color, 0.00001525878 + normalized_color, normalized_color < vec3(0.00003051757)); + normalized_color = clamp( + log2(normalized_color / in_midgray), + vec3(minimum_ev), + vec3(maximum_ev) + ); + let total_exposure = maximum_ev - minimum_ev; + + return (normalized_color - minimum_ev) / total_exposure; +} + +// Inverse of above +fn convertNormalizedLog2ToOpenDomain(color: vec3, minimum_ev: f32, maximum_ev: f32) -> vec3 { + var open_color = color; + let in_midgray = 0.18; + let total_exposure = maximum_ev - minimum_ev; + + open_color = (open_color * total_exposure) + minimum_ev; + open_color = pow(vec3(2.0), open_color); + open_color = open_color * in_midgray; + + return open_color; +} + + +/*================= + Main processes +=================*/ + +// Prepare the data for display encoding. Converted to log domain. +fn applyAgXLog(Image: vec3) -> vec3 { + var prepared_image = max(vec3(0.0), Image); // clamp negatives + let r = dot(prepared_image, vec3(0.84247906, 0.0784336, 0.07922375)); + let g = dot(prepared_image, vec3(0.04232824, 0.87846864, 0.07916613)); + let b = dot(prepared_image, vec3(0.04237565, 0.0784336, 0.87914297)); + prepared_image = vec3(r, g, b); + + prepared_image = convertOpenDomainToNormalizedLog2_(prepared_image, -10.0, 6.5); + + prepared_image = clamp(prepared_image, vec3(0.0), vec3(1.0)); + return prepared_image; +} + +fn applyLUT3D(Image: vec3, block_size: f32) -> vec3 { + return sample_current_lut(Image * ((block_size - 1.0) / block_size) + 0.5 / block_size).rgb; +} + +// ------------------------- +// ------------------------- +// ------------------------- + +fn sample_blender_filmic_lut(stimulus: vec3) -> vec3 { + let block_size = 64.0; + let normalized = saturate(convertOpenDomainToNormalizedLog2_(stimulus, -11.0, 12.0)); + return applyLUT3D(normalized, block_size); +} + +// from https://64.github.io/tonemapping/ +// reinhard on RGB oversaturates colors +fn tonemapping_reinhard(color: vec3) -> vec3 { + return color / (1.0 + color); +} + +fn tonemapping_reinhard_extended(color: vec3, max_white: f32) -> vec3 { + let numerator = color * (1.0 + (color / vec3(max_white * max_white))); + return numerator / (1.0 + color); +} + +// luminance coefficients from Rec. 709. +// https://en.wikipedia.org/wiki/Rec._709 +fn tonemapping_luminance(v: vec3) -> f32 { + return dot(v, vec3(0.2126, 0.7152, 0.0722)); +} + +fn tonemapping_change_luminance(c_in: vec3, l_out: f32) -> vec3 { + let l_in = tonemapping_luminance(c_in); + return c_in * (l_out / l_in); +} + +fn tonemapping_reinhard_luminance(color: vec3) -> vec3 { + let l_old = tonemapping_luminance(color); + let l_new = l_old / (1.0 + l_old); + return tonemapping_change_luminance(color, l_new); +} + +fn rgb_to_srgb_simple(color: vec3) -> vec3 { + return pow(color, vec3(1.0 / 2.2)); +} + +// Source: Advanced VR Rendering, GDC 2015, Alex Vlachos, Valve, Slide 49 +// https://media.steampowered.com/apps/valve/2015/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf +fn screen_space_dither(frag_coord: vec2) -> vec3 { + var dither = vec3(dot(vec2(171.0, 231.0), frag_coord)).xxx; + dither = fract(dither.rgb / vec3(103.0, 71.0, 97.0)); + return (dither - 0.5) / 255.0; +} + +// Performs the "sectional" color grading: i.e. the color grading that applies +// individually to shadows, midtones, and highlights. +fn sectional_color_grading( + in: vec3, + color_grading: ptr, +) -> vec3 { + var color = in; + + // Determine whether the color is a shadow, midtone, or highlight. Colors + // close to the edges are considered a mix of both, to avoid sharp + // discontinuities. The formulas are taken from Blender's compositor. + + let level = (color.r + color.g + color.b) / 3.0; + + // Determine whether this color is a shadow, midtone, or highlight. If close + // to the cutoff points, blend between the two to avoid sharp color + // discontinuities. + var levels = vec3(0.0); + let midtone_range = (*color_grading).midtone_range; + if (level < midtone_range.x - LEVEL_MARGIN) { + levels.x = 1.0; + } else if (level < midtone_range.x + LEVEL_MARGIN) { + levels.y = ((level - midtone_range.x) * LEVEL_MARGIN_DIV) + 0.5; + levels.z = 1.0 - levels.y; + } else if (level < midtone_range.y - LEVEL_MARGIN) { + levels.y = 1.0; + } else if (level < midtone_range.y + LEVEL_MARGIN) { + levels.z = ((level - midtone_range.y) * LEVEL_MARGIN_DIV) + 0.5; + levels.y = 1.0 - levels.z; + } else { + levels.z = 1.0; + } + + // Calculate contrast/saturation/gamma/gain/lift. + let contrast = dot(levels, (*color_grading).contrast); + let saturation = dot(levels, (*color_grading).saturation); + let gamma = dot(levels, (*color_grading).gamma); + let gain = dot(levels, (*color_grading).gain); + let lift = dot(levels, (*color_grading).lift); + + // Adjust saturation and contrast. + let luma = tonemapping_luminance(color); + color = luma + saturation * (color - luma); + color = 0.5 + (color - 0.5) * contrast; + + // The [ASC CDL] formula for color correction. Given *i*, an input color, we + // have: + // + // out = (i × s + o)ⁿ + // + // Following the normal photographic naming convention, *gain* is the *s* + // factor, *lift* is the *o* term, and the inverse of *gamma* is the *n* + // exponent. + // + // [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function + color = powsafe(color * gain + lift, 1.0 / gamma); + + // Account for exposure. + color = color * powsafe(vec3(2.0), (*color_grading).exposure); + return max(color, vec3(0.0)); +} + +fn tone_mapping(in: vec4, in_color_grading: ColorGrading) -> vec4 { + var color = max(in.rgb, vec3(0.0)); + var color_grading = in_color_grading; // So we can take pointers to it. + + // Rotate hue if needed, by converting to and from HSV. Remember that hue is + // an angle, so it needs to be modulo 2π. +#ifdef HUE_ROTATE + var hsv = rgb_to_hsv(color); + hsv.r = (hsv.r + color_grading.hue) % PI_2; + color = hsv_to_rgb(hsv); +#endif + + // Perform white balance correction. Conveniently, this is a linear + // transform. The matrix was pre-calculated from the temperature and tint + // values on the CPU. +#ifdef WHITE_BALANCE + color = max(color_grading.balance * color, vec3(0.0)); +#endif + + // Perform the "sectional" color grading: i.e. the color grading that + // applies individually to shadows, midtones, and highlights. +#ifdef SECTIONAL_COLOR_GRADING + color = sectional_color_grading(color, &color_grading); +#else + // If we're not doing sectional color grading, the exposure might still need + // to be applied, for example when using auto exposure. + color = color * powsafe(vec3(2.0), color_grading.exposure); +#endif + + // tone_mapping +#ifdef TONEMAP_METHOD_NONE + color = color; +#else ifdef TONEMAP_METHOD_REINHARD + color = tonemapping_reinhard(color.rgb); +#else ifdef TONEMAP_METHOD_REINHARD_LUMINANCE + color = tonemapping_reinhard_luminance(color.rgb); +#else ifdef TONEMAP_METHOD_ACES_FITTED + color = ACESFitted(color.rgb); +#else ifdef TONEMAP_METHOD_AGX + color = applyAgXLog(color); + color = applyLUT3D(color, 32.0); +#else ifdef TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + color = somewhat_boring_display_transform(color.rgb); +#else ifdef TONEMAP_METHOD_TONY_MC_MAPFACE + color = sample_tony_mc_mapface_lut(color); +#else ifdef TONEMAP_METHOD_BLENDER_FILMIC + color = sample_blender_filmic_lut(color.rgb); +#endif + + // Perceptual post tonemapping grading + color = saturation(color, color_grading.post_saturation); + + return vec4(color, in.a); +} + +// This is an **incredibly crude** approximation of the inverse of the tone mapping function. +// We assume here that there's a simple linear relationship between the input and output +// which is not true at all, but useful to at least preserve the overall luminance of colors +// when sampling from an already tonemapped image. (e.g. for transmissive materials when HDR is off) +fn approximate_inverse_tone_mapping(in: vec4, color_grading: ColorGrading) -> vec4 { + let out = tone_mapping(in, color_grading); + let approximate_ratio = length(in.rgb) / length(out.rgb); + return vec4(in.rgb * approximate_ratio, in.a); +} diff --git a/crates/libmarathon/src/render/upscaling/mod.rs b/crates/libmarathon/src/render/upscaling/mod.rs new file mode 100644 index 0000000..2a679ad --- /dev/null +++ b/crates/libmarathon/src/render/upscaling/mod.rs @@ -0,0 +1,88 @@ +use crate::render::blit::{BlitPipeline, BlitPipelineKey}; +use bevy_app::prelude::*; +use bevy_camera::CameraOutputMode; +use bevy_ecs::prelude::*; +use bevy_platform::collections::HashSet; +use crate::render::{ + camera::ExtractedCamera, render_resource::*, view::ViewTarget, Render, RenderApp, RenderSystems, +}; + +mod node; + +pub use node::UpscalingNode; + +pub struct UpscalingPlugin; + +impl Plugin for UpscalingPlugin { + fn build(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.add_systems( + Render, + // This system should probably technically be run *after* all of the other systems + // that might modify `PipelineCache` via interior mutability, but for now, + // we've chosen to simply ignore the ambiguities out of a desire for a better refactor + // and aversion to extensive and intrusive system ordering. + // See https://github.com/bevyengine/bevy/issues/14770 for more context. + prepare_view_upscaling_pipelines + .in_set(RenderSystems::Prepare) + .ambiguous_with_all(), + ); + } + } +} + +#[derive(Component)] +pub struct ViewUpscalingPipeline(CachedRenderPipelineId); + +fn prepare_view_upscaling_pipelines( + mut commands: Commands, + mut pipeline_cache: ResMut, + mut pipelines: ResMut>, + blit_pipeline: Res, + view_targets: Query<(Entity, &ViewTarget, Option<&ExtractedCamera>)>, +) { + let mut output_textures = >::default(); + for (entity, view_target, camera) in view_targets.iter() { + let out_texture_id = view_target.out_texture().id(); + let blend_state = if let Some(extracted_camera) = camera { + match extracted_camera.output_mode { + CameraOutputMode::Skip => None, + CameraOutputMode::Write { blend_state, .. } => { + let already_seen = output_textures.contains(&out_texture_id); + output_textures.insert(out_texture_id); + + match blend_state { + None => { + // If we've already seen this output for a camera and it doesn't have an output blend + // mode configured, default to alpha blend so that we don't accidentally overwrite + // the output texture + if already_seen { + Some(BlendState::ALPHA_BLENDING) + } else { + None + } + } + _ => blend_state, + } + } + } + } else { + output_textures.insert(out_texture_id); + None + }; + + let key = BlitPipelineKey { + texture_format: view_target.out_texture_format(), + blend_state, + samples: 1, + }; + let pipeline = pipelines.specialize(&pipeline_cache, &blit_pipeline, key); + + // Ensure the pipeline is loaded before continuing the frame to prevent frames without any GPU work submitted + pipeline_cache.block_on_render_pipeline(pipeline); + + commands + .entity(entity) + .insert(ViewUpscalingPipeline(pipeline)); + } +} diff --git a/crates/libmarathon/src/render/upscaling/node.rs b/crates/libmarathon/src/render/upscaling/node.rs new file mode 100644 index 0000000..57ac164 --- /dev/null +++ b/crates/libmarathon/src/render/upscaling/node.rs @@ -0,0 +1,104 @@ +use crate::render::{blit::BlitPipeline, upscaling::ViewUpscalingPipeline}; +use bevy_camera::{CameraOutputMode, ClearColor, ClearColorConfig}; +use bevy_ecs::{prelude::*, query::QueryItem}; +use crate::render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{BindGroup, PipelineCache, RenderPassDescriptor, TextureViewId}, + renderer::RenderContext, + view::ViewTarget, +}; +use std::sync::Mutex; + +#[derive(Default)] +pub struct UpscalingNode { + cached_texture_bind_group: Mutex>, +} + +impl ViewNode for UpscalingNode { + type ViewQuery = ( + &'static ViewTarget, + &'static ViewUpscalingPipeline, + Option<&'static ExtractedCamera>, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (target, upscaling_target, camera): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let blit_pipeline = world.resource::(); + let clear_color_global = world.resource::(); + + let diagnostics = render_context.diagnostic_recorder(); + + let clear_color = if let Some(camera) = camera { + match camera.output_mode { + CameraOutputMode::Write { clear_color, .. } => clear_color, + CameraOutputMode::Skip => return Ok(()), + } + } else { + ClearColorConfig::Default + }; + let clear_color = match clear_color { + ClearColorConfig::Default => Some(clear_color_global.0), + ClearColorConfig::Custom(color) => Some(color), + ClearColorConfig::None => None, + }; + let converted_clear_color = clear_color.map(Into::into); + // texture to be upscaled to the output texture + let main_texture_view = target.main_texture_view(); + + let mut cached_bind_group = self.cached_texture_bind_group.lock().unwrap(); + let bind_group = match &mut *cached_bind_group { + Some((id, bind_group)) if main_texture_view.id() == *id => bind_group, + cached_bind_group => { + let bind_group = blit_pipeline + .create_bind_group(render_context.render_device(), main_texture_view); + + let (_, bind_group) = + cached_bind_group.insert((main_texture_view.id(), bind_group)); + bind_group + } + }; + + let Some(pipeline) = pipeline_cache.get_render_pipeline(upscaling_target.0) else { + return Ok(()); + }; + + let pass_descriptor = RenderPassDescriptor { + label: Some("upscaling"), + color_attachments: &[Some( + target.out_texture_color_attachment(converted_clear_color), + )], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + let pass_span = diagnostics.pass_span(&mut render_pass, "upscaling"); + + if let Some(camera) = camera + && let Some(viewport) = &camera.viewport + { + let size = viewport.physical_size; + let position = viewport.physical_position; + render_pass.set_scissor_rect(position.x, position.y, size.x, size.y); + } + + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group(0, bind_group, &[]); + render_pass.draw(0..3, 0..1); + + pass_span.end(&mut render_pass); + + Ok(()) + } +} diff --git a/crates/libmarathon/src/render/view/mod.rs b/crates/libmarathon/src/render/view/mod.rs new file mode 100644 index 0000000..d116455 --- /dev/null +++ b/crates/libmarathon/src/render/view/mod.rs @@ -0,0 +1,1135 @@ +pub mod visibility; +pub mod window; + +use bevy_camera::{ + primitives::Frustum, CameraMainTextureUsages, ClearColor, ClearColorConfig, Exposure, + MainPassResolutionOverride, NormalizedRenderTarget, +}; +use bevy_diagnostic::FrameCount; +pub use visibility::*; +pub use window::*; + +use crate::render::{ + camera::{ExtractedCamera, MipBias, NormalizedRenderTargetExt as _, TemporalJitter}, + experimental::occlusion_culling::OcclusionCulling, + extract_component::ExtractComponentPlugin, + render_asset::RenderAssets, + render_phase::ViewRangefinder3d, + render_resource::{DynamicUniformBuffer, ShaderType, Texture, TextureView}, + renderer::{RenderDevice, RenderQueue}, + sync_world::MainEntity, + texture::{ + CachedTexture, ColorAttachment, DepthAttachment, GpuImage, ManualTextureViews, + OutputColorAttachment, TextureCache, + }, + Render, RenderApp, RenderSystems, +}; +use std::sync::Arc; +use bevy_app::{App, Plugin}; +use bevy_color::LinearRgba; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::prelude::*; +use bevy_image::{BevyDefault as _, ToExtents}; +use bevy_math::{mat3, vec2, vec3, Mat3, Mat4, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles}; +use bevy_platform::collections::{hash_map::Entry, HashMap}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use macros::ExtractComponent; +use bevy_shader::load_shader_library; +use bevy_transform::components::GlobalTransform; +use core::{ + ops::Range, + sync::atomic::{AtomicUsize, Ordering}, +}; +use wgpu::{ + BufferUsages, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp, + TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, +}; + +/// The matrix that converts from the RGB to the LMS color space. +/// +/// To derive this, first we convert from RGB to [CIE 1931 XYZ]: +/// +/// ```text +/// ⎡ X ⎤ ⎡ 0.490 0.310 0.200 ⎤ ⎡ R ⎤ +/// ⎢ Y ⎥ = ⎢ 0.177 0.812 0.011 ⎥ ⎢ G ⎥ +/// ⎣ Z ⎦ ⎣ 0.000 0.010 0.990 ⎦ ⎣ B ⎦ +/// ``` +/// +/// Then we convert to LMS according to the [CAM16 standard matrix]: +/// +/// ```text +/// ⎡ L ⎤ ⎡ 0.401 0.650 -0.051 ⎤ ⎡ X ⎤ +/// ⎢ M ⎥ = ⎢ -0.250 1.204 0.046 ⎥ ⎢ Y ⎥ +/// ⎣ S ⎦ ⎣ -0.002 0.049 0.953 ⎦ ⎣ Z ⎦ +/// ``` +/// +/// The resulting matrix is just the concatenation of these two matrices, to do +/// the conversion in one step. +/// +/// [CIE 1931 XYZ]: https://en.wikipedia.org/wiki/CIE_1931_color_space +/// [CAM16 standard matrix]: https://en.wikipedia.org/wiki/LMS_color_space +static RGB_TO_LMS: Mat3 = mat3( + vec3(0.311692, 0.0905138, 0.00764433), + vec3(0.652085, 0.901341, 0.0486554), + vec3(0.0362225, 0.00814478, 0.943700), +); + +/// The inverse of the [`RGB_TO_LMS`] matrix, converting from the LMS color +/// space back to RGB. +static LMS_TO_RGB: Mat3 = mat3( + vec3(4.06305, -0.40791, -0.0118812), + vec3(-2.93241, 1.40437, -0.0486532), + vec3(-0.130646, 0.00353630, 1.0605344), +); + +/// The [CIE 1931] *xy* chromaticity coordinates of the [D65 white point]. +/// +/// [CIE 1931]: https://en.wikipedia.org/wiki/CIE_1931_color_space +/// [D65 white point]: https://en.wikipedia.org/wiki/Standard_illuminant#D65_values +static D65_XY: Vec2 = vec2(0.31272, 0.32903); + +/// The [D65 white point] in [LMS color space]. +/// +/// [LMS color space]: https://en.wikipedia.org/wiki/LMS_color_space +/// [D65 white point]: https://en.wikipedia.org/wiki/Standard_illuminant#D65_values +static D65_LMS: Vec3 = vec3(0.975538, 1.01648, 1.08475); + +pub struct ViewPlugin; + +impl Plugin for ViewPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "view.wgsl"); + + app + // NOTE: windows.is_changed() handles cases where a window was resized + .add_plugins(( + ExtractComponentPlugin::::default(), + ExtractComponentPlugin::::default(), + ExtractComponentPlugin::::default(), + RenderVisibilityRangePlugin, + )); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.add_systems( + Render, + ( + // `TextureView`s need to be dropped before reconfiguring window surfaces. + clear_view_attachments + .in_set(RenderSystems::ManageViews) + .before(create_surfaces), + prepare_view_attachments + .in_set(RenderSystems::ManageViews) + .before(prepare_view_targets) + .after(prepare_windows), + prepare_view_targets + .in_set(RenderSystems::ManageViews) + .after(prepare_windows) + .after(crate::render::render_asset::prepare_assets::) + .ambiguous_with(crate::render::camera::sort_cameras), // doesn't use `sorted_camera_index_for_target` + prepare_view_uniforms.in_set(RenderSystems::PrepareResources), + ), + ); + } + } + + fn finish(&self, app: &mut App) { + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::(); + } + } +} + +/// Component for configuring the number of samples for [Multi-Sample Anti-Aliasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing) +/// for a [`Camera`](bevy_camera::Camera). +/// +/// Defaults to 4 samples. A higher number of samples results in smoother edges. +/// +/// Some advanced rendering features may require that MSAA is disabled. +/// +/// Note that the web currently only supports 1 or 4 samples. +#[derive( + Component, + Default, + Clone, + Copy, + ExtractComponent, + Reflect, + PartialEq, + PartialOrd, + Eq, + Hash, + Debug, +)] +#[reflect(Component, Default, PartialEq, Hash, Debug)] +pub enum Msaa { + Off = 1, + Sample2 = 2, + #[default] + Sample4 = 4, + Sample8 = 8, +} + +impl Msaa { + #[inline] + pub fn samples(&self) -> u32 { + *self as u32 + } + + pub fn from_samples(samples: u32) -> Self { + match samples { + 1 => Msaa::Off, + 2 => Msaa::Sample2, + 4 => Msaa::Sample4, + 8 => Msaa::Sample8, + _ => panic!("Unsupported MSAA sample count: {samples}"), + } + } +} + +/// If this component is added to a camera, the camera will use an intermediate "high dynamic range" render texture. +/// This allows rendering with a wider range of lighting values. However, this does *not* affect +/// whether the camera will render with hdr display output (which bevy does not support currently) +/// and only affects the intermediate render texture. +#[derive( + Component, Default, Copy, Clone, ExtractComponent, Reflect, PartialEq, Eq, Hash, Debug, +)] +#[reflect(Component, Default, PartialEq, Hash, Debug)] +pub struct Hdr; + +/// An identifier for a view that is stable across frames. +/// +/// We can't use [`Entity`] for this because render world entities aren't +/// stable, and we can't use just [`MainEntity`] because some main world views +/// extract to multiple render world views. For example, a directional light +/// extracts to one render world view per cascade, and a point light extracts to +/// one render world view per cubemap face. So we pair the main entity with an +/// *auxiliary entity* and a *subview index*, which *together* uniquely identify +/// a view in the render world in a way that's stable from frame to frame. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct RetainedViewEntity { + /// The main entity that this view corresponds to. + pub main_entity: MainEntity, + + /// Another entity associated with the view entity. + /// + /// This is currently used for shadow cascades. If there are multiple + /// cameras, each camera needs to have its own set of shadow cascades. Thus + /// the light and subview index aren't themselves enough to uniquely + /// identify a shadow cascade: we need the camera that the cascade is + /// associated with as well. This entity stores that camera. + /// + /// If not present, this will be `MainEntity(Entity::PLACEHOLDER)`. + pub auxiliary_entity: MainEntity, + + /// The index of the view corresponding to the entity. + /// + /// For example, for point lights that cast shadows, this is the index of + /// the cubemap face (0 through 5 inclusive). For directional lights, this + /// is the index of the cascade. + pub subview_index: u32, +} + +impl RetainedViewEntity { + /// Creates a new [`RetainedViewEntity`] from the given main world entity, + /// auxiliary main world entity, and subview index. + /// + /// See [`RetainedViewEntity::subview_index`] for an explanation of what + /// `auxiliary_entity` and `subview_index` are. + pub fn new( + main_entity: MainEntity, + auxiliary_entity: Option, + subview_index: u32, + ) -> Self { + Self { + main_entity, + auxiliary_entity: auxiliary_entity.unwrap_or(Entity::PLACEHOLDER.into()), + subview_index, + } + } +} + +/// Describes a camera in the render world. +/// +/// Each entity in the main world can potentially extract to multiple subviews, +/// each of which has a [`RetainedViewEntity::subview_index`]. For instance, 3D +/// cameras extract to both a 3D camera subview with index 0 and a special UI +/// subview with index 1. Likewise, point lights with shadows extract to 6 +/// subviews, one for each side of the shadow cubemap. +#[derive(Component)] +pub struct ExtractedView { + /// The entity in the main world corresponding to this render world view. + pub retained_view_entity: RetainedViewEntity, + /// Typically a column-major right-handed projection matrix, one of either: + /// + /// Perspective (infinite reverse z) + /// ```text + /// f = 1 / tan(fov_y_radians / 2) + /// + /// ⎡ f / aspect 0 0 0 ⎤ + /// ⎢ 0 f 0 0 ⎥ + /// ⎢ 0 0 0 near ⎥ + /// ⎣ 0 0 -1 0 ⎦ + /// ``` + /// + /// Orthographic + /// ```text + /// w = right - left + /// h = top - bottom + /// d = far - near + /// cw = -right - left + /// ch = -top - bottom + /// + /// ⎡ 2 / w 0 0 cw / w ⎤ + /// ⎢ 0 2 / h 0 ch / h ⎥ + /// ⎢ 0 0 1 / d far / d ⎥ + /// ⎣ 0 0 0 1 ⎦ + /// ``` + /// + /// `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic + /// + /// Glam matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]` + /// + /// Custom projections are also possible however. + pub clip_from_view: Mat4, + pub world_from_view: GlobalTransform, + // The view-projection matrix. When provided it is used instead of deriving it from + // `projection` and `transform` fields, which can be helpful in cases where numerical + // stability matters and there is a more direct way to derive the view-projection matrix. + pub clip_from_world: Option, + pub hdr: bool, + // uvec4(origin.x, origin.y, width, height) + pub viewport: UVec4, + pub color_grading: ColorGrading, +} + +impl ExtractedView { + /// Creates a 3D rangefinder for a view + pub fn rangefinder3d(&self) -> ViewRangefinder3d { + ViewRangefinder3d::from_world_from_view(&self.world_from_view.affine()) + } +} + +/// Configures filmic color grading parameters to adjust the image appearance. +/// +/// Color grading is applied just before tonemapping for a given +/// [`Camera`](bevy_camera::Camera) entity, with the sole exception of the +/// `post_saturation` value in [`ColorGradingGlobal`], which is applied after +/// tonemapping. +#[derive(Component, Reflect, Debug, Default, Clone)] +#[reflect(Component, Default, Debug, Clone)] +pub struct ColorGrading { + /// Filmic color grading values applied to the image as a whole (as opposed + /// to individual sections, like shadows and highlights). + pub global: ColorGradingGlobal, + + /// Color grading values that are applied to the darker parts of the image. + /// + /// The cutoff points can be customized with the + /// [`ColorGradingGlobal::midtones_range`] field. + pub shadows: ColorGradingSection, + + /// Color grading values that are applied to the parts of the image with + /// intermediate brightness. + /// + /// The cutoff points can be customized with the + /// [`ColorGradingGlobal::midtones_range`] field. + pub midtones: ColorGradingSection, + + /// Color grading values that are applied to the lighter parts of the image. + /// + /// The cutoff points can be customized with the + /// [`ColorGradingGlobal::midtones_range`] field. + pub highlights: ColorGradingSection, +} + +/// Filmic color grading values applied to the image as a whole (as opposed to +/// individual sections, like shadows and highlights). +#[derive(Clone, Debug, Reflect)] +#[reflect(Default, Clone)] +pub struct ColorGradingGlobal { + /// Exposure value (EV) offset, measured in stops. + pub exposure: f32, + + /// An adjustment made to the [CIE 1931] chromaticity *x* value. + /// + /// Positive values make the colors redder. Negative values make the colors + /// bluer. This has no effect on luminance (brightness). + /// + /// [CIE 1931]: https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_xy_chromaticity_diagram_and_the_CIE_xyY_color_space + pub temperature: f32, + + /// An adjustment made to the [CIE 1931] chromaticity *y* value. + /// + /// Positive values make the colors more magenta. Negative values make the + /// colors greener. This has no effect on luminance (brightness). + /// + /// [CIE 1931]: https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_xy_chromaticity_diagram_and_the_CIE_xyY_color_space + pub tint: f32, + + /// An adjustment to the [hue], in radians. + /// + /// Adjusting this value changes the perceived colors in the image: red to + /// yellow to green to blue, etc. It has no effect on the saturation or + /// brightness of the colors. + /// + /// [hue]: https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation + pub hue: f32, + + /// Saturation adjustment applied after tonemapping. + /// Values below 1.0 desaturate, with a value of 0.0 resulting in a grayscale image + /// with luminance defined by ITU-R BT.709 + /// Values above 1.0 increase saturation. + pub post_saturation: f32, + + /// The luminance (brightness) ranges that are considered part of the + /// "midtones" of the image. + /// + /// This affects which [`ColorGradingSection`]s apply to which colors. Note + /// that the sections smoothly blend into one another, to avoid abrupt + /// transitions. + /// + /// The default value is 0.2 to 0.7. + pub midtones_range: Range, +} + +/// The [`ColorGrading`] structure, packed into the most efficient form for the +/// GPU. +#[derive(Clone, Copy, Debug, ShaderType)] +pub struct ColorGradingUniform { + pub balance: Mat3, + pub saturation: Vec3, + pub contrast: Vec3, + pub gamma: Vec3, + pub gain: Vec3, + pub lift: Vec3, + pub midtone_range: Vec2, + pub exposure: f32, + pub hue: f32, + pub post_saturation: f32, +} + +/// A section of color grading values that can be selectively applied to +/// shadows, midtones, and highlights. +#[derive(Reflect, Debug, Copy, Clone, PartialEq)] +#[reflect(Clone, PartialEq)] +pub struct ColorGradingSection { + /// Values below 1.0 desaturate, with a value of 0.0 resulting in a grayscale image + /// with luminance defined by ITU-R BT.709. + /// Values above 1.0 increase saturation. + pub saturation: f32, + + /// Adjusts the range of colors. + /// + /// A value of 1.0 applies no changes. Values below 1.0 move the colors more + /// toward a neutral gray. Values above 1.0 spread the colors out away from + /// the neutral gray. + pub contrast: f32, + + /// A nonlinear luminance adjustment, mainly affecting the high end of the + /// range. + /// + /// This is the *n* exponent in the standard [ASC CDL] formula for color + /// correction: + /// + /// ```text + /// out = (i × s + o)ⁿ + /// ``` + /// + /// [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function + pub gamma: f32, + + /// A linear luminance adjustment, mainly affecting the middle part of the + /// range. + /// + /// This is the *s* factor in the standard [ASC CDL] formula for color + /// correction: + /// + /// ```text + /// out = (i × s + o)ⁿ + /// ``` + /// + /// [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function + pub gain: f32, + + /// A fixed luminance adjustment, mainly affecting the lower part of the + /// range. + /// + /// This is the *o* term in the standard [ASC CDL] formula for color + /// correction: + /// + /// ```text + /// out = (i × s + o)ⁿ + /// ``` + /// + /// [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function + pub lift: f32, +} + +impl Default for ColorGradingGlobal { + fn default() -> Self { + Self { + exposure: 0.0, + temperature: 0.0, + tint: 0.0, + hue: 0.0, + post_saturation: 1.0, + midtones_range: 0.2..0.7, + } + } +} + +impl Default for ColorGradingSection { + fn default() -> Self { + Self { + saturation: 1.0, + contrast: 1.0, + gamma: 1.0, + gain: 1.0, + lift: 0.0, + } + } +} + +impl ColorGrading { + /// Creates a new [`ColorGrading`] instance in which shadows, midtones, and + /// highlights all have the same set of color grading values. + pub fn with_identical_sections( + global: ColorGradingGlobal, + section: ColorGradingSection, + ) -> ColorGrading { + ColorGrading { + global, + highlights: section, + midtones: section, + shadows: section, + } + } + + /// Returns an iterator that visits the shadows, midtones, and highlights + /// sections, in that order. + pub fn all_sections(&self) -> impl Iterator { + [&self.shadows, &self.midtones, &self.highlights].into_iter() + } + + /// Applies the given mutating function to the shadows, midtones, and + /// highlights sections, in that order. + /// + /// Returns an array composed of the results of such evaluation, in that + /// order. + pub fn all_sections_mut(&mut self) -> impl Iterator { + [&mut self.shadows, &mut self.midtones, &mut self.highlights].into_iter() + } +} + +#[derive(Clone, ShaderType)] +pub struct ViewUniform { + pub clip_from_world: Mat4, + pub unjittered_clip_from_world: Mat4, + pub world_from_clip: Mat4, + pub world_from_view: Mat4, + pub view_from_world: Mat4, + /// Typically a column-major right-handed projection matrix, one of either: + /// + /// Perspective (infinite reverse z) + /// ```text + /// f = 1 / tan(fov_y_radians / 2) + /// + /// ⎡ f / aspect 0 0 0 ⎤ + /// ⎢ 0 f 0 0 ⎥ + /// ⎢ 0 0 0 near ⎥ + /// ⎣ 0 0 -1 0 ⎦ + /// ``` + /// + /// Orthographic + /// ```text + /// w = right - left + /// h = top - bottom + /// d = far - near + /// cw = -right - left + /// ch = -top - bottom + /// + /// ⎡ 2 / w 0 0 cw / w ⎤ + /// ⎢ 0 2 / h 0 ch / h ⎥ + /// ⎢ 0 0 1 / d far / d ⎥ + /// ⎣ 0 0 0 1 ⎦ + /// ``` + /// + /// `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic + /// + /// Glam matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]` + /// + /// Custom projections are also possible however. + pub clip_from_view: Mat4, + pub view_from_clip: Mat4, + pub world_position: Vec3, + pub exposure: f32, + // viewport(x_origin, y_origin, width, height) + pub viewport: Vec4, + pub main_pass_viewport: Vec4, + /// 6 world-space half spaces (normal: vec3, distance: f32) ordered left, right, top, bottom, near, far. + /// The normal vectors point towards the interior of the frustum. + /// A half space contains `p` if `normal.dot(p) + distance > 0.` + pub frustum: [Vec4; 6], + pub color_grading: ColorGradingUniform, + pub mip_bias: f32, + pub frame_count: u32, +} + +#[derive(Resource)] +pub struct ViewUniforms { + pub uniforms: DynamicUniformBuffer, +} + +impl FromWorld for ViewUniforms { + fn from_world(world: &mut World) -> Self { + let mut uniforms = DynamicUniformBuffer::default(); + uniforms.set_label(Some("view_uniforms_buffer")); + + let render_device = world.resource::(); + if render_device.limits().max_storage_buffers_per_shader_stage > 0 { + uniforms.add_usages(BufferUsages::STORAGE); + } + + Self { uniforms } + } +} + +#[derive(Component)] +pub struct ViewUniformOffset { + pub offset: u32, +} + +#[derive(Component)] +pub struct ViewTarget { + main_textures: MainTargetTextures, + main_texture_format: TextureFormat, + /// 0 represents `main_textures.a`, 1 represents `main_textures.b` + /// This is shared across view targets with the same render target + main_texture: Arc, + out_texture: OutputColorAttachment, +} + +/// Contains [`OutputColorAttachment`] used for each target present on any view in the current +/// frame, after being prepared by [`prepare_view_attachments`]. Users that want to override +/// the default output color attachment for a specific target can do so by adding a +/// [`OutputColorAttachment`] to this resource before [`prepare_view_targets`] is called. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct ViewTargetAttachments(HashMap); + +pub struct PostProcessWrite<'a> { + pub source: &'a TextureView, + pub source_texture: &'a Texture, + pub destination: &'a TextureView, + pub destination_texture: &'a Texture, +} + +impl From for ColorGradingUniform { + fn from(component: ColorGrading) -> Self { + // Compute the balance matrix that will be used to apply the white + // balance adjustment to an RGB color. Our general approach will be to + // convert both the color and the developer-supplied white point to the + // LMS color space, apply the conversion, and then convert back. + // + // First, we start with the CIE 1931 *xy* values of the standard D65 + // illuminant: + // + // + // We then adjust them based on the developer's requested white balance. + let white_point_xy = D65_XY + vec2(-component.global.temperature, component.global.tint); + + // Convert the white point from CIE 1931 *xy* to LMS. First, we convert to XYZ: + // + // Y Y + // Y = 1 X = ─ x Z = ─ (1 - x - y) + // y y + // + // Then we convert from XYZ to LMS color space, using the CAM16 matrix + // from : + // + // ⎡ L ⎤ ⎡ 0.401 0.650 -0.051 ⎤ ⎡ X ⎤ + // ⎢ M ⎥ = ⎢ -0.250 1.204 0.046 ⎥ ⎢ Y ⎥ + // ⎣ S ⎦ ⎣ -0.002 0.049 0.953 ⎦ ⎣ Z ⎦ + // + // The following formula is just a simplification of the above. + + let white_point_lms = vec3(0.701634, 1.15856, -0.904175) + + (vec3(-0.051461, 0.045854, 0.953127) + + vec3(0.452749, -0.296122, -0.955206) * white_point_xy.x) + / white_point_xy.y; + + // Now that we're in LMS space, perform the white point scaling. + let white_point_adjustment = Mat3::from_diagonal(D65_LMS / white_point_lms); + + // Finally, combine the RGB → LMS → corrected LMS → corrected RGB + // pipeline into a single 3×3 matrix. + let balance = LMS_TO_RGB * white_point_adjustment * RGB_TO_LMS; + + Self { + balance, + saturation: vec3( + component.shadows.saturation, + component.midtones.saturation, + component.highlights.saturation, + ), + contrast: vec3( + component.shadows.contrast, + component.midtones.contrast, + component.highlights.contrast, + ), + gamma: vec3( + component.shadows.gamma, + component.midtones.gamma, + component.highlights.gamma, + ), + gain: vec3( + component.shadows.gain, + component.midtones.gain, + component.highlights.gain, + ), + lift: vec3( + component.shadows.lift, + component.midtones.lift, + component.highlights.lift, + ), + midtone_range: vec2( + component.global.midtones_range.start, + component.global.midtones_range.end, + ), + exposure: component.global.exposure, + hue: component.global.hue, + post_saturation: component.global.post_saturation, + } + } +} + +/// Add this component to a camera to disable *indirect mode*. +/// +/// Indirect mode, automatically enabled on supported hardware, allows Bevy to +/// offload transform and cull operations to the GPU, reducing CPU overhead. +/// Doing this, however, reduces the amount of control that your app has over +/// instancing decisions. In certain circumstances, you may want to disable +/// indirect drawing so that your app can manually instance meshes as it sees +/// fit. See the `custom_shader_instancing` example. +/// +/// The vast majority of applications will not need to use this component, as it +/// generally reduces rendering performance. +/// +/// Note: This component should only be added when initially spawning a camera. Adding +/// or removing after spawn can result in unspecified behavior. +#[derive(Component, Default)] +pub struct NoIndirectDrawing; + +impl ViewTarget { + pub const TEXTURE_FORMAT_HDR: TextureFormat = TextureFormat::Rgba16Float; + + /// Retrieve this target's main texture's color attachment. + pub fn get_color_attachment(&self) -> RenderPassColorAttachment<'_> { + if self.main_texture.load(Ordering::SeqCst) == 0 { + self.main_textures.a.get_attachment() + } else { + self.main_textures.b.get_attachment() + } + } + + /// Retrieve this target's "unsampled" main texture's color attachment. + pub fn get_unsampled_color_attachment(&self) -> RenderPassColorAttachment<'_> { + if self.main_texture.load(Ordering::SeqCst) == 0 { + self.main_textures.a.get_unsampled_attachment() + } else { + self.main_textures.b.get_unsampled_attachment() + } + } + + /// The "main" unsampled texture. + pub fn main_texture(&self) -> &Texture { + if self.main_texture.load(Ordering::SeqCst) == 0 { + &self.main_textures.a.texture.texture + } else { + &self.main_textures.b.texture.texture + } + } + + /// The _other_ "main" unsampled texture. + /// In most cases you should use [`Self::main_texture`] instead and never this. + /// The textures will naturally be swapped when [`Self::post_process_write`] is called. + /// + /// A use case for this is to be able to prepare a bind group for all main textures + /// ahead of time. + pub fn main_texture_other(&self) -> &Texture { + if self.main_texture.load(Ordering::SeqCst) == 0 { + &self.main_textures.b.texture.texture + } else { + &self.main_textures.a.texture.texture + } + } + + /// The "main" unsampled texture. + pub fn main_texture_view(&self) -> &TextureView { + if self.main_texture.load(Ordering::SeqCst) == 0 { + &self.main_textures.a.texture.default_view + } else { + &self.main_textures.b.texture.default_view + } + } + + /// The _other_ "main" unsampled texture view. + /// In most cases you should use [`Self::main_texture_view`] instead and never this. + /// The textures will naturally be swapped when [`Self::post_process_write`] is called. + /// + /// A use case for this is to be able to prepare a bind group for all main textures + /// ahead of time. + pub fn main_texture_other_view(&self) -> &TextureView { + if self.main_texture.load(Ordering::SeqCst) == 0 { + &self.main_textures.b.texture.default_view + } else { + &self.main_textures.a.texture.default_view + } + } + + /// The "main" sampled texture. + pub fn sampled_main_texture(&self) -> Option<&Texture> { + self.main_textures + .a + .resolve_target + .as_ref() + .map(|sampled| &sampled.texture) + } + + /// The "main" sampled texture view. + pub fn sampled_main_texture_view(&self) -> Option<&TextureView> { + self.main_textures + .a + .resolve_target + .as_ref() + .map(|sampled| &sampled.default_view) + } + + #[inline] + pub fn main_texture_format(&self) -> TextureFormat { + self.main_texture_format + } + + /// Returns `true` if and only if the main texture is [`Self::TEXTURE_FORMAT_HDR`] + #[inline] + pub fn is_hdr(&self) -> bool { + self.main_texture_format == ViewTarget::TEXTURE_FORMAT_HDR + } + + /// The final texture this view will render to. + #[inline] + pub fn out_texture(&self) -> &TextureView { + &self.out_texture.view + } + + pub fn out_texture_color_attachment( + &self, + clear_color: Option, + ) -> RenderPassColorAttachment<'_> { + self.out_texture.get_attachment(clear_color) + } + + /// The format of the final texture this view will render to + #[inline] + pub fn out_texture_format(&self) -> TextureFormat { + self.out_texture.format + } + + /// This will start a new "post process write", which assumes that the caller + /// will write the [`PostProcessWrite`]'s `source` to the `destination`. + /// + /// `source` is the "current" main texture. This will internally flip this + /// [`ViewTarget`]'s main texture to the `destination` texture, so the caller + /// _must_ ensure `source` is copied to `destination`, with or without modifications. + /// Failing to do so will cause the current main texture information to be lost. + pub fn post_process_write(&self) -> PostProcessWrite<'_> { + let old_is_a_main_texture = self.main_texture.fetch_xor(1, Ordering::SeqCst); + // if the old main texture is a, then the post processing must write from a to b + if old_is_a_main_texture == 0 { + self.main_textures.b.mark_as_cleared(); + PostProcessWrite { + source: &self.main_textures.a.texture.default_view, + source_texture: &self.main_textures.a.texture.texture, + destination: &self.main_textures.b.texture.default_view, + destination_texture: &self.main_textures.b.texture.texture, + } + } else { + self.main_textures.a.mark_as_cleared(); + PostProcessWrite { + source: &self.main_textures.b.texture.default_view, + source_texture: &self.main_textures.b.texture.texture, + destination: &self.main_textures.a.texture.default_view, + destination_texture: &self.main_textures.a.texture.texture, + } + } + } +} + +#[derive(Component)] +pub struct ViewDepthTexture { + pub texture: Texture, + attachment: DepthAttachment, +} + +impl ViewDepthTexture { + pub fn new(texture: CachedTexture, clear_value: Option) -> Self { + Self { + texture: texture.texture, + attachment: DepthAttachment::new(texture.default_view, clear_value), + } + } + + pub fn get_attachment(&self, store: StoreOp) -> RenderPassDepthStencilAttachment<'_> { + self.attachment.get_attachment(store) + } + + pub fn view(&self) -> &TextureView { + &self.attachment.view + } +} + +pub fn prepare_view_uniforms( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut view_uniforms: ResMut, + views: Query<( + Entity, + Option<&ExtractedCamera>, + &ExtractedView, + Option<&Frustum>, + Option<&TemporalJitter>, + Option<&MipBias>, + Option<&MainPassResolutionOverride>, + )>, + frame_count: Res, +) { + let view_iter = views.iter(); + let view_count = view_iter.len(); + let Some(mut writer) = + view_uniforms + .uniforms + .get_writer(view_count, &render_device, &render_queue) + else { + return; + }; + for ( + entity, + extracted_camera, + extracted_view, + frustum, + temporal_jitter, + mip_bias, + resolution_override, + ) in &views + { + let viewport = extracted_view.viewport.as_vec4(); + let mut main_pass_viewport = viewport; + if let Some(resolution_override) = resolution_override { + main_pass_viewport.z = resolution_override.0.x as f32; + main_pass_viewport.w = resolution_override.0.y as f32; + } + + let unjittered_projection = extracted_view.clip_from_view; + let mut clip_from_view = unjittered_projection; + + if let Some(temporal_jitter) = temporal_jitter { + temporal_jitter.jitter_projection(&mut clip_from_view, main_pass_viewport.zw()); + } + + let view_from_clip = clip_from_view.inverse(); + let world_from_view = extracted_view.world_from_view.to_matrix(); + let view_from_world = world_from_view.inverse(); + + let clip_from_world = if temporal_jitter.is_some() { + clip_from_view * view_from_world + } else { + extracted_view + .clip_from_world + .unwrap_or_else(|| clip_from_view * view_from_world) + }; + + // Map Frustum type to shader array, 6> + let frustum = frustum + .map(|frustum| frustum.half_spaces.map(|h| h.normal_d())) + .unwrap_or([Vec4::ZERO; 6]); + + let view_uniforms = ViewUniformOffset { + offset: writer.write(&ViewUniform { + clip_from_world, + unjittered_clip_from_world: unjittered_projection * view_from_world, + world_from_clip: world_from_view * view_from_clip, + world_from_view, + view_from_world, + clip_from_view, + view_from_clip, + world_position: extracted_view.world_from_view.translation(), + exposure: extracted_camera + .map(|c| c.exposure) + .unwrap_or_else(|| Exposure::default().exposure()), + viewport, + main_pass_viewport, + frustum, + color_grading: extracted_view.color_grading.clone().into(), + mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0, + frame_count: frame_count.0, + }), + }; + + commands.entity(entity).insert(view_uniforms); + } +} + +#[derive(Clone)] +struct MainTargetTextures { + a: ColorAttachment, + b: ColorAttachment, + /// 0 represents `main_textures.a`, 1 represents `main_textures.b` + /// This is shared across view targets with the same render target + main_texture: Arc, +} + +/// Prepares the view target [`OutputColorAttachment`] for each view in the current frame. +pub fn prepare_view_attachments( + windows: Res, + images: Res>, + manual_texture_views: Res, + cameras: Query<&ExtractedCamera>, + mut view_target_attachments: ResMut, +) { + for camera in cameras.iter() { + let Some(target) = &camera.target else { + continue; + }; + + match view_target_attachments.entry(target.clone()) { + Entry::Occupied(_) => {} + Entry::Vacant(entry) => { + let Some(attachment) = target + .get_texture_view(&windows, &images, &manual_texture_views) + .cloned() + .zip(target.get_texture_format(&windows, &images, &manual_texture_views)) + .map(|(view, format)| { + OutputColorAttachment::new(view.clone(), format.add_srgb_suffix()) + }) + else { + continue; + }; + entry.insert(attachment); + } + }; + } +} + +/// Clears the view target [`OutputColorAttachment`]s. +pub fn clear_view_attachments(mut view_target_attachments: ResMut) { + view_target_attachments.clear(); +} + +pub fn prepare_view_targets( + mut commands: Commands, + clear_color_global: Res, + render_device: Res, + mut texture_cache: ResMut, + cameras: Query<( + Entity, + &ExtractedCamera, + &ExtractedView, + &CameraMainTextureUsages, + &Msaa, + )>, + view_target_attachments: Res, +) { + let mut textures = >::default(); + for (entity, camera, view, texture_usage, msaa) in cameras.iter() { + let (Some(target_size), Some(target)) = (camera.physical_target_size, &camera.target) + else { + continue; + }; + + let Some(out_attachment) = view_target_attachments.get(target) else { + continue; + }; + + let main_texture_format = if view.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }; + + let clear_color = match camera.clear_color { + ClearColorConfig::Custom(color) => Some(color), + ClearColorConfig::None => None, + _ => Some(clear_color_global.0), + }; + + let (a, b, sampled, main_texture) = textures + .entry((camera.target.clone(), texture_usage.0, view.hdr, msaa)) + .or_insert_with(|| { + let descriptor = TextureDescriptor { + label: None, + size: target_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: main_texture_format, + usage: texture_usage.0, + view_formats: match main_texture_format { + TextureFormat::Bgra8Unorm => &[TextureFormat::Bgra8UnormSrgb], + TextureFormat::Rgba8Unorm => &[TextureFormat::Rgba8UnormSrgb], + _ => &[], + }, + }; + let a = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("main_texture_a"), + ..descriptor + }, + ); + let b = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("main_texture_b"), + ..descriptor + }, + ); + let sampled = if msaa.samples() > 1 { + let sampled = texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("main_texture_sampled"), + size: target_size.to_extents(), + mip_level_count: 1, + sample_count: msaa.samples(), + dimension: TextureDimension::D2, + format: main_texture_format, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: descriptor.view_formats, + }, + ); + Some(sampled) + } else { + None + }; + let main_texture = Arc::new(AtomicUsize::new(0)); + (a, b, sampled, main_texture) + }); + + let converted_clear_color = clear_color.map(Into::into); + + let main_textures = MainTargetTextures { + a: ColorAttachment::new(a.clone(), sampled.clone(), converted_clear_color), + b: ColorAttachment::new(b.clone(), sampled.clone(), converted_clear_color), + main_texture: main_texture.clone(), + }; + + commands.entity(entity).insert(ViewTarget { + main_texture: main_textures.main_texture.clone(), + main_textures, + main_texture_format, + out_texture: out_attachment.clone(), + }); + } +} diff --git a/crates/libmarathon/src/render/view/view.wgsl b/crates/libmarathon/src/render/view/view.wgsl new file mode 100644 index 0000000..23ded53 --- /dev/null +++ b/crates/libmarathon/src/render/view/view.wgsl @@ -0,0 +1,272 @@ +#define_import_path bevy_render::view + +struct ColorGrading { + balance: mat3x3, + saturation: vec3, + contrast: vec3, + gamma: vec3, + gain: vec3, + lift: vec3, + midtone_range: vec2, + exposure: f32, + hue: f32, + post_saturation: f32, +} + +struct View { + clip_from_world: mat4x4, + unjittered_clip_from_world: mat4x4, + world_from_clip: mat4x4, + world_from_view: mat4x4, + view_from_world: mat4x4, + // Typically a column-major right-handed projection matrix, one of either: + // + // Perspective (infinite reverse z) + // ``` + // f = 1 / tan(fov_y_radians / 2) + // + // ⎡ f / aspect 0 0 0 ⎤ + // ⎢ 0 f 0 0 ⎥ + // ⎢ 0 0 0 near ⎥ + // ⎣ 0 0 -1 0 ⎦ + // ``` + // + // Orthographic + // ``` + // w = right - left + // h = top - bottom + // d = far - near + // cw = -right - left + // ch = -top - bottom + // + // ⎡ 2 / w 0 0 cw / w ⎤ + // ⎢ 0 2 / h 0 ch / h ⎥ + // ⎢ 0 0 1 / d far / d ⎥ + // ⎣ 0 0 0 1 ⎦ + // ``` + // + // `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic + // + // Wgsl matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]` + // + // Custom projections are also possible however. + clip_from_view: mat4x4, + view_from_clip: mat4x4, + world_position: vec3, + exposure: f32, + // viewport(x_origin, y_origin, width, height) + viewport: vec4, + main_pass_viewport: vec4, + // 6 world-space half spaces (normal: vec3, distance: f32) ordered left, right, top, bottom, near, far. + // The normal vectors point towards the interior of the frustum. + // A half space contains `p` if `normal.dot(p) + distance > 0.` + frustum: array, 6>, + color_grading: ColorGrading, + mip_bias: f32, + frame_count: u32, +}; + +/// World space: +/// +y is up + +/// View space: +/// -z is forward, +x is right, +y is up +/// Forward is from the camera position into the scene. +/// (0.0, 0.0, -1.0) is linear distance of 1.0 in front of the camera's view relative to the camera's rotation +/// (0.0, 1.0, 0.0) is linear distance of 1.0 above the camera's view relative to the camera's rotation + +/// NDC (normalized device coordinate): +/// https://www.w3.org/TR/webgpu/#coordinate-systems +/// (-1.0, -1.0) in NDC is located at the bottom-left corner of NDC +/// (1.0, 1.0) in NDC is located at the top-right corner of NDC +/// Z is depth where: +/// 1.0 is near clipping plane +/// Perspective projection: 0.0 is inf far away +/// Orthographic projection: 0.0 is far clipping plane + +/// Clip space: +/// This is NDC before the perspective divide, still in homogenous coordinate space. +/// Dividing a clip space point by its w component yields a point in NDC space. + +/// UV space: +/// 0.0, 0.0 is the top left +/// 1.0, 1.0 is the bottom right + + +// ----------------- +// TO WORLD -------- +// ----------------- + +/// Convert a view space position to world space +fn position_view_to_world(view_pos: vec3, world_from_view: mat4x4) -> vec3 { + let world_pos = world_from_view * vec4(view_pos, 1.0); + return world_pos.xyz; +} + +/// Convert a clip space position to world space +fn position_clip_to_world(clip_pos: vec4, world_from_clip: mat4x4) -> vec3 { + let world_pos = world_from_clip * clip_pos; + return world_pos.xyz; +} + +/// Convert a ndc space position to world space +fn position_ndc_to_world(ndc_pos: vec3, world_from_clip: mat4x4) -> vec3 { + let world_pos = world_from_clip * vec4(ndc_pos, 1.0); + return world_pos.xyz / world_pos.w; +} + +/// Convert a view space direction to world space +fn direction_view_to_world(view_dir: vec3, world_from_view: mat4x4) -> vec3 { + let world_dir = world_from_view * vec4(view_dir, 0.0); + return world_dir.xyz; +} + +/// Convert a clip space direction to world space +fn direction_clip_to_world(clip_dir: vec4, world_from_clip: mat4x4) -> vec3 { + let world_dir = world_from_clip * clip_dir; + return world_dir.xyz; +} + +// ----------------- +// TO VIEW --------- +// ----------------- + +/// Convert a world space position to view space +fn position_world_to_view(world_pos: vec3, view_from_world: mat4x4) -> vec3 { + let view_pos = view_from_world * vec4(world_pos, 1.0); + return view_pos.xyz; +} + +/// Convert a clip space position to view space +fn position_clip_to_view(clip_pos: vec4, view_from_clip: mat4x4) -> vec3 { + let view_pos = view_from_clip * clip_pos; + return view_pos.xyz; +} + +/// Convert a ndc space position to view space +fn position_ndc_to_view(ndc_pos: vec3, view_from_clip: mat4x4) -> vec3 { + let view_pos = view_from_clip * vec4(ndc_pos, 1.0); + return view_pos.xyz / view_pos.w; +} + +/// Convert a world space direction to view space +fn direction_world_to_view(world_dir: vec3, view_from_world: mat4x4) -> vec3 { + let view_dir = view_from_world * vec4(world_dir, 0.0); + return view_dir.xyz; +} + +/// Convert a clip space direction to view space +fn direction_clip_to_view(clip_dir: vec4, view_from_clip: mat4x4) -> vec3 { + let view_dir = view_from_clip * clip_dir; + return view_dir.xyz; +} + +// ----------------- +// TO CLIP --------- +// ----------------- + +/// Convert a world space position to clip space +fn position_world_to_clip(world_pos: vec3, clip_from_world: mat4x4) -> vec4 { + let clip_pos = clip_from_world * vec4(world_pos, 1.0); + return clip_pos; +} + +/// Convert a view space position to clip space +fn position_view_to_clip(view_pos: vec3, clip_from_view: mat4x4) -> vec4 { + let clip_pos = clip_from_view * vec4(view_pos, 1.0); + return clip_pos; +} + +/// Convert a world space direction to clip space +fn direction_world_to_clip(world_dir: vec3, clip_from_world: mat4x4) -> vec4 { + let clip_dir = clip_from_world * vec4(world_dir, 0.0); + return clip_dir; +} + +/// Convert a view space direction to clip space +fn direction_view_to_clip(view_dir: vec3, clip_from_view: mat4x4) -> vec4 { + let clip_dir = clip_from_view * vec4(view_dir, 0.0); + return clip_dir; +} + +// ----------------- +// TO NDC ---------- +// ----------------- + +/// Convert a world space position to ndc space +fn position_world_to_ndc(world_pos: vec3, clip_from_world: mat4x4) -> vec3 { + let ndc_pos = clip_from_world * vec4(world_pos, 1.0); + return ndc_pos.xyz / ndc_pos.w; +} + +/// Convert a view space position to ndc space +fn position_view_to_ndc(view_pos: vec3, clip_from_view: mat4x4) -> vec3 { + let ndc_pos = clip_from_view * vec4(view_pos, 1.0); + return ndc_pos.xyz / ndc_pos.w; +} + +// ----------------- +// DEPTH ----------- +// ----------------- + +/// Retrieve the perspective camera near clipping plane +fn perspective_camera_near(clip_from_view: mat4x4) -> f32 { + return clip_from_view[3][2]; +} + +/// Convert ndc depth to linear view z. +/// Note: Depth values in front of the camera will be negative as -z is forward +fn depth_ndc_to_view_z(ndc_depth: f32, clip_from_view: mat4x4, view_from_clip: mat4x4) -> f32 { +#ifdef VIEW_PROJECTION_PERSPECTIVE + return -perspective_camera_near(clip_from_view) / ndc_depth; +#else ifdef VIEW_PROJECTION_ORTHOGRAPHIC + return -(clip_from_view[3][2] - ndc_depth) / clip_from_view[2][2]; +#else + let view_pos = view_from_clip * vec4(0.0, 0.0, ndc_depth, 1.0); + return view_pos.z / view_pos.w; +#endif +} + +/// Convert linear view z to ndc depth. +/// Note: View z input should be negative for values in front of the camera as -z is forward +fn view_z_to_depth_ndc(view_z: f32, clip_from_view: mat4x4) -> f32 { +#ifdef VIEW_PROJECTION_PERSPECTIVE + return -perspective_camera_near(clip_from_view) / view_z; +#else ifdef VIEW_PROJECTION_ORTHOGRAPHIC + return clip_from_view[3][2] + view_z * clip_from_view[2][2]; +#else + let ndc_pos = clip_from_view * vec4(0.0, 0.0, view_z, 1.0); + return ndc_pos.z / ndc_pos.w; +#endif +} + +// ----------------- +// UV -------------- +// ----------------- + +/// Convert ndc space xy coordinate [-1.0 .. 1.0] to uv [0.0 .. 1.0] +fn ndc_to_uv(ndc: vec2) -> vec2 { + return ndc * vec2(0.5, -0.5) + vec2(0.5); +} + +/// Convert uv [0.0 .. 1.0] coordinate to ndc space xy [-1.0 .. 1.0] +fn uv_to_ndc(uv: vec2) -> vec2 { + return uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0); +} + +/// returns the (0.0, 0.0) .. (1.0, 1.0) position within the viewport for the current render target +/// [0 .. render target viewport size] eg. [(0.0, 0.0) .. (1280.0, 720.0)] to [(0.0, 0.0) .. (1.0, 1.0)] +fn frag_coord_to_uv(frag_coord: vec2, viewport: vec4) -> vec2 { + return (frag_coord - viewport.xy) / viewport.zw; +} + +/// Convert frag coord to ndc +fn frag_coord_to_ndc(frag_coord: vec4, viewport: vec4) -> vec3 { + return vec3(uv_to_ndc(frag_coord_to_uv(frag_coord.xy, viewport)), frag_coord.z); +} + +/// Convert ndc space xy coordinate [-1.0 .. 1.0] to [0 .. render target +/// viewport size] +fn ndc_to_frag_coord(ndc: vec2, viewport: vec4) -> vec2 { + return ndc_to_uv(ndc) * viewport.zw; +} diff --git a/crates/libmarathon/src/render/view/visibility/mod.rs b/crates/libmarathon/src/render/view/visibility/mod.rs new file mode 100644 index 0000000..353da4b --- /dev/null +++ b/crates/libmarathon/src/render/view/visibility/mod.rs @@ -0,0 +1,54 @@ +use core::any::TypeId; + +use bevy_ecs::{component::Component, entity::Entity, prelude::ReflectComponent}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_utils::TypeIdMap; + +use crate::render::sync_world::MainEntity; + +mod range; +use bevy_camera::visibility::*; +pub use range::*; + +/// Collection of entities visible from the current view. +/// +/// This component is extracted from [`VisibleEntities`]. +#[derive(Clone, Component, Default, Debug, Reflect)] +#[reflect(Component, Default, Debug, Clone)] +pub struct RenderVisibleEntities { + #[reflect(ignore, clone)] + pub entities: TypeIdMap>, +} + +impl RenderVisibleEntities { + pub fn get(&self) -> &[(Entity, MainEntity)] + where + QF: 'static, + { + match self.entities.get(&TypeId::of::()) { + Some(entities) => &entities[..], + None => &[], + } + } + + pub fn iter(&self) -> impl DoubleEndedIterator + where + QF: 'static, + { + self.get::().iter() + } + + pub fn len(&self) -> usize + where + QF: 'static, + { + self.get::().len() + } + + pub fn is_empty(&self) -> bool + where + QF: 'static, + { + self.get::().is_empty() + } +} diff --git a/crates/libmarathon/src/render/view/visibility/range.rs b/crates/libmarathon/src/render/view/visibility/range.rs new file mode 100644 index 0000000..a6d2532 --- /dev/null +++ b/crates/libmarathon/src/render/view/visibility/range.rs @@ -0,0 +1,228 @@ +//! Specific distances from the camera in which entities are visible, also known +//! as *hierarchical levels of detail* or *HLOD*s. + +use super::VisibilityRange; +use bevy_app::{App, Plugin}; +use bevy_ecs::{ + entity::Entity, + lifecycle::RemovedComponents, + query::Changed, + resource::Resource, + schedule::IntoScheduleConfigs as _, + system::{Query, Res, ResMut}, +}; +use bevy_math::{vec4, Vec4}; +use bevy_platform::collections::HashMap; +use bevy_utils::prelude::default; +use nonmax::NonMaxU16; +use wgpu::{BufferBindingType, BufferUsages}; + +use crate::render::{ + render_resource::BufferVec, + renderer::{RenderDevice, RenderQueue}, + sync_world::{MainEntity, MainEntityHashMap}, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; + +/// We need at least 4 storage buffer bindings available to enable the +/// visibility range buffer. +/// +/// Even though we only use one storage buffer, the first 3 available storage +/// buffers will go to various light-related buffers. We will grab the fourth +/// buffer slot. +pub const VISIBILITY_RANGES_STORAGE_BUFFER_COUNT: u32 = 4; + +/// The size of the visibility ranges buffer in elements (not bytes) when fewer +/// than 6 storage buffers are available and we're forced to use a uniform +/// buffer instead (most notably, on WebGL 2). +const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: usize = 64; + +/// A plugin that enables [`RenderVisibilityRanges`]s, which allow entities to be +/// hidden or shown based on distance to the camera. +pub struct RenderVisibilityRangePlugin; + +impl Plugin for RenderVisibilityRangePlugin { + fn build(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .add_systems(ExtractSchedule, extract_visibility_ranges) + .add_systems( + Render, + write_render_visibility_ranges.in_set(RenderSystems::PrepareResourcesFlush), + ); + } +} + +/// Stores information related to [`VisibilityRange`]s in the render world. +#[derive(Resource)] +pub struct RenderVisibilityRanges { + /// Information corresponding to each entity. + entities: MainEntityHashMap, + + /// Maps a [`VisibilityRange`] to its index within the `buffer`. + /// + /// This map allows us to deduplicate identical visibility ranges, which + /// saves GPU memory. + range_to_index: HashMap, + + /// The GPU buffer that stores [`VisibilityRange`]s. + /// + /// Each [`Vec4`] contains the start margin start, start margin end, end + /// margin start, and end margin end distances, in that order. + buffer: BufferVec, + + /// True if the buffer has been changed since the last frame and needs to be + /// reuploaded to the GPU. + buffer_dirty: bool, +} + +/// Per-entity information related to [`VisibilityRange`]s. +struct RenderVisibilityEntityInfo { + /// The index of the range within the GPU buffer. + buffer_index: NonMaxU16, + /// True if the range is abrupt: i.e. has no crossfade. + is_abrupt: bool, +} + +impl Default for RenderVisibilityRanges { + fn default() -> Self { + Self { + entities: default(), + range_to_index: default(), + buffer: BufferVec::new( + BufferUsages::STORAGE | BufferUsages::UNIFORM | BufferUsages::VERTEX, + ), + buffer_dirty: true, + } + } +} + +impl RenderVisibilityRanges { + /// Clears out the [`RenderVisibilityRanges`] in preparation for a new + /// frame. + fn clear(&mut self) { + self.entities.clear(); + self.range_to_index.clear(); + self.buffer.clear(); + self.buffer_dirty = true; + } + + /// Inserts a new entity into the [`RenderVisibilityRanges`]. + fn insert(&mut self, entity: MainEntity, visibility_range: &VisibilityRange) { + // Grab a slot in the GPU buffer, or take the existing one if there + // already is one. + let buffer_index = *self + .range_to_index + .entry(visibility_range.clone()) + .or_insert_with(|| { + NonMaxU16::try_from(self.buffer.push(vec4( + visibility_range.start_margin.start, + visibility_range.start_margin.end, + visibility_range.end_margin.start, + visibility_range.end_margin.end, + )) as u16) + .unwrap_or_default() + }); + + self.entities.insert( + entity, + RenderVisibilityEntityInfo { + buffer_index, + is_abrupt: visibility_range.is_abrupt(), + }, + ); + } + + /// Returns the index in the GPU buffer corresponding to the visible range + /// for the given entity. + /// + /// If the entity has no visible range, returns `None`. + #[inline] + pub fn lod_index_for_entity(&self, entity: MainEntity) -> Option { + self.entities.get(&entity).map(|info| info.buffer_index) + } + + /// Returns true if the entity has a visibility range and it isn't abrupt: + /// i.e. if it has a crossfade. + #[inline] + pub fn entity_has_crossfading_visibility_ranges(&self, entity: MainEntity) -> bool { + self.entities + .get(&entity) + .is_some_and(|info| !info.is_abrupt) + } + + /// Returns a reference to the GPU buffer that stores visibility ranges. + #[inline] + pub fn buffer(&self) -> &BufferVec { + &self.buffer + } +} + +/// Extracts all [`VisibilityRange`] components from the main world to the +/// render world and inserts them into [`RenderVisibilityRanges`]. +pub fn extract_visibility_ranges( + mut render_visibility_ranges: ResMut, + visibility_ranges_query: Extract>, + changed_ranges_query: Extract>>, + mut removed_visibility_ranges: Extract>, +) { + if changed_ranges_query.is_empty() && removed_visibility_ranges.read().next().is_none() { + return; + } + + render_visibility_ranges.clear(); + for (entity, visibility_range) in visibility_ranges_query.iter() { + render_visibility_ranges.insert(entity.into(), visibility_range); + } +} + +/// Writes the [`RenderVisibilityRanges`] table to the GPU. +pub fn write_render_visibility_ranges( + render_device: Res, + render_queue: Res, + mut render_visibility_ranges: ResMut, +) { + // If there haven't been any changes, early out. + if !render_visibility_ranges.buffer_dirty { + return; + } + + // Mess with the length of the buffer to meet API requirements if necessary. + match render_device.get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT) + { + // If we're using a uniform buffer, we must have *exactly* + // `VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE` elements. + BufferBindingType::Uniform + if render_visibility_ranges.buffer.len() > VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE => + { + render_visibility_ranges + .buffer + .truncate(VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE); + } + BufferBindingType::Uniform + if render_visibility_ranges.buffer.len() < VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE => + { + while render_visibility_ranges.buffer.len() < VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE { + render_visibility_ranges.buffer.push(default()); + } + } + + // Otherwise, if we're using a storage buffer, just ensure there's + // something in the buffer, or else it won't get allocated. + BufferBindingType::Storage { .. } if render_visibility_ranges.buffer.is_empty() => { + render_visibility_ranges.buffer.push(default()); + } + + _ => {} + } + + // Schedule the write. + render_visibility_ranges + .buffer + .write_buffer(&render_device, &render_queue); + render_visibility_ranges.buffer_dirty = false; +} diff --git a/crates/libmarathon/src/render/view/window/mod.rs b/crates/libmarathon/src/render/view/window/mod.rs new file mode 100644 index 0000000..d73e734 --- /dev/null +++ b/crates/libmarathon/src/render/view/window/mod.rs @@ -0,0 +1,401 @@ +use crate::render::renderer::WgpuWrapper; +use crate::render::{ + render_resource::{SurfaceTexture, TextureView}, + renderer::{RenderAdapter, RenderDevice, RenderInstance}, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use bevy_app::{App, Plugin}; +use bevy_ecs::{entity::EntityHashMap, prelude::*}; +use bevy_platform::collections::HashSet; +use bevy_utils::default; +use bevy_window::{ + CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing, +}; +use core::{ + num::NonZero, + ops::{Deref, DerefMut}, +}; +use tracing::{debug, warn}; +use wgpu::{ + SurfaceConfiguration, SurfaceTargetUnsafe, TextureFormat, TextureUsages, TextureViewDescriptor, +}; + +pub mod screenshot; + +use screenshot::ScreenshotPlugin; + +pub struct WindowRenderPlugin; + +impl Plugin for WindowRenderPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(ScreenshotPlugin); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::() + .add_systems(ExtractSchedule, extract_windows) + .add_systems( + Render, + create_surfaces + .run_if(need_surface_configuration) + .before(prepare_windows), + ) + .add_systems(Render, prepare_windows.in_set(RenderSystems::ManageViews)); + } + } +} + +pub struct ExtractedWindow { + /// An entity that contains the components in [`Window`]. + pub entity: Entity, + pub handle: RawHandleWrapper, + pub physical_width: u32, + pub physical_height: u32, + pub present_mode: PresentMode, + pub desired_maximum_frame_latency: Option>, + /// Note: this will not always be the swap chain texture view. When taking a screenshot, + /// this will point to an alternative texture instead to allow for copying the render result + /// to CPU memory. + pub swap_chain_texture_view: Option, + pub swap_chain_texture: Option, + pub swap_chain_texture_format: Option, + pub size_changed: bool, + pub present_mode_changed: bool, + pub alpha_mode: CompositeAlphaMode, +} + +impl ExtractedWindow { + fn set_swapchain_texture(&mut self, frame: wgpu::SurfaceTexture) { + let texture_view_descriptor = TextureViewDescriptor { + format: Some(frame.texture.format().add_srgb_suffix()), + ..default() + }; + self.swap_chain_texture_view = Some(TextureView::from( + frame.texture.create_view(&texture_view_descriptor), + )); + self.swap_chain_texture = Some(SurfaceTexture::from(frame)); + } +} + +#[derive(Default, Resource)] +pub struct ExtractedWindows { + pub primary: Option, + pub windows: EntityHashMap, +} + +impl Deref for ExtractedWindows { + type Target = EntityHashMap; + + fn deref(&self) -> &Self::Target { + &self.windows + } +} + +impl DerefMut for ExtractedWindows { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.windows + } +} + +fn extract_windows( + mut extracted_windows: ResMut, + mut closing: Extract>, + windows: Extract)>>, + mut removed: Extract>, + mut window_surfaces: ResMut, +) { + for (entity, window, handle, primary) in windows.iter() { + if primary.is_some() { + extracted_windows.primary = Some(entity); + } + + let (new_width, new_height) = ( + window.resolution.physical_width().max(1), + window.resolution.physical_height().max(1), + ); + + let extracted_window = extracted_windows.entry(entity).or_insert(ExtractedWindow { + entity, + handle: handle.clone(), + physical_width: new_width, + physical_height: new_height, + present_mode: window.present_mode, + desired_maximum_frame_latency: window.desired_maximum_frame_latency, + swap_chain_texture: None, + swap_chain_texture_view: None, + size_changed: false, + swap_chain_texture_format: None, + present_mode_changed: false, + alpha_mode: window.composite_alpha_mode, + }); + + // NOTE: Drop the swap chain frame here + extracted_window.swap_chain_texture_view = None; + extracted_window.size_changed = new_width != extracted_window.physical_width + || new_height != extracted_window.physical_height; + extracted_window.present_mode_changed = + window.present_mode != extracted_window.present_mode; + + if extracted_window.size_changed { + debug!( + "Window size changed from {}x{} to {}x{}", + extracted_window.physical_width, + extracted_window.physical_height, + new_width, + new_height + ); + extracted_window.physical_width = new_width; + extracted_window.physical_height = new_height; + } + + if extracted_window.present_mode_changed { + debug!( + "Window Present Mode changed from {:?} to {:?}", + extracted_window.present_mode, window.present_mode + ); + extracted_window.present_mode = window.present_mode; + } + } + + for closing_window in closing.read() { + extracted_windows.remove(&closing_window.window); + window_surfaces.remove(&closing_window.window); + } + for removed_window in removed.read() { + extracted_windows.remove(&removed_window); + window_surfaces.remove(&removed_window); + } +} + +struct SurfaceData { + // TODO: what lifetime should this be? + surface: WgpuWrapper>, + configuration: SurfaceConfiguration, +} + +#[derive(Resource, Default)] +pub struct WindowSurfaces { + surfaces: EntityHashMap, + /// List of windows that we have already called the initial `configure_surface` for + configured_windows: HashSet, +} + +impl WindowSurfaces { + fn remove(&mut self, window: &Entity) { + self.surfaces.remove(window); + self.configured_windows.remove(window); + } +} + +/// (re)configures window surfaces, and obtains a swapchain texture for rendering. +/// +/// NOTE: `get_current_texture` in `prepare_windows` can take a long time if the GPU workload is +/// the performance bottleneck. This can be seen in profiles as multiple prepare-set systems all +/// taking an unusually long time to complete, and all finishing at about the same time as the +/// `prepare_windows` system. Improvements in bevy are planned to avoid this happening when it +/// should not but it will still happen as it is easy for a user to create a large GPU workload +/// relative to the GPU performance and/or CPU workload. +/// This can be caused by many reasons, but several of them are: +/// - GPU workload is more than your current GPU can manage +/// - Error / performance bug in your custom shaders +/// - wgpu was unable to detect a proper GPU hardware-accelerated device given the chosen +/// [`Backends`](crate::settings::Backends), [`WgpuLimits`](crate::settings::WgpuLimits), +/// and/or [`WgpuFeatures`](crate::settings::WgpuFeatures). For example, on Windows currently +/// `DirectX 11` is not supported by wgpu 0.12 and so if your GPU/drivers do not support Vulkan, +/// it may be that a software renderer called "Microsoft Basic Render Driver" using `DirectX 12` +/// will be chosen and performance will be very poor. This is visible in a log message that is +/// output during renderer initialization. +/// Another alternative is to try to use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle) and +/// [`Backends::GL`](crate::settings::Backends::GL) with the `gles` feature enabled if your +/// GPU/drivers support `OpenGL 4.3` / `OpenGL ES 3.0` or later. +pub fn prepare_windows( + mut windows: ResMut, + mut window_surfaces: ResMut, + render_device: Res, + #[cfg(target_os = "linux")] render_instance: Res, +) { + for window in windows.windows.values_mut() { + let window_surfaces = window_surfaces.deref_mut(); + let Some(surface_data) = window_surfaces.surfaces.get(&window.entity) else { + continue; + }; + + // A recurring issue is hitting `wgpu::SurfaceError::Timeout` on certain Linux + // mesa driver implementations. This seems to be a quirk of some drivers. + // We'd rather keep panicking when not on Linux mesa, because in those case, + // the `Timeout` is still probably the symptom of a degraded unrecoverable + // application state. + // see https://github.com/bevyengine/bevy/pull/5957 + // and https://github.com/gfx-rs/wgpu/issues/1218 + #[cfg(target_os = "linux")] + let may_erroneously_timeout = || { + render_instance + .enumerate_adapters(wgpu::Backends::VULKAN) + .iter() + .any(|adapter| { + let name = adapter.get_info().name; + name.starts_with("Radeon") + || name.starts_with("AMD") + || name.starts_with("Intel") + }) + }; + + let surface = &surface_data.surface; + match surface.get_current_texture() { + Ok(frame) => { + window.set_swapchain_texture(frame); + } + Err(wgpu::SurfaceError::Outdated) => { + render_device.configure_surface(surface, &surface_data.configuration); + let frame = match surface.get_current_texture() { + Ok(frame) => frame, + Err(err) => { + // This is a common occurrence on X11 and Xwayland with NVIDIA drivers + // when opening and resizing the window. + warn!("Couldn't get swap chain texture after configuring. Cause: '{err}'"); + continue; + } + }; + window.set_swapchain_texture(frame); + } + #[cfg(target_os = "linux")] + Err(wgpu::SurfaceError::Timeout) if may_erroneously_timeout() => { + tracing::trace!( + "Couldn't get swap chain texture. This is probably a quirk \ + of your Linux GPU driver, so it can be safely ignored." + ); + } + Err(err) => { + panic!("Couldn't get swap chain texture, operation unrecoverable: {err}"); + } + } + window.swap_chain_texture_format = Some(surface_data.configuration.format); + } +} + +pub fn need_surface_configuration( + windows: Res, + window_surfaces: Res, +) -> bool { + for window in windows.windows.values() { + if !window_surfaces.configured_windows.contains(&window.entity) + || window.size_changed + || window.present_mode_changed + { + return true; + } + } + false +} + +// 2 is wgpu's default/what we've been using so far. +// 1 is the minimum, but may cause lower framerates due to the cpu waiting for the gpu to finish +// all work for the previous frame before starting work on the next frame, which then means the gpu +// has to wait for the cpu to finish to start on the next frame. +const DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY: u32 = 2; + +/// Creates window surfaces. +pub fn create_surfaces( + // By accessing a NonSend resource, we tell the scheduler to put this system on the main thread, + // which is necessary for some OS's + #[cfg(any(target_os = "macos", target_os = "ios"))] _marker: bevy_ecs::system::NonSendMarker, + windows: Res, + mut window_surfaces: ResMut, + render_instance: Res, + render_adapter: Res, + render_device: Res, +) { + for window in windows.windows.values() { + let data = window_surfaces + .surfaces + .entry(window.entity) + .or_insert_with(|| { + let surface_target = SurfaceTargetUnsafe::RawHandle { + raw_display_handle: window.handle.get_display_handle(), + raw_window_handle: window.handle.get_window_handle(), + }; + // SAFETY: The window handles in ExtractedWindows will always be valid objects to create surfaces on + let surface = unsafe { + // NOTE: On some OSes this MUST be called from the main thread. + // As of wgpu 0.15, only fallible if the given window is a HTML canvas and obtaining a WebGPU or WebGL2 context fails. + render_instance + .create_surface_unsafe(surface_target) + .expect("Failed to create wgpu surface") + }; + let caps = surface.get_capabilities(&render_adapter); + let formats = caps.formats; + // For future HDR output support, we'll need to request a format that supports HDR, + // but as of wgpu 0.15 that is not yet supported. + // Prefer sRGB formats for surfaces, but fall back to first available format if no sRGB formats are available. + let mut format = *formats.first().expect("No supported formats for surface"); + for available_format in formats { + // Rgba8UnormSrgb and Bgra8UnormSrgb and the only sRGB formats wgpu exposes that we can use for surfaces. + if available_format == TextureFormat::Rgba8UnormSrgb + || available_format == TextureFormat::Bgra8UnormSrgb + { + format = available_format; + break; + } + } + + let configuration = SurfaceConfiguration { + format, + width: window.physical_width, + height: window.physical_height, + usage: TextureUsages::RENDER_ATTACHMENT, + present_mode: match window.present_mode { + PresentMode::Fifo => wgpu::PresentMode::Fifo, + PresentMode::FifoRelaxed => wgpu::PresentMode::FifoRelaxed, + PresentMode::Mailbox => wgpu::PresentMode::Mailbox, + PresentMode::Immediate => wgpu::PresentMode::Immediate, + PresentMode::AutoVsync => wgpu::PresentMode::AutoVsync, + PresentMode::AutoNoVsync => wgpu::PresentMode::AutoNoVsync, + }, + desired_maximum_frame_latency: window + .desired_maximum_frame_latency + .map(NonZero::::get) + .unwrap_or(DEFAULT_DESIRED_MAXIMUM_FRAME_LATENCY), + alpha_mode: match window.alpha_mode { + CompositeAlphaMode::Auto => wgpu::CompositeAlphaMode::Auto, + CompositeAlphaMode::Opaque => wgpu::CompositeAlphaMode::Opaque, + CompositeAlphaMode::PreMultiplied => { + wgpu::CompositeAlphaMode::PreMultiplied + } + CompositeAlphaMode::PostMultiplied => { + wgpu::CompositeAlphaMode::PostMultiplied + } + CompositeAlphaMode::Inherit => wgpu::CompositeAlphaMode::Inherit, + }, + view_formats: if !format.is_srgb() { + vec![format.add_srgb_suffix()] + } else { + vec![] + }, + }; + + render_device.configure_surface(&surface, &configuration); + + SurfaceData { + surface: WgpuWrapper::new(surface), + configuration, + } + }); + + if window.size_changed || window.present_mode_changed { + data.configuration.width = window.physical_width; + data.configuration.height = window.physical_height; + data.configuration.present_mode = match window.present_mode { + PresentMode::Fifo => wgpu::PresentMode::Fifo, + PresentMode::FifoRelaxed => wgpu::PresentMode::FifoRelaxed, + PresentMode::Mailbox => wgpu::PresentMode::Mailbox, + PresentMode::Immediate => wgpu::PresentMode::Immediate, + PresentMode::AutoVsync => wgpu::PresentMode::AutoVsync, + PresentMode::AutoNoVsync => wgpu::PresentMode::AutoNoVsync, + }; + render_device.configure_surface(&data.surface, &data.configuration); + } + + window_surfaces.configured_windows.insert(window.entity); + } +} diff --git a/crates/libmarathon/src/render/view/window/screenshot.rs b/crates/libmarathon/src/render/view/window/screenshot.rs new file mode 100644 index 0000000..f56ac4a --- /dev/null +++ b/crates/libmarathon/src/render/view/window/screenshot.rs @@ -0,0 +1,695 @@ +use super::ExtractedWindows; +use crate::render::{ + gpu_readback, + render_asset::RenderAssets, + render_resource::{ + binding_types::texture_2d, BindGroup, BindGroupEntries, BindGroupLayout, + BindGroupLayoutEntries, Buffer, BufferUsages, CachedRenderPipelineId, FragmentState, + PipelineCache, RenderPipelineDescriptor, SpecializedRenderPipeline, + SpecializedRenderPipelines, Texture, TextureUsages, TextureView, VertexState, + }, + renderer::RenderDevice, + texture::{GpuImage, ManualTextureViews, OutputColorAttachment}, + view::{prepare_view_attachments, prepare_view_targets, ViewTargetAttachments, WindowSurfaces}, + ExtractSchedule, MainWorld, Render, RenderApp, RenderStartup, RenderSystems, +}; +use std::{borrow::Cow, sync::Arc}; +use bevy_app::{First, Plugin, Update}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle, RenderAssetUsages}; +use bevy_camera::{ManualTextureViewHandle, NormalizedRenderTarget, RenderTarget}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + entity::EntityHashMap, message::message_update_system, prelude::*, system::SystemState, +}; +use bevy_image::{Image, TextureFormatPixelInfo, ToExtents}; +use bevy_platform::collections::HashSet; +use bevy_reflect::Reflect; +use bevy_shader::Shader; +use bevy_tasks::AsyncComputeTaskPool; +use bevy_utils::default; +use bevy_window::{PrimaryWindow, WindowRef}; +use core::ops::Deref; +use std::{ + path::Path, + sync::{ + mpsc::{Receiver, Sender}, + Mutex, + }, +}; +use tracing::{error, info, warn}; +use wgpu::{CommandEncoder, Extent3d, TextureFormat}; + +#[derive(EntityEvent, Reflect, Deref, DerefMut, Debug)] +#[reflect(Debug)] +pub struct ScreenshotCaptured { + pub entity: Entity, + #[deref] + pub image: Image, +} + +/// A component that signals to the renderer to capture a screenshot this frame. +/// +/// This component should be spawned on a new entity with an observer that will trigger +/// with [`ScreenshotCaptured`] when the screenshot is ready. +/// +/// Screenshots are captured asynchronously and may not be available immediately after the frame +/// that the component is spawned on. The observer should be used to handle the screenshot when it +/// is ready. +/// +/// Note that the screenshot entity will be despawned after the screenshot is captured and the +/// observer is triggered. +/// +/// # Usage +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use crate::render::view::screenshot::{save_to_disk, Screenshot}; +/// +/// fn take_screenshot(mut commands: Commands) { +/// commands.spawn(Screenshot::primary_window()) +/// .observe(save_to_disk("screenshot.png")); +/// } +/// ``` +#[derive(Component, Deref, DerefMut, Reflect, Debug)] +#[reflect(Component, Debug)] +pub struct Screenshot(pub RenderTarget); + +/// A marker component that indicates that a screenshot is currently being captured. +#[derive(Component, Default)] +pub struct Capturing; + +/// A marker component that indicates that a screenshot has been captured, the image is ready, and +/// the screenshot entity can be despawned. +#[derive(Component, Default)] +pub struct Captured; + +impl Screenshot { + /// Capture a screenshot of the provided window entity. + pub fn window(window: Entity) -> Self { + Self(RenderTarget::Window(WindowRef::Entity(window))) + } + + /// Capture a screenshot of the primary window, if one exists. + pub fn primary_window() -> Self { + Self(RenderTarget::Window(WindowRef::Primary)) + } + + /// Capture a screenshot of the provided render target image. + pub fn image(image: Handle) -> Self { + Self(RenderTarget::Image(image.into())) + } + + /// Capture a screenshot of the provided manual texture view. + pub fn texture_view(texture_view: ManualTextureViewHandle) -> Self { + Self(RenderTarget::TextureView(texture_view)) + } +} + +struct ScreenshotPreparedState { + pub texture: Texture, + pub buffer: Buffer, + pub bind_group: BindGroup, + pub pipeline_id: CachedRenderPipelineId, + pub size: Extent3d, +} + +#[derive(Resource, Deref, DerefMut)] +pub struct CapturedScreenshots(pub Arc>>); + +#[derive(Resource, Deref, DerefMut, Default)] +struct RenderScreenshotTargets(EntityHashMap); + +#[derive(Resource, Deref, DerefMut, Default)] +struct RenderScreenshotsPrepared(EntityHashMap); + +#[derive(Resource, Deref, DerefMut)] +struct RenderScreenshotsSender(Sender<(Entity, Image)>); + +/// Saves the captured screenshot to disk at the provided path. +pub fn save_to_disk(path: impl AsRef) -> impl FnMut(On) { + let path = path.as_ref().to_owned(); + move |screenshot_captured| { + let img = screenshot_captured.image.clone(); + match img.try_into_dynamic() { + Ok(dyn_img) => match image::ImageFormat::from_path(&path) { + Ok(format) => { + // discard the alpha channel which stores brightness values when HDR is enabled to make sure + // the screenshot looks right + let img = dyn_img.to_rgb8(); + #[cfg(not(target_arch = "wasm32"))] + match img.save_with_format(&path, format) { + Ok(_) => info!("Screenshot saved to {}", path.display()), + Err(e) => error!("Cannot save screenshot, IO error: {e}"), + } + + #[cfg(target_arch = "wasm32")] + { + let save_screenshot = || { + use image::EncodableLayout; + use wasm_bindgen::{JsCast, JsValue}; + + let mut image_buffer = std::io::Cursor::new(Vec::new()); + img.write_to(&mut image_buffer, format) + .map_err(|e| JsValue::from_str(&format!("{e}")))?; + // SAFETY: `image_buffer` only exist in this closure, and is not used after this line + let parts = js_sys::Array::of1(&unsafe { + js_sys::Uint8Array::view(image_buffer.into_inner().as_bytes()) + .into() + }); + let blob = web_sys::Blob::new_with_u8_array_sequence(&parts)?; + let url = web_sys::Url::create_object_url_with_blob(&blob)?; + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let link = document.create_element("a")?; + link.set_attribute("href", &url)?; + link.set_attribute( + "download", + path.file_name() + .and_then(|filename| filename.to_str()) + .ok_or_else(|| JsValue::from_str("Invalid filename"))?, + )?; + let html_element = link.dyn_into::()?; + html_element.click(); + web_sys::Url::revoke_object_url(&url)?; + Ok::<(), JsValue>(()) + }; + + match (save_screenshot)() { + Ok(_) => info!("Screenshot saved to {}", path.display()), + Err(e) => error!("Cannot save screenshot, error: {e:?}"), + }; + } + } + Err(e) => error!("Cannot save screenshot, requested format not recognized: {e}"), + }, + Err(e) => error!("Cannot save screenshot, screen format cannot be understood: {e}"), + } + } +} + +fn clear_screenshots(mut commands: Commands, screenshots: Query>) { + for entity in screenshots.iter() { + commands.entity(entity).despawn(); + } +} + +pub fn trigger_screenshots( + mut commands: Commands, + captured_screenshots: ResMut, +) { + let captured_screenshots = captured_screenshots.lock().unwrap(); + while let Ok((entity, image)) = captured_screenshots.try_recv() { + commands.entity(entity).insert(Captured); + commands.trigger(ScreenshotCaptured { image, entity }); + } +} + +fn extract_screenshots( + mut targets: ResMut, + mut main_world: ResMut, + mut system_state: Local< + Option< + SystemState<( + Commands, + Query>, + Query<(Entity, &Screenshot), Without>, + )>, + >, + >, + mut seen_targets: Local>, +) { + if system_state.is_none() { + *system_state = Some(SystemState::new(&mut main_world)); + } + let system_state = system_state.as_mut().unwrap(); + let (mut commands, primary_window, screenshots) = system_state.get_mut(&mut main_world); + + targets.clear(); + seen_targets.clear(); + + let primary_window = primary_window.iter().next(); + + for (entity, screenshot) in screenshots.iter() { + let render_target = screenshot.0.clone(); + let Some(render_target) = render_target.normalize(primary_window) else { + warn!( + "Unknown render target for screenshot, skipping: {:?}", + render_target + ); + continue; + }; + if seen_targets.contains(&render_target) { + warn!( + "Duplicate render target for screenshot, skipping entity {}: {:?}", + entity, render_target + ); + // If we don't despawn the entity here, it will be captured again in the next frame + commands.entity(entity).despawn(); + continue; + } + seen_targets.insert(render_target.clone()); + targets.insert(entity, render_target); + commands.entity(entity).insert(Capturing); + } + + system_state.apply(&mut main_world); +} + +fn prepare_screenshots( + targets: Res, + mut prepared: ResMut, + window_surfaces: Res, + render_device: Res, + screenshot_pipeline: Res, + pipeline_cache: Res, + mut pipelines: ResMut>, + images: Res>, + manual_texture_views: Res, + mut view_target_attachments: ResMut, +) { + prepared.clear(); + for (entity, target) in targets.iter() { + match target { + NormalizedRenderTarget::Window(window) => { + let window = window.entity(); + let Some(surface_data) = window_surfaces.surfaces.get(&window) else { + warn!("Unknown window for screenshot, skipping: {}", window); + continue; + }; + let format = surface_data.configuration.format.add_srgb_suffix(); + let size = Extent3d { + width: surface_data.configuration.width, + height: surface_data.configuration.height, + ..default() + }; + let (texture_view, state) = prepare_screenshot_state( + size, + format, + &render_device, + &screenshot_pipeline, + &pipeline_cache, + &mut pipelines, + ); + prepared.insert(*entity, state); + view_target_attachments.insert( + target.clone(), + OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()), + ); + } + NormalizedRenderTarget::Image(image) => { + let Some(gpu_image) = images.get(&image.handle) else { + warn!("Unknown image for screenshot, skipping: {:?}", image); + continue; + }; + let format = gpu_image.texture_format; + let (texture_view, state) = prepare_screenshot_state( + gpu_image.size, + format, + &render_device, + &screenshot_pipeline, + &pipeline_cache, + &mut pipelines, + ); + prepared.insert(*entity, state); + view_target_attachments.insert( + target.clone(), + OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()), + ); + } + NormalizedRenderTarget::TextureView(texture_view) => { + let Some(manual_texture_view) = manual_texture_views.get(texture_view) else { + warn!( + "Unknown manual texture view for screenshot, skipping: {:?}", + texture_view + ); + continue; + }; + let format = manual_texture_view.format; + let size = manual_texture_view.size.to_extents(); + let (texture_view, state) = prepare_screenshot_state( + size, + format, + &render_device, + &screenshot_pipeline, + &pipeline_cache, + &mut pipelines, + ); + prepared.insert(*entity, state); + view_target_attachments.insert( + target.clone(), + OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()), + ); + } + NormalizedRenderTarget::None { .. } => { + // Nothing to screenshot! + } + } + } +} + +fn prepare_screenshot_state( + size: Extent3d, + format: TextureFormat, + render_device: &RenderDevice, + pipeline: &ScreenshotToScreenPipeline, + pipeline_cache: &PipelineCache, + pipelines: &mut SpecializedRenderPipelines, +) -> (TextureView, ScreenshotPreparedState) { + let texture = render_device.create_texture(&wgpu::TextureDescriptor { + label: Some("screenshot-capture-rendertarget"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format, + usage: TextureUsages::RENDER_ATTACHMENT + | TextureUsages::COPY_SRC + | TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }); + let texture_view = texture.create_view(&Default::default()); + let buffer = render_device.create_buffer(&wgpu::BufferDescriptor { + label: Some("screenshot-transfer-buffer"), + size: gpu_readback::get_aligned_size(size, format.pixel_size().unwrap_or(0) as u32) as u64, + usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let bind_group = render_device.create_bind_group( + "screenshot-to-screen-bind-group", + &pipeline.bind_group_layout, + &BindGroupEntries::single(&texture_view), + ); + let pipeline_id = pipelines.specialize(pipeline_cache, pipeline, format); + + ( + texture_view, + ScreenshotPreparedState { + texture, + buffer, + bind_group, + pipeline_id, + size, + }, + ) +} + +pub struct ScreenshotPlugin; + +impl Plugin for ScreenshotPlugin { + fn build(&self, app: &mut bevy_app::App) { + embedded_asset!(app, "screenshot.wgsl"); + + let (tx, rx) = std::sync::mpsc::channel(); + app.insert_resource(CapturedScreenshots(Arc::new(Mutex::new(rx)))) + .add_systems( + First, + clear_screenshots + .after(message_update_system) + .before(ApplyDeferred), + ) + .add_systems(Update, trigger_screenshots); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .insert_resource(RenderScreenshotsSender(tx)) + .init_resource::() + .init_resource::() + .init_resource::>() + .add_systems(RenderStartup, init_screenshot_to_screen_pipeline) + .add_systems(ExtractSchedule, extract_screenshots.ambiguous_with_all()) + .add_systems( + Render, + prepare_screenshots + .after(prepare_view_attachments) + .before(prepare_view_targets) + .in_set(RenderSystems::ManageViews), + ); + } +} + +#[derive(Resource)] +pub struct ScreenshotToScreenPipeline { + pub bind_group_layout: BindGroupLayout, + pub shader: Handle, +} + +pub fn init_screenshot_to_screen_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, +) { + let bind_group_layout = render_device.create_bind_group_layout( + "screenshot-to-screen-bgl", + &BindGroupLayoutEntries::single( + wgpu::ShaderStages::FRAGMENT, + texture_2d(wgpu::TextureSampleType::Float { filterable: false }), + ), + ); + + let shader = load_embedded_asset!(asset_server.as_ref(), "screenshot.wgsl"); + + commands.insert_resource(ScreenshotToScreenPipeline { + bind_group_layout, + shader, + }); +} + +impl SpecializedRenderPipeline for ScreenshotToScreenPipeline { + type Key = TextureFormat; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + RenderPipelineDescriptor { + label: Some(Cow::Borrowed("screenshot-to-screen")), + layout: vec![self.bind_group_layout.clone()], + vertex: VertexState { + shader: self.shader.clone(), + ..default() + }, + primitive: wgpu::PrimitiveState { + cull_mode: Some(wgpu::Face::Back), + ..Default::default() + }, + multisample: Default::default(), + fragment: Some(FragmentState { + shader: self.shader.clone(), + targets: vec![Some(wgpu::ColorTargetState { + format: key, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + ..default() + }), + ..default() + } + } +} + +pub(crate) fn submit_screenshot_commands(world: &World, encoder: &mut CommandEncoder) { + let targets = world.resource::(); + let prepared = world.resource::(); + let pipelines = world.resource::(); + let gpu_images = world.resource::>(); + let windows = world.resource::(); + let manual_texture_views = world.resource::(); + + for (entity, render_target) in targets.iter() { + match render_target { + NormalizedRenderTarget::Window(window) => { + let window = window.entity(); + let Some(window) = windows.get(&window) else { + continue; + }; + let width = window.physical_width; + let height = window.physical_height; + let Some(texture_format) = window.swap_chain_texture_format else { + continue; + }; + let Some(swap_chain_texture) = window.swap_chain_texture.as_ref() else { + continue; + }; + let texture_view = swap_chain_texture.texture.create_view(&Default::default()); + render_screenshot( + encoder, + prepared, + pipelines, + entity, + width, + height, + texture_format, + &texture_view, + ); + } + NormalizedRenderTarget::Image(image) => { + let Some(gpu_image) = gpu_images.get(&image.handle) else { + warn!("Unknown image for screenshot, skipping: {:?}", image); + continue; + }; + let width = gpu_image.size.width; + let height = gpu_image.size.height; + let texture_format = gpu_image.texture_format; + let texture_view = gpu_image.texture_view.deref(); + render_screenshot( + encoder, + prepared, + pipelines, + entity, + width, + height, + texture_format, + texture_view, + ); + } + NormalizedRenderTarget::TextureView(texture_view) => { + let Some(texture_view) = manual_texture_views.get(texture_view) else { + warn!( + "Unknown manual texture view for screenshot, skipping: {:?}", + texture_view + ); + continue; + }; + let width = texture_view.size.x; + let height = texture_view.size.y; + let texture_format = texture_view.format; + let texture_view = texture_view.texture_view.deref(); + render_screenshot( + encoder, + prepared, + pipelines, + entity, + width, + height, + texture_format, + texture_view, + ); + } + NormalizedRenderTarget::None { .. } => { + // Nothing to screenshot! + } + }; + } +} + +fn render_screenshot( + encoder: &mut CommandEncoder, + prepared: &RenderScreenshotsPrepared, + pipelines: &PipelineCache, + entity: &Entity, + width: u32, + height: u32, + texture_format: TextureFormat, + texture_view: &wgpu::TextureView, +) { + if let Some(prepared_state) = &prepared.get(entity) { + let extent = Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + encoder.copy_texture_to_buffer( + prepared_state.texture.as_image_copy(), + wgpu::TexelCopyBufferInfo { + buffer: &prepared_state.buffer, + layout: gpu_readback::layout_data(extent, texture_format), + }, + extent, + ); + + if let Some(pipeline) = pipelines.get_render_pipeline(prepared_state.pipeline_id) { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("screenshot_to_screen_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: texture_view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + pass.set_pipeline(pipeline); + pass.set_bind_group(0, &prepared_state.bind_group, &[]); + pass.draw(0..3, 0..1); + } + } +} + +pub(crate) fn collect_screenshots(world: &mut World) { + #[cfg(feature = "trace")] + let _span = tracing::info_span!("collect_screenshots").entered(); + + let sender = world.resource::().deref().clone(); + let prepared = world.resource::(); + + for (entity, prepared) in prepared.iter() { + let entity = *entity; + let sender = sender.clone(); + let width = prepared.size.width; + let height = prepared.size.height; + let texture_format = prepared.texture.format(); + let Ok(pixel_size) = texture_format.pixel_size() else { + continue; + }; + let buffer = prepared.buffer.clone(); + + let finish = async move { + let (tx, rx) = async_channel::bounded(1); + let buffer_slice = buffer.slice(..); + // The polling for this map call is done every frame when the command queue is submitted. + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + let err = result.err(); + if err.is_some() { + panic!("{}", err.unwrap().to_string()); + } + tx.try_send(()).unwrap(); + }); + rx.recv().await.unwrap(); + let data = buffer_slice.get_mapped_range(); + // we immediately move the data to CPU memory to avoid holding the mapped view for long + let mut result = Vec::from(&*data); + drop(data); + + if result.len() != ((width * height) as usize * pixel_size) { + // Our buffer has been padded because we needed to align to a multiple of 256. + // We remove this padding here + let initial_row_bytes = width as usize * pixel_size; + let buffered_row_bytes = + gpu_readback::align_byte_size(width * pixel_size as u32) as usize; + + let mut take_offset = buffered_row_bytes; + let mut place_offset = initial_row_bytes; + for _ in 1..height { + result.copy_within(take_offset..take_offset + buffered_row_bytes, place_offset); + take_offset += buffered_row_bytes; + place_offset += initial_row_bytes; + } + result.truncate(initial_row_bytes * height as usize); + } + + if let Err(e) = sender.send(( + entity, + Image::new( + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + wgpu::TextureDimension::D2, + result, + texture_format, + RenderAssetUsages::RENDER_WORLD, + ), + )) { + error!("Failed to send screenshot: {}", e); + } + }; + + AsyncComputeTaskPool::get().spawn(finish).detach(); + } +} diff --git a/crates/libmarathon/src/render/view/window/screenshot.wgsl b/crates/libmarathon/src/render/view/window/screenshot.wgsl new file mode 100644 index 0000000..2743fa1 --- /dev/null +++ b/crates/libmarathon/src/render/view/window/screenshot.wgsl @@ -0,0 +1,16 @@ +// This vertex shader will create a triangle that will cover the entire screen +// with minimal effort, avoiding the need for a vertex buffer etc. +@vertex +fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> @builtin(position) vec4 { + let x = f32((in_vertex_index & 1u) << 2u); + let y = f32((in_vertex_index & 2u) << 1u); + return vec4(x - 1.0, y - 1.0, 0.0, 1.0); +} + +@group(0) @binding(0) var t: texture_2d; + +@fragment +fn fs_main(@builtin(position) pos: vec4) -> @location(0) vec4 { + let coords = floor(pos.xy); + return textureLoad(t, vec2(coords), 0i); +} diff --git a/crates/libmarathon/src/sync.rs b/crates/libmarathon/src/sync.rs index a181ebc..98accd2 100644 --- a/crates/libmarathon/src/sync.rs +++ b/crates/libmarathon/src/sync.rs @@ -17,8 +17,8 @@ use serde::{ Deserialize, Serialize, }; -// Re-export the Synced derive macro -pub use sync_macros::Synced; +// TODO: Re-export the Synced derive macro (not part of bevy_render_macros) +// pub use macros::Synced; pub type NodeId = uuid::Uuid; diff --git a/crates/sync-macros/Cargo.toml b/crates/macros/Cargo.toml similarity index 90% rename from crates/sync-macros/Cargo.toml rename to crates/macros/Cargo.toml index 24d5e82..88bfd7a 100644 --- a/crates/sync-macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "sync-macros" +name = "macros" version = "0.1.0" edition.workspace = true @@ -12,6 +12,7 @@ quote = "1.0" proc-macro2 = "1.0" inventory = { workspace = true } bytes = "1.0" +bevy_macro_utils = "0.17.2" [dev-dependencies] libmarathon = { path = "../libmarathon" } diff --git a/crates/macros/src/as_bind_group.rs b/crates/macros/src/as_bind_group.rs new file mode 100644 index 0000000..2df46dd --- /dev/null +++ b/crates/macros/src/as_bind_group.rs @@ -0,0 +1,1817 @@ +use bevy_macro_utils::{get_lit_bool, get_lit_str, BevyManifest, Symbol}; +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span}; +use quote::{quote, ToTokens}; +use syn::{ + parenthesized, + parse::{Parse, ParseStream}, + punctuated::Punctuated, + token::{Comma, DotDot}, + Data, DataStruct, Error, Fields, LitInt, LitStr, Meta, MetaList, Result, +}; + +const UNIFORM_ATTRIBUTE_NAME: Symbol = Symbol("uniform"); +const TEXTURE_ATTRIBUTE_NAME: Symbol = Symbol("texture"); +const STORAGE_TEXTURE_ATTRIBUTE_NAME: Symbol = Symbol("storage_texture"); +const SAMPLER_ATTRIBUTE_NAME: Symbol = Symbol("sampler"); +const STORAGE_ATTRIBUTE_NAME: Symbol = Symbol("storage"); +const BIND_GROUP_DATA_ATTRIBUTE_NAME: Symbol = Symbol("bind_group_data"); +const BINDLESS_ATTRIBUTE_NAME: Symbol = Symbol("bindless"); +const DATA_ATTRIBUTE_NAME: Symbol = Symbol("data"); +const BINDING_ARRAY_MODIFIER_NAME: Symbol = Symbol("binding_array"); +const LIMIT_MODIFIER_NAME: Symbol = Symbol("limit"); +const INDEX_TABLE_MODIFIER_NAME: Symbol = Symbol("index_table"); +const RANGE_MODIFIER_NAME: Symbol = Symbol("range"); +const BINDING_MODIFIER_NAME: Symbol = Symbol("binding"); + +#[derive(Copy, Clone, Debug)] +enum BindingType { + Uniform, + Texture, + StorageTexture, + Sampler, + Storage, +} + +#[derive(Clone)] +enum BindingState<'a> { + Free, + Occupied { + binding_type: BindingType, + ident: &'a Ident, + }, + OccupiedConvertedUniform, + OccupiedMergeableUniform { + uniform_fields: Vec<&'a syn::Field>, + }, +} + +enum BindlessSlabResourceLimitAttr { + Auto, + Limit(LitInt), +} + +// The `bindless(index_table(range(M..N)))` attribute. +struct BindlessIndexTableRangeAttr { + start: LitInt, + end: LitInt, +} + +pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { + let manifest = BevyManifest::shared(); + let render_path = crate::bevy_render_path(); + let image_path = manifest.get_path("bevy_image"); + let asset_path = manifest.get_path("bevy_asset"); + let ecs_path = manifest.get_path("bevy_ecs"); + + let mut binding_states: Vec = Vec::new(); + let mut binding_impls = Vec::new(); + let mut bindless_binding_layouts = Vec::new(); + let mut non_bindless_binding_layouts = Vec::new(); + let mut bindless_resource_types = Vec::new(); + let mut bindless_buffer_descriptors = Vec::new(); + let mut attr_prepared_data_ident = None; + // After the first attribute pass, this will be `None` if the object isn't + // bindless and `Some` if it is. + let mut attr_bindless_count = None; + let mut attr_bindless_index_table_range = None; + let mut attr_bindless_index_table_binding = None; + + // `actual_bindless_slot_count` holds the actual number of bindless slots + // per bind group, taking into account whether the current platform supports + // bindless resources. + let actual_bindless_slot_count = Ident::new("actual_bindless_slot_count", Span::call_site()); + let bind_group_layout_entries = Ident::new("bind_group_layout_entries", Span::call_site()); + + // The `BufferBindingType` and corresponding `BufferUsages` used for + // uniforms. We need this because bindless uniforms don't exist, so in + // bindless mode we must promote uniforms to storage buffers. + let uniform_binding_type = Ident::new("uniform_binding_type", Span::call_site()); + let uniform_buffer_usages = Ident::new("uniform_buffer_usages", Span::call_site()); + + // Read struct-level attributes, first pass. + for attr in &ast.attrs { + if let Some(attr_ident) = attr.path().get_ident() { + if attr_ident == BIND_GROUP_DATA_ATTRIBUTE_NAME { + if let Ok(prepared_data_ident) = + attr.parse_args_with(|input: ParseStream| input.parse::()) + { + attr_prepared_data_ident = Some(prepared_data_ident); + } + } else if attr_ident == BINDLESS_ATTRIBUTE_NAME { + attr_bindless_count = Some(BindlessSlabResourceLimitAttr::Auto); + if let Meta::List(_) = attr.meta { + // Parse bindless features. + attr.parse_nested_meta(|submeta| { + if submeta.path.is_ident(&LIMIT_MODIFIER_NAME) { + let content; + parenthesized!(content in submeta.input); + let lit: LitInt = content.parse()?; + + attr_bindless_count = Some(BindlessSlabResourceLimitAttr::Limit(lit)); + return Ok(()); + } + + if submeta.path.is_ident(&INDEX_TABLE_MODIFIER_NAME) { + submeta.parse_nested_meta(|subsubmeta| { + if subsubmeta.path.is_ident(&RANGE_MODIFIER_NAME) { + let content; + parenthesized!(content in subsubmeta.input); + let start: LitInt = content.parse()?; + content.parse::()?; + let end: LitInt = content.parse()?; + attr_bindless_index_table_range = + Some(BindlessIndexTableRangeAttr { start, end }); + return Ok(()); + } + + if subsubmeta.path.is_ident(&BINDING_MODIFIER_NAME) { + let content; + parenthesized!(content in subsubmeta.input); + let lit: LitInt = content.parse()?; + + attr_bindless_index_table_binding = Some(lit); + return Ok(()); + } + + Err(Error::new_spanned( + attr, + "Expected `range(M..N)` or `binding(N)`", + )) + })?; + return Ok(()); + } + + Err(Error::new_spanned( + attr, + "Expected `limit` or `index_table`", + )) + })?; + } + } + } + } + + // Read struct-level attributes, second pass. + for attr in &ast.attrs { + if let Some(attr_ident) = attr.path().get_ident() + && (attr_ident == UNIFORM_ATTRIBUTE_NAME || attr_ident == DATA_ATTRIBUTE_NAME) + { + let UniformBindingAttr { + binding_type, + binding_index, + converted_shader_type, + binding_array: binding_array_binding, + } = get_uniform_binding_attr(attr)?; + match binding_type { + UniformBindingAttrType::Uniform => { + binding_impls.push(quote! {{ + use #render_path::render_resource::AsBindGroupShaderType; + let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new()); + let converted: #converted_shader_type = self.as_bind_group_shader_type(&images); + buffer.write(&converted).unwrap(); + ( + #binding_index, + #render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data( + &#render_path::render_resource::BufferInitDescriptor { + label: None, + usage: #uniform_buffer_usages, + contents: buffer.as_ref(), + }, + )) + ) + }}); + + match (&binding_array_binding, &attr_bindless_count) { + (&None, &Some(_)) => { + return Err(Error::new_spanned( + attr, + "Must specify `binding_array(...)` with `#[uniform]` if the \ + object is bindless", + )); + } + (&Some(_), &None) => { + return Err(Error::new_spanned( + attr, + "`binding_array(...)` with `#[uniform]` requires the object to \ + be bindless", + )); + } + _ => {} + } + + let binding_array_binding = binding_array_binding.unwrap_or(0); + bindless_binding_layouts.push(quote! { + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_array_binding, + visibility: #render_path::render_resource::ShaderStages::FRAGMENT | #render_path::render_resource::ShaderStages::VERTEX | #render_path::render_resource::ShaderStages::COMPUTE, + ty: #render_path::render_resource::BindingType::Buffer { + ty: #uniform_binding_type, + has_dynamic_offset: false, + min_binding_size: Some(<#converted_shader_type as #render_path::render_resource::ShaderType>::min_size()), + }, + count: #actual_bindless_slot_count, + } + ); + }); + + add_bindless_resource_type( + &render_path, + &mut bindless_resource_types, + binding_index, + quote! { #render_path::render_resource::BindlessResourceType::Buffer }, + ); + } + + UniformBindingAttrType::Data => { + binding_impls.push(quote! {{ + use #render_path::render_resource::AsBindGroupShaderType; + use #render_path::render_resource::encase::{ShaderType, internal::WriteInto}; + let mut buffer: Vec = Vec::new(); + let converted: #converted_shader_type = self.as_bind_group_shader_type(&images); + converted.write_into( + &mut #render_path::render_resource::encase::internal::Writer::new( + &converted, + &mut buffer, + 0, + ).unwrap(), + ); + let min_size = <#converted_shader_type as #render_path::render_resource::ShaderType>::min_size().get() as usize; + while buffer.len() < min_size { + buffer.push(0); + } + ( + #binding_index, + #render_path::render_resource::OwnedBindingResource::Data( + #render_path::render_resource::OwnedData(buffer) + ) + ) + }}); + + let binding_array_binding = binding_array_binding.unwrap_or(0); + bindless_binding_layouts.push(quote! { + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_array_binding, + visibility: #render_path::render_resource::ShaderStages::FRAGMENT | #render_path::render_resource::ShaderStages::VERTEX | #render_path::render_resource::ShaderStages::COMPUTE, + ty: #render_path::render_resource::BindingType::Buffer { + ty: #uniform_binding_type, + has_dynamic_offset: false, + min_binding_size: Some(<#converted_shader_type as #render_path::render_resource::ShaderType>::min_size()), + }, + count: None, + } + ); + }); + + add_bindless_resource_type( + &render_path, + &mut bindless_resource_types, + binding_index, + quote! { #render_path::render_resource::BindlessResourceType::DataBuffer }, + ); + } + } + + // Push the non-bindless binding layout. + + non_bindless_binding_layouts.push(quote!{ + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #render_path::render_resource::ShaderStages::FRAGMENT | #render_path::render_resource::ShaderStages::VERTEX | #render_path::render_resource::ShaderStages::COMPUTE, + ty: #render_path::render_resource::BindingType::Buffer { + ty: #uniform_binding_type, + has_dynamic_offset: false, + min_binding_size: Some(<#converted_shader_type as #render_path::render_resource::ShaderType>::min_size()), + }, + count: None, + } + ); + }); + + bindless_buffer_descriptors.push(quote! { + #render_path::render_resource::BindlessBufferDescriptor { + // Note that, because this is bindless, *binding + // index* here refers to the index in the + // bindless index table (`bindless_index`), and + // the actual binding number is the *binding + // array binding*. + binding_number: #render_path::render_resource::BindingNumber( + #binding_array_binding + ), + bindless_index: + #render_path::render_resource::BindlessIndex(#binding_index), + size: Some( + < + #converted_shader_type as + #render_path::render_resource::ShaderType + >::min_size().get() as usize + ), + } + }); + + let required_len = binding_index as usize + 1; + if required_len > binding_states.len() { + binding_states.resize(required_len, BindingState::Free); + } + binding_states[binding_index as usize] = BindingState::OccupiedConvertedUniform; + } + } + + let fields = match &ast.data { + Data::Struct(DataStruct { + fields: Fields::Named(fields), + .. + }) => &fields.named, + _ => { + return Err(Error::new_spanned( + ast, + "Expected a struct with named fields", + )); + } + }; + + // Count the number of sampler fields needed. We might have to disable + // bindless if bindless arrays take the GPU over the maximum number of + // samplers. + let mut sampler_binding_count: u32 = 0; + + // Read field-level attributes + for field in fields { + // Search ahead for texture attributes so we can use them with any + // corresponding sampler attribute. + let mut tex_attrs = None; + for attr in &field.attrs { + let Some(attr_ident) = attr.path().get_ident() else { + continue; + }; + if attr_ident == TEXTURE_ATTRIBUTE_NAME { + let (_binding_index, nested_meta_items) = get_binding_nested_attr(attr)?; + tex_attrs = Some(get_texture_attrs(nested_meta_items)?); + } + } + + for attr in &field.attrs { + let Some(attr_ident) = attr.path().get_ident() else { + continue; + }; + + let binding_type = if attr_ident == UNIFORM_ATTRIBUTE_NAME { + BindingType::Uniform + } else if attr_ident == TEXTURE_ATTRIBUTE_NAME { + BindingType::Texture + } else if attr_ident == STORAGE_TEXTURE_ATTRIBUTE_NAME { + BindingType::StorageTexture + } else if attr_ident == SAMPLER_ATTRIBUTE_NAME { + BindingType::Sampler + } else if attr_ident == STORAGE_ATTRIBUTE_NAME { + BindingType::Storage + } else { + continue; + }; + + let (binding_index, nested_meta_items) = get_binding_nested_attr(attr)?; + + let field_name = field.ident.as_ref().unwrap(); + let required_len = binding_index as usize + 1; + if required_len > binding_states.len() { + binding_states.resize(required_len, BindingState::Free); + } + + match &mut binding_states[binding_index as usize] { + value @ BindingState::Free => { + *value = match binding_type { + BindingType::Uniform => BindingState::OccupiedMergeableUniform { + uniform_fields: vec![field], + }, + _ => { + // only populate bind group entries for non-uniforms + // uniform entries are deferred until the end + BindingState::Occupied { + binding_type, + ident: field_name, + } + } + } + } + BindingState::Occupied { + binding_type, + ident: occupied_ident, + } => { + return Err(Error::new_spanned( + attr, + format!("The '{field_name}' field cannot be assigned to binding {binding_index} because it is already occupied by the field '{occupied_ident}' of type {binding_type:?}.") + )); + } + BindingState::OccupiedConvertedUniform => { + return Err(Error::new_spanned( + attr, + format!("The '{field_name}' field cannot be assigned to binding {binding_index} because it is already occupied by a struct-level uniform binding at the same index.") + )); + } + BindingState::OccupiedMergeableUniform { uniform_fields } => match binding_type { + BindingType::Uniform => { + uniform_fields.push(field); + } + _ => { + return Err(Error::new_spanned( + attr, + format!("The '{field_name}' field cannot be assigned to binding {binding_index} because it is already occupied by a {:?}.", BindingType::Uniform) + )); + } + }, + } + + match binding_type { + BindingType::Uniform => { + if attr_bindless_count.is_some() { + return Err(Error::new_spanned( + attr, + "Only structure-level `#[uniform]` attributes are supported in \ + bindless mode", + )); + } + + // uniform codegen is deferred to account for combined uniform bindings + } + + BindingType::Storage => { + let StorageAttrs { + visibility, + binding_array: binding_array_binding, + read_only, + buffer, + } = get_storage_binding_attr(nested_meta_items)?; + let visibility = + visibility.hygienic_quote("e! { #render_path::render_resource }); + + let field_name = field.ident.as_ref().unwrap(); + + if buffer { + binding_impls.push(quote! { + ( + #binding_index, + #render_path::render_resource::OwnedBindingResource::Buffer({ + self.#field_name.clone() + }) + ) + }); + } else { + binding_impls.push(quote! { + ( + #binding_index, + #render_path::render_resource::OwnedBindingResource::Buffer({ + let handle: &#asset_path::Handle<#render_path::storage::ShaderStorageBuffer> = (&self.#field_name); + storage_buffers.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.buffer.clone() + }) + ) + }); + } + + non_bindless_binding_layouts.push(quote! { + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #visibility, + ty: #render_path::render_resource::BindingType::Buffer { + ty: #render_path::render_resource::BufferBindingType::Storage { read_only: #read_only }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: #actual_bindless_slot_count, + } + ); + }); + + if let Some(binding_array_binding) = binding_array_binding { + // Add the storage buffer to the `BindlessResourceType` list + // in the bindless descriptor. + let bindless_resource_type = quote! { + #render_path::render_resource::BindlessResourceType::Buffer + }; + add_bindless_resource_type( + &render_path, + &mut bindless_resource_types, + binding_index, + bindless_resource_type, + ); + + // Push the buffer descriptor. + bindless_buffer_descriptors.push(quote! { + #render_path::render_resource::BindlessBufferDescriptor { + // Note that, because this is bindless, *binding + // index* here refers to the index in the bindless + // index table (`bindless_index`), and the actual + // binding number is the *binding array binding*. + binding_number: #render_path::render_resource::BindingNumber( + #binding_array_binding + ), + bindless_index: + #render_path::render_resource::BindlessIndex(#binding_index), + size: None, + } + }); + + // Declare the binding array. + bindless_binding_layouts.push(quote!{ + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_array_binding, + visibility: #render_path::render_resource::ShaderStages::FRAGMENT | #render_path::render_resource::ShaderStages::VERTEX | #render_path::render_resource::ShaderStages::COMPUTE, + ty: #render_path::render_resource::BindingType::Buffer { + ty: #render_path::render_resource::BufferBindingType::Storage { + read_only: #read_only + }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: #actual_bindless_slot_count, + } + ); + }); + } + } + + BindingType::StorageTexture => { + if attr_bindless_count.is_some() { + return Err(Error::new_spanned( + attr, + "Storage textures are unsupported in bindless mode", + )); + } + + let StorageTextureAttrs { + dimension, + image_format, + access, + visibility, + } = get_storage_texture_binding_attr(nested_meta_items)?; + + let visibility = + visibility.hygienic_quote("e! { #render_path::render_resource }); + + let fallback_image = get_fallback_image(&render_path, dimension); + + // insert fallible texture-based entries at 0 so that if we fail here, we exit before allocating any buffers + binding_impls.insert(0, quote! { + ( #binding_index, + #render_path::render_resource::OwnedBindingResource::TextureView( + #render_path::render_resource::#dimension, + { + let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into(); + if let Some(handle) = handle { + images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone() + } else { + #fallback_image.texture_view.clone() + } + } + ) + ) + }); + + non_bindless_binding_layouts.push(quote! { + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #visibility, + ty: #render_path::render_resource::BindingType::StorageTexture { + access: #render_path::render_resource::StorageTextureAccess::#access, + format: #render_path::render_resource::TextureFormat::#image_format, + view_dimension: #render_path::render_resource::#dimension, + }, + count: #actual_bindless_slot_count, + } + ); + }); + } + + BindingType::Texture => { + let TextureAttrs { + dimension, + sample_type, + multisampled, + visibility, + } = tex_attrs.as_ref().unwrap(); + + let visibility = + visibility.hygienic_quote("e! { #render_path::render_resource }); + + let fallback_image = get_fallback_image(&render_path, *dimension); + + // insert fallible texture-based entries at 0 so that if we fail here, we exit before allocating any buffers + binding_impls.insert(0, quote! { + ( + #binding_index, + #render_path::render_resource::OwnedBindingResource::TextureView( + #render_path::render_resource::#dimension, + { + let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into(); + if let Some(handle) = handle { + images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone() + } else { + #fallback_image.texture_view.clone() + } + } + ) + ) + }); + + sampler_binding_count += 1; + + non_bindless_binding_layouts.push(quote! { + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #visibility, + ty: #render_path::render_resource::BindingType::Texture { + multisampled: #multisampled, + sample_type: #render_path::render_resource::#sample_type, + view_dimension: #render_path::render_resource::#dimension, + }, + count: #actual_bindless_slot_count, + } + ); + }); + + let bindless_resource_type = match *dimension { + BindingTextureDimension::D1 => { + quote! { + #render_path::render_resource::BindlessResourceType::Texture1d + } + } + BindingTextureDimension::D2 => { + quote! { + #render_path::render_resource::BindlessResourceType::Texture2d + } + } + BindingTextureDimension::D2Array => { + quote! { + #render_path::render_resource::BindlessResourceType::Texture2dArray + } + } + BindingTextureDimension::Cube => { + quote! { + #render_path::render_resource::BindlessResourceType::TextureCube + } + } + BindingTextureDimension::CubeArray => { + quote! { + #render_path::render_resource::BindlessResourceType::TextureCubeArray + } + } + BindingTextureDimension::D3 => { + quote! { + #render_path::render_resource::BindlessResourceType::Texture3d + } + } + }; + + // Add the texture to the `BindlessResourceType` list in the + // bindless descriptor. + add_bindless_resource_type( + &render_path, + &mut bindless_resource_types, + binding_index, + bindless_resource_type, + ); + } + + BindingType::Sampler => { + let SamplerAttrs { + sampler_binding_type, + visibility, + .. + } = get_sampler_attrs(nested_meta_items)?; + let TextureAttrs { dimension, .. } = tex_attrs + .as_ref() + .expect("sampler attribute must have matching texture attribute"); + + let visibility = + visibility.hygienic_quote("e! { #render_path::render_resource }); + + let fallback_image = get_fallback_image(&render_path, *dimension); + + let expected_samplers = match sampler_binding_type { + SamplerBindingType::Filtering => { + quote!( [#render_path::render_resource::TextureSampleType::Float { filterable: true }] ) + } + SamplerBindingType::NonFiltering => quote!([ + #render_path::render_resource::TextureSampleType::Float { filterable: false }, + #render_path::render_resource::TextureSampleType::Sint, + #render_path::render_resource::TextureSampleType::Uint, + ]), + SamplerBindingType::Comparison => { + quote!( [#render_path::render_resource::TextureSampleType::Depth] ) + } + }; + + // insert fallible texture-based entries at 0 so that if we fail here, we exit before allocating any buffers + binding_impls.insert(0, quote! { + ( + #binding_index, + #render_path::render_resource::OwnedBindingResource::Sampler( + // TODO: Support other types. + #render_path::render_resource::WgpuSamplerBindingType::Filtering, + { + let handle: Option<&#asset_path::Handle<#image_path::Image>> = (&self.#field_name).into(); + if let Some(handle) = handle { + let image = images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?; + + let Some(sample_type) = image.texture_format.sample_type(None, Some(render_device.features())) else { + return Err(#render_path::render_resource::AsBindGroupError::InvalidSamplerType( + #binding_index, + "None".to_string(), + format!("{:?}", #expected_samplers), + )); + }; + + let valid = #expected_samplers.contains(&sample_type); + + if !valid { + return Err(#render_path::render_resource::AsBindGroupError::InvalidSamplerType( + #binding_index, + format!("{:?}", sample_type), + format!("{:?}", #expected_samplers), + )); + } + image.sampler.clone() + } else { + #fallback_image.sampler.clone() + } + }) + ) + }); + + sampler_binding_count += 1; + + non_bindless_binding_layouts.push(quote!{ + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #visibility, + ty: #render_path::render_resource::BindingType::Sampler(#render_path::render_resource::#sampler_binding_type), + count: #actual_bindless_slot_count, + } + ); + }); + + // Add the sampler to the `BindlessResourceType` list in the + // bindless descriptor. + // + // TODO: Support other types of samplers. + add_bindless_resource_type( + &render_path, + &mut bindless_resource_types, + binding_index, + quote! { + #render_path::render_resource::BindlessResourceType::SamplerFiltering + }, + ); + } + } + } + } + + // Produce impls for fields with uniform bindings + let struct_name = &ast.ident; + let struct_name_literal = struct_name.to_string(); + let struct_name_literal = struct_name_literal.as_str(); + let mut field_struct_impls = Vec::new(); + + let uniform_binding_type_declarations = match attr_bindless_count { + Some(_) => { + quote! { + let (#uniform_binding_type, #uniform_buffer_usages) = + if Self::bindless_supported(render_device) && !force_no_bindless { + ( + #render_path::render_resource::BufferBindingType::Storage { read_only: true }, + #render_path::render_resource::BufferUsages::STORAGE, + ) + } else { + ( + #render_path::render_resource::BufferBindingType::Uniform, + #render_path::render_resource::BufferUsages::UNIFORM, + ) + }; + } + } + None => { + quote! { + let (#uniform_binding_type, #uniform_buffer_usages) = ( + #render_path::render_resource::BufferBindingType::Uniform, + #render_path::render_resource::BufferUsages::UNIFORM, + ); + } + } + }; + + for (binding_index, binding_state) in binding_states.iter().enumerate() { + let binding_index = binding_index as u32; + if let BindingState::OccupiedMergeableUniform { uniform_fields } = binding_state { + // single field uniform bindings for a given index can use a straightforward binding + if uniform_fields.len() == 1 { + let field = &uniform_fields[0]; + let field_name = field.ident.as_ref().unwrap(); + let field_ty = &field.ty; + binding_impls.push(quote! {{ + let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new()); + buffer.write(&self.#field_name).unwrap(); + ( + #binding_index, + #render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data( + &#render_path::render_resource::BufferInitDescriptor { + label: None, + usage: #uniform_buffer_usages, + contents: buffer.as_ref(), + }, + )) + ) + }}); + + non_bindless_binding_layouts.push(quote!{ + #bind_group_layout_entries.push( + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #render_path::render_resource::ShaderStages::FRAGMENT | #render_path::render_resource::ShaderStages::VERTEX | #render_path::render_resource::ShaderStages::COMPUTE, + ty: #render_path::render_resource::BindingType::Buffer { + ty: #uniform_binding_type, + has_dynamic_offset: false, + min_binding_size: Some(<#field_ty as #render_path::render_resource::ShaderType>::min_size()), + }, + count: #actual_bindless_slot_count, + } + ); + }); + // multi-field uniform bindings for a given index require an intermediate struct to derive ShaderType + } else { + let uniform_struct_name = Ident::new( + &format!("_{struct_name}AsBindGroupUniformStructBindGroup{binding_index}"), + Span::call_site(), + ); + + let field_name = uniform_fields.iter().map(|f| f.ident.as_ref().unwrap()); + let field_type = uniform_fields.iter().map(|f| &f.ty); + field_struct_impls.push(quote! { + #[derive(#render_path::render_resource::ShaderType)] + struct #uniform_struct_name<'a> { + #(#field_name: &'a #field_type,)* + } + }); + + let field_name = uniform_fields.iter().map(|f| f.ident.as_ref().unwrap()); + binding_impls.push(quote! {{ + let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new()); + buffer.write(&#uniform_struct_name { + #(#field_name: &self.#field_name,)* + }).unwrap(); + ( + #binding_index, + #render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data( + &#render_path::render_resource::BufferInitDescriptor { + label: None, + usage: #uniform_buffer_usages, + contents: buffer.as_ref(), + }, + )) + ) + }}); + + non_bindless_binding_layouts.push(quote!{ + #bind_group_layout_entries.push(#render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #render_path::render_resource::ShaderStages::FRAGMENT | #render_path::render_resource::ShaderStages::VERTEX | #render_path::render_resource::ShaderStages::COMPUTE, + ty: #render_path::render_resource::BindingType::Buffer { + ty: #uniform_binding_type, + has_dynamic_offset: false, + min_binding_size: Some(<#uniform_struct_name as #render_path::render_resource::ShaderType>::min_size()), + }, + count: #actual_bindless_slot_count, + }); + }); + } + } + } + + let generics = ast.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let (prepared_data, get_prepared_data) = if let Some(prepared) = attr_prepared_data_ident { + let get_prepared_data = quote! { self.into() }; + (quote! {#prepared}, get_prepared_data) + } else { + let prepared_data = quote! { () }; + (prepared_data.clone(), prepared_data) + }; + + // Calculate the number of samplers that we need, so that we don't go over + // the limit on certain platforms. See + // https://github.com/bevyengine/bevy/issues/16988. + let bindless_count_syntax = match attr_bindless_count { + Some(BindlessSlabResourceLimitAttr::Auto) => { + quote! { #render_path::render_resource::AUTO_BINDLESS_SLAB_RESOURCE_LIMIT } + } + Some(BindlessSlabResourceLimitAttr::Limit(ref count)) => { + quote! { #count } + } + None => quote! { 0 }, + }; + + // Calculate the actual bindless index table range, taking the + // `#[bindless(index_table(range(M..N)))]` attribute into account. + let bindless_index_table_range = match attr_bindless_index_table_range { + None => { + let resource_count = bindless_resource_types.len() as u32; + quote! { + #render_path::render_resource::BindlessIndex(0).. + #render_path::render_resource::BindlessIndex(#resource_count) + } + } + Some(BindlessIndexTableRangeAttr { start, end }) => { + quote! { + #render_path::render_resource::BindlessIndex(#start).. + #render_path::render_resource::BindlessIndex(#end) + } + } + }; + + // Calculate the actual binding number of the bindless index table, taking + // the `#[bindless(index_table(binding(B)))]` into account. + let bindless_index_table_binding_number = match attr_bindless_index_table_binding { + None => quote! { #render_path::render_resource::BindingNumber(0) }, + Some(binding_number) => { + quote! { #render_path::render_resource::BindingNumber(#binding_number) } + } + }; + + // Calculate the actual number of bindless slots, taking hardware + // limitations into account. + let (bindless_slot_count, actual_bindless_slot_count_declaration, bindless_descriptor_syntax) = + match attr_bindless_count { + Some(ref bindless_count) => { + let bindless_supported_syntax = quote! { + fn bindless_supported( + render_device: &#render_path::renderer::RenderDevice + ) -> bool { + render_device.features().contains( + #render_path::settings::WgpuFeatures::BUFFER_BINDING_ARRAY | + #render_path::settings::WgpuFeatures::TEXTURE_BINDING_ARRAY + ) && + render_device.limits().max_storage_buffers_per_shader_stage > 0 && + render_device.limits().max_samplers_per_shader_stage >= + (#sampler_binding_count * #bindless_count_syntax) + } + }; + let actual_bindless_slot_count_declaration = quote! { + let #actual_bindless_slot_count = if Self::bindless_supported(render_device) && + !force_no_bindless { + ::core::num::NonZeroU32::new(#bindless_count_syntax) + } else { + None + }; + }; + let bindless_slot_count_declaration = match bindless_count { + BindlessSlabResourceLimitAttr::Auto => { + quote! { + fn bindless_slot_count() -> Option< + #render_path::render_resource::BindlessSlabResourceLimit + > { + Some(#render_path::render_resource::BindlessSlabResourceLimit::Auto) + } + } + } + BindlessSlabResourceLimitAttr::Limit(lit) => { + quote! { + fn bindless_slot_count() -> Option< + #render_path::render_resource::BindlessSlabResourceLimit + > { + Some(#render_path::render_resource::BindlessSlabResourceLimit::Custom(#lit)) + } + } + } + }; + + let bindless_buffer_descriptor_count = bindless_buffer_descriptors.len(); + + // We use `LazyLock` so that we can call `min_size`, which isn't + // a `const fn`. + let bindless_descriptor_syntax = quote! { + static RESOURCES: &[#render_path::render_resource::BindlessResourceType] = &[ + #(#bindless_resource_types),* + ]; + static BUFFERS: ::std::sync::LazyLock<[ + #render_path::render_resource::BindlessBufferDescriptor; + #bindless_buffer_descriptor_count + ]> = ::std::sync::LazyLock::new(|| { + [#(#bindless_buffer_descriptors),*] + }); + static INDEX_TABLES: &[ + #render_path::render_resource::BindlessIndexTableDescriptor + ] = &[ + #render_path::render_resource::BindlessIndexTableDescriptor { + indices: #bindless_index_table_range, + binding_number: #bindless_index_table_binding_number, + } + ]; + Some(#render_path::render_resource::BindlessDescriptor { + resources: ::std::borrow::Cow::Borrowed(RESOURCES), + buffers: ::std::borrow::Cow::Borrowed(&*BUFFERS), + index_tables: ::std::borrow::Cow::Borrowed(&*INDEX_TABLES), + }) + }; + + ( + quote! { + #bindless_slot_count_declaration + #bindless_supported_syntax + }, + actual_bindless_slot_count_declaration, + bindless_descriptor_syntax, + ) + } + None => ( + TokenStream::new().into(), + quote! { let #actual_bindless_slot_count: Option<::core::num::NonZeroU32> = None; }, + quote! { None }, + ), + }; + + Ok(TokenStream::from(quote! { + #(#field_struct_impls)* + + impl #impl_generics #render_path::render_resource::AsBindGroup for #struct_name #ty_generics #where_clause { + type Data = #prepared_data; + + type Param = ( + #ecs_path::system::lifetimeless::SRes<#render_path::render_asset::RenderAssets<#render_path::texture::GpuImage>>, + #ecs_path::system::lifetimeless::SRes<#render_path::texture::FallbackImage>, + #ecs_path::system::lifetimeless::SRes<#render_path::render_asset::RenderAssets<#render_path::storage::GpuShaderStorageBuffer>>, + ); + + #bindless_slot_count + + fn label() -> Option<&'static str> { + Some(#struct_name_literal) + } + + fn unprepared_bind_group( + &self, + layout: &#render_path::render_resource::BindGroupLayout, + render_device: &#render_path::renderer::RenderDevice, + (images, fallback_image, storage_buffers): &mut #ecs_path::system::SystemParamItem<'_, '_, Self::Param>, + force_no_bindless: bool, + ) -> Result<#render_path::render_resource::UnpreparedBindGroup, #render_path::render_resource::AsBindGroupError> { + #uniform_binding_type_declarations + + let bindings = #render_path::render_resource::BindingResources(vec![#(#binding_impls,)*]); + + Ok(#render_path::render_resource::UnpreparedBindGroup { + bindings, + }) + } + + #[allow(clippy::unused_unit)] + fn bind_group_data(&self) -> Self::Data { + #get_prepared_data + } + + fn bind_group_layout_entries( + render_device: &#render_path::renderer::RenderDevice, + force_no_bindless: bool + ) -> Vec<#render_path::render_resource::BindGroupLayoutEntry> { + #actual_bindless_slot_count_declaration + #uniform_binding_type_declarations + + let mut #bind_group_layout_entries = Vec::new(); + match #actual_bindless_slot_count { + Some(bindless_slot_count) => { + let bindless_index_table_range = #bindless_index_table_range; + #bind_group_layout_entries.extend( + #render_path::render_resource::create_bindless_bind_group_layout_entries( + bindless_index_table_range.end.0 - + bindless_index_table_range.start.0, + bindless_slot_count.into(), + #bindless_index_table_binding_number, + ).into_iter() + ); + #(#bindless_binding_layouts)*; + } + None => { + #(#non_bindless_binding_layouts)*; + } + }; + #bind_group_layout_entries + } + + fn bindless_descriptor() -> Option<#render_path::render_resource::BindlessDescriptor> { + #bindless_descriptor_syntax + } + } + })) +} + +/// Adds a bindless resource type to the `BindlessResourceType` array in the +/// bindless descriptor we're building up. +/// +/// See the `bevy_render::render_resource::bindless::BindlessResourceType` +/// documentation for more information. +fn add_bindless_resource_type( + render_path: &syn::Path, + bindless_resource_types: &mut Vec, + binding_index: u32, + bindless_resource_type: proc_macro2::TokenStream, +) { + // If we need to grow the array, pad the unused fields with + // `BindlessResourceType::None`. + if bindless_resource_types.len() < (binding_index as usize + 1) { + bindless_resource_types.resize_with(binding_index as usize + 1, || { + quote! { #render_path::render_resource::BindlessResourceType::None } + }); + } + + // Assign the `BindlessResourceType`. + bindless_resource_types[binding_index as usize] = bindless_resource_type; +} + +fn get_fallback_image( + render_path: &syn::Path, + dimension: BindingTextureDimension, +) -> proc_macro2::TokenStream { + quote! { + match #render_path::render_resource::#dimension { + #render_path::render_resource::TextureViewDimension::D1 => &fallback_image.d1, + #render_path::render_resource::TextureViewDimension::D2 => &fallback_image.d2, + #render_path::render_resource::TextureViewDimension::D2Array => &fallback_image.d2_array, + #render_path::render_resource::TextureViewDimension::Cube => &fallback_image.cube, + #render_path::render_resource::TextureViewDimension::CubeArray => &fallback_image.cube_array, + #render_path::render_resource::TextureViewDimension::D3 => &fallback_image.d3, + } + } +} + +/// Represents the arguments for the `uniform` binding attribute. +/// +/// If parsed, represents an attribute +/// like `#[uniform(LitInt, Ident)]` +struct UniformBindingMeta { + lit_int: LitInt, + ident: Ident, + binding_array: Option, +} + +/// The parsed structure-level `#[uniform]` or `#[data]` attribute. +/// +/// The corresponding syntax is `#[uniform(BINDING_INDEX, CONVERTED_SHADER_TYPE, +/// binding_array(BINDING_ARRAY)]`, optionally replacing `uniform` with `data`. +struct UniformBindingAttr { + /// Whether the declaration is `#[uniform]` or `#[data]`. + binding_type: UniformBindingAttrType, + /// The binding index. + binding_index: u32, + /// The uniform data type. + converted_shader_type: Ident, + /// The binding number of the binding array, if this is a bindless material. + binding_array: Option, +} + +/// Whether a structure-level shader type declaration is `#[uniform]` or +/// `#[data]`. +enum UniformBindingAttrType { + /// `#[uniform]`: i.e. in bindless mode, we need a separate buffer per data + /// instance. + Uniform, + /// `#[data]`: i.e. in bindless mode, we concatenate all instance data into + /// a single buffer. + Data, +} + +/// Represents the arguments for any general binding attribute. +/// +/// If parsed, represents an attribute +/// like `#[foo(LitInt, ...)]` where the rest is optional [`Meta`]. +enum BindingMeta { + IndexOnly(LitInt), + IndexWithOptions(BindingIndexOptions), +} + +/// Represents the arguments for an attribute with a list of arguments. +/// +/// This represents, for example, `#[texture(0, dimension = "2d_array")]`. +struct BindingIndexOptions { + lit_int: LitInt, + _comma: Comma, + meta_list: Punctuated, +} + +impl Parse for BindingMeta { + fn parse(input: ParseStream) -> Result { + if input.peek2(Comma) { + input.parse().map(Self::IndexWithOptions) + } else { + input.parse().map(Self::IndexOnly) + } + } +} + +impl Parse for BindingIndexOptions { + fn parse(input: ParseStream) -> Result { + Ok(Self { + lit_int: input.parse()?, + _comma: input.parse()?, + meta_list: input.parse_terminated(Meta::parse, Comma)?, + }) + } +} + +impl Parse for UniformBindingMeta { + // Parse syntax like `#[uniform(0, StandardMaterial, binding_array(10))]`. + fn parse(input: ParseStream) -> Result { + let lit_int = input.parse()?; + input.parse::()?; + let ident = input.parse()?; + + // Look for a `binding_array(BINDING_NUMBER)` declaration. + let mut binding_array: Option = None; + if input.parse::().is_ok() { + if input + .parse::()? + .get_ident() + .is_none_or(|ident| *ident != BINDING_ARRAY_MODIFIER_NAME) + { + return Err(Error::new_spanned(ident, "Expected `binding_array`")); + } + let parser; + parenthesized!(parser in input); + binding_array = Some(parser.parse()?); + } + + Ok(Self { + lit_int, + ident, + binding_array, + }) + } +} + +/// Parses a structure-level `#[uniform]` attribute (not a field-level +/// `#[uniform]` attribute). +fn get_uniform_binding_attr(attr: &syn::Attribute) -> Result { + let attr_ident = attr + .path() + .get_ident() + .expect("Shouldn't be here if we didn't have an attribute"); + + let uniform_binding_meta = attr.parse_args_with(UniformBindingMeta::parse)?; + + let binding_index = uniform_binding_meta.lit_int.base10_parse()?; + let ident = uniform_binding_meta.ident; + let binding_array = match uniform_binding_meta.binding_array { + None => None, + Some(binding_array) => Some(binding_array.base10_parse()?), + }; + + Ok(UniformBindingAttr { + binding_type: if attr_ident == UNIFORM_ATTRIBUTE_NAME { + UniformBindingAttrType::Uniform + } else { + UniformBindingAttrType::Data + }, + binding_index, + converted_shader_type: ident, + binding_array, + }) +} + +fn get_binding_nested_attr(attr: &syn::Attribute) -> Result<(u32, Vec)> { + let binding_meta = attr.parse_args_with(BindingMeta::parse)?; + + match binding_meta { + BindingMeta::IndexOnly(lit_int) => Ok((lit_int.base10_parse()?, Vec::new())), + BindingMeta::IndexWithOptions(BindingIndexOptions { + lit_int, + _comma: _, + meta_list, + }) => Ok((lit_int.base10_parse()?, meta_list.into_iter().collect())), + } +} + +#[derive(Default)] +enum ShaderStageVisibility { + #[default] + All, + None, + Flags(VisibilityFlags), +} + +#[derive(Default)] +struct VisibilityFlags { + vertex: bool, + fragment: bool, + compute: bool, +} + +impl ShaderStageVisibility { + fn vertex_fragment() -> Self { + Self::Flags(VisibilityFlags::vertex_fragment()) + } + + fn compute() -> Self { + Self::Flags(VisibilityFlags::compute()) + } +} + +impl VisibilityFlags { + fn vertex_fragment() -> Self { + Self { + vertex: true, + fragment: true, + ..Default::default() + } + } + + fn compute() -> Self { + Self { + compute: true, + ..Default::default() + } + } +} + +impl ShaderStageVisibility { + fn hygienic_quote(&self, path: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { + match self { + ShaderStageVisibility::All => quote! { + if cfg!(feature = "webgpu") { + todo!("Please use a more specific shader stage: https://github.com/gfx-rs/wgpu/issues/7708") + } else { + #path::ShaderStages::all() + } + }, + ShaderStageVisibility::None => quote! { #path::ShaderStages::NONE }, + ShaderStageVisibility::Flags(flags) => { + let mut quoted = Vec::new(); + + if flags.vertex { + quoted.push(quote! { #path::ShaderStages::VERTEX }); + } + if flags.fragment { + quoted.push(quote! { #path::ShaderStages::FRAGMENT }); + } + if flags.compute { + quoted.push(quote! { #path::ShaderStages::COMPUTE }); + } + + quote! { #(#quoted)|* } + } + } + } +} + +const VISIBILITY: Symbol = Symbol("visibility"); +const VISIBILITY_VERTEX: Symbol = Symbol("vertex"); +const VISIBILITY_FRAGMENT: Symbol = Symbol("fragment"); +const VISIBILITY_COMPUTE: Symbol = Symbol("compute"); +const VISIBILITY_ALL: Symbol = Symbol("all"); +const VISIBILITY_NONE: Symbol = Symbol("none"); + +fn get_visibility_flag_value(meta_list: &MetaList) -> Result { + let mut flags = Vec::new(); + + meta_list.parse_nested_meta(|meta| { + flags.push(meta.path); + Ok(()) + })?; + + if flags.is_empty() { + return Err(Error::new_spanned( + meta_list, + "Invalid visibility format. Must be `visibility(flags)`, flags can be `all`, `none`, or a list-combination of `vertex`, `fragment` and/or `compute`." + )); + } + + if flags.len() == 1 + && let Some(flag) = flags.first() + { + if flag == VISIBILITY_ALL { + return Ok(ShaderStageVisibility::All); + } else if flag == VISIBILITY_NONE { + return Ok(ShaderStageVisibility::None); + } + } + + let mut visibility = VisibilityFlags::default(); + + for flag in flags { + if flag == VISIBILITY_VERTEX { + visibility.vertex = true; + } else if flag == VISIBILITY_FRAGMENT { + visibility.fragment = true; + } else if flag == VISIBILITY_COMPUTE { + visibility.compute = true; + } else { + return Err(Error::new_spanned( + flag, + "Not a valid visibility flag. Must be `all`, `none`, or a list-combination of `vertex`, `fragment` and/or `compute`." + )); + } + } + + Ok(ShaderStageVisibility::Flags(visibility)) +} + +// Returns the `binding_array(10)` part of a field-level declaration like +// `#[storage(binding_array(10))]`. +fn get_binding_array_flag_value(meta_list: &MetaList) -> Result { + meta_list + .parse_args_with(|input: ParseStream| input.parse::())? + .base10_parse() +} + +#[derive(Clone, Copy, Default)] +enum BindingTextureDimension { + D1, + #[default] + D2, + D2Array, + Cube, + CubeArray, + D3, +} + +enum BindingTextureSampleType { + Float { filterable: bool }, + Depth, + Sint, + Uint, +} + +impl ToTokens for BindingTextureDimension { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(match self { + BindingTextureDimension::D1 => quote! { TextureViewDimension::D1 }, + BindingTextureDimension::D2 => quote! { TextureViewDimension::D2 }, + BindingTextureDimension::D2Array => quote! { TextureViewDimension::D2Array }, + BindingTextureDimension::Cube => quote! { TextureViewDimension::Cube }, + BindingTextureDimension::CubeArray => quote! { TextureViewDimension::CubeArray }, + BindingTextureDimension::D3 => quote! { TextureViewDimension::D3 }, + }); + } +} + +impl ToTokens for BindingTextureSampleType { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(match self { + BindingTextureSampleType::Float { filterable } => { + quote! { TextureSampleType::Float { filterable: #filterable } } + } + BindingTextureSampleType::Depth => quote! { TextureSampleType::Depth }, + BindingTextureSampleType::Sint => quote! { TextureSampleType::Sint }, + BindingTextureSampleType::Uint => quote! { TextureSampleType::Uint }, + }); + } +} + +struct TextureAttrs { + dimension: BindingTextureDimension, + sample_type: BindingTextureSampleType, + multisampled: bool, + visibility: ShaderStageVisibility, +} + +impl Default for BindingTextureSampleType { + fn default() -> Self { + BindingTextureSampleType::Float { filterable: true } + } +} + +impl Default for TextureAttrs { + fn default() -> Self { + Self { + dimension: Default::default(), + sample_type: Default::default(), + multisampled: true, + visibility: Default::default(), + } + } +} + +struct StorageTextureAttrs { + dimension: BindingTextureDimension, + // Parsing of the image_format parameter is deferred to the type checker, + // which will error if the format is not member of the TextureFormat enum. + image_format: proc_macro2::TokenStream, + // Parsing of the access parameter is deferred to the type checker, + // which will error if the access is not member of the StorageTextureAccess enum. + access: proc_macro2::TokenStream, + visibility: ShaderStageVisibility, +} + +impl Default for StorageTextureAttrs { + fn default() -> Self { + Self { + dimension: Default::default(), + image_format: quote! { Rgba8Unorm }, + access: quote! { ReadWrite }, + visibility: ShaderStageVisibility::compute(), + } + } +} + +fn get_storage_texture_binding_attr(metas: Vec) -> Result { + let mut storage_texture_attrs = StorageTextureAttrs::default(); + + for meta in metas { + use syn::Meta::{List, NameValue}; + match meta { + // Parse #[storage_texture(0, dimension = "...")]. + NameValue(m) if m.path == DIMENSION => { + let value = get_lit_str(DIMENSION, &m.value)?; + storage_texture_attrs.dimension = get_texture_dimension_value(value)?; + } + // Parse #[storage_texture(0, format = ...))]. + NameValue(m) if m.path == IMAGE_FORMAT => { + storage_texture_attrs.image_format = m.value.into_token_stream(); + } + // Parse #[storage_texture(0, access = ...))]. + NameValue(m) if m.path == ACCESS => { + storage_texture_attrs.access = m.value.into_token_stream(); + } + // Parse #[storage_texture(0, visibility(...))]. + List(m) if m.path == VISIBILITY => { + storage_texture_attrs.visibility = get_visibility_flag_value(&m)?; + } + NameValue(m) => { + return Err(Error::new_spanned( + m.path, + "Not a valid name. Available attributes: `dimension`, `image_format`, `access`.", + )); + } + _ => { + return Err(Error::new_spanned( + meta, + "Not a name value pair: `foo = \"...\"`", + )); + } + } + } + + Ok(storage_texture_attrs) +} + +const DIMENSION: Symbol = Symbol("dimension"); +const IMAGE_FORMAT: Symbol = Symbol("image_format"); +const ACCESS: Symbol = Symbol("access"); +const SAMPLE_TYPE: Symbol = Symbol("sample_type"); +const FILTERABLE: Symbol = Symbol("filterable"); +const MULTISAMPLED: Symbol = Symbol("multisampled"); + +// Values for `dimension` attribute. +const DIM_1D: &str = "1d"; +const DIM_2D: &str = "2d"; +const DIM_3D: &str = "3d"; +const DIM_2D_ARRAY: &str = "2d_array"; +const DIM_CUBE: &str = "cube"; +const DIM_CUBE_ARRAY: &str = "cube_array"; + +// Values for sample `type` attribute. +const FLOAT: &str = "float"; +const DEPTH: &str = "depth"; +const S_INT: &str = "s_int"; +const U_INT: &str = "u_int"; + +fn get_texture_attrs(metas: Vec) -> Result { + let mut dimension = Default::default(); + let mut sample_type = Default::default(); + let mut multisampled = Default::default(); + let mut filterable = None; + let mut filterable_ident = None; + + let mut visibility = ShaderStageVisibility::vertex_fragment(); + + for meta in metas { + use syn::Meta::{List, NameValue}; + match meta { + // Parse #[texture(0, dimension = "...")]. + NameValue(m) if m.path == DIMENSION => { + let value = get_lit_str(DIMENSION, &m.value)?; + dimension = get_texture_dimension_value(value)?; + } + // Parse #[texture(0, sample_type = "...")]. + NameValue(m) if m.path == SAMPLE_TYPE => { + let value = get_lit_str(SAMPLE_TYPE, &m.value)?; + sample_type = get_texture_sample_type_value(value)?; + } + // Parse #[texture(0, multisampled = "...")]. + NameValue(m) if m.path == MULTISAMPLED => { + multisampled = get_lit_bool(MULTISAMPLED, &m.value)?; + } + // Parse #[texture(0, filterable = "...")]. + NameValue(m) if m.path == FILTERABLE => { + filterable = get_lit_bool(FILTERABLE, &m.value)?.into(); + filterable_ident = m.path.into(); + } + // Parse #[texture(0, visibility(...))]. + List(m) if m.path == VISIBILITY => { + visibility = get_visibility_flag_value(&m)?; + } + NameValue(m) => { + return Err(Error::new_spanned( + m.path, + "Not a valid name. Available attributes: `dimension`, `sample_type`, `multisampled`, or `filterable`." + )); + } + _ => { + return Err(Error::new_spanned( + meta, + "Not a name value pair: `foo = \"...\"`", + )); + } + } + } + + // Resolve `filterable` since the float + // sample type is the one that contains the value. + if let Some(filterable) = filterable { + let path = filterable_ident.unwrap(); + match sample_type { + BindingTextureSampleType::Float { filterable: _ } => { + sample_type = BindingTextureSampleType::Float { filterable } + } + _ => { + return Err(Error::new_spanned( + path, + "Type must be `float` to use the `filterable` attribute.", + )); + } + }; + } + + Ok(TextureAttrs { + dimension, + sample_type, + multisampled, + visibility, + }) +} + +fn get_texture_dimension_value(lit_str: &LitStr) -> Result { + match lit_str.value().as_str() { + DIM_1D => Ok(BindingTextureDimension::D1), + DIM_2D => Ok(BindingTextureDimension::D2), + DIM_2D_ARRAY => Ok(BindingTextureDimension::D2Array), + DIM_3D => Ok(BindingTextureDimension::D3), + DIM_CUBE => Ok(BindingTextureDimension::Cube), + DIM_CUBE_ARRAY => Ok(BindingTextureDimension::CubeArray), + + _ => Err(Error::new_spanned( + lit_str, + "Not a valid dimension. Must be `1d`, `2d`, `2d_array`, `3d`, `cube` or `cube_array`.", + )), + } +} + +fn get_texture_sample_type_value(lit_str: &LitStr) -> Result { + match lit_str.value().as_str() { + FLOAT => Ok(BindingTextureSampleType::Float { filterable: true }), + DEPTH => Ok(BindingTextureSampleType::Depth), + S_INT => Ok(BindingTextureSampleType::Sint), + U_INT => Ok(BindingTextureSampleType::Uint), + + _ => Err(Error::new_spanned( + lit_str, + "Not a valid sample type. Must be `float`, `depth`, `s_int` or `u_int`.", + )), + } +} + +#[derive(Default)] +struct SamplerAttrs { + sampler_binding_type: SamplerBindingType, + visibility: ShaderStageVisibility, +} + +#[derive(Default)] +enum SamplerBindingType { + #[default] + Filtering, + NonFiltering, + Comparison, +} + +impl ToTokens for SamplerBindingType { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.extend(match self { + SamplerBindingType::Filtering => quote! { SamplerBindingType::Filtering }, + SamplerBindingType::NonFiltering => quote! { SamplerBindingType::NonFiltering }, + SamplerBindingType::Comparison => quote! { SamplerBindingType::Comparison }, + }); + } +} + +const SAMPLER_TYPE: Symbol = Symbol("sampler_type"); + +const FILTERING: &str = "filtering"; +const NON_FILTERING: &str = "non_filtering"; +const COMPARISON: &str = "comparison"; + +fn get_sampler_attrs(metas: Vec) -> Result { + let mut sampler_binding_type = Default::default(); + let mut visibility = ShaderStageVisibility::vertex_fragment(); + + for meta in metas { + use syn::Meta::{List, NameValue}; + match meta { + // Parse #[sampler(0, sampler_type = "..."))]. + NameValue(m) if m.path == SAMPLER_TYPE => { + let value = get_lit_str(DIMENSION, &m.value)?; + sampler_binding_type = get_sampler_binding_type_value(value)?; + } + // Parse #[sampler(0, visibility(...))]. + List(m) if m.path == VISIBILITY => { + visibility = get_visibility_flag_value(&m)?; + } + NameValue(m) => { + return Err(Error::new_spanned( + m.path, + "Not a valid name. Available attributes: `sampler_type`.", + )); + } + _ => { + return Err(Error::new_spanned( + meta, + "Not a name value pair: `foo = \"...\"`", + )); + } + } + } + + Ok(SamplerAttrs { + sampler_binding_type, + visibility, + }) +} + +fn get_sampler_binding_type_value(lit_str: &LitStr) -> Result { + match lit_str.value().as_str() { + FILTERING => Ok(SamplerBindingType::Filtering), + NON_FILTERING => Ok(SamplerBindingType::NonFiltering), + COMPARISON => Ok(SamplerBindingType::Comparison), + + _ => Err(Error::new_spanned( + lit_str, + "Not a valid dimension. Must be `filtering`, `non_filtering`, or `comparison`.", + )), + } +} + +#[derive(Default)] +struct StorageAttrs { + visibility: ShaderStageVisibility, + binding_array: Option, + read_only: bool, + buffer: bool, +} + +const READ_ONLY: Symbol = Symbol("read_only"); +const BUFFER: Symbol = Symbol("buffer"); + +fn get_storage_binding_attr(metas: Vec) -> Result { + let mut visibility = ShaderStageVisibility::vertex_fragment(); + let mut binding_array = None; + let mut read_only = false; + let mut buffer = false; + + for meta in metas { + use syn::Meta::{List, Path}; + match meta { + // Parse #[storage(0, visibility(...))]. + List(m) if m.path == VISIBILITY => { + visibility = get_visibility_flag_value(&m)?; + } + // Parse #[storage(0, binding_array(...))] for bindless mode. + List(m) if m.path == BINDING_ARRAY_MODIFIER_NAME => { + binding_array = Some(get_binding_array_flag_value(&m)?); + } + Path(path) if path == READ_ONLY => { + read_only = true; + } + Path(path) if path == BUFFER => { + buffer = true; + } + _ => { + return Err(Error::new_spanned( + meta, + "Not a valid attribute. Available attributes: `read_only`, `visibility`", + )); + } + } + } + + Ok(StorageAttrs { + visibility, + binding_array, + read_only, + buffer, + }) +} diff --git a/crates/macros/src/extract_component.rs b/crates/macros/src/extract_component.rs new file mode 100644 index 0000000..8526f7b --- /dev/null +++ b/crates/macros/src/extract_component.rs @@ -0,0 +1,51 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, parse_quote, DeriveInput, Path}; + +pub fn derive_extract_component(input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + let bevy_render_path: Path = crate::bevy_render_path(); + let bevy_ecs_path: Path = bevy_macro_utils::BevyManifest::shared() + .maybe_get_path("bevy_ecs") + .expect("bevy_ecs should be found in manifest"); + + ast.generics + .make_where_clause() + .predicates + .push(parse_quote! { Self: Clone }); + + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + let filter = if let Some(attr) = ast + .attrs + .iter() + .find(|a| a.path().is_ident("extract_component_filter")) + { + let filter = match attr.parse_args::() { + Ok(filter) => filter, + Err(e) => return e.to_compile_error().into(), + }; + + quote! { + #filter + } + } else { + quote! { + () + } + }; + + TokenStream::from(quote! { + impl #impl_generics #bevy_render_path::extract_component::ExtractComponent for #struct_name #type_generics #where_clause { + type QueryData = &'static Self; + + type QueryFilter = #filter; + type Out = Self; + + fn extract_component(item: #bevy_ecs_path::query::QueryItem<'_, '_, Self::QueryData>) -> Option { + Some(item.clone()) + } + } + }) +} diff --git a/crates/macros/src/extract_resource.rs b/crates/macros/src/extract_resource.rs new file mode 100644 index 0000000..0a35eb4 --- /dev/null +++ b/crates/macros/src/extract_resource.rs @@ -0,0 +1,26 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, parse_quote, DeriveInput, Path}; + +pub fn derive_extract_resource(input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + let bevy_render_path: Path = crate::bevy_render_path(); + + ast.generics + .make_where_clause() + .predicates + .push(parse_quote! { Self: Clone }); + + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #bevy_render_path::extract_resource::ExtractResource for #struct_name #type_generics #where_clause { + type Source = Self; + + fn extract_resource(source: &Self::Source) -> Self { + source.clone() + } + } + }) +} diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs new file mode 100644 index 0000000..dbe6363 --- /dev/null +++ b/crates/macros/src/lib.rs @@ -0,0 +1,152 @@ +#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +#![cfg_attr(docsrs, feature(doc_cfg))] + +mod as_bind_group; +mod extract_component; +mod extract_resource; +mod specializer; + +use bevy_macro_utils::{derive_label, BevyManifest}; +use proc_macro::TokenStream; +use quote::format_ident; +use syn::{parse_macro_input, DeriveInput}; + +pub(crate) fn bevy_render_path() -> syn::Path { + // Use our vendored render module + // When used from within libmarathon, use crate::render + // When used from other crates, they would use libmarathon::render + syn::parse_quote!(crate::render) +} + +pub(crate) fn bevy_ecs_path() -> syn::Path { + // Still use bevy_ecs from the external crate + BevyManifest::shared().get_path("bevy_ecs") +} + +#[proc_macro_derive(ExtractResource)] +pub fn derive_extract_resource(input: TokenStream) -> TokenStream { + extract_resource::derive_extract_resource(input) +} + +/// Implements `ExtractComponent` trait for a component. +/// +/// The component must implement [`Clone`]. +/// The component will be extracted into the render world via cloning. +/// Note that this only enables extraction of the component, it does not execute the extraction. +/// See `ExtractComponentPlugin` to actually perform the extraction. +/// +/// If you only want to extract a component conditionally, you may use the `extract_component_filter` attribute. +/// +/// # Example +/// +/// ```no_compile +/// use bevy_ecs::component::Component; +/// use bevy_render_macros::ExtractComponent; +/// +/// #[derive(Component, Clone, ExtractComponent)] +/// #[extract_component_filter(With)] +/// pub struct Foo { +/// pub should_foo: bool, +/// } +/// +/// // Without a filter (unconditional). +/// #[derive(Component, Clone, ExtractComponent)] +/// pub struct Bar { +/// pub should_bar: bool, +/// } +/// ``` +#[proc_macro_derive(ExtractComponent, attributes(extract_component_filter))] +pub fn derive_extract_component(input: TokenStream) -> TokenStream { + extract_component::derive_extract_component(input) +} + +#[proc_macro_derive( + AsBindGroup, + attributes( + uniform, + storage_texture, + texture, + sampler, + bind_group_data, + storage, + bindless, + data + ) +)] +pub fn derive_as_bind_group(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + as_bind_group::derive_as_bind_group(input).unwrap_or_else(|err| err.to_compile_error().into()) +} + +/// Derive macro generating an impl of the trait `RenderLabel`. +/// +/// This does not work for unions. +#[proc_macro_derive(RenderLabel)] +pub fn derive_render_label(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut trait_path = bevy_render_path(); + trait_path + .segments + .push(format_ident!("render_graph").into()); + trait_path + .segments + .push(format_ident!("RenderLabel").into()); + derive_label(input, "RenderLabel", &trait_path) +} + +/// Derive macro generating an impl of the trait `RenderSubGraph`. +/// +/// This does not work for unions. +#[proc_macro_derive(RenderSubGraph)] +pub fn derive_render_sub_graph(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut trait_path = bevy_render_path(); + trait_path + .segments + .push(format_ident!("render_graph").into()); + trait_path + .segments + .push(format_ident!("RenderSubGraph").into()); + derive_label(input, "RenderSubGraph", &trait_path) +} + +/// Derive macro generating an impl of the trait `Specializer` +/// +/// This only works for structs whose members all implement `Specializer` +#[proc_macro_derive(Specializer, attributes(specialize, key, base_descriptor))] +pub fn derive_specialize(input: TokenStream) -> TokenStream { + specializer::impl_specializer(input) +} + +/// Derive macro generating the most common impl of the trait `SpecializerKey` +#[proc_macro_derive(SpecializerKey)] +pub fn derive_specializer_key(input: TokenStream) -> TokenStream { + specializer::impl_specializer_key(input) +} + +#[proc_macro_derive(ShaderLabel)] +pub fn derive_shader_label(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut trait_path = bevy_render_path(); + trait_path + .segments + .push(format_ident!("render_phase").into()); + trait_path + .segments + .push(format_ident!("ShaderLabel").into()); + derive_label(input, "ShaderLabel", &trait_path) +} + +#[proc_macro_derive(DrawFunctionLabel)] +pub fn derive_draw_function_label(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut trait_path = bevy_render_path(); + trait_path + .segments + .push(format_ident!("render_phase").into()); + trait_path + .segments + .push(format_ident!("DrawFunctionLabel").into()); + derive_label(input, "DrawFunctionLabel", &trait_path) +} diff --git a/crates/macros/src/specializer.rs b/crates/macros/src/specializer.rs new file mode 100644 index 0000000..d59d496 --- /dev/null +++ b/crates/macros/src/specializer.rs @@ -0,0 +1,379 @@ +use bevy_macro_utils::{ + fq_std::{FQDefault, FQResult}, + get_struct_fields, +}; +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, parse_quote, + punctuated::Punctuated, + spanned::Spanned, + DeriveInput, Expr, Field, Ident, Index, Member, Meta, MetaList, Pat, Path, Token, Type, + WherePredicate, +}; + +const SPECIALIZE_ATTR_IDENT: &str = "specialize"; +const SPECIALIZE_ALL_IDENT: &str = "all"; + +const KEY_ATTR_IDENT: &str = "key"; +const KEY_DEFAULT_IDENT: &str = "default"; + +enum SpecializeImplTargets { + All, + Specific(Vec), +} + +impl Parse for SpecializeImplTargets { + fn parse(input: ParseStream) -> syn::Result { + let paths = input.parse_terminated(Path::parse, Token![,])?; + if paths + .first() + .is_some_and(|p| p.is_ident(SPECIALIZE_ALL_IDENT)) + { + Ok(SpecializeImplTargets::All) + } else { + Ok(SpecializeImplTargets::Specific(paths.into_iter().collect())) + } + } +} + +#[derive(Clone)] +enum Key { + Whole, + Default, + Index(Index), + Custom(Expr), +} + +impl Key { + fn expr(&self) -> Expr { + match self { + Key::Whole => parse_quote!(key), + Key::Default => parse_quote!(#FQDefault::default()), + Key::Index(index) => { + let member = Member::Unnamed(index.clone()); + parse_quote!(key.#member) + } + Key::Custom(expr) => expr.clone(), + } + } +} + +const KEY_ERROR_MSG: &str = "Invalid key override. Must be either `default` or a valid Rust expression of the correct key type"; + +impl Parse for Key { + fn parse(input: ParseStream) -> syn::Result { + if let Ok(ident) = input.parse::() { + if ident == KEY_DEFAULT_IDENT { + Ok(Key::Default) + } else { + Err(syn::Error::new_spanned(ident, KEY_ERROR_MSG)) + } + } else { + input.parse::().map(Key::Custom).map_err(|mut err| { + err.extend(syn::Error::new(err.span(), KEY_ERROR_MSG)); + err + }) + } + } +} + +#[derive(Clone)] +struct FieldInfo { + ty: Type, + member: Member, + key: Key, +} + +impl FieldInfo { + fn key_ty(&self, specialize_path: &Path, target_path: &Path) -> Option { + let ty = &self.ty; + matches!(self.key, Key::Whole | Key::Index(_)) + .then_some(parse_quote!(<#ty as #specialize_path::Specializer<#target_path>>::Key)) + } + + fn key_ident(&self, ident: Ident) -> Option { + matches!(self.key, Key::Whole | Key::Index(_)).then_some(ident) + } + + fn specialize_expr(&self, specialize_path: &Path, target_path: &Path) -> Expr { + let FieldInfo { + ty, member, key, .. + } = &self; + let key_expr = key.expr(); + parse_quote!(<#ty as #specialize_path::Specializer<#target_path>>::specialize(&self.#member, #key_expr, descriptor)) + } + + fn specialize_predicate(&self, specialize_path: &Path, target_path: &Path) -> WherePredicate { + let ty = &self.ty; + if matches!(&self.key, Key::Default) { + parse_quote!(#ty: #specialize_path::Specializer<#target_path, Key: #FQDefault>) + } else { + parse_quote!(#ty: #specialize_path::Specializer<#target_path>) + } + } +} + +fn get_field_info( + fields: &Punctuated, + targets: &SpecializeImplTargets, +) -> syn::Result> { + let mut field_info: Vec = Vec::new(); + let mut used_count = 0; + let mut single_index = 0; + for (index, field) in fields.iter().enumerate() { + let field_ty = field.ty.clone(); + let field_member = field.ident.clone().map_or( + Member::Unnamed(Index { + index: index as u32, + span: field.span(), + }), + Member::Named, + ); + let key_index = Index { + index: used_count, + span: field.span(), + }; + + let mut use_key_field = true; + let mut key = Key::Index(key_index); + for attr in &field.attrs { + match &attr.meta { + Meta::List(MetaList { path, tokens, .. }) if path.is_ident(&KEY_ATTR_IDENT) => { + let owned_tokens = tokens.clone().into(); + let Ok(parsed_key) = syn::parse::(owned_tokens) else { + return Err(syn::Error::new( + attr.span(), + "Invalid key override attribute", + )); + }; + key = parsed_key; + if matches!( + (&key, &targets), + (Key::Custom(_), SpecializeImplTargets::All) + ) { + return Err(syn::Error::new( + attr.span(), + "#[key(default)] is the only key override type allowed with #[specialize(all)]", + )); + } + use_key_field = false; + } + _ => {} + } + } + + if use_key_field { + used_count += 1; + single_index = index; + } + + field_info.push(FieldInfo { + ty: field_ty, + member: field_member, + key, + }); + } + + if used_count == 1 { + field_info[single_index].key = Key::Whole; + } + + Ok(field_info) +} + +fn get_specialize_targets( + ast: &DeriveInput, + derive_name: &str, +) -> syn::Result { + let specialize_attr = ast.attrs.iter().find_map(|attr| { + if attr.path().is_ident(SPECIALIZE_ATTR_IDENT) + && let Meta::List(meta_list) = &attr.meta + { + return Some(meta_list); + } + None + }); + let Some(specialize_meta_list) = specialize_attr else { + return Err(syn::Error::new( + Span::call_site(), + format!("#[derive({derive_name})] must be accompanied by #[specialize(..targets)].\n Example usages: #[specialize(RenderPipeline)], #[specialize(all)]") + )); + }; + syn::parse::(specialize_meta_list.tokens.clone().into()) +} + +macro_rules! guard { + ($expr: expr) => { + match $expr { + Ok(__val) => __val, + Err(err) => return err.to_compile_error().into(), + } + }; +} + +pub fn impl_specializer(input: TokenStream) -> TokenStream { + let bevy_render_path: Path = crate::bevy_render_path(); + let specialize_path = { + let mut path = bevy_render_path.clone(); + path.segments.push(format_ident!("render_resource").into()); + path + }; + + let ecs_path = crate::bevy_ecs_path(); + + let ast = parse_macro_input!(input as DeriveInput); + let targets = guard!(get_specialize_targets(&ast, "Specializer")); + let fields = guard!(get_struct_fields(&ast.data, "Specializer")); + let field_info = guard!(get_field_info(fields, &targets)); + + let key_idents: Vec> = field_info + .iter() + .enumerate() + .map(|(i, field_info)| field_info.key_ident(format_ident!("key{i}"))) + .collect(); + let key_tuple_idents: Vec = key_idents.iter().flatten().cloned().collect(); + let ignore_pat: Pat = parse_quote!(_); + let key_patterns: Vec = key_idents + .iter() + .map(|key_ident| match key_ident { + Some(key_ident) => parse_quote!(#key_ident), + None => ignore_pat.clone(), + }) + .collect(); + + match targets { + SpecializeImplTargets::All => impl_specialize_all( + &specialize_path, + &ecs_path, + &ast, + &field_info, + &key_patterns, + &key_tuple_idents, + ), + SpecializeImplTargets::Specific(targets) => targets + .iter() + .map(|target| { + impl_specialize_specific( + &specialize_path, + &ecs_path, + &ast, + &field_info, + target, + &key_patterns, + &key_tuple_idents, + ) + }) + .collect(), + } +} + +fn impl_specialize_all( + specialize_path: &Path, + ecs_path: &Path, + ast: &DeriveInput, + field_info: &[FieldInfo], + key_patterns: &[Pat], + key_tuple_idents: &[Ident], +) -> TokenStream { + let target_path = Path::from(format_ident!("T")); + let key_elems: Vec = field_info + .iter() + .filter_map(|field_info| field_info.key_ty(specialize_path, &target_path)) + .collect(); + let specialize_exprs: Vec = field_info + .iter() + .map(|field_info| field_info.specialize_expr(specialize_path, &target_path)) + .collect(); + + let struct_name = &ast.ident; + let mut generics = ast.generics.clone(); + generics.params.insert( + 0, + parse_quote!(#target_path: #specialize_path::Specializable), + ); + + if !field_info.is_empty() { + let where_clause = generics.make_where_clause(); + for field in field_info { + where_clause + .predicates + .push(field.specialize_predicate(specialize_path, &target_path)); + } + } + + let (_, type_generics, _) = ast.generics.split_for_impl(); + let (impl_generics, _, where_clause) = &generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #specialize_path::Specializer<#target_path> for #struct_name #type_generics #where_clause { + type Key = (#(#key_elems),*); + + fn specialize( + &self, + key: Self::Key, + descriptor: &mut <#target_path as #specialize_path::Specializable>::Descriptor + ) -> #FQResult<#specialize_path::Canonical, #ecs_path::error::BevyError> { + #(let #key_patterns = #specialize_exprs?;)* + #FQResult::Ok((#(#key_tuple_idents),*)) + } + } + }) +} + +fn impl_specialize_specific( + specialize_path: &Path, + ecs_path: &Path, + ast: &DeriveInput, + field_info: &[FieldInfo], + target_path: &Path, + key_patterns: &[Pat], + key_tuple_idents: &[Ident], +) -> TokenStream { + let key_elems: Vec = field_info + .iter() + .filter_map(|field_info| field_info.key_ty(specialize_path, target_path)) + .collect(); + let specialize_exprs: Vec = field_info + .iter() + .map(|field_info| field_info.specialize_expr(specialize_path, target_path)) + .collect(); + + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #specialize_path::Specializer<#target_path> for #struct_name #type_generics #where_clause { + type Key = (#(#key_elems),*); + + fn specialize( + &self, + key: Self::Key, + descriptor: &mut <#target_path as #specialize_path::Specializable>::Descriptor + ) -> #FQResult<#specialize_path::Canonical, #ecs_path::error::BevyError> { + #(let #key_patterns = #specialize_exprs?;)* + #FQResult::Ok((#(#key_tuple_idents),*)) + } + } + }) +} + +pub fn impl_specializer_key(input: TokenStream) -> TokenStream { + let bevy_render_path: Path = crate::bevy_render_path(); + let specialize_path = { + let mut path = bevy_render_path.clone(); + path.segments.push(format_ident!("render_resource").into()); + path + }; + + let ast = parse_macro_input!(input as DeriveInput); + let ident = ast.ident; + TokenStream::from(quote!( + impl #specialize_path::SpecializerKey for #ident { + const IS_CANONICAL: bool = true; + type Canonical = Self; + } + )) +} diff --git a/crates/sync-macros/tests/basic_macro_test.rs b/crates/macros/tests/basic_macro_test.rs similarity index 100% rename from crates/sync-macros/tests/basic_macro_test.rs rename to crates/macros/tests/basic_macro_test.rs diff --git a/crates/sync-macros/src/lib.rs b/crates/sync-macros/src/lib.rs deleted file mode 100644 index 6315d4e..0000000 --- a/crates/sync-macros/src/lib.rs +++ /dev/null @@ -1,578 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{ - DeriveInput, - ItemStruct, - parse_macro_input, -}; - -/// Sync strategy types -#[derive(Debug, Clone, PartialEq)] -enum SyncStrategy { - LastWriteWins, - Set, - Sequence, - Custom, -} - -impl SyncStrategy { - fn from_str(s: &str) -> Result { - match s { - | "LastWriteWins" => Ok(SyncStrategy::LastWriteWins), - | "Set" => Ok(SyncStrategy::Set), - | "Sequence" => Ok(SyncStrategy::Sequence), - | "Custom" => Ok(SyncStrategy::Custom), - | _ => Err(format!( - "Unknown strategy '{}'. Choose one of: \"LastWriteWins\", \"Set\", \"Sequence\", \"Custom\"", - s - )), - } - } - - fn to_tokens(&self) -> proc_macro2::TokenStream { - match self { - | SyncStrategy::LastWriteWins => { - quote! { libmarathon::networking::SyncStrategy::LastWriteWins } - }, - | SyncStrategy::Set => quote! { libmarathon::networking::SyncStrategy::Set }, - | SyncStrategy::Sequence => quote! { libmarathon::networking::SyncStrategy::Sequence }, - | SyncStrategy::Custom => quote! { libmarathon::networking::SyncStrategy::Custom }, - } - } -} - -/// Parsed sync attributes -struct SyncAttributes { - version: u32, - strategy: SyncStrategy, -} - -impl SyncAttributes { - fn parse(input: &DeriveInput) -> Result { - let mut version: Option = None; - let mut strategy: Option = None; - - // Find the #[sync(...)] attribute - for attr in &input.attrs { - if !attr.path().is_ident("sync") { - continue; - } - - attr.parse_nested_meta(|meta| { - if meta.path.is_ident("version") { - let value: syn::LitInt = meta.value()?.parse()?; - version = Some(value.base10_parse()?); - Ok(()) - } else if meta.path.is_ident("strategy") { - let value: syn::LitStr = meta.value()?.parse()?; - let strategy_str = value.value(); - strategy = Some( - SyncStrategy::from_str(&strategy_str) - .map_err(|e| syn::Error::new_spanned(&value, e))?, - ); - Ok(()) - } else { - Err(meta.error("unrecognized sync attribute")) - } - })?; - } - - // Require version and strategy - let version = version.ok_or_else(|| { - syn::Error::new( - proc_macro2::Span::call_site(), - "Missing required attribute `version`\n\n \n\n = help: Add #[sync(version = 1, strategy = \"...\")] to your struct\n\n = note: See documentation: https://docs.rs/lonni/sync/strategies.html", - ) - })?; - - let strategy = strategy.ok_or_else(|| { - syn::Error::new( - proc_macro2::Span::call_site(), - "Missing required attribute `strategy`\n\n \n\n = help: Choose one of: \"LastWriteWins\", \"Set\", \"Sequence\", \"Custom\"\n\n = help: Add #[sync(version = 1, strategy = \"LastWriteWins\")] to your struct\n\n = note: See documentation: https://docs.rs/lonni/sync/strategies.html", - ) - })?; - - Ok(SyncAttributes { - version, - strategy, - }) - } -} - -/// RFC 0003 macro: Generate SyncComponent trait implementation -/// -/// # Example -/// ```ignore -/// use bevy::prelude::*; -/// use libmarathon::networking::Synced; -/// use sync_macros::Synced as SyncedDerive; -/// -/// #[derive(Component, Clone)] -/// #[derive(Synced)] -/// #[sync(version = 1, strategy = "LastWriteWins")] -/// struct Health(f32); -/// -/// // In a Bevy system: -/// fn spawn_health(mut commands: Commands) { -/// commands.spawn((Health(100.0), Synced)); -/// } -/// ``` -#[proc_macro_derive(Synced, attributes(sync))] -pub fn derive_synced(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - - // Parse attributes - let attrs = match SyncAttributes::parse(&input) { - | Ok(attrs) => attrs, - | Err(e) => return TokenStream::from(e.to_compile_error()), - }; - - let name = &input.ident; - let name_str = name.to_string(); - let version = attrs.version; - let strategy_tokens = attrs.strategy.to_tokens(); - - // Generate serialization method based on type - let serialize_impl = generate_serialize(&input); - let deserialize_impl = generate_deserialize(&input, name); - - // Generate merge method based on strategy - let merge_impl = generate_merge(&input, &attrs.strategy); - - // Extract struct attributes and visibility for re-emission - let vis = &input.vis; - let attrs_without_sync: Vec<_> = input - .attrs - .iter() - .filter(|attr| !attr.path().is_ident("sync")) - .collect(); - let struct_token = match &input.data { - | syn::Data::Struct(_) => quote! { struct }, - | _ => quote! {}, - }; - - // Re-emit the struct with rkyv derives added - let rkyv_struct = match &input.data { - | syn::Data::Struct(data_struct) => { - let fields = &data_struct.fields; - quote! { - #[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] - #(#attrs_without_sync)* - #vis #struct_token #name #fields - } - }, - | _ => quote! {}, - }; - - let expanded = quote! { - // Re-emit struct with rkyv derives - #rkyv_struct - - // Register component with inventory for type registry - // Build type path at compile time using concat! and module_path! - // since std::any::type_name() is not yet const - const _: () = { - const TYPE_PATH: &str = concat!(module_path!(), "::", stringify!(#name)); - - inventory::submit! { - libmarathon::persistence::ComponentMeta { - type_name: #name_str, - type_path: TYPE_PATH, - type_id: std::any::TypeId::of::<#name>(), - deserialize_fn: |bytes: &[u8]| -> anyhow::Result> { - let component: #name = rkyv::from_bytes::<#name, rkyv::rancor::Failure>(bytes)?; - Ok(Box::new(component)) - }, - serialize_fn: |world: &bevy::ecs::world::World, entity: bevy::ecs::entity::Entity| -> Option { - world.get::<#name>(entity).and_then(|component| { - rkyv::to_bytes::(component) - .map(|vec| bytes::Bytes::from(vec.to_vec())) - .ok() - }) - }, - insert_fn: |entity_mut: &mut bevy::ecs::world::EntityWorldMut, boxed: Box| { - if let Ok(component) = boxed.downcast::<#name>() { - entity_mut.insert(*component); - } - }, - } - }; - }; - - impl libmarathon::networking::SyncComponent for #name { - const VERSION: u32 = #version; - const STRATEGY: libmarathon::networking::SyncStrategy = #strategy_tokens; - - #[inline] - fn serialize_sync(&self) -> anyhow::Result { - #serialize_impl - } - - #[inline] - fn deserialize_sync(data: &[u8]) -> anyhow::Result { - #deserialize_impl - } - - #[inline] - fn merge(&mut self, remote: Self, clock_cmp: libmarathon::networking::ClockComparison) -> libmarathon::networking::ComponentMergeDecision { - #merge_impl - } - } - }; - - TokenStream::from(expanded) -} - -/// Generate specialized serialization code -fn generate_serialize(_input: &DeriveInput) -> proc_macro2::TokenStream { - // Use rkyv for zero-copy serialization - // Later we can optimize for specific types (e.g., f32 -> to_le_bytes) - quote! { - rkyv::to_bytes::(self).map(|bytes| bytes::Bytes::from(bytes.to_vec())).map_err(|e| anyhow::anyhow!("Serialization failed: {}", e)) - } -} - -/// Generate specialized deserialization code -fn generate_deserialize(_input: &DeriveInput, _name: &syn::Ident) -> proc_macro2::TokenStream { - quote! { - rkyv::from_bytes::(data).map_err(|e| anyhow::anyhow!("Deserialization failed: {}", e)) - } -} - -/// Generate merge logic based on strategy -fn generate_merge(input: &DeriveInput, strategy: &SyncStrategy) -> proc_macro2::TokenStream { - match strategy { - | SyncStrategy::LastWriteWins => generate_lww_merge(input), - | SyncStrategy::Set => generate_set_merge(input), - | SyncStrategy::Sequence => generate_sequence_merge(input), - | SyncStrategy::Custom => generate_custom_merge(input), - } -} - -/// Generate hash calculation code for tiebreaking in concurrent merges -/// -/// Returns a TokenStream that computes hashes for both local and remote values -/// and compares them for deterministic conflict resolution. -fn generate_hash_tiebreaker() -> proc_macro2::TokenStream { - quote! { - let local_hash = { - let bytes = rkyv::to_bytes::(self).map(|b| b.to_vec()).unwrap_or_default(); - bytes.iter().fold(0u64, |acc, &b| acc.wrapping_mul(31).wrapping_add(b as u64)) - }; - let remote_hash = { - let bytes = rkyv::to_bytes::(&remote).map(|b| b.to_vec()).unwrap_or_default(); - bytes.iter().fold(0u64, |acc, &b| acc.wrapping_mul(31).wrapping_add(b as u64)) - }; - } -} - -/// Generate Last-Write-Wins merge logic -fn generate_lww_merge(_input: &DeriveInput) -> proc_macro2::TokenStream { - let hash_tiebreaker = generate_hash_tiebreaker(); - - quote! { - use tracing::info; - - match clock_cmp { - libmarathon::networking::ClockComparison::RemoteNewer => { - info!( - component = std::any::type_name::(), - ?clock_cmp, - "Taking remote (newer)" - ); - *self = remote; - libmarathon::networking::ComponentMergeDecision::TookRemote - } - libmarathon::networking::ClockComparison::LocalNewer => { - libmarathon::networking::ComponentMergeDecision::KeptLocal - } - libmarathon::networking::ClockComparison::Concurrent => { - // Tiebreaker: Compare serialized representations for deterministic choice - // In a real implementation, we'd use node_id, but for now use a simple hash - #hash_tiebreaker - - if remote_hash > local_hash { - info!( - component = std::any::type_name::(), - ?clock_cmp, - "Taking remote (concurrent, tiebreaker)" - ); - *self = remote; - libmarathon::networking::ComponentMergeDecision::TookRemote - } else { - libmarathon::networking::ComponentMergeDecision::KeptLocal - } - } - } - } -} - -/// Generate OR-Set merge logic -/// -/// For OR-Set strategy, the component must contain an OrSet field. -/// We merge by calling the OrSet's merge method which implements add-wins -/// semantics. -fn generate_set_merge(_input: &DeriveInput) -> proc_macro2::TokenStream { - let hash_tiebreaker = generate_hash_tiebreaker(); - - quote! { - use tracing::info; - - // For Set strategy, we always merge the sets - // The OrSet CRDT handles the conflict resolution with add-wins semantics - info!( - component = std::any::type_name::(), - "Merging OR-Set (add-wins semantics)" - ); - - // Assuming the component wraps an OrSet or has a field with merge() - // For now, we'll do a structural merge by replacing the whole value - // This is a simplified implementation - full implementation would require - // the component to expose merge() method or implement it directly - - match clock_cmp { - libmarathon::networking::ClockComparison::RemoteNewer => { - *self = remote; - libmarathon::networking::ComponentMergeDecision::TookRemote - } - libmarathon::networking::ClockComparison::LocalNewer => { - libmarathon::networking::ComponentMergeDecision::KeptLocal - } - libmarathon::networking::ClockComparison::Concurrent => { - // In a full implementation, we would merge the OrSet here - // For now, use LWW with tiebreaker as fallback - #hash_tiebreaker - - if remote_hash > local_hash { - *self = remote; - libmarathon::networking::ComponentMergeDecision::TookRemote - } else { - libmarathon::networking::ComponentMergeDecision::KeptLocal - } - } - } - } -} - -/// Generate RGA/Sequence merge logic -/// -/// For Sequence strategy, the component must contain an Rga field. -/// We merge by calling the Rga's merge method which maintains causal ordering. -fn generate_sequence_merge(_input: &DeriveInput) -> proc_macro2::TokenStream { - let hash_tiebreaker = generate_hash_tiebreaker(); - - quote! { - use tracing::info; - - // For Sequence strategy, we always merge the sequences - // The RGA CRDT handles the conflict resolution with causal ordering - info!( - component = std::any::type_name::(), - "Merging RGA sequence (causal ordering)" - ); - - // Assuming the component wraps an Rga or has a field with merge() - // For now, we'll do a structural merge by replacing the whole value - // This is a simplified implementation - full implementation would require - // the component to expose merge() method or implement it directly - - match clock_cmp { - libmarathon::networking::ClockComparison::RemoteNewer => { - *self = remote; - libmarathon::networking::ComponentMergeDecision::TookRemote - } - libmarathon::networking::ClockComparison::LocalNewer => { - libmarathon::networking::ComponentMergeDecision::KeptLocal - } - libmarathon::networking::ClockComparison::Concurrent => { - // In a full implementation, we would merge the Rga here - // For now, use LWW with tiebreaker as fallback - #hash_tiebreaker - - if remote_hash > local_hash { - *self = remote; - libmarathon::networking::ComponentMergeDecision::TookRemote - } else { - libmarathon::networking::ComponentMergeDecision::KeptLocal - } - } - } - } -} - -/// Generate custom merge logic placeholder -fn generate_custom_merge(input: &DeriveInput) -> proc_macro2::TokenStream { - let name = &input.ident; - quote! { - compile_error!( - concat!( - "Custom strategy requires implementing ConflictResolver trait for ", - stringify!(#name) - ) - ); - libmarathon::networking::ComponentMergeDecision::KeptLocal - } -} - - -/// Attribute macro for synced components -/// -/// This is an alternative to the derive macro that automatically adds rkyv derives. -/// -/// # Example -/// ```ignore -/// #[synced(version = 1, strategy = "LastWriteWins")] -/// struct Health(f32); -/// ``` -#[proc_macro_attribute] -pub fn synced(attr: TokenStream, item: TokenStream) -> TokenStream { - let input_struct = match syn::parse::(item.clone()) { - Ok(s) => s, - Err(e) => { - return syn::Error::new_spanned( - proc_macro2::TokenStream::from(item), - format!("synced attribute can only be applied to structs: {}", e), - ) - .to_compile_error() - .into(); - } - }; - - // Parse the attribute arguments manually - let attr_str = attr.to_string(); - let (version, strategy) = parse_attr_string(&attr_str); - - // Generate the same implementations as the derive macro - let name = &input_struct.ident; - let name_str = name.to_string(); - let strategy_tokens = strategy.to_tokens(); - let vis = &input_struct.vis; - let attrs = &input_struct.attrs; - let generics = &input_struct.generics; - let fields = &input_struct.fields; - - // Convert ItemStruct to DeriveInput for compatibility with existing functions - // Build it manually to avoid parse_quote issues with tuple structs - let derive_input = DeriveInput { - attrs: attrs.clone(), - vis: vis.clone(), - ident: name.clone(), - generics: generics.clone(), - data: syn::Data::Struct(syn::DataStruct { - struct_token: syn::token::Struct::default(), - fields: fields.clone(), - semi_token: if matches!(fields, syn::Fields::Unit) { - Some(syn::token::Semi::default()) - } else { - None - }, - }), - }; - - let serialize_impl = generate_serialize(&derive_input); - let deserialize_impl = generate_deserialize(&derive_input, name); - let merge_impl = generate_merge(&derive_input, &strategy); - - // Add semicolon for tuple/unit structs - let semi = if matches!(fields, syn::Fields::Named(_)) { - quote! {} - } else { - quote! { ; } - }; - - let expanded = quote! { - // Output the struct with rkyv derives added - #[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] - #(#attrs)* - #vis struct #name #generics #fields #semi - - // Register component with inventory for type registry - const _: () = { - const TYPE_PATH: &str = concat!(module_path!(), "::", stringify!(#name)); - - inventory::submit! { - libmarathon::persistence::ComponentMeta { - type_name: #name_str, - type_path: TYPE_PATH, - type_id: std::any::TypeId::of::<#name>(), - deserialize_fn: |bytes: &[u8]| -> anyhow::Result> { - let component: #name = rkyv::from_bytes::<#name, rkyv::rancor::Failure>(bytes)?; - Ok(Box::new(component)) - }, - serialize_fn: |world: &bevy::ecs::world::World, entity: bevy::ecs::entity::Entity| -> Option { - world.get::<#name>(entity).and_then(|component| { - rkyv::to_bytes::(component) - .map(|vec| bytes::Bytes::from(vec.to_vec())) - .ok() - }) - }, - insert_fn: |entity_mut: &mut bevy::ecs::world::EntityWorldMut, boxed: Box| { - if let Ok(component) = boxed.downcast::<#name>() { - entity_mut.insert(*component); - } - }, - } - }; - }; - - impl libmarathon::networking::SyncComponent for #name { - const VERSION: u32 = #version; - const STRATEGY: libmarathon::networking::SyncStrategy = #strategy_tokens; - - #[inline] - fn serialize_sync(&self) -> anyhow::Result { - #serialize_impl - } - - #[inline] - fn deserialize_sync(data: &[u8]) -> anyhow::Result { - #deserialize_impl - } - - #[inline] - fn merge(&mut self, remote: Self, clock_cmp: libmarathon::networking::ClockComparison) -> libmarathon::networking::ComponentMergeDecision { - #merge_impl - } - } - }; - - TokenStream::from(expanded) -} - -/// Parse attribute string (simple parser for version and strategy) -fn parse_attr_string(attr: &str) -> (u32, SyncStrategy) { - let mut version = 1; - let mut strategy = SyncStrategy::LastWriteWins; - - // Simple parsing - look for version = N and strategy = "..." - if let Some(v_pos) = attr.find("version") { - if let Some(eq_pos) = attr[v_pos..].find('=') { - let start = v_pos + eq_pos + 1; - let rest = &attr[start..].trim(); - if let Some(comma_pos) = rest.find(',') { - if let Ok(v) = rest[..comma_pos].trim().parse() { - version = v; - } - } else if let Ok(v) = rest.trim().parse() { - version = v; - } - } - } - - if let Some(s_pos) = attr.find("strategy") { - if let Some(eq_pos) = attr[s_pos..].find('=') { - let start = s_pos + eq_pos + 1; - let rest = &attr[start..].trim(); - if let Some(quote_start) = rest.find('"') { - if let Some(quote_end) = rest[quote_start + 1..].find('"') { - let strategy_str = &rest[quote_start + 1..quote_start + 1 + quote_end]; - if let Ok(s) = SyncStrategy::from_str(strategy_str) { - strategy = s; - } - } - } - } - } - - (version, strategy) -}

    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