@woosh/meep-engine 2.168.0 → 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/EngineHarness.d.ts.map +1 -1
- package/src/engine/EngineHarness.js +4 -1
- 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/Trail2D.d.ts +5 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.js +15 -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/Trail3D.d.ts +4 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.js +488 -476
- 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 +280 -253
- 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,740 @@
|
|
|
1
|
+
# Correctness review — `engine/graphics` (ecs, render, shaders, camera, root)
|
|
2
|
+
|
|
3
|
+
Scope: every non-spec, non-prototype source file under
|
|
4
|
+
`meep/src/engine/graphics/{ecs,render,shaders,camera}` plus the files directly in
|
|
5
|
+
`meep/src/engine/graphics/`. three.js r136.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
### GFXR-1 `Camera.projectWorldPointOntoNearPlane` projects onto the RIGHT frustum plane
|
|
10
|
+
- **Severity**: MAJOR
|
|
11
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/camera/Camera.js:279`
|
|
12
|
+
- **Defect**: The method takes `scratch_frustum.planes[0]` as "the near plane". `frustum_from_camera`
|
|
13
|
+
fills the frustum via three's `Frustum.setFromProjectionMatrix`, whose plane order is
|
|
14
|
+
`[0]=right, [1]=left, [2]=bottom, [3]=top, [4]=far, [5]=near`. Index 0 is the RIGHT side plane.
|
|
15
|
+
The sibling file `ecs/camera/pp/PerfectPanner.js:9-10` states the correct convention
|
|
16
|
+
(`NEAR_PLANE_INDEX = 5; FAR_PLANE_INDEX = 4;`), so the codebase already knows the ordering.
|
|
17
|
+
- **Failure scenario**: Camera at the origin looking down `-Z`, 45° fov, near 0.1. Call
|
|
18
|
+
`camera.projectWorldPointOntoNearPlane(0, 0, -10, out)`. Expected `out ≈ (0, 0, -0.1)` — the point
|
|
19
|
+
pushed onto the near plane in front of the eye. Actual: the point is projected onto the right
|
|
20
|
+
side plane of the frustum, landing several units off to the camera's right at a depth of 10.
|
|
21
|
+
Any UI/marker/clamping code built on this public method places its output in the wrong place
|
|
22
|
+
entirely, and gets *worse* the further off-axis the input point is.
|
|
23
|
+
- **Fix**: `const near = scratch_frustum.planes[5];`
|
|
24
|
+
- **Confidence**: Confirmed
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
### GFXR-2 Orthographic aspect correction uses the wrong delta when the viewport narrows
|
|
29
|
+
- **Severity**: MAJOR
|
|
30
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/camera/set_camera_aspect_ratio.js:35-40`
|
|
31
|
+
- **Defect**: The widening branch (`aspectDelta > 0`) grows the horizontal extent by `h * aspectDelta`,
|
|
32
|
+
which is exactly right (`w' = w + h·aspect − w = h·aspect`). The narrowing branch grows the vertical
|
|
33
|
+
extent by `-h * aspectDelta`, which does **not** solve `w / h' = aspect`. The required delta is
|
|
34
|
+
`w/aspect − h`, i.e. `-h * aspectDelta / aspect`.
|
|
35
|
+
```js
|
|
36
|
+
} else if (aspectDelta < 0) {
|
|
37
|
+
const d = -h * aspectDelta; // <-- missing the /aspect
|
|
38
|
+
c.top += d / 2;
|
|
39
|
+
c.bottom -= d / 2;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
- **Failure scenario**: `OrthographicCamera(-10, 10, 10, -10, …)` (square frustum) in a portrait
|
|
43
|
+
viewport, `set_camera_aspect_ratio(camera, 100, 200)`. Requested aspect 0.5; the code produces
|
|
44
|
+
`top=15, bottom=-15`, i.e. frustum aspect `20/30 = 0.667`. Everything rendered through that camera
|
|
45
|
+
is stretched vertically by 33%: circles become ellipses, a square tile is drawn as a rectangle.
|
|
46
|
+
Verified numerically. The existing spec (`set_camera_aspect_ratio.spec.js`) only exercises the
|
|
47
|
+
widening case, which is why this survives.
|
|
48
|
+
- **Fix**: `const d = -h * aspectDelta / aspect;` — or, uniformly for both branches, set
|
|
49
|
+
`h' = w / aspect` when `aspect < existingAspect` and `w' = h * aspect` otherwise.
|
|
50
|
+
- **Confidence**: Confirmed
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### GFXR-3 `CameraSystem` rebuild leaks a THREE camera into the scene on every fov/projection change
|
|
55
|
+
- **Severity**: MAJOR
|
|
56
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/camera/CameraSystem.js:146-148`
|
|
57
|
+
- **Defect**: `rebuild()` replaces `camera.object` with a freshly built THREE camera but never removes
|
|
58
|
+
the previous one from `this.scene`. `link()` is the only place that does `this.scene.add(...)`, and
|
|
59
|
+
`unlink()` removes only the *current* `camera.object`. The new object gets re-added to the scene by
|
|
60
|
+
`GraphicsEngine.render` (`if (scene.children.indexOf(camera) < 0) scene.add(camera)`), so every
|
|
61
|
+
rebuild adds one more permanent child. `rebuild` is wired to `camera.fov.onChanged` and
|
|
62
|
+
`camera.projectionType.onChanged` (lines 172, 174).
|
|
63
|
+
- **Failure scenario**: A fov-zoom effect animates `Camera.fov` (a `Vector1`, observable precisely so
|
|
64
|
+
it can be animated) at 60 Hz. Each frame builds a new `THREE.PerspectiveCamera` and appends it to
|
|
65
|
+
`graphics.scene.children`, which is never pruned. After one minute `scene.children` holds ~3600 dead
|
|
66
|
+
cameras: unbounded memory growth, an O(n) `scene.children.indexOf(camera)` in `GraphicsEngine.render`
|
|
67
|
+
every frame, and an O(n) root walk in `NormalFrameBuffer.render`, which traverses `scene.children`
|
|
68
|
+
each frame. Frame time degrades continuously for as long as the zoom runs.
|
|
69
|
+
- **Fix**: In `rebuild()`, capture the old object, `this.scene.remove(old)` and
|
|
70
|
+
`this.scene.add(camera.object)` after building. Better still, do not rebuild for a fov change — set
|
|
71
|
+
`camera.object.fov` and call `updateProjectionMatrix()`; only `projectionType` genuinely needs a new
|
|
72
|
+
object.
|
|
73
|
+
- **Confidence**: Confirmed
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### GFXR-4 `Trail2DSystem` seeds the ribbon at a different point from where the head is driven
|
|
78
|
+
- **Severity**: MAJOR
|
|
79
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/trail2d/Trail2DSystem.js:156-158` vs `:234-235`
|
|
80
|
+
- **Defect**: Build-time seeding adds the offset in world space, ignoring the entity's rotation and
|
|
81
|
+
scale:
|
|
82
|
+
```js
|
|
83
|
+
const position_x = trail_offset.x + transform_position.x; // __build_trail
|
|
84
|
+
```
|
|
85
|
+
while the per-frame head update carries the offset through the entity's full transform:
|
|
86
|
+
```js
|
|
87
|
+
newPosition.copy(trail.offset);
|
|
88
|
+
newPosition.applyMatrix4(transform.matrix); // updateTrailEntity
|
|
89
|
+
```
|
|
90
|
+
The sibling `Trail3DSystem` has exactly this invariant written down and enforced via a shared
|
|
91
|
+
`compute_head_position` helper (`ecs/trail3d/Trail3DSystem.js:34-53`), whose doc comment describes
|
|
92
|
+
the resulting artifact verbatim. `Trail2DSystem` was not given the same treatment.
|
|
93
|
+
- **Failure scenario**: A projectile entity with `Trail2D.offset = (0, 0, -1)` (trail emitted from the
|
|
94
|
+
tail) rotated 90° about Y so it flies along `+X`. `__build_trail` seeds every ribbon point at
|
|
95
|
+
`position + (0, 0, -1)`; the first `updateTrailEntity` puts the head at `position + (-1, 0, 0)`.
|
|
96
|
+
The first rendered frame contains a ~1.41-unit ribbon streak pointing in a direction that has
|
|
97
|
+
nothing to do with the projectile's travel — a visible flash at every spawn.
|
|
98
|
+
- **Fix**: Replace lines 156-158 with the same transform-carried computation used in
|
|
99
|
+
`updateTrailEntity` (ideally extract a shared `compute_head_position(result, trail, transform)` as
|
|
100
|
+
Trail3D does).
|
|
101
|
+
- **Confidence**: Confirmed
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### GFXR-5 `loadCubeTexture` calls `ImageUtils.loadTextureCube`, removed from three long before r136
|
|
106
|
+
- **Severity**: MAJOR
|
|
107
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/loadCubeTexture.js:27`
|
|
108
|
+
- **Defect**: `const textureCube = ImageUtils.loadTextureCube(urls);` — in the vendored
|
|
109
|
+
`three@0.136.0`, `src/extras/ImageUtils.js` declares exactly one member, `static getDataURL(image)`.
|
|
110
|
+
There is no `loadTextureCube`.
|
|
111
|
+
- **Failure scenario**: Any caller of this exported (public-surface) helper —
|
|
112
|
+
`loadCubeTexture('assets/sky', 'png')` — throws `TypeError: ImageUtils.loadTextureCube is not a
|
|
113
|
+
function` on the first line that matters, so no cubemap is ever produced. The function cannot
|
|
114
|
+
succeed under any input.
|
|
115
|
+
- **Fix**: Delete the function, or reimplement it on `CubeTextureLoader`:
|
|
116
|
+
`new CubeTextureLoader().load(urls)` (which already returns a `CubeTexture` with
|
|
117
|
+
`CubeReflectionMapping`). Note the package already has a working path for this —
|
|
118
|
+
`texture/cubemap/load_environment_map.js`, used by `load_and_set_cubemap_v0.js`.
|
|
119
|
+
- **Confidence**: Confirmed
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### GFXR-6 `Lines` writes `drawRange.count` in the wrong units (twice, two different wrong values)
|
|
124
|
+
- **Severity**: MINOR
|
|
125
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/render/Lines.js:61` and `:105`
|
|
126
|
+
- **Defect**: `geometry.drawRange.count` is a **vertex** count for a non-indexed geometry. The
|
|
127
|
+
geometry stores `capacity * 2` vertices (2 per line segment).
|
|
128
|
+
- `setCount(x)` writes `x * 6` — the *float* count of the position attribute.
|
|
129
|
+
- `add_line` then overwrites it with `this.count` — the *line* count.
|
|
130
|
+
Neither is `count * 2`. Separately, `ensure_capacity` → `build()` replaces `this.geometry` without
|
|
131
|
+
disposing the old `BufferGeometry`, and `dispose()` never disposes `this.mesh.material`.
|
|
132
|
+
- **Failure scenario**: `const l = new Lines(); l.add_line([0,0,0],[1,0,0],[1,1,1,1]);` and add
|
|
133
|
+
`l.mesh` to a scene. `drawRange.count === 1`; `LineSegments` needs vertex pairs, so zero segments
|
|
134
|
+
are drawn. Adding 10 lines draws 5. Had `add_line` not overwritten line 61's value, `x*6` would
|
|
135
|
+
exceed the allocated `x*2` vertices and WebGL would raise
|
|
136
|
+
`INVALID_OPERATION: attempt to access out of range vertices`.
|
|
137
|
+
- **Fix**: `this.geometry.drawRange.count = x * 2;` in `setCount`, and delete the redundant
|
|
138
|
+
assignment at line 105. Dispose the replaced geometry in `build()`.
|
|
139
|
+
- **Confidence**: Confirmed (the class also logs `'feature not fully implemented'`, so this is
|
|
140
|
+
acknowledged WIP — listed for completeness)
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
### GFXR-7 `OutlineRenderer` caches the ID-pass mesh with a frozen geometry reference
|
|
145
|
+
- **Severity**: MINOR
|
|
146
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/highlight/renderer/OutlineRenderer.js:96-117`
|
|
147
|
+
- **Defect**: `obtain_id_mesh` builds the silhouette proxy once per source `Object3D` and caches it in
|
|
148
|
+
a module-level `WeakMap`. On a cache hit it refreshes the skinning state (`bindMatrix`,
|
|
149
|
+
`bindMatrixInverse`, `skeleton`) but never re-reads `object.geometry`. The equivalent proxy builder
|
|
150
|
+
in the same package, `render/utils/ThreeBypassRenderer.js:101`, *does* re-assign
|
|
151
|
+
`mesh.geometry = geometry` on every rebuild — the divergence is the tell.
|
|
152
|
+
- **Failure scenario**: A highlighted `SGMesh` entity crosses an LOD threshold and
|
|
153
|
+
`SGMeshSystem`/`ShadedGeometrySystem` swaps the `Object3D`'s `geometry` for the next LOD. The
|
|
154
|
+
visible mesh changes silhouette; the outline keeps tracing the previous LOD's geometry, so the
|
|
155
|
+
highlight no longer hugs the object — offset/oversized outline until the object is unhighlighted.
|
|
156
|
+
- **Fix**: In `obtain_id_mesh`, after the cache lookup, add `mesh.geometry = object.geometry;`
|
|
157
|
+
unconditionally.
|
|
158
|
+
- **Confidence**: Likely
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### GFXR-8 `Water.updateShaderForTerrain` leaks a full terrain heightmap texture per call
|
|
163
|
+
- **Severity**: MINOR
|
|
164
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/water/Water.js:138-152`
|
|
165
|
+
- **Defect**: A new `DataTexture` wrapping the whole terrain height sampler is allocated and assigned
|
|
166
|
+
to `shader.uniforms.tHeightTexture.value`; the texture previously held by that uniform is never
|
|
167
|
+
`.dispose()`d. `WaterSystem.link` pushes the component onto `updateQueue`, so this runs once per
|
|
168
|
+
link — and `link` reuses the existing `component.__shader` when the component is re-linked
|
|
169
|
+
(`WaterSystem.js:164-175`), so each re-link allocates another one.
|
|
170
|
+
- **Failure scenario**: Level reload cycles unlink and re-link the Water component. Each cycle uploads
|
|
171
|
+
a fresh `R32F` heightmap — for a 1024×1024 terrain that is 4 MB of GPU memory — and orphans the
|
|
172
|
+
previous one. Twenty reloads leak ~80 MB of VRAM that three.js will never reclaim, because nothing
|
|
173
|
+
holds a reference to call `dispose()` on.
|
|
174
|
+
- **Fix**: Capture `const previous = shader.uniforms.tHeightTexture.value;` before the assignment and
|
|
175
|
+
`previous?.dispose()` after — the same replace-then-release shape `GraphicsEngine#bake_environment`
|
|
176
|
+
already uses for the PMREM target.
|
|
177
|
+
- **Confidence**: Confirmed
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### GFXR-9 `FrameGraph.exportToJson` conflates resource-node ids with resource-registry ids
|
|
182
|
+
- **Severity**: MINOR
|
|
183
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/render/frame_graph/FrameGraph.js:479-508`
|
|
184
|
+
- **Defect**: The two id spaces are independent — `_createResourceEntry` numbers entries by
|
|
185
|
+
`__resource_registry.length`, `_createResourceNode` numbers nodes by `__resource_nodes.length` —
|
|
186
|
+
and `clone_resource` adds nodes without adding entries, so they diverge. The export passes a
|
|
187
|
+
registry id where a node id is required:
|
|
188
|
+
```js
|
|
189
|
+
const node = this.getResourceNode(entry.resource_id); // registry id -> node lookup
|
|
190
|
+
...
|
|
191
|
+
if (pass.reads(entry.resource_id)) // registry id vs node ids in resource_reads
|
|
192
|
+
```
|
|
193
|
+
`getResourceNode`'s only protection is an `assert.equal`, which is compiled out in production.
|
|
194
|
+
- **Failure scenario**: `FramePassBuilder.write()` calls `graph.clone_resource()` for any write to a
|
|
195
|
+
resource the pass did not create — the normal case as soon as two passes touch one resource. In a
|
|
196
|
+
three-pass graph where pass B writes pass A's target, node ids run ahead of registry ids and
|
|
197
|
+
`exportToJson()` labels resources with another resource's name and emits reader/writer lists that
|
|
198
|
+
point at the wrong passes. The rendered GraphViz/JSON diagram misrepresents the pipeline it exists
|
|
199
|
+
to debug.
|
|
200
|
+
- **Fix**: Track the owning node per entry (or resolve registry id → node by scanning
|
|
201
|
+
`__resource_nodes` for `resource_id === entry.resource_id`), and compare `pass.reads/writes` against
|
|
202
|
+
node ids, not registry ids.
|
|
203
|
+
- **Confidence**: Confirmed
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### GFXR-10 `PathDisplayContext.__build` aborts the whole build on one unknown spec type
|
|
208
|
+
- **Severity**: MINOR
|
|
209
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/path/PathDisplaySystem.js:135-139`
|
|
210
|
+
- **Defect**: Inside the loop over `display.specs`, an unrecognised `spec.type` logs a warning and
|
|
211
|
+
`return`s out of the entire method — skipping the remaining specs, the `ParentEntity` attachment
|
|
212
|
+
loop, `__build_existing_entities()`, and the `PathDisplayEvents.BuildComplete` dispatch. The
|
|
213
|
+
neighbouring failure path in the same loop (a builder that throws, lines 141-146) correctly
|
|
214
|
+
`catch`es and continues.
|
|
215
|
+
- **Failure scenario**: A `PathDisplay` with two specs, `[Ribbon, <stale type from an old save>]`. The
|
|
216
|
+
Ribbon builder runs and pushes its entities into `__owned_entities`, then the second spec hits the
|
|
217
|
+
`return`. Those entities are never `build(ecd)`-ed into the dataset, so the path renders nothing at
|
|
218
|
+
all — and `BuildComplete` never fires, so anything awaiting that event waits forever.
|
|
219
|
+
- **Fix**: `continue;` instead of `return;`.
|
|
220
|
+
- **Confidence**: Confirmed
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
### GFXR-11 `ShadedGeometrySystem.shutdown` never removes its render layer
|
|
225
|
+
- **Severity**: CRITICAL
|
|
226
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js:254` (create) vs `:280-294` (shutdown)
|
|
227
|
+
- **Defect**: `startup` does `this.__render_layer = graphics.layers.create('shaded-geometry')` and installs a
|
|
228
|
+
`buildVisibleSet` closure that resolves a per-view context from `this.__view_contexts`. `shutdown`
|
|
229
|
+
tears down every view context but never calls `graphics.layers.remove(this.__render_layer)`.
|
|
230
|
+
`graphics.layers` lives on the `GraphicsEngine`, which outlives the `EntityManager`. Every peer
|
|
231
|
+
system does remove its layer — `MeshSystem.js:247`, `WaterSystem.js:73`, `Trail2DSystem.js:117`,
|
|
232
|
+
`Trail3DSystem.js:134`, `GizmoRenderingPlugin.js:18`.
|
|
233
|
+
- **Failure scenario**: Level teardown/reload. `EntityManager.shutdown()` runs
|
|
234
|
+
`ShadedGeometrySystem.shutdown`, emptying `__view_contexts`; the layer stays registered and visible.
|
|
235
|
+
(a) If any frame renders before the new dataset is up, `VisibilityComputer.__processLayer` calls the
|
|
236
|
+
stale `buildVisibleSet`, `this.__view_contexts.get(view)` is `undefined`, and
|
|
237
|
+
`ctx.collect(...)` throws `TypeError: Cannot read properties of undefined (reading 'collect')` —
|
|
238
|
+
which aborts the whole visibility build, so *every* layer stops rendering.
|
|
239
|
+
(b) Adding a fresh `ShadedGeometrySystem` for the next level makes
|
|
240
|
+
`graphics.layers.create('shaded-geometry')` throw
|
|
241
|
+
`RenderLayer named 'shaded-geometry' already exists` (`RenderLayerManager.js:117`) — a hard crash on
|
|
242
|
+
level reload. The stale `compute_depth_range` closure also pins the dead BVH.
|
|
243
|
+
- **Fix**: In `shutdown`, `graphics.layers.remove(this.__render_layer); this.__render_layer = null;`
|
|
244
|
+
- **Confidence**: Confirmed
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### GFXR-12 `SGMesh.materialOverride` is applied after `link`, bypassing the Forward+ material transform
|
|
249
|
+
- **Severity**: MAJOR
|
|
250
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshSystem.js:205-207`, and the same defect at `aggregate/SGMesh.js:120-131`
|
|
251
|
+
- **Defect**: `ShadedGeometrySystem.link` exists precisely to close this hole; its own comment says the
|
|
252
|
+
material is forced "through the manager's transform chain, so that shader injection performed by the
|
|
253
|
+
lighting engine (Forward+ clustered lights, decals) cannot be skipped by callers that assign
|
|
254
|
+
`sg.material` directly" (`ShadedGeometrySystem.js:305-311`). In `SGMeshSystem`,
|
|
255
|
+
`entity_node.build(ecd)` (line 179) synchronously drives `ShadedGeometrySystem.link` for every child
|
|
256
|
+
`ShadedGeometry`, installing the transformed material — and *then* the traverse at 190-207 overwrites
|
|
257
|
+
it with the raw override:
|
|
258
|
+
```js
|
|
259
|
+
if (material_override !== null) {
|
|
260
|
+
sg.material = material_override;
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
`SGMesh.applyMaterialOverride()`, reached from the `set materialOverride` setter, does the same on an
|
|
264
|
+
already-linked hierarchy.
|
|
265
|
+
- **Failure scenario**: A unit is spawned with `SGMesh.fromURL('unit.gltf')` and a `materialOverride`
|
|
266
|
+
(team tint / ghost preview / damage flash) using a lit `MeshStandardMaterial`. Every part receives
|
|
267
|
+
the raw material, which never went through `MaterialManager.obtain`, so the Forward+
|
|
268
|
+
`onBeforeCompile` clustered-light injection is absent. The overridden unit renders with *no*
|
|
269
|
+
clustered point lights and no decals while every neighbouring unit in the same scene is lit
|
|
270
|
+
correctly — it looks flat/black at night. The `assert.ok(!isForwardPlusLightingSkipped(...))` guard
|
|
271
|
+
in `link` cannot catch it: it runs before the override, and asserts are stripped in production.
|
|
272
|
+
Reference counting is also skipped, so `MaterialManager` can neither dedupe nor reclaim the override.
|
|
273
|
+
- **Fix**: Route both sites through the manager
|
|
274
|
+
(`sg.material = materialManager.obtain(material_override).getValue()`), tracking/releasing the
|
|
275
|
+
reference the same way `ShadedGeometrySystem`'s `#material_references` does.
|
|
276
|
+
- **Confidence**: Confirmed (ordering verified; the lighting consequence follows from the contract
|
|
277
|
+
documented in `ShadedGeometrySystem.link`)
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### GFXR-13 `BoneMapping.build` commits the WORST bone name match first
|
|
282
|
+
- **Severity**: MAJOR
|
|
283
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/mesh/skeleton/BoneMapping.js:13-15, 55, 61`
|
|
284
|
+
- **Defect**: `string_compute_similarity` is Jaro-Winkler and is documented "Higher value means string
|
|
285
|
+
are more similar". The comparator sorts **descending** (best first), and the consumption loop then
|
|
286
|
+
takes from the tail:
|
|
287
|
+
```js
|
|
288
|
+
function byDistance(a, b) { return b.distance - a.distance; } // descending
|
|
289
|
+
matches.sort(byDistance);
|
|
290
|
+
...
|
|
291
|
+
const match = matches.pop(); // takes the LEAST similar pair
|
|
292
|
+
```
|
|
293
|
+
Because the inner loop immediately evicts every remaining entry sharing that `boneIndex` or
|
|
294
|
+
`inputIndex`, the worst pairing is locked in permanently — this is a greedy assignment run in
|
|
295
|
+
exactly reverse order.
|
|
296
|
+
- **Failure scenario**: Load a rigged glTF whose bones are `Hips, Spine, Chest, Neck, Head, …`.
|
|
297
|
+
`SkeletonUtils.findSkeletonBoneByType` builds the mapping on first use. The globally least-similar
|
|
298
|
+
(bone, name) pair — say (`HumanoidBoneType.Head`, `"toe-right"`) — is committed first and consumes
|
|
299
|
+
the `Head` slot. `Mesh.getBonePositionByType(result, HumanoidBoneType.Head)` then returns the world
|
|
300
|
+
position of the right toe, so anything anchored to a humanoid bone (weapon socket, head-mounted VFX,
|
|
301
|
+
nameplate) attaches to the wrong body part.
|
|
302
|
+
- **Fix**: Consume best-first — either flip the comparator to `a.distance - b.distance` and keep
|
|
303
|
+
`pop()`, or keep the sort and use `matches.shift()` (with the inner splice loop starting at 0).
|
|
304
|
+
- **Confidence**: Confirmed
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
### GFXR-14 `SGMeshAnimationControllerSystem.update` spins on one unloaded mesh and starves the rest of the queue
|
|
309
|
+
- **Severity**: MAJOR
|
|
310
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshAnimationControllerSystem.js:159-167` with `:57-68`, `:90-92`
|
|
311
|
+
- **Defect**: `update` pops up to 1000 entities per tick and calls `this.#process(entity)` without
|
|
312
|
+
awaiting. `#process` re-enqueues synchronously when the mesh has not loaded
|
|
313
|
+
(`if (mesh.node === null) { this.#enqueue(entity); return; }`), and `#enqueue` **pushes to the tail**
|
|
314
|
+
while `update` **pops from the tail**. The same entity is therefore popped and re-pushed
|
|
315
|
+
immediately, for all 1000 iterations.
|
|
316
|
+
```js
|
|
317
|
+
const entity = this.#queue.pop();
|
|
318
|
+
this.#process(entity); // re-pushes to the same end it was popped from
|
|
319
|
+
```
|
|
320
|
+
- **Failure scenario**: Three units spawn in the same frame; A and B have cached meshes, C's glTF is
|
|
321
|
+
still downloading. The queue is `[A, B, C]`. Frame 1: `pop()` yields C, C re-queues, `pop()` yields C
|
|
322
|
+
again — 1000 times. A and B are never reached. This repeats every frame until C's asset resolves, so
|
|
323
|
+
A and B never get `controller.bound` populated and stand completely frozen for the whole duration of
|
|
324
|
+
C's download, even though their own meshes were ready immediately. Also burns 1000 pop/push cycles
|
|
325
|
+
per frame doing nothing.
|
|
326
|
+
- **Fix**: Use FIFO — `this.#queue.shift()` in `update` (or push to the head in `#enqueue`) — and cap
|
|
327
|
+
the loop by the queue length captured *before* the loop, so re-queued entities are deferred to the
|
|
328
|
+
next tick rather than reprocessed within the same one.
|
|
329
|
+
- **Confidence**: Confirmed
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
### GFXR-15 HiZ occlusion query samples `n_max.y` as an x coordinate
|
|
334
|
+
- **Severity**: MAJOR
|
|
335
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/render/visibility/hiz/query/QueryShader.js:108`
|
|
336
|
+
- **Defect**: The four taps are meant to be the four corners of the projected AABB. The
|
|
337
|
+
bottom-right tap reads `vec2(n_max.y, n_min.y)` — `n_max.y` where `n_max.x` was meant:
|
|
338
|
+
```glsl
|
|
339
|
+
float n1 = textureLod(utDepth, vec2(n_min.x, n_max.y), float(mip_to_use)).x;
|
|
340
|
+
float n2 = textureLod(utDepth, vec2(n_max.y, n_min.y), float(mip_to_use)).x; // n_max.x
|
|
341
|
+
```
|
|
342
|
+
- **Failure scenario**: A wide, short object — a wall whose projected box spans x∈[0.1, 0.9],
|
|
343
|
+
y∈[0.40, 0.45]. `n2` samples at x = 0.45 instead of x = 0.9, so the depth in front of the wall's
|
|
344
|
+
right-hand half is never read. `n = max(n0..n3)` therefore under-reports the scene depth over that
|
|
345
|
+
region and `depth_delta = n - n_min.z` goes negative: the wall is classified occluded and culled
|
|
346
|
+
while plainly on screen. The error is silent, view-dependent, and worse the wider the object.
|
|
347
|
+
- **Fix**: `float n2 = textureLod(utDepth, vec2(n_max.x, n_min.y), float(mip_to_use)).x;`
|
|
348
|
+
- **Confidence**: Confirmed
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
### GFXR-16 `HierarchicalZBuffer.build_mips` publishes the mip chain on a property three.js never reads
|
|
353
|
+
- **Severity**: MINOR
|
|
354
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/render/visibility/hiz/buffer/HierarchicalZBuffer.js:168-169`
|
|
355
|
+
- **Defect**: `this.__input_buffer.mips = mips.map(m => m.texture);` — `THREE.Texture` in r136 has no
|
|
356
|
+
`mips` field; the array `WebGLTextures` consults when `generateMipmaps === false` is `mipmaps`.
|
|
357
|
+
The assignment creates an inert own-property, so the reduction targets are never bound as mip levels
|
|
358
|
+
of the texture that `BatchOcclusionQuery.execute` hands to the shader as `utDepth`.
|
|
359
|
+
- **Failure scenario**: The reduction pass fills every mip render target correctly, but `utDepth` still
|
|
360
|
+
has only level 0 (`minFilter: NearestFilter`, no uploaded chain), so every
|
|
361
|
+
`textureLod(utDepth, uv, float(mip_to_use))` clamps to level 0. A distant object whose AABB covers
|
|
362
|
+
one texel at `mip_to_use = 6` is tested with four full-resolution point samples instead of the
|
|
363
|
+
conservative max over its footprint: the hierarchical test degenerates into a 4-sample point test
|
|
364
|
+
and reports visible objects as occluded — geometry pops in and out at distance.
|
|
365
|
+
- **Fix**: Bind the reduction targets' textures as explicit `sampler2D` uniforms and index them in the
|
|
366
|
+
shader, or build a real `mipmaps` chain on `__input_buffer` that `WebGLTextures` will upload.
|
|
367
|
+
Renaming the property alone is not sufficient — `mipmaps` entries are image-like objects, not render
|
|
368
|
+
targets.
|
|
369
|
+
- **Confidence**: Likely (only in-repo consumer today is `hiz/prototypeHiZ.js`, so this is latent)
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
### GFXR-17 `SGMesh.copy()` does not copy `flags`, so `clone()` re-enables shadow casting
|
|
374
|
+
- **Severity**: MINOR
|
|
375
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/mesh-v2/aggregate/SGMesh.js:82-88`
|
|
376
|
+
- **Defect**: `copy(other)` transfers `__url`, `__node`, `__initial_bounds` and `#material_override`
|
|
377
|
+
but not `flags` — the sole backing store for `castShadow`/`receiveShadow`/`Loaded`. `equals()`
|
|
378
|
+
(line 68) nonetheless compares `this.flags === other.flags`.
|
|
379
|
+
- **Failure scenario**: A prefab `SGMesh` with `castShadow = false` (a flat ground decal mesh, say).
|
|
380
|
+
`prefab.clone()` yields a component with `DEFAULT_FLAGS = CastShadow | ReceiveShadow`, so every
|
|
381
|
+
cloned instance casts a shadow the original explicitly disabled. Additionally
|
|
382
|
+
`prefab.equals(prefab.clone())` is `false`, defeating any equality/hash-keyed reuse of the component.
|
|
383
|
+
- **Fix**: Add `this.flags = other.flags & ~SGMeshFlags.Loaded;` to `copy()` (`Loaded` must be cleared
|
|
384
|
+
because `__node` is reset to `null`).
|
|
385
|
+
- **Confidence**: Confirmed
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
### GFXR-18 `SGMeshAnimationControllerSystem` has no `unlink`, so destroyed entities stay queued
|
|
390
|
+
- **Severity**: MINOR
|
|
391
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshAnimationControllerSystem.js:57-68` (no `unlink` anywhere in the class)
|
|
392
|
+
- **Defect**: `link` enqueues the entity when its mesh has not loaded. The class defines no `unlink`,
|
|
393
|
+
so nothing drains `#queue` when the entity is destroyed. `#process` then does
|
|
394
|
+
`ecd.getComponent(entity, SGMesh)` — which returns `undefined` for a removed entity
|
|
395
|
+
(`EntityComponentDataset` deletes the slot) — and dereferences `mesh.node`.
|
|
396
|
+
- **Failure scenario**: Spawn an entity with `SGMesh.fromURL(...)` + `SGMeshAnimationController`; the
|
|
397
|
+
asset is not yet resolved so `link` queues it. Destroy the entity the same frame (unit dies, preview
|
|
398
|
+
cancelled). Next tick, `update` pops it and `#process` throws
|
|
399
|
+
`TypeError: Cannot read properties of undefined (reading 'node')`. Because `update` calls the async
|
|
400
|
+
`#process` without awaiting, this surfaces as an unhandled promise rejection rather than a thrown
|
|
401
|
+
frame error — silent in production, and the diagnostics for the real problem are lost.
|
|
402
|
+
- **Fix**: Add `unlink(anim, mesh, entity)` that removes `entity` from `#queue`.
|
|
403
|
+
- **Confidence**: Confirmed
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
### GFXR-19 Ambient light with `castShadow` builds a shadow view, then crashes the frame
|
|
408
|
+
- **Severity**: MAJOR
|
|
409
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/light/binding/three/ThreeLightBinding.js:476-491`
|
|
410
|
+
- **Defect**: `__apply_castShadow` calls `__shadowmap_link()` whenever `castShadow` is true, with no check
|
|
411
|
+
that the underlying three light actually owns a `shadow`. `THREE.AmbientLight` does not — only
|
|
412
|
+
Directional/Spot/Point define one. Every *other* shadow site in the file guards for exactly this
|
|
413
|
+
(`applySettings:525` `if (shadow !== undefined)`, `__shadowmap_unlink:316`), and
|
|
414
|
+
`LightSystem.__visit_entity_to_update_shadow:210` excludes AMBIENT with the comment "only non-ambient
|
|
415
|
+
lights can cast shadow" — so this is a state the code already treats as reachable, not a can't-happen.
|
|
416
|
+
- **Failure scenario**: An entity carries `Light{type: AMBIENT, castShadow: true}` —
|
|
417
|
+
`Light.fromJSON` and `LightSerializationAdapter.serialize` both persist `castShadow` unconditionally
|
|
418
|
+
for every light type, so an editor toggle or a saved blueprint produces it. `link()` registers a
|
|
419
|
+
shadow `CameraView` with `graphics.views`. On the next `constructVisibleScene`,
|
|
420
|
+
`__shadowmap_prepare_view` does `const shadow = light.shadow;` (undefined) then
|
|
421
|
+
`const camera = shadow.camera;` → `TypeError`. That throw propagates out of
|
|
422
|
+
`CameraView.build_visibility` → `CameraViewManager.build_visibility` →
|
|
423
|
+
`GraphicsEngine.constructVisibleScene`, so the whole frame dies — every frame, permanently.
|
|
424
|
+
- **Fix**: `if (v && this.__shadow_map === null && light.shadow !== undefined) { … }`
|
|
425
|
+
- **Confidence**: Confirmed (crash mechanism verified against three r136; reachability requires the
|
|
426
|
+
ambient+castShadow authoring combination)
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
### GFXR-20 Shadow cameras have `matrixAutoUpdate = false`, so `LightShadow.updateMatrices` cannot refresh their pose
|
|
431
|
+
- **Severity**: MAJOR
|
|
432
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/light/binding/three/ThreeLightBinding.js:549`
|
|
433
|
+
- **Defect**: `applySettings` sets `shadow.camera.matrixAutoUpdate = false` for directional/spot shadows.
|
|
434
|
+
three r136's `LightShadow.updateMatrices` writes `shadowCamera.position` / calls `lookAt(...)` and then
|
|
435
|
+
relies on `shadowCamera.updateMatrixWorld()` to recompose — but `Object3D.updateMatrixWorld` only calls
|
|
436
|
+
`updateMatrix()` when `matrixAutoUpdate` is true, and `matrixWorldNeedsUpdate` is false at that point,
|
|
437
|
+
so neither `matrix` nor `matrixWorld`/`matrixWorldInverse` is updated. (`lookAt` itself calls
|
|
438
|
+
`updateWorldMatrix(true,false)`, which is equally inert, so even the look rotation is derived from the
|
|
439
|
+
stale world position.) The point-light branch two lines above documents precisely this requirement and
|
|
440
|
+
keeps `matrixAutoUpdate = true`; the directional/spot branch does the opposite. `LightSystem.js:325`
|
|
441
|
+
even has a comment asserting the opposite of what happens: *"refresh shadow camera world matrix from
|
|
442
|
+
the light's pose before we read matrixWorldInverse"* — `shadow.updateMatrices(light, 0)` there is a
|
|
443
|
+
no-op with respect to `matrixWorldInverse`.
|
|
444
|
+
- **Failure scenario**: The only place that ever bakes the pose is `__shadowmap_prepare_view`
|
|
445
|
+
(`camera.updateMatrix(); camera.updateMatrixWorld();`), fired from `preVisibilityBuild` — i.e. earlier
|
|
446
|
+
in the frame than `three_update_shadow_camera_extents`, which runs on
|
|
447
|
+
`visibilityConstructionEnded`. (a) First frame a light starts casting: `prepare_view` bakes the
|
|
448
|
+
constructor pose (origin, identity), so `compute_view_frustum_aabb_in_space` fits the ortho box in
|
|
449
|
+
**world** space instead of light space — a visibly wrong/missing shadow flash on level load.
|
|
450
|
+
(b) Steady state: a spot light carried by a moving character always renders and fits from the pose
|
|
451
|
+
captured on the previous frame, so its shadow trails the light while strafing.
|
|
452
|
+
- **Fix**: Leave `matrixAutoUpdate` at three's default `true` for all shadow cameras (three's own
|
|
453
|
+
`WebGLShadowMap` never disables it), or explicitly call `shadow.camera.updateMatrix();
|
|
454
|
+
shadow.camera.updateMatrixWorld();` after every `shadow.updateMatrices(light, i)`.
|
|
455
|
+
- **Confidence**: Confirmed (mechanism verified against `three/src/lights/LightShadow.js` and
|
|
456
|
+
`Object3D.updateMatrixWorld` in r136)
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
### GFXR-21 `three_update_shadow_camera_extents` rebuilds the main camera's projection matrix, discarding every projection modifier
|
|
461
|
+
- **Severity**: MAJOR
|
|
462
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/light/LightSystem.js:334-345`
|
|
463
|
+
- **Defect**: To fit the shadow box, the function truncates `camera.far`, calls
|
|
464
|
+
`camera.updateProjectionMatrix()`, reads the matrix, then restores `far` and calls
|
|
465
|
+
`updateProjectionMatrix()` again. The in-code claim that "callers downstream see no change" is wrong:
|
|
466
|
+
`updateProjectionMatrix()` regenerates the matrix from `fov/aspect/near/far`, restoring the *default*
|
|
467
|
+
projection — not whatever the matrix previously contained. `CameraView.set_from_camera:123` writes
|
|
468
|
+
modified matrices straight back into `camera.projectionMatrix.elements`, and it runs *earlier* in the
|
|
469
|
+
same frame (`GraphicsEngine.constructVisibleScene`: `set_from_camera` at line 707,
|
|
470
|
+
`visibilityConstructionEnded` — where this function is hooked — at line 710).
|
|
471
|
+
- **Failure scenario**: `TemporalSupersamplingRenderPlugin` is enabled and one shadow-casting
|
|
472
|
+
directional light has a finite `maxShadowDistance` below `camera.far` (e.g. 60 vs 1000), so
|
|
473
|
+
`needs_truncation` is true. Every frame the jitter written into `elements[8]/[9]` is wiped before the
|
|
474
|
+
scene is drawn, so TAA resamples the identical position forever: the image never converges and edge
|
|
475
|
+
aliasing is exactly as if TAA were off, while the plugin still pays its full cost. Setting
|
|
476
|
+
`maxShadowDistance = Infinity` makes TAA start working — a baffling coupling. Any other registered
|
|
477
|
+
`CameraView.projection_modifier` is destroyed the same way.
|
|
478
|
+
- **Fix**: Do not mutate the live camera. Build the truncated projection into a scratch `Matrix4` with
|
|
479
|
+
`makePerspective(...)` and multiply that by `camera.matrixWorldInverse`; failing that, save and restore
|
|
480
|
+
the 16 elements of `projectionMatrix` (and `projectionMatrixInverse`) verbatim.
|
|
481
|
+
- **Confidence**: Confirmed (mechanism and frame ordering both verified)
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
### GFXR-22 Binary light serializer drops POINT `distance` and DIRECTION `maxShadowDistance`
|
|
486
|
+
- **Severity**: MAJOR
|
|
487
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/light/LightSerializationAdapter.js:22-26` and `:41-45`
|
|
488
|
+
- **Defect**: `distance` is written and read only inside `if (type === LightType.SPOT)`, but the
|
|
489
|
+
component uses `distance` for POINT as well (`Light.js:51`, `LightBinding.scaled_distance`,
|
|
490
|
+
`ThreeLightBinding.__apply_distance`, `FPLightBinding.#updateDistance`). The JSON adapter gets it right
|
|
491
|
+
— `Light.toJSON` writes `distance` for `SPOT || POINT` (verified) — which pins this as an oversight
|
|
492
|
+
rather than a schema decision. `maxShadowDistance` (written by `toJSON` for DIRECTION) is absent from
|
|
493
|
+
the binary format entirely.
|
|
494
|
+
- **Failure scenario**: A torch entity `Light{type: POINT, distance: 20}` is saved through the binary ECS
|
|
495
|
+
serializer. On load, `deserialize` never reads `distance`, so the component keeps
|
|
496
|
+
`DEFAULT_DISTANCE = 1`; `__apply_distance` sets the three `PointLight.distance = 1` (or the Forward+
|
|
497
|
+
`radius = 1`). The torch illuminates a 1-unit sphere and the room around it is black — while the exact
|
|
498
|
+
same scene loaded from a JSON save looks correct.
|
|
499
|
+
- **Fix**: Write/read `distance` in a `SPOT || POINT` branch mirroring `Light.toJSON`, add
|
|
500
|
+
`maxShadowDistance` for DIRECTION, and bump `version` from 0 since the byte layout changes.
|
|
501
|
+
- **Confidence**: Confirmed
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
### GFXR-23 `LightSystem.shutdown` never detaches the three light cache
|
|
506
|
+
- **Severity**: MAJOR
|
|
507
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/light/LightSystem.js:97` (attach, in the constructor) vs `:162-171` (shutdown)
|
|
508
|
+
- **Defect**: The constructor does `this.__three_light_cache.attach(this.scene)`. `shutdown()` releases
|
|
509
|
+
the Forward+ plugin and detaches the `visibilityConstructionEnded` handler, but never calls the
|
|
510
|
+
`ThreeLightCache.detach()` that exists for exactly this purpose. Every three light the cache ever
|
|
511
|
+
created — including pooled, currently-unused ones — stays parented to `graphics.scene`, which outlives
|
|
512
|
+
the `EntityManager`.
|
|
513
|
+
- **Failure scenario**: The host app keeps one `GraphicsEngine` across level loads and rebuilds the
|
|
514
|
+
`EntityManager` (a new `LightSystem` each time). After each cycle the previous cycle's
|
|
515
|
+
`DirectionalLight`/`PointLight`/`SpotLight`/`AmbientLight` objects remain in the scene.
|
|
516
|
+
`WebGLRenderer.projectObject` pushes every visible `isLight` object into `WebGLLights` regardless of
|
|
517
|
+
intensity, so `NUM_DIR_LIGHTS`/`NUM_POINT_LIGHTS`/`NUM_SPOT_LIGHTS` grow one generation per load: every
|
|
518
|
+
material's program cache key changes so **all** materials recompile on each load (multi-second hitch),
|
|
519
|
+
and after a handful of cycles the fragment uniform budget is exceeded and materials fail to link —
|
|
520
|
+
black scene.
|
|
521
|
+
- **Fix**: Call `this.__three_light_cache.detach()` in `shutdown()`, and move the `attach(this.scene)`
|
|
522
|
+
from the constructor into `startup()` so a stopped-then-restarted system still works.
|
|
523
|
+
- **Confidence**: Confirmed (leak); the recompile/black-scene escalation depends on the host recycling
|
|
524
|
+
the `EntityManager`
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
### GFXR-24 Shadow texel snapping uses a grid whose spacing changes every frame
|
|
529
|
+
- **Severity**: MINOR
|
|
530
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/ecs/light/shadow/setShadowCameraDimensionsDiscrete.js:28-36`
|
|
531
|
+
- **Defect**: `rX = dX / shadow_map_width` where `dX`/`dY` are the light-space extents of the *current*
|
|
532
|
+
frame's view-frustum AABB, recomputed every frame by `three_update_shadow_camera_extents`. Snapping a
|
|
533
|
+
moving origin to a grid whose spacing also moves does not produce a stable world-space texel lattice —
|
|
534
|
+
the step the algorithm depends on ("Fix the overall size of the frustum based on the longest diagonal
|
|
535
|
+
… Don't allow the shadow frustum to change size as the camera rotates", `LightSystem.js:201`) is
|
|
536
|
+
documented but never implemented. Secondary: for negative `x0`, JS `%` returns a negative remainder, so
|
|
537
|
+
`x0_d = x0 - (x0 % rX) - rX/2` can land up to half a texel *inside* `x0`, clipping coverage off the
|
|
538
|
+
left/bottom edge (the `(w+1)/w` slack is added only at the far edge).
|
|
539
|
+
- **Failure scenario**: Slowly orbit the camera over a static scene lit by one shadow-casting sun. The
|
|
540
|
+
shadow map re-quantises onto a different lattice each frame, so shadow boundaries crawl and shimmer
|
|
541
|
+
texel by texel — exactly the artifact the discretisation exists to remove. With the camera positioned
|
|
542
|
+
so the fit box straddles the origin, up to half a texel of shadow is also missing along one edge.
|
|
543
|
+
- **Fix**: Derive a frame-invariant extent from the view frustum's longest diagonal at the shadow
|
|
544
|
+
distance and use it for `dX`/`dY`, then snap with `Math.floor(x / rX) * rX` instead of `%`.
|
|
545
|
+
- **Confidence**: Likely
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
### GFXR-25 Forward+ vertex-only lighting reads `gl_Position` before `<project_vertex>` writes it
|
|
550
|
+
- **Severity**: MAJOR
|
|
551
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/render/forward_plus/materials/fp_build_vertex_lighting_shared.js:9-13, 26`
|
|
552
|
+
- **Defect**: The froxel-lookup preamble is injected immediately after `void main() {`:
|
|
553
|
+
```js
|
|
554
|
+
result = insert_after(result, 'void main() {', accumulation_pars);
|
|
555
|
+
```
|
|
556
|
+
and `accumulation_pars` begins with
|
|
557
|
+
`vec3 fp_normalized_screen_position = ((gl_Position.xyz / gl_Position.w)+vec3(1.0))*0.5;`.
|
|
558
|
+
In a three.js vertex shader `gl_Position` is assigned much later, by `#include <project_vertex>`.
|
|
559
|
+
This is an unconditional read of a vertex-stage built-in output before it is written.
|
|
560
|
+
- **Failure scenario**: A lit material compiled with `defines.LIGHTING_VERTEX_ONLY === true` (the branch
|
|
561
|
+
`MaterialTransformer.js:63` selects) computes `clip_v` from an undefined value, so
|
|
562
|
+
`v3_cluster_position` indexes a random froxel. Out-of-range `texelFetch` returns 0 →
|
|
563
|
+
`fp_cluster_metadata.y == 0` → the mesh receives **zero** clustered light and renders unlit; in-range,
|
|
564
|
+
it is lit by an unrelated part of the screen and flickers per vertex as the garbage changes.
|
|
565
|
+
- **Fix**: Inject after `#include <project_vertex>` instead, or compute the clip position locally
|
|
566
|
+
(`projectionMatrix * modelViewMatrix * vec4(position, 1.0)`) rather than reading `gl_Position`.
|
|
567
|
+
- **Confidence**: Confirmed (the read-before-write is unconditional; no in-repo material currently sets
|
|
568
|
+
`LIGHTING_VERTEX_ONLY`, so this is public-surface breakage rather than a live regression)
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
### GFXR-26 `LightManager.dispose()` frees GPU objects but leaves every light still subscribed to the dead manager
|
|
573
|
+
- **Severity**: MAJOR
|
|
574
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/render/forward_plus/LightManager.js:1137-1142`
|
|
575
|
+
- **Defect**: `dispose()` disposes four textures and nothing else. Three subscription families survive:
|
|
576
|
+
`LightRenderMetadata.link` (`LightRenderMetadata.js:52`) binds `this.update` to
|
|
577
|
+
`light.position.onChanged` / `light.radius.onChanged` (removed only by `removeLight` → `unlink()`);
|
|
578
|
+
`__handle_visible_light_added` (`:596-597`) binds `__handle_light_dimensions_change` /
|
|
579
|
+
`__handle_light_appearance_change`; `__handle_visible_decal_added` (`:538-546`) binds to
|
|
580
|
+
`patch_uv.position.onChanged` / `patch_uv.size.onChanged` and stores an un-released
|
|
581
|
+
`Reference<AtlasPatch>`. `__decal_atlas` is never disposed either.
|
|
582
|
+
- **Failure scenario**: `ForwardPlusRenderingPlugin.shutdown()` disposes the manager while a scene with
|
|
583
|
+
FP point lights and decals is still loaded (plugin released before entities unlink, or an engine
|
|
584
|
+
reconfiguration). Every live `PointLight.position` signal still holds a handler bound to the dead
|
|
585
|
+
manager, keeping it, its BVH, its cluster-plane buffers and all atlas patches reachable — a full leak
|
|
586
|
+
per plugin cycle. Worse, moving any of those lights afterwards still runs
|
|
587
|
+
`LightRenderMetadata.update()`, writing bounds into the abandoned BVH; re-acquiring the plugin creates
|
|
588
|
+
a second `LightManager` while the first keeps mutating.
|
|
589
|
+
- **Fix**: In `dispose()`, iterate `#metadata_map` calling `data.unlink()` and clear it; drain
|
|
590
|
+
`__visible_lights`/`__visible_decals` through the existing `__handle_visible_*_removed` teardown so the
|
|
591
|
+
handlers and atlas references are released; dispose `__decal_atlas`.
|
|
592
|
+
- **Confidence**: Likely
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
### GFXR-27 Forward+ `SpotLight` is accepted by `addLight` and then silently never rendered
|
|
597
|
+
- **Severity**: MAJOR
|
|
598
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/render/forward_plus/LightManager.js:771-796`
|
|
599
|
+
- **Defect**: The visibility dispatch branches on `light.isDecal` then `light.isPointLight` and drops
|
|
600
|
+
everything else on the floor. `render/forward_plus/model/SpotLight.js` implements everything `addLight`
|
|
601
|
+
requires (`getAABB` via `cone_compute_bounding_box`, `onDimensionChanged`, `toArray`), so
|
|
602
|
+
`addLight(spotLight)` succeeds and inserts a BVH leaf — it is simply never binned.
|
|
603
|
+
`encode_light_type` would have thrown on it and `spot_light_inside_volume` exists, but neither is
|
|
604
|
+
reached.
|
|
605
|
+
- **Failure scenario**: A consumer builds a `SpotLight` from `forward_plus/model/SpotLight.js` and adds
|
|
606
|
+
it to the manager expecting a cone of light. Nothing appears and nothing is logged — the light silently
|
|
607
|
+
contributes zero for the lifetime of the scene. Companion trap: `SpotLight.prototype.ENCODED_SLOT_COUNT`
|
|
608
|
+
is never assigned and `toArray` returns 13 (not a multiple of 4), so naively adding the branch makes
|
|
609
|
+
`address += undefined` produce `NaN` and trips the "Expected light data size" throw at `:878`.
|
|
610
|
+
- **Fix**: Either reject unsupported light types loudly in `addLight`, or complete the spot path —
|
|
611
|
+
`SpotLight.prototype.ENCODED_SLOT_COUNT = 16` (pad `toArray` to a 4-slot boundary), a type code in
|
|
612
|
+
`encode_light_type`, an `isSpotLight` branch here using `spot_light_inside_volume`, and the matching
|
|
613
|
+
branch in `query_bvh_frustum_from_texture`.
|
|
614
|
+
- **Confidence**: Likely
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
### GFXR-28 `decal_filtering_enabled` setter never re-uploads, and leaves a mipmap-incomplete texture
|
|
619
|
+
- **Severity**: MINOR
|
|
620
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/render/forward_plus/LightManager.js:359-367` (constructor at `:221-223`)
|
|
621
|
+
- **Defect**: The setter flips `t.generateMipmaps` and stops — `texture.version` is never bumped, so
|
|
622
|
+
three does not re-upload and the setting has no effect until an unrelated atlas repack. When that
|
|
623
|
+
repack eventually happens, `generateMipmaps === false` while `minFilter` is still
|
|
624
|
+
`LinearMipMapLinearFilter`; on WebGL2 three's `setTextureParameters` applies `LINEAR_MIPMAP_LINEAR`
|
|
625
|
+
verbatim and `uploadTexture` skips `generateMipmap`.
|
|
626
|
+
- **Failure scenario**: A game follows the method's own JSDoc ("Please set this to false if you have a
|
|
627
|
+
lot of overlapping decals") and sets `lightManager.decal_filtering_enabled = false`. Nothing changes on
|
|
628
|
+
screen. Later a new decal texture is registered, the atlas repacks and re-uploads — and every decal now
|
|
629
|
+
samples a mipmap-incomplete texture, rendering solid black instead of its diffuse.
|
|
630
|
+
- **Fix**: `t.generateMipmaps = v; t.minFilter = v ? LinearMipMapLinearFilter : LinearFilter;
|
|
631
|
+
t.needsUpdate = true;`
|
|
632
|
+
- **Confidence**: Confirmed (missing re-upload); Likely (black-decal consequence)
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
### GFXR-29 Debug froxel grid uses a 3-channel stride on the RGBA cluster texture and overruns each axis
|
|
637
|
+
- **Severity**: MINOR
|
|
638
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/render/forward_plus/debug/createScreenGrid.js:47-51` and `:272-281`
|
|
639
|
+
- **Defect**: `const texel_address = texel_index * 3; // RGB format` — the cluster texture is RGBA:
|
|
640
|
+
`LightManager.__assign_lights_to_clusters` writes at `tile_index * 4` (`LightManager.js:970`) and
|
|
641
|
+
`LightManager.spec.js:23` asserts a 4-element image for a 1×1×1 grid. Stride 3 aliases into the wrong
|
|
642
|
+
texel for every cell but index 0. The loops additionally run `i < resolution.N + 1`, so
|
|
643
|
+
`resolution.x - 1 - i_x` reaches `-1` and the z loop reads a slice past the end.
|
|
644
|
+
- **Failure scenario**: Open the debug screen grid on a 12×6×4 grid. Cell (3,2,1)'s reported light count
|
|
645
|
+
is read from a completely different froxel, so both the heatmap and the click-tooltip light list
|
|
646
|
+
describe the wrong cells — the tool actively misleads anyone debugging the binning.
|
|
647
|
+
- **Fix**: Use `texel_index * 4` / `index * 4` in both places and drop the `+ 1` from all three loop
|
|
648
|
+
bounds.
|
|
649
|
+
- **Confidence**: Confirmed
|
|
650
|
+
|
|
651
|
+
---
|
|
652
|
+
|
|
653
|
+
### GFXR-30 Cluster-volume debug widget clones the 3D cluster texture every frame and never disposes it
|
|
654
|
+
- **Severity**: MINOR
|
|
655
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/render/forward_plus/debug/buildLightClusterWidget.js:56-64`
|
|
656
|
+
- **Defect**: `const t = lm.getTextureClusters().clone(); t.needsUpdate = true; m.uniforms.map.value = t;`
|
|
657
|
+
— a fresh `DataTexture3D` per `update()`, with the previous clone dropped without `.dispose()`. Its GL
|
|
658
|
+
texture object stays in `WebGLTextures`' property map for the widget's renderer until context loss.
|
|
659
|
+
- **Failure scenario**: `update()` is driven from the render loop. At 60 fps with the default
|
|
660
|
+
32×16×16 RGBA16UI volume (~64 KB each) that is ~4 MB/s of leaked GPU memory; a couple of minutes of
|
|
661
|
+
debugging and the tab hits out-of-memory / context-lost.
|
|
662
|
+
- **Fix**: Clone once outside `update()` (the clone shares `image`, so only `needsUpdate = true` is
|
|
663
|
+
needed per frame), or `m.uniforms.map.value?.dispose()` before assigning the new clone.
|
|
664
|
+
- **Confidence**: Confirmed
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
### GFXR-31 `sort_decal_data` uses a moving pivot and sorts on slots that hold colour, not priority
|
|
669
|
+
- **Severity**: MINOR
|
|
670
|
+
- **File**: `H:/git/moh/app/src/mir-engine/meep/src/engine/graphics/render/forward_plus/sort_decal_data.js:20-24, 65-76`
|
|
671
|
+
- **Defect**: Two independent errors. (a) The comparator reads `data[address + 20]` as `draw_priority`
|
|
672
|
+
and `+ 21` as `id`, but `Decal.toArray` (`model/Decal.js:116-121`) has both of those writes commented
|
|
673
|
+
out and `array_copy(this.color, 0, destination, address + 20, 4)` occupies slots 20-23 — so it sorts
|
|
674
|
+
decals by their red and green channels. (b) The quicksort pivot is an *index*, not a captured value;
|
|
675
|
+
`lookup[pivotIndex]` is itself swapped during partitioning, destroying the scan sentinel. The correct
|
|
676
|
+
sibling `core/collection/array/array_sort_quick.js` captures the pivot value before the loop.
|
|
677
|
+
- **Failure scenario**: Re-enable the call site the `TODO fix decal sorting artifacts` block at
|
|
678
|
+
`LightManager.js:995-1003` currently guards (or call this exported function directly). When the pivot
|
|
679
|
+
entry is swapped to a higher-priority value, `i` walks past `right`, `compare_decals` starts reading
|
|
680
|
+
`undefined`, and the subsequent `lookup[j] = lookup[i]` stores `0` into a neighbouring froxel's lookup
|
|
681
|
+
run — that froxel's decal slot then points at light-data texel 0 and renders the wrong decal. This is
|
|
682
|
+
precisely the artifact the TODO describes.
|
|
683
|
+
- **Fix**: Capture the pivot record (`const pivot_encoded = lookup[pivotIndex];`) and compare values, not
|
|
684
|
+
indices; and restore the `draw_priority`/`id` writes in `Decal.toArray` (widening
|
|
685
|
+
`ENCODED_SLOT_COUNT` past 24) or read priority from a slot that is actually written.
|
|
686
|
+
- **Confidence**: Likely
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## Verified clean / non-issues checked along the way
|
|
691
|
+
|
|
692
|
+
- `GraphicsEngine.prepareRenderPass` writes `visibleGroup.length = j` where `children.length` was
|
|
693
|
+
meant — harmless: `clearVisibleGroup()` truncates `children` to 0 first, so `children.length`
|
|
694
|
+
already equals `j`. No failure scenario, not reported.
|
|
695
|
+
- `OutlineRenderer.writeParameters` → `HashMap.forEach` passes `(value, key)`, which matches
|
|
696
|
+
`__write_parameters_for_definition(id, definition)`. Correct.
|
|
697
|
+
- `OutlineRenderPlugin` subscribe/unsubscribe pairs match (`Vector2.process` adds to `onChanged` with
|
|
698
|
+
the same handler + context that `shutdown` removes).
|
|
699
|
+
- `AmbientOcclusionPostProcessEffect` startup/shutdown binding pairs all match; all four GPU resources
|
|
700
|
+
are disposed.
|
|
701
|
+
- TAA jitter indices (`elements[8]`/`[9]`) and magnitude match the pinned spec; the `=` (rather than
|
|
702
|
+
`+=`) is deliberate, since `CameraView.set_from_camera` writes the jittered matrix back onto the
|
|
703
|
+
camera and `CameraSystem`'s `visibilityConstructionStarted` hook rebuilds it each frame.
|
|
704
|
+
- `Water.updateBounds` BVH box agrees with the plane mesh's rotated/offset world extent.
|
|
705
|
+
- `FrameBufferManager.computeRenderOrder` splice-while-iterating is correctly compensated
|
|
706
|
+
(`i--; openSetCount--`), as is `WaterSystem.processUpdateQueue`.
|
|
707
|
+
- `GizmoRenderingPlugin` overrides `RenderLayer.buildVisibleSet` (whose prototype version throws
|
|
708
|
+
`'deprecated'`) — but `VisibilityComputer.__processLayer` still calls it, so gizmos do render.
|
|
709
|
+
- `RenderLayer.js:59`'s comment claims near is `planes[4]` and far is `planes[5]` (they are the other
|
|
710
|
+
way round), but it documents `computeNearFarClippingPlanes`, which unconditionally throws
|
|
711
|
+
`'deprecated'`. Stale doc only.
|
|
712
|
+
- Forward+ cluster index arithmetic, depth-slice distribution, plane orientation, the near/far swap and
|
|
713
|
+
X-flip in `computeFrustumCorners`, texel packing and the light-count overflow/recursion path are all
|
|
714
|
+
internally consistent and agree with `cluster_binning.spec.js` /
|
|
715
|
+
`light_culling_no_false_exclusion.spec.js`.
|
|
716
|
+
- `query_bvh_frustum_from_texture.js:132` masks the light address with `& 0xFFFFFFF8` where the encoding
|
|
717
|
+
(`type | address << 2`) calls for `& 0xFFFFFFFC`. Currently harmless — every record length in use
|
|
718
|
+
(`PointLight` 8, `Decal` 24) is a multiple of 8 — so no failure scenario exists today. Noted, not filed.
|
|
719
|
+
- `LightBinding` / `FPLightBinding` / `LightContext` link-unlink subscription pairs are symmetric;
|
|
720
|
+
`ThreeLightCache.release` keys off `light2type(threeObject)` rather than the already-changed component
|
|
721
|
+
type, so type-change rebuilds return the light to the correct pool.
|
|
722
|
+
- `__shadowmap_unlink` disposes both `shadow.map` and `shadow.mapPass`;
|
|
723
|
+
`three_set_shadow_resolution` disposes before resizing; `compute_view_frustum_aabb_in_space`'s plane
|
|
724
|
+
ordering and `near = -z1 / far = -z0` are correct.
|
|
725
|
+
- `ShadedGeometrySystem.link/unlink`, `SGMeshSystem.process`, `SGMeshHighlightSystem` and
|
|
726
|
+
`IncrementalDeltaSet` were checked in detail and are balanced/correct.
|
|
727
|
+
- `FPDecalSystem.Context` correctly handles the async texture-load race (`__is_linked` and
|
|
728
|
+
`this.__fp_decal === fpDecal` re-checks) — the only defect there is the known `subscribe`/`unsubscribe`
|
|
729
|
+
typo.
|
|
730
|
+
- `SpriteSystemPE`, `AnimationControllerSystem`, `RenderableHighlightSystem`,
|
|
731
|
+
`ShadedGeometryHighlightSystem`, `Trail3DSystem` and `GizmoRenderingPlugin` have matching
|
|
732
|
+
subscribe/unsubscribe pairs.
|
|
733
|
+
|
|
734
|
+
## Coverage note
|
|
735
|
+
|
|
736
|
+
Not exhaustively reviewed for lack of time: `ecs/path/tube/build/*` and `ecs/path/ribbon/`
|
|
737
|
+
(heavily spec-covered geometry builders), `ecs/animation/animator/*` (graph/blend-space state machine,
|
|
738
|
+
little graphics-correctness surface), and the GLSL bodies of `shaders/TerrainShader.js` /
|
|
739
|
+
`shaders/AmbientOcclusionShader.js` / `render/buffer/simple-fx/ao/SAOShader.js` (the JS-side uniform
|
|
740
|
+
wiring for those *was* reviewed and is clean).
|