@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.
- package/README.md +2 -2
- package/build/bundle-worker-image-decoder.js +1 -1
- package/package.json +1 -1
- package/src/REVIEW_2026_08_06/adv-core.md +511 -0
- package/src/REVIEW_2026_08_06/adv-gfx.md +480 -0
- package/src/REVIEW_2026_08_06/adv-view.md +446 -0
- package/src/REVIEW_2026_08_06/api-core.md +296 -0
- package/src/REVIEW_2026_08_06/api-engine.md +195 -0
- package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +507 -0
- package/src/REVIEW_2026_08_06/core-binary.md +348 -0
- package/src/REVIEW_2026_08_06/core-collection.md +376 -0
- package/src/REVIEW_2026_08_06/core-math.md +221 -0
- package/src/REVIEW_2026_08_06/core-process.md +266 -0
- package/src/REVIEW_2026_08_06/core-science.md +223 -0
- package/src/REVIEW_2026_08_06/engine-ai-nav.md +279 -0
- package/src/REVIEW_2026_08_06/engine-animation.md +167 -0
- package/src/REVIEW_2026_08_06/engine-asset.md +166 -0
- package/src/REVIEW_2026_08_06/engine-ecs.md +920 -0
- package/src/REVIEW_2026_08_06/engine-input.md +197 -0
- package/src/REVIEW_2026_08_06/engine-misc.md +248 -0
- package/src/REVIEW_2026_08_06/engine-terrain.md +52 -0
- package/src/REVIEW_2026_08_06/generation.md +176 -0
- package/src/REVIEW_2026_08_06/geom-2d.md +434 -0
- package/src/REVIEW_2026_08_06/geom-3d.md +117 -0
- package/src/REVIEW_2026_08_06/gfx-gi.md +143 -0
- package/src/REVIEW_2026_08_06/gfx-render.md +740 -0
- package/src/REVIEW_2026_08_06/gfx-texture.md +690 -0
- package/src/REVIEW_2026_08_06/prior-criticals.md +49 -0
- package/src/REVIEW_2026_08_06/prior-highs.md +138 -0
- package/src/REVIEW_2026_08_06/test-gaps.md +364 -0
- package/src/REVIEW_2026_08_06/view.md +667 -0
- package/src/REVIEW_2026_08_06.md +571 -0
- package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
- package/src/core/binary/BinaryBuffer.js +12 -1
- package/src/core/binary/BitImage2.d.ts +5 -1
- package/src/core/binary/BitImage2.d.ts.map +1 -1
- package/src/core/binary/BitImage2.js +5 -1
- package/src/core/bvh8/build/NodeProxy.d.ts.map +1 -1
- package/src/core/bvh8/build/NodeProxy.js +314 -308
- package/src/core/collection/list/List.d.ts +15 -0
- package/src/core/collection/list/List.d.ts.map +1 -1
- package/src/core/collection/list/List.js +34 -0
- package/src/core/collection/list/SortedListProjection.d.ts.map +1 -1
- package/src/core/collection/list/SortedListProjection.js +4 -1
- package/src/core/collection/map/BiMap.d.ts +25 -2
- package/src/core/collection/map/BiMap.d.ts.map +1 -1
- package/src/core/collection/map/BiMap.js +57 -3
- package/src/core/debug/matchers/IsAnything.d.ts.map +1 -1
- package/src/core/debug/matchers/IsAnything.js +5 -1
- package/src/core/events/signal/Signal.d.ts.map +1 -1
- package/src/core/events/signal/Signal.js +3 -1
- package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +433 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts +31 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.js +84 -0
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts +24 -2
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/polygon/polygon2_signed_area.js +52 -34
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +86 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts +40 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.js +1280 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.js +77 -0
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts +6 -3
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +26 -13
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts +17 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.js +35 -23
- package/src/core/geom/2d/v2_morton_encode.d.ts +20 -0
- package/src/core/geom/2d/v2_morton_encode.d.ts.map +1 -0
- package/src/core/geom/2d/v2_morton_encode.js +23 -0
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -1
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +4 -1
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts +27 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.js +60 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts +34 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.js +94 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +8 -2
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -1
- package/src/core/geom/3d/quaternion/quat3_multiply.js +8 -2
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/ConvexHullShape3D.js +32 -2
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts +23 -6
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.js +268 -124
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts +17 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.js +44 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts +20 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.js +40 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts +19 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.js +34 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts +22 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.js +37 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts +87 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.js +599 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts +32 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.js +66 -7
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts +22 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.js +83 -38
- package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +554 -0
- package/src/core/geom/vec3/v3_cotangent.d.ts +6 -2
- package/src/core/geom/vec3/v3_cotangent.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cotangent.js +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cross.js +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_multiply.js +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_subtract.js +6 -2
- package/src/core/localization/Localization.js +1 -1
- package/src/core/math/random/randomSeed.d.ts +13 -0
- package/src/core/math/random/randomSeed.d.ts.map +1 -0
- package/src/core/math/random/randomSeed.js +16 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts +50 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.js +107 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts +34 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_reverse.js +49 -0
- package/src/core/math/statistics/hammersley_sequence.d.ts +17 -4
- package/src/core/math/statistics/hammersley_sequence.d.ts.map +1 -1
- package/src/core/math/statistics/hammersley_sequence.js +25 -16
- package/src/core/process/Future.d.ts.map +1 -1
- package/src/core/process/Future.js +12 -0
- package/src/core/process/PromiseWatcher.d.ts.map +1 -1
- package/src/core/process/PromiseWatcher.js +9 -2
- package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
- package/src/core/process/executor/ConcurrentExecutor.js +12 -2
- package/src/core/process/worker/WorkerProxy.d.ts.map +1 -1
- package/src/core/process/worker/WorkerProxy.js +5 -2
- package/src/core/process/worker/extractTransferables.d.ts.map +1 -1
- package/src/core/process/worker/extractTransferables.js +4 -1
- package/src/engine/Engine.d.ts +12 -12
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +149 -10
- package/src/engine/asset/loaders/ArrayBufferLoader.d.ts.map +1 -1
- package/src/engine/asset/loaders/ArrayBufferLoader.js +7 -0
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.d.ts.map +1 -1
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.js +3 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.js +4 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +29 -0
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +84 -23
- package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +446 -0
- package/src/engine/graphics/GraphicsEngine.d.ts +56 -7
- package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
- package/src/engine/graphics/GraphicsEngine.js +196 -24
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts +9 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.js +17 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts +106 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextMonitor.js +341 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts +14 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextState.js +24 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts +2 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts.map +1 -0
- package/src/engine/graphics/context/testWebGLContextLoss.js +696 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.js +68 -32
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts +14 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.js +41 -0
- package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +5 -0
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +2 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +3 -5
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.js +3 -2
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js +3 -1
- package/src/engine/graphics/geometry/computeMeshSurfaceArea.js +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts +5 -0
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts.map +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.js +26 -21
- package/src/engine/graphics/particles/particular/group/ParticleGroup.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/group/ParticleGroup.js +19 -8
- package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.js +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.js +10 -3
- package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/TextureAtlas.js +4 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js +11 -0
- package/src/engine/graphics/texture/cubemap/load_environment_map.d.ts.map +1 -1
- package/src/engine/graphics/texture/cubemap/load_environment_map.js +5 -2
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js +26 -18
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts +15 -2
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.js +29 -3
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts +11 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js +158 -171
- package/src/engine/graphics/texture/sampler/sampler2d_paint.js +1 -1
- package/src/engine/graphics/texture/sampler/util/bitSet2Sampler2D.js +2 -2
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.d.ts.map +1 -1
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.js +10 -3
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts +14 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts.map +1 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.js +38 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +433 -425
- package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +38 -13
- package/src/engine/physics/contact/ManifoldStore.d.ts +15 -3
- package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
- package/src/engine/physics/contact/ManifoldStore.js +15 -3
- package/src/engine/physics/ecs/Joint.d.ts +7 -4
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.js +7 -4
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +14 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +16 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +15 -7
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/convex_convex_manifold.js +33 -10
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_concave.js +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_hit.js +6 -2
- package/src/engine/physics/queries/raycast.d.ts.map +1 -1
- package/src/engine/physics/queries/raycast.js +11 -4
- package/src/engine/save/storage/IndexedDBStorage.d.ts.map +1 -1
- package/src/engine/save/storage/IndexedDBStorage.js +21 -3
- package/src/engine/simulation/Ticker.d.ts +12 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +18 -0
- package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
- package/src/engine/sound/simulation/AcousticSimulator.js +9 -1
- package/src/engine/sound/simulation/core/VolumeField.d.ts +6 -2
- package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/core/VolumeField.js +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/AcousticProbeField.js +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.js +6 -2
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts +3 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.js +3 -1
- package/src/engine/ui/GUIEngine.d.ts.map +1 -1
- package/src/engine/ui/GUIEngine.js +8 -1
- package/src/generation/theme/TerrainTheme.d.ts.map +1 -1
- package/src/generation/theme/TerrainTheme.js +2 -1
- package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
- package/src/generation/theme/ThemeEngine.js +6 -5
- package/src/view/ViewGroup.d.ts.map +1 -1
- package/src/view/ViewGroup.js +9 -7
- package/src/view/graphics/WebGLContextFailureView.d.ts +19 -0
- package/src/view/graphics/WebGLContextFailureView.d.ts.map +1 -0
- package/src/view/graphics/WebGLContextFailureView.js +76 -0
- package/src/view/minimap/Minimap.d.ts +8 -0
- package/src/view/minimap/Minimap.d.ts.map +1 -1
- package/src/view/minimap/Minimap.js +16 -3
- package/src/view/minimap/dom/MinimapMarkerView.d.ts.map +1 -1
- 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
|
|
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
|
-
*
|
|
133
|
-
* texture
|
|
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":"
|
|
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"}
|