@woosh/meep-engine 3.24.0 → 3.26.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 (349) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +59 -0
  3. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
  4. package/src/core/geom/3d/shape/HeightMapShape3D.js +285 -0
  5. package/src/core/geom/3d/shape/PosedShape3D.d.ts.map +1 -1
  6. package/src/core/geom/3d/shape/PosedShape3D.js +57 -0
  7. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  8. package/src/core/geom/3d/shape/TransformedShape3D.js +102 -0
  9. package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
  10. package/src/core/geom/3d/shape/UnionShape3D.js +64 -0
  11. package/src/engine/graphics3/PickingSystem.d.ts +3 -3
  12. package/src/engine/graphics3/PickingSystem.d.ts.map +1 -1
  13. package/src/engine/graphics3/PickingSystem.js +52 -55
  14. package/src/engine/graphics3/shader_picking_gather.d.ts +40 -0
  15. package/src/engine/graphics3/shader_picking_gather.d.ts.map +1 -0
  16. package/src/engine/graphics3/shader_picking_gather.js +120 -0
  17. package/src/engine/physics/cloth/MEASUREMENTS.md +1632 -0
  18. package/src/engine/physics/cloth/PLAN.md +738 -95
  19. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts +84 -17
  20. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts.map +1 -1
  21. package/src/engine/physics/cloth/collider/ClothColliderIndex.js +451 -42
  22. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.d.ts +52 -0
  23. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.d.ts.map +1 -0
  24. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.js +70 -0
  25. package/src/engine/physics/cloth/ecs/ClothDynamics.d.ts +12 -3
  26. package/src/engine/physics/cloth/ecs/ClothDynamics.d.ts.map +1 -1
  27. package/src/engine/physics/cloth/ecs/ClothDynamics.js +340 -331
  28. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +101 -29
  29. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
  30. package/src/engine/physics/cloth/ecs/ClothInstance.js +110 -31
  31. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.d.ts +84 -0
  32. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.d.ts.map +1 -0
  33. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.js +141 -0
  34. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +71 -81
  35. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
  36. package/src/engine/physics/cloth/ecs/ClothSystem.js +135 -856
  37. package/src/engine/physics/cloth/ecs/ClothWorkerCore.d.ts +48 -0
  38. package/src/engine/physics/cloth/ecs/ClothWorkerCore.d.ts.map +1 -0
  39. package/src/engine/physics/cloth/ecs/ClothWorkerCore.js +107 -0
  40. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts +323 -0
  41. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -0
  42. package/src/engine/physics/cloth/ecs/ClothWorld.js +1248 -0
  43. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts +192 -0
  44. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts.map +1 -0
  45. package/src/engine/physics/cloth/ecs/WorkerClothSystem.js +578 -0
  46. package/src/engine/physics/cloth/ecs/cloth.worker.d.ts +2 -0
  47. package/src/engine/physics/cloth/ecs/cloth.worker.d.ts.map +1 -0
  48. package/src/engine/physics/cloth/ecs/cloth.worker.js +12 -0
  49. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts.map +1 -1
  50. package/src/engine/physics/cloth/ecs/cloth_build_rows.js +2 -1
  51. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts +85 -3
  52. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts.map +1 -1
  53. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.js +366 -271
  54. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts +95 -12
  55. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -1
  56. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +170 -27
  57. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts +19 -0
  58. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts.map +1 -1
  59. package/src/engine/physics/cloth/ecs/cloth_seed_rig.js +249 -201
  60. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts +21 -0
  61. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
  62. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +62 -1
  63. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.d.ts +29 -0
  64. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.d.ts.map +1 -0
  65. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.js +78 -0
  66. package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
  67. package/src/engine/physics/cloth/ecs/cloth_write_back.js +18 -10
  68. package/src/engine/physics/cloth/ecs/makeClothWorker.d.ts +17 -0
  69. package/src/engine/physics/cloth/ecs/makeClothWorker.d.ts.map +1 -0
  70. package/src/engine/physics/cloth/ecs/makeClothWorker.js +18 -0
  71. package/src/engine/physics/cloth/playground/README.md +73 -2
  72. package/src/engine/physics/cloth/playground/collide_main.js +1 -1
  73. package/src/engine/physics/cloth/playground/wind.html +166 -0
  74. package/src/engine/physics/cloth/playground/wind_build.d.ts +81 -0
  75. package/src/engine/physics/cloth/playground/wind_build.d.ts.map +1 -0
  76. package/src/engine/physics/cloth/playground/wind_build.js +141 -0
  77. package/src/engine/physics/cloth/playground/wind_main.d.ts +2 -0
  78. package/src/engine/physics/cloth/playground/wind_main.d.ts.map +1 -0
  79. package/src/engine/physics/cloth/playground/wind_main.js +681 -0
  80. package/src/engine/physics/cloth/solver/ClothState.d.ts +134 -79
  81. package/src/engine/physics/cloth/solver/ClothState.d.ts.map +1 -1
  82. package/src/engine/physics/cloth/solver/ClothState.js +273 -12
  83. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts +29 -8
  84. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -1
  85. package/src/engine/physics/cloth/solver/cloth_contact_find.js +37 -9
  86. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts +45 -2
  87. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts.map +1 -1
  88. package/src/engine/physics/cloth/solver/cloth_solver_constants.js +46 -2
  89. package/src/engine/physics/cloth/solver/cloth_step.d.ts +21 -2
  90. package/src/engine/physics/cloth/solver/cloth_step.d.ts.map +1 -1
  91. package/src/engine/physics/cloth/solver/cloth_step.js +173 -4
  92. package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts +40 -0
  93. package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts.map +1 -0
  94. package/src/engine/physics/cloth/solver/cloth_step_instance.js +284 -0
  95. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.d.ts +107 -0
  96. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.d.ts.map +1 -0
  97. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.js +263 -0
  98. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts +9 -2
  99. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts.map +1 -1
  100. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.js +9 -2
  101. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts +102 -0
  102. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts.map +1 -0
  103. package/src/engine/physics/cloth/wind/AbstractClothWind.js +114 -0
  104. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts +87 -0
  105. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts.map +1 -0
  106. package/src/engine/physics/cloth/wind/ClothAmbientWind.js +185 -0
  107. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts +79 -0
  108. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts.map +1 -0
  109. package/src/engine/physics/cloth/wind/ClothFluidWind.js +213 -0
  110. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts +106 -0
  111. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts.map +1 -0
  112. package/src/engine/physics/cloth/wind/cloth_sample_air.js +297 -0
  113. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  114. package/src/shade/device/mock/texture_copy.d.ts.map +1 -1
  115. package/src/shade/device/mock/texture_copy.js +28 -0
  116. package/src/shade/playground/vgeo_runtime/README.md +94 -0
  117. package/src/shade/playground/vgeo_runtime/index.html +62 -0
  118. package/src/shade/playground/vgeo_runtime/main.d.ts +2 -0
  119. package/src/shade/playground/vgeo_runtime/main.d.ts.map +1 -0
  120. package/src/shade/playground/vgeo_runtime/main.js +399 -0
  121. package/src/shade/playground/vgeo_viewer/sample_asset.js +1 -1
  122. package/src/shade/renderer/camera/Camera.d.ts +14 -0
  123. package/src/shade/renderer/camera/Camera.d.ts.map +1 -1
  124. package/src/shade/renderer/camera/Camera.js +7 -1
  125. package/src/shade/renderer/geometry/GEOMETRY_METADATA_STRUCT.d.ts.map +1 -1
  126. package/src/shade/renderer/geometry/GEOMETRY_METADATA_STRUCT.js +16 -1
  127. package/src/shade/renderer/geometry/GPUGeometryManager.d.ts.map +1 -1
  128. package/src/shade/renderer/geometry/GPUGeometryManager.js +29 -9
  129. package/src/shade/renderer/geometry/GPUGeometryMetadata.d.ts +10 -0
  130. package/src/shade/renderer/geometry/GPUGeometryMetadata.d.ts.map +1 -1
  131. package/src/shade/renderer/geometry/GPUGeometryMetadata.js +13 -0
  132. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +46 -18
  133. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +70 -22
  134. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +97 -19
  135. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +12 -2
  136. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  137. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +14 -3
  138. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts +2 -2
  139. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts.map +1 -1
  140. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.js +124 -6
  141. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts +22 -0
  142. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts.map +1 -0
  143. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.js +53 -0
  144. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts +8 -1
  145. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts.map +1 -1
  146. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.js +8 -1
  147. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.d.ts +16 -0
  148. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.d.ts.map +1 -0
  149. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.js +15 -0
  150. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.d.ts +54 -0
  151. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.d.ts.map +1 -0
  152. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.js +53 -0
  153. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts +13 -2
  154. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts.map +1 -1
  155. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.js +14 -2
  156. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts +100 -0
  157. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts.map +1 -1
  158. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.js +113 -0
  159. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.d.ts.map +1 -1
  160. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.js +5 -0
  161. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -1
  162. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +108 -0
  163. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
  164. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +23 -0
  165. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  166. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +40 -0
  167. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts +228 -0
  168. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts.map +1 -0
  169. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.js +932 -0
  170. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +176 -0
  171. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -0
  172. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +425 -0
  173. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +241 -0
  174. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +574 -0
  175. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts +120 -0
  176. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -0
  177. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +674 -0
  178. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts +287 -0
  179. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts.map +1 -0
  180. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.js +976 -0
  181. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts +42 -0
  182. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -0
  183. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +96 -0
  184. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.d.ts +16 -0
  185. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.d.ts.map +1 -0
  186. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.js +15 -0
  187. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.d.ts +19 -0
  188. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.d.ts.map +1 -0
  189. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.js +83 -0
  190. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.d.ts +9 -0
  191. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.d.ts.map +1 -0
  192. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.js +8 -0
  193. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts +19 -0
  194. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -0
  195. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +160 -0
  196. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts +3 -0
  197. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts.map +1 -0
  198. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.js +255 -0
  199. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.d.ts +3 -0
  200. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.d.ts.map +1 -0
  201. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.js +80 -0
  202. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts +3 -0
  203. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts.map +1 -0
  204. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.js +144 -0
  205. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts +3 -0
  206. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -0
  207. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +530 -0
  208. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts +39 -0
  209. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts.map +1 -0
  210. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.js +54 -0
  211. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.d.ts +41 -0
  212. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.d.ts.map +1 -0
  213. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.js +56 -0
  214. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts +49 -0
  215. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts.map +1 -0
  216. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.js +328 -0
  217. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.d.ts +10 -0
  218. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.d.ts.map +1 -0
  219. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.js +51 -0
  220. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts +13 -0
  221. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts.map +1 -0
  222. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.js +12 -0
  223. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts +17 -0
  224. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts.map +1 -0
  225. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.js +99 -0
  226. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.d.ts +9 -0
  227. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.d.ts.map +1 -0
  228. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.js +8 -0
  229. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.d.ts +7 -0
  230. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.d.ts.map +1 -0
  231. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.js +27 -0
  232. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.d.ts +14 -0
  233. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.d.ts.map +1 -0
  234. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.js +13 -0
  235. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.d.ts +7 -0
  236. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.d.ts.map +1 -0
  237. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.js +35 -0
  238. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.d.ts +9 -0
  239. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.d.ts.map +1 -0
  240. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.js +8 -0
  241. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.d.ts +15 -0
  242. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.d.ts.map +1 -0
  243. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.js +14 -0
  244. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.d.ts +7 -0
  245. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.d.ts.map +1 -0
  246. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.js +36 -0
  247. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts +10 -0
  248. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts.map +1 -0
  249. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.js +9 -0
  250. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.d.ts +11 -0
  251. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.d.ts.map +1 -0
  252. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.js +54 -0
  253. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.d.ts +9 -0
  254. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.d.ts.map +1 -0
  255. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.js +8 -0
  256. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.d.ts +11 -0
  257. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.d.ts.map +1 -0
  258. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.js +10 -0
  259. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.d.ts +7 -0
  260. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.d.ts.map +1 -0
  261. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.js +30 -0
  262. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.d.ts +9 -0
  263. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.d.ts.map +1 -0
  264. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.js +8 -0
  265. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.d.ts +13 -0
  266. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.d.ts.map +1 -0
  267. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.js +12 -0
  268. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.d.ts +14 -0
  269. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.d.ts.map +1 -0
  270. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.js +13 -0
  271. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.d.ts +11 -0
  272. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.d.ts.map +1 -0
  273. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.js +61 -0
  274. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.d.ts +9 -0
  275. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.d.ts.map +1 -0
  276. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.js +8 -0
  277. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.d.ts +13 -0
  278. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.d.ts.map +1 -0
  279. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.js +12 -0
  280. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.d.ts +9 -0
  281. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.d.ts.map +1 -0
  282. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.js +8 -0
  283. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.d.ts +20 -0
  284. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.d.ts.map +1 -0
  285. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.js +19 -0
  286. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts +23 -0
  287. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -0
  288. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +70 -0
  289. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts +18 -0
  290. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts.map +1 -0
  291. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.js +25 -0
  292. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts +35 -0
  293. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts.map +1 -0
  294. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.js +41 -0
  295. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts +32 -0
  296. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts.map +1 -0
  297. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.js +138 -0
  298. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  299. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  300. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  301. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +9 -0
  302. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  303. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +12 -1
  304. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  305. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +12 -1
  306. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  307. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +10 -0
  308. package/src/shade/renderer/scene/GPUSceneContext.d.ts +18 -0
  309. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  310. package/src/shade/renderer/scene/GPUSceneContext.js +647 -594
  311. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts.map +1 -1
  312. package/src/shade/renderer/scene/rows/GPUSceneRows.js +22 -0
  313. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.d.ts +35 -0
  314. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.d.ts.map +1 -0
  315. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.js +91 -0
  316. package/src/engine/graphics3/instantiate_scene_bundle.d.ts +0 -54
  317. package/src/engine/graphics3/instantiate_scene_bundle.d.ts.map +0 -1
  318. package/src/engine/graphics3/shade_node_to_entity_composition.d.ts +0 -39
  319. package/src/engine/graphics3/shade_node_to_entity_composition.d.ts.map +0 -1
  320. package/src/shade/renderer/animation/compute_skin_world_bounds.d.ts +0 -35
  321. package/src/shade/renderer/animation/compute_skin_world_bounds.d.ts.map +0 -1
  322. package/src/shade/renderer/animation/compute_skinned_mesh_clip_bounding_sphere.d.ts +0 -96
  323. package/src/shade/renderer/animation/compute_skinned_mesh_clip_bounding_sphere.d.ts.map +0 -1
  324. package/src/shade/renderer/animation/pose/pose_evaluate_world.d.ts +0 -20
  325. package/src/shade/renderer/animation/pose/pose_evaluate_world.d.ts.map +0 -1
  326. package/src/shade/renderer/animation/skin_bind_transform.d.ts +0 -25
  327. package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +0 -1
  328. package/src/shade/renderer/animation/skin_frame_defect.d.ts +0 -34
  329. package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +0 -1
  330. package/src/shade/renderer/animation/skin_joint_matrix.d.ts +0 -24
  331. package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +0 -1
  332. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +0 -70
  333. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +0 -1
  334. package/src/shade/renderer/buffer/table/GPUReadbackStagingRing.d.ts +0 -96
  335. package/src/shade/renderer/buffer/table/GPUReadbackStagingRing.d.ts.map +0 -1
  336. package/src/shade/renderer/scene/InstanceBatch.d.ts +0 -92
  337. package/src/shade/renderer/scene/InstanceBatch.d.ts.map +0 -1
  338. package/src/shade/renderer/scene/Mesh.d.ts +0 -55
  339. package/src/shade/renderer/scene/Mesh.d.ts.map +0 -1
  340. package/src/shade/renderer/scene/Node3D.d.ts +0 -159
  341. package/src/shade/renderer/scene/Node3D.d.ts.map +0 -1
  342. package/src/shade/renderer/scene/Node3DDirtyList.d.ts +0 -59
  343. package/src/shade/renderer/scene/Node3DDirtyList.d.ts.map +0 -1
  344. package/src/shade/renderer/scene/SceneNodeBinding.d.ts +0 -52
  345. package/src/shade/renderer/scene/SceneNodeBinding.d.ts.map +0 -1
  346. package/src/shade/renderer/scene/SkinnedMesh.d.ts +0 -27
  347. package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +0 -1
  348. package/src/shade/renderer/scene/TransformAuthority.d.ts +0 -19
  349. package/src/shade/renderer/scene/TransformAuthority.d.ts.map +0 -1
@@ -2237,3 +2237,1635 @@ it too, at seed, alongside the subtree walk and the graph colouring that seeding
2237
2237
  already does. Nothing pays it per step.
2238
2238
 
2239
2239
  ---
2240
+
2241
+ ## 38. What M5 proved wrong in PLAN.md
2242
+
2243
+ Four things, and the first two are the same mistake read from two directions:
2244
+ §12 described the worker's memory as a property of the *world*, and it is a
2245
+ property of the *cloth*.
2246
+
2247
+ | # | The design said | The measurement said |
2248
+ |---|---|---|
2249
+ | 0 | §12 — "**One `SharedArrayBuffer` for the whole cloth world**, not one per instance", with "each instance gets a region inside the buffer" | It is one per cloth, and the reseed contract is why. A `ClothState` is sized at seed from that cloth's particle and row counts, and `ClothSystem#reseed` — a chain gaining a link, a `ClothDynamics` change the row tables depend on — builds a new one of a different size while other cloths are mid-flight. A world buffer would need an allocator with relocation, and a relocation invalidates every view of every cloth in it. One buffer per cloth costs one cold-plane message per seed and makes a re-seed a re-registration. §39 |
2250
+ | 1 | §12 — the collider table is a region of the per-cloth layout; `ClothInstance` (M2) — it lives on the instance instead, "because its size is a property of the *world* … §12 puts it in the shared buffer when M5 needs it there" | §12 was right about *where* and M2 was right about *why*, and the two are not in conflict: the size is a property of the world, so the region has a capacity rather than a growth policy. A growable array is memory the worker would have to be told about again mid-run. What the capacity has to be sized against is not a garment but a **scene** — a cloth's table holds every collider on every character standing inside its query box: ten for one body, twenty for two, thirty for three. §41 |
2251
+ | 2 | §12 — "`ClothDynamics` reaches the worker through the command region, written every step" | Every step, yes; `ClothDynamics` , no. By the time the step can use them a cloth's dynamics have been through `cloth_dynamics_map.js` — a retention, a velocity limit, a self-collision radius, gravity rotated into the local frame — and sending the raw component would mean running that mapping a second time on the far side with no way to prove the two agreed. What crosses is the **step's own parameter block**, and `ClothSystem` fills the identical block in a private scratch array. §40 |
2252
+ | 3 | §18 — "**Open — the write-back has no interpolation.** §12 gives cloth `PhysicsSystem`'s producer / `__interp_restore` contract … It lands with M5" | It does not, and the contract named is not the one cloth needs. `Interpolated` is an **entity-keyed marker** — `InterpolationSystem` files snapshots under a key it assigns from the entity id — and a rigged garment's simulated things are `Node3D`s inside a skinned model, which are not entities. Cloth also needs only half of the contract it was pointed at: `__interp_restore` exists to undo render-time writes before the sim reads them, and cloth never reads back what it writes. §43 |
2253
+
2254
+ **And one thing the implementation confirmed rather than corrected**, which is
2255
+ the milestone's own gate. §12: "`ClothSystem` and `WorkerClothSystem` must
2256
+ produce bit-identical results. Identical code over identical data in an
2257
+ identical order." They do, and the number is zero — not at the end of a run but
2258
+ at **every step of one**, over the whole `ClothState` buffer. §40.
2259
+
2260
+ ---
2261
+
2262
+ ## 39. One buffer for the whole cloth world is one buffer per cloth
2263
+
2264
+ §12's opening sentence is the one M5 could not implement as written, and the
2265
+ obstacle is not the worker — it is `ClothSystem#reseed`, which M1 built and M5
2266
+ inherited.
2267
+
2268
+ A `ClothState` is one allocation whose every region is sized from the cloth:
2269
+ particle count, stretch rows, bend rows, leash rows, backstops, colours. Seeding
2270
+ produces it. **Re-seeding produces a different one**, and re-seeding is an
2271
+ ordinary event rather than a teardown: `cloth_dynamics_change_needs_reseed`
2272
+ returns true whenever a field the row tables depend on moves, a chain that gains
2273
+ a link says so through `reseed(entity)`, and a rigged garment re-seeds when its
2274
+ model finishes loading. Each of those changes at least one region's length.
2275
+
2276
+ Putting every cloth in one buffer therefore means an allocator with relocation —
2277
+ and a relocation is not a local event. Every view of every region of the moved
2278
+ cloth has to be re-cut, on both threads, while other cloths in the same buffer
2279
+ have steps in flight against their own regions. The engine has `OffsetAllocator`
2280
+ and it could do this; what it cannot do is make the *re-cut* free, and the
2281
+ re-cut is the part that has to be coordinated across a thread boundary.
2282
+
2283
+ One buffer per cloth costs, instead, exactly one cold-plane message per seed:
2284
+
2285
+ ```
2286
+ add_cloth { id, state: {buffer, six counts}, self_exclusions }
2287
+ remove_cloth { id }
2288
+ ```
2289
+
2290
+ and makes a re-seed a re-registration, which is the same code path as a first
2291
+ seed. The registration is what the worker is waiting for, so a cloth that
2292
+ re-seeds is simply not stepped for the few ticks the message takes — the same
2293
+ window a newly spawned garment already sits in.
2294
+
2295
+ **The layout is not serialized anywhere.** The descriptor carries the buffer and
2296
+ the six counts, and the worker runs the *same* `allocate` the main thread ran, so
2297
+ there is no second description of the layout that could drift from the first.
2298
+ That is worth saying because the alternative — a table of byte offsets in the
2299
+ message — is the obvious implementation and it is the one that rots.
2300
+
2301
+ ### What it cost to move the collider table inside
2302
+
2303
+ M2 left `collider_table` on the instance, growing by doubling. M5 moved it into
2304
+ the state at a fixed `CLOTH_COLLIDER_SLOTS`. The layout change is a `Float64`
2305
+ result region at the head of the buffer (three words, 8-byte aligned where the
2306
+ buffer's own alignment is free) and the table plus its key array inserted before
2307
+ the contact slots.
2308
+
2309
+ Both are on the hot path's critical structure, so the question is whether the
2310
+ step got slower. It did not:
2311
+
2312
+ ```
2313
+ interleaved A/B in one process, per-frame minimum over 1,200 frames
2314
+
2315
+ case HEAD's layout M5's layout ratio states agree
2316
+ 400p 2x4 1.100 ms 1.082 ms 0.983 bit-identical
2317
+ 400p 2x4 + self 1.454 ms 1.399 ms 0.962 bit-identical
2318
+ 576p 2x2 0.847 ms 0.803 ms 0.949 bit-identical
2319
+ 1024p 2x4 4.582 ms 4.487 ms 0.979 bit-identical
2320
+ ```
2321
+
2322
+ 0.95 to 0.98 is not a speed-up; it is noise with a sign. The bit-identical
2323
+ column is the load-bearing one: the two states are built from the same rest pose
2324
+ and stepped with the same inputs, and they stay equal to the last bit for 1,600
2325
+ steps, so the layout change moved bytes and nothing else.
2326
+
2327
+ ### Two things about measuring it that cost an hour
2328
+
2329
+ **The machine was not idle and could not be made idle.** Nineteen node processes
2330
+ and 30–38% background CPU throughout — other work in the same checkout. M4's own
2331
+ lesson says a timing run needs a quiet machine; what M4 did not have to answer is
2332
+ what to do when the machine will not go quiet. The answer is to stop measuring
2333
+ absolutes and measure a **ratio from interleaved samples**: whatever the rest of
2334
+ the machine is doing, it is doing it to both variants inside the same frame.
2335
+
2336
+ **And an interleaved A/B has to alternate its order**, which the first version of
2337
+ this harness did not. Running A then B every frame charges A for everything the
2338
+ rest of the machine evicted between frames, and B reads the cache A just warmed.
2339
+ The bias is not small:
2340
+
2341
+ ```
2342
+ ratio, M5 against HEAD A always first alternating
2343
+ 400p 2x4 0.693 0.983
2344
+ 400p 2x4 + self 0.738 0.962
2345
+ 576p 2x2 0.662 0.949
2346
+ 1024p 2x4 0.882 0.979
2347
+ ```
2348
+
2349
+ A 30% "improvement" that is entirely an artifact of going second. Anyone
2350
+ re-running a paired measurement in this repo should alternate.
2351
+
2352
+ ---
2353
+
2354
+ ## 40. The parameter block is the protocol, and that is why the parity is structural
2355
+
2356
+ §12 asks for bit-identical results between the two systems and calls the
2357
+ mechanism "identical code over identical data in an identical order". M3 met the
2358
+ same bar between the entity and rig paths (§25) by making the row builder and
2359
+ the pose reconstruction **one function each** rather than two that agreed. M5
2360
+ does the same thing one level down, and the shape it takes is a block of words.
2361
+
2362
+ `cloth_step_instance(state, exclusions, params, offset)` reads every number it
2363
+ needs out of a `Float64Array`: `dt`, gravity already rotated into the cloth's
2364
+ local frame, the substep and iteration counts, the damping retention, the
2365
+ velocity clamp, the contact radius, the self-collision radius, the collider
2366
+ count, and whether self-collision is on at all. `ClothSystem` fills that block in
2367
+ a thirteen-word private scratch array and calls the function. `WorkerClothSystem`
2368
+ fills a thirteen-word slice of the shared command region and the worker calls the
2369
+ function. **Neither system computes a step parameter of its own**, so neither can
2370
+ compute one differently.
2371
+
2372
+ `Float64` throughout the block is load-bearing rather than incidental. Every
2373
+ number in it is produced in double on the main thread and consumed in double by
2374
+ the step, so the round trip through shared memory is exact. An `f32` command
2375
+ region would round the velocity clamp, and a clamp that rounds is a velocity that
2376
+ rounds.
2377
+
2378
+ The same argument decided where the *results* live. The step answers with three
2379
+ scalars — the largest speed left, which the sleep test reads, and two diagnostic
2380
+ counts — and a `ClothState` attached over shared memory in a worker is a
2381
+ different JavaScript object over the same bytes, so a plain field on it is
2382
+ invisible to the main thread. They go in the buffer, in `Float64`, at the head
2383
+ where the alignment is free. `Float32` would have been enough for a diagnostic
2384
+ count and not for the speed: it is compared against `CLOTH_SLEEP_SPEED_RATIO ·
2385
+ reach`, and an f32 round on one side of that comparison and not the other is a
2386
+ cloth that falls asleep one step earlier than its twin and diverges from there.
2387
+
2388
+ ### The number
2389
+
2390
+ ```
2391
+ 240 steps, a 10-link rope, anchor walking a sine with a turn in it
2392
+ 180 steps, three 10-link strands in cotton, two colliders, one of them moving
2393
+
2394
+ differing bytes in ClothState's buffer, per step: 0
2395
+ differing bytes at the end of the run: 0
2396
+ ```
2397
+
2398
+ **Compared every step, not at the end.** A divergence that appears at step 200
2399
+ and one that appears at step 2 read the same from the end of a run and they are
2400
+ very different bugs — the first is drift, the second is a wrong parameter. The
2401
+ comparison is the whole buffer rather than selected fields: particle positions
2402
+ and velocities, every dual and penalty, the collider table, the contact slots and
2403
+ their warm-started multipliers, the self-collision pair list, and the step's own
2404
+ result words.
2405
+
2406
+ The second run is there because the first never takes two of the branches:
2407
+ `cloth_contact_find` with records in the table, and `cloth_self_find` at all.
2408
+ Cotton is one of §33's three entries that set `ClothDynamicsFlags.SelfCollision`,
2409
+ and the run is self-checked — it asserts that the collider table filled, that
2410
+ contact rows were created, and that self-collision pairs were found — because a
2411
+ parity test over a branch that did not run is a parity test over nothing.
2412
+
2413
+ ### What the write-back does, which is not identical and is not meant to be
2414
+
2415
+ The scene's poses lag by one tick. At tick `T` the worker system joins step
2416
+ `T − 1` and writes it back, then dispatches step `T`. The spec pins that exactly:
2417
+ the local transforms the worker system writes at tick `T` equal the ones
2418
+ `ClothSystem` wrote at tick `T − 1`, every tick, for the whole run.
2419
+
2420
+ **The local transforms rather than the world ones**, and the reason is a sentence
2421
+ M1 already wrote. A node's world pose is a function of its local pose *and of the
2422
+ anchor's pose now*, so two scenes whose steps are one tick apart show the same
2423
+ shape in different places: "the cloth's *shape* is one fixed step stale; its
2424
+ *position* is not, because the attachment cascade carries the whole subtree
2425
+ rigidly with the anchor". §4.1 says that about running cloth before the poser.
2426
+ The worker's one-tick lag is the same statement with the same consequence, which
2427
+ is a reassuring place for it to land: the quality cost of the worker is a cost
2428
+ the design had already priced.
2429
+
2430
+ ---
2431
+
2432
+ ## 41. What the main thread stops paying for
2433
+
2434
+ §17 asks M5 for "a measured main-thread cost reduction". The loopback is what
2435
+ makes it measurable: in production the worker's step and the main thread's tick
2436
+ overlap, but under `SystemWorkerLoopback` they are two calls on one thread, so
2437
+ `em.update()` is *exactly* what the main thread pays and `runPendingStep()` is
2438
+ *exactly* what moved.
2439
+
2440
+ ```
2441
+ 400 warm-up ticks then 900 measured, 3 rounds, minimum of the minima
2442
+ a garment as an entity subtree, anchor walking; "+ body" is a ten-part
2443
+ character collider set in the scene
2444
+
2445
+ workload ClothSystem WorkerClothSystem the worker's main thread
2446
+ main thread main thread step saved
2447
+
2448
+ COTTON 1024p 3.623 ms 0.181 ms 4.084 ms 95.0%
2449
+ COTTON 1024p + body 5.222 ms 0.206 ms 4.845 ms 96.1%
2450
+ SILK 400p 1.666 ms 0.059 ms 1.488 ms 96.5%
2451
+ SILK 400p + body 1.853 ms 0.066 ms 1.739 ms 96.4%
2452
+ DENIM 576p 1.106 ms 0.081 ms 0.992 ms 92.7%
2453
+ ROPE 40 links 0.130 ms 0.006 ms 0.129 ms 95.5%
2454
+ ```
2455
+
2456
+ **92% to 96%**, and the figure is not a surprise — `MEASUREMENTS.md` §7 put 69%
2457
+ of a frame in the stretch rows and 18% in the bend rows, and those are what
2458
+ crossed. What is worth stating is what is *left* on the main thread: the anchor's
2459
+ pose, the local-frame carry, the leash centres, the broadphase query and its
2460
+ rewrite into the cloth's frame, the sleep test, and the write-back. All of it
2461
+ reads components, walks hierarchies or queries an index, and all of it together
2462
+ is a fifth of a millisecond for a thousand-particle garment.
2463
+
2464
+ The columns are minima taken from separate runs on a machine carrying other work,
2465
+ so they do not add up exactly — the 1024p inline minimum landed below the
2466
+ worker's own step minimum, which is impossible within one scene and ordinary
2467
+ across two. The ratio is the result; the absolute column is an upper bound.
2468
+
2469
+ **Note what this does not do**, and §17 already says it: the worker moves the
2470
+ cost, it does not reduce it. "2 ms on the worker" with several characters in
2471
+ flight is a scheduling question, and `WorkerSystem` answers it with one step in
2472
+ flight at a time and a fixed-step gate — so the budget §17 sets is now a budget
2473
+ for *all the cloth in the scene together*, on one thread, per tick. What changed
2474
+ is that it no longer competes with the renderer for the same thread.
2475
+
2476
+ ### How full the per-cloth collider table gets
2477
+
2478
+ This is the measurement that sized `CLOTH_COLLIDER_SLOTS`, and it moved the
2479
+ number, because it showed the table is bounded by the **scene** rather than by
2480
+ the garment:
2481
+
2482
+ ```
2483
+ 600 ticks, a garment walking through a scene of ten-part character bodies
2484
+
2485
+ garment colliders in the scene worst mean capacity
2486
+ SILK 10 10 10.0 64
2487
+ COTTON 10 10 10.0 64
2488
+ DENIM 10 8 7.7 64
2489
+ ROPE 10 10 9.0 64
2490
+ COTTON 20 20 19.1 64
2491
+ COTTON 30 30 24.9 64
2492
+ ```
2493
+
2494
+ A garment's query box is its own particle bounds fattened by a step's travel, and
2495
+ a garment is worn *on* a body — so what lands in the table is every collider that
2496
+ body carries, and every collider carried by anyone standing close enough to touch
2497
+ it. One character fills ten slots. Three fill thirty. There is no garment-shaped
2498
+ number here at all.
2499
+
2500
+ Thirty-two was the first choice, against M2's twenty-collider scene, and three
2501
+ characters would have been inside 88% of it with an **arbitrary** drop past that
2502
+ — `ClothColliderIndex#query` answers in its own order, not in order of how much a
2503
+ collider matters, so an overflow discards colliders the cloth may be resting on.
2504
+ Sixty-four is six characters, nine kilobytes a cloth, and 1.4% of the §17
2505
+ workload's state — the same order as the 16% §33 already spends on
2506
+ self-collision storage that four of seven entries never read. The drop still
2507
+ warns once, because the capacity is a bound and not a rule.
2508
+
2509
+ ---
2510
+
2511
+ ## 42. The library, re-measured, and why no entry is re-sized
2512
+
2513
+ §17 says to re-measure at M5 and re-size anything that drifted outside. The
2514
+ absolute table taken with `cloth_library_bench.mjs` reads 15% to 25% above M4's:
2515
+
2516
+ ```
2517
+ M4's §33 M5's run entry
2518
+ 1.427 → 1.802 1.641 → 2.069 SILK 2x4
2519
+ 1.478 → 1.802 1.547 → 2.002 COTTON 2x4
2520
+ 1.297 → 1.765 1.458 → 2.020 DENIM 2x2
2521
+ 1.315 → 1.828 1.435 → 2.039 LEATHER 2x2
2522
+ 0.116 → 0.129 0.126 → 0.133 ROPE 2x4
2523
+ 0.165 → 0.183 0.183 → 0.215 CHAIN 2x6
2524
+ 1.438 → 1.798 1.571 → 1.997 HAIR 2x4
2525
+ ```
2526
+
2527
+ **That is the machine, not the milestone**, and the paired measurement in §39 is
2528
+ what says so. `cloth_library_bench.mjs` does not go anywhere near
2529
+ `WorkerClothSystem` — it calls `cloth_self_find` and `cloth_step` directly over a
2530
+ `ClothState`, so the only thing M5 changed that it can see is the buffer layout,
2531
+ and that is 0.95 to 0.98 with the states staying bit-identical. Every one of
2532
+ these entries is 400 to 576 particles at 2×4 or 2×2, which are exactly the four
2533
+ shapes §39 paired.
2534
+
2535
+ The run was taken with nineteen other node processes on the machine and 30–38%
2536
+ background CPU, which M4's own lesson predicts will cost 9% to 29%. It cost 15%
2537
+ to 25%. **No entry is re-sized**: re-sizing a library on numbers taken from a
2538
+ busy machine would bake someone else's test run into a shipped garment.
2539
+
2540
+ What the measurement *is* good for is the shape, and the shape is unchanged: the
2541
+ three entries that set `ClothDynamicsFlags.SelfCollision` are still the tightest,
2542
+ the chains still find no pairs at all, and leather still pays the most for the
2543
+ fewest. §33's ordering stands entry for entry.
2544
+
2545
+ **The thing to re-measure on a quiet machine** is the absolute column, and the
2546
+ question it answers is no longer "does a garment fit in a frame" but "does all
2547
+ the cloth in a scene fit in a worker tick" — §41's last paragraph. Whoever does
2548
+ it should check the process count first; this section exists so they know to.
2549
+
2550
+ ---
2551
+
2552
+ ## 43. Interpolation: the named contract cannot express a rigged garment
2553
+
2554
+ §18 carried "the write-back has no interpolation" as open and said it lands with
2555
+ M5, "where the worker makes the fixed-step producer contract load-bearing for a
2556
+ second reason". The second reason is real and it points the other way: the worker
2557
+ is what makes the simple implementation impossible.
2558
+
2559
+ **Three findings, in the order they change the answer.**
2560
+
2561
+ **1. `Interpolated` is entity-keyed, and a garment's joints are not entities.**
2562
+ The contract §13's reuse ledger points at is `PhysicsSystem`'s: an `Interpolated`
2563
+ marker component per interpolated body, carrying a `key` the
2564
+ `InterpolationSystem` assigns from the entity id at link time, and a list of
2565
+ interpolands each naming a component class and its serialization adapter. That
2566
+ works for a rope, whose links are entities. It cannot express a **rigged**
2567
+ garment at all: M3's write-back drives `Node3D.transform_local` on the joints of
2568
+ a skinned model, and `shade_node_to_entity_composition` does not expand a skinned
2569
+ node into entities — "that is a boundary rather than an omission" (D47, quoted in
2570
+ §3). There is no entity to put the component on and no key to file the snapshot
2571
+ under. §13's ledger row is wrong for half the feature.
2572
+
2573
+ **2. Cloth needs half the contract, and it is the half with no name.**
2574
+ `__interp_restore` exists because the render-time blend writes into the very
2575
+ components the simulation reads, so the sim has to be handed its authoritative
2576
+ pose back before it integrates. Cloth's simulation state is `ClothState.position`
2577
+ in the cloth's local frame, and `cloth_write_back` writes nodes `1` upward —
2578
+ never the anchor, which is the one transform it reads. **Cloth never reads back
2579
+ what it writes**, so there is nothing to restore. It needs the record/publish
2580
+ half and none of the restore half, and it gets the restore it *does* depend on
2581
+ for free: an anchor on a physics-driven character is restored by
2582
+ `PhysicsSystem.__interp_restore` before cloth reads it.
2583
+
2584
+ **3. The worker takes the cheap implementation away.** Without it, a render-time
2585
+ pass could read `ClothState.position`, reconstruct the pose and blend. With it,
2586
+ that memory is owned by the worker from dispatch until the next tick's join —
2587
+ which is essentially all of the time outside the fixed pass. An interpolating
2588
+ cloth therefore needs a **published pose slot**: the last two steps' node poses,
2589
+ copied out on the main thread at join, blended at render time from the copy. That
2590
+ is the same double-buffered publish slot `WorkerFluidSystem` documents as the
2591
+ unbuilt prerequisite for its own render-time reads, and it is the honest cost of
2592
+ the feature rather than an implementation detail of it.
2593
+
2594
+ ### What it would cost, and what it would buy
2595
+
2596
+ The slot is nearly free to *fill*, because `cloth_write_back` already computes
2597
+ every node's local TRS and would only have to keep it. What costs is the
2598
+ per-frame pass, which is a pose per node blended and written — strictly less work
2599
+ than the write-back, because it reads two stored poses instead of reconstructing
2600
+ one from particles, so the write-back's own cost is an upper bound on it:
2601
+
2602
+ ```
2603
+ write-back, 401 nodes 0.0487 ms
2604
+ ```
2605
+
2606
+ The arithmetic, spelled out, because the first draft of this section guessed at
2607
+ it and was wrong by a factor of two. A blend pass runs **every frame**, where the
2608
+ write-back runs every tick, and the write-back keeps running either way. So on a
2609
+ 165 Hz display against a 60 Hz fixed step:
2610
+
2611
+ ```
2612
+ 400 nodes 1,024 nodes (§17)
2613
+ a blend pass, per frame 0.049 ms 0.124 ms
2614
+ ... at 165 frames a second 8.1 ms/s 20.5 ms/s
2615
+
2616
+ what the main thread pays today 3.5 ms/s 10.9 ms/s (§41, x60)
2617
+ what it paid before M5 100.0 ms/s 217.4 ms/s (§41, x60)
2618
+ ```
2619
+
2620
+ **Interpolating a cloth would roughly triple what the main thread pays for it**
2621
+ — and leave it at a seventh of what it paid before the worker. That is the shape
2622
+ of the trade: the feature is affordable *because* M5 happened, and it is not free.
2623
+
2624
+ What it buys is the size of the hold, measured on M3's cape geometry — five
2625
+ strips of 8 cm links, 8.5 cm apart, which is the garment §37 measured:
2626
+
2627
+ ```
2628
+ 600 steps past settling; the largest world-space distance a node covers in one
2629
+ fixed step
2630
+
2631
+ anchor worst mean worst, in link lengths
2632
+ still 46.74 mm 12.51 mm 0.58
2633
+ walk 37.90 mm 27.98 mm 0.47
2634
+ sprint 106.71 mm 67.42 mm 1.33
2635
+ ```
2636
+
2637
+ Half a link length a step at a walk, and a whole one at a sprint. That is
2638
+ visible, and it is the *size* of what a display running faster than the fixed
2639
+ step shows as a hold — 105 of every 165 frames repeating a pose in the general
2640
+ case.
2641
+
2642
+ **The decision: it does not land with M5.** §17's M5 is four artifacts and three
2643
+ proofs and interpolation is none of them; the feature it turns out to need is a
2644
+ publish slot, a second system, an ordering contract with `InterpolationSystem`,
2645
+ and a way to name a `Node3D` that the engine's interpolation layer does not have.
2646
+ Landing a worker and a half-built interpolator together would have been the thing
2647
+ §17 asks milestones not to do. What has changed is that the open question is no
2648
+ longer "wire the existing contract": it is "build the cloth-shaped half of a
2649
+ contract whose entity-shaped half does not fit", and §18 now says so with the
2650
+ three findings above beside it.
2651
+
2652
+ The one piece of good news is that the cloth-shaped version is *simpler* than the
2653
+ engine's, not harder: no restore pass, no serialization adapters, no log — two
2654
+ poses per node and a lerp, driven by the same `alpha` `EntityManager` already
2655
+ exposes for the purpose, and it covers the rigged path and the entity path
2656
+ identically because both of them end in a pose per node.
2657
+
2658
+ ---
2659
+
2660
+ ## 44. Two things M5 deliberately did not build
2661
+
2662
+ **A playground page.** M0 through M4 each added one and M5 does not, for a
2663
+ reason that is about the dev server rather than about the cloth: a
2664
+ `SharedArrayBuffer` needs a cross-origin isolated page, and
2665
+ `packages/meep-engine/vite.config.mjs` sends no `Cross-Origin-Opener-Policy` or
2666
+ `Cross-Origin-Embedder-Policy`. Adding them is four lines and it is four lines
2667
+ that change how **every** playground page in the package loads its subresources,
2668
+ including the Sponza path-tracer pages under `src/shade/playground/`. That is a
2669
+ change to shared infrastructure to demonstrate a bridge that
2670
+ `SystemWorkerLoopback` already tests more precisely than a page could — the
2671
+ loopback makes message delivery and step execution separate events a spec can
2672
+ order, and a real worker makes them a race nobody can observe.
2673
+
2674
+ **A second step in flight.** `WorkerSystem` allows exactly one, for reasons its
2675
+ own docblock gives, and cloth has no reason to want more: the fixed-step gate
2676
+ means a cloth's step must complete before the next tick begins regardless.
2677
+
2678
+ ---
2679
+
2680
+ # Cloth — M6 measurements
2681
+
2682
+ > Written 2026-09-12, against the M6 implementation in `wind/`, `solver/` and
2683
+ > `ecs/`. Everything above this line is M0's through M5's and is unchanged.
2684
+ >
2685
+ > Two scripts, both runnable directly under node — the M0 note applies unchanged:
2686
+ >
2687
+ > ```
2688
+ > node src/engine/physics/cloth/ecs/__fixtures__/cloth_m6_measure.mjs
2689
+ > node src/engine/physics/cloth/ecs/__fixtures__/cloth_library_bench.mjs
2690
+ > ```
2691
+ >
2692
+ > The second is M1's, extended here with a section that prices §10's aerodynamic
2693
+ > term against each entry's own size and budget beside the one M4 added for
2694
+ > self-collision.
2695
+ >
2696
+ > **The behavioural numbers come off the playground**, driven from its own
2697
+ > `globalThis.cloth_wind_playground` handle rather than clicked, because the
2698
+ > question is what a banner does over four hundred settled steps and a screenshot
2699
+ > cannot answer that.
2700
+
2701
+ ---
2702
+
2703
+ ## 45. What M6 proved wrong in PLAN.md
2704
+
2705
+ Five things. The first is the one the milestone is *about* — §10 names a
2706
+ quantity that half the system cannot have — and the second is the one that would
2707
+ have shipped a garment blowing at a speed that depended on how finely someone
2708
+ had voxelised the room.
2709
+
2710
+ | # | The design said | The measurement said |
2711
+ |---|---|---|
2712
+ | 0 | §10 — `a_drag = drag · (u_air − v) · \|n·û\|`, "the normal-facing factor making a sheet catch the wind broadside and slice through it edge-on — the difference between a flag and a rope" | The **rope has no `n`**. A chain's particles span a line and every direction perpendicular to it is equally a normal, so any one this code picked would be the history-free fabrication §7 P4(a) exists to keep out — and a rope wants the *opposite* dependence anyway, catching the flow across it and slicing along it. Both are one quantity: the part of the flow the particle's own incident rows cannot let past. Rank 2 gives §10's `\|û·n\|` exactly, rank 1 gives the cross-flow, rank 0 gives 1. §47 |
2713
+ | 1 | §10 — "a cloth overlapping a `FluidComponent` **samples its velocity field** per particle" | It samples it and then has to **convert it**. `FluidField#sampleVelocity` answers in *grid cells* per second and documents that it deliberately does not rescale, "on the assumption that callers (vegetation sway, dust drift) interpret the field as a direction × magnitude wind". Cloth's drag row is a relaxation toward a velocity, so without the multiply by `cell_size` the same wind voxelised at 0.25 m instead of 1 m blows a garment four times as hard. §46 |
2714
+ | 2 | §10 — the whole section, which never says **which thread** samples | It cannot be the worker's. A fluid field is a hundred thousand floats its own solver rewrites every step and may itself live on another worker; the *answer* is three floats a particle. So the sample runs on the main thread and lands in a region of the cloth's own `SharedArrayBuffer`, where the worker already reads — nothing is copied — and everything built from it runs in `cloth_step_instance` with the rest of the arithmetic, which is what keeps §40's parity argument intact. §46 |
2715
+ | 3 | §10 — `drag` multiplies the relative flow directly | What the step multiplies by cannot be a rate. One acceleration is held for the whole fixed step, so an explicit `rate·(u − v)` overshoots the air once `rate·dt > 1` and diverges past 2 — and §1's first commitment is "not at 5 fps", which is a `dt` of 0.2 s and a rate of 10. What crosses is the **per-step response** `(1 − e^(−rate·dt))/dt`, the exact solution of the same ODE, which closes a fraction of the gap that is strictly below one for every rate and every step. It is also the step `GlobalFluidEffector` takes for the air itself. §49 |
2716
+ | 4 | §12 — "thirteen words into memory the worker already shares"; §10 — "sleep wakes on a wind change" | Fourteen now, and the gust needs a clock that counts **steps rather than ticks**. A worker cloth is not advanced until its registration is acknowledged (§39), so the two systems reach a cloth's first step at different tick counts — and a clock that counted ticks would hand that cloth a different gust on the same step under the two systems. That is a divergence the parity gate reports, and it is the only thing in M6 that did. §50 |
2717
+
2718
+ **And one thing the implementation found that the design had no line for at
2719
+ all.** §10's normal-facing factor is what makes a flag a flag, and it does —
2720
+ measured on the playground's banner, turning a *uniform* wind ninety degrees
2721
+ takes the mean facing factor from 0.221 to **0.000** and the mean aerodynamic
2722
+ acceleration from 43.2 m/s² to 0.08. Turning a **wake** ninety degrees does
2723
+ nothing of the kind: 0.216 to 0.280. A wake has velocity in every direction, so
2724
+ there is no orientation of a banner that is edge-on to one. §10's argument for
2725
+ reading a fluid simulation is therefore stronger than §10 knew — the simulation
2726
+ knows something the factor's own intuition does not — and the factor's headline
2727
+ claim is a statement about *laminar* air. §51.
2728
+
2729
+ ---
2730
+
2731
+ ## 46. Which side of the worker bridge, and the units nobody had converted
2732
+
2733
+ §10 describes what cloth does with the air and never says where the reading
2734
+ happens, which was fine until M5 put the arithmetic on a thread. M6 has to
2735
+ answer it, and both answers cost something real: sampling on the main thread
2736
+ gives back part of what M5 bought, and sampling in the worker means the field
2737
+ has to get there.
2738
+
2739
+ **The field cannot get there.** A `FluidComponent` is not a parameter, it is a
2740
+ simulation:
2741
+
2742
+ ```
2743
+ a 32³ fluid field, the size a room-sized volume wants
2744
+
2745
+ velocity_x 33 x 32 x 32 33,792 floats
2746
+ velocity_y 32 x 33 x 32 33,792
2747
+ velocity_z 32 x 32 x 33 33,792
2748
+ 101,376 floats = 396 KB
2749
+
2750
+ rewritten, every one of them, every fixed step, by a solver that may itself
2751
+ already be on a different worker (`WorkerFluidSystem`)
2752
+ ```
2753
+
2754
+ Against a cloth step that is 1 to 4 ms and a main-thread share that M5 got down
2755
+ to 0.18 ms, shipping 396 KB a step to feed it is not a trade, it is the feature
2756
+ costing more than the thing it feeds. And there is a second objection that is
2757
+ structural rather than arithmetic: `ClothSystem` would have to sample the field
2758
+ too, so the field would have to be on **both** sides and the two copies would
2759
+ have to be proved equal — which is exactly the shape `MEASUREMENTS.md` §40 says
2760
+ breaks the parity gate.
2761
+
2762
+ **So the field stays where it is and the answer crosses.** `cloth_sample_air`
2763
+ runs on the main thread in both systems, writes one air velocity per particle
2764
+ into `ClothState#air_velocity` — a region of the cloth's own buffer — and the
2765
+ step reads it there. Nothing is copied and nothing is marshalled: §39's "one
2766
+ buffer per cloth" already made that memory shared, and this is the first thing
2767
+ to use it in the main-thread-writes direction.
2768
+
2769
+ The **drag arithmetic** is not on the main thread. It reads positions and
2770
+ velocities the state already holds, so it belongs with the rest of the
2771
+ arithmetic, and it is one more word of the parameter block rather than a second
2772
+ computation. §49 is what it costs; §48 is what the sample costs.
2773
+
2774
+ ### The field answers in cells, and cloth wants metres
2775
+
2776
+ `FluidField#sampleVelocity` is trilinear over a MAC grid and answers in **grid
2777
+ cells per second**. Its docblock says so and says why: it "does NOT rescale
2778
+ them, on the assumption that callers (vegetation sway, dust drift) interpret the
2779
+ field as a direction × magnitude wind and want consistent strength regardless of
2780
+ the underlying cell size".
2781
+
2782
+ Cloth is not such a caller. §10's row is a relaxation toward a *velocity*, and a
2783
+ velocity in cells is a garment that blows differently depending on how finely
2784
+ someone voxelised the room:
2785
+
2786
+ ```
2787
+ the same 3-cells-per-second flow, sampled by ClothFluidWind
2788
+
2789
+ cell size what the field says what the cloth is handed
2790
+ 2.00 m 3 cells/s 6.0 m/s
2791
+ 0.50 m 3 cells/s 1.5 m/s
2792
+ ```
2793
+
2794
+ A factor of four between two authorings of the same room. `ClothFluidWind`
2795
+ multiplies by `cell_size`, `cloth_wind.spec.js` gates on it, and it is the one
2796
+ line of M6 that would have been silently wrong rather than loudly wrong.
2797
+
2798
+ ---
2799
+
2800
+ ## 47. The factor §10 wrote as `|n·û|`, and the rope that has no `n`
2801
+
2802
+ §10 asks for `|n·û|`, and it is right about what it wants: a sheet should catch
2803
+ the wind broadside and slice it edge-on. What it is wrong about is that a normal
2804
+ exists to take the dot product with. Half of what this system simulates is
2805
+ chains — `CLOTH_ROPE`, `CLOTH_CHAIN`, `CLOTH_HAIR`, and every cape whose proxy
2806
+ is strips (§37) — and a chain's particles span a **line**. Every direction
2807
+ perpendicular to it is equally a normal. Picking one would be the same
2808
+ fabrication §7 P4(a) rules out for a contact normal, and it would be worse than
2809
+ arbitrary, because a rope's aerodynamics are the *complement* of a sheet's: it
2810
+ catches the flow across it and slices along it.
2811
+
2812
+ Both are one quantity. Take the span `T` of the directions to the particle's
2813
+ stretch-row neighbours **in the current configuration** and the factor is the
2814
+ part of the flow that span cannot let past, `‖û − proj_T(û)‖`:
2815
+
2816
+ ```
2817
+ rank of T what the particle is the factor what §10 wanted
2818
+ 2 a point on a surface |û·n|, n = b1 x b2 exactly this
2819
+ 1 a link of a chain sqrt(1 - (û·t)^2) the complement
2820
+ 0 an isolated particle 1 (no case)
2821
+ ```
2822
+
2823
+ One expression, three topologies, no stored normal, no orientation, no sign, and
2824
+ nothing carried between steps — recomputed from the configuration every step,
2825
+ which is what P4(a) asks of a normal and what a baked one could not offer.
2826
+
2827
+ Measured on the topologies the two paths actually build
2828
+ (`cloth_wind_accelerate.spec.js`):
2829
+
2830
+ ```
2831
+ a 5x5 triangulated sheet in the XZ plane, interior particle
2832
+
2833
+ flow direction facing
2834
+ along the normal 1.000
2835
+ along the surface 0.000
2836
+ 45 degrees 0.707 (the cosine, as a sheet should)
2837
+
2838
+ boundary and corner particles, which carry three rows and two
2839
+ along the normal 1.000
2840
+ along the surface 0.000
2841
+
2842
+ a 5-link chain along X, middle particle and end particle
2843
+
2844
+ along the chain 0.000
2845
+ across it 1.000
2846
+ 60 degrees off it 0.866 (the *sine*, which is the rope's answer
2847
+ and not the sheet's)
2848
+ ```
2849
+
2850
+ **And the rows that would have faked a plane out of a line do not.** §11.1's
2851
+ chain bends through a skip row to its grandparent, which runs parallel to the
2852
+ two links it spans; §6's strain-limit row is a second row on an edge that
2853
+ already has one. Either would give a straight rope a surface, and a straight
2854
+ rope with a surface slices through a wind blowing across it — the exact opposite
2855
+ of the right answer. A second direction has to survive projection out of the
2856
+ first by a quarter of its length (about fourteen degrees) before it counts, and
2857
+ a six-link chain carrying both kinds of row still measures 0.000 along itself
2858
+ and 1.000 across.
2859
+
2860
+ ### What it does on a whole garment, which is not 1
2861
+
2862
+ A flat sheet held rigid and broadside would read 1. A banner does not:
2863
+
2864
+ ```
2865
+ the playground's 16x16 silk banner, 500 settled steps in a uniform 9 m/s wind
2866
+
2867
+ mean facing over the free particles 0.221
2868
+ ```
2869
+
2870
+ That is the correct answer and it is worth stating because it looks like a
2871
+ failure. A free flag **streams**: at equilibrium its surface lies along the
2872
+ flow, which is edge-on, and what is left facing the wind is the part near the
2873
+ pinned edge that cannot turn. A flag that stayed broadside would be a flag
2874
+ nailed to a board. The factor is what produces that shape rather than a number
2875
+ the shape should make large.
2876
+
2877
+ ---
2878
+
2879
+ ## 48. What the sample costs the main thread
2880
+
2881
+ This is §17's question for M6 — "a measured cost showing the per-step sampling
2882
+ stays inside budget" — and the answer has two very different halves depending on
2883
+ whether a fluid field actually reaches the cloth.
2884
+
2885
+ ```
2886
+ cloth_m6_measure.mjs section 1, paired and alternated, per-frame minima
2887
+ a triangulated sheet at 2 cm, the §17 topology
2888
+
2889
+ particles ambient field ratio per particle (field)
2890
+ 144 0.50 us 9.00 us 18.0x 62 ns
2891
+ 400 0.90 us 23.40 us 26.0x 58 ns
2892
+ 576 1.30 us 34.00 us 26.2x 59 ns
2893
+ 1,024 2.20 us 59.70 us 27.1x 58 ns
2894
+ ```
2895
+
2896
+ **A trilinear sample costs 58 to 62 nanoseconds a particle**, flat in particle
2897
+ count, which is what a per-particle pass with no term in the neighbour count
2898
+ should look like. For the §17 workload that is 0.060 ms a step.
2899
+
2900
+ **A uniform source costs a twenty-fifth of that**, and the reason is §10's own
2901
+ cost-control sentence read one step further. §10 says "a cloth outside every
2902
+ fluid field's bounds skips it entirely"; what makes that worth having is that
2903
+ the *skip* is a property the source can answer for a whole cloth at once.
2904
+ `AbstractClothWind#varies` is that question, `ClothAmbientWind` answers `false`
2905
+ for every box, and the sampler then takes one sample and fills the region —
2906
+ which is a bounds scan and a write, both linear with a handful of operations
2907
+ each. Weather is nearly free; air is not.
2908
+
2909
+ ### The whole main thread, which is what M5's number was
2910
+
2911
+ §41 measured `em.update()` under a `SystemWorkerLoopback` and called it "exactly
2912
+ what the main thread pays, because the worker's step is a separate call". M6
2913
+ runs the same measurement with a wind source attached:
2914
+
2915
+ ```
2916
+ cloth_m6_measure.mjs section 4
2917
+ a garment as an entity subtree, anchor walking; the field reaches all of it
2918
+
2919
+ garment no wind ambient field ambient field
2920
+ COTTON 1,024p 0.269 ms 0.293 ms 0.334 ms +6% +24%
2921
+ SILK 400p 0.059 ms 0.063 ms 0.092 ms +6% +55%
2922
+ ROPE 40 links 0.006 ms 0.006 ms 0.009 ms +5% +54%
2923
+ ```
2924
+
2925
+ The absolute column is this machine's and this scene's — the garment is strands
2926
+ rather than §41's patch — so the ratios are what the pairing licenses. Read
2927
+ against §41's own figures, a thousand-particle garment standing in a simulated
2928
+ wake pays about **a third more main thread** than one that is not, and about a
2929
+ twentieth more if the air is only weather. Against the 3.62 ms the same garment
2930
+ cost before M5, the whole of M6's main-thread share is under two per cent of
2931
+ what the worker removed.
2932
+
2933
+ The ambient column's +5% to +6% is the bounds scan and the fill, and it is worth
2934
+ naming what it is *not*: a source that answers `varies() === false` is not
2935
+ approximated, it is exact, because `false` is a promise rather than a hint.
2936
+
2937
+ ---
2938
+
2939
+ ## 49. What the aerodynamic term costs the step, and the trap in measuring it
2940
+
2941
+ The first version of this section measured a cloth in a gale against a cloth
2942
+ hanging still and reported the difference as the cost of the code. It is not,
2943
+ and §7 already said so — "the variants sit in different physical states, and an
2944
+ isolated loop over one accumulate measures a converged mesh rather than a moving
2945
+ one". The size of that mistake here:
2946
+
2947
+ ```
2948
+ the same 40-link rope, 2x4, stepped 3,300 times
2949
+
2950
+ hanging dead straight 0.029 ms per step
2951
+ blown horizontal at 9 m/s 0.095 ms per step 3.24x
2952
+ ```
2953
+
2954
+ None of that 3.24× is arithmetic. A rope blown out to 1.79 m of its own 2 m is
2955
+ fighting its own hard rows at every iteration; a rope hanging straight down has
2956
+ converged and is barely working. The wind's own code, measured on the same rope
2957
+ with the physics held fixed, is **0.7%**.
2958
+
2959
+ ### Holding the physics fixed
2960
+
2961
+ Fill the external-acceleration array with exactly gravity and the two paths
2962
+ simulate the same cloth, so the only difference left is which predict and
2963
+ integrate run. (Exactly the same cloth: the region is `Float32`, so the uniform
2964
+ side is handed `Math.fround(-9.81)` too, and the harness asserts the two
2965
+ position arrays are bit-identical after every round. Without that the two
2966
+ accelerations differ by 4e-7 and a hanging sheet — which is chaotic — is
2967
+ somewhere else entirely three thousand steps later, which is the confound above
2968
+ arriving by the back door.)
2969
+
2970
+ ```
2971
+ cloth_m6_measure.mjs section 2, paired and alternated
2972
+
2973
+ entry budget uniform varying path + term total
2974
+ SILK 2x4 0.930 0.934 +0.4% 0.2% +0.6%
2975
+ COTTON 2x4 0.933 0.931 -0.2% 0.2% -0.1%
2976
+ DENIM 2x2 0.649 0.648 -0.2% 0.3% +0.2%
2977
+ LEATHER 2x2 0.645 0.647 +0.3% 0.3% +0.6%
2978
+ ROPE 2x4 0.029 0.029 +0.3% 0.7% +1.0%
2979
+ CHAIN 2x6 0.050 0.050 -0.2% 0.4% +0.2%
2980
+ HAIR 2x4 0.918 0.927 +1.0% 0.2% +1.2%
2981
+ COTTON §17 2x4 3.232 3.196 -1.1% 0.1% -1.0%
2982
+ ```
2983
+
2984
+ **The whole feature is one per cent of a step, and the sign of the path column
2985
+ is noise.** Two loops rather than a branch inside one is why the uniform path
2986
+ does not pay: a cloth with no wind runs the code M0 through M5 measured, byte
2987
+ for byte, and `cloth_wind.spec.js` holds a 120-step run to exactly that.
2988
+
2989
+ `cloth_wind_accelerate` itself — the Gram-Schmidt walk over incident rows plus
2990
+ the arithmetic — is 0.1% to 0.7% of a step, larger as a fraction on a chain
2991
+ because a chain's step is small rather than because its walk is long.
2992
+
2993
+ ### What `drag` does
2994
+
2995
+ ```
2996
+ a 20-link rope hanging under gravity in a steady 2 m/s cross-wind, 2x4
2997
+ "lean" is the tip's downwind travel once settled; "half" the step at which it
2998
+ first passes half of that; "settle" the step the fastest particle first falls
2999
+ under 1 mm/s
3000
+
3001
+ drag rate 1/s response lean m half settle peak m/s
3002
+ 0.00 0.0 0.00 0.000 - 67 0.11
3003
+ 0.05 3.0 2.93 0.468 21 111 1.02
3004
+ 0.08 4.8 4.61 0.602 20 115 1.28
3005
+ 0.12 7.2 6.78 0.704 19 116 1.48
3006
+ 0.20 12.0 10.88 0.802 18 119 1.67
3007
+ 0.30 18.0 15.55 0.857 17 134 1.77
3008
+ 0.45 27.0 21.74 0.897 17 143 1.85
3009
+ 0.70 42.0 30.20 0.926 16 149 1.90
3010
+ 1.00 60.0 37.93 0.942 16 152 1.95
3011
+ ```
3012
+
3013
+ Monotone in every column and exactly zero at zero, which is what makes it a dial
3014
+ rather than a switch. The library's own spread — leather at 0.05 to silk at
3015
+ 0.45 — is 0.47 m of lean against 0.90 m at the same wind, which is the
3016
+ difference between a skirt that moves and a banner that flies.
3017
+
3018
+ Two things in that table are worth naming. **Settling gets slower, not faster**,
3019
+ as drag rises: 111 steps to 152. A livelier cloth is a cloth with more to
3020
+ dissipate, and §7 P5's sleep still arrives for every value — a banner in a
3021
+ steady wind settles into a displaced equilibrium and sleeps there, which
3022
+ `cloth_wind.spec.js` gates on. And **peak speed approaches the air speed**,
3023
+ 1.02 m/s to 1.95 against 2 m/s of air, which is `drag = 1` meaning what
3024
+ `cloth_dynamics_map.js` says it means: a cloth with no aerodynamic memory.
3025
+
3026
+ ### The response cannot overshoot, at any step length
3027
+
3028
+ ```
3029
+ a free particle, no rows, no gravity, air at 10 m/s, drag = 1
3030
+
3031
+ dt rate*dt closed/step v after 1 step v after 20
3032
+ 0.00417 0.25 0.221199 2.2120 9.9326
3033
+ 0.01667 1.00 0.632121 6.3212 10.0000
3034
+ 0.03333 2.00 0.864665 8.6466 10.0000
3035
+ 0.10000 6.00 0.997521 9.9752 10.0000
3036
+ 0.20000 12.00 0.999994 9.9999 10.0000
3037
+ ```
3038
+
3039
+ An explicit `rate·(u − v)` at the fourth row would move the particle to 60 m/s
3040
+ in one step and to 590 in two. The exact form closes 0.9975 of the gap and
3041
+ stops. §1's first design commitment is "it does not explode… not at 5 fps", and
3042
+ this is that commitment discharged by construction rather than by a clamp.
3043
+
3044
+ ### Memory
3045
+
3046
+ ```
3047
+ particles state bytes the two regions share
3048
+ 144 97,400 3,456 3.5%
3049
+ 400 262,776 9,600 3.7%
3050
+ 576 377,528 13,824 3.7%
3051
+ 1,024 671,160 24,576 3.7%
3052
+ ```
3053
+
3054
+ Twenty-four bytes a particle, unconditionally — the same shape as M4's
3055
+ self-collision regions, and for the same reason: `ClothState` is one allocation
3056
+ sized at seed, and making `drag` a reseed trigger would be a change to the
3057
+ reseed contract rather than a tidy-up. Against §33's 16% for three regions four
3058
+ of seven entries never read, 3.7% for two regions every cloth in a windy scene
3059
+ does is not the thing to fix first.
3060
+
3061
+ ---
3062
+
3063
+ ## 50. The clock counts steps, not ticks, and the parity gate is why
3064
+
3065
+ §12 asks the two systems for bit-identical results and M5 made that structural:
3066
+ one `ClothWorld`, one `cloth_step_instance`, one parameter block. M6 adds the
3067
+ first thing in the system that is a function of **time**, and time is where the
3068
+ two systems genuinely differ.
3069
+
3070
+ A worker cloth is not stepped until its registration is acknowledged, and §39's
3071
+ own argument is that it is not *advanced* either — "not stepped, not written
3072
+ back" — so `ClothSystem` reaches a cloth's first step on tick 1 and
3073
+ `WorkerClothSystem` reaches it on tick 2 or later. A gust that is a function of
3074
+ ticks would therefore be a different gust on the same step:
3075
+
3076
+ ```
3077
+ step ClothSystem's clock WorkerClothSystem's clock, ticking
3078
+ 1 dt 2dt
3079
+ 2 2dt 3dt
3080
+ ...
3081
+ ```
3082
+
3083
+ Measured, by putting the clock back on ticks and running the gate: the two
3084
+ buffers differ at **step 1, byte 0**. Byte 0 is the first word of
3085
+ `ClothState#result` — the largest particle speed the step produced — so the very
3086
+ first step of the run is already a different step. Not drift, a wrong parameter,
3087
+ which is exactly the distinction §40 built the per-step comparison to make: from
3088
+ the end of a run the two would look like any other divergence.
3089
+
3090
+ **The fix is not a synchronisation.** It is that the clock advances at the first
3091
+ `advance` of a tick rather than at `begin`, so a tick in which no cloth is
3092
+ advanced does not move the weather. A cloth that has not started stepping has no
3093
+ weather yet, which is the right semantic on its own terms — a paused simulation
3094
+ should not have the wind move — and it makes the two systems agree by
3095
+ construction rather than by arrangement. It sits **above** the sleep tests
3096
+ deliberately: a world whose every cloth is asleep still has weather, and §10's
3097
+ "sleep wakes on a wind change" is the whole reason it needs to.
3098
+
3099
+ > **Corrected by §57.** Everything above is right about a scene with *one* cloth
3100
+ > in it, which is the scene this was measured in. A second cloth registering
3101
+ > while the first is running spends its registration ticks in a world whose clock
3102
+ > is moving, so the world clock — however it counts — hands it a different gust
3103
+ > under the two systems. The gust now reads the asking cloth's own age and
3104
+ > `ClothWorld#time` keeps only the job below: telling a source that reads the
3105
+ > scene when to walk it.
3106
+
3107
+ ### The number
3108
+
3109
+ ```
3110
+ 200 steps, two 9-link strands in silk, a real fluid volume with a sheared
3111
+ flow in it, an ambient gust of 1.5 m/s on a 0.7 s period, and the anchor
3112
+ walked out of the volume partway through
3113
+
3114
+ differing bytes in ClothState's buffer, per step: 0
3115
+ steps sampled per particle: 104
3116
+ steps sampled once for the cloth: 96
3117
+ ```
3118
+
3119
+ Both of the sampler's paths ran, and the run is self-checked on that, because
3120
+ §40's own lesson is that a parity test over a branch that did not run is a
3121
+ parity test over nothing. The run also asserts the banner actually moved — a
3122
+ rope that hangs straight down under a gale would make every byte agree for the
3123
+ wrong reason.
3124
+
3125
+ ---
3126
+
3127
+ ## 51. The banner in the wake, and what a wake turns out not to be
3128
+
3129
+ §17 asks M6 for "a playground with a banner in a simulated wake". `wind.html` is
3130
+ that, and it is simulated in the strong sense: a `FluidField`, a
3131
+ `FluidSimulator`, a `GlobalFluidEffector` in its prevailing-wind regime, and a
3132
+ solid post written into `FluidField#solid`. The banner reads it through the same
3133
+ `ClothFluidWind` and `cloth_sample_air` a scene would.
3134
+
3135
+ M5 skipped a playground for a reason about the dev server — a `SharedArrayBuffer`
3136
+ needs a cross-origin isolated page (§44) — and that reason does not reach M6:
3137
+ this page drives `cloth_step` in process, like the solver and collision pages,
3138
+ and needs no shared memory at all.
3139
+
3140
+ ### The post's deficit is a third of the free stream
3141
+
3142
+ ```
3143
+ driven at 9 m/s, 18 x 18 x 29 cells at 8 cm, measured at the banner
3144
+
3145
+ post present 6.00 m/s
3146
+ post absent 8.69 m/s
3147
+ ```
3148
+
3149
+ That 31% is the incompressible projection's answer to a solid standing in a
3150
+ flow, and there is no authored falloff anywhere in it. It is also the whole of
3151
+ §10's argument in one number: nothing cloth-specific was written to produce it.
3152
+
3153
+ ### Turning the flow: the factor works, and the wake does not care
3154
+
3155
+ ```
3156
+ the 16x16 silk banner, 500 settled steps, gust off
3157
+
3158
+ source flow mean relative flow mean drag accel mean facing
3159
+ ambient 0° 9.00 m/s 43.24 m/s² 0.221
3160
+ ambient 90° 9.00 m/s 0.08 m/s² 0.000
3161
+ field 0° 7.25 m/s 34.00 m/s² 0.216
3162
+ field 90° 5.56 m/s 33.83 m/s² 0.280
3163
+ ```
3164
+
3165
+ **In uniform air the factor is everything.** Turn the wind ninety degrees at the
3166
+ same speed and the banner is not merely less driven, it is *undriven* — 0.08 m/s²
3167
+ of aerodynamic acceleration against 43.24, a factor of five hundred. That is
3168
+ §10's flag-versus-rope claim, and it is the sharpest single result in M6.
3169
+
3170
+ **In a wake it does nothing.** The factor reads 0.216 broadside and 0.280
3171
+ edge-on — slightly *higher* edge-on, which is noise around "no dependence at
3172
+ all". The reason is not a failure of the factor: a wake has velocity in every
3173
+ direction, so no orientation of a surface is edge-on to it, and `‖û − proj_T(û)‖`
3174
+ is correctly reporting that there is nothing to slice through.
3175
+
3176
+ This is a correction to §10's intuition rather than to its design. §10 offers
3177
+ the factor as the thing that makes reading the fluid worth doing; the
3178
+ measurement says the factor's headline behaviour is a statement about *laminar*
3179
+ air, and that what reading the fluid actually buys is the half §10 stated more
3180
+ quietly — a wake is a place where the air is slower and turned, and no
3181
+ orientation escapes it.
3182
+
3183
+ ### What the wake is not: a shedding street
3184
+
3185
+ ```
3186
+ the air a banner's width downstream of the post, over 4 settled seconds
3187
+ "wobble" is the largest deviation from its own mean, m/s
3188
+
3189
+ cells damping confinement speed wobble ms/step
3190
+ 18 0 0 3.11 0.002 5.0
3191
+ 18 0.2 0.6 3.07 0.001 5.2
3192
+ 26 0 0 7.29 0.005 15.5
3193
+ 26 0.2 0.6 6.29 0.232 16.0
3194
+ 34 0 0 6.77 0.031 34.7
3195
+ 34 0.2 0.6 5.79 0.000 37.0
3196
+ ```
3197
+
3198
+ At every resolution the page can afford, and with the simulator's damping and
3199
+ vorticity confinement at both ends of their useful range, the wake is a
3200
+ **steady** velocity deficit rather than a von Kármán street. The banner's own
3201
+ free corner moves 1.44 µm a step with the post present and 0.12 µm without — a
3202
+ factor of twelve, and both of them settled rather than fluttering.
3203
+
3204
+ That is the fluid solver's answer at this scale and not the cloth's: resolving a
3205
+ shedding shear layer needs a grid whose step is 15 ms at 26 cells and 35 ms at
3206
+ 34, against the 5 ms an 18-cell volume costs. It is recorded here rather than
3207
+ tuned away because the page says "a banner in a simulated wake" and a reader is
3208
+ entitled to know that the wake is steady.
3209
+
3210
+ ---
3211
+
3212
+ ## 52. The library, re-measured, and why no entry is re-sized
3213
+
3214
+ §17 says to re-measure with `cloth_library_bench.mjs` and re-size anything that
3215
+ drifted outside. The bench gains a section that runs §33's own scenario — the
3216
+ reversing gust that folds the sheet, each entry's own self-collision flag — with
3217
+ and without §10's aerodynamic term.
3218
+
3219
+ **And the first thing it measured is §42's warning, three times.** Three runs of
3220
+ the same bench on the same machine inside an hour, same code, no wind in any of
3221
+ these columns:
3222
+
3223
+ ```
3224
+ the §17 workload's baseline row, "M0 rows only", 1,024p at 2x4
3225
+
3226
+ run A 3.490 ms 12 node processes, nothing else of this session running
3227
+ run B 3.826 ms 8 node processes, this session driving a browser
3228
+ run C 4.257 ms 9 node processes, this session running the test suite
3229
+ ```
3230
+
3231
+ Run B had *fewer* processes than run A and was slower anyway, because the four
3232
+ that had gone were idle and what run B was actually sharing the machine with was
3233
+ a Vite dev server and a browser pane. §42 said "check the process count first";
3234
+ M6's amendment is that a process count is a proxy for load and not a measure of
3235
+ it, and that the only quantity worth reading off this bench is the **paired
3236
+ ratio** inside one run.
3237
+
3238
+ The size of what that does to the library, before wind is anywhere near it:
3239
+
3240
+ ```
3241
+ §33's scenario, self-collision on, no wind — the same measurement in four places
3242
+
3243
+ entry M4 §33 M5 §42 M6 run A M6 run C
3244
+ SILK 1.802 2.069 1.730 2.042
3245
+ COTTON 1.802 2.002 1.742 1.905
3246
+ DENIM 1.765 2.020 1.693 1.967
3247
+ LEATHER 1.828 2.039 1.676 1.814
3248
+ ROPE 0.129 0.133 0.123 0.134
3249
+ CHAIN 0.183 0.215 0.186 0.208
3250
+ HAIR 1.798 1.997 1.722 1.930
3251
+ ```
3252
+
3253
+ **Silk reads 2.042 ms in run C with no wind in it at all** — the same code M4
3254
+ measured at 1.802 and M6 measured at 1.730 forty minutes earlier. Anything a
3255
+ re-sizing decision took from that column would be a reading of the machine.
3256
+
3257
+ ### What the wind term costs, paired
3258
+
3259
+ ```
3260
+ cloth_library_bench.mjs, §33's scenario, with and without the wind term
3261
+ three runs, each paired inside itself
3262
+
3263
+ entry drag run A run B run C
3264
+ SILK 0.45 +4.5% +3.4% -2.3%
3265
+ COTTON 0.20 -0.5% -0.1% +0.5%
3266
+ DENIM 0.08 +6.2% +6.9% +5.5%
3267
+ LEATHER 0.05 +0.7% +2.1% +8.5%
3268
+ ROPE 0.12 +1.0% +9.3% -1.1%
3269
+ CHAIN 0.05 -2.7% -1.1% +0.3%
3270
+ HAIR 0.30 +6.1% +8.1% +4.4%
3271
+ ```
3272
+
3273
+ Between −2.7% and +9.3%, and **not monotone in `drag`** — the entry with the most
3274
+ of it comes back negative twice and the entry with the least comes back at
3275
+ +8.5%. That is what a column carrying both a code cost of one per cent (§49) and
3276
+ a *different cloth* should look like, and the second is the larger term: a
3277
+ garment blown out of its drape is not the garment beside it, which is §7's
3278
+ warning in its fourth setting.
3279
+
3280
+ **No entry is re-sized**, and M6 has a measured reason to add to M5's. M5's was
3281
+ that the absolute column came off a busy machine. M6's is §49: with the physics
3282
+ held fixed so both sides simulate the same cloth, the whole aerodynamic term is
3283
+ **one per cent of a step** — an order of magnitude below the run-to-run spread of
3284
+ the thing that would decide a re-sizing, and two below the levers §17 prices.
3285
+ Re-sizing on that would bake noise into a shipped garment.
3286
+
3287
+ §33's ordering stands entry for entry: the three entries that set
3288
+ `ClothDynamicsFlags.SelfCollision` are still the tightest, the chains still find
3289
+ no pairs at all, and leather still pays the most for the fewest.
3290
+
3291
+ **What is true, and is the thing to watch**, is that those three entries are now
3292
+ within a few per cent of 2 ms on this machine with wind live — and were already
3293
+ within a few per cent of it *without*. `MEASUREMENTS.md` §42's closing advice
3294
+ stands and M6 adds one clause to it: re-measure the absolute column on a quiet
3295
+ machine, take a paired reading even then, and if the answer is that the library
3296
+ is genuinely at its ceiling, the lever to spend is the one §17 prices rather than
3297
+ the feature that happened to be measured last.
3298
+
3299
+ ---
3300
+
3301
+ # Cloth — post-M6 measurements
3302
+
3303
+ > Written 2026-09-13, against the fixes listed in §17's post-M6 entry.
3304
+ > Everything above this line is M0's through M6's and is unchanged, except where
3305
+ > a section below is named *inside* the one it corrects.
3306
+ >
3307
+ > The scripts are the ones the milestones already carry; where a number here
3308
+ > came from something else, the section says what.
3309
+
3310
+ ---
3311
+
3312
+ ## 53. A cloth resting on a collider never slept, and the speed was not the thing to read
3313
+
3314
+ §7 P5's gate is free-hanging chains, and every library fabric passes it. Put the
3315
+ same chain on a floor and four of the seven never sleep at all.
3316
+
3317
+ ```
3318
+ 8-link rope, 10 cm links, 2 % of lateral lean, dropped for 2,400 steps
3319
+ "-1" is "still awake when the run ended"
3320
+
3321
+ entry free floor mu=0 floor mu=0.5 sphere mu=0
3322
+ SILK 386 1029 398 631
3323
+ COTTON 236 -1 -1 362
3324
+ DENIM 101 198 164 220
3325
+ LEATHER 139 192 -1 2312
3326
+ ROPE 283 911 -1 427
3327
+ CHAIN 144 419 245 293
3328
+ HAIR 143 271 2129 280
3329
+ ```
3330
+
3331
+ A cloth that sleeps in 283 steps hanging free and never sleeps lying on the
3332
+ ground is not a cloth that is behaving differently; it is a **test** that is
3333
+ reading the wrong quantity.
3334
+
3335
+ ### What is actually happening
3336
+
3337
+ Traced on cotton on a frictionless floor, six hundred steps after it has come to
3338
+ rest, the configuration is a clean **period-4 limit cycle**:
3339
+
3340
+ ```
3341
+ step peak speed peak travel contact penalty, four live slots
3342
+ 0 6.806e-3 5.801e-5 628.4 1544.9 1553.7 561.1
3343
+ 1 9.568e-3 5.328e-5 628.9 1545.1 1554.0 561.6
3344
+ 2 1.443e-2 6.116e-5 629.3 1545.7 1554.3 562.1
3345
+ 3 1.229e-2 1.061e-4 629.8 1546.3 1554.8 562.6
3346
+ 4 6.806e-3 5.801e-5 630.2 1546.6 1555.3 563.2 <- repeats
3347
+ ```
3348
+
3349
+ The cloth travels **a tenth of a millimetre a step**, which nobody can see, at a
3350
+ peak particle speed of **1.5e-2 m/s**, which is **eighteen times** the sleep
3351
+ threshold of 8.0e-4 m/s. Both numbers are correct and they disagree about
3352
+ whether the cloth is at rest, because a limit cycle has bounded displacement and
3353
+ un-bounded-below speed.
3354
+
3355
+ The penalty column says where the cycle gets its energy: it climbs
3356
+ monotonically — `k + β·|C|` in `cloth_contact_dual_update` — and **never comes
3357
+ back down**, because `γ = 1` and `clamp_band`'s floor is the contact's own
3358
+ `0.25·(m/h²)`. Two of the four slots are already at the ceiling of `8·(m/h²)`,
3359
+ which is exactly the ratio §6 measured a row adding energy above.
3360
+
3361
+ ### Two candidates, and only the second survives
3362
+
3363
+ The *friction re-anchor* — `cloth_contact_row.js` moving the anchor to the
3364
+ particle whenever the row's force clamps out — is **not** it, and no experiment
3365
+ is needed to say so: cotton fails on a floor whose friction is **zero**, where
3366
+ `friction <= 0` zeroes the friction multiplier before the anchor is ever read.
3367
+ It is ruled out by the μ = 0 column of the table above.
3368
+
3369
+ The *penalty ramp* is it, and the fix for it is not in scope: `γ` is
3370
+ unmeasured-by-decision (§5.2), a `γ < 1` would have to be re-measured against
3371
+ every hard row in the system, and the motion it sustains is a tenth of a
3372
+ millimetre. **The defect worth fixing is that the sleep test disagrees with the
3373
+ plan.** §7 P5 has said "maximum particle displacement over a window" since M0 and
3374
+ M1 implemented an instantaneous speed.
3375
+
3376
+ ### The rule, and the threshold
3377
+
3378
+ Displacement from an anchor taken when the window opened, re-anchored the moment
3379
+ any particle leaves the threshold; `CLOTH_SLEEP_STEPS` consecutive steps inside
3380
+ it is sleep. Swept over the ratio, every entry × every scene, "steps to sleep":
3381
+
3382
+ ```
3383
+ ratio of reach worst where any awake at 2,400?
3384
+ 1e-3 (as speed) -1 COTTON on a floor 4 of 28
3385
+ 1e-3 872 SILK on a frictionless floor no
3386
+ 5e-4 961 SILK on a frictionless floor no
3387
+ 2e-4 965 SILK on a frictionless floor no
3388
+ 1e-4 1773 COTTON on a frictionless floor no
3389
+ ```
3390
+
3391
+ **5e-4 is the value**, and it is chosen to be *no looser* than the speed test it
3392
+ replaces rather than to be the fastest: over a 30-step window at the engine's
3393
+ 60 Hz fixed step, half a thousandth of a reach per window is the same millimetre
3394
+ a second per step that `CLOTH_SLEEP_SPEED_RATIO` named. 1e-3 would sleep sooner
3395
+ and would be a real relaxation of the gate; 1e-4 costs nearly twice the worst
3396
+ case and buys nothing visible.
3397
+
3398
+ With it in place, the table at the top of this section reads:
3399
+
3400
+ ```
3401
+ entry free floor mu=0 floor mu=0.5 sphere mu=0
3402
+ SILK 325 961 309 573
3403
+ COTTON 180 361 277 325
3404
+ DENIM 92 195 128 194
3405
+ LEATHER 127 175 109 151
3406
+ ROPE 235 840 941 393
3407
+ CHAIN 143 425 201 261
3408
+ HAIR 132 260 177 246
3409
+ ```
3410
+
3411
+ Nothing is awake, the worst case is 961 steps and the free-hanging column has
3412
+ not got worse — it is uniformly *faster*, because a chain settling through a
3413
+ swing covers less ground per window than its instantaneous speed suggested.
3414
+
3415
+ ### What it costs
3416
+
3417
+ One compare-only pass over the particle positions per cloth per step, on the
3418
+ main thread, in `ClothWorld#finish`, with no square root — the displacements are
3419
+ compared squared — and one `Float32Array` of `3·particle_count` per instance. It
3420
+ does not enter the step, so `cloth_library_bench.mjs` cannot see it and does not:
3421
+ the library column is unchanged across this commit.
3422
+
3423
+ *Reproduce:* `cloth_pathology.spec.js`, the two P5 sleep tests. The table above
3424
+ is the same scenario driven from a script rather than through vitest.
3425
+
3426
+ ---
3427
+
3428
+ ## 54. A collider that does not move is not a collider a cloth cannot hit
3429
+
3430
+ §20 measured the discovery reach missing a **moving collider** and fixed it by
3431
+ folding the collider's travel into the reach. The mirror case was argued away in
3432
+ the same docblock rather than measured: a fixed margin of `2·(thickness +
3433
+ inflation)` "covers half a metre a second of approach", and anything faster is
3434
+ "one the velocity clamp of §7 P1 is already the bound on".
3435
+
3436
+ **It is not.** That clamp is `4 · shortest_row · substeps · iterations / dt`,
3437
+ which for a garment's rows at 2 × 4 is about **154 m/s** — three orders of
3438
+ magnitude above the half metre a second the margin actually covers. Everything
3439
+ between is uncovered, and a character walks through it.
3440
+
3441
+ ### The reproduction
3442
+
3443
+ One particle at 8 mm thickness, thrown along +X at a **static** 3 cm capsule,
3444
+ no gravity so that nothing but the contact can change its velocity, 2 substeps ×
3445
+ 4 iterations. "Warned" is whether a row existed by the time the particle was
3446
+ first sampled inside the contact offset; "far side" is whether it ended past the
3447
+ capsule rather than bounced off it.
3448
+
3449
+ ```
3450
+ with |v|·dt in the reach as M2 shipped
3451
+ speed warned far side x_final warned far side x_final
3452
+ 1 m/s yes no -0.672 no no -1.464
3453
+ 2 yes no -1.268 no no -2.058
3454
+ 3 yes no -1.371 no YES +13.315
3455
+ 4 yes no -2.220 no YES +14.294
3456
+ 5 yes no -0.532 no YES +25.516
3457
+ 6 yes no -1.287 no YES +19.800
3458
+ 8 yes YES +26.285 — YES +26.467
3459
+ 10 yes YES +31.285 no YES +33.133
3460
+ ```
3461
+
3462
+ Two separate readings. **`warned` is never true without the term, at any
3463
+ speed** — the first row a particle ever gets is the one that finds it already
3464
+ inside, which is the opposite of what a speculative contact is for. And from
3465
+ **3 m/s** the particle is not stopped at all: the row the next step finally
3466
+ creates is evaluated where the particle now is, which is on the far side, and
3467
+ its normal points away from the collider, so the first thing the contact does is
3468
+ push it the rest of the way out. §7 P4(a) chose orientation over history
3469
+ deliberately and this is the price of that choice when discovery is late.
3470
+
3471
+ ### What the fix does and does not buy
3472
+
3473
+ Swept, in half-metre steps from 1 to 12 m/s, whether the particle is stopped:
3474
+
3475
+ ```
3476
+ substeps stopped up to through from
3477
+ 2 6.0 m/s 6.5 m/s
3478
+ 4 12.0 m/s — nothing in range
3479
+ ```
3480
+
3481
+ A clean threshold, and it is `substeps` rather than discovery: a row that exists
3482
+ is still evaluated at substep positions, so a particle that crosses a whole
3483
+ collider inside one substep has a row that never sees it touching. §7 P3 has
3484
+ always said "more substeps in the affected `ClothDynamics`"; this is the first
3485
+ measurement of where that lands. Discovery being right is necessary and not
3486
+ sufficient, and the two are now separable — unswept at four substeps still
3487
+ tunnels at 8 m/s.
3488
+
3489
+ ### What it costs
3490
+
3491
+ Interleaved A/B in one process, alternating which side runs first, `dt` against
3492
+ zero — zero being exactly the reach M2 shipped:
3493
+
3494
+ ```
3495
+ 1,024 particles, one ground plane, best of eight interleaved pairs
3496
+
3497
+ approach with travel as M2 shipped ratio
3498
+ at rest 45.16 ns 45.35 ns 0.996
3499
+ falling at 3 m/s 92.32 ns 55.88 ns 1.652
3500
+ ```
3501
+
3502
+ **At rest it is free** — the square root disappears into the measurement's own
3503
+ noise. Moving, it is 36 ns a particle a step, and that is not the arithmetic: it
3504
+ is the records a wider reach lets past the AABB reject, which is the feature
3505
+ rather than its overhead. Against the 4.5 ms §23 measures for this workload it
3506
+ is **0.8 % of a step**, and `cloth_library_bench.mjs` cannot see it at all,
3507
+ because the bench drives `cloth_step` with no collider table — the library
3508
+ column is unchanged across this commit, which is the regression check rather
3509
+ than the measurement.
3510
+
3511
+ ### And it moved a sleep number, which is a coupling worth naming
3512
+
3513
+ `cloth_contact_dual_update` ramps the penalty by `β·|C|` on the **unsigned**
3514
+ residual, so a row that is not touching ramps too. That was nearly invisible
3515
+ while rows only appeared within 16 mm; with the reach carrying a falling cloth's
3516
+ travel, rows now appear tens of centimetres out and reach `k_max` long before
3517
+ contact, and `γ = 1` never brings them down again. §53's table moves with it:
3518
+ most entries settle *sooner*, and hair on a floor with friction goes from 177
3519
+ steps to **2,202** — a creep at 1.6 times the sleep window's threshold rather
3520
+ than a cloth that has stopped.
3521
+
3522
+ Making the ramp one-sided is the obvious repair and **the measurement says not
3523
+ to**:
3524
+
3525
+ ```
3526
+ stopped up to, half-metre steps from 1 to 12 m/s
3527
+
3528
+ 2 substeps 4 substeps
3529
+ ramp on |C| 6.0 m/s 12.0 m/s, no holes
3530
+ ramp on max(C, 0) 6.0 m/s, hole at 5 9.5 m/s, holes at 10–11.5
3531
+ ```
3532
+
3533
+ The pre-stiffening of a speculative row is what gives a fast arrival something
3534
+ solid to hit; removing it trades a visible failure — cloth through a character —
3535
+ for an invisible one — a hair rope taking thirty-seven seconds to stop creeping
3536
+ instead of three. So the ramp is left alone and the coupling is recorded in §18.
3537
+
3538
+ ---
3539
+
3540
+ ## 55. A collider arriving and a collider retiring, and what each one was not told
3541
+
3542
+ Two defects at the two ends of a collider's life, both of them a *set* question
3543
+ answered with a *pose*.
3544
+
3545
+ ### Arriving
3546
+
3547
+ §7 P5's wake test compares each overlapping collider's previous pose with its
3548
+ current one. `ClothColliderSystem#link` calls `placePose`, which sets both — on
3549
+ purpose, so a new collider's first step sweeps nothing, because it did not move,
3550
+ it appeared. The two facts together say a collider that appears inside a
3551
+ sleeping cloth has not moved.
3552
+
3553
+ ```
3554
+ 6-link rope, 10 cm links, left to settle: asleep at step 236
3555
+ a sphere of radius 0.2 spawned around its lower half, never moved
3556
+
3557
+ 300 steps later: asleep links inside the sphere
3558
+ before yes 4 of 6
3559
+ after no 0 of 6
3560
+ ```
3561
+
3562
+ §1's first commitment names this case in its own words, and the gate for it was
3563
+ `cloth_collision.spec.js`'s "a collider that appeared but has not moved does not
3564
+ wake anything" — a test that was passing because the thing it asserted was the
3565
+ defect.
3566
+
3567
+ The fix is the **handle set**: `ClothInstance` keeps the handles the last step
3568
+ gathered, and the wake test compares this step's against them before it compares
3569
+ poses. One integer per record, on a path that was already doing a broadphase
3570
+ query, against a step three orders of magnitude more expensive.
3571
+
3572
+ Two details it turns on. The wake test's query has to use the **same reach** the
3573
+ last step's gather used, or it is comparing the answers to two different
3574
+ questions and wakes on the margin rather than on the world; `gather_reach` is
3575
+ recorded beside the set. And a `PlaneShape3D`'s AABB is unbounded, so a plane is
3576
+ in *every* cloth's set the moment it exists: adding one anywhere wakes every
3577
+ sleeping cloth once, which costs the thirty steps it takes to find nothing and
3578
+ go back to sleep. That is stated rather than worked around — a bounded collider
3579
+ five metres away does not wake anything, and the spec pins both.
3580
+
3581
+ ### Retiring
3582
+
3583
+ `ClothColliderIndex#add` popped a retired slot immediately. A handle is a slot
3584
+ index, and `cloth_contact_find` matches warm-started contacts on the handle
3585
+ alone — so a collider removed and another added within one tick are, to every
3586
+ cloth in the scene, the same collider.
3587
+
3588
+ ```
3589
+ a rope settled on a floor for 400 steps, then in one step:
3590
+ remove the floor, add a different collider
3591
+
3592
+ the floor's handle: 0
3593
+ the replacement's handle: 0 <- the cloth cannot tell
3594
+ the particle's multiplier: 0.0981 before, 0.0970 after
3595
+ the friction anchor: carried, still in the old floor's frame
3596
+ ```
3597
+
3598
+ Both index docblocks claimed the opposite — "any cloth holding warm-started
3599
+ contacts against it drops them on its next step" — and the claim is true only if
3600
+ the handle is *absent* for a step, which nothing made it. `remove` now retires
3601
+ into a list `advance` drains one step later. The cost is one slot held back for
3602
+ one step; the alternative, a generation word beside the handle, is the same
3603
+ guarantee threaded through the per-cloth table, the key array and the warm-start
3604
+ scan.
3605
+
3606
+ ### They compose, and that is not a coincidence
3607
+
3608
+ The deferral makes the handle absent for a step; the set comparison is what makes
3609
+ a **sleeping** cloth notice that absence, wake, and clear its slots before
3610
+ anything else can take the number. Either alone leaves a case open: without the
3611
+ deferral the slot is reused before the cloth steps, and without the set test a
3612
+ sleeping cloth never steps at all.
3613
+
3614
+ *Reproduce:* `cloth_collision.spec.js` — the far collider, the overlapping
3615
+ collider, the leaving collider and the replaced collider — and
3616
+ `ClothColliderIndex.spec.js`'s retirement test.
3617
+
3618
+ ---
3619
+
3620
+ ## 56. The parity gate had one cloth in it, and a second cloth is a different gate
3621
+
3622
+ §40 made the gate structural: one parameter block, one `cloth_step_instance`,
3623
+ neither system computing a number of its own. Every scene it was measured on has
3624
+ exactly one cloth in it, and two of the three things that can differ between the
3625
+ systems only exist when there are two.
3626
+
3627
+ ### A nested pair
3628
+
3629
+ `ClothSystem` ran advance, step and finish **per cloth**, walking
3630
+ `ClothWorld#instances` in hierarchy-depth order — a sort that existed precisely
3631
+ so a nested cloth would advance against an anchor the cloth above it had already
3632
+ written this step. `WorkerClothSystem` cannot do that: the step it writes back at
3633
+ the top of a tick happened on another thread during the last one, so its `apply`
3634
+ writes T−1 and its `collect` then advances everything for T.
3635
+
3636
+ ```
3637
+ a 4-link rope hung from link 3 of a 6-link rope, no wind, 120 steps
3638
+
3639
+ outer cloth: identical, every step
3640
+ inner cloth: differs at step 1, byte 24
3641
+ ```
3642
+
3643
+ Byte 24 is inside the particle positions, which is the honest place for it: the
3644
+ child is simulating against a different anchor, not reporting a different number
3645
+ about the same one.
3646
+
3647
+ **Three loops on both sides** — advance every cloth, then step every cloth, then
3648
+ finish every cloth — and the depth sort has nothing left to decide, so it is
3649
+ gone. What a nested cloth reads is its parent one fixed step stale, which is the
3650
+ same trade §4.1 already makes for an anchor and measures at a few centimetres.
3651
+
3652
+ **That was necessary and not sufficient**, and the second half is more
3653
+ interesting than the first. `cloth_write_back` composed every node's world pose
3654
+ as `anchor ⊗ local particle position`, reading the anchor from the live
3655
+ `Transform64`. For `ClothSystem` that is the anchor the step ran on, because the
3656
+ step happened between the two. For the worker it is not: it writes step T−1's
3657
+ particles against tick T's anchor.
3658
+
3659
+ ```
3660
+ the inner cloth's anchor, as each system's write-back left it
3661
+
3662
+ step reference worker (before) worker (after)
3663
+ 2 0.038945 0.069849 0.038945
3664
+ 3 0.038341 0.069079 0.038341
3665
+ 4 0.037680 0.068171 0.037680
3666
+ ```
3667
+
3668
+ The worker's column before the fix is the parent's *current* anchor plus a
3669
+ one-step-old shape — a pose that describes neither step. Nothing rendered can
3670
+ tell, because the attachment cascade re-derives position from the anchor and the
3671
+ local transforms either way (§4.1); a nested cloth reading that pose as its own
3672
+ anchor can. The write-back now uses `ClothInstance#anchor_translation` and
3673
+ `#anchor_rotation`, which `advance` already caches and which the collider gather
3674
+ already reads for the same reason.
3675
+
3676
+ ### What the gate is now
3677
+
3678
+ `WorkerClothSystem.spec.js` compares the whole buffer of **both** cloths at every
3679
+ one of 120 steps, with the parent's anchor walked through a turn so the child has
3680
+ something to be wrong about, and self-checks that the child actually moved.
3681
+
3682
+ ---
3683
+
3684
+ ## 57. A world clock is right for the first cloth in a scene and wrong for the second
3685
+
3686
+ §50 put the wind clock on steps rather than ticks so that a cloth's first step
3687
+ got the same weather under both systems. It did, and the scene it was measured
3688
+ in had one cloth.
3689
+
3690
+ With cloth A already running, cloth B's registration takes k ticks — and A is
3691
+ advanced on every one of them, so the world clock moves while B waits. B
3692
+ therefore reaches **its own first step** at a different clock value under the two
3693
+ systems:
3694
+
3695
+ ```
3696
+ A primed and running, B spawned, a two-tick delivery for B's registration
3697
+
3698
+ B's own step 1: reference clock 0.1000 s
3699
+ worker clock 0.1333 s
3700
+
3701
+ differing bytes in B's buffer: step 1, byte 0, and every step after
3702
+ the same run with gust = 0: none
3703
+ ```
3704
+
3705
+ The `gust = 0` row is what identifies the cause. Latency is not the problem —
3706
+ §39 already made an unacknowledged cloth neither stepped nor advanced, so B's
3707
+ step *sequence* is identical — the problem is that a shared clock is not a
3708
+ property of the cloth asking.
3709
+
3710
+ **The clock is now the asking cloth's own age**, `ClothInstance#age`, seconds
3711
+ *this instance* has stepped, handed to `AbstractClothWind#sample` per call. The
3712
+ source holds no clock: `ClothAmbientWind#__time` is gone and `begin` keeps only
3713
+ the job it can do correctly, which is telling a source that reads the scene —
3714
+ `ClothFluidWind`'s field walk — when to walk it. `ClothWorld#time` stays for
3715
+ exactly that.
3716
+
3717
+ *What it changes behaviourally:* two garments spawned a second apart are a second
3718
+ apart in the gust rather than in phase with it. For a bounded sum of three
3719
+ sinusoids with no spatial structure (§48) that is not a weaker model, it is the
3720
+ same model sampled at a different phase — and a scene in which every banner
3721
+ gusts in lockstep was the odder of the two pictures.
3722
+
3723
+ *What it does not change:* a fluid field's velocities are a snapshot of the tick,
3724
+ not a function of a clock, so `ClothFluidWind` passes the age through to its
3725
+ ambient fallback and ignores it otherwise.
3726
+
3727
+ *Reproduce:* `WorkerClothSystem.spec.js`, the staggered-spawn parity test.
3728
+
3729
+ ---
3730
+
3731
+ ## 58. The library, re-measured, and what the bench can and cannot see
3732
+
3733
+ `cloth_library_bench.mjs`, before this pass and after it, on the same machine
3734
+ within the hour:
3735
+
3736
+ ```
3737
+ entry budget before after delta
3738
+ SILK 2x4 1.561 ms 1.649 ms +5.6%
3739
+ COTTON 2x4 1.655 ms 1.546 ms -6.6%
3740
+ DENIM 2x2 1.261 ms 1.343 ms +6.5%
3741
+ LEATHER 2x2 1.346 ms 1.246 ms -7.4%
3742
+ ROPE 2x4 0.043 ms 0.042 ms -2.3%
3743
+ CHAIN 2x6 0.063 ms 0.063 ms +0.0%
3744
+ HAIR 2x4 1.537 ms 1.702 ms +10.7%
3745
+ ```
3746
+
3747
+ **Both signs, no pattern, ±11%** — which is §42's and §52's lesson arriving a
3748
+ third time, and here it is not merely a disclaimer: **the bench cannot see any
3749
+ of this pass's solver work**, and reading the code is what says so rather than
3750
+ reading the column. It builds a `ClothState` and calls `cloth_step` with a null
3751
+ collider table, so `cloth_contact_find` — the one function §54 changed — never
3752
+ runs; and it never constructs a `ClothWorld`, so the sleep window, the wake test,
3753
+ the write-back's anchor and the wind clock are all outside it. The column is a
3754
+ **regression check** on the paths this pass did not touch, and it passes: no
3755
+ entry moves outside the spread three runs of the unchanged bench already produce
3756
+ (§52 put the §17 baseline row at 3.49, 3.83 and 4.26 ms within an hour).
3757
+
3758
+ What *is* priced, and is priced by a paired measurement rather than by this
3759
+ column, is §54's discovery reach: free at rest, 1.65× on the discovery pass for a
3760
+ cloth falling at 3 m/s, which is 0.8% of a step. §53's sleep window is one
3761
+ compare-only pass over positions on the main thread, in `ClothWorld#finish`,
3762
+ which the bench also cannot see and which sits beside a write-back that already
3763
+ walks every node.
3764
+
3765
+ **No entry is re-sized.** Three passes have now said the same thing about this
3766
+ column and the advice does not change: re-measure it on a quiet machine, take a
3767
+ paired reading even then, and check the process count first.
3768
+
3769
+ ---
3770
+
3771
+ ## 59. The broadphase was a scan, and the margin it scanned with was metres wide
3772
+
3773
+ Two defects, found from one downstream console line: a cape overlapping **80**
3774
+ cloth colliders against a per-cloth table of 64. Both are about the gather, and
3775
+ only one of them is the one the warning names.
3776
+
3777
+ ### The margin
3778
+
3779
+ `cloth_gather_colliders` fattened the cloth's particle bounds by the §7 P1
3780
+ velocity clamp read as a distance — `4 · shortest_row · substeps · iterations`.
3781
+ That is not a distance the cloth can travel; it is a bound on what the solver
3782
+ could undo. Over the shipped library at 60 Hz:
3783
+
3784
+ ```
3785
+ entry sub x it row 2cm row 4cm row 8cm
3786
+ SILK 2 x 4 0.64 m 1.28 m 2.56 m
3787
+ COTTON 2 x 4 0.64 m 1.28 m 2.56 m
3788
+ DENIM 2 x 2 0.32 m 0.64 m 1.28 m
3789
+ CHAIN 2 x 6 0.96 m 1.92 m 3.84 m
3790
+ ```
3791
+
3792
+ The fattening is routinely larger than the garment. `cloth_contact_find` had
3793
+ already reached this conclusion for itself at §54 — it calls the same clamp
3794
+ "about 154 m/s … three orders of magnitude" past the speeds that matter and
3795
+ reaches with `|v_i|·dt` instead — and the gather was never brought along.
3796
+
3797
+ Being enormous also hid what it was **missing**. The narrowphase reaches
3798
+ `(radius + inflation) · (1 + CLOTH_CONTACT_MARGIN) + collider travel +
3799
+ |v_i|·dt`; the gather's fixed term was `2 · contact_radius` and nothing else, so
3800
+ a wide enough `ClothCollider#inflation` was a contact the gather never handed
3801
+ over. `cloth_gather_reach` is now that expression, maximised over the records
3802
+ the gather cannot yet know it will get — which is what `ClothColliderIndex`'s
3803
+ `max_inflation` and `max_collider_travel` are for. It is a **bound** and not an
3804
+ estimate: the gather reads velocity before the step and `cloth_contact_find`
3805
+ reads it after `damp`, which only ever scales down.
3806
+
3807
+ Measured on the rope of §20 — eight 10 cm links at 2 × 4, five colliders placed
3808
+ 2 m away and one against the tail:
3809
+
3810
+ ```
3811
+ gather margin, hanging 3.2006 m -> 0.0209 m
3812
+ colliders gathered 6 -> 1
3813
+ ```
3814
+
3815
+ The 3.2006 m does not move when the rope is thrown at 9 m/s. The new one does,
3816
+ which is the entire point of it.
3817
+
3818
+ ### The scan
3819
+
3820
+ `ClothColliderIndex#query` was a linear scan over every live record, run once
3821
+ per cloth per step — `clothes × colliders`. The docblock defended it on two
3822
+ grounds and both were wrong. The set size ("twenty or so, and never the whole
3823
+ world") was §41's count of colliders overlapping **one garment**, which is a
3824
+ narrowphase input being used to size a world-wide structure. The churn argument
3825
+ ("a tree over twenty leaves that all move every frame costs more to keep than to
3826
+ skip") is answered by `PhysicsSystem`, which keeps two dynamic BVHs over bodies
3827
+ that all move every frame and pays for it with `compute_fat_world_aabb`'s refit
3828
+ skip. The third ground, `PlaneShape3D`'s unbounded AABB, is real and is not a
3829
+ reason for anything: the half-space is constructed in exactly one place in this
3830
+ engine — the JSON shape adapter — and no scene builds one, so the planes sit in
3831
+ a list beside the tree that is empty in every scene that has ever run.
3832
+
3833
+ Colliders drifting 0.4 to 2 mm a step, every cloth gathering every step, paired
3834
+ runs with the order alternated:
3835
+
3836
+ ```
3837
+ colliders cloths scan pose scan gather tree pose tree gather gather
3838
+ 64 8 0.012 0.001 0.010 0.004 0.4x
3839
+ 256 16 0.047 0.009 0.046 0.011 0.8x
3840
+ 1024 32 0.207 0.073 0.205 0.029 2.5x
3841
+ 4096 64 0.961 1.090 0.912 0.076 14.4x
3842
+ 16384 128 4.684 9.930 4.293 0.205 48.5x
3843
+ ```
3844
+
3845
+ `pose` is `advance` plus a `setPose` per collider, which is O(colliders) once a
3846
+ step in both and is unchanged — tree maintenance is inside the noise. `gather`
3847
+ is the `clothes × colliders` term, and it is gone. Below a few hundred
3848
+ colliders the scan is very slightly ahead, on numbers too small to matter.
3849
+
3850
+ ### Two things the tree got wrong first, both measured
3851
+
3852
+ **A leaf inserted before its pose homes at the origin.** `add` built the leaf
3853
+ and `link` placed it immediately afterwards — but `insert_leaf` picks a sibling
3854
+ from where the box *is*, and `node_move_aabb` refits ancestors without ever
3855
+ asking whether the leaf still belongs under them. So a scene added-then-placed
3856
+ built its whole tree out of one coincident point and never rebuilt it: a root 21
3857
+ deep whose every branch overlapped every query, **128 gathers at 128 ms against
3858
+ 0.2 ms** for the same leaves inserted at their real positions. The leaf is now
3859
+ created on the first `setPose`, and a leaf that escapes its slack is removed and
3860
+ re-inserted rather than refit.
3861
+
3862
+ **A `Float32` leaf over a `Float64` world loses a bound.** The tree's nodes are
3863
+ `Float32` and the table is `Float64` precisely because a world coordinate can be
3864
+ far from the origin. One `Float32` ulp is a proportion of the magnitude — a
3865
+ millimetre at 10 km, a whole metre at 10,000 km — so a box widened by a fixed
3866
+ 5 cm and then stored can come back **45 cm inside its own record**, and a leaf
3867
+ smaller than its record is a collider the tree silently stops answering with.
3868
+ The slack now carries a relative term of two ulps as well as the constant. It
3869
+ fails without it, and only for a cloth grazing a face: a query box as wide as
3870
+ the collider survives losing one side, which is why the first spec written for
3871
+ this passed and pinned nothing.