@woosh/meep-engine 2.168.1 → 2.169.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 (287) hide show
  1. package/README.md +2 -2
  2. package/build/bundle-worker-image-decoder.js +1 -1
  3. package/package.json +1 -1
  4. package/src/REVIEW_2026_08_06/adv-core.md +511 -0
  5. package/src/REVIEW_2026_08_06/adv-gfx.md +480 -0
  6. package/src/REVIEW_2026_08_06/adv-view.md +446 -0
  7. package/src/REVIEW_2026_08_06/api-core.md +296 -0
  8. package/src/REVIEW_2026_08_06/api-engine.md +195 -0
  9. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +507 -0
  10. package/src/REVIEW_2026_08_06/core-binary.md +348 -0
  11. package/src/REVIEW_2026_08_06/core-collection.md +376 -0
  12. package/src/REVIEW_2026_08_06/core-math.md +221 -0
  13. package/src/REVIEW_2026_08_06/core-process.md +266 -0
  14. package/src/REVIEW_2026_08_06/core-science.md +223 -0
  15. package/src/REVIEW_2026_08_06/engine-ai-nav.md +279 -0
  16. package/src/REVIEW_2026_08_06/engine-animation.md +167 -0
  17. package/src/REVIEW_2026_08_06/engine-asset.md +166 -0
  18. package/src/REVIEW_2026_08_06/engine-ecs.md +920 -0
  19. package/src/REVIEW_2026_08_06/engine-input.md +197 -0
  20. package/src/REVIEW_2026_08_06/engine-misc.md +248 -0
  21. package/src/REVIEW_2026_08_06/engine-terrain.md +52 -0
  22. package/src/REVIEW_2026_08_06/generation.md +176 -0
  23. package/src/REVIEW_2026_08_06/geom-2d.md +434 -0
  24. package/src/REVIEW_2026_08_06/geom-3d.md +117 -0
  25. package/src/REVIEW_2026_08_06/gfx-gi.md +143 -0
  26. package/src/REVIEW_2026_08_06/gfx-render.md +740 -0
  27. package/src/REVIEW_2026_08_06/gfx-texture.md +690 -0
  28. package/src/REVIEW_2026_08_06/prior-criticals.md +49 -0
  29. package/src/REVIEW_2026_08_06/prior-highs.md +138 -0
  30. package/src/REVIEW_2026_08_06/test-gaps.md +364 -0
  31. package/src/REVIEW_2026_08_06/view.md +667 -0
  32. package/src/REVIEW_2026_08_06.md +571 -0
  33. package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
  34. package/src/core/binary/BinaryBuffer.js +12 -1
  35. package/src/core/binary/BitImage2.d.ts +5 -1
  36. package/src/core/binary/BitImage2.d.ts.map +1 -1
  37. package/src/core/binary/BitImage2.js +5 -1
  38. package/src/core/bvh8/build/NodeProxy.d.ts.map +1 -1
  39. package/src/core/bvh8/build/NodeProxy.js +314 -308
  40. package/src/core/collection/list/List.d.ts +15 -0
  41. package/src/core/collection/list/List.d.ts.map +1 -1
  42. package/src/core/collection/list/List.js +34 -0
  43. package/src/core/collection/list/SortedListProjection.d.ts.map +1 -1
  44. package/src/core/collection/list/SortedListProjection.js +4 -1
  45. package/src/core/collection/map/BiMap.d.ts +25 -2
  46. package/src/core/collection/map/BiMap.d.ts.map +1 -1
  47. package/src/core/collection/map/BiMap.js +57 -3
  48. package/src/core/debug/matchers/IsAnything.d.ts.map +1 -1
  49. package/src/core/debug/matchers/IsAnything.js +5 -1
  50. package/src/core/events/signal/Signal.d.ts.map +1 -1
  51. package/src/core/events/signal/Signal.js +3 -1
  52. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +433 -0
  53. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts +31 -0
  54. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts.map +1 -0
  55. package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.js +84 -0
  56. package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts +24 -2
  57. package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts.map +1 -1
  58. package/src/core/geom/2d/polygon/polygon2_signed_area.js +52 -34
  59. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts +28 -0
  60. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts.map +1 -0
  61. package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +86 -0
  62. package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts +40 -0
  63. package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts.map +1 -0
  64. package/src/core/geom/2d/polygon/polygon2_triangulate.js +1280 -0
  65. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts +28 -0
  66. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts.map +1 -0
  67. package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.js +77 -0
  68. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts +6 -3
  69. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -1
  70. package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +26 -13
  71. package/src/core/geom/2d/triangle/tri2_signed_area.d.ts +17 -1
  72. package/src/core/geom/2d/triangle/tri2_signed_area.d.ts.map +1 -1
  73. package/src/core/geom/2d/triangle/tri2_signed_area.js +35 -23
  74. package/src/core/geom/2d/v2_morton_encode.d.ts +20 -0
  75. package/src/core/geom/2d/v2_morton_encode.d.ts.map +1 -0
  76. package/src/core/geom/2d/v2_morton_encode.js +23 -0
  77. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -1
  78. package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +4 -1
  79. package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts +27 -0
  80. package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts.map +1 -0
  81. package/src/core/geom/3d/polygon/polygon3_compute_normal.js +60 -0
  82. package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts +34 -0
  83. package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts.map +1 -0
  84. package/src/core/geom/3d/polygon/polygon3_triangulate.js +94 -0
  85. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +8 -2
  86. package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -1
  87. package/src/core/geom/3d/quaternion/quat3_multiply.js +8 -2
  88. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
  89. package/src/core/geom/3d/shape/ConvexHullShape3D.js +32 -2
  90. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts +23 -6
  91. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts.map +1 -1
  92. package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.js +268 -124
  93. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts +17 -0
  94. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts.map +1 -0
  95. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.js +44 -0
  96. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts +20 -0
  97. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts.map +1 -0
  98. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.js +40 -0
  99. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts +19 -0
  100. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts.map +1 -0
  101. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.js +34 -0
  102. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts +22 -0
  103. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts.map +1 -0
  104. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.js +37 -0
  105. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts +87 -0
  106. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts.map +1 -0
  107. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.js +599 -0
  108. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts +32 -0
  109. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts.map +1 -1
  110. package/src/core/geom/3d/triangle/tri3_compute_signed_volume.js +66 -7
  111. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts +22 -1
  112. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts.map +1 -1
  113. package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.js +83 -38
  114. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +554 -0
  115. package/src/core/geom/vec3/v3_cotangent.d.ts +6 -2
  116. package/src/core/geom/vec3/v3_cotangent.d.ts.map +1 -1
  117. package/src/core/geom/vec3/v3_cotangent.js +6 -2
  118. package/src/core/geom/vec3/v3_cross.d.ts +6 -2
  119. package/src/core/geom/vec3/v3_cross.d.ts.map +1 -1
  120. package/src/core/geom/vec3/v3_cross.js +6 -2
  121. package/src/core/geom/vec3/v3_multiply.d.ts +6 -2
  122. package/src/core/geom/vec3/v3_multiply.d.ts.map +1 -1
  123. package/src/core/geom/vec3/v3_multiply.js +6 -2
  124. package/src/core/geom/vec3/v3_subtract.d.ts +6 -2
  125. package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -1
  126. package/src/core/geom/vec3/v3_subtract.js +6 -2
  127. package/src/core/localization/Localization.js +1 -1
  128. package/src/core/math/random/randomSeed.d.ts +13 -0
  129. package/src/core/math/random/randomSeed.d.ts.map +1 -0
  130. package/src/core/math/random/randomSeed.js +16 -0
  131. package/src/core/math/spline/spline3_hermite_apply_transform.d.ts +50 -0
  132. package/src/core/math/spline/spline3_hermite_apply_transform.d.ts.map +1 -0
  133. package/src/core/math/spline/spline3_hermite_apply_transform.js +107 -0
  134. package/src/core/math/spline/spline3_hermite_reverse.d.ts +34 -0
  135. package/src/core/math/spline/spline3_hermite_reverse.d.ts.map +1 -0
  136. package/src/core/math/spline/spline3_hermite_reverse.js +49 -0
  137. package/src/core/math/statistics/hammersley_sequence.d.ts +17 -4
  138. package/src/core/math/statistics/hammersley_sequence.d.ts.map +1 -1
  139. package/src/core/math/statistics/hammersley_sequence.js +25 -16
  140. package/src/core/process/Future.d.ts.map +1 -1
  141. package/src/core/process/Future.js +12 -0
  142. package/src/core/process/PromiseWatcher.d.ts.map +1 -1
  143. package/src/core/process/PromiseWatcher.js +9 -2
  144. package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
  145. package/src/core/process/executor/ConcurrentExecutor.js +12 -2
  146. package/src/core/process/worker/WorkerProxy.d.ts.map +1 -1
  147. package/src/core/process/worker/WorkerProxy.js +5 -2
  148. package/src/core/process/worker/extractTransferables.d.ts.map +1 -1
  149. package/src/core/process/worker/extractTransferables.js +4 -1
  150. package/src/engine/Engine.d.ts +12 -12
  151. package/src/engine/Engine.d.ts.map +1 -1
  152. package/src/engine/Engine.js +149 -10
  153. package/src/engine/asset/loaders/ArrayBufferLoader.d.ts.map +1 -1
  154. package/src/engine/asset/loaders/ArrayBufferLoader.js +7 -0
  155. package/src/engine/ecs/speaker/lines/sets/LineSetDescription.d.ts.map +1 -1
  156. package/src/engine/ecs/speaker/lines/sets/LineSetDescription.js +3 -1
  157. package/src/engine/ecs/terrain/ecs/TerrainSystem.d.ts.map +1 -1
  158. package/src/engine/ecs/terrain/ecs/TerrainSystem.js +4 -1
  159. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +29 -0
  160. package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
  161. package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +84 -23
  162. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +446 -0
  163. package/src/engine/graphics/GraphicsEngine.d.ts +56 -7
  164. package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
  165. package/src/engine/graphics/GraphicsEngine.js +196 -24
  166. package/src/engine/graphics/context/WebGLContextFailureReason.d.ts +9 -0
  167. package/src/engine/graphics/context/WebGLContextFailureReason.d.ts.map +1 -0
  168. package/src/engine/graphics/context/WebGLContextFailureReason.js +17 -0
  169. package/src/engine/graphics/context/WebGLContextMonitor.d.ts +106 -0
  170. package/src/engine/graphics/context/WebGLContextMonitor.d.ts.map +1 -0
  171. package/src/engine/graphics/context/WebGLContextMonitor.js +341 -0
  172. package/src/engine/graphics/context/WebGLContextState.d.ts +14 -0
  173. package/src/engine/graphics/context/WebGLContextState.d.ts.map +1 -0
  174. package/src/engine/graphics/context/WebGLContextState.js +24 -0
  175. package/src/engine/graphics/context/testWebGLContextLoss.d.ts +2 -0
  176. package/src/engine/graphics/context/testWebGLContextLoss.d.ts.map +1 -0
  177. package/src/engine/graphics/context/testWebGLContextLoss.js +696 -0
  178. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.d.ts.map +1 -1
  179. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.js +68 -32
  180. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts +14 -0
  181. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts.map +1 -0
  182. package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.js +41 -0
  183. package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +1 -1
  184. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
  185. package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +5 -0
  186. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts +1 -1
  187. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
  188. package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +2 -1
  189. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts +1 -1
  190. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
  191. package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +3 -5
  192. package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.d.ts.map +1 -1
  193. package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.js +3 -2
  194. package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.d.ts.map +1 -1
  195. package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js +3 -1
  196. package/src/engine/graphics/geometry/computeMeshSurfaceArea.js +1 -1
  197. package/src/engine/graphics/load_and_set_cubemap_v0.d.ts +5 -0
  198. package/src/engine/graphics/load_and_set_cubemap_v0.d.ts.map +1 -1
  199. package/src/engine/graphics/load_and_set_cubemap_v0.js +26 -21
  200. package/src/engine/graphics/particles/particular/group/ParticleGroup.d.ts.map +1 -1
  201. package/src/engine/graphics/particles/particular/group/ParticleGroup.js +19 -8
  202. package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.js +1 -1
  203. package/src/engine/graphics/texture/atlas/AtlasLookupTexture.d.ts.map +1 -1
  204. package/src/engine/graphics/texture/atlas/AtlasLookupTexture.js +10 -3
  205. package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
  206. package/src/engine/graphics/texture/atlas/TextureAtlas.js +4 -1
  207. package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.d.ts.map +1 -1
  208. package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js +11 -0
  209. package/src/engine/graphics/texture/cubemap/load_environment_map.d.ts.map +1 -1
  210. package/src/engine/graphics/texture/cubemap/load_environment_map.js +5 -2
  211. package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.d.ts.map +1 -1
  212. package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js +26 -18
  213. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts +15 -2
  214. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts.map +1 -1
  215. package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.js +29 -3
  216. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts +11 -1
  217. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts.map +1 -1
  218. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js +158 -171
  219. package/src/engine/graphics/texture/sampler/sampler2d_paint.js +1 -1
  220. package/src/engine/graphics/texture/sampler/util/bitSet2Sampler2D.js +2 -2
  221. package/src/engine/input/ecs/util/TopDownCameraControllerHelper.d.ts.map +1 -1
  222. package/src/engine/input/ecs/util/TopDownCameraControllerHelper.js +10 -3
  223. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts +14 -0
  224. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts.map +1 -0
  225. package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.js +38 -0
  226. package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
  227. package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +433 -425
  228. package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
  229. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  230. package/src/engine/physics/constraint/solve_constraints.js +38 -13
  231. package/src/engine/physics/contact/ManifoldStore.d.ts +15 -3
  232. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  233. package/src/engine/physics/contact/ManifoldStore.js +15 -3
  234. package/src/engine/physics/ecs/Joint.d.ts +7 -4
  235. package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
  236. package/src/engine/physics/ecs/Joint.js +7 -4
  237. package/src/engine/physics/ecs/PhysicsSystem.d.ts +14 -0
  238. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  239. package/src/engine/physics/ecs/PhysicsSystem.js +16 -0
  240. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  241. package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
  242. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +15 -7
  243. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
  244. package/src/engine/physics/narrowphase/convex_convex_manifold.js +33 -10
  245. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +6 -2
  246. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  247. package/src/engine/physics/narrowphase/refine_ray_concave.js +6 -2
  248. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +6 -2
  249. package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
  250. package/src/engine/physics/narrowphase/refine_ray_hit.js +6 -2
  251. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  252. package/src/engine/physics/queries/raycast.js +11 -4
  253. package/src/engine/save/storage/IndexedDBStorage.d.ts.map +1 -1
  254. package/src/engine/save/storage/IndexedDBStorage.js +21 -3
  255. package/src/engine/simulation/Ticker.d.ts +12 -0
  256. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  257. package/src/engine/simulation/Ticker.js +18 -0
  258. package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
  259. package/src/engine/sound/simulation/AcousticSimulator.js +9 -1
  260. package/src/engine/sound/simulation/core/VolumeField.d.ts +6 -2
  261. package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
  262. package/src/engine/sound/simulation/core/VolumeField.js +6 -2
  263. package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts +6 -2
  264. package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
  265. package/src/engine/sound/simulation/probe/AcousticProbeField.js +6 -2
  266. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts +6 -2
  267. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts.map +1 -1
  268. package/src/engine/sound/simulation/probe/acoustic_probe_transfer.js +6 -2
  269. package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts +3 -1
  270. package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts.map +1 -1
  271. package/src/engine/sound/simulation/probe/bakeProbeReflectors.js +3 -1
  272. package/src/engine/ui/GUIEngine.d.ts.map +1 -1
  273. package/src/engine/ui/GUIEngine.js +8 -1
  274. package/src/generation/theme/TerrainTheme.d.ts.map +1 -1
  275. package/src/generation/theme/TerrainTheme.js +2 -1
  276. package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
  277. package/src/generation/theme/ThemeEngine.js +6 -5
  278. package/src/view/ViewGroup.d.ts.map +1 -1
  279. package/src/view/ViewGroup.js +9 -7
  280. package/src/view/graphics/WebGLContextFailureView.d.ts +19 -0
  281. package/src/view/graphics/WebGLContextFailureView.d.ts.map +1 -0
  282. package/src/view/graphics/WebGLContextFailureView.js +76 -0
  283. package/src/view/minimap/Minimap.d.ts +8 -0
  284. package/src/view/minimap/Minimap.d.ts.map +1 -1
  285. package/src/view/minimap/Minimap.js +16 -3
  286. package/src/view/minimap/dom/MinimapMarkerView.d.ts.map +1 -1
  287. package/src/view/minimap/dom/MinimapMarkerView.js +3 -1
@@ -0,0 +1,446 @@
1
+ # WebGL context loss recovery — investigation, plan, and results
2
+
3
+ Date: 2026-07-30. Scope: `meep` graphics stack (three.js r136, WebGL2) as consumed by the Askara app
4
+ (`app/src/mir-engine/model/game/makeMirEngineConfig.js`).
5
+
6
+ Goal: when the browser drops the WebGL context, detect it, recover in place, and let the player continue —
7
+ no application restart, no reload, no lost session state.
8
+
9
+ **Status: phases 0–3 implemented and verified.** Phases 4–6 remain (§11). Claims below are tagged
10
+ **[src]** (read from three.js / meep source) or **[measured]** (observed in the browser harness, §10).
11
+
12
+ ---
13
+
14
+ ## 1. TL;DR
15
+
16
+ * **The engine was much closer to recoverable than it looked.** three.js r136 rebuilds all of its
17
+ GPU-object bookkeeping on `webglcontextrestored` (`initGLContext()` re-instantiates `WebGLProperties`),
18
+ so every texture, geometry, render target and shader program is re-created from CPU-side data on first
19
+ use after restore. meep keeps CPU-side sources for essentially everything it uploads. **[src]**
20
+ **[measured]**: on a fresh context the same scene renders with identical draw calls (7) and triangle
21
+ count (412,999), and pixel-identical output once one known defect is accounted for.
22
+ * **That one defect was the environment map.** `load_and_set_cubemap_v0` baked PMREM into a render target
23
+ and dropped the source texture, so IBL died with the context — worth **59% of scene luminance** in the
24
+ test scene (83.8 → 34.7). It is now re-baked on restore from a retained source. **[measured]**
25
+ * **Everything else needed was state, not content**: three silently resets the clear colour on restore
26
+ (`WebGLBackground` is re-instantiated), rendering was not gated while the context was gone, and nothing
27
+ told the user or the metrics pipeline anything.
28
+ * **Recovery is now pixel-exact.** Repeated loss/restore cycles on the harness scene come back
29
+ bit-identical to the baseline, with no resource growth across cycles. **[measured]**
30
+ * **Cost**: steady-state overhead is one boolean compare per frame and no allocations — the design is
31
+ push-based (signals), never polled. The unavoidable cost is the first frame after restore:
32
+ **~37 ms** on the harness scene, **~214 ms** on the game's title scene, against ~0.6 ms steady state.
33
+ **[measured]**
34
+ * **What is not yet proven**: that the *browser* will hand the context back. The restore path only runs
35
+ while the page is visible, and the harness available here runs in a hidden tab (§10.4). The failure
36
+ ladder exists precisely because that decision is not ours to make.
37
+
38
+ ---
39
+
40
+ ## 2. What happened before this change
41
+
42
+ | Step | Old behaviour | File |
43
+ | --- | --- | --- |
44
+ | Context lost | three set `_isContextLost = true`; meep logged a typo'd warning | three.module.js:26236, GraphicsEngine.js |
45
+ | During loss | `Engine.render()` kept calling `graphics.render()` every rAF. three guards only `render()`; `setRenderTarget` / `clear` / `copyTextureToTexture` / `readRenderTargetPixels` are unguarded | three.module.js:26555, [Engine.js:438](src/engine/Engine.js:438) |
46
+ | Context restored | three ran `initGLContext()`, meep logged and did nothing | three.module.js:26246 |
47
+ | Result | scene mostly returned by accident, with black IBL, a reset clear colour, a multi-second hitch, and no diagnostics | — |
48
+ | User feedback | none, ever, including on unrecoverable failure | — |
49
+
50
+ ---
51
+
52
+ ## 3. Why most of it recovers for free
53
+
54
+ `onContextRestore` → `initGLContext()` replaces `properties`, `textures`, `attributes`, `geometries`,
55
+ `bindingStates`, `programCache`, `state`, `background`, `info`, `renderLists`, `renderStates`
56
+ (three.module.js:25921, 26246). Because `WebGLProperties` is a **fresh `WeakMap`**:
57
+
58
+ * **Textures** — `initTexture()` sees no `__webglInit`, `uploadTexture()` sees `__version === undefined`,
59
+ so every texture re-uploads from `texture.image` / `texture.mipmaps` (three.module.js:22505).
60
+ * **Render targets** — `setRenderTarget()` sees `__webglFramebuffer === undefined` and calls
61
+ `setupRenderTarget()` (three.module.js:27473). The objects and their `texture` identities survive, so
62
+ uniforms pointing at them stay valid. Their *contents* are gone (§4.2).
63
+ * **Geometry / VAOs** — re-uploaded from the `BufferAttribute` arrays; meep never uses
64
+ `BufferAttribute.onUpload` to release them.
65
+ * **Programs** — `WebGLPrograms` cache is new, so every material recompiles on first draw.
66
+ * **Extensions** — `extensions.init()` re-requests `EXT_color_buffer_float` on WebGL2, which is what the
67
+ float/half-float buffers need (three.module.js:16065).
68
+ * **Shadow maps** — re-rendered every frame from `postVisibilityBuild`
69
+ ([ThreeLightBinding.js:291](src/engine/graphics/ecs/light/binding/three/ThreeLightBinding.js:291)).
70
+ * **CPU-sourced data textures** — FoW (`Sampler2D` → `DataTexture`), Forward+ cluster tables, terrain
71
+ splat/height/AO maps, LPG SH textures. All keep their typed arrays, so all re-upload.
72
+ * **Extra memory cost of relying on this**: none. Those CPU-side sources are retained today regardless.
73
+
74
+ **Corollary that shaped the design:** recovery must be *narrow*. Re-running startup would be actively
75
+ harmful — `FrameBuffer.initialize()` allocates a **new** render target per call
76
+ ([ColorAndDepthFrameBuffer.js:31](src/engine/graphics/render/buffer/buffers/ColorAndDepthFrameBuffer.js:31))
77
+ and plugins allocate their own
78
+ ([AmbientOcclusionPostProcessEffect.js:91](src/engine/graphics/render/buffer/simple-fx/ao/AmbientOcclusionPostProcessEffect.js:91)),
79
+ so "just restart graphics" would leak a full G-buffer set per loss and turn a recoverable event into an
80
+ out-of-memory crash.
81
+
82
+ ---
83
+
84
+ ## 4. What is lost and does not come back by itself
85
+
86
+ ### 4.1 Renderer configuration dropped by `initGLContext()` — FIXED
87
+
88
+ `WebGLBackground` is re-instantiated and owns the clear colour/alpha (`clearColor = 0x000000`,
89
+ `clearAlpha = 0`; three.module.js:13615), and `setClearColor` delegates to it (three.module.js:26154).
90
+ **[measured]** counterfactual on a bare renderer: `setClearColor(0xBBBBFF, 0)` then a restore event →
91
+ `getClearColor` returns `(0, 0, 0)`. Plain renderer properties (`autoClear`, `toneMapping`,
92
+ `outputEncoding`, `sortObjects`, `info.autoReset`, `shadowMap.*`) do survive — three preserves the
93
+ shadow-map and `info.autoReset` fields explicitly (three.module.js:26250).
94
+
95
+ Fix: `configureThreeRenderer` is exported and re-applied on restore, along with `enableExtensions()`,
96
+ `debug.checkShaderErrors` and `updateSize()`.
97
+
98
+ ### 4.2 Render-target contents that are not rebuilt every frame — FIXED
99
+
100
+ Every `WebGLRenderTarget` construction site (16 files) was audited. In the shipped configuration all are
101
+ re-rendered each frame (G-buffer, normal buffer, composit layers, outline, AO, shadow maps) **except** the
102
+ PMREM environment map: `load_and_set_cubemap_v0` → `load_environment_map` baked a `WebGLCubeRenderTarget`
103
+ once, assigned `scene.environment`, and then `dispose()`d the source cube texture, dropping the only
104
+ reference — so the bake could not be repeated. **[measured]**: losing it costs 59% of scene luminance, and
105
+ `scene.environment = null` on a live renderer is pixel-identical to a fresh context, which is how the
106
+ defect was isolated.
107
+
108
+ Fix: the graphics engine owns the bake. `set_environment_map(texture, projection)` retains the source and
109
+ produces the prefiltered radiance; `set_environment_texture` adds the skybox on top; the restore handler
110
+ re-bakes and disposes the previous target. `load_and_set_cubemap_v0` now loads the source and hands it
111
+ over instead of baking behind the engine's back.
112
+
113
+ ### 4.3 Raw GL handles and residency bookkeeping — library tier, not in the shipped game
114
+
115
+ These are `meep` public surface but are not reachable from `makeMirEngineConfig`, so they are correctness
116
+ debt rather than shipping blockers (Phase 6):
117
+
118
+ | Subsystem | What breaks | File |
119
+ | --- | --- | --- |
120
+ | Virtual texture | page atlas written with `copyTextureToTexture`; CPU residency map still claims pages are valid → garbage tiles forever | [VirtualTexturePage.js:389](src/engine/graphics/texture/virtual/VirtualTexturePage.js:389) |
121
+ | `WebGLTextureAtlas` | same pattern | [WebGLTextureAtlas.js:178](src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js:178) |
122
+ | Node-based particles | raw `gl.createBuffer()` handles + cached `gl`, no invalidation | [GLDataBuffer.js:48](src/engine/graphics/particles/node-based/simulation/GLDataBuffer.js:48), [TransformFeedback.js:414](src/engine/graphics/particles/node-based/simulation/TransformFeedback.js:414) |
123
+ | Hi-Z occlusion | readback of a lost target yields zeroes → whole-frame visibility misclassification | [BatchOcclusionQuery.js:149](src/engine/graphics/render/visibility/hiz/query/BatchOcclusionQuery.js:149) |
124
+ | Impostors | baked atlases are GPU-only | [ImpostorBaker.js](src/engine/graphics/impostors/octahedral/ImpostorBaker.js) |
125
+
126
+ ### 4.4 Secondary contexts — Phase 4
127
+
128
+ A GPU-process crash kills every context on the page at once, so recovery is ultimately a page-level
129
+ concern. The secondary contexts, in priority order:
130
+
131
+ * **`computeImageBitmapEquality`** — caches a module-level WebGL2 context plus **one texture and one
132
+ framebuffer created once** ([computeImageBitmapEquality.js:43](src/engine/asset/loaders/material/computeImageBitmapEquality.js:43)).
133
+ After a loss those handles are dead and never recreated, so `bitmap2sampler_gl` silently reads zeroes and
134
+ texture dedup starts declaring unrelated textures identical. This is the worst failure mode found:
135
+ silent, wrong, and it corrupts CPU-side results rather than just pixels. Worth fixing on its own merits.
136
+ * **`MinimapWorldGL`** — holds a pooled renderer for the lifetime of the minimap
137
+ ([MinimapWorldGL.js:124](src/view/minimap/gl/MinimapWorldGL.js:124)); it has a full re-render path, so
138
+ recovery is a re-render, not a rebuild.
139
+ * **`MeshPreview`** — takes a renderer per linked view. Editor-level feature, low priority.
140
+
141
+ `WebGLRendererPool` itself is not a significant risk: most of the engine takes a supplied renderer rather
142
+ than reaching into the pool, and the remaining callers are short-lived bakes. (Noted in passing while
143
+ reading it: `useAsync()` drops the callback's return value — `await` without `return`,
144
+ [RendererPool.js:74](src/engine/graphics/render/RendererPool.js:74). `buildLightTexture` happens to rely
145
+ only on the side effect, so nothing is broken today.)
146
+
147
+ ### 4.5 Async work in flight across a loss — partly addressed
148
+
149
+ Bakes that straddle a loss complete with garbage and nobody notices: terrain AO
150
+ ([BuildLightTexture.js:31](src/engine/ecs/terrain/ecs/BuildLightTexture.js:31)), impostor bakes,
151
+ `compileAllMaterials` during `transitionToScene`. The environment bake is now immune because it is repeated
152
+ on restore from a retained source; the general mechanism is the context generation counter
153
+ (`graphics.context.generation`), which async work can compare against the generation it started in. No
154
+ existing bake uses it yet — wiring them is Phase 4 work, and there is no way to detect the problem by
155
+ catching, because a lost context does not throw.
156
+
157
+ ---
158
+
159
+ ## 5. What was built
160
+
161
+ ### 5.1 `WebGLContextMonitor` — the state machine
162
+
163
+ `src/engine/graphics/context/WebGLContextMonitor.js`, with `WebGLContextState` (Live / Lost / Failed) and
164
+ `WebGLContextFailureReason` (`restore-timeout` / `too-many-losses`).
165
+
166
+ * **Push, not poll.** No `gl.isContextLost()` per frame, no per-resource generation comparison in hot
167
+ loops. Consumers subscribe; the per-frame cost is one field compare (`monitor.is_live`).
168
+ * **No separate `Restoring` state.** The monitor always calls `preventDefault()`, so `Lost` already means
169
+ "gone, waiting for the browser", and the fix-ups run synchronously inside the restore handler. Three
170
+ states, no observable in-between.
171
+ * **Generation counter** — monotonic, bumped per restore, never reset (even when re-attaching to a new
172
+ canvas), so stale async results are always recognisable.
173
+ * **Bounded recovery**: `restore_timeout` (default 5 s) escalates to `Failed`; `max_loss_count` (default 3)
174
+ within `loss_window` (default 60 s) does too. The window matters: a session that hits one unrelated loss
175
+ every couple of hours should keep recovering, one that dies four times in a minute should not.
176
+ * **Terminal means terminal** — a restore that arrives after `Failed` is ignored, because the application
177
+ has already been told the device is gone and may have torn down presentation state.
178
+ * **No new virtual hooks.** Deliberately not adding `onContextRestored()` no-ops to `FrameBuffer`,
179
+ `EnginePlugin`, `System` and friends: that would spread an empty method across dozens of classes for a
180
+ handful of real subscribers.
181
+
182
+ ### 5.2 Graphics engine wiring
183
+
184
+ `GraphicsEngine` owns the monitor (`graphics.context`) and re-emits ordered engine-level signals
185
+ `on.contextLost` / `on.contextRestored(generation)` / `on.contextFailed(reason)` — dispatched *after* its
186
+ own fix-ups, so subscribers can render immediately. `render()` returns early while the context is not
187
+ live, which is also what keeps meep's unguarded `setRenderTarget`/`clear`/readback calls away from a dead
188
+ context. Loss, restore and failure are recorded through `globalMetrics`, next to the existing `gpu-type`
189
+ metric, because without telemetry we will never learn the real-world loss rate.
190
+
191
+ Restore sequence, in order: `configureThreeRenderer` → `debug.checkShaderErrors` → `enableExtensions()` →
192
+ `updateSize()` → re-bake environment → `on.contextRestored`. Explicitly *not* included:
193
+ `initializeFrameBuffers()` or any plugin startup (§3 corollary).
194
+
195
+ ### 5.3 Simulation and failure policy
196
+
197
+ `Engine` subscribes to the three signals. Rendering stops immediately, but the simulation keeps running for
198
+ the first `CONTEXT_LOSS_SIMULATION_PAUSE_DELAY` (2 s) — most losses resolve in well under a second and
199
+ stopping the world for those is more disruptive than letting it run. A longer outage pauses the ticker via
200
+ `Ticker.pause()`, which freezes the clock so the first step after resuming is normal-sized (`Clock.getDelta`
201
+ does not clamp, so pausing without that would hand physics one enormous step). A ticker that somebody else
202
+ already paused is left alone, and never resumed by us — hence the new `Ticker.is_active` / `Ticker.is_paused`
203
+ accessors, since `pause()`/`resume()` throw when the loop is not cycling.
204
+
205
+ On `Failed`: the simulation is paused, application state is left intact (so a save-on-failure handler
206
+ stays viable), and `WebGLContextFailureView` explains the situation with a reload button, instead of
207
+ leaving the player looking at a black canvas.
208
+
209
+ ---
210
+
211
+ ## 6. Performance impact
212
+
213
+ **Steady state (no loss):**
214
+
215
+ | Cost | Magnitude |
216
+ | --- | --- |
217
+ | Per-frame gate in `GraphicsEngine.render()` | one field compare; unmeasurable |
218
+ | Signal subscriptions | 4 subscribers, invoked only on context events |
219
+ | Extra retained memory | the environment source texture (~24 KB for the shipped 32² cubemap) |
220
+ | Polling / `isContextLost()` | none, by design |
221
+ | Hot-path branches added elsewhere | none |
222
+
223
+ **During recovery [measured]** — intrinsic to context loss, not to this design:
224
+
225
+ | Scene | First frame after restore | Steady state |
226
+ | --- | --- | --- |
227
+ | Harness (terrain + metal sphere + sun/shadows, 5 programs) | 35–48 ms | ~0.6 ms |
228
+ | Game title scene (14 programs) | 214 ms | — |
229
+
230
+ The dominant term is shader recompilation plus lazy texture re-upload, both driven by three.js as materials
231
+ are drawn. A one-off couple of hundred milliseconds is well inside what the failure overlay's absence
232
+ implies (nothing is shown; the frame simply arrives late). If a larger scene turns out to stall long enough
233
+ to be objectionable, the existing time-sliced `compileAllMaterials` task is the lever — it is already used
234
+ on scene transitions — but on this evidence pre-warming is not worth its complexity yet.
235
+
236
+ ---
237
+
238
+ ## 7. Complexity
239
+
240
+ ~640 lines added across 8 files, of which ~250 are the state machine plus its 12 tests and ~340 are the
241
+ harness. No new abstractions imposed on unrelated code, no base-class churn, no per-resource generation
242
+ plumbing:
243
+
244
+ | File | Change |
245
+ | --- | --- |
246
+ | `graphics/context/WebGLContextState.js` | new, enum |
247
+ | `graphics/context/WebGLContextFailureReason.js` | new, enum |
248
+ | `graphics/context/WebGLContextMonitor.js` | new, state machine |
249
+ | `graphics/context/WebGLContextMonitor.spec.js` | new, 12 tests |
250
+ | `graphics/context/testWebGLContextLoss.js` | new, browser harness |
251
+ | `graphics/GraphicsEngine.js` | monitor wiring, render gate, restore fix-ups, environment retention/re-bake |
252
+ | `graphics/load_and_set_cubemap_v0.js` | hand the source to the engine instead of baking |
253
+ | `Engine.js` | ticker policy, failure view, logging |
254
+ | `simulation/Ticker.js` | `is_active` / `is_paused` accessors |
255
+ | `view/graphics/WebGLContextFailureView.js` | new, failure overlay |
256
+
257
+ ---
258
+
259
+ ## 8. Adversarial review
260
+
261
+ Each claim challenged, with what would falsify it, and what the experiments actually showed.
262
+
263
+ **#1 "three re-uploads everything automatically, so most of the engine is already recoverable."**
264
+ This was the load-bearing claim and it rested on reading `initGLContext()`. It could have failed if a
265
+ texture's CPU source had been released, if a `Texture` subclass cached upload state outside `properties`,
266
+ or if three left a dangling internal reference across restore. `_transmissionRenderTarget` is a closure
267
+ variable *not* reset by `initGLContext()` — inspected, and it is re-allocated lazily through the fresh
268
+ properties map, so it is fine, but it is exactly the shape of bug that would have invalidated the claim
269
+ elsewhere. **Outcome: confirmed by pixels**, twice (fresh context, and synthetic restore). A grep would
270
+ not have been enough; the fresh-context experiment is what turned it into evidence.
271
+
272
+ **#2 "Nothing in the shipped game holds GPU-only content except PMREM."**
273
+ Established by grepping construction sites and reading each hit — a static audit that could have missed a
274
+ dirty-flag-driven cache that looks per-frame in code and behaves as cached at runtime. Shadow maps were
275
+ specifically checked for this. **Outcome: confirmed for the harness scene**, and confirmed sharply: the
276
+ fresh-context delta was *exactly* reproduced by setting `scene.environment = null` on the live renderer
277
+ (34.70 vs 34.71 luminance, identical region breakdown), which leaves no room for a second hidden defect in
278
+ that scene. Still unproven for content the harness scene does not exercise — FoW, decals, particles,
279
+ Forward+ clusters, water — because the game's own scenes cannot be driven in a hidden tab (§10.4). Those
280
+ are all CPU-sourced by inspection, which is weaker than a measurement.
281
+
282
+ **#3 "In-place restore is sufficient."**
283
+ Still the weakest assumption, and the experiments *sharpened* the worry rather than settling it:
284
+ `preventDefault()` makes the browser *allowed* to restore, not obliged to, and in this environment it
285
+ never did (§10.4) — for an environmental reason (hidden tab), but the failure mode was real and the code
286
+ had to handle it. **Outcome: the fallback ladder is baseline, and it fired correctly** — timeout →
287
+ `Failed` → overlay → simulation paused. Whatever the browser decides, the player ends up with a coherent
288
+ state rather than a black screen.
289
+
290
+ **#4 "Gating `GraphicsEngine.render()` stops all GL traffic during loss."**
291
+ Not strictly: ECS-tick-driven work (Forward+ cluster uploads, trail/geometry updates) can still touch the
292
+ renderer, and only three's `render()` is guarded internally. Analysis said this is benign because GL calls
293
+ on a lost context are no-ops — **[measured]** `gl.getError()` returns `CONTEXT_LOST_WEBGL` (0x9242) and
294
+ five driven frames threw nothing. There *was* a latent trap here: a `setupRenderTarget` during loss writes
295
+ `__webglFramebuffer = null` into three's properties map, and `setRenderTarget` tests `=== undefined`, so
296
+ that target would never be re-set-up again. It is unreachable today because `initGLContext()` replaces the
297
+ map wholesale, and now doubly unreachable because the gate stops us drawing at all. Recorded so nobody
298
+ "optimises" the gate away.
299
+
300
+ **#5 "Readbacks during loss are harmless."**
301
+ True for the shipped set (no production `readRenderTargetPixels`), false in general — §4.4's
302
+ `bitmap2sampler_gl` turns a lost context into silently wrong *CPU* data. The general rule adopted: any
303
+ GPU→CPU readback must be generation-checked, because "returns zeroes" is indistinguishable from "returns a
304
+ valid black frame". This is also why there is no `try`/`catch` anywhere in the recovery path: a lost
305
+ context does not throw, so exception handling would buy nothing.
306
+
307
+ **#6 "The renderer pool is a likely cause of loss."**
308
+ Corrected by the code owner: most of the engine works with a supplied renderer rather than reaching into
309
+ the pool, and `MeshPreview` — the one per-instance consumer — is an editor feature. The context-cap theory
310
+ was reasoned from documented Chrome behaviour, not observed, and it is not where the risk lives. Telemetry
311
+ (§5.2) ships with detection so the real loss causes can be measured instead of guessed.
312
+
313
+ **#7 "Recovery cost is dominated by shader recompilation."**
314
+ **Outcome: consistent with measurement but not isolated.** First-frame cost scales with program count
315
+ (5 programs → ~37 ms, 14 → ~214 ms), which fits, but texture upload was not separately instrumented, so
316
+ this remains an inference. It matters only if pre-warming is ever built, which the numbers do not
317
+ currently justify.
318
+
319
+ **#8 "Re-running startup would be a simpler design."**
320
+ Tempting and wrong: `FrameBuffer.initialize()` and plugin `startup()` allocate per call, so per-loss leaks
321
+ would accumulate and a loss loop would exhaust GPU memory — turning a recoverable event into a hard crash.
322
+ **[measured]** with the narrow sequence: three consecutive cycles held frame buffers at 2, programs at 5,
323
+ textures at 6, and the environment target was replaced (id 31 → 33 → 35) rather than accumulated.
324
+
325
+ **#9 "Test strategy is weak by nature."**
326
+ Partly resolved. Jest cannot exercise WebGL, so the 12 unit tests cover the state machine only — using a
327
+ real `HTMLCanvasElement` and real event dispatch, so nothing is mocked. Pixel-level behaviour is covered by
328
+ the harness. The residual gap is honest and stated in §10.4: `WEBGL_lose_context` is a *cooperative* loss
329
+ and does not reproduce a real GPU-process crash (all contexts dying at once, driver reset, restore refusal).
330
+
331
+ **#10 "The `computeImageBitmapEquality` context produces silent garbage after a loss."**
332
+ Still untested — it needs its own harness pass, deferred with the rest of Phase 4. If the browser instead
333
+ fails loudly there, the item drops in priority.
334
+
335
+ **#11 New: the loss budget was wrong when first written.**
336
+ The original policy failed permanently after 3 losses *per session*, which would punish a long session
337
+ that hit four unrelated hiccups hours apart. Caught while reviewing the 3-cycle measurement (which ended
338
+ one loss short of tripping it) and replaced with a 3-per-60-seconds window. Worth recording as an argument
339
+ for measuring your own defaults: the bug was invisible in code review and obvious in a graph.
340
+
341
+ ---
342
+
343
+ ## 9. Non-goals
344
+
345
+ * Recovering the *contents* of anything genuinely transient (one frame of temporal history, in-flight
346
+ bakes) — re-derive, do not restore.
347
+ * WebGPU migration, or making the engine context-agnostic.
348
+ * Surviving a browser tab crash or an OOM abort.
349
+ * Deleting the unused GL-direct subsystems in §4.3 — they are public library surface; they get fixed, not
350
+ removed.
351
+
352
+ ---
353
+
354
+ ## 10. Verification
355
+
356
+ ### 10.1 Unit tests
357
+
358
+ `WebGLContextMonitor.spec.js`, 12 tests, real canvas and real event dispatch: initial state, loss →
359
+ `preventDefault` + `Lost`, restore → `Live` + generation increment, restore-timeout escalation, deadline
360
+ cancellation, burst budget exhaustion, budget decay over an hour of spaced losses, re-attach semantics,
361
+ terminal-state behaviour on late restore and on further losses, detach.
362
+
363
+ ```bash
364
+ npx jest -c jest.conf.json app/src/mir-engine/meep/src/engine/graphics/context/WebGLContextMonitor.spec.js
365
+ ```
366
+
367
+ ### 10.2 Browser harness
368
+
369
+ `src/engine/graphics/context/testWebGLContextLoss.js`, run by pointing the webpack entry at it (the entry
370
+ line is already present, commented, in `webpack.config.js`). Static scene — terrain, sun with shadows, and
371
+ a mirror-metal sphere whose shading is almost entirely IBL, which is what makes the environment map visible
372
+ in a pixel A/B. Two consecutive frames are bit-identical, so the noise floor is zero and any difference is
373
+ real. Because the harness is expected to run in a hidden tab it drives frames by hand
374
+ (`engine.ticker.onTick.send1(dt)` + `engine.render()`), reads the framebuffer in the same task as the draw,
375
+ and can print a coarse ASCII luminance map — which is how the environment defect was localised without
376
+ screenshots.
377
+
378
+ | API | Scenario | Runs in a hidden tab |
379
+ | --- | --- | --- |
380
+ | `__context_loss_test.synthetic()` | loss/restore lifecycle via synthetic canvas events; exercises the gate, the fix-up sequence, the environment re-bake and three's own restore path | yes |
381
+ | `__context_loss_test.fresh()` | same scene through a brand new renderer/context; measures what has to come back from CPU-side data | yes |
382
+ | `__context_loss_test.run()` | real `WEBGL_lose_context` loss and restore | needs a visible tab |
383
+ | `__context_loss_test.repeat(n)` | n real cycles, watching for resource growth | needs a visible tab |
384
+ | `image()`, `capture()`, `state()`, `frame(n)` | inspection helpers | yes |
385
+
386
+ ### 10.3 Results
387
+
388
+ | Check | Result |
389
+ | --- | --- |
390
+ | Frames drawn while lost | 0, no exceptions, `gl.getError()` = `CONTEXT_LOST_WEBGL` |
391
+ | Synthetic restore, harness scene | bit-identical to baseline, including IBL |
392
+ | Three consecutive cycles | cycles 2 and 3 bit-identical; frame buffers 2, programs 5, textures 6 unchanged; environment target replaced, not accumulated |
393
+ | Clear colour after restore | 0xBBBBFF preserved (counterfactual on a bare renderer: resets to black) |
394
+ | Fresh context | identical draw calls / triangles; only delta is the environment map, reproduced exactly by `scene.environment = null` |
395
+ | Real loss, no restore possible (hidden tab) | escalated to `Failed` after the 5 s deadline, overlay shown, no exceptions. Superseded: the deadline now only runs while the page is visible (see the note below the table), so this scenario stays `Lost` until the page is revealed, then fails 5 s later |
396
+ | Production game bundle, synthetic cycle | frame counter frozen while lost; generation 0 → 1; environment re-baked (texture id 24 → 77); clear colour preserved; no GL errors; first frame 214 ms |
397
+
398
+ Follow-up (post-review): the restore deadline is now suspended while the page is hidden. Chrome does not
399
+ restore a context in a hidden tab, so counting hidden time turned any loss in a backgrounded tab — the
400
+ place where the browser prefers to evict contexts — into a terminal `Failed`, shown to the user on their
401
+ return even though the context was recoverable. `WebGLContextMonitor` now arms the watchdog only while
402
+ the page is visible; each return to visibility restarts the full `restore_timeout`. Covered by three
403
+ visibility tests in `WebGLContextMonitor.spec.js`.
404
+
405
+ ### 10.4 What remains unverified, and why
406
+
407
+ The Browser pane available here does not composite, and Chrome only restores a lost context while the page
408
+ is visible, so `webglcontextrestored` never arrives — a real loss escalates to `Failed` instead of
409
+ recovering. Consequently these need one run in a visible tab:
410
+
411
+ 1. **A real browser-driven restore** (`__context_loss_test.run()` and `.repeat(3)`): does the browser hand
412
+ the context back, and do stale GL handles from the dead context misbehave? Everything downstream of the
413
+ restore event is already proven by the synthetic path; what is untested is the event itself.
414
+ 2. **The prolonged-outage ticker pause.** In a hidden tab the ticker loop never bootstraps
415
+ (`requestAnimationFrame` never fires, so `is_active` is false) and the guard correctly declines to pause
416
+ — which exercised the guard but not the pause.
417
+ 3. **The game's own scenes.** The game boots in a hidden tab but never renders, so per-frame GPU state in
418
+ FoW, decals, particles, Forward+ and water is covered by inspection only.
419
+
420
+ To run them: point the webpack entry at the harness, open the page with the pane visible, then
421
+
422
+ ```bash
423
+ npx jest -c jest.conf.json app/src/mir-engine/meep/src/engine/graphics/context/WebGLContextMonitor.spec.js
424
+ ```
425
+
426
+ and in the page console: `await __context_loss_test.run()`, then `await __context_loss_test.repeat(3)`.
427
+ Expect `restored: true`, `settled_vs_baseline` near zero, and no growth in `textures` / `geometries` /
428
+ `programs` across cycles.
429
+
430
+ ---
431
+
432
+ ## 11. Remaining phases
433
+
434
+ **Phase 4 — secondary contexts and async generation checks.** `computeImageBitmapEquality` handle reset
435
+ (§4.4, the silent-corruption one, worth doing on its own), `MinimapWorldGL` re-render on restore, and
436
+ generation checks on the long-running bakes (§4.5). `MeshPreview` is editor-only and can trail. Estimate
437
+ 1–2 days.
438
+
439
+ **Phase 5 (conditional) — renderer rebuild fallback.** If telemetry shows browsers refusing to restore in
440
+ practice, replace the terminal `Failed` state with a rebuild: dispose the renderer, create a new canvas,
441
+ re-attach `viewport.el` / `graphics.domElement` / styles / size, and re-run frame-buffer init *with*
442
+ disposal of the old targets. The monitor already supports it — `attach()` resets the loss budget and returns
443
+ to `Live` while keeping the generation counter monotonic. Only worth building on evidence.
444
+
445
+ **Phase 6 (deferred) — library tier.** VT and GPU-atlas residency invalidation, node-based particle GL
446
+ buffers, Hi-Z, impostor atlases (§4.3). Each is self-contained; do them when the subsystem is next touched.
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Applies the renderer configuration the engine relies on.
3
+ *
4
+ * Must be re-applied after a context restore: three.js rebuilds its internal modules from scratch in
5
+ * that case, and some of the settings live inside those modules rather than on the renderer itself -
6
+ * most notably the clear colour and alpha, which are owned by `WebGLBackground` and silently revert to
7
+ * transparent black.
8
+ *
9
+ * @param {WebGLRenderer} webGLRenderer
10
+ */
11
+ export function configureThreeRenderer(webGLRenderer: WebGLRenderer): void;
1
12
  export class GraphicsEngine {
2
13
  /**
3
14
  *
@@ -6,6 +17,13 @@ export class GraphicsEngine {
6
17
  * @constructor
7
18
  */
8
19
  constructor({ camera, debug }: Camera);
20
+ /**
21
+ * Tracks WebGL context loss. Rendering is suppressed while the context is gone, and GPU-side state
22
+ * that is not re-derived every frame is rebuilt when it comes back.
23
+ * @readonly
24
+ * @type {WebGLContextMonitor}
25
+ */
26
+ readonly context: WebGLContextMonitor;
9
27
  get isGraphicsEngine(): boolean;
10
28
  /**
11
29
  *
@@ -29,6 +47,25 @@ export class GraphicsEngine {
29
47
  buffersRendered: Signal;
30
48
  visibilityConstructionStarted: Signal;
31
49
  visibilityConstructionEnded: Signal;
50
+ /**
51
+ * WebGL context has been lost. No rendering happens until the context comes back; GPU-side
52
+ * resources must be treated as gone.
53
+ * @type {Signal}
54
+ */
55
+ contextLost: Signal;
56
+ /**
57
+ * WebGL context has been restored, carries the context generation number. Dispatched after
58
+ * the graphics engine has re-applied its own renderer state, so subscribers can render.
59
+ * Anything held on the GPU that is not rebuilt every frame has to be rebuilt here.
60
+ * @type {Signal<number>}
61
+ */
62
+ contextRestored: any;
63
+ /**
64
+ * Context recovery was abandoned, carries a {@link WebGLContextFailureReason}. Terminal -
65
+ * there will be no further rendering.
66
+ * @type {Signal<string>}
67
+ */
68
+ contextFailed: any;
32
69
  };
33
70
  /**
34
71
  * @type {Vector1}
@@ -124,19 +161,30 @@ export class GraphicsEngine {
124
161
  */
125
162
  getRenderer(): WebGLRenderer;
126
163
  /**
127
- * Set the scene's environment map. The supplied texture is treated as BOTH:
128
- * - the skybox background (shown directly), and
129
- * - the image-based-lighting (IBL) source for physically based materials.
164
+ * Set the scene's image-based-lighting (IBL) source.
130
165
  *
131
- * The texture is run through three.js' PMREMGenerator to produce the
132
- * prefiltered radiance assigned to `scene.environment`, while the original
133
- * texture is assigned to `scene.background`.
166
+ * The texture is run through three.js' PMREMGenerator to produce the prefiltered radiance assigned
167
+ * to `scene.environment`. The scene background is left alone, use
168
+ * {@link GraphicsEngine#set_environment_texture} to show the same texture as a skybox as well.
169
+ *
170
+ * The source texture is retained: the prefiltered radiance lives in a render target, which means its
171
+ * contents are lost along with the WebGL context, so the bake has to be repeatable. The bake is
172
+ * re-run automatically on context restore.
134
173
  *
135
174
  * Must be called after the renderer has started up.
136
175
  *
137
176
  * @param {THREE.Texture} texture environment texture (equirectangular or cube)
138
177
  * @param {EnvironmentTextureProjection} [projection=EnvironmentTextureProjection.Equirectangular] how the texture is projected
139
178
  */
179
+ set_environment_map(texture: THREE.Texture, projection?: EnvironmentTextureProjection): void;
180
+ /**
181
+ * Set the scene's environment map. The supplied texture is treated as BOTH:
182
+ * - the skybox background (shown directly), and
183
+ * - the image-based-lighting (IBL) source for physically based materials.
184
+ *
185
+ * @param {THREE.Texture} texture environment texture (equirectangular or cube)
186
+ * @param {EnvironmentTextureProjection} [projection=EnvironmentTextureProjection.Equirectangular] how the texture is projected
187
+ */
140
188
  set_environment_texture(texture: THREE.Texture, projection?: EnvironmentTextureProjection): void;
141
189
  updateSize(): void;
142
190
  /**
@@ -207,13 +255,14 @@ export class GraphicsEngine {
207
255
  render(): void;
208
256
  #private;
209
257
  }
258
+ import { WebGLRenderer } from "three";
259
+ import { WebGLContextMonitor } from "./context/WebGLContextMonitor.js";
210
260
  import Signal from "../../core/events/signal/Signal.js";
211
261
  import Vector1 from "../../core/geom/Vector1.js";
212
262
  import { Vector2 } from "../../core/geom/Vector2.js";
213
263
  import { RenderLayerManager } from "./render/layers/RenderLayerManager.js";
214
264
  import { Group } from "three";
215
265
  import { Camera } from "./ecs/camera/Camera.js";
216
- import { WebGLRenderer } from "three";
217
266
  import LayerCompositer from "./composit/LayerCompositer.js";
218
267
  import { FrameBufferManager } from "./render/buffer/FrameBufferManager.js";
219
268
  import { ShadowMapRenderer } from "./shadows/ShadowMapRenderer.js";
@@ -1 +1 @@
1
- {"version":3,"file":"GraphicsEngine.d.ts","sourceRoot":"","sources":["../../../../src/engine/graphics/GraphicsEngine.js"],"names":[],"mappings":"AAiEA;IAOI;;;;;OAKG;IACH,+BAJW,MAAM,EAoLhB;IA1LD,gCAEC;IAgBG;;;;OAIG;IACH,2BAA+C;IAK/C;;;;;QAOI;;WAEG;;QAEH;;WAEG;;;;;MAMN;IAED;;OAEG;IACH,YAFU,OAAO,CAEe;IAEhC;;;;;;;;;;OAUG;IACH,4BAFU,OAAO,CAEyB;IAE1C;;;OAGG;IACH,QAFU,kBAAkB,CAEU;IAWtC;;;OAGG;IACH,aAAkB;IAGlB;;;OAGG;IACH,cAFU,KAAK,CAEgB;IAK/B;;;OAGG;IACH,QAFU,MAAM,CAEI;IAEpB;;;OAGG;IACH,UAFU,aAAa,CAEH;IAGpB,+BAA0C;IAU1C;;OAEG;IACH,eAA+B;IAI/B;;;OAGG;IACH,uBAFU,kBAAkB,CAEgB;IAE5C;;;OAGG;IACH,oBAFU,iBAAiB,CAEsB;IAEjD;;;OAGG;IACH,gBAFU,iBAAiB,CAES;IAEpC;;;OAGG;IACH,sBAAoC;IAGpC;;;;OAIG;IACH,UAFU,OAAO,CAEG;IAEpB;;;OAGG;IACH,UAFU,OAAO,CAEG;IAEpB;;;OAGG;IACH,YAFU,MAAM,CAEG;IACnB,kKAcI;IAGR;;;OAGG;IACH,sBAFa,eAAe,CAI3B;IAED;;;OAGG;IACH,eAFa,aAAa,CAIzB;IAED;;;;;;;;;;;;;OAaG;IACH,iCAHW,MAAM,OAAO,eACb,4BAA4B,QAkCtC;IAED,mBAqCC;IAED;;;;OAIG;IACH,iCAEC;IAED;;;OAGG;IACH,0BAFa,MAAM,CAIlB;IAED,+BAcC;IAED,cAgEC;IAzBsB,8BAA0C;IA2BjE;;;OAGG;IACH,cAFa,MAAM,CAYlB;IAED;;OAEG;IACH,aAQC;IAED;;OAEG;IACH,yBAQC;IAED;;;;;;OAMG;IACH,yBALW,MAAM,KACN,MAAM,6CAMhB;IAED;;;;OAIG;IACH,8BAHW,OAAO,UAAQ,UACf,OAAO,UAAQ,QAczB;IAED;;;;;OAKG;IACH,8BAQC;IAED,0BAEC;IAED;;;OAGG;IACH,4BAFW,cAAc,QA8BxB;IAED;;OAEG;IACH,qBAqBC;IAED;;OAEG;IACH,0BAeC;IAED;;OAEG;IACH,eA8CC;;CACJ;mBAxpBkB,oCAAoC;oBACnC,4BAA4B;wBACxB,4BAA4B;mCAajB,uCAAuC;sBAlBnE,OAAO;uBAaS,wBAAwB;8BAbxC,OAAO;4BAYc,+BAA+B;mCAKxB,uCAAuC;kCAKxC,gCAAgC;kCADhC,oCAAoC;gCAPtC,uCAAuC;6CAW1B,2CAA2C;+BANzD,4BAA4B"}
1
+ {"version":3,"file":"GraphicsEngine.d.ts","sourceRoot":"","sources":["../../../../src/engine/graphics/GraphicsEngine.js"],"names":[],"mappings":"AAyCA;;;;;;;;;GASG;AACH,sDAFW,aAAa,QAoBvB;AAGD;IAyCI;;;;;OAKG;IACH,+BAJW,MAAM,EAwMhB;IA7ND;;;;;OAKG;IACH,kBAFU,mBAAmB,CAEO;IASpC,gCAEC;IAgBG;;;;OAIG;IACH,2BAA+C;IAK/C;;;;;QAOI;;WAEG;;QAEH;;WAEG;;;;;QAOH;;;;WAIG;qBADO,MAAM;QAGhB;;;;;WAKG;;QAEH;;;;WAIG;;MAEN;IAED;;OAEG;IACH,YAFU,OAAO,CAEe;IAEhC;;;;;;;;;;OAUG;IACH,4BAFU,OAAO,CAEyB;IAE1C;;;OAGG;IACH,QAFU,kBAAkB,CAEU;IAWtC;;;OAGG;IACH,aAAkB;IAGlB;;;OAGG;IACH,cAFU,KAAK,CAEgB;IAK/B;;;OAGG;IACH,QAFU,MAAM,CAEI;IAEpB;;;OAGG;IACH,UAFU,aAAa,CAEH;IAGpB,+BAA0C;IAU1C;;OAEG;IACH,eAA+B;IAI/B;;;OAGG;IACH,uBAFU,kBAAkB,CAEgB;IAE5C;;;OAGG;IACH,oBAFU,iBAAiB,CAEsB;IAEjD;;;OAGG;IACH,gBAFU,iBAAiB,CAES;IAEpC;;;OAGG;IACH,sBAAoC;IAGpC;;;;OAIG;IACH,UAFU,OAAO,CAEG;IAEpB;;;OAGG;IACH,UAFU,OAAO,CAEG;IAEpB;;;OAGG;IACH,YAFU,MAAM,CAEG;IACnB,kKAcI;IAGR;;;OAGG;IACH,sBAFa,eAAe,CAI3B;IAED;;;OAGG;IACH,eAFa,aAAa,CAIzB;IAED;;;;;;;;;;;;;;;OAeG;IACH,6BAHW,MAAM,OAAO,eACb,4BAA4B,QAStC;IAED;;;;;;;OAOG;IACH,iCAHW,MAAM,OAAO,eACb,4BAA4B,QAQtC;IA8CD,mBAqCC;IAED;;;;OAIG;IACH,iCAEC;IAED;;;OAGG;IACH,0BAFa,MAAM,CAIlB;IAED,+BAcC;IAED,cA6DC;IAzBsB,8BAA0C;IAgGjE;;;OAGG;IACH,cAFa,MAAM,CAYlB;IAED;;OAEG;IACH,aAUC;IAED;;OAEG;IACH,yBAQC;IAED;;;;;;OAMG;IACH,yBALW,MAAM,KACN,MAAM,6CAMhB;IAED;;;;OAIG;IACH,8BAHW,OAAO,UAAQ,UACf,OAAO,UAAQ,QAczB;IAED;;;;;OAKG;IACH,8BAQC;IAED,0BAEC;IAED;;;OAGG;IACH,4BAFW,cAAc,QA8BxB;IAED;;OAEG;IACH,qBAqBC;IAED;;OAEG;IACH,0BAeC;IAED;;OAEG;IACH,eAoDC;;CACJ;8BAv0BM,OAAO;oCAasB,kCAAkC;mBAVnD,oCAAoC;oBACnC,4BAA4B;wBACxB,4BAA4B;mCAcjB,uCAAuC;sBAnBnE,OAAO;uBAcS,wBAAwB;4BAFnB,+BAA+B;mCAMxB,uCAAuC;kCAKxC,gCAAgC;kCADhC,oCAAoC;gCAPtC,uCAAuC;6CAW1B,2CAA2C;+BANzD,4BAA4B"}