@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,607 @@
1
+ //! The HDR scene pass: sky-view LUT refresh, sky + immediate-mode 3D +
2
+ //! retained scene graph rendered into the linear-HDR MRT set (HDR +
3
+ //! material + velocity + albedo + depth), followed by the opaque
4
+ //! material pass running on the inner render graph. Split from
5
+ //! end_frame_with_scene (2000-line file policy + render-graph
6
+ //! migration).
7
+
8
+ use super::*;
9
+
10
+ impl Renderer {
11
+ pub(super) fn record_hdr_scene_pass(
12
+ &mut self,
13
+ encoder: &mut wgpu::CommandEncoder,
14
+ profiler: &mut crate::profiler::Profiler,
15
+ scene: &mut crate::scene::SceneGraph,
16
+ ) {
17
+ // Rebind: the immediate-mode 3D upload just before this call
18
+ // checks the same predicate; vertices_3d is untouched between.
19
+ let has_3d = !self.vertices_3d.is_empty();
20
+ // ============================================================
21
+ // HDR pass: sky + 3D + scene → linear HDR offscreen RT.
22
+ // ============================================================
23
+ // The composite-tonemap pass downstream reads this RT and
24
+ // writes the final image to the sRGB surface. Keeping the
25
+ // intermediate radiance in HDR sets up a future bloom pass
26
+ // and means tonemap + sRGB encode happen exactly once, in
27
+ // one place.
28
+ // EN-005 Phase 2 — refresh the sky-view LUT before the HDR
29
+ // pass opens. The compute dispatch can't be nested inside a
30
+ // render pass, and `maybe_update_sky_view_lut` is a no-op
31
+ // unless the sun (or atmosphere knobs) actually changed.
32
+ // EN-005 V2 — also re-bake the aerial-perspective volume,
33
+ // which must happen every frame because the camera moves.
34
+ if self.procedural_sky_enabled {
35
+ self.maybe_update_sky_view_lut();
36
+ self.dispatch_aerial_perspective_lut();
37
+ }
38
+
39
+ // EN-044 — DEPTH PREPASS over the cached-model draws.
40
+ //
41
+ // The scene fragment shader can `discard` (alpha-cutout foliage), and a shader
42
+ // that may discard cannot early-Z *write*: the GPU must run it in full before it
43
+ // knows whether the pixel survives. So an 88-tree forest of overlapping leaf
44
+ // cards shaded the whole 5-target MRT several layers deep and threw most of it
45
+ // away. Measured: the forest alone was 5.6 ms of a 7.4 ms main_hdr_pass, and
46
+ // dropping it took the title screen from 46.7 fps to the 60 fps vsync cap.
47
+ //
48
+ // Priming depth first turns that around. The prepass writes depth only (no MRT,
49
+ // no lighting, alpha cutout honoured so cards keep their real silhouette), and
50
+ // the main pass then early-Z *rejects* the occluded leaves before its shader
51
+ // ever runs. The main pass tests LessEqual, not Less — the visible surface
52
+ // arrives with a depth exactly equal to the one the prepass stored, and `Less`
53
+ // would throw it away.
54
+ //
55
+ // Same vertex stage, so the foliage wind displaces identically in both and the
56
+ // depths agree to the bit.
57
+ // Runs even with no cached models, because it now owns the depth CLEAR that
58
+ // main_hdr_pass used to do — skipping it would hand the main pass a depth
59
+ // buffer full of last frame's garbage.
60
+ profiler.begin("depth_prepass");
61
+ {
62
+ let prepass_ts = profiler.pass_timestamp_writes("depth_prepass");
63
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
64
+ label: Some("bloom_depth_prepass"),
65
+ color_attachments: &[],
66
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
67
+ view: &self.depth_view,
68
+ depth_ops: Some(wgpu::Operations {
69
+ load: wgpu::LoadOp::Clear(1.0),
70
+ store: wgpu::StoreOp::Store,
71
+ }),
72
+ stencil_ops: None,
73
+ }),
74
+ timestamp_writes: prepass_ts,
75
+ occlusion_query_set: None,
76
+ multiview_mask: None,
77
+ });
78
+ // EN-063 — on wasm the prepass DRAWS are skipped (the pass still
79
+ // owns the depth clear). The prepass/main pairing requires the two
80
+ // pipelines to produce bit-identical depths; Tint (the browser's
81
+ // WGSL compiler) does not preserve that even under @invariant once
82
+ // the foliage-wind displacement is in the chain, and the main
83
+ // pass's Equal test then discards whole leaf cards (white torn
84
+ // canopies, streaks). The main pass runs the classic Less+write
85
+ // pipeline on wasm instead — overdraw over early-Z, correctness
86
+ // over speed.
87
+ #[cfg(not(target_arch = "wasm32"))]
88
+ {
89
+ pass.set_pipeline(&self.scene_depth_pipeline);
90
+ pass.set_bind_group(1, &self.lighting_bind_group, &[]);
91
+ pass.set_bind_group(3, &self.joint_bind_group, &[]);
92
+ let cam_vp = mat4_multiply(
93
+ self.current_proj_matrix_unjittered,
94
+ self.current_view_matrix,
95
+ );
96
+ let cam_planes = crate::scene::extract_frustum_planes(&cam_vp);
97
+ for cmd in &self.model_draw_commands {
98
+ if let Some(Some(meshes)) = self.model_gpu_cache.get(&cmd.cache_handle) {
99
+ if cmd.mesh_idx < meshes.len() {
100
+ let mesh = &meshes[cmd.mesh_idx];
101
+ let (wmin, wmax) = cmd.bounds_override.unwrap_or_else(|| {
102
+ transform_aabb(&cmd.model, mesh.local_min, mesh.local_max)
103
+ });
104
+ if wmin[0] <= wmax[0]
105
+ && crate::scene::aabb_outside_frustum(&cam_planes, wmin, wmax)
106
+ {
107
+ continue;
108
+ }
109
+ pass.set_bind_group(0, &self.model_uniform_bind_groups[cmd.uniform_slot], &[]);
110
+ pass.set_bind_group(2, &mesh.material_bg, &[]);
111
+ pass.set_vertex_buffer(0, mesh.vb.slice(..));
112
+ pass.set_index_buffer(mesh.ib.slice(..), wgpu::IndexFormat::Uint32);
113
+ pass.draw_indexed(0..mesh.index_count, 0, 0..1);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ profiler.end("depth_prepass");
120
+
121
+ profiler.begin("main_hdr_pass");
122
+ {
123
+ // HDR clear: the user's clear_color is in 0-1 srgb-ish
124
+ // range; treat it as the linear background for the HDR
125
+ // RT. After tonemap it ends up roughly the same shade.
126
+ let hdr_ts = profiler.pass_timestamp_writes("main_hdr_pass");
127
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
128
+ label: Some("bloom_hdr_pass"),
129
+ color_attachments: &[
130
+ Some(wgpu::RenderPassColorAttachment {
131
+ view: &self.hdr_rt_view,
132
+ resolve_target: None,
133
+ depth_slice: None,
134
+ ops: wgpu::Operations {
135
+ load: wgpu::LoadOp::Clear(self.clear_color),
136
+ store: wgpu::StoreOp::Store,
137
+ },
138
+ }),
139
+ Some(wgpu::RenderPassColorAttachment {
140
+ view: &self.material_rt_view,
141
+ resolve_target: None,
142
+ depth_slice: None,
143
+ ops: wgpu::Operations {
144
+ // Blank pixels clear to metallic=0. SSR's
145
+ // `metallic < 0.2` gate early-outs before
146
+ // roughness is read, so the roughness
147
+ // component of the clear is dead — leaving
148
+ // it at 0 instead of 1 keeps the material
149
+ // texture black in frame captures and
150
+ // avoids a false "green G-buffer" readout
151
+ // if the RT is ever viewed as RGBA.
152
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
153
+ store: wgpu::StoreOp::Store,
154
+ },
155
+ }),
156
+ Some(wgpu::RenderPassColorAttachment {
157
+ view: &self.velocity_rt_view,
158
+ resolve_target: None,
159
+ depth_slice: None,
160
+ ops: wgpu::Operations {
161
+ // Zero velocity = stationary pixel.
162
+ load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }),
163
+ store: wgpu::StoreOp::Store,
164
+ },
165
+ }),
166
+ Some(wgpu::RenderPassColorAttachment {
167
+ view: &self.albedo_rt_view,
168
+ resolve_target: None,
169
+ depth_slice: None,
170
+ ops: wgpu::Operations {
171
+ // Clear to zero albedo — pixels the scene
172
+ // doesn't cover (before sky writes) absorb
173
+ // indirect light fully. Sky then writes 0
174
+ // too so SSGI rays landing on sky don't
175
+ // re-tint bounce by background radiance.
176
+ load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }),
177
+ store: wgpu::StoreOp::Store,
178
+ },
179
+ }),
180
+ ],
181
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
182
+ view: &self.depth_view,
183
+ depth_ops: Some(wgpu::Operations {
184
+ // EN-044 — LOAD, not Clear: the depth prepass just primed this
185
+ // buffer, and clearing it here would throw that away.
186
+ load: wgpu::LoadOp::Load,
187
+ store: wgpu::StoreOp::Store,
188
+ }),
189
+ stencil_ops: None,
190
+ }),
191
+ timestamp_writes: hdr_ts,
192
+ occlusion_query_set: None,
193
+ multiview_mask: None,
194
+ });
195
+
196
+ // Sky uses the same env_intensity as IBL so the background
197
+ // and lighting stay in sync — otherwise bumping IBL down
198
+ // would leave the sky blown out.
199
+ if self.procedural_sky_enabled {
200
+ self.render_procedural_sky_pass(&mut pass, self.lighting_uniforms.camera_pos[3]);
201
+ } else {
202
+ self.render_sky_pass(&mut pass, self.lighting_uniforms.camera_pos[3]);
203
+ }
204
+
205
+ if has_3d {
206
+ pass.set_pipeline(&self.pipeline_3d);
207
+ pass.set_bind_group(0, &self.uniform_bind_group_3d, &[]);
208
+ pass.set_bind_group(1, &self.lighting_bind_group, &[]);
209
+ pass.set_bind_group(3, &self.joint_bind_group, &[]);
210
+ pass.set_vertex_buffer(0, self.persistent_vb_3d.slice(..));
211
+ pass.set_index_buffer(self.persistent_ib_3d.slice(..), wgpu::IndexFormat::Uint32);
212
+
213
+ if self.draw_calls_3d.is_empty() {
214
+ pass.set_bind_group(2, &self.texture_bind_groups[0], &[]);
215
+ pass.draw_indexed(0..self.indices_3d.len() as u32, 0, 0..1);
216
+ } else {
217
+ let num_calls = self.draw_calls_3d.len();
218
+ for i in 0..num_calls {
219
+ let call = &self.draw_calls_3d[i];
220
+ let next_start = if i + 1 < num_calls {
221
+ self.draw_calls_3d[i + 1].index_start
222
+ } else {
223
+ self.indices_3d.len() as u32
224
+ };
225
+ let count = next_start - call.index_start;
226
+ if count == 0 { continue; }
227
+ let tex_idx = call.texture_idx as usize;
228
+ if tex_idx < self.texture_bind_groups.len() {
229
+ pass.set_bind_group(2, &self.texture_bind_groups[tex_idx], &[]);
230
+ } else {
231
+ pass.set_bind_group(2, &self.texture_bind_groups[0], &[]);
232
+ }
233
+ pass.draw_indexed(call.index_start..next_start, 0, 0..1);
234
+ }
235
+ }
236
+ }
237
+
238
+ // Cached models + retained scene graph — both via scene_pipeline.
239
+ let has_cached_models = !self.model_draw_commands.is_empty();
240
+ if has_cached_models || scene.node_count() > 0 {
241
+ // EN-044 — cached models go through the PREPASSED pipeline (no depth
242
+ // write, Equal test), because the depth prepass above already stored
243
+ // their exact depth. That is what lets the hardware early-Z reject the
244
+ // occluded leaf cards instead of shading every one of them.
245
+ #[cfg(not(target_arch = "wasm32"))]
246
+ pass.set_pipeline(&self.scene_pipeline_prepassed);
247
+ // wasm: no prepass priming (see above) — classic Less + write.
248
+ #[cfg(target_arch = "wasm32")]
249
+ pass.set_pipeline(&self.scene_pipeline);
250
+ pass.set_bind_group(1, &self.lighting_bind_group, &[]);
251
+ pass.set_bind_group(3, &self.joint_bind_group, &[]);
252
+
253
+ if has_cached_models {
254
+ // Frustum-cull cached-model draws against the camera. These
255
+ // commands (the forest: ~400 draws/frame) previously had no
256
+ // culling in any pass — everything behind the camera still
257
+ // paid bind-group switches + a full VS pass. AABBs are
258
+ // conservative (cache-time local bounds × model matrix), and
259
+ // the unjittered projection is used so TAA's sub-pixel
260
+ // jitter can't flicker a borderline caster (the AABB slop
261
+ // exceeds jitter by orders of magnitude).
262
+ let cam_vp = mat4_multiply(
263
+ self.current_proj_matrix_unjittered,
264
+ self.current_view_matrix,
265
+ );
266
+ let cam_planes = crate::scene::extract_frustum_planes(&cam_vp);
267
+ for cmd in &self.model_draw_commands {
268
+ if let Some(Some(meshes)) = self.model_gpu_cache.get(&cmd.cache_handle) {
269
+ if cmd.mesh_idx < meshes.len() {
270
+ let mesh = &meshes[cmd.mesh_idx];
271
+ // Skinned draws carry a pre-computed joint-union
272
+ // AABB (their rest AABB × model matrix would be
273
+ // wrong once posed); static draws derive theirs.
274
+ let (wmin, wmax) = cmd.bounds_override.unwrap_or_else(|| {
275
+ transform_aabb(&cmd.model, mesh.local_min, mesh.local_max)
276
+ });
277
+ if wmin[0] <= wmax[0]
278
+ && crate::scene::aabb_outside_frustum(&cam_planes, wmin, wmax)
279
+ {
280
+ continue;
281
+ }
282
+ pass.set_bind_group(0, &self.model_uniform_bind_groups[cmd.uniform_slot], &[]);
283
+ pass.set_bind_group(2, &mesh.material_bg, &[]);
284
+ pass.set_vertex_buffer(0, mesh.vb.slice(..));
285
+ pass.set_index_buffer(mesh.ib.slice(..), wgpu::IndexFormat::Uint32);
286
+ pass.draw_indexed(0..mesh.index_count, 0, 0..1);
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ // Retained scene-graph nodes are not in the prepass, so they still need
293
+ // the depth-writing pipeline.
294
+ pass.set_pipeline(&self.scene_pipeline);
295
+ scene.render(&mut pass);
296
+ }
297
+ }
298
+ profiler.end("main_hdr_pass");
299
+
300
+ // EN-011 — render every registered planar reflection probe
301
+ // BEFORE the main material pass so the probe RTs are
302
+ // sampleable when materials run. No-op when no probes are
303
+ // registered or no opaque material draws are queued.
304
+ profiler.begin("planar_reflections");
305
+ self.dispatch_planar_reflections(&mut *encoder, scene, profiler);
306
+ profiler.end("planar_reflections");
307
+
308
+ // Phase 2c — schedule the material pass through the render
309
+ // graph. First real consumer of `renderer::graph` from #35.
310
+ // For now a one-node graph; later phases add more nodes
311
+ // (main_hdr, ssao, bloom, translucent, composite) and the
312
+ // graph's topological sort picks the order from read/write
313
+ // declarations.
314
+ //
315
+ // All per-frame borrows that the pass body needs are captured
316
+ // here from `&self` before we build the context that wraps
317
+ // `&mut *encoder` + `&mut profiler`. Rust's borrow checker is
318
+ // happy because the immutable and mutable borrows are
319
+ // disjoint fields of the same struct.
320
+ if !self.material_system.commands.is_empty() {
321
+ use graph::{Graph, PassNode, PassOutput};
322
+
323
+ let hdr_rt_view = &self.hdr_rt_view;
324
+ let material_rt_view = &self.material_rt_view;
325
+ let velocity_rt_view = &self.velocity_rt_view;
326
+ let albedo_rt_view = &self.albedo_rt_view;
327
+ let depth_view = &self.depth_view;
328
+ let material_system = &self.material_system;
329
+ let model_gpu_cache = &self.model_gpu_cache;
330
+ // Camera frustum for instance-tile culling (same unjittered
331
+ // matrices as the cached-model culling above).
332
+ let cam_planes = crate::scene::extract_frustum_planes(&mat4_multiply(
333
+ self.current_proj_matrix_unjittered,
334
+ self.current_view_matrix,
335
+ ));
336
+
337
+ struct FrameCtx<'a> {
338
+ encoder: &'a mut wgpu::CommandEncoder,
339
+ profiler: &'a mut crate::profiler::Profiler,
340
+ }
341
+
342
+ let mut graph: Graph<FrameCtx<'_>> = Graph::new();
343
+ graph.push(
344
+ PassNode::new("material_pass", Box::new(move |ctx: &mut FrameCtx| {
345
+ ctx.profiler.begin("material_pass");
346
+ {
347
+ let mat_ts = ctx.profiler.pass_timestamp_writes("material_pass");
348
+ let mut pass = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
349
+ label: Some("bloom_material_pass"),
350
+ color_attachments: &[
351
+ Some(wgpu::RenderPassColorAttachment {
352
+ view: hdr_rt_view,
353
+ resolve_target: None, depth_slice: None,
354
+ ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store },
355
+ }),
356
+ Some(wgpu::RenderPassColorAttachment {
357
+ view: material_rt_view,
358
+ resolve_target: None, depth_slice: None,
359
+ ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store },
360
+ }),
361
+ Some(wgpu::RenderPassColorAttachment {
362
+ view: velocity_rt_view,
363
+ resolve_target: None, depth_slice: None,
364
+ ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store },
365
+ }),
366
+ Some(wgpu::RenderPassColorAttachment {
367
+ view: albedo_rt_view,
368
+ resolve_target: None, depth_slice: None,
369
+ ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store },
370
+ }),
371
+ ],
372
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
373
+ view: depth_view,
374
+ depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store }),
375
+ stencil_ops: None,
376
+ }),
377
+ timestamp_writes: mat_ts,
378
+ occlusion_query_set: None,
379
+ multiview_mask: None,
380
+ });
381
+ material_system.dispatch(&mut pass, Some(&cam_planes), |handle, idx| {
382
+ if let Some(Some(meshes)) = model_gpu_cache.get(&handle) {
383
+ if idx < meshes.len() {
384
+ let mesh = &meshes[idx];
385
+ return Some((
386
+ &mesh.vb, &mesh.ib, mesh.index_count,
387
+ mesh.local_min, mesh.local_max,
388
+ ));
389
+ }
390
+ }
391
+ None
392
+ });
393
+ }
394
+ ctx.profiler.end("material_pass");
395
+ }))
396
+ // Writes HdrColor + the G-buffer so Phase 2d's scheduler
397
+ // can order downstream passes (SSAO, bloom, translucent)
398
+ // correctly once they're nodes too.
399
+ .with_writes(&[
400
+ PassOutput::HdrColor,
401
+ PassOutput::MaterialRt,
402
+ PassOutput::VelocityRt,
403
+ PassOutput::AlbedoRt,
404
+ PassOutput::Depth,
405
+ ]),
406
+ );
407
+
408
+ let mut ctx = FrameCtx { encoder: &mut *encoder, profiler: &mut *profiler };
409
+ if let Err(e) = graph.execute(&mut ctx) {
410
+ eprintln!("[graph] material_pass failed: {:?}", e);
411
+ }
412
+ }
413
+
414
+ }
415
+ }
416
+
417
+ impl Renderer {
418
+ /// Translucent / refractive / additive material pass: after opaque,
419
+ /// before post-FX; loads hdr_rt, depth read-only, back-to-front
420
+ /// sorted; snapshots scene color for reads_scene materials. Split
421
+ /// from end_frame_with_scene.
422
+ pub(super) fn record_translucent_pass(
423
+ &mut self,
424
+ encoder: &mut wgpu::CommandEncoder,
425
+ profiler: &mut crate::profiler::Profiler,
426
+ ) {
427
+ // ============================================================
428
+ // Phase 4b — translucent / refractive / additive material pass
429
+ // ============================================================
430
+ //
431
+ // Runs after opaque materials, before post-FX. Loads hdr_rt so
432
+ // opaque output survives; alpha-blends into it. Depth is
433
+ // bound as read-only so translucent draws participate in the
434
+ // depth test without writing.
435
+ //
436
+ // If any submitted translucent material declared
437
+ // `reads_scene = true`, we first snapshot hdr_rt into a
438
+ // swapchain-sized transient and bind that as group 4
439
+ // scene_color_tex for the dispatch. Free after the pass so
440
+ // the transient pool reuses on the next frame.
441
+ if !self.material_system.translucent_commands.is_empty() {
442
+ // Back-to-front by view depth — required for correct alpha
443
+ // compositing; submission order is only kept between
444
+ // equal-depth draws (stable sort).
445
+ self.material_system.sort_translucent();
446
+ profiler.begin("translucent_pass");
447
+ let swap_w = self.surface_config.width;
448
+ let swap_h = self.surface_config.height;
449
+ self.transient_pool.begin_frame(swap_w, swap_h);
450
+
451
+ // Phase 7 — run the impulse decay + splat compute BEFORE
452
+ // we build scene_inputs so the front view reflects this
453
+ // frame's submissions.
454
+ self.impulse_field.update(&self.device, &self.queue, &mut *encoder);
455
+
456
+ // Does any queued translucent material need the scene
457
+ // colour snapshot?
458
+ let needs_scene = self.material_system.translucent_commands
459
+ .iter()
460
+ .any(|c| self.material_system.pipelines
461
+ .get(c.material as usize - 1)
462
+ .and_then(|p| p.as_ref())
463
+ .map(|p| p.reads_scene)
464
+ .unwrap_or(false));
465
+
466
+ // The snapshots mirror hdr_rt/depth, which live at RENDER
467
+ // resolution (surface × render_scale) — not swapchain size.
468
+ // Sizing them from the swapchain overruns the source copy
469
+ // whenever render_scale < 1 (TSR upscaling).
470
+ let (render_w, render_h) = self.render_extent();
471
+ let scene_color_tid = if needs_scene {
472
+ let desc = transient::TransientDesc::new(
473
+ formats::HDR_FORMAT,
474
+ wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
475
+ transient::SizePolicy::Fixed(render_w, render_h),
476
+ );
477
+ Some(self.transient_pool.acquire(&self.device, desc))
478
+ } else {
479
+ None
480
+ };
481
+
482
+ // Phase 4c — depth snapshot. wgpu forbids sampling a
483
+ // texture that is also a depth-stencil attachment of the
484
+ // same pass, so we copy the opaque depth buffer into a
485
+ // transient before beginning the translucent pass and
486
+ // bind the transient at group 4 binding 2. Acquired
487
+ // whenever any translucent material reads_scene (same
488
+ // gate as colour) — cheap enough that it's not worth a
489
+ // separate `reads_depth` flag yet.
490
+ let scene_depth_tid = if needs_scene {
491
+ let desc = transient::TransientDesc::new(
492
+ formats::DEPTH_FORMAT,
493
+ wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
494
+ transient::SizePolicy::Fixed(render_w, render_h),
495
+ );
496
+ Some(self.transient_pool.acquire(&self.device, desc))
497
+ } else {
498
+ None
499
+ };
500
+
501
+ // Snapshot hdr_rt + live depth -> transients.
502
+ if let (Some(ctid), Some(dtid)) = (scene_color_tid, scene_depth_tid) {
503
+ let color_tex = self.transient_pool.texture(ctid).expect("fresh color transient");
504
+ encoder.copy_texture_to_texture(
505
+ wgpu::TexelCopyTextureInfo {
506
+ texture: &self.hdr_rt_texture,
507
+ mip_level: 0,
508
+ origin: wgpu::Origin3d::ZERO,
509
+ aspect: wgpu::TextureAspect::All,
510
+ },
511
+ wgpu::TexelCopyTextureInfo {
512
+ texture: color_tex,
513
+ mip_level: 0,
514
+ origin: wgpu::Origin3d::ZERO,
515
+ aspect: wgpu::TextureAspect::All,
516
+ },
517
+ wgpu::Extent3d { width: render_w, height: render_h, depth_or_array_layers: 1 },
518
+ );
519
+ let depth_tex = self.transient_pool.texture(dtid).expect("fresh depth transient");
520
+ encoder.copy_texture_to_texture(
521
+ wgpu::TexelCopyTextureInfo {
522
+ texture: &self.depth_texture,
523
+ mip_level: 0,
524
+ origin: wgpu::Origin3d::ZERO,
525
+ aspect: wgpu::TextureAspect::DepthOnly,
526
+ },
527
+ wgpu::TexelCopyTextureInfo {
528
+ texture: depth_tex,
529
+ mip_level: 0,
530
+ origin: wgpu::Origin3d::ZERO,
531
+ aspect: wgpu::TextureAspect::DepthOnly,
532
+ },
533
+ wgpu::Extent3d { width: render_w, height: render_h, depth_or_array_layers: 1 },
534
+ );
535
+ let color_view = self.transient_pool.view(ctid).unwrap();
536
+ let depth_view = self.transient_pool.view(dtid).unwrap();
537
+ let imp_view = self.impulse_field.front_view();
538
+ let imp_samp = self.impulse_field.sampler();
539
+ self.material_system.update_scene_inputs(
540
+ &self.device, color_view, Some(depth_view),
541
+ Some((imp_view, imp_samp)),
542
+ );
543
+ } else {
544
+ // No refractive/depth-reading materials this frame —
545
+ // still need a valid bind group. None → internal stubs.
546
+ self.material_system.update_scene_inputs(
547
+ &self.device, &self.hdr_rt_view, None, None,
548
+ );
549
+ }
550
+
551
+ {
552
+ let t_ts = profiler.pass_timestamp_writes("translucent_pass");
553
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
554
+ label: Some("bloom_translucent_pass"),
555
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
556
+ view: &self.hdr_rt_view,
557
+ resolve_target: None,
558
+ depth_slice: None,
559
+ ops: wgpu::Operations {
560
+ load: wgpu::LoadOp::Load,
561
+ store: wgpu::StoreOp::Store,
562
+ },
563
+ })],
564
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
565
+ view: &self.depth_view,
566
+ depth_ops: Some(wgpu::Operations {
567
+ load: wgpu::LoadOp::Load,
568
+ // Translucents don't write depth — keep
569
+ // the opaque pass's depth pristine so
570
+ // downstream post-FX (SSR/SSGI) still
571
+ // sees the opaque geometry.
572
+ store: wgpu::StoreOp::Store,
573
+ }),
574
+ stencil_ops: None,
575
+ }),
576
+ timestamp_writes: t_ts,
577
+ occlusion_query_set: None,
578
+ multiview_mask: None,
579
+ });
580
+ let cache = &self.model_gpu_cache;
581
+ self.material_system.dispatch_translucent(&mut pass, |handle, idx| {
582
+ if let Some(Some(meshes)) = cache.get(&handle) {
583
+ if idx < meshes.len() {
584
+ let mesh = &meshes[idx];
585
+ return Some((&mesh.vb, &mesh.ib, mesh.index_count));
586
+ }
587
+ }
588
+ None
589
+ });
590
+ }
591
+
592
+ if let Some(tid) = scene_color_tid {
593
+ self.transient_pool.release(tid);
594
+ }
595
+ // Round-2 audit: the depth snapshot was acquired every frame but
596
+ // NEVER released — the pool (which has no auto-reclaim by design)
597
+ // allocated a brand-new render-res Depth32Float every frame with
598
+ // refractive water on screen. ~8 MB/frame of texture creation plus
599
+ // an ever-growing slot scan; measured as translucent_pass CPU
600
+ // climbing 0.4 ms → 6.7 ms over ~50 s of play, in every session.
601
+ if let Some(tid) = scene_depth_tid {
602
+ self.transient_pool.release(tid);
603
+ }
604
+ profiler.end("translucent_pass");
605
+ }
606
+ }
607
+ }