@woosh/meep-engine 3.12.0 → 3.13.1

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 (301) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
  3. package/src/core/geom/3d/shape/ConvexHullShape3D.js +41 -25
  4. package/src/core/geom/3d/shape/PosedShape3D.d.ts.map +1 -1
  5. package/src/core/geom/3d/shape/PosedShape3D.js +7 -2
  6. package/src/engine/graphics3/decal/GPUDecalRenderer.js +3 -3
  7. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  8. package/src/engine/graphics3/decal/shader_decal_composite.js +4 -2
  9. package/src/engine/graphics3/highlight/GPUHighlightRenderer.d.ts.map +1 -1
  10. package/src/engine/graphics3/highlight/GPUHighlightRenderer.js +2 -3
  11. package/src/engine/graphics3/highlight/shader_highlight_outline.js +150 -150
  12. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  13. package/src/shade/RENDERER_CONTRACT.md +2 -1
  14. package/src/shade/descriptor/pipeline/compute/ComputePipelineDescriptor.d.ts +18 -0
  15. package/src/shade/descriptor/pipeline/compute/ComputePipelineDescriptor.d.ts.map +1 -1
  16. package/src/shade/descriptor/pipeline/compute/ComputePipelineDescriptor.js +73 -0
  17. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +755 -0
  18. package/src/shade/device/ShadeGPUCommandContext.d.ts +10 -16
  19. package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
  20. package/src/shade/device/ShadeGPUCommandContext.js +801 -994
  21. package/src/shade/device/pass/ShadeGPUComputePassEncoder.d.ts +107 -0
  22. package/src/shade/device/pass/ShadeGPUComputePassEncoder.d.ts.map +1 -0
  23. package/src/shade/device/pass/ShadeGPUComputePassEncoder.js +222 -0
  24. package/src/shade/device/pass/ShadeGPURenderPassEncoder.d.ts +168 -0
  25. package/src/shade/device/pass/ShadeGPURenderPassEncoder.d.ts.map +1 -0
  26. package/src/shade/device/pass/ShadeGPURenderPassEncoder.js +317 -0
  27. package/src/shade/device/timing/GPUTimerArray.d.ts +4 -9
  28. package/src/shade/device/timing/GPUTimerArray.d.ts.map +1 -1
  29. package/src/shade/device/timing/GPUTimerArray.js +261 -289
  30. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +20 -2
  31. package/src/shade/device/timing/ShadeGPUContextProfiler.d.ts +59 -0
  32. package/src/shade/device/timing/ShadeGPUContextProfiler.d.ts.map +1 -0
  33. package/src/shade/device/timing/ShadeGPUContextProfiler.js +182 -0
  34. package/src/shade/device/timing/profile/GPUProfileSession.d.ts +4 -4
  35. package/src/shade/device/timing/profile/GPUProfileSession.js +4 -4
  36. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_leaf_triangles.d.ts.map +1 -1
  37. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_leaf_triangles.js +3 -1
  38. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_points.d.ts.map +1 -1
  39. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_points.js +3 -1
  40. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_visibility.d.ts.map +1 -1
  41. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_visibility.js +3 -1
  42. package/src/shade/playground/skinned_blas_refit/verify_leaf_triangles.js +2 -2
  43. package/src/shade/playground/skinned_blas_refit/verify_traversal_reachability.js +2 -2
  44. package/src/shade/renderer/Renderer.d.ts +0 -6
  45. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  46. package/src/shade/renderer/Renderer.js +0 -29
  47. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  48. package/src/shade/renderer/animation/GPUAnimationManager.js +1 -3
  49. package/src/shade/renderer/animation/shader_animation_tick.d.ts.map +1 -1
  50. package/src/shade/renderer/animation/shader_animation_tick.js +128 -125
  51. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.d.ts.map +1 -1
  52. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.js +8 -18
  53. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_geometry_sphere_derive.d.ts.map +1 -1
  54. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_geometry_sphere_derive.js +100 -98
  55. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_reduce.d.ts.map +1 -1
  56. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_reduce.js +215 -212
  57. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_scatter.d.ts +1 -1
  58. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_scatter.d.ts.map +1 -1
  59. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_scatter.js +135 -120
  60. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_skinned_mesh_bounds_refresh.d.ts.map +1 -1
  61. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_skinned_mesh_bounds_refresh.js +77 -75
  62. package/src/shade/renderer/animation/skinning/shader_meshlet_apply_skinning.d.ts.map +1 -1
  63. package/src/shade/renderer/animation/skinning/shader_meshlet_apply_skinning.js +375 -371
  64. package/src/shade/renderer/atmosphere/shader_transmittance_lut.js +3 -3
  65. package/src/shade/renderer/buffer/graph_inspect_gpu_buffer.js +1 -1
  66. package/src/shade/renderer/buffer/table/GPUDatabase.d.ts.map +1 -1
  67. package/src/shade/renderer/buffer/table/GPUDatabase.js +14 -19
  68. package/src/shade/renderer/buffer/table/gpu_table_element_upload_shader.d.ts.map +1 -1
  69. package/src/shade/renderer/buffer/table/gpu_table_element_upload_shader.js +7 -2
  70. package/src/shade/renderer/buffer/table/single/GPUSingleTypeTable.d.ts.map +1 -1
  71. package/src/shade/renderer/buffer/table/single/GPUSingleTypeTable.js +325 -319
  72. package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.d.ts.map +1 -1
  73. package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.js +3 -1
  74. package/src/shade/renderer/deferred/shader_tonemap_HDR.d.ts.map +1 -1
  75. package/src/shade/renderer/deferred/shader_tonemap_HDR.js +4 -1
  76. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.d.ts +2 -2
  77. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.d.ts.map +1 -1
  78. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.js +167 -167
  79. package/src/shade/renderer/fow/GPUFogOfWarRenderer.js +3 -3
  80. package/src/shade/renderer/fow/shader_fog_of_war.d.ts.map +1 -1
  81. package/src/shade/renderer/fow/shader_fog_of_war.js +3 -1
  82. package/src/shade/renderer/geometry/bvh/record_blas_refit.js +102 -102
  83. package/src/shade/renderer/geometry/bvh/shader_blas_refit_leaf_bounds.d.ts.map +1 -1
  84. package/src/shade/renderer/geometry/bvh/shader_blas_refit_leaf_bounds.js +125 -121
  85. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts.map +1 -1
  86. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +766 -774
  87. package/src/shade/renderer/geometry/meshlet/shader_meshlet_metadata_address_patch.d.ts.map +1 -1
  88. package/src/shade/renderer/geometry/meshlet/shader_meshlet_metadata_address_patch.js +72 -69
  89. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts.map +1 -1
  90. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +5 -10
  91. package/src/shade/renderer/geometry/sdf/shader_build_sdf.js +129 -129
  92. package/src/shade/renderer/geometry/sdf/shader_sdf_visualize.d.ts.map +1 -1
  93. package/src/shade/renderer/geometry/sdf/shader_sdf_visualize.js +5 -3
  94. package/src/shade/renderer/global_illumination/brick4/gpu/bake/shader_brick4_bake_probes.d.ts.map +1 -1
  95. package/src/shade/renderer/global_illumination/brick4/gpu/bake/shader_brick4_bake_probes.js +4 -1
  96. package/src/shade/renderer/global_illumination/brick4/gpu/bake/shader_brick4_bake_resolve_probes.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/brick4/gpu/bake/shader_brick4_bake_resolve_probes.js +3 -1
  98. package/src/shade/renderer/global_illumination/clipmap/shader_update_clipmap_cascade.d.ts.map +1 -1
  99. package/src/shade/renderer/global_illumination/clipmap/shader_update_clipmap_cascade.js +114 -112
  100. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lpv_store_sh.d.ts.map +1 -1
  101. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lpv_store_sh.js +4 -1
  102. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lvp_atlas_to_sh.d.ts.map +1 -1
  103. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lvp_atlas_to_sh.js +187 -184
  104. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_reduce_atlas_to_sh.d.ts.map +1 -1
  105. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_reduce_atlas_to_sh.js +4 -1
  106. package/src/shade/renderer/global_illumination/sharc/SpatialHashRadianceCache.d.ts.map +1 -1
  107. package/src/shade/renderer/global_illumination/sharc/SpatialHashRadianceCache.js +6 -8
  108. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.d.ts.map +1 -1
  109. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.js +3 -1
  110. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_update.d.ts.map +1 -1
  111. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_update.js +3 -1
  112. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +3 -3
  113. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +3 -3
  114. package/src/shade/renderer/gpu_primitive/bvh/graph_bvh_build_lbvh.js +217 -217
  115. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_emit_hierarchy.d.ts.map +1 -1
  116. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_emit_hierarchy.js +145 -142
  117. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_morton.d.ts.map +1 -1
  118. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_morton.js +87 -82
  119. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_refit.d.ts.map +1 -1
  120. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_refit.js +143 -140
  121. package/src/shade/renderer/gpu_primitive/bvh/verify_gpu_bvh_build.d.ts +2 -2
  122. package/src/shade/renderer/gpu_primitive/bvh/verify_gpu_bvh_build.d.ts.map +1 -1
  123. package/src/shade/renderer/gpu_primitive/bvh/verify_gpu_bvh_build.js +43 -8
  124. package/src/shade/renderer/gpu_primitive/sort/graph_radix_sort.d.ts.map +1 -1
  125. package/src/shade/renderer/gpu_primitive/sort/graph_radix_sort.js +4 -5
  126. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_histogram.d.ts.map +1 -1
  127. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_histogram.js +88 -84
  128. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_scatter.d.ts.map +1 -1
  129. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_scatter.js +194 -191
  130. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  131. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +33 -69
  132. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  133. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +6 -1
  134. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +1 -1
  135. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.js +23 -7
  136. package/src/shade/renderer/light/environment/execute_filter_environment_map.d.ts.map +1 -1
  137. package/src/shade/renderer/light/environment/execute_filter_environment_map.js +1 -2
  138. package/src/shade/renderer/light/environment/shader_convolve_reflection_map.d.ts.map +1 -1
  139. package/src/shade/renderer/light/environment/shader_convolve_reflection_map.js +6 -1
  140. package/src/shade/renderer/lightmap/bake/raster/graph_draw_lightmap_viz_buffer.d.ts.map +1 -1
  141. package/src/shade/renderer/lightmap/bake/raster/graph_draw_lightmap_viz_buffer.js +234 -219
  142. package/src/shade/renderer/lightmap/bake/raster/shader_lightmap_to_viz_buffer.js +2 -2
  143. package/src/shade/renderer/lightmap/bake/shader_lightmap_denoise_sh.js +191 -191
  144. package/src/shade/renderer/lightmap/bake/tile/shader_lightmap_accummulate_tile.d.ts.map +1 -1
  145. package/src/shade/renderer/lightmap/bake/tile/shader_lightmap_accummulate_tile.js +85 -82
  146. package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
  147. package/src/shade/renderer/particles/graph_particles.js +5 -3
  148. package/src/shade/renderer/particles/prototypeParticleSystem.js +3 -4
  149. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
  150. package/src/shade/renderer/particles/shaders/shader_particle_emit.js +132 -117
  151. package/src/shade/renderer/particles/shaders/shader_particle_finalize.d.ts.map +1 -1
  152. package/src/shade/renderer/particles/shaders/shader_particle_finalize.js +79 -77
  153. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  154. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +111 -99
  155. package/src/shade/renderer/particles/sort/shader_particle_sort.d.ts.map +1 -1
  156. package/src/shade/renderer/particles/sort/shader_particle_sort.js +187 -158
  157. package/src/shade/renderer/path_tracer/accumulating/AccumulatingPathTracer.d.ts.map +1 -1
  158. package/src/shade/renderer/path_tracer/accumulating/AccumulatingPathTracer.js +7 -10
  159. package/src/shade/renderer/path_tracer/accumulating/shader_accumulating_path_tracer.d.ts.map +1 -1
  160. package/src/shade/renderer/path_tracer/accumulating/shader_accumulating_path_tracer.js +4 -1
  161. package/src/shade/renderer/postprocess/bloom/shader_bloom_mix.d.ts.map +1 -1
  162. package/src/shade/renderer/postprocess/bloom/shader_bloom_mix.js +3 -1
  163. package/src/shade/renderer/postprocess/cas/shader_ffx_rcas.d.ts.map +1 -1
  164. package/src/shade/renderer/postprocess/cas/shader_ffx_rcas.js +4 -2
  165. package/src/shade/renderer/postprocess/denoise/fragment_shader_denoise_ao.js +147 -147
  166. package/src/shade/renderer/postprocess/denoise/graph_atorus_denoise_luma.d.ts.map +1 -1
  167. package/src/shade/renderer/postprocess/denoise/graph_atorus_denoise_luma.js +121 -119
  168. package/src/shade/renderer/postprocess/dof/gather/graph_postprocess_dof2.d.ts.map +1 -1
  169. package/src/shade/renderer/postprocess/dof/gather/graph_postprocess_dof2.js +1 -2
  170. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_gather_far.d.ts.map +1 -1
  171. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_gather_far.js +4 -1
  172. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_gather_near.d.ts.map +1 -1
  173. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_gather_near.js +4 -1
  174. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_recombine.d.ts.map +1 -1
  175. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_recombine.js +4 -1
  176. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_setup.d.ts.map +1 -1
  177. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_setup.js +4 -1
  178. package/src/shade/renderer/postprocess/dof/raymarch/graph_postprocess_dof.d.ts.map +1 -1
  179. package/src/shade/renderer/postprocess/dof/raymarch/graph_postprocess_dof.js +2 -4
  180. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_composite.d.ts.map +1 -1
  181. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_composite.js +4 -1
  182. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_prefilter.d.ts.map +1 -1
  183. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_prefilter.js +4 -1
  184. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_raymarch.d.ts.map +1 -1
  185. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_raymarch.js +4 -1
  186. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_raymarch_compute.d.ts.map +1 -1
  187. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_raymarch_compute.js +269 -266
  188. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_slice_compute.d.ts.map +1 -1
  189. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_slice_compute.js +273 -270
  190. package/src/shade/renderer/postprocess/dof/shader_dof_autofocus.d.ts.map +1 -1
  191. package/src/shade/renderer/postprocess/dof/shader_dof_autofocus.js +4 -1
  192. package/src/shade/renderer/postprocess/eye/GPUCameraExposureManager.d.ts.map +1 -1
  193. package/src/shade/renderer/postprocess/eye/GPUCameraExposureManager.js +8 -11
  194. package/src/shade/renderer/postprocess/eye/shader_autoexposure_adapt.d.ts.map +1 -1
  195. package/src/shade/renderer/postprocess/eye/shader_autoexposure_adapt.js +5 -1
  196. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts.map +1 -1
  197. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js +11 -8
  198. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts.map +1 -1
  199. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +7 -8
  200. package/src/shade/renderer/postprocess/motion_blur/shader_motion_blur_reconstruct.d.ts.map +1 -1
  201. package/src/shade/renderer/postprocess/motion_blur/shader_motion_blur_reconstruct.js +3 -1
  202. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  203. package/src/shade/renderer/postprocess/nss/shader_nss_concat.d.ts.map +1 -1
  204. package/src/shade/renderer/postprocess/nss/shader_nss_concat.js +70 -68
  205. package/src/shade/renderer/postprocess/ssr/depth_max_shader.d.ts.map +1 -1
  206. package/src/shade/renderer/postprocess/ssr/depth_max_shader.js +70 -68
  207. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader_IBL.d.ts.map +1 -1
  208. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader_IBL.js +3 -1
  209. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader_LPV.d.ts.map +1 -1
  210. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader_LPV.js +4 -1
  211. package/src/shade/renderer/postprocess/ssr/shader_downsample_encoded_normal.d.ts.map +1 -1
  212. package/src/shade/renderer/postprocess/ssr/shader_downsample_encoded_normal.js +103 -101
  213. package/src/shade/renderer/postprocess/ssr/ssr_spatial_denoise_shader.d.ts.map +1 -1
  214. package/src/shade/renderer/postprocess/ssr/ssr_spatial_denoise_shader.js +4 -2
  215. package/src/shade/renderer/postprocess/ssr/ssr_trace_shader.d.ts.map +1 -1
  216. package/src/shade/renderer/postprocess/ssr/ssr_trace_shader.js +3 -1
  217. package/src/shade/renderer/postprocess/taa/shader_taa.d.ts.map +1 -1
  218. package/src/shade/renderer/postprocess/taa/shader_taa.js +4 -1
  219. package/src/shade/renderer/postprocess/upscale-dither/fragment_shader_upscale_dither.js +5 -5
  220. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  221. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +339 -341
  222. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.d.ts.map +1 -1
  223. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.js +114 -109
  224. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.d.ts.map +1 -1
  225. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.js +118 -115
  226. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.d.ts.map +1 -1
  227. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.js +69 -65
  228. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.js +722 -722
  229. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  230. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +1 -9
  231. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  232. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +1 -6
  233. package/src/shade/renderer/restir/di/graph_restir_di.d.ts.map +1 -1
  234. package/src/shade/renderer/restir/di/graph_restir_di.js +2 -3
  235. package/src/shade/renderer/restir/di/graph_restir_di_denoise.d.ts.map +1 -1
  236. package/src/shade/renderer/restir/di/graph_restir_di_denoise.js +1 -2
  237. package/src/shade/renderer/restir/di/shader_restir_di_denoise.d.ts.map +1 -1
  238. package/src/shade/renderer/restir/di/shader_restir_di_denoise.js +139 -136
  239. package/src/shade/renderer/restir/di/shader_restir_di_resample.d.ts.map +1 -1
  240. package/src/shade/renderer/restir/di/shader_restir_di_resample.js +131 -129
  241. package/src/shade/renderer/restir/di/shader_restir_di_resolve.d.ts.map +1 -1
  242. package/src/shade/renderer/restir/di/shader_restir_di_resolve.js +101 -99
  243. package/src/shade/renderer/scene/GPUInstancesAccelerationStructure.d.ts.map +1 -1
  244. package/src/shade/renderer/scene/GPUInstancesAccelerationStructure.js +379 -383
  245. package/src/shade/renderer/scene/Mesh.d.ts.map +1 -1
  246. package/src/shade/renderer/scene/Mesh.js +4 -6
  247. package/src/shade/renderer/scene/shader_bvh_extract_instance_leaves.d.ts.map +1 -1
  248. package/src/shade/renderer/scene/shader_bvh_extract_instance_leaves.js +147 -144
  249. package/src/shade/renderer/shader/ComputeShader.d.ts +2 -2
  250. package/src/shade/renderer/shader/ComputeShader.d.ts.map +1 -1
  251. package/src/shade/renderer/shader/ComputeShader.js +14 -17
  252. package/src/shade/renderer/shader/pass/RenderPassDescriptor.d.ts +2 -2
  253. package/src/shade/renderer/shader/pass/RenderPassDescriptor.d.ts.map +1 -1
  254. package/src/shade/renderer/shader/pass/RenderPassDescriptor.js +189 -189
  255. package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.d.ts.map +1 -1
  256. package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.js +3 -1
  257. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts +16 -7
  258. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  259. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +1342 -1326
  260. package/src/shade/renderer/shadow/map/Shadowmap.d.ts +13 -0
  261. package/src/shade/renderer/shadow/map/Shadowmap.d.ts.map +1 -1
  262. package/src/shade/renderer/shadow/map/Shadowmap.js +14 -0
  263. package/src/shade/renderer/shadow/map/ShadowmapDrawSelector.d.ts +79 -0
  264. package/src/shade/renderer/shadow/map/ShadowmapDrawSelector.d.ts.map +1 -0
  265. package/src/shade/renderer/shadow/map/ShadowmapDrawSelector.js +196 -0
  266. package/src/shade/renderer/shadow/map/ShadowmapRegistry.d.ts +91 -0
  267. package/src/shade/renderer/shadow/map/ShadowmapRegistry.d.ts.map +1 -0
  268. package/src/shade/renderer/shadow/map/ShadowmapRegistry.js +156 -0
  269. package/src/shade/renderer/shadow/map/shader/shader_shadowmap_remap_cube_to_octahedral.d.ts.map +1 -1
  270. package/src/shade/renderer/shadow/map/shader/shader_shadowmap_remap_cube_to_octahedral.js +5 -1
  271. package/src/shade/renderer/shadow/ray/shader_shadow_upscale.d.ts.map +1 -1
  272. package/src/shade/renderer/shadow/ray/shader_shadow_upscale.js +3 -1
  273. package/src/shade/renderer/shadow/ray/staged/gen/graph_gen_rays.js +73 -73
  274. package/src/shade/renderer/shadow/ray/staged/gen/shader_generate_rays.d.ts.map +1 -1
  275. package/src/shade/renderer/shadow/ray/staged/gen/shader_generate_rays.js +151 -148
  276. package/src/shade/renderer/shadow/ray/staged/trace/graph_trace_rays.d.ts.map +1 -1
  277. package/src/shade/renderer/shadow/ray/staged/trace/graph_trace_rays.js +4 -2
  278. package/src/shade/renderer/shadow/ray/staged/trace/shader_trace_rays.d.ts.map +1 -1
  279. package/src/shade/renderer/shadow/ray/staged/trace/shader_trace_rays.js +18 -4
  280. package/src/shade/renderer/shadow/sdf/cascade/CascadedSceneSDF.d.ts.map +1 -1
  281. package/src/shade/renderer/shadow/sdf/cascade/CascadedSceneSDF.js +7 -8
  282. package/src/shade/renderer/shadow/sdf/cascade/shader_build_scene_sdf_f32.d.ts.map +1 -1
  283. package/src/shade/renderer/shadow/sdf/cascade/shader_build_scene_sdf_f32.js +6 -2
  284. package/src/shade/renderer/shadow/sdf/cascade/shader_scene_sdf_debug_viz.d.ts.map +1 -1
  285. package/src/shade/renderer/shadow/sdf/cascade/shader_scene_sdf_debug_viz.js +129 -127
  286. package/src/shade/renderer/shadow/sdf/graph_draw_sdf_shadows.d.ts.map +1 -1
  287. package/src/shade/renderer/shadow/sdf/graph_draw_sdf_shadows.js +5 -3
  288. package/src/shade/renderer/texture/virtual/VirtualTextureManager.d.ts.map +1 -1
  289. package/src/shade/renderer/texture/virtual/VirtualTextureManager.js +2 -5
  290. package/src/shade/renderer/texture/virtual/build_shader_vt_feedback.d.ts.map +1 -1
  291. package/src/shade/renderer/texture/virtual/build_shader_vt_feedback.js +233 -231
  292. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  293. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +2 -4
  294. package/src/shade/wgsl/validate_wgsl_source.d.ts.map +1 -1
  295. package/src/shade/wgsl/validate_wgsl_source.js +8 -3
  296. package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts +0 -21
  297. package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts.map +0 -1
  298. package/src/shade/device/timing/profile/make_profiling_pass_encoder.js +0 -114
  299. package/src/shade/device/timing/profile/parse_workgroup_size.d.ts +0 -20
  300. package/src/shade/device/timing/profile/parse_workgroup_size.d.ts.map +0 -1
  301. package/src/shade/device/timing/profile/parse_workgroup_size.js +0 -43
@@ -0,0 +1,755 @@
1
+ # Pass encoder decorators, and the end of the debug-timer path
2
+
3
+ **Status:** implemented — revision 3
4
+ **Date:** 2026-08-31
5
+ **Scope:** `src/shade/device/ShadeGPUCommandContext.js`, `src/shade/device/timing/`, `src/shade/descriptor/pipeline/compute/`, `src/shade/renderer/Renderer.js`
6
+ **Supersedes:** [GPU_PROFILER_PROPOSAL_2026_08_28.md](timing/GPU_PROFILER_PROPOSAL_2026_08_28.md) §6.1 (last paragraph) and §6.2 hook 3
7
+
8
+ > **Revision 2** carried the workgroup size rather than re-parsing it (§3.5), dropped the pretence
9
+ > of WebGPU overload compatibility (§3.2), removed the scratch-object side channel revision 1
10
+ > introduced (§3.4), and added an adversarial pass (§8) that changed four decisions.
11
+ >
12
+ > **Revision 3** records what shipped. The §9 decisions were taken (§9), the error-carrying
13
+ > `workgroup_size_error` of revision 2 was rejected in favour of throwing at construction (§3.5),
14
+ > and the code landed in three commits (§10).
15
+ >
16
+ > **Revision 4** rewrites §3.2, which was wrong and shipped a renderer that could not draw. Revision
17
+ > 2 identified `setBindGroup` as the one forward that could be actively wrong and then guarded the
18
+ > wrong end of it. §8.9 is the post-mortem; §3.2 is now the rule that replaced it.
19
+
20
+ ---
21
+
22
+ ## 1. What is there now
23
+
24
+ Profiling reaches a GPU pass through four private members of `ShadeGPUCommandContext` and one `Proxy`.
25
+
26
+ | Piece | Where | What it does |
27
+ |---|---|---|
28
+ | `#with_timestamp_writes(descriptor, kind)` | [ShadeGPUCommandContext.js:461](ShadeGPUCommandContext.js:461) | Branches on the string `'compute'`/`'render'`, asks `GPUTimerArray` for a slot, shallow-copies the descriptor to add `timestampWrites`, and **as a side effect** writes `#pending_work`. |
29
+ | `#pending_work` | [:128](ShadeGPUCommandContext.js:128) | A one-slot channel between `#with_timestamp_writes` and `#instrument_pass`. |
30
+ | `#instrument_pass(encoder)` | [:136](ShadeGPUCommandContext.js:136) | Reads `#pending_work`, clears it, writes `#last_work`, and returns either the raw encoder or a `Proxy`. |
31
+ | `#last_work` | [:577](ShadeGPUCommandContext.js:577) | A second one-slot channel, this time between `begin*Pass` and `#note_pipeline`. |
32
+ | `make_profiling_pass_encoder` | [timing/profile/make_profiling_pass_encoder.js](timing/profile/make_profiling_pass_encoder.js) | A `Proxy` whose `get` trap switches on the method **name as a string**, wraps six of them, memoises bound closures in a `Map`, and forwards the rest by `Reflect`. |
33
+ | `parse_workgroup_size` | [timing/profile/parse_workgroup_size.js](timing/profile/parse_workgroup_size.js) | Re-parses WGSL per pass to recover `@workgroup_size`. Already marked `@deprecated`: it duplicates `compute_infer_workgroup_size_from_source`, badly. |
34
+
35
+ So `beginComputePass` reads:
36
+
37
+ ```js
38
+ const _descriptor = this.#with_timestamp_writes(descriptor, 'compute');
39
+
40
+ return this.#instrument_pass(encoder.beginComputePass(_descriptor))
41
+ ```
42
+
43
+ Two calls, neither of which names what it produced, coupled through two private fields that neither
44
+ signature mentions.
45
+
46
+ Alongside it, a second entry point into the same machinery:
47
+
48
+ | Piece | Where | What it does |
49
+ |---|---|---|
50
+ | `enable_debug_timers(cb)` | [:93](ShadeGPUCommandContext.js:93) | Allocates the `GPUTimerArray`, and at `finish()` hands `results_to_console_table()` to a callback. |
51
+ | `Renderer.add_debug_frame(n)` / `Renderer.onFrameDebug` | [Renderer.js:671](../renderer/Renderer.js:671), [:431](../renderer/Renderer.js:431) | Arms N frames, fires the table. |
52
+
53
+ ## 2. What is wrong with it
54
+
55
+ **The two hidden channels.** `#pending_work` and `#last_work` are the whole objection. Neither
56
+ appears in a signature. `#with_timestamp_writes` is a *getter-shaped* method that mutates; the only
57
+ thing keeping it correct is that the very next statement consumes what it left behind. Insert a line
58
+ between them, reach `beginComputePass` from inside its call tree, or throw part way, and the next
59
+ pass silently inherits the previous pass's work record. Nothing would catch it.
60
+
61
+ **`kind` as a string.** `#with_timestamp_writes(descriptor, 'compute')` re-derives, by string
62
+ comparison, a fact the call site already knew statically — it is *literally inside*
63
+ `beginComputePass`.
64
+
65
+ **Reflection where a method call belongs.** `make_profiling_pass_encoder` switches on
66
+ `'dispatchWorkgroups'`, `'draw'`, `'drawIndexed'` as strings, in a `Proxy` `get` trap, on the hot
67
+ encoding path. `work.dispatch_count++` is three characters of real logic wrapped in a dispatch
68
+ mechanism.
69
+
70
+ **The Proxy's own justification does not survive contact with the fix.** The file argues a
71
+ hand-written forwarder is dangerous because a missed method "does not fail loudly, it silently drops
72
+ a call … and only while profiling". That is true *only because the wrapper is conditional*. Make the
73
+ decorator the one and only pass type Shade hands out, and the failure mode inverts: a missing method
74
+ is `pass.setScissorRect is not a function`, on the first frame, in every build, profiling or not. The
75
+ argument for the Proxy is an argument against *conditional* wrapping, not against hand-writing the
76
+ forwarder.
77
+
78
+ **Three silent returns.** `#with_timestamp_writes` returns the descriptor unchanged for three
79
+ different reasons (timers off, device lacks `timestamp-query`, array full) and the caller cannot tell
80
+ which — nor that the pass ended up untimed.
81
+
82
+ **A second, worse WGSL parser, run per pass.** `parse_workgroup_size` reads a bare decimal literal
83
+ and returns `[0,0,0]` for anything else — while `compute_infer_workgroup_size_from_source`, twenty
84
+ metres away, resolves `const` references, strips comments, handles hex and `i`/`u` suffixes, and
85
+ throws with a specific message when it genuinely cannot tell. `ComputeShader` already calls the good
86
+ one **once, at construction**, and keeps the answer. The profiler re-derives a worse answer at encode
87
+ time, for a value that has not changed since the pipeline was created.
88
+
89
+ **`enable_debug_timers` is a redundant second face.** It shares the `GPUTimerArray` with
90
+ `enable_profiling`, either may allocate it, and `finish()` runs both drains. It produces a
91
+ slot-shaped console table with no graph-pass attribution, no workload, no pipeline identity — a
92
+ strictly worse view than the profiler's, reached through a separate API. Its consumers:
93
+
94
+ - `Renderer.add_debug_frame` / `Renderer.onFrameDebug` — **zero subscribers anywhere in the tree**
95
+ (grep across all packages; the 2026-08-28 proposal noted the same and left them in).
96
+ - [verify_gpu_bvh_build.js:316](../renderer/gpu_primitive/bvh/verify_gpu_bvh_build.js:316) — one
97
+ developer verification tool that wants a console table.
98
+
99
+ That is the entire public surface. It is the cheapest moment this will ever be to remove it.
100
+
101
+ ---
102
+
103
+ ## 3. Proposed design
104
+
105
+ ### 3.1 `ShadeGPUComputePassEncoder` / `ShadeGPURenderPassEncoder`
106
+
107
+ Two concrete decorator classes, in `src/shade/device/pass/`. **Every** pass Shade opens is one of
108
+ these; there is no unwrapped path and no conditional wrapping. They hold a nullable
109
+ `GPUProfileWork` and tally into it.
110
+
111
+ They import **nothing** from `timing/profile/`. `work` is a bag of counters, `null` is the ordinary
112
+ case, and the classes do not know what a capture is — which preserves (in fact improves) the
113
+ tree-shaking property `GPUProfileSession`'s header promises.
114
+
115
+ ### 3.2 Forward exactly the arity you received
116
+
117
+ These are not a compatibility shim. Following WebGPU's method names removes friction for anyone who
118
+ already knows the API; it is not a promise of substitutability.
119
+
120
+ **The rule, learnt the hard way (§8.9): a forwarder passes exactly the arity it was given, and never
121
+ reasons about what a binding layer will do with a trailing `undefined`.** Three shapes satisfy it,
122
+ and every method in both classes uses one of them:
123
+
124
+ ```js
125
+ // 1. Only forwards, and has trailing optionals → rest arguments. Arity is preserved by
126
+ // construction, which is the property that matters and the one a reader should not have to
127
+ // verify branch by branch. `size` here means "the rest of the buffer" and has no default to
128
+ // stand in for at all, so an absent argument and an `undefined` one are different requests.
129
+ setVertexBuffer(...args) {
130
+ this.#pass.setVertexBuffer(...args);
131
+ }
132
+
133
+ // 2. One trailing optional → branch on it. Cheap, and it also means this form can never select
134
+ // the five-argument overload by accident, which is what makes `setBindGroupRange` an honest
135
+ // name rather than only a convention.
136
+ setBindGroup(index, bind_group, dynamic_offsets) {
137
+ if (dynamic_offsets === undefined) {
138
+ this.#pass.setBindGroup(index, bind_group);
139
+ } else {
140
+ this.#pass.setBindGroup(index, bind_group, dynamic_offsets);
141
+ }
142
+ }
143
+
144
+ // 3. Has to read its arguments in order to count them → named parameters, with WebGPU's own
145
+ // defaults spelled out. Those defaults are 1 and 0, so materialising them passes four defined
146
+ // values rather than a trailing `undefined`, and is equivalent to omitting them.
147
+ draw(vertex_count, instance_count = 1, first_vertex = 0, first_instance = 0) { /* ... */ }
148
+ ```
149
+
150
+ Where WebGPU resolves an overload by arity, Shade gives each form its own name: `setBindGroup` for
151
+ the `sequence` form and `setBindGroupRange` for the `Uint32Array`-slice form. Nothing in the
152
+ renderer uses the slice form today; it exists because `SoftwareGPUComputePassEncoder` models it.
153
+
154
+ > Revision 2 got this exactly half right. It identified `setBindGroup` as the one forward that could
155
+ > be *wrong* rather than merely incomplete, then guarded the wrong end of it — see §8.9.
156
+
157
+ ### 3.3 The compute decorator in full
158
+
159
+ ```js
160
+ export class ShadeGPUComputePassEncoder {
161
+
162
+ /** @type {GPUComputePassEncoder} */
163
+ #pass;
164
+
165
+ /**
166
+ * Where this pass's encoded work is tallied, or null when nothing is profiling.
167
+ * @type {GPUProfileWork|null}
168
+ */
169
+ #work;
170
+
171
+ #ended = false;
172
+
173
+ /**
174
+ * @param {GPUComputePassEncoder} pass
175
+ * @param {GPUProfileWork|null} work
176
+ */
177
+ constructor(pass, work) {
178
+ assert.defined(pass, 'pass');
179
+
180
+ this.#pass = pass;
181
+ this.#work = work;
182
+ }
183
+
184
+ /** @returns {string} */
185
+ get label() { return this.#pass.label; }
186
+
187
+ /** @param {string} value */
188
+ set label(value) { this.#pass.label = value; }
189
+
190
+ // --- GPUComputePassEncoder ------------------------------------------------
191
+
192
+ setPipeline(pipeline) {
193
+ this.#pass.setPipeline(pipeline);
194
+ }
195
+
196
+ dispatchWorkgroups(x, y = 1, z = 1) {
197
+ const work = this.#work;
198
+
199
+ if (work !== null) {
200
+ work.dispatch_count++;
201
+ work.workgroups[0] += x;
202
+ work.workgroups[1] += y;
203
+ work.workgroups[2] += z;
204
+ }
205
+
206
+ this.#pass.dispatchWorkgroups(x, y, z);
207
+ }
208
+
209
+ dispatchWorkgroupsIndirect(indirect_buffer, indirect_offset) {
210
+ const work = this.#work;
211
+
212
+ if (work !== null) {
213
+ // Size lives in a GPU buffer, so there is nothing to add to `workgroups`. Counted
214
+ // apart so the totals are not read as covering this.
215
+ work.indirect_dispatch_count++;
216
+ }
217
+
218
+ this.#pass.dispatchWorkgroupsIndirect(indirect_buffer, indirect_offset);
219
+ }
220
+
221
+ end() {
222
+ assert.notOk(this.#ended, 'pass already ended');
223
+
224
+ this.#ended = true;
225
+
226
+ this.#pass.end();
227
+ }
228
+
229
+ // --- GPUBindingCommandsMixin ----------------------------------------------
230
+
231
+ setBindGroup(index, bind_group, dynamic_offsets) { /* §3.2 */ }
232
+
233
+ setBindGroupRange(index, bind_group, data, data_start, data_length) { /* §3.2 */ }
234
+
235
+ setImmediates(...args) { /* §3.2, shape 1 */ }
236
+
237
+ // --- GPUDebugCommandsMixin ------------------------------------------------
238
+
239
+ pushDebugGroup(label) { this.#pass.pushDebugGroup(label); }
240
+
241
+ popDebugGroup() { this.#pass.popDebugGroup(); }
242
+
243
+ insertDebugMarker(label) { this.#pass.insertDebugMarker(label); }
244
+
245
+ // --- Shade ----------------------------------------------------------------
246
+
247
+ /**
248
+ * Record which pipeline descriptor this pass ran.
249
+ *
250
+ * Separate from `setPipeline` because `setPipeline` receives the compiled
251
+ * {@link GPUComputePipeline}, and neither the label the descriptor was given nor its workgroup
252
+ * size is on it. Only `constructComputePass` holds the descriptor, so a pass begun the long way
253
+ * records its counts but not what ran them — which the model represents as an unknown rather
254
+ * than a zero.
255
+ *
256
+ * @param {ComputePipelineDescriptor} descriptor
257
+ * @returns {void}
258
+ */
259
+ record_pipeline(descriptor) {
260
+ const work = this.#work;
261
+
262
+ if (work === null) {
263
+ return;
264
+ }
265
+
266
+ work.pipeline = descriptor.label ?? "";
267
+ work.workgroup_size = descriptor.workgroup_size;
268
+ }
269
+ }
270
+ ```
271
+
272
+ `ShadeGPURenderPassEncoder` is the mirror image over the render surface: `setViewport`,
273
+ `setScissorRect`, `setBlendConstant`, `setStencilReference`, `setIndexBuffer`, `setVertexBuffer`,
274
+ `beginOcclusionQuery`, `endOcclusionQuery`, `executeBundles`, `draw`, `drawIndexed`, `drawIndirect`,
275
+ `drawIndexedIndirect`, plus the same two mixins, `end` and `record_pipeline`. Only the four `draw*`
276
+ methods carry a tally; the rest forward.
277
+
278
+ **Only six methods branch** — four `draw*` and two `dispatch*`. Everything else is a one-line
279
+ forward. Both classes come to roughly 130 lines and read top to bottom as a complete statement of the
280
+ interface they decorate, which is the entire point.
281
+
282
+ > **Known blind spot, inherited:** draws inside a `GPURenderBundle` passed to `executeBundles` are
283
+ > not counted — the bundle was encoded elsewhere. The Proxy had the same gap and nothing in Shade
284
+ > uses bundles today. It belongs in `GPUProfileWork`'s documentation rather than being left implicit.
285
+
286
+ ### 3.4 The context's profiling state, in one object
287
+
288
+ `ShadeGPUCommandContext` currently carries six fields for a feature that is off by default:
289
+ `#debug_timers`, `#debug_timers_callback`, `#profile_recorder`, `#profile_query_set_id`,
290
+ `#pending_work`, `#last_work`, plus `#profiling_absorbed` and its resolver. Collapse them into one
291
+ nullable `#profiler`.
292
+
293
+ The profiler **opens the pass itself**. Two values are needed at two different instants —
294
+ `timestampWrites` before the encoder exists, the `GPUProfileWork` after — and any design that returns
295
+ them to the caller has to carry them across that gap somehow. Handing the profiler the encoder
296
+ collapses the gap: both values are produced and consumed inside one method, and there is nothing to
297
+ carry. (Revision 1 returned a reused `{writes, work}` struct, which was `#pending_work` again in
298
+ better clothes. See §8.3.)
299
+
300
+ ```js
301
+ /**
302
+ * Everything one command context needs in order to be profiled: its timer array, the recorder the
303
+ * timings go to, and which query set it is within the frame.
304
+ */
305
+ export class ShadeGPUContextProfiler {
306
+
307
+ #timers;
308
+ #recorder;
309
+ #query_set_id;
310
+
311
+ #absorbed;
312
+ #absorbed_resolve;
313
+
314
+ constructor(device, recorder, query_set_id) {
315
+ this.#timers = new GPUTimerArray(device);
316
+ this.#recorder = recorder;
317
+ this.#query_set_id = query_set_id;
318
+
319
+ this.#absorbed = new Promise(resolve => { this.#absorbed_resolve = resolve; });
320
+ }
321
+
322
+ get absorbed() { return this.#absorbed; }
323
+
324
+ /**
325
+ * @param {GPUCommandEncoder} encoder
326
+ * @param {GPUComputePassDescriptor} [descriptor]
327
+ * @returns {ShadeGPUComputePassEncoder}
328
+ */
329
+ begin_compute_pass(encoder, descriptor) {
330
+ const writes = this.#timers.getComputeWrites(descriptor?.label);
331
+
332
+ if (writes === undefined) {
333
+ /*
334
+ No slot left, or the device withheld `timestamp-query`. An untimed pass is the correct
335
+ degradation in both; a pass carrying query indices the set does not have is a
336
+ validation error blamed on a pass that did nothing wrong.
337
+ */
338
+ return new ShadeGPUComputePassEncoder(encoder.beginComputePass(descriptor), null);
339
+ }
340
+
341
+ /*
342
+ Copied, never written through. Several call sites reuse one descriptor across passes, and
343
+ stamping `timestampWrites` onto it left the next pass carrying the previous pass's query
344
+ indices. `{...undefined}` is `{}`, which is also how the no-descriptor form stays legal.
345
+ */
346
+ const gpu_pass = encoder.beginComputePass({ ...descriptor, timestampWrites: writes });
347
+
348
+ return new ShadeGPUComputePassEncoder(gpu_pass, this.#note_slot(writes));
349
+ }
350
+
351
+ /** The render mirror of {@link begin_compute_pass}. */
352
+ begin_render_pass(encoder, descriptor) { /* getRenderWrites, ShadeGPURenderPassEncoder */ }
353
+
354
+ /**
355
+ * @param {GPUComputePassTimestampWrites|GPURenderPassTimestampWrites} writes
356
+ * @returns {GPUProfileWork|null}
357
+ */
358
+ #note_slot(writes) {
359
+ /*
360
+ The one moment the join can be made. The recorder knows which frame graph pass is
361
+ executing right now; the slot is what the timings will come back keyed by. Neither fact is
362
+ recoverable later — see GPUFrameRecorder.
363
+ */
364
+ return this.#recorder.note_pass_slot(
365
+ this.#query_set_id,
366
+ writes.beginningOfPassWriteIndex / 2
367
+ );
368
+ }
369
+
370
+ /** @param {GPUCommandEncoder} encoder */
371
+ resolve(encoder) { this.#timers.resolve(encoder); }
372
+
373
+ /**
374
+ * Read the timings back and hand them to the recorder. Resolves {@link absorbed} either way.
375
+ * @returns {void}
376
+ */
377
+ absorb() {
378
+ const timers = this.#timers;
379
+
380
+ timers.download_results()
381
+ .then(() => { this.#recorder.absorb(timers, this.#query_set_id); })
382
+ .finally(() => {
383
+ timers.destroy();
384
+
385
+ // In `finally` so a failed readback still releases whoever is waiting on the frame.
386
+ // A profile session that hangs because one download rejected is worse than one
387
+ // missing a frame.
388
+ this.#absorbed_resolve();
389
+ });
390
+ }
391
+ }
392
+ ```
393
+
394
+ `beginComputePass` on the context becomes five lines with two named outcomes:
395
+
396
+ ```js
397
+ /**
398
+ * @param {GPUComputePassDescriptor} [descriptor]
399
+ * @returns {ShadeGPUComputePassEncoder}
400
+ */
401
+ beginComputePass(descriptor) {
402
+ const profiler = this.#profiler;
403
+
404
+ if (profiler === null) {
405
+ return new ShadeGPUComputePassEncoder(this.#gpu_encoder.beginComputePass(descriptor), null);
406
+ }
407
+
408
+ return profiler.begin_compute_pass(this.#gpu_encoder, descriptor);
409
+ }
410
+ ```
411
+
412
+ No `kind` string. No hidden field. `beginRenderPass` is the same five lines.
413
+
414
+ `constructComputePass` loses `#note_pipeline` and `#last_work` entirely:
415
+
416
+ ```js
417
+ const pass = this.beginComputePass(pass_descriptor);
418
+
419
+ pass.setPipeline(gpu_pipeline);
420
+ pass.record_pipeline(pipeline);
421
+
422
+ graphics.setPipelineBindings(pass, pipeline, bindings, immediate);
423
+ ```
424
+
425
+ ### 3.5 The workgroup size is carried, not re-parsed
426
+
427
+ `ComputePipelineDescriptor` reads `@workgroup_size` out of its code **as the code is set**, in
428
+ `from`, `simplified` and `fromJSON`, and becomes the single owner of the answer:
429
+
430
+ ```js
431
+ /**
432
+ * `[x, y, z]` of this pipeline's `@workgroup_size`.
433
+ *
434
+ * Read once, when the code is set, and kept — the source cannot change under a descriptor without
435
+ * going back through one of the factories, each of which reads it again.
436
+ *
437
+ * Frozen, because it is handed out by reference to per-pass profile records and a descriptor that
438
+ * outlives a whole capture must not be mutable through one of them.
439
+ *
440
+ * Takes no part in `equals`/`hash`: it is derived from `compute.module`, which both already cover,
441
+ * and a cache key computed two ways is a cache that can disagree with itself.
442
+ *
443
+ * @returns {readonly number[]}
444
+ */
445
+ get workgroup_size()
446
+ ```
447
+
448
+ Backed by `compute_infer_workgroup_size_from_source`. **Source it cannot read throws, at
449
+ construction.** Revision 2 proposed a `workgroup_size_error` companion so the descriptor could
450
+ report rather than throw, on the grounds that a capture must not take a frame down. That was
451
+ rejected, and rightly:
452
+
453
+ > The user can't construct a ComputeShader because their source is messed up — that's preferred to
454
+ > them constructing an invalid shader that we then have to recognize and deal with.
455
+
456
+ Every consumer of this number divides a thread count by it. A descriptor that cannot state its own
457
+ workgroup size is one nobody downstream can use correctly, so `[0, 0, 0]` would only defer the
458
+ failure to somewhere with less context to explain it. The only `[0, 0, 0]` left is a bare
459
+ `new ComputePipelineDescriptor()` that has no code yet — not a fallback, a state `new` alone
460
+ produces.
461
+
462
+ `ComputeShader` reads the descriptor instead of parsing separately. Its message keeps the shader's
463
+ label because the pipeline is given the same one, so nothing is wrapped and nothing is lost.
464
+
465
+ This is strictly better than what either side did before:
466
+
467
+ - **One parse**, at pipeline-descriptor construction, by the parser that actually handles WGSL.
468
+ - The nine `ComputePipelineDescriptor` sites that do **not** go through `ComputeShader` — VSM
469
+ residence, tile allocation, coarse bounds, probe dering, indirect command build ×2, irradiance
470
+ lookup, debug reprojection, virtual shadow residence — get correct workgroup sizes in profiles
471
+ for the first time. `parse_workgroup_size` returned `[0,0,0]` for any of them stating the size
472
+ through a `const`.
473
+ - `parse_workgroup_size.js` is deleted, resolving the `@deprecated` note already on it.
474
+ - `timing/profile/` loses its last unconditionally-linked module, so the whole profiler directory
475
+ is now unreachable from any engine entry point — which is what `GPUProfileSession`'s header
476
+ claims and, before this, was not quite true.
477
+
478
+ **`fromJSON` re-reads it.** `ComputePipelineDescriptor.fromJSON` replaces `compute` in place, so a
479
+ size read for whatever was there before would be the wrong shader's — and wrong here means a wrong
480
+ *divisor*. Covered by a spec.
481
+
482
+ Verified before landing that eager reading breaks nothing: every compute pipeline reachable in the
483
+ tree parses. Two modules would not, and both are unreferenced — `GPUVirtualShadowTextureResidence`
484
+ (which already throws on import today, for an unrelated missing default export) and
485
+ `graph_pass_debug_reprojection`.
486
+
487
+ ### 3.6 A check the device already does, dropped
488
+
489
+ `#with_timestamp_writes` guards on `this.#graphics.device.features.has('timestamp-query')`. This is
490
+ redundant: `GPUTimerArray`'s constructor returns early without the feature, leaving `#query_set`
491
+ undefined, and `#get_writes` already returns `undefined` in that case
492
+ ([GPUTimerArray.js:97](timing/GPUTimerArray.js:97)). One fewer place that has to know the feature
493
+ exists.
494
+
495
+ ---
496
+
497
+ ## 4. Removing the debug-timer path
498
+
499
+ | File | Change |
500
+ |---|---|
501
+ | `ShadeGPUCommandContext.js` | Delete `enable_debug_timers` (:93), `#debug_timers_callback` (:87), and the `results_to_console_table` branch in `finish()` (:975). The `noop` import becomes unused — its only two uses are `#debug_timers_callback` and `#profiling_absorbed_resolve`, and both leave. |
502
+ | `renderer/Renderer.js` | Delete `onFrameDebug` (:431), `#debug_frame_count` (:606), `add_debug_frame` (:671), and the arming block (:1736–1744). **Zero subscribers repo-wide.** |
503
+ | `renderer/gpu_primitive/bvh/verify_gpu_bvh_build.js` | Move onto the profiler (below). |
504
+ | `RENDERER_CONTRACT.md:544` | Drop `add_debug_frame(n)` from the surface table. |
505
+ | `timing/GPU_PROFILER_PROPOSAL_2026_08_28.md` | §6.1 last ¶, §6.2 hook 3, and table rows 57/59/60 describe things that no longer exist. |
506
+ | `gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md:235`, `PHASE1_HANDOVER.md:148` | Same. |
507
+
508
+ The replacement for the one real consumer is *better* than what it replaces — it gains dispatch and
509
+ invocation counts, which is what a BVH build verification actually wants:
510
+
511
+ ```js
512
+ let recorder = null;
513
+
514
+ if (test_case.count >= 100000) {
515
+ recorder = new GPUFrameRecorder(0, performance.now());
516
+ recorder.enable_work_capture();
517
+
518
+ cmd.enable_profiling(recorder, 0);
519
+ }
520
+
521
+ // ... build the graph ...
522
+
523
+ if (recorder !== null) {
524
+ recorder.attach_graph(graph);
525
+ }
526
+
527
+ cmd.encodeGraph(graph);
528
+ cmd.finish();
529
+
530
+ if (recorder !== null) {
531
+ const captured = recorder;
532
+
533
+ cmd.profiling_absorbed.then(() => {
534
+ const frame = captured.close(performance.now());
535
+
536
+ console.log(`[BVH] GPU timings — ${test_case.name}`);
537
+ console.table(frame.spans.map(span => ({
538
+ label: span.label,
539
+ duration_ms: span.duration_ns * 1e-6,
540
+ dispatches: span.work?.dispatch_count ?? 0,
541
+ invocations: span.work?.invocation_count ?? 0
542
+ })));
543
+ });
544
+ }
545
+ ```
546
+
547
+ "Profile the next N frames" — what `add_debug_frame` was for — is already
548
+ `new GPUProfileSession({ frame_limit: N })`.
549
+
550
+ ---
551
+
552
+ ## 5. One migration hazard, worth stating plainly
553
+
554
+ **Do not null `#profiler` inside `finish()`.** The renderer reads `profiling_absorbed` *after*
555
+ `finish()` ([Renderer.js:2520, then :2531](../renderer/Renderer.js:2531)). If `profiling_absorbed`
556
+ becomes a getter that returns `Promise.resolve()` when `#profiler === null`, nulling the field in
557
+ `finish()` hands the renderer an already-resolved promise and it commits an empty frame — every
558
+ frame, silently, with no error anywhere. The current code survives this only because
559
+ `#profiling_absorbed` is a separate cached field.
560
+
561
+ The profiler holds no GPU resources after `absorb()` — it destroys the timer array itself — so
562
+ leaving the reference in place is correct and costs nothing.
563
+
564
+ ---
565
+
566
+ ## 6. Testing
567
+
568
+ | Tier | What |
569
+ |---|---|
570
+ | **Unit, node** | Port `sgpt_workload.spec.js` off `make_profiling_pass_encoder` onto the two classes: counting *and* forwarding, which it already tests. Its "a method the proxy has never heard of still works" test is deleted — that behaviour is gone deliberately. |
571
+ | **Unit, node** | `ComputePipelineDescriptor.workgroup_size`: the size for a literal, for a `const`, `[0,0,0]` plus a populated `workgroup_size_error` for an override expression, the memo invalidated by `fromJSON`/`copy`, and the returned array frozen. |
572
+ | **Unit, node** | `ComputeShader` still throws the same message for an unreadable size — a spec that pins the wrapping, since §3.5 rewrites how it gets there. |
573
+ | **Unit, node** | Drift guard: every method name on `SoftwareGPUComputePassEncoder.prototype` / `SoftwareGPURenderPassEncoder.prototype` has a counterpart on the decorator, modulo the §3.2 rename. This catches the decorator falling behind **the mock**, which is what breaks node-tier specs. It is *not* a completeness check against WebGPU — see §8.2. |
574
+ | **`SoftwareGPUDevice`** | `ShadeGPUCommandContextFrame.spec.js` already drives `setViewport`/`draw`/`dispatchWorkgroups`/`end` through the context and asserts on the underlying mock encoder — it passes unchanged if the forwarding is right, which makes it the integration check for free. `GPUFrameRecorder.spec.js` and `GPUProfileSession.spec.js` cover the join end to end. |
575
+ | **Browser playground** | The **only** real completeness check: diff each decorator against the live `GPUComputePassEncoder.prototype` / `GPURenderPassEncoder.prototype` and **throw**, not warn. A console warning in a playground page is a warning nobody reads. |
576
+ | **Measurement** | `shade/playground/volumetrics_froxel/measure.js`, before and after, profiling **off**. |
577
+
578
+ ---
579
+
580
+ ## 7. Cost, honestly
581
+
582
+ **Not profiling** — per pass: one object allocation, one `assert.defined`, one `assert.notOk`; per
583
+ encoder method: one call frame. Asserts in this codebase are **not** stripped in production
584
+ ([core/assert.js](../../core/assert.js) has no build-time gate), so those two are real work on every
585
+ pass, not a debug-build cost.
586
+
587
+ How many passes per frame is **not known** — the 2026-08-28 proposal lists it as an open question
588
+ (§734 there) and `GPUTimerArray`'s 1024-slot default is a ceiling, not a measurement. So the total is
589
+ unquantified by construction and §6's measurement is the answer, not this paragraph.
590
+
591
+ **Profiling** — strictly faster. A `Proxy` `get` trap plus a `Map` lookup on every property access
592
+ becomes a direct method call. Plus one WGSL parse removed per profiled compute pass (§3.5).
593
+
594
+ **Correctness** — the two hidden fields go, and with them any defect that could only manifest while
595
+ profiling.
596
+
597
+ ---
598
+
599
+ ## 8. Adversarial pass
600
+
601
+ Attacking the above. Four findings changed the design; the rest are recorded because they are the
602
+ things most likely to be wrong.
603
+
604
+ ### 8.1 `label` was write-only-broken *(fixed above)*
605
+
606
+ Revision 1 gave the decorators a `label` getter and no setter. `GPUObjectBase.label` is a read-write
607
+ attribute, ES modules are always strict mode, and assigning to an accessor with no setter throws
608
+ `TypeError`. Nothing writes `pass.label` today (checked), so this would have sat dormant until
609
+ someone tried — and the §6 drift guard compares *method* names and would never have caught a missing
610
+ accessor. Fixed: `get`/`set` pair. The general lesson is that the drift guard's scope is narrower
611
+ than it looks, which is §8.2.
612
+
613
+ ### 8.2 The node conformance test was oversold *(claim corrected above)*
614
+
615
+ Revision 1 called the node-tier test "the completeness guarantee … which the Proxy could not do".
616
+ That is wrong twice over. It compares meep's decorator against meep's own hand-written mock, so it
617
+ cannot possibly catch a method Chrome adds — both sides would be missing it. And `toHaveProperty`
618
+ walks the prototype chain, so `Object.prototype` members pass vacuously. It is a **drift guard**
619
+ between two files in this repo, which is worth having and is not the same claim. Only the browser
620
+ check tests completeness, and it has to *throw*. §6 now says so.
621
+
622
+ This weakens — but does not overturn — the §2 argument. The Proxy's real advantage was forwarding
623
+ methods **nobody had heard of yet**; that advantage is real and this proposal gives it up on purpose,
624
+ in exchange for a failure that is loud and immediate instead of silent and capture-only. Worth being
625
+ explicit that it is a trade, not a free win.
626
+
627
+ ### 8.3 Revision 1 reintroduced the bug it was removing *(design changed above)*
628
+
629
+ Revision 1 replaced `#pending_work` with a reused `ShadeGPUPassProfile` scratch object returned from
630
+ `open_compute_pass`, "valid until the next call". That is the same coupling — a mutable value whose
631
+ correctness depends on the caller consuming it before anything else runs — moved one class down and
632
+ given a name. Having just written two pages about why that pattern is bad, shipping it would have
633
+ been indefensible. §3.4 now has the profiler open the pass, so both values are produced and consumed
634
+ inside one method and there is nothing to carry.
635
+
636
+ ### 8.4 The commit sequence was not actually independently green *(fixed above, see §9)*
637
+
638
+ Revision 1 ordered "decorators first, profiler-state consolidation second". But the decorators need
639
+ the work record to come from *somewhere*, so step 1 would have had to build `begin*Pass` against the
640
+ old `#pending_work` channel and then immediately rewrite it. Reordered.
641
+
642
+ ### 8.5 Sharing the workgroup-size array is a real aliasing hazard *(mitigated above)*
643
+
644
+ `work.workgroup_size = descriptor.workgroup_size` hands a per-pass profile record a reference to an
645
+ array owned by a descriptor that outlives the whole capture. Nothing mutates it today —
646
+ `GPUProfileWork` assigns it and never writes through it, unlike `workgroups`, which is mutated in
647
+ place three lines away in the same class. That is close enough to be worth removing as a possibility
648
+ rather than as a bug: §3.5 freezes it.
649
+
650
+ ### 8.6 The memo has two invalidation paths and I nearly missed both *(covered above)*
651
+
652
+ `ComputePipelineDescriptor.fromJSON` mutates `this.compute` in place and `PipelineDescriptorBase` has
653
+ a `copy`. A memoised `workgroup_size` read before either would be silently stale afterwards — and
654
+ stale in the worst way, since the number is used as a *divisor* for thread counts. §3.5 and §6 both
655
+ call it out. This is the single most likely place for this change to introduce a real bug.
656
+
657
+ ### 8.7 Claims I softened rather than defended
658
+
659
+ - **"Perfectly predicted branch."** The `#work !== null` check is uniform within a frame but flips
660
+ when a capture starts or stops. Predictable, not free, and not worth a sentence of advocacy.
661
+ - **"~200 passes/frame, ~1200 calls/frame."** Invented. The number is unknown and is listed as an
662
+ open question in the profiler proposal itself. §7 now says so instead of quoting a fabricated
663
+ figure at two decimal places of confidence.
664
+ - **"Both classes ~120 lines."** Now ~130 with the `label` setter and the split `setBindGroup`.
665
+ Immaterial, but the earlier number was a guess presented as a measurement.
666
+
667
+ ### 8.8 Residual risks, accepted
668
+
669
+ - **A WebGPU method Chrome ships that Shade has not modelled** is now a hard failure rather than a
670
+ silent forward. That is the intended trade (§8.2), but it means the browser check is load-bearing
671
+ and needs to actually run in CI or a pre-release checklist, not just exist.
672
+ - **`executeBundles` work is uncounted** (§3.3). Pre-existing, documented, nothing uses bundles.
673
+ - **99 call sites change type.** No reflection over pass objects exists anywhere in the tree
674
+ (`typeof pass.x`, `Object.keys(pass)`, spread, `instanceof` — all checked, all absent), so the
675
+ surface is method calls only. If one is missed it throws at that call; it cannot silently misbehave.
676
+
677
+ ### 8.9 It shipped broken anyway — the arity bug *(found in a browser, after landing)*
678
+
679
+ `setBindGroup(0, bind_group)` forwarded `setBindGroup(0, bind_group, undefined)`, and the renderer
680
+ died on its first bind group with "the provided value cannot be converted to a sequence".
681
+
682
+ The failure is worth recording in full, because every safeguard in this document was pointed at it
683
+ and it got through anyway.
684
+
685
+ **§3.2 named the right method and guarded the wrong end of it.** It identified `setBindGroup` as the
686
+ one forward that could be actively wrong, correctly diagnosed *why* — an overload resolved by
687
+ counting arguments — and then wrote a three-argument forwarder whose whole purpose was to avoid the
688
+ five-argument one. The hazard was never the arity it might reach; it was the `undefined` it would
689
+ hand over on the way. The proposal even asserted the opposite in as many words: "Every other method
690
+ has all-optional trailing parameters with WebIDL defaults, for which forwarding `undefined` is
691
+ correct." That sentence was reasoning about a specification, presented as a fact about a browser,
692
+ and it was load-bearing for seven methods.
693
+
694
+ **Six more had the same defect waiting.** `setImmediates`, `setIndexBuffer`, `setVertexBuffer` and
695
+ the render-side `setBindGroup`. `setVertexBuffer(0, buffer)` — from `prototypeParticleSystem` — was
696
+ next in line, and worse: `size` there has no default to stand in for, it means "the rest of the
697
+ buffer", so an absent argument and an `undefined` one are genuinely different requests.
698
+
699
+ **The node tier could not have caught it.** The mock encoders are plain JS functions and take a
700
+ trailing `undefined` happily, so the mock and the browser disagree in exactly the place the design
701
+ depended on. §6's drift guard compared method *names*, which was never going to help — §8.2 already
702
+ observed the node tier was weaker than it looked, and still put a name-comparison in the plan.
703
+
704
+ **What replaced it:** specs that assert arity preservation against a recording fake, which is
705
+ mock-independent and would have failed on the first run; and the fix verified against real WebGPU
706
+ in a browser rather than against the mock.
707
+
708
+ **Not fully explained.** The in-app browser (Chrome 148) *accepts* the explicit `undefined`; the
709
+ build that crashed does not. So the fix is verified correct on both, but the exact behavioural
710
+ difference was not reproduced, and the code comment says "some builds accept it, others do not"
711
+ rather than claiming a mechanism it cannot demonstrate.
712
+
713
+ **The standing lesson.** The browser-tier check §9 leaves open is not a nice-to-have. Two of the
714
+ three tiers in §6 are blind to the mock/browser boundary by construction, and this change has now
715
+ been wrong at that boundary twice.
716
+
717
+ ---
718
+
719
+ ## 9. Decisions taken
720
+
721
+ | # | Question | Decision |
722
+ |---|---|---|
723
+ | 1 | Location | **`device/pass/`**, with `ShadeGPUContextProfiler` in `device/timing/` beside the timer array it owns. |
724
+ | 2 | `GPUTimerArray.results_to_console_table` | **Deleted** — "a crude tool, no longer required". Its two specs moved onto `traverse_results`. |
725
+ | 3 | `setBindGroupRange` | **Kept.** The mock models the slice form, and naming it is the whole point of §3.2. |
726
+ | 4 | A `gpu_pass` escape hatch | **Omitted.** Nothing needs one and it invites bypass. |
727
+ | 5 | `end()` double-end assert | **In.** Tracking open passes on the context stays a separate change. |
728
+ | 6 | `workgroup_size_error` | **Rejected — throw instead.** See §3.5. |
729
+
730
+ Still open, and deliberately not done here:
731
+
732
+ - **The browser-tier completeness check** against the live `GPUComputePassEncoder.prototype` /
733
+ `GPURenderPassEncoder.prototype`. It is the only real completeness check (§8.2) and it is now
734
+ load-bearing, since a method Chrome ships that Shade has not modelled throws rather than being
735
+ forwarded blind.
736
+ - **The measurement.** `shade/playground/volumetrics_froxel/measure.js`, profiling off, before and
737
+ after. §7 says why the numbers are not guessed at here.
738
+ - **Two unreferenced modules** found on the way: `GPUVirtualShadowTextureResidence.js` imports a
739
+ default export that does not exist and throws on import; `graph_pass_debug_reprojection.js` is
740
+ dead. Both predate this change and neither was touched.
741
+
742
+ ---
743
+
744
+ ## 10. Commits
745
+
746
+ 1. `refactor(shade): pipeline descriptors carry their workgroup size` — §3.5.
747
+ 2. `refactor(shade): pass encoders become explicit decorator classes` — §§3.1–3.4 and 3.6, plus the
748
+ BVH verification tool moved onto a `GPUFrameRecorder`.
749
+ 3. `refactor(shade): remove the debug-timer path, the profiler is the only one` — §4.
750
+ 4. `fix(shade): pass encoders forward the arity they received` — §3.2, after the renderer failed on
751
+ its first bind group in a browser. §8.9.
752
+
753
+ Revision 2 ordered these decorators-first; that was wrong (§8.4) and the workgroup size went first
754
+ instead. Full suite green after each: 15,014 tests — which, per §8.9, was never going to be enough
755
+ on its own.