@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,601 @@
1
+ //! Tests for material_system: headless-GPU dispatch tests + the
2
+ //! translucent sort. Child module of material_system via #[path] —
3
+ //! the glob below re-exposes the parent's items to the inner mods.
4
+
5
+ #[allow(unused_imports)]
6
+ use super::*;
7
+
8
+ #[cfg(test)]
9
+ mod tests {
10
+ use super::*;
11
+ use crate::renderer::formats;
12
+ use crate::renderer::types::Vertex3D;
13
+
14
+ /// Headless wgpu device. See sibling helpers in `transient.rs` /
15
+ /// `impulse_field.rs` — same fallback adapter pattern. Returns None
16
+ /// (test skips gracefully) when no GPU is available.
17
+ fn try_create_device() -> Option<(wgpu::Device, wgpu::Queue)> {
18
+ let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
19
+ backends: wgpu::Backends::all(),
20
+ ..wgpu::InstanceDescriptor::new_without_display_handle()
21
+ });
22
+ let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
23
+ power_preference: wgpu::PowerPreference::LowPower,
24
+ compatible_surface: None,
25
+ force_fallback_adapter: true,
26
+ })).ok()?;
27
+ // The material ABI uses 5 bind groups (PerFrame, PerView,
28
+ // PerMaterial, PerDraw, SceneInputs). downlevel_defaults caps
29
+ // max_bind_groups at 4, which is fine on Metal (it silently
30
+ // accepts more) but DX12 enforces it strictly — bump to 5 so
31
+ // the user-material pipeline validates on every backend.
32
+ // Also bump max_uniform_buffer_binding_size from 16KB to 64KB
33
+ // for the JointMatrices UBO (1024 × mat4x4 = 64KB).
34
+ let mut required_limits = wgpu::Limits::downlevel_defaults();
35
+ required_limits.max_bind_groups = 5;
36
+ required_limits.max_uniform_buffer_binding_size = 64 << 10;
37
+ let (device, queue) = pollster::block_on(adapter.request_device(
38
+ &wgpu::DeviceDescriptor {
39
+ label: Some("material-test-device"),
40
+ required_features: wgpu::Features::empty(),
41
+ required_limits,
42
+ ..Default::default()
43
+ },
44
+ )).ok()?;
45
+ Some((device, queue))
46
+ }
47
+
48
+ /// Minimal translucent (Bucket::Refractive) WGSL material. Writes
49
+ /// a constant red+0.5α colour through the alpha-blended HDR target.
50
+ /// Uses #include "material_abi.wgsl" so the same pipeline-layout
51
+ /// / per-frame / per-view bindings are validated as production
52
+ /// materials. Vertex stage transforms via `draw.mvp` so we can
53
+ /// emit a fullscreen-ish triangle from any geometry.
54
+ const TRANSLUCENT_WGSL: &str = r#"
55
+ #include "material_abi.wgsl"
56
+
57
+ struct VsOut {
58
+ @builtin(position) clip_position: vec4<f32>,
59
+ };
60
+
61
+ @vertex
62
+ fn vs_main(in: VertexInput) -> VsOut {
63
+ var out: VsOut;
64
+ out.clip_position = draw.mvp * vec4<f32>(in.position, 1.0);
65
+ return out;
66
+ }
67
+
68
+ @fragment
69
+ fn fs_main(_in: VsOut) -> TranslucentOut {
70
+ var out: TranslucentOut;
71
+ // Red, half alpha — alpha-blended onto whatever the load-op set.
72
+ out.hdr = vec4<f32>(1.0, 0.0, 0.0, 0.5);
73
+ return out;
74
+ }
75
+ "#;
76
+
77
+ /// Create a tiny joint buffer so MaterialSystem::new is happy. The
78
+ /// per_draw layout binds it at @binding(1); the test material
79
+ /// doesn't read it but the bind group still has to validate.
80
+ fn make_joint_buffer(device: &wgpu::Device) -> wgpu::Buffer {
81
+ device.create_buffer(&wgpu::BufferDescriptor {
82
+ label: Some("test_joint_buffer"),
83
+ // 1024 mat4s × 64 B = 64 KiB. Same size as production.
84
+ size: 65536,
85
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
86
+ mapped_at_creation: false,
87
+ })
88
+ }
89
+
90
+ /// Build a single fullscreen triangle covering NDC: three vertices
91
+ /// at (-1,-1), (3,-1), (-1,3). The pipeline's MVP starts as
92
+ /// identity (we override it below) so the triangle covers the
93
+ /// whole viewport.
94
+ fn make_fullscreen_tri(device: &wgpu::Device, queue: &wgpu::Queue) -> (wgpu::Buffer, wgpu::Buffer, u32) {
95
+ let mut verts: [Vertex3D; 3] = [Vertex3D::default(); 3];
96
+ verts[0].position = [-1.0, -1.0, 0.5];
97
+ verts[1].position = [ 3.0, -1.0, 0.5];
98
+ verts[2].position = [-1.0, 3.0, 0.5];
99
+ // The MaterialPipeline's depth-stencil uses Less; the load-op
100
+ // for a translucent pass clears to 1.0 (far) by default in
101
+ // production but in this test we use a depth attachment with
102
+ // the CLEAR op and clear value 1.0, so anything < 1.0 passes.
103
+ let vb = device.create_buffer(&wgpu::BufferDescriptor {
104
+ label: Some("test_tri_vb"),
105
+ size: std::mem::size_of_val(&verts) as u64,
106
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
107
+ mapped_at_creation: false,
108
+ });
109
+ queue.write_buffer(&vb, 0, bytemuck::cast_slice(&verts));
110
+ let indices: [u32; 3] = [0, 1, 2];
111
+ let ib = device.create_buffer(&wgpu::BufferDescriptor {
112
+ label: Some("test_tri_ib"),
113
+ size: std::mem::size_of_val(&indices) as u64,
114
+ usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
115
+ mapped_at_creation: false,
116
+ });
117
+ queue.write_buffer(&ib, 0, bytemuck::cast_slice(&indices));
118
+ (vb, ib, 3)
119
+ }
120
+
121
+ /// EN-006 — translucent dispatch path. Compiles a refractive
122
+ /// material via `MaterialSystem::compile`, submits one draw,
123
+ /// dispatches into a 64×64 Rgba16Float HDR render target via
124
+ /// `dispatch_translucent`, then reads back the HDR and verifies
125
+ /// the alpha-blended red shows through the cyan background.
126
+ ///
127
+ /// This is the scoped-down version of the acceptance scenario:
128
+ /// it exercises the dispatch site and pipeline-layout / blend-state
129
+ /// wiring without standing up the full `Renderer` (which requires
130
+ /// a surface and ~30 other resources). What this DOES cover:
131
+ /// - MaterialSystem::compile against a refractive WGSL source
132
+ /// - dispatch_translucent's pipeline / bind-group binding loop
133
+ /// - alpha blending via the translucent target's BlendState
134
+ /// - the per-draw / per-view / per-frame UBO writes
135
+ /// What it does NOT cover:
136
+ /// - The scene-color snapshot (test material has reads_scene=false
137
+ /// so group 4 doesn't bind; the e2e copy_texture_to_texture
138
+ /// before this dispatch is exercised by the depth-snapshot test
139
+ /// in transient.rs).
140
+ /// - Sort order across multiple translucent draws (single-draw test).
141
+ /// Skipped on adapters where `try_create_device` returns None.
142
+ #[test]
143
+ fn dispatch_translucent_alpha_blends_into_hdr() {
144
+ let Some((device, queue)) = try_create_device() else { return; };
145
+ let joint_buf = make_joint_buffer(&device);
146
+ let mut sys = MaterialSystem::new(&device, &queue, &joint_buf);
147
+
148
+ // Compile a refractive (translucent) material. Use the engine's
149
+ // production format constants so the pipeline matches what
150
+ // Renderer::new would have produced.
151
+ let handle = sys.compile(
152
+ &device,
153
+ TRANSLUCENT_WGSL,
154
+ FragmentProfile::Translucent,
155
+ Bucket::Transparent,
156
+ false, // reads_scene
157
+ false, // wants_instancing
158
+ wgpu::TextureFormat::Rgba16Float, // hdr_format
159
+ wgpu::TextureFormat::Rg8Unorm, // material_format (unused in translucent)
160
+ wgpu::TextureFormat::Rg16Float, // velocity_format (unused)
161
+ wgpu::TextureFormat::Rgba8Unorm, // albedo_format (unused)
162
+ formats::DEPTH_FORMAT,
163
+ ).expect("translucent material compiles");
164
+ assert!(handle != 0, "compile returns a 1-based handle");
165
+
166
+ // Frame uniforms — zeros are fine for a constant-colour shader.
167
+ let pf = PerFrameUniforms {
168
+ time: 0.0, delta_time: 0.0, frame_index: 0, _pad0: 0,
169
+ screen_resolution: [64.0, 64.0], render_resolution: [64.0, 64.0],
170
+ taa_jitter: [0.0; 2], _pad1: [0.0; 2], wind: [0.0; 4],
171
+ cloud: [0.0; 4],
172
+ };
173
+ let pv = bytemuck::Zeroable::zeroed();
174
+ sys.update_frame_uniforms(&queue, &pf, &pv);
175
+ sys.reset_draw_slot(crate::renderer::IDENTITY_MAT4);
176
+
177
+ // MVP = identity so the fullscreen tri stays in NDC.
178
+ let identity = [
179
+ [1.0, 0.0, 0.0, 0.0],
180
+ [0.0, 1.0, 0.0, 0.0],
181
+ [0.0, 0.0, 1.0, 0.0],
182
+ [0.0, 0.0, 0.0, 1.0],
183
+ ];
184
+ let (vb, ib, icount) = make_fullscreen_tri(&device, &queue);
185
+
186
+ sys.submit_draw(
187
+ &device, &queue, &joint_buf,
188
+ handle, /* mesh_handle */ 1, /* mesh_idx */ 0,
189
+ identity, identity, identity,
190
+ [1.0; 4], [0; 4],
191
+ );
192
+ assert_eq!(sys.translucent_commands.len(), 1, "draw queued in translucent bucket");
193
+
194
+ // Build the HDR + depth render targets for the dispatch.
195
+ let (rt_w, rt_h) = (64u32, 64u32);
196
+ let hdr_rt = device.create_texture(&wgpu::TextureDescriptor {
197
+ label: Some("test_hdr_rt"),
198
+ size: wgpu::Extent3d { width: rt_w, height: rt_h, depth_or_array_layers: 1 },
199
+ mip_level_count: 1, sample_count: 1,
200
+ dimension: wgpu::TextureDimension::D2,
201
+ format: wgpu::TextureFormat::Rgba16Float,
202
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
203
+ view_formats: &[],
204
+ });
205
+ let hdr_view = hdr_rt.create_view(&Default::default());
206
+ let depth_rt = device.create_texture(&wgpu::TextureDescriptor {
207
+ label: Some("test_depth_rt"),
208
+ size: wgpu::Extent3d { width: rt_w, height: rt_h, depth_or_array_layers: 1 },
209
+ mip_level_count: 1, sample_count: 1,
210
+ dimension: wgpu::TextureDimension::D2,
211
+ format: formats::DEPTH_FORMAT,
212
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
213
+ view_formats: &[],
214
+ });
215
+ let depth_view = depth_rt.create_view(&Default::default());
216
+
217
+ // Pre-clear HDR to opaque cyan so we can detect alpha-blended red:
218
+ // (1, 0, 0, 0.5) over (0, 1, 1, 1) → (0.5, 0.5, 0.5, 1.0).
219
+ let bg_color = wgpu::Color { r: 0.0, g: 1.0, b: 1.0, a: 1.0 };
220
+ let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
221
+ label: Some("test_translucent_encoder"),
222
+ });
223
+ {
224
+ // Clear HDR + depth in one pass.
225
+ let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
226
+ label: Some("test_clear_pass"),
227
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
228
+ view: &hdr_view,
229
+ resolve_target: None,
230
+ depth_slice: None,
231
+ ops: wgpu::Operations {
232
+ load: wgpu::LoadOp::Clear(bg_color),
233
+ store: wgpu::StoreOp::Store,
234
+ },
235
+ })],
236
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
237
+ view: &depth_view,
238
+ depth_ops: Some(wgpu::Operations {
239
+ load: wgpu::LoadOp::Clear(1.0),
240
+ store: wgpu::StoreOp::Store,
241
+ }),
242
+ stencil_ops: None,
243
+ }),
244
+ timestamp_writes: None,
245
+ occlusion_query_set: None,
246
+ multiview_mask: None,
247
+ });
248
+ }
249
+ // Translucent dispatch — Load (don't clear) HDR; depth read-only.
250
+ {
251
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
252
+ label: Some("test_translucent_pass"),
253
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
254
+ view: &hdr_view,
255
+ resolve_target: None,
256
+ depth_slice: None,
257
+ ops: wgpu::Operations {
258
+ load: wgpu::LoadOp::Load,
259
+ store: wgpu::StoreOp::Store,
260
+ },
261
+ })],
262
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
263
+ view: &depth_view,
264
+ depth_ops: Some(wgpu::Operations {
265
+ load: wgpu::LoadOp::Load,
266
+ store: wgpu::StoreOp::Store,
267
+ }),
268
+ stencil_ops: None,
269
+ }),
270
+ timestamp_writes: None,
271
+ occlusion_query_set: None,
272
+ multiview_mask: None,
273
+ });
274
+ sys.dispatch_translucent(&mut pass, |mh, _idx| {
275
+ if mh == 1 { Some((&vb, &ib, icount)) } else { None }
276
+ });
277
+ }
278
+
279
+ // Read back the HDR target (Rgba16Float = 8 B / texel).
280
+ let bpr_unpadded = rt_w * 8;
281
+ let bpr = (bpr_unpadded + 255) & !255;
282
+ let buf_size = (bpr * rt_h) as u64;
283
+ let staging = device.create_buffer(&wgpu::BufferDescriptor {
284
+ label: Some("test_hdr_staging"),
285
+ size: buf_size,
286
+ usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
287
+ mapped_at_creation: false,
288
+ });
289
+ encoder.copy_texture_to_buffer(
290
+ wgpu::TexelCopyTextureInfo {
291
+ texture: &hdr_rt,
292
+ mip_level: 0,
293
+ origin: wgpu::Origin3d::ZERO,
294
+ aspect: wgpu::TextureAspect::All,
295
+ },
296
+ wgpu::TexelCopyBufferInfo {
297
+ buffer: &staging,
298
+ layout: wgpu::TexelCopyBufferLayout {
299
+ offset: 0,
300
+ bytes_per_row: Some(bpr),
301
+ rows_per_image: Some(rt_h),
302
+ },
303
+ },
304
+ wgpu::Extent3d { width: rt_w, height: rt_h, depth_or_array_layers: 1 },
305
+ );
306
+ queue.submit(std::iter::once(encoder.finish()));
307
+
308
+ let slice = staging.slice(..);
309
+ let (tx, rx) = std::sync::mpsc::channel();
310
+ slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); });
311
+ let _ = device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None });
312
+ rx.recv().expect("map sender").expect("map failed");
313
+ let data = slice.get_mapped_range();
314
+
315
+ // Sample the centre texel. Rgba16Float = 4 × half = 8 bytes.
316
+ // Use half::f16 via bytemuck cast through u16 then manual decode.
317
+ let cx = rt_w / 2;
318
+ let cy = rt_h / 2;
319
+ let row_start = (cy * bpr) as usize;
320
+ let texel_start = row_start + (cx as usize) * 8;
321
+ let halfs: [u16; 4] = [
322
+ u16::from_le_bytes([data[texel_start], data[texel_start + 1]]),
323
+ u16::from_le_bytes([data[texel_start + 2], data[texel_start + 3]]),
324
+ u16::from_le_bytes([data[texel_start + 4], data[texel_start + 5]]),
325
+ u16::from_le_bytes([data[texel_start + 6], data[texel_start + 7]]),
326
+ ];
327
+ drop(data);
328
+ staging.unmap();
329
+
330
+ let r = f16_to_f32(halfs[0]);
331
+ let g = f16_to_f32(halfs[1]);
332
+ let b = f16_to_f32(halfs[2]);
333
+ let _a = f16_to_f32(halfs[3]);
334
+
335
+ // Expected SrcAlpha/OneMinusSrcAlpha blend:
336
+ // src = (1, 0, 0, 0.5)
337
+ // dst = (0, 1, 1, 1)
338
+ // out.rgb = src.rgb * src.a + dst.rgb * (1 - src.a)
339
+ // = (0.5, 0.5, 0.5)
340
+ // Allow 1/256 tolerance for half-precision round-trip.
341
+ let eps = 0.02;
342
+ assert!((r - 0.5).abs() < eps, "red channel = {} (expected ~0.5)", r);
343
+ assert!((g - 0.5).abs() < eps, "green channel = {} (expected ~0.5)", g);
344
+ assert!((b - 0.5).abs() < eps, "blue channel = {} (expected ~0.5)", b);
345
+ }
346
+
347
+ /// IEEE-754 binary16 → binary32. We don't pull in the `half` crate
348
+ /// for a single readback; the manual decode is short and exact for
349
+ /// the values this test produces (no NaN / Inf / subnormal cases).
350
+ fn f16_to_f32(bits: u16) -> f32 {
351
+ let sign = (bits >> 15) & 0x1;
352
+ let exp = (bits >> 10) & 0x1f;
353
+ let frac = bits & 0x3ff;
354
+ if exp == 0 {
355
+ if frac == 0 {
356
+ return if sign == 1 { -0.0 } else { 0.0 };
357
+ }
358
+ // Subnormal — not expected in this test; decode for completeness.
359
+ let f = (frac as f32) / 1024.0 * (2.0f32).powi(-14);
360
+ return if sign == 1 { -f } else { f };
361
+ }
362
+ if exp == 0x1f {
363
+ return f32::NAN; // Inf or NaN — unexpected in this test.
364
+ }
365
+ let f = (1.0 + (frac as f32) / 1024.0) * (2.0f32).powi(exp as i32 - 15);
366
+ if sign == 1 { -f } else { f }
367
+ }
368
+
369
+ /// Samples layer 0 of the albedo texture array and writes it straight out.
370
+ /// If the array is bound, this is the array's colour; if the stub is bound,
371
+ /// it is the stub's.
372
+ const ARRAY_SAMPLING_WGSL: &str = r#"
373
+ #include "material_abi.wgsl"
374
+
375
+ struct VsOut {
376
+ @builtin(position) clip_position: vec4<f32>,
377
+ };
378
+
379
+ @vertex
380
+ fn vs_main(in: VertexInput) -> VsOut {
381
+ var out: VsOut;
382
+ out.clip_position = draw.mvp * vec4<f32>(in.position, 1.0);
383
+ return out;
384
+ }
385
+
386
+ @fragment
387
+ fn fs_main(_in: VsOut) -> TranslucentOut {
388
+ var out: TranslucentOut;
389
+ let c = textureSampleLevel(albedo_array, albedo_array_samp, vec2<f32>(0.5, 0.5), 0, 0.0);
390
+ out.hdr = vec4<f32>(c.rgb, 1.0);
391
+ return out;
392
+ }
393
+ "#;
394
+
395
+ /// EN-014 regression, end-to-end.
396
+ ///
397
+ /// `set_user_params` used to rebuild the per-material bind group with the
398
+ /// 1×1 stub array HARDCODED on bindings 14/15/16, so this sequence —
399
+ ///
400
+ /// set_material_texture_array(m, ALBEDO, arr); // link the art
401
+ /// set_material_params(m, [...]); // ...and silently lose it
402
+ ///
403
+ /// — left the material sampling the stub. Nothing errored, no validation
404
+ /// complained; the pixels just never appeared. It cost an afternoon in the
405
+ /// VFX round, with particles and decals both simulating perfectly and
406
+ /// rendering nothing.
407
+ ///
408
+ /// Asserting the *link* survives is not enough (it always did — it was the
409
+ /// bind group that lost it), so this renders a quad that samples the array
410
+ /// and reads the pixel back. Green means the array is bound; the stub is
411
+ /// not green.
412
+ #[test]
413
+ fn set_user_params_preserves_a_linked_texture_array() {
414
+ let Some((device, queue)) = try_create_device() else { return; };
415
+ let joint_buf = make_joint_buffer(&device);
416
+ let mut sys = MaterialSystem::new(&device, &queue, &joint_buf);
417
+
418
+ let handle = sys.compile(
419
+ &device,
420
+ ARRAY_SAMPLING_WGSL,
421
+ FragmentProfile::Translucent,
422
+ Bucket::Transparent,
423
+ false,
424
+ false,
425
+ wgpu::TextureFormat::Rgba16Float,
426
+ wgpu::TextureFormat::Rg8Unorm,
427
+ wgpu::TextureFormat::Rg16Float,
428
+ wgpu::TextureFormat::Rgba8Unorm,
429
+ formats::DEPTH_FORMAT,
430
+ ).expect("array-sampling material compiles");
431
+
432
+ // One 2×2 layer of pure green (linear Rgba8 → format code 1).
433
+ let px: [u8; 2 * 2 * 4] = [
434
+ 0, 255, 0, 255, 0, 255, 0, 255,
435
+ 0, 255, 0, 255, 0, 255, 0, 255,
436
+ ];
437
+ let arr = sys.create_texture_array_ex(&device, &queue, &[(&px[..], 2, 2)], 1, 1);
438
+ assert!(arr != 0, "texture array allocates");
439
+
440
+ // Link the array, THEN set params — the order that used to break.
441
+ // (Clone the stub probe view: set_material_texture_array borrows `sys`
442
+ // mutably and would otherwise conflict with holding a & into it.)
443
+ let probe_view = sys.default_black_view.clone();
444
+ sys.set_material_texture_array(&device, handle, 0, arr, &probe_view);
445
+ sys.set_user_params(&device, &queue, handle, &[0u8; 16]).expect("params set");
446
+
447
+ // Render it.
448
+ let pf = PerFrameUniforms {
449
+ time: 0.0, delta_time: 0.0, frame_index: 0, _pad0: 0,
450
+ screen_resolution: [64.0, 64.0], render_resolution: [64.0, 64.0],
451
+ taa_jitter: [0.0; 2], _pad1: [0.0; 2], wind: [0.0; 4],
452
+ cloud: [0.0; 4],
453
+ };
454
+ let pv = bytemuck::Zeroable::zeroed();
455
+ sys.update_frame_uniforms(&queue, &pf, &pv);
456
+ sys.reset_draw_slot(crate::renderer::IDENTITY_MAT4);
457
+
458
+ let identity = crate::renderer::IDENTITY_MAT4;
459
+ let (vb, ib, icount) = make_fullscreen_tri(&device, &queue);
460
+ sys.submit_draw(
461
+ &device, &queue, &joint_buf,
462
+ handle, 1, 0, identity, identity, identity, [1.0; 4], [0; 4],
463
+ );
464
+
465
+ let (rt_w, rt_h) = (64u32, 64u32);
466
+ let hdr_rt = device.create_texture(&wgpu::TextureDescriptor {
467
+ label: Some("test_arr_hdr"),
468
+ size: wgpu::Extent3d { width: rt_w, height: rt_h, depth_or_array_layers: 1 },
469
+ mip_level_count: 1, sample_count: 1,
470
+ dimension: wgpu::TextureDimension::D2,
471
+ format: wgpu::TextureFormat::Rgba16Float,
472
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
473
+ view_formats: &[],
474
+ });
475
+ let hdr_view = hdr_rt.create_view(&Default::default());
476
+ let depth_rt = device.create_texture(&wgpu::TextureDescriptor {
477
+ label: Some("test_arr_depth"),
478
+ size: wgpu::Extent3d { width: rt_w, height: rt_h, depth_or_array_layers: 1 },
479
+ mip_level_count: 1, sample_count: 1,
480
+ dimension: wgpu::TextureDimension::D2,
481
+ format: formats::DEPTH_FORMAT,
482
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
483
+ view_formats: &[],
484
+ });
485
+ let depth_view = depth_rt.create_view(&Default::default());
486
+
487
+ let mut encoder = device.create_command_encoder(&Default::default());
488
+ {
489
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
490
+ label: Some("test_arr_pass"),
491
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
492
+ view: &hdr_view,
493
+ resolve_target: None,
494
+ depth_slice: None,
495
+ ops: wgpu::Operations {
496
+ // Clear to RED: if the draw never lands, or the stub
497
+ // (white/black) is sampled, the assert below fails loudly
498
+ // instead of accidentally passing.
499
+ load: wgpu::LoadOp::Clear(wgpu::Color { r: 1.0, g: 0.0, b: 0.0, a: 1.0 }),
500
+ store: wgpu::StoreOp::Store,
501
+ },
502
+ })],
503
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
504
+ view: &depth_view,
505
+ depth_ops: Some(wgpu::Operations {
506
+ load: wgpu::LoadOp::Clear(1.0),
507
+ store: wgpu::StoreOp::Store,
508
+ }),
509
+ stencil_ops: None,
510
+ }),
511
+ timestamp_writes: None,
512
+ occlusion_query_set: None,
513
+ multiview_mask: None,
514
+ });
515
+ sys.dispatch_translucent(&mut pass, |mh, _idx| {
516
+ if mh == 1 { Some((&vb, &ib, icount)) } else { None }
517
+ });
518
+ }
519
+
520
+ let bpr = ((rt_w * 8) + 255) & !255;
521
+ let staging = device.create_buffer(&wgpu::BufferDescriptor {
522
+ label: Some("test_arr_staging"),
523
+ size: (bpr * rt_h) as u64,
524
+ usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
525
+ mapped_at_creation: false,
526
+ });
527
+ encoder.copy_texture_to_buffer(
528
+ wgpu::TexelCopyTextureInfo {
529
+ texture: &hdr_rt, mip_level: 0,
530
+ origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All,
531
+ },
532
+ wgpu::TexelCopyBufferInfo {
533
+ buffer: &staging,
534
+ layout: wgpu::TexelCopyBufferLayout {
535
+ offset: 0, bytes_per_row: Some(bpr), rows_per_image: Some(rt_h),
536
+ },
537
+ },
538
+ wgpu::Extent3d { width: rt_w, height: rt_h, depth_or_array_layers: 1 },
539
+ );
540
+ queue.submit(std::iter::once(encoder.finish()));
541
+
542
+ let slice = staging.slice(..);
543
+ let (tx, rx) = std::sync::mpsc::channel();
544
+ slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); });
545
+ let _ = device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None });
546
+ rx.recv().expect("map sender").expect("map failed");
547
+ let data = slice.get_mapped_range();
548
+
549
+ let texel = ((rt_h / 2) * bpr) as usize + ((rt_w / 2) as usize) * 8;
550
+ let r = f16_to_f32(u16::from_le_bytes([data[texel], data[texel + 1]]));
551
+ let g = f16_to_f32(u16::from_le_bytes([data[texel + 2], data[texel + 3]]));
552
+ drop(data);
553
+ staging.unmap();
554
+
555
+ assert!(
556
+ g > 0.9 && r < 0.1,
557
+ "material sampled the stub array, not the linked one \
558
+ (expected green, got r={r:.2} g={g:.2}) — set_user_params clobbered \
559
+ the texture-array binding",
560
+ );
561
+ }
562
+ }
563
+
564
+ #[cfg(test)]
565
+ mod translucent_sort_tests {
566
+ use super::*;
567
+
568
+ fn cmd(material: MaterialHandle, view_depth: f32) -> MaterialDrawCommand {
569
+ MaterialDrawCommand {
570
+ material,
571
+ mesh_handle: 0,
572
+ mesh_idx: 0,
573
+ draw_slot: 0,
574
+ view_depth,
575
+ instance: None,
576
+ // Identity — the sort under test only reads view_depth.
577
+ model: [
578
+ [1.0, 0.0, 0.0, 0.0],
579
+ [0.0, 1.0, 0.0, 0.0],
580
+ [0.0, 0.0, 1.0, 0.0],
581
+ [0.0, 0.0, 0.0, 1.0],
582
+ ],
583
+ }
584
+ }
585
+
586
+ #[test]
587
+ fn sorts_back_to_front_keeping_equal_depth_submission_order() {
588
+ let mut ms_cmds = vec![
589
+ cmd(1, 5.0),
590
+ cmd(2, 20.0),
591
+ cmd(3, 10.0),
592
+ cmd(4, 10.0), // same depth as material 3 — must stay after it
593
+ cmd(5, 1.0),
594
+ ];
595
+ // exercise the same comparator sort_translucent uses
596
+ ms_cmds.sort_by(|a, b| b.view_depth.total_cmp(&a.view_depth));
597
+ let order: Vec<MaterialHandle> = ms_cmds.iter().map(|c| c.material).collect();
598
+ assert_eq!(order, vec![2, 3, 4, 1, 5]);
599
+ }
600
+
601
+ }
@@ -0,0 +1,41 @@
1
+ //! wasm32-only material bind-group helpers, split out of
2
+ //! `material_system.rs` to keep that file under the 2000-line policy (the
3
+ //! same reason `material_system_tests.rs` is a `#[path]` child). The whole
4
+ //! module is gated `#[cfg(target_arch = "wasm32")]` at its declaration in
5
+ //! `renderer/mod.rs`, so nothing here compiles on native.
6
+
7
+ /// EN-063 — wasm32-only per_frame bind group builder: the PerFrame UBO
8
+ /// at binding 0 plus the seven folded SceneInputs resources at
9
+ /// `WASM_SCENE_INPUTS_BASE..+6` (order and types mirror
10
+ /// `update_scene_inputs` / `create_scene_inputs_layout` exactly). The
11
+ /// single creation path for every bind group made against the wasm32
12
+ /// `abi_per_frame` layout — that layout requires all eight entries.
13
+ #[allow(clippy::too_many_arguments)]
14
+ pub(crate) fn build_per_frame_bg_wasm(
15
+ device: &wgpu::Device,
16
+ layout: &wgpu::BindGroupLayout,
17
+ per_frame_buffer: &wgpu::Buffer,
18
+ scene_color_view: &wgpu::TextureView,
19
+ scene_color_samp: &wgpu::Sampler,
20
+ scene_depth_view: &wgpu::TextureView,
21
+ scene_depth_samp: &wgpu::Sampler,
22
+ impulse_view: &wgpu::TextureView,
23
+ impulse_samp: &wgpu::Sampler,
24
+ motion_vectors_view: &wgpu::TextureView,
25
+ ) -> wgpu::BindGroup {
26
+ use super::material_pipeline::WASM_SCENE_INPUTS_BASE as B;
27
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
28
+ label: Some("material_per_frame_bg"),
29
+ layout,
30
+ entries: &[
31
+ wgpu::BindGroupEntry { binding: 0, resource: per_frame_buffer.as_entire_binding() },
32
+ wgpu::BindGroupEntry { binding: B, resource: wgpu::BindingResource::TextureView(scene_color_view) },
33
+ wgpu::BindGroupEntry { binding: B + 1, resource: wgpu::BindingResource::Sampler(scene_color_samp) },
34
+ wgpu::BindGroupEntry { binding: B + 2, resource: wgpu::BindingResource::TextureView(scene_depth_view) },
35
+ wgpu::BindGroupEntry { binding: B + 3, resource: wgpu::BindingResource::Sampler(scene_depth_samp) },
36
+ wgpu::BindGroupEntry { binding: B + 4, resource: wgpu::BindingResource::TextureView(impulse_view) },
37
+ wgpu::BindGroupEntry { binding: B + 5, resource: wgpu::BindingResource::Sampler(impulse_samp) },
38
+ wgpu::BindGroupEntry { binding: B + 6, resource: wgpu::BindingResource::TextureView(motion_vectors_view) },
39
+ ],
40
+ })
41
+ }