@woosh/meep-engine 3.60.4 → 3.60.5
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/CHANGELOG.md +2602 -0
- package/CONTRIBUTING.md +7 -0
- package/MIGRATION_2_TO_3.md +136 -0
- package/SECURITY.md +4 -0
- package/editor/EditorCameraNavigation.d.ts +2 -1
- package/editor/EditorCameraNavigation.d.ts.map +1 -1
- package/editor/EditorCameraNavigation.js +51 -57
- package/package.json +6 -2
- package/src/engine/EngineHarness.d.ts +3 -9
- package/src/engine/EngineHarness.d.ts.map +1 -1
- package/src/engine/EngineHarness.js +6 -8
- package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
- package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
- package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
- package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
- package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
- package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
- package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
- package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
- package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
- package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
- package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
- package/src/engine/graphics/ecs/water/Water.js +8 -0
- package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
- package/src/engine/graphics3/WaterSystem.js +731 -734
- package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
- package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
- package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
- package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
- package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
- package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
- package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
- package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
- package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
- package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
- package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
- package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
- package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
- package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
- package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
- package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
- package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
- package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
- package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
- package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
- package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
- package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
- package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
- package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
- package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
- package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
- package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/shade/playground/water_refraction/README.md +5 -6
- package/src/shade/playground/water_refraction/index.html +1 -1
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +7 -5
- package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
- package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
- package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
- package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
- package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
- package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
- package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
- package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +2 -2
- package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +2 -2
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +2 -1
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +18 -1
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
- package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +5 -5
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +10 -10
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
- package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
- package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
- package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
- package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
- package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
- package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
- package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
- package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
- package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
- package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
- package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
- package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
- package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
- package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/material/GPUMaterialManager.d.ts +7 -0
- package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
- package/src/shade/renderer/material/GPUMaterialManager.js +11 -0
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
- package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
- package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
- package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
- package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
- package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
- package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
- package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
- package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
- package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
- package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
- package/src/shade/renderer/water/shader_water.js +22 -13
- package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
- package/src/shade/renderer/water/shader_water_avboit.js +7 -7
- package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
- package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
- package/editor/REWORK_PLAN_2026_08.md +0 -692
- package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
- package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
- package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
- package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
- package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
- package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
- package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
- package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
- package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
- package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
- package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
- package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
- package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
- package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
- package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
- package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
- package/src/REVIEW_2026_08_06/adv-core.md +0 -511
- package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
- package/src/REVIEW_2026_08_06/adv-view.md +0 -446
- package/src/REVIEW_2026_08_06/api-core.md +0 -296
- package/src/REVIEW_2026_08_06/api-engine.md +0 -195
- package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
- package/src/REVIEW_2026_08_06/core-binary.md +0 -348
- package/src/REVIEW_2026_08_06/core-collection.md +0 -376
- package/src/REVIEW_2026_08_06/core-math.md +0 -221
- package/src/REVIEW_2026_08_06/core-process.md +0 -266
- package/src/REVIEW_2026_08_06/core-science.md +0 -223
- package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
- package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
- package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
- package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
- package/src/REVIEW_2026_08_06/engine-input.md +0 -197
- package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
- package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
- package/src/REVIEW_2026_08_06/generation.md +0 -176
- package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
- package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
- package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
- package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
- package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
- package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
- package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
- package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
- package/src/REVIEW_2026_08_06/view.md +0 -667
- package/src/REVIEW_2026_08_06.md +0 -610
- package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
- package/src/core/bvh2/bvh3/NOTES.md +0 -2
- package/src/core/bvh8/NOTES.md +0 -63
- package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
- package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
- package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
- package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
- package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
- package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
- package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
- package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
- package/src/core/geom/3d/gjk/NOTES.md +0 -8
- package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
- package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
- package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
- package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
- package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
- package/src/engine/REVIEW_2026_07_03.md +0 -211
- package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
- package/src/engine/control/first-person/DESIGN.md +0 -658
- package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
- package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
- package/src/engine/control/first-person/TODO.md +0 -160
- package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
- package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
- package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
- package/src/engine/ecs/ik/PLAN.md +0 -179
- package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
- package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
- package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
- package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
- package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
- package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
- package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
- package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
- package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
- package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
- package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
- package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
- package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
- package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
- package/src/engine/network/CONGESTION_CONTROL.md +0 -87
- package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
- package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
- package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
- package/src/engine/physics/cloth/PLAN.md +0 -3056
- package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
- package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
- package/src/engine/physics/fluid/solver/NOTES.md +0 -46
- package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
- package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
- package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
- package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
- package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
- package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
- package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
- package/src/format/image/avif/DECISIONS.md +0 -545
- package/src/format/image/jpeg/NOTES.md +0 -3
- package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
- package/src/generation/COORDINATES.md +0 -54
- package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
- package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
- package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
- package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
- package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
- package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
- package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
- package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
- package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
- package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
- package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
- package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
- package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
- package/src/shade/RENDERER_CONTRACT.md +0 -697
- package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
- package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
- package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
- package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
- package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
- package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
- package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
- package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
- package/src/shade/renderer/NOTES.md +0 -78
- package/src/shade/renderer/PLAN.md +0 -8
- package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
- package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
- package/src/shade/renderer/atmosphere/NOTES.md +0 -6
- package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
- package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
- package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
- package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
- package/src/shade/renderer/camera/orbital/README.md +0 -81
- package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
- package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/index.js +0 -9
- package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
- package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
- package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
- package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
- package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
- package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
- package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
- package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
- package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
- package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
- package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
- package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
- package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
- package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
- package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
- package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
- package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
- package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
- package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
- package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
- package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
- package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
- package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
- package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
- package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
- package/src/shade/renderer/hiz/NOTES.md +0 -3
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
- package/src/shade/renderer/light/cluster/NOTES.md +0 -11
- package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
- package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
- package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
- package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
- package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
- package/src/shade/renderer/particles/DESIGN.md +0 -1011
- package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
- package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
- package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
- package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
- package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
- package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
- package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
- package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
- package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
- package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
- package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
- package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
- package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
- package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
- package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
- package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
- package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
- package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
- package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
- package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
- package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
- package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
- package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
- package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
- package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
- package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
- package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
- package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
- package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
- package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
- package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
- package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
- package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
- package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
- package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
- package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
- package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
- package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
- package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
- package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
- package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
- package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
- package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
- package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
- package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
- package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
- package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
- package/src/shade/wgsl/NOTES.md +0 -4
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
# W-TinyLFU benchmarks
|
|
2
|
-
|
|
3
|
-
Benchmarks comparing `CacheWTinylfu` against the LRU `Cache` sibling. They split into **two axes**
|
|
4
|
-
that answer different questions:
|
|
5
|
-
|
|
6
|
-
| Axis | Question | Answered by |
|
|
7
|
-
|------|----------|-------------|
|
|
8
|
-
| **Effectiveness** (hit rate) | What fraction of accesses hit? Determined by the **policy**. | `hitRate.spec.js` |
|
|
9
|
-
| **Performance** (throughput / latency / space) | What does each operation **cost**? Determined by the **implementation**. | everything else |
|
|
10
|
-
|
|
11
|
-
You need both to judge the trade: the policy benchmark tells you what W-TinyLFU *saves you in misses*;
|
|
12
|
-
the performance benchmarks tell you what it *costs per op and in RAM*. The exchange rate between them is
|
|
13
|
-
the cost of a miss in your system (a disk/network miss dwarfs a few hundred ns; a cheap recompute does not).
|
|
14
|
-
|
|
15
|
-
> ⚠️ **About the numbers below.** They're from one dev machine (Node v20.14.0, Windows 11) and microbenchmarks
|
|
16
|
-
> are noisy — absolute nanoseconds wander run-to-run by tens of percent. **Trust the ratios and the shape, not
|
|
17
|
-
> the exact ns.** The ratios are measured *interleaved* (LRU and W-TinyLFU timed back-to-back each round, then
|
|
18
|
-
> the per-round ratios medianed), so they stay stable even as the absolutes drift. Re-run on your own hardware
|
|
19
|
-
> for absolutes.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## Running
|
|
24
|
-
|
|
25
|
-
Each benchmark is a self-contained `<name>.spec.js` with a skipped test entry and an explicit manual Node
|
|
26
|
-
entry. The measurement **only ever runs under plain `node`** — never in-process under
|
|
27
|
-
Jest, because Jest's babel transform badly distorts the timings (see *Why not measure under Jest* below).
|
|
28
|
-
The files are stripped from the published npm package by the `*.spec.js` rule.
|
|
29
|
-
|
|
30
|
-
**To get numbers — run it directly with `node`, from `packages/meep-engine`:**
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
node src/core/cache/wtinylfu/benchmark/<name>.spec.js --benchmark
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
where `<name>` is one of: `hitRate`, `fillDrain`, `readAll`, `continuousPut`, `mixed`, `latency`, `scaling`,
|
|
37
|
-
`memory`.
|
|
38
|
-
|
|
39
|
-
The files remain `describe.skip` under the test runner. Vitest's `--mode all` includes them in collection;
|
|
40
|
-
un-skipping one makes its test spawn a native Node child and check that the report contains finite
|
|
41
|
-
measurements. Importing a file or invoking it without `--benchmark` performs no workload setup.
|
|
42
|
-
|
|
43
|
-
Options for the manual `node` invocation:
|
|
44
|
-
|
|
45
|
-
- **`--expose-gc`** (a Node flag, goes *before* the script path): lets a benchmark force GC between trials for
|
|
46
|
-
cleaner numbers. Optional for most; **required** for `memory` (it refuses to run without it). *(When run via
|
|
47
|
-
Jest, the child is always spawned with `--expose-gc`.)*
|
|
48
|
-
```bash
|
|
49
|
-
node --expose-gc src/core/cache/wtinylfu/benchmark/fillDrain.spec.js --benchmark
|
|
50
|
-
```
|
|
51
|
-
- **`HASH=int`** (an env var): the **isolated-hash toggle** — use integer keys + an identity hash instead of
|
|
52
|
-
string keys + `computeStringHash`, to strip string-hashing cost and see pure-policy overhead. Works on
|
|
53
|
-
**every** benchmark.
|
|
54
|
-
```bash
|
|
55
|
-
HASH=int node --expose-gc src/core/cache/wtinylfu/benchmark/readAll.spec.js --benchmark
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
All workloads are **seeded**, so runs are reproducible. Both caches are always built at **matched capacity**
|
|
59
|
-
(N weight-1 entries) so the comparison is fair.
|
|
60
|
-
|
|
61
|
-
### Layout & file structure
|
|
62
|
-
|
|
63
|
-
Each benchmark lives in its own self-contained file in this `benchmark/` folder as `<name>.spec.js`.
|
|
64
|
-
There is **no shared harness** — each file inlines the scaffolding it needs, so it stands alone (copy it,
|
|
65
|
-
run it, read it without chasing imports). Every file follows the same skeleton:
|
|
66
|
-
|
|
67
|
-
```
|
|
68
|
-
1. HEADER what it measures · how to run · how to read it
|
|
69
|
-
2. CONFIG the knobs: CAPACITY / WARMUP / TRIALS / SEED
|
|
70
|
-
3. SCAFFOLDING inlined: clock, rng, matched-capacity factories, measureInterleaved, any workload
|
|
71
|
-
(top-level — pure definitions, safe when Jest imports the file)
|
|
72
|
-
if (under tests) → skipped native Node child + finite report checks
|
|
73
|
-
if (manual entry with --benchmark) → runBenchmark():
|
|
74
|
-
4. WORKLOAD the setup + the timed body (the "arrange")
|
|
75
|
-
5. MEASURE measureInterleaved([LRU, W-TinyLFU], {...}) (the "act")
|
|
76
|
-
6. REPORT formatting + console.log (the "assert" analog — reports, no pass/fail)
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
The `if (typeof describe === "function")` split keeps measurements in the native child. Workload setup is
|
|
80
|
-
inside `runBenchmark()`, called only by the explicit main-file `--benchmark` entry.
|
|
81
|
-
|
|
82
|
-
A benchmark **reports** numbers; it does not **assert** pass/fail. That's why the measurement always runs
|
|
83
|
-
under plain `node` — the Jest side only launches it and checks the output isn't empty.
|
|
84
|
-
|
|
85
|
-
### Why not measure under Jest
|
|
86
|
-
|
|
87
|
-
Jest runs every file through **babel-jest** before V8 sees it (transpiled to CommonJS, wrapped in Jest's
|
|
88
|
-
module registry). That transformed code JITs differently from the native ES module `node` runs — and the
|
|
89
|
-
overhead is **not uniform**: a cheap op (LRU) takes a bigger proportional hit than a heavier one (W-TinyLFU).
|
|
90
|
-
Measured in-process under Jest, absolutes inflated ~2–12× **and several ratios inverted** — `fill` read
|
|
91
|
-
1.76x → 0.91x, `mixed` 1.48x → 0.90x, `latency p50` 3.0x → 0.86x — i.e. Jest would tell you W-TinyLFU is
|
|
92
|
-
*faster* than LRU on inserts, which is false. It was also ~4× slower wall-clock. So the benchmark body never
|
|
93
|
-
runs under Jest; the spec spawns a clean `node` child and only checks the output is well-formed.
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## Effectiveness — `hitRate.spec.js`
|
|
98
|
-
|
|
99
|
-
Replays seeded access traces as a read-through cache and reports **hit ratio** (capacity N = 500). This is
|
|
100
|
-
where W-TinyLFU earns its keep.
|
|
101
|
-
|
|
102
|
-
```
|
|
103
|
-
LRU W-TinyLFU Δ
|
|
104
|
-
uniform random (no skew) 10.0% 10.0% 0.0 ← control: no harm
|
|
105
|
-
zipfian skew 0.7 / 0.9 / 1.1 22.7/45.4/70.7 ~equal ±1 ← realistic skew: roughly even
|
|
106
|
-
scan (hot set + one-shot scans) 31.6% 47.0% +15.4 ← scan resistance: the big win
|
|
107
|
-
cyclic loop (size > capacity) LRU pathological case win ← incumbent-favoring ties
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**Takeaway:** roughly even with LRU on plain skew, *no worse* on uniform, and a large win on **scan-heavy**
|
|
111
|
-
and **cyclic** workloads (where LRU is catastrophic). That scan resistance is the reason to pay the costs below.
|
|
112
|
-
|
|
113
|
-
---
|
|
114
|
-
|
|
115
|
-
## Performance
|
|
116
|
-
|
|
117
|
-
Realistic (string-key) mode, `--expose-gc`, interleaved, median of 100 trials. **Capacities:** the read / fill /
|
|
118
|
-
space and `continuousPut` benchmarks use **N = 65,536**; `mixed` uses a quarter of that (**N = 16,384**,
|
|
119
|
-
keyspace 10×N) so its keyspace can dwarf the cache for realistic misses. All are powers of two — safe under
|
|
120
|
-
churn because the shared HashMap grows headroom at its load factor.
|
|
121
|
-
|
|
122
|
-
### `fillDrain` — insert + teardown
|
|
123
|
-
|
|
124
|
-
```
|
|
125
|
-
fill (put) | LRU ~197 ns/op | W-TinyLFU ~378 ns/op | ~2.1x
|
|
126
|
-
drain (clear) | LRU ~21 ms/call | W-TinyLFU ~11 ms/call | ~0.52x
|
|
127
|
-
drain (remove) | LRU ~259 ns/op | W-TinyLFU ~317 ns/op | ~1.2x
|
|
128
|
-
```
|
|
129
|
-
`fill` is the policy's insert cost (sketch + region routing). The `drain` rows reflect each class's *teardown
|
|
130
|
-
implementation*, not the policy — LRU's `clear()` removes element-by-element while W-TinyLFU bulk-resets, so the
|
|
131
|
-
drain difference is an implementation artifact.
|
|
132
|
-
|
|
133
|
-
### `readAll` — read path (cold vs warm)
|
|
134
|
-
|
|
135
|
-
```
|
|
136
|
-
read cold | LRU ~370 ns/op | W-TinyLFU ~710 ns/op | ~1.9x
|
|
137
|
-
read warm | LRU ~290 ns/op | W-TinyLFU ~490 ns/op | ~1.7x
|
|
138
|
-
```
|
|
139
|
-
Every get is a hit. `cold` = first read-pass (W-TinyLFU's promotion activity); `warm` = settled steady state.
|
|
140
|
-
The premium is modest and one-time, not a recurring spike.
|
|
141
|
-
|
|
142
|
-
### `continuousPut` — sustained load (churn vs update)
|
|
143
|
-
|
|
144
|
-
```
|
|
145
|
-
put churn | LRU ~660 ns/op | W-TinyLFU ~656 ns/op | ~1.0x (new keys → evict + admission duel)
|
|
146
|
-
put update | LRU ~220 ns/op | W-TinyLFU ~447 ns/op | ~2.0x (re-put existing keys → in-place)
|
|
147
|
-
```
|
|
148
|
-
**The key reframe.** W-TinyLFU's extra policy work is roughly **additive (~150–200 ns/op)**, not a multiplier —
|
|
149
|
-
on the cheap `update` baseline it shows as ~2.1×. On `churn` it doesn't show at all: **element pooling** makes
|
|
150
|
-
W-TinyLFU's inserts allocation-free, while LRU still allocates an element per insert, so that saving *offsets*
|
|
151
|
-
the policy overhead and W-TinyLFU draws level with LRU (~1.0×).
|
|
152
|
-
|
|
153
|
-
### `mixed` — realistic blended throughput
|
|
154
|
-
|
|
155
|
-
```
|
|
156
|
-
mixed get/put | LRU ~157 ns/op | W-TinyLFU ~224 ns/op | ~1.4x
|
|
157
|
-
```
|
|
158
|
-
Zipfian read-through (get; on miss, put) over a keyspace 10× the cache — the most representative single number.
|
|
159
|
-
|
|
160
|
-
### `latency` — the tail (percentiles)
|
|
161
|
-
|
|
162
|
-
```
|
|
163
|
-
p50 p99 p999 max
|
|
164
|
-
LRU 200 ~700 ~2400 multi-ms (GC)
|
|
165
|
-
W-TinyLFU 300 ~1300 ~7400 multi-ms (GC)
|
|
166
|
-
```
|
|
167
|
-
Per-op timing during a fill. W-TinyLFU's whole distribution shifts up ~2× — its heavier insert, paid broadly
|
|
168
|
-
rather than in a rare spike. The **max is a multi-ms GC pause on both**, not a policy event — read the policy
|
|
169
|
-
tail in p99/p999, not max.
|
|
170
|
-
|
|
171
|
-
### `memory` — bytes per entry (space) · requires `--expose-gc`
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
fresh fill after churn (steady state)
|
|
175
|
-
LRU | ~144 (heap ~136 + ext ~8) | ~224 (heap ~208 + ext ~16) bytes/entry
|
|
176
|
-
W-TinyLFU | ~164 (heap ~152 + ext ~12) | ~245 (heap ~225 + ext ~20) bytes/entry
|
|
177
|
-
→ fresh, W-TinyLFU vs LRU: +~20 B/entry (+~16 heap = per-element `region` + `keyHash`; +~4 sketch)
|
|
178
|
-
→ churn cost (both caches): +~80 B/entry (the shared HashMap doubling its slab at the load factor)
|
|
179
|
-
```
|
|
180
|
-
~14% more RAM per entry for the **policy** (fresh) — the **W-vs-LRU difference of ~4 B ≈ the sketch**. The
|
|
181
|
-
**churn cost** is the same +~80 for both because it's the *shared HashMap*, not the policy: once the cache is
|
|
182
|
-
full and evicting, the entries slab (and its bins) hits the 0.75 load factor and **doubles** to keep amortised
|
|
183
|
-
O(1) inserts, so a churning cache carries ~2× the entry headroom of a fresh fill.
|
|
184
|
-
|
|
185
|
-
### `scaling` — O(1) check + locality across N
|
|
186
|
-
|
|
187
|
-
```
|
|
188
|
-
N LRU W-TinyLFU ratio
|
|
189
|
-
2,048 62.3 175.6 2.88x
|
|
190
|
-
16,384 102.8 230.0 2.25x
|
|
191
|
-
131,072 379.2 556.8 1.47x
|
|
192
|
-
1,048,576 667.7 911.8 1.37x (warm random read, ns/op)
|
|
193
|
-
```
|
|
194
|
-
Per-op cost rises only ~11× from 2k→1M (not ~500×) → **O(1) confirmed**; the rise is **CPU-cache locality**.
|
|
195
|
-
The ratio **compresses as N grows** — both caches increasingly bottlenecked on RAM latency at large N.
|
|
196
|
-
|
|
197
|
-
### `HASH=int` — pure-policy overhead (cross-cutting)
|
|
198
|
-
|
|
199
|
-
Run any benchmark with `HASH=int` to strip string hashing. Example (read warm):
|
|
200
|
-
|
|
201
|
-
```
|
|
202
|
-
realistic isolated (HASH=int)
|
|
203
|
-
LRU ~290 ns/op ~151 ns/op
|
|
204
|
-
W-TinyLFU ~490 ns/op ~338 ns/op
|
|
205
|
-
ratio ~1.7x ~2.3x
|
|
206
|
-
```
|
|
207
|
-
Reveals that **string hashing is a large *shared* cost** (~140 ns of LRU's warm read was just `Map` string
|
|
208
|
-
hashing, paid by both caches). Removing it shrinks the baseline, so the *same* fixed policy overhead becomes a
|
|
209
|
-
bigger multiple.
|
|
210
|
-
|
|
211
|
-
---
|
|
212
|
-
|
|
213
|
-
## Headline findings
|
|
214
|
-
|
|
215
|
-
1. **Cost is a roughly fixed additive overhead, not a multiplier** — ~100–300 ns/op of extra work (sketch
|
|
216
|
-
increment + region bookkeeping), so the "Nx" you see depends mostly on how cheap the underlying op already was.
|
|
217
|
-
2. **O(1) and predictable** — confirmed across 3 orders of magnitude; the only growth is shared CPU-cache locality.
|
|
218
|
-
3. **No tail surprises** — the overhead is spread across the whole distribution (median through p999), not a rare
|
|
219
|
-
spike; the worst-case `max` is a GC pause both caches share.
|
|
220
|
-
4. **~14% more memory per entry** — the per-element `region` + `keyHash` fields plus the O(N) frequency sketch
|
|
221
|
-
(plus a shared ~+80 B/entry once any cache is full and churning — the HashMap doubling its slab, not policy-specific).
|
|
222
|
-
5. **Bought against scan resistance** — the effectiveness axis shows large hit-rate wins on scan/cyclic workloads,
|
|
223
|
-
while staying roughly even with LRU on plain skew and doing no harm on uniform.
|
|
224
|
-
|
|
225
|
-
**The trade in one line:** ~150–200 ns/op and ~14% RAM, predictable and O(1), in exchange for scan-resistant,
|
|
226
|
-
frequency-aware retention — worth it exactly when your misses are expensive and your traffic isn't purely uniform.
|
|
227
|
-
|
|
228
|
-
## Methodology notes (why the numbers are trustworthy)
|
|
229
|
-
|
|
230
|
-
- **Pre-generated keys** outside the timed region (else you measure string allocation, not the cache).
|
|
231
|
-
- **Warmup** discarded so the JIT has compiled the hot path (we measure optimized machine code, not the cold
|
|
232
|
-
interpreter).
|
|
233
|
-
- **Interleaved trials** — every round times LRU and W-TinyLFU back-to-back, and the ratio is the **median of
|
|
234
|
-
the per-round ratios** (paired). So machine drift within a round hits both equally and cancels in the ratio.
|
|
235
|
-
- **Median of trials** (not mean) to shrug off the occasional GC pause.
|
|
236
|
-
- **Selective `forceGc`** — forced for low-allocation bodies (reads, teardown) where a stray pause is pure noise;
|
|
237
|
-
*not* for allocation-heavy bodies (fill, churn) where GC is part of the real cost.
|
|
238
|
-
- **Result sinks** so pure-read loops aren't deleted by dead-code elimination.
|
|
239
|
-
- **Matched capacity & shared seeded workloads** so LRU and W-TinyLFU face identical conditions.
|
|
240
|
-
- **Measured under `node`, never in-process under a test runner** — a transform (babel/esbuild) distorts both
|
|
241
|
-
the absolute ns *and* the ratio (a cheap op suffers proportionally more overhead). Measured under Jest
|
|
242
|
-
in-process, several ratios *inverted* (e.g. `fill` read 1.76x → 0.91x). That's why the self-executing spec
|
|
243
|
-
spawns a clean `node` child instead of measuring inside Jest — see *Why not measure under Jest*.
|
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
# COLOR_FIX_PLAN — `src/core/color`
|
|
2
|
-
|
|
3
|
-
Date: 2026-08-28 · Companion to [COLOR_REVIEW_2026_08_28.md](COLOR_REVIEW_2026_08_28.md) ·
|
|
4
|
-
Findings are referenced by their review IDs (`C1`…`C16`, `§n`).
|
|
5
|
-
|
|
6
|
-
Each step is independently shippable and ordered so that nothing later depends on a decision not yet
|
|
7
|
-
made. Every step names the files it touches and the test that proves it.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Scope corrections applied
|
|
12
|
-
|
|
13
|
-
Four corrections to the review were supplied and are binding on this plan:
|
|
14
|
-
|
|
15
|
-
| # | Correction | Effect on the plan |
|
|
16
|
-
|---|---|---|
|
|
17
|
-
| **R1** | Don't overload the `Color` class | No new methods on `Color`. All new behaviour ships as free functions (`color_*`). §4.2's "missing members" become **Phase 6 files**, not class methods. Existing methods stay where they are — this is an additive rule, not a migration mandate. |
|
|
18
|
-
| **R2** | Colour-space tag refused; `Color` is a low-level class | The review's §3.4 recommendation (a) and (b) are both **dropped**. `Color` stays four floats plus alpha with no space metadata. The space contract moves to *function* names and docs instead — **Phase 3**. |
|
|
19
|
-
| **R3** | Unused/unimported code is not suspect; meep is a library with downstream consumers | **No deletions anywhere.** Review §1.4, §5.5, §5.6 and §5.8 are struck as *deletion* items. Their real consequence inverts: unimported code is public API that no in-repo caller exercises, so defects in it are unguarded and its **missing specs move up in priority** (Phase 7). |
|
|
20
|
-
| **R4** | `prototype*.js` are playgrounds, stripped at ship | `kelvin/prototype_kelvin_to_rgb.js` is **out of scope**. Its `document.body` side effect and `engine/`/`view/` imports are expected for that file class. |
|
|
21
|
-
|
|
22
|
-
### Explicitly not doing
|
|
23
|
-
|
|
24
|
-
- Deleting `int2rgb.js`, `sRGB_cmf.js`, `xyz_cmf_wyman.js`, `D65_spd_analytical.js`,
|
|
25
|
-
`planckian_radiance.js`, `find_gamut_intersection.js`, `xyz_to_hpe.js`, `MAGMA_LUT.js`, or any of
|
|
26
|
-
`operations/`. (R3)
|
|
27
|
-
- Touching `prototype_kelvin_to_rgb.js`. (R4)
|
|
28
|
-
- Adding a colour-space field, enum, or brand to `Color`. (R2)
|
|
29
|
-
- Adding `add`/`sub`/`lerp`/`clamp`/`fromHex` &c. as `Color` methods. (R1)
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## Conventions this plan introduces
|
|
34
|
-
|
|
35
|
-
Two placement rules, so Phase 6 doesn't turn into a debate per file:
|
|
36
|
-
|
|
37
|
-
- **`operations/`** — free functions whose *input* is a `Color`. Existing precedent:
|
|
38
|
-
`color_lerp`, `color_darken`, `color_scale_okhsv_channel`.
|
|
39
|
-
- **`construct/`** — new directory; free functions that *produce* a `Color` from non-`Color` input
|
|
40
|
-
(`color_from_hex`, `color_from_uint24`, …). Keeps `operations/` meaning "transform", and keeps
|
|
41
|
-
factories out of the class per R1.
|
|
42
|
-
|
|
43
|
-
Naming follows the newer Layer-A style throughout: `snake_case`, `color_*` prefix, verb after the
|
|
44
|
-
noun (`color_from_hex`, not `hex_to_color`, so the whole family sorts together).
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
## Phase 1 — Major correctness
|
|
49
|
-
|
|
50
|
-
> Every step here: **write the failing spec first**, then fix. All four are currently invisible to
|
|
51
|
-
> the suite (33 files / 158 tests, all green).
|
|
52
|
-
|
|
53
|
-
### Step 1 — Stop dropping alpha in the two `Color` sRGB statics · `C5` · major
|
|
54
|
-
|
|
55
|
-
**Files:** [Color.js](Color.js) (`from_linear_to_sRGB` ~L788, `from_sRGB_to_linear` ~L801)
|
|
56
|
-
|
|
57
|
-
**Do:**
|
|
58
|
-
1. Convert into a local scratch triple, then write the result through `output.set(r, g, b, input.a)`.
|
|
59
|
-
This fixes both halves of C5 at once: alpha is carried, and the write goes through `set()` so
|
|
60
|
-
`onChanged` fires and the no-change short-circuit applies.
|
|
61
|
-
2. Do **not** keep the positional `output[0] = …` path — that is the mechanism of the silent
|
|
62
|
-
mutation.
|
|
63
|
-
|
|
64
|
-
**Test:** `Color.spec.js` — (a) alpha survives a round trip through both statics; (b) passing an
|
|
65
|
-
existing `Color` as `output` fires `onChanged` exactly once with the correct old/new payload.
|
|
66
|
-
|
|
67
|
-
**Why first:** `Color.onChanged` has live consumers — `engine/graphics/render/forward_plus/model/PointLight.js`,
|
|
68
|
-
`SpotLight.js`, and `view/elements/ColorPickerView.js` all subscribe. Today a colour converted in
|
|
69
|
-
place updates without notifying any of them.
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
|
|
73
|
-
### Step 2 — Stop dropping alpha in the four OkHSV operations · `C6` · major
|
|
74
|
-
|
|
75
|
-
**Files:** [operations/color_scale_okhsv_channel.js](operations/color_scale_okhsv_channel.js)
|
|
76
|
-
|
|
77
|
-
**Do:** add `result.a = input.a;` before the return. One line fixes `color_lighten`,
|
|
78
|
-
`color_darken`, `color_saturate` and `color_desaturate` together, and brings them in line with
|
|
79
|
-
`color_lerp`, which already interpolates alpha.
|
|
80
|
-
|
|
81
|
-
**Test:** new `operations/color_scale_okhsv_channel.spec.js` asserting alpha passthrough; extend
|
|
82
|
-
`color_darken.spec.js` with an alpha case.
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
### Step 3 — Make `hex2rgb` reject or expand short-form hex · `C3` · major
|
|
87
|
-
|
|
88
|
-
**Files:** [hex/hex2rgb.js](hex/hex2rgb.js)
|
|
89
|
-
|
|
90
|
-
**Do:**
|
|
91
|
-
1. Branch on `hex.length`: accept `#RGB` (4) and `#RGBA` (5) by nibble expansion (`f` → `ff`),
|
|
92
|
-
plus the existing `#RRGGBB` (7) and `#RRGGBBAA` (9).
|
|
93
|
-
2. Anything else → `throw new Error()` naming the offending string. Do not rely on the caller's
|
|
94
|
-
`assert.notNaN` — that guard is downstream, mislabels the cause, and disappears if asserts are
|
|
95
|
-
stripped from a build.
|
|
96
|
-
|
|
97
|
-
**Test:** extend `hex/hex2rgb.spec.js` — `#f00` → `{255,0,0,255}`, `#f00a` → alpha `0xAA`,
|
|
98
|
-
`#ff00` (4 digits *without* `#`-relative alpha intent), `#gg0000`, and `''` all throw.
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
### Step 4 — Mask and validate `parse_color`'s numeric branch · `C4` · major
|
|
103
|
-
|
|
104
|
-
**Files:** [parse_color.js](parse_color.js) (~L66-70)
|
|
105
|
-
|
|
106
|
-
**Do:**
|
|
107
|
-
1. Mask all three channels (`(c >> 16) & 0xFF`), not two of three.
|
|
108
|
-
2. Reject `c < 0 || c > 0xFFFFFF` with a thrown error. Decide and document explicitly whether
|
|
109
|
-
`0xAARRGGBB` is supported — today it is neither supported nor rejected, which is how
|
|
110
|
-
`0xFF8000FF` becomes `r: -0.502`.
|
|
111
|
-
3. `Color`'s HDR tolerance means no assert downstream will ever catch this, so the check has to be
|
|
112
|
-
here.
|
|
113
|
-
|
|
114
|
-
**Test:** `parse_color.spec.js` — `0xFF8000` decodes; `0xFF8000FF`, `-1`, `0x1000000` throw.
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
### Step 5 — Close the 2000 K seam in `kelvin_to_rgb` · `C1` · major
|
|
119
|
-
|
|
120
|
-
**Files:** [kelvin/kelvin_to_rgb.js](kelvin/kelvin_to_rgb.js) (`approximate_regime_red`, ~L23)
|
|
121
|
-
|
|
122
|
-
**Do:** the blue gate `t <= 20 ? 0 : clamp01(fit)` steps 0 → 0.0779 in one 0.01 K increment because
|
|
123
|
-
the fit does not reach zero at its own gate. Pick one:
|
|
124
|
-
- **(a) preferred** — shift/rescale the blue term so it is continuous at `t = 20`; or
|
|
125
|
-
- **(b)** blend across a short window (`t ∈ [20, 21]`) using `inverseLerp` + `lerp`, mirroring the
|
|
126
|
-
existing `BLEND_MIN`/`BLEND_MAX` treatment of the 6600 K seam.
|
|
127
|
-
|
|
128
|
-
(b) matches the file's established house style and is the lower-risk change; (a) is correct rather
|
|
129
|
-
than papered over. The 1000 K seam already does the right thing (hue locked to the red regime's
|
|
130
|
-
`t = 10` value, matching to 6 dp) and needs no change.
|
|
131
|
-
|
|
132
|
-
**Test:** new `kelvin_seam.spec.js` — sweep 500 K…40000 K in fine steps, assert a bounded
|
|
133
|
-
first difference per channel. This test shape catches all three seams and is the reason C1 survived
|
|
134
|
-
9 existing tests.
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
### Step 6 — Restore injectivity to `rgb_to_kelvin` below 2000 K · `C2` · major
|
|
139
|
-
|
|
140
|
-
**Files:** [kelvin/rgb_to_kelvin.js](kelvin/rgb_to_kelvin.js)
|
|
141
|
-
|
|
142
|
-
**Do:** depends on Step 5 — the collapse is downstream of the flat `b/r === 0` region. After Step 5,
|
|
143
|
-
re-measure. Then either:
|
|
144
|
-
- **(a) minimal** — keep the bisection, verify it now resolves below 2000 K, and correct the JSDoc
|
|
145
|
-
(it promises "approx 1000 to 40000" but returns `0` for black); or
|
|
146
|
-
- **(b) better** — replace with McCamy's approximation over CIE 1960 `uv`, or Robertson. No solver,
|
|
147
|
-
uses all three channels (the current version never reads green, so any colour with a matching
|
|
148
|
-
`b/r` maps to the same temperature regardless of distance from the Planckian locus), and has a
|
|
149
|
-
defined Duv validity domain.
|
|
150
|
-
|
|
151
|
-
**Test:** extend `rgb_to_kelvin.spec.js` — round-trip `kelvin_to_rgb → rgb_to_kelvin` at 800, 1200,
|
|
152
|
-
1500, 1800, 1999, 2100, 2700, 5000, 6500, 20000 K within tolerance.
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
## Phase 2 — Minor correctness
|
|
157
|
-
|
|
158
|
-
Small, independent, one commit each.
|
|
159
|
-
|
|
160
|
-
| Step | Finding | File | Do | Test |
|
|
161
|
-
|---|---|---|---|---|
|
|
162
|
-
| **7** | `C7` | [rgb2uint32.js](rgb2uint32.js) | Append `>>> 0` to the return. Behaviour-preserving: both existing consumers (`Color.spec.js`, `deserialize_scene.js`) already normalise. Removes a method whose name promises `uint32` and returns `-1` for white. | Drop the `>>> 0` from `Color.spec.js`'s assertions; they must still pass. |
|
|
163
|
-
| **8** | `C12` | [oklab/linear_srgb_to_okhsv.js](oklab/linear_srgb_to_okhsv.js) | Early-return `h = 0, s = 0, v = toe(L)` when `C < epsilon`, not only when `L === 0`. Today every neutral gets the arbitrary hue `0.2496` with `s ≈ 1e-7`. | Neutrals at several lightnesses return `s === 0`; `v` still round-trips. |
|
|
164
|
-
| **9** | `C12` | [operations/color_scale_okhsv_channel.js](operations/color_scale_okhsv_channel.js) | `clamp01` the three output channels. OkHSV round-trips overshoot by ~1e-7 (`v = 1.0000000270` for white), so the operations can return out-of-`[0,1]` colours. | White in → all channels exactly ≤ 1. |
|
|
165
|
-
| **10** | `C13` | [operations/color_lighten.js](operations/color_lighten.js) | Multiplicative `v * (1 + amount)` has a fixed point at 0, so black cannot be lightened. Switch to `v + amount * (1 - v)` (no fixed point at either end) or document the limitation. Mirror the decision in `color_saturate`. | `color_lighten(black, 1)` ≠ black. |
|
|
166
|
-
| **11** | `C8` | [Color.js](Color.js) `setHCL` ~L332, `setHSI` ~L393 | Decide per method: clamped-to-gamut, or unbounded-caller-clamps. Document the choice. Add a `default:` to both switches (unreachable today, one refactor from `undefined`). If `setHSI` cannot be validated against a reference, its own `NOTE: I'm not sure if the implementation is correct` should be resolved or the method marked `@deprecated` — **not deleted** (R3). | New `Color` spec cases for both, at the primaries and at neutral. |
|
|
167
|
-
| **12** | `C9` | [Color.js](Color.js) ~L702 | `multiplyScalar` and `multiply` scale alpha. Keep the behaviour, document it explicitly, and add `color_multiply_rgb` in Phase 6 for the exposure/tint case. | Doc-only; spec pins current behaviour so it can't drift. |
|
|
168
|
-
| **13** | `C10` | [Color.js](Color.js) ~L501 | `fromUint`: mask red with `& 0xFF`. Superseded in substance by Step 20 (single unpacker) but harmless and immediate. | `fromUint(toUint())` round-trip across the full byte range. |
|
|
169
|
-
| **14** | `C11` | [parse_color.js](parse_color.js) L6-8 | Anchor the three regexes (`^…$` after `trim()`), **or** document the substring-scan behaviour. Today `'background: rgb(1,2,3) !important'` parses, which contradicts the docstring. | Malformed-wrapper strings throw (or are documented as accepted). |
|
|
170
|
-
| **15** | `C15` | [Color.js](Color.js) ~L563 | `hash()` → `toUint32()` (after Step 7) so alpha participates. Currently colours differing only in alpha collide. | Two colours differing only in alpha hash differently. |
|
|
171
|
-
| **16** | `C16` | [illuminant/D65_spd_tabulated.js](illuminant/D65_spd_tabulated.js), [xyz/xyz_cmf_tabulated.js](xyz/xyz_cmf_tabulated.js) | Same problem, two shapes: `<=`/`>=` with an inner equality re-test vs `<`/`>` with a clamped `i1`; one asserts its input, the other doesn't. Converge on one. | Both return the tabulated endpoint exactly at both bounds and `0` outside. |
|
|
172
|
-
| **17** | `C14` | [rgb_to_luminance.js](rgb_to_luminance.js) | Doc-only. Replace *"linear or gamma-corrected"* with the GPU twin's wording: input must be linear for the result to be luminance; on encoded input this is luma (`Y′`). Cross-reference `chunk_rgb_to_luminance`. | — |
|
|
173
|
-
|
|
174
|
-
---
|
|
175
|
-
|
|
176
|
-
## Phase 3 — Space contract without a space tag
|
|
177
|
-
|
|
178
|
-
> Replaces review §3.4. R2 forbids putting the contract *in* `Color`; it goes into function names and
|
|
179
|
-
> docs instead, which is the right layer given `Color` is deliberately low-level.
|
|
180
|
-
|
|
181
|
-
### Step 18 — State the contract on `Color` and on every consumer
|
|
182
|
-
|
|
183
|
-
**Files:** [Color.js](Color.js) class JSDoc; `operations/*`; `rgb_to_luminance.js` (done in Step 17)
|
|
184
|
-
|
|
185
|
-
**Do:**
|
|
186
|
-
1. `Color` class docstring: state plainly that `Color` is a **space-agnostic** 4-float container —
|
|
187
|
-
it holds whatever the caller put in it, values may fall outside `[0,1]` for HDR, and *the
|
|
188
|
-
functions operating on it* declare which space they expect. Also correct the false
|
|
189
|
-
`@extends {Array.<number>}` (`Array.isArray(color) === false`) and document that the numeric
|
|
190
|
-
index setters (`set 0(v)`) bypass `set()` and therefore do not notify `onChanged`.
|
|
191
|
-
2. Every function in `operations/` gets an explicit `@param {Color} input **linear** sRGB` line.
|
|
192
|
-
They all call `linear_srgb_to_okhsv` today, so linear is the existing de facto contract —
|
|
193
|
-
write it down.
|
|
194
|
-
3. `Color.parse` / `toHex` / `toCssRGBAString` get the converse note: these produce/consume
|
|
195
|
-
**encoded** sRGB, so a `Color` built by `parse` must be converted before reaching `operations/`.
|
|
196
|
-
|
|
197
|
-
**Test:** doc-only, but add one spec to `color_darken.spec.js` that builds its input via
|
|
198
|
-
`Color.from_sRGB_to_linear(Color.parse('#808080'))` — the correct pipeline — so the intended usage is
|
|
199
|
-
executable and visible.
|
|
200
|
-
|
|
201
|
-
### Step 19 — Add encoded-input variants so callers aren't required to remember
|
|
202
|
-
|
|
203
|
-
**Files:** new `operations/color_srgb_scale_okhsv_channel.js` (or a `{ encoded: true }` option — pick one)
|
|
204
|
-
|
|
205
|
-
**Do:** the UI path (`ColorPickerView`, `ColorEditor`, anything built from `Color.parse`) holds
|
|
206
|
-
encoded values, and the operations assume linear. Provide the decode→operate→encode wrapper once,
|
|
207
|
-
here, rather than in every caller. This is the practical substitute for the refused space tag.
|
|
208
|
-
|
|
209
|
-
**Test:** encoded variant on `#808080` produces a visibly different (and correct) result from
|
|
210
|
-
feeding the same encoded values to the linear variant.
|
|
211
|
-
|
|
212
|
-
---
|
|
213
|
-
|
|
214
|
-
## Phase 4 — Deduplication (no deletions — R3)
|
|
215
|
-
|
|
216
|
-
Consolidate by **delegation**, keeping every existing export in place so downstream imports keep
|
|
217
|
-
working.
|
|
218
|
-
|
|
219
|
-
### Step 20 — One uint24 unpacker, three call sites · `§5.1`
|
|
220
|
-
|
|
221
|
-
**Files:** [int2rgb.js](int2rgb.js) (keep, becomes the implementation), [Color.js](Color.js)
|
|
222
|
-
`fromUint`, [parse_color.js](parse_color.js) numeric branch
|
|
223
|
-
|
|
224
|
-
**Do:** three copies exist with three masking behaviours. Make `int2rgb` the masked, validating
|
|
225
|
-
implementation and have `Color.fromUint` and `parse_color` call it. Subsumes Steps 4 and 13.
|
|
226
|
-
`int2rgb` keeps its export and signature.
|
|
227
|
-
|
|
228
|
-
**Test:** all three paths agree across the byte range and reject out-of-range identically.
|
|
229
|
-
|
|
230
|
-
### Step 21 — Reuse the existing hue-wrap helper · `§5.2`
|
|
231
|
-
|
|
232
|
-
**Files:** [Color.js](Color.js) `setHSL`/`setHCL`/`setHSI`, [hsv/hsv2rgb_float.js](hsv/hsv2rgb_float.js)
|
|
233
|
-
|
|
234
|
-
**Do:** the same wrap is inlined four times in two different orders (`% 1` then fix-up, vs fix-up
|
|
235
|
-
then `% 1`). `core/math/euclidean_modulo.js` **already exists** and is exactly this:
|
|
236
|
-
`euclidean_modulo(h, 1)`. Replace all four. No new helper needed.
|
|
237
|
-
|
|
238
|
-
**Test:** existing specs must stay green; add negative and >1 hue cases (`-3.5`, `2.25`) to each of
|
|
239
|
-
the four.
|
|
240
|
-
|
|
241
|
-
### Step 22 — One sextant dispatch · `§5.3`
|
|
242
|
-
|
|
243
|
-
**Files:** [Color.js](Color.js) `setHCL`/`setHSI`, [hsv/hsv2rgb_float.js](hsv/hsv2rgb_float.js),
|
|
244
|
-
new `hsv/hue_to_rgb_sextant.js`
|
|
245
|
-
|
|
246
|
-
**Do:** `setHCL` and `setHSI` contain byte-identical 30-line switches; `hsv2rgb_float` has a third
|
|
247
|
-
variant. Extract `hue_to_rgb_sextant(out, h_face, c, x)`. This is also what makes Step 11's clamp a
|
|
248
|
-
one-place change instead of three.
|
|
249
|
-
|
|
250
|
-
**Test:** existing HSV/HSL specs green; new direct spec for the helper at all six sextant boundaries.
|
|
251
|
-
|
|
252
|
-
### Step 23 — Cross-reference the two lerps · `§5.4`
|
|
253
|
-
|
|
254
|
-
**Files:** [Color.js](Color.js) `lerpColors`, [operations/color_lerp.js](operations/color_lerp.js)
|
|
255
|
-
|
|
256
|
-
**Do:** doc-only. `lerpColors` is per-channel linear; `color_lerp` is OkHSV shortest-arc. Neither
|
|
257
|
-
mentions the other, and the results differ substantially at mid-`t`. Add `@see` both ways and one
|
|
258
|
-
sentence on when to pick which.
|
|
259
|
-
|
|
260
|
-
### Step 24 — Disambiguate spec filenames and suite names · `§5.9`
|
|
261
|
-
|
|
262
|
-
**Files:** [xyz/XYZ.spec.js](xyz/XYZ.spec.js) → `rgb_xyz_roundtrip.spec.js`;
|
|
263
|
-
[oklab/XYZ.spec.js](oklab/XYZ.spec.js) → `oklab_xyz_roundtrip.spec.js`;
|
|
264
|
-
[sRGB/sRGB.spec.js](sRGB/sRGB.spec.js) → `srgb_transfer_roundtrip.spec.js`
|
|
265
|
-
|
|
266
|
-
**Do:** two files share the name `XYZ.spec.js` and three share the test name
|
|
267
|
-
`"to/from consistency"`, so a failure report is ambiguous. Rename files and give each test a
|
|
268
|
-
distinct name.
|
|
269
|
-
|
|
270
|
-
---
|
|
271
|
-
|
|
272
|
-
## Phase 5 — Cross-boundary consistency
|
|
273
|
-
|
|
274
|
-
### Step 25 — Reconcile the PQ/ST 2084 reference white · `§3.5` · **highest cross-cutting risk**
|
|
275
|
-
|
|
276
|
-
**Files:** [PQ/PQ_constants.js](PQ/PQ_constants.js) (203 nits, BT.2408) vs
|
|
277
|
-
`src/shade/renderer/shader/chunk/color/iCtCp/chunk_eotf_st2084.js` and
|
|
278
|
-
`chunk_inverse_eotf_st2084.js` (100 nits)
|
|
279
|
-
|
|
280
|
-
**Do:** the same engine holds two SDR-white conventions — content encoded on the CPU path and decoded
|
|
281
|
-
on the GPU path is off by 2.03×. Decide which is normative (BT.2408's 203 is the modern answer and is
|
|
282
|
-
what `avif/encoder_worker.js` already ships), then make the other match. `PQ_constants.js` explicitly
|
|
283
|
-
documents why folding a white point into the transfer function is dangerous; the GPU chunk folds a
|
|
284
|
-
*different* one in.
|
|
285
|
-
|
|
286
|
-
**Test:** a spec that runs the JS `linear_to_PQ`/`PQ_to_linear` pair against the WGSL chunks' expected
|
|
287
|
-
values at several nit levels — per the project's WGSL-validate / JS-emulator tiering.
|
|
288
|
-
|
|
289
|
-
### Step 26 — Single source of truth for shared constants · `§5.7`
|
|
290
|
-
|
|
291
|
-
**Files:** [rgb_to_luminance.js](rgb_to_luminance.js), [xyz/rgb_to_xyz.js](xyz/rgb_to_xyz.js),
|
|
292
|
-
the corresponding WGSL chunks
|
|
293
|
-
|
|
294
|
-
**Do:** the Rec.709 luma weights appear at 4, 15 and 20 significant digits in three places; the sRGB
|
|
295
|
-
OETF exponent and the XYZ matrix each have two spellings. Generate the shared matrices and
|
|
296
|
-
transfer-function coefficients into both JS and WGSL from one definition — the AVIF work already does
|
|
297
|
-
exactly this (`feat(avif): generate the normative AV1 constant tables`), so the mechanism exists.
|
|
298
|
-
This is what makes Step 25 stay fixed.
|
|
299
|
-
|
|
300
|
-
**Test:** generated output is committed; a spec asserts JS and WGSL constants agree bit-for-bit.
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
## Phase 6 — Missing members, as free functions (R1)
|
|
305
|
-
|
|
306
|
-
New files only. **No `Color` methods are added.** Order within the phase is by value; each is
|
|
307
|
-
independent.
|
|
308
|
-
|
|
309
|
-
### Step 27 — Gamut clipping · `§4.3` · highest-value missing feature
|
|
310
|
-
|
|
311
|
-
**Files:** new `oklab/gamut_clip_preserve_chroma.js`, `oklab/gamut_clip_project_to_lcusp.js`,
|
|
312
|
-
`oklab/gamut_clip_adaptive_l0.js`
|
|
313
|
-
|
|
314
|
-
**Do:** [find_gamut_intersection.js](oklab/find_gamut_intersection.js) is present and correct and is
|
|
315
|
-
the primitive these are built from — the hard part is already written, only the thin wrappers are
|
|
316
|
-
missing. Ships the package's only route to "make this out-of-gamut colour displayable".
|
|
317
|
-
|
|
318
|
-
**Test:** new `gamut_clip.spec.js` — clipped colours are in-gamut; in-gamut colours are unchanged;
|
|
319
|
-
hue is preserved by the preserve-chroma variant.
|
|
320
|
-
|
|
321
|
-
### Step 28 — Missing inverses · `§4.1`
|
|
322
|
-
|
|
323
|
-
| New file | Note |
|
|
324
|
-
|---|---|
|
|
325
|
-
| `lab/lab_to_xyz.js` | Lab is currently write-only, which defeats the main reason to enter it. |
|
|
326
|
-
| `hunt/hpe_to_xyz.js` | Pairs with the existing `xyz_to_hpe`. |
|
|
327
|
-
| `hex/rgba2hex.js` | `hex2rgb` already **reads** `#RRGGBBAA`; nothing can write it. |
|
|
328
|
-
| `oklab/oklab_to_oklch.js`, `oklch_to_oklab.js` | The space CSS Color 4 exposes and the natural one for hue rotation. |
|
|
329
|
-
| `oklab/okhsl_*.js` | Ottosson defines OkHSL beside OkHSV; `toe`/`toe_inv` are already here. Better basis for lighten/darken than OkHSV (Step 10). |
|
|
330
|
-
|
|
331
|
-
**Test:** absolute reference values, not only mutual round-trips — see Step 32.
|
|
332
|
-
|
|
333
|
-
### Step 29 — `Color` free functions replacing the "missing methods" · `§4.2` under R1
|
|
334
|
-
|
|
335
|
-
**Files:** new, in `operations/` (Color in) and `construct/` (Color out)
|
|
336
|
-
|
|
337
|
-
| File | Replaces the rejected method |
|
|
338
|
-
|---|---|
|
|
339
|
-
| `operations/color_add.js`, `color_sub.js`, `color_add_scaled.js` | `Color.prototype.add` &c. |
|
|
340
|
-
| `operations/color_multiply_rgb.js` | the alpha-preserving multiply (Step 12) |
|
|
341
|
-
| `operations/color_clamp.js`, `color_is_in_gamut.js` | pairs with Steps 9 & 11 |
|
|
342
|
-
| `construct/color_from_hex.js` | `Color.fromHex` |
|
|
343
|
-
| `construct/color_from_uint24.js`, `color_from_uint32.js` | `Color.fromUint` as a static |
|
|
344
|
-
| `construct/color_from_temperature.js` | wraps the existing `kelvin_to_rgb` |
|
|
345
|
-
| `construct/color_gray.js` | — |
|
|
346
|
-
| `operations/color_get_hsl.js` | `getHSV` exists, `getHSL` does not |
|
|
347
|
-
|
|
348
|
-
**Do:** each is a thin function over machinery that already exists. Existing `Color` methods are left
|
|
349
|
-
untouched — this is additive.
|
|
350
|
-
|
|
351
|
-
### Step 30 — Lazy `onChanged` · `§4.2` · **defer; highest blast radius**
|
|
352
|
-
|
|
353
|
-
**Files:** [Color.js](Color.js) constructor + `set()`
|
|
354
|
-
|
|
355
|
-
**Do:** every `Color` eagerly allocates a `Signal`; measured **71.7 ms vs 17.9 ms per 500k
|
|
356
|
-
constructions** — a 4× cost on a low-level type used in per-particle and per-vertex paths, which sits
|
|
357
|
-
badly with R2's "Color is a low-level class". Make the signal lazy (allocate on first `add`, guard
|
|
358
|
-
the `send8`). `Color.onChanged` has three known consumer sites (`PointLight`, `SpotLight`,
|
|
359
|
-
`ColorPickerView`) but is accessed as a plain property, so a getter must preserve that shape.
|
|
360
|
-
|
|
361
|
-
**Test:** existing `onChanged` specs green; a new allocation benchmark spec pinning the improvement;
|
|
362
|
-
manual check of the three consumers.
|
|
363
|
-
|
|
364
|
-
**Sequencing:** do this **after** Step 1, which is the step that makes `onChanged` correct in the
|
|
365
|
-
first place.
|
|
366
|
-
|
|
367
|
-
---
|
|
368
|
-
|
|
369
|
-
## Phase 7 — Test coverage (raised in priority by R3)
|
|
370
|
-
|
|
371
|
-
> Because nothing downstream is visible from this repo, the specs *are* the contract for the 33
|
|
372
|
-
> unspecced source files. Absolute reference values, not just mutual inverses.
|
|
373
|
-
|
|
374
|
-
| Step | Target | What to add |
|
|
375
|
-
|---|---|---|
|
|
376
|
-
| **31** | `oklab/` primitives | `linear_srgb_to_oklab`, `oklab_to_linear_srgb`, `find_cusp`, `toe`/`toe_inv`, `find_gamut_intersection` — currently 6 assertions across two whole-pipeline round-trips, and `find_gamut_intersection` has none. Add per-function specs with published reference values. |
|
|
377
|
-
| **32** | Matrix pairs | `rgb_to_xyz`/`xyz_to_rgb`, `ycxcz`, `YCoCg`, `PQ` are covered only by mutual round-trips — a forward matrix wrong in both directions passes. Add absolute expected values (D65 white, the three primaries, mid-grey). |
|
|
378
|
-
| **33** | Untested leaves | `rgb_to_luminance` (3 non-spec importers), `rgb2uint24`/`rgb2uint32`, `hsv2rgb_float`, `xyz_to_hpe`, `rgb_to_xyz`, `PQ_constants`. |
|
|
379
|
-
| **34** | Seams | The sweep spec from Step 5, generalised: assert bounded first differences across every piecewise function in the package (`kelvin_to_rgb`, `convert_channel_*`, `xyz_to_lab`'s `compress`). |
|
|
380
|
-
| **35** | Alpha | An alpha case on every function in `operations/` and on both `Color` sRGB statics. Steps 1 and 2 survived because none existed. |
|
|
381
|
-
| **36** | Allocation | A guard spec on `Color` construction cost, so Step 30 can't regress. |
|
|
382
|
-
|
|
383
|
-
---
|
|
384
|
-
|
|
385
|
-
## Phase 8 — Convention convergence (last, one sweep)
|
|
386
|
-
|
|
387
|
-
> Large, mechanical, mostly rename. Deliberately last so churn lands once, after behaviour is
|
|
388
|
-
> correct. Nothing here is a deletion (R3) — old names stay as re-exports for one release if needed.
|
|
389
|
-
|
|
390
|
-
| Step | Finding | Do |
|
|
391
|
-
|---|---|---|
|
|
392
|
-
| **37** | `§3.1` | Give `rgb_to_xyz` / `xyz_to_rgb` the `(out, out_off, in, in_off)` signature. Highest-value single change in the phase — 4 non-spec importers each, and they're the two most-imported conversions in the package. Keep the current 2-arg form as a delegating overload. |
|
|
393
|
-
| **38** | `§3.1` | Same treatment for the remaining `(out, in)` member, `xyz_to_hpe`. Leave `(out, …scalars)` alone where inputs genuinely aren't a contiguous triple (`kelvin_to_rgb`, the CMFs). |
|
|
394
|
-
| **39** | `§3.2` | Rename `hsv2rgb` → an explicitly byte-scaled name so the unsuffixed name isn't the non-default unit. Fix `parse_color`'s mixed `[0-255, 0-255, 0-255, 0-1]` return, moving the `/255` knowledge out of `Color.parse`. |
|
|
395
|
-
| **40** | `§3.3` | `name2name` → `name_to_name` across Layer B (`hsv2rgb`, `rgb2hsv`, `rgb2hex`, `hex2rgb`, `int2rgb`, `rgb2uint24`, `rgb2uint32`). Splits cleanly by age, so it's mechanical. |
|
|
396
|
-
| **41** | `§3.3` | One casing for the `sRGB` token (`sRGB_to_linear` vs `linear_srgb_to_oklab`), and one for directory names (`PQ`/`YCbCr`/`YCoCg`/`sRGB` vs the twelve lowercase ones). |
|
|
397
|
-
| **42** | `§1.3` | `hunt/` holds `xyz_to_hpe`, the Hunt–Pointer–Estévez *matrix*, while the Hunt-*effect* function lives in `lab/` and carries a disclaimer saying so. Move `xyz_to_hpe` to `xyz/` and retire the `hunt/` directory (move, not delete). |
|
|
398
|
-
| **43** | `§3.6` | One assert policy. Today `planckian_radiance` and `D65_spd_tabulated` validate scalars while `xyz_cmf_tabulated`, `xyz_cmf_wyman`, `linear_to_PQ` and `D65_spd_analytical` don't; `kelvin_to_rgb` asserts, `rgb_to_ycocg` doesn't. Also add the missing `@author`/`@copyright` to `linear_srgb_to_okhsv`, `linear_srgb_to_oklab`, `toe`. |
|
|
399
|
-
|
|
400
|
-
---
|
|
401
|
-
|
|
402
|
-
## Suggested batching
|
|
403
|
-
|
|
404
|
-
| Batch | Steps | Character |
|
|
405
|
-
|---|---|---|
|
|
406
|
-
| **A** | 1–4 | Alpha + ingest. Small edits, immediate risk reduction, no design decisions. |
|
|
407
|
-
| **B** | 5–6 | Kelvin. One decision (a vs b), one new test shape that pays off in Step 34. |
|
|
408
|
-
| **C** | 7–17 | Minor correctness sweep. Mostly one-liners plus docs. |
|
|
409
|
-
| **D** | 18–19 | Space contract. Doc + one new wrapper; unblocks correct downstream usage. |
|
|
410
|
-
| **E** | 20–24 | Deduplication by delegation. No exports removed. |
|
|
411
|
-
| **F** | 25–26 | CPU/GPU reconciliation. Do before more HDR work lands. |
|
|
412
|
-
| **G** | 27–29 | New surface area. Purely additive, no risk to existing callers. |
|
|
413
|
-
| **H** | 31–36, 30 | Coverage, then the lazy signal last (Step 30 needs 36 as its guard). |
|
|
414
|
-
| **I** | 37–43 | Convention sweep. |
|
|
415
|
-
|
|
416
|
-
**Regression baseline for every batch:** `npx vitest run src/core/color` — currently 33 files, 158
|
|
417
|
-
tests, all passing. No step in this plan may reduce that count.
|