@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,4 +1,6 @@
1
1
  import { normalize_dynamic_offsets } from "./normalize_dynamic_offsets.js";
2
+ import { pipeline_immediate_size } from "./pipeline_immediate_size.js";
3
+ import { SoftwareGPUImmediateData } from "./SoftwareGPUImmediateData.js";
2
4
  import { SoftwareGPUObjectBase } from "./SoftwareGPUObjectBase.js";
3
5
  import { write_pass_timestamp } from "./write_pass_timestamp.js";
4
6
 
@@ -79,6 +81,14 @@ export class SoftwareGPURenderPassEncoder extends SoftwareGPUObjectBase {
79
81
  */
80
82
  #scissor = null;
81
83
 
84
+ /**
85
+ * Immediate data for this pass. Per-encoder state in WebGPU, and a new pass starts over — which
86
+ * is why it is constructed here rather than carried in from the command encoder.
87
+ *
88
+ * @type {SoftwareGPUImmediateData}
89
+ */
90
+ #immediates;
91
+
82
92
  #ended = false;
83
93
 
84
94
  /**
@@ -99,6 +109,7 @@ export class SoftwareGPURenderPassEncoder extends SoftwareGPUObjectBase {
99
109
 
100
110
  this.#encoder = encoder;
101
111
  this.descriptor = descriptor;
112
+ this.#immediates = new SoftwareGPUImmediateData(encoder.device.limits.maxImmediateSize);
102
113
 
103
114
  write_pass_timestamp(descriptor, 'beginningOfPassWriteIndex');
104
115
  }
@@ -138,6 +149,22 @@ export class SoftwareGPURenderPassEncoder extends SoftwareGPUObjectBase {
138
149
  );
139
150
  }
140
151
 
152
+ /**
153
+ * @param {number} range_offset byte offset into the immediate data range
154
+ * @param {ArrayBuffer|ArrayBufferView} data
155
+ * @param {number} [data_offset] in elements if `data` is a TypedArray, bytes otherwise
156
+ * @param {number} [data_size] in elements if `data` is a TypedArray, bytes otherwise
157
+ */
158
+ setImmediates(range_offset, data, data_offset = 0, data_size) {
159
+ this.#assert_encoding();
160
+
161
+ const message = this.#immediates.write(range_offset, data, data_offset, data_size);
162
+
163
+ if (message !== null) {
164
+ this.#encoder.notify_pass_validation_error(message);
165
+ }
166
+ }
167
+
141
168
  /**
142
169
  * @param {number} slot
143
170
  * @param {GPUBuffer} buffer
@@ -206,6 +233,24 @@ export class SoftwareGPURenderPassEncoder extends SoftwareGPUObjectBase {
206
233
  this.stencil_reference = reference;
207
234
  }
208
235
 
236
+ /**
237
+ * WebGPU's *Validate immediate data*: every slot the pipeline's immediate range covers must have
238
+ * been written before a command reads it. The bytes default to zero but the slots do not default
239
+ * to set, so a forgotten `setImmediates` is a refused draw on a real device rather than a shader
240
+ * quietly reading zeroes. Catching it here is the whole reason the slot bookkeeping exists.
241
+ *
242
+ * @returns {void}
243
+ */
244
+ #validate_immediates() {
245
+ const size = pipeline_immediate_size(this.#pipeline);
246
+
247
+ if (size > 0 && !this.#immediates.isRangeSet(0, size)) {
248
+ this.#encoder.notify_pass_validation_error(
249
+ `draw: pipeline declares ${size} bytes of immediate data, and not all of it was set by setImmediates`
250
+ );
251
+ }
252
+ }
253
+
209
254
  /**
210
255
  * The state a draw would run under, copied so later state changes do not rewrite a draw already
211
256
  * recorded.
@@ -221,6 +266,7 @@ export class SoftwareGPURenderPassEncoder extends SoftwareGPUObjectBase {
221
266
  index_buffer: this.#index_buffer,
222
267
  viewport: this.#viewport,
223
268
  scissor: this.#scissor,
269
+ immediates: this.#immediates.snapshot(),
224
270
  };
225
271
  }
226
272
 
@@ -230,6 +276,7 @@ export class SoftwareGPURenderPassEncoder extends SoftwareGPUObjectBase {
230
276
  */
231
277
  draw(vertex_count, instance_count = 1) {
232
278
  this.#assert_encoding();
279
+ this.#validate_immediates();
233
280
 
234
281
  this.draws.push({
235
282
  ...this.#snapshot(),
@@ -246,6 +293,7 @@ export class SoftwareGPURenderPassEncoder extends SoftwareGPUObjectBase {
246
293
  */
247
294
  drawIndexed(index_count, instance_count = 1) {
248
295
  this.#assert_encoding();
296
+ this.#validate_immediates();
249
297
 
250
298
  this.draws.push({
251
299
  ...this.#snapshot(),
@@ -262,6 +310,7 @@ export class SoftwareGPURenderPassEncoder extends SoftwareGPUObjectBase {
262
310
  */
263
311
  drawIndirect(indirect_buffer, indirect_offset) {
264
312
  this.#assert_encoding();
313
+ this.#validate_immediates();
265
314
 
266
315
  this.draws.push({
267
316
  ...this.#snapshot(),
@@ -281,6 +330,7 @@ export class SoftwareGPURenderPassEncoder extends SoftwareGPUObjectBase {
281
330
  */
282
331
  drawIndexedIndirect(indirect_buffer, indirect_offset) {
283
332
  this.#assert_encoding();
333
+ this.#validate_immediates();
284
334
 
285
335
  this.draws.push({
286
336
  ...this.#snapshot(),
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Bytes of immediate data a pipeline's layout declares, or 0 when it declares none.
3
+ *
4
+ * A pipeline built with `layout: "auto"` reports 0 rather than deriving a size: deriving one would
5
+ * mean reading the WGSL, and nothing in this folder compiles WGSL.
6
+ *
7
+ * @param {GPUPipelineBase|null} pipeline
8
+ * @returns {number}
9
+ */
10
+ export function pipeline_immediate_size(pipeline: GPUPipelineBase | null): number;
11
+ //# sourceMappingURL=pipeline_immediate_size.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline_immediate_size.d.ts","sourceRoot":"","sources":["../../../../../src/shade/device/mock/pipeline_immediate_size.js"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,kDAHW,kBAAgB,IAAI,GAClB,MAAM,CAUlB"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Bytes of immediate data a pipeline's layout declares, or 0 when it declares none.
3
+ *
4
+ * A pipeline built with `layout: "auto"` reports 0 rather than deriving a size: deriving one would
5
+ * mean reading the WGSL, and nothing in this folder compiles WGSL.
6
+ *
7
+ * @param {GPUPipelineBase|null} pipeline
8
+ * @returns {number}
9
+ */
10
+ export function pipeline_immediate_size(pipeline) {
11
+ const layout = pipeline?.descriptor?.layout;
12
+
13
+ if (layout === undefined || layout === null || layout === "auto") {
14
+ return 0;
15
+ }
16
+
17
+ return layout.immediateSize ?? 0;
18
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"PipelineLayoutManager.d.ts","sourceRoot":"","sources":["../../../../../src/shade/device/pipeline/PipelineLayoutManager.js"],"names":[],"mappings":"AAGA;IAMI;;;OAGG;IACH,+BAEC;IAoBD;;;;OAIG;IACH,2DAFa,4BAA0B,SAAS,CAI/C;IAgBD;;;;OAIG;IACH,iFAKC;IAED;;;;OAIG;IACH,8EAEC;;CAkBJ"}
1
+ {"version":3,"file":"PipelineLayoutManager.d.ts","sourceRoot":"","sources":["../../../../../src/shade/device/pipeline/PipelineLayoutManager.js"],"names":[],"mappings":"AAGA;IAMI;;;OAGG;IACH,+BAEC;IAoBD;;;;OAIG;IACH,2DAFa,4BAA0B,SAAS,CAI/C;IAgBD;;;;OAIG;IACH,iFAKC;IAED;;;;OAIG;IACH,8EAEC;;CAmBJ"}
@@ -89,6 +89,7 @@ export class PipelineLayoutManager {
89
89
  return this.#device.createPipelineLayout({
90
90
  label: descriptor.label,
91
91
  bindGroupLayouts,
92
+ immediateSize: descriptor.immediateSize,
92
93
  });
93
94
 
94
95
  }
@@ -30,6 +30,36 @@ Those fixes are in the permanent path now and there is nothing left to A/B. What
30
30
  the *next* change here — the performance findings especially, since every one of them trades image
31
31
  quality for time and none can be judged from a screenshot.
32
32
 
33
+ Q-01 was judged here and now ships unconditionally: every clustered point and spot light is occluded
34
+ by geometry, sampled with one nearest tap out of the shadow atlas. The two checkboxes that switched
35
+ that off, or swapped the tap for the deferred pass's filter kernels, are gone along with the metadata
36
+ word behind them — `volumetrics/NOTES.md` keeps the measurements that closed the question. The
37
+ torches are the local lights here: 130 point lights, every one of them a shadow caster, and **zero
38
+ spot lights**, so the spot half has no on-screen coverage on this page and is covered by
39
+ `shader_volumetrics_build_lighting_local_shadow.spec.js` instead. Adding a `SpotLight` from the
40
+ console is the way to look at it.
41
+
42
+ Q-04 went the same way and its switch is gone too. The sun's media self-shadowing is one fetch into
43
+ a ladder of three cascaded volumes built once a frame with their third axis on the sun, and that is
44
+ now the only path for it: the `__VOL_SUN_SHADOW` global and the checkbox over it are removed along
45
+ with the `sun_shadow_mode` word they drove, so nothing on this page reproduces the march/cascade
46
+ comparison any more. Restoring it means re-adding that word and the branch in
47
+ `shader_volumetrics_build_lighting` that reads it.
48
+
49
+ Worth knowing what it was, because it is the size of what the page can no longer show. Against a
50
+ march at `STEP_COUNT = 512` the cascades read within 1.2 mean luminance at every viewpoint here and
51
+ across 40 to 110 degrees of field of view, while sixteen steps read **+58** and **+70** at the two
52
+ poses beside the small fog boxes — the step there is eight units, the box is three thick, and the
53
+ march goes straight over it. They are also cheaper: 0.37 ms against 0.53 at 1080p, 0.81 against 1.52
54
+ at 2160p, per pass. `volumetrics/NOTES.md` has the whole measurement, including the one that says a
55
+ *single* world-uniform box could not have been sized correctly at any resolution — every shape
56
+ measured has a field of view at which it is 28 to 50 luminance from the truth, and which one that is
57
+ moves with the resolution.
58
+
59
+ The march itself is still in the tree and still runs: every point and spot light uses it, and so
60
+ would a second directional light. Only the first one is amortized, because there is one ladder of
61
+ boxes and it is aligned to one direction.
62
+
33
63
  ## Measuring, rather than eyeballing
34
64
 
35
65
  This pass is stochastic and temporally filtered, so most changes to it move the image by less than
@@ -50,6 +80,14 @@ confident wrong answer during the work above:
50
80
  *change → settle ~45 frames → average*, and take the floor by making an identical no-op change so
51
81
  both paths cost the same.
52
82
 
83
+ - **Frame time here is presentation-throttled, not GPU-bound.** At 1920×1080 every setting reads
84
+ ~16.5 ms, including settings that do obviously different amounts of work — `ms_octaves` at 1 and at
85
+ 8 come back within noise of each other, which is the tell. The swap chain is pacing the
86
+ measurement. Raise `renderer.pixel_ratio` until the GPU is the bottleneck (3 gives 3840×2160 and
87
+ ~10 ms of real work here), interleave the settings round-robin so drift hits all of them equally,
88
+ and take medians. Remember the froxel grid scales with resolution, so a per-froxel cost measured at
89
+ 4× the pixels is 4× the one you will pay.
90
+
53
91
  Prefer the **signed mean** when the change has a direction; it discriminates far better than the
54
92
  mean-absolute, which is dominated by noise. C-01b read −0.148 at 60° FOV against a floor of ±0.05,
55
93
  and −1.071 at 110°: same fix, and that 7× is the `1/cos θ` scaling it predicts.
@@ -57,6 +95,68 @@ and −1.071 at 110°: same fix, and that 7× is the `1/cos θ` scaling it predi
57
95
  Also: mutating `light.intensity` does not mark the light buffer dirty, so a sweep of it renders
58
96
  identically. Remove and re-add the light instead.
59
97
 
98
+ ### measure.js
99
+
100
+ `measure.js` is that protocol as code, loaded from the console rather than by the page:
101
+
102
+ ```js
103
+ const m = await (await import('./measure.js')).attach();
104
+ const r = await m.ab(v => globalThis.__VOL_TAA_ENABLED = v, false, true);
105
+ ```
106
+
107
+ `attach` pauses the page's loop — which matters for a reason beyond frame counting: that loop
108
+ assigns `renderer.pixel_ratio = window.devicePixelRatio` on every tick, ahead of its own pause check,
109
+ so a raised pixel ratio is undone before the next frame unless the loop is stopped. `set_pixel_ratio`
110
+ replaces the property for the same reason.
111
+
112
+ Everything renders into a texture the module owns, through `Renderer.render_to_target`, which shares
113
+ the view context and therefore all the temporal history with the canvas path. That is not a
114
+ refinement: a canvas texture is configured by the presentation path, cannot carry `COPY_SRC`, and is
115
+ rotated away the moment the frame is presented, so a `drawImage` off it a task later returns an empty
116
+ bitmap — and a comparison of two blanks reports perfect agreement. Driving the frames from here also
117
+ takes the swap chain out of the timing entirely, which is the presentation-throttling trap above.
118
+
119
+ `ab` runs the two settings interleaved and takes its own floor from a repeat of each. `fly`,
120
+ `converge_at` and `series_stats` are the camera-path half: a structure fitted to the camera can be
121
+ perfectly correct standing still and swim under motion, and neither swimming nor the ghosting the
122
+ 0.95-blend TAA turns it into is visible in a still. `time` and `profile` answer different questions and
123
+ `profile` is the one to believe — at 1080p, whole-frame timing of Q-04 came back with the wrong sign
124
+ three runs in a row while the per-pass capture read it cleanly.
125
+
126
+ `profile` records a `WORKLOAD` session and decodes it in the page, so a cost number does not need a
127
+ downloaded `.sgpt` and a second tool. It returns the median duration per pass label over the frames
128
+ it recorded. Interleave the settings and take medians across rounds even so — the run-to-run spread
129
+ on this machine moves a pass by more than most changes to it do:
130
+
131
+ ```js
132
+ const p = await m.profile({ frames: 24 });
133
+ p['Volumetrics / build sun shadow volume 0'];
134
+ ```
135
+
136
+ ## Recording a GPU profile
137
+
138
+ **T** starts a recording, **T** again stops it and downloads a `.sgpt` capture. The engine side is
139
+ already there — `Renderer.profile_session` is a nullable field and the null checks around it are the
140
+ whole integration — so this page is only the two imports, the key handler, and the metadata a capture
141
+ needs in order to mean anything later: adapter, engine version, device features, and a note carrying
142
+ the viewpoint, FOV, pixel ratio and volumetric TAA setting that were in force when you pressed the
143
+ key.
144
+
145
+ Level is `WORKLOAD`: pass spans, the frame graph, and dispatch/draw counts. Measured on this page it
146
+ runs about 11 KB a frame — roughly 660 KB/s at 60 Hz, and around 393 spans per frame — with the frame
147
+ graph topology stored once and shared by every frame. `VERBOSE` above it adds bind group contents at
148
+ about seven times the rate and wants a frame limit set; `PROFILE_LEVEL` in `main.js` is the one line
149
+ to change.
150
+
151
+ Two things to know. Stopping does not end the recording immediately: a frame is committed when its
152
+ timestamp readback lands, a `mapAsync` a frame or two behind submit, so the handler drains the queue
153
+ and waits for the frame count to settle before it writes the file — otherwise the last frames, the
154
+ ones you were probably looking at, are the ones missing. And the browser asks before letting a page
155
+ save more than one file, so a second capture may never appear; the filename goes to the console
156
+ either way.
157
+
158
+ Frames only accumulate while the page is rendering, so a recording taken while paused stays at zero.
159
+
60
160
  ## Scene
61
161
 
62
162
  `flying_world_-_battle_of_the_trash_god/v2`, six participating-media volumes, a low sun, and the
@@ -80,7 +180,10 @@ accepts version 2 only, deliberately and with no backward compatibility.
80
180
  ## Notes
81
181
 
82
182
  Two viewpoints from the sibling repo (`v_volumetrics_light_extinction_0` and `_1`) sit inside
83
- geometry here and were replaced with placements verified against this build. *Log camera* writes the
183
+ geometry here and were replaced with placements verified against this build. Two more
184
+ *Beside the fog box* and *Beside the fog box, deeper* — were added for Q-04: they are the only
185
+ placements in the scene where the sun's path through the medium is short enough for the 16-step
186
+ march to miss it, which makes them the ones any change to that path has to be judged at. *Log camera* writes the
84
187
  current placement to the console in the form the `VIEWS` table takes, which is how to add more.
85
188
 
86
189
  Frame time settles around 28 ms at 1920×1080 on a 4070-class part. That is the whole frame, not the
@@ -117,12 +117,14 @@
117
117
  #status.fail { color: var(--fail); }
118
118
  #status.warn { color: var(--warn); }
119
119
 
120
- #stats {
120
+ #stats, #profile_status {
121
121
  margin-top: 4px;
122
122
  font: 12px ui-monospace, SFMono-Regular, Consolas, monospace;
123
123
  color: var(--text);
124
124
  }
125
125
 
126
+ #profile_status.recording { color: var(--fail); }
127
+
126
128
  p.note { margin: 6px 0 0; font-size: 11.5px; color: var(--muted); }
127
129
  </style>
128
130
  </head>
@@ -220,6 +222,23 @@
220
222
 
221
223
  <hr>
222
224
 
225
+ <h2>Profiling</h2>
226
+
227
+ <div id="profile_status">idle</div>
228
+ <p class="note">
229
+ <strong>T</strong> starts a GPU profile recording; <strong>T</strong> again stops it and
230
+ downloads the <code>.sgpt</code> capture. Level is <code>WORKLOAD</code> — pass spans, the
231
+ frame graph, and dispatch/draw counts, which measures around 11 KB a frame on this page.
232
+ Frames only accumulate while the page is rendering, so a recording taken while paused stays
233
+ at zero.
234
+ </p>
235
+ <p class="note">
236
+ The browser asks before letting a page save more than one file. If a second capture never
237
+ appears, that prompt is why — allow it, and the filename is on the console either way.
238
+ </p>
239
+
240
+ <hr>
241
+
223
242
  <div class="row">
224
243
  <button id="pause">Pause</button>
225
244
  <button id="log_camera">Log camera</button>
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/volumetrics_froxel/main.js"],"names":[],"mappings":"AA+TA,uCA2SC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/volumetrics_froxel/main.js"],"names":[],"mappings":"AA8ZA,uCAsfC"}
@@ -1,7 +1,10 @@
1
+ import package_json from "../../../../package.json";
1
2
  import Quaternion from "../../../core/geom/Quaternion.js";
2
3
  import Vector3 from "../../../core/geom/Vector3.js";
3
4
  import { DEG_TO_RAD } from "../../../core/math/DEG_TO_RAD.js";
4
5
  import { MIE_PARTICLES_STANDARD_PRECOMPUTED } from "../../../core/math/physics/mie/MIE_PARTICLES_STANDARD_PRECOMPUTED.js";
6
+ import { GPUProfileLevel } from "../../device/timing/profile/GPUProfileLevel.js";
7
+ import { GPUProfileSession } from "../../device/timing/profile/GPUProfileSession.js";
5
8
  import { OrbitalCameraController } from "../../renderer/camera/OrbitalCameraController.js";
6
9
  import { PerspectiveCamera } from "../../renderer/camera/PerspectiveCamera.js";
7
10
  import { create_frame_loop } from "../../renderer/create_frame_loop.js";
@@ -143,6 +146,32 @@ const VIEWS = [
143
146
  position: { x: -36.56903863396318, y: 19.1017887067011, z: 13.881382214134083 },
144
147
  rotation: { x: -0.0961072760435774, y: 0.9036695806244697, z: -0.3054532939528032, w: -0.28432897453277794 }
145
148
  },
149
+ {
150
+ id: 'fog_box_0',
151
+ label: 'Beside the fog box',
152
+ c: 'Q-04',
153
+ hint: 'the sun crosses three units of medium here — where sixteen march steps read +58',
154
+ position: { x: -49.64917207609583, y: 3.9195318273572317, z: -4.211048766762115 },
155
+ rotation: {
156
+ x: -0.037898077937503966,
157
+ y: 0.6176094542736352,
158
+ z: 0.029816629610983452,
159
+ w: 0.7850052651282708
160
+ }
161
+ },
162
+ {
163
+ id: 'fog_box_1',
164
+ label: 'Beside the fog box, deeper',
165
+ c: 'Q-04',
166
+ hint: 'the same medium a little further in; +70 for the march, and where the volume strobed',
167
+ position: { x: -50.28590468756837, y: 4.239170044321892, z: -3.9966900541348767 },
168
+ rotation: {
169
+ x: -0.03789807793750395,
170
+ y: 0.6176094542736352,
171
+ z: 0.02981662961098345,
172
+ w: 0.7850052651282708
173
+ }
174
+ },
146
175
  {
147
176
  id: 'inside_out',
148
177
  label: 'Under the island',
@@ -317,6 +346,72 @@ function aim_light(light, azimuth_degrees, elevation_degrees) {
317
346
  light.updateMatrices();
318
347
  }
319
348
 
349
+ /**
350
+ * How much each profile capture records. `WORKLOAD` is the highest of the three levels the proposal
351
+ * calls shippable: pass spans, the frame graph, and dispatch/draw counts.
352
+ *
353
+ * Measured on this page it runs ~11 KB a frame over ~393 spans, so ~660 KB/s at 60 Hz — the
354
+ * proposal's estimate of 6 KB assumed ~200 passes and this graph is about twice that. Still small
355
+ * enough that a hand-driven recording will not notice. `VERBOSE` above it adds bind group contents
356
+ * at roughly seven times the rate and wants a frame limit set, not a key you can lean on.
357
+ *
358
+ * @type {number}
359
+ */
360
+ const PROFILE_LEVEL = GPUProfileLevel.WORKLOAD;
361
+
362
+ /**
363
+ * Hand an ArrayBuffer to the browser as a file.
364
+ *
365
+ * @param {ArrayBuffer} bytes
366
+ * @param {string} filename
367
+ */
368
+ function download_bytes(bytes, filename) {
369
+ const url = URL.createObjectURL(new Blob([bytes], { type: 'application/octet-stream' }));
370
+
371
+ const anchor = document.createElement('a');
372
+
373
+ anchor.href = url;
374
+ anchor.download = filename;
375
+
376
+ document.body.appendChild(anchor);
377
+ anchor.click();
378
+ anchor.remove();
379
+
380
+ // Revoking in the same task cancels the download in some browsers — the click only schedules it.
381
+ setTimeout(() => URL.revokeObjectURL(url), 60000);
382
+ }
383
+
384
+ /**
385
+ * `YYYYMMDD-HHMMSS` in local time, matching the naming the other `.sgpt` captures on this machine
386
+ * already use — same sort order, and it reads as the clock you took it by rather than as UTC.
387
+ *
388
+ * @returns {string}
389
+ */
390
+ function file_timestamp() {
391
+ const now = new Date();
392
+
393
+ const pad = value => String(value).padStart(2, '0');
394
+
395
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
396
+ + `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
397
+ }
398
+
399
+ /**
400
+ * @param {number} bytes
401
+ * @returns {string}
402
+ */
403
+ function format_bytes(bytes) {
404
+ if (bytes < 1024) {
405
+ return `${bytes} B`;
406
+ }
407
+
408
+ if (bytes < 1024 * 1024) {
409
+ return `${(bytes / 1024).toFixed(1)} KiB`;
410
+ }
411
+
412
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
413
+ }
414
+
320
415
  export async function start() {
321
416
 
322
417
  const element = id => document.getElementById(id);
@@ -407,8 +502,17 @@ export async function start() {
407
502
 
408
503
  const views_element = element('views');
409
504
 
505
+ /**
506
+ * Which placement the camera was last sent to, for the profile capture's note — a capture that
507
+ * cannot say where it was taken is much less use a week later.
508
+ * @type {object|null}
509
+ */
510
+ let current_view = null;
511
+
410
512
  /** @param {object} view */
411
513
  function go_to(view) {
514
+ current_view = view;
515
+
412
516
  camera.transform.fromJSON({ position: view.position, rotation: view.rotation });
413
517
 
414
518
  // the controller holds its own spherical state and would otherwise snap the camera back on
@@ -559,6 +663,200 @@ export async function start() {
559
663
  repaint();
560
664
  });
561
665
 
666
+ // -- profiling: T starts a recording, T again stops it and downloads the capture
667
+
668
+ const profile_status = element('profile_status');
669
+
670
+ /**
671
+ * The recording in progress, or null.
672
+ *
673
+ * The engine never constructs one of these — `Renderer.profile_session` is a nullable field and
674
+ * the null checks around it are the entire integration, which is what keeps the profiler out of
675
+ * a bundle that does not ask for it. The two imports at the top of this file are the whole
676
+ * opt-in.
677
+ *
678
+ * @type {GPUProfileSession|null}
679
+ */
680
+ let profile_session = null;
681
+
682
+ /** Guards against T being leaned on while a stop is still draining. */
683
+ let profile_busy = false;
684
+
685
+ function set_profile_status(text, recording = false) {
686
+ profile_status.textContent = text;
687
+ profile_status.className = recording ? 'recording' : '';
688
+ }
689
+
690
+ function start_profile() {
691
+ const device = renderer.device;
692
+
693
+ const settings = [
694
+ current_view === null ? 'free camera' : current_view.label,
695
+ `fov ${Math.round(camera.fov / DEG_TO_RAD)}°`,
696
+ `volumetric TAA ${(globalThis.__VOL_TAA_ENABLED ?? true) ? 'on' : 'off'}`,
697
+ `${renderer.pixel_ratio.toFixed(2)}x`,
698
+ ];
699
+
700
+ const session = new GPUProfileSession({
701
+ level: PROFILE_LEVEL,
702
+ note: `volumetrics_froxel — ${settings.join(' · ')}`,
703
+ });
704
+
705
+ // Populate before start(): the meta record is written into the stream there and not read
706
+ // again. Without it the capture is unreadable in the sense that matters — a pass taking
707
+ // 0.4 ms says nothing until you know which GPU and at what resolution.
708
+ const meta = session.meta;
709
+
710
+ // `GPUDevice.adapterInfo` is recent enough that a browser running this page may not have it,
711
+ // and several browsers withhold the fields even when they do. Empty is what the format
712
+ // expects in that case.
713
+ const info = device.adapterInfo;
714
+
715
+ if (info !== undefined && info !== null) {
716
+ meta.adapter_vendor = info.vendor ?? "";
717
+ meta.adapter_architecture = info.architecture ?? "";
718
+ meta.adapter_device = info.device ?? "";
719
+ meta.adapter_description = info.description ?? "";
720
+ }
721
+
722
+ meta.features = [...device.features];
723
+ meta.engine_version = package_json.version;
724
+
725
+ session.onBytesWritten.add(bytes => {
726
+ set_profile_status(
727
+ `● recording — ${session.frames_recorded} frames, ${format_bytes(bytes)} · T to stop`,
728
+ true
729
+ );
730
+ });
731
+
732
+ renderer.profile_session = session;
733
+ session.start();
734
+
735
+ profile_session = session;
736
+
737
+ // The timers go quiet without this rather than failing, so the capture still carries the
738
+ // frame graph and the workload — it just has no durations in it. Worth knowing before you
739
+ // record for a minute and then look.
740
+ const timed = device.features.has('timestamp-query');
741
+
742
+ set_profile_status(
743
+ timed
744
+ ? '● recording — 0 frames · T to stop'
745
+ : '● recording without timestamp-query — structure only, no durations · T to stop',
746
+ true
747
+ );
748
+ }
749
+
750
+ /**
751
+ * Wait for frames already submitted to finish landing in the session.
752
+ *
753
+ * A frame is committed when its timestamp readback resolves — a `mapAsync` a frame or two behind
754
+ * submit, see `ShadeGPUCommandContext.profiling_absorbed` — and `record_frame` silently ignores
755
+ * anything arriving after `stop()`. Stopping the instant the key goes down therefore discards
756
+ * the last few frames, which are the ones you were most likely looking at.
757
+ *
758
+ * Drains the queue, then waits for the frame count to stop moving. Capped, because a recording
759
+ * taken while the page is paused never moves at all.
760
+ *
761
+ * @param {GPUProfileSession} session
762
+ */
763
+ async function drain_profile(session) {
764
+ const DEADLINE_MS = 1000;
765
+ const QUIET_MS = 60;
766
+
767
+ await renderer.device.queue.onSubmittedWorkDone();
768
+
769
+ const deadline = performance.now() + DEADLINE_MS;
770
+
771
+ let previous = session.frames_recorded;
772
+
773
+ while (performance.now() < deadline) {
774
+ await new Promise(resolve => setTimeout(resolve, QUIET_MS));
775
+
776
+ if (session.frames_recorded === previous) {
777
+ return;
778
+ }
779
+
780
+ previous = session.frames_recorded;
781
+ }
782
+ }
783
+
784
+ async function stop_profile() {
785
+ const session = profile_session;
786
+
787
+ profile_session = null;
788
+
789
+ // Detach first, so nothing encoded during the drain below joins the recording. Frames
790
+ // already encoded are unaffected: Renderer captures the session into a local before it
791
+ // subscribes to the readback.
792
+ renderer.profile_session = null;
793
+
794
+ set_profile_status('stopping — draining in-flight frames…');
795
+
796
+ await drain_profile(session);
797
+
798
+ const bytes = session.stop();
799
+
800
+ const filename = `volumetrics_froxel-${file_timestamp()}.sgpt`;
801
+
802
+ download_bytes(bytes, filename);
803
+
804
+ // Also to the console, because the status line is the only other place the name appears and
805
+ // a browser that declines the save says nothing about which capture it declined.
806
+ console.log(
807
+ `profile: ${session.frames_recorded} frames, ${format_bytes(bytes.byteLength)}, `
808
+ + `${session.topology_count} topolog${session.topology_count === 1 ? 'y' : 'ies'} -> ${filename}`
809
+ );
810
+
811
+ set_profile_status(
812
+ `${session.frames_recorded} frames, ${format_bytes(bytes.byteLength)}`
813
+ + `, ${session.topology_count} topolog${session.topology_count === 1 ? 'y' : 'ies'}`
814
+ + ` · ${filename}`
815
+ );
816
+ }
817
+
818
+ window.addEventListener('keydown', event => {
819
+ if (event.ctrlKey || event.metaKey || event.altKey || event.repeat) {
820
+ return;
821
+ }
822
+
823
+ // `key` first, because it is the letter actually printed on the key the user pressed —
824
+ // `code` is physical position, so on AZERTY it names the key where T sits on QWERTY. `code`
825
+ // is the fallback for layouts whose `key` is not Latin at all (Cyrillic gives "е" here).
826
+ if (event.key.toLowerCase() !== 't' && event.code !== 'KeyT') {
827
+ return;
828
+ }
829
+
830
+ // the panel's sliders and buttons are focusable; do not steal a key from a field
831
+ const target = event.target;
832
+
833
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
834
+ return;
835
+ }
836
+
837
+ if (profile_busy) {
838
+ return;
839
+ }
840
+
841
+ event.preventDefault();
842
+
843
+ profile_busy = true;
844
+
845
+ const done = profile_session === null
846
+ ? Promise.resolve(start_profile())
847
+ : stop_profile();
848
+
849
+ done.catch(error => {
850
+ set_profile_status(`profiling failed: ${error?.message ?? error}`);
851
+ console.error(error);
852
+
853
+ profile_session = null;
854
+ renderer.profile_session = null;
855
+ }).finally(() => {
856
+ profile_busy = false;
857
+ });
858
+ });
859
+
562
860
  element('pause').addEventListener('click', () => {
563
861
  paused = !paused;
564
862
  element('pause').textContent = paused ? 'Resume' : 'Pause';