@woosh/meep-engine 3.60.4 → 3.60.6
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 +2621 -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 +11 -0
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +50 -6
- 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 +28 -45
- package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts.map +1 -1
- package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +111 -119
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +22 -3
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +84 -3
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts +19 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.js +25 -0
- 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/VirtualGeometry.d.ts +15 -0
- package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +38 -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/geometry/virtual/sdf/VGeoSDFProxies.d.ts +28 -59
- package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.js +84 -789
- 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 +7 -6
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +12 -11
- package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.d.ts +2 -2
- package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.js +2 -2
- 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 +11 -1
- package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
- package/src/shade/renderer/material/GPUMaterialManager.js +16 -8
- 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/postprocess/gtao/GTAO.d.ts +7 -3
- package/src/shade/renderer/postprocess/gtao/GTAO.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/gtao/GTAO.js +26 -13
- package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts +14 -0
- package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.js +196 -68
- package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js +42 -28
- package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +429 -414
- package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts +16 -0
- package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts.map +1 -0
- package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.js +156 -0
- package/src/shade/renderer/scene/GPUSceneContext.d.ts +2 -2
- package/src/shade/renderer/scene/GPUSceneContext.js +5 -5
- 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/geometry/virtual/sdf/VGEO_SDF.d.ts +0 -47
- package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts.map +0 -1
- package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.js +0 -80
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts +0 -16
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts.map +0 -1
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.js +0 -67
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts +0 -17
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts.map +0 -1
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.js +0 -145
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts +0 -12
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts.map +0 -1
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.js +0 -105
- 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,1218 +0,0 @@
|
|
|
1
|
-
# AVBOIT — ledger
|
|
2
|
-
|
|
3
|
-
Companion to [`AVBOIT_PLAN_2026_09_03.md`](./AVBOIT_PLAN_2026_09_03.md). One entry per step of §8, in the
|
|
4
|
-
order they landed, each recording what was built, how it was verified, and what the step found that the plan
|
|
5
|
-
did not predict. Findings that change the plan are noted here and the plan's revision header is bumped, never
|
|
6
|
-
silently edited.
|
|
7
|
-
|
|
8
|
-
Branch: `claude/avboit-transparency-pipeline-192cff`, worktree
|
|
9
|
-
`.claude/worktrees/avboit-transparency-pipeline-192cff`, from `9ab84a154`.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## 0. Survey — 2026-09-03
|
|
14
|
-
|
|
15
|
-
**Done.** Read the deck (text extraction via `pdftotext`; the page renderer is not installed here) and the
|
|
16
|
-
MBOIT pipeline end to end: orchestrator, three shaders, frame-graph API, resource descriptors, the bucket →
|
|
17
|
-
draw-command prep, the emulator's capabilities, the software device's recording API, and the `Renderer` call
|
|
18
|
-
site and mode-switch precedents.
|
|
19
|
-
|
|
20
|
-
**Findings that shaped the plan:**
|
|
21
|
-
|
|
22
|
-
- The sorted meshlet buffer the draws consume has no count header
|
|
23
|
-
(`shader_sort_meshlets_by_material.js:20` writes a bare `array<MeshletDefinition>`); the collection *before*
|
|
24
|
-
sorting does, and the prep helper already builds an indirect dispatch command over it. The occupancy pass
|
|
25
|
-
needs both, so the helper returns two more handles (plan §3.1).
|
|
26
|
-
- No render pipeline in the tree writes a storage buffer from the fragment stage, and the pipeline-layout
|
|
27
|
-
helper applies one visibility to all groups. The splat pass builds its layout per group (plan §3.11).
|
|
28
|
-
- The emulator models `atomic<T>` as `{ value }` objects and implements `atomicAdd/Min/Max/Or`, `textureStore`
|
|
29
|
-
on 3D `CPUBitmapData`, `extractBits/insertBits`, `countOneBits`; barriers are no-ops and there is no
|
|
30
|
-
workgroup-cooperative dispatch. The warp build is therefore a single invocation so it stays
|
|
31
|
-
emulator-testable (plan §3.4).
|
|
32
|
-
- View-space depth sign: the volumetrics take `-(view_matrix × p).z`, so the camera looks down −Z; the
|
|
33
|
-
occupancy pass uses the same (plan §3.3).
|
|
34
|
-
- `float32-blendable` is on the device floor purely for MBOIT's `r32float`/`rgba32float` targets; AVBOIT
|
|
35
|
-
blends `rgba16float` only (plan §3.11).
|
|
36
|
-
- The in-app browser pane cannot run the full renderer (immediate data unsupported there, per memory), so the
|
|
37
|
-
playground look (step 9) needs a real Chrome tab; module compilation (step 8) works in either.
|
|
38
|
-
|
|
39
|
-
---
|
|
40
|
-
|
|
41
|
-
## 1. Skeleton and switch — 2026-09-03
|
|
42
|
-
|
|
43
|
-
**Done.** `ShadeTransparencyMode` (`MBOIT = 0`, `AVBOIT = 1`), `Renderer.transparency_mode` defaulting to
|
|
44
|
-
MBOIT, a ternary at the single transparency call site selecting the recorder, and
|
|
45
|
-
`graph_prepare_meshlet_draw_commands_by_material` returning `meshlet_collection` and
|
|
46
|
-
`meshlet_iterator_command` beside what it returned before. `Renderer.js` and the prep helper are CRLF files;
|
|
47
|
-
edited byte-exactly.
|
|
48
|
-
|
|
49
|
-
**Verified.** The rasterize spec tree (14 files, 81 tests) passes with the helper change; the source-level spec
|
|
50
|
-
in `graph_rasterize_meshes_transparent_avboit.spec.js` pins the import, the default and the ternary.
|
|
51
|
-
|
|
52
|
-
**Found.** Nothing the plan did not predict.
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## 2. Depth curve, packing, warp — 2026-09-03
|
|
57
|
-
|
|
58
|
-
**Done.** `chunk_avboit_depth_curve` (t(z), z(t)), `chunk_avboit_pack` (10|10|10, counts ↔ extinction ↔
|
|
59
|
-
transmittance, the overflow predicate), `AVBOIT_WARP_STRUCT`, `chunk_avboit_warp` (φ, and the view-depth →
|
|
60
|
-
virtual → physical composition), each with an emulator spec.
|
|
61
|
-
|
|
62
|
-
**Found.**
|
|
63
|
-
|
|
64
|
-
- The emulator hands a `u32` with its top bit set back as a negative JS number (`0xFFFFFF00` reads as
|
|
65
|
-
`-256`). Specs compare through `>>> 0`. `countOneBits` and the shift operators are unsigned internally, so
|
|
66
|
-
the shaders themselves are unaffected.
|
|
67
|
-
- `vec3<f32>` values travel as `Float32Array` through the emulator, so round-trip expectations are f32-exact
|
|
68
|
-
(six digits), not f64.
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## 3. Occupancy and warp build — 2026-09-03
|
|
73
|
-
|
|
74
|
-
**Done.** `chunk_avboit_occupancy` (range, word mask, dilate, fold), `shader_avboit_occupancy` (per meshlet,
|
|
75
|
-
indirect over the filtered collection), `shader_avboit_build_warp` (one invocation: dilate → count → halve
|
|
76
|
-
until it fits, then the exclusive-prefix LUT), with specs for the chunk and for the whole build shader
|
|
77
|
-
dispatched through the emulator.
|
|
78
|
-
|
|
79
|
-
**Found.** The implementation dilates at *every* level it evaluates rather than once at the finest and once at
|
|
80
|
-
the settled level, which is the same thing where it matters (the settled level) and simpler to reason about.
|
|
81
|
-
`AVBOIT_OCCUPANCY_DILATION` at the prepass is therefore only a belt-and-braces one slice. Plan §3.4 describes
|
|
82
|
-
the per-level version.
|
|
83
|
-
|
|
84
|
-
Specs pin: empty → `V_max` and an all-zero LUT; a run that dilates to exactly `P` does not halve; one over
|
|
85
|
-
halves once and is re-dilated at the coarser level; everywhere-occupied halves down to `V = P`; the prefix is
|
|
86
|
-
exclusive, monotone, and never exceeds `P`.
|
|
87
|
-
|
|
88
|
-
---
|
|
89
|
-
|
|
90
|
-
## 4. Splat — 2026-09-03
|
|
91
|
-
|
|
92
|
-
**Done.** `shader_avboit_splat`: low-resolution render pass, no depth attachment, a `r8unorm` dummy target
|
|
93
|
-
with `writeMask: 0`, per-group pipeline layout with the atomics group fragment-only, linear splat with
|
|
94
|
-
per-channel overflow marks stored inverted under `atomicMax`. `chunk_avboit_tint` factored out so the splat
|
|
95
|
-
and the draw compute the transmission tint from one function.
|
|
96
|
-
|
|
97
|
-
**Found.** No un-jitter in the splat: `unjitter_uv` is derivative-based and the jitter is in
|
|
98
|
-
internal-resolution pixels, so at 1/8 resolution the correction would be eight times too large. Recorded in
|
|
99
|
-
the shader's docblock; the plan did not mention it.
|
|
100
|
-
|
|
101
|
-
**Verified.** Validity tier (vertex + fragment) and the tier-3 layout spec: group 3 entries are `FRAGMENT`
|
|
102
|
-
with `buffer.type === "storage"`, groups 0–2 are `VERTEX | FRAGMENT`, the target's `writeMask` is 0, no
|
|
103
|
-
`depthStencil`.
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## 5. Integrate — 2026-09-03
|
|
108
|
-
|
|
109
|
-
**Done.** `shader_avboit_integrate`, 8×8 workgroups, exclusive prefix into a `P + 1`-deep `rgba8unorm` 3D
|
|
110
|
-
storage texture, per-channel saturation and overflow pinning. Emulator spec over one column: exclusive
|
|
111
|
-
prefix, independent channels, saturation, a summed field past 255 saturating in one step, an overflow mark
|
|
112
|
-
pinning only its channel, a mark at slice 0.
|
|
113
|
-
|
|
114
|
-
**Found.** Nothing unpredicted.
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
## 6. Draw and sample — 2026-09-03
|
|
119
|
-
|
|
120
|
-
**Done.** `chunk_avboit_sample_transmittance` (bias in virtual slices, warp, `(p + 0.5) / (P + 1)` depth
|
|
121
|
-
coordinate, `exp`), `avboit_draw_fragment_body` (one WGSL text shared by the IBL and Brick4 pipelines, where
|
|
122
|
-
the two MBOIT resolve shaders share theirs by duplication), `shader_avboit_draw_ibl`,
|
|
123
|
-
`shader_avboit_draw_brick4`, `AVBOIT_INTEGRAL_SAMPLER_DESCRIPTOR`. Emulator spec of the sample chunk against a
|
|
124
|
-
hand-built integral: run boundaries land on texel centres, an event in front attenuates by `exp(-E × ln 256)`,
|
|
125
|
-
the bias clears a surface's own linear splat, a gap sample reads the previous run's total and none of the
|
|
126
|
-
next run's, beyond the far plane the sample reads the total.
|
|
127
|
-
|
|
128
|
-
**Found.** The emulator's `null` sampler is the linear clamp-to-edge fallback the spec wants, so no sampler
|
|
129
|
-
object is built there.
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
|
-
## 7. Resolve, orchestrator, tier 3 — 2026-09-03
|
|
134
|
-
|
|
135
|
-
**Done.** `shader_avboit_resolve` (reads the scene, writes a fresh `rgba16float`; RGB total transmittance
|
|
136
|
-
cannot blend in place), the full `graph_rasterize_meshes_transparent_avboit` with its sub-recorders exported
|
|
137
|
-
(`graph_avboit_occupancy`, `_build_warp`, `_splat`, `_integrate`, `_draw`),
|
|
138
|
-
`avboit_shaders_wgsl_validity.spec.js` (10 modules, zero diagnostics),
|
|
139
|
-
`graph_rasterize_meshes_transparent_avboit.spec.js` on the software device.
|
|
140
|
-
|
|
141
|
-
**Found.**
|
|
142
|
-
|
|
143
|
-
- `graph_compute_pass` cannot create a `read_write` storage buffer from a descriptor: the helper puts every
|
|
144
|
-
readable binding through the read path first, so the descriptor arrives at `pass.write` as an object. The
|
|
145
|
-
warp build is recorded by hand instead, and its `settings` uniform is serialized with
|
|
146
|
-
`encoder.allocateTransientValueBuffer` — the plain-object convenience only exists inside the graph
|
|
147
|
-
helpers, not in `ComputeShader.dispatch`. Plan §3.1's table is unchanged; the pass is the same, just not
|
|
148
|
-
via the helper.
|
|
149
|
-
- No spec builds a `GPUViewContext` over a scene on the mock, so the tier-3 spec pins the exported
|
|
150
|
-
sub-recorders (warp build: one workgroup into a buffer of the struct's size; integration: the dispatch
|
|
151
|
-
covers the voxel grid and the texture is `3d`, `P + 1` deep, storage + sampled; resolve: one 3-vertex draw
|
|
152
|
-
into a fresh target) and the `Renderer` switch at source level. The whole orchestrator's first encode is on
|
|
153
|
-
a real device (steps 8–9).
|
|
154
|
-
|
|
155
|
-
**Verified.** 62 tests across the avboit directory; 81 across `rasterize/`; the full `src/shade` suite
|
|
156
|
-
(489 files, 3360 tests) passes; `scripts/check-types.mjs` reports the declaration emit clean and zero
|
|
157
|
-
consumer-visible errors.
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## 8–9. Real compile and the look — 2026-09-03, handed over
|
|
162
|
-
|
|
163
|
-
**Done.** `src/shade/playground/avboit/`: the deck's harness in miniature (a stack of cards at a controlled
|
|
164
|
-
spacing, a tinted glass pane, an opaque backdrop), the MBOIT/AVBOIT switch, and a **Compile check** that
|
|
165
|
-
pushes all ten AVBOIT modules through `createShaderModule` on the live device and prints
|
|
166
|
-
`getCompilationInfo`. The README carries the one scripted session (six steps) with what "pass" looks like
|
|
167
|
-
for each.
|
|
168
|
-
|
|
169
|
-
**Not done here, and why.** The in-app browser pane returned `null` from `requestAdapter()` this session
|
|
170
|
-
(its `wgslLanguageFeatures` are present, the adapter is not — the same state
|
|
171
|
-
`playground-needs-immediate-data` recorded on 2026-09-02), and no Claude-in-Chrome browser was connected.
|
|
172
|
-
So neither the real compile nor the picture happened on this side. What the pane did prove: the page
|
|
173
|
-
loads under the dev server, every import resolves (the ten shader modules assemble at import time, so a
|
|
174
|
-
chunk-dependency or struct error would have thrown there), and `start()` runs up to
|
|
175
|
-
`Renderer.initialize`, which reports `requestAdapter returned null` cleanly. The sources were dumped
|
|
176
|
-
(`scratchpad/wgsl/*.wgsl`, 217 KB) for a pane compile should an adapter come back. What is known: every
|
|
177
|
-
module passes the validator, which has no type checker; the type-level risks are listed below so the first
|
|
178
|
-
compile failure, if any, is quick to place.
|
|
179
|
-
|
|
180
|
-
**Type-level things the validator could not see** (in the order a compiler would meet them):
|
|
181
|
-
|
|
182
|
-
- `avboit_warp` takes `ptr<storage, AVBOITWarp, read>`; the splat and draw pass `&warp` where `warp` is
|
|
183
|
-
declared through `addStorageBuffer(..., false)`. `var<storage>` without an explicit mode is `read`, which
|
|
184
|
-
matches.
|
|
185
|
-
- `atomicAdd(&extinction_volume[column + u32(s)], …)` indexes an `array<atomic<u32>>` binding from the
|
|
186
|
-
fragment stage — core WGSL, but the first such pipeline in the tree.
|
|
187
|
-
- `textureStore(integral, vec3<i32>, vec4<f32>)` on `texture_storage_3d<rgba8unorm, write>`.
|
|
188
|
-
- `select(vec3<u32>, vec3<u32>, vec3<bool>)` and `select(vec3<f32>, vec3<f32>, vec3<bool>)` in the
|
|
189
|
-
integration; both are the component-wise overload.
|
|
190
|
-
- The splat fragment entry has no return type against a pipeline with one colour target at `writeMask: 0`.
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
## 10. Docs — 2026-09-03
|
|
195
|
-
|
|
196
|
-
**Done.** `RENDERER_CONTRACT.md` §2 says which mode needs `float32-blendable`; §9 lists
|
|
197
|
-
`transparency_mode`. `CHANGELOG.md` carries the entry under the unreleased 4.0.0 "Added". The plan is at
|
|
198
|
-
revision 1; nothing in steps 1–9 required a revision of its design sections.
|
|
199
|
-
|
|
200
|
-
**Open, for the session in the playground README:** the ten real compiles; the first frame under AVBOIT;
|
|
201
|
-
the stack at the default spacing (does the bar behind it show through?); the spacing at which the cards
|
|
202
|
-
fall into one slice; the red pane's tint on the backdrop. Results go here as entry 11.
|
|
203
|
-
|
|
204
|
-
---
|
|
205
|
-
|
|
206
|
-
## 11. Path-traced reference on the playground — 2026-09-03
|
|
207
|
-
|
|
208
|
-
**Done.** A third mode on the page, "path traced (reference)": `feature_path_tracing_enabled` on, TAA
|
|
209
|
-
forced off while it is, the tracer's running-mean floor lowered from 0.01 to 0.002 so the image keeps
|
|
210
|
-
settling past 100 samples, a **Reset accumulation** button, and a once-a-second readout of how converged
|
|
211
|
-
the image is. That readout needed one small engine addition: `AccumulatingPathTracer.min_sample_count`,
|
|
212
|
-
the sample count of the tile that has had the fewest (the per-tile counters were private, and frames
|
|
213
|
-
elapsed is not a sample count — a frame advances one tile).
|
|
214
|
-
|
|
215
|
-
**What the reference is good for, and not** (README "The reference"):
|
|
216
|
-
|
|
217
|
-
- The tracer rolls each transparent hit against alpha and continues the ray, so it converges to the
|
|
218
|
-
exact order-independent composite of the card stack — the thing both OIT modes approximate. Good.
|
|
219
|
-
- It has no per-channel transmission tint (thin-walled transmission carries only `(1 - F)`), so it
|
|
220
|
-
cannot confirm AVBOIT's glass tint; the pane is compared with the tint off.
|
|
221
|
-
- Its shadow rays treat geometry as opaque (the tracer's own `TODO`), so the cards cast full shadows
|
|
222
|
-
there and none under either OIT mode. The stack, not its shadow, is the comparison.
|
|
223
|
-
|
|
224
|
-
**Verified.** `path_tracer/` has no specs of its own; the getter is a read of a private array. The
|
|
225
|
-
page's module graph was re-loaded in the pane (see entry 8–9 for what the pane can and cannot prove).
|
|
226
|
-
|
|
227
|
-
---
|
|
228
|
-
|
|
229
|
-
## 12. First renders on a real device — 2026-09-03
|
|
230
|
-
|
|
231
|
-
Alex ran the playground and pasted three canvas captures at the default settings (four cards, alpha
|
|
232
|
-
0.85, spacing 1.5, red glass pane): path traced, AVBOIT, MBOIT. Read against the code, not measured.
|
|
233
|
-
|
|
234
|
-
**The stack — the deck's depth-complexity claim reproduces.** Under MBOIT the green card, two layers
|
|
235
|
-
back, shows through both the red and the blue card as a lighter rectangle inside the red one. Under
|
|
236
|
-
the path tracer and under AVBOIT it does not; the overlap is red with the blue card directly behind
|
|
237
|
-
faintly visible. AVBOIT is the closer of the two to the reference on the stack. Both rasterized modes
|
|
238
|
-
render the cards darker than the tracer, and they agree with each other, so that is a forward-shading
|
|
239
|
-
versus tracer difference and not this pipeline's.
|
|
240
|
-
|
|
241
|
-
**Also as predicted:** the tracer casts full shadows from the cards (its occlusion query treats
|
|
242
|
-
geometry as opaque), neither OIT mode does.
|
|
243
|
-
|
|
244
|
-
**The glass pane — the tint is working as written, and is the wrong default.** Under AVBOIT the pane
|
|
245
|
-
is deep red; under the tracer and MBOIT it is clear glass. The material is transmission 1 with a red
|
|
246
|
-
base colour, and `chunk_avboit_tint` multiplies transmitted light by that base colour, so green and
|
|
247
|
-
blue drop to ~14 %. It is not opaque — the horizon shows through as the dark-to-bright step at floor
|
|
248
|
-
level — but it is a material semantic the other paths do not have: the forward shader states there is
|
|
249
|
-
no per-channel transmission tint in the material model, and the tracer's thin-walled transmission
|
|
250
|
-
carries only the Fresnel split. The contract's promise that the two modes take the same inputs and
|
|
251
|
-
are safe to flip per frame does not survive a flip that recolours glass.
|
|
252
|
-
|
|
253
|
-
Two corrections to what the code says about itself: the chunk docblock calls base-colour-as-tint
|
|
254
|
-
"the OpenPBR default", which it is not (OpenPBR's transmission colour defaults to white; glTF's
|
|
255
|
-
transmission extension is the one that uses the base colour); and plan §4's first row rests on the
|
|
256
|
-
same reading.
|
|
257
|
-
|
|
258
|
-
**Proposed:** keep the RGB plumbing, default the tint off, expose it as a runtime switch
|
|
259
|
-
(`renderer.avboit.transmission_tint_from_base_color`, carried in the warp header so the splat and
|
|
260
|
-
draw agree per frame), a checkbox on the page, plan to revision 2. The alternative — keep it on and
|
|
261
|
-
document it as an AVBOIT-only semantic — makes one asset mean two things depending on a renderer
|
|
262
|
-
switch. Alex chose the first; entry 14.
|
|
263
|
-
|
|
264
|
-
**Also closed by these renders:** step 8's real compile, for the IBL variant. An AVBOIT frame on a real
|
|
265
|
-
device means the occupancy, warp, splat, integrate, IBL draw and resolve modules all compiled and the
|
|
266
|
-
pipeline ran end to end. The Brick4 draw variant is not exercised by the page (`indirect_lighting_mode`
|
|
267
|
-
is IBL there) and remains compiled only by the validator.
|
|
268
|
-
|
|
269
|
-
---
|
|
270
|
-
|
|
271
|
-
## 13. The deck's opaque-card case on the page — 2026-09-03
|
|
272
|
-
|
|
273
|
-
**Done.** At Alex's request: a small pale quad, alpha 1 on a `Transparent` material, one unit behind
|
|
274
|
-
the last card and offset so part of it is seen through the stack and part stands against the backdrop
|
|
275
|
-
and the dark bar. Both pipelines clamp its coverage at `OIT_COVERAGE_MAX` (0.997), so it is the "0 %
|
|
276
|
-
transmittance card" of deck slides 35–38: the case where a moment reconstruction cannot hold the
|
|
277
|
-
step, leaks the background through the surface, and tears along the depth discontinuity at its edge.
|
|
278
|
-
A checkbox (`opaque_quad`, default on) and step 8 of the README's session.
|
|
279
|
-
|
|
280
|
-
**What to expect from AVBOIT here, from the design:** the quad's extinction saturates its slice
|
|
281
|
-
(`-ln(1 - 0.997) / ln 256 ≈ 1.05`, clamped to 1.0), so the integral behind it reads 1.0 and every
|
|
282
|
-
fragment further back is weighted by a transmittance of 1/256. Its edge is a step in the extinction
|
|
283
|
-
volume at 1/8 resolution, softened by the draw's trilinear sample over about one voxel — so a slight
|
|
284
|
-
halo of that width is the expected artefact, not a leak. Anything behind the quad showing through its
|
|
285
|
-
interior would be a finding.
|
|
286
|
-
|
|
287
|
-
---
|
|
288
|
-
|
|
289
|
-
## 14. The tint, kept and off by default — 2026-09-03
|
|
290
|
-
|
|
291
|
-
**Done.** `AVBOITSettings` on `Renderer.avboit` with one switch, `transmission_tint_from_base_color`
|
|
292
|
-
(default false). The orchestrator takes `avboit` beside its other inputs (the MBOIT one ignores it), the
|
|
293
|
-
warp build copies it into a new `tint_from_base_color` header word, and both the splat and the draw
|
|
294
|
-
weight `chunk_avboit_tint` by `transmission_factor × f32(warp.tint_from_base_color)`. The chunk's
|
|
295
|
-
docblock now says whose semantics the mapping is (glTF's, not OpenPBR's). Plan to revision 2 (§3.5,
|
|
296
|
-
§4, §7). Contract §9 lists `avboit`. The page has a checkbox for it, off by default, and step 6 of the
|
|
297
|
-
session turns it on before comparing the pane.
|
|
298
|
-
|
|
299
|
-
**Why a header word and not a shader constant:** the splat and the draw must agree within a frame, and
|
|
300
|
-
the header is already the one place both read per-frame facts from; a runtime flag lets the page do
|
|
301
|
-
the A/B without a rebuild.
|
|
302
|
-
|
|
303
|
-
**Verified.** Build-warp emulator spec asserts the header copies the flag; validity tier over the
|
|
304
|
-
edited modules; rasterize spec tree.
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
## 15. Decisions on the open items — 2026-09-03
|
|
309
|
-
|
|
310
|
-
Alex's answers to the open list, and what each became.
|
|
311
|
-
|
|
312
|
-
**Slice floor — mine to call; 5 cm is a spacing worth resolving.** Virtual slice thickness is
|
|
313
|
-
`(z0 + z - near) · ln(1 + (far - near) / z0) / V`. At the page's camera (`near 0.1`, `far 200`,
|
|
314
|
-
`z0 = 0.8`) and `V = 1024` that was 5.8 cm at 10 m: one slice, under the two-slice bias, so 5 cm
|
|
315
|
-
blended. `AVBOIT_VIRTUAL_SLICES_MAX` is now 4096 — 1.5 cm at 10 m, 2.8 cm at 20 m, 7 cm at 50 m — so
|
|
316
|
-
5 cm resolves out to about 17 m before the bias eats it, and the adaptive halving still pulls it back
|
|
317
|
-
when more than 128 slices are occupied. The bias stays at 2.0 rather than dropping to the analytic
|
|
318
|
-
1.5: the half-slice margin is what covers the depth difference between a splat fragment (a
|
|
319
|
-
1/8-resolution pixel centre) and a draw fragment on a sloped surface. Cost: 128 occupancy words, a
|
|
320
|
-
16 KB LUT, a longer single-lane loop in the warp build. Specs unchanged in intent (they read the
|
|
321
|
-
constant) and green.
|
|
322
|
-
|
|
323
|
-
**The opaque quad — fine as is.** Stays on the page.
|
|
324
|
-
|
|
325
|
-
**Camera.** `OrbitalCameraController` on the page (drag, wheel, right-drag), **Copy camera** to the
|
|
326
|
-
clipboard and a box, **Apply camera** from the box, `camera_state()` / `apply_camera()` /
|
|
327
|
-
`configure({ camera })` from the console. Plan §9 gains the slope risk in its own words: a splat
|
|
328
|
-
fragment carries its low-resolution pixel centre's depth, and on a grazing surface that can exceed
|
|
329
|
-
the bias.
|
|
330
|
-
|
|
331
|
-
**Brick4 — should land.** The variant exists and is validator-clean; the page now has an
|
|
332
|
-
indirect-lighting select (IBL / Brick4) so the Brick4 draw pipeline compiles and draws on a device.
|
|
333
|
-
The scene has no probes, so the indirect term is black either way; the step is in the session.
|
|
334
|
-
|
|
335
|
-
**Timings — hook up the profiler.** Done as on `volumetrics_froxel`: **Profile** starts a
|
|
336
|
-
`GPUProfileSession` at `WORKLOAD`, pressing it again drains and downloads
|
|
337
|
-
`avboit-<mode>-<timestamp>.sgpt` for the gpu-inspector-tool, with the mode and the page's settings in
|
|
338
|
-
the note and the adapter in the meta. Same settings, two recordings, one per mode, is the comparison.
|
|
339
|
-
|
|
340
|
-
**Software device — wrong tool for an end-to-end spec, and that is fine.** Plan §6 tier 3 now says
|
|
341
|
-
so; the full-orchestrator spec is off the list.
|
|
342
|
-
|
|
343
|
-
**Phase 2 — only clean, unambiguous, non-situational.** Decided item by item in plan §7 revision 3.
|
|
344
|
-
One landed: the draw discards a fragment the volume already calls extinct (`T ≤ 1/256` in every
|
|
345
|
-
channel) *before* forward shading. No new resource or pass; the error is bounded by `1/256` of the
|
|
346
|
-
background (`AVBOIT_EXTINCT_TRANSMITTANCE`), below the 8-bit volume's own resolution. Tile skipping
|
|
347
|
-
(no: sub-millisecond passes), the zero-transmittance depth cull (not now: situational, touches the
|
|
348
|
-
depth other passes read, wants its own plan and a scene that needs it), the monochrome buffer (no:
|
|
349
|
-
two representations for content we do not have), the sparse texture (no: speculative).
|
|
350
|
-
|
|
351
|
-
**Back-sided materials — ignored for now.** Plan §7 keeps the note.
|
|
352
|
-
|
|
353
|
-
**AVBOIT is the default.** `Renderer.transparency_mode = ShadeTransparencyMode.AVBOIT`; the enum,
|
|
354
|
-
the contract (§2 keeps `float32-blendable` on the floor while MBOIT is selectable; §9 row), the
|
|
355
|
-
changelog and the page's default select follow. The source-level spec pins the new default.
|
|
356
|
-
|
|
357
|
-
**Committed** on the branch after this entry.
|
|
358
|
-
|
|
359
|
-
---
|
|
360
|
-
|
|
361
|
-
## 16. The page's camera and profile buttons, reported not working — 2026-09-03
|
|
362
|
-
|
|
363
|
-
Alex: the camera could not be moved, and **Profile** appeared to do nothing with no way to stop it.
|
|
364
|
-
|
|
365
|
-
**What was verified here.** The page was restructured so the camera, its controller and every control
|
|
366
|
-
are wired *before* the device is requested. That made the input path testable in the in-app pane,
|
|
367
|
-
which has no adapter: a synthetic pointer drag dispatched on the canvas orbited the camera from
|
|
368
|
-
`(0, 2.5, 12)` to `(-7.3, 3.7, 9.4)` and a wheel event zoomed it, read back through `camera_state()`.
|
|
369
|
-
So `OrbitalCameraController` on `document.body`, the pointer device and the transform write are
|
|
370
|
-
correct as wired. What could not be reproduced is the failure in a real session.
|
|
371
|
-
|
|
372
|
-
**What the restructure found.** Building geometry before `Renderer.initialize` threw
|
|
373
|
-
`Cannot read properties of undefined (reading 'exports')`: the meshlet builder runs on a WASM
|
|
374
|
-
instance (`MeshoptClusterizer`) that instantiates asynchronously at import, and the renderer's own
|
|
375
|
-
initialization had been what gave it time. The page now awaits `MeshoptClusterizer.ready` itself. Not
|
|
376
|
-
the reported fault (the committed order built geometry after the device), but the same class of
|
|
377
|
-
thing: a step that silently depended on another step's timing.
|
|
378
|
-
|
|
379
|
-
**Made robust, since the fault could not be seen.** Every control handler runs through a guard that
|
|
380
|
-
puts a failure into the report panel (`<what> failed: <message>`) instead of only the console; the
|
|
381
|
-
final settings application is guarded so a settings error cannot stop the frame loop from starting;
|
|
382
|
-
the Profile button carries a `recording` state (label and red border) so a running capture is
|
|
383
|
-
unmistakable and the same button stops it; the controls are spelled out under the canvas. If a real
|
|
384
|
-
session still does not orbit, the report panel now says why.
|
|
385
|
-
|
|
386
|
-
**Suspicion, unverified.** With HMR off, a reload taken while the page's files were being rewritten
|
|
387
|
-
on disk can pair one version of `index.html` with another of `main.js` or `Renderer.js`; a
|
|
388
|
-
`TypeError` in the settings step then killed `start()` before the frame loop, which is the one
|
|
389
|
-
sequence that leaves the buttons registered, the camera frozen and no picture of the cause. A hard
|
|
390
|
-
reload after this commit is the first thing to try.
|
|
391
|
-
|
|
392
|
-
---
|
|
393
|
-
|
|
394
|
-
## 17. First timings — 2026-09-03
|
|
395
|
-
|
|
396
|
-
Alex ran a short profiling session on the page (the deck's harness in miniature: four cards, the
|
|
397
|
-
glass pane, the opaque quad, 1280x720): **AVBOIT 289.9 us, MBOIT 282.4 us**, averages of the
|
|
398
|
-
transparency work, which he reads as within the noise. Device and capture files not recorded here.
|
|
399
|
-
|
|
400
|
-
**What that says.** On this scene the two pipelines cost the same. That is the expected shape, not a
|
|
401
|
-
surprise: the deck's cost argument (AVBOIT approaching no-OIT, slides 106-110) comes from VFX content
|
|
402
|
-
with tens of millions of overdrawn transparent pixels, where the 1/8-resolution splat, the saturated
|
|
403
|
-
integral and the early-out remove most of the shading work. Six quads at 720p have almost nothing
|
|
404
|
-
for those to remove, and AVBOIT's fixed costs — the occupancy pass, the warp build, the integration
|
|
405
|
-
over a 160x90x129 volume, the resolve into a fresh target — are of the same order as MBOIT's two
|
|
406
|
-
full-resolution passes over the same few quads. So the number closes the question "does the new path
|
|
407
|
-
cost more on light content" (no), and leaves open the one the deck actually answers, which needs a
|
|
408
|
-
scene with real transparent overdraw. Plan §9's contention risk is unmeasured until then.
|
|
409
|
-
|
|
410
|
-
**Committed** with this entry.
|
|
411
|
-
|
|
412
|
-
---
|
|
413
|
-
|
|
414
|
-
## 18. Review against the engine — 2026-09-03
|
|
415
|
-
|
|
416
|
-
Alex asked three questions of the code: is anything reinvented, does it follow one function per
|
|
417
|
-
chunk, does it fight the engine. Surveyed and fixed; plan to revision 4.
|
|
418
|
-
|
|
419
|
-
**Reinvented, now reused.**
|
|
420
|
-
|
|
421
|
-
- *The depth curve.* `chunk_avboit_depth_curve` (t(z), z(t), a `z0` knob) was a second copy of what the
|
|
422
|
-
light clusters and the froxels already use: `cluster_depth_to_z_slice` with parameters from
|
|
423
|
-
`build_light_cluster_parameters`. That builder's `scale` argument is the linearization the deck
|
|
424
|
-
describes; `avboit_build_depth_curve` chooses it so the curve is pure log from the near offset,
|
|
425
|
-
because the warp makes near precision free. Gone: the chunk, its inverse, its spec, the
|
|
426
|
-
`AVBOIT_DEPTH_CURVE_LINEARIZATION` constant, and the camera binding of the warp build (the curve now
|
|
427
|
-
arrives as three floats in the settings and in the occupancy pass's own settings uniform).
|
|
428
|
-
- *The bit fold.* `avboit_fold_pairs` is the OR of the even bits compacted and the odd bits compacted,
|
|
429
|
-
which is `decode32_morton2`'s job. Found on the way: for a 32-bit code that function's `.y` is *not*
|
|
430
|
-
the odd bits — the `<< 31` that separates them in the 16-bit variant (`<< 15`) cannot work in 32
|
|
431
|
-
bits — and the first reuse trusted it; the warp-build spec caught it as an occupied count of 70
|
|
432
|
-
against 66. The fold now goes through the correct `.x` half twice, once on `x >> 1`, and has a spec
|
|
433
|
-
against a bit-by-bit reference. `decode32_morton2` is used nowhere else in the tree; its `.y` is a
|
|
434
|
-
latent engine defect, noted for whoever reaches for it next.
|
|
435
|
-
- *The sampler.* `AVBOIT_INTEGRAL_SAMPLER_DESCRIPTOR` duplicated `SAMPLER_LINEAR_UPSCALE` (linear,
|
|
436
|
-
clamped on every axis), which the draw already binds as `sLinear` for the split-sum LUT. The integral
|
|
437
|
-
samples through that; one binding fewer in the view group.
|
|
438
|
-
- *The draw loop.* `material_draw` existed privately in both orchestrators. It is
|
|
439
|
-
`rasterize/native/material_draw_indirect.js` now, with the material bind-group layout it needs,
|
|
440
|
-
imported by both.
|
|
441
|
-
|
|
442
|
-
**Convention.** 650 of the engine's ~700 chunk files hold exactly one function named after the file;
|
|
443
|
-
mine held two to six. Five chunks became seventeen (`chunk_avboit_pack_counts`, `_unpack_counts`,
|
|
444
|
-
`_transmittance_to_extinction`, `_extinction_to_transmittance`, `_extinction_to_counts`,
|
|
445
|
-
`_counts_to_extinction`, `_overflows`, `_occupancy_range`, `_occupancy_word_mask`, `_dilate_word`,
|
|
446
|
-
`_fold_pairs`, `_warp`, `_view_depth_to_virtual`, `_view_depth_to_physical`,
|
|
447
|
-
`_integral_depth_coordinate`, `_sample_transmittance`, `_transmission_tint`), each with its spec; the
|
|
448
|
-
hand-built warp and the curve's inverse the specs share live in `avboit_spec_support.js`, test-only.
|
|
449
|
-
|
|
450
|
-
**Kept, on purpose.** `avboit_draw_fragment_body.js` shares one WGSL text between the IBL and Brick4
|
|
451
|
-
draw pipelines where MBOIT duplicates its resolve body. No precedent either way in the tree; two
|
|
452
|
-
45-line bodies drifting apart is the worse failure, and the two files stay concrete modules with no
|
|
453
|
-
conditionals in them.
|
|
454
|
-
|
|
455
|
-
**Also checked and found conforming:** the manual frame-graph passes match the MBOIT orchestrator's
|
|
456
|
-
shape; `AVBOITSettings` on `Renderer.avboit` matches the other sub-object settings; the settings
|
|
457
|
-
structs defined beside their shaders match `shader_read_out_bucket`; the bit packing uses WGSL's
|
|
458
|
-
`insertBits`/`extractBits` directly, which is what the engine's `bitfield_*` chunks are thin proxies
|
|
459
|
-
for; no `safe_*` math helper was reinvented — the one floor (`max(T, 1e-6)` before a log) has no
|
|
460
|
-
counterpart in `shader/chunk/math/`.
|
|
461
|
-
|
|
462
|
-
**Verified.** Every spec in `rasterize/` (the avboit directory's now-larger set, MBOIT's, the shared
|
|
463
|
-
bucket code) and the type check.
|
|
464
|
-
|
|
465
|
-
## 19. Side channels, and particles as the first one — 2026-09-04
|
|
466
|
-
|
|
467
|
-
Alex: *"let's widen out AVBOIT so we can put particle render in there, that's where particles should
|
|
468
|
-
go. Make sure the extension is clean. No need to bother with MBOIT, it's going to be deprecated...
|
|
469
|
-
I don't think packing particle quads into meshlets is the way to go, so likely a side channel."*
|
|
470
|
-
|
|
471
|
-
**The seam.** `AVBOITSideChannel.js` is a JSDoc contract, no base class: three methods, called by
|
|
472
|
-
the orchestrator at the three points where the meshlet buckets have just contributed theirs —
|
|
473
|
-
`avboit_occupancy` before the warp is built, `avboit_splat` before integrate, `avboit_draw` before
|
|
474
|
-
resolve. Each takes the handles the orchestrator holds and returns the handles the next reader must
|
|
475
|
-
take, which is the frame graph's own threading rule and nothing new. The orchestrator gained one
|
|
476
|
-
parameter, `side_channels = []`, and one changed condition: it used to return early when no
|
|
477
|
-
transparent material had meshlets, and now returns early only if there are also no channels.
|
|
478
|
-
Integrate and resolve were not touched — they cannot tell a channel's fragment from a meshlet's,
|
|
479
|
-
which is the property that makes this a seam rather than a special case.
|
|
480
|
-
|
|
481
|
-
**The fourth accumulator.** An additive fragment covers nothing: it writes zero extinction and zero
|
|
482
|
-
norm, so the resolve's renormalization — `(1 - T_total) / Σnorm`, which redistributes exactly the
|
|
483
|
-
light the covering surfaces took from the scene — must not scale it. It cannot share the `color`
|
|
484
|
-
accumulator for that reason. So `emission` joins as a fourth target, added to the resolve outside
|
|
485
|
-
the normalization, holding light the draw already attenuated by the transmittance in front of it.
|
|
486
|
-
The cost is honest and paid whether or not a channel is present: one more `rgba16float` at internal
|
|
487
|
-
resolution, and every transparent mesh fragment writing `vec4(0)` to it. Buying it back would mean a
|
|
488
|
-
three-target draw pipeline beside the four-target one — a variant, which the house rule and the
|
|
489
|
-
plan's §5 both refuse — or a post-resolve additive pass with its own pipeline and bind group. One
|
|
490
|
-
target is the cheaper of the three.
|
|
491
|
-
|
|
492
|
-
**Shared, not copied.** The splat's voxel write (the two-slice linear split, the overflow mark) came
|
|
493
|
-
out of the meshlet splat into `chunk_avboit_splat_slice`, so the particle splat writes the volume
|
|
494
|
-
through the same code, not a second implementation of it. On the particle side the billboard vertex
|
|
495
|
-
stage came out of the standalone renderer into `chunk_particle_billboard_vertex`, shared by three
|
|
496
|
-
pipelines, and the emitter's world bounds sphere into `chunk_particle_emitter_world_sphere`, shared
|
|
497
|
-
by the tick's frustum cull and the new occupancy.
|
|
498
|
-
|
|
499
|
-
**What the review found.** Four things, all fixed here:
|
|
500
|
-
|
|
501
|
-
- *A binding that lied.* The shared vertex chunk read its slot list from a binding named `sorted`.
|
|
502
|
-
The standalone path binds the depth-sorted list there; the AVBOIT passes bind the alive list,
|
|
503
|
-
because the volume makes order irrelevant and the sort does not run on that path. Renamed
|
|
504
|
-
`draw_list` in the chunk and all three pipelines: which list it is, is the caller's choice.
|
|
505
|
-
- *Bounds with new teeth.* The occupancy is per emitter, from the bounds sphere — the field that
|
|
506
|
-
until now only fed the optional simulation culling and that an emitter not opting in could leave
|
|
507
|
-
at its default. A particle outside it still draws, but its extinction lands in whatever physical
|
|
508
|
-
slice the warp put nearest, so it occludes at slightly the wrong depth. Documented on the field
|
|
509
|
-
and in the particle design, because it is now a rendering input.
|
|
510
|
-
- *One simulation per view.* `simulate` steps the system and hands out handles into the graph it
|
|
511
|
-
recorded into, so two views of one scene would step it twice. The renderer has one view per
|
|
512
|
-
`render` call, so it holds today; the fix when that changes is to separate the step from the
|
|
513
|
-
import. Documented on the method rather than built speculatively.
|
|
514
|
-
- *A teardown that leaked.* `Renderer.destroy` did not destroy the particle systems it creates per
|
|
515
|
-
scene context. It does now.
|
|
516
|
-
|
|
517
|
-
**Checked and found sound.** The volume and the accumulators are always created by the meshlet
|
|
518
|
-
passes before a channel is asked, even at zero materials, so a particles-only frame has them; the
|
|
519
|
-
splat's dummy attachment is cleared by the meshlet splat before the particle splat loads it; passes
|
|
520
|
-
that write imported buffers are marked as side effects by `FramePassBuilder`, so the simulation
|
|
521
|
-
still advances in a frame whose transparency pass is off; a fresh `FrameGraph` per frame makes the
|
|
522
|
-
"simulated into this graph" assertion catch a stale handle set loudly; and the depth texture is
|
|
523
|
-
bound as a read-only attachment and a sampled texture in the same pass, which WebGPU allows and the
|
|
524
|
-
soft fade needs.
|
|
525
|
-
|
|
526
|
-
**Under MBOIT.** Particles simulate and do not draw. MBOIT is the legacy path and gets no new
|
|
527
|
-
sources; `Renderer.feature_particles_enabled` says so where a reader will hit it.
|
|
528
|
-
|
|
529
|
-
**Verified.** Tiers 1–3: the WGSL validity spec (the three new pipelines parse, validate, and stay
|
|
530
|
-
inside the eight-storage-buffer budget), the occupancy pass on the emulator against the depth
|
|
531
|
-
curve's own arithmetic, the three graph passes on the software device (pass order, attachments,
|
|
532
|
-
indirect draws, the read-only depth), the orchestrator's call order and the renderer's wiring, and
|
|
533
|
-
the whole engine suite — 2321 files, 16119 tests — plus the type check. Not verified: any of it on a
|
|
534
|
-
device. The Browser pane handed out no WebGPU adapter this session, so even the real Tint compile
|
|
535
|
-
that ledger 18's review leaned on is still owed for these three shaders.
|
|
536
|
-
|
|
537
|
-
## 20. Adversarial review of the side channel — 2026-09-04
|
|
538
|
-
|
|
539
|
-
Alex ran the playground, said it works, and asked for an adversarial review. A picture that looks
|
|
540
|
-
right is weak evidence for this pipeline: every defect below survives one, because each is either
|
|
541
|
-
a second copy of something small, a disagreement between two passes about the same fragment, or a
|
|
542
|
-
path the scene on screen does not take.
|
|
543
|
-
|
|
544
|
-
**What the volume and the accumulators disagreed about.** The pipeline's whole contract is that the
|
|
545
|
-
extinction the splat records is the coverage the draw weights its accumulators by
|
|
546
|
-
(`avboit_draw_fragment_body`'s own header says so). Two fragments broke it:
|
|
547
|
-
|
|
548
|
-
- *The soft fade.* The draw faded a particle's alpha against the scene depth; the splat had no depth
|
|
549
|
-
binding at all and recorded the unfaded coverage. A smoke card fading out where it meets the floor
|
|
550
|
-
drew nothing there and still wrote full extinction, so the floor was darkened through a card that
|
|
551
|
-
was invisible — the seam soft particles exist to remove, one layer down. The splat now samples the
|
|
552
|
-
same depth (one texel per voxel) and applies the same fade, through
|
|
553
|
-
`chunk_particle_scene_view_depth`, which the draw took its inline copy of the linearization from.
|
|
554
|
-
`avboit_splat` therefore takes `depth_texture` — the one contract change.
|
|
555
|
-
- *Additive and the fog.* The draw composited volumetric in-scatter into every fragment and then put
|
|
556
|
-
additive ones in `emission`, which the resolve adds *outside* the normalization. The scene behind
|
|
557
|
-
already carries that in-scatter, and nothing subtracts it: a hundred overlapping sparks added a
|
|
558
|
-
hundred copies of the fog. An emitting fragment is now dimmed by the fog's transmittance and
|
|
559
|
-
carries none of its light; a covering one still re-adds it, weighted by the coverage it took,
|
|
560
|
-
because it hides the part of the scene it owes it to.
|
|
561
|
-
|
|
562
|
-
**Two more in the same fragment.** `PREMULTIPLIED` with zero alpha is the engine's other spelling of
|
|
563
|
-
additive — the billboard pipeline blends it that way — and the draw branched on the blend enum, so
|
|
564
|
-
such a sprite was treated as covering, written to `color` with zero `norm`, and multiplied by a
|
|
565
|
-
normalization of zero: it disappeared. The routing is on coverage now, which is what the splat
|
|
566
|
-
already used, so the two agree by construction. And the coverage ceiling was applied to `norm` and
|
|
567
|
-
`extinction` but not to `color`, unbounded above (a curve can drive alpha past 1) and unguarded
|
|
568
|
-
below (a negative alpha made a negative extinction, and `exp(-sum)` then brightened the scene).
|
|
569
|
-
|
|
570
|
-
All four are pinned on the emulator: the fragment's decision came out into
|
|
571
|
-
`chunk_particle_avboit_contribution` as its own no-behaviour-change step, then nine tests, four of
|
|
572
|
-
them red against the old code.
|
|
573
|
-
|
|
574
|
-
**Teardown.** `destroy()` freed the buffers and left every emitter node holding a row of the table it
|
|
575
|
-
had just freed. A renderer rebuilt after a device loss makes a fresh system whose membership sweep
|
|
576
|
-
reads `row !== -1` as "already mine", takes the moved branch rather than the arrived one, and never
|
|
577
|
-
registers the emitter: it stops emitting for good. It retires them now. The default 1×1 atlas was
|
|
578
|
-
also unreachable — only its view was kept — and is held and freed.
|
|
579
|
-
|
|
580
|
-
**The orchestrator can be run at tier 3 after all.** This file said it could not: it needs a view
|
|
581
|
-
context over meshlet geometry no spec builds. That is true of a frame *with transparent meshes* —
|
|
582
|
-
and the path side channels added is the other one. A scene with no transparent materials and one
|
|
583
|
-
channel reaches for no geometry, so the whole orchestrator now runs on the mock, and the ordering
|
|
584
|
-
contract and the handle threading are pinned from the outside instead of by reading the source for
|
|
585
|
-
call order. The mock gained `indirect-first-instance` for it, on the argument its own header already
|
|
586
|
-
makes for `subgroups`: it permits a field in an indirect draw the mock never reads, issuing a draw it
|
|
587
|
-
never runs, and refusing it left the transparency orchestration untestable for a reason unrelated to
|
|
588
|
-
what the tier asserts.
|
|
589
|
-
|
|
590
|
-
**Found and left alone, on purpose:**
|
|
591
|
-
|
|
592
|
-
- *The occupancy marks every emitter*, with no frustum test and no liveness test, where the meshlet
|
|
593
|
-
occupancy dispatches over the HZB-filtered collection. An idle or off-screen emitter still reserves
|
|
594
|
-
its depth run, and the warp then fits more occupied virtual slices into the same 128 physical ones.
|
|
595
|
-
It costs precision, never correctness. A frustum test is the obvious fix and belongs with a scene
|
|
596
|
-
that shows the cost.
|
|
597
|
-
- *The occupancy sphere bounds particle centres*, not the quads' own depth extent, which for a
|
|
598
|
-
stretched or world-oriented particle reaches a size (or more) past the centre. Documented on
|
|
599
|
-
`ParticleEmitter.bounds_radius` rather than inflated by a guess.
|
|
600
|
-
- *Particles have no motion vectors.* The transparency composite happens before TAA, so a fast spark
|
|
601
|
-
is reprojected with the background's motion and ghosts. Transparent meshes have had this property
|
|
602
|
-
since before AVBOIT; particles make it easier to see. The playground has a TAA switch.
|
|
603
|
-
- *One system per scene context is never released*, mirroring `SceneManager`'s own map. A per-scene
|
|
604
|
-
release path is a design addition, not a fix.
|
|
605
|
-
- *`simulate` steps once per view*, so two views of one scene would step it twice. Documented on the
|
|
606
|
-
method; the renderer has one view per `render` call today.
|
|
607
|
-
|
|
608
|
-
**Verified.** The whole engine suite and the type check at budget. The playground page was loaded
|
|
609
|
-
from the dev server again — import graph, WASM meshlet build, scene, camera controller under a
|
|
610
|
-
synthetic drag, and every control handler including the two error paths added here. Still no adapter
|
|
611
|
-
in the Browser pane, so still no picture and still no real Tint compile.
|
|
612
|
-
|
|
613
|
-
## 21. What the device caught and the review did not — 2026-09-04
|
|
614
|
-
|
|
615
|
-
Alex ran entry 20's work and got:
|
|
616
|
-
|
|
617
|
-
```
|
|
618
|
-
Binding entry textureView not set.
|
|
619
|
-
- While validating entries[6] against { binding: 6, ..., texture: {sampleType: UnfilterableFloat, ...} }
|
|
620
|
-
- While encoding [RenderPassEncoder "Particle AVBOIT Splat"].SetBindGroup(0, ...)
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
Entry 20 added `scene_depth` to the splat's bindings between the atlas and its sampler, and added
|
|
624
|
-
the depth view to the *end* of the positional array the pass hands to `BindGroupDescriptor.from`.
|
|
625
|
-
Everything after the insertion point slid along by one: binding 6 wanted a texture and got the
|
|
626
|
-
sampler. Nothing on this side of the device could see it. The frame graph does not know what a
|
|
627
|
-
binding means, the mock does not validate an entry against its layout, and both of entry 20's new
|
|
628
|
-
tests looked for the depth view's *presence* in the group rather than its position.
|
|
629
|
-
|
|
630
|
-
The fix is not the index. Both particle render passes now build their bindings by name, through
|
|
631
|
-
`ShaderResourceSetDescriptor#generateBindings` — the path the standalone billboard pass already
|
|
632
|
-
used, and the reason it never had this bug — so the order a shader declares its bindings in is the
|
|
633
|
-
shader's own business and nothing here mirrors it. `constructRenderPass` sets the pipeline and every
|
|
634
|
-
group from that, which also took the manual `setBindGroup` calls out.
|
|
635
|
-
|
|
636
|
-
And the tier that missed it can see it now: `expect_bindings_match_layout` walks a recorded pass and
|
|
637
|
-
asserts every entry is the *kind* — buffer, texture or sampler — its pipeline layout declares at that
|
|
638
|
-
binding. It fails against the shipped code. That check is worth copying to any pass built by hand.
|
|
639
|
-
|
|
640
|
-
**The lesson for this directory:** a positional bind-entry list is a latent version of this defect in
|
|
641
|
-
every pass that has one, and it surfaces only on hardware, only when that pass first encodes. The
|
|
642
|
-
meshlet passes here still build theirs positionally.
|
|
643
|
-
|
|
644
|
-
## 22. Trails as the second side channel — 2026-09-24
|
|
645
|
-
|
|
646
|
-
Alex asked whether `Trail3D` went through AVBOIT (it did not: `Trail3DSystem` drew its tubes with
|
|
647
|
-
`GPUDynamicMeshRenderer` from an `AfterTransparency` extension, straight alpha over the resolved
|
|
648
|
-
frame, depth-tested against the opaque scene only — so a trail behind a transparent card was drawn
|
|
649
|
-
on top of it), then: *"implement this please. Keep within the parameters - AVBOIT for Trails only,
|
|
650
|
-
and refactoring necessary to make it happen."* MBOIT is being deprecated and was out of scope.
|
|
651
|
-
|
|
652
|
-
**The door.** Side channels could only come from inside the renderer: `#particle_side_channels`
|
|
653
|
-
held the GPU particle system and nothing else, and `RENDER_EXTENSION_DESIGN.md` §9 names "extensions
|
|
654
|
-
cannot participate in OIT" as the phase model's ceiling. `Renderer.add_transparency_channel` /
|
|
655
|
-
`remove_transparency_channel` / `transparency_channel_count` open it, and `GraphicsEngine` holds
|
|
656
|
-
channels across a restart exactly as it holds extensions. The frame's list (now `#side_channels`) is
|
|
657
|
-
the particles, then every registered channel that does not say it is empty — `avboit_is_empty`, an
|
|
658
|
-
optional fourth member of the contract — because the orchestrator runs whenever it is handed a
|
|
659
|
-
channel, and one that is always registered would otherwise buy every frame the pipeline's fixed
|
|
660
|
-
cost. Registering one channel twice is refused: it would draw everything twice.
|
|
661
|
-
|
|
662
|
-
**The channel.** `DynamicMeshAVBOITChannel` meets the three contracts for a `DynamicMeshBatch`:
|
|
663
|
-
|
|
664
|
-
- *Liveness is decided once, at occupancy*: a mesh is live when visible, with positive flat alpha,
|
|
665
|
-
and with a triangle one vertex of which has positive alpha. The splat and the draw take the same
|
|
666
|
-
list — and refuse a graph the occupancy was not recorded into — so the occupancy, the volume and
|
|
667
|
-
the accumulators agree about which meshes exist.
|
|
668
|
-
- *Occupancy from CPU boxes.* The vertices are CPU data already, so a box per live mesh is taken
|
|
669
|
-
there (`dynamic_mesh_live_bounds`), carried to world space by the mesh's transform, uploaded as a
|
|
670
|
-
transient buffer and marked on the GPU through the particles' own chunks. **Per triangle, not
|
|
671
|
-
per vertex**: a trail's faint tail is a triangle whose far corners have aged to zero, and bounds
|
|
672
|
-
over the live vertices would stop a segment short; while an aged-out knot keeps its old position,
|
|
673
|
-
so bounds over every position would reserve depth for a stopped trail's whole history.
|
|
674
|
-
- *The upload happens in the occupancy pass*, the channel's first to execute; the splat and the draw
|
|
675
|
-
read the buffers later in the same frame, behind the warp the occupancy feeds.
|
|
676
|
-
- *Splat and draw* are the particle passes' shape with the dynamic-mesh vertex stage: coverage
|
|
677
|
-
through `avboit_coverage` in both, the draw routed through `avboit_premultiplied_contribution`,
|
|
678
|
-
fogged and weighted by the transmittance in front. Straight alpha with no coverage carries no
|
|
679
|
-
light, so the draw masks the emission target rather than blending zeros into it.
|
|
680
|
-
- Group 0 of both pipelines is `DYNAMIC_MESH_RESOURCES`' own group — the same object — so a mesh's
|
|
681
|
-
bindings are the ones `GPUDynamicMeshResidency.create_bindings` already builds; the pass groups are
|
|
682
|
-
built by name (entry 21).
|
|
683
|
-
|
|
684
|
-
**Shared, not copied.** Out of the particle channel into chunks, the particles switched onto them
|
|
685
|
-
with their specs unchanged: `avboit_coverage`, `avboit_premultiplied_contribution` (everything
|
|
686
|
-
`particle_avboit_contribution` did after its premultiply), `aabb3_view_depth_range` and
|
|
687
|
-
`avboit_occupancy_mark`. Out of `GPUDynamicMeshRenderer`: `GPUDynamicMeshResidencyTable` (buffers,
|
|
688
|
-
staging, the once-per-version upload) and `chunk_dynamic_mesh_vertex` (the vertex pull) — pinned by
|
|
689
|
-
a new execute-level spec of the renderer written against the old code first, since there was none.
|
|
690
|
-
Entry 21's layout-kind check is `recorded_binding_kind_mismatches` now, shared by both recorder
|
|
691
|
-
specs. **Left alone on purpose:** the meshlet occupancy and the meshlet splat keep their own copies
|
|
692
|
-
of the slice marking and the two-slice split — this change does not touch the core passes.
|
|
693
|
-
|
|
694
|
-
**Trails.** `Trail3DSystem` registers its batch's channel with the facade instead of an extension;
|
|
695
|
-
`TrailExtension` is gone (breaking, and its only use was internal). A trail is now occluded and
|
|
696
|
-
tinted by transparent surfaces and particles in front of it, occludes them behind it, and picks up
|
|
697
|
-
the volumetric fog at its depth — it was drawn unfogged over a fogged scene. Under MBOIT, or with
|
|
698
|
-
transparency off, trails are not drawn, as particles are not. Fog of war still covers them: it runs
|
|
699
|
-
at `AfterTransparency`, after the resolve they are now part of.
|
|
700
|
-
|
|
701
|
-
**Verified.** Tiers 1–3: emulator specs for the four chunks, the dynamic-mesh occupancy pass and the
|
|
702
|
-
live bounds; the WGSL validity tier for every new and changed shader; software-device specs for the
|
|
703
|
-
channel's three passes (order, attachments, per-draw binding kinds, liveness, refusal, residency),
|
|
704
|
-
for `GPUDynamicMeshRenderer`, for registration on the renderer and the facade, and **the whole
|
|
705
|
-
orchestrator run with a real channel and no transparent materials**; `Trail3DSystem`'s specs; the
|
|
706
|
-
type check at budget. Tier 4, on a Metal adapter in the Browser pane — which hands out an adapter
|
|
707
|
-
again: every new and changed module compiles and every pipeline builds with no validation error.
|
|
708
|
-
On the playground page here, a real `Trail3D` driven by `seed_trail_tube` / `advance_trail` /
|
|
709
|
-
`trail_tube_write_geometry` and registered with `renderer.add_transparency_channel`:
|
|
710
|
-
|
|
711
|
-
- laid in front of the card stack it is drawn over all four cards; laid behind it, it is hidden by
|
|
712
|
-
them at alpha 0.997 (the old path drew it on top), dimmed card by card at 0.3, and faint at 0.85;
|
|
713
|
-
- behind the red pane with **tint transmitted light by base colour** on, it is filtered to the
|
|
714
|
-
desaturated tone the resolve predicts, `(0.19, 0.14, 0.14)`-ish against the pane's red, while its
|
|
715
|
-
head clear of the pane stays cyan;
|
|
716
|
-
- under MBOIT it is absent, and back under AVBOIT it returns.
|
|
717
|
-
|
|
718
|
-
On `../../playground/particle_ecs/` in `?debug` (frames driven by `__step`, the pane being hidden), a
|
|
719
|
-
real `Trail3DSystem` added to the running `EntityManager` and a `Trail3D` entity circling the fire: the
|
|
720
|
-
stretch behind the plume is dimmed and warmed by the smoke and flames in front of it, the stretch in
|
|
721
|
-
front hides the smoke and embers behind it; removing the system takes its channel out of the live
|
|
722
|
-
renderer and adding it back re-uploads from nothing. **Zero uncaptured device errors** across every
|
|
723
|
-
frame of both sessions. The bonfire's sprites need `build/bundle-worker-image-decoder.js`, which the
|
|
724
|
-
dev server does not build: `npx rollup -c rollup.config.js` first (the output is gitignored).
|
|
725
|
-
|
|
726
|
-
## 23. Adversarial review of the trail channel — 2026-09-25
|
|
727
|
-
|
|
728
|
-
Alex asked for an adversarial review, "especially on the blast radius". Three reviewers read
|
|
729
|
-
`master...feature/trail-avboit` independently — blast radius, GPU correctness, lifecycle and API —
|
|
730
|
-
and every finding below was checked against the code before it was acted on. None was critical or
|
|
731
|
-
high. Both reviewers who compared the shared chunks with master found them textually equivalent, the
|
|
732
|
-
dynamic-mesh upload moved without change, and the meshlet passes untouched; the GPU reviewer
|
|
733
|
-
confirmed the splat and the draw agree on coverage, depth and fragments, that emission is
|
|
734
|
-
unreachable from straight alpha, and that the fog is not counted twice.
|
|
735
|
-
|
|
736
|
-
**Fixed.**
|
|
737
|
-
|
|
738
|
-
- *Buffers from the old device.* An `Engine` with an external `EntityManager` stops `GraphicsEngine`
|
|
739
|
-
without shutting its systems down (`Engine.stop`), and a restart then replays the channel into a
|
|
740
|
-
renderer on a new device while its residency holds buffers made on the old one — written with
|
|
741
|
-
`queue.writeBuffer` and bound into bind groups on the new device, a validation error that loses the
|
|
742
|
-
frame's commands. Not new: `GPUDynamicMeshRenderer`'s map did the same for path display and debug
|
|
743
|
-
draw, and `TrailExtension` was replayed the same way. But entry 22 advertised channels as held across
|
|
744
|
-
a restart, so `GPUDynamicMeshResidencyTable` now drops everything when the device changes — the
|
|
745
|
-
pattern `GPUParticleEmitterSystem` already has for its atlas — which fixes all three drawers.
|
|
746
|
-
- *The last frame kept alive.* The channel held `#frame_graph` — the whole frame graph, every pass
|
|
747
|
-
closure in it, the imported canvas — and its live meshes until it was next asked, which for a
|
|
748
|
-
channel whose last trail had faded, or whose renderer had been replaced, is never; `Renderer.js`
|
|
749
|
-
calls exactly that retention a bug in its profiler path. The decision is now a per-frame record,
|
|
750
|
-
dropped when the draw is recorded; the passes carry their own references to the list and the boxes.
|
|
751
|
-
Each frame's list and boxes being its own also closes the aliasing the GPU reviewer called fragile
|
|
752
|
-
(record A, record B, execute A drew B's meshes into A): a spec now records two frames before
|
|
753
|
-
executing either, and fails against the old code.
|
|
754
|
-
- *Consulted when nothing would draw it.* `avboit_is_empty` ran every frame under MBOIT and with
|
|
755
|
-
transparency off; registered channels are now consulted only when the pipeline that takes them will
|
|
756
|
-
run.
|
|
757
|
-
- *An assert that shipped builds strip.* Registering a channel twice was refused by `assert` — gone
|
|
758
|
-
in a shipped build, where the channel would then be drawn twice — and the facade recorded a
|
|
759
|
-
registration the renderer had refused. Both are idempotent now; a repeat is a no-op.
|
|
760
|
-
- *The optional method unchecked.* A non-function `avboit_is_empty` passed registration and threw in
|
|
761
|
-
the middle of every frame; it is refused at the door.
|
|
762
|
-
- *`TrailExtension` removed outright.* meep ships ES modules, and an import that stops linking stops
|
|
763
|
-
the application loading, not just its trails — the reason `Trail3DSystem3` is kept. It is a
|
|
764
|
-
deprecated extension again, recording nothing and warning once.
|
|
765
|
-
- *No frustum test in the occupancy.* A live trail behind the camera or off to the side reserved its
|
|
766
|
-
whole depth run, and the warp fitted the on-screen surfaces into what was left. The dynamic-mesh
|
|
767
|
-
occupancy now tests each box against `camera.frustum` first. On the device, the real camera's planes
|
|
768
|
-
keep a visible trail's box and cull one behind the camera. The particles' occupancy still has no
|
|
769
|
-
such test (entry 20's open item); this change does not touch it.
|
|
770
|
-
- *Nothing pinned "the fog covers trails".* It used to be a declaration in
|
|
771
|
-
`after_transparency_order.spec.js`; it is construction now — the transparency call site precedes the
|
|
772
|
-
`AfterTransparency` phase — and a source pin says so.
|
|
773
|
-
|
|
774
|
-
**Disclosed, not changed** — each is in the CHANGELOG entry too:
|
|
775
|
-
|
|
776
|
-
- *Draw order at `AfterTransparency`.* Trails were the last content extension there; they are
|
|
777
|
-
resolved before that phase now, so path display, the selection highlight and the deprecated CPU
|
|
778
|
-
`ParticleEmitterSystem` draw over a trail whatever its depth, where they drew under it. Neither
|
|
779
|
-
order was depth-correct; the GPU particles are the ones that now sort against trails.
|
|
780
|
-
- *The fixed cost.* A frame with a live trail runs the transparency pipeline even with nothing else
|
|
781
|
-
transparent — occupancy, warp, integration, the accumulator clears, the resolve — where it had one
|
|
782
|
-
blended pass. Of the order of entry 17's 0.3 ms at 720p on light content.
|
|
783
|
-
- *Shared precision.* A trail spanning a deep range marks a long run and the warp halves its virtual
|
|
784
|
-
resolution to fit: the GPU reviewer's example, a box spanning 10–60 m at near 0.1 / far 200, takes
|
|
785
|
-
the warp from 4096 to 256 virtual slices, about 1.6 cm to 25 cm at 10 m. Emitter boxes do the same;
|
|
786
|
-
it is how the adaptive warp shares its 128 slices. Off-screen trails no longer take part.
|
|
787
|
-
- *Thin trails and the 1/8-resolution splat.* A tube narrower than about one voxel reaches the volume
|
|
788
|
-
only in the columns whose low-resolution pixel centres it covers. The GPU reviewer's numbers: a 0.1 m
|
|
789
|
-
trail at 20 m, 1080p, 60° is 0.58 voxel wide, and in a missed column a smoke card behind an α 0.8
|
|
790
|
-
trail comes out 3.5× too strong through it, crawling in 8-pixel blocks as the trail moves. Plan §9's
|
|
791
|
-
sub-pixel risk, which meshlets and small particles share. The suggested mitigation — widen the
|
|
792
|
-
splat's triangles by half a voxel — trades the misses for halos wherever the widened footprint falls
|
|
793
|
-
between two other layers, and is left for a decision rather than taken.
|
|
794
|
-
- *Renderer-wide channels.* A thumbnail rendered through the same renderer includes live trails that
|
|
795
|
-
fall in its view, as it did when they were an extension — and now brings the pipeline with them on
|
|
796
|
-
those frames. Particles are per scene; channels, like extensions, are not.
|
|
797
|
-
- *Work done twice.* The emptiness query and the gather both walk the first live mesh's triangles, and
|
|
798
|
-
each live mesh's group-0 bindings — a transient uniform — are built once for the splat and once for
|
|
799
|
-
the draw. Small at trail counts; noted, not shaved.
|
|
800
|
-
|
|
801
|
-
**Verified.** Before the fixes, the whole engine suite: the only failures were
|
|
802
|
-
`VGeoResidency.spec.js`, failing identically on master, and a `PromiseWatcher` timeout under load that
|
|
803
|
-
passes alone. After them, the suites the change reaches (217 files, 2386 tests) and the type check at
|
|
804
|
-
budget. On the device: every new and changed module compiles and its pipeline builds; on the bonfire
|
|
805
|
-
page, the trail through the plume again, with every fix in, and no uncaptured device errors.
|
|
806
|
-
|
|
807
|
-
## 24. Thin trails: a coverage-weighted splat — 2026-09-25
|
|
808
|
-
|
|
809
|
-
Alex said *"continue please"*, then *"Try again"*; the open item was entry 23's thin trail, disclosed and
|
|
810
|
-
left for a decision. It is taken here, for the trail channel only — the meshlet and particle splats are
|
|
811
|
-
unchanged.
|
|
812
|
-
|
|
813
|
-
**Confirmed on the device first.** On the playground page, a dimmed trail at α 0.8, 0.04 wide (about
|
|
814
|
-
3.7 pixels, 0.46 of a voxel), laid across the red card at eight offsets an eighth of a voxel apart —
|
|
815
|
-
one voxel's period — and read at the trail's centre. TAA off:
|
|
816
|
-
|
|
817
|
-
| offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|
|
818
|
-
|---|---|---|---|---|---|---|---|---|
|
|
819
|
-
| before | 107,144 | 107,144 | 77,166 | 76,167 | 73,168 | 72,169 | 111,140 | 111,140 |
|
|
820
|
-
| after | 95,155 | 93,156 | 90,158 | 88,160 | 85,162 | 85,162 | 95,154 | 100,151 |
|
|
821
|
-
|
|
822
|
-
(red, green; the card alone is 147,77; the same trail 0.3 wide, which covers whole voxels, is 72,169.)
|
|
823
|
-
Before, the trail either crossed a voxel centre and occluded the card fully or missed it and did not
|
|
824
|
-
occlude it at all: two colours and a jump of 39 between neighbouring offsets. With TAA on it was the
|
|
825
|
-
same split (70–111, a jump of 38), so the temporal filter does not hide it: it moves as the trail does.
|
|
826
|
-
|
|
827
|
-
**The options.**
|
|
828
|
-
|
|
829
|
-
- *Widen the splat's triangles by half a voxel* (entry 23's suggestion): every voxel the trail touches
|
|
830
|
-
gets its full extinction — halos wherever the widened footprint falls between two other layers.
|
|
831
|
-
- *Multisample and write the full extinction if any sample is covered*: the same halo, and worse — the
|
|
832
|
-
triangles of one ribbon sharing a voxel would each add a whole layer.
|
|
833
|
-
- *Multisample and weight by the covered share* — taken. Four samples, the only count above one WebGPU
|
|
834
|
-
guarantees; the fragment runs where any is covered and scales its extinction by
|
|
835
|
-
`countOneBits(sample_mask) / 4`. The triangles of one mesh cover disjoint samples, so their shares in
|
|
836
|
-
a voxel add to at most one layer, and a fully covered voxel is written exactly as before. The voxel
|
|
837
|
-
gets the trail's extinction averaged over its footprint, which is what a volume that coarse can hold.
|
|
838
|
-
- *More samples* — a raster at twice the volume's resolution, sixteen per voxel. A model of the lookup
|
|
839
|
-
(a 0.46-voxel band at α 0.8, transmittance behind the trail's centre over every position) says it is
|
|
840
|
-
not worth four times the fragments: one sample 0.20–1.00, four 0.45–0.78, sixteen 0.45–0.73, and an
|
|
841
|
-
exact area average 0.48–0.69. What is left at four is mostly the floor: a band thinner than a voxel,
|
|
842
|
-
box-averaged into voxels and read back through the draw's bilinear lookup, still sees more of itself
|
|
843
|
-
where it sits on a voxel centre than between two.
|
|
844
|
-
|
|
845
|
-
**What changed.** `shader_dynamic_mesh_avboit_splat` has `multisample: { count: 4 }` and the weight;
|
|
846
|
-
`graph_dynamic_mesh_avboit_splat` makes its own 4-sample `r8unorm` target at the volume's resolution
|
|
847
|
-
(never written, cleared and discarded — about 130 KB at 1080p, transient) and hands the volume's
|
|
848
|
-
single-sampled one back untouched, since a multisampled pipeline cannot draw into it; the channel passes
|
|
849
|
-
it the `voxel_resolution` the orchestrator already offered. The shared vertex chunk interpolates at the
|
|
850
|
-
**centroid**: a thin triangle covering samples but not the pixel centre would otherwise have its depth
|
|
851
|
-
and alpha extrapolated past its edge, which on a ribbon seen nearly edge-on moves the extinction off the
|
|
852
|
-
ribbon. The draw is single-sampled, where a fragment exists only if the centre is covered and the
|
|
853
|
-
centroid is the centre, so its output does not change.
|
|
854
|
-
|
|
855
|
-
**After, on the device.** The table's second row, and with TAA on 82–102 with a largest jump of 19. On a
|
|
856
|
-
curved thin trail — the case that beaded — the per-column red at the trail's centre over 225 columns:
|
|
857
|
-
|
|
858
|
-
| | stdev | range | mean | neighbours ≥ 10 apart |
|
|
859
|
-
|---|---|---|---|---|
|
|
860
|
-
| TAA off, before → after | 13.7 → 4.8 | 67–109 → 78–101 | 85.8 → 85.5 | 6 → 0 |
|
|
861
|
-
| TAA on, before → after | 12.7 → 5.0 | 58–108 → 71–98 | 79.8 → 81.5 | 20 → 5 |
|
|
862
|
-
|
|
863
|
-
Two and a half to three times steadier, and the same on average, as an area average of point samples
|
|
864
|
-
should be. The wide trail reads 69,170 against 72,169 before.
|
|
865
|
-
|
|
866
|
-
**What remains, and what it costs.**
|
|
867
|
-
|
|
868
|
-
- *The average's own bias.* A trail a fraction of a voxel wide occludes what is behind it by about that
|
|
869
|
-
fraction of its extinction, steadily. The trail itself still draws at full resolution and full
|
|
870
|
-
opacity; only its occlusion of what is behind it is diluted. That is the resolution the volume has.
|
|
871
|
-
- *Faint trails can round away.* The volume counts 255 steps per `ln 256` of extinction, so a voxel a
|
|
872
|
-
trail below about α 0.04 only grazes gets nothing, where before a centre it happened to cross got a
|
|
873
|
-
step. Occlusion of about 2 %.
|
|
874
|
-
- *More fragments.* Every voxel a thin trail crosses runs the fragment, not only those whose centres
|
|
875
|
-
it covers — two to three times as many along a thin trail, one more row along each edge of a wide
|
|
876
|
-
one, and never more than four per voxel per layer.
|
|
877
|
-
|
|
878
|
-
The emulator compiles WGSL to JavaScript and takes names as they are, so a fragment parameter named
|
|
879
|
-
`in` — a JavaScript keyword — failed to load; the splat's is `fragment`.
|
|
880
|
-
|
|
881
|
-
**Verified.** A new emulator spec runs the splat's fragment with each sample mask and checks the share it
|
|
882
|
-
writes, that four disjoint quarters add to one layer, and that a faded fragment writes nothing; with the
|
|
883
|
-
weight removed it fails five of its eight tests. The channel's recorder spec checks the target is the
|
|
884
|
-
pass's own, has the pipeline's sample count, is cleared and discarded, and that the volume's target
|
|
885
|
-
comes back untouched; with the old recorder it fails exactly those two. Every spec the change can
|
|
886
|
-
reach — the dynamic-mesh directory, the orchestrator run with a real channel, `Trail3DSystem`: 11 files,
|
|
887
|
-
93 tests — passes, and the type check is at budget. On the Metal adapter, `@builtin(sample_mask)` and
|
|
888
|
-
`@interpolate(perspective, centroid)` compile and the multisampled pipeline builds; no uncaptured device
|
|
889
|
-
errors, including 240 frames of a live trail on a moving path, fading in and out.
|
|
890
|
-
|
|
891
|
-
## 25. A trail's draw constants go up once a frame — 2026-09-27
|
|
892
|
-
|
|
893
|
-
Entry 23 left each live mesh's constants built twice — a transient uniform and a bind group of its own
|
|
894
|
-
for the splat, and again for the draw — noted, not shaved. They are shaved here, as part of moving
|
|
895
|
-
every dynamic mesh's per-draw constants into a table (CHANGELOG, Unreleased, Rendering): the
|
|
896
|
-
occupancy, after its upload, writes the frame's `GPUDynamicMeshDrawList` — one transient storage
|
|
897
|
-
buffer and one write per 1,024 draws — and the splat and the draw both bind it and name their rows by
|
|
898
|
-
`first_instance`. The splat's layout is four groups now (constants, geometry, warp, volume), which is
|
|
899
|
-
WebGPU's default `maxBindGroups`.
|
|
900
|
-
|
|
901
|
-
**Measured** with 1,500 live meshes through the channel at 1280×720 on the RTX 4090, asserts
|
|
902
|
-
stripped, paired: 9.8–11.4 ms of CPU a frame to 2.0–2.5; 3,032 copies a frame to 34 and 7,624
|
|
903
|
-
`setBindGroup` to 3,131. The pictures after equal frame counts are bit-identical to before.
|
|
904
|
-
|
|
905
|
-
**Verified.** `DynamicMeshAVBOITChannel.draws.spec.js` checks the splat and the draw read each mesh's own
|
|
906
|
-
constants from one table, and fails with a draw pass that uploads its own copy (two tables).
|
|
907
|
-
`chunk_dynamic_mesh_vertex.spec.js` runs the shared AVBOIT vertex stage on the emulator and checks a
|
|
908
|
-
vertex is carried by the row its draw names. The other half of the item — the emptiness query and the
|
|
909
|
-
gather both walking the first live mesh's triangles — is left as it was.
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
## 26. Review against the deck and the Godot proof of concept — 2026-10-04
|
|
913
|
-
|
|
914
|
-
Alex asked for the implementation to be checked against the deck and against
|
|
915
|
-
`github.com/IsaacMarovitz/godot-avoit` (*"AVBOIT works and is shipped, but I'm worried there might be
|
|
916
|
-
mistakes"*), then for an adversarial review of the proposal before it was carried out, and for the
|
|
917
|
-
particle off-centre read to be looked at again. The work is on a branch, not on master, for him to look at
|
|
918
|
-
first.
|
|
919
|
-
|
|
920
|
-
**Checked against the deck and found faithful.** The 10|10|10 packing with two guard bits, and the
|
|
921
|
-
per-channel first-overflow mark the integration pins from (slides 57–60); the exclusive-prefix integral at
|
|
922
|
-
`P + 1` slice boundaries; the continuous piecewise-linear warp, which gets what the deck's "filterable"
|
|
923
|
-
begin/end flags are for (slide 95) without them; the two-slice bias taken in virtual space; halving with a
|
|
924
|
-
fresh dilation per level; the resolve (slides 13–15). Two choices differ from the deck without being
|
|
925
|
-
defects: halving keeps the curve's shape and doubles every slice, where the deck re-adjusts its log curve so
|
|
926
|
-
that precision near the camera holds; and the per-fragment extinct discard stands in for the
|
|
927
|
-
zero-transmittance depth cull.
|
|
928
|
-
|
|
929
|
-
**The splat's rounding — measured, not a defect.** Meshes, particles and trails split a fragment's float
|
|
930
|
-
extinction between its two slices and round each share; the water rounded once and partitioned the counts.
|
|
931
|
-
Through the shipped chunks: the first makes a faint surface's count depend on where it sits in its slice
|
|
932
|
-
(α 3 %: one count or two) and is right on average over depth; the second is stable over depth and biased
|
|
933
|
-
(α 1.5 %: one count where 0.7 is exact). Either way an event below α ≈ 1.1 % registers nothing — the deck's
|
|
934
|
-
8-bit floor.
|
|
935
|
-
|
|
936
|
-
**Found and measured.** Items 1 and 3 are fixed, each red then green; item 2 is open.
|
|
937
|
-
|
|
938
|
-
1. *Tilted surfaces striped* — plan §9's sloped-surface risk, never measured until now. Every voxel column
|
|
939
|
-
holds a surface at the depth under its centre, and the draw reads the four nearest columns bilinearly; on a
|
|
940
|
-
surface tilted away from the camera the near-side neighbour holds the same surface nearer by its depth change
|
|
941
|
-
over up to a voxel, and past the two-slice bias the surface occluded itself by an amount that depended on
|
|
942
|
-
where the pixel sat in its voxel. A model of two overlapping α 0.5 cards at 1080p, through the shipped warp
|
|
943
|
-
build, curve, rounding and lookup, put it from about 30° (red −13 %, green +25 % at 45° with the front one
|
|
944
|
-
tilted). On the RTX 4090, the playground at 720p with a card tilted behind a camera-facing one: a sawtooth
|
|
945
|
-
locked to `x mod 8`, 52.2–55.5 sRGB codes in red at 45° and 50.5–55.6 at 60°, against a flat 52.3 under
|
|
946
|
-
MBOIT and a flat camera-facing control. Fixed by `avboit_footprint_bias`: the lookup goes a further
|
|
947
|
-
`8 × fwidth(v)` virtual slices toward the camera — the bilinear reach — capped at `2 V / P`, the base
|
|
948
|
-
bias of the coarsest slicing (§3.8). After, on the device: 52.3–52.4 at 45°, flat in the interior at 60°.
|
|
949
|
-
The cost is the band: a tilted fragment is not occluded by what lies within its own voxel's depth span in
|
|
950
|
-
front of it. Past the cap — about 85° at 1080p in a sparse volume — the stripes can return.
|
|
951
|
-
2. *The water pinned the warp at its floor.* Its occupancy marked every 8×8 tile's depth range, and a water
|
|
952
|
-
plane covers every depth from the shore to the horizon, so the warp halved to `V = P` whenever water was on
|
|
953
|
-
screen — AVBOIT became plain 128-slice VBOIT. Modelled with the shipped warp build: two α 0.5 cards at 10 m
|
|
954
|
-
separate at 0.25 m without water (V = 4096) and blend into one average at 0.25–0.5 m with a water plane
|
|
955
|
-
spanning 2–200 m in view, partly at 1 m. **Open.** A fix was built and taken out again: the water
|
|
956
|
-
moved out of the volume into a full-resolution *transmittance layer* that every other draw compared its
|
|
957
|
-
depth against. Alex: *"It's Anti-OIT."* It put a water special case into every transparent pipeline —
|
|
958
|
-
two bindings and a depth comparison per fragment in the mesh, particle and trail draws, a pass every
|
|
959
|
-
frame without water to bind something, and a contract every channel inherits — where order independence
|
|
960
|
-
means every source goes through the one volume. The water is in the volume again, exactly as before
|
|
961
|
-
this entry, and now takes the footprint like every surface that splats (its splat takes the nearest
|
|
962
|
-
depth over its 8×8 tile, so a voxel's width covers its own column; a neighbour's nearest point can lie
|
|
963
|
-
further). What bears on the pinning from inside the warp — hysteresis on halving and the deck's curve
|
|
964
|
-
re-adjustment — is measured in entry 27.
|
|
965
|
-
3. *The particle splat read the soft fade at the voxel's corner*, `8 × voxel`: the top-left pixel, and not
|
|
966
|
-
the voxel's own footprint at all when the volume does not divide the frame. On a floor the row four pixels
|
|
967
|
-
up is farther by a good part of the 0.5 m fade range, so a smoke card standing on the floor went into the
|
|
968
|
-
volume at up to 2.4× the coverage it was drawn with (0.94 against a voxel average of 0.39 at 10 m) — entry
|
|
969
|
-
20's seam, one layer down again. Fixed by `chunk_particle_avboit_voxel_fade`: four reads at WebGPU's
|
|
970
|
-
standard 4× multisample positions over the voxel's footprint, through the exact low-to-full mapping.
|
|
971
|
-
|
|
972
|
-
**The adversarial review of the proposal**, before any of it was built, changed it in four places:
|
|
973
|
-
|
|
974
|
-
- *The footprint's reach.* A throwaway version at half a voxel cut the device sawtooth to 0.5 and 1.5 codes;
|
|
975
|
-
the bilinear lookup reaches a full voxel, so the full voxel is what removes it, and the spec holds that to no
|
|
976
|
-
self-occlusion at all.
|
|
977
|
-
- *The footprint's cap.* Unbounded, it runs away at silhouettes and grazing angles — a sphere's rim would
|
|
978
|
-
ignore everything in front of it. It is capped at the coarsest slicing's base bias, the separation every
|
|
979
|
-
surface already has whenever the volume is full: the extra a fragment ignores is never more than that.
|
|
980
|
-
The whole reach, base bias included, is at most `2 + 2 V / P` virtual slices.
|
|
981
|
-
- *The fade's sampling.* A read at the centre under-records the floor contact (0.24 against 0.39 at 10 m).
|
|
982
|
-
Over fifteen modelled contacts a 2×2 grid was off the voxel average by 0.05 on average and 0.12 at worst;
|
|
983
|
-
the rotated grid by 0.05 and 0.08, because its four reads fall on four rows and four columns.
|
|
984
|
-
- *Withdrawn:* the meshlet splat takes glass's Fresnel from the geometric normal where the draw uses the
|
|
985
|
-
normal-mapped one. At the splat's coarse mip a high-frequency normal map averages back to the geometric
|
|
986
|
-
normal, and glass's Fresnel at near-normal incidence barely moves (about 0.04 across ±20°), so the two agree
|
|
987
|
-
well within what the volume resolves.
|
|
988
|
-
|
|
989
|
-
**Corrected.** `AVBOIT_EXTINCT_TRANSMITTANCE`'s docblock bounded the discard's error by `1/256` of the
|
|
990
|
-
background. That holds where the 1/8-resolution volume is right at the full-resolution pixel; behind a gap
|
|
991
|
-
narrower than a voxel in a near-opaque layer the volume calls the pixel extinct and the surface seen through
|
|
992
|
-
the gap is discarded, where the resolve's normalization would have drawn it. Constructed, not seen on a
|
|
993
|
-
scene; the deck's zero-transmittance cull has the same limit. **Out of date:** entry 20's "the occupancy
|
|
994
|
-
marks every emitter, with no frustum test" — the particles now mark the box their bounds pass measures from
|
|
995
|
-
the quads, and skip additive emitters and unknown bounds.
|
|
996
|
-
|
|
997
|
-
**The Godot proof of concept** is monochrome and composited over Godot's own transparents; every piece of it
|
|
998
|
-
ports to WGSL. It splats at full resolution weighted by the cell's pixel area (64× the atomics), stores 16.16
|
|
999
|
-
fixed point per slice (no 8-bit floor), reconstructs with a 4×4×4 cubic B-spline (whose wider XY footprint
|
|
1000
|
-
would stripe worse on slopes), takes occupancy from rasterized fragments, and keeps hysteresis on halving — it
|
|
1001
|
-
refines only once the occupied count drops below three quarters of the budget. That last is the one idea worth
|
|
1002
|
-
having here, where the virtual resolution is decided afresh every frame and a scene hovering at the budget
|
|
1003
|
-
could flip between two slicings; unmeasured.
|
|
1004
|
-
|
|
1005
|
-
**An independent review of the commits**, by a reviewer who had only the diffs, the plan and this
|
|
1006
|
-
ledger, found nothing high or medium; it checked `fwidth` before any branch or discard at every call site
|
|
1007
|
-
and the spec's splat against the shader's. Taken from it:
|
|
1008
|
-
|
|
1009
|
-
- *Additive particles took a footprint.* They write nothing into the volume, so nothing of theirs can
|
|
1010
|
-
occlude them; the derivative is still taken in uniform control flow and selected away.
|
|
1011
|
-
- *Wording.* Camera-facing and stretched billboards are built from the camera's right and up, so their
|
|
1012
|
-
footprint is zero (the draw's comment said they were tilted); the cap bounds the band, not the whole
|
|
1013
|
-
reach; the side-channel contract now names the footprint.
|
|
1014
|
-
- Its findings about the layer went with the layer.
|
|
1015
|
-
- *Left:* the fade's four reads land on pixel boundaries at a scale of 8 and take the pixel past each, half a
|
|
1016
|
-
pixel off the voxel's centre — inside what four reads resolve.
|
|
1017
|
-
|
|
1018
|
-
**Verified.** Each fix red, then green, as above. The suites the change reaches (130 files, 1,518 tests
|
|
1019
|
-
with the layer gone); the whole engine suite (2,897 files, 22,247 tests, none failing, before the layer
|
|
1020
|
-
came out); the type check (declaration emit clean, 18 consumer-visible errors against the budget of 19, as
|
|
1021
|
-
before). On the RTX 4090, from the scratchpad and not in the repo: the slope probe; every AVBOIT module
|
|
1022
|
-
through `createShaderModule` and a frame under Brick4; the water page with the water back in the volume
|
|
1023
|
-
and taking the footprint, the card below the surface filtered by it (red 65 → 2 against the same view
|
|
1024
|
-
dry); no WebGPU validation error on any page (the water page's "Worker error" is the image-decoder worker
|
|
1025
|
-
bundle the dev server does not build, entry 22). A dev server left running across the revert's
|
|
1026
|
-
`git checkout` served the old recorder beside the new shader and threw; restarted, clean.
|
|
1027
|
-
|
|
1028
|
-
## 27. Hysteresis and the curve re-adjustment, measured — 2026-10-04
|
|
1029
|
-
|
|
1030
|
-
Alex, after entry 26's water layer was taken out: *"Go ahead and try hysteresis and the curve
|
|
1031
|
-
re-adjustment, I want you to measure the results."* Both are in the warp build behind two switches on
|
|
1032
|
-
`Renderer.avboit`, off by default, so the shipped frame is unchanged until one is turned on.
|
|
1033
|
-
|
|
1034
|
-
**The deck's re-adjustment does nothing from a pure-log curve — measured, not built.** When the deck lowers
|
|
1035
|
-
its virtual slice count it re-parametrizes its log curve so slices near the camera keep their thickness.
|
|
1036
|
-
Searched over the free parameter of the engine's curve family (`build_light_cluster_parameters`:
|
|
1037
|
-
`slice = scale · log2(B z + O)`, pinned at the offset plane and the far plane), for each reference depth,
|
|
1038
|
-
the thinnest slice any member allows after halving 4096 → 2048 (near 0.1, far 200):
|
|
1039
|
-
|
|
1040
|
-
| at | the deck wants | uniform halving | best member |
|
|
1041
|
-
|---|---|---|---|
|
|
1042
|
-
| 0.5 m | 0.08 cm | 0.16 cm | 0.10 cm — and 1.37× thicker at 50 m |
|
|
1043
|
-
| 1 m | 0.16 cm | 0.32 cm | 0.30 cm |
|
|
1044
|
-
| 2 m | 0.32 cm | 0.64 cm | 0.64 cm (pure log) |
|
|
1045
|
-
| 5 m | 0.79 cm | 1.59 cm | 1.48 cm |
|
|
1046
|
-
|
|
1047
|
-
The same ratios hold for every halving. The deck's base curve is linearized and its re-adjustment moves it
|
|
1048
|
-
toward log; AVBOIT's curve is pure log at every level, the near-preserving end of the family already.
|
|
1049
|
-
|
|
1050
|
-
**What re-adjusting can recover is the octave's waste** (`AVBOITSettings.warp_steps_per_octave`). Halving
|
|
1051
|
-
lands the occupied count anywhere from about half the budget to all of it. With four steps per octave the
|
|
1052
|
-
build tries `2 L · 2^(-i/4)` between the octave that fits and the one above it, finest first, rewriting the
|
|
1053
|
-
finest bits into those slices conservatively, run by run; consumers take any virtual count unchanged.
|
|
1054
|
-
|
|
1055
|
-
- *Random scenes* (emulator, the shipped shader; 4–64 objects at 1–150 m log-uniform, extents 0.1–20 % of
|
|
1056
|
-
their depth; 120 scenes per count): where the warp halves, the virtual resolution reached is 1.28–1.43×
|
|
1057
|
-
finer on average (medians 1.19–1.41, at most 1.68), and the share of the 128 slices used goes from
|
|
1058
|
-
72–83 % to 93–95 %.
|
|
1059
|
-
- *The bonfire* (`playground/particle_ecs`, `?debug`, 400 stepped frames, RTX 4090, the virtual count read
|
|
1060
|
-
back every frame through a hook on the history buffer, no engine change): octaves sit at **128, the
|
|
1061
|
-
floor, every frame**; four steps at **215**, 1.68× finer.
|
|
1062
|
-
- *Cost* (the build dispatched alone on the 4090, warmed, configurations interleaved, medians of 320):
|
|
1063
|
-
12 runs of occupied slices 34 → 54 µs, 40 runs 22.5 → 53 µs, 600 fragmented runs 22.5 → 317 µs — the
|
|
1064
|
-
single-lane run walk, three times per frame at most.
|
|
1065
|
-
|
|
1066
|
-
**Hysteresis** (`AVBOITSettings.warp_hysteresis`, `AVBOIT_WARP_RELEASE` = 0.75, Godot's figure). The
|
|
1067
|
-
virtual resolution the warp settled on is kept per view in a one-word GPU buffer; a finer one is taken only
|
|
1068
|
-
when it fits in three quarters of the budget, coarsening is immediate.
|
|
1069
|
-
|
|
1070
|
-
- *Synthetic* (emulator, the shipped shader, 600 frames; a smoke column's extent jittering frame to frame at
|
|
1071
|
-
the extent where the level steps): without hysteresis the level flips about 290 times, every other frame,
|
|
1072
|
-
at 1, 3 and 10 % jitter alike; with it, none — the frames that flipped to the finer level are held at the
|
|
1073
|
-
coarser one instead (2× coarser with octaves, 1.19× with steps). A slow drift through the threshold:
|
|
1074
|
-
25 flips → 1 with octaves, 62 → 2 with steps.
|
|
1075
|
-
- *The release* (a replica of the decision, agreeing with the shader on 400 of 400 frames): it sets the
|
|
1076
|
-
jitter tolerated and the precision given up while a scene shrinks. With four steps:
|
|
1077
|
-
|
|
1078
|
-
| release | flips at 10 % jitter | at 25 % | budget used, shrinking scene |
|
|
1079
|
-
|---|---|---|---|
|
|
1080
|
-
| 1 (off) | 314 | 310 | 0.90 |
|
|
1081
|
-
| 0.95 | 104 | 259 | 0.87 |
|
|
1082
|
-
| 0.9 | 0 | 198 | 0.84 |
|
|
1083
|
-
| 0.75 | 0 | 34 | 0.75 |
|
|
1084
|
-
|
|
1085
|
-
With octaves the cost is larger, down to 0.59 of the budget at 0.75: refining needs the next level's count,
|
|
1086
|
-
twice this one's, to fit in three quarters.
|
|
1087
|
-
- *The bonfire*: with the camera still, **no flip in 400 frames** at any setting — real particle bounds did
|
|
1088
|
-
not jitter across a threshold here. Orbiting 90°: octaves flipped twice (128 ↔ 256), steps twice
|
|
1089
|
-
(215 ↔ 256), steps with hysteresis never — held at 215 for the 21 frames where 256 was available.
|
|
1090
|
-
- *Cost*: one load and one store in a single-invocation pass; nothing measurable.
|
|
1091
|
-
|
|
1092
|
-
**Found on the way: the warp build's serial LUT costs 136 µs whenever the volume is sparse.** With the
|
|
1093
|
-
occupancy fitting at 4096 virtual slices the build took 136 µs on the 4090, against 22.5 µs at 128: one
|
|
1094
|
-
invocation writing 4,097 LUT entries. Entry 15 raised `AVBOIT_VIRTUAL_SLICES_MAX` to 4096 counting "a longer
|
|
1095
|
-
single-lane loop in the warp build" among costs that do not register; it registers, against entry 17's
|
|
1096
|
-
290 µs for the whole of the transparency work on a page whose occupancy fits at 4096. A workgroup prefix
|
|
1097
|
-
sum would take it to a few microseconds, and the steps' run walk with it. Not done here.
|
|
1098
|
-
|
|
1099
|
-
**Verified.** Seven new emulator specs on the build: the first step down that fits, the fall-through to the
|
|
1100
|
-
octave, a step's slices covering every finest slice they overlap, hysteresis holding without room to spare
|
|
1101
|
-
and refining with it, coarsening at once, the history recorded when hysteresis is off. The suites the change
|
|
1102
|
-
reaches (132 files, 1,537 tests); the type check at 18 against the budget of 19. On the 4090 the build
|
|
1103
|
-
compiles and runs with both switches in the bonfire, no validation error.
|
|
1104
|
-
|
|
1105
|
-
**Open:** whether to turn either on — the steps are a measured 1.2–1.7× in over-budget scenes for 20–300 µs
|
|
1106
|
-
of single-lane work, hysteresis a guard against a flicker the bonfire did not show; the release, if
|
|
1107
|
-
hysteresis goes on (0.9 with steps by the table above); a parallel warp build, which would make the steps
|
|
1108
|
-
nearly free and take back the 136 µs.
|
|
1109
|
-
|
|
1110
|
-
## 28. The warp build, one workgroup — 2026-10-04
|
|
1111
|
-
|
|
1112
|
-
Alex: *"go ahead and parallelize the warp build please, go ahead and use subgroups too if it's relevant"*,
|
|
1113
|
-
with a snippet from the IOI Smolder talk — CuteSort, which ranks a lane by `WaveActiveBallot` per key bit
|
|
1114
|
-
and `countbits`.
|
|
1115
|
-
|
|
1116
|
-
**What it was.** One invocation — §3.4 had it that a 1 K-entry prefix sum is not a workload, and that a single
|
|
1117
|
-
lane keeps the shader runnable in the emulator. Entry 27 measured it at 22.5 µs halving to 128 virtual
|
|
1118
|
-
slices, 136 µs whenever the occupancy fits at 4096 (the 4,097-entry LUT written serially), and 317 µs with
|
|
1119
|
-
four steps over 600 fragmented runs (the run walk).
|
|
1120
|
-
|
|
1121
|
-
**What it is.** One workgroup of 128 invocations, one per occupancy word, each holding its finest word in a
|
|
1122
|
-
register for the whole build.
|
|
1123
|
-
|
|
1124
|
-
- *A resolution tried:* every invocation marks the virtual slices its word's runs touch
|
|
1125
|
-
(`chunk_avboit_warp_marks`) — finest slice `f` touches `floor(f V / V_max)` to `ceil((f + 1) V / V_max) - 1`,
|
|
1126
|
-
division-free because `V_max` is a power of two, and 32 finest slices touch at most two virtual words — and
|
|
1127
|
-
`atomicOr`s them into workgroup memory. Each invocation then dilates its own word of that resolution
|
|
1128
|
-
(`chunk_avboit_warp_level_word`), and a `subgroupAdd` per subgroup and one pass over the partials count
|
|
1129
|
-
them. Octaves are marked by the same rule, so halving is no longer a fold of the level above; where
|
|
1130
|
-
`V_max / V` is a power of two the overlap rule is the fold.
|
|
1131
|
-
- *Decisions* are made on that count read back through `workgroupUniformLoad`, which is what lets Tint accept
|
|
1132
|
-
loops with barriers and subgroup operations inside: the octave loop bounded at 13, the steps loop by the
|
|
1133
|
-
uniform setting.
|
|
1134
|
-
- *The LUT:* `subgroupExclusiveAdd` over the chosen words' counts plus the earlier subgroups' totals, then 32
|
|
1135
|
-
coalesced writes per invocation (`chunk_avboit_warp_lut_entry`).
|
|
1136
|
-
- The marks alternate between two halves of a workgroup array, each try zeroing the half the next will mark,
|
|
1137
|
-
so a try costs three barriers.
|
|
1138
|
-
|
|
1139
|
-
A first cut had each invocation compute its virtual word straight from the finest bits — 34 slices, two
|
|
1140
|
-
integer divisions each. Bit-exact, and 7 µs at 4096, but slower than the single lane wherever the warp halves
|
|
1141
|
-
(26.6 µs against 22.5 at 128): at 128 virtual slices four invocations had a word and 124 waited. Marking from
|
|
1142
|
-
the finest side keeps all 128 busy at every resolution.
|
|
1143
|
-
|
|
1144
|
-
**The CuteSort snippet.** Its rank — the popcount of a ballot masked to the lower lanes — is what a LUT entry
|
|
1145
|
-
already is: `countOneBits(word & ((1u << bit) - 1u))`. An occupancy word is that ballot, 32 slices to a
|
|
1146
|
-
word, so within a word no subgroup operation is needed and nothing anywhere needs sorting; subgroups carry the
|
|
1147
|
-
counts across words.
|
|
1148
|
-
|
|
1149
|
-
**Measured** (RTX 4090, headless Chrome; the build dispatched alone, warm, single-lane and parallel
|
|
1150
|
-
interleaved in alternating order, medians of 240):
|
|
1151
|
-
|
|
1152
|
-
| occupancy | V | single-lane | one workgroup |
|
|
1153
|
-
|---|---|---|---|
|
|
1154
|
-
| sparse, fits at 4096 (steps 1 or 4) | 4096 | 136.2 µs | 3.1 µs |
|
|
1155
|
-
| 12 runs, steps 1 | 512 | 33.8 µs | 3.1 µs |
|
|
1156
|
-
| 12 runs, steps 4 | 724 | 54.3 µs | 4.1 µs |
|
|
1157
|
-
| 40 runs (bonfire-like), steps 1 | 128 | 22.5 µs | 4.1 µs |
|
|
1158
|
-
| 40 runs, steps 4 | 128 | 53.3 µs | 5.1 µs |
|
|
1159
|
-
| 600 fragmented runs, steps 1 | 128 | 22.5 µs | 7.2 µs |
|
|
1160
|
-
| 600 fragmented runs, steps 4 | 128 | 317.4 µs | 9.2 µs |
|
|
1161
|
-
| occupied everywhere, steps 1 | 128 | 22.5 µs | 4.1 µs |
|
|
1162
|
-
| occupied everywhere, steps 4 | 128 | 34.8 µs | 5.1 µs |
|
|
1163
|
-
|
|
1164
|
-
A step tried now costs about a microsecond, so the cost half of entry 27's open question is gone.
|
|
1165
|
-
|
|
1166
|
-
**Verified.**
|
|
1167
|
-
|
|
1168
|
-
- *Bit-exact against the single-lane build on the 4090:* both shaders in one page on the same inputs, every
|
|
1169
|
-
word of the warp and the history word compared. 5,304 cases: 397 occupancy patterns (empty, full, single
|
|
1170
|
-
slices at word edges, runs sized around every octave's budget at seven offsets, trains of runs, 160 random
|
|
1171
|
-
run sets, random bits at seven densities) × physical 128 (96 and 200 on a third of them) × steps 1, 2, 4, 7
|
|
1172
|
-
× hysteresis off and on with assorted previous resolutions. Every level from 64 to 4096 was reached; no
|
|
1173
|
-
word differs. Tint compiles it with no message.
|
|
1174
|
-
- *Through the shipped pipeline* (the bonfire, entry 27's probe, 80 frames per setting): 128 virtual slices
|
|
1175
|
-
with octaves, 215 with steps, the same two flips orbiting and none with hysteresis; no validation error.
|
|
1176
|
-
- *Not exercised:* subgroup sizes other than the 4090's 32. The scan takes invocations to fill subgroups in
|
|
1177
|
-
index order, as `shader_prefix_scan_csdldf` does.
|
|
1178
|
-
- *Specs:* the emulator cannot run the workgroup, so — as with the bucket sorts (entry 22's sort, the
|
|
1179
|
-
rasterization buckets) — the shader's spec holds the assembled source to the parser and the validator, and
|
|
1180
|
-
the five new chunks carry their own: the marks against the overlap definition at octaves and between, runs
|
|
1181
|
-
split across words, a word straddling a virtual word; the dilation and the cut at `V`; the budget; the
|
|
1182
|
-
steps; the LUT entry. Flooring a run's last slice instead of ceiling it fails four of the marks specs. The
|
|
1183
|
-
14 emulator tests of the whole single-lane build went with it; their scenarios are among the device cases.
|
|
1184
|
-
The suites the change reaches: 160 files, 1,619 tests; the type check at 18 against 19.
|
|
1185
|
-
|
|
1186
|
-
`chunk_avboit_fold_pairs` has no consumer now; kept.
|
|
1187
|
-
|
|
1188
|
-
The water playground got a tickbox for the steps on the way (d915095b2). Its one view sits at 256 virtual
|
|
1189
|
-
slices in all six presets with or without them: the water's occupancy fits at 256 and at none of 431, 362
|
|
1190
|
-
or 304.
|
|
1191
|
-
|
|
1192
|
-
**Open:** entry 27's — whether to turn the steps or hysteresis on, the steps now at about a microsecond per
|
|
1193
|
-
resolution tried.
|
|
1194
|
-
|
|
1195
|
-
## 29. Four steps per octave by default; hysteresis removed — 2026-10-04
|
|
1196
|
-
|
|
1197
|
-
Alex, after the warp, its defaults and entries 27–28 were explained: *"make 4 default, remove hysteresis."*
|
|
1198
|
-
|
|
1199
|
-
**Steps.** `AVBOITSettings.warp_steps_per_octave` is 4. What it was measured to buy (entry 27): 1.28–1.43×
|
|
1200
|
-
finer virtual slices on average where the warp halves, 1.68× in the bonfire (128 → 215), and smaller level
|
|
1201
|
-
changes when the count crosses a threshold (215 ↔ 256 orbiting the bonfire, against 128 ↔ 256). What it
|
|
1202
|
-
costs since entry 28: about a microsecond per resolution tried, at most three per view per frame. Where
|
|
1203
|
-
halving already lands close to the budget no step fits and the warp is the octave's, as before. The water
|
|
1204
|
-
playground's tickbox starts ticked, mirroring the default.
|
|
1205
|
-
|
|
1206
|
-
**Hysteresis** is gone: `AVBOITSettings.warp_hysteresis`, `AVBOIT_WARP_RELEASE`, `chunk_avboit_warp_budget`,
|
|
1207
|
-
the warp build's `history` binding and the per-view buffer behind it (`graph_avboit_build_warp` no longer
|
|
1208
|
-
takes a `history_key`). It guarded against a flicker measured only in synthetic jitter — the bonfire with a
|
|
1209
|
-
still camera never changed level — and cost precision whenever a scene shrinks, holding the coarser
|
|
1210
|
-
slicing until a quarter of the budget was free.
|
|
1211
|
-
|
|
1212
|
-
**Verified.** The build without the history binding against the single-lane build of entry 27 with
|
|
1213
|
-
hysteresis off, on the 4090: entry 28's 397 occupancy patterns × physical counts × steps 1, 2, 4, 7, 2,652
|
|
1214
|
-
cases, every word of the warp identical; timing unchanged (3.1–9.2 µs). The suites the change reaches; the
|
|
1215
|
-
type check.
|
|
1216
|
-
|
|
1217
|
-
**Open:** none from this.
|
|
1218
|
-
|