@bornengine/engine 0.4.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -0
  3. package/native/android/Cargo.lock +1848 -0
  4. package/native/android/Cargo.toml +24 -0
  5. package/native/android/src/lib.rs +702 -0
  6. package/native/ios/Cargo.lock +1690 -0
  7. package/native/ios/Cargo.toml +32 -0
  8. package/native/ios/src/lib.rs +1267 -0
  9. package/native/linux/Cargo.lock +3279 -0
  10. package/native/linux/Cargo.toml +29 -0
  11. package/native/linux/src/lib.rs +1331 -0
  12. package/native/macos/Cargo.lock +3310 -0
  13. package/native/macos/Cargo.toml +46 -0
  14. package/native/macos/src/lib.rs +1302 -0
  15. package/native/shared/Cargo.lock +1899 -0
  16. package/native/shared/Cargo.toml +62 -0
  17. package/native/shared/assets/default_font.ttf +0 -0
  18. package/native/shared/build.rs +270 -0
  19. package/native/shared/shaders/common/clouds.wgsl +122 -0
  20. package/native/shared/shaders/common/fog.wgsl +16 -0
  21. package/native/shared/shaders/common/foliage_wind.wgsl +98 -0
  22. package/native/shared/shaders/common/imposter.wgsl +112 -0
  23. package/native/shared/shaders/common/pbr.wgsl +186 -0
  24. package/native/shared/shaders/common/shadows.wgsl +186 -0
  25. package/native/shared/shaders/common/sky.wgsl +8 -0
  26. package/native/shared/shaders/common/tonemap.wgsl +25 -0
  27. package/native/shared/shaders/impulse_field.wgsl +57 -0
  28. package/native/shared/shaders/material_abi.wgsl +383 -0
  29. package/native/shared/shaders/materials/test_minimal.wgsl +42 -0
  30. package/native/shared/src/anim_mixer.rs +61 -0
  31. package/native/shared/src/attach.rs +263 -0
  32. package/native/shared/src/audio/decode.rs +123 -0
  33. package/native/shared/src/audio/mod.rs +863 -0
  34. package/native/shared/src/audio/render.rs +892 -0
  35. package/native/shared/src/audio/spsc.rs +156 -0
  36. package/native/shared/src/audio/stream.rs +226 -0
  37. package/native/shared/src/custom_shaders.rs +104 -0
  38. package/native/shared/src/decals.rs +245 -0
  39. package/native/shared/src/drs.rs +211 -0
  40. package/native/shared/src/engine.rs +261 -0
  41. package/native/shared/src/ffi.rs +116 -0
  42. package/native/shared/src/ffi_core/assets.rs +388 -0
  43. package/native/shared/src/ffi_core/audio_ffi.rs +184 -0
  44. package/native/shared/src/ffi_core/draw.rs +334 -0
  45. package/native/shared/src/ffi_core/game_loop.rs +577 -0
  46. package/native/shared/src/ffi_core/input.rs +234 -0
  47. package/native/shared/src/ffi_core/mod.rs +127 -0
  48. package/native/shared/src/ffi_core/models.rs +1154 -0
  49. package/native/shared/src/ffi_core/ragdoll_ffi.rs +261 -0
  50. package/native/shared/src/ffi_core/scene.rs +626 -0
  51. package/native/shared/src/ffi_core/vfx.rs +212 -0
  52. package/native/shared/src/ffi_core/visual.rs +691 -0
  53. package/native/shared/src/frame_callbacks.rs +122 -0
  54. package/native/shared/src/geometry.rs +236 -0
  55. package/native/shared/src/handles.rs +182 -0
  56. package/native/shared/src/input.rs +448 -0
  57. package/native/shared/src/jolt_sys.rs +822 -0
  58. package/native/shared/src/lib.rs +55 -0
  59. package/native/shared/src/models.rs +1093 -0
  60. package/native/shared/src/models_gltf.rs +1280 -0
  61. package/native/shared/src/particles.rs +391 -0
  62. package/native/shared/src/physics_jolt.rs +1908 -0
  63. package/native/shared/src/picking.rs +298 -0
  64. package/native/shared/src/postfx.rs +345 -0
  65. package/native/shared/src/profiler.rs +492 -0
  66. package/native/shared/src/ragdoll.rs +474 -0
  67. package/native/shared/src/renderer/atmosphere_lut.rs +573 -0
  68. package/native/shared/src/renderer/brdf_lut.rs +154 -0
  69. package/native/shared/src/renderer/draw2d.rs +143 -0
  70. package/native/shared/src/renderer/formats.rs +822 -0
  71. package/native/shared/src/renderer/froxel.rs +421 -0
  72. package/native/shared/src/renderer/gi_bake.rs +653 -0
  73. package/native/shared/src/renderer/graph.rs +462 -0
  74. package/native/shared/src/renderer/hiz.rs +269 -0
  75. package/native/shared/src/renderer/hot_reload.rs +390 -0
  76. package/native/shared/src/renderer/impulse_field.rs +456 -0
  77. package/native/shared/src/renderer/lighting.rs +154 -0
  78. package/native/shared/src/renderer/material_instancing.rs +171 -0
  79. package/native/shared/src/renderer/material_pipeline.rs +700 -0
  80. package/native/shared/src/renderer/material_system.rs +1996 -0
  81. package/native/shared/src/renderer/material_system_tests.rs +601 -0
  82. package/native/shared/src/renderer/material_system_wasm.rs +41 -0
  83. package/native/shared/src/renderer/mod.rs +12556 -0
  84. package/native/shared/src/renderer/model_draw.rs +641 -0
  85. package/native/shared/src/renderer/occlusion.rs +429 -0
  86. package/native/shared/src/renderer/planar_pass.rs +593 -0
  87. package/native/shared/src/renderer/planar_reflection.rs +499 -0
  88. package/native/shared/src/renderer/post_pass.rs +249 -0
  89. package/native/shared/src/renderer/postfx_chain.rs +728 -0
  90. package/native/shared/src/renderer/pt_pass.rs +577 -0
  91. package/native/shared/src/renderer/scene_pass.rs +607 -0
  92. package/native/shared/src/renderer/shader_include.rs +205 -0
  93. package/native/shared/src/renderer/shader_library.rs +135 -0
  94. package/native/shared/src/renderer/shaders/ao.rs +570 -0
  95. package/native/shared/src/renderer/shaders/core.rs +1243 -0
  96. package/native/shared/src/renderer/shaders/env.rs +907 -0
  97. package/native/shared/src/renderer/shaders/gi.rs +810 -0
  98. package/native/shared/src/renderer/shaders/mod.rs +19 -0
  99. package/native/shared/src/renderer/shaders/post.rs +1558 -0
  100. package/native/shared/src/renderer/shaders/pt.rs +1859 -0
  101. package/native/shared/src/renderer/shaders/ssgi.rs +1586 -0
  102. package/native/shared/src/renderer/shadow_pass.rs +731 -0
  103. package/native/shared/src/renderer/ssgi_pass.rs +392 -0
  104. package/native/shared/src/renderer/ssr_pass.rs +188 -0
  105. package/native/shared/src/renderer/texture_store.rs +473 -0
  106. package/native/shared/src/renderer/transient.rs +591 -0
  107. package/native/shared/src/renderer/types.rs +941 -0
  108. package/native/shared/src/renderer/util.rs +152 -0
  109. package/native/shared/src/scene.rs +1362 -0
  110. package/native/shared/src/sdf_cache.rs +274 -0
  111. package/native/shared/src/shadows.rs +1036 -0
  112. package/native/shared/src/staging.rs +102 -0
  113. package/native/shared/src/string_header.rs +266 -0
  114. package/native/shared/src/text_renderer.rs +502 -0
  115. package/native/shared/src/textures.rs +197 -0
  116. package/native/tvos/Cargo.lock +1693 -0
  117. package/native/tvos/Cargo.toml +36 -0
  118. package/native/tvos/metal-patched/Cargo.toml +178 -0
  119. package/native/tvos/metal-patched/LICENSE-APACHE +201 -0
  120. package/native/tvos/metal-patched/LICENSE-MIT +25 -0
  121. package/native/tvos/metal-patched/src/acceleration_structure.rs +667 -0
  122. package/native/tvos/metal-patched/src/acceleration_structure_pass.rs +108 -0
  123. package/native/tvos/metal-patched/src/argument.rs +366 -0
  124. package/native/tvos/metal-patched/src/blitpass.rs +102 -0
  125. package/native/tvos/metal-patched/src/buffer.rs +71 -0
  126. package/native/tvos/metal-patched/src/capturedescriptor.rs +76 -0
  127. package/native/tvos/metal-patched/src/capturemanager.rs +113 -0
  128. package/native/tvos/metal-patched/src/commandbuffer.rs +192 -0
  129. package/native/tvos/metal-patched/src/commandqueue.rs +44 -0
  130. package/native/tvos/metal-patched/src/computepass.rs +107 -0
  131. package/native/tvos/metal-patched/src/constants.rs +152 -0
  132. package/native/tvos/metal-patched/src/counters.rs +119 -0
  133. package/native/tvos/metal-patched/src/depthstencil.rs +190 -0
  134. package/native/tvos/metal-patched/src/device.rs +2134 -0
  135. package/native/tvos/metal-patched/src/drawable.rs +39 -0
  136. package/native/tvos/metal-patched/src/encoder.rs +2041 -0
  137. package/native/tvos/metal-patched/src/heap.rs +281 -0
  138. package/native/tvos/metal-patched/src/indirect_encoder.rs +344 -0
  139. package/native/tvos/metal-patched/src/lib.rs +657 -0
  140. package/native/tvos/metal-patched/src/library.rs +902 -0
  141. package/native/tvos/metal-patched/src/mps.rs +575 -0
  142. package/native/tvos/metal-patched/src/pipeline/compute.rs +475 -0
  143. package/native/tvos/metal-patched/src/pipeline/mod.rs +71 -0
  144. package/native/tvos/metal-patched/src/pipeline/render.rs +762 -0
  145. package/native/tvos/metal-patched/src/renderpass.rs +443 -0
  146. package/native/tvos/metal-patched/src/resource.rs +182 -0
  147. package/native/tvos/metal-patched/src/sampler.rs +165 -0
  148. package/native/tvos/metal-patched/src/sync.rs +178 -0
  149. package/native/tvos/metal-patched/src/texture.rs +352 -0
  150. package/native/tvos/metal-patched/src/types.rs +90 -0
  151. package/native/tvos/metal-patched/src/vertexdescriptor.rs +250 -0
  152. package/native/tvos/src/audio_backend.rs +197 -0
  153. package/native/tvos/src/lib.rs +1891 -0
  154. package/native/visionos/Cargo.lock +1693 -0
  155. package/native/visionos/Cargo.toml +40 -0
  156. package/native/visionos/src/audio_backend.rs +197 -0
  157. package/native/visionos/src/lib.rs +1887 -0
  158. package/native/watchos/Cargo.lock +16 -0
  159. package/native/watchos/Cargo.toml +19 -0
  160. package/native/watchos/shaders/bloom_postfx.metal +99 -0
  161. package/native/watchos/src/BloomWatchApp.swift +1267 -0
  162. package/native/watchos/src/BloomWatchAudio.swift +179 -0
  163. package/native/watchos/src/audio.rs +55 -0
  164. package/native/watchos/src/draw_list.rs +229 -0
  165. package/native/watchos/src/ffi_stubs.rs +915 -0
  166. package/native/watchos/src/ffi_stubs_manual.rs +35 -0
  167. package/native/watchos/src/lib.rs +1124 -0
  168. package/native/watchos/src/models.rs +746 -0
  169. package/native/watchos/src/postfx.rs +95 -0
  170. package/native/watchos/src/scene.rs +534 -0
  171. package/native/watchos/src/textures.rs +184 -0
  172. package/native/web/Cargo.lock +1657 -0
  173. package/native/web/Cargo.toml +43 -0
  174. package/native/web/bloom_glue.js +695 -0
  175. package/native/web/build.sh +131 -0
  176. package/native/web/index.html +35 -0
  177. package/native/web/jolt_bridge.js +1519 -0
  178. package/native/web/src/input_ffi.rs +286 -0
  179. package/native/web/src/lib.rs +1796 -0
  180. package/native/web/src/material_ffi.rs +710 -0
  181. package/native/web/src/parity_ffi.rs +343 -0
  182. package/native/web/src/physics_ffi.rs +643 -0
  183. package/native/web/src/ragdoll_ffi.rs +250 -0
  184. package/native/web/src/render_settings.rs +98 -0
  185. package/native/windows/Cargo.lock +1815 -0
  186. package/native/windows/Cargo.toml +68 -0
  187. package/native/windows/src/lib.rs +1486 -0
  188. package/package.json +4279 -0
  189. package/src/audio/index.ts +315 -0
  190. package/src/core/colors.ts +63 -0
  191. package/src/core/index.ts +1206 -0
  192. package/src/core/keys.ts +63 -0
  193. package/src/core/types.ts +104 -0
  194. package/src/index.ts +171 -0
  195. package/src/math/index.ts +516 -0
  196. package/src/mobile/index.ts +294 -0
  197. package/src/models/index.ts +1258 -0
  198. package/src/physics/index.ts +1134 -0
  199. package/src/scene/index.ts +698 -0
  200. package/src/shapes/index.ts +120 -0
  201. package/src/text/index.ts +48 -0
  202. package/src/textures/index.ts +187 -0
  203. package/src/vfx/index.ts +191 -0
  204. package/src/world/index.ts +24 -0
  205. package/src/world/loader.ts +423 -0
  206. package/src/world/prefab.ts +217 -0
  207. package/src/world/render.ts +172 -0
  208. package/src/world/saver.ts +108 -0
  209. package/src/world/serialize.ts +301 -0
  210. package/src/world/terrain.ts +355 -0
  211. package/src/world/types.ts +160 -0
  212. package/src/world/validate.ts +319 -0
  213. package/src/world/version.ts +114 -0
@@ -0,0 +1,429 @@
1
+ //! Hi-Z occlusion culling — coarse max-depth grid with async readback.
2
+ //!
3
+ //! The engine already builds a linear-depth Hi-Z pyramid for SSAO/SSR,
4
+ //! but that chain is min-reduced (nearest depth — what ray marching
5
+ //! wants). Occlusion needs the opposite bound: a node is provably hidden
6
+ //! only if its nearest point is farther than the FARTHEST depth across
7
+ //! its whole screen footprint. So this module adds one small compute
8
+ //! reduce: Hi-Z mip 0 → a 64×64 max-depth grid, copied to a mappable
9
+ //! buffer and read back asynchronously.
10
+ //!
11
+ //! The CPU test runs one frame late (against last frame's grid and last
12
+ //! frame's view-projection) — the standard latency trade that avoids any
13
+ //! GPU stall. Every uncertain case resolves to "visible": no grid yet,
14
+ //! corner behind the near plane, footprint off the captured screen,
15
+ //! depth within the safety margin. Disocclusion artifacts from camera
16
+ //! cuts last exactly one frame.
17
+ //!
18
+ //! `bloom_set_occlusion_culling(0/1)` exposes the kill switch to games;
19
+ //! default on.
20
+
21
+ use wgpu::util::DeviceExt;
22
+
23
+ pub(super) const GRID_W: u32 = 64;
24
+ pub(super) const GRID_H: u32 = 64;
25
+ // 64 texels * 4 bytes = 256 bytes per row — exactly wgpu's
26
+ // COPY_BYTES_PER_ROW_ALIGNMENT, so the readback needs no padding.
27
+ const ROW_BYTES: u32 = GRID_W * 4;
28
+
29
+ const REDUCE_SHADER: &str = "
30
+ struct Params {
31
+ // xy = source (hiz mip0) size, zw = tile size in source texels
32
+ size: vec4<u32>,
33
+ };
34
+ @group(0) @binding(0) var<uniform> u: Params;
35
+ @group(0) @binding(1) var src_tex: texture_2d<f32>;
36
+ @group(0) @binding(2) var dst_tex: texture_storage_2d<r32float, write>;
37
+
38
+ @compute @workgroup_size(8, 8, 1)
39
+ fn cs_main(@builtin(global_invocation_id) gid: vec3<u32>) {
40
+ if (gid.x >= 64u || gid.y >= 64u) { return; }
41
+ let base = vec2<u32>(gid.x * u.size.z, gid.y * u.size.w);
42
+ var m: f32 = 0.0;
43
+ for (var ty: u32 = 0u; ty < u.size.w; ty = ty + 1u) {
44
+ for (var tx: u32 = 0u; tx < u.size.z; tx = tx + 1u) {
45
+ let p = base + vec2<u32>(tx, ty);
46
+ if (p.x < u.size.x && p.y < u.size.y) {
47
+ m = max(m, textureLoad(src_tex, vec2<i32>(p), 0).r);
48
+ }
49
+ }
50
+ }
51
+ textureStore(dst_tex, vec2<i32>(gid.xy), vec4<f32>(m, 0.0, 0.0, 0.0));
52
+ }
53
+ ";
54
+
55
+ struct Readback {
56
+ buffer: wgpu::Buffer,
57
+ /// capture VP for the data this buffer holds
58
+ vp: [[f32; 4]; 4],
59
+ /// copy submitted, map_async issued, result not yet collected
60
+ in_flight: bool,
61
+ map_done: std::sync::Arc<std::sync::atomic::AtomicBool>,
62
+ }
63
+
64
+ pub struct OcclusionCuller {
65
+ pipeline: wgpu::ComputePipeline,
66
+ layout: wgpu::BindGroupLayout,
67
+ uniform: wgpu::Buffer,
68
+ grid_tex: wgpu::Texture,
69
+ grid_view: wgpu::TextureView,
70
+ bg_cache: Option<wgpu::BindGroup>,
71
+ readbacks: [Readback; 2],
72
+ parity: usize,
73
+ /// most recent completed grid
74
+ grid: Vec<f32>,
75
+ grid_valid: bool,
76
+ grid_vp: [[f32; 4]; 4],
77
+ pub enabled: bool,
78
+ /// EN-057 — false when no rasterized scene node exists to consume the
79
+ /// culling verdicts (e.g. a scene whose only nodes are gi_only proxies:
80
+ /// they never draw, so the reduce + readback benefited zero draws every
81
+ /// frame). Set per frame by the engine from the scene graph; defaults to
82
+ /// true so hosts that never call the setter keep today's behaviour.
83
+ has_consumers: bool,
84
+ /// set when record() ran this frame so after_submit() knows to map
85
+ recorded_this_frame: bool,
86
+ }
87
+
88
+ impl OcclusionCuller {
89
+ pub fn new(device: &wgpu::Device) -> Self {
90
+ let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
91
+ label: Some("occlusion_reduce_shader"),
92
+ source: wgpu::ShaderSource::Wgsl(REDUCE_SHADER.into()),
93
+ });
94
+ let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
95
+ label: Some("occlusion_reduce_layout"),
96
+ entries: &[
97
+ wgpu::BindGroupLayoutEntry {
98
+ binding: 0,
99
+ visibility: wgpu::ShaderStages::COMPUTE,
100
+ ty: wgpu::BindingType::Buffer {
101
+ ty: wgpu::BufferBindingType::Uniform,
102
+ has_dynamic_offset: false,
103
+ min_binding_size: None,
104
+ },
105
+ count: None,
106
+ },
107
+ wgpu::BindGroupLayoutEntry {
108
+ binding: 1,
109
+ visibility: wgpu::ShaderStages::COMPUTE,
110
+ ty: wgpu::BindingType::Texture {
111
+ sample_type: wgpu::TextureSampleType::Float { filterable: false },
112
+ view_dimension: wgpu::TextureViewDimension::D2,
113
+ multisampled: false,
114
+ },
115
+ count: None,
116
+ },
117
+ wgpu::BindGroupLayoutEntry {
118
+ binding: 2,
119
+ visibility: wgpu::ShaderStages::COMPUTE,
120
+ ty: wgpu::BindingType::StorageTexture {
121
+ access: wgpu::StorageTextureAccess::WriteOnly,
122
+ format: wgpu::TextureFormat::R32Float,
123
+ view_dimension: wgpu::TextureViewDimension::D2,
124
+ },
125
+ count: None,
126
+ },
127
+ ],
128
+ });
129
+ let pl = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
130
+ label: Some("occlusion_reduce_pl"),
131
+ bind_group_layouts: &[Some(&layout)],
132
+ ..Default::default()
133
+ });
134
+ let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
135
+ label: Some("occlusion_reduce_pipeline"),
136
+ layout: Some(&pl),
137
+ module: &shader,
138
+ entry_point: Some("cs_main"),
139
+ compilation_options: Default::default(),
140
+ cache: None,
141
+ });
142
+ let uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
143
+ label: Some("occlusion_reduce_uniform"),
144
+ contents: &[0u8; 16],
145
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
146
+ });
147
+ let grid_tex = device.create_texture(&wgpu::TextureDescriptor {
148
+ label: Some("occlusion_grid"),
149
+ size: wgpu::Extent3d { width: GRID_W, height: GRID_H, depth_or_array_layers: 1 },
150
+ mip_level_count: 1,
151
+ sample_count: 1,
152
+ dimension: wgpu::TextureDimension::D2,
153
+ format: wgpu::TextureFormat::R32Float,
154
+ usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC,
155
+ view_formats: &[],
156
+ });
157
+ let grid_view = grid_tex.create_view(&wgpu::TextureViewDescriptor::default());
158
+ let mk_readback = || Readback {
159
+ buffer: device.create_buffer(&wgpu::BufferDescriptor {
160
+ label: Some("occlusion_readback"),
161
+ size: (ROW_BYTES * GRID_H) as u64,
162
+ usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
163
+ mapped_at_creation: false,
164
+ }),
165
+ vp: [[0.0; 4]; 4],
166
+ in_flight: false,
167
+ map_done: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
168
+ };
169
+ Self {
170
+ pipeline,
171
+ layout,
172
+ uniform,
173
+ grid_tex,
174
+ grid_view,
175
+ bg_cache: None,
176
+ readbacks: [mk_readback(), mk_readback()],
177
+ parity: 0,
178
+ grid: vec![0.0; (GRID_W * GRID_H) as usize],
179
+ grid_valid: false,
180
+ grid_vp: [[0.0; 4]; 4],
181
+ enabled: true,
182
+ has_consumers: true,
183
+ recorded_this_frame: false,
184
+ }
185
+ }
186
+
187
+ /// EN-057 — tell the culler whether any rasterized consumer exists this
188
+ /// frame. Going consumer-less also invalidates the grid, so if a
189
+ /// consumer appears later the interim frames read the conservative
190
+ /// "potentially visible" answer (test_aabb on an invalid grid) instead
191
+ /// of a stale capture — the gate cannot cost a wrongly-culled draw by
192
+ /// construction.
193
+ pub fn set_has_consumers(&mut self, has: bool) {
194
+ if !has {
195
+ self.grid_valid = false;
196
+ }
197
+ self.has_consumers = has;
198
+ }
199
+
200
+ /// Drop cached bind group (call when the Hi-Z chain is reallocated,
201
+ /// e.g. on resize / render-scale change).
202
+ pub fn invalidate_bindings(&mut self) {
203
+ self.bg_cache = None;
204
+ }
205
+
206
+ /// Collect any finished readback. Call once per frame before culling.
207
+ pub fn poll(&mut self, device: &wgpu::Device) {
208
+ use std::sync::atomic::Ordering;
209
+ // Non-blocking pump so map_async callbacks make progress even on
210
+ // frames where nothing else polls the device.
211
+ let _ = device.poll(wgpu::PollType::Poll);
212
+ for rb in &mut self.readbacks {
213
+ if rb.in_flight && rb.map_done.load(Ordering::Acquire) {
214
+ {
215
+ let n = self.grid.len();
216
+ let view = rb.buffer.slice(..).get_mapped_range();
217
+ let floats: &[f32] = bytemuck::cast_slice(&view);
218
+ self.grid.copy_from_slice(&floats[..n]);
219
+ }
220
+ rb.buffer.unmap();
221
+ rb.in_flight = false;
222
+ rb.map_done.store(false, Ordering::Release);
223
+ self.grid_vp = rb.vp;
224
+ self.grid_valid = true;
225
+ }
226
+ }
227
+ }
228
+
229
+ /// Record the reduce + copy for this frame's grid capture.
230
+ /// `src` is Hi-Z mip 0 (linear |view_z|, sky = 10000) of `src_size`.
231
+ pub fn record(
232
+ &mut self,
233
+ device: &wgpu::Device,
234
+ queue: &wgpu::Queue,
235
+ encoder: &mut wgpu::CommandEncoder,
236
+ src: &wgpu::TextureView,
237
+ src_size: (u32, u32),
238
+ vp: [[f32; 4]; 4],
239
+ ) {
240
+ self.recorded_this_frame = false;
241
+ if !self.enabled || !self.has_consumers {
242
+ return;
243
+ }
244
+ let rb = &mut self.readbacks[self.parity];
245
+ if rb.in_flight {
246
+ // Previous capture still in flight (GPU more than a frame
247
+ // behind) — skip; the grid just stays one frame staler.
248
+ return;
249
+ }
250
+ let tile_w = src_size.0.div_ceil(GRID_W).max(1);
251
+ let tile_h = src_size.1.div_ceil(GRID_H).max(1);
252
+ let params: [u32; 4] = [src_size.0, src_size.1, tile_w, tile_h];
253
+ queue.write_buffer(&self.uniform, 0, bytemuck::cast_slice(&params));
254
+
255
+ if self.bg_cache.is_none() {
256
+ self.bg_cache = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
257
+ label: Some("occlusion_reduce_bg"),
258
+ layout: &self.layout,
259
+ entries: &[
260
+ wgpu::BindGroupEntry { binding: 0, resource: self.uniform.as_entire_binding() },
261
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(src) },
262
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self.grid_view) },
263
+ ],
264
+ }));
265
+ }
266
+ {
267
+ let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
268
+ label: Some("occlusion_reduce_pass"),
269
+ timestamp_writes: None,
270
+ });
271
+ pass.set_pipeline(&self.pipeline);
272
+ pass.set_bind_group(0, self.bg_cache.as_ref().unwrap(), &[]);
273
+ pass.dispatch_workgroups(GRID_W / 8, GRID_H / 8, 1);
274
+ }
275
+ encoder.copy_texture_to_buffer(
276
+ wgpu::TexelCopyTextureInfo {
277
+ texture: &self.grid_tex,
278
+ mip_level: 0,
279
+ origin: wgpu::Origin3d::ZERO,
280
+ aspect: wgpu::TextureAspect::All,
281
+ },
282
+ wgpu::TexelCopyBufferInfo {
283
+ buffer: &rb.buffer,
284
+ layout: wgpu::TexelCopyBufferLayout {
285
+ offset: 0,
286
+ bytes_per_row: Some(ROW_BYTES),
287
+ rows_per_image: Some(GRID_H),
288
+ },
289
+ },
290
+ wgpu::Extent3d { width: GRID_W, height: GRID_H, depth_or_array_layers: 1 },
291
+ );
292
+ rb.vp = vp;
293
+ self.recorded_this_frame = true;
294
+ }
295
+
296
+ /// Issue the async map for the capture recorded this frame. Call
297
+ /// after queue.submit() of the encoder passed to record().
298
+ pub fn after_submit(&mut self) {
299
+ if !self.recorded_this_frame {
300
+ return;
301
+ }
302
+ let rb = &mut self.readbacks[self.parity];
303
+ let done = rb.map_done.clone();
304
+ rb.in_flight = true;
305
+ rb.buffer.slice(..).map_async(wgpu::MapMode::Read, move |res| {
306
+ if res.is_ok() {
307
+ done.store(true, std::sync::atomic::Ordering::Release);
308
+ }
309
+ // On error the buffer stays flagged in-flight until the next
310
+ // successful cycle on the other parity; culling simply keeps
311
+ // using the older grid.
312
+ });
313
+ self.parity = 1 - self.parity;
314
+ self.recorded_this_frame = false;
315
+ }
316
+
317
+ /// Conservative visibility test for a world-space AABB against the
318
+ /// last completed grid. `true` = potentially visible (draw it).
319
+ pub fn test_aabb(&self, wmin: [f32; 3], wmax: [f32; 3]) -> bool {
320
+ if !self.enabled || !self.grid_valid {
321
+ return true;
322
+ }
323
+ let vp = &self.grid_vp;
324
+ let mut uv_min = [f32::MAX, f32::MAX];
325
+ let mut uv_max = [f32::MIN, f32::MIN];
326
+ let mut nearest = f32::MAX;
327
+ for ix in 0..2 {
328
+ for iy in 0..2 {
329
+ for iz in 0..2 {
330
+ let x = if ix == 0 { wmin[0] } else { wmax[0] };
331
+ let y = if iy == 0 { wmin[1] } else { wmax[1] };
332
+ let z = if iz == 0 { wmin[2] } else { wmax[2] };
333
+ let cw = vp[0][3] * x + vp[1][3] * y + vp[2][3] * z + vp[3][3];
334
+ if cw <= 0.05 {
335
+ // corner at/behind the captured near plane —
336
+ // can't bound the footprint; play safe
337
+ return true;
338
+ }
339
+ let cx = vp[0][0] * x + vp[1][0] * y + vp[2][0] * z + vp[3][0];
340
+ let cy = vp[0][1] * x + vp[1][1] * y + vp[2][1] * z + vp[3][1];
341
+ let u = (cx / cw) * 0.5 + 0.5;
342
+ let v = 1.0 - ((cy / cw) * 0.5 + 0.5);
343
+ uv_min[0] = uv_min[0].min(u);
344
+ uv_min[1] = uv_min[1].min(v);
345
+ uv_max[0] = uv_max[0].max(u);
346
+ uv_max[1] = uv_max[1].max(v);
347
+ nearest = nearest.min(cw);
348
+ }
349
+ }
350
+ }
351
+ // Fully outside the captured view → last frame's depth says
352
+ // nothing about it. (Current-frame frustum culling handles
353
+ // actual offscreen-ness.)
354
+ if uv_max[0] <= 0.0 || uv_min[0] >= 1.0 || uv_max[1] <= 0.0 || uv_min[1] >= 1.0 {
355
+ return true;
356
+ }
357
+ // Expand by one texel for footprint conservatism, clamp to grid.
358
+ let tx0 = ((uv_min[0] * GRID_W as f32) as i32 - 1).clamp(0, GRID_W as i32 - 1) as usize;
359
+ let tx1 = ((uv_max[0] * GRID_W as f32) as i32 + 1).clamp(0, GRID_W as i32 - 1) as usize;
360
+ let ty0 = ((uv_min[1] * GRID_H as f32) as i32 - 1).clamp(0, GRID_H as i32 - 1) as usize;
361
+ let ty1 = ((uv_max[1] * GRID_H as f32) as i32 + 1).clamp(0, GRID_H as i32 - 1) as usize;
362
+ let mut grid_max = 0.0f32;
363
+ for ty in ty0..=ty1 {
364
+ for tx in tx0..=tx1 {
365
+ grid_max = grid_max.max(self.grid[ty * GRID_W as usize + tx]);
366
+ }
367
+ }
368
+ // Margin: 2% relative + 0.1 absolute absorbs linearization and
369
+ // one frame of camera motion for typical scenes.
370
+ nearest <= grid_max * 1.02 + 0.1
371
+ }
372
+ }
373
+
374
+ #[cfg(test)]
375
+ mod tests {
376
+ use super::*;
377
+
378
+ fn try_device() -> Option<(wgpu::Device, wgpu::Queue)> {
379
+ let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
380
+ backends: wgpu::Backends::all(),
381
+ ..wgpu::InstanceDescriptor::new_without_display_handle()
382
+ });
383
+ let adapter =
384
+ pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default()))
385
+ .ok()?;
386
+ pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default())).ok()
387
+ }
388
+
389
+ /// VP looking down -Z from the origin in the engine's column-major
390
+ /// convention (vp[col][row]): clip.w = -z, x/y pass through.
391
+ fn look_down_neg_z() -> [[f32; 4]; 4] {
392
+ let mut m = [[0.0f32; 4]; 4];
393
+ m[0][0] = 1.0;
394
+ m[1][1] = 1.0;
395
+ m[2][2] = -1.0;
396
+ m[2][3] = -1.0; // w = -z
397
+ m
398
+ }
399
+
400
+ #[test]
401
+ fn occluded_and_visible_cases() {
402
+ let Some((device, _queue)) = try_device() else {
403
+ eprintln!("skip: no GPU adapter in this environment");
404
+ return;
405
+ };
406
+ let mut c = OcclusionCuller::new(&device);
407
+ // Grid: farthest visible surface everywhere is at depth 10.
408
+ c.grid.fill(10.0);
409
+ c.grid_valid = true;
410
+ c.grid_vp = look_down_neg_z();
411
+
412
+ // Box fully behind that wall (depth 20..21, small footprint).
413
+ assert!(
414
+ !c.test_aabb([-0.1, -0.1, -21.0], [0.1, 0.1, -20.0]),
415
+ "box behind a full-screen depth-10 wall should be culled"
416
+ );
417
+ // Box in front of the wall (depth 5).
418
+ assert!(c.test_aabb([-0.1, -0.1, -5.2], [0.1, 0.1, -5.0]));
419
+ // Box straddling the near plane — must play safe.
420
+ assert!(c.test_aabb([-0.1, -0.1, -20.0], [0.1, 0.1, 1.0]));
421
+ // Without a valid grid, never cull.
422
+ c.grid_valid = false;
423
+ assert!(c.test_aabb([-0.1, -0.1, -21.0], [0.1, 0.1, -20.0]));
424
+ // Disabled culler never culls.
425
+ c.grid_valid = true;
426
+ c.enabled = false;
427
+ assert!(c.test_aabb([-0.1, -0.1, -21.0], [0.1, 0.1, -20.0]));
428
+ }
429
+ }