@woosh/meep-engine 3.12.0 → 3.13.0

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 (285) hide show
  1. package/package.json +1 -1
  2. package/src/engine/graphics3/decal/GPUDecalRenderer.js +3 -3
  3. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  4. package/src/engine/graphics3/decal/shader_decal_composite.js +4 -2
  5. package/src/engine/graphics3/highlight/GPUHighlightRenderer.d.ts.map +1 -1
  6. package/src/engine/graphics3/highlight/GPUHighlightRenderer.js +2 -3
  7. package/src/engine/graphics3/highlight/shader_highlight_outline.js +150 -150
  8. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  9. package/src/shade/RENDERER_CONTRACT.md +2 -1
  10. package/src/shade/descriptor/pipeline/compute/ComputePipelineDescriptor.d.ts +18 -0
  11. package/src/shade/descriptor/pipeline/compute/ComputePipelineDescriptor.d.ts.map +1 -1
  12. package/src/shade/descriptor/pipeline/compute/ComputePipelineDescriptor.js +73 -0
  13. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +750 -0
  14. package/src/shade/device/ShadeGPUCommandContext.d.ts +10 -16
  15. package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
  16. package/src/shade/device/ShadeGPUCommandContext.js +801 -994
  17. package/src/shade/device/pass/ShadeGPUComputePassEncoder.d.ts +107 -0
  18. package/src/shade/device/pass/ShadeGPUComputePassEncoder.d.ts.map +1 -0
  19. package/src/shade/device/pass/ShadeGPUComputePassEncoder.js +222 -0
  20. package/src/shade/device/pass/ShadeGPURenderPassEncoder.d.ts +168 -0
  21. package/src/shade/device/pass/ShadeGPURenderPassEncoder.d.ts.map +1 -0
  22. package/src/shade/device/pass/ShadeGPURenderPassEncoder.js +317 -0
  23. package/src/shade/device/timing/GPUTimerArray.d.ts +4 -9
  24. package/src/shade/device/timing/GPUTimerArray.d.ts.map +1 -1
  25. package/src/shade/device/timing/GPUTimerArray.js +261 -289
  26. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +20 -2
  27. package/src/shade/device/timing/ShadeGPUContextProfiler.d.ts +59 -0
  28. package/src/shade/device/timing/ShadeGPUContextProfiler.d.ts.map +1 -0
  29. package/src/shade/device/timing/ShadeGPUContextProfiler.js +182 -0
  30. package/src/shade/device/timing/profile/GPUProfileSession.d.ts +4 -4
  31. package/src/shade/device/timing/profile/GPUProfileSession.js +4 -4
  32. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_leaf_triangles.d.ts.map +1 -1
  33. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_leaf_triangles.js +3 -1
  34. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_points.d.ts.map +1 -1
  35. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_points.js +3 -1
  36. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_visibility.d.ts.map +1 -1
  37. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_visibility.js +3 -1
  38. package/src/shade/playground/skinned_blas_refit/verify_leaf_triangles.js +2 -2
  39. package/src/shade/playground/skinned_blas_refit/verify_traversal_reachability.js +2 -2
  40. package/src/shade/renderer/Renderer.d.ts +0 -6
  41. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  42. package/src/shade/renderer/Renderer.js +0 -29
  43. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  44. package/src/shade/renderer/animation/GPUAnimationManager.js +1 -3
  45. package/src/shade/renderer/animation/shader_animation_tick.d.ts.map +1 -1
  46. package/src/shade/renderer/animation/shader_animation_tick.js +128 -125
  47. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.d.ts.map +1 -1
  48. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.js +8 -18
  49. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_geometry_sphere_derive.d.ts.map +1 -1
  50. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_geometry_sphere_derive.js +100 -98
  51. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_reduce.d.ts.map +1 -1
  52. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_reduce.js +215 -212
  53. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_scatter.d.ts +1 -1
  54. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_scatter.d.ts.map +1 -1
  55. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_meshlet_bounds_scatter.js +135 -120
  56. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_skinned_mesh_bounds_refresh.d.ts.map +1 -1
  57. package/src/shade/renderer/animation/skinning/bounds_refresh/shader_skinned_mesh_bounds_refresh.js +77 -75
  58. package/src/shade/renderer/animation/skinning/shader_meshlet_apply_skinning.d.ts.map +1 -1
  59. package/src/shade/renderer/animation/skinning/shader_meshlet_apply_skinning.js +375 -371
  60. package/src/shade/renderer/atmosphere/shader_transmittance_lut.js +3 -3
  61. package/src/shade/renderer/buffer/graph_inspect_gpu_buffer.js +1 -1
  62. package/src/shade/renderer/buffer/table/GPUDatabase.d.ts.map +1 -1
  63. package/src/shade/renderer/buffer/table/GPUDatabase.js +14 -19
  64. package/src/shade/renderer/buffer/table/gpu_table_element_upload_shader.d.ts.map +1 -1
  65. package/src/shade/renderer/buffer/table/gpu_table_element_upload_shader.js +7 -2
  66. package/src/shade/renderer/buffer/table/single/GPUSingleTypeTable.d.ts.map +1 -1
  67. package/src/shade/renderer/buffer/table/single/GPUSingleTypeTable.js +325 -319
  68. package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.d.ts.map +1 -1
  69. package/src/shade/renderer/buffer/util/shader_buffer_copy_storage_to_storage.js +3 -1
  70. package/src/shade/renderer/deferred/shader_tonemap_HDR.d.ts.map +1 -1
  71. package/src/shade/renderer/deferred/shader_tonemap_HDR.js +4 -1
  72. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.d.ts +2 -2
  73. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.d.ts.map +1 -1
  74. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.js +167 -167
  75. package/src/shade/renderer/fow/GPUFogOfWarRenderer.js +3 -3
  76. package/src/shade/renderer/fow/shader_fog_of_war.d.ts.map +1 -1
  77. package/src/shade/renderer/fow/shader_fog_of_war.js +3 -1
  78. package/src/shade/renderer/geometry/bvh/record_blas_refit.js +102 -102
  79. package/src/shade/renderer/geometry/bvh/shader_blas_refit_leaf_bounds.d.ts.map +1 -1
  80. package/src/shade/renderer/geometry/bvh/shader_blas_refit_leaf_bounds.js +125 -121
  81. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts.map +1 -1
  82. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +766 -774
  83. package/src/shade/renderer/geometry/meshlet/shader_meshlet_metadata_address_patch.d.ts.map +1 -1
  84. package/src/shade/renderer/geometry/meshlet/shader_meshlet_metadata_address_patch.js +72 -69
  85. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts.map +1 -1
  86. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +5 -10
  87. package/src/shade/renderer/geometry/sdf/shader_build_sdf.js +129 -129
  88. package/src/shade/renderer/geometry/sdf/shader_sdf_visualize.d.ts.map +1 -1
  89. package/src/shade/renderer/geometry/sdf/shader_sdf_visualize.js +5 -3
  90. package/src/shade/renderer/global_illumination/brick4/gpu/bake/shader_brick4_bake_probes.d.ts.map +1 -1
  91. package/src/shade/renderer/global_illumination/brick4/gpu/bake/shader_brick4_bake_probes.js +4 -1
  92. package/src/shade/renderer/global_illumination/brick4/gpu/bake/shader_brick4_bake_resolve_probes.d.ts.map +1 -1
  93. package/src/shade/renderer/global_illumination/brick4/gpu/bake/shader_brick4_bake_resolve_probes.js +3 -1
  94. package/src/shade/renderer/global_illumination/clipmap/shader_update_clipmap_cascade.d.ts.map +1 -1
  95. package/src/shade/renderer/global_illumination/clipmap/shader_update_clipmap_cascade.js +114 -112
  96. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lpv_store_sh.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lpv_store_sh.js +4 -1
  98. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lvp_atlas_to_sh.d.ts.map +1 -1
  99. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_lvp_atlas_to_sh.js +187 -184
  100. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_reduce_atlas_to_sh.d.ts.map +1 -1
  101. package/src/shade/renderer/global_illumination/probe/octahedral/convolve/graph_reduce_atlas_to_sh.js +4 -1
  102. package/src/shade/renderer/global_illumination/sharc/SpatialHashRadianceCache.d.ts.map +1 -1
  103. package/src/shade/renderer/global_illumination/sharc/SpatialHashRadianceCache.js +6 -8
  104. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.d.ts.map +1 -1
  105. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.js +3 -1
  106. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_update.d.ts.map +1 -1
  107. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_update.js +3 -1
  108. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +3 -3
  109. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +3 -3
  110. package/src/shade/renderer/gpu_primitive/bvh/graph_bvh_build_lbvh.js +217 -217
  111. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_emit_hierarchy.d.ts.map +1 -1
  112. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_emit_hierarchy.js +145 -142
  113. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_morton.d.ts.map +1 -1
  114. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_morton.js +87 -82
  115. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_refit.d.ts.map +1 -1
  116. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_refit.js +143 -140
  117. package/src/shade/renderer/gpu_primitive/bvh/verify_gpu_bvh_build.d.ts +2 -2
  118. package/src/shade/renderer/gpu_primitive/bvh/verify_gpu_bvh_build.d.ts.map +1 -1
  119. package/src/shade/renderer/gpu_primitive/bvh/verify_gpu_bvh_build.js +43 -8
  120. package/src/shade/renderer/gpu_primitive/sort/graph_radix_sort.d.ts.map +1 -1
  121. package/src/shade/renderer/gpu_primitive/sort/graph_radix_sort.js +4 -5
  122. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_histogram.d.ts.map +1 -1
  123. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_histogram.js +88 -84
  124. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_scatter.d.ts.map +1 -1
  125. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_scatter.js +194 -191
  126. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  127. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +33 -69
  128. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  129. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +6 -1
  130. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +1 -1
  131. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.js +23 -7
  132. package/src/shade/renderer/light/environment/execute_filter_environment_map.d.ts.map +1 -1
  133. package/src/shade/renderer/light/environment/execute_filter_environment_map.js +1 -2
  134. package/src/shade/renderer/light/environment/shader_convolve_reflection_map.d.ts.map +1 -1
  135. package/src/shade/renderer/light/environment/shader_convolve_reflection_map.js +6 -1
  136. package/src/shade/renderer/lightmap/bake/raster/graph_draw_lightmap_viz_buffer.d.ts.map +1 -1
  137. package/src/shade/renderer/lightmap/bake/raster/graph_draw_lightmap_viz_buffer.js +234 -219
  138. package/src/shade/renderer/lightmap/bake/raster/shader_lightmap_to_viz_buffer.js +2 -2
  139. package/src/shade/renderer/lightmap/bake/shader_lightmap_denoise_sh.js +191 -191
  140. package/src/shade/renderer/lightmap/bake/tile/shader_lightmap_accummulate_tile.d.ts.map +1 -1
  141. package/src/shade/renderer/lightmap/bake/tile/shader_lightmap_accummulate_tile.js +85 -82
  142. package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
  143. package/src/shade/renderer/particles/graph_particles.js +5 -3
  144. package/src/shade/renderer/particles/prototypeParticleSystem.js +3 -4
  145. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
  146. package/src/shade/renderer/particles/shaders/shader_particle_emit.js +132 -117
  147. package/src/shade/renderer/particles/shaders/shader_particle_finalize.d.ts.map +1 -1
  148. package/src/shade/renderer/particles/shaders/shader_particle_finalize.js +79 -77
  149. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  150. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +111 -99
  151. package/src/shade/renderer/particles/sort/shader_particle_sort.d.ts.map +1 -1
  152. package/src/shade/renderer/particles/sort/shader_particle_sort.js +187 -158
  153. package/src/shade/renderer/path_tracer/accumulating/AccumulatingPathTracer.d.ts.map +1 -1
  154. package/src/shade/renderer/path_tracer/accumulating/AccumulatingPathTracer.js +7 -10
  155. package/src/shade/renderer/path_tracer/accumulating/shader_accumulating_path_tracer.d.ts.map +1 -1
  156. package/src/shade/renderer/path_tracer/accumulating/shader_accumulating_path_tracer.js +4 -1
  157. package/src/shade/renderer/postprocess/bloom/shader_bloom_mix.d.ts.map +1 -1
  158. package/src/shade/renderer/postprocess/bloom/shader_bloom_mix.js +3 -1
  159. package/src/shade/renderer/postprocess/cas/shader_ffx_rcas.d.ts.map +1 -1
  160. package/src/shade/renderer/postprocess/cas/shader_ffx_rcas.js +4 -2
  161. package/src/shade/renderer/postprocess/denoise/fragment_shader_denoise_ao.js +147 -147
  162. package/src/shade/renderer/postprocess/denoise/graph_atorus_denoise_luma.d.ts.map +1 -1
  163. package/src/shade/renderer/postprocess/denoise/graph_atorus_denoise_luma.js +121 -119
  164. package/src/shade/renderer/postprocess/dof/gather/graph_postprocess_dof2.d.ts.map +1 -1
  165. package/src/shade/renderer/postprocess/dof/gather/graph_postprocess_dof2.js +1 -2
  166. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_gather_far.d.ts.map +1 -1
  167. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_gather_far.js +4 -1
  168. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_gather_near.d.ts.map +1 -1
  169. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_gather_near.js +4 -1
  170. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_recombine.d.ts.map +1 -1
  171. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_recombine.js +4 -1
  172. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_setup.d.ts.map +1 -1
  173. package/src/shade/renderer/postprocess/dof/gather/shader_dof2_setup.js +4 -1
  174. package/src/shade/renderer/postprocess/dof/raymarch/graph_postprocess_dof.d.ts.map +1 -1
  175. package/src/shade/renderer/postprocess/dof/raymarch/graph_postprocess_dof.js +2 -4
  176. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_composite.d.ts.map +1 -1
  177. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_composite.js +4 -1
  178. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_prefilter.d.ts.map +1 -1
  179. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_prefilter.js +4 -1
  180. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_raymarch.d.ts.map +1 -1
  181. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_raymarch.js +4 -1
  182. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_raymarch_compute.d.ts.map +1 -1
  183. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_raymarch_compute.js +269 -266
  184. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_slice_compute.d.ts.map +1 -1
  185. package/src/shade/renderer/postprocess/dof/raymarch/shader_dof_slice_compute.js +273 -270
  186. package/src/shade/renderer/postprocess/dof/shader_dof_autofocus.d.ts.map +1 -1
  187. package/src/shade/renderer/postprocess/dof/shader_dof_autofocus.js +4 -1
  188. package/src/shade/renderer/postprocess/eye/GPUCameraExposureManager.d.ts.map +1 -1
  189. package/src/shade/renderer/postprocess/eye/GPUCameraExposureManager.js +8 -11
  190. package/src/shade/renderer/postprocess/eye/shader_autoexposure_adapt.d.ts.map +1 -1
  191. package/src/shade/renderer/postprocess/eye/shader_autoexposure_adapt.js +5 -1
  192. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts.map +1 -1
  193. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js +11 -8
  194. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts.map +1 -1
  195. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +7 -8
  196. package/src/shade/renderer/postprocess/motion_blur/shader_motion_blur_reconstruct.d.ts.map +1 -1
  197. package/src/shade/renderer/postprocess/motion_blur/shader_motion_blur_reconstruct.js +3 -1
  198. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  199. package/src/shade/renderer/postprocess/nss/shader_nss_concat.d.ts.map +1 -1
  200. package/src/shade/renderer/postprocess/nss/shader_nss_concat.js +70 -68
  201. package/src/shade/renderer/postprocess/ssr/depth_max_shader.d.ts.map +1 -1
  202. package/src/shade/renderer/postprocess/ssr/depth_max_shader.js +70 -68
  203. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader_IBL.d.ts.map +1 -1
  204. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader_IBL.js +3 -1
  205. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader_LPV.d.ts.map +1 -1
  206. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader_LPV.js +4 -1
  207. package/src/shade/renderer/postprocess/ssr/shader_downsample_encoded_normal.d.ts.map +1 -1
  208. package/src/shade/renderer/postprocess/ssr/shader_downsample_encoded_normal.js +103 -101
  209. package/src/shade/renderer/postprocess/ssr/ssr_spatial_denoise_shader.d.ts.map +1 -1
  210. package/src/shade/renderer/postprocess/ssr/ssr_spatial_denoise_shader.js +4 -2
  211. package/src/shade/renderer/postprocess/ssr/ssr_trace_shader.d.ts.map +1 -1
  212. package/src/shade/renderer/postprocess/ssr/ssr_trace_shader.js +3 -1
  213. package/src/shade/renderer/postprocess/taa/shader_taa.d.ts.map +1 -1
  214. package/src/shade/renderer/postprocess/taa/shader_taa.js +4 -1
  215. package/src/shade/renderer/postprocess/upscale-dither/fragment_shader_upscale_dither.js +5 -5
  216. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  217. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +339 -341
  218. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.d.ts.map +1 -1
  219. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.js +114 -109
  220. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.d.ts.map +1 -1
  221. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.js +118 -115
  222. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.d.ts.map +1 -1
  223. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.js +69 -65
  224. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.js +722 -722
  225. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  226. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +1 -9
  227. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  228. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +1 -6
  229. package/src/shade/renderer/restir/di/graph_restir_di.d.ts.map +1 -1
  230. package/src/shade/renderer/restir/di/graph_restir_di.js +2 -3
  231. package/src/shade/renderer/restir/di/graph_restir_di_denoise.d.ts.map +1 -1
  232. package/src/shade/renderer/restir/di/graph_restir_di_denoise.js +1 -2
  233. package/src/shade/renderer/restir/di/shader_restir_di_denoise.d.ts.map +1 -1
  234. package/src/shade/renderer/restir/di/shader_restir_di_denoise.js +139 -136
  235. package/src/shade/renderer/restir/di/shader_restir_di_resample.d.ts.map +1 -1
  236. package/src/shade/renderer/restir/di/shader_restir_di_resample.js +131 -129
  237. package/src/shade/renderer/restir/di/shader_restir_di_resolve.d.ts.map +1 -1
  238. package/src/shade/renderer/restir/di/shader_restir_di_resolve.js +101 -99
  239. package/src/shade/renderer/scene/GPUInstancesAccelerationStructure.d.ts.map +1 -1
  240. package/src/shade/renderer/scene/GPUInstancesAccelerationStructure.js +379 -383
  241. package/src/shade/renderer/scene/shader_bvh_extract_instance_leaves.d.ts.map +1 -1
  242. package/src/shade/renderer/scene/shader_bvh_extract_instance_leaves.js +147 -144
  243. package/src/shade/renderer/shader/ComputeShader.d.ts +2 -2
  244. package/src/shade/renderer/shader/ComputeShader.d.ts.map +1 -1
  245. package/src/shade/renderer/shader/ComputeShader.js +14 -17
  246. package/src/shade/renderer/shader/pass/RenderPassDescriptor.d.ts +2 -2
  247. package/src/shade/renderer/shader/pass/RenderPassDescriptor.d.ts.map +1 -1
  248. package/src/shade/renderer/shader/pass/RenderPassDescriptor.js +189 -189
  249. package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.d.ts.map +1 -1
  250. package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.js +3 -1
  251. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  252. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +14 -18
  253. package/src/shade/renderer/shadow/map/shader/shader_shadowmap_remap_cube_to_octahedral.d.ts.map +1 -1
  254. package/src/shade/renderer/shadow/map/shader/shader_shadowmap_remap_cube_to_octahedral.js +5 -1
  255. package/src/shade/renderer/shadow/ray/shader_shadow_upscale.d.ts.map +1 -1
  256. package/src/shade/renderer/shadow/ray/shader_shadow_upscale.js +3 -1
  257. package/src/shade/renderer/shadow/ray/staged/gen/graph_gen_rays.js +73 -73
  258. package/src/shade/renderer/shadow/ray/staged/gen/shader_generate_rays.d.ts.map +1 -1
  259. package/src/shade/renderer/shadow/ray/staged/gen/shader_generate_rays.js +151 -148
  260. package/src/shade/renderer/shadow/ray/staged/trace/graph_trace_rays.d.ts.map +1 -1
  261. package/src/shade/renderer/shadow/ray/staged/trace/graph_trace_rays.js +4 -2
  262. package/src/shade/renderer/shadow/ray/staged/trace/shader_trace_rays.d.ts.map +1 -1
  263. package/src/shade/renderer/shadow/ray/staged/trace/shader_trace_rays.js +18 -4
  264. package/src/shade/renderer/shadow/sdf/cascade/CascadedSceneSDF.d.ts.map +1 -1
  265. package/src/shade/renderer/shadow/sdf/cascade/CascadedSceneSDF.js +7 -8
  266. package/src/shade/renderer/shadow/sdf/cascade/shader_build_scene_sdf_f32.d.ts.map +1 -1
  267. package/src/shade/renderer/shadow/sdf/cascade/shader_build_scene_sdf_f32.js +6 -2
  268. package/src/shade/renderer/shadow/sdf/cascade/shader_scene_sdf_debug_viz.d.ts.map +1 -1
  269. package/src/shade/renderer/shadow/sdf/cascade/shader_scene_sdf_debug_viz.js +129 -127
  270. package/src/shade/renderer/shadow/sdf/graph_draw_sdf_shadows.d.ts.map +1 -1
  271. package/src/shade/renderer/shadow/sdf/graph_draw_sdf_shadows.js +5 -3
  272. package/src/shade/renderer/texture/virtual/VirtualTextureManager.d.ts.map +1 -1
  273. package/src/shade/renderer/texture/virtual/VirtualTextureManager.js +2 -5
  274. package/src/shade/renderer/texture/virtual/build_shader_vt_feedback.d.ts.map +1 -1
  275. package/src/shade/renderer/texture/virtual/build_shader_vt_feedback.js +233 -231
  276. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  277. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +2 -4
  278. package/src/shade/wgsl/validate_wgsl_source.d.ts.map +1 -1
  279. package/src/shade/wgsl/validate_wgsl_source.js +8 -3
  280. package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts +0 -21
  281. package/src/shade/device/timing/profile/make_profiling_pass_encoder.d.ts.map +0 -1
  282. package/src/shade/device/timing/profile/make_profiling_pass_encoder.js +0 -114
  283. package/src/shade/device/timing/profile/parse_workgroup_size.d.ts +0 -20
  284. package/src/shade/device/timing/profile/parse_workgroup_size.d.ts.map +0 -1
  285. package/src/shade/device/timing/profile/parse_workgroup_size.js +0 -43
@@ -1,775 +1,767 @@
1
- import { assert } from "../../../../core/assert.js";
2
- import { ALLOCATOR_NO_SPACE, OffsetAllocator } from "../../../../core/binary/allocator/OffsetAllocator.js";
3
- import { BinaryBuffer } from "../../../../core/binary/BinaryBuffer.js";
4
- import { platform_compute_endianness } from "../../../../core/binary/platform_compute_endianness.js";
5
- import Signal from "../../../../core/events/signal/Signal.js";
6
- import { MEGABYTE } from "../../../../core/science/units/memory/MEGABYTE.js";
7
- import { read_wgsl_type_value } from "../../shader/type/serde/read_gpu_typed_buffer.js";
8
- import { write_wgsl_type_value } from "../../shader/type/serde/write_gpu_typed_buffer.js";
9
- import { u32 } from "../../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
10
- import { inspect_gpu_buffer_typed } from "../../buffer/inspect_gpu_buffer_typed.js";
11
- import {
12
- BUFFER_INTRA_COPY_REGION_STRUCT,
13
- shader_buffer_intra_copy_regions
14
- } from "../../buffer/util/shader_buffer_intra_copy_regions.js";
15
- import { MESHLET_METADATA_STRUCT } from "./encoding/MESHLET_METADATA_STRUCT.js";
16
- import { shader_meshlet_metadata_address_patch } from "./shader_meshlet_metadata_address_patch.js";
17
-
18
- /**
19
- * In meshlets
20
- * @type {number}
21
- */
22
- const DEFAULT_METADATA_CAPACITY = 128 * 1024;
23
-
24
- /**
25
- * In bytes
26
- * @type {number}
27
- */
28
- const DEFAULT_DATA_CAPACITY = 32 * MEGABYTE;
29
-
30
- class MeshletBatchAllocationRecord {
31
- /**
32
- * @type {Allocation}
33
- */
34
- metadata
35
- /**
36
- * @type {Allocation}
37
- */
38
- data
39
-
40
- /**
41
- * Number of words
42
- * @type {number}
43
- */
44
- data_size = 0;
45
-
46
- /**
47
- * Meshlet count
48
- * @type {number}
49
- */
50
- meshlet_count = 0;
51
-
52
- /**
53
- *
54
- * @type {ArrayBuffer}
55
- */
56
- metadata_buffer_original;
57
-
58
- /**
59
- *
60
- * @type {Signal<>}
61
- */
62
- changed = new Signal()
63
- }
64
-
65
- export class GPUMeshletManager {
66
-
67
- /**
68
- * Allocation sizes in words of 4 bytes
69
- * @type {OffsetAllocator}
70
- */
71
- #allocator_data = new OffsetAllocator(1024);
72
-
73
- /**
74
- * Allocation sizes in meshlets
75
- * @type {OffsetAllocator}
76
- */
77
- #allocator_metadata = new OffsetAllocator(1024);
78
-
79
- /**
80
- * @type {GPUBuffer}
81
- */
82
- #meshlet_data_buffer;
83
-
84
- get buffer_data() {
85
- return this.#meshlet_data_buffer;
86
- }
87
-
88
- /**
89
- * @type {GPUBuffer}
90
- */
91
- #meshlet_metadata_buffer;
92
-
93
- get buffer_metadata() {
94
- return this.#meshlet_metadata_buffer;
95
- }
96
-
97
- /**
98
- * @type {GPUDevice}
99
- */
100
- #device;
101
-
102
- /**
103
- *
104
- * @type {number}
105
- */
106
- #deallocations_since_last_rebuild = 0;
107
-
108
- /**
109
- *
110
- * @type {MeshletBatchAllocationRecord[]}
111
- */
112
- #records = [];
113
-
114
- /**
115
- * Queue of GPU-side clones that have been allocated but not yet
116
- * copied + patched. Drained by {@link update}. Each entry holds the
117
- * minimum bookkeeping needed to issue two `copyBufferToBuffer`
118
- * commands and N `(record_index, address_delta)` patch entries.
119
- *
120
- * Entries name the two records, not the addresses they held when the
121
- * clone was queued: a {@link compact} or {@link grow} in between
122
- * migrates both, and {@link update} reads where they ended up. What
123
- * an entry does require is that neither record is handed back to
124
- * {@link remove_batch} before the flush.
125
- *
126
- * @type {{
127
- * source: MeshletBatchAllocationRecord,
128
- * clone: MeshletBatchAllocationRecord,
129
- * }[]}
130
- */
131
- #pending_clones = [];
132
-
133
- /**
134
- *
135
- * @param {GPUDevice} device
136
- * @param {number} [initial_data_capacity] in bytes
137
- * @param {number} [initial_metadata_capacity] in meshlets
138
- */
139
- constructor({
140
- device,
141
- initial_data_capacity = DEFAULT_DATA_CAPACITY,
142
- initial_metadata_capacity = DEFAULT_METADATA_CAPACITY,
143
- }) {
144
- assert.defined(device, 'device');
145
-
146
- assert.isNonNegativeInteger(initial_data_capacity, 'initial_data_capacity');
147
- assert.isNonNegativeInteger(initial_metadata_capacity, 'initial_metadata_capacity');
148
-
149
- assert.greaterThan(initial_data_capacity, 0, 'initial_data_capacity');
150
- assert.greaterThan(initial_metadata_capacity, 0, 'initial_data_capacity');
151
-
152
- this.#device = device;
153
-
154
- const data_buffer_size_words = Math.ceil(initial_data_capacity / 4);
155
-
156
- // TODO manage max-allocations as well
157
- this.#allocator_data = new OffsetAllocator(data_buffer_size_words);
158
- this.#allocator_metadata = new OffsetAllocator(initial_metadata_capacity);
159
-
160
- this.#meshlet_data_buffer = device.createBuffer({
161
- label: 'GPUMeshletManager/data',
162
- size: data_buffer_size_words * 4,
163
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
164
- });
165
-
166
- this.#meshlet_metadata_buffer = device.createBuffer({
167
- label: 'GPUMeshletManager/metadata',
168
- size: initial_metadata_capacity * MESHLET_METADATA_STRUCT.aligned_size,
169
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
170
- });
171
-
172
- }
173
-
174
- compact() {
175
- this.#rebuild(
176
- this.#meshlet_data_buffer.size >>> 2,
177
- Math.ceil(this.#meshlet_metadata_buffer.size / MESHLET_METADATA_STRUCT.aligned_size)
178
- );
179
- }
180
-
181
- /**
182
- *
183
- * @param {number} extra_data_size in words (4 bytes)
184
- * @param {number} extra_metadata_size in metadata elements
185
- */
186
- grow(extra_data_size, extra_metadata_size) {
187
- assert.isNonNegativeInteger(extra_data_size, 'extra_data_size');
188
- assert.isNonNegativeInteger(extra_metadata_size, 'extra_metadata_size');
189
-
190
- if (extra_data_size === 0 && extra_metadata_size === 0) {
191
- // nothing to do
192
- return;
193
- }
194
-
195
- const current_data_buffer_size_bytes = this.#meshlet_data_buffer.size;
196
- const current_metadata_buffer_size = this.#meshlet_metadata_buffer.size;
197
-
198
- const current_buffer_size_words = current_data_buffer_size_bytes >> 2;
199
- const current_metadata_buffer_size_elements = Math.ceil(current_metadata_buffer_size / MESHLET_METADATA_STRUCT.aligned_size);
200
-
201
- const DATA_GROWTH_STEP = 16 * 1024;
202
- const METADATA_GROWTH_STEP = 256;
203
- const GROWTH_MULTIPLIER = 1.25;
204
-
205
- let new_data_size;
206
- if (extra_data_size === 0) {
207
- new_data_size = current_buffer_size_words;
208
- } else {
209
- // grow
210
- new_data_size = Math.max(
211
- current_buffer_size_words + extra_data_size,
212
- current_buffer_size_words + DATA_GROWTH_STEP,
213
- Math.ceil(current_buffer_size_words * GROWTH_MULTIPLIER)
214
- );
215
- }
216
-
217
- let new_metadata_size;
218
- if (extra_metadata_size === 0) {
219
- new_metadata_size = current_metadata_buffer_size_elements;
220
- } else {
221
- // grow
222
- new_metadata_size = Math.max(
223
- current_metadata_buffer_size_elements + extra_metadata_size,
224
- current_metadata_buffer_size_elements + METADATA_GROWTH_STEP,
225
- current_metadata_buffer_size_elements * GROWTH_MULTIPLIER
226
- );
227
- }
228
-
229
- this.#rebuild(
230
- new_data_size,
231
- new_metadata_size
232
- );
233
- }
234
-
235
- /**
236
- * Helper to patch pointers in metadata and upload to GPU
237
- * @param {ArrayBuffer} source_array_buffer
238
- * @param {number} meshlet_count
239
- * @param {number} data_offset_words
240
- * @param {number} metadata_offset_index
241
- * @param {GPUBuffer} target_buffer
242
- */
243
- #write_patched_metadata(
244
- source_array_buffer,
245
- meshlet_count,
246
- data_offset_words,
247
- metadata_offset_index,
248
- target_buffer
249
- ) {
250
- const temp_writer = BinaryBuffer.fromEndianness(platform_compute_endianness());
251
- temp_writer.setCapacity(meshlet_count * MESHLET_METADATA_STRUCT.size); // pre-allocate to avoid resizing
252
-
253
- const source_reader = BinaryBuffer.fromArrayBuffer(source_array_buffer);
254
- source_reader.endianness = platform_compute_endianness();
255
-
256
- // Iterate over meshlets in the batch, patch the address, and write to temp buffer
257
- for (let i = 0; i < meshlet_count; i++) {
258
- const header = read_wgsl_type_value(source_reader, MESHLET_METADATA_STRUCT);
259
-
260
- // PATCH: Add the allocation offset to the relative address
261
- header.address += data_offset_words;
262
-
263
- write_wgsl_type_value(header, temp_writer, MESHLET_METADATA_STRUCT);
264
- }
265
-
266
- // Upload
267
- this.#device.queue.writeBuffer(
268
- target_buffer,
269
- metadata_offset_index * MESHLET_METADATA_STRUCT.aligned_size,
270
- temp_writer.data,
271
- 0,
272
- meshlet_count * MESHLET_METADATA_STRUCT.aligned_size
273
- );
274
- }
275
-
276
- /**
277
- *
278
- * @param {number} new_data_size
279
- * @param {number} new_metadata_size
280
- */
281
- #rebuild(
282
- new_data_size,
283
- new_metadata_size
284
- ) {
285
-
286
- assert.isNonNegativeInteger(new_data_size, 'new_data_size');
287
- assert.isNonNegativeInteger(new_metadata_size, 'new_metadata_size');
288
-
289
- // Outstanding clone work survives this. A queued clone names its
290
- // source and destination records, both of which are migrated
291
- // below, and the flush reads their addresses when it runs — so a
292
- // rebuild moves the copy rather than invalidating it.
293
-
294
- // console.warn('GPUMeshletManager/rebuild');
295
-
296
- const device = this.#device;
297
-
298
- const records = this.#records;
299
- const record_count = records.length;
300
- const new_allocator_data = new OffsetAllocator(
301
- new_data_size,
302
- Math.max(record_count * 2, 128 * 1024)
303
- );
304
-
305
- const old_buffer_data = this.#meshlet_data_buffer;
306
- const new_buffer_data = device.createBuffer({
307
- label: old_buffer_data.label,
308
- usage: old_buffer_data.usage,
309
- size: new_data_size * 4,
310
- });
311
-
312
- const new_allocator_metadata = new OffsetAllocator(
313
- new_metadata_size,
314
- Math.max(record_count * 2, 128 * 1024)
315
- );
316
-
317
- const old_buffer_metadata = this.#meshlet_metadata_buffer;
318
- const new_buffer_metadata = device.createBuffer({
319
- label: old_buffer_metadata.label,
320
- usage: old_buffer_metadata.usage,
321
- size: new_metadata_size * MESHLET_METADATA_STRUCT.aligned_size,
322
- });
323
-
324
- const encoder = device.createCommandEncoder({
325
- label: "GPUMeshletManager/rebuild",
326
- });
327
-
328
- const moved_records = [];
329
-
330
- // move data
331
- for (let i = 0; i < record_count; i++) {
332
- const record = records[i];
333
-
334
- const new_allocation_data = new_allocator_data.allocate(record.data_size);
335
- const new_allocation_metadata = new_allocator_metadata.allocate(record.meshlet_count);
336
-
337
- assert.notEqual(new_allocation_data.offset, ALLOCATOR_NO_SPACE);
338
- assert.notEqual(new_allocation_metadata.offset, ALLOCATOR_NO_SPACE);
339
-
340
- encoder.copyBufferToBuffer(
341
- old_buffer_data, record.data.offset * 4,
342
- new_buffer_data, new_allocation_data.offset * 4,
343
- record.data_size * 4
344
- );
345
-
346
- const metadata_record_moved = record.data.offset !== new_allocation_data.offset;
347
-
348
- // B. Patch Metadata (CPU -> GPU Write)
349
- // Metadata contains `address` which points to the Data offset.
350
- // Since the Data offset changed, we must re-calculate and upload the metadata.
351
- // We reuse the helper to avoid code duplication.
352
- this.#write_patched_metadata(
353
- record.metadata_buffer_original,
354
- record.meshlet_count,
355
- new_allocation_data.offset, // The NEW data offset
356
- new_allocation_metadata.offset, // The NEW metadata location
357
- new_buffer_metadata
358
- );
359
-
360
- // update record
361
- record.data = new_allocation_data;
362
- record.metadata = new_allocation_metadata;
363
-
364
- if (metadata_record_moved) {
365
- moved_records.push(record);
366
- }
367
- }
368
-
369
- for (const record of moved_records) {
370
- // notify
371
- record.changed.send0();
372
- }
373
-
374
- const command_buffer = encoder.finish();
375
-
376
- device.queue.submit([command_buffer]);
377
-
378
- old_buffer_data.destroy();
379
- old_buffer_metadata.destroy();
380
-
381
- this.#meshlet_data_buffer = new_buffer_data;
382
- this.#meshlet_metadata_buffer = new_buffer_metadata;
383
-
384
- this.#allocator_data = new_allocator_data;
385
- this.#allocator_metadata = new_allocator_metadata;
386
-
387
- // reset count
388
- this.#deallocations_since_last_rebuild = 0;
389
- }
390
-
391
- /**
392
- * Allocate `data_size_words` in the data buffer and `meshlet_count`
393
- * in the metadata buffer, retrying with compact-then-grow on failure.
394
- * Throws if the third attempt still can't satisfy both. The two
395
- * allocations are kept in lockstep — partial successes are released
396
- * before retrying so we never leak.
397
- *
398
- * Compaction triggers a full {@link #rebuild} which migrates every
399
- * existing allocation to a new buffer pair. Queued clone work is
400
- * carried through it {@link #pending_clones} names records, not
401
- * addresses — so an allocation made while a clone is pending is safe.
402
- *
403
- * @param {number} data_size_words
404
- * @param {number} meshlet_count
405
- * @returns {{data: Allocation, metadata: Allocation}}
406
- */
407
- #allocate_data_and_metadata(data_size_words, meshlet_count) {
408
- let data_allocation;
409
- let metadata_allocation;
410
-
411
- for (let try_index = 0; try_index < 3; try_index++) {
412
-
413
- data_allocation = this.#allocator_data.allocate(data_size_words);
414
- metadata_allocation = this.#allocator_metadata.allocate(meshlet_count);
415
-
416
- const data_ok = data_allocation.offset !== ALLOCATOR_NO_SPACE;
417
- const metadata_ok = metadata_allocation.offset !== ALLOCATOR_NO_SPACE;
418
-
419
- if (data_ok && metadata_ok) {
420
- break;
421
- }
422
-
423
- // release partial allocation
424
- if (data_ok) {
425
- this.#allocator_data.free(data_allocation);
426
-
427
- data_allocation.offset = ALLOCATOR_NO_SPACE;
428
- data_allocation.metadata = ALLOCATOR_NO_SPACE;
429
- }
430
- if (metadata_ok) {
431
- this.#allocator_metadata.free(metadata_allocation);
432
-
433
- metadata_allocation.offset = ALLOCATOR_NO_SPACE;
434
- metadata_allocation.metadata = ALLOCATOR_NO_SPACE;
435
- }
436
-
437
- if (this.#deallocations_since_last_rebuild > 0) {
438
- // try compaction
439
- this.compact();
440
- continue;
441
- }
442
-
443
- let grow_data_size = data_ok ? 0 : data_size_words;
444
- let grow_metadata_size = metadata_ok ? 0 : meshlet_count;
445
-
446
- this.grow(grow_data_size, grow_metadata_size);
447
-
448
- }
449
-
450
- if (data_allocation.offset === ALLOCATOR_NO_SPACE || metadata_allocation.offset === ALLOCATOR_NO_SPACE) {
451
- throw new Error('Failed to allocate GPU memory for meshlet batch');
452
- }
453
-
454
- return { data: data_allocation, metadata: metadata_allocation };
455
- }
456
-
457
- /**
458
- *
459
- * @param {MeshletBatch} batch
460
- * @returns {MeshletBatchAllocationRecord}
461
- */
462
- #allocate_for_batch(batch) {
463
- assert.defined(batch, 'batch');
464
-
465
- const data_buffer = batch.data_buffer;
466
-
467
- const data_size_words = Math.ceil(data_buffer.byteLength / 4);
468
-
469
- const meshlet_count = batch.count;
470
-
471
- const { data, metadata } = this.#allocate_data_and_metadata(data_size_words, meshlet_count);
472
-
473
- const record = new MeshletBatchAllocationRecord();
474
-
475
- record.data = data;
476
- record.metadata = metadata;
477
-
478
- record.meshlet_count = meshlet_count;
479
- record.data_size = data_size_words;
480
-
481
- record.metadata_buffer_original = batch.metadata_buffer;
482
-
483
- this.#records.push(record);
484
-
485
- return record;
486
- }
487
-
488
- /**
489
- * Release a batch allocation back to the data and metadata allocators. The GPU bytes are
490
- * not cleared the freed slices are reclaimed lazily by the next {@link compact} (a
491
- * deallocation bumps the compaction counter the allocator consults under pressure).
492
- * Other batches' allocations are unaffected (the OffsetAllocator frees disjoint slices),
493
- * so callers only need to ensure nothing still references this batch's metadata rows
494
- * (the geometry table rebuild in {@link GPUGeometryManager} handles that).
495
- *
496
- * @param {MeshletBatchAllocationRecord} record a record returned by {@link add_batch} or
497
- * {@link clone_batch_allocation}
498
- * @returns {boolean} true if the record was owned and freed
499
- */
500
- remove_batch(record) {
501
- assert.defined(record, 'record');
502
-
503
- const index = this.#records.indexOf(record);
504
- if (index === -1) {
505
- return false;
506
- }
507
-
508
- this.#allocator_data.free(record.data);
509
- this.#allocator_metadata.free(record.metadata);
510
-
511
- this.#records.splice(index, 1);
512
- this.#deallocations_since_last_rebuild++;
513
-
514
- return true;
515
- }
516
-
517
- /**
518
- * Allocate a fresh (data, metadata) pair sized to mirror `source`,
519
- * and queue a GPU-side clone: the source's data and metadata are
520
- * copied into the new ranges via `copyBufferToBuffer`, then a
521
- * compute pass patches the cloned metadata records' `address`
522
- * fields to point at the new data range. Both the copies and the
523
- * patch dispatch land when the host next calls
524
- * {@link update update(cmd_ctx)}.
525
- *
526
- * The returned record can be wired into the geometry table
527
- * immediately its `data.offset` and `metadata.offset` are valid
528
- * the moment this call returns. The GPU bytes at those addresses
529
- * just aren't populated yet; downstream passes that consume the
530
- * clone must run after the next `update`. Those addresses may still
531
- * move before the flush, which is why nothing outside the record is
532
- * allowed to remember them.
533
- *
534
- * Sharing the source's `metadata_buffer_original` lets a future
535
- * compaction patch the clone's metadata from the same un-patched
536
- * CPU bytes the source uses the post-patch state on the GPU is
537
- * identical to the source aside from the data offset.
538
- *
539
- * @param {MeshletBatchAllocationRecord} source
540
- * @returns {MeshletBatchAllocationRecord}
541
- */
542
- clone_batch_allocation(source) {
543
- assert.defined(source, 'source');
544
- assert.greaterThan(source.meshlet_count, 0, 'source.meshlet_count');
545
-
546
- const { data, metadata } = this.#allocate_data_and_metadata(source.data_size, source.meshlet_count);
547
-
548
- const record = new MeshletBatchAllocationRecord();
549
-
550
- record.data = data;
551
- record.metadata = metadata;
552
-
553
- record.meshlet_count = source.meshlet_count;
554
- record.data_size = source.data_size;
555
-
556
- // Share the un-patched CPU bytes so a future compact rewrites
557
- // the clone's metadata correctly under its new data offset.
558
- record.metadata_buffer_original = source.metadata_buffer_original;
559
-
560
- this.#records.push(record);
561
-
562
- // The queue names the two records rather than the addresses they
563
- // hold right now. Both are in `#records`, so a rebuild between
564
- // this call and the flush moves them and the flush still reads
565
- // where they went which is what lets a clone be queued in the
566
- // same frame as the growth another geometry's arrival forces.
567
- this.#pending_clones.push({ source, clone: record });
568
-
569
- return record;
570
- }
571
-
572
- /**
573
- * Flush any pending clone work into `cmd_ctx`. For each queued
574
- * clone, emits two `copyBufferToBuffer` commands (one for vertex
575
- * data, one for metadata records). After every queued clone's
576
- * copies are recorded, dispatches a single batched patch shader
577
- * pass that walks a flat `(record_index, address_delta)` table
578
- * N clones → 1 patch dispatch, total threads = sum of clone
579
- * meshlet counts.
580
- *
581
- * Idempotent and cheap when nothing's queued.
582
- *
583
- * @param {ShadeGPUCommandContext} cmd_ctx
584
- */
585
- update(cmd_ctx) {
586
- const pending = this.#pending_clones;
587
- if (pending.length === 0) {
588
- return;
589
- }
590
-
591
- const data_buffer = this.#meshlet_data_buffer;
592
- const metadata_buffer = this.#meshlet_metadata_buffer;
593
- const metadata_stride = MESHLET_METADATA_STRUCT.aligned_size;
594
- const metadata_stride_words = metadata_stride / 4;
595
-
596
- // WebGPU forbids `copyBufferToBuffer` with src === dst even when
597
- // ranges don't overlap (the validation check is buffer-identity,
598
- // not range-overlap), so we relocate via a small compute-shader
599
- // memcpy bound to the buffer once as `read_write`. Two dispatches
600
- // — one over the data-buffer regions, one over the metadata-buffer
601
- // regions — handle every queued clone in a batched pass.
602
- //
603
- // Region (src, dst) ranges are non-overlapping by construction:
604
- // the OffsetAllocator hands out fresh disjoint slices, so the
605
- // memcpy shader can safely skip the per-thread overlap check.
606
- const region_size_bytes = BUFFER_INTRA_COPY_REGION_STRUCT.aligned_size;
607
-
608
- const data_regions_array = new Uint32Array(pending.length * (region_size_bytes / 4));
609
- const metadata_regions_array = new Uint32Array(pending.length * (region_size_bytes / 4));
610
-
611
- let total_records = 0;
612
- for (let i = 0; i < pending.length; i++) {
613
- const { source, clone } = pending[i];
614
-
615
- // Region struct is just three u32s — write directly. Order
616
- // matches the field declaration: src_offset, dst_offset,
617
- // word_count.
618
- const region_words = i * 3;
619
- data_regions_array[region_words] = source.data.offset;
620
- data_regions_array[region_words + 1] = clone.data.offset;
621
- data_regions_array[region_words + 2] = clone.data_size;
622
-
623
- metadata_regions_array[region_words] = source.metadata.offset * metadata_stride_words;
624
- metadata_regions_array[region_words + 1] = clone.metadata.offset * metadata_stride_words;
625
- metadata_regions_array[region_words + 2] = clone.meshlet_count * metadata_stride_words;
626
-
627
- total_records += clone.meshlet_count;
628
- }
629
-
630
- const data_regions_buffer = cmd_ctx.allocateTransientBufferAndLoad(
631
- data_regions_array.buffer,
632
- GPUBufferUsage.STORAGE,
633
- );
634
- shader_buffer_intra_copy_regions.dispatch({
635
- encoder: cmd_ctx,
636
- group_count_x: pending.length,
637
- bindings: {
638
- buffer: data_buffer,
639
- regions: data_regions_buffer,
640
- },
641
- });
642
-
643
- const metadata_regions_buffer = cmd_ctx.allocateTransientBufferAndLoad(
644
- metadata_regions_array.buffer,
645
- GPUBufferUsage.STORAGE,
646
- );
647
- shader_buffer_intra_copy_regions.dispatch({
648
- encoder: cmd_ctx,
649
- group_count_x: pending.length,
650
- bindings: {
651
- buffer: metadata_buffer,
652
- regions: metadata_regions_buffer,
653
- },
654
- });
655
-
656
- // Build the flat (record_index, address_delta) table. One pair
657
- // per cloned meshlet record. Uploaded as a transient storage
658
- // buffer the patch shader binds for one dispatch.
659
- const patches_array = new Uint32Array(total_records * 2);
660
- let p = 0;
661
- for (let i = 0; i < pending.length; i++) {
662
- const { source, clone } = pending[i];
663
-
664
- // Address delta is in u32 word units. When the clone's offset
665
- // is below the source's, the unsigned subtraction wraps the
666
- // patch shader does the matching wrapping add, so the final
667
- // per-record `address` lands at the right absolute word offset
668
- // either way.
669
- const address_delta = (clone.data.offset - source.data.offset) >>> 0;
670
-
671
- for (let m = 0; m < clone.meshlet_count; m++) {
672
- patches_array[p++] = clone.metadata.offset + m;
673
- patches_array[p++] = address_delta;
674
- }
675
- }
676
-
677
- const patches_buffer = cmd_ctx.allocateTransientBufferAndLoad(
678
- patches_array.buffer,
679
- GPUBufferUsage.STORAGE,
680
- );
681
-
682
- // patch_count uniform is a bare u32 — see
683
- // shader_meshlet_metadata_address_patch.
684
- const patch_count_buffer = cmd_ctx.allocateTransientValueBuffer(
685
- u32,
686
- total_records,
687
- );
688
-
689
- shader_meshlet_metadata_address_patch.dispatchThreads({
690
- encoder: cmd_ctx,
691
- thread_count_x: total_records,
692
- bindings: {
693
- meshlet_metadata: metadata_buffer,
694
- patches: patches_buffer,
695
- patch_count: patch_count_buffer,
696
- },
697
- });
698
-
699
- pending.length = 0;
700
- }
701
-
702
- destroy() {
703
- this.#meshlet_data_buffer.destroy();
704
- this.#meshlet_metadata_buffer.destroy();
705
- }
706
-
707
- /**
708
- *
709
- * @param {MeshletBatch} batch
710
- * @returns {MeshletBatchAllocationRecord}
711
- */
712
- add_batch(batch) {
713
- assert.defined(batch, 'batch');
714
- assert.notNull(batch, 'batch');
715
- assert.equal(batch.isMeshletBatch, true, 'batch.isMeshletBatch !== true');
716
-
717
- const record = this.#allocate_for_batch(batch);
718
-
719
- // plonk data
720
- const data_buffer = batch.data_buffer;
721
- const data_allocation = record.data;
722
-
723
- this.#device.queue.writeBuffer(
724
- this.#meshlet_data_buffer,
725
- data_allocation.offset << 2, // allocator works in words of 4 bytes
726
- data_buffer
727
- );
728
-
729
- // Write Metadata (Patch & Upload)
730
- this.#write_patched_metadata(
731
- record.metadata_buffer_original,
732
- record.meshlet_count,
733
- record.data.offset,
734
- record.metadata.offset,
735
- this.#meshlet_metadata_buffer
736
- );
737
-
738
- return record;
739
- }
740
-
741
- async debug_dump_metadata() {
742
- const device = this.#device;
743
-
744
- return await inspect_gpu_buffer_typed({
745
- buffer: this.#meshlet_metadata_buffer,
746
- device,
747
- type: MESHLET_METADATA_STRUCT,
748
- count: Math.floor(this.#meshlet_metadata_buffer.size / MESHLET_METADATA_STRUCT.aligned_size),
749
- });
750
- }
751
-
752
- get gpu_memory_usage_occupied() {
753
- let result = 0;
754
-
755
- for (const record of this.#records) {
756
- result += record.data_size * 4;
757
- }
758
-
759
- return result;
760
- }
761
-
762
- get gpu_memory_usage() {
763
- let result = 0;
764
-
765
- if (this.#meshlet_data_buffer) {
766
- result += this.#meshlet_data_buffer.size;
767
- }
768
-
769
- if (this.#meshlet_metadata_buffer) {
770
- result += this.#meshlet_metadata_buffer.size;
771
- }
772
-
773
- return result;
774
- }
1
+ import { assert } from "../../../../core/assert.js";
2
+ import { ALLOCATOR_NO_SPACE, OffsetAllocator } from "../../../../core/binary/allocator/OffsetAllocator.js";
3
+ import { BinaryBuffer } from "../../../../core/binary/BinaryBuffer.js";
4
+ import { platform_compute_endianness } from "../../../../core/binary/platform_compute_endianness.js";
5
+ import Signal from "../../../../core/events/signal/Signal.js";
6
+ import { MEGABYTE } from "../../../../core/science/units/memory/MEGABYTE.js";
7
+ import { read_wgsl_type_value } from "../../shader/type/serde/read_gpu_typed_buffer.js";
8
+ import { write_wgsl_type_value } from "../../shader/type/serde/write_gpu_typed_buffer.js";
9
+ import { inspect_gpu_buffer_typed } from "../../buffer/inspect_gpu_buffer_typed.js";
10
+ import {
11
+ BUFFER_INTRA_COPY_REGION_STRUCT,
12
+ shader_buffer_intra_copy_regions
13
+ } from "../../buffer/util/shader_buffer_intra_copy_regions.js";
14
+ import { MESHLET_METADATA_STRUCT } from "./encoding/MESHLET_METADATA_STRUCT.js";
15
+ import { shader_meshlet_metadata_address_patch } from "./shader_meshlet_metadata_address_patch.js";
16
+
17
+ /**
18
+ * In meshlets
19
+ * @type {number}
20
+ */
21
+ const DEFAULT_METADATA_CAPACITY = 128 * 1024;
22
+
23
+ /**
24
+ * In bytes
25
+ * @type {number}
26
+ */
27
+ const DEFAULT_DATA_CAPACITY = 32 * MEGABYTE;
28
+
29
+ class MeshletBatchAllocationRecord {
30
+ /**
31
+ * @type {Allocation}
32
+ */
33
+ metadata
34
+ /**
35
+ * @type {Allocation}
36
+ */
37
+ data
38
+
39
+ /**
40
+ * Number of words
41
+ * @type {number}
42
+ */
43
+ data_size = 0;
44
+
45
+ /**
46
+ * Meshlet count
47
+ * @type {number}
48
+ */
49
+ meshlet_count = 0;
50
+
51
+ /**
52
+ *
53
+ * @type {ArrayBuffer}
54
+ */
55
+ metadata_buffer_original;
56
+
57
+ /**
58
+ *
59
+ * @type {Signal<>}
60
+ */
61
+ changed = new Signal()
62
+ }
63
+
64
+ export class GPUMeshletManager {
65
+
66
+ /**
67
+ * Allocation sizes in words of 4 bytes
68
+ * @type {OffsetAllocator}
69
+ */
70
+ #allocator_data = new OffsetAllocator(1024);
71
+
72
+ /**
73
+ * Allocation sizes in meshlets
74
+ * @type {OffsetAllocator}
75
+ */
76
+ #allocator_metadata = new OffsetAllocator(1024);
77
+
78
+ /**
79
+ * @type {GPUBuffer}
80
+ */
81
+ #meshlet_data_buffer;
82
+
83
+ get buffer_data() {
84
+ return this.#meshlet_data_buffer;
85
+ }
86
+
87
+ /**
88
+ * @type {GPUBuffer}
89
+ */
90
+ #meshlet_metadata_buffer;
91
+
92
+ get buffer_metadata() {
93
+ return this.#meshlet_metadata_buffer;
94
+ }
95
+
96
+ /**
97
+ * @type {GPUDevice}
98
+ */
99
+ #device;
100
+
101
+ /**
102
+ *
103
+ * @type {number}
104
+ */
105
+ #deallocations_since_last_rebuild = 0;
106
+
107
+ /**
108
+ *
109
+ * @type {MeshletBatchAllocationRecord[]}
110
+ */
111
+ #records = [];
112
+
113
+ /**
114
+ * Queue of GPU-side clones that have been allocated but not yet
115
+ * copied + patched. Drained by {@link update}. Each entry holds the
116
+ * minimum bookkeeping needed to issue two `copyBufferToBuffer`
117
+ * commands and N `(record_index, address_delta)` patch entries.
118
+ *
119
+ * Entries name the two records, not the addresses they held when the
120
+ * clone was queued: a {@link compact} or {@link grow} in between
121
+ * migrates both, and {@link update} reads where they ended up. What
122
+ * an entry does require is that neither record is handed back to
123
+ * {@link remove_batch} before the flush.
124
+ *
125
+ * @type {{
126
+ * source: MeshletBatchAllocationRecord,
127
+ * clone: MeshletBatchAllocationRecord,
128
+ * }[]}
129
+ */
130
+ #pending_clones = [];
131
+
132
+ /**
133
+ *
134
+ * @param {GPUDevice} device
135
+ * @param {number} [initial_data_capacity] in bytes
136
+ * @param {number} [initial_metadata_capacity] in meshlets
137
+ */
138
+ constructor({
139
+ device,
140
+ initial_data_capacity = DEFAULT_DATA_CAPACITY,
141
+ initial_metadata_capacity = DEFAULT_METADATA_CAPACITY,
142
+ }) {
143
+ assert.defined(device, 'device');
144
+
145
+ assert.isNonNegativeInteger(initial_data_capacity, 'initial_data_capacity');
146
+ assert.isNonNegativeInteger(initial_metadata_capacity, 'initial_metadata_capacity');
147
+
148
+ assert.greaterThan(initial_data_capacity, 0, 'initial_data_capacity');
149
+ assert.greaterThan(initial_metadata_capacity, 0, 'initial_data_capacity');
150
+
151
+ this.#device = device;
152
+
153
+ const data_buffer_size_words = Math.ceil(initial_data_capacity / 4);
154
+
155
+ // TODO manage max-allocations as well
156
+ this.#allocator_data = new OffsetAllocator(data_buffer_size_words);
157
+ this.#allocator_metadata = new OffsetAllocator(initial_metadata_capacity);
158
+
159
+ this.#meshlet_data_buffer = device.createBuffer({
160
+ label: 'GPUMeshletManager/data',
161
+ size: data_buffer_size_words * 4,
162
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
163
+ });
164
+
165
+ this.#meshlet_metadata_buffer = device.createBuffer({
166
+ label: 'GPUMeshletManager/metadata',
167
+ size: initial_metadata_capacity * MESHLET_METADATA_STRUCT.aligned_size,
168
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
169
+ });
170
+
171
+ }
172
+
173
+ compact() {
174
+ this.#rebuild(
175
+ this.#meshlet_data_buffer.size >>> 2,
176
+ Math.ceil(this.#meshlet_metadata_buffer.size / MESHLET_METADATA_STRUCT.aligned_size)
177
+ );
178
+ }
179
+
180
+ /**
181
+ *
182
+ * @param {number} extra_data_size in words (4 bytes)
183
+ * @param {number} extra_metadata_size in metadata elements
184
+ */
185
+ grow(extra_data_size, extra_metadata_size) {
186
+ assert.isNonNegativeInteger(extra_data_size, 'extra_data_size');
187
+ assert.isNonNegativeInteger(extra_metadata_size, 'extra_metadata_size');
188
+
189
+ if (extra_data_size === 0 && extra_metadata_size === 0) {
190
+ // nothing to do
191
+ return;
192
+ }
193
+
194
+ const current_data_buffer_size_bytes = this.#meshlet_data_buffer.size;
195
+ const current_metadata_buffer_size = this.#meshlet_metadata_buffer.size;
196
+
197
+ const current_buffer_size_words = current_data_buffer_size_bytes >> 2;
198
+ const current_metadata_buffer_size_elements = Math.ceil(current_metadata_buffer_size / MESHLET_METADATA_STRUCT.aligned_size);
199
+
200
+ const DATA_GROWTH_STEP = 16 * 1024;
201
+ const METADATA_GROWTH_STEP = 256;
202
+ const GROWTH_MULTIPLIER = 1.25;
203
+
204
+ let new_data_size;
205
+ if (extra_data_size === 0) {
206
+ new_data_size = current_buffer_size_words;
207
+ } else {
208
+ // grow
209
+ new_data_size = Math.max(
210
+ current_buffer_size_words + extra_data_size,
211
+ current_buffer_size_words + DATA_GROWTH_STEP,
212
+ Math.ceil(current_buffer_size_words * GROWTH_MULTIPLIER)
213
+ );
214
+ }
215
+
216
+ let new_metadata_size;
217
+ if (extra_metadata_size === 0) {
218
+ new_metadata_size = current_metadata_buffer_size_elements;
219
+ } else {
220
+ // grow
221
+ new_metadata_size = Math.max(
222
+ current_metadata_buffer_size_elements + extra_metadata_size,
223
+ current_metadata_buffer_size_elements + METADATA_GROWTH_STEP,
224
+ current_metadata_buffer_size_elements * GROWTH_MULTIPLIER
225
+ );
226
+ }
227
+
228
+ this.#rebuild(
229
+ new_data_size,
230
+ new_metadata_size
231
+ );
232
+ }
233
+
234
+ /**
235
+ * Helper to patch pointers in metadata and upload to GPU
236
+ * @param {ArrayBuffer} source_array_buffer
237
+ * @param {number} meshlet_count
238
+ * @param {number} data_offset_words
239
+ * @param {number} metadata_offset_index
240
+ * @param {GPUBuffer} target_buffer
241
+ */
242
+ #write_patched_metadata(
243
+ source_array_buffer,
244
+ meshlet_count,
245
+ data_offset_words,
246
+ metadata_offset_index,
247
+ target_buffer
248
+ ) {
249
+ const temp_writer = BinaryBuffer.fromEndianness(platform_compute_endianness());
250
+ temp_writer.setCapacity(meshlet_count * MESHLET_METADATA_STRUCT.size); // pre-allocate to avoid resizing
251
+
252
+ const source_reader = BinaryBuffer.fromArrayBuffer(source_array_buffer);
253
+ source_reader.endianness = platform_compute_endianness();
254
+
255
+ // Iterate over meshlets in the batch, patch the address, and write to temp buffer
256
+ for (let i = 0; i < meshlet_count; i++) {
257
+ const header = read_wgsl_type_value(source_reader, MESHLET_METADATA_STRUCT);
258
+
259
+ // PATCH: Add the allocation offset to the relative address
260
+ header.address += data_offset_words;
261
+
262
+ write_wgsl_type_value(header, temp_writer, MESHLET_METADATA_STRUCT);
263
+ }
264
+
265
+ // Upload
266
+ this.#device.queue.writeBuffer(
267
+ target_buffer,
268
+ metadata_offset_index * MESHLET_METADATA_STRUCT.aligned_size,
269
+ temp_writer.data,
270
+ 0,
271
+ meshlet_count * MESHLET_METADATA_STRUCT.aligned_size
272
+ );
273
+ }
274
+
275
+ /**
276
+ *
277
+ * @param {number} new_data_size
278
+ * @param {number} new_metadata_size
279
+ */
280
+ #rebuild(
281
+ new_data_size,
282
+ new_metadata_size
283
+ ) {
284
+
285
+ assert.isNonNegativeInteger(new_data_size, 'new_data_size');
286
+ assert.isNonNegativeInteger(new_metadata_size, 'new_metadata_size');
287
+
288
+ // Outstanding clone work survives this. A queued clone names its
289
+ // source and destination records, both of which are migrated
290
+ // below, and the flush reads their addresses when it runs — so a
291
+ // rebuild moves the copy rather than invalidating it.
292
+
293
+ // console.warn('GPUMeshletManager/rebuild');
294
+
295
+ const device = this.#device;
296
+
297
+ const records = this.#records;
298
+ const record_count = records.length;
299
+ const new_allocator_data = new OffsetAllocator(
300
+ new_data_size,
301
+ Math.max(record_count * 2, 128 * 1024)
302
+ );
303
+
304
+ const old_buffer_data = this.#meshlet_data_buffer;
305
+ const new_buffer_data = device.createBuffer({
306
+ label: old_buffer_data.label,
307
+ usage: old_buffer_data.usage,
308
+ size: new_data_size * 4,
309
+ });
310
+
311
+ const new_allocator_metadata = new OffsetAllocator(
312
+ new_metadata_size,
313
+ Math.max(record_count * 2, 128 * 1024)
314
+ );
315
+
316
+ const old_buffer_metadata = this.#meshlet_metadata_buffer;
317
+ const new_buffer_metadata = device.createBuffer({
318
+ label: old_buffer_metadata.label,
319
+ usage: old_buffer_metadata.usage,
320
+ size: new_metadata_size * MESHLET_METADATA_STRUCT.aligned_size,
321
+ });
322
+
323
+ const encoder = device.createCommandEncoder({
324
+ label: "GPUMeshletManager/rebuild",
325
+ });
326
+
327
+ const moved_records = [];
328
+
329
+ // move data
330
+ for (let i = 0; i < record_count; i++) {
331
+ const record = records[i];
332
+
333
+ const new_allocation_data = new_allocator_data.allocate(record.data_size);
334
+ const new_allocation_metadata = new_allocator_metadata.allocate(record.meshlet_count);
335
+
336
+ assert.notEqual(new_allocation_data.offset, ALLOCATOR_NO_SPACE);
337
+ assert.notEqual(new_allocation_metadata.offset, ALLOCATOR_NO_SPACE);
338
+
339
+ encoder.copyBufferToBuffer(
340
+ old_buffer_data, record.data.offset * 4,
341
+ new_buffer_data, new_allocation_data.offset * 4,
342
+ record.data_size * 4
343
+ );
344
+
345
+ const metadata_record_moved = record.data.offset !== new_allocation_data.offset;
346
+
347
+ // B. Patch Metadata (CPU -> GPU Write)
348
+ // Metadata contains `address` which points to the Data offset.
349
+ // Since the Data offset changed, we must re-calculate and upload the metadata.
350
+ // We reuse the helper to avoid code duplication.
351
+ this.#write_patched_metadata(
352
+ record.metadata_buffer_original,
353
+ record.meshlet_count,
354
+ new_allocation_data.offset, // The NEW data offset
355
+ new_allocation_metadata.offset, // The NEW metadata location
356
+ new_buffer_metadata
357
+ );
358
+
359
+ // update record
360
+ record.data = new_allocation_data;
361
+ record.metadata = new_allocation_metadata;
362
+
363
+ if (metadata_record_moved) {
364
+ moved_records.push(record);
365
+ }
366
+ }
367
+
368
+ for (const record of moved_records) {
369
+ // notify
370
+ record.changed.send0();
371
+ }
372
+
373
+ const command_buffer = encoder.finish();
374
+
375
+ device.queue.submit([command_buffer]);
376
+
377
+ old_buffer_data.destroy();
378
+ old_buffer_metadata.destroy();
379
+
380
+ this.#meshlet_data_buffer = new_buffer_data;
381
+ this.#meshlet_metadata_buffer = new_buffer_metadata;
382
+
383
+ this.#allocator_data = new_allocator_data;
384
+ this.#allocator_metadata = new_allocator_metadata;
385
+
386
+ // reset count
387
+ this.#deallocations_since_last_rebuild = 0;
388
+ }
389
+
390
+ /**
391
+ * Allocate `data_size_words` in the data buffer and `meshlet_count`
392
+ * in the metadata buffer, retrying with compact-then-grow on failure.
393
+ * Throws if the third attempt still can't satisfy both. The two
394
+ * allocations are kept in lockstep partial successes are released
395
+ * before retrying so we never leak.
396
+ *
397
+ * Compaction triggers a full {@link #rebuild} which migrates every
398
+ * existing allocation to a new buffer pair. Queued clone work is
399
+ * carried through it {@link #pending_clones} names records, not
400
+ * addresses so an allocation made while a clone is pending is safe.
401
+ *
402
+ * @param {number} data_size_words
403
+ * @param {number} meshlet_count
404
+ * @returns {{data: Allocation, metadata: Allocation}}
405
+ */
406
+ #allocate_data_and_metadata(data_size_words, meshlet_count) {
407
+ let data_allocation;
408
+ let metadata_allocation;
409
+
410
+ for (let try_index = 0; try_index < 3; try_index++) {
411
+
412
+ data_allocation = this.#allocator_data.allocate(data_size_words);
413
+ metadata_allocation = this.#allocator_metadata.allocate(meshlet_count);
414
+
415
+ const data_ok = data_allocation.offset !== ALLOCATOR_NO_SPACE;
416
+ const metadata_ok = metadata_allocation.offset !== ALLOCATOR_NO_SPACE;
417
+
418
+ if (data_ok && metadata_ok) {
419
+ break;
420
+ }
421
+
422
+ // release partial allocation
423
+ if (data_ok) {
424
+ this.#allocator_data.free(data_allocation);
425
+
426
+ data_allocation.offset = ALLOCATOR_NO_SPACE;
427
+ data_allocation.metadata = ALLOCATOR_NO_SPACE;
428
+ }
429
+ if (metadata_ok) {
430
+ this.#allocator_metadata.free(metadata_allocation);
431
+
432
+ metadata_allocation.offset = ALLOCATOR_NO_SPACE;
433
+ metadata_allocation.metadata = ALLOCATOR_NO_SPACE;
434
+ }
435
+
436
+ if (this.#deallocations_since_last_rebuild > 0) {
437
+ // try compaction
438
+ this.compact();
439
+ continue;
440
+ }
441
+
442
+ let grow_data_size = data_ok ? 0 : data_size_words;
443
+ let grow_metadata_size = metadata_ok ? 0 : meshlet_count;
444
+
445
+ this.grow(grow_data_size, grow_metadata_size);
446
+
447
+ }
448
+
449
+ if (data_allocation.offset === ALLOCATOR_NO_SPACE || metadata_allocation.offset === ALLOCATOR_NO_SPACE) {
450
+ throw new Error('Failed to allocate GPU memory for meshlet batch');
451
+ }
452
+
453
+ return { data: data_allocation, metadata: metadata_allocation };
454
+ }
455
+
456
+ /**
457
+ *
458
+ * @param {MeshletBatch} batch
459
+ * @returns {MeshletBatchAllocationRecord}
460
+ */
461
+ #allocate_for_batch(batch) {
462
+ assert.defined(batch, 'batch');
463
+
464
+ const data_buffer = batch.data_buffer;
465
+
466
+ const data_size_words = Math.ceil(data_buffer.byteLength / 4);
467
+
468
+ const meshlet_count = batch.count;
469
+
470
+ const { data, metadata } = this.#allocate_data_and_metadata(data_size_words, meshlet_count);
471
+
472
+ const record = new MeshletBatchAllocationRecord();
473
+
474
+ record.data = data;
475
+ record.metadata = metadata;
476
+
477
+ record.meshlet_count = meshlet_count;
478
+ record.data_size = data_size_words;
479
+
480
+ record.metadata_buffer_original = batch.metadata_buffer;
481
+
482
+ this.#records.push(record);
483
+
484
+ return record;
485
+ }
486
+
487
+ /**
488
+ * Release a batch allocation back to the data and metadata allocators. The GPU bytes are
489
+ * not cleared the freed slices are reclaimed lazily by the next {@link compact} (a
490
+ * deallocation bumps the compaction counter the allocator consults under pressure).
491
+ * Other batches' allocations are unaffected (the OffsetAllocator frees disjoint slices),
492
+ * so callers only need to ensure nothing still references this batch's metadata rows
493
+ * (the geometry table rebuild in {@link GPUGeometryManager} handles that).
494
+ *
495
+ * @param {MeshletBatchAllocationRecord} record a record returned by {@link add_batch} or
496
+ * {@link clone_batch_allocation}
497
+ * @returns {boolean} true if the record was owned and freed
498
+ */
499
+ remove_batch(record) {
500
+ assert.defined(record, 'record');
501
+
502
+ const index = this.#records.indexOf(record);
503
+ if (index === -1) {
504
+ return false;
505
+ }
506
+
507
+ this.#allocator_data.free(record.data);
508
+ this.#allocator_metadata.free(record.metadata);
509
+
510
+ this.#records.splice(index, 1);
511
+ this.#deallocations_since_last_rebuild++;
512
+
513
+ return true;
514
+ }
515
+
516
+ /**
517
+ * Allocate a fresh (data, metadata) pair sized to mirror `source`,
518
+ * and queue a GPU-side clone: the source's data and metadata are
519
+ * copied into the new ranges via `copyBufferToBuffer`, then a
520
+ * compute pass patches the cloned metadata records' `address`
521
+ * fields to point at the new data range. Both the copies and the
522
+ * patch dispatch land when the host next calls
523
+ * {@link update update(cmd_ctx)}.
524
+ *
525
+ * The returned record can be wired into the geometry table
526
+ * immediately its `data.offset` and `metadata.offset` are valid
527
+ * the moment this call returns. The GPU bytes at those addresses
528
+ * just aren't populated yet; downstream passes that consume the
529
+ * clone must run after the next `update`. Those addresses may still
530
+ * move before the flush, which is why nothing outside the record is
531
+ * allowed to remember them.
532
+ *
533
+ * Sharing the source's `metadata_buffer_original` lets a future
534
+ * compaction patch the clone's metadata from the same un-patched
535
+ * CPU bytes the source uses the post-patch state on the GPU is
536
+ * identical to the source aside from the data offset.
537
+ *
538
+ * @param {MeshletBatchAllocationRecord} source
539
+ * @returns {MeshletBatchAllocationRecord}
540
+ */
541
+ clone_batch_allocation(source) {
542
+ assert.defined(source, 'source');
543
+ assert.greaterThan(source.meshlet_count, 0, 'source.meshlet_count');
544
+
545
+ const { data, metadata } = this.#allocate_data_and_metadata(source.data_size, source.meshlet_count);
546
+
547
+ const record = new MeshletBatchAllocationRecord();
548
+
549
+ record.data = data;
550
+ record.metadata = metadata;
551
+
552
+ record.meshlet_count = source.meshlet_count;
553
+ record.data_size = source.data_size;
554
+
555
+ // Share the un-patched CPU bytes so a future compact rewrites
556
+ // the clone's metadata correctly under its new data offset.
557
+ record.metadata_buffer_original = source.metadata_buffer_original;
558
+
559
+ this.#records.push(record);
560
+
561
+ // The queue names the two records rather than the addresses they
562
+ // hold right now. Both are in `#records`, so a rebuild between
563
+ // this call and the flush moves them and the flush still reads
564
+ // where they went which is what lets a clone be queued in the
565
+ // same frame as the growth another geometry's arrival forces.
566
+ this.#pending_clones.push({ source, clone: record });
567
+
568
+ return record;
569
+ }
570
+
571
+ /**
572
+ * Flush any pending clone work into `cmd_ctx`. For each queued
573
+ * clone, emits two `copyBufferToBuffer` commands (one for vertex
574
+ * data, one for metadata records). After every queued clone's
575
+ * copies are recorded, dispatches a single batched patch shader
576
+ * pass that walks a flat `(record_index, address_delta)` table
577
+ * N clones 1 patch dispatch, total threads = sum of clone
578
+ * meshlet counts.
579
+ *
580
+ * Idempotent and cheap when nothing's queued.
581
+ *
582
+ * @param {ShadeGPUCommandContext} cmd_ctx
583
+ */
584
+ update(cmd_ctx) {
585
+ const pending = this.#pending_clones;
586
+ if (pending.length === 0) {
587
+ return;
588
+ }
589
+
590
+ const data_buffer = this.#meshlet_data_buffer;
591
+ const metadata_buffer = this.#meshlet_metadata_buffer;
592
+ const metadata_stride = MESHLET_METADATA_STRUCT.aligned_size;
593
+ const metadata_stride_words = metadata_stride / 4;
594
+
595
+ // WebGPU forbids `copyBufferToBuffer` with src === dst even when
596
+ // ranges don't overlap (the validation check is buffer-identity,
597
+ // not range-overlap), so we relocate via a small compute-shader
598
+ // memcpy bound to the buffer once as `read_write`. Two dispatches
599
+ // one over the data-buffer regions, one over the metadata-buffer
600
+ // regions handle every queued clone in a batched pass.
601
+ //
602
+ // Region (src, dst) ranges are non-overlapping by construction:
603
+ // the OffsetAllocator hands out fresh disjoint slices, so the
604
+ // memcpy shader can safely skip the per-thread overlap check.
605
+ const region_size_bytes = BUFFER_INTRA_COPY_REGION_STRUCT.aligned_size;
606
+
607
+ const data_regions_array = new Uint32Array(pending.length * (region_size_bytes / 4));
608
+ const metadata_regions_array = new Uint32Array(pending.length * (region_size_bytes / 4));
609
+
610
+ let total_records = 0;
611
+ for (let i = 0; i < pending.length; i++) {
612
+ const { source, clone } = pending[i];
613
+
614
+ // Region struct is just three u32s — write directly. Order
615
+ // matches the field declaration: src_offset, dst_offset,
616
+ // word_count.
617
+ const region_words = i * 3;
618
+ data_regions_array[region_words] = source.data.offset;
619
+ data_regions_array[region_words + 1] = clone.data.offset;
620
+ data_regions_array[region_words + 2] = clone.data_size;
621
+
622
+ metadata_regions_array[region_words] = source.metadata.offset * metadata_stride_words;
623
+ metadata_regions_array[region_words + 1] = clone.metadata.offset * metadata_stride_words;
624
+ metadata_regions_array[region_words + 2] = clone.meshlet_count * metadata_stride_words;
625
+
626
+ total_records += clone.meshlet_count;
627
+ }
628
+
629
+ const data_regions_buffer = cmd_ctx.allocateTransientBufferAndLoad(
630
+ data_regions_array.buffer,
631
+ GPUBufferUsage.STORAGE,
632
+ );
633
+ shader_buffer_intra_copy_regions.dispatch({
634
+ encoder: cmd_ctx,
635
+ group_count_x: pending.length,
636
+ bindings: {
637
+ buffer: data_buffer,
638
+ regions: data_regions_buffer,
639
+ },
640
+ });
641
+
642
+ const metadata_regions_buffer = cmd_ctx.allocateTransientBufferAndLoad(
643
+ metadata_regions_array.buffer,
644
+ GPUBufferUsage.STORAGE,
645
+ );
646
+ shader_buffer_intra_copy_regions.dispatch({
647
+ encoder: cmd_ctx,
648
+ group_count_x: pending.length,
649
+ bindings: {
650
+ buffer: metadata_buffer,
651
+ regions: metadata_regions_buffer,
652
+ },
653
+ });
654
+
655
+ // Build the flat (record_index, address_delta) table. One pair
656
+ // per cloned meshlet record. Uploaded as a transient storage
657
+ // buffer the patch shader binds for one dispatch.
658
+ const patches_array = new Uint32Array(total_records * 2);
659
+ let p = 0;
660
+ for (let i = 0; i < pending.length; i++) {
661
+ const { source, clone } = pending[i];
662
+
663
+ // Address delta is in u32 word units. When the clone's offset
664
+ // is below the source's, the unsigned subtraction wraps the
665
+ // patch shader does the matching wrapping add, so the final
666
+ // per-record `address` lands at the right absolute word offset
667
+ // either way.
668
+ const address_delta = (clone.data.offset - source.data.offset) >>> 0;
669
+
670
+ for (let m = 0; m < clone.meshlet_count; m++) {
671
+ patches_array[p++] = clone.metadata.offset + m;
672
+ patches_array[p++] = address_delta;
673
+ }
674
+ }
675
+
676
+ const patches_buffer = cmd_ctx.allocateTransientBufferAndLoad(
677
+ patches_array.buffer,
678
+ GPUBufferUsage.STORAGE,
679
+ );
680
+
681
+ shader_meshlet_metadata_address_patch.dispatchThreads({
682
+ encoder: cmd_ctx,
683
+ thread_count_x: total_records,
684
+ bindings: {
685
+ meshlet_metadata: metadata_buffer,
686
+ patches: patches_buffer,
687
+ patch_count: total_records,
688
+ },
689
+ });
690
+
691
+ pending.length = 0;
692
+ }
693
+
694
+ destroy() {
695
+ this.#meshlet_data_buffer.destroy();
696
+ this.#meshlet_metadata_buffer.destroy();
697
+ }
698
+
699
+ /**
700
+ *
701
+ * @param {MeshletBatch} batch
702
+ * @returns {MeshletBatchAllocationRecord}
703
+ */
704
+ add_batch(batch) {
705
+ assert.defined(batch, 'batch');
706
+ assert.notNull(batch, 'batch');
707
+ assert.equal(batch.isMeshletBatch, true, 'batch.isMeshletBatch !== true');
708
+
709
+ const record = this.#allocate_for_batch(batch);
710
+
711
+ // plonk data
712
+ const data_buffer = batch.data_buffer;
713
+ const data_allocation = record.data;
714
+
715
+ this.#device.queue.writeBuffer(
716
+ this.#meshlet_data_buffer,
717
+ data_allocation.offset << 2, // allocator works in words of 4 bytes
718
+ data_buffer
719
+ );
720
+
721
+ // Write Metadata (Patch & Upload)
722
+ this.#write_patched_metadata(
723
+ record.metadata_buffer_original,
724
+ record.meshlet_count,
725
+ record.data.offset,
726
+ record.metadata.offset,
727
+ this.#meshlet_metadata_buffer
728
+ );
729
+
730
+ return record;
731
+ }
732
+
733
+ async debug_dump_metadata() {
734
+ const device = this.#device;
735
+
736
+ return await inspect_gpu_buffer_typed({
737
+ buffer: this.#meshlet_metadata_buffer,
738
+ device,
739
+ type: MESHLET_METADATA_STRUCT,
740
+ count: Math.floor(this.#meshlet_metadata_buffer.size / MESHLET_METADATA_STRUCT.aligned_size),
741
+ });
742
+ }
743
+
744
+ get gpu_memory_usage_occupied() {
745
+ let result = 0;
746
+
747
+ for (const record of this.#records) {
748
+ result += record.data_size * 4;
749
+ }
750
+
751
+ return result;
752
+ }
753
+
754
+ get gpu_memory_usage() {
755
+ let result = 0;
756
+
757
+ if (this.#meshlet_data_buffer) {
758
+ result += this.#meshlet_data_buffer.size;
759
+ }
760
+
761
+ if (this.#meshlet_metadata_buffer) {
762
+ result += this.#meshlet_metadata_buffer.size;
763
+ }
764
+
765
+ return result;
766
+ }
775
767
  }