@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,653 @@
1
+ //! GI bake methods — scene-wide SDF clipmap (binned + sliced amortized
2
+ //! bake), WSRC radiance cascades, and per-mesh SDF drain. Split out of
3
+ //! renderer/mod.rs to keep it under the file-line ceiling (see
4
+ //! tools/check-file-lines.js); pure code move, no behaviour change.
5
+
6
+ use super::*;
7
+
8
+ // EN-054 — cached world-triangle soup for the clipmap rebake. The soup is a
9
+ // pure function of the scene graph (node set, geometry, transforms,
10
+ // visibility), and every one of those bumps `tlas_version` — so a travel-
11
+ // triggered rebake only needs to RE-BIN against its new origin, not re-gather
12
+ // the whole scene. Before this, every ~10 m of camera travel re-transformed
13
+ // every vertex of every node and allocated two multi-MB Vecs on the frame
14
+ // that starts the rebake (measured 2–9 ms gather per event on the shooter's
15
+ // 203k-triangle scene, on top of the origin-dependent binning that stays).
16
+ // Module-local static, same pattern as staging.rs's stores — the ratcheted
17
+ // renderer/mod.rs gains no field.
18
+ struct SdfTriCache {
19
+ version: u64,
20
+ vertices: Vec<f32>,
21
+ indices: Vec<u32>,
22
+ tri_count: u32,
23
+ }
24
+ fn sdf_tri_cache() -> &'static std::sync::Mutex<Option<SdfTriCache>> {
25
+ static INSTANCE: std::sync::OnceLock<std::sync::Mutex<Option<SdfTriCache>>> =
26
+ std::sync::OnceLock::new();
27
+ INSTANCE.get_or_init(|| std::sync::Mutex::new(None))
28
+ }
29
+
30
+ impl Renderer {
31
+ /// Ticket 014 V2 — bake the scene-wide SDF clipmap once, on the
32
+ /// frame when all per-mesh queues (BLAS, cards, per-mesh SDFs)
33
+ /// have drained. Gathers every visible mesh's triangles into a
34
+ /// world-space buffer via `scene.build_world_triangles()` and
35
+ /// runs `SDF_BAKE_WGSL` against the unified data with the
36
+ /// clipmap's fixed world-space AABB. 64³ voxel × scene triangle
37
+ /// count = expensive one-shot (~100-200 ms on Sponza), but
38
+ /// happens after a visible frame and never repeats for static
39
+ /// scenes.
40
+ /// Ticket 014 V5 — camera world-space position. Uses
41
+ /// `current_camera_pos`, which `begin_mode_3d` writes every frame
42
+ /// from the user-supplied camera position (cheaper than inverting
43
+ /// the view matrix and always in sync with what the game sees).
44
+ pub(super) fn current_camera_world_pos(&self) -> [f32; 3] {
45
+ self.current_camera_pos
46
+ }
47
+
48
+ /// Ticket 014 V5 — flag the SDF clipmap for a re-bake if the camera
49
+ /// has moved past the rebake threshold from the current clipmap
50
+ /// centre. Fullscreen-lag fix: instead of clearing `built` (which
51
+ /// used to fire a full-volume single-dispatch rebake that stalled
52
+ /// weak GPUs for seconds), this only raises `rebake_needed`; the
53
+ /// live clipmap keeps serving traces while the amortized job bakes
54
+ /// the re-centred volume a few Z-slices per frame.
55
+ pub(super) fn maybe_invalidate_sdf_clipmap(&mut self) {
56
+ // A job in flight already re-centres on its own origin — let it
57
+ // land before measuring drift again.
58
+ if !self.scene_sdf_clipmap_built || self.sdf_clipmap_job.is_some() {
59
+ return;
60
+ }
61
+ let cam = self.current_camera_world_pos();
62
+ let dx = cam[0] - self.scene_sdf_clipmap_origin[0];
63
+ let dy = cam[1] - self.scene_sdf_clipmap_origin[1];
64
+ let dz = cam[2] - self.scene_sdf_clipmap_origin[2];
65
+ let dist_sq = dx * dx + dy * dy + dz * dz;
66
+ let threshold = SCENE_SDF_CLIPMAP_EXTENT * SCENE_SDF_CLIPMAP_REBAKE_THRESHOLD;
67
+ if dist_sq > threshold * threshold {
68
+ self.scene_sdf_clipmap_rebake_needed = true;
69
+ }
70
+ }
71
+
72
+ pub(super) fn bake_scene_sdf_clipmap(
73
+ &mut self,
74
+ scene: &crate::scene::SceneGraph,
75
+ encoder: &mut wgpu::CommandEncoder,
76
+ ) {
77
+ // Continue an in-flight job first: one slice batch per frame.
78
+ if let Some(job) = self.sdf_clipmap_job.take() {
79
+ self.encode_clipmap_bake_slices(job, encoder);
80
+ return;
81
+ }
82
+ if !self.scene_sdf_clipmap_rebake_needed {
83
+ return;
84
+ }
85
+ // Wait for all per-mesh queues to drain — builds the clipmap
86
+ // from a fully-loaded scene rather than a partial one, and
87
+ // keeps first-frame cost spread across the card/BLAS work
88
+ // already scheduled.
89
+ if !scene.pending_blas_builds.is_empty()
90
+ || !scene.pending_card_captures.is_empty()
91
+ || !scene.pending_sdf_bakes.is_empty()
92
+ {
93
+ return;
94
+ }
95
+
96
+ let mut cache_guard = sdf_tri_cache().lock().unwrap();
97
+ let stale = match &*cache_guard {
98
+ Some(c) => c.version != scene.tlas_version,
99
+ None => true,
100
+ };
101
+ if stale {
102
+ let (v, i, n) = scene.build_world_triangles();
103
+ *cache_guard = Some(SdfTriCache {
104
+ version: scene.tlas_version,
105
+ vertices: v,
106
+ indices: i,
107
+ tri_count: n,
108
+ });
109
+ }
110
+ let cached = cache_guard.as_ref().unwrap();
111
+ let vertices = &cached.vertices;
112
+ let indices = &cached.indices;
113
+ let tri_count = cached.tri_count;
114
+ if tri_count == 0 {
115
+ return;
116
+ }
117
+
118
+ // V5 — centre the clipmap on the current camera position,
119
+ // voxel-snapped for sampling stability (sub-voxel shifts
120
+ // would change which voxel each sphere-trace step reads).
121
+ // The live origin flips only when the job completes.
122
+ let half = SCENE_SDF_CLIPMAP_EXTENT * 0.5;
123
+ let voxel = SCENE_SDF_CLIPMAP_EXTENT / SCENE_SDF_CLIPMAP_RES as f32;
124
+ let cam = self.current_camera_world_pos();
125
+ let origin = [
126
+ (cam[0] / voxel).round() * voxel,
127
+ (cam[1] / voxel).round() * voxel,
128
+ (cam[2] / voxel).round() * voxel,
129
+ ];
130
+ let aabb_min = [origin[0] - half, origin[1] - half, origin[2] - half, 0.0];
131
+ let aabb_max = [origin[0] + half, origin[1] + half, origin[2] + half, 0.0];
132
+
133
+ // Bin triangles into BIN_CELLS³ cells, each list expanded by one
134
+ // cell (the shader's narrow band) so the per-cell clamp stays a
135
+ // conservative lower bound for sphere tracing. Two-pass counting
136
+ // sort: count, prefix-sum, fill.
137
+ let cells = SCENE_SDF_CLIPMAP_BIN_CELLS as usize;
138
+ let cell_size = SCENE_SDF_CLIPMAP_EXTENT / cells as f32;
139
+ let grid_min = [aabb_min[0], aabb_min[1], aabb_min[2]];
140
+ let cell_range = |tri: usize| -> Option<([usize; 3], [usize; 3])> {
141
+ let i0 = indices[tri * 3] as usize * 12;
142
+ let i1 = indices[tri * 3 + 1] as usize * 12;
143
+ let i2 = indices[tri * 3 + 2] as usize * 12;
144
+ let mut lo = [f32::MAX; 3];
145
+ let mut hi = [f32::MIN; 3];
146
+ for base in [i0, i1, i2] {
147
+ for a in 0..3 {
148
+ let v = vertices[base + a];
149
+ lo[a] = lo[a].min(v);
150
+ hi[a] = hi[a].max(v);
151
+ }
152
+ }
153
+ let mut c_lo = [0usize; 3];
154
+ let mut c_hi = [0usize; 3];
155
+ for a in 0..3 {
156
+ // Expand by one cell width (= the shader's band).
157
+ let lo_c = ((lo[a] - cell_size - grid_min[a]) / cell_size).floor();
158
+ let hi_c = ((hi[a] + cell_size - grid_min[a]) / cell_size).floor();
159
+ if hi_c < 0.0 || lo_c >= cells as f32 {
160
+ return None; // entirely outside the clipmap volume
161
+ }
162
+ c_lo[a] = lo_c.max(0.0) as usize;
163
+ c_hi[a] = hi_c.min(cells as f32 - 1.0) as usize;
164
+ }
165
+ Some((c_lo, c_hi))
166
+ };
167
+ let cell_count = cells * cells * cells;
168
+ let mut counts = vec![0u32; cell_count];
169
+ for t in 0..tri_count as usize {
170
+ if let Some((lo, hi)) = cell_range(t) {
171
+ for z in lo[2]..=hi[2] {
172
+ for y in lo[1]..=hi[1] {
173
+ for x in lo[0]..=hi[0] {
174
+ counts[(z * cells + y) * cells + x] += 1;
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ let mut offsets = vec![0u32; cell_count + 1];
181
+ for i in 0..cell_count {
182
+ offsets[i + 1] = offsets[i] + counts[i];
183
+ }
184
+ let total_refs = offsets[cell_count] as usize;
185
+ let mut cursor: Vec<u32> = offsets[..cell_count].to_vec();
186
+ // wgpu rejects zero-sized buffers — keep one dummy entry when no
187
+ // triangle touches the volume (all cells then read empty ranges).
188
+ let mut tri_refs = vec![0u32; total_refs.max(1)];
189
+ for t in 0..tri_count as usize {
190
+ if let Some((lo, hi)) = cell_range(t) {
191
+ for z in lo[2]..=hi[2] {
192
+ for y in lo[1]..=hi[1] {
193
+ for x in lo[0]..=hi[0] {
194
+ let ci = (z * cells + y) * cells + x;
195
+ tri_refs[cursor[ci] as usize] = t as u32;
196
+ cursor[ci] += 1;
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ let vbuf = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
204
+ label: Some("scene_sdf_bake_vbuf"),
205
+ contents: bytemuck::cast_slice(vertices.as_slice()),
206
+ usage: wgpu::BufferUsages::STORAGE,
207
+ });
208
+ let ibuf = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
209
+ label: Some("scene_sdf_bake_ibuf"),
210
+ contents: bytemuck::cast_slice(indices.as_slice()),
211
+ usage: wgpu::BufferUsages::STORAGE,
212
+ });
213
+ let cell_offsets_buf = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
214
+ label: Some("scene_sdf_bake_cell_offsets"),
215
+ contents: bytemuck::cast_slice(&offsets),
216
+ usage: wgpu::BufferUsages::STORAGE,
217
+ });
218
+ let cell_tris_buf = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
219
+ label: Some("scene_sdf_bake_cell_tris"),
220
+ contents: bytemuck::cast_slice(&tri_refs),
221
+ usage: wgpu::BufferUsages::STORAGE,
222
+ });
223
+ // Per-job uniform: sharing sdf_bake_uniform would alias with the
224
+ // per-mesh bakes — queue.write_buffer applies before any of this
225
+ // frame's commands, so the last write would win for every pass.
226
+ let uniform = self.device.create_buffer(&wgpu::BufferDescriptor {
227
+ label: Some("scene_sdf_clipmap_bake_uniform"),
228
+ size: std::mem::size_of::<SdfBakeParams>() as u64,
229
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
230
+ mapped_at_creation: false,
231
+ });
232
+ let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
233
+ label: Some("scene_sdf_clipmap_bake_bg"),
234
+ layout: &self.sdf_clipmap_bake_layout,
235
+ entries: &[
236
+ wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() },
237
+ wgpu::BindGroupEntry { binding: 1, resource: vbuf.as_entire_binding() },
238
+ wgpu::BindGroupEntry { binding: 2, resource: ibuf.as_entire_binding() },
239
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.scene_sdf_clipmap_staging_view) },
240
+ wgpu::BindGroupEntry { binding: 4, resource: cell_offsets_buf.as_entire_binding() },
241
+ wgpu::BindGroupEntry { binding: 5, resource: cell_tris_buf.as_entire_binding() },
242
+ ],
243
+ });
244
+
245
+ self.scene_sdf_clipmap_rebake_needed = false;
246
+ let job = SdfClipmapBakeJob {
247
+ origin,
248
+ aabb_min,
249
+ aabb_max,
250
+ uniform,
251
+ bind_group,
252
+ next_z: 0,
253
+ };
254
+ // Encode the first slice batch right away so a full rebake takes
255
+ // exactly RES / LAYERS_PER_FRAME frames end to end.
256
+ self.encode_clipmap_bake_slices(job, encoder);
257
+ }
258
+
259
+ /// Fullscreen-lag fix — encode this frame's slice batch of the
260
+ /// in-flight clipmap bake. On the final batch, copy the staging
261
+ /// volume over the live clipmap and flip the origin — the copy is
262
+ /// encoded before this frame's probe traces, so the swap is atomic
263
+ /// from the tracer's point of view.
264
+ pub(super) fn encode_clipmap_bake_slices(
265
+ &mut self,
266
+ mut job: SdfClipmapBakeJob,
267
+ encoder: &mut wgpu::CommandEncoder,
268
+ ) {
269
+ let res = SCENE_SDF_CLIPMAP_RES;
270
+ let layers = SCENE_SDF_CLIPMAP_LAYERS_PER_FRAME.min(res - job.next_z);
271
+ let params = SdfBakeParams {
272
+ aabb_min: job.aabb_min,
273
+ aabb_max: job.aabb_max,
274
+ counts: [SCENE_SDF_CLIPMAP_BIN_CELLS, res, job.next_z, 0],
275
+ };
276
+ self.queue.write_buffer(&job.uniform, 0, bytemuck::bytes_of(&params));
277
+
278
+ let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
279
+ label: Some("scene_sdf_clipmap_bake_slice"),
280
+ timestamp_writes: None,
281
+ });
282
+ pass.set_pipeline(&self.sdf_clipmap_bake_pipeline);
283
+ pass.set_bind_group(0, &job.bind_group, &[]);
284
+ pass.dispatch_workgroups(res / 4, res / 4, layers / 4);
285
+ drop(pass);
286
+
287
+ job.next_z += layers;
288
+ if job.next_z >= res {
289
+ encoder.copy_texture_to_texture(
290
+ self.scene_sdf_clipmap_staging_tex.as_image_copy(),
291
+ self.scene_sdf_clipmap_tex.as_image_copy(),
292
+ wgpu::Extent3d {
293
+ width: res,
294
+ height: res,
295
+ depth_or_array_layers: res,
296
+ },
297
+ );
298
+ self.scene_sdf_clipmap_origin = job.origin;
299
+ self.scene_sdf_clipmap_built = true;
300
+ } else {
301
+ self.sdf_clipmap_job = Some(job);
302
+ }
303
+ }
304
+
305
+ /// Ticket 014 V6/V7/V12/V13 — invalidate WSRC cascades on
306
+ /// camera travel OR meaningful lighting change. V13 runs the
307
+ /// V12 hysteresis checks per cascade, so a sun rotation or
308
+ /// camera shift only rebakes the affected cascade(s). Typical
309
+ /// pattern: camera moves 10 m → near cascade (1.875 m cell,
310
+ /// ~0.47 m threshold) rebakes every few frames, mid cascade
311
+ /// (7.5 m cell, ~1.9 m threshold) rebakes occasionally, far
312
+ /// cascade (31 m cell, ~7.8 m threshold) stays cached for
313
+ /// much longer.
314
+ pub(super) fn maybe_invalidate_wsrc(&mut self) {
315
+ let cam = self.current_camera_world_pos();
316
+ let ld = self.lighting_uniforms.light_dir;
317
+ let lc = self.lighting_uniforms.light_color;
318
+ let amb = self.lighting_uniforms.ambient;
319
+ let cur_sun_color = [lc[0] * ld[3], lc[1] * ld[3], lc[2] * ld[3]];
320
+ let cur_sky_color = [amb[0] * amb[3], amb[1] * amb[3], amb[2] * amb[3]];
321
+
322
+ fn luma(c: [f32; 3]) -> f32 {
323
+ c[0] * 0.2126 + c[1] * 0.7152 + c[2] * 0.0722
324
+ }
325
+ fn rel_diff(a: f32, b: f32) -> f32 {
326
+ (a - b).abs() / a.max(b).max(1e-4)
327
+ }
328
+
329
+ for c in 0..WSRC_CASCADE_COUNT as usize {
330
+ if !self.wsrc_built[c] {
331
+ continue;
332
+ }
333
+ // Camera travel — per-cascade threshold scales with the
334
+ // cascade's extent, so each cascade has its own
335
+ // "moved enough" metric.
336
+ let extent = WSRC_CASCADE_EXTENTS[c];
337
+ let origin = self.wsrc_origin[c];
338
+ let dx = cam[0] - origin[0];
339
+ let dy = cam[1] - origin[1];
340
+ let dz = cam[2] - origin[2];
341
+ let dist_sq = dx * dx + dy * dy + dz * dz;
342
+ let threshold = extent * WSRC_REBAKE_THRESHOLD;
343
+ if dist_sq > threshold * threshold {
344
+ self.wsrc_built[c] = false;
345
+ continue;
346
+ }
347
+
348
+ // V12 hysteresis — angular sun + 5% relative luma.
349
+ let last = self.wsrc_last_sun_dir[c];
350
+ let sun_dot = ld[0] * last[0] + ld[1] * last[1] + ld[2] * last[2];
351
+ if sun_dot < 0.99985 {
352
+ self.wsrc_built[c] = false;
353
+ continue;
354
+ }
355
+ if rel_diff(luma(cur_sun_color), luma(self.wsrc_last_sun_color[c])) > 0.05 {
356
+ self.wsrc_built[c] = false;
357
+ continue;
358
+ }
359
+ if rel_diff(luma(cur_sky_color), luma(self.wsrc_last_sky_color[c])) > 0.05 {
360
+ self.wsrc_built[c] = false;
361
+ }
362
+ }
363
+ }
364
+
365
+ /// Ticket 014 V6 — bake the world-space radiance cache. One
366
+ /// dispatch covers all `WSRC_GRID_RES³` probes × 64 octel texels.
367
+ /// Cheap: per-texel work is one shadow-cascade lookup + analytic
368
+ /// sun/sky math, roughly matching a single card-lighting pixel.
369
+ /// Runs at most once per `WSRC_REBAKE_THRESHOLD × extent` of
370
+ /// camera travel — same amortisation pattern as the clipmap.
371
+ pub(super) fn bake_wsrc(
372
+ &mut self,
373
+ encoder: &mut wgpu::CommandEncoder,
374
+ ) {
375
+ // V13 — bake only cascades that are marked not-built. Each
376
+ // cascade snaps to its own cell grid (cell = extent / 16)
377
+ // and writes into its own 16-slice block of the shared
378
+ // atlas.
379
+ if self.wsrc_built.iter().all(|b| *b) {
380
+ return;
381
+ }
382
+
383
+ // V14 — pick the HW ray-traced bake when the adapter has
384
+ // ray-query AND the TLAS is ready. The SW path stays the
385
+ // fallback for non-RT adapters and for the early frames
386
+ // before BLAS / TLAS have been built.
387
+ let use_hw = self.hw_rt_enabled
388
+ && self.wsrc_bake_hw_pipeline.is_some()
389
+ && self.tlas.is_some()
390
+ && self.tlas_instance_data_buffer.is_some();
391
+
392
+ // Resolve a single set of lighting params — they're the same
393
+ // across all cascades in one frame. Per-cascade differences
394
+ // come from the origin + extent passed through the uniform.
395
+ let ld = self.lighting_uniforms.light_dir;
396
+ let inv_len = 1.0 / (ld[0]*ld[0] + ld[1]*ld[1] + ld[2]*ld[2]).sqrt().max(1e-4);
397
+ let sun_dir_ws = [-ld[0]*inv_len, -ld[1]*inv_len, -ld[2]*inv_len, ld[3]];
398
+ let lc = self.lighting_uniforms.light_color;
399
+ let sun_intensity = ld[3].max(0.0);
400
+ let sun_color = [
401
+ lc[0] * sun_intensity,
402
+ lc[1] * sun_intensity,
403
+ lc[2] * sun_intensity,
404
+ 0.0,
405
+ ];
406
+ let amb = self.lighting_uniforms.ambient;
407
+ let sky_intensity = amb[3].max(0.0);
408
+ let sky_color = [
409
+ amb[0] * sky_intensity,
410
+ amb[1] * sky_intensity,
411
+ amb[2] * sky_intensity,
412
+ 0.0,
413
+ ];
414
+
415
+ let shadows_enabled = self.shadow_map.enabled;
416
+ let shadow_vps: [[[f32; 4]; 4]; 3] = if shadows_enabled {
417
+ self.shadow_map.light_vps
418
+ } else {
419
+ [IDENTITY_MAT4; 3]
420
+ };
421
+ let shadow_splits = if shadows_enabled {
422
+ let s = self.shadow_map.cascade_splits;
423
+ [s[0], s[1], s[2], 0.0]
424
+ } else {
425
+ [f32::INFINITY, f32::INFINITY, f32::INFINITY, 0.0]
426
+ };
427
+
428
+ // Lazy-build whichever bind group the selected path needs.
429
+ // The two caches are independent — switching between paths
430
+ // (e.g. if TLAS becomes available mid-session) is fine.
431
+ if use_hw {
432
+ if self.wsrc_bake_hw_bg_cache.is_none() {
433
+ let tlas = self.tlas.as_ref().unwrap();
434
+ let instance_buf = self.tlas_instance_data_buffer.as_ref().unwrap();
435
+ self.wsrc_bake_hw_bg_cache = Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
436
+ label: Some("wsrc_bake_hw_bg"),
437
+ layout: self.wsrc_bake_hw_layout.as_ref().unwrap(),
438
+ entries: &[
439
+ wgpu::BindGroupEntry { binding: 0, resource: self.wsrc_bake_uniform.as_entire_binding() },
440
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[0]) },
441
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[1]) },
442
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[2]) },
443
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(&self.shadow_map.sampler) },
444
+ wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(&self.wsrc_atlas_view) },
445
+ wgpu::BindGroupEntry { binding: 6, resource: tlas.as_binding() },
446
+ wgpu::BindGroupEntry { binding: 7, resource: instance_buf.as_entire_binding() },
447
+ wgpu::BindGroupEntry { binding: 8, resource: wgpu::BindingResource::TextureView(&self.mesh_card_radiance_view) },
448
+ wgpu::BindGroupEntry { binding: 9, resource: wgpu::BindingResource::Sampler(&self.mesh_card_atlas_sampler) },
449
+ ],
450
+ }));
451
+ }
452
+ } else if self.wsrc_bake_bg_cache.is_none() {
453
+ self.wsrc_bake_bg_cache = Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
454
+ label: Some("wsrc_bake_bg"),
455
+ layout: &self.wsrc_bake_layout,
456
+ entries: &[
457
+ wgpu::BindGroupEntry { binding: 0, resource: self.wsrc_bake_uniform.as_entire_binding() },
458
+ wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[0]) },
459
+ wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[1]) },
460
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&self.shadow_map.depth_views[2]) },
461
+ wgpu::BindGroupEntry { binding: 4, resource: wgpu::BindingResource::Sampler(&self.shadow_map.sampler) },
462
+ wgpu::BindGroupEntry { binding: 5, resource: wgpu::BindingResource::TextureView(&self.wsrc_atlas_view) },
463
+ ],
464
+ }));
465
+ }
466
+
467
+ let cam = self.current_camera_world_pos();
468
+
469
+ // At most ONE cascade per frame. Besides amortizing the cost,
470
+ // this fixes a params-aliasing bug: all cascades share
471
+ // `wsrc_bake_uniform`, and `queue.write_buffer` applies every
472
+ // write before any of this frame's dispatches execute — baking
473
+ // two cascades in one frame made both dispatches read the last
474
+ // cascade's params (wrong extent + wrong atlas slice flag).
475
+ let mut baked_one = false;
476
+ for c in 0..WSRC_CASCADE_COUNT as usize {
477
+ if self.wsrc_built[c] || baked_one {
478
+ continue;
479
+ }
480
+ let extent = WSRC_CASCADE_EXTENTS[c];
481
+ let cell = extent / WSRC_GRID_RES as f32;
482
+ let origin = [
483
+ (cam[0] / cell).round() * cell,
484
+ (cam[1] / cell).round() * cell,
485
+ (cam[2] / cell).round() * cell,
486
+ ];
487
+ self.wsrc_origin[c] = origin;
488
+
489
+ let params = WsrcBakeParams {
490
+ sun_dir: sun_dir_ws,
491
+ sun_color,
492
+ sky_color,
493
+ grid: [origin[0], origin[1], origin[2], extent],
494
+ shadow_vps,
495
+ shadow_splits,
496
+ flags: [
497
+ 0.002,
498
+ if shadows_enabled { 1.0 } else { 0.0 },
499
+ c as f32,
500
+ 0.0,
501
+ ],
502
+ ground_albedo: [
503
+ self.gi_scene_avg_albedo[0],
504
+ self.gi_scene_avg_albedo[1],
505
+ self.gi_scene_avg_albedo[2],
506
+ 0.0,
507
+ ],
508
+ };
509
+ self.queue.write_buffer(
510
+ &self.wsrc_bake_uniform,
511
+ 0,
512
+ bytemuck::bytes_of(&params),
513
+ );
514
+
515
+ let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
516
+ label: Some(if use_hw { "wsrc_bake_hw_pass" } else { "wsrc_bake_pass" }),
517
+ timestamp_writes: None,
518
+ });
519
+ if use_hw {
520
+ pass.set_pipeline(self.wsrc_bake_hw_pipeline.as_ref().unwrap());
521
+ pass.set_bind_group(0, self.wsrc_bake_hw_bg_cache.as_ref().unwrap(), &[]);
522
+ } else {
523
+ pass.set_pipeline(&self.wsrc_bake_pipeline);
524
+ pass.set_bind_group(0, self.wsrc_bake_bg_cache.as_ref().unwrap(), &[]);
525
+ }
526
+ // One workgroup per probe in this cascade (16³),
527
+ // 10×10 threads per workgroup (padded octel).
528
+ pass.dispatch_workgroups(WSRC_GRID_RES, WSRC_GRID_RES, WSRC_GRID_RES);
529
+ drop(pass);
530
+
531
+ self.wsrc_built[c] = true;
532
+ self.wsrc_last_sun_dir[c] = ld;
533
+ self.wsrc_last_sun_color[c] = [sun_color[0], sun_color[1], sun_color[2]];
534
+ self.wsrc_last_sky_color[c] = [sky_color[0], sky_color[1], sky_color[2]];
535
+ baked_one = true;
536
+ }
537
+ }
538
+
539
+ /// Ticket 014 V1 — bake per-mesh unsigned distance fields via
540
+ /// the compute pipeline. Drains `scene.pending_sdf_bakes` with a
541
+ /// per-frame budget; expensive workload (O(voxels × triangles)
542
+ /// per mesh), so the rate-limit keeps first-frame stutter
543
+ /// bounded. Static scenes amortise and never re-bake.
544
+ ///
545
+ /// Ticket 022 — after each dispatch, encode a copy_texture_to_buffer
546
+ /// against a fresh staging buffer and stash (hash, buffer) on
547
+ /// `sdf_cache_writes`. The frame's main submit picks up the copies
548
+ /// alongside the bake; `flush_sdf_cache_writes` then maps and
549
+ /// persists each buffer to the on-disk cache so the next launch
550
+ /// hits the load path in scene.rs and skips the bake entirely.
551
+ pub(super) fn bake_pending_sdfs(
552
+ &mut self,
553
+ scene: &mut crate::scene::SceneGraph,
554
+ encoder: &mut wgpu::CommandEncoder,
555
+ ) {
556
+ if !self.hw_rt_enabled || scene.pending_sdf_bakes.is_empty() {
557
+ return;
558
+ }
559
+ const SDF_BAKE_MAX_PER_FRAME: usize = 8;
560
+ let take = scene.pending_sdf_bakes.len().min(SDF_BAKE_MAX_PER_FRAME);
561
+ let pending: Vec<f64> = scene.pending_sdf_bakes.drain(..take).collect();
562
+
563
+ for handle in pending {
564
+ let (sdf_tex, sdf_view, vb_ptr, ib_ptr, bmin, bmax, index_count, mesh_hash) = {
565
+ let Some(node) = scene.nodes.get(handle) else { continue; };
566
+ let Some(sdf_tex) = node.mesh_sdf.as_ref() else { continue; };
567
+ let Some(sdf_view) = node.mesh_sdf_view.as_ref() else { continue; };
568
+ let Some(vb) = node.gpu_vb.as_ref() else { continue; };
569
+ let Some(ib) = node.gpu_ib.as_ref() else { continue; };
570
+ (
571
+ sdf_tex.clone(),
572
+ sdf_view.clone(),
573
+ vb.clone(),
574
+ ib.clone(),
575
+ node.bounds_min,
576
+ node.bounds_max,
577
+ node.gpu_index_count,
578
+ node.mesh_hash,
579
+ )
580
+ };
581
+ if index_count == 0 {
582
+ continue;
583
+ }
584
+ let tri_count = index_count / 3;
585
+ let params = SdfBakeParams {
586
+ aabb_min: [bmin[0], bmin[1], bmin[2], 0.0],
587
+ aabb_max: [bmax[0], bmax[1], bmax[2], 0.0],
588
+ counts: [tri_count, MESH_SDF_RES, 0, 0],
589
+ };
590
+ self.queue.write_buffer(
591
+ &self.sdf_bake_uniform,
592
+ 0,
593
+ bytemuck::bytes_of(&params),
594
+ );
595
+ let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
596
+ label: Some("sdf_bake_bg"),
597
+ layout: &self.sdf_bake_layout,
598
+ entries: &[
599
+ wgpu::BindGroupEntry { binding: 0, resource: self.sdf_bake_uniform.as_entire_binding() },
600
+ wgpu::BindGroupEntry { binding: 1, resource: vb_ptr.as_entire_binding() },
601
+ wgpu::BindGroupEntry { binding: 2, resource: ib_ptr.as_entire_binding() },
602
+ wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&sdf_view) },
603
+ ],
604
+ });
605
+ {
606
+ let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
607
+ label: Some("sdf_bake_pass"),
608
+ timestamp_writes: None,
609
+ });
610
+ pass.set_pipeline(&self.sdf_bake_pipeline);
611
+ pass.set_bind_group(0, &bg, &[]);
612
+ pass.dispatch_workgroups(MESH_SDF_RES / 4, MESH_SDF_RES / 4, MESH_SDF_RES / 4);
613
+ }
614
+
615
+ // Ticket 022 — schedule a readback against the freshly-baked
616
+ // texture so the next launch can skip the bake. We only do
617
+ // this when scene.rs computed a hash (it always does, but
618
+ // skip defensively); padded staging size is bound by
619
+ // wgpu's COPY_BYTES_PER_ROW alignment.
620
+ if let Some(hash) = mesh_hash {
621
+ let row_padded = ((MESH_SDF_RES * 4 + 255) & !255) as u64;
622
+ let staging = self.device.create_buffer(&wgpu::BufferDescriptor {
623
+ label: Some("sdf_cache_readback"),
624
+ size: row_padded * (MESH_SDF_RES as u64) * (MESH_SDF_RES as u64),
625
+ usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
626
+ mapped_at_creation: false,
627
+ });
628
+ encoder.copy_texture_to_buffer(
629
+ wgpu::TexelCopyTextureInfo {
630
+ texture: &sdf_tex,
631
+ mip_level: 0,
632
+ origin: wgpu::Origin3d::ZERO,
633
+ aspect: wgpu::TextureAspect::All,
634
+ },
635
+ wgpu::TexelCopyBufferInfo {
636
+ buffer: &staging,
637
+ layout: wgpu::TexelCopyBufferLayout {
638
+ offset: 0,
639
+ bytes_per_row: Some(row_padded as u32),
640
+ rows_per_image: Some(MESH_SDF_RES),
641
+ },
642
+ },
643
+ wgpu::Extent3d {
644
+ width: MESH_SDF_RES,
645
+ height: MESH_SDF_RES,
646
+ depth_or_array_layers: MESH_SDF_RES,
647
+ },
648
+ );
649
+ self.sdf_cache_writes.push((hash, staging));
650
+ }
651
+ }
652
+ }
653
+ }