@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,700 @@
1
+ // Material pipeline — the entry point for custom WGSL materials against
2
+ // the ABI described in docs/rfc/0001-material-render-graph.md.
3
+ //
4
+ // This module is deliberately self-contained. It owns the bind-group
5
+ // layouts, pipeline-layout composition, and the preprocessor driver.
6
+ // The rest of the renderer doesn't know or care how a material is
7
+ // compiled — it just receives a `MaterialPipeline` and uses the
8
+ // layouts for binding groups that the shader declares it consumes.
9
+ //
10
+ // Phase 1b scope: everything needed to *compile* a material pipeline
11
+ // from user WGSL against the ABI. Draw dispatch, per-draw uniform
12
+ // writes, and FFI glue all land in follow-up phases.
13
+
14
+ use super::shader_include::{BakedSource, IncludeError, process};
15
+
16
+ // =====================================================================
17
+ // Bind-group layouts — one struct, five layouts, matching RFC §1
18
+ // =====================================================================
19
+
20
+ /// The five bind-group layouts every ABI-compliant pipeline binds.
21
+ /// Owned by Renderer once per process (not per pipeline). Cheap to clone
22
+ /// references since `wgpu::BindGroupLayout` is Arc'd internally.
23
+ pub struct MaterialAbiLayouts {
24
+ pub per_frame: wgpu::BindGroupLayout,
25
+ pub per_view: wgpu::BindGroupLayout,
26
+ pub per_material: wgpu::BindGroupLayout,
27
+ pub per_draw: wgpu::BindGroupLayout,
28
+ pub scene_inputs: wgpu::BindGroupLayout,
29
+ }
30
+
31
+ impl MaterialAbiLayouts {
32
+ pub fn create(device: &wgpu::Device) -> Self {
33
+ Self {
34
+ per_frame: create_per_frame_layout(device),
35
+ per_view: create_per_view_layout(device),
36
+ per_material: create_per_material_layout(device),
37
+ per_draw: create_per_draw_layout(device),
38
+ scene_inputs: create_scene_inputs_layout(device),
39
+ }
40
+ }
41
+ }
42
+
43
+ /// EN-063 — wasm32 ABI contract. WebGPU in the browser caps
44
+ /// `maxBindGroups` at 4, so the SceneInputs group (group 4 on native)
45
+ /// cannot exist on the web: one reads_scene pipeline would fail
46
+ /// creation and poison the whole frame's command buffer. On wasm32
47
+ /// the seven scene-input bindings fold into the per_frame group
48
+ /// (group 0) at bindings `WASM_SCENE_INPUTS_BASE .. +6`, in the same
49
+ /// order and with the same types as `create_scene_inputs_layout`.
50
+ /// per_frame's only native binding is 0 (the PerFrame UBO), so the
51
+ /// folded block starts at 1.
52
+ ///
53
+ /// Three places must agree on this contract and all read this
54
+ /// constant:
55
+ /// 1. the shader-source rewrite in `compile_material`
56
+ /// (`rewrite_scene_inputs_for_wasm`),
57
+ /// 2. the wasm32 `create_per_frame_layout` below,
58
+ /// 3. `MaterialSystem`'s per_frame bind-group builder
59
+ /// (`build_per_frame_bg_wasm` in material_system.rs).
60
+ ///
61
+ /// Native targets keep the five-group layout bit-identically.
62
+ #[cfg(target_arch = "wasm32")]
63
+ pub const WASM_SCENE_INPUTS_BASE: u32 = 1;
64
+
65
+ #[cfg(not(target_arch = "wasm32"))]
66
+ fn create_per_frame_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
67
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
68
+ label: Some("abi_per_frame"),
69
+ entries: &[wgpu::BindGroupLayoutEntry {
70
+ binding: 0,
71
+ visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
72
+ ty: wgpu::BindingType::Buffer {
73
+ ty: wgpu::BufferBindingType::Uniform,
74
+ has_dynamic_offset: false,
75
+ min_binding_size: None,
76
+ },
77
+ count: None,
78
+ }],
79
+ })
80
+ }
81
+
82
+ /// EN-063 — wasm32 variant: the PerFrame UBO at binding 0 plus the
83
+ /// seven folded SceneInputs entries (same types, same order as
84
+ /// `create_scene_inputs_layout`) at `WASM_SCENE_INPUTS_BASE..+6`.
85
+ /// Every bind group created against this layout must supply all
86
+ /// eight entries — see `build_per_frame_bg_wasm` in
87
+ /// material_system.rs, the single creation site.
88
+ #[cfg(target_arch = "wasm32")]
89
+ fn create_per_frame_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
90
+ const B: u32 = WASM_SCENE_INPUTS_BASE;
91
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
92
+ label: Some("abi_per_frame"),
93
+ entries: &[
94
+ entry_ubo(0, wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT),
95
+ entry_tex_f(B, wgpu::ShaderStages::FRAGMENT),
96
+ entry_samp(B + 1, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::Filtering),
97
+ entry_tex_depth(B + 2, wgpu::ShaderStages::FRAGMENT),
98
+ entry_samp(B + 3, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::NonFiltering),
99
+ entry_tex_f_nonfilt(B + 4, wgpu::ShaderStages::FRAGMENT),
100
+ entry_samp(B + 5, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::NonFiltering),
101
+ entry_tex_f(B + 6, wgpu::ShaderStages::FRAGMENT),
102
+ ],
103
+ })
104
+ }
105
+
106
+ fn create_per_view_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
107
+ // Mirror of the ABI header: UBO at 0, env colour + sampler at 1+2,
108
+ // env diffuse at 3, BRDF LUT + sampler at 4+5, three cascades at
109
+ // 6..8, comparison sampler at 9.
110
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
111
+ label: Some("abi_per_view"),
112
+ entries: &[
113
+ entry_ubo(0, wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT),
114
+ entry_tex_f(1, wgpu::ShaderStages::FRAGMENT),
115
+ entry_samp(2, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::Filtering),
116
+ entry_tex_f(3, wgpu::ShaderStages::FRAGMENT),
117
+ entry_tex_f(4, wgpu::ShaderStages::FRAGMENT),
118
+ entry_samp(5, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::Filtering),
119
+ entry_tex_depth(6, wgpu::ShaderStages::FRAGMENT),
120
+ entry_tex_depth(7, wgpu::ShaderStages::FRAGMENT),
121
+ entry_tex_depth(8, wgpu::ShaderStages::FRAGMENT),
122
+ entry_samp(9, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::Comparison),
123
+ ],
124
+ })
125
+ }
126
+
127
+ fn create_per_material_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
128
+ // PBR texture + sampler pairs: base, normal, mr, emissive, occlusion.
129
+ // Bindings 0..9 are texture/sampler pairs. Binding 10 is
130
+ // MaterialFactors UBO. Binding 11 is the user_params UBO (shader-
131
+ // defined type; 256-byte cap enforced by the pipeline-creation
132
+ // helper, not by the layout itself). Bindings 12 (texture) + 13
133
+ // (sampler) are the EN-011 planar reflection RT — bound to the
134
+ // engine's 1×1 black default for materials without a probe so
135
+ // unconditional sampling is always safe. Bindings 14/15/16/17 are
136
+ // the EN-014 texture-array slots (albedo / normal / MR + a shared
137
+ // sampler) for splat-mapped terrain materials — bound to a 1×1×1
138
+ // black stub array when a material doesn't declare its own.
139
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
140
+ label: Some("abi_per_material"),
141
+ entries: &[
142
+ entry_tex_f(0, wgpu::ShaderStages::FRAGMENT),
143
+ entry_samp(1, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::Filtering),
144
+ entry_tex_f(2, wgpu::ShaderStages::FRAGMENT),
145
+ entry_samp(3, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::Filtering),
146
+ entry_tex_f(4, wgpu::ShaderStages::FRAGMENT),
147
+ entry_samp(5, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::Filtering),
148
+ entry_tex_f(6, wgpu::ShaderStages::FRAGMENT),
149
+ entry_samp(7, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::Filtering),
150
+ entry_tex_f(8, wgpu::ShaderStages::FRAGMENT),
151
+ entry_samp(9, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::Filtering),
152
+ entry_ubo(10, wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT),
153
+ entry_ubo(11, wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT),
154
+ // EN-011 — planar reflection RT (texture + sampler).
155
+ entry_tex_f(12, wgpu::ShaderStages::FRAGMENT),
156
+ entry_samp(13, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::Filtering),
157
+ // EN-014 — texture-array slots for splat-mapped terrain.
158
+ entry_tex_f_array(14, wgpu::ShaderStages::FRAGMENT),
159
+ entry_tex_f_array(15, wgpu::ShaderStages::FRAGMENT),
160
+ entry_tex_f_array(16, wgpu::ShaderStages::FRAGMENT),
161
+ entry_samp(17, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::Filtering),
162
+ ],
163
+ })
164
+ }
165
+
166
+ fn create_per_draw_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
167
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
168
+ label: Some("abi_per_draw"),
169
+ entries: &[
170
+ entry_ubo(0, wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT),
171
+ entry_ubo(1, wgpu::ShaderStages::VERTEX), // JointMatrices (1024 × mat4)
172
+ ],
173
+ })
174
+ }
175
+
176
+ fn create_scene_inputs_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
177
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
178
+ label: Some("abi_scene_inputs"),
179
+ entries: &[
180
+ entry_tex_f(0, wgpu::ShaderStages::FRAGMENT),
181
+ entry_samp(1, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::Filtering),
182
+ entry_tex_depth(2, wgpu::ShaderStages::FRAGMENT),
183
+ entry_samp(3, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::NonFiltering),
184
+ // Phase 7 — impulse_tex is R32Float which is non-filterable
185
+ // in wgpu 29 without a feature flag, so the binding is
186
+ // declared NonFiltering. Materials sample via textureLoad
187
+ // (no filtering; 0.5 m / texel is already coarse).
188
+ entry_tex_f_nonfilt(4, wgpu::ShaderStages::FRAGMENT),
189
+ entry_samp(5, wgpu::ShaderStages::FRAGMENT, wgpu::SamplerBindingType::NonFiltering),
190
+ entry_tex_f(6, wgpu::ShaderStages::FRAGMENT),
191
+ ],
192
+ })
193
+ }
194
+
195
+ fn entry_tex_f_nonfilt(binding: u32, vis: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
196
+ wgpu::BindGroupLayoutEntry {
197
+ binding, visibility: vis,
198
+ ty: wgpu::BindingType::Texture {
199
+ sample_type: wgpu::TextureSampleType::Float { filterable: false },
200
+ view_dimension: wgpu::TextureViewDimension::D2,
201
+ multisampled: false,
202
+ },
203
+ count: None,
204
+ }
205
+ }
206
+
207
+ // Small helpers for binding entry construction.
208
+ fn entry_ubo(binding: u32, vis: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
209
+ wgpu::BindGroupLayoutEntry {
210
+ binding, visibility: vis,
211
+ ty: wgpu::BindingType::Buffer {
212
+ ty: wgpu::BufferBindingType::Uniform,
213
+ has_dynamic_offset: false,
214
+ min_binding_size: None,
215
+ },
216
+ count: None,
217
+ }
218
+ }
219
+ fn entry_tex_f(binding: u32, vis: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
220
+ wgpu::BindGroupLayoutEntry {
221
+ binding, visibility: vis,
222
+ ty: wgpu::BindingType::Texture {
223
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
224
+ view_dimension: wgpu::TextureViewDimension::D2,
225
+ multisampled: false,
226
+ },
227
+ count: None,
228
+ }
229
+ }
230
+ /// EN-014 — `texture_2d_array<f32>` binding entry. Used for the
231
+ /// splat-mapped terrain slots (albedo / normal / MR arrays) at
232
+ /// bindings 14/15/16. Filterable so games can use linear filtering
233
+ /// per-layer.
234
+ fn entry_tex_f_array(binding: u32, vis: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
235
+ wgpu::BindGroupLayoutEntry {
236
+ binding, visibility: vis,
237
+ ty: wgpu::BindingType::Texture {
238
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
239
+ view_dimension: wgpu::TextureViewDimension::D2Array,
240
+ multisampled: false,
241
+ },
242
+ count: None,
243
+ }
244
+ }
245
+ fn entry_tex_depth(binding: u32, vis: wgpu::ShaderStages) -> wgpu::BindGroupLayoutEntry {
246
+ wgpu::BindGroupLayoutEntry {
247
+ binding, visibility: vis,
248
+ ty: wgpu::BindingType::Texture {
249
+ sample_type: wgpu::TextureSampleType::Depth,
250
+ view_dimension: wgpu::TextureViewDimension::D2,
251
+ multisampled: false,
252
+ },
253
+ count: None,
254
+ }
255
+ }
256
+ fn entry_samp(binding: u32, vis: wgpu::ShaderStages, ty: wgpu::SamplerBindingType)
257
+ -> wgpu::BindGroupLayoutEntry {
258
+ wgpu::BindGroupLayoutEntry {
259
+ binding, visibility: vis,
260
+ ty: wgpu::BindingType::Sampler(ty),
261
+ count: None,
262
+ }
263
+ }
264
+
265
+ // =====================================================================
266
+ // Fragment output profile — opaque or translucent
267
+ // =====================================================================
268
+
269
+ /// Fragment output profile a material declares. Decides the pipeline's
270
+ /// colour attachment layout and blend state. See ABI §1.8.
271
+ #[derive(Copy, Clone, Debug, Eq, PartialEq)]
272
+ pub enum FragmentProfile {
273
+ /// Four MRT attachments: HDR, material, velocity, albedo.
274
+ Opaque,
275
+ /// Single HDR attachment, alpha-blended. Does not write depth.
276
+ Translucent,
277
+ }
278
+
279
+ /// Scheduling bucket — tells the render graph which pass a draw
280
+ /// belongs in and what sort order to apply. Distinct from
281
+ /// `FragmentProfile` (which describes pipeline outputs): a material
282
+ /// with profile `Translucent` could be in either `Transparent` or
283
+ /// `Refractive`, and `Additive` is its own bucket even though it
284
+ /// shares attachment layout with `Transparent`.
285
+ ///
286
+ /// See RFC 0001 §3.2.
287
+ #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
288
+ pub enum Bucket {
289
+ /// Opaque draws. Front-to-back sort for early-z efficiency. Runs
290
+ /// in the main HDR pass; writes the full 4-MRT G-buffer.
291
+ Opaque,
292
+ /// Alpha-cutout draws (foliage cards, chain-link fences, leaf
293
+ /// silhouettes). Runs in the opaque pass with full G-buffer write
294
+ /// + sun shadow + SSAO, but the fragment shader is expected to
295
+ /// `discard` against `MaterialFactors.alpha_cutoff`. Rendered
296
+ /// double-sided so foliage is visible from both faces.
297
+ Cutout,
298
+ /// Translucent draws. Back-to-front sort for correct blending.
299
+ /// Single HDR attachment, alpha-blended, depth-test without
300
+ /// depth-write. Runs in the translucent sub-pass (Phase 4b).
301
+ Transparent,
302
+ /// Translucent + reads the scene colour snapshot (for refraction,
303
+ /// Fresnel, shoreline effects). Same pass + sort as Transparent
304
+ /// but the graph inserts a SceneColor snapshot before this
305
+ /// bucket runs.
306
+ Refractive,
307
+ /// Additive-blend draws — particle flares, sparks, weapon
308
+ /// glows. Order-independent, so no sort needed. Runs in the
309
+ /// translucent sub-pass.
310
+ Additive,
311
+ }
312
+
313
+ impl Bucket {
314
+ /// True if this bucket dispatches in the translucent sub-pass
315
+ /// (single HDR attachment, alpha/additive blending) rather than
316
+ /// the main HDR pass.
317
+ pub fn is_translucent(self) -> bool {
318
+ matches!(self, Bucket::Transparent | Bucket::Refractive | Bucket::Additive)
319
+ }
320
+ /// True if this bucket requires a SceneColor snapshot before it
321
+ /// runs. Only Refractive does today; future buckets (e.g.
322
+ /// VolumetricFog) may join.
323
+ pub fn needs_scene_color(self) -> bool {
324
+ matches!(self, Bucket::Refractive)
325
+ }
326
+ }
327
+
328
+ // =====================================================================
329
+ // Material pipeline — the compiled artefact
330
+ // =====================================================================
331
+
332
+ /// A pipeline ready to receive draws. Owns only the `RenderPipeline`;
333
+ /// the layouts are borrowed from the shared `MaterialAbiLayouts`.
334
+ pub struct MaterialPipeline {
335
+ pub pipeline: wgpu::RenderPipeline,
336
+ pub profile: FragmentProfile,
337
+ pub bucket: Bucket,
338
+ pub reads_scene: bool,
339
+ /// EN-001 — true when the pipeline was compiled with the
340
+ /// per-instance vertex layout (slot 1, step_mode = Instance). The
341
+ /// dispatch site uses this as a sanity flag — it doesn't change
342
+ /// what bind groups get bound, only which `set_vertex_buffer(1, …)`
343
+ /// path runs for a given draw command.
344
+ pub wants_instancing: bool,
345
+ /// Label carried through for debug output.
346
+ pub label: String,
347
+ /// EN-011 V2 — sibling pipeline with front-face culling for use in
348
+ /// planar reflection passes. Reflection inverts triangle winding,
349
+ /// so an opaque material that would normally cull back-faces needs
350
+ /// to cull front-faces in the mirrored pass — otherwise single-
351
+ /// sided geometry renders inside-out. Lazily compiled the first
352
+ /// time the material gets linked to a probe via
353
+ /// `MaterialSystem::set_reflection_probe`. Reuses the main
354
+ /// pipeline's shader module — the only difference is `cull_mode`.
355
+ ///
356
+ /// `None` for materials whose original cull mode is already
357
+ /// `None` (translucent, cutout) — flipping a non-culled pipeline
358
+ /// is a no-op so we don't bother compiling a duplicate.
359
+ pub reflection_pipeline: Option<wgpu::RenderPipeline>,
360
+ }
361
+
362
+ /// Options passed to compile a material. Matches the material-descriptor
363
+ /// shape in RFC §3.1, minus the textures / parameters (those are game
364
+ /// data, set per draw).
365
+ pub struct MaterialCompileDesc<'a> {
366
+ pub label: &'a str,
367
+ pub entry_path: &'a str, // e.g. "materials/water.wgsl"
368
+ /// Additional (path, source) entries layered over the baked library.
369
+ /// Game-supplied shaders live here.
370
+ pub extra_sources: &'a [(&'a str, &'a str)],
371
+ pub profile: FragmentProfile,
372
+ pub bucket: Bucket,
373
+ pub reads_scene: bool,
374
+ pub hdr_format: wgpu::TextureFormat,
375
+ pub material_format: wgpu::TextureFormat,
376
+ pub velocity_format: wgpu::TextureFormat,
377
+ pub albedo_format: wgpu::TextureFormat,
378
+ pub depth_format: wgpu::TextureFormat,
379
+ pub vertex_buffers: &'a [wgpu::VertexBufferLayout<'a>],
380
+ /// EN-001 — when true, `compile_material` appends
381
+ /// `InstanceData3D::desc()` to `vertex_buffers` so the pipeline
382
+ /// expects a second VB at slot 1 with step_mode = Instance.
383
+ pub wants_instancing: bool,
384
+ }
385
+
386
+ #[derive(Debug)]
387
+ pub enum MaterialCompileError {
388
+ Include(IncludeError),
389
+ Naga(String),
390
+ Wgpu(String),
391
+ }
392
+ impl From<IncludeError> for MaterialCompileError {
393
+ fn from(e: IncludeError) -> Self { MaterialCompileError::Include(e) }
394
+ }
395
+
396
+ /// Compile a material pipeline. This is the happy-path you call at
397
+ /// `loadMaterial()` time.
398
+ pub fn compile_material(
399
+ device: &wgpu::Device,
400
+ layouts: &MaterialAbiLayouts,
401
+ desc: &MaterialCompileDesc<'_>,
402
+ ) -> Result<MaterialPipeline, MaterialCompileError> {
403
+ // 1. Resolve #include chain against the baked library +
404
+ // game-supplied overlay.
405
+ let baked_entries = BAKED_ENTRIES_SNAPSHOT; // from library snapshot below
406
+ let mut entries: Vec<(&str, &str)> = baked_entries.to_vec();
407
+ for &(p, s) in desc.extra_sources {
408
+ entries.push((p, s));
409
+ }
410
+ let source = BakedSource { entries: &entries };
411
+ let expanded = process(&source, desc.entry_path)?;
412
+
413
+ // EN-063 — WebGPU in the browser caps maxBindGroups at 4, so no
414
+ // shader module may declare group 4 on wasm32. Rewrite the seven
415
+ // engine-owned SceneInputs declarations from material_abi.wgsl
416
+ // into the per_frame group (group 0) at WASM_SCENE_INPUTS_BASE..+6.
417
+ // String replacement is safe here: the declarations exist only in
418
+ // engine-owned material_abi.wgsl with exactly this formatting.
419
+ #[cfg(target_arch = "wasm32")]
420
+ let expanded = rewrite_scene_inputs_for_wasm(expanded);
421
+
422
+ // 2. Create shader module. wgpu's WGSL parser surfaces errors as
423
+ // panics through the default handler; we catch them by
424
+ // pushing the scope and popping on failure.
425
+ let _ = device.push_error_scope(wgpu::ErrorFilter::Validation);
426
+ let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
427
+ label: Some(desc.label),
428
+ source: wgpu::ShaderSource::Wgsl(expanded.into()),
429
+ });
430
+ // Note: we don't poll the error scope here because wgpu 29 returns
431
+ // validation errors synchronously via the device's uncaptured-error
432
+ // handler; callers should install their own handler for hot-reload.
433
+
434
+ // 3. Pipeline layout — always binds groups 0..3; only includes
435
+ // scene_inputs when the material declares it.
436
+ let mut bg_layouts: Vec<Option<&wgpu::BindGroupLayout>> = vec![
437
+ Some(&layouts.per_frame),
438
+ Some(&layouts.per_view),
439
+ Some(&layouts.per_material),
440
+ Some(&layouts.per_draw),
441
+ ];
442
+ // EN-063 — on wasm32 the scene inputs live inside per_frame
443
+ // (group 0, bindings WASM_SCENE_INPUTS_BASE..+6), so the pipeline
444
+ // layout stays at 4 groups: the browser caps maxBindGroups at 4.
445
+ if desc.reads_scene && cfg!(not(target_arch = "wasm32")) {
446
+ bg_layouts.push(Some(&layouts.scene_inputs));
447
+ }
448
+ let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
449
+ label: Some(desc.label),
450
+ bind_group_layouts: &bg_layouts,
451
+ immediate_size: 0,
452
+ });
453
+
454
+ // 4. Colour targets based on profile.
455
+ let opaque_targets = [
456
+ Some(wgpu::ColorTargetState {
457
+ format: desc.hdr_format,
458
+ blend: None,
459
+ write_mask: wgpu::ColorWrites::ALL,
460
+ }),
461
+ Some(wgpu::ColorTargetState {
462
+ format: desc.material_format,
463
+ blend: None,
464
+ write_mask: wgpu::ColorWrites::ALL,
465
+ }),
466
+ Some(wgpu::ColorTargetState {
467
+ format: desc.velocity_format,
468
+ blend: None,
469
+ write_mask: wgpu::ColorWrites::ALL,
470
+ }),
471
+ Some(wgpu::ColorTargetState {
472
+ format: desc.albedo_format,
473
+ blend: None,
474
+ write_mask: wgpu::ColorWrites::ALL,
475
+ }),
476
+ ];
477
+ // Additive bucket uses src+dst on color; alpha-blending buckets
478
+ // (Transparent, Refractive) use SrcAlpha/OneMinusSrcAlpha. Both
479
+ // share the single HDR attachment.
480
+ let additive_blend = wgpu::BlendState {
481
+ color: wgpu::BlendComponent {
482
+ src_factor: wgpu::BlendFactor::One,
483
+ dst_factor: wgpu::BlendFactor::One,
484
+ operation: wgpu::BlendOperation::Add,
485
+ },
486
+ alpha: wgpu::BlendComponent::OVER,
487
+ };
488
+ let translucent_blend = match desc.bucket {
489
+ Bucket::Additive => additive_blend,
490
+ _ => wgpu::BlendState::ALPHA_BLENDING,
491
+ };
492
+ let translucent_targets = [Some(wgpu::ColorTargetState {
493
+ format: desc.hdr_format,
494
+ blend: Some(translucent_blend),
495
+ write_mask: wgpu::ColorWrites::ALL,
496
+ })];
497
+ let targets: &[Option<wgpu::ColorTargetState>] = match desc.profile {
498
+ FragmentProfile::Opaque => &opaque_targets,
499
+ FragmentProfile::Translucent => &translucent_targets,
500
+ };
501
+
502
+ // 5. Depth-stencil — translucent reads depth, doesn't write.
503
+ let depth_stencil = Some(wgpu::DepthStencilState {
504
+ format: desc.depth_format,
505
+ depth_write_enabled: Some(matches!(desc.profile, FragmentProfile::Opaque)),
506
+ depth_compare: Some(wgpu::CompareFunction::Less),
507
+ stencil: wgpu::StencilState::default(),
508
+ bias: wgpu::DepthBiasState::default(),
509
+ });
510
+
511
+ // 6. Vertex + fragment entry points. Convention: `vs_main` / `fs_main`.
512
+ // Materials can override by prefixing their shader with
513
+ // `// @entry vs:foo fs:bar` but the first version is fixed names.
514
+ //
515
+ // EN-001 — when `wants_instancing` is set, append the standard
516
+ // InstanceData3D layout so the pipeline expects a second VB at
517
+ // slot 1 (step_mode = Instance). The owned Vec only lives long
518
+ // enough to be referenced by the RenderPipelineDescriptor.
519
+ let vertex_buffers_owned: Vec<wgpu::VertexBufferLayout<'_>>;
520
+ let vertex_buffers: &[wgpu::VertexBufferLayout<'_>] = if desc.wants_instancing {
521
+ vertex_buffers_owned = desc.vertex_buffers
522
+ .iter()
523
+ .cloned()
524
+ .chain(std::iter::once(crate::renderer::types::InstanceData3D::desc()))
525
+ .collect();
526
+ &vertex_buffers_owned
527
+ } else {
528
+ desc.vertex_buffers
529
+ };
530
+ // Translucent materials (water, glass, particles) are commonly
531
+ // viewed from both sides, so they render double-sided. Cutout
532
+ // materials (foliage cards, chain-link fences) likewise need to
533
+ // be visible from both faces. Plain Opaque materials cull backfaces.
534
+ let main_cull = if matches!(desc.profile, FragmentProfile::Translucent)
535
+ || matches!(desc.bucket, Bucket::Cutout) {
536
+ None
537
+ } else {
538
+ Some(wgpu::Face::Back)
539
+ };
540
+
541
+ let make_pipeline = |cull: Option<wgpu::Face>, label_suffix: &str| {
542
+ let label_owned = format!("{}{}", desc.label, label_suffix);
543
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
544
+ label: Some(&label_owned),
545
+ layout: Some(&pipeline_layout),
546
+ vertex: wgpu::VertexState {
547
+ module: &module,
548
+ entry_point: Some("vs_main"),
549
+ buffers: vertex_buffers,
550
+ compilation_options: Default::default(),
551
+ },
552
+ fragment: Some(wgpu::FragmentState {
553
+ module: &module,
554
+ entry_point: Some("fs_main"),
555
+ targets,
556
+ compilation_options: Default::default(),
557
+ }),
558
+ primitive: wgpu::PrimitiveState {
559
+ topology: wgpu::PrimitiveTopology::TriangleList,
560
+ front_face: wgpu::FrontFace::Ccw,
561
+ cull_mode: cull,
562
+ ..Default::default()
563
+ },
564
+ depth_stencil: depth_stencil.clone(),
565
+ multisample: wgpu::MultisampleState::default(),
566
+ multiview_mask: None,
567
+ cache: None,
568
+ })
569
+ };
570
+
571
+ let pipeline = make_pipeline(main_cull, "");
572
+
573
+ // EN-011 V2 — eagerly compile a reflection sibling with cull_mode
574
+ // flipped (Back → Front). Reflection mirrors world-space, which
575
+ // inverts triangle winding; without this swap, single-sided opaque
576
+ // geometry renders inside-out in the planar-reflection pass.
577
+ //
578
+ // Only build the variant when the original cull mode is `Back` —
579
+ // for `None` (translucent, cutout) and any future `Front`-default
580
+ // case, the reflection variant would be identical to the main
581
+ // pipeline (or already-flipped), so we save the compile cost.
582
+ //
583
+ // The cost when we DO build is a single extra pipeline per
584
+ // material (typically 5-30 in a scene). Compiled here rather
585
+ // than lazily at `set_reflection_probe` time so we never need to
586
+ // stash the WGSL source for a later recompile.
587
+ let reflection_pipeline = match main_cull {
588
+ Some(wgpu::Face::Back) => Some(make_pipeline(Some(wgpu::Face::Front), "_reflection")),
589
+ Some(wgpu::Face::Front) => Some(make_pipeline(Some(wgpu::Face::Back), "_reflection")),
590
+ None => None,
591
+ };
592
+
593
+ Ok(MaterialPipeline {
594
+ pipeline,
595
+ profile: desc.profile,
596
+ bucket: desc.bucket,
597
+ reads_scene: desc.reads_scene,
598
+ wants_instancing: desc.wants_instancing,
599
+ label: desc.label.to_string(),
600
+ reflection_pipeline,
601
+ })
602
+ }
603
+
604
+ /// EN-063 — fold the SceneInputs declarations into group 0 for wasm32.
605
+ ///
606
+ /// Rewrites each of the seven exact strings `@group(4) @binding(N)`
607
+ /// (N = 0..6) that material_abi.wgsl declares to
608
+ /// `@group(0) @binding(WASM_SCENE_INPUTS_BASE + N)`. The rewrite is
609
+ /// unconditional (not gated on reads_scene): every material includes
610
+ /// material_abi.wgsl, and a browser shader module must never declare
611
+ /// group 4 at all. Materials that don't use the scene inputs simply
612
+ /// leave the group-0 bindings statically unused, which wgpu ignores
613
+ /// at pipeline-layout validation exactly as it did for group 4.
614
+ #[cfg(target_arch = "wasm32")]
615
+ fn rewrite_scene_inputs_for_wasm(expanded: String) -> String {
616
+ let had_group4 = expanded.contains("@group(4)");
617
+ let mut out = expanded;
618
+ let mut replaced: u32 = 0;
619
+ for n in 0..7u32 {
620
+ let from = format!("@group(4) @binding({n})");
621
+ let to = format!("@group(0) @binding({})", WASM_SCENE_INPUTS_BASE + n);
622
+ replaced += out.matches(from.as_str()).count() as u32;
623
+ out = out.replace(from.as_str(), to.as_str());
624
+ }
625
+ if had_group4 {
626
+ debug_assert_eq!(
627
+ replaced, 7,
628
+ "material_abi.wgsl scene-input declarations changed; \
629
+ update the EN-063 wasm32 group-4 fold to match"
630
+ );
631
+ debug_assert!(
632
+ !out.contains("@group(4)"),
633
+ "a @group(4) binding survived the EN-063 wasm32 fold — \
634
+ group 4 exceeds the browser's maxBindGroups"
635
+ );
636
+ }
637
+ out
638
+ }
639
+
640
+ // =====================================================================
641
+ // Baked library snapshot
642
+ // =====================================================================
643
+ //
644
+ // `shader_library::library()` returns an `impl ShaderSource`; for the
645
+ // compile path above we need a `&[(&str, &str)]` slice so the
646
+ // BakedSource we build can layer user overrides on top. Mirror the
647
+ // library contents here; kept in sync by a test.
648
+
649
+ const BAKED_ENTRIES_SNAPSHOT: &[(&str, &str)] = &[
650
+ ("material_abi.wgsl", include_str!("../../../shared/shaders/material_abi.wgsl")),
651
+ ("common/pbr.wgsl", include_str!("../../../shared/shaders/common/pbr.wgsl")),
652
+ ("common/shadows.wgsl", include_str!("../../../shared/shaders/common/shadows.wgsl")),
653
+ ("common/fog.wgsl", include_str!("../../../shared/shaders/common/fog.wgsl")),
654
+ ("common/tonemap.wgsl", include_str!("../../../shared/shaders/common/tonemap.wgsl")),
655
+ ("common/sky.wgsl", include_str!("../../../shared/shaders/common/sky.wgsl")),
656
+ ("common/clouds.wgsl", include_str!("../../../shared/shaders/common/clouds.wgsl")),
657
+ ("materials/test_minimal.wgsl", include_str!("../../../shared/shaders/materials/test_minimal.wgsl")),
658
+ ];
659
+
660
+ #[cfg(test)]
661
+ mod tests {
662
+ use super::*;
663
+
664
+ /// Ensure the baked snapshot used by compile_material matches the
665
+ /// shader library. If this fails, an entry was added to the library
666
+ /// but not the snapshot (or vice-versa) — fix either.
667
+ #[test]
668
+ fn snapshot_matches_library() {
669
+ use super::super::shader_include::ShaderSource;
670
+ use super::super::shader_library;
671
+ let lib = shader_library::library();
672
+ for (path, body) in BAKED_ENTRIES_SNAPSHOT {
673
+ let from_lib = lib.fetch(path)
674
+ .unwrap_or_else(|| panic!("snapshot includes '{}' not in library", path));
675
+ assert_eq!(*body, from_lib, "mismatch for {}", path);
676
+ }
677
+ }
678
+
679
+ /// End-to-end validation: resolve the minimal test material's
680
+ /// includes and parse the result through naga (wgpu's WGSL
681
+ /// front-end). If the ABI header has a syntax error, or if a
682
+ /// struct reference is missing, naga fails this test.
683
+ ///
684
+ /// This test does not create a wgpu device — it's pure front-end
685
+ /// parsing — so it runs in any CI or dev environment without a
686
+ /// GPU. The downside is it doesn't verify the full pipeline
687
+ /// descriptor (blend state, vertex buffer layout, etc.); those are
688
+ /// exercised when `compile_material` runs at application startup.
689
+ #[test]
690
+ fn test_minimal_parses_through_naga() {
691
+ let source = BakedSource { entries: BAKED_ENTRIES_SNAPSHOT };
692
+ let expanded = process(&source, "materials/test_minimal.wgsl")
693
+ .expect("preprocessor resolves test_minimal.wgsl");
694
+ let result = wgpu::naga::front::wgsl::parse_str(&expanded);
695
+ if let Err(ref e) = result {
696
+ eprintln!("naga parse error:\n{}", e.emit_to_string(&expanded));
697
+ }
698
+ assert!(result.is_ok(), "test_minimal.wgsl should parse via naga after include expansion");
699
+ }
700
+ }