@woosh/meep-engine 3.11.1 → 3.11.3

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 (225) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/package.json +1 -1
  3. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  4. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +294 -290
  5. package/src/engine/graphics3/ParticipatingMedia.d.ts +111 -0
  6. package/src/engine/graphics3/ParticipatingMedia.d.ts.map +1 -0
  7. package/src/engine/graphics3/ParticipatingMedia.js +203 -0
  8. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts +34 -0
  9. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.d.ts.map +1 -0
  10. package/src/engine/graphics3/ParticipatingMediaSerializationAdapter.js +73 -0
  11. package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts +87 -0
  12. package/src/engine/graphics3/ParticipatingMediaSystem3.d.ts.map +1 -0
  13. package/src/engine/graphics3/ParticipatingMediaSystem3.js +184 -0
  14. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  15. package/src/shade/descriptor/pipeline/PipelineLayoutDescriptor.d.ts +16 -2
  16. package/src/shade/descriptor/pipeline/PipelineLayoutDescriptor.d.ts.map +1 -1
  17. package/src/shade/descriptor/pipeline/PipelineLayoutDescriptor.js +28 -4
  18. package/src/shade/descriptor/pipeline/compute/ComputePipelineDescriptor.d.ts +1 -1
  19. package/src/shade/descriptor/pipeline/compute/ComputePipelineDescriptor.d.ts.map +1 -1
  20. package/src/shade/descriptor/pipeline/compute/ComputePipelineDescriptor.js +5 -2
  21. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +564 -451
  22. package/src/shade/device/ShadeGPUCommandContext.d.ts +18 -2
  23. package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
  24. package/src/shade/device/ShadeGPUCommandContext.js +994 -940
  25. package/src/shade/device/immediate_data_supported.d.ts +33 -0
  26. package/src/shade/device/immediate_data_supported.d.ts.map +1 -0
  27. package/src/shade/device/immediate_data_supported.js +53 -0
  28. package/src/shade/device/mock/SoftwareGPUCommandEncoder.d.ts +18 -0
  29. package/src/shade/device/mock/SoftwareGPUCommandEncoder.d.ts.map +1 -1
  30. package/src/shade/device/mock/SoftwareGPUCommandEncoder.js +24 -0
  31. package/src/shade/device/mock/SoftwareGPUComputePassEncoder.d.ts +7 -0
  32. package/src/shade/device/mock/SoftwareGPUComputePassEncoder.d.ts.map +1 -1
  33. package/src/shade/device/mock/SoftwareGPUComputePassEncoder.js +49 -1
  34. package/src/shade/device/mock/SoftwareGPUDevice.d.ts.map +1 -1
  35. package/src/shade/device/mock/SoftwareGPUDevice.js +20 -2
  36. package/src/shade/device/mock/SoftwareGPUImmediateData.d.ts +65 -0
  37. package/src/shade/device/mock/SoftwareGPUImmediateData.d.ts.map +1 -0
  38. package/src/shade/device/mock/SoftwareGPUImmediateData.js +160 -0
  39. package/src/shade/device/mock/SoftwareGPURenderPassEncoder.d.ts +7 -0
  40. package/src/shade/device/mock/SoftwareGPURenderPassEncoder.d.ts.map +1 -1
  41. package/src/shade/device/mock/SoftwareGPURenderPassEncoder.js +50 -0
  42. package/src/shade/device/mock/pipeline_immediate_size.d.ts +11 -0
  43. package/src/shade/device/mock/pipeline_immediate_size.d.ts.map +1 -0
  44. package/src/shade/device/mock/pipeline_immediate_size.js +18 -0
  45. package/src/shade/device/pipeline/PipelineLayoutManager.d.ts.map +1 -1
  46. package/src/shade/device/pipeline/PipelineLayoutManager.js +1 -0
  47. package/src/shade/playground/volumetrics_froxel/README.md +104 -1
  48. package/src/shade/playground/volumetrics_froxel/index.html +20 -1
  49. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
  50. package/src/shade/playground/volumetrics_froxel/main.js +298 -0
  51. package/src/shade/playground/volumetrics_froxel/measure.d.ts +119 -0
  52. package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -0
  53. package/src/shade/playground/volumetrics_froxel/measure.js +559 -0
  54. package/src/shade/renderer/GraphicsContext.d.ts +9 -2
  55. package/src/shade/renderer/GraphicsContext.d.ts.map +1 -1
  56. package/src/shade/renderer/GraphicsContext.js +13 -1
  57. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  58. package/src/shade/renderer/Renderer.js +22 -18
  59. package/src/shade/renderer/atmosphere/GPUSky.d.ts.map +1 -1
  60. package/src/shade/renderer/atmosphere/GPUSky.js +145 -142
  61. package/src/shade/renderer/atmosphere/MULTISCATTER_LUT_CONFIG.d.ts +34 -0
  62. package/src/shade/renderer/atmosphere/MULTISCATTER_LUT_CONFIG.d.ts.map +1 -0
  63. package/src/shade/renderer/atmosphere/MULTISCATTER_LUT_CONFIG.js +36 -0
  64. package/src/shade/renderer/atmosphere/SAMPLER_SKY_LUT.d.ts +20 -0
  65. package/src/shade/renderer/atmosphere/SAMPLER_SKY_LUT.d.ts.map +1 -0
  66. package/src/shade/renderer/atmosphere/SAMPLER_SKY_LUT.js +25 -0
  67. package/src/shade/renderer/atmosphere/chunk_get_multiscatter_values.d.ts +14 -1
  68. package/src/shade/renderer/atmosphere/chunk_get_multiscatter_values.d.ts.map +1 -1
  69. package/src/shade/renderer/atmosphere/chunk_get_multiscatter_values.js +163 -177
  70. package/src/shade/renderer/atmosphere/chunk_multiscatter_sample_direction.d.ts +20 -0
  71. package/src/shade/renderer/atmosphere/chunk_multiscatter_sample_direction.d.ts.map +1 -0
  72. package/src/shade/renderer/atmosphere/chunk_multiscatter_sample_direction.js +46 -0
  73. package/src/shade/renderer/atmosphere/chunk_sample_multiscatter_lut.d.ts +11 -1
  74. package/src/shade/renderer/atmosphere/chunk_sample_multiscatter_lut.d.ts.map +1 -1
  75. package/src/shade/renderer/atmosphere/chunk_sample_multiscatter_lut.js +57 -27
  76. package/src/shade/renderer/atmosphere/chunk_sample_transmittance_lut.d.ts +19 -1
  77. package/src/shade/renderer/atmosphere/chunk_sample_transmittance_lut.d.ts.map +1 -1
  78. package/src/shade/renderer/atmosphere/chunk_sample_transmittance_lut.js +69 -33
  79. package/src/shade/renderer/atmosphere/chunk_sky_lut_compute_uv.d.ts +13 -0
  80. package/src/shade/renderer/atmosphere/chunk_sky_lut_compute_uv.d.ts.map +1 -1
  81. package/src/shade/renderer/atmosphere/chunk_sky_lut_compute_uv.js +35 -35
  82. package/src/shade/renderer/atmosphere/chunk_sky_lut_params.d.ts +13 -0
  83. package/src/shade/renderer/atmosphere/chunk_sky_lut_params.d.ts.map +1 -0
  84. package/src/shade/renderer/atmosphere/chunk_sky_lut_params.js +38 -0
  85. package/src/shade/renderer/atmosphere/chunk_sky_lut_texel_to_unit.d.ts +17 -0
  86. package/src/shade/renderer/atmosphere/chunk_sky_lut_texel_to_unit.d.ts.map +1 -0
  87. package/src/shade/renderer/atmosphere/chunk_sky_lut_texel_to_unit.js +29 -0
  88. package/src/shade/renderer/atmosphere/chunk_sky_lut_uv.d.ts +21 -0
  89. package/src/shade/renderer/atmosphere/chunk_sky_lut_uv.d.ts.map +1 -0
  90. package/src/shade/renderer/atmosphere/chunk_sky_lut_uv.js +42 -0
  91. package/src/shade/renderer/atmosphere/shader_multiscatter_lut.d.ts +2 -2
  92. package/src/shade/renderer/atmosphere/shader_multiscatter_lut.d.ts.map +1 -1
  93. package/src/shade/renderer/atmosphere/shader_multiscatter_lut.js +165 -84
  94. package/src/shade/renderer/atmosphere/shader_sky_irradiance_lut.d.ts.map +1 -1
  95. package/src/shade/renderer/atmosphere/shader_sky_irradiance_lut.js +215 -204
  96. package/src/shade/renderer/atmosphere/shader_transmittance_lut.d.ts.map +1 -1
  97. package/src/shade/renderer/atmosphere/shader_transmittance_lut.js +79 -67
  98. package/src/shade/renderer/light/cluster/graph_assign_clsuters.d.ts.map +1 -1
  99. package/src/shade/renderer/light/cluster/graph_assign_clsuters.js +6 -2
  100. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts.map +1 -1
  101. package/src/shade/renderer/postprocess/gtao/GTAO.js +4 -0
  102. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  103. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  104. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +341 -345
  105. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts +0 -2
  106. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts.map +1 -1
  107. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.js +58 -57
  108. package/src/shade/renderer/rasterize/expand/mesh/graph_expand_meshes_to_meshlets.d.ts.map +1 -1
  109. package/src/shade/renderer/rasterize/expand/mesh/graph_expand_meshes_to_meshlets.js +7 -3
  110. package/src/shade/renderer/rasterize/fast/graph_extract_meshes_from_bucket.d.ts.map +1 -1
  111. package/src/shade/renderer/rasterize/fast/graph_extract_meshes_from_bucket.js +77 -81
  112. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.d.ts.map +1 -1
  113. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.js +5 -1
  114. package/src/shade/renderer/shader/ComputeShader.d.ts.map +1 -1
  115. package/src/shade/renderer/shader/ComputeShader.js +6 -2
  116. package/src/shade/renderer/shader/ImageShader.d.ts.map +1 -1
  117. package/src/shade/renderer/shader/ImageShader.js +4 -1
  118. package/src/shade/renderer/shader/ShaderDescriptor.d.ts.map +1 -1
  119. package/src/shade/renderer/shader/ShaderDescriptor.js +26 -0
  120. package/src/shade/renderer/shader/chunk/atmosphere/ATMOSPHERE_RADII_DECLARATION.d.ts +18 -0
  121. package/src/shade/renderer/shader/chunk/atmosphere/ATMOSPHERE_RADII_DECLARATION.d.ts.map +1 -0
  122. package/src/shade/renderer/shader/chunk/atmosphere/ATMOSPHERE_RADII_DECLARATION.js +32 -0
  123. package/src/shade/renderer/shader/chunk/atmosphere/PLANET_CENTER_DECLARATION.d.ts +7 -0
  124. package/src/shade/renderer/shader/chunk/atmosphere/PLANET_CENTER_DECLARATION.d.ts.map +1 -1
  125. package/src/shade/renderer/shader/chunk/atmosphere/PLANET_CENTER_DECLARATION.js +17 -6
  126. package/src/shade/renderer/shader/chunk/atmosphere/chunk_atmosphere_ray_bounds.d.ts +21 -0
  127. package/src/shade/renderer/shader/chunk/atmosphere/chunk_atmosphere_ray_bounds.d.ts.map +1 -0
  128. package/src/shade/renderer/shader/chunk/atmosphere/chunk_atmosphere_ray_bounds.js +69 -0
  129. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_contains_aabb3.d.ts +9 -0
  130. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_contains_aabb3.d.ts.map +1 -0
  131. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_contains_aabb3.js +22 -0
  132. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_from_transformed_corners.d.ts +16 -0
  133. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_from_transformed_corners.d.ts.map +1 -0
  134. package/src/shade/renderer/shader/chunk/geometry/aabb3/chunk_aabb3_from_transformed_corners.js +41 -0
  135. package/src/shade/renderer/shader/chunk/geometry/frustum/chunk_frustum_transform_m4.d.ts +11 -0
  136. package/src/shade/renderer/shader/chunk/geometry/frustum/chunk_frustum_transform_m4.d.ts.map +1 -1
  137. package/src/shade/renderer/shader/chunk/geometry/frustum/chunk_frustum_transform_m4.js +15 -19
  138. package/src/shade/renderer/shader/chunk/geometry/frustum/chunk_frustum_transform_plane_matrix.d.ts +23 -0
  139. package/src/shade/renderer/shader/chunk/geometry/frustum/chunk_frustum_transform_plane_matrix.d.ts.map +1 -0
  140. package/src/shade/renderer/shader/chunk/geometry/frustum/chunk_frustum_transform_plane_matrix.js +44 -0
  141. package/src/shade/renderer/shader/chunk/geometry/octahedron/chunk_uv_octahedral_unit_decode.d.ts.map +1 -1
  142. package/src/shade/renderer/shader/chunk/geometry/octahedron/chunk_uv_octahedral_unit_decode.js +1 -4
  143. package/src/shade/renderer/shader/compiler/CodeChunk.d.ts +7 -1
  144. package/src/shade/renderer/shader/compiler/CodeChunk.d.ts.map +1 -1
  145. package/src/shade/renderer/shader/compiler/CodeChunk.js +37 -2
  146. package/src/shade/renderer/shader/graph/compute/compute_binding_read_names.d.ts.map +1 -1
  147. package/src/shade/renderer/shader/graph/compute/compute_binding_read_names.js +6 -0
  148. package/src/shade/renderer/shader/graph/compute/graph_compute_indirect_pass.js +1 -1
  149. package/src/shade/renderer/shader/graph/compute/graph_compute_pass.js +1 -1
  150. package/src/shade/renderer/shader/graph/graph_image_pass.js +1 -1
  151. package/src/shade/renderer/shader/graph/graph_prepare_bindings.d.ts.map +1 -1
  152. package/src/shade/renderer/shader/graph/graph_prepare_bindings.js +7 -0
  153. package/src/shade/renderer/shader/graph/graph_prepare_pass_input_data.d.ts +3 -1
  154. package/src/shade/renderer/shader/graph/graph_prepare_pass_input_data.d.ts.map +1 -1
  155. package/src/shade/renderer/shader/graph/graph_prepare_pass_input_data.js +12 -1
  156. package/src/shade/renderer/shader/pass/RenderPassDescriptor.d.ts.map +1 -1
  157. package/src/shade/renderer/shader/pass/RenderPassDescriptor.js +1 -0
  158. package/src/shade/renderer/shader/resource/IMMEDIATE_DATA.d.ts +21 -0
  159. package/src/shade/renderer/shader/resource/IMMEDIATE_DATA.d.ts.map +1 -0
  160. package/src/shade/renderer/shader/resource/IMMEDIATE_DATA.js +21 -0
  161. package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.d.ts +59 -1
  162. package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.d.ts.map +1 -1
  163. package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.js +195 -2
  164. package/src/shade/renderer/shader/type/type_has_arrays.d.ts +14 -0
  165. package/src/shade/renderer/shader/type/type_has_arrays.d.ts.map +1 -0
  166. package/src/shade/renderer/shader/type/type_has_arrays.js +33 -0
  167. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts +14 -13
  168. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.d.ts.map +1 -1
  169. package/src/shade/renderer/shadow/map/shader/chunk_shadowmap_sample_point2.js +133 -129
  170. package/src/shade/renderer/view/GPUViewSkyContext.d.ts.map +1 -1
  171. package/src/shade/renderer/view/GPUViewSkyContext.js +169 -173
  172. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  173. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +480 -315
  174. package/src/shade/renderer/volumetrics/NOTES.md +887 -8
  175. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +5 -0
  176. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
  177. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.js +21 -2
  178. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts +26 -0
  179. package/src/shade/renderer/volumetrics/SceneVolumetrics.d.ts.map +1 -1
  180. package/src/shade/renderer/volumetrics/SceneVolumetrics.js +64 -21
  181. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.d.ts.map +1 -1
  182. package/src/shade/renderer/volumetrics/VOLUMETRICS_METADATA_STRUCT.js +84 -28
  183. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts +98 -0
  184. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.d.ts.map +1 -0
  185. package/src/shade/renderer/volumetrics/VOLUMETRICS_SUN_SHADOW_RESOLUTION.js +111 -0
  186. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts +84 -0
  187. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.d.ts.map +1 -0
  188. package/src/shade/renderer/volumetrics/build_volumetrics_sun_shadow_transform.js +252 -0
  189. package/src/shade/renderer/volumetrics/chunk_integrate_optical_depth.js +75 -75
  190. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts +43 -0
  191. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.d.ts.map +1 -0
  192. package/src/shade/renderer/volumetrics/chunk_volumetrics_local_light_shadow.js +145 -0
  193. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts +20 -0
  194. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.d.ts.map +1 -0
  195. package/src/shade/renderer/volumetrics/chunk_volumetrics_sample_froxel_extinction.js +48 -0
  196. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts +38 -0
  197. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.d.ts.map +1 -0
  198. package/src/shade/renderer/volumetrics/chunk_volumetrics_sun_optical_depth.js +130 -0
  199. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +18 -3
  200. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  201. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +193 -70
  202. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts +16 -0
  203. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  204. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +83 -15
  205. package/src/shade/renderer/volumetrics/shader_volumetrics_build_participating_media.d.ts.map +1 -1
  206. package/src/shade/renderer/volumetrics/shader_volumetrics_build_participating_media.js +29 -18
  207. package/src/shade/renderer/volumetrics/shader_volumetrics_build_scattering_lut.js +260 -260
  208. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts +58 -0
  209. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.d.ts.map +1 -0
  210. package/src/shade/renderer/volumetrics/shader_volumetrics_build_sun_shadow_volume.js +290 -0
  211. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts +25 -0
  212. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts.map +1 -1
  213. package/src/shade/wgsl/emulator/CPUBitmapData.js +33 -0
  214. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2228 -2215
  215. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  216. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +210 -2
  217. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts +69 -0
  218. package/src/shade/wgsl/emulator/cpu_texture_gather.d.ts.map +1 -0
  219. package/src/shade/wgsl/emulator/cpu_texture_gather.js +148 -0
  220. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts +16 -0
  221. package/src/shade/wgsl/emulator/cpu_texture_sample.d.ts.map +1 -1
  222. package/src/shade/wgsl/emulator/cpu_texture_sample.js +176 -172
  223. package/src/shade/renderer/volumetrics/VOLUMETRICS_WORKGROUP_RESOLUTION.d.ts +0 -2
  224. package/src/shade/renderer/volumetrics/VOLUMETRICS_WORKGROUP_RESOLUTION.d.ts.map +0 -1
  225. package/src/shade/renderer/volumetrics/VOLUMETRICS_WORKGROUP_RESOLUTION.js +0 -1
@@ -1,451 +1,564 @@
1
- # WGSL immediate data in Shade — landing plan, 2026-08-28
2
-
3
- How to bring the WGSL `immediate` address space (`var<immediate>` + `GPUBindingCommandsMixin.setImmediates`)
4
- into Shade, starting from `ShadeGPUCommandContext.allocateTransientBuffer` and the call sites that
5
- feed it.
6
-
7
- **Verdict:** worth landing. Shade already models this exact concept, under this exact name, on top of
8
- uniform buffers — nineteen of the twenty structs literally called `PUSH_CONSTANTS` fit inside the
9
- 64-byte budget unmodified. With no fallback in scope the remaining work is small and almost entirely
10
- additive.
11
-
12
- > **Scope decision (rev 2).** An earlier revision of this plan carried a dual-lowering fallback:
13
- > `var<immediate>` where supported, `@group(N) @binding(0) var<uniform>` where not, chosen by a
14
- > capability probe. That is now **out of scope**, and it should never have been in it —
15
- > [`RENDERER_CONTRACT.md` §2](../RENDERER_CONTRACT.md) already states the rule it violated: *"Shade
16
- > requires a WebGPU device that meets a fixed floor. There is no feature detection beyond it and no
17
- > branch that produces a lesser frame."* `ShadeDeviceFailureReason` says the same thing in its header
18
- > comment, citing D6/I-8: no degradation tiers, no fallbacks, no feature-detection branches.
19
- >
20
- > Immediates therefore join the floor rather than getting a compatibility path. That deletes: the
21
- > import-time capability probe and its unrecoverable failure mode, the extra bind group the fallback
22
- > lowering needed, the resulting exclusion of the 7 shaders already at `maxBindGroups: 4`, and the
23
- > `vi.resetModules()` dance every dual-lowering spec would have required. If a fallback is ever
24
- > genuinely wanted, §9 shows why it belongs in a shader variant rather than in the binding machinery.
25
-
26
- ---
27
-
28
- ## 1. What the feature is, restricted to what matters here
29
-
30
- Immediates are WebGPU's portable spelling of Vulkan push constants / D3D12 root constants / Metal
31
- `setBytes`. The parts that constrain this plan:
32
-
33
- | | Value |
34
- |---|---|
35
- | WGSL gate | `requires immediate_address_space;` a **language extension**, not a `GPUFeatureName` |
36
- | Detection | `navigator.gpu.wgslLanguageFeatures.has('immediate_address_space')` synchronous, no adapter needed |
37
- | Size budget | `maxImmediateSize`, default **64 bytes**, multiple of 4, **no separate Compatibility Mode default** |
38
- | Declaration | Module scope, immutable, **no `@group` / `@binding`** |
39
- | Store type | Host-shareable constructible, **arrays and structs containing arrays are disallowed** |
40
- | Count | **At most one** immediate variable statically accessed per entry point |
41
- | Layout rules | Storage-space (natural alignment), **not** uniform-space |
42
- | API | `pass.setImmediates(rangeOffset, data, dataOffset, dataSize)`, on render pass, compute pass and bundle encoders |
43
- | Pipeline layout | `GPUPipelineLayoutDescriptor.immediateSize` (default 0, `maxImmediateSize`, multiple of 4) |
44
- | Init | Bytes start zeroed but **slots start unset**; a draw reading an unset 4-byte slot is a validation error |
45
- | Reset | Per-encoder; survives `setPipeline`, cleared by a new pass and by `executeBundles` |
46
-
47
- Because `maxImmediateSize` is a `maximum`-class limit with a default of 64 and no compat-mode
48
- reduction, **any adapter that exposes the WGSL extension reports at least 64 bytes**. There is no
49
- device where the language feature is present and the budget is not. That is what makes a floor check
50
- sufficient and a runtime branch unnecessary.
51
-
52
- Availability is a browser-version question, not a hardware one: Vulkan's `maxPushConstantsSize` is
53
- core-1.0 with a 128-byte required minimum and no feature bit, and no device in the
54
- `vulkan.gpuinfo.org` corpus reports below 128. Chrome ships immediates in 149–150.
55
-
56
- ---
57
-
58
- ## 2. The current path, traced
59
-
60
- Shade already has this pattern, three layers deep.
61
-
62
- **Declaration** — `ShaderResourceGroupDescriptor.addUniform` (`ShaderResourceGroupDescriptor.js:335`):
63
-
64
- ```js
65
- const PUSH_CONSTANTS = WebGPUStruct.from({
66
- probe_index_offset: u32,
67
- probe_update_count: u32,
68
- probe_resolution: u32,
69
- probe_count: u32
70
- });
71
-
72
- resources.createGroup()
73
- .addUniform("settings", PUSH_CONSTANTS)
74
- .addTexture("tAtlas_radiance");
75
- ```
76
-
77
- **Codegen** — `ShaderResourceSetDescriptor.generateCodeChunk` (`ShaderResourceSetDescriptor.js:305`)
78
- emits `@group(0) @binding(0) var<uniform> settings : PUSH_CONSTANTS;`.
79
-
80
- **Upload, per dispatch** — `ShadeGPUCommandContext.allocateTransientValueBuffer` (`:598`):
81
-
82
- ```js
83
- const settings = context.encoder.allocateTransientValueBuffer(PUSH_CONSTANTS, { });
84
-
85
- shader.dispatch({ encoder: context.encoder, bindings: { settings, } });
86
- ```
87
-
88
- which lands in `allocateTransientBuffer` (`:538`) `GPUBufferAllocator.get` (pooled), then
89
- `writeValueBuffer` (`:620`) → staging buffer → `copyBufferToBuffer`, then a `BindGroupDescriptor` hash
90
- lookup in `GraphicsContext.setPipelineBindings` (`GraphicsContext.js:211`) and finally
91
- `pass.setBindGroup(i, bind_group)`.
92
-
93
- The FrameGraph route reaches the same place: `graph_compute_pass` → `graph_prepare_bindings`
94
- `encoder_prepare_shader_binding` (`:34`), which turns a raw value or `ArrayBuffer` into a transient
95
- uniform buffer on the spot.
96
-
97
- **Scale of the surface:**
98
-
99
- - 67 `allocateTransientValueBuffer` call sites, 11 `allocateTransientBufferAndLoad`, 21 `graph_import_typed_buffer`
100
- - 300 `addUniform` declarations; 22 of those *call sites* name a `*PUSH_CONSTANTS*` type, across
101
- **20 distinct struct definitions** (`LPV_ACCUMULATION_PUSH_CONSTANTS` is shared by four shaders).
102
- The table in §6 is per-struct, so it has 20 rows, not 22
103
- - The single most common transient value type is a bare `u32` (12 sites)
104
-
105
- ---
106
-
107
- ## 3. What the win actually is
108
-
109
- Not bandwidth. 64 bytes at a few hundred dispatches a frame is noise on any bus.
110
-
111
- The win is the machinery each of those 20-byte structs currently drags behind it:
112
-
113
- 1. **A pooled `GPUBuffer` acquire/release per value.** `GPUBufferAllocator.get` plus the transient
114
- tracking array in `ShadeGPUCommandContext`, per dispatch, per frame.
115
- 2. **A staging buffer, a `getMappedRange`, an `unmap` and a `copyBufferToBuffer` per value** —
116
- `writeValueBuffer` does all four for what is often four bytes.
117
- 3. **A `BindGroupDescriptor` construction plus a `HashMap` lookup per value**, and a cache entry
118
- whose key changes every time the value changes so the small-uniform bind groups are the ones
119
- least likely to hit the cache.
120
- 4. **Two JS allocations per value, in the serialiser.** `writeValueBuffer` reaches
121
- `write_gpu_typed_buffer`, which calls `scratch_buffer.fromArrayBuffer(data)`, which constructs a
122
- fresh `DataView` and `Uint8Array` over the target every single call. Per dispatch, per frame, in a
123
- codebase that advertises "zero-allocation" in its own package keywords. The immediate path writes
124
- into a persistent `BinaryBuffer` and allocates nothing (§6, L3).
125
- 5. **A `@binding` slot** in the group the settings share.
126
-
127
- Point 5 is *only* a binding, not a group. Measured across all twenty candidates: the settings uniform
128
- shares its group with textures and storage buffers in every case, at a median of about four resources
129
- per group. No bind group slot is recovered anywhere in the current codebase. (An earlier revision
130
- claimed otherwise and built an argument on it; that argument is withdrawn.)
131
-
132
- **The strongest case is `graph_import_typed_buffer`** (`renderer/shader/graph/graph_import_typed_buffer.js`,
133
- 21 call sites). It does not use the transient pool at all per execution it calls `device.createBuffer`
134
- with `mappedAtCreation`, `getMappedRange`, `unmap`, `copyBufferToBuffer`, then `destroy` on
135
- `encoder.onFinished`. A full buffer create-and-destroy per frame per value, carrying things like
136
- `LPV_ACCUMULATION_PUSH_CONSTANTS` at 20 bytes. The source carries a standing TODO ("this is pretty
137
- inefficient, figure out a way to skip needing 2 buffers") and a 2025-09-13 note that the obvious fix —
138
- transient buffer plus `queue.writeBuffer`produced garbage for reasons never tracked down.
139
- Immediates make the two-buffer dance unnecessary by construction for the ≤64-byte cases rather than
140
- requiring that bug to be found first.
141
-
142
- Caveat: converting a `graph_import_typed_buffer` site is *not* a drop-in. It returns a graph resource
143
- id that passes consume as an ordinary binding, so the consuming pass stops declaring a graph input.
144
- Higher payoff per site, higher cost per site.
145
-
146
- Honest counterweight: on hardware, a 64-byte immediate may well land in a driver-managed constant
147
- bank rather than in registers, so do not promise a shader-side speedup. Promise the removal of
148
- CPU-side per-draw allocation, and measure the rest.
149
-
150
- ---
151
-
152
- ## 4. The floor
153
-
154
- Immediates become a floor requirement, stated in `RENDERER_CONTRACT.md` §2 next to
155
- `indirect-first-instance` and `maxStorageBuffersPerShaderStage ≥ 10`, and checked in `Renderer.js`
156
- using the idiom already there (`Renderer.js:1123`).
157
-
158
- Two checks, both before the device is requested, so the failure names what was missing:
159
-
160
- ```js
161
- // earliest wgslLanguageFeatures lives on navigator.gpu, no adapter required
162
- if (!gpu.wgslLanguageFeatures.has('immediate_address_space')) {
163
- throw ShadeDeviceFailure.below_floor(
164
- "requires WGSL immediate data (Chrome 149+); this browser does not support it"
165
- );
166
- }
167
-
168
- // beside the existing maxStorageBuffersPerShaderStage check, on the adapter
169
- const maxImmediateSize = adapter.limits.maxImmediateSize;
170
- if (maxImmediateSize < 64) {
171
- throw ShadeDeviceFailure.below_floor(
172
- `requires 64 bytes of immediate data, adapter offers ${maxImmediateSize}`
173
- );
174
- }
175
- ```
176
-
177
- `below_floor` is the right reason code: an adapter that does not meet §2, with the detail. Nothing
178
- goes in `requiredFeatures` — `immediate_address_space` is a WGSL language extension, not a
179
- `GPUFeatureName`, and adding it there would make `requestDevice` reject on every device. Nothing goes
180
- in `requiredLimits` either: 64 is the spec default, so a device gets it without asking.
181
-
182
- **When the floor rises.** Not in the plumbing commits — a shader that declares no immediate emits no
183
- `requires` directive and needs nothing. The floor and the checks land in the same commit as the
184
- **first migrated shader** (§8 step 6), so the engine keeps running on older Chrome for as long as it
185
- still can. That is one commit where the browser support statement changes, and it should say so in
186
- its message.
187
-
188
- ---
189
-
190
- ## 5. The one structural obstacle
191
-
192
- `ShaderResourceSetDescriptor` is a list of groups; a group is a list of resources; a resource has a
193
- `binding_index`. Everything downstream — `generateBindings` returning `GPUBindingResource[][]`,
194
- `generatePipelineLayoutDescriptor` producing one `BindGroupLayoutDescriptor` per group,
195
- `generateCodeChunk` prefixing every declaration with `@group(g) @binding(b)` assumes that shape.
196
-
197
- An immediate has no group, no binding, and there is at most one per entry point. It has to live
198
- *beside* the groups, and its value needs a second route to the pass encoder that ends in
199
- `setImmediates` rather than `setBindGroup`.
200
-
201
- Nothing about that requires a device, so the whole thing stays where Shade already does its work:
202
- in descriptors built at module scope. `PipelineLayoutDescriptor` is device-free by design and stays
203
- that way.
204
-
205
- ---
206
-
207
- ## 6. The landing plan
208
-
209
- **L1 declaration + codegen**
210
-
211
- - `ShaderResourceSetDescriptor`: an `#immediate` slot beside `#groups`, a `setImmediate(name, type)`
212
- that rejects a second call, plus validation at declaration time: `type.size <= 64`, no arrays
213
- anywhere in the type (walk it — `WebGPUArray` members and nested structs both), no atomics, size a
214
- multiple of 4.
215
- - `generateCodeChunk` (`ShaderResourceSetDescriptor.js:305`): emit `var<immediate> name : Type;`
216
- first, with no `@group`/`@binding` prefix.
217
- - `ShaderDescriptor.compile` (`ShaderDescriptor.js:53`): the existing mechanism emits
218
- `enable ${extension};`. Language extensions use **`requires`**. That needs a second channel on
219
- `CodeChunk` `addLanguageExtension` beside `addExtension` and a `requires` line in the prelude.
220
- Do not overload `addExtension`; its assert is against `WebGPUExtensionType`, which is the
221
- `GPUFeatureName` namespace.
222
-
223
- **L2 pipeline layout**
224
-
225
- - `PipelineLayoutDescriptor`: an `immediateSize = 0` field, threaded through `from`, `forStage`,
226
- `fromJSON`, `hash`, `equals`, `copy`, `clone`. Missing it from `hash`/`equals` would silently alias
227
- two layouts that differ only in immediate size cheap mistake, expensive symptom.
228
- - `PipelineLayoutManager.#create_pipeline_layout` (`PipelineLayoutManager.js:85`): pass it to
229
- `createPipelineLayout`.
230
- - `generatePipelineLayoutDescriptor` / `generatePipelineLayout`: set it from `roundUp(4, type.size)`.
231
-
232
- **L3 binding resolution and the call**
233
-
234
- **Do not change the return shape of `generateBindings`.** Returning `{ groups, immediate }` would
235
- break all **10** external callers (`GPUParticularRenderer`, `GPUDynamicMeshResidency`,
236
- `graph_draw_lightmap_viz_buffer`, `prototypeParticleSystem`, `graph_atorus_denoise_luma`,
237
- `graph_rasterize_meshes_transparent_oit`, `ComputeShader`, `ImageShader`, `RenderPassDescriptor`,
238
- `GPUWaterRenderer`) and force the new field through the **22** external
239
- `constructRenderPass`/`constructComputePass` call sites. Purely additive instead:
240
-
241
- - `ShaderResourceSetDescriptor.writeImmediate(map, buffer)` a *new* method that **allocates
242
- nothing**. It writes the immediate value into a caller-owned `BinaryBuffer` at that buffer's
243
- current `position`, advances `position` by the type's size, and returns `false` when the shader
244
- declares no immediate. Every existing `generateBindings` caller is untouched.
245
-
246
- Use the already-exported `write_wgsl_type_value(value, buffer, type)`
247
- (`serde/write_gpu_typed_buffer.js:114`), which takes a `BinaryBuffer` directly. **Do not** route
248
- this through `write_gpu_typed_buffer` (`:188`): that wrapper calls
249
- `scratch_buffer.fromArrayBuffer(data)`, and `fromArrayBuffer` constructs `new DataView(data)` and
250
- `new Uint8Array(data)` on every call two allocations per write, and it resets `position` to 0.
251
- The zero-allocation entry point already exists and is public; the wrapper is the thing to avoid.
252
-
253
- - `GraphicsContext.setPipelineBindings` (`GraphicsContext.js:211`) gains an optional fourth
254
- parameter the `BinaryBuffer`, plus the byte offset and length written. After the `setBindGroup`
255
- loop: `pass.setImmediates(0, buffer.data, byte_offset, byte_length)`. `setImmediates` takes
256
- `AllowSharedBufferSource`, and for a plain `ArrayBuffer` the offset and size arguments are in
257
- **bytes**, so the backing store is passed through with no copy on our side. All four existing
258
- callers keep working.
259
-
260
- - `ImageShader.draw` and `ComputeShader.constructPass`/`dispatch` set `position`, call
261
- `writeImmediate`, and thread the buffer plus range through as optional fields.
262
-
263
- Three properties of `BinaryBuffer` that this design has to respect:
264
-
265
- 1. **`data` is not stable.** `setCapacity` (and therefore `ensureCapacity`) reallocates the backing
266
- `ArrayBuffer` and rebinds `dataView` / `__data_uint8`. Read `buffer.data` at the point of the
267
- `setImmediates` call, never cache it and size the buffer once at construction so it never grows
268
- mid-frame.
269
- 2. **Endianness is not the default.** `BinaryBuffer` defaults to `EndianType.LittleEndian`, but the
270
- serde module's own scratch sets `endianness = platform_compute_endianness()` (`:179`) because GPU
271
- buffer contents must match the host. A caller-supplied buffer that skips this writes correct-looking
272
- bytes that are wrong on a big-endian host and identical on the one you test on. Set it at
273
- construction, and assert it in `writeImmediate`.
274
- 3. **Ownership.** One long-lived buffer on `ShadeGPUCommandContext`, sized to `maxImmediateSize` at
275
- construction, is enough `setImmediates` copies the bytes at call time, so the region is free for
276
- reuse immediately after. The API is still parameterised on `position` rather than hardcoding 0,
277
- because that costs nothing and leaves room for a caller that wants to stage several values or keep
278
- a frame log of them.
279
- - `encoder_prepare_shader_binding` (`:34`): in the immediate case, return the raw value rather than
280
- allocating a transient buffer.
281
-
282
- **The eight bypass paths.** `setPipelineBindings` does not cover everything. Eight files drive
283
- `pass.setBindGroup` directly and never touch it — `GPUMaterialManager`, `prototypeParticleSystem`,
284
- `graph_build_depth_mipmap`, `render_copy_texture`, `graph_rasterize_meshes_alpha_tested`,
285
- `graph_rasterize_meshes_transparent_oit`, `draw_texture_as_rgba`, `WebGPUMipmapGenerator`. A shader
286
- migrated in one of those needs its own `setImmediates` call at the right point in the loop. None of
287
- the twenty candidates lives there, so this constrains future migration rather than this landing — but
288
- it must not be assumed away.
289
-
290
- **L4 the test tiers**
291
-
292
- Without a fallback these are not optional: they are the only way immediates get exercised outside a
293
- browser.
294
-
295
- - **WGSL validation** (`wgsl/validate_wgsl_source.js`) teach it that `var<immediate>` is legal and
296
- `requires immediate_address_space;` is a valid directive, so the new declaration does not trip the
297
- foreign-token or undeclared-call rules.
298
- - **JS emulator** — a real trap. `collectGlobalVariable`
299
- (`wgsl/emulator/WGSLJavaScriptCompiler.js:825`) only pushes to `globalBindings` when *both* `group`
300
- and `binding` are non-null (`:862`). A `var<immediate>` has neither, is not `workgroup`, and falls
301
- through to the `privateGlobals` branch, where it is **silently default-initialised**. Every emulator
302
- spec for an immediate-using chunk would pass while reading zeros. Route
303
- `address_space === 'immediate'` into `globalBindings` with a null group/binding so
304
- `emu.bindings.settings = {…}` keeps working by name.
305
- - **SoftwareGPUDevice** — `DEFAULT_LIMITS.maxImmediateSize` (`mock/SoftwareGPUDevice.js:67`) is
306
- already present, set to `0`. Raise it to 64, add `setImmediates` to `SoftwareGPURenderPassEncoder`
307
- (`:130`) and `SoftwareGPUComputePassEncoder` (`:108`) with the spec's `[[immediate_data]]` /
308
- `[[immediate_slots_set]]` state, validate `rangeOffset % 4`, size `% 4` and the range bound, and
309
- snapshot the bytes into each recorded draw/dispatch the way `#snapshot_bindings` already snapshots
310
- bind groups. Follow the folder's stated policy in `mock/IMPLEMENTATION_PLAN.md` §2: validation
311
- errors go through `generate_error`, they do not throw. `createPipelineLayout` (`:766`) gains the
312
- `immediateSize ≤ maxImmediateSize` and `% 4` checks. Emulate the unset-slot rule too — it is the
313
- mock's chance to catch a missing `setImmediates` before a browser does.
314
- - **Playground** one pass converted end to end and run in Chrome before any bulk migration.
315
-
316
- **L5 migration**
317
-
318
- Smallest first. `shader_read_out_bucket` (4 bytes, one `u32`) is the ideal first patient;
319
- `shader_generate_rays` and `shader_shadow_upscale` are the same shape.
320
-
321
- **It is not one line per shader.** Only 5 of the 20 candidates supply their settings implicitly, by
322
- passing a plain object in `bindings` and letting `encoder_prepare_shader_binding` allocate. The other
323
- ~15 have an explicit `allocateTransientValueBuffer` or `graph_import_typed_buffer` call that
324
- pre-builds the buffer and passes the `GPUBuffer`. Those need two edits: the declaration, and the call
325
- site that stops pre-allocating.
326
-
327
- ---
328
-
329
- ## 7. Candidate inventory
330
-
331
- Every struct named `*PUSH_CONSTANTS*`, sized by `WebGPUStruct` (the same natural-alignment layout the
332
- `immediate` address space uses). Sizes were re-derived programmatically — field lists extracted from
333
- source and fed back through the real `WebGPUStruct.from`, not computed by hand.
334
-
335
- | Struct | Size | Verdict |
336
- |---|---:|---|
337
- | `shader_read_out_bucket` | 4 | fits |
338
- | `shader_generate_rays` | 4 | fits |
339
- | `shader_shadow_upscale` | 4 | fits |
340
- | `shader_sharc_evict` | 8 | fits |
341
- | `shader_lightmap_accummulate_tile` | 8 | fits |
342
- | `shader_lightmap_tile_composit` | 8 | fits |
343
- | `graph_lpv_store_sh` | 16 | fits |
344
- | `shader_perfect_perspective` | 16 | fits, see note |
345
- | `graph_lvp_atlas_to_sh` | 20 | fits |
346
- | `LPV_ACCUMULATION_PUSH_CONSTANTS` | 20 | fits |
347
- | `shader_lpv_accumulate_depth` | 20 | fits |
348
- | `graph_sample_light_probes` | 20 | fits |
349
- | `graph_reduce_atlas_to_sh` | 24 | fits |
350
- | `shader_lpv_build_gbuffer` | 24 | fits |
351
- | `shader_update_clipmap_cascade` | 32 | fits |
352
- | `graph_lpv_shade` | 32 | fits |
353
- | `shader_build_scene_sdf_f32` | 32 | fits |
354
- | `shader_scene_sdf_debug_viz` | 32 | fits |
355
- | `shader_scene_sdf_soft_shadows` | 48 | fits |
356
- | `LIGHT_CLUSTER_ASSIGNMENT_PUSH_CONSTANTS` | 128 | **cannot move** |
357
-
358
- `LIGHT_CLUSTER_ASSIGNMENT_PUSH_CONSTANTS` fails twice: 128 bytes against a 64-byte budget, and
359
- `frustum: WebGPUArray.from(vec4f, 6)` is an array, which the `immediate` address space disallows
360
- outright. It stays a uniform buffer. If the rest is worth moving later, the split is
361
- `{cluster_params, screen_resolution, distance_min, distance_max}` at 32 bytes as an immediate with the
362
- frustum left in a buffer a follow-up, not part of this landing.
363
-
364
- Note on `shader_perfect_perspective`: its `use_vignette` member is typed `bool`, and Shade's `bool`
365
- primitive (`WEBGPU_WGSL_PRIMITIVE_TYPES.js:8`) is a 4-byte type. WGSL's host-shareable set is numeric
366
- scalars, vectors, matrices, atomics, arrays and structs of those — `bool` is in none of them, in *any*
367
- address space, uniform included. So this is a pre-existing condition rather than something immediates
368
- introduce, but the member needs to become `u32` before this struct can move. Worth checking whether
369
- that shader is reachable at all; grep finds no importer.
370
-
371
- Beyond the named ones, the 12 bare-`u32` `allocateTransientValueBuffer` sites are the same pattern
372
- without the naming, and are equally good candidates.
373
-
374
- ### What must not move
375
-
376
- `CAMERA_UNIFORM_STRUCT` (~640 bytes: eight `mat4x4f`, a six-element `vec4f` frustum array, and more)
377
- and `VIEW_UNIFORM_STRUCT` account for 116 of the 300 `addUniform` calls. They are per-view, written
378
- once, read by everything over budget, containing arrays, and not per-draw data. They stay. Same for
379
- anything reached through `GPUDatabase` or `GPUSingleTypeTable`.
380
-
381
- ---
382
-
383
- ## 8. Commit sequence
384
-
385
- Steps 1–5 are inert without a shader that declares an immediate, so the engine still runs on older
386
- Chrome throughout. Step 6 is where the floor rises.
387
-
388
- 1. `feat(shade): the software device carries immediate data` L4's mock work alone. Raises
389
- `maxImmediateSize` to 64, adds `setImmediates` to both pass encoders with spec state and
390
- validation, `immediateSize` validation on `createPipelineLayout`.
391
- Note while doing this: the `DEFAULT_LIMITS` doc comment claims the table was "verified field by
392
- field against a Dawn default device (`webgpu` package, already a dependency)". **That package is
393
- not a dependency** `webgpu` appears in `package.json` only as a `keywords` entry, and is absent
394
- from `node_modules`. So `maxImmediateSize: 0` cannot be re-verified from this repo, and raising it
395
- to 64 rests on the spec's `maximum`-class default rather than a Dawn reading. Say that in the
396
- comment instead of repeating an unverifiable citation.
397
- 2. `fix(shade): the emulator does not silently zero an immediate` the `WGSLJavaScriptCompiler`
398
- fall-through at `:862`, plus the `validate_wgsl_source` rules. A spec that fails before and passes
399
- after.
400
- 3. `feat(shade): pipeline layouts carry an immediate size` L2, pinned by a `SoftwareGPUDevice` spec.
401
- 4. `feat(shade): shaders can declare immediate data` L1. The `requires` channel on `CodeChunk`,
402
- `setImmediate` on the set descriptor, the declaration-time validation.
403
- 5. `feat(shade): immediate data reaches the pass encoder` — L3. `generateImmediateBytes`, the optional
404
- `setPipelineBindings` parameter, `encoder_prepare_shader_binding` stops allocating for immediates.
405
- 6. `feat(shade): immediate data joins the device floor` the two `Renderer.js` checks, the
406
- `RENDERER_CONTRACT.md` §2 amendment, **and** the first migrated shader
407
- (`shader_read_out_bucket`), together. This is the commit that raises the browser requirement to
408
- Chrome 149+; its message should say so plainly.
409
- 7. `refactor(shade): <pass> passes its settings as immediate data` — one per shader thereafter.
410
-
411
- Measure between 6 and 7, and again after the first handful of 7s: transient buffer acquisitions per
412
- frame and bind-group cache misses per frame are the two numbers that should move. If they do not,
413
- stop migrating the `allocateTransientValueBuffer` sites and spend the effort on the
414
- `graph_import_typed_buffer` consumers instead, where the per-frame cost is a whole buffer rather than
415
- a pool hit.
416
-
417
- ---
418
-
419
- ## 9. If a fallback is ever wanted anyway
420
-
421
- It belongs in a shader variant, not in the binding machinery and Shade already supports that with
422
- no new mechanism. `ShaderDescriptor.compile()` clones the body chunk and adds the resource set's
423
- generated chunk as a dependency:
424
-
425
- ```js
426
- const chunk = this.#body.clone();
427
- chunk.addDependency(this.#resources.generateCodeChunk());
428
- ```
429
-
430
- So one `CodeChunk` body pairs with two different `ShaderResourceSetDescriptor`s to produce two
431
- `ShaderDescriptor`s immediate and uniform from a single source of shader text, selected by
432
- whoever constructs them. The body never changes: `settings.probe_count` reads identically either way.
433
-
434
- That is a per-shader, opt-in decision made by the shader's author, which is a very different thing
435
- from a capability branch threaded through codegen, pipeline layout, binding resolution and the test
436
- harness. Keeping the door open costs nothing; walking through it preemptively cost a great deal.
437
-
438
- ---
439
-
440
- ## 10. Open questions
441
-
442
- - **Is Chrome 149+ acceptable as the engine floor, and when?** This is now the only gating question
443
- in the plan. Steps 1–5 can land regardless; step 6 needs an answer. Immediates are in Chrome
444
- 149–150, so the answer is partly a calendar question about how far behind the target audience runs.
445
- - **Should `setImmediate` live on `ShaderResourceSetDescriptor` or on `ShaderDescriptor`?** The set
446
- descriptor is where every other resource is declared; but an immediate is not a bound resource —
447
- the spec is explicit that it is part of the *shader interface*, not the *resource interface*.
448
- Recommendation: the set descriptor, because that is what `generateCodeChunk` and the binding path
449
- already walk, and splitting across two objects buys nothing.
450
- - **Is 64 bytes worth revisiting later?** `maxImmediateSize` is raisable, but D3D12's root signature
451
- budget means no browser is likely to expose more. Plan around 64 permanently.
1
+ # WGSL immediate data in Shade — landing plan, 2026-08-28
2
+
3
+ How to bring the WGSL `immediate` address space (`var<immediate>` + `GPUBindingCommandsMixin.setImmediates`)
4
+ into Shade, starting from `ShadeGPUCommandContext.allocateTransientBuffer` and the call sites that
5
+ feed it.
6
+
7
+ **Verdict:** worth landing. Shade already models this exact concept, under this exact name, on top of
8
+ uniform buffers — nineteen of the twenty structs literally called `PUSH_CONSTANTS` fit inside the
9
+ 64-byte budget unmodified. With no fallback in scope the remaining work is small and almost entirely
10
+ additive.
11
+
12
+ > **Scope decision (rev 2).** An earlier revision of this plan carried a dual-lowering fallback:
13
+ > `var<immediate>` where supported, `@group(N) @binding(0) var<uniform>` where not, chosen by a
14
+ > capability probe. That is now **out of scope**, and it should never have been in it —
15
+ > [`RENDERER_CONTRACT.md` §2](../RENDERER_CONTRACT.md) already states the rule it violated: *"Shade
16
+ > requires a WebGPU device that meets a fixed floor. There is no feature detection beyond it and no
17
+ > branch that produces a lesser frame."* `ShadeDeviceFailureReason` says the same thing in its header
18
+ > comment, citing D6/I-8: no degradation tiers, no fallbacks, no feature-detection branches.
19
+ >
20
+ > Immediates therefore join the floor rather than getting a compatibility path. That deletes: the
21
+ > import-time capability probe and its unrecoverable failure mode, the extra bind group the fallback
22
+ > lowering needed, the resulting exclusion of the 7 shaders already at `maxBindGroups: 4`, and the
23
+ > `vi.resetModules()` dance every dual-lowering spec would have required. If a fallback is ever
24
+ > genuinely wanted, §9 shows why it belongs in a shader variant rather than in the binding machinery.
25
+ >
26
+ > **Scope decision (rev 3).** Immediates are also *not* a device floor. `Renderer.initialize()` does
27
+ > not check for them and the contract's §2 is unchanged; a browser without immediates starts and
28
+ > renders as before. Detection is exported instead (§4), so a shader author decides between refusing
29
+ > and reaching for a variant. This keeps the engine's floor where it was while the feature lands.
30
+
31
+ ---
32
+
33
+ ## 1. What the feature is, restricted to what matters here
34
+
35
+ Immediates are WebGPU's portable spelling of Vulkan push constants / D3D12 root constants / Metal
36
+ `setBytes`. The parts that constrain this plan:
37
+
38
+ | | Value |
39
+ |---|---|
40
+ | WGSL gate | `requires immediate_address_space;` a **language extension**, not a `GPUFeatureName` |
41
+ | Detection | `navigator.gpu.wgslLanguageFeatures.has('immediate_address_space')` — synchronous, no adapter needed |
42
+ | Size budget | `maxImmediateSize`, default **64 bytes**, multiple of 4, **no separate Compatibility Mode default** |
43
+ | Declaration | Module scope, immutable, **no `@group` / `@binding`** |
44
+ | Store type | Host-shareable constructible, **arrays and structs containing arrays are disallowed** |
45
+ | Count | **At most one** immediate variable statically accessed per entry point |
46
+ | Layout rules | Storage-space (natural alignment), **not** uniform-space |
47
+ | API | `pass.setImmediates(rangeOffset, data, dataOffset, dataSize)`, on render pass, compute pass and bundle encoders |
48
+ | Pipeline layout | `GPUPipelineLayoutDescriptor.immediateSize` (default 0, `maxImmediateSize`, multiple of 4) |
49
+ | Init | Bytes start zeroed but **slots start unset**; a draw reading an unset 4-byte slot is a validation error |
50
+ | Reset | Per-encoder; survives `setPipeline`, cleared by a new pass and by `executeBundles` |
51
+
52
+ Because `maxImmediateSize` is a `maximum`-class limit with a default of 64 and no compat-mode
53
+ reduction, **any adapter that exposes the WGSL extension reports at least 64 bytes**. There is no
54
+ device where the language feature is present and the budget is not. That is what makes a floor check
55
+ sufficient and a runtime branch unnecessary.
56
+
57
+ Availability is a browser-version question, not a hardware one: Vulkan's `maxPushConstantsSize` is
58
+ core-1.0 with a 128-byte required minimum and no feature bit, and no device in the
59
+ `vulkan.gpuinfo.org` corpus reports below 128. Chrome ships immediates in 149–150.
60
+
61
+ ---
62
+
63
+ ## 2. The current path, traced
64
+
65
+ Shade already has this pattern, three layers deep.
66
+
67
+ **Declaration** — `ShaderResourceGroupDescriptor.addUniform` (`ShaderResourceGroupDescriptor.js:335`):
68
+
69
+ ```js
70
+ const PUSH_CONSTANTS = WebGPUStruct.from({
71
+ probe_index_offset: u32,
72
+ probe_update_count: u32,
73
+ probe_resolution: u32,
74
+ probe_count: u32
75
+ });
76
+
77
+ resources.createGroup()
78
+ .addUniform("settings", PUSH_CONSTANTS)
79
+ .addTexture("tAtlas_radiance");
80
+ ```
81
+
82
+ **Codegen** — `ShaderResourceSetDescriptor.generateCodeChunk` (`ShaderResourceSetDescriptor.js:305`)
83
+ emits `@group(0) @binding(0) var<uniform> settings : PUSH_CONSTANTS;`.
84
+
85
+ **Upload, per dispatch** `ShadeGPUCommandContext.allocateTransientValueBuffer` (`:598`):
86
+
87
+ ```js
88
+ const settings = context.encoder.allocateTransientValueBuffer(PUSH_CONSTANTS, { });
89
+
90
+ shader.dispatch({ encoder: context.encoder, bindings: { settings, … } });
91
+ ```
92
+
93
+ which lands in `allocateTransientBuffer` (`:538`) → `GPUBufferAllocator.get` (pooled), then
94
+ `writeValueBuffer` (`:620`) staging buffer `copyBufferToBuffer`, then a `BindGroupDescriptor` hash
95
+ lookup in `GraphicsContext.setPipelineBindings` (`GraphicsContext.js:211`) and finally
96
+ `pass.setBindGroup(i, bind_group)`.
97
+
98
+ The FrameGraph route reaches the same place: `graph_compute_pass` → `graph_prepare_bindings` →
99
+ `encoder_prepare_shader_binding` (`:34`), which turns a raw value or `ArrayBuffer` into a transient
100
+ uniform buffer on the spot.
101
+
102
+ **Scale of the surface:**
103
+
104
+ - 67 `allocateTransientValueBuffer` call sites, 11 `allocateTransientBufferAndLoad`, 21 `graph_import_typed_buffer`
105
+ - 300 `addUniform` declarations; 22 of those *call sites* name a `*PUSH_CONSTANTS*` type, across
106
+ **20 distinct struct definitions** (`LPV_ACCUMULATION_PUSH_CONSTANTS` is shared by four shaders).
107
+ The table in §6 is per-struct, so it has 20 rows, not 22
108
+ - The single most common transient value type is a bare `u32` (12 sites)
109
+
110
+ ---
111
+
112
+ ## 3. What the win actually is
113
+
114
+ Not bandwidth. 64 bytes at a few hundred dispatches a frame is noise on any bus.
115
+
116
+ The win is the machinery each of those 20-byte structs currently drags behind it:
117
+
118
+ 1. **A pooled `GPUBuffer` acquire/release per value.** `GPUBufferAllocator.get` plus the transient
119
+ tracking array in `ShadeGPUCommandContext`, per dispatch, per frame.
120
+ 2. **A staging buffer, a `getMappedRange`, an `unmap` and a `copyBufferToBuffer` per value** —
121
+ `writeValueBuffer` does all four for what is often four bytes.
122
+ 3. **A `BindGroupDescriptor` construction plus a `HashMap` lookup per value**, and a cache entry
123
+ whose key changes every time the value changes so the small-uniform bind groups are the ones
124
+ least likely to hit the cache.
125
+ 4. **Two JS allocations per value, in the serialiser.** `writeValueBuffer` reaches
126
+ `write_gpu_typed_buffer`, which calls `scratch_buffer.fromArrayBuffer(data)`, which constructs a
127
+ fresh `DataView` and `Uint8Array` over the target every single call. Per dispatch, per frame, in a
128
+ codebase that advertises "zero-allocation" in its own package keywords. The immediate path writes
129
+ into a persistent `BinaryBuffer` and allocates nothing (§6, L3).
130
+ 5. **A `@binding` slot** in the group the settings share.
131
+
132
+ Point 5 is *only* a binding, not a group. Measured across all twenty candidates: the settings uniform
133
+ shares its group with textures and storage buffers in every case, at a median of about four resources
134
+ per group. No bind group slot is recovered anywhere in the current codebase. (An earlier revision
135
+ claimed otherwise and built an argument on it; that argument is withdrawn.)
136
+
137
+ **The strongest case is `graph_import_typed_buffer`** (`renderer/shader/graph/graph_import_typed_buffer.js`,
138
+ 21 call sites). It does not use the transient pool at allper execution it calls `device.createBuffer`
139
+ with `mappedAtCreation`, `getMappedRange`, `unmap`, `copyBufferToBuffer`, then `destroy` on
140
+ `encoder.onFinished`. A full buffer create-and-destroy per frame per value, carrying things like
141
+ `LPV_ACCUMULATION_PUSH_CONSTANTS` at 20 bytes. The source carries a standing TODO ("this is pretty
142
+ inefficient, figure out a way to skip needing 2 buffers") and a 2025-09-13 note that the obvious fix —
143
+ transient buffer plus `queue.writeBuffer` produced garbage for reasons never tracked down.
144
+ Immediates make the two-buffer dance unnecessary by construction for the ≤64-byte cases rather than
145
+ requiring that bug to be found first.
146
+
147
+ Caveat: converting a `graph_import_typed_buffer` site is *not* a drop-in. It returns a graph resource
148
+ id that passes consume as an ordinary binding, so the consuming pass stops declaring a graph input.
149
+ Higher payoff per site, higher cost per site.
150
+
151
+ Honest counterweight: on hardware, a 64-byte immediate may well land in a driver-managed constant
152
+ bank rather than in registers, so do not promise a shader-side speedup. Promise the removal of
153
+ CPU-side per-draw allocation, and measure the rest.
154
+
155
+ ---
156
+
157
+ ## 4. Capability, not a floor
158
+
159
+ **Built as optional.** `Renderer.initialize()` does not consult it and nothing was added to
160
+ `RENDERER_CONTRACT.md` §2 — a browser without immediates still starts and still renders. It simply
161
+ cannot run a shader that declares one, and deciding what to do about that belongs to whoever wrote
162
+ the shader.
163
+
164
+ `device/immediate_data_supported.js` answers the two separate questions:
165
+
166
+ ```js
167
+ immediate_data_supported() // does this browser's WGSL have the address space?
168
+ device_supports_immediate_size(device, bytes) // does this adapter have the budget?
169
+ ```
170
+
171
+ The first is synchronous and needs no adapter — `wgslLanguageFeatures` hangs off `navigator.gpu` —
172
+ so it can be asked at module scope, where shader descriptors are built. That is what makes the
173
+ shader-variant approach in §9 workable: one `if` at the point a shader is chosen.
174
+
175
+ Nothing goes in `requiredFeatures`: `immediate_address_space` is a WGSL *language* extension, not a
176
+ `GPUFeatureName`, and putting it there would make `requestDevice` reject on every device. Nothing
177
+ goes in `requiredLimits` either 64 is the spec default, granted without asking.
178
+
179
+ `ShaderDescriptor.validate_against_device` does check the budget, and throws naming the shader and
180
+ the shortfall. That is a per-draw guard against a device that disagrees with the language extension,
181
+ not a startup gate; it fires only for a shader that actually declares immediate data.
182
+
183
+ ## 5. The one structural obstacle
184
+
185
+ `ShaderResourceSetDescriptor` is a list of groups; a group is a list of resources; a resource has a
186
+ `binding_index`. Everything downstream — `generateBindings` returning `GPUBindingResource[][]`,
187
+ `generatePipelineLayoutDescriptor` producing one `BindGroupLayoutDescriptor` per group,
188
+ `generateCodeChunk` prefixing every declaration with `@group(g) @binding(b)` — assumes that shape.
189
+
190
+ An immediate has no group, no binding, and there is at most one per entry point. It has to live
191
+ *beside* the groups, and its value needs a second route to the pass encoder that ends in
192
+ `setImmediates` rather than `setBindGroup`.
193
+
194
+ Nothing about that requires a device, so the whole thing stays where Shade already does its work:
195
+ in descriptors built at module scope. `PipelineLayoutDescriptor` is device-free by design and stays
196
+ that way.
197
+
198
+ ---
199
+
200
+ ## 6. The landing plan
201
+
202
+ **L1 declaration + codegen**
203
+
204
+ - `ShaderResourceSetDescriptor`: an `#immediate` slot beside `#groups`, a `setImmediate(name, type)`
205
+ that rejects a second call, plus validation at declaration time: `type.size <= 64`, no arrays
206
+ anywhere in the type (walk it — `WebGPUArray` members and nested structs both), no atomics, size a
207
+ multiple of 4.
208
+ - `generateCodeChunk` (`ShaderResourceSetDescriptor.js:305`): emit `var<immediate> name : Type;`
209
+ first, with no `@group`/`@binding` prefix.
210
+ - `ShaderDescriptor.compile` (`ShaderDescriptor.js:53`): the existing mechanism emits
211
+ `enable ${extension};`. Language extensions use **`requires`**. That needs a second channel on
212
+ `CodeChunk` `addLanguageExtension` beside `addExtension` and a `requires` line in the prelude.
213
+ Do not overload `addExtension`; its assert is against `WebGPUExtensionType`, which is the
214
+ `GPUFeatureName` namespace.
215
+
216
+ **L2 pipeline layout**
217
+
218
+ - `PipelineLayoutDescriptor`: an `immediateSize = 0` field, threaded through `from`, `forStage`,
219
+ `fromJSON`, `hash`, `equals`, `copy`, `clone`. Missing it from `hash`/`equals` would silently alias
220
+ two layouts that differ only in immediate size cheap mistake, expensive symptom.
221
+ - `PipelineLayoutManager.#create_pipeline_layout` (`PipelineLayoutManager.js:85`): pass it to
222
+ `createPipelineLayout`.
223
+ - `generatePipelineLayoutDescriptor` / `generatePipelineLayout`: set it from `roundUp(4, type.size)`.
224
+
225
+ **L3 binding resolution and the call**
226
+
227
+ **Do not change the return shape of `generateBindings`.** Returning `{ groups, immediate }` would
228
+ break all **10** external callers (`GPUParticularRenderer`, `GPUDynamicMeshResidency`,
229
+ `graph_draw_lightmap_viz_buffer`, `prototypeParticleSystem`, `graph_atorus_denoise_luma`,
230
+ `graph_rasterize_meshes_transparent_oit`, `ComputeShader`, `ImageShader`, `RenderPassDescriptor`,
231
+ `GPUWaterRenderer`) and force the new field through the **22** external
232
+ `constructRenderPass`/`constructComputePass` call sites. Purely additive instead:
233
+
234
+ - `ShaderResourceSetDescriptor.writeImmediate(map, buffer)` a *new* method that **allocates
235
+ nothing**. It writes the immediate value into a caller-owned `BinaryBuffer` at that buffer's
236
+ current `position`, advances `position` by the type's size, and returns `false` when the shader
237
+ declares no immediate. Every existing `generateBindings` caller is untouched.
238
+
239
+ Use the already-exported `write_wgsl_type_value(value, buffer, type)`
240
+ (`serde/write_gpu_typed_buffer.js:114`), which takes a `BinaryBuffer` directly. **Do not** route
241
+ this through `write_gpu_typed_buffer` (`:188`): that wrapper calls
242
+ `scratch_buffer.fromArrayBuffer(data)`, and `fromArrayBuffer` constructs `new DataView(data)` and
243
+ `new Uint8Array(data)` on every call two allocations per write, and it resets `position` to 0.
244
+ The zero-allocation entry point already exists and is public; the wrapper is the thing to avoid.
245
+
246
+ - `GraphicsContext.setPipelineBindings` (`GraphicsContext.js:211`) gains an optional fourth
247
+ parameter the `BinaryBuffer`, plus the byte offset and length written. After the `setBindGroup`
248
+ loop: `pass.setImmediates(0, buffer.data, byte_offset, byte_length)`. `setImmediates` takes
249
+ `AllowSharedBufferSource`, and for a plain `ArrayBuffer` the offset and size arguments are in
250
+ **bytes**, so the backing store is passed through with no copy on our side. All four existing
251
+ callers keep working.
252
+
253
+ - `ImageShader.draw` and `ComputeShader.constructPass`/`dispatch` set `position`, call
254
+ `writeImmediate`, and thread the buffer plus range through as optional fields.
255
+
256
+ Three properties of `BinaryBuffer` that this design has to respect:
257
+
258
+ 1. **`data` is not stable.** `setCapacity` (and therefore `ensureCapacity`) reallocates the backing
259
+ `ArrayBuffer` and rebinds `dataView` / `__data_uint8`. Read `buffer.data` at the point of the
260
+ `setImmediates` call, never cache it — and size the buffer once at construction so it never grows
261
+ mid-frame.
262
+ 2. **Endianness is not the default.** `BinaryBuffer` defaults to `EndianType.LittleEndian`, but the
263
+ serde module's own scratch sets `endianness = platform_compute_endianness()` (`:179`) because GPU
264
+ buffer contents must match the host. A caller-supplied buffer that skips this writes correct-looking
265
+ bytes that are wrong on a big-endian host and identical on the one you test on. Set it at
266
+ construction, and assert it in `writeImmediate`.
267
+ 3. **Ownership.** One long-lived buffer on `ShadeGPUCommandContext`, sized to `maxImmediateSize` at
268
+ construction, is enough — `setImmediates` copies the bytes at call time, so the region is free for
269
+ reuse immediately after. The API is still parameterised on `position` rather than hardcoding 0,
270
+ because that costs nothing and leaves room for a caller that wants to stage several values or keep
271
+ a frame log of them.
272
+ - `encoder_prepare_shader_binding` (`:34`): in the immediate case, return the raw value rather than
273
+ allocating a transient buffer.
274
+
275
+ **The eight bypass paths.** `setPipelineBindings` does not cover everything. Eight files drive
276
+ `pass.setBindGroup` directly and never touch it `GPUMaterialManager`, `prototypeParticleSystem`,
277
+ `graph_build_depth_mipmap`, `render_copy_texture`, `graph_rasterize_meshes_alpha_tested`,
278
+ `graph_rasterize_meshes_transparent_oit`, `draw_texture_as_rgba`, `WebGPUMipmapGenerator`. A shader
279
+ migrated in one of those needs its own `setImmediates` call at the right point in the loop. None of
280
+ the twenty candidates lives there, so this constrains future migration rather than this landing — but
281
+ it must not be assumed away.
282
+
283
+ **L4 the test tiers**
284
+
285
+ Without a fallback these are not optional: they are the only way immediates get exercised outside a
286
+ browser.
287
+
288
+ - **WGSL validation** (`wgsl/validate_wgsl_source.js`) — teach it that `var<immediate>` is legal and
289
+ `requires immediate_address_space;` is a valid directive, so the new declaration does not trip the
290
+ foreign-token or undeclared-call rules.
291
+ - **JS emulator** — a real trap. `collectGlobalVariable`
292
+ (`wgsl/emulator/WGSLJavaScriptCompiler.js:825`) only pushes to `globalBindings` when *both* `group`
293
+ and `binding` are non-null (`:862`). A `var<immediate>` has neither, is not `workgroup`, and falls
294
+ through to the `privateGlobals` branch, where it is **silently default-initialised**. Every emulator
295
+ spec for an immediate-using chunk would pass while reading zeros. Route
296
+ `address_space === 'immediate'` into `globalBindings` with a null group/binding so
297
+ `emu.bindings.settings = {…}` keeps working by name.
298
+ - **SoftwareGPUDevice** — `DEFAULT_LIMITS.maxImmediateSize` (`mock/SoftwareGPUDevice.js:67`) is
299
+ already present, set to `0`. Raise it to 64, add `setImmediates` to `SoftwareGPURenderPassEncoder`
300
+ (`:130`) and `SoftwareGPUComputePassEncoder` (`:108`) with the spec's `[[immediate_data]]` /
301
+ `[[immediate_slots_set]]` state, validate `rangeOffset % 4`, size `% 4` and the range bound, and
302
+ snapshot the bytes into each recorded draw/dispatch the way `#snapshot_bindings` already snapshots
303
+ bind groups. Follow the folder's stated policy in `mock/IMPLEMENTATION_PLAN.md` §2: validation
304
+ errors go through `generate_error`, they do not throw. `createPipelineLayout` (`:766`) gains the
305
+ `immediateSize maxImmediateSize` and `% 4` checks. Emulate the unset-slot rule too — it is the
306
+ mock's chance to catch a missing `setImmediates` before a browser does.
307
+ - **Playground** — one pass converted end to end and run in Chrome before any bulk migration.
308
+
309
+ **L5 migration**
310
+
311
+ Smallest first. `shader_read_out_bucket` (4 bytes, one `u32`) is the ideal first patient;
312
+ `shader_generate_rays` and `shader_shadow_upscale` are the same shape.
313
+
314
+ **It is not one line per shader.** Only 5 of the 20 candidates supply their settings implicitly, by
315
+ passing a plain object in `bindings` and letting `encoder_prepare_shader_binding` allocate. The other
316
+ ~15 have an explicit `allocateTransientValueBuffer` or `graph_import_typed_buffer` call that
317
+ pre-builds the buffer and passes the `GPUBuffer`. Those need two edits: the declaration, and the call
318
+ site that stops pre-allocating.
319
+
320
+ ---
321
+
322
+ ## 7. Candidate inventory
323
+
324
+ Every struct named `*PUSH_CONSTANTS*`, sized by `WebGPUStruct` (the same natural-alignment layout the
325
+ `immediate` address space uses). Sizes were re-derived programmatically — field lists extracted from
326
+ source and fed back through the real `WebGPUStruct.from`, not computed by hand.
327
+
328
+ | Struct | Size | Verdict |
329
+ |---|---:|---|
330
+ | `shader_read_out_bucket` | 4 | fits |
331
+ | `shader_generate_rays` | 4 | fits |
332
+ | `shader_shadow_upscale` | 4 | fits |
333
+ | `shader_sharc_evict` | 8 | fits |
334
+ | `shader_lightmap_accummulate_tile` | 8 | fits |
335
+ | `shader_lightmap_tile_composit` | 8 | fits |
336
+ | `graph_lpv_store_sh` | 16 | fits |
337
+ | `shader_perfect_perspective` | 16 | fits, see note |
338
+ | `graph_lvp_atlas_to_sh` | 20 | fits |
339
+ | `LPV_ACCUMULATION_PUSH_CONSTANTS` | 20 | fits |
340
+ | `shader_lpv_accumulate_depth` | 20 | fits |
341
+ | `graph_sample_light_probes` | 20 | fits |
342
+ | `graph_reduce_atlas_to_sh` | 24 | fits |
343
+ | `shader_lpv_build_gbuffer` | 24 | fits |
344
+ | `shader_update_clipmap_cascade` | 32 | fits |
345
+ | `graph_lpv_shade` | 32 | fits |
346
+ | `shader_build_scene_sdf_f32` | 32 | fits |
347
+ | `shader_scene_sdf_debug_viz` | 32 | fits |
348
+ | `shader_scene_sdf_soft_shadows` | 48 | fits |
349
+ | `LIGHT_CLUSTER_ASSIGNMENT_PUSH_CONSTANTS` | 128 | **cannot move** |
350
+
351
+ `LIGHT_CLUSTER_ASSIGNMENT_PUSH_CONSTANTS` fails twice: 128 bytes against a 64-byte budget, and
352
+ `frustum: WebGPUArray.from(vec4f, 6)` is an array, which the `immediate` address space disallows
353
+ outright. It stays a uniform buffer. If the rest is worth moving later, the split is
354
+ `{cluster_params, screen_resolution, distance_min, distance_max}` at 32 bytes as an immediate with the
355
+ frustum left in a buffer a follow-up, not part of this landing.
356
+
357
+ Note on `shader_perfect_perspective`: its `use_vignette` member is typed `bool`, and Shade's `bool`
358
+ primitive (`WEBGPU_WGSL_PRIMITIVE_TYPES.js:8`) is a 4-byte type. WGSL's host-shareable set is numeric
359
+ scalars, vectors, matrices, atomics, arrays and structs of those — `bool` is in none of them, in *any*
360
+ address space, uniform included. So this is a pre-existing condition rather than something immediates
361
+ introduce, but the member needs to become `u32` before this struct can move. Worth checking whether
362
+ that shader is reachable at all; grep finds no importer.
363
+
364
+ Beyond the named ones, the 12 bare-`u32` `allocateTransientValueBuffer` sites are the same pattern
365
+ without the naming, and are equally good candidates.
366
+
367
+ ### What must not move
368
+
369
+ `CAMERA_UNIFORM_STRUCT` (~640 bytes: eight `mat4x4f`, a six-element `vec4f` frustum array, and more)
370
+ and `VIEW_UNIFORM_STRUCT` account for 116 of the 300 `addUniform` calls. They are per-view, written
371
+ once, read by everything — over budget, containing arrays, and not per-draw data. They stay. Same for
372
+ anything reached through `GPUDatabase` or `GPUSingleTypeTable`.
373
+
374
+ ---
375
+
376
+ ## 8. What was built
377
+
378
+ Landed in one pass, all against a green suite (14,873 tests). Deviations from the plan above are
379
+ noted where they occurred.
380
+
381
+ **Mock device** — `SoftwareGPUImmediateData.js` holds WebGPU's `[[immediate_data]]` /
382
+ `[[immediate_slots_set]]`; `setImmediates` on both pass encoders, with content-timeline failures
383
+ throwing `OperationError` and device-timeline failures invalidating the encoder through a new
384
+ `SoftwareGPUCommandEncoder#notify_pass_validation_error`; `maxImmediateSize` raised 0 → 64;
385
+ `createPipelineLayout` validates `immediateSize`. Draws and dispatches now run WebGPU's *Validate
386
+ immediate data*, so a forgotten `setImmediates` is caught here rather than in a browser.
387
+
388
+ **Emulator** `WGSLJavaScriptCompiler` routes `var<immediate>` into `globalBindings` instead of
389
+ letting it fall through to `privateGlobals`, where it was silently default-initialised. Pinned by a
390
+ spec that fails without the fix.
391
+
392
+ **Validator** *no change needed.* The plan assumed `validate_wgsl_source` would have to learn about
393
+ `requires` and `var<immediate>`; it already accepts both. A regression test pins that so a future
394
+ rule cannot start false-positiving on them.
395
+
396
+ **Pipeline layout** `PipelineLayoutDescriptor.immediateSize`, through `from`/`forStage`/`fromJSON`/
397
+ `hash`/`equals`/`copy`, and passed to `createPipelineLayout` by the manager.
398
+
399
+ **Codegen** — `CodeChunk#addLanguageExtension` as a separate channel from `addExtension`, emitted as
400
+ `requires` rather than `enable` and deliberately not folded into `#features` (a different namespace
401
+ from `GPUFeatureName`). `ShaderResourceSetDescriptor#setImmediate` validates size, arrays at any
402
+ depth, atomics and runtime sizing at declaration time.
403
+
404
+ **Binding** — `writeImmediate(map, buffer)` writes into a caller-owned `BinaryBuffer` at its
405
+ `position` via the already-public `write_wgsl_type_value`, allocating nothing.
406
+ `ShadeGPUCommandContext` owns one such buffer for the life of the context.
407
+ `GraphicsContext#setPipelineBindings` took an optional fourth parameter, so all four existing callers
408
+ are untouched — `generateBindings` kept its return shape and its 10 callers were not disturbed.
409
+
410
+ **FrameGraph** — the plan missed this entirely. `getResourceByName` only searches groups, so the
411
+ graph path threw for any immediate; and worse, `graph_prepare_pass_input_data` and
412
+ `graph_prepare_bindings` read a numeric input as a graph resource id, which would have silently
413
+ misread a bare `u32` immediate as a handle. `isImmediateName` now short-circuits all three helpers.
414
+ Pinned by a spec that fails without it.
415
+
416
+ **Detection** — `immediate_data_supported()` / `device_supports_immediate_size()`, plus a budget
417
+ check in `ShaderDescriptor.validate_against_device`.
418
+
419
+ ### Not done
420
+
421
+ - **No production shader migrated.** The 19 candidates in §7 are unchanged; this is the machinery
422
+ only. `shader_read_out_bucket` remains the right first patient.
423
+ - **`shader_perfect_perspective`'s `bool` member** is still a `bool` (§7's note). Pre-existing, and
424
+ it blocks only that one struct.
425
+ - **The eight `setBindGroup` bypass paths** (§6, L3) are untouched. No candidate lives there.
426
+
427
+ ## 9. Transition candidates, proved
428
+
429
+ Every candidate below was **actually transitioned** — declaration and call site edited in the working
430
+ tree then proved, then reverted. This section records what the proof showed, not what inspection
431
+ suggested. The tier-1 entries were carried all the way through; the rest were assessed against the
432
+ same checks without being applied.
433
+
434
+ ### What "proved" means here
435
+
436
+ The six checks each applied transition had to pass:
437
+
438
+ 1. **Module imports.** These shaders build at module scope, so a rejected type, a broken declaration
439
+ or bad codegen throws at import.
440
+ 2. **`immediateSize` is what the struct says.**
441
+ 3. **Generated WGSL declares `var<immediate>` with no `@group`/`@binding`, and emits `requires`.**
442
+ 4. **`validate_wgsl_source` returns no diagnostics** on the generated source.
443
+ 5. **`pipeline.layout.immediateSize` matches**, so a device would accept the pipeline.
444
+ 6. **A value round-trips** through `writeImmediate` into the expected words.
445
+
446
+ Plus, across the whole set: the settings name must no longer resolve through `getResourceByName` or
447
+ appear in any group a stale uniform left beside a new immediate would bind twice.
448
+
449
+ **What the proof does not cover, and why the scores are not higher.** *None of these shaders has any
450
+ spec coverage* `grep -rl <name> --include=*.spec.js` returns zero for every one, and for their call
451
+ sites too. The full suite passing with all five applied (14,887 tests) therefore says only that
452
+ nothing *else* broke. Nothing executes these passes outside a browser, so the call-site edit is
453
+ verified by inspection and by the diff being a verbatim move, not by running it.
454
+
455
+ **The one silent failure mode.** A misspelled *field* inside the value object is not an error:
456
+ `write_gpu_typed_buffer` writes 0 for a member it cannot find. Probed and confirmed —
457
+ `{bucket_ID: 11, frame: 2}` against `{bucket_id, frame}` yields words `[0, 2]`, no throw. This is
458
+ pre-existing behaviour that immediates inherit rather than introduce (the same is true of
459
+ `allocateTransientValueBuffer` today), but it is why every transition below moves the object literal
460
+ **verbatim** rather than retyping it, and why the diffs are worth reading rather than trusting.
461
+
462
+ ### Tier 1 — transitioned and proved end to end
463
+
464
+ | Shader | Bytes | Call-site change | Group effect | Net lines | Confidence |
465
+ |---|---:|---|---|---:|---:|
466
+ | `shader_sharc_evict` | 8 | **none** — already passes a plain object | leads a group of 4; siblings shift down one | −0 | **95%** |
467
+ | `shader_shadow_upscale` | 4 | **none** — already passes a plain object | leads a group of 5; siblings shift down one | −0 | **95%** |
468
+ | `shader_read_out_bucket` | 4 | 2 sites, inline `{ bucket_id }` | **owns group 0 outright** — group disappears, later groups renumber | −8 | **90%** |
469
+ | `shader_generate_rays` | 4 | 1 site, inline `{ frame_index }` | **last in its group** — no sibling index moves | −4 | **92%** |
470
+ | `graph_lvp_atlas_to_sh` | 20 | 1 site, inline 5-field object | owns group 0 outright — group disappears | −5 | **85%** |
471
+
472
+ The two at 95% need *no call-site edit at all*: they already pass `settings: { … }` as a plain
473
+ object, because the immediate path accepts a value where the uniform path demands a bindable buffer.
474
+ Their entire diff is one line in the declaration. The residual 5% is that no test executes them.
475
+
476
+ `shader_read_out_bucket` scores lower than `shader_generate_rays` despite being smaller: removing its
477
+ uniform empties group 0, so every later group renumbers. That is consistent between the WGSL and the
478
+ layout because both regenerate from the same descriptor, and it is why it still scores 90 — but it is
479
+ a larger blast radius than a declaration that sits last in a shared group and moves nothing.
480
+
481
+ `graph_lvp_atlas_to_sh` is the lowest of the tier because its value object has five fields drawn from
482
+ four different sources, which is the most surface for the silent-misspelling failure above.
483
+
484
+ ### Tier 2 — same shape, not yet applied
485
+
486
+ Assessed against the same checks by inspection. All are live (one importer each), all fit, none
487
+ contains an array.
488
+
489
+ | Shader | Bytes | Notes | Confidence |
490
+ |---|---:|---|---:|
491
+ | `graph_lpv_store_sh` | 16 | leads a group of 3 | 80% |
492
+ | `graph_reduce_atlas_to_sh` | 24 | owns its group | 80% |
493
+ | `shader_lpv_build_gbuffer` | 24 | ImageShader | 75% |
494
+ | `shader_lpv_accumulate_depth` | 20 | ImageShader, group of 5 | 75% |
495
+ | `graph_lpv_shade` | 32 | group of 3 | 75% |
496
+ | `graph_sample_light_probes` | 20 | ImageShader, group of 7 | 70% |
497
+ | `shader_build_scene_sdf_f32` | 32 | **two** resource sets (f32 and f16) share one struct; both must move together | 65% |
498
+
499
+ `LPV_ACCUMULATION_PUSH_CONSTANTS` (20 bytes, 5 importers) is deliberately absent. It is shared across
500
+ four shaders and supplied through `graph_import_typed_buffer`, so it is not a per-shader edit — it is
501
+ the `graph_import_typed_buffer` conversion described in §3, which changes how the consuming pass
502
+ declares its input. Higher payoff, different job.
503
+
504
+ ### Not worth doing
505
+
506
+ Six candidates have **zero importers** — nothing in the tree constructs them:
507
+ `shader_lightmap_tile_composit`, `shader_lightmap_accummulate_tile`, `shader_update_clipmap_cascade`,
508
+ `shader_perfect_perspective`, `shader_scene_sdf_debug_viz`, `shader_scene_sdf_soft_shadows`.
509
+
510
+ They would transition trivially and change nothing that runs. `shader_perfect_perspective` also still
511
+ carries the `bool` member from §7. Migrating dead shaders would inflate the count and prove nothing;
512
+ they are listed so nobody spends a morning on them.
513
+
514
+ `LIGHT_CLUSTER_ASSIGNMENT_PUSH_CONSTANTS` remains excluded on both counts — 128 bytes and an array.
515
+
516
+ ### Recommended order
517
+
518
+ `shader_sharc_evict` first, then `shader_shadow_upscale`: one line each, no call site, and they
519
+ establish the pattern with the smallest possible diff. `shader_generate_rays` next, as the first with
520
+ a call-site edit. Then `shader_read_out_bucket`, which is the first to renumber groups and therefore
521
+ the first worth looking at in a browser before moving on.
522
+
523
+ Before any of them lands in a build anyone plays: run one in Chrome 149+. Nothing in the suite
524
+ executes these passes, and that gap is the whole of the residual risk.
525
+
526
+ ---
527
+
528
+ ## 10. If a fallback is ever wanted anyway
529
+
530
+ It belongs in a shader variant, not in the binding machinery — and Shade already supports that with
531
+ no new mechanism. `ShaderDescriptor.compile()` clones the body chunk and adds the resource set's
532
+ generated chunk as a dependency:
533
+
534
+ ```js
535
+ const chunk = this.#body.clone();
536
+ chunk.addDependency(this.#resources.generateCodeChunk());
537
+ ```
538
+
539
+ So one `CodeChunk` body pairs with two different `ShaderResourceSetDescriptor`s to produce two
540
+ `ShaderDescriptor`s — immediate and uniform — from a single source of shader text, selected by
541
+ whoever constructs them. The body never changes: `settings.probe_count` reads identically either way.
542
+
543
+ That is a per-shader, opt-in decision made by the shader's author, which is a very different thing
544
+ from a capability branch threaded through codegen, pipeline layout, binding resolution and the test
545
+ harness. Keeping the door open costs nothing; walking through it preemptively cost a great deal.
546
+
547
+ ---
548
+
549
+ ## 11. Open questions
550
+
551
+ - **Which shader migrates first, and when?** Answered in §9: `shader_sharc_evict` and
552
+ `shader_shadow_upscale` need no call-site edit at all. The machinery is inert until one lands.
553
+ - **Do these passes get a spec before or after they migrate?** None of the twelve live candidates has
554
+ any test coverage, which is the entire residual risk in §9. A spec that drives one through the
555
+ software device would convert several 75s into 90s, and is probably worth more than the next three
556
+ migrations.
557
+ - **Is 64 bytes worth revisiting later?** `maxImmediateSize` is raisable, but D3D12's root signature
558
+ budget means no browser is likely to expose more. `IMMEDIATE_SIZE_FLOOR` refuses more than 64 at
559
+ declaration; if a project ever knows its device offers more, that constant is the single place to
560
+ relax.
561
+ - **Should `graph_import_typed_buffer`'s two-buffer dance be fixed independently?** §3 argues its 21
562
+ call sites are the strongest case for immediates, but the standing TODO there is a separate bug —
563
+ the 2025-09-13 note says the obvious fix produced garbage and was never chased down. Immediates
564
+ route around it for the ≤64-byte cases without explaining it.