@woosh/meep-engine 3.20.0 → 3.21.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 (701) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/editor/Editor.d.ts.map +1 -1
  3. package/editor/enableEditor.d.ts.map +1 -1
  4. package/editor/particles/effect/ParticleEffectDocument.d.ts.map +1 -1
  5. package/editor/particles/effect/ParticleReferenceSimulation.d.ts.map +1 -1
  6. package/editor/templates/entity_templates.d.ts.map +1 -1
  7. package/editor/tools/v2/GizmoNode.d.ts +1 -1
  8. package/editor/tools/v2/GizmoNode.d.ts.map +1 -1
  9. package/editor/view/ecs/HierarchicalEntityListView.d.ts +3 -3
  10. package/editor/view/ecs/HierarchicalEntityListView.d.ts.map +1 -1
  11. package/editor/view/makeEntityDecorators.d.ts.map +1 -1
  12. package/editor/view/node-graph/NodeGraphClipboard.d.ts.map +1 -1
  13. package/editor/view/tools/ToolSettingsView.d.ts +1 -1
  14. package/editor/view/tools/ToolSettingsView.d.ts.map +1 -1
  15. package/package.json +12 -12
  16. package/src/core/IdPool.d.ts.map +1 -1
  17. package/src/core/binary/32BitEncoder.d.ts.map +1 -1
  18. package/src/core/binary/BinaryBuffer.d.ts +1 -1
  19. package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
  20. package/src/core/bvh2/binary/2/BinaryUint32BVH.d.ts +1 -1
  21. package/src/core/bvh2/binary/2/BinaryUint32BVH.d.ts.map +1 -1
  22. package/src/core/bvh2/bvh3/BVH.d.ts +3 -3
  23. package/src/core/bvh2/bvh3/BVH.d.ts.map +1 -1
  24. package/src/core/bvh2/bvh3/BVH.js +1 -2
  25. package/src/core/bvh2/bvh3/query/bvh_query_user_data_nearest_to_point_filtered.d.ts.map +1 -1
  26. package/src/core/bvh2/traversal/aabb3_detailed_volume_intersection_callback_based.d.ts.map +1 -1
  27. package/src/core/cache/Cache.d.ts +1 -1
  28. package/src/core/cache/Cache.d.ts.map +1 -1
  29. package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts.map +1 -1
  30. package/src/core/collection/list/List.d.ts.map +1 -1
  31. package/src/core/collection/list/ListForwarder.d.ts +1 -1
  32. package/src/core/collection/list/ListForwarder.d.ts.map +1 -1
  33. package/src/core/collection/map/HashMap.d.ts.map +1 -1
  34. package/src/core/collection/set/ArraySet.d.ts.map +1 -1
  35. package/src/core/collection/table/RowFirstTable.d.ts +1 -1
  36. package/src/core/collection/table/RowFirstTable.d.ts.map +1 -1
  37. package/src/core/collection/table/bind/TableRecord.d.ts +1 -1
  38. package/src/core/collection/table/bind/TableRecord.d.ts.map +1 -1
  39. package/src/core/color/Color.d.ts +8 -8
  40. package/src/core/color/Color.d.ts.map +1 -1
  41. package/src/core/color/hsv/hsv_to_rgb_uint8.d.ts.map +1 -1
  42. package/src/core/color/hsv/rgb_to_hsv.d.ts.map +1 -1
  43. package/src/core/events/signal/Signal.d.ts +1 -1
  44. package/src/core/events/signal/Signal.d.ts.map +1 -1
  45. package/src/core/events/signal/signal_aggregate_by_time_window.d.ts.map +1 -1
  46. package/src/core/fsm/simple/SimpleStateMachineDescription.d.ts.map +1 -1
  47. package/src/core/geom/2d/aabb/AABB2.d.ts +4 -4
  48. package/src/core/geom/2d/aabb/AABB2.d.ts.map +1 -1
  49. package/src/core/geom/2d/bvh/BVH2D.d.ts +1 -1
  50. package/src/core/geom/2d/bvh/BVH2D.d.ts.map +1 -1
  51. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -1
  52. package/src/core/geom/3d/aabb/AABB3.d.ts +6 -6
  53. package/src/core/geom/3d/aabb/AABB3.d.ts.map +1 -1
  54. package/src/core/geom/3d/atlas/atlas_bench_lib.d.ts.map +1 -1
  55. package/src/core/geom/3d/atlas/atlas_compute_charts.d.ts.map +1 -1
  56. package/src/core/geom/3d/atlas/atlas_generate.d.ts.map +1 -1
  57. package/src/core/geom/3d/atlas/pack/atlas_pack_charts.d.ts.map +1 -1
  58. package/src/core/geom/3d/atlas/param/atlas_chart_param_validity.d.ts.map +1 -1
  59. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_bff.d.ts.map +1 -1
  60. package/src/core/geom/3d/atlas/param/atlas_chart_parameterize_lscm.d.ts.map +1 -1
  61. package/src/core/geom/3d/atlas/segment/atlas_compute_planar_regions.d.ts.map +1 -1
  62. package/src/core/geom/3d/atlas/segment/atlas_segment_charts.d.ts.map +1 -1
  63. package/src/core/geom/3d/atlas/segment/atlas_segment_charts_atomic.d.ts.map +1 -1
  64. package/src/core/geom/3d/mat4/m4_decompose.d.ts +7 -1
  65. package/src/core/geom/3d/mat4/m4_decompose.d.ts.map +1 -1
  66. package/src/core/geom/3d/quadric/Quadric3.d.ts +11 -11
  67. package/src/core/geom/3d/quadric/Quadric3.d.ts.map +1 -1
  68. package/src/core/geom/3d/ray/Ray3.d.ts +3 -3
  69. package/src/core/geom/3d/ray/Ray3.d.ts.map +1 -1
  70. package/src/core/geom/3d/shape/TransformedShape3D.d.ts +1 -1
  71. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  72. package/src/core/geom/3d/tetrahedra/TetrahedralMesh.d.ts.map +1 -1
  73. package/src/core/geom/3d/tetrahedra/delaunay/debug/push_boundary_with_validation.d.ts.map +1 -1
  74. package/src/core/geom/3d/tetrahedra/delaunay/debug/validate_cavity_boundary.d.ts.map +1 -1
  75. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_carve_outside_surface.d.ts.map +1 -1
  76. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.d.ts.map +1 -1
  77. package/src/core/geom/3d/tetrahedra/validate_tetrahedral_mesh.d.ts.map +1 -1
  78. package/src/core/geom/3d/tetrahedra/validate_tetrahedron_neighbourhood.d.ts.map +1 -1
  79. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_to_indexed_geometry.d.ts.map +1 -1
  80. package/src/core/geom/3d/util/aabb3_emit_point_grid.d.ts.map +1 -1
  81. package/src/core/geom/Quaternion.d.ts +4 -4
  82. package/src/core/geom/Quaternion.d.ts.map +1 -1
  83. package/src/core/geom/Vector1.d.ts +1 -1
  84. package/src/core/geom/Vector1.d.ts.map +1 -1
  85. package/src/core/geom/Vector2.d.ts +2 -2
  86. package/src/core/geom/Vector2.d.ts.map +1 -1
  87. package/src/core/geom/Vector3.d.ts +3 -3
  88. package/src/core/geom/Vector3.d.ts.map +1 -1
  89. package/src/core/geom/Vector4.d.ts +4 -4
  90. package/src/core/geom/Vector4.d.ts.map +1 -1
  91. package/src/core/geom/packing/max-rect/find_best_container.d.ts.map +1 -1
  92. package/src/core/geom/vec3/v3_hash.d.ts +12 -0
  93. package/src/core/geom/vec3/v3_hash.d.ts.map +1 -0
  94. package/src/core/geom/vec3/v3_hash.js +19 -0
  95. package/src/core/geom/vec3/v3_lerp.d.ts +3 -1
  96. package/src/core/geom/vec3/v3_lerp.d.ts.map +1 -1
  97. package/src/core/geom/vec3/v3_set.d.ts +14 -0
  98. package/src/core/geom/vec3/v3_set.d.ts.map +1 -0
  99. package/src/core/geom/vec3/v3_set.js +17 -0
  100. package/src/core/geom/vec3/v3_slerp.d.ts +3 -1
  101. package/src/core/geom/vec3/v3_slerp.d.ts.map +1 -1
  102. package/src/core/geom/vec4/v4_equals_array.d.ts +11 -0
  103. package/src/core/geom/vec4/v4_equals_array.d.ts.map +1 -0
  104. package/src/core/geom/vec4/v4_equals_array.js +16 -0
  105. package/src/core/geom/vec4/v4_hash.d.ts +12 -0
  106. package/src/core/geom/vec4/v4_hash.d.ts.map +1 -0
  107. package/src/core/geom/vec4/v4_hash.js +18 -0
  108. package/src/core/geom/vec4/v4_set.d.ts +15 -0
  109. package/src/core/geom/vec4/v4_set.d.ts.map +1 -0
  110. package/src/core/geom/vec4/v4_set.js +19 -0
  111. package/src/core/graph/convert_graph_to_dot_string.d.ts +1 -1
  112. package/src/core/graph/convert_graph_to_dot_string.d.ts.map +1 -1
  113. package/src/core/graph/metis/native/bisection/split_graph_two_way.d.ts.map +1 -1
  114. package/src/core/graph/metis/native/metis_partition_kway.d.ts.map +1 -1
  115. package/src/core/graph/metis/native/refine/fm_kway.d.ts.map +1 -1
  116. package/src/core/json/abstractJSONSerializer.d.ts.map +1 -1
  117. package/src/core/lang/reactive/AbstractCachingParser.d.ts.map +1 -1
  118. package/src/core/localization/Localization.d.ts.map +1 -1
  119. package/src/core/math/linalg/cg/cg_solve.d.ts.map +1 -1
  120. package/src/core/math/noise/create_simplex_noise_2d.d.ts.map +1 -1
  121. package/src/core/math/physics/mie/compute_bhmie_optical_properties.d.ts.map +1 -1
  122. package/src/core/math/physics/mie/compute_mie_particle_properties_rgb.d.ts.map +1 -1
  123. package/src/core/math/random/seededRandom_Mulberry32.d.ts +1 -1
  124. package/src/core/math/random/seededRandom_Mulberry32.d.ts.map +1 -1
  125. package/src/core/model/ModuleRegistry.d.ts.map +1 -1
  126. package/src/core/model/node-graph/visual/layout/layout_assign_coordinates.d.ts.map +1 -1
  127. package/src/core/model/node-graph/visual/layout/layout_node_graph.d.ts.map +1 -1
  128. package/src/core/model/node-graph/visual/layout/layout_order_layers.d.ts.map +1 -1
  129. package/src/core/model/reactive/model/ReactiveExpression.d.ts.map +1 -1
  130. package/src/core/model/validate_enum_schema.d.ts.map +1 -1
  131. package/src/core/process/Future.d.ts.map +1 -1
  132. package/src/core/process/task/Task.d.ts.map +1 -1
  133. package/src/core/process/task/util/countTask.d.ts.map +1 -1
  134. package/src/core/process/task/util/randomCountTask.d.ts.map +1 -1
  135. package/src/core/process/worker/WorkerBuilder.d.ts.map +1 -1
  136. package/src/engine/Clock.d.ts +1 -1
  137. package/src/engine/Clock.d.ts.map +1 -1
  138. package/src/engine/EngineHarness.d.ts.map +1 -1
  139. package/src/engine/animation/Animations.d.ts.map +1 -1
  140. package/src/engine/animation/EntityAnimation.d.ts.map +1 -1
  141. package/src/engine/animation/TransitionFunctions.d.ts.map +1 -1
  142. package/src/engine/animation/Tween.d.ts.map +1 -1
  143. package/src/engine/animation/curve/editor/uploadViaElement.d.ts.map +1 -1
  144. package/src/engine/animation/curve/view/AnimationCurveView.d.ts +1 -1
  145. package/src/engine/animation/curve/view/AnimationCurveView.d.ts.map +1 -1
  146. package/src/engine/asset/AssetManager.d.ts.map +1 -1
  147. package/src/engine/asset/AssetRequest.d.ts.map +1 -1
  148. package/src/engine/asset/loaders/AssetLoader.d.ts.map +1 -1
  149. package/src/engine/asset/loaders/image/avif/encode_image_source.d.ts.map +1 -1
  150. package/src/engine/asset/loaders/image/avif/threaded_image_encoder.d.ts.map +1 -1
  151. package/src/engine/browserInfo.d.ts.map +1 -1
  152. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +1 -1
  153. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  154. package/src/engine/control/first-person/abilities/LedgeGrab.js +9 -9
  155. package/src/engine/control/first-person/abilities/Mantle.js +8 -8
  156. package/src/engine/control/first-person/abilities/ScrambleUp.js +2 -2
  157. package/src/engine/control/first-person/abilities/WallJump.js +2 -2
  158. package/src/engine/control/first-person/abilities/WallRun.js +3 -3
  159. package/src/engine/control/first-person/collision/KinematicMover.d.ts +1 -1
  160. package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
  161. package/src/engine/control/first-person/collision/KinematicMover.js +19 -46
  162. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts +17 -5
  163. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts.map +1 -1
  164. package/src/engine/control/first-person/sensors/FirstPersonSensors.js +20 -7
  165. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts.map +1 -1
  166. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js +9 -8
  167. package/src/engine/control/first-person/test/buildTestPlayer.d.ts.map +1 -1
  168. package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
  169. package/src/engine/ecs/EntityManager.d.ts.map +1 -1
  170. package/src/engine/ecs/EntityObserver.d.ts.map +1 -1
  171. package/src/engine/ecs/System.d.ts.map +1 -1
  172. package/src/engine/ecs/animation/Animation.d.ts +1 -1
  173. package/src/engine/ecs/animation/Animation.d.ts.map +1 -1
  174. package/src/engine/ecs/async/SystemWorkerLoopback.d.ts +3 -3
  175. package/src/engine/ecs/async/SystemWorkerLoopback.d.ts.map +1 -1
  176. package/src/engine/ecs/dynamic_actions/DynamicActorSystem.d.ts.map +1 -1
  177. package/src/engine/ecs/grid/pick.d.ts.map +1 -1
  178. package/src/engine/ecs/guid/UUID.d.ts +1 -1
  179. package/src/engine/ecs/guid/UUID.d.ts.map +1 -1
  180. package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.d.ts.map +1 -1
  181. package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.js +26 -9
  182. package/src/engine/ecs/parent/EntityNode.d.ts +1 -1
  183. package/src/engine/ecs/parent/EntityNode.d.ts.map +1 -1
  184. package/src/engine/ecs/parent/FaceEditor.d.ts +7 -7
  185. package/src/engine/ecs/parent/FaceEditor.d.ts.map +1 -1
  186. package/src/engine/ecs/storage/binary/collection/BinaryCollectionDeSerializer.d.ts.map +1 -1
  187. package/src/engine/ecs/storage/binary/object/BinaryObjectSerializationAdapter2.d.ts +2 -2
  188. package/src/engine/ecs/storage/binary/object/BinaryObjectSerializationAdapter2.d.ts.map +1 -1
  189. package/src/engine/ecs/terrain/TerrainClouds.d.ts +1 -1
  190. package/src/engine/ecs/terrain/ecs/Terrain.d.ts +1 -1
  191. package/src/engine/ecs/terrain/ecs/Terrain.d.ts.map +1 -1
  192. package/src/engine/ecs/terrain/ecs/TerrainSystem.d.ts.map +1 -1
  193. package/src/engine/ecs/terrain/overlay/TerrainOverlay.d.ts +1 -1
  194. package/src/engine/ecs/terrain/overlay/TerrainOverlay.d.ts.map +1 -1
  195. package/src/engine/ecs/terrain/tiles/TerrainTile.d.ts +1 -1
  196. package/src/engine/ecs/terrain/tiles/TerrainTile.d.ts.map +1 -1
  197. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +1 -1
  198. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
  199. package/src/engine/ecs/terrain/util/obtainTerrain.d.ts.map +1 -1
  200. package/src/engine/ecs/transform/Transform64.d.ts +6 -6
  201. package/src/engine/ecs/transform/Transform64.d.ts.map +1 -1
  202. package/src/engine/ecs/transform-attachment/TransformAttachment.d.ts +1 -1
  203. package/src/engine/ecs/transform-attachment/TransformAttachment.d.ts.map +1 -1
  204. package/src/engine/graphics/FrameRunner.d.ts.map +1 -1
  205. package/src/engine/graphics/ecs/camera/camera_traverse_active.d.ts.map +1 -1
  206. package/src/engine/graphics/ecs/camera/pp/PerfectPanner.d.ts +1 -1
  207. package/src/engine/graphics/ecs/camera/pp/PerfectPanner.d.ts.map +1 -1
  208. package/src/engine/graphics/ecs/highlight/HighlightDefinition.d.ts +1 -1
  209. package/src/engine/graphics/ecs/highlight/HighlightDefinition.d.ts.map +1 -1
  210. package/src/engine/graphics/ecs/make_bvh_depth_range_computer.d.ts.map +1 -1
  211. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMesh.d.ts +4 -4
  212. package/src/engine/graphics/ecs/mesh-v2/aggregate/SGMesh.d.ts.map +1 -1
  213. package/src/engine/graphics/ecs/path/tube/TubePathStyle.d.ts +1 -1
  214. package/src/engine/graphics/ecs/path/tube/TubePathStyle.d.ts.map +1 -1
  215. package/src/engine/graphics/ecs/path/tube/build/computeFrenetFrames.d.ts.map +1 -1
  216. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +3 -3
  217. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
  218. package/src/engine/graphics/geometry/buffered/build_height_field_geometry.d.ts +1 -1
  219. package/src/engine/graphics/geometry/buffered/build_height_field_geometry.d.ts.map +1 -1
  220. package/src/engine/graphics/geometry/buffered/query/GeometrySpatialQueryAccelerator.d.ts +1 -1
  221. package/src/engine/graphics/geometry/buffered/query/GeometrySpatialQueryAccelerator.d.ts.map +1 -1
  222. package/src/engine/graphics/make_ray_from_viewport_position.d.ts.map +1 -1
  223. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.d.ts.map +1 -1
  224. package/src/engine/graphics/particles/particular/engine/emitter/computeEmissionFunction.d.ts.map +1 -1
  225. package/src/engine/graphics/particles/particular/engine/utils/distribute_points_on_indexed_triangles.d.ts.map +1 -1
  226. package/src/engine/graphics/particles/particular/engine/utils/distrubuteParticlesOnMesh.d.ts.map +1 -1
  227. package/src/engine/graphics/particles/particular/engine/utils/volume/AttributeValue.d.ts +3 -3
  228. package/src/engine/graphics/particles/particular/engine/utils/volume/AttributeValue.d.ts.map +1 -1
  229. package/src/engine/graphics/render/frame_graph/FrameGraph.d.ts.map +1 -1
  230. package/src/engine/graphics/render/gizmo/GizmoShapeRenderingInterface.d.ts +1 -1
  231. package/src/engine/graphics/render/gizmo/GizmoShapeRenderingInterface.d.ts.map +1 -1
  232. package/src/engine/graphics/render/layers/RenderLayer.d.ts +3 -3
  233. package/src/engine/graphics/render/layers/RenderLayer.d.ts.map +1 -1
  234. package/src/engine/graphics/render/visibility/IncrementalDeltaSet.d.ts +1 -1
  235. package/src/engine/graphics/render/visibility/IncrementalDeltaSet.d.ts.map +1 -1
  236. package/src/engine/graphics/render/visibility/VisibilityComputer.d.ts +2 -2
  237. package/src/engine/graphics/render/visibility/VisibilityComputer.d.ts.map +1 -1
  238. package/src/engine/graphics/sh3/path_tracer/PathTracedMesh.d.ts +1 -1
  239. package/src/engine/graphics/sh3/path_tracer/PathTracedMesh.d.ts.map +1 -1
  240. package/src/engine/graphics/sh3/path_tracer/populate_path_traced_scene_from_ecd.d.ts.map +1 -1
  241. package/src/engine/graphics/sh3/path_tracer/populate_path_traced_scene_from_ecd.js +5 -1
  242. package/src/engine/graphics/texture/3d/SingleChannelSampler3D.d.ts +1 -1
  243. package/src/engine/graphics/texture/3d/SingleChannelSampler3D.d.ts.map +1 -1
  244. package/src/engine/graphics/texture/sampler/Sampler2D.d.ts.map +1 -1
  245. package/src/engine/graphics/texture/sampler/filter/sampler2d_scale_down_generic.d.ts +1 -1
  246. package/src/engine/graphics/texture/sampler/filter/sampler2d_scale_down_generic.d.ts.map +1 -1
  247. package/src/engine/graphics/texture/sampler/sampler2d_channel_compute_max.d.ts +1 -1
  248. package/src/engine/graphics/texture/sampler/sampler2d_channel_compute_max.d.ts.map +1 -1
  249. package/src/engine/graphics/texture/sampler/sampler2d_channel_compute_min.d.ts +1 -1
  250. package/src/engine/graphics/texture/sampler/sampler2d_channel_compute_min.d.ts.map +1 -1
  251. package/src/engine/graphics/texture/sampler/sampler2d_combine.d.ts.map +1 -1
  252. package/src/engine/graphics/texture/sampler/sampler2d_compute_texel_value_conversion_scale_to_uint8.d.ts.map +1 -1
  253. package/src/engine/graphics/texture/sampler/search/make_edge_condition_channel_threshold.d.ts.map +1 -1
  254. package/src/engine/graphics/texture/sampler/search/sampler2d_find_pixels.d.ts.map +1 -1
  255. package/src/engine/graphics/texture/virtual/VirtualTextureTileLoader.d.ts +3 -3
  256. package/src/engine/graphics/texture/virtual/VirtualTextureTileLoader.d.ts.map +1 -1
  257. package/src/engine/graphics/texture/virtual/VirtualTextureUsage.d.ts +1 -1
  258. package/src/engine/graphics/texture/virtual/VirtualTextureUsage.d.ts.map +1 -1
  259. package/src/engine/graphics/texture/virtual/debug/UsageDebugView.d.ts +2 -2
  260. package/src/engine/graphics/texture/virtual/debug/UsageDebugView.d.ts.map +1 -1
  261. package/src/engine/graphics/texture/virtual/debug/UsagePyramidDebugView.d.ts +1 -1
  262. package/src/engine/graphics/texture/virtual/debug/UsagePyramidDebugView.d.ts.map +1 -1
  263. package/src/engine/graphics/texture/virtual/tile/decompose_finger_print.d.ts.map +1 -1
  264. package/src/engine/graphics3/HighlightOutlineSystem.d.ts.map +1 -1
  265. package/src/engine/graphics3/ParticipatingMedia.d.ts +1 -1
  266. package/src/engine/graphics3/ParticipatingMedia.d.ts.map +1 -1
  267. package/src/engine/graphics3/VolumetricLightMap.d.ts +1 -1
  268. package/src/engine/graphics3/VolumetricLightMap.d.ts.map +1 -1
  269. package/src/engine/graphics3/decal/graph_build_decal_clusters.d.ts.map +1 -1
  270. package/src/engine/graphics3/highlight/pack_highlight_table.d.ts.map +1 -1
  271. package/src/engine/graphics3/preview/make_model_preview_scene.d.ts.map +1 -1
  272. package/src/engine/graphics3/terrain/GPUTerrainSplatRenderer.d.ts.map +1 -1
  273. package/src/engine/graphics3/terrain/pack_terrain_row_table.d.ts.map +1 -1
  274. package/src/engine/grid/obstacle/GridObstacle.d.ts.map +1 -1
  275. package/src/engine/input/devices/GamepadDevice.d.ts +1 -1
  276. package/src/engine/input/devices/GamepadDevice.d.ts.map +1 -1
  277. package/src/engine/input/devices/PointerDevice.d.ts +1 -1
  278. package/src/engine/input/devices/PointerDevice.d.ts.map +1 -1
  279. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
  280. package/src/engine/input/ecs/controllers/KeyboardCameraController.js +11 -2
  281. package/src/engine/intelligence/behavior/ecs/BehaviorComponent.d.ts +1 -1
  282. package/src/engine/intelligence/behavior/ecs/BehaviorComponent.d.ts.map +1 -1
  283. package/src/engine/intelligence/behavior/ecs/SendEventBehavior.d.ts.map +1 -1
  284. package/src/engine/intelligence/behavior/primitive/ActionBehavior.d.ts.map +1 -1
  285. package/src/engine/intelligence/behavior/util/RotationBehavior.d.ts.map +1 -1
  286. package/src/engine/intelligence/behavior/util/RotationBehavior.js +14 -2
  287. package/src/engine/intelligence/blackboard/Blackboard.d.ts.map +1 -1
  288. package/src/engine/intelligence/mcts/MonteCarlo.d.ts +1 -1
  289. package/src/engine/intelligence/mcts/MonteCarlo.d.ts.map +1 -1
  290. package/src/engine/intelligence/mcts/StateNode.d.ts.map +1 -1
  291. package/src/engine/intelligence/optimization/RandomOptimizer.d.ts +1 -1
  292. package/src/engine/intelligence/optimization/RandomOptimizer.d.ts.map +1 -1
  293. package/src/engine/knowledge/database/StaticKnowledgeDataTable.d.ts.map +1 -1
  294. package/src/engine/knowledge/database/StaticKnowledgeDatabase.d.ts +1 -1
  295. package/src/engine/knowledge/database/StaticKnowledgeDatabase.d.ts.map +1 -1
  296. package/src/engine/navigation/ecs/path_following/PathFollower.d.ts +2 -2
  297. package/src/engine/navigation/ecs/path_following/PathFollower.d.ts.map +1 -1
  298. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  299. package/src/engine/network/PriorityFetch.d.ts +2 -2
  300. package/src/engine/network/PriorityFetch.d.ts.map +1 -1
  301. package/src/engine/network/diagnostics/BandwidthMeter.d.ts.map +1 -1
  302. package/src/engine/network/ecs/owner_authorization.d.ts.map +1 -1
  303. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  304. package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
  305. package/src/engine/network/replication/Replicator.d.ts +1 -1
  306. package/src/engine/network/replication/Replicator.d.ts.map +1 -1
  307. package/src/engine/network/sim/SimActionExecutor.d.ts +1 -1
  308. package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
  309. package/src/engine/network/time/RenderPlayout.d.ts.map +1 -1
  310. package/src/engine/network/transport/Channel.d.ts.map +1 -1
  311. package/src/engine/network/transport/fragments/FragmentRetention.d.ts.map +1 -1
  312. package/src/engine/physics/body/SolverBodyState.d.ts +11 -11
  313. package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -1
  314. package/src/engine/physics/body/SolverBodyState.js +13 -13
  315. package/src/engine/physics/broadphase/compute_fat_world_aabb.d.ts +4 -4
  316. package/src/engine/physics/broadphase/compute_fat_world_aabb.d.ts.map +1 -1
  317. package/src/engine/physics/broadphase/compute_fat_world_aabb.js +2 -2
  318. package/src/engine/physics/broadphase/generate_pairs.d.ts +3 -3
  319. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  320. package/src/engine/physics/broadphase/generate_pairs.js +1 -1
  321. package/src/engine/physics/ccd/linear_sweep.d.ts +2 -3
  322. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
  323. package/src/engine/physics/ccd/linear_sweep.js +4 -5
  324. package/src/engine/physics/constraint/solve_constraints.js +3 -3
  325. package/src/engine/physics/ecs/BodyKind.d.ts +1 -1
  326. package/src/engine/physics/ecs/BodyKind.js +1 -1
  327. package/src/engine/physics/ecs/Collider.d.ts +1 -1
  328. package/src/engine/physics/ecs/Collider.js +1 -1
  329. package/src/engine/physics/ecs/Joint.d.ts +15 -10
  330. package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
  331. package/src/engine/physics/ecs/Joint.js +27 -18
  332. package/src/engine/physics/ecs/JointSerializationAdapter.d.ts.map +1 -1
  333. package/src/engine/physics/ecs/JointSerializationAdapter.js +8 -6
  334. package/src/engine/physics/ecs/PhysicsSystem.d.ts +40 -77
  335. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  336. package/src/engine/physics/ecs/PhysicsSystem.js +113 -75
  337. package/src/engine/physics/ecs/RigidBody.d.ts +17 -12
  338. package/src/engine/physics/ecs/RigidBody.d.ts.map +1 -1
  339. package/src/engine/physics/ecs/RigidBody.js +58 -24
  340. package/src/engine/physics/ecs/RigidBodySerializationAdapter.d.ts.map +1 -1
  341. package/src/engine/physics/ecs/RigidBodySerializationAdapter.js +13 -12
  342. package/src/engine/physics/ecs/find_non_finite_physics_state.d.ts +1 -1
  343. package/src/engine/physics/ecs/find_non_finite_physics_state.js +1 -1
  344. package/src/engine/physics/fluid/FluidField.d.ts.map +1 -1
  345. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +4 -4
  346. package/src/engine/physics/fluid/SliceVisualiser.d.ts.map +1 -1
  347. package/src/engine/physics/fluid/ecs/FluidComponent.d.ts +10 -10
  348. package/src/engine/physics/fluid/ecs/FluidComponent.js +10 -10
  349. package/src/engine/physics/fluid/ecs/FluidEffectorsComponent.d.ts +2 -2
  350. package/src/engine/physics/fluid/ecs/FluidEffectorsComponent.js +2 -2
  351. package/src/engine/physics/fluid/ecs/FluidObstacle.d.ts +3 -3
  352. package/src/engine/physics/fluid/ecs/FluidObstacle.js +3 -3
  353. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +1 -1
  354. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.js +4 -4
  355. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +2 -2
  356. package/src/engine/physics/fluid/ecs/FluidSystem.js +3 -3
  357. package/src/engine/physics/fluid/ecs/WorkerFluidSystem.d.ts +1 -1
  358. package/src/engine/physics/fluid/ecs/WorkerFluidSystem.js +2 -2
  359. package/src/engine/physics/fluid/ecs/fluid_reanchor_field.d.ts +3 -3
  360. package/src/engine/physics/fluid/ecs/fluid_reanchor_field.d.ts.map +1 -1
  361. package/src/engine/physics/fluid/ecs/fluid_reanchor_field.js +3 -3
  362. package/src/engine/physics/fluid/ecs/fluid_sync_effectors_from_transform.d.ts +3 -3
  363. package/src/engine/physics/fluid/ecs/fluid_sync_effectors_from_transform.d.ts.map +1 -1
  364. package/src/engine/physics/fluid/ecs/fluid_sync_effectors_from_transform.js +2 -2
  365. package/src/engine/physics/fluid/effector/AbstractFluidEffector.d.ts +3 -3
  366. package/src/engine/physics/fluid/effector/AbstractFluidEffector.d.ts.map +1 -1
  367. package/src/engine/physics/fluid/effector/AbstractFluidEffector.js +2 -2
  368. package/src/engine/physics/fluid/effector/ImpulseFluidEffector.js +2 -2
  369. package/src/engine/physics/fluid/effector/WakeFluidEffector.d.ts +4 -4
  370. package/src/engine/physics/fluid/effector/WakeFluidEffector.js +8 -8
  371. package/src/engine/physics/inertia/world_inverse_inertia.d.ts +8 -17
  372. package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
  373. package/src/engine/physics/inertia/world_inverse_inertia.js +11 -11
  374. package/src/engine/physics/integration/integrate_position.d.ts +6 -7
  375. package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
  376. package/src/engine/physics/integration/integrate_position.js +5 -6
  377. package/src/engine/physics/integration/integrate_velocity.d.ts +4 -4
  378. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
  379. package/src/engine/physics/integration/integrate_velocity.js +7 -7
  380. package/src/engine/physics/inverse_kinematics/fabrik/fabrik_solve.d.ts +4 -4
  381. package/src/engine/physics/inverse_kinematics/fabrik/fabrik_solve.d.ts.map +1 -1
  382. package/src/engine/physics/inverse_kinematics/fabrik/fabrik_solve.js +3 -3
  383. package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -1
  384. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts +1 -2
  385. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts.map +1 -1
  386. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.js +1 -2
  387. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +7 -6
  388. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  389. package/src/engine/physics/narrowphase/narrowphase_step.js +8 -8
  390. package/src/engine/physics/queries/PhysicsSurfacePoint.d.ts +10 -5
  391. package/src/engine/physics/queries/PhysicsSurfacePoint.d.ts.map +1 -1
  392. package/src/engine/physics/queries/PhysicsSurfacePoint.js +10 -6
  393. package/src/engine/physics/queries/overlap_shape.d.ts +5 -14
  394. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  395. package/src/engine/physics/queries/overlap_shape.js +4 -4
  396. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  397. package/src/engine/physics/queries/shape_cast.d.ts +9 -1
  398. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -1
  399. package/src/engine/physics/queries/shape_cast.js +2 -2
  400. package/src/engine/physics/solver/solve_contacts.d.ts +2 -2
  401. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  402. package/src/engine/physics/solver/solve_contacts.js +2 -2
  403. package/src/engine/physics/vehicle/RaycastVehicle.d.ts +4 -8
  404. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
  405. package/src/engine/physics/vehicle/RaycastVehicle.js +16 -12
  406. package/src/engine/save/Storage.d.ts.map +1 -1
  407. package/src/engine/sound/SoundEngine.d.ts +1 -1
  408. package/src/engine/sound/SoundEngine.d.ts.map +1 -1
  409. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts.map +1 -1
  410. package/src/engine/sound/ecs/audio/AudioEmitterSystem.js +27 -2
  411. package/src/engine/sound/simulation/AcousticSimulator.d.ts +2 -2
  412. package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
  413. package/src/engine/sound/simulation/core/VolumeField.d.ts +1 -1
  414. package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
  415. package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
  416. package/src/engine/sound/simulation/probe/probe_delaunay_edges.d.ts.map +1 -1
  417. package/src/engine/sound/simulation/probe/probe_densify_portals.d.ts.map +1 -1
  418. package/src/engine/sound/simulation/probe/probe_detect_portals.d.ts.map +1 -1
  419. package/src/engine/sound/simulation/probe/probe_place_sdf_cover.d.ts.map +1 -1
  420. package/src/engine/sound/simulation/probe/probe_refine_connectivity.d.ts.map +1 -1
  421. package/src/engine/sound/simulation/render/ProbeReverbRenderer.d.ts +1 -1
  422. package/src/engine/sound/simulation/render/ProbeReverbRenderer.d.ts.map +1 -1
  423. package/src/engine/sound/simulation/render/foaReverbImpulseResponse.d.ts.map +1 -1
  424. package/src/engine/sound/sopra/SopraEngine.d.ts.map +1 -1
  425. package/src/engine/sound/sopra/legacy/SoundEmitter.d.ts +3 -3
  426. package/src/engine/sound/sopra/legacy/SoundEmitter.d.ts.map +1 -1
  427. package/src/engine/sound/sopra/legacy/SoundTrack.d.ts +3 -3
  428. package/src/engine/sound/sopra/legacy/SoundTrack.d.ts.map +1 -1
  429. package/src/engine/sound/sopra/util/buildAttenuationCurve.d.ts.map +1 -1
  430. package/src/engine/ui/notification/NotificationManager.d.ts.map +1 -1
  431. package/src/engine/ui/notification/ViewEmitter.d.ts +1 -1
  432. package/src/engine/ui/notification/ViewEmitter.d.ts.map +1 -1
  433. package/src/engine/ui/tiles2d/TileGrid.d.ts.map +1 -1
  434. package/src/extractName.d.ts.map +1 -1
  435. package/src/format/image/avif/api/convert_to_rgba.d.ts.map +1 -1
  436. package/src/format/image/avif/av1/encode/encode_av1_still.d.ts.map +1 -1
  437. package/src/format/image/avif/heif/write_avif_file.d.ts +2 -2
  438. package/src/format/image/avif/heif/write_avif_file.d.ts.map +1 -1
  439. package/src/format/image/jpeg/JpegImage.d.ts.map +1 -1
  440. package/src/format/image/png/PNG.d.ts.map +1 -1
  441. package/src/format/image/png/chunk/png_chunk_decode_iTXt.d.ts.map +1 -1
  442. package/src/format/image/png/chunk/png_chunk_decode_zTXt.d.ts.map +1 -1
  443. package/src/format/scene/gltf/ext/GltfBufferViewExtensionSet.d.ts.map +1 -1
  444. package/src/format/scene/gltf/gltf_node_world_matrices.d.ts.map +1 -1
  445. package/src/format/scene/usd/unpack_usdz.d.ts.map +1 -1
  446. package/src/format/scene/usd/usd_triangulate.d.ts.map +1 -1
  447. package/src/format/texture/basis/astc_integer_sequence.d.ts.map +1 -1
  448. package/src/generation/grid/actions/ContinuousGridCellActionWriteObstacle.d.ts.map +1 -1
  449. package/src/generation/markers/actions/placement/MarkerNodeEntityProcessorClingToTerrain.d.ts.map +1 -1
  450. package/src/generation/markers/actions/placement/MarkerNodeEntityProcessorClingToTerrain.js +22 -3
  451. package/src/generation/markers/transform/MarkerNodeTransformerRecordPropertyClosure.d.ts.map +1 -1
  452. package/src/generation/theme/ThemeEngine.d.ts +2 -2
  453. package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
  454. package/src/shade/descriptor/ObjectDescriptorBase.d.ts +1 -1
  455. package/src/shade/descriptor/ObjectDescriptorBase.d.ts.map +1 -1
  456. package/src/shade/descriptor/texture/TextureDescriptor.d.ts +1 -1
  457. package/src/shade/device/ShadeGPUCommandContext.d.ts +1 -1
  458. package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
  459. package/src/shade/device/pass/ShadeGPUComputePassEncoder.d.ts +1 -1
  460. package/src/shade/device/pass/ShadeGPUComputePassEncoder.d.ts.map +1 -1
  461. package/src/shade/device/pass/ShadeGPURenderPassEncoder.d.ts +1 -1
  462. package/src/shade/device/pass/ShadeGPURenderPassEncoder.d.ts.map +1 -1
  463. package/src/shade/device/timing/GPUTimerArray.d.ts.map +1 -1
  464. package/src/shade/device/timing/GPUTimerStats.d.ts +1 -1
  465. package/src/shade/device/timing/GPUTimerStats.d.ts.map +1 -1
  466. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +894 -894
  467. package/src/shade/playground/basis_textures/texture_inventory.d.ts.map +1 -1
  468. package/src/shade/playground/bvh_repro/bvh_repro_device.d.ts.map +1 -1
  469. package/src/shade/playground/ground_seam/capture_scene_color.d.ts.map +1 -1
  470. package/src/shade/playground/ground_seam/measure_flicker.d.ts.map +1 -1
  471. package/src/shade/playground/particle_system/index.html +0 -1
  472. package/src/shade/playground/particle_system/main.js +1 -2
  473. package/src/shade/playground/particle_system/particle_prototype.d.ts.map +1 -1
  474. package/src/shade/playground/particle_system/particle_scene.d.ts +5 -6
  475. package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -1
  476. package/src/shade/playground/particle_system/particle_scene.js +6 -7
  477. package/src/shade/playground/skinned_blas_refit/sweep_views.js +2 -2
  478. package/src/shade/playground/skinned_blas_refit/verify_traversal_reachability.js +1 -1
  479. package/src/shade/playground/vgeo_viewer/README.md +123 -0
  480. package/src/shade/playground/vgeo_viewer/cut_geometry.d.ts +61 -0
  481. package/src/shade/playground/vgeo_viewer/cut_geometry.d.ts.map +1 -0
  482. package/src/shade/playground/vgeo_viewer/cut_geometry.js +497 -0
  483. package/src/shade/playground/vgeo_viewer/index.html +77 -0
  484. package/src/shade/playground/vgeo_viewer/main.d.ts +2 -0
  485. package/src/shade/playground/vgeo_viewer/main.d.ts.map +1 -0
  486. package/src/shade/playground/vgeo_viewer/main.js +1288 -0
  487. package/src/shade/playground/vgeo_viewer/sample_asset.d.ts +18 -0
  488. package/src/shade/playground/vgeo_viewer/sample_asset.d.ts.map +1 -0
  489. package/src/shade/playground/vgeo_viewer/sample_asset.js +117 -0
  490. package/src/shade/playground/vgeo_viewer/select_cut.d.ts +154 -0
  491. package/src/shade/playground/vgeo_viewer/select_cut.d.ts.map +1 -0
  492. package/src/shade/playground/vgeo_viewer/select_cut.js +437 -0
  493. package/src/shade/renderer/DynamicResolutionScaling.d.ts +1 -1
  494. package/src/shade/renderer/DynamicResolutionScaling.d.ts.map +1 -1
  495. package/src/shade/renderer/Renderer.d.ts +5 -5
  496. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  497. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  498. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +1 -1
  499. package/src/shade/renderer/camera/Camera.d.ts +3 -3
  500. package/src/shade/renderer/camera/Camera.d.ts.map +1 -1
  501. package/src/shade/renderer/camera/GPUCameraContext.d.ts +1 -1
  502. package/src/shade/renderer/camera/GPUCameraContext.d.ts.map +1 -1
  503. package/src/shade/renderer/camera/OrthographicCamera.d.ts +4 -4
  504. package/src/shade/renderer/camera/OrthographicCamera.d.ts.map +1 -1
  505. package/src/shade/renderer/camera/PerspectiveCamera.d.ts +2 -2
  506. package/src/shade/renderer/camera/PerspectiveCamera.d.ts.map +1 -1
  507. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +11 -3
  508. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  509. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +25 -5
  510. package/src/shade/renderer/extension/FrameContext.d.ts +1 -1
  511. package/src/shade/renderer/extension/FrameContext.d.ts.map +1 -1
  512. package/src/shade/renderer/extension/GBufferTextures.d.ts +4 -4
  513. package/src/shade/renderer/extension/GBufferTextures.d.ts.map +1 -1
  514. package/src/shade/renderer/extension/PresentTarget.d.ts +1 -1
  515. package/src/shade/renderer/extension/PresentTarget.d.ts.map +1 -1
  516. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +918 -918
  517. package/src/shade/renderer/extension/SceneColor.d.ts +1 -1
  518. package/src/shade/renderer/extension/SceneColor.d.ts.map +1 -1
  519. package/src/shade/renderer/extension/ViewTextures.d.ts +4 -4
  520. package/src/shade/renderer/extension/ViewTextures.d.ts.map +1 -1
  521. package/src/shade/renderer/geometry/Attribute.d.ts +1 -1
  522. package/src/shade/renderer/geometry/Attribute.d.ts.map +1 -1
  523. package/src/shade/renderer/geometry/Geometry.d.ts +1 -1
  524. package/src/shade/renderer/geometry/Geometry.d.ts.map +1 -1
  525. package/src/shade/renderer/geometry/bvh/bvh2_check_bounds.d.ts.map +1 -1
  526. package/src/shade/renderer/geometry/bvh/bvh2_derive_topology.d.ts.map +1 -1
  527. package/src/shade/renderer/geometry/meshlet/MeshletBatch.d.ts +3 -3
  528. package/src/shade/renderer/geometry/meshlet/MeshletBatch.d.ts.map +1 -1
  529. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_collect_geometries.d.ts.map +1 -1
  530. package/src/shade/renderer/geometry/virtual/format/attribute/VGEO_ATTRIBUTES.d.ts.map +1 -1
  531. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_read_frame_extent.d.ts +29 -0
  532. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_read_frame_extent.d.ts.map +1 -0
  533. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_read_frame_extent.js +59 -0
  534. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_scan_frames.d.ts +0 -1
  535. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_scan_frames.d.ts.map +1 -1
  536. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_scan_frames.js +2 -23
  537. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.d.ts +94 -0
  538. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.d.ts.map +1 -0
  539. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.js +170 -0
  540. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts +122 -0
  541. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts.map +1 -0
  542. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.js +143 -0
  543. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts +185 -0
  544. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts.map +1 -0
  545. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.js +572 -0
  546. package/src/shade/renderer/geometry/virtual/format/read/VGeoDirectory.d.ts +64 -0
  547. package/src/shade/renderer/geometry/virtual/format/read/VGeoDirectory.d.ts.map +1 -0
  548. package/src/shade/renderer/geometry/virtual/format/read/VGeoDirectory.js +80 -0
  549. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts +198 -0
  550. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts.map +1 -0
  551. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.js +232 -0
  552. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageWant.d.ts +51 -0
  553. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageWant.d.ts.map +1 -0
  554. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageWant.js +55 -0
  555. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.d.ts +68 -0
  556. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.d.ts.map +1 -0
  557. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.js +72 -0
  558. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.d.ts +28 -0
  559. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.d.ts.map +1 -0
  560. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.js +108 -0
  561. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_directory.d.ts +23 -0
  562. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_directory.d.ts.map +1 -0
  563. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_directory.js +83 -0
  564. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.d.ts +53 -0
  565. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.d.ts.map +1 -0
  566. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.js +211 -0
  567. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts +26 -0
  568. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -0
  569. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +271 -0
  570. package/src/shade/renderer/global_illumination/brick4/cpu/brick4_bake_for_scene.d.ts.map +1 -1
  571. package/src/shade/renderer/global_illumination/clipmap/GPUClipMap3D.d.ts +2 -2
  572. package/src/shade/renderer/global_illumination/clipmap/GPUClipMap3D.d.ts.map +1 -1
  573. package/src/shade/renderer/global_illumination/lpv/LightProbeVolume.d.ts +1 -1
  574. package/src/shade/renderer/global_illumination/lpv/LightProbeVolume.d.ts.map +1 -1
  575. package/src/shade/renderer/global_illumination/lpv/bake/GPULightProbeVolumeRenderer.d.ts +1 -1
  576. package/src/shade/renderer/global_illumination/lpv/bake/GPULightProbeVolumeRenderer.d.ts.map +1 -1
  577. package/src/shade/renderer/global_illumination/sharc/SpatialHashRadianceCache.d.ts +1 -1
  578. package/src/shade/renderer/global_illumination/sharc/SpatialHashRadianceCache.d.ts.map +1 -1
  579. package/src/shade/renderer/gpu_primitive/bvh/check_bvh_structure.d.ts.map +1 -1
  580. package/src/shade/renderer/light/LightCollection.d.ts +2 -2
  581. package/src/shade/renderer/light/LightCollection.d.ts.map +1 -1
  582. package/src/shade/renderer/light/model/PointLight.d.ts +1 -1
  583. package/src/shade/renderer/light/model/PointLight.d.ts.map +1 -1
  584. package/src/shade/renderer/light/model/SpotLight.d.ts +1 -1
  585. package/src/shade/renderer/light/model/SpotLight.d.ts.map +1 -1
  586. package/src/shade/renderer/loader/gltf/fix_up_material_sides.d.ts.map +1 -1
  587. package/src/shade/renderer/loader/gltf/gltf_create_material.d.ts +1 -1
  588. package/src/shade/renderer/loader/gltf/gltf_create_material.d.ts.map +1 -1
  589. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +2 -2
  590. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts.map +1 -1
  591. package/src/shade/renderer/loader/usd/usd_decode_image.d.ts +1 -1
  592. package/src/shade/renderer/loader/usd/usd_decode_image.d.ts.map +1 -1
  593. package/src/shade/renderer/material/util/material_convert_metallic_roughness_to_specular_glossiness.d.ts.map +1 -1
  594. package/src/shade/renderer/material/util/material_convert_specular_glossiness_to_metallic_roughness.d.ts.map +1 -1
  595. package/src/shade/renderer/particles/DESIGN.md +18 -7
  596. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +24 -10
  597. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  598. package/src/shade/renderer/particles/GPUParticleSystem.js +77 -72
  599. package/src/shade/renderer/particles/coherence/graph_particle_bucket.d.ts +17 -16
  600. package/src/shade/renderer/particles/coherence/graph_particle_bucket.d.ts.map +1 -1
  601. package/src/shade/renderer/particles/coherence/graph_particle_bucket.js +35 -22
  602. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.d.ts +6 -6
  603. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.d.ts.map +1 -1
  604. package/src/shade/renderer/particles/graph/ParticleNodeGroup.d.ts.map +1 -1
  605. package/src/shade/renderer/particles/graph/ParticleValueType.d.ts +1 -1
  606. package/src/shade/renderer/particles/graph/ParticleValueType.d.ts.map +1 -1
  607. package/src/shade/renderer/particles/graph/make_particle_group.d.ts.map +1 -1
  608. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -1
  609. package/src/shade/renderer/particles/graph/particle_group_json.d.ts.map +1 -1
  610. package/src/shade/renderer/particles/graph_particles.d.ts +11 -0
  611. package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
  612. package/src/shade/renderer/particles/graph_particles.js +55 -37
  613. package/src/shade/renderer/particles/graph_particles_billboard.d.ts.map +1 -1
  614. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts.map +1 -1
  615. package/src/shade/renderer/particles/isa/particle_assembly.d.ts.map +1 -1
  616. package/src/shade/renderer/particles/layout/ParticleLayout.d.ts.map +1 -1
  617. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lift.d.ts.map +1 -1
  618. package/src/shade/renderer/particles/optimizer/dag/particle_vector_pack.d.ts.map +1 -1
  619. package/src/shade/renderer/particles/optimizer/optimize_particle_program.d.ts +1 -1
  620. package/src/shade/renderer/particles/optimizer/optimize_particle_program.d.ts.map +1 -1
  621. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.d.ts.map +1 -1
  622. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
  623. package/src/shade/renderer/particles/sort/graph_particle_sort.d.ts +17 -13
  624. package/src/shade/renderer/particles/sort/graph_particle_sort.d.ts.map +1 -1
  625. package/src/shade/renderer/particles/sort/graph_particle_sort.js +43 -33
  626. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
  627. package/src/shade/renderer/postprocess/denoise/LigmaDenoiser.d.ts +1 -1
  628. package/src/shade/renderer/postprocess/denoise/LigmaDenoiser.d.ts.map +1 -1
  629. package/src/shade/renderer/postprocess/graph_pass_composit_red_as_rgba.d.ts.map +1 -1
  630. package/src/shade/renderer/postprocess/nss/NSS.d.ts +4 -4
  631. package/src/shade/renderer/postprocess/nss/NSS.d.ts.map +1 -1
  632. package/src/shade/renderer/postprocess/ssr/SSR.d.ts +3 -3
  633. package/src/shade/renderer/postprocess/ssr/SSR.d.ts.map +1 -1
  634. package/src/shade/renderer/postprocess/taa/TAA.d.ts +2 -2
  635. package/src/shade/renderer/postprocess/taa/TAA.d.ts.map +1 -1
  636. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  637. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts.map +1 -1
  638. package/src/shade/renderer/scene/InstanceBatch.d.ts +1 -1
  639. package/src/shade/renderer/scene/InstanceBatch.d.ts.map +1 -1
  640. package/src/shade/renderer/scene/Node3D.d.ts +3 -3
  641. package/src/shade/renderer/scene/Node3D.d.ts.map +1 -1
  642. package/src/shade/renderer/scene/bvh/StaticSceneBVH.d.ts.map +1 -1
  643. package/src/shade/renderer/shader/compiler/CodeChunk.d.ts.map +1 -1
  644. package/src/shade/renderer/shader/graph/graph_prepare_pass_input_data.d.ts.map +1 -1
  645. package/src/shade/renderer/shader/graph/graph_prepare_pass_input_data.js +11 -0
  646. package/src/shade/renderer/shader/type/WebGPUStruct.d.ts +1 -1
  647. package/src/shade/renderer/shader/type/WebGPUStruct.d.ts.map +1 -1
  648. package/src/shade/renderer/texture/GPUTextureContext.d.ts +1 -1
  649. package/src/shade/renderer/texture/GPUTextureContext.d.ts.map +1 -1
  650. package/src/shade/renderer/texture/format/sampler_descriptor_unpack.d.ts.map +1 -1
  651. package/src/shade/renderer/texture/source/ShadeImage.d.ts +7 -7
  652. package/src/shade/renderer/texture/source/ShadeImage.d.ts.map +1 -1
  653. package/src/shade/renderer/texture/virtual/VTPageTable.d.ts.map +1 -1
  654. package/src/shade/renderer/view/GPUViewContext.d.ts +2 -2
  655. package/src/shade/renderer/view/GPUViewContext.d.ts.map +1 -1
  656. package/src/shade/renderer/view/GPUViewSkyContext.d.ts +1 -1
  657. package/src/shade/renderer/view/GPUViewSkyContext.d.ts.map +1 -1
  658. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +2 -2
  659. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  660. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts +1 -1
  661. package/src/shade/renderer/volumetrics/ParticipatingMediaVolume.d.ts.map +1 -1
  662. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts.map +1 -1
  663. package/src/view/View.d.ts +1 -1
  664. package/src/view/View.d.ts.map +1 -1
  665. package/src/view/elements/button/ButtonView.d.ts +1 -1
  666. package/src/view/elements/button/ButtonView.d.ts.map +1 -1
  667. package/src/view/elements/progress/SmoothProgressBar.d.ts +2 -2
  668. package/src/view/elements/progress/SmoothProgressBar.d.ts.map +1 -1
  669. package/src/view/elements/radial/RadialMenuElement.d.ts +3 -3
  670. package/src/view/elements/radial/RadialMenuElement.d.ts.map +1 -1
  671. package/src/view/elements/windrose/WindRoseDiagram.d.ts.map +1 -1
  672. package/src/view/layout/DockLayoutView.d.ts.map +1 -1
  673. package/src/view/layout/DrawerView.d.ts +2 -2
  674. package/src/view/layout/DrawerView.d.ts.map +1 -1
  675. package/src/view/layout/SplitView.d.ts +9 -9
  676. package/src/view/layout/SplitView.d.ts.map +1 -1
  677. package/src/view/minimap/MinimapMarkerCollection.d.ts +3 -2
  678. package/src/view/minimap/MinimapMarkerCollection.d.ts.map +1 -1
  679. package/src/view/minimap/MinimapMarkerCollection.js +28 -5
  680. package/src/view/minimap/dom/MinimapMarkerView.js +65 -34
  681. package/src/view/minimap/gl/MarkerGL.d.ts +15 -3
  682. package/src/view/minimap/gl/MarkerGL.d.ts.map +1 -1
  683. package/src/view/minimap/gl/MarkerGL.js +31 -4
  684. package/src/engine/physics/BULLET_REVIEW.md +0 -945
  685. package/src/engine/physics/CANNON_REVIEW.md +0 -1300
  686. package/src/engine/physics/CONSTRAINT_SOLVER_BENCH_LOG.md +0 -208
  687. package/src/engine/physics/CONSTRAINT_SOLVER_IMPROVEMENTS_PLAN.md +0 -364
  688. package/src/engine/physics/INTEPOLATION_SYSTEM_PLAN.md +0 -287
  689. package/src/engine/physics/JOLT_REVIEW.md +0 -913
  690. package/src/engine/physics/PLAN.md +0 -1120
  691. package/src/engine/physics/RAPIER_REVIEW.md +0 -934
  692. package/src/engine/physics/REVIEW_001_ACTION_PLAN.md +0 -642
  693. package/src/engine/physics/REVIEW_002.md +0 -151
  694. package/src/engine/physics/REVIEW_003.md +0 -166
  695. package/src/shade/descriptor/util/optional_clone.d.ts +0 -7
  696. package/src/shade/descriptor/util/optional_clone.d.ts.map +0 -1
  697. package/src/shade/descriptor/util/optional_equality.d.ts +0 -10
  698. package/src/shade/descriptor/util/optional_equality.d.ts.map +0 -1
  699. package/src/shade/descriptor/util/optional_hash.d.ts +0 -9
  700. package/src/shade/descriptor/util/optional_hash.d.ts.map +0 -1
  701. package/src/shade/renderer/geometry/virtual/format/prototypeVGEOFormatViewer.js +0 -2063
@@ -1,894 +1,894 @@
1
- # A graphical GPU profiler for Shade — recorder, format, inspector
2
-
3
- Proposal, 2026-08-28. Decisions folded in 2026-08-29. Alex Goldring / Company Named Limited.
4
-
5
- > **Partly superseded, 2026-08-31**, by
6
- > [PASS_ENCODER_PROPOSAL_2026_08_31.md](../PASS_ENCODER_PROPOSAL_2026_08_31.md). The recording
7
- > proxy of §6.2 hook 3 and M4 is now a pair of decorator classes; `enable_debug_timers`,
8
- > `Renderer.add_debug_frame` and `Renderer.onFrameDebug` — which §6.1 kept — are gone, leaving the
9
- > profiler as the only path. §§1–5 remain a record of the state this design was written against.
10
-
11
- Scope: a **recording** side that ships with the engine, a **binary container** that carries a
12
- capture, and an **inspector** web application under `packages/gpu-inspector-tool/` that reads the
13
- container and nothing else.
14
-
15
- > The request spelled the directory `gpu-inespector-tool`. Reading that as a typo and using
16
- > `packages/gpu-inspector-tool/` throughout. Say the word if the misspelling was deliberate.
17
-
18
- **Four decisions are settled and written into the design below** — npm workspaces with the existing
19
- tree moved to conform (§4.2), a user-instantiated recorder passed in rather than a renderer-owned one
20
- (§6.1), the minimal `FrameGraph` bracket via `Signal` (§1.4), and an open-ended uncapped stream of
21
- self-contained frame records rather than a ring buffer (§5.8). §11 records them and what each costs.
22
-
23
- ---
24
-
25
- ## 0. Verdict
26
-
27
- **Worth building, and the engine is further along than it looks — but not in the place you would
28
- expect.**
29
-
30
- The timing half is the *small* half. `GPUTimerArray` already lands begin/end timestamps per GPU
31
- pass, and `SoftwareGPUDevice` already emulates timestamp queries well enough to test the recorder in
32
- node without a GPU. What is genuinely missing is **attribution**: the timestamps are keyed by
33
- `GPUComputePassDescriptor.label`, the dependency structure lives in `FrameGraph`, and *nothing
34
- connects the two*. One frame-graph pass can open several GPU passes with unrelated labels
35
- (`graph_build_hzb` → `hzb.build()` → its own `constructComputePass` labels), so a label is not a key.
36
- Closing that gap is the central piece of engineering in this proposal, and it is about thirty lines
37
- in `FrameGraph.execute` plus a field on the command context.
38
-
39
- The dependency half is nearly free. `FrameGraph.exportToJson()`
40
- ([FrameGraph.js:644](src/engine/graphics/render/frame_graph/FrameGraph.js:644)) already emits passes,
41
- resource nodes, versions, readers, writers, producers and cull state, keyed consistently. Resource
42
- *sizes* fall out of the descriptors that graph already holds. Nesting for a flame-graph view falls
43
- out of `FrameGraphScope`.
44
-
45
- The work-size half needs a wrapper that does not exist: `beginComputePass` hands back the raw
46
- `GPUComputePassEncoder`, so `dispatchWorkgroups(x, y, z)` is invisible to us. A recording-only proxy
47
- around the pass encoder buys every dispatch and draw count for one class and no call-site churn.
48
-
49
- **The single biggest risk is not ours.** Chrome quantizes WebGPU timestamps to **100 µs** unless the
50
- user sets `chrome://flags/#enable-webgpu-developer-features`. Most Shade passes run well under that,
51
- so on a default browser almost every pass measures as exactly 0 or exactly 100 µs. A profiler that
52
- does not say this out loud, in the recording *and* in the inspector, is a profiler that lies. §2.1.
53
-
54
- ---
55
-
56
- ## 1. What exists today
57
-
58
- ### 1.1 The timing path, end to end
59
-
60
- | Piece | Where | What it does |
61
- |---|---|---|
62
- | `GPUTimerArray` | [GPUTimerArray.js](src/shade/device/timing/GPUTimerArray.js) | One `GPUQuerySet` of `size * 2` timestamps. `getComputeWrites(label)` / `getRenderWrites(label)` claim a slot from an `IdPool` and return the `timestampWrites` struct. Resolve + copy to a `MAP_READ` buffer, `download_results()` maps it, `results_to_console_table()` turns slots into `{label, type, duration_ms, start, end}`. |
63
- | `ShadeGPUCommandContext.enable_debug_timers(cb)` | [ShadeGPUCommandContext.js:89](src/shade/device/ShadeGPUCommandContext.js:89) | Constructs a `GPUTimerArray` for this context. `beginComputePass` / `beginRenderPass` then inject `timestampWrites` into every descriptor. |
64
- | `finish()` | [ShadeGPUCommandContext.js:699](src/shade/device/ShadeGPUCommandContext.js:699) | Resolves the query set into the command buffer before `encoder.finish()`, then after submit downloads results and calls the callback, then destroys the timer array. |
65
- | `Renderer.add_debug_frame(count)` | [Renderer.js:626](src/shade/renderer/Renderer.js:626) | Arms N future frames. |
66
- | `Renderer.onFrameDebug` | [Renderer.js:408](src/shade/renderer/Renderer.js:408) | `send2(frame_index, table)`. **Currently has no subscribers anywhere in the tree.** |
67
- | `GPUTimerStats`, `format_nanosecond_time` | `src/shade/device/timing/` | Ring-buffer averaging and human formatting. Reusable in the inspector. |
68
-
69
- Degradation is already correct: with `timestamp-query` withheld, the query set is never created and
70
- `resolve` / `download_results` / `destroy` all no-op rather than throwing. Keep that property.
71
-
72
- ### 1.2 Four defects the recorder must fix, not inherit
73
-
74
- 1. **No slot bound check.** `GPUTimerArray.#bind_slot` takes an id from the `IdPool` and never
75
- compares it to `#size`. Pass 1025 in a frame (default `size = 1024`) writes a query index past
76
- `querySet.count` and takes a WebGPU validation error, at the `beginRenderPass` rather than at the
77
- place that overflowed. A recording session runs *more* passes instrumented than a debug frame
78
- does, so this is on the path. Clamp and count drops; report the drop count in the recording.
79
- 2. **Per-context GPU allocation.** `enable_debug_timers` builds a query set and two buffers per
80
- context and destroys them after the download. Fine for one frame; recording 600 frames means 600
81
- query-set create/destroy pairs and 600 `mapAsync` round trips. Wants a small pool of timer arrays
82
- cycled N-deep against frames in flight.
83
- 3. **Descriptor mutation.** `beginComputePass` does `let _descriptor = descriptor;` and then writes
84
- `_descriptor.timestampWrites`, mutating the caller's object. Several call sites reuse descriptor
85
- objects. Copy, or document the mutation.
86
- 4. **`beginComputePass()` with no descriptor throws** once timers are on, at
87
- `timers.getComputeWrites(descriptor.label)`. Currently latent because every call site passes one.
88
-
89
- ### 1.3 The structure half
90
-
91
- `FrameGraph.exportToJson()` is most of the dependency model already:
92
-
93
- ```js
94
- {
95
- passes: [{ id, name, culled, reads: [node_id], writes: [node_id] }],
96
- resources: [{ id, name, transient, version?, description?, createdBy?, readers?, writers? }]
97
- }
98
- ```
99
-
100
- Keyed by **resource node** (per version), not by registry entry — deliberately, so
101
- `passes[].reads/writes` stay resolvable across versions. The recording format must preserve that
102
- choice; a registry-keyed export dangles every reference past version 0.
103
-
104
- What it does *not* carry, and we want:
105
-
106
- - `FrameGraphNode.scope` — the `FrameGraphScope` chain, which is the hierarchy a flame graph needs.
107
- - `ref_count` and `has_side_effects` — why a pass survived culling.
108
- - Typed descriptors. `description` is `resource_descriptor.toString()`, a human string
109
- (`"Buffer{ size = 1,048,576, usage = STORAGE | COPY_DST }"`). We want the fields.
110
-
111
- ### 1.4 The gap: labels are not keys
112
-
113
- ```js
114
- // graph_build_hzb.js
115
- const builder = graph.add("hzb/ build", data, (data, resources, context) => {
116
- hzb.build(context.encoder, source, viewport); // opens N compute passes, its own labels
117
- });
118
- ```
119
-
120
- `GPUTimerArray` records the labels `hzb.build` chose. `FrameGraph` records `"hzb/ build"`. Nothing
121
- relates them, and the relation is not derivable after the fact — labels are not unique, not stable,
122
- and one graph pass legitimately produces many GPU passes.
123
-
124
- **Fix:** bracket `node.execute(...)` inside `FrameGraph.execute`
125
- ([FrameGraph.js:604](src/engine/graphics/render/frame_graph/FrameGraph.js:604)) so the context knows
126
- which graph pass is open. Every GPU pass begun while that bracket is open is attributed to it. The
127
- same bracket should emit `pushDebugGroup(node.name)` / `popDebugGroup()`, which costs nothing and
128
- immediately improves what RenderDoc, PIX and webgpu-inspector show — worth doing on its own merits.
129
-
130
- ### 1.5 Work sizes are not observable today
131
-
132
- `beginComputePass` / `constructComputePass` return the **raw** `GPUComputePassEncoder`. All 11
133
- production `dispatchWorkgroups` call sites talk to it directly, as does every draw call across the
134
- 31 render-pass construction sites. Editing every one of them to report its own counts is the wrong
135
- trade. A proxy encoder, installed only while recording, is one class.
136
-
137
- `SoftwareGPUComputePassEncoder` already models exactly this: it records
138
- `{pipeline, bind_groups, dynamic_offsets, group_counts}` per dispatch. The recording proxy is the
139
- same shape with a real encoder behind it.
140
-
141
- ---
142
-
143
- ## 2. What the platform will give us
144
-
145
- ### 2.1 Timestamp quantization — the headline constraint
146
-
147
- Chrome quantizes `timestamp-query` results to **100 µs** as a timing-attack mitigation. It is
148
- disabled by `chrome://flags/#enable-webgpu-developer-features`; that flag does not itself enable the
149
- feature, which additionally needs the device to expose `timestamp-query`.
150
-
151
- 100 µs is 0.1 ms. A Shade frame at 60 Hz has ~16.6 ms of budget spread over a couple of hundred
152
- passes. **The median pass is below the quantum.** Unmitigated, the tool reports a histogram of zeros
153
- with occasional 100 µs spikes, and every conclusion drawn from it is noise.
154
-
155
- **The decision is to leave it entirely alone.** Not measure it, not calibrate against it, not
156
- correct for it, not store a period in the container. Rejected 2026-08-29, and the reasoning is
157
- worth keeping because it is not obvious:
158
-
159
- - Recovering the quantum — GCD over observed timestamps, or any equivalent — **is the timing attack
160
- the mitigation exists to prevent.** Doing it *well* means defeating a browser security control on
161
- purpose, inside code we ship to other people.
162
- - Doing it *badly* is worse than not doing it. A period that is a multiple of the real one, or a
163
- stale one from a browser update, becomes a correction applied to every number in the capture. A
164
- profiler that silently skews its own data is worse than one that reports coarse data honestly.
165
- - Either way it is standing complexity and a source of fragility, bought for something the reader
166
- can be told in one sentence.
167
-
168
- So: **note it, document it, do not treat it.** The capture records what the device reported —
169
- zeros included. `GPUProfileMeta` documents that browsers quantize and names the flag. The inspector
170
- says so where a reader will see it. What to make of a coarse capture is the reader's judgement, and
171
- the honest advice is the same either way: **aggregate across frames rather than trusting any single
172
- one.** That shapes the format in exactly one way, which it already did — keep every frame's raw
173
- values and never pre-aggregate on the recorder side.
174
-
175
- ### 2.2 Pass granularity is the floor
176
-
177
- WebGPU writes timestamps at pass boundaries only. Per-draw timing needs
178
- `chromium-experimental-timestamp-query-inside-passes`, which is Chromium-only and experimental.
179
-
180
- **Design for pass granularity.** Treat inside-passes as an optional capability the recorder probes
181
- for and, if present, uses to add sub-pass markers — recorded as a distinct span kind so the format
182
- does not pretend the two are the same measurement.
183
-
184
- ### 2.3 No CPU↔GPU clock sync
185
-
186
- WebGPU exposes no calibration between `performance.now()` and the GPU timestamp domain. Anything
187
- claiming to place CPU and GPU events on one axis is guessing.
188
-
189
- **Be honest in the format.** Record two clocks explicitly: a CPU track from `performance.now()`
190
- (encode time, submit time, callback time) and a GPU track from timestamps. The inspector shows two
191
- tracks anchored per frame at submit, and says the alignment is nominal. Perfetto solves this problem
192
- with explicit `ClockSnapshot` packets and a clock graph; we do not have the snapshots, so we do not
193
- get to claim the sync.
194
-
195
- ### 2.4 Cross-query-set comparability
196
-
197
- Timestamps from two different `GPUQuerySet`s are not specified to share a domain. In Dawn they do in
198
- practice. Since each `ShadeGPUCommandContext` currently gets its own `GPUTimerArray`, and a frame has
199
- several contexts, **every frame already spans several query sets.**
200
-
201
- Record a `query_set_id` per span. Then the inspector can, if a capture ever looks wrong, colour
202
- spans by origin and let the reader see whether the anomaly follows a set boundary. Cheap; makes an
203
- otherwise unfalsifiable class of bug visible.
204
-
205
- ### 2.5 Indirect work is unknowable at encode time
206
-
207
- `dispatchWorkgroupsIndirect` and `drawIndirect` take their counts from a GPU buffer. At encode time
208
- we know only *that* it was indirect. Two options, both worth having:
209
-
210
- - **v1:** record the indirect flag plus the source buffer's id and offset. The inspector shows
211
- "indirect (unknown)" and links to the buffer.
212
- - **v2 (opt-in, costly):** the recorder copies indirect argument buffers into a readback staging
213
- buffer at encode time and resolves the actual counts after submit. This is the only way to see the
214
- real post-cull draw count, which for a GPU-driven renderer is one of the numbers most worth seeing.
215
- It changes memory traffic, so it must be a flag, and the format must mark such counts as
216
- *resolved* rather than *encoded* so nobody compares the two carelessly.
217
-
218
- ### 2.6 Prior art
219
-
220
- | Tool | What to take | What to leave |
221
- |---|---|---|
222
- | [webgpu_inspector](https://github.com/brendan-duncan/webgpu_inspector) (Brendan Duncan) | The reference point for WebGPU frame capture in a browser. Object inspection with creation stacktraces; frame-time plotting; buffer content view. Its recorder emits a standalone replayable HTML file. | It is a *generic API* interceptor, extension-hosted. We want *engine-semantic* data — frame graph passes, resource lifetimes, cull decisions — which a generic interceptor cannot see. Complementary, not competing. |
223
- | [Perfetto](https://perfetto.dev/docs/getting-started/other-formats) | Track/slice model; nested slices; counter tracks; explicit clock domains. Its JSON importer is a free escape hatch — see §5.6. | The protobuf format and the full trace-processor stack are far more than we need, and its UI knows nothing about resource dependency. |
224
- | RenderDoc / PIX / Radeon GPU Profiler | The three-pane idiom: event list, timeline, per-event detail; a resource view keyed by lifetime; "why is this bound" attribution. | Native capture, replay, driver counters. Out of reach on WebGPU. |
225
- | Chrome Trace Event Format | Trivially writable, universally readable. | Text JSON at our event rates is 5–10× our binary size. Export target, not storage. |
226
- | [webgpufundamentals timing](https://webgpufundamentals.org/webgpu/lessons/webgpu-timing.html) | The canonical treatment of the query-set/resolve/map dance and its pitfalls. | — |
227
-
228
- The gap in that table is the whole reason to build this: **no existing tool knows what a Shade frame
229
- graph is.** Timings without the dependency structure tell you a pass is slow; timings *with* it tell
230
- you a pass is slow because it waits on a resource that a culled branch still produced.
231
-
232
- ---
233
-
234
- ## 3. What a recording must contain
235
-
236
- Three axes were asked for. A fourth is needed to make the first three legible.
237
-
238
- ### 3.1 Timings
239
-
240
- | Datum | Source | Cost |
241
- |---|---|---|
242
- | GPU pass begin/end, ns | `GPUTimerArray` | free, already there |
243
- | Which query set a span came from | recorder | 1 byte |
244
- | CPU encode time per graph pass | `performance.now()` around `node.execute` | ~2 × 200 calls/frame |
245
- | CPU submit timestamp | `ShadeGPUCommandContext.finish` | free |
246
- | Callback/readback latency | recorder | free |
247
- | Sub-pass markers | `chromium-experimental-timestamp-query-inside-passes` if present | optional |
248
-
249
- ### 3.2 Dependencies
250
-
251
- Straight from `FrameGraph`, enriched:
252
-
253
- - Passes: id, name, scope chain, culled, `ref_count`, `has_side_effects`, reads/writes/creates.
254
- - Resource nodes: id, name, version, transient/imported, producer, readers, writers.
255
- - Per graph pass: the GPU passes it opened, in order — the join from §1.4.
256
- - Resource lifetime: first write → last read, derivable from `ResourceEntry.last`, which `compile()`
257
- already computes.
258
-
259
- ### 3.3 Data sizes
260
-
261
- Descriptors carry everything:
262
-
263
- | Resource | Fields | Footprint |
264
- |---|---|---|
265
- | Buffer | `size` bytes, `usage` bitmask, `ensure_cleared` range | `size` directly |
266
- | Texture | `resolution[3]`, `format`, `dimension`, `mipLevelCount`, `sampleCount`, `usage` | computed from `gpu_texture_format_info` — `bytes_per_block`, `block_width`, `block_height` — summed over the mip chain, × `sampleCount` |
267
-
268
- These are **declared** sizes, not allocated ones; a pooled allocator may serve a larger block.
269
- `GraphicsContext.gpu_memory_usage` gives the real total per frame — record it as a counter track and
270
- let the inspector show declared-vs-actual as the aliasing metric it is.
271
-
272
- ### 3.4 Work sizes
273
-
274
- | Datum | Needs |
275
- |---|---|
276
- | `dispatchWorkgroups(x, y, z)` | proxy encoder |
277
- | Workgroup size from the shader (`@workgroup_size`) | pipeline descriptor, recorded once per pipeline |
278
- | Total invocations = groups × workgroup size | derived in the inspector |
279
- | `draw` / `drawIndexed` vertex, index, instance counts | proxy encoder |
280
- | Indirect flag + argument buffer id/offset | proxy encoder |
281
- | Resolved indirect counts | §2.5 v2, opt-in |
282
- | Bind group contents per dispatch | proxy encoder; **large** — gate behind a verbosity level |
283
- | Render pass attachment formats and load/store ops | pass descriptor |
284
-
285
- ### 3.5 The fourth axis — context, so any of it means anything
286
-
287
- Without this a shared recording is unreadable:
288
-
289
- - Adapter info (`vendor`, `architecture`, `device`, `description`), features, the limits that matter.
290
- - Engine version, git revision, build flags, `ENV_PRODUCTION`.
291
- - Renderer settings: internal vs output resolution, upscaler, which features are on.
292
- - Nothing about timestamp quantization: see §2.1 for why that is a deliberate absence.
293
- - Wall-clock start, session id, user-supplied note.
294
- - Scene scale: mesh/instance/light counts, resident material and geometry bytes.
295
-
296
- ---
297
-
298
- ## 4. Architecture
299
-
300
- ```
301
- ENGINE (ships) CONTAINER INSPECTOR (published separately)
302
- ───────────────────── ───────── ───────────────────────────────
303
- GPUTimerArray ────┐
304
- FrameGraph ───────┤ ┌── timeline / flame graph
305
- proxy encoders ───┼──> GPUProfileSession ───> .sgpt ──drop──> ├── dependency graph
306
- GraphicsContext ──┘ │ bytes ├── resource table
307
- │ ├── pass detail
308
- sgpt_write_* └── frame comparison
309
- │ │
310
- BinaryBuffer <─────────────────────────── sgpt_read_*
311
- ```
312
-
313
- **Three rules that keep this from rotting.**
314
-
315
- 1. **The container is the only interface.** The inspector imports the format readers and
316
- `BinaryBuffer`. It never imports Shade, `FrameGraph`, or anything under `src/shade/renderer/`.
317
- This is a licensing constraint as much as an architectural one — meep is proprietary and
318
- source-available, and a separately published inspector must not carry renderer internals.
319
- 2. **The recorder never blocks the frame.** Every buffer it touches is written at encode time or
320
- read after submit. Serialization runs off the frame — accumulate into per-frame plain records,
321
- encode to bytes when the session stops or when a chunk fills.
322
- 3. **Reader and writer live together, in the engine tree.** Under
323
- `src/shade/device/timing/profile/`, one file per concern, matching the `vgeo_*` convention. The
324
- inspector package imports them by path. One definition of the format; no drift.
325
-
326
- ### 4.1 Verbosity levels
327
-
328
- A capture is a trade between fidelity and cost. Four levels, recorded in the header:
329
-
330
- | Level | Adds | Bytes/frame, ~200 passes |
331
- |---|---|---|
332
- | 0 `TIMING` | GPU pass spans, frame boundaries | ~2 KB |
333
- | 1 `STRUCTURE` | + frame graph topology, resource descriptors, cull state | ~2 KB amortised (§5.4) |
334
- | 2 `WORKLOAD` | + dispatch/draw counts, pipeline identities, attachment state | ~6 KB |
335
- | 3 `VERBOSE` | + bind group contents, indirect readback, CPU per-pass timing | ~40 KB |
336
-
337
- Levels 0–2 are the product. Level 3 is for us, on a repro, and the inspector should say so when it
338
- opens one.
339
-
340
- Because a session is now **uncapped by default** (§5.8), those per-frame figures are also a rate.
341
- At 60 Hz:
342
-
343
- | Level | Per second | Per minute |
344
- |---|---|---|
345
- | 0 `TIMING` | ~120 KB | ~7 MB |
346
- | 1 `STRUCTURE` | ~120 KB | ~7 MB |
347
- | 2 `WORKLOAD` | ~360 KB | ~21 MB |
348
- | 3 `VERBOSE` | ~2.4 MB | ~144 MB |
349
-
350
- Levels 0–2 will run for many minutes without anyone noticing. **Level 3 will not**, and the session
351
- should say so — see the byte-budget warning in §5.8. This is the honest cost of "full and uncapped
352
- explicit history", and it is the right trade at levels 0–2.
353
-
354
- ### 4.2 Repository layout
355
-
356
- The repo becomes an npm workspace root, and the engine moves down a level so the two packages are
357
- siblings rather than one being nested inside the other.
358
-
359
- ```
360
- meep/
361
- package.json workspace root: { "private": true, "workspaces": ["packages/*"] }
362
- .gitlab-ci.yml
363
- LICENSE, README.md, CONTRIBUTING.md, CHANGELOG.md, ...
364
- packages/
365
- meep-engine/ @woosh/meep-engine — everything that is published today
366
- package.json unchanged `exports`, `files`, version
367
- src/ editor/ samples/ tools/
368
- rollup.config.js vite.config.mjs vitest.config.mjs tsconfig.types.json babel.config.cjs
369
- gpu-inspector-tool/ the inspector, published separately
370
- package.json index.html src/ fixtures/
371
- ```
372
-
373
- `@woosh/meep-engine`'s own `exports` map (`./src/*`, `./editor/*`) is relative to its own
374
- `package.json`, so **consumers installing from npm see no change at all**. The paths that move are
375
- in-repo and in anything vendoring this tree by path.
376
-
377
- **The cost is in `moh`, not here.** This tree is vendored into the game at
378
- `app/src/mir-engine/meep/`, and that is where the restructure is felt: every engine path becomes
379
- `app/src/mir-engine/meep/packages/meep-engine/src/...`. Two ways to absorb it, and this is the one
380
- call the restructure actually needs:
381
-
382
- - **Re-point the vendoring one level deeper** — mount `packages/meep-engine` as
383
- `app/src/mir-engine/meep`. Every path in `moh` stays byte-identical, and the whole cost collapses
384
- to one submodule/copy path. **This is the one to take** unless something in `moh` needs the
385
- workspace root itself.
386
- - **Update paths in `moh`** — mechanical, wide, and it churns the ~20 design documents in this tree
387
- that quote `app/src/mir-engine/meep/src/...` as a source root.
388
-
389
- In-repo edits either way: `vitest.config.mjs` `include`/`coverage.include` globs, `tsconfig.types.json`
390
- `include`/`rootDir`, `rollup.config.js` `input` paths, `.gitlab-ci.yml` script and artifact paths, and
391
- `vite.config.mjs`. All of them are prefix changes, and all of them get shorter, not longer, because
392
- they become relative to the package rather than the root.
393
-
394
- > Worth knowing before committing: npm lets a workspace root *also* be a package, so
395
- > `packages/gpu-inspector-tool` could have been added with the engine left exactly where it is and
396
- > zero paths touched. That option is on the table if the `moh` re-pointing turns out to be more
397
- > awkward than it looks. Proceeding with the move as decided — it is the cleaner end state and the
398
- > re-pointing above makes it cheap.
399
-
400
- ---
401
-
402
- ## 5. The `.sgpt` container
403
-
404
- **S**hade **G**PU **P**rofile **T**race. Magic `0x54504753` — the ASCII bytes `S G P T` read as a
405
- little-endian u32 at offset 0, matching the `VGEO_MAGIC` convention in
406
- [VGEO_MAGIC.js](packages/meep-engine/src/shade/renderer/geometry/virtual/format/header/VGEO_MAGIC.js).
407
- `sgpt_fourcc.js` computes these and a spec asserts every literal against it, because hand-deriving
408
- a little-endian FourCC is exactly the kind of arithmetic that is wrong once and then forever.
409
-
410
- ### 5.1 Why binary, concretely
411
-
412
- A 600-frame capture at level 2, ~200 passes/frame, in Chrome Trace Event JSON: each span is roughly
413
- `{"ph":"X","name":"...","cat":"gpu","ts":123456.7,"dur":234.5,"pid":1,"tid":3,"args":{...}}` — call
414
- it 140 bytes minified, times 120,000 spans, **~17 MB**, before pass arguments. The same content in
415
- the layout below is **~1.5 MB**, and gzip (already a dependency via `pako`, and available natively
416
- via `CompressionStream`) takes it under 400 KB. That is the difference between a capture you attach
417
- to a bug report and one you do not.
418
-
419
- ### 5.2 Layout
420
-
421
- Chunked, directory at the tail — so a writer streams forward and only seeks back once, and a reader
422
- can pull the header and the frame index without reading the payload.
423
-
424
- ```
425
- offset size field
426
- ──────────────────────────────────────────────────────────────────
427
- 0 4 magic u32 0x54504753
428
- 4 2 format_version u16
429
- 6 2 min_reader_version u16
430
- 8 4 flags u32 bit0 = CLOSED (writer reached stop)
431
- 12 4 header_checksum u32 over bytes [0, 8) only — see below
432
- 16 8 directory_offset u64 0 when the session never stopped
433
- 24 8 directory_byte_length u64
434
- 32 ... records, in write order
435
- ... ... directory record
436
- ```
437
-
438
- **The checksum stops at byte 8, short of `flags`.** Everything from `flags` onward is patched at
439
- stop — the CLOSED bit and the directory pointer — and a checksum covering them would have to be
440
- recomputed then. Fine for a capture that stops; exactly wrong for one that does not, which would
441
- carry a checksum over bytes never written and read as corrupt. What stays under it is what a reader
442
- must trust before it can do anything at all: is this an `.sgpt`, and can this build read it.
443
-
444
- ### 5.3 Chunks
445
-
446
- | FourCC | Purpose | Cardinality |
447
- |---|---|---|
448
- | `META` | §3.5 context. Adapter, engine version, settings, verbosity level, session note. | 1 |
449
- | `SYMS` | Symbol block. Names, labels, formats, scope names — emitted **inside the record that first needs them** and referenced by u32 index thereafter, so a truncated capture still resolves every name it references (§5.8). | 0..n |
450
- | `PIPE` | Pipeline table: id, label, kind, shader module name, `@workgroup_size`, entry point, vertex layout digest. | 1 |
451
- | `TOPO` | Frame-graph **topologies**: pass list, resource node list, edges, scopes, descriptors. Content-hashed and deduplicated across frames (§5.4). | 0..n |
452
- | `FRAM` | One per frame: topology id, CPU timestamps, span array, dispatch/draw array, counters. | 0..n |
453
- | `CNTR` | Counter tracks sampled per frame: `gpu_memory_usage`, mesh/instance/light counts, resolution. | 0..1 |
454
- | `NOTE` | Free-form user annotations with a frame index — "this is where it hitches". | 0..n |
455
- | `DIRE` | Directory: `{chunk_type, offset, byte_length}` plus a frame index of `{frame_number, chunk_offset, gpu_duration_ns}` so the inspector can draw the frame-time strip before parsing anything else. | 1 |
456
-
457
- Unknown chunk types are skipped by length. That is what makes level-3 payloads addable without a
458
- version bump, and what lets an old inspector open a new capture and say honestly which parts it
459
- cannot show.
460
-
461
- ### 5.4 The compression that matters: topology deduplication
462
-
463
- **A Shade frame graph is nearly identical frame to frame.** Same passes, same resources, same edges;
464
- what changes is the timings, the cull decisions, and the occasional resolution change. Recording the
465
- full topology 600 times is the difference between a 20 MB file and a 1.5 MB one.
466
-
467
- So: hash the recorded topology (pass names, edges, descriptors, scope chain). If the hash matches a
468
- `TOPO` already written, the frame stores only the topology id. Cull state and per-pass timings stay
469
- in `FRAM`, because they are exactly what varies.
470
-
471
- Expected behaviour on a real capture: a handful of distinct topologies over 600 frames — steady
472
- state, plus the shadow-refresh variants, plus resolution changes. §4.1's "~2 KB amortised" for level
473
- 1 is that claim.
474
-
475
- This must be **measured, not assumed**. If topologies turn out to churn every frame, the design
476
- still works (it degrades to storing each), but the size estimates in §5.1 do not. First milestone
477
- work item, §8.
478
-
479
- ### 5.5 Per-frame span encoding
480
-
481
- Per span, level 0:
482
-
483
- | Field | Type | Note |
484
- |---|---|---|
485
- | `pass_ref` | `uintVar` | index into the topology's GPU-pass list |
486
- | `t_begin` | `u32` | ns offset from the frame's `gpu_epoch_ns` |
487
- | `t_end_delta` | `uintVar` | ns from `t_begin` |
488
- | `query_set_id` | `u8` | §2.4 |
489
-
490
- `u32` for the frame-relative begin holds 4.29 s — three orders of magnitude of headroom over a
491
- frame, and it survives a stall without overflowing. `BinaryBuffer.writeUintVar` / `readUintVar`
492
- already exist and handle the two variable-length fields.
493
-
494
- That is ~10 bytes per span against 26 for a naive absolute-u64 encoding, and the frame's absolute
495
- epoch is stored once as a `u64`.
496
-
497
- ### 5.6 Chrome Trace Event export
498
-
499
- The inspector exports the loaded capture as Chrome Trace Event JSON, for opening in
500
- [ui.perfetto.dev](https://perfetto.dev/docs/getting-started/other-formats). Costs an afternoon and
501
- buys: a second opinion when the inspector looks wrong, a viewer for anyone unwilling to run ours, and
502
- a sanity check on our own timeline maths. Passes become `X` slices on a GPU track, scopes become
503
- nesting, counters become `C` events. Lossy — the dependency graph has no representation there — which
504
- is precisely why it is an export and not the storage format.
505
-
506
- ### 5.7 String encoding
507
-
508
- Symbols live in `SYMS` blocks with u32 indices, emitted inside the record that first needs them
509
- (§5.8). Not `EncodingBinaryBuffer`: it deduplicates by writing back-references to **absolute buffer
510
- positions** ([EncodingBinaryBuffer.js:21](src/core/binary/EncodingBinaryBuffer.js:21)), which is
511
- correct for a single flat buffer and wrong the moment records are skipped by a reader that does not
512
- understand them, resynchronised after corruption, or read from a truncated file — all three of which
513
- this format is explicitly built to survive. An index table costs one indirection and holds up under
514
- all of them.
515
-
516
- ### 5.8 Streaming: self-contained frame records
517
-
518
- The session is an **open-ended forward stream with an explicit stop**. No ring, no cap: `frame_limit`
519
- defaults to `Infinity`, `stop()` ends it early, and everything recorded is kept.
520
-
521
- **A frame is written as one complete, self-describing record and pushed when it closes.** That is
522
- the organising decision, and three properties follow from it:
523
-
524
- - **It is resynchronisable.** Every record opens with a sync marker, its type, its payload length and
525
- a checksum. A reader that lands on garbage scans forward for the next marker and carries on. A
526
- capture cut off mid-session — tab killed, GPU reset, `device.lost` — stays readable up to the last
527
- intact record, and those are exactly the captures worth having.
528
- - **Symbols travel with the record that introduces them.** A record carrying a new topology carries
529
- the names that topology needs; a frame record reusing a known topology carries none. There is no
530
- global string table to finalise, which is what would otherwise force a tail chunk and make a
531
- truncated file unreadable — the failure mode where the one capture of the crash resolves no names.
532
- - **Nothing is back-patched except the header's directory pointer**, and even that is optional: a
533
- reader can recover the whole stream by scanning records when the directory is missing or the file
534
- was truncated before `stop()` ran.
535
-
536
- Order on the wire:
537
-
538
- ```
539
- header magic, versions, flags; directory_offset patched at stop, 0 if never stopped
540
- META at start — adapter, engine version, renderer settings, level
541
- TOPO #0 + its symbols first frame's topology
542
- FRAM #0 topology id + spans + counts
543
- FRAM #1 same topology → id only
544
- ...
545
- TOPO #1 + its symbols shadow refresh changed the graph
546
- FRAM #57
547
- ...
548
- CNTR, DIRE at stop — the directory is an index, not a dependency
549
- ```
550
-
551
- Record framing, every record identical:
552
-
553
- ```
554
- 0 4 sync u32 0x43455253 ('SREC')
555
- 4 4 record_type u32 FourCC
556
- 8 4 payload_byte_length u32
557
- 12 4 payload_checksum u32 crc32 over the payload
558
- 16 ... payload
559
- ```
560
-
561
- u32 for the length rather than u64: a record describes one frame's control flow, and resource
562
- *contents* are a non-goal, so nothing in one scales with the size of what it describes. Four
563
- gigabytes is not a ceiling anything can approach.
564
-
565
- **The sync marker alone is not enough to resynchronise on**, and the reader does not treat it as
566
- though it were. Four bytes of payload can spell `SREC` — a pass label could — so a candidate counts
567
- only when its length also fits and its payload also checksums. There is a spec for exactly that
568
- case.
569
-
570
- **Reallocation is a non-issue, and the slack reservation settles what is left of it.** We are
571
- measuring GPU time, not CPU time; a `setCapacity` copy is a heap allocation and a memcpy that may
572
- evict some cache, which at worst makes the next GPU upload marginally slower. That is a rounding
573
- error against what is being measured. To keep even that out of the frame interior, the writer
574
- reserves at the frame boundary:
575
-
576
- ```js
577
- buffer.ensureCapacity(buffer.position + FRAME_SLACK); // FRAME_SLACK = 1 MiB
578
- ```
579
-
580
- Any growth then happens between frames, never inside a record. 1 MiB against level 2's ~6 KB per
581
- frame is three orders of magnitude of headroom, so the reservation only actually forces a grow once
582
- every ~170 frames. One `BinaryBuffer`, no segmentation, no assembly pass.
583
-
584
- **Scope note, and it is what makes the above generous rather than marginal: GPU buffer and texture
585
- contents are a non-goal.** The stream carries control flow and dataflow metadata — passes, edges,
586
- descriptors, counts, timings. Nothing in it scales with the size of the resources it describes. That
587
- is why a frame record is kilobytes and why 1 MiB of slack is never the binding constraint.
588
-
589
- **Uncapped still means uncapped, so the session stays legible about it.** `bytes_written` is readable
590
- at any time and `onBytesWritten` fires per frame, so an application can show a counter. A configurable
591
- `byte_budget` — default 512 MB — does not stop the recording; it warns once, naming the level and the
592
- observed rate. Silent unbounded growth in a debug tool is how you lose a browser tab and the capture
593
- with it.
594
-
595
- ---
596
-
597
- ## 6. The recorder
598
-
599
- ### 6.1 Surface — the caller owns the session
600
-
601
- **The engine never constructs a profile session.** The caller builds one and hands it in; the
602
- renderer holds a nullable reference and nothing more. Nothing in `Renderer` imports the profiler.
603
-
604
- ```js
605
- import { GPUProfileSession } from "@woosh/meep-engine/src/shade/device/timing/profile/GPUProfileSession.js";
606
- import { GPUProfileLevel } from "@woosh/meep-engine/src/shade/device/timing/profile/GPUProfileLevel.js";
607
-
608
- const session = new GPUProfileSession({
609
- level: GPUProfileLevel.WORKLOAD,
610
- frame_limit: Infinity, // default; a number caps it
611
- note: "hitch on shadow refresh, RTX 3070"
612
- });
613
-
614
- renderer.profile_session = session; // nullable field, that is the whole API
615
-
616
- session.start(); // META is written here; populate meta first
617
- // ... frames run, for as long as you like ...
618
- const bytes = await session.stop(); // ArrayBuffer, ready to save
619
-
620
- renderer.profile_session = null;
621
- ```
622
-
623
- Three things fall out of the caller owning it, and all three are why this is the right shape:
624
-
625
- - **It is genuinely optional.** The profiler is a leaf module nothing in the engine imports. An
626
- application that never imports `GPUProfileSession` does not have it in its bundle — the answer to
627
- "does this ship in production builds" is *it ships, and it costs nothing to anyone who does not ask
628
- for it*. No build flag, no strip-plugin interaction, no dead-code branch to keep honest.
629
- - **Lifetime is explicit.** A session that outlives a device, or two sessions at once, are the
630
- caller's problem to not create, and `start()` asserts against both rather than papering over them.
631
- - **The engine surface is one nullable field.** `Renderer.profile_session`, forwarded to the
632
- `GraphicsContext` so command contexts can find it. That is the entire integration.
633
-
634
- `frame_limit` reaching zero stops the session exactly as `stop()` does. Both resolve the same
635
- promise, so a caller that wants "500 frames or until I say" writes one `await`.
636
-
637
- ~~`add_debug_frame` and `onFrameDebug` stay exactly as they are — cheap, synchronous, console-shaped,
638
- and a different tool for a different question.~~
639
-
640
- > **Reversed 2026-08-31.** They were not a different tool, they were a worse view of the same one
641
- > reached through a second API — no graph-pass attribution, no workload, no pipeline identity — and
642
- > `onFrameDebug` never acquired a subscriber. Removed; the profiler is the only path. "Profile the
643
- > next N frames" is `new GPUProfileSession({ frame_limit: N })`.
644
-
645
- ### 6.2 The five hooks
646
-
647
- 1. **`FrameGraph.execute`** — bracket `node.execute(...)`. Notify listeners which graph pass is open;
648
- take CPU timestamps either side; push/pop a debug group. The only edit outside `src/shade/`.
649
-
650
- Two `Signal`s on `FrameGraph`, `onPassBegin` / `onPassEnd`, alongside the `onExecuted` that is
651
- already there. **Signal, not a nullable callback field** — it is the project's standardised
652
- observer interface, it supports more than one listener, and `remove` is symmetric with `add`.
653
- A bare nullable callback lets the second consumer silently clobber the first, which is exactly
654
- the bug that does not announce itself.
655
-
656
- Guard the dispatch with `hasHandlers()`: `send1` on an empty signal still bumps `generation` and
657
- walks a Map iterator, and this fires twice per pass — a couple of hundred passes a frame. One
658
- `Map.size` comparison ahead of it keeps the disabled path honest.
659
-
660
- `FrameGraph` still learns nothing about what a profiler is; it announces its own pass boundaries
661
- and the profiler is one possible listener.
662
- 2. **`ShadeGPUCommandContext`** — a `#profile_sink` field. When set, `beginComputePass` /
663
- `beginRenderPass` report `{label, kind, graph_pass_id, query_slot, query_set_id}` and wrap the
664
- returned encoder in the recording proxy.
665
- 3. **The proxy encoders** — `ProfilingComputePassEncoder`, `ProfilingRenderPassEncoder`. Forward
666
- everything; record `dispatchWorkgroups*`, `draw*`, `setPipeline`, `setBindGroup`. Modelled on
667
- `SoftwareGPUComputePassEncoder`, which already records exactly this shape.
668
- 4. **`GPUTimerArray`** — pooling (§1.2.2), bound checking (§1.2.1), and expose raw slot data rather
669
- than only the console table.
670
- 5. **`GraphicsContext`** — sample `gpu_memory_usage` and the collection counters once per frame into
671
- the counter track.
672
-
673
- ### 6.3 Cost when off
674
-
675
- Every hook is a null check against a field that is `null` in normal operation. No allocation and no
676
- proxy construction; the frame-graph bracket is two comparisons per pass. This must stay true — a
677
- profiler that costs something when disabled will be disabled at the build level and then rot.
678
-
679
- Because the session is caller-constructed (§6.1), an application that never imports it pays not even
680
- that: the profiler modules are unreachable from any engine entry point and drop out of the bundle
681
- entirely. The null checks are the only residue, and they are the price of the feature existing.
682
-
683
- ### 6.4 Cost when on
684
-
685
- Level 0–2 add: one `performance.now()` pair per graph pass, one small record per GPU pass, one per
686
- dispatch/draw, and a `push`/`popDebugGroup` pair per graph pass. The existing per-context query set
687
- churn (§1.2.2) is the largest cost and the pool removes it. Expect single-digit percent frame-time
688
- overhead at level 2; measure it and record the measurement in `META`, so a reader can see how much of
689
- what they are looking at is the observer.
690
-
691
- ---
692
-
693
- ## 7. The inspector
694
-
695
- `packages/gpu-inspector-tool/`. A static site: drop a `.sgpt` on it, or pass `?file=` for a
696
- bookmarkable view — the affordance `prototypeVGEOFormatViewer` already established in this codebase.
697
-
698
- ### 7.1 Technology
699
-
700
- **Plain ES modules, Canvas 2D for the timeline, DOM for panels, no runtime dependencies, built with
701
- Vite.** Reasons: the repo has no UI framework and adding one for this is unjustified; the timeline is
702
- a custom-drawn virtualised widget that a framework would only get in the way of; zero dependencies
703
- keeps a separately published proprietary artifact simple to reason about; and Vite is already the dev
704
- server here.
705
-
706
- The only imports from the engine tree are `src/core/binary/BinaryBuffer.js` and the `sgpt_*` readers.
707
- That boundary is a build-time assertion, not a convention — a lint rule that fails the build on any
708
- other engine import.
709
-
710
- ### 7.2 Views
711
-
712
- | View | Answers |
713
- |---|---|
714
- | **Frame strip** | Which frame is interesting. Frame time over the session, GPU and CPU overlaid, hitches marked, brush to select a range. Drawn from the directory index alone, so it appears before the payload finishes parsing. |
715
- | **Timeline / flame graph** | Where the time went in *this* frame. Spans on a GPU track, nested by `FrameGraphScope`; a CPU track above with encode time; hover for exact ns; click to select. |
716
- | **Statistics** | Where the time goes *in general*. Per-pass min/median/p95/max/total across the selected frame range, sorted by total contribution. **On a quantized capture this is the only honest view** — and since the tool does not measure quantization (§2.1), it cannot detect that case and switch by itself. It says so plainly instead, always. |
717
- | **Dependency graph** | Why this pass runs, and what it waits on. Passes and resource nodes, culled ones greyed, edges directed. Select a pass → highlight its transitive inputs. Select a resource → its version chain and every reader. |
718
- | **Resource table** | What memory costs. Every resource node with declared bytes, format, usage, transient/imported, lifetime span, peak concurrent footprint. Sorted by size. Declared total vs `gpu_memory_usage` side by side. |
719
- | **Pass detail** | Everything about one pass. Timings across frames as a sparkline, dispatch/draw counts, derived total invocations, pipeline and workgroup size, attachments, bindings at level 3. |
720
- | **Frame comparison** | What changed. Two frames or two ranges side by side, per-pass deltas sorted by regression. This is the view that makes the tool useful for optimisation work rather than only for diagnosis. |
721
-
722
- ### 7.3 What it must refuse to do
723
-
724
- - **Never invent precision the data does not have.** Durations are drawn as reported. Where many
725
- spans read as exactly zero, that is shown as what it is — a quantized capture — with a note
726
- naming the browser flag, not smoothed into plausible-looking small numbers.
727
- - **Never present the CPU and GPU tracks as one clock** (§2.3). Two tracks, anchored per frame,
728
- labelled as nominal.
729
- - **Never hide dropped spans.** If the recorder dropped passes on slot exhaustion (§1.2.1), say how
730
- many, on the frame that dropped them.
731
-
732
- ---
733
-
734
- ## 8. Phasing
735
-
736
- Each milestone is independently useful and independently shippable.
737
-
738
- ### M0 — Measure the assumptions (½ day)
739
-
740
- Before any of the below. Instrument one Sponza capture and answer three questions, because three
741
- size estimates and one whole design decision rest on them:
742
-
743
- 1. How many distinct frame-graph topologies over 600 frames? (§5.4)
744
- 2. ~~What is the observed timestamp period~~ — dropped; the tool does not measure this (§2.1).
745
- 3. How many GPU passes per frame, actually? (`GPUTimerArray` default is 1024 slots; §1.2.1)
746
-
747
- Write the answers into this document.
748
-
749
- ### M0.5 — Workspace restructure — **DONE**
750
-
751
- Landed 2026-08-29. `packages/meep-engine/` holds the engine, the workspace root holds the two
752
- `.gitignore` halves, the CI paths and a README. Suite green at 2156 files / 14580 tests. One spec
753
- had to move with it: `meep_three_free.spec.js` asserts the `*.d.ts` ignore rule at the engine root,
754
- so that rule lives in the package's `.gitignore` rather than the workspace's.
755
-
756
- **`moh` still needs its vendoring re-pointed one level deeper** — mount `packages/meep-engine` where
757
- it currently mounts the repository root, and every path inside it stays byte-identical.
758
-
759
- <details><summary>Original plan</summary>
760
-
761
- §4.2. Independent of everything else and worth landing first so no profiler work has to be moved
762
- afterwards. Order: create `packages/meep-engine/`, `git mv` the tree, fix the five config files,
763
- green the suite, re-point `moh`'s vendoring one level deeper, green `moh`. Land as its own commit —
764
- a pure move with no content changes, so the diff stays reviewable and a bisect through it is honest.
765
-
766
- </details>
767
-
768
- ### M1 — Timing spine — **DONE**
769
-
770
- Landed:
771
-
772
- - `FrameGraph.onPassBegin` / `onPassEnd`, guarded by `hasHandlers()`, closed in a `finally` so a
773
- throwing pass still ends its bracket. Five specs including the exception path.
774
- - `GPUTimerArray`: bound check (a full array now refuses a slot instead of addressing past its query
775
- set), `dropped_count`, `traverse_results` for raw `BigInt` timestamps. Seven specs.
776
- - `ShadeGPUCommandContext`: `#with_timestamp_writes` — copies the descriptor instead of stamping the
777
- caller's, tolerates a missing descriptor, and leaves `timestampWrites` off when no slot is free.
778
- - The whole `.sgpt` container: header, framing, resync reader, defect model, symbol interning,
779
- `META`/`SYMS`/`FRAM`/`DIRE` codecs, `GPUProfileSession`, `snapshot()`. 26 specs.
780
- - `GPUFrameRecorder` — holds the graph-pass ↔ timer-slot join, accumulates across every command
781
- context in a frame, and rebases spans onto the frame epoch at close.
782
-
783
- Removed again on 2026-08-29: a `timestamp_period_ns` field and the GCD estimator that filled it.
784
- See §2.1 — quantization is documented and left alone.
785
-
786
- Wired end to end: `Renderer.profile_session`, `GPUFrameRecorder` holding the graph-pass ↔ timer-slot
787
- join, and `ShadeGPUCommandContext.profiling_absorbed` — which exists because `done` resolves at
788
- submit, before any timestamp has been read back.
789
-
790
- The calibration step is gone rather than outstanding: see §2.1.
791
-
792
- <details><summary>Original plan</summary>
793
-
794
- `GPUTimerArray` pooling and bound checks; `FrameGraph.onPassBegin`/`onPassEnd` and debug groups;
795
- `GPUProfileSession` at level 0, caller-constructed, streaming framed records with per-frame slack;
796
- `META`/`SYMS`/`FRAM`/`DIRE` records; round-trip and truncation-recovery tests on `SoftwareGPUDevice`.
797
- Deliverable: a `.sgpt` with real spans and no viewer.
798
-
799
- </details>
800
-
801
- ### M2 — Inspector skeleton — **DONE**
802
-
803
- `packages/gpu-inspector-tool`. Plain ES modules, Canvas 2D, no runtime dependencies. Session strip,
804
- timeline, statistics, pass detail. `make_demo_capture` builds a synthetic capture through the
805
- engine's own writer, so the tool works without a GPU and the format's two halves are exercised
806
- against each other.
807
-
808
- ### M3 — Structure — **DONE**
809
-
810
- `TOPO` with content-hash deduplication — 50 frames of an unchanged graph come to under 4 KB. Scopes,
811
- cull state, resource descriptors with footprints computed at record time. The inspector's Structure
812
- view is two cross-linked tables rather than a node-link diagram: two hundred passes over as many
813
- resource versions do not lay out into anything readable, and the questions actually asked are local.
814
-
815
- ### M4 — Workload — **DONE**
816
-
817
- `make_profiling_pass_encoder` is a `Proxy` rather than a hand-written forwarder: a pass encoder has
818
- around twenty methods and WebGPU keeps adding them, and one silently dropped would break the
819
- renderer only while profiling. Dispatch and draw counts, pipeline identity, and `@workgroup_size`
820
- parsed from WGSL where it is a literal — an override expression records as *unknown*, which is not
821
- the same as zero.
822
-
823
- > **Reversed 2026-08-31.** The argument holds only while the wrapper is *conditional*; that is what
824
- > makes a missing method silent. `ShadeGPUComputePassEncoder` / `ShadeGPURenderPassEncoder` wrap
825
- > every pass, so a gap throws on the first frame of every build. The workgroup size now comes off
826
- > `ComputePipelineDescriptor`, which reads it as its code is set, with the parser that resolves
827
- > `const` references — a size it cannot read refuses to build the descriptor rather than recording
828
- > as unknown.
829
-
830
- No `PIPE` table in the end: a pass's pipeline label and workgroup size ride in its own workload
831
- block, which costs a symbol reference and avoids a second id space.
832
-
833
- ### M5 — Analysis — **partly done**
834
-
835
- Statistics view and Chrome Trace Event export are in. Frame comparison and counter tracks are not;
836
- `CNTR` is reserved in the format for the latter.
837
-
838
- ### M6 — Publish — **partly done**
839
-
840
- [`SGPT_FORMAT.md`](profile/SGPT_FORMAT.md) is the normative format specification, written for
841
- somebody outside the company, with a conformance summary for readers and writers. The inspector's
842
- README covers recording, levels, and the reading caveats.
843
-
844
- Outstanding: a hosting target for the built site, and deciding the licence the inspector ships
845
- under.
846
-
847
- **Not in scope, listed so it stays that way:** buffer/texture content capture, shader source in the
848
- container, replay, live attach to a running session, anything requiring a browser extension.
849
-
850
- ---
851
-
852
- ## 9. Testing
853
-
854
- Per the established tiers:
855
-
856
- | Tier | Covers |
857
- |---|---|
858
- | **Unit, node** | Format round-trip: build a synthetic session, encode, decode, assert structural equality. Every chunk. Forward compatibility: a reader skipping an unknown chunk. Truncation and corruption: a bad checksum, a short chunk, a directory pointing past EOF — all must fail with a named error, not a stack trace out of `BinaryBuffer`. |
859
- | **`SoftwareGPUDevice`** | The recorder end to end without a GPU. The device already emulates `timestamp-query`, query sets and `write_pass_timestamp`, and already refuses a query set over 4096. Encode a frame graph, run a session, decode the output, assert the topology matches what was recorded. This is where the §1.4 attribution logic gets its coverage. |
860
- | **Playground page** | `src/shade/playground/` gets a capture harness against Sponza, which is also how M0 gets answered. |
861
- | **Inspector** | Golden captures checked into the package as fixtures. Parse-and-render smoke tests. A capture recorded before a format change must still open — that is what `min_reader_version` is for and it needs a test that proves it. |
862
-
863
- ---
864
-
865
- ## 10. Risks
866
-
867
- | Risk | Severity | Handling |
868
- |---|---|---|
869
- | **Timestamp quantization makes per-frame timings unreadable on default browsers** (§2.1) | **High, and deliberately untreated** | Documented, not measured and not corrected — measuring it is the timing attack the mitigation prevents, and measuring it badly skews everything. The inspector names the flag and steers toward cross-frame aggregation, which is the honest reading of a coarse capture. |
870
- | Topology dedup does not pay off | Medium | M0 measures it. Design degrades gracefully; the size claims do not. |
871
- | Proxy encoder overhead distorts what it measures | Medium | Level-gate it; record measured overhead in `META`; forward-only methods, no allocation per call. |
872
- | One `GPUTimerArray` per context means several query sets per frame (§2.4) | Low–Medium | Record `query_set_id`. Consider a per-frame shared array as a follow-up, which would also simplify pooling. |
873
- | Format churn during development invalidates captures | Low | `min_reader_version` from day one; the inspector reads every version it ever supported; fixtures in the test suite. |
874
- | Publishing a proprietary-engine tool separately | Low, but real | The §4 rule-1 boundary is what makes this tractable. Enforce it in the build. Confirm the licensing intent for the published inspector before M6. |
875
- | **Uncapped session exhausts memory on a long level-3 capture** (§4.1, §5.8) | Medium | `bytes_written` readable and signalled; a `byte_budget` that warns rather than truncates. Accepted deliberately — full history is the point, and buffer/texture contents are a non-goal so nothing scales with resource size. |
876
- | Workspace restructure churns `moh` (§4.2) | Medium | Re-point the vendoring one level deeper so every `moh` path stays identical. Land as a pure move, separately. |
877
-
878
- ## 11. Decisions taken — 2026-08-29
879
-
880
- | # | Question | Decision | Where it lives |
881
- |---|---|---|---|
882
- | 1 | Monorepo or not | **npm workspaces, and the existing tree moves to conform.** `packages/meep-engine/` + `packages/gpu-inspector-tool/`, symmetric siblings. | §4.2, M0.5 |
883
- | 2 | Does the recorder ship in production? | **Yes, but optional — the caller instantiates a `GPUProfileSession` and passes it in.** The engine never constructs one, so an application that does not import it does not carry it. | §6.1, §6.3 |
884
- | 3 | The `FrameGraph.execute` bracket | **Agreed, kept minimal — via `Signal`.** `onPassBegin` / `onPassEnd` beside the existing `onExecuted`, dispatch guarded by `hasHandlers()`. `Signal` is the project's standardised observer interface and a nullable callback field would let a second consumer silently clobber the first. | §1.4, §6.2 hook 1 |
885
- | 4 | Recording length policy | **Open-ended forward stream of self-contained frame records. `frame_limit` of N (1..∞) with an explicit `stop()`.** No ring buffer — full, uncapped, explicit history is the desired behaviour. Records are self-describing and checksummed, so a truncated capture stays readable; 1 MiB of slack reserved per frame keeps reallocation out of the record interior. | §5.8 |
886
-
887
- Two consequences of (4) worth carrying forward:
888
-
889
- - **There is no global string table.** Symbols are emitted inside the record that first needs them
890
- (`SYMS`, §5.3), not gathered into a `STRS` chunk. A tail table would have made a truncated capture
891
- resolve no names at all — the failure mode that hits precisely the capture of the crash you were
892
- trying to record.
893
- - **Records are framed and checksummed** so a reader can resynchronise. This is what buys corruption
894
- and truncation resilience, and it costs 20 bytes per frame.
1
+ # A graphical GPU profiler for Shade — recorder, format, inspector
2
+
3
+ Proposal, 2026-08-28. Decisions folded in 2026-08-29. Alex Goldring / Company Named Limited.
4
+
5
+ > **Partly superseded, 2026-08-31**, by
6
+ > [PASS_ENCODER_PROPOSAL_2026_08_31.md](../PASS_ENCODER_PROPOSAL_2026_08_31.md). The recording
7
+ > proxy of §6.2 hook 3 and M4 is now a pair of decorator classes; `enable_debug_timers`,
8
+ > `Renderer.add_debug_frame` and `Renderer.onFrameDebug` — which §6.1 kept — are gone, leaving the
9
+ > profiler as the only path. §§1–5 remain a record of the state this design was written against.
10
+
11
+ Scope: a **recording** side that ships with the engine, a **binary container** that carries a
12
+ capture, and an **inspector** web application under `packages/gpu-inspector-tool/` that reads the
13
+ container and nothing else.
14
+
15
+ > The request spelled the directory `gpu-inespector-tool`. Reading that as a typo and using
16
+ > `packages/gpu-inspector-tool/` throughout. Say the word if the misspelling was deliberate.
17
+
18
+ **Four decisions are settled and written into the design below** — npm workspaces with the existing
19
+ tree moved to conform (§4.2), a user-instantiated recorder passed in rather than a renderer-owned one
20
+ (§6.1), the minimal `FrameGraph` bracket via `Signal` (§1.4), and an open-ended uncapped stream of
21
+ self-contained frame records rather than a ring buffer (§5.8). §11 records them and what each costs.
22
+
23
+ ---
24
+
25
+ ## 0. Verdict
26
+
27
+ **Worth building, and the engine is further along than it looks — but not in the place you would
28
+ expect.**
29
+
30
+ The timing half is the *small* half. `GPUTimerArray` already lands begin/end timestamps per GPU
31
+ pass, and `SoftwareGPUDevice` already emulates timestamp queries well enough to test the recorder in
32
+ node without a GPU. What is genuinely missing is **attribution**: the timestamps are keyed by
33
+ `GPUComputePassDescriptor.label`, the dependency structure lives in `FrameGraph`, and *nothing
34
+ connects the two*. One frame-graph pass can open several GPU passes with unrelated labels
35
+ (`graph_build_hzb` → `hzb.build()` → its own `constructComputePass` labels), so a label is not a key.
36
+ Closing that gap is the central piece of engineering in this proposal, and it is about thirty lines
37
+ in `FrameGraph.execute` plus a field on the command context.
38
+
39
+ The dependency half is nearly free. `FrameGraph.exportToJson()`
40
+ ([FrameGraph.js:644](src/engine/graphics/render/frame_graph/FrameGraph.js:644)) already emits passes,
41
+ resource nodes, versions, readers, writers, producers and cull state, keyed consistently. Resource
42
+ *sizes* fall out of the descriptors that graph already holds. Nesting for a flame-graph view falls
43
+ out of `FrameGraphScope`.
44
+
45
+ The work-size half needs a wrapper that does not exist: `beginComputePass` hands back the raw
46
+ `GPUComputePassEncoder`, so `dispatchWorkgroups(x, y, z)` is invisible to us. A recording-only proxy
47
+ around the pass encoder buys every dispatch and draw count for one class and no call-site churn.
48
+
49
+ **The single biggest risk is not ours.** Chrome quantizes WebGPU timestamps to **100 µs** unless the
50
+ user sets `chrome://flags/#enable-webgpu-developer-features`. Most Shade passes run well under that,
51
+ so on a default browser almost every pass measures as exactly 0 or exactly 100 µs. A profiler that
52
+ does not say this out loud, in the recording *and* in the inspector, is a profiler that lies. §2.1.
53
+
54
+ ---
55
+
56
+ ## 1. What exists today
57
+
58
+ ### 1.1 The timing path, end to end
59
+
60
+ | Piece | Where | What it does |
61
+ |---|---|---|
62
+ | `GPUTimerArray` | [GPUTimerArray.js](src/shade/device/timing/GPUTimerArray.js) | One `GPUQuerySet` of `size * 2` timestamps. `getComputeWrites(label)` / `getRenderWrites(label)` claim a slot from an `IdPool` and return the `timestampWrites` struct. Resolve + copy to a `MAP_READ` buffer, `download_results()` maps it, `results_to_console_table()` turns slots into `{label, type, duration_ms, start, end}`. |
63
+ | `ShadeGPUCommandContext.enable_debug_timers(cb)` | [ShadeGPUCommandContext.js:89](src/shade/device/ShadeGPUCommandContext.js:89) | Constructs a `GPUTimerArray` for this context. `beginComputePass` / `beginRenderPass` then inject `timestampWrites` into every descriptor. |
64
+ | `finish()` | [ShadeGPUCommandContext.js:699](src/shade/device/ShadeGPUCommandContext.js:699) | Resolves the query set into the command buffer before `encoder.finish()`, then after submit downloads results and calls the callback, then destroys the timer array. |
65
+ | `Renderer.add_debug_frame(count)` | [Renderer.js:626](src/shade/renderer/Renderer.js:626) | Arms N future frames. |
66
+ | `Renderer.onFrameDebug` | [Renderer.js:408](src/shade/renderer/Renderer.js:408) | `send2(frame_index, table)`. **Currently has no subscribers anywhere in the tree.** |
67
+ | `GPUTimerStats`, `format_nanosecond_time` | `src/shade/device/timing/` | Ring-buffer averaging and human formatting. Reusable in the inspector. |
68
+
69
+ Degradation is already correct: with `timestamp-query` withheld, the query set is never created and
70
+ `resolve` / `download_results` / `destroy` all no-op rather than throwing. Keep that property.
71
+
72
+ ### 1.2 Four defects the recorder must fix, not inherit
73
+
74
+ 1. **No slot bound check.** `GPUTimerArray.#bind_slot` takes an id from the `IdPool` and never
75
+ compares it to `#size`. Pass 1025 in a frame (default `size = 1024`) writes a query index past
76
+ `querySet.count` and takes a WebGPU validation error, at the `beginRenderPass` rather than at the
77
+ place that overflowed. A recording session runs *more* passes instrumented than a debug frame
78
+ does, so this is on the path. Clamp and count drops; report the drop count in the recording.
79
+ 2. **Per-context GPU allocation.** `enable_debug_timers` builds a query set and two buffers per
80
+ context and destroys them after the download. Fine for one frame; recording 600 frames means 600
81
+ query-set create/destroy pairs and 600 `mapAsync` round trips. Wants a small pool of timer arrays
82
+ cycled N-deep against frames in flight.
83
+ 3. **Descriptor mutation.** `beginComputePass` does `let _descriptor = descriptor;` and then writes
84
+ `_descriptor.timestampWrites`, mutating the caller's object. Several call sites reuse descriptor
85
+ objects. Copy, or document the mutation.
86
+ 4. **`beginComputePass()` with no descriptor throws** once timers are on, at
87
+ `timers.getComputeWrites(descriptor.label)`. Currently latent because every call site passes one.
88
+
89
+ ### 1.3 The structure half
90
+
91
+ `FrameGraph.exportToJson()` is most of the dependency model already:
92
+
93
+ ```js
94
+ {
95
+ passes: [{ id, name, culled, reads: [node_id], writes: [node_id] }],
96
+ resources: [{ id, name, transient, version?, description?, createdBy?, readers?, writers? }]
97
+ }
98
+ ```
99
+
100
+ Keyed by **resource node** (per version), not by registry entry — deliberately, so
101
+ `passes[].reads/writes` stay resolvable across versions. The recording format must preserve that
102
+ choice; a registry-keyed export dangles every reference past version 0.
103
+
104
+ What it does *not* carry, and we want:
105
+
106
+ - `FrameGraphNode.scope` — the `FrameGraphScope` chain, which is the hierarchy a flame graph needs.
107
+ - `ref_count` and `has_side_effects` — why a pass survived culling.
108
+ - Typed descriptors. `description` is `resource_descriptor.toString()`, a human string
109
+ (`"Buffer{ size = 1,048,576, usage = STORAGE | COPY_DST }"`). We want the fields.
110
+
111
+ ### 1.4 The gap: labels are not keys
112
+
113
+ ```js
114
+ // graph_build_hzb.js
115
+ const builder = graph.add("hzb/ build", data, (data, resources, context) => {
116
+ hzb.build(context.encoder, source, viewport); // opens N compute passes, its own labels
117
+ });
118
+ ```
119
+
120
+ `GPUTimerArray` records the labels `hzb.build` chose. `FrameGraph` records `"hzb/ build"`. Nothing
121
+ relates them, and the relation is not derivable after the fact — labels are not unique, not stable,
122
+ and one graph pass legitimately produces many GPU passes.
123
+
124
+ **Fix:** bracket `node.execute(...)` inside `FrameGraph.execute`
125
+ ([FrameGraph.js:604](src/engine/graphics/render/frame_graph/FrameGraph.js:604)) so the context knows
126
+ which graph pass is open. Every GPU pass begun while that bracket is open is attributed to it. The
127
+ same bracket should emit `pushDebugGroup(node.name)` / `popDebugGroup()`, which costs nothing and
128
+ immediately improves what RenderDoc, PIX and webgpu-inspector show — worth doing on its own merits.
129
+
130
+ ### 1.5 Work sizes are not observable today
131
+
132
+ `beginComputePass` / `constructComputePass` return the **raw** `GPUComputePassEncoder`. All 11
133
+ production `dispatchWorkgroups` call sites talk to it directly, as does every draw call across the
134
+ 31 render-pass construction sites. Editing every one of them to report its own counts is the wrong
135
+ trade. A proxy encoder, installed only while recording, is one class.
136
+
137
+ `SoftwareGPUComputePassEncoder` already models exactly this: it records
138
+ `{pipeline, bind_groups, dynamic_offsets, group_counts}` per dispatch. The recording proxy is the
139
+ same shape with a real encoder behind it.
140
+
141
+ ---
142
+
143
+ ## 2. What the platform will give us
144
+
145
+ ### 2.1 Timestamp quantization — the headline constraint
146
+
147
+ Chrome quantizes `timestamp-query` results to **100 µs** as a timing-attack mitigation. It is
148
+ disabled by `chrome://flags/#enable-webgpu-developer-features`; that flag does not itself enable the
149
+ feature, which additionally needs the device to expose `timestamp-query`.
150
+
151
+ 100 µs is 0.1 ms. A Shade frame at 60 Hz has ~16.6 ms of budget spread over a couple of hundred
152
+ passes. **The median pass is below the quantum.** Unmitigated, the tool reports a histogram of zeros
153
+ with occasional 100 µs spikes, and every conclusion drawn from it is noise.
154
+
155
+ **The decision is to leave it entirely alone.** Not measure it, not calibrate against it, not
156
+ correct for it, not store a period in the container. Rejected 2026-08-29, and the reasoning is
157
+ worth keeping because it is not obvious:
158
+
159
+ - Recovering the quantum — GCD over observed timestamps, or any equivalent — **is the timing attack
160
+ the mitigation exists to prevent.** Doing it *well* means defeating a browser security control on
161
+ purpose, inside code we ship to other people.
162
+ - Doing it *badly* is worse than not doing it. A period that is a multiple of the real one, or a
163
+ stale one from a browser update, becomes a correction applied to every number in the capture. A
164
+ profiler that silently skews its own data is worse than one that reports coarse data honestly.
165
+ - Either way it is standing complexity and a source of fragility, bought for something the reader
166
+ can be told in one sentence.
167
+
168
+ So: **note it, document it, do not treat it.** The capture records what the device reported —
169
+ zeros included. `GPUProfileMeta` documents that browsers quantize and names the flag. The inspector
170
+ says so where a reader will see it. What to make of a coarse capture is the reader's judgement, and
171
+ the honest advice is the same either way: **aggregate across frames rather than trusting any single
172
+ one.** That shapes the format in exactly one way, which it already did — keep every frame's raw
173
+ values and never pre-aggregate on the recorder side.
174
+
175
+ ### 2.2 Pass granularity is the floor
176
+
177
+ WebGPU writes timestamps at pass boundaries only. Per-draw timing needs
178
+ `chromium-experimental-timestamp-query-inside-passes`, which is Chromium-only and experimental.
179
+
180
+ **Design for pass granularity.** Treat inside-passes as an optional capability the recorder probes
181
+ for and, if present, uses to add sub-pass markers — recorded as a distinct span kind so the format
182
+ does not pretend the two are the same measurement.
183
+
184
+ ### 2.3 No CPU↔GPU clock sync
185
+
186
+ WebGPU exposes no calibration between `performance.now()` and the GPU timestamp domain. Anything
187
+ claiming to place CPU and GPU events on one axis is guessing.
188
+
189
+ **Be honest in the format.** Record two clocks explicitly: a CPU track from `performance.now()`
190
+ (encode time, submit time, callback time) and a GPU track from timestamps. The inspector shows two
191
+ tracks anchored per frame at submit, and says the alignment is nominal. Perfetto solves this problem
192
+ with explicit `ClockSnapshot` packets and a clock graph; we do not have the snapshots, so we do not
193
+ get to claim the sync.
194
+
195
+ ### 2.4 Cross-query-set comparability
196
+
197
+ Timestamps from two different `GPUQuerySet`s are not specified to share a domain. In Dawn they do in
198
+ practice. Since each `ShadeGPUCommandContext` currently gets its own `GPUTimerArray`, and a frame has
199
+ several contexts, **every frame already spans several query sets.**
200
+
201
+ Record a `query_set_id` per span. Then the inspector can, if a capture ever looks wrong, colour
202
+ spans by origin and let the reader see whether the anomaly follows a set boundary. Cheap; makes an
203
+ otherwise unfalsifiable class of bug visible.
204
+
205
+ ### 2.5 Indirect work is unknowable at encode time
206
+
207
+ `dispatchWorkgroupsIndirect` and `drawIndirect` take their counts from a GPU buffer. At encode time
208
+ we know only *that* it was indirect. Two options, both worth having:
209
+
210
+ - **v1:** record the indirect flag plus the source buffer's id and offset. The inspector shows
211
+ "indirect (unknown)" and links to the buffer.
212
+ - **v2 (opt-in, costly):** the recorder copies indirect argument buffers into a readback staging
213
+ buffer at encode time and resolves the actual counts after submit. This is the only way to see the
214
+ real post-cull draw count, which for a GPU-driven renderer is one of the numbers most worth seeing.
215
+ It changes memory traffic, so it must be a flag, and the format must mark such counts as
216
+ *resolved* rather than *encoded* so nobody compares the two carelessly.
217
+
218
+ ### 2.6 Prior art
219
+
220
+ | Tool | What to take | What to leave |
221
+ |---|---|---|
222
+ | [webgpu_inspector](https://github.com/brendan-duncan/webgpu_inspector) (Brendan Duncan) | The reference point for WebGPU frame capture in a browser. Object inspection with creation stacktraces; frame-time plotting; buffer content view. Its recorder emits a standalone replayable HTML file. | It is a *generic API* interceptor, extension-hosted. We want *engine-semantic* data — frame graph passes, resource lifetimes, cull decisions — which a generic interceptor cannot see. Complementary, not competing. |
223
+ | [Perfetto](https://perfetto.dev/docs/getting-started/other-formats) | Track/slice model; nested slices; counter tracks; explicit clock domains. Its JSON importer is a free escape hatch — see §5.6. | The protobuf format and the full trace-processor stack are far more than we need, and its UI knows nothing about resource dependency. |
224
+ | RenderDoc / PIX / Radeon GPU Profiler | The three-pane idiom: event list, timeline, per-event detail; a resource view keyed by lifetime; "why is this bound" attribution. | Native capture, replay, driver counters. Out of reach on WebGPU. |
225
+ | Chrome Trace Event Format | Trivially writable, universally readable. | Text JSON at our event rates is 5–10× our binary size. Export target, not storage. |
226
+ | [webgpufundamentals timing](https://webgpufundamentals.org/webgpu/lessons/webgpu-timing.html) | The canonical treatment of the query-set/resolve/map dance and its pitfalls. | — |
227
+
228
+ The gap in that table is the whole reason to build this: **no existing tool knows what a Shade frame
229
+ graph is.** Timings without the dependency structure tell you a pass is slow; timings *with* it tell
230
+ you a pass is slow because it waits on a resource that a culled branch still produced.
231
+
232
+ ---
233
+
234
+ ## 3. What a recording must contain
235
+
236
+ Three axes were asked for. A fourth is needed to make the first three legible.
237
+
238
+ ### 3.1 Timings
239
+
240
+ | Datum | Source | Cost |
241
+ |---|---|---|
242
+ | GPU pass begin/end, ns | `GPUTimerArray` | free, already there |
243
+ | Which query set a span came from | recorder | 1 byte |
244
+ | CPU encode time per graph pass | `performance.now()` around `node.execute` | ~2 × 200 calls/frame |
245
+ | CPU submit timestamp | `ShadeGPUCommandContext.finish` | free |
246
+ | Callback/readback latency | recorder | free |
247
+ | Sub-pass markers | `chromium-experimental-timestamp-query-inside-passes` if present | optional |
248
+
249
+ ### 3.2 Dependencies
250
+
251
+ Straight from `FrameGraph`, enriched:
252
+
253
+ - Passes: id, name, scope chain, culled, `ref_count`, `has_side_effects`, reads/writes/creates.
254
+ - Resource nodes: id, name, version, transient/imported, producer, readers, writers.
255
+ - Per graph pass: the GPU passes it opened, in order — the join from §1.4.
256
+ - Resource lifetime: first write → last read, derivable from `ResourceEntry.last`, which `compile()`
257
+ already computes.
258
+
259
+ ### 3.3 Data sizes
260
+
261
+ Descriptors carry everything:
262
+
263
+ | Resource | Fields | Footprint |
264
+ |---|---|---|
265
+ | Buffer | `size` bytes, `usage` bitmask, `ensure_cleared` range | `size` directly |
266
+ | Texture | `resolution[3]`, `format`, `dimension`, `mipLevelCount`, `sampleCount`, `usage` | computed from `gpu_texture_format_info` — `bytes_per_block`, `block_width`, `block_height` — summed over the mip chain, × `sampleCount` |
267
+
268
+ These are **declared** sizes, not allocated ones; a pooled allocator may serve a larger block.
269
+ `GraphicsContext.gpu_memory_usage` gives the real total per frame — record it as a counter track and
270
+ let the inspector show declared-vs-actual as the aliasing metric it is.
271
+
272
+ ### 3.4 Work sizes
273
+
274
+ | Datum | Needs |
275
+ |---|---|
276
+ | `dispatchWorkgroups(x, y, z)` | proxy encoder |
277
+ | Workgroup size from the shader (`@workgroup_size`) | pipeline descriptor, recorded once per pipeline |
278
+ | Total invocations = groups × workgroup size | derived in the inspector |
279
+ | `draw` / `drawIndexed` vertex, index, instance counts | proxy encoder |
280
+ | Indirect flag + argument buffer id/offset | proxy encoder |
281
+ | Resolved indirect counts | §2.5 v2, opt-in |
282
+ | Bind group contents per dispatch | proxy encoder; **large** — gate behind a verbosity level |
283
+ | Render pass attachment formats and load/store ops | pass descriptor |
284
+
285
+ ### 3.5 The fourth axis — context, so any of it means anything
286
+
287
+ Without this a shared recording is unreadable:
288
+
289
+ - Adapter info (`vendor`, `architecture`, `device`, `description`), features, the limits that matter.
290
+ - Engine version, git revision, build flags, `ENV_PRODUCTION`.
291
+ - Renderer settings: internal vs output resolution, upscaler, which features are on.
292
+ - Nothing about timestamp quantization: see §2.1 for why that is a deliberate absence.
293
+ - Wall-clock start, session id, user-supplied note.
294
+ - Scene scale: mesh/instance/light counts, resident material and geometry bytes.
295
+
296
+ ---
297
+
298
+ ## 4. Architecture
299
+
300
+ ```
301
+ ENGINE (ships) CONTAINER INSPECTOR (published separately)
302
+ ───────────────────── ───────── ───────────────────────────────
303
+ GPUTimerArray ────┐
304
+ FrameGraph ───────┤ ┌── timeline / flame graph
305
+ proxy encoders ───┼──> GPUProfileSession ───> .sgpt ──drop──> ├── dependency graph
306
+ GraphicsContext ──┘ │ bytes ├── resource table
307
+ │ ├── pass detail
308
+ sgpt_write_* └── frame comparison
309
+ │ │
310
+ BinaryBuffer <─────────────────────────── sgpt_read_*
311
+ ```
312
+
313
+ **Three rules that keep this from rotting.**
314
+
315
+ 1. **The container is the only interface.** The inspector imports the format readers and
316
+ `BinaryBuffer`. It never imports Shade, `FrameGraph`, or anything under `src/shade/renderer/`.
317
+ This is a licensing constraint as much as an architectural one — meep is proprietary and
318
+ source-available, and a separately published inspector must not carry renderer internals.
319
+ 2. **The recorder never blocks the frame.** Every buffer it touches is written at encode time or
320
+ read after submit. Serialization runs off the frame — accumulate into per-frame plain records,
321
+ encode to bytes when the session stops or when a chunk fills.
322
+ 3. **Reader and writer live together, in the engine tree.** Under
323
+ `src/shade/device/timing/profile/`, one file per concern, matching the `vgeo_*` convention. The
324
+ inspector package imports them by path. One definition of the format; no drift.
325
+
326
+ ### 4.1 Verbosity levels
327
+
328
+ A capture is a trade between fidelity and cost. Four levels, recorded in the header:
329
+
330
+ | Level | Adds | Bytes/frame, ~200 passes |
331
+ |---|---|---|
332
+ | 0 `TIMING` | GPU pass spans, frame boundaries | ~2 KB |
333
+ | 1 `STRUCTURE` | + frame graph topology, resource descriptors, cull state | ~2 KB amortised (§5.4) |
334
+ | 2 `WORKLOAD` | + dispatch/draw counts, pipeline identities, attachment state | ~6 KB |
335
+ | 3 `VERBOSE` | + bind group contents, indirect readback, CPU per-pass timing | ~40 KB |
336
+
337
+ Levels 0–2 are the product. Level 3 is for us, on a repro, and the inspector should say so when it
338
+ opens one.
339
+
340
+ Because a session is now **uncapped by default** (§5.8), those per-frame figures are also a rate.
341
+ At 60 Hz:
342
+
343
+ | Level | Per second | Per minute |
344
+ |---|---|---|
345
+ | 0 `TIMING` | ~120 KB | ~7 MB |
346
+ | 1 `STRUCTURE` | ~120 KB | ~7 MB |
347
+ | 2 `WORKLOAD` | ~360 KB | ~21 MB |
348
+ | 3 `VERBOSE` | ~2.4 MB | ~144 MB |
349
+
350
+ Levels 0–2 will run for many minutes without anyone noticing. **Level 3 will not**, and the session
351
+ should say so — see the byte-budget warning in §5.8. This is the honest cost of "full and uncapped
352
+ explicit history", and it is the right trade at levels 0–2.
353
+
354
+ ### 4.2 Repository layout
355
+
356
+ The repo becomes an npm workspace root, and the engine moves down a level so the two packages are
357
+ siblings rather than one being nested inside the other.
358
+
359
+ ```
360
+ meep/
361
+ package.json workspace root: { "private": true, "workspaces": ["packages/*"] }
362
+ .gitlab-ci.yml
363
+ LICENSE, README.md, CONTRIBUTING.md, CHANGELOG.md, ...
364
+ packages/
365
+ meep-engine/ @woosh/meep-engine — everything that is published today
366
+ package.json unchanged `exports`, `files`, version
367
+ src/ editor/ samples/ tools/
368
+ rollup.config.js vite.config.mjs vitest.config.mjs tsconfig.types.json babel.config.cjs
369
+ gpu-inspector-tool/ the inspector, published separately
370
+ package.json index.html src/ fixtures/
371
+ ```
372
+
373
+ `@woosh/meep-engine`'s own `exports` map (`./src/*`, `./editor/*`) is relative to its own
374
+ `package.json`, so **consumers installing from npm see no change at all**. The paths that move are
375
+ in-repo and in anything vendoring this tree by path.
376
+
377
+ **The cost is in `moh`, not here.** This tree is vendored into the game at
378
+ `app/src/mir-engine/meep/`, and that is where the restructure is felt: every engine path becomes
379
+ `app/src/mir-engine/meep/packages/meep-engine/src/...`. Two ways to absorb it, and this is the one
380
+ call the restructure actually needs:
381
+
382
+ - **Re-point the vendoring one level deeper** — mount `packages/meep-engine` as
383
+ `app/src/mir-engine/meep`. Every path in `moh` stays byte-identical, and the whole cost collapses
384
+ to one submodule/copy path. **This is the one to take** unless something in `moh` needs the
385
+ workspace root itself.
386
+ - **Update paths in `moh`** — mechanical, wide, and it churns the ~20 design documents in this tree
387
+ that quote `app/src/mir-engine/meep/src/...` as a source root.
388
+
389
+ In-repo edits either way: `vitest.config.mjs` `include`/`coverage.include` globs, `tsconfig.types.json`
390
+ `include`/`rootDir`, `rollup.config.js` `input` paths, `.gitlab-ci.yml` script and artifact paths, and
391
+ `vite.config.mjs`. All of them are prefix changes, and all of them get shorter, not longer, because
392
+ they become relative to the package rather than the root.
393
+
394
+ > Worth knowing before committing: npm lets a workspace root *also* be a package, so
395
+ > `packages/gpu-inspector-tool` could have been added with the engine left exactly where it is and
396
+ > zero paths touched. That option is on the table if the `moh` re-pointing turns out to be more
397
+ > awkward than it looks. Proceeding with the move as decided — it is the cleaner end state and the
398
+ > re-pointing above makes it cheap.
399
+
400
+ ---
401
+
402
+ ## 5. The `.sgpt` container
403
+
404
+ **S**hade **G**PU **P**rofile **T**race. Magic `0x54504753` — the ASCII bytes `S G P T` read as a
405
+ little-endian u32 at offset 0, matching the `VGEO_MAGIC` convention in
406
+ [VGEO_MAGIC.js](packages/meep-engine/src/shade/renderer/geometry/virtual/format/header/VGEO_MAGIC.js).
407
+ `sgpt_fourcc.js` computes these and a spec asserts every literal against it, because hand-deriving
408
+ a little-endian FourCC is exactly the kind of arithmetic that is wrong once and then forever.
409
+
410
+ ### 5.1 Why binary, concretely
411
+
412
+ A 600-frame capture at level 2, ~200 passes/frame, in Chrome Trace Event JSON: each span is roughly
413
+ `{"ph":"X","name":"...","cat":"gpu","ts":123456.7,"dur":234.5,"pid":1,"tid":3,"args":{...}}` — call
414
+ it 140 bytes minified, times 120,000 spans, **~17 MB**, before pass arguments. The same content in
415
+ the layout below is **~1.5 MB**, and gzip (already a dependency via `pako`, and available natively
416
+ via `CompressionStream`) takes it under 400 KB. That is the difference between a capture you attach
417
+ to a bug report and one you do not.
418
+
419
+ ### 5.2 Layout
420
+
421
+ Chunked, directory at the tail — so a writer streams forward and only seeks back once, and a reader
422
+ can pull the header and the frame index without reading the payload.
423
+
424
+ ```
425
+ offset size field
426
+ ──────────────────────────────────────────────────────────────────
427
+ 0 4 magic u32 0x54504753
428
+ 4 2 format_version u16
429
+ 6 2 min_reader_version u16
430
+ 8 4 flags u32 bit0 = CLOSED (writer reached stop)
431
+ 12 4 header_checksum u32 over bytes [0, 8) only — see below
432
+ 16 8 directory_offset u64 0 when the session never stopped
433
+ 24 8 directory_byte_length u64
434
+ 32 ... records, in write order
435
+ ... ... directory record
436
+ ```
437
+
438
+ **The checksum stops at byte 8, short of `flags`.** Everything from `flags` onward is patched at
439
+ stop — the CLOSED bit and the directory pointer — and a checksum covering them would have to be
440
+ recomputed then. Fine for a capture that stops; exactly wrong for one that does not, which would
441
+ carry a checksum over bytes never written and read as corrupt. What stays under it is what a reader
442
+ must trust before it can do anything at all: is this an `.sgpt`, and can this build read it.
443
+
444
+ ### 5.3 Chunks
445
+
446
+ | FourCC | Purpose | Cardinality |
447
+ |---|---|---|
448
+ | `META` | §3.5 context. Adapter, engine version, settings, verbosity level, session note. | 1 |
449
+ | `SYMS` | Symbol block. Names, labels, formats, scope names — emitted **inside the record that first needs them** and referenced by u32 index thereafter, so a truncated capture still resolves every name it references (§5.8). | 0..n |
450
+ | `PIPE` | Pipeline table: id, label, kind, shader module name, `@workgroup_size`, entry point, vertex layout digest. | 1 |
451
+ | `TOPO` | Frame-graph **topologies**: pass list, resource node list, edges, scopes, descriptors. Content-hashed and deduplicated across frames (§5.4). | 0..n |
452
+ | `FRAM` | One per frame: topology id, CPU timestamps, span array, dispatch/draw array, counters. | 0..n |
453
+ | `CNTR` | Counter tracks sampled per frame: `gpu_memory_usage`, mesh/instance/light counts, resolution. | 0..1 |
454
+ | `NOTE` | Free-form user annotations with a frame index — "this is where it hitches". | 0..n |
455
+ | `DIRE` | Directory: `{chunk_type, offset, byte_length}` plus a frame index of `{frame_number, chunk_offset, gpu_duration_ns}` so the inspector can draw the frame-time strip before parsing anything else. | 1 |
456
+
457
+ Unknown chunk types are skipped by length. That is what makes level-3 payloads addable without a
458
+ version bump, and what lets an old inspector open a new capture and say honestly which parts it
459
+ cannot show.
460
+
461
+ ### 5.4 The compression that matters: topology deduplication
462
+
463
+ **A Shade frame graph is nearly identical frame to frame.** Same passes, same resources, same edges;
464
+ what changes is the timings, the cull decisions, and the occasional resolution change. Recording the
465
+ full topology 600 times is the difference between a 20 MB file and a 1.5 MB one.
466
+
467
+ So: hash the recorded topology (pass names, edges, descriptors, scope chain). If the hash matches a
468
+ `TOPO` already written, the frame stores only the topology id. Cull state and per-pass timings stay
469
+ in `FRAM`, because they are exactly what varies.
470
+
471
+ Expected behaviour on a real capture: a handful of distinct topologies over 600 frames — steady
472
+ state, plus the shadow-refresh variants, plus resolution changes. §4.1's "~2 KB amortised" for level
473
+ 1 is that claim.
474
+
475
+ This must be **measured, not assumed**. If topologies turn out to churn every frame, the design
476
+ still works (it degrades to storing each), but the size estimates in §5.1 do not. First milestone
477
+ work item, §8.
478
+
479
+ ### 5.5 Per-frame span encoding
480
+
481
+ Per span, level 0:
482
+
483
+ | Field | Type | Note |
484
+ |---|---|---|
485
+ | `pass_ref` | `uintVar` | index into the topology's GPU-pass list |
486
+ | `t_begin` | `u32` | ns offset from the frame's `gpu_epoch_ns` |
487
+ | `t_end_delta` | `uintVar` | ns from `t_begin` |
488
+ | `query_set_id` | `u8` | §2.4 |
489
+
490
+ `u32` for the frame-relative begin holds 4.29 s — three orders of magnitude of headroom over a
491
+ frame, and it survives a stall without overflowing. `BinaryBuffer.writeUintVar` / `readUintVar`
492
+ already exist and handle the two variable-length fields.
493
+
494
+ That is ~10 bytes per span against 26 for a naive absolute-u64 encoding, and the frame's absolute
495
+ epoch is stored once as a `u64`.
496
+
497
+ ### 5.6 Chrome Trace Event export
498
+
499
+ The inspector exports the loaded capture as Chrome Trace Event JSON, for opening in
500
+ [ui.perfetto.dev](https://perfetto.dev/docs/getting-started/other-formats). Costs an afternoon and
501
+ buys: a second opinion when the inspector looks wrong, a viewer for anyone unwilling to run ours, and
502
+ a sanity check on our own timeline maths. Passes become `X` slices on a GPU track, scopes become
503
+ nesting, counters become `C` events. Lossy — the dependency graph has no representation there — which
504
+ is precisely why it is an export and not the storage format.
505
+
506
+ ### 5.7 String encoding
507
+
508
+ Symbols live in `SYMS` blocks with u32 indices, emitted inside the record that first needs them
509
+ (§5.8). Not `EncodingBinaryBuffer`: it deduplicates by writing back-references to **absolute buffer
510
+ positions** ([EncodingBinaryBuffer.js:21](src/core/binary/EncodingBinaryBuffer.js:21)), which is
511
+ correct for a single flat buffer and wrong the moment records are skipped by a reader that does not
512
+ understand them, resynchronised after corruption, or read from a truncated file — all three of which
513
+ this format is explicitly built to survive. An index table costs one indirection and holds up under
514
+ all of them.
515
+
516
+ ### 5.8 Streaming: self-contained frame records
517
+
518
+ The session is an **open-ended forward stream with an explicit stop**. No ring, no cap: `frame_limit`
519
+ defaults to `Infinity`, `stop()` ends it early, and everything recorded is kept.
520
+
521
+ **A frame is written as one complete, self-describing record and pushed when it closes.** That is
522
+ the organising decision, and three properties follow from it:
523
+
524
+ - **It is resynchronisable.** Every record opens with a sync marker, its type, its payload length and
525
+ a checksum. A reader that lands on garbage scans forward for the next marker and carries on. A
526
+ capture cut off mid-session — tab killed, GPU reset, `device.lost` — stays readable up to the last
527
+ intact record, and those are exactly the captures worth having.
528
+ - **Symbols travel with the record that introduces them.** A record carrying a new topology carries
529
+ the names that topology needs; a frame record reusing a known topology carries none. There is no
530
+ global string table to finalise, which is what would otherwise force a tail chunk and make a
531
+ truncated file unreadable — the failure mode where the one capture of the crash resolves no names.
532
+ - **Nothing is back-patched except the header's directory pointer**, and even that is optional: a
533
+ reader can recover the whole stream by scanning records when the directory is missing or the file
534
+ was truncated before `stop()` ran.
535
+
536
+ Order on the wire:
537
+
538
+ ```
539
+ header magic, versions, flags; directory_offset patched at stop, 0 if never stopped
540
+ META at start — adapter, engine version, renderer settings, level
541
+ TOPO #0 + its symbols first frame's topology
542
+ FRAM #0 topology id + spans + counts
543
+ FRAM #1 same topology → id only
544
+ ...
545
+ TOPO #1 + its symbols shadow refresh changed the graph
546
+ FRAM #57
547
+ ...
548
+ CNTR, DIRE at stop — the directory is an index, not a dependency
549
+ ```
550
+
551
+ Record framing, every record identical:
552
+
553
+ ```
554
+ 0 4 sync u32 0x43455253 ('SREC')
555
+ 4 4 record_type u32 FourCC
556
+ 8 4 payload_byte_length u32
557
+ 12 4 payload_checksum u32 crc32 over the payload
558
+ 16 ... payload
559
+ ```
560
+
561
+ u32 for the length rather than u64: a record describes one frame's control flow, and resource
562
+ *contents* are a non-goal, so nothing in one scales with the size of what it describes. Four
563
+ gigabytes is not a ceiling anything can approach.
564
+
565
+ **The sync marker alone is not enough to resynchronise on**, and the reader does not treat it as
566
+ though it were. Four bytes of payload can spell `SREC` — a pass label could — so a candidate counts
567
+ only when its length also fits and its payload also checksums. There is a spec for exactly that
568
+ case.
569
+
570
+ **Reallocation is a non-issue, and the slack reservation settles what is left of it.** We are
571
+ measuring GPU time, not CPU time; a `setCapacity` copy is a heap allocation and a memcpy that may
572
+ evict some cache, which at worst makes the next GPU upload marginally slower. That is a rounding
573
+ error against what is being measured. To keep even that out of the frame interior, the writer
574
+ reserves at the frame boundary:
575
+
576
+ ```js
577
+ buffer.ensureCapacity(buffer.position + FRAME_SLACK); // FRAME_SLACK = 1 MiB
578
+ ```
579
+
580
+ Any growth then happens between frames, never inside a record. 1 MiB against level 2's ~6 KB per
581
+ frame is three orders of magnitude of headroom, so the reservation only actually forces a grow once
582
+ every ~170 frames. One `BinaryBuffer`, no segmentation, no assembly pass.
583
+
584
+ **Scope note, and it is what makes the above generous rather than marginal: GPU buffer and texture
585
+ contents are a non-goal.** The stream carries control flow and dataflow metadata — passes, edges,
586
+ descriptors, counts, timings. Nothing in it scales with the size of the resources it describes. That
587
+ is why a frame record is kilobytes and why 1 MiB of slack is never the binding constraint.
588
+
589
+ **Uncapped still means uncapped, so the session stays legible about it.** `bytes_written` is readable
590
+ at any time and `onBytesWritten` fires per frame, so an application can show a counter. A configurable
591
+ `byte_budget` — default 512 MB — does not stop the recording; it warns once, naming the level and the
592
+ observed rate. Silent unbounded growth in a debug tool is how you lose a browser tab and the capture
593
+ with it.
594
+
595
+ ---
596
+
597
+ ## 6. The recorder
598
+
599
+ ### 6.1 Surface — the caller owns the session
600
+
601
+ **The engine never constructs a profile session.** The caller builds one and hands it in; the
602
+ renderer holds a nullable reference and nothing more. Nothing in `Renderer` imports the profiler.
603
+
604
+ ```js
605
+ import { GPUProfileSession } from "@woosh/meep-engine/src/shade/device/timing/profile/GPUProfileSession.js";
606
+ import { GPUProfileLevel } from "@woosh/meep-engine/src/shade/device/timing/profile/GPUProfileLevel.js";
607
+
608
+ const session = new GPUProfileSession({
609
+ level: GPUProfileLevel.WORKLOAD,
610
+ frame_limit: Infinity, // default; a number caps it
611
+ note: "hitch on shadow refresh, RTX 3070"
612
+ });
613
+
614
+ renderer.profile_session = session; // nullable field, that is the whole API
615
+
616
+ session.start(); // META is written here; populate meta first
617
+ // ... frames run, for as long as you like ...
618
+ const bytes = await session.stop(); // ArrayBuffer, ready to save
619
+
620
+ renderer.profile_session = null;
621
+ ```
622
+
623
+ Three things fall out of the caller owning it, and all three are why this is the right shape:
624
+
625
+ - **It is genuinely optional.** The profiler is a leaf module nothing in the engine imports. An
626
+ application that never imports `GPUProfileSession` does not have it in its bundle — the answer to
627
+ "does this ship in production builds" is *it ships, and it costs nothing to anyone who does not ask
628
+ for it*. No build flag, no strip-plugin interaction, no dead-code branch to keep honest.
629
+ - **Lifetime is explicit.** A session that outlives a device, or two sessions at once, are the
630
+ caller's problem to not create, and `start()` asserts against both rather than papering over them.
631
+ - **The engine surface is one nullable field.** `Renderer.profile_session`, forwarded to the
632
+ `GraphicsContext` so command contexts can find it. That is the entire integration.
633
+
634
+ `frame_limit` reaching zero stops the session exactly as `stop()` does. Both resolve the same
635
+ promise, so a caller that wants "500 frames or until I say" writes one `await`.
636
+
637
+ ~~`add_debug_frame` and `onFrameDebug` stay exactly as they are — cheap, synchronous, console-shaped,
638
+ and a different tool for a different question.~~
639
+
640
+ > **Reversed 2026-08-31.** They were not a different tool, they were a worse view of the same one
641
+ > reached through a second API — no graph-pass attribution, no workload, no pipeline identity — and
642
+ > `onFrameDebug` never acquired a subscriber. Removed; the profiler is the only path. "Profile the
643
+ > next N frames" is `new GPUProfileSession({ frame_limit: N })`.
644
+
645
+ ### 6.2 The five hooks
646
+
647
+ 1. **`FrameGraph.execute`** — bracket `node.execute(...)`. Notify listeners which graph pass is open;
648
+ take CPU timestamps either side; push/pop a debug group. The only edit outside `src/shade/`.
649
+
650
+ Two `Signal`s on `FrameGraph`, `onPassBegin` / `onPassEnd`, alongside the `onExecuted` that is
651
+ already there. **Signal, not a nullable callback field** — it is the project's standardised
652
+ observer interface, it supports more than one listener, and `remove` is symmetric with `add`.
653
+ A bare nullable callback lets the second consumer silently clobber the first, which is exactly
654
+ the bug that does not announce itself.
655
+
656
+ Guard the dispatch with `hasHandlers()`: `send1` on an empty signal still bumps `generation` and
657
+ walks a Map iterator, and this fires twice per pass — a couple of hundred passes a frame. One
658
+ `Map.size` comparison ahead of it keeps the disabled path honest.
659
+
660
+ `FrameGraph` still learns nothing about what a profiler is; it announces its own pass boundaries
661
+ and the profiler is one possible listener.
662
+ 2. **`ShadeGPUCommandContext`** — a `#profile_sink` field. When set, `beginComputePass` /
663
+ `beginRenderPass` report `{label, kind, graph_pass_id, query_slot, query_set_id}` and wrap the
664
+ returned encoder in the recording proxy.
665
+ 3. **The proxy encoders** — `ProfilingComputePassEncoder`, `ProfilingRenderPassEncoder`. Forward
666
+ everything; record `dispatchWorkgroups*`, `draw*`, `setPipeline`, `setBindGroup`. Modelled on
667
+ `SoftwareGPUComputePassEncoder`, which already records exactly this shape.
668
+ 4. **`GPUTimerArray`** — pooling (§1.2.2), bound checking (§1.2.1), and expose raw slot data rather
669
+ than only the console table.
670
+ 5. **`GraphicsContext`** — sample `gpu_memory_usage` and the collection counters once per frame into
671
+ the counter track.
672
+
673
+ ### 6.3 Cost when off
674
+
675
+ Every hook is a null check against a field that is `null` in normal operation. No allocation and no
676
+ proxy construction; the frame-graph bracket is two comparisons per pass. This must stay true — a
677
+ profiler that costs something when disabled will be disabled at the build level and then rot.
678
+
679
+ Because the session is caller-constructed (§6.1), an application that never imports it pays not even
680
+ that: the profiler modules are unreachable from any engine entry point and drop out of the bundle
681
+ entirely. The null checks are the only residue, and they are the price of the feature existing.
682
+
683
+ ### 6.4 Cost when on
684
+
685
+ Level 0–2 add: one `performance.now()` pair per graph pass, one small record per GPU pass, one per
686
+ dispatch/draw, and a `push`/`popDebugGroup` pair per graph pass. The existing per-context query set
687
+ churn (§1.2.2) is the largest cost and the pool removes it. Expect single-digit percent frame-time
688
+ overhead at level 2; measure it and record the measurement in `META`, so a reader can see how much of
689
+ what they are looking at is the observer.
690
+
691
+ ---
692
+
693
+ ## 7. The inspector
694
+
695
+ `packages/gpu-inspector-tool/`. A static site: drop a `.sgpt` on it, or pass `?file=` for a
696
+ bookmarkable view — the affordance the `vgeo_viewer` playground already establishes in this codebase.
697
+
698
+ ### 7.1 Technology
699
+
700
+ **Plain ES modules, Canvas 2D for the timeline, DOM for panels, no runtime dependencies, built with
701
+ Vite.** Reasons: the repo has no UI framework and adding one for this is unjustified; the timeline is
702
+ a custom-drawn virtualised widget that a framework would only get in the way of; zero dependencies
703
+ keeps a separately published proprietary artifact simple to reason about; and Vite is already the dev
704
+ server here.
705
+
706
+ The only imports from the engine tree are `src/core/binary/BinaryBuffer.js` and the `sgpt_*` readers.
707
+ That boundary is a build-time assertion, not a convention — a lint rule that fails the build on any
708
+ other engine import.
709
+
710
+ ### 7.2 Views
711
+
712
+ | View | Answers |
713
+ |---|---|
714
+ | **Frame strip** | Which frame is interesting. Frame time over the session, GPU and CPU overlaid, hitches marked, brush to select a range. Drawn from the directory index alone, so it appears before the payload finishes parsing. |
715
+ | **Timeline / flame graph** | Where the time went in *this* frame. Spans on a GPU track, nested by `FrameGraphScope`; a CPU track above with encode time; hover for exact ns; click to select. |
716
+ | **Statistics** | Where the time goes *in general*. Per-pass min/median/p95/max/total across the selected frame range, sorted by total contribution. **On a quantized capture this is the only honest view** — and since the tool does not measure quantization (§2.1), it cannot detect that case and switch by itself. It says so plainly instead, always. |
717
+ | **Dependency graph** | Why this pass runs, and what it waits on. Passes and resource nodes, culled ones greyed, edges directed. Select a pass → highlight its transitive inputs. Select a resource → its version chain and every reader. |
718
+ | **Resource table** | What memory costs. Every resource node with declared bytes, format, usage, transient/imported, lifetime span, peak concurrent footprint. Sorted by size. Declared total vs `gpu_memory_usage` side by side. |
719
+ | **Pass detail** | Everything about one pass. Timings across frames as a sparkline, dispatch/draw counts, derived total invocations, pipeline and workgroup size, attachments, bindings at level 3. |
720
+ | **Frame comparison** | What changed. Two frames or two ranges side by side, per-pass deltas sorted by regression. This is the view that makes the tool useful for optimisation work rather than only for diagnosis. |
721
+
722
+ ### 7.3 What it must refuse to do
723
+
724
+ - **Never invent precision the data does not have.** Durations are drawn as reported. Where many
725
+ spans read as exactly zero, that is shown as what it is — a quantized capture — with a note
726
+ naming the browser flag, not smoothed into plausible-looking small numbers.
727
+ - **Never present the CPU and GPU tracks as one clock** (§2.3). Two tracks, anchored per frame,
728
+ labelled as nominal.
729
+ - **Never hide dropped spans.** If the recorder dropped passes on slot exhaustion (§1.2.1), say how
730
+ many, on the frame that dropped them.
731
+
732
+ ---
733
+
734
+ ## 8. Phasing
735
+
736
+ Each milestone is independently useful and independently shippable.
737
+
738
+ ### M0 — Measure the assumptions (½ day)
739
+
740
+ Before any of the below. Instrument one Sponza capture and answer three questions, because three
741
+ size estimates and one whole design decision rest on them:
742
+
743
+ 1. How many distinct frame-graph topologies over 600 frames? (§5.4)
744
+ 2. ~~What is the observed timestamp period~~ — dropped; the tool does not measure this (§2.1).
745
+ 3. How many GPU passes per frame, actually? (`GPUTimerArray` default is 1024 slots; §1.2.1)
746
+
747
+ Write the answers into this document.
748
+
749
+ ### M0.5 — Workspace restructure — **DONE**
750
+
751
+ Landed 2026-08-29. `packages/meep-engine/` holds the engine, the workspace root holds the two
752
+ `.gitignore` halves, the CI paths and a README. Suite green at 2156 files / 14580 tests. One spec
753
+ had to move with it: `meep_three_free.spec.js` asserts the `*.d.ts` ignore rule at the engine root,
754
+ so that rule lives in the package's `.gitignore` rather than the workspace's.
755
+
756
+ **`moh` still needs its vendoring re-pointed one level deeper** — mount `packages/meep-engine` where
757
+ it currently mounts the repository root, and every path inside it stays byte-identical.
758
+
759
+ <details><summary>Original plan</summary>
760
+
761
+ §4.2. Independent of everything else and worth landing first so no profiler work has to be moved
762
+ afterwards. Order: create `packages/meep-engine/`, `git mv` the tree, fix the five config files,
763
+ green the suite, re-point `moh`'s vendoring one level deeper, green `moh`. Land as its own commit —
764
+ a pure move with no content changes, so the diff stays reviewable and a bisect through it is honest.
765
+
766
+ </details>
767
+
768
+ ### M1 — Timing spine — **DONE**
769
+
770
+ Landed:
771
+
772
+ - `FrameGraph.onPassBegin` / `onPassEnd`, guarded by `hasHandlers()`, closed in a `finally` so a
773
+ throwing pass still ends its bracket. Five specs including the exception path.
774
+ - `GPUTimerArray`: bound check (a full array now refuses a slot instead of addressing past its query
775
+ set), `dropped_count`, `traverse_results` for raw `BigInt` timestamps. Seven specs.
776
+ - `ShadeGPUCommandContext`: `#with_timestamp_writes` — copies the descriptor instead of stamping the
777
+ caller's, tolerates a missing descriptor, and leaves `timestampWrites` off when no slot is free.
778
+ - The whole `.sgpt` container: header, framing, resync reader, defect model, symbol interning,
779
+ `META`/`SYMS`/`FRAM`/`DIRE` codecs, `GPUProfileSession`, `snapshot()`. 26 specs.
780
+ - `GPUFrameRecorder` — holds the graph-pass ↔ timer-slot join, accumulates across every command
781
+ context in a frame, and rebases spans onto the frame epoch at close.
782
+
783
+ Removed again on 2026-08-29: a `timestamp_period_ns` field and the GCD estimator that filled it.
784
+ See §2.1 — quantization is documented and left alone.
785
+
786
+ Wired end to end: `Renderer.profile_session`, `GPUFrameRecorder` holding the graph-pass ↔ timer-slot
787
+ join, and `ShadeGPUCommandContext.profiling_absorbed` — which exists because `done` resolves at
788
+ submit, before any timestamp has been read back.
789
+
790
+ The calibration step is gone rather than outstanding: see §2.1.
791
+
792
+ <details><summary>Original plan</summary>
793
+
794
+ `GPUTimerArray` pooling and bound checks; `FrameGraph.onPassBegin`/`onPassEnd` and debug groups;
795
+ `GPUProfileSession` at level 0, caller-constructed, streaming framed records with per-frame slack;
796
+ `META`/`SYMS`/`FRAM`/`DIRE` records; round-trip and truncation-recovery tests on `SoftwareGPUDevice`.
797
+ Deliverable: a `.sgpt` with real spans and no viewer.
798
+
799
+ </details>
800
+
801
+ ### M2 — Inspector skeleton — **DONE**
802
+
803
+ `packages/gpu-inspector-tool`. Plain ES modules, Canvas 2D, no runtime dependencies. Session strip,
804
+ timeline, statistics, pass detail. `make_demo_capture` builds a synthetic capture through the
805
+ engine's own writer, so the tool works without a GPU and the format's two halves are exercised
806
+ against each other.
807
+
808
+ ### M3 — Structure — **DONE**
809
+
810
+ `TOPO` with content-hash deduplication — 50 frames of an unchanged graph come to under 4 KB. Scopes,
811
+ cull state, resource descriptors with footprints computed at record time. The inspector's Structure
812
+ view is two cross-linked tables rather than a node-link diagram: two hundred passes over as many
813
+ resource versions do not lay out into anything readable, and the questions actually asked are local.
814
+
815
+ ### M4 — Workload — **DONE**
816
+
817
+ `make_profiling_pass_encoder` is a `Proxy` rather than a hand-written forwarder: a pass encoder has
818
+ around twenty methods and WebGPU keeps adding them, and one silently dropped would break the
819
+ renderer only while profiling. Dispatch and draw counts, pipeline identity, and `@workgroup_size`
820
+ parsed from WGSL where it is a literal — an override expression records as *unknown*, which is not
821
+ the same as zero.
822
+
823
+ > **Reversed 2026-08-31.** The argument holds only while the wrapper is *conditional*; that is what
824
+ > makes a missing method silent. `ShadeGPUComputePassEncoder` / `ShadeGPURenderPassEncoder` wrap
825
+ > every pass, so a gap throws on the first frame of every build. The workgroup size now comes off
826
+ > `ComputePipelineDescriptor`, which reads it as its code is set, with the parser that resolves
827
+ > `const` references — a size it cannot read refuses to build the descriptor rather than recording
828
+ > as unknown.
829
+
830
+ No `PIPE` table in the end: a pass's pipeline label and workgroup size ride in its own workload
831
+ block, which costs a symbol reference and avoids a second id space.
832
+
833
+ ### M5 — Analysis — **partly done**
834
+
835
+ Statistics view and Chrome Trace Event export are in. Frame comparison and counter tracks are not;
836
+ `CNTR` is reserved in the format for the latter.
837
+
838
+ ### M6 — Publish — **partly done**
839
+
840
+ [`SGPT_FORMAT.md`](profile/SGPT_FORMAT.md) is the normative format specification, written for
841
+ somebody outside the company, with a conformance summary for readers and writers. The inspector's
842
+ README covers recording, levels, and the reading caveats.
843
+
844
+ Outstanding: a hosting target for the built site, and deciding the licence the inspector ships
845
+ under.
846
+
847
+ **Not in scope, listed so it stays that way:** buffer/texture content capture, shader source in the
848
+ container, replay, live attach to a running session, anything requiring a browser extension.
849
+
850
+ ---
851
+
852
+ ## 9. Testing
853
+
854
+ Per the established tiers:
855
+
856
+ | Tier | Covers |
857
+ |---|---|
858
+ | **Unit, node** | Format round-trip: build a synthetic session, encode, decode, assert structural equality. Every chunk. Forward compatibility: a reader skipping an unknown chunk. Truncation and corruption: a bad checksum, a short chunk, a directory pointing past EOF — all must fail with a named error, not a stack trace out of `BinaryBuffer`. |
859
+ | **`SoftwareGPUDevice`** | The recorder end to end without a GPU. The device already emulates `timestamp-query`, query sets and `write_pass_timestamp`, and already refuses a query set over 4096. Encode a frame graph, run a session, decode the output, assert the topology matches what was recorded. This is where the §1.4 attribution logic gets its coverage. |
860
+ | **Playground page** | `src/shade/playground/` gets a capture harness against Sponza, which is also how M0 gets answered. |
861
+ | **Inspector** | Golden captures checked into the package as fixtures. Parse-and-render smoke tests. A capture recorded before a format change must still open — that is what `min_reader_version` is for and it needs a test that proves it. |
862
+
863
+ ---
864
+
865
+ ## 10. Risks
866
+
867
+ | Risk | Severity | Handling |
868
+ |---|---|---|
869
+ | **Timestamp quantization makes per-frame timings unreadable on default browsers** (§2.1) | **High, and deliberately untreated** | Documented, not measured and not corrected — measuring it is the timing attack the mitigation prevents, and measuring it badly skews everything. The inspector names the flag and steers toward cross-frame aggregation, which is the honest reading of a coarse capture. |
870
+ | Topology dedup does not pay off | Medium | M0 measures it. Design degrades gracefully; the size claims do not. |
871
+ | Proxy encoder overhead distorts what it measures | Medium | Level-gate it; record measured overhead in `META`; forward-only methods, no allocation per call. |
872
+ | One `GPUTimerArray` per context means several query sets per frame (§2.4) | Low–Medium | Record `query_set_id`. Consider a per-frame shared array as a follow-up, which would also simplify pooling. |
873
+ | Format churn during development invalidates captures | Low | `min_reader_version` from day one; the inspector reads every version it ever supported; fixtures in the test suite. |
874
+ | Publishing a proprietary-engine tool separately | Low, but real | The §4 rule-1 boundary is what makes this tractable. Enforce it in the build. Confirm the licensing intent for the published inspector before M6. |
875
+ | **Uncapped session exhausts memory on a long level-3 capture** (§4.1, §5.8) | Medium | `bytes_written` readable and signalled; a `byte_budget` that warns rather than truncates. Accepted deliberately — full history is the point, and buffer/texture contents are a non-goal so nothing scales with resource size. |
876
+ | Workspace restructure churns `moh` (§4.2) | Medium | Re-point the vendoring one level deeper so every `moh` path stays identical. Land as a pure move, separately. |
877
+
878
+ ## 11. Decisions taken — 2026-08-29
879
+
880
+ | # | Question | Decision | Where it lives |
881
+ |---|---|---|---|
882
+ | 1 | Monorepo or not | **npm workspaces, and the existing tree moves to conform.** `packages/meep-engine/` + `packages/gpu-inspector-tool/`, symmetric siblings. | §4.2, M0.5 |
883
+ | 2 | Does the recorder ship in production? | **Yes, but optional — the caller instantiates a `GPUProfileSession` and passes it in.** The engine never constructs one, so an application that does not import it does not carry it. | §6.1, §6.3 |
884
+ | 3 | The `FrameGraph.execute` bracket | **Agreed, kept minimal — via `Signal`.** `onPassBegin` / `onPassEnd` beside the existing `onExecuted`, dispatch guarded by `hasHandlers()`. `Signal` is the project's standardised observer interface and a nullable callback field would let a second consumer silently clobber the first. | §1.4, §6.2 hook 1 |
885
+ | 4 | Recording length policy | **Open-ended forward stream of self-contained frame records. `frame_limit` of N (1..∞) with an explicit `stop()`.** No ring buffer — full, uncapped, explicit history is the desired behaviour. Records are self-describing and checksummed, so a truncated capture stays readable; 1 MiB of slack reserved per frame keeps reallocation out of the record interior. | §5.8 |
886
+
887
+ Two consequences of (4) worth carrying forward:
888
+
889
+ - **There is no global string table.** Symbols are emitted inside the record that first needs them
890
+ (`SYMS`, §5.3), not gathered into a `STRS` chunk. A tail table would have made a truncated capture
891
+ resolve no names at all — the failure mode that hits precisely the capture of the crash you were
892
+ trying to record.
893
+ - **Records are framed and checksummed** so a reader can resynchronise. This is what buys corruption
894
+ and truncation resilience, and it costs 20 bytes per frame.