@woosh/meep-engine 3.42.0 → 3.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/editor/prototypeEditorShell.d.ts.map +1 -1
- package/editor/prototypeEditorShell.js +4 -2
- package/package.json +91 -96
- package/src/engine/Clock.d.ts +15 -0
- package/src/engine/Clock.d.ts.map +1 -1
- package/src/engine/Clock.js +29 -5
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +11 -19
- package/src/engine/graphics/ecs/mesh-v2/MeshSkin.d.ts +57 -0
- package/src/engine/graphics/ecs/mesh-v2/MeshSkin.d.ts.map +1 -0
- package/src/engine/graphics/ecs/mesh-v2/MeshSkin.js +84 -0
- package/src/engine/graphics3/AnimationGraphSystem.d.ts +23 -13
- package/src/engine/graphics3/AnimationGraphSystem.d.ts.map +1 -1
- package/src/engine/graphics3/AnimationGraphSystem.js +61 -64
- package/src/engine/graphics3/AnimationSystem.d.ts +24 -15
- package/src/engine/graphics3/AnimationSystem.d.ts.map +1 -1
- package/src/engine/graphics3/AnimationSystem.js +67 -61
- package/src/engine/graphics3/MeshSkinSystem.d.ts +56 -0
- package/src/engine/graphics3/MeshSkinSystem.d.ts.map +1 -0
- package/src/engine/graphics3/MeshSkinSystem.js +317 -0
- package/src/engine/graphics3/MeshSystem.d.ts +15 -35
- package/src/engine/graphics3/MeshSystem.d.ts.map +1 -1
- package/src/engine/graphics3/MeshSystem.js +21 -298
- package/src/engine/graphics3/RowChains.d.ts +76 -0
- package/src/engine/graphics3/RowChains.d.ts.map +1 -0
- package/src/engine/graphics3/RowChains.js +347 -0
- package/src/engine/graphics3/ShadedGeometrySystem.d.ts +6 -0
- package/src/engine/graphics3/ShadedGeometrySystem.d.ts.map +1 -1
- package/src/engine/graphics3/ShadedGeometrySystem.js +329 -309
- package/src/engine/graphics3/animation/AnimationClips.d.ts +56 -0
- package/src/engine/graphics3/animation/AnimationClips.d.ts.map +1 -0
- package/src/engine/graphics3/animation/AnimationClips.js +79 -0
- package/src/engine/graphics3/animation/ClipListPlayer.d.ts +13 -14
- package/src/engine/graphics3/animation/ClipListPlayer.d.ts.map +1 -1
- package/src/engine/graphics3/animation/ClipListPlayer.js +262 -263
- package/src/engine/graphics3/animation/write_clip_playback.d.ts +3 -3
- package/src/engine/graphics3/animation/write_clip_playback.js +4 -4
- package/src/engine/graphics3/pose/query_entity_node_world_pose.d.ts +5 -0
- package/src/engine/graphics3/pose/query_entity_node_world_pose.d.ts.map +1 -1
- package/src/engine/graphics3/pose/query_entity_node_world_pose.js +80 -75
- package/src/engine/graphics3/prefab/PrefabInstance.d.ts +4 -24
- package/src/engine/graphics3/prefab/PrefabInstance.d.ts.map +1 -1
- package/src/engine/graphics3/prefab/PrefabInstance.js +60 -80
- package/src/engine/graphics3/prefab/prefab_instantiate.d.ts +16 -3
- package/src/engine/graphics3/prefab/prefab_instantiate.d.ts.map +1 -1
- package/src/engine/graphics3/prefab/prefab_instantiate.js +49 -12
- package/src/engine/graphics3/prefab/prefab_place.d.ts +31 -17
- package/src/engine/graphics3/prefab/prefab_place.d.ts.map +1 -1
- package/src/engine/graphics3/prefab/prefab_place.js +175 -130
- package/src/engine/graphics3/register_clips.d.ts +64 -0
- package/src/engine/graphics3/register_clips.d.ts.map +1 -0
- package/src/engine/graphics3/register_clips.js +114 -0
- package/src/engine/physics/cloth/MEASUREMENTS.md +795 -0
- package/src/engine/physics/cloth/PLAN.md +339 -9
- package/src/engine/physics/cloth/build/ClothProxy.d.ts +113 -61
- package/src/engine/physics/cloth/build/ClothProxy.d.ts.map +1 -1
- package/src/engine/physics/cloth/build/ClothProxy.js +142 -77
- package/src/engine/physics/cloth/build/cloth_bind_in_frame.d.ts +22 -0
- package/src/engine/physics/cloth/build/cloth_bind_in_frame.d.ts.map +1 -0
- package/src/engine/physics/cloth/build/cloth_bind_in_frame.js +75 -0
- package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts +43 -30
- package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts.map +1 -1
- package/src/engine/physics/cloth/build/cloth_proxy_from_joints.js +357 -284
- package/src/engine/physics/cloth/build/cloth_proxy_from_mesh.d.ts +62 -0
- package/src/engine/physics/cloth/build/cloth_proxy_from_mesh.d.ts.map +1 -0
- package/src/engine/physics/cloth/build/cloth_proxy_from_mesh.js +280 -0
- package/src/engine/physics/cloth/collider/cloth_collider_sdf.d.ts +1 -1
- package/src/engine/physics/cloth/collider/cloth_collider_sdf.js +1 -1
- package/src/engine/physics/cloth/ecs/Cloth.d.ts +17 -9
- package/src/engine/physics/cloth/ecs/Cloth.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/Cloth.js +220 -212
- package/src/engine/physics/cloth/ecs/ClothDynamicsFlags.js +6 -0
- package/src/engine/physics/cloth/ecs/ClothHold.d.ts +91 -0
- package/src/engine/physics/cloth/ecs/ClothHold.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/ClothHold.js +103 -0
- package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +254 -48
- package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothInstance.js +277 -47
- package/src/engine/physics/cloth/ecs/ClothPin.d.ts +73 -0
- package/src/engine/physics/cloth/ecs/ClothPin.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/ClothPin.js +92 -0
- package/src/engine/physics/cloth/ecs/ClothPinSerializationAdapter.d.ts +36 -0
- package/src/engine/physics/cloth/ecs/ClothPinSerializationAdapter.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/ClothPinSerializationAdapter.js +61 -0
- package/src/engine/physics/cloth/ecs/ClothRig.d.ts +23 -17
- package/src/engine/physics/cloth/ecs/ClothRig.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothRig.js +29 -27
- package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +12 -14
- package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothSystem.js +20 -15
- package/src/engine/physics/cloth/ecs/ClothWorld.d.ts +57 -36
- package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/ClothWorld.js +816 -179
- package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts +12 -13
- package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/WorkerClothSystem.js +20 -14
- package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts +63 -11
- package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_build_rows.js +321 -84
- package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts +9 -7
- package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_dynamics_map.js +9 -7
- package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts +3 -3
- package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +30 -30
- package/src/engine/physics/cloth/ecs/cloth_geodesic.d.ts +37 -0
- package/src/engine/physics/cloth/ecs/cloth_geodesic.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/cloth_geodesic.js +142 -0
- package/src/engine/physics/cloth/ecs/cloth_holds.d.ts +97 -0
- package/src/engine/physics/cloth/ecs/cloth_holds.d.ts.map +1 -0
- package/src/engine/physics/cloth/ecs/cloth_holds.js +608 -0
- package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.d.ts +2 -2
- package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.js +160 -9
- package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts +17 -22
- package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_seed_rig.js +55 -117
- package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts +21 -6
- package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +44 -15
- package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts +5 -0
- package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_write_back.js +60 -16
- package/src/engine/physics/cloth/ecs/cloth_write_back_rig.d.ts.map +1 -1
- package/src/engine/physics/cloth/ecs/cloth_write_back_rig.js +65 -58
- package/src/engine/physics/cloth/playground/collide_main.js +1 -1
- package/src/engine/physics/cloth/playground/garment_build.d.ts.map +1 -1
- package/src/engine/physics/cloth/playground/garment_build.js +17 -5
- package/src/engine/physics/cloth/playground/garment_main.js +1 -1
- package/src/engine/physics/cloth/playground/rig_build.d.ts.map +1 -1
- package/src/engine/physics/cloth/playground/rig_build.js +302 -296
- package/src/engine/physics/cloth/solver/ClothState.d.ts +45 -7
- package/src/engine/physics/cloth/solver/ClothState.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/ClothState.js +63 -7
- package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts +6 -1
- package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/cloth_contact_find.js +7 -2
- package/src/engine/physics/cloth/solver/cloth_hold_line.d.ts +114 -0
- package/src/engine/physics/cloth/solver/cloth_hold_line.d.ts.map +1 -0
- package/src/engine/physics/cloth/solver/cloth_hold_line.js +362 -0
- package/src/engine/physics/cloth/solver/cloth_self_find.d.ts +6 -2
- package/src/engine/physics/cloth/solver/cloth_self_find.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/cloth_self_find.js +13 -13
- package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts +24 -12
- package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/cloth_solver_constants.js +24 -12
- package/src/engine/physics/cloth/solver/cloth_step.d.ts +16 -0
- package/src/engine/physics/cloth/solver/cloth_step.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/cloth_step.js +183 -20
- package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts +1 -0
- package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/cloth_step_instance.js +10 -3
- package/src/engine/physics/cloth/solver/constraint/cloth_leash_row.d.ts +1 -1
- package/src/engine/physics/cloth/solver/constraint/cloth_leash_row.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/constraint/cloth_leash_row.js +353 -346
- package/src/engine/physics/cloth/solver/constraint/cloth_self_row.d.ts +7 -5
- package/src/engine/physics/cloth/solver/constraint/cloth_self_row.d.ts.map +1 -1
- package/src/engine/physics/cloth/solver/constraint/cloth_self_row.js +7 -5
- package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts +9 -9
- package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts.map +1 -1
- package/src/engine/physics/cloth/wind/cloth_sample_air.js +26 -26
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/simulation/Ticker.d.ts +13 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +43 -8
- package/src/shade/RENDERER_CONTRACT.md +8 -2
- package/src/shade/playground/mesh_row_ceiling/main.js +2 -1
- package/src/shade/playground/skinned_blas_refit/main.d.ts.map +1 -1
- package/src/shade/playground/skinned_blas_refit/main.js +11 -19
- package/src/shade/playground/skinned_blas_refit/place_character.d.ts +6 -3
- package/src/shade/playground/skinned_blas_refit/place_character.d.ts.map +1 -1
- package/src/shade/playground/skinned_blas_refit/place_character.js +90 -90
- package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -1
- package/src/shade/playground/skinned_mesh_soup/main.js +502 -509
- package/src/shade/playground/texture_encode/README.md +129 -109
- package/src/shade/playground/texture_encode/main.js +1430 -1430
- package/src/shade/playground/vgeo_runtime/README.md +5 -4
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +6 -5
- package/src/shade/renderer/animation/GPUAnimationManager.d.ts +45 -14
- package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
- package/src/shade/renderer/animation/GPUAnimationManager.js +316 -64
- package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.d.ts +5 -0
- package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.d.ts.map +1 -1
- package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.js +11 -1
- package/src/shade/renderer/atmosphere/GPUSky.d.ts +12 -0
- package/src/shade/renderer/atmosphere/GPUSky.d.ts.map +1 -1
- package/src/shade/renderer/atmosphere/GPUSky.js +162 -147
- package/src/shade/renderer/geometry/BoxGeometry.d.ts +1 -11
- package/src/shade/renderer/geometry/BoxGeometry.d.ts.map +1 -1
- package/src/shade/renderer/geometry/BoxGeometry.js +6 -13
- package/src/shade/renderer/geometry/CapsuleGeometry.d.ts +19 -0
- package/src/shade/renderer/geometry/CapsuleGeometry.d.ts.map +1 -0
- package/src/shade/renderer/geometry/CapsuleGeometry.js +24 -0
- package/src/shade/renderer/geometry/ConeGeometry.d.ts +20 -0
- package/src/shade/renderer/geometry/ConeGeometry.d.ts.map +1 -0
- package/src/shade/renderer/geometry/ConeGeometry.js +33 -0
- package/src/shade/renderer/geometry/CylinderGeometry.d.ts +21 -0
- package/src/shade/renderer/geometry/CylinderGeometry.d.ts.map +1 -0
- package/src/shade/renderer/geometry/CylinderGeometry.js +35 -0
- package/src/shade/renderer/geometry/PlaneGeometry.d.ts +17 -0
- package/src/shade/renderer/geometry/PlaneGeometry.d.ts.map +1 -0
- package/src/shade/renderer/geometry/PlaneGeometry.js +22 -0
- package/src/shade/renderer/geometry/TorusGeometry.d.ts +18 -0
- package/src/shade/renderer/geometry/TorusGeometry.d.ts.map +1 -0
- package/src/shade/renderer/geometry/TorusGeometry.js +23 -0
- package/src/shade/renderer/geometry/UVSphereGeometry.d.ts +21 -0
- package/src/shade/renderer/geometry/UVSphereGeometry.d.ts.map +1 -0
- package/src/shade/renderer/geometry/UVSphereGeometry.js +34 -0
- package/src/shade/renderer/geometry/primitives/make_capsule_geometry.d.ts +19 -0
- package/src/shade/renderer/geometry/primitives/make_capsule_geometry.d.ts.map +1 -0
- package/src/shade/renderer/geometry/primitives/make_capsule_geometry.js +205 -0
- package/src/shade/renderer/geometry/primitives/make_cone_geometry.d.ts +21 -0
- package/src/shade/renderer/geometry/primitives/make_cone_geometry.d.ts.map +1 -0
- package/src/shade/renderer/geometry/primitives/make_cone_geometry.js +36 -0
- package/src/shade/renderer/geometry/primitives/make_cylinder_geometry.d.ts +1 -1
- package/src/shade/renderer/geometry/primitives/make_cylinder_geometry.d.ts.map +1 -1
- package/src/shade/renderer/geometry/primitives/make_cylinder_geometry.js +44 -9
- package/src/shade/renderer/geometry/primitives/make_uv_sphere_geometry.d.ts +20 -0
- package/src/shade/renderer/geometry/primitives/make_uv_sphere_geometry.d.ts.map +1 -0
- package/src/shade/renderer/geometry/primitives/make_uv_sphere_geometry.js +190 -0
- package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.js +7 -2
- package/src/shade/renderer/gpu_primitive/prefix_sum/v1/graph_prefix_scan_csdldf.d.ts +26 -8
- package/src/shade/renderer/gpu_primitive/prefix_sum/v1/graph_prefix_scan_csdldf.d.ts.map +1 -1
- package/src/shade/renderer/gpu_primitive/prefix_sum/v1/graph_prefix_scan_csdldf.js +56 -39
- package/src/shade/renderer/gpu_primitive/prefix_sum/v1/shader_prefix_scan_csdldf.d.ts +7 -0
- package/src/shade/renderer/gpu_primitive/prefix_sum/v1/shader_prefix_scan_csdldf.d.ts.map +1 -1
- package/src/shade/renderer/gpu_primitive/prefix_sum/v1/shader_prefix_scan_csdldf.js +46 -4
- package/src/shade/renderer/gpu_primitive/prefix_sum/v1/test_prefix_sum.d.ts.map +1 -1
- package/src/shade/renderer/gpu_primitive/prefix_sum/v1/test_prefix_sum.js +4 -2
- package/src/shade/renderer/gpu_primitive/sort/defs.d.ts +2 -2
- package/src/shade/renderer/gpu_primitive/sort/defs.js +79 -79
- package/src/shade/renderer/gpu_primitive/sort/graph_radix_sort.d.ts +1 -1
- package/src/shade/renderer/gpu_primitive/sort/graph_radix_sort.d.ts.map +1 -1
- package/src/shade/renderer/gpu_primitive/sort/graph_radix_sort.js +7 -3
- package/src/shade/renderer/lightmap/bake/shader_lightmap_denoise_sh.d.ts.map +1 -1
- package/src/shade/renderer/lightmap/bake/shader_lightmap_denoise_sh.js +10 -3
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/particles/DESIGN.md +13 -9
- package/src/shade/renderer/particles/GPUParticleSystem.js +1 -1
- package/src/shade/renderer/particles/coherence/graph_particle_bucket.d.ts +4 -3
- package/src/shade/renderer/particles/coherence/graph_particle_bucket.d.ts.map +1 -1
- package/src/shade/renderer/particles/coherence/graph_particle_bucket.js +200 -194
- package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph_particles.js +6 -2
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +1 -1
- package/src/shade/renderer/particles/warmup/graph_particle_warmup.d.ts.map +1 -1
- package/src/shade/renderer/particles/warmup/graph_particle_warmup.js +334 -329
- package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
- package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +7 -2
- package/src/shade/renderer/rasterize/expand/mesh/graph_expand_meshes_to_meshlets.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/mesh/graph_expand_meshes_to_meshlets.js +16 -4
- package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts +3 -2
- package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.js +3 -2
- package/src/shade/renderer/scene/GPUSceneContext.d.ts +9 -1
- package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
- package/src/shade/renderer/scene/GPUSceneContext.js +23 -1
- package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +2444 -2345
- package/src/shade/renderer/texture/encode/chunk_astc_encode_block_4x4_rgb.d.ts.map +1 -1
- package/src/shade/renderer/texture/encode/chunk_astc_encode_block_4x4_rgb.js +312 -310
- package/src/shade/renderer/texture/encode/chunk_bc1_encode_block.d.ts +3 -3
- package/src/shade/renderer/texture/encode/chunk_bc1_encode_block.d.ts.map +1 -1
- package/src/shade/renderer/texture/encode/chunk_bc1_encode_block.js +383 -375
- package/src/shade/renderer/texture/encode/chunk_bc6h_encode_block_mode11.d.ts.map +1 -1
- package/src/shade/renderer/texture/encode/chunk_bc6h_encode_block_mode11.js +349 -346
- package/src/shade/renderer/texture/encode/chunk_bc7_encode_block.d.ts +58 -0
- package/src/shade/renderer/texture/encode/chunk_bc7_encode_block.d.ts.map +1 -0
- package/src/shade/renderer/texture/encode/chunk_bc7_encode_block.js +536 -0
- package/src/shade/renderer/texture/encode/chunk_bc7_encode_block_mode6.d.ts +21 -7
- package/src/shade/renderer/texture/encode/chunk_bc7_encode_block_mode6.d.ts.map +1 -1
- package/src/shade/renderer/texture/encode/chunk_bc7_encode_block_mode6.js +291 -327
- package/src/shade/renderer/texture/encode/chunk_bc7_pack_mode1.d.ts +25 -0
- package/src/shade/renderer/texture/encode/chunk_bc7_pack_mode1.d.ts.map +1 -0
- package/src/shade/renderer/texture/encode/chunk_bc7_pack_mode1.js +99 -0
- package/src/shade/renderer/texture/encode/chunk_bc7_pack_mode6.d.ts +5 -3
- package/src/shade/renderer/texture/encode/chunk_bc7_pack_mode6.d.ts.map +1 -1
- package/src/shade/renderer/texture/encode/chunk_bc7_pack_mode6.js +47 -71
- package/src/shade/renderer/texture/encode/chunk_bc7_partition_2.d.ts +10 -0
- package/src/shade/renderer/texture/encode/chunk_bc7_partition_2.d.ts.map +1 -0
- package/src/shade/renderer/texture/encode/chunk_bc7_partition_2.js +50 -0
- package/src/shade/renderer/texture/encode/shader_texture_encode_bc7.d.ts +4 -5
- package/src/shade/renderer/texture/encode/shader_texture_encode_bc7.d.ts.map +1 -1
- package/src/shade/renderer/texture/encode/shader_texture_encode_bc7.js +7 -8
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +4 -1
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +74 -3
- package/src/shade/renderer/volumetrics/NOTES.md +1023 -957
- package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts +5 -4
- package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.js +5 -4
- package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.d.ts +1 -1
- package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.js +1 -1
- package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.d.ts +1 -1
- package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.js +1 -1
- package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.d.ts +41 -0
- package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/graph_build_froxel_irradiance.js +112 -0
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +9 -23
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +18 -126
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.d.ts +22 -0
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.d.ts.map +1 -0
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_irradiance.js +228 -0
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
- package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +0 -53
- package/src/shade/renderer/water/chunk_water_transport.d.ts.map +1 -1
- package/src/shade/renderer/water/chunk_water_transport.js +7 -5
- package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +1 -1
- package/src/shade/renderer/water/medium/GPUWaterBodies.js +8 -2
- package/src/shade/renderer/water/optics/chunk_water_receiver_downwelling.d.ts +3 -2
- package/src/shade/renderer/water/optics/chunk_water_receiver_downwelling.d.ts.map +1 -1
- package/src/shade/renderer/water/optics/chunk_water_receiver_downwelling.js +3 -2
- package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
- package/src/shade/renderer/water/shader_water.js +3 -1
- package/src/shade/renderer/water/surface/GPUWaterSurface.d.ts.map +1 -1
- package/src/shade/renderer/water/surface/GPUWaterSurface.js +3 -1
- package/src/shade/renderer/water/surface/chunk_water_surface_sample.d.ts +4 -3
- package/src/shade/renderer/water/surface/chunk_water_surface_sample.d.ts.map +1 -1
- package/src/shade/renderer/water/surface/chunk_water_surface_sample.js +17 -7
- package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts.map +1 -1
- package/src/shade/renderer/water/surface/graph_water_tessellation.js +11 -4
- package/src/shade/renderer/water/surface/particles/GPUWaterParticles.d.ts.map +1 -1
- package/src/shade/renderer/water/surface/particles/GPUWaterParticles.js +93 -30
- package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.d.ts.map +1 -1
- package/src/shade/renderer/water/surface/particles/WATER_PARTICLE_STRUCT.js +13 -5
- package/src/shade/renderer/water/surface/particles/chunk_water_particle.d.ts +29 -8
- package/src/shade/renderer/water/surface/particles/chunk_water_particle.d.ts.map +1 -1
- package/src/shade/renderer/water/surface/particles/chunk_water_particle.js +155 -30
- package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.d.ts +6 -1
- package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.d.ts.map +1 -1
- package/src/shade/renderer/water/surface/particles/chunk_water_particle_step.js +157 -39
- package/src/shade/renderer/water/surface/particles/shader_water_particles.d.ts +91 -21
- package/src/shade/renderer/water/surface/particles/shader_water_particles.d.ts.map +1 -1
- package/src/shade/renderer/water/surface/particles/shader_water_particles.js +442 -70
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +11 -3
- package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
- package/src/shade/wgsl/emulator/WGSLLanguageCore.js +3 -0
- package/src/shade/wgsl/validate_wgsl_source.d.ts.map +1 -1
- package/src/shade/wgsl/validate_wgsl_source.js +74 -3
- package/src/engine/graphics3/register_instance_animation.d.ts +0 -114
- package/src/engine/graphics3/register_instance_animation.d.ts.map +0 -1
- package/src/engine/graphics3/register_instance_animation.js +0 -201
|
@@ -1,1430 +1,1430 @@
|
|
|
1
|
-
import { MAGMA_LUT } from "../../../core/color/colormap/MAGMA_LUT.js";
|
|
2
|
-
import { FlipParameters } from "../../../engine/graphics/texture/sampler/compare/flip/FlipParameters.js";
|
|
3
|
-
import { flip_pool } from "../../../engine/graphics/texture/sampler/compare/flip/flip_pool.js";
|
|
4
|
-
import { sampler2d_flip_ldr } from "../../../engine/graphics/texture/sampler/compare/flip/sampler2d_flip_ldr.js";
|
|
5
|
-
import { Sampler2D } from "../../../engine/graphics/texture/sampler/Sampler2D.js";
|
|
6
|
-
import { sampler2d_channel_apply_lut } from "../../../engine/graphics/texture/sampler/sampler2d_channel_apply_lut.js";
|
|
7
|
-
import {
|
|
8
|
-
sampler2d_max_absolute_difference
|
|
9
|
-
} from "../../../engine/graphics/texture/sampler/compare/sampler2d_max_absolute_difference.js";
|
|
10
|
-
import { sampler2d_psnr } from "../../../engine/graphics/texture/sampler/compare/sampler2d_psnr.js";
|
|
11
|
-
import {
|
|
12
|
-
decode_astc_4x4,
|
|
13
|
-
decode_bc1,
|
|
14
|
-
decode_bc4,
|
|
15
|
-
decode_bc4_snorm,
|
|
16
|
-
decode_bc5,
|
|
17
|
-
decode_bc5_snorm,
|
|
18
|
-
decode_bc6h_ufloat,
|
|
19
|
-
decode_bc7
|
|
20
|
-
} from "../../../format/texture/basis/__fixtures__/decode_blocks.js";
|
|
21
|
-
import { ShadeGPUCommandContext } from "../../device/ShadeGPUCommandContext.js";
|
|
22
|
-
import { GraphicsContext } from "../../renderer/GraphicsContext.js";
|
|
23
|
-
import {
|
|
24
|
-
block_bytes,
|
|
25
|
-
encode_image_with_block_encoder
|
|
26
|
-
} from "../../renderer/texture/encode/__fixtures__/encode_image_with_block_encoder.js";
|
|
27
|
-
import {
|
|
28
|
-
color_correlated_image,
|
|
29
|
-
hdr_image,
|
|
30
|
-
mask_image,
|
|
31
|
-
normal_map_image,
|
|
32
|
-
rough_image,
|
|
33
|
-
smooth_image
|
|
34
|
-
} from "../../renderer/texture/encode/__fixtures__/encode_test_images.js";
|
|
35
|
-
import { chunk_astc_encode_block_4x4_rgb } from "../../renderer/texture/encode/chunk_astc_encode_block_4x4_rgb.js";
|
|
36
|
-
import { chunk_bc1_encode_block } from "../../renderer/texture/encode/chunk_bc1_encode_block.js";
|
|
37
|
-
import { chunk_bc4_encode_block } from "../../renderer/texture/encode/chunk_bc4_encode_block.js";
|
|
38
|
-
import { chunk_bc6h_encode_block_mode11 } from "../../renderer/texture/encode/chunk_bc6h_encode_block_mode11.js";
|
|
39
|
-
import {
|
|
40
|
-
import { texture_encode } from "../../renderer/texture/encode/texture_encode.js";
|
|
41
|
-
import { texture_encode_level_blocks } from "../../renderer/texture/encode/texture_encode_layout.js";
|
|
42
|
-
import { gpu_texture_compression_support } from "../../renderer/texture/format/gpu_texture_compression_support.js";
|
|
43
|
-
import { texture_write_level } from "../../renderer/texture/texture_write_level.js";
|
|
44
|
-
import { texture_mip_level_count } from "../../util/texture_mip_extent.js";
|
|
45
|
-
import { ComputeShaderEmulator } from "../../wgsl/emulator/ComputeShaderEmulator.js";
|
|
46
|
-
import { create_panel_group } from "./panel_view.js";
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* The GPU block encoder on a real device.
|
|
50
|
-
*
|
|
51
|
-
* **Drives the device directly, without the renderer.** The renderer's frame loop needs immediate
|
|
52
|
-
* data, which not every browser build offers, and nothing here needs a frame: the encoder is a
|
|
53
|
-
* dispatch and some copies recorded into a command context, and the proof is read back. What the
|
|
54
|
-
* device-free tiers established — the encoder's quality, the layout, the wiring — is taken as read;
|
|
55
|
-
* what this page adds is everything only hardware can answer:
|
|
56
|
-
*
|
|
57
|
-
* 1. `createTexture` accepts the block format, and `copyBufferToTexture` accepts the padded rows.
|
|
58
|
-
* 2. The blocks in the texture are the blocks the shader wrote — read back with
|
|
59
|
-
* `copyTextureToBuffer` and compared byte for byte with the block buffer.
|
|
60
|
-
* 3. The hardware decodes them to the texels the test decoder predicts — read back through a
|
|
61
|
-
* `textureLoad` pass, which is the GPU's own BC decoder speaking.
|
|
62
|
-
* 4. The device's f32 arithmetic lands on the blocks the f64 emulator chose, or on blocks no worse.
|
|
63
|
-
*
|
|
64
|
-
* Every number on the page is against the source image, through the quality harness the transcoders
|
|
65
|
-
* were held to.
|
|
66
|
-
*
|
|
67
|
-
* @author Alex Goldring
|
|
68
|
-
* @copyright Company Named Limited (c) 2026
|
|
69
|
-
*/
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* @param {string} id
|
|
73
|
-
* @returns {HTMLElement}
|
|
74
|
-
*/
|
|
75
|
-
function element(id) {
|
|
76
|
-
return document.getElementById(id);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Side of each of the three panels, CSS pixels
|
|
81
|
-
* @type {number}
|
|
82
|
-
*/
|
|
83
|
-
const PANEL_SIZE = 384;
|
|
84
|
-
|
|
85
|
-
const PANEL_SOURCE = 0;
|
|
86
|
-
const PANEL_HARDWARE = 1;
|
|
87
|
-
const PANEL_FLIP = 2;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* How long to wait for a frame before going on without one, milliseconds. A document that is not
|
|
91
|
-
* shown runs no frames, and a run should not stand still until its tab is looked at.
|
|
92
|
-
*
|
|
93
|
-
* @type {number}
|
|
94
|
-
*/
|
|
95
|
-
const PAINT_FALLBACK_MS = 250;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Resolves once the page has painted, or once the fallback has passed. A status set right before a
|
|
99
|
-
* long synchronous pass is never seen unless a frame goes by first.
|
|
100
|
-
*
|
|
101
|
-
* @returns {Promise<void>}
|
|
102
|
-
*/
|
|
103
|
-
function painted() {
|
|
104
|
-
return new Promise(resolve => {
|
|
105
|
-
const fallback = setTimeout(resolve, PAINT_FALLBACK_MS);
|
|
106
|
-
|
|
107
|
-
requestAnimationFrame(() => setTimeout(() => {
|
|
108
|
-
clearTimeout(fallback);
|
|
109
|
-
resolve();
|
|
110
|
-
}, 0));
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* One level of a source image, as bytes and as the sampler the harness compares with.
|
|
116
|
-
*
|
|
117
|
-
* @typedef {Object} SourceLevel
|
|
118
|
-
* @property {number} width
|
|
119
|
-
* @property {number} height
|
|
120
|
-
* @property {Uint8Array} rgba `width * height * 4` bytes of the texel format — `rgba8unorm`,
|
|
121
|
-
* `rgba8snorm`, or the half floats of `rgba16float` for an HDR level, in which case it is
|
|
122
|
-
* `width * height * 8` bytes
|
|
123
|
-
* @property {Float32Array} [hdr] `width * height * 3` linear RGB floats, present for an HDR level
|
|
124
|
-
*/
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* A 2x2 box filter, per channel, rounded — the mip chain a source is given so every level of the
|
|
128
|
-
* encode has a truth to be judged against.
|
|
129
|
-
*
|
|
130
|
-
* @param {SourceLevel} level
|
|
131
|
-
* @returns {SourceLevel}
|
|
132
|
-
*/
|
|
133
|
-
function halve(level) {
|
|
134
|
-
const width = Math.max(1, level.width >> 1);
|
|
135
|
-
const height = Math.max(1, level.height >> 1);
|
|
136
|
-
|
|
137
|
-
if (level.hdr !== undefined) {
|
|
138
|
-
const hdr = new Float32Array(width * height * 3);
|
|
139
|
-
|
|
140
|
-
for (let y = 0; y < height; y++) {
|
|
141
|
-
for (let x = 0; x < width; x++) {
|
|
142
|
-
for (let c = 0; c < 3; c++) {
|
|
143
|
-
const x0 = Math.min(x * 2, level.width - 1);
|
|
144
|
-
const x1 = Math.min(x * 2 + 1, level.width - 1);
|
|
145
|
-
const y0 = Math.min(y * 2, level.height - 1);
|
|
146
|
-
const y1 = Math.min(y * 2 + 1, level.height - 1);
|
|
147
|
-
|
|
148
|
-
hdr[(y * width + x) * 3 + c] = (
|
|
149
|
-
level.hdr[(y0 * level.width + x0) * 3 + c]
|
|
150
|
-
+ level.hdr[(y0 * level.width + x1) * 3 + c]
|
|
151
|
-
+ level.hdr[(y1 * level.width + x0) * 3 + c]
|
|
152
|
-
+ level.hdr[(y1 * level.width + x1) * 3 + c]
|
|
153
|
-
) / 4;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return { width, height, hdr, rgba: hdr_bytes(hdr, width * height) };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const rgba = new Uint8Array(width * height * 4);
|
|
162
|
-
|
|
163
|
-
for (let y = 0; y < height; y++) {
|
|
164
|
-
for (let x = 0; x < width; x++) {
|
|
165
|
-
for (let c = 0; c < 4; c++) {
|
|
166
|
-
const x0 = Math.min(x * 2, level.width - 1);
|
|
167
|
-
const x1 = Math.min(x * 2 + 1, level.width - 1);
|
|
168
|
-
const y0 = Math.min(y * 2, level.height - 1);
|
|
169
|
-
const y1 = Math.min(y * 2 + 1, level.height - 1);
|
|
170
|
-
|
|
171
|
-
const sum = level.rgba[(y0 * level.width + x0) * 4 + c]
|
|
172
|
-
+ level.rgba[(y0 * level.width + x1) * 4 + c]
|
|
173
|
-
+ level.rgba[(y1 * level.width + x0) * 4 + c]
|
|
174
|
-
+ level.rgba[(y1 * level.width + x1) * 4 + c];
|
|
175
|
-
|
|
176
|
-
rgba[(y * width + x) * 4 + c] = Math.round(sum / 4);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return { width, height, rgba };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* The bytes of an `rgba16float` level: each channel a half, alpha one.
|
|
186
|
-
*
|
|
187
|
-
* @param {Float32Array} hdr `texels * 3` linear floats
|
|
188
|
-
* @param {number} texels
|
|
189
|
-
* @returns {Uint8Array} `texels * 8`
|
|
190
|
-
*/
|
|
191
|
-
function hdr_bytes(hdr, texels) {
|
|
192
|
-
const halves = new Float16Array(texels * 4);
|
|
193
|
-
|
|
194
|
-
for (let i = 0; i < texels; i++) {
|
|
195
|
-
halves[i * 4] = hdr[i * 3];
|
|
196
|
-
halves[i * 4 + 1] = hdr[i * 3 + 1];
|
|
197
|
-
halves[i * 4 + 2] = hdr[i * 3 + 2];
|
|
198
|
-
halves[i * 4 + 3] = 1;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return new Uint8Array(halves.buffer);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* The half-float bit patterns of linear values, which is the domain BC6H's error lives in.
|
|
206
|
-
*
|
|
207
|
-
* @param {Float32Array} values
|
|
208
|
-
* @returns {Uint16Array}
|
|
209
|
-
*/
|
|
210
|
-
function half_bits_of(values) {
|
|
211
|
-
const halves = new Float16Array(values.length);
|
|
212
|
-
|
|
213
|
-
for (let i = 0; i < values.length; i++) {
|
|
214
|
-
halves[i] = Math.min(65504, Math.max(0, values[i]));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return new Uint16Array(halves.buffer);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* @param {SourceLevel} base
|
|
222
|
-
* @returns {SourceLevel[]} the full chain, base first
|
|
223
|
-
*/
|
|
224
|
-
function mip_chain(base) {
|
|
225
|
-
const levels = [base];
|
|
226
|
-
|
|
227
|
-
const count = texture_mip_level_count(base.width, base.height, 1, "2d");
|
|
228
|
-
|
|
229
|
-
for (let level = 1; level < count; level++) {
|
|
230
|
-
levels.push(halve(levels[level - 1]));
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return levels;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* The three fixture images the specs measure with, one per channel, at a size worth timing —
|
|
238
|
-
* three channels with nothing in common, which is the hard case for a colour format. Or, with
|
|
239
|
-
* `correlated`, the colour fixture whose channels move together, which is what a photograph is. Or,
|
|
240
|
-
* with `normal`, a tangent-space normal map in red and green — as unsigned bytes, or as the signed
|
|
241
|
-
* bytes a signed target reads, so the picture is the same picture either way rather than a ramp
|
|
242
|
-
* wrapped into a cliff by a change of sign.
|
|
243
|
-
*
|
|
244
|
-
* @param {number} size
|
|
245
|
-
* @param {'independent'|'correlated'|'normal'} kind
|
|
246
|
-
* @param {boolean} signed
|
|
247
|
-
* @returns {SourceLevel}
|
|
248
|
-
*/
|
|
249
|
-
function synthetic_source(size, kind, signed) {
|
|
250
|
-
const rgba = new Uint8Array(size * size * 4);
|
|
251
|
-
|
|
252
|
-
if (kind === 'hdr') {
|
|
253
|
-
const hdr = hdr_image(size, size);
|
|
254
|
-
|
|
255
|
-
return { width: size, height: size, hdr, rgba: hdr_bytes(hdr, size * size) };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (kind === 'normal') {
|
|
259
|
-
const normals = normal_map_image(size, size, 3);
|
|
260
|
-
|
|
261
|
-
for (let i = 0; i < size * size; i++) {
|
|
262
|
-
for (let c = 0; c < 2; c++) {
|
|
263
|
-
const unorm = normals[i * 2 + c];
|
|
264
|
-
|
|
265
|
-
// the byte a signed format stores for the same direction: [-127, 127], wrapped
|
|
266
|
-
rgba[i * 4 + c] = signed ? (Math.round((unorm / 255) * 254 - 127) & 0xFF) : unorm;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
rgba[i * 4 + 2] = signed ? 127 : 255;
|
|
270
|
-
rgba[i * 4 + 3] = signed ? 127 : 255;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return { width: size, height: size, rgba };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (kind === 'correlated') {
|
|
277
|
-
const rgb = color_correlated_image(size, size);
|
|
278
|
-
|
|
279
|
-
for (let i = 0; i < size * size; i++) {
|
|
280
|
-
rgba[i * 4] = rgb[i * 3];
|
|
281
|
-
rgba[i * 4 + 1] = rgb[i * 3 + 1];
|
|
282
|
-
rgba[i * 4 + 2] = rgb[i * 3 + 2];
|
|
283
|
-
rgba[i * 4 + 3] = 255;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return { width: size, height: size, rgba };
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const channels = [smooth_image(size, size), rough_image(size, size, 7), mask_image(size, size)];
|
|
290
|
-
|
|
291
|
-
for (let i = 0; i < size * size; i++) {
|
|
292
|
-
rgba[i * 4] = channels[0][i];
|
|
293
|
-
rgba[i * 4 + 1] = channels[1][i];
|
|
294
|
-
rgba[i * 4 + 2] = channels[2][i];
|
|
295
|
-
rgba[i * 4 + 3] = 255;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return { width: size, height: size, rgba };
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* @param {File} file
|
|
303
|
-
* @returns {Promise<SourceLevel>}
|
|
304
|
-
*/
|
|
305
|
-
async function file_source(file) {
|
|
306
|
-
const bitmap = await createImageBitmap(file);
|
|
307
|
-
|
|
308
|
-
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
309
|
-
const context = canvas.getContext('2d');
|
|
310
|
-
|
|
311
|
-
context.drawImage(bitmap, 0, 0);
|
|
312
|
-
|
|
313
|
-
const data = context.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
314
|
-
|
|
315
|
-
return { width: bitmap.width, height: bitmap.height, rgba: new Uint8Array(data.data.buffer) };
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* The leading `count` channels of interleaved texels, re-interleaved on their own.
|
|
320
|
-
*
|
|
321
|
-
* @param {Uint8Array} data
|
|
322
|
-
* @param {number} stride channels per texel in `data`
|
|
323
|
-
* @param {number} count channels to keep
|
|
324
|
-
* @param {number} texels
|
|
325
|
-
* @returns {Uint8Array} `texels * count` bytes
|
|
326
|
-
*/
|
|
327
|
-
function leading_channels(data, stride, count, texels, signed = false) {
|
|
328
|
-
const out = data instanceof Float32Array
|
|
329
|
-
? new Float32Array(texels * count)
|
|
330
|
-
: signed ? new Int8Array(texels * count) : new Uint8Array(texels * count);
|
|
331
|
-
|
|
332
|
-
for (let i = 0; i < texels; i++) {
|
|
333
|
-
for (let c = 0; c < count; c++) {
|
|
334
|
-
const value = data[i * stride + c];
|
|
335
|
-
|
|
336
|
-
// a signed format's byte is two's complement, and -128 reads as -127
|
|
337
|
-
out[i * count + c] = signed ? Math.max(-127, (value << 24) >> 24) : value;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return out;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* What the page knows about each target it can encode to: how many of the source's channels the
|
|
346
|
-
* format carries, how the test decoder reads its blocks back to those channels, and how the same
|
|
347
|
-
* encoder runs in the emulator on the host.
|
|
348
|
-
*
|
|
349
|
-
* @type {Object<string, {channels: number, bytes_per_block: number, decode: (blocks: Uint8Array, width: number, height: number) => Uint8Array, emulate: () => Promise<(values: Uint8Array) => Uint8Array>}>}
|
|
350
|
-
*/
|
|
351
|
-
const FORMATS = {
|
|
352
|
-
'bc4-r-snorm': {
|
|
353
|
-
channels: 1,
|
|
354
|
-
bytes_per_block: 8,
|
|
355
|
-
signed: true,
|
|
356
|
-
decode: (blocks, width, height) => decode_bc4_snorm(blocks, width, height),
|
|
357
|
-
emulate: async () => {
|
|
358
|
-
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_bc4_encode_block);
|
|
359
|
-
|
|
360
|
-
return values => {
|
|
361
|
-
const snorm = new Float32Array(16);
|
|
362
|
-
|
|
363
|
-
for (let i = 0; i < 16; i++) {
|
|
364
|
-
snorm[i] = Math.max(-127, (values[i] << 24) >> 24) / 127;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return block_bytes(lib.bc4_encode_block_snorm(snorm));
|
|
368
|
-
};
|
|
369
|
-
},
|
|
370
|
-
},
|
|
371
|
-
'bc5-rg-snorm': {
|
|
372
|
-
channels: 2,
|
|
373
|
-
bytes_per_block: 16,
|
|
374
|
-
signed: true,
|
|
375
|
-
decode: (blocks, width, height) => decode_bc5_snorm(blocks, width, height),
|
|
376
|
-
emulate: async () => {
|
|
377
|
-
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_bc4_encode_block);
|
|
378
|
-
|
|
379
|
-
return values => {
|
|
380
|
-
const out = new Uint8Array(16);
|
|
381
|
-
|
|
382
|
-
for (let channel = 0; channel < 2; channel++) {
|
|
383
|
-
const snorm = new Float32Array(16);
|
|
384
|
-
|
|
385
|
-
for (let i = 0; i < 16; i++) {
|
|
386
|
-
snorm[i] = Math.max(-127, (values[i * 2 + channel] << 24) >> 24) / 127;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
out.set(block_bytes(lib.bc4_encode_block_snorm(snorm)), channel * 8);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return out;
|
|
393
|
-
};
|
|
394
|
-
},
|
|
395
|
-
},
|
|
396
|
-
'bc4-r-unorm': {
|
|
397
|
-
channels: 1,
|
|
398
|
-
bytes_per_block: 8,
|
|
399
|
-
signed: false,
|
|
400
|
-
decode: (blocks, width, height) => decode_bc4(blocks, width, height),
|
|
401
|
-
emulate: async () => {
|
|
402
|
-
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_bc4_encode_block);
|
|
403
|
-
|
|
404
|
-
return values => {
|
|
405
|
-
const unorm = new Float32Array(16);
|
|
406
|
-
|
|
407
|
-
for (let i = 0; i < 16; i++) {
|
|
408
|
-
unorm[i] = values[i] / 255;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return block_bytes(lib.bc4_encode_block(unorm));
|
|
412
|
-
};
|
|
413
|
-
},
|
|
414
|
-
},
|
|
415
|
-
'bc5-rg-unorm': {
|
|
416
|
-
channels: 2,
|
|
417
|
-
bytes_per_block: 16,
|
|
418
|
-
signed: false,
|
|
419
|
-
decode: (blocks, width, height) => leading_channels(decode_bc5(blocks, width, height), 4, 2, width * height),
|
|
420
|
-
emulate: async () => {
|
|
421
|
-
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_bc4_encode_block);
|
|
422
|
-
|
|
423
|
-
return values => {
|
|
424
|
-
const out = new Uint8Array(16);
|
|
425
|
-
|
|
426
|
-
for (let channel = 0; channel < 2; channel++) {
|
|
427
|
-
const unorm = new Float32Array(16);
|
|
428
|
-
|
|
429
|
-
for (let i = 0; i < 16; i++) {
|
|
430
|
-
unorm[i] = values[i * 2 + channel] / 255;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
out.set(block_bytes(lib.bc4_encode_block(unorm)), channel * 8);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return out;
|
|
437
|
-
};
|
|
438
|
-
},
|
|
439
|
-
},
|
|
440
|
-
'bc6h-rgb-ufloat': {
|
|
441
|
-
channels: 3,
|
|
442
|
-
bytes_per_block: 16,
|
|
443
|
-
signed: false,
|
|
444
|
-
hdr: true,
|
|
445
|
-
decode: (blocks, width, height) => decode_bc6h_ufloat(blocks, width, height),
|
|
446
|
-
emulate: async () => {
|
|
447
|
-
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_bc6h_encode_block_mode11);
|
|
448
|
-
|
|
449
|
-
// handed floats rather than bytes: an HDR level's truth is its linear values
|
|
450
|
-
return values => {
|
|
451
|
-
const texels = [];
|
|
452
|
-
|
|
453
|
-
for (let i = 0; i < 16; i++) {
|
|
454
|
-
texels.push(new Float32Array([values[i * 3], values[i * 3 + 1], values[i * 3 + 2]]));
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
return block_bytes(lib.bc6h_encode_block_mode11(texels));
|
|
458
|
-
};
|
|
459
|
-
},
|
|
460
|
-
},
|
|
461
|
-
'bc7-rgba-unorm': {
|
|
462
|
-
channels: 4,
|
|
463
|
-
bytes_per_block: 16,
|
|
464
|
-
signed: false,
|
|
465
|
-
decode: (blocks, width, height) => decode_bc7(blocks, width, height),
|
|
466
|
-
emulate: async () => {
|
|
467
|
-
const lib = await ComputeShaderEmulator.fromCodeChunk(
|
|
468
|
-
|
|
469
|
-
return values => {
|
|
470
|
-
const texels = [];
|
|
471
|
-
|
|
472
|
-
for (let i = 0; i < 16; i++) {
|
|
473
|
-
texels.push(new Float32Array([values[i * 4] / 255, values[i * 4 + 1] / 255, values[i * 4 + 2] / 255, values[i * 4 + 3] / 255]));
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
return block_bytes(lib.
|
|
477
|
-
};
|
|
478
|
-
},
|
|
479
|
-
},
|
|
480
|
-
'astc-4x4-unorm': {
|
|
481
|
-
channels: 3,
|
|
482
|
-
bytes_per_block: 16,
|
|
483
|
-
signed: false,
|
|
484
|
-
decode: (blocks, width, height) => {
|
|
485
|
-
const blocks_x = Math.ceil(width / 4);
|
|
486
|
-
const blocks_y = Math.ceil(height / 4);
|
|
487
|
-
|
|
488
|
-
const out = new Uint8Array(width * height * 3);
|
|
489
|
-
|
|
490
|
-
for (let by = 0; by < blocks_y; by++) {
|
|
491
|
-
for (let bx = 0; bx < blocks_x; bx++) {
|
|
492
|
-
const texels = decode_astc_4x4(blocks, (by * blocks_x + bx) * 16);
|
|
493
|
-
|
|
494
|
-
if (texels === null) {
|
|
495
|
-
throw new Error(`block ${by * blocks_x + bx} is not the shape the decoder reads`);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
for (let i = 0; i < 16; i++) {
|
|
499
|
-
const x = bx * 4 + (i & 3);
|
|
500
|
-
const y = by * 4 + (i >> 2);
|
|
501
|
-
|
|
502
|
-
if (x < width && y < height) {
|
|
503
|
-
out.set(texels[i], (y * width + x) * 3);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
return out;
|
|
510
|
-
},
|
|
511
|
-
emulate: async () => {
|
|
512
|
-
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_astc_encode_block_4x4_rgb);
|
|
513
|
-
|
|
514
|
-
return values => {
|
|
515
|
-
const texels = [];
|
|
516
|
-
|
|
517
|
-
for (let i = 0; i < 16; i++) {
|
|
518
|
-
texels.push(new Float32Array([values[i * 3] / 255, values[i * 3 + 1] / 255, values[i * 3 + 2] / 255]));
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return block_bytes(lib.astc_encode_block_4x4_rgb(texels));
|
|
522
|
-
};
|
|
523
|
-
},
|
|
524
|
-
},
|
|
525
|
-
'bc1-rgba-unorm': {
|
|
526
|
-
channels: 3,
|
|
527
|
-
bytes_per_block: 8,
|
|
528
|
-
signed: false,
|
|
529
|
-
decode: (blocks, width, height) => leading_channels(decode_bc1(blocks, width, height), 4, 3, width * height),
|
|
530
|
-
emulate: async () => {
|
|
531
|
-
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_bc1_encode_block);
|
|
532
|
-
|
|
533
|
-
return values => {
|
|
534
|
-
const texels = [];
|
|
535
|
-
|
|
536
|
-
for (let i = 0; i < 16; i++) {
|
|
537
|
-
texels.push(new Float32Array([values[i * 3] / 255, values[i * 3 + 1] / 255, values[i * 3 + 2] / 255]));
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
return block_bytes(lib.bc1_encode_block(texels));
|
|
541
|
-
};
|
|
542
|
-
},
|
|
543
|
-
},
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Read one level of a block-compressed texture back through the hardware's decoder: a
|
|
548
|
-
* `textureLoad` per texel into a buffer of words.
|
|
549
|
-
*
|
|
550
|
-
* @param {GPUDevice} device
|
|
551
|
-
* @param {GPUTexture} texture
|
|
552
|
-
* @param {number} level
|
|
553
|
-
* @param {number} width
|
|
554
|
-
* @param {number} height
|
|
555
|
-
* @param {'bytes'|'signed'|'float'|'exact_red'} mode `bytes` answers every channel rounded to
|
|
556
|
-
* eight-bit levels, `signed` the same as signed bytes, `float` every channel as the f32 the
|
|
557
|
-
* sampler delivers, and `exact_red` the red channel alone as f32 — the decoder produces more
|
|
558
|
-
* precision than eight bits, and the probe wants it
|
|
559
|
-
* @returns {Promise<Uint8Array|Int8Array|Float32Array>} interleaved RGBA, `width * height * 4`
|
|
560
|
-
* samples; or the red channel's `width * height` floats
|
|
561
|
-
*/
|
|
562
|
-
async function hardware_decode(device, texture, level, width, height, mode) {
|
|
563
|
-
const per_texel = mode === 'float' ? 4 : 1;
|
|
564
|
-
|
|
565
|
-
const store = {
|
|
566
|
-
bytes: 'out[at] = pack4x8unorm(texel);',
|
|
567
|
-
signed: 'out[at] = pack4x8snorm(texel);',
|
|
568
|
-
exact_red: 'out[at] = bitcast<u32>(texel.r);',
|
|
569
|
-
float: 'out[at * 4u] = bitcast<u32>(texel.r); out[at * 4u + 1u] = bitcast<u32>(texel.g); out[at * 4u + 2u] = bitcast<u32>(texel.b); out[at * 4u + 3u] = bitcast<u32>(texel.a);',
|
|
570
|
-
}[mode];
|
|
571
|
-
|
|
572
|
-
const module = device.createShaderModule({
|
|
573
|
-
code: `
|
|
574
|
-
@group(0) @binding(0) var tex: texture_2d<f32>;
|
|
575
|
-
@group(0) @binding(1) var<storage, read_write> out: array<u32>;
|
|
576
|
-
@group(0) @binding(2) var<uniform> level: vec4<u32>;
|
|
577
|
-
|
|
578
|
-
@compute @workgroup_size(8, 8, 1)
|
|
579
|
-
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
580
|
-
let size = textureDimensions(tex, i32(level.x));
|
|
581
|
-
|
|
582
|
-
if (id.x >= size.x || id.y >= size.y) {
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
let texel = textureLoad(tex, vec2<i32>(id.xy), i32(level.x));
|
|
587
|
-
let at = id.y * size.x + id.x;
|
|
588
|
-
|
|
589
|
-
${store}
|
|
590
|
-
}
|
|
591
|
-
`,
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
const pipeline = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main' } });
|
|
595
|
-
|
|
596
|
-
const byte_length = width * height * 4 * per_texel;
|
|
597
|
-
|
|
598
|
-
const out = device.createBuffer({ size: byte_length, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });
|
|
599
|
-
const uniform = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
600
|
-
const readback = device.createBuffer({ size: byte_length, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
|
|
601
|
-
|
|
602
|
-
device.queue.writeBuffer(uniform, 0, new Uint32Array([level, 0, 0, 0]));
|
|
603
|
-
|
|
604
|
-
const bind_group = device.createBindGroup({
|
|
605
|
-
layout: pipeline.getBindGroupLayout(0),
|
|
606
|
-
entries: [
|
|
607
|
-
{ binding: 0, resource: texture.createView() },
|
|
608
|
-
{ binding: 1, resource: { buffer: out } },
|
|
609
|
-
{ binding: 2, resource: { buffer: uniform } },
|
|
610
|
-
],
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
const encoder = device.createCommandEncoder();
|
|
614
|
-
|
|
615
|
-
const pass = encoder.beginComputePass();
|
|
616
|
-
|
|
617
|
-
pass.setPipeline(pipeline);
|
|
618
|
-
pass.setBindGroup(0, bind_group);
|
|
619
|
-
pass.dispatchWorkgroups(Math.ceil(width / 8), Math.ceil(height / 8), 1);
|
|
620
|
-
pass.end();
|
|
621
|
-
|
|
622
|
-
encoder.copyBufferToBuffer(out, 0, readback, 0, byte_length);
|
|
623
|
-
|
|
624
|
-
device.queue.submit([encoder.finish()]);
|
|
625
|
-
|
|
626
|
-
await readback.mapAsync(GPUMapMode.READ);
|
|
627
|
-
|
|
628
|
-
const range = readback.getMappedRange().slice(0);
|
|
629
|
-
|
|
630
|
-
readback.unmap();
|
|
631
|
-
|
|
632
|
-
out.destroy();
|
|
633
|
-
uniform.destroy();
|
|
634
|
-
readback.destroy();
|
|
635
|
-
|
|
636
|
-
// the packing puts red in the low byte, which is where a little-endian byte view starts
|
|
637
|
-
if (mode === 'bytes') {
|
|
638
|
-
return new Uint8Array(range);
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
if (mode === 'signed') {
|
|
642
|
-
return new Int8Array(range);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
return new Float32Array(range);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* @param {GPUDevice} device
|
|
650
|
-
* @param {GPUBuffer} source
|
|
651
|
-
* @param {number} byte_length
|
|
652
|
-
* @returns {Promise<Uint8Array>}
|
|
653
|
-
*/
|
|
654
|
-
async function read_buffer(device, source, byte_length) {
|
|
655
|
-
const readback = device.createBuffer({ size: byte_length, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
|
|
656
|
-
|
|
657
|
-
const encoder = device.createCommandEncoder();
|
|
658
|
-
|
|
659
|
-
encoder.copyBufferToBuffer(source, 0, readback, 0, byte_length);
|
|
660
|
-
|
|
661
|
-
device.queue.submit([encoder.finish()]);
|
|
662
|
-
|
|
663
|
-
await readback.mapAsync(GPUMapMode.READ);
|
|
664
|
-
|
|
665
|
-
const bytes = new Uint8Array(readback.getMappedRange().slice(0));
|
|
666
|
-
|
|
667
|
-
readback.unmap();
|
|
668
|
-
readback.destroy();
|
|
669
|
-
|
|
670
|
-
return bytes;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* The blocks of one level of a compressed texture, read back with the copy the encoder's copy is the
|
|
675
|
-
* inverse of.
|
|
676
|
-
*
|
|
677
|
-
* @param {GPUDevice} device
|
|
678
|
-
* @param {GPUTexture} texture
|
|
679
|
-
* @param {TextureEncodeLevel} level
|
|
680
|
-
* @param {number} bytes_per_block
|
|
681
|
-
* @returns {Promise<Uint8Array>} tightly packed
|
|
682
|
-
*/
|
|
683
|
-
async function read_texture_level(device, texture, level, bytes_per_block) {
|
|
684
|
-
const padded = level.bytes_per_row * level.blocks_y;
|
|
685
|
-
|
|
686
|
-
const readback = device.createBuffer({ size: padded, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
|
|
687
|
-
|
|
688
|
-
const encoder = device.createCommandEncoder();
|
|
689
|
-
|
|
690
|
-
// sized in whole blocks, as the encoder's own copy is — the device refuses a 2x2 extent
|
|
691
|
-
encoder.copyTextureToBuffer(
|
|
692
|
-
{ texture, mipLevel: level.level },
|
|
693
|
-
{ buffer: readback, bytesPerRow: level.bytes_per_row, rowsPerImage: level.blocks_y },
|
|
694
|
-
[level.copy_width, level.copy_height, 1]
|
|
695
|
-
);
|
|
696
|
-
|
|
697
|
-
device.queue.submit([encoder.finish()]);
|
|
698
|
-
|
|
699
|
-
await readback.mapAsync(GPUMapMode.READ);
|
|
700
|
-
|
|
701
|
-
const bytes = new Uint8Array(readback.getMappedRange().slice(0));
|
|
702
|
-
|
|
703
|
-
readback.unmap();
|
|
704
|
-
readback.destroy();
|
|
705
|
-
|
|
706
|
-
const row_bytes = level.blocks_x * bytes_per_block;
|
|
707
|
-
|
|
708
|
-
const out = new Uint8Array(row_bytes * level.blocks_y);
|
|
709
|
-
|
|
710
|
-
for (let row = 0; row < level.blocks_y; row++) {
|
|
711
|
-
out.set(bytes.subarray(row * level.bytes_per_row, row * level.bytes_per_row + row_bytes), row * row_bytes);
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
return out;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Linear HDR values as display bytes: a Reinhard curve and a display gamma, so the sun clips and
|
|
719
|
-
* the shadows show.
|
|
720
|
-
*
|
|
721
|
-
* @param {Float32Array} values
|
|
722
|
-
* @returns {Uint8Array}
|
|
723
|
-
*/
|
|
724
|
-
function tonemap(values) {
|
|
725
|
-
const out = new Uint8Array(values.length);
|
|
726
|
-
|
|
727
|
-
for (let i = 0; i < values.length; i++) {
|
|
728
|
-
const v = Math.max(0, values[i]);
|
|
729
|
-
|
|
730
|
-
out[i] = Math.round(Math.min(1, (v / (1 + v)) ** (1 / 2.2)) * 255);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
return out;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
* An HDR image through a block encoder that takes forty-eight floats, edge texels repeated.
|
|
738
|
-
*
|
|
739
|
-
* @param {(values: Float32Array) => Uint8Array} encode_block
|
|
740
|
-
* @param {Float32Array} image `width * height * 3`
|
|
741
|
-
* @param {number} width
|
|
742
|
-
* @param {number} height
|
|
743
|
-
* @returns {Uint8Array} tightly packed blocks
|
|
744
|
-
*/
|
|
745
|
-
function encode_hdr_image(encode_block, image, width, height) {
|
|
746
|
-
const blocks_x = Math.ceil(width / 4);
|
|
747
|
-
const blocks_y = Math.ceil(height / 4);
|
|
748
|
-
|
|
749
|
-
const out = new Uint8Array(blocks_x * blocks_y * 16);
|
|
750
|
-
const values = new Float32Array(48);
|
|
751
|
-
|
|
752
|
-
for (let by = 0; by < blocks_y; by++) {
|
|
753
|
-
for (let bx = 0; bx < blocks_x; bx++) {
|
|
754
|
-
for (let i = 0; i < 16; i++) {
|
|
755
|
-
const x = Math.min(bx * 4 + (i & 3), width - 1);
|
|
756
|
-
const y = Math.min(by * 4 + (i >> 2), height - 1);
|
|
757
|
-
|
|
758
|
-
for (let c = 0; c < 3; c++) {
|
|
759
|
-
values[i * 3 + c] = image[(y * width + x) * 3 + c];
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
out.set(encode_block(values), (by * blocks_x + bx) * 16);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
return out;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* The bytes a canvas shows for one, two or three interleaved channels: one as grey, two as red and
|
|
772
|
-
* green, three as colour — opaque RGBA, which is also what FLIP compares, since FLIP judges what is
|
|
773
|
-
* displayed.
|
|
774
|
-
*
|
|
775
|
-
* @param {ArrayLike<number>} data
|
|
776
|
-
* @param {number} channels
|
|
777
|
-
* @param {number} texels
|
|
778
|
-
* @param {number} gain
|
|
779
|
-
* @param {boolean} signed values in [-127, 127], shown with zero at mid grey
|
|
780
|
-
* @returns {Uint8Array} `texels * 4`
|
|
781
|
-
*/
|
|
782
|
-
function display_rgba(data, channels, texels, gain = 1, signed = false) {
|
|
783
|
-
const out = new Uint8Array(texels * 4);
|
|
784
|
-
|
|
785
|
-
for (let i = 0; i < texels; i++) {
|
|
786
|
-
for (let c = 0; c < 3; c++) {
|
|
787
|
-
const source = channels === 1 ? 0 : c;
|
|
788
|
-
|
|
789
|
-
const value = source < channels ? data[i * channels + source] : (signed ? -128 : 0);
|
|
790
|
-
|
|
791
|
-
out[i * 4 + c] = Math.min(255, (signed ? value + 128 : value) * gain);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
out[i * 4 + 3] = 255;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
return out;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
/**
|
|
801
|
-
* Stand the page up.
|
|
802
|
-
*
|
|
803
|
-
* @returns {Promise<void>}
|
|
804
|
-
*/
|
|
805
|
-
export async function start() {
|
|
806
|
-
const status_element = element('status');
|
|
807
|
-
const report_element = element('report');
|
|
808
|
-
const readout_element = element('readout');
|
|
809
|
-
|
|
810
|
-
/**
|
|
811
|
-
* What each panel holds at a texel, for the line under the panels; set by the run that drew
|
|
812
|
-
* them, null before the first.
|
|
813
|
-
*
|
|
814
|
-
* @type {?function(number, number): string}
|
|
815
|
-
*/
|
|
816
|
-
let describe_texel = null;
|
|
817
|
-
|
|
818
|
-
const panels = create_panel_group({
|
|
819
|
-
canvases: [element('source_view'), element('decoded_view'), element('difference_view')],
|
|
820
|
-
size: PANEL_SIZE,
|
|
821
|
-
on_cursor: texel => {
|
|
822
|
-
readout_element.textContent = texel === null || describe_texel === null ? '' : describe_texel(texel.x, texel.y);
|
|
823
|
-
}
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
/**
|
|
827
|
-
* @param {string} text
|
|
828
|
-
* @param {string} [kind]
|
|
829
|
-
*/
|
|
830
|
-
function set_status(text, kind = '') {
|
|
831
|
-
status_element.textContent = text;
|
|
832
|
-
status_element.className = kind;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
if (navigator.gpu === undefined) {
|
|
836
|
-
set_status('no WebGPU in this browser', 'fail');
|
|
837
|
-
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
const adapter = await navigator.gpu.requestAdapter();
|
|
842
|
-
|
|
843
|
-
if (adapter === null) {
|
|
844
|
-
set_status('no WebGPU adapter', 'fail');
|
|
845
|
-
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// every compression family the adapter offers is asked for, so the encoder can be handed any
|
|
850
|
-
// of its targets; the encoder itself needs no feature at all
|
|
851
|
-
const requiredFeatures = ['texture-compression-bc', 'texture-compression-etc2', 'texture-compression-astc']
|
|
852
|
-
.filter(feature => adapter.features.has(feature));
|
|
853
|
-
|
|
854
|
-
const device = await adapter.requestDevice({ requiredFeatures });
|
|
855
|
-
|
|
856
|
-
const support = gpu_texture_compression_support(device);
|
|
857
|
-
|
|
858
|
-
element('device').textContent = `${adapter.info?.vendor ?? '?'} ${adapter.info?.architecture ?? ''}`
|
|
859
|
-
+ ` — ${support.size} compressed formats sampleable, features: ${requiredFeatures.join(', ') || 'none'}`;
|
|
860
|
-
|
|
861
|
-
const graphics = new GraphicsContext(device);
|
|
862
|
-
|
|
863
|
-
/** @type {SourceLevel|null} */
|
|
864
|
-
let uploaded_source = null;
|
|
865
|
-
|
|
866
|
-
globalThis.device = device;
|
|
867
|
-
|
|
868
|
-
/**
|
|
869
|
-
* @param {SourceLevel} base
|
|
870
|
-
*/
|
|
871
|
-
async function run(base) {
|
|
872
|
-
const format = element('format').value;
|
|
873
|
-
|
|
874
|
-
const info = FORMATS[format];
|
|
875
|
-
|
|
876
|
-
if (info === undefined) {
|
|
877
|
-
set_status(`${format} is not a format this page knows how to judge`, 'fail');
|
|
878
|
-
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
if (!support.has(format)) {
|
|
883
|
-
set_status(`${format} cannot be sampled on this device, so the texture cannot be created`, 'fail');
|
|
884
|
-
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
set_status(`encoding ${base.width}x${base.height} to ${format}…`);
|
|
889
|
-
|
|
890
|
-
const levels = mip_chain(base);
|
|
891
|
-
|
|
892
|
-
if (info.hdr === true && base.hdr === undefined) {
|
|
893
|
-
set_status(`${format} wants an HDR source; pick the HDR synthetic one`, 'fail');
|
|
894
|
-
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// a signed target reads a signed source: the same bytes, meaning [-1, 1] to the sampler;
|
|
899
|
-
// an HDR target reads halves
|
|
900
|
-
const source = device.createTexture({
|
|
901
|
-
label: 'source',
|
|
902
|
-
size: [base.width, base.height, 1],
|
|
903
|
-
format: info.hdr ? 'rgba16float' : (info.signed ? 'rgba8snorm' : 'rgba8unorm'),
|
|
904
|
-
mipLevelCount: levels.length,
|
|
905
|
-
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
906
|
-
});
|
|
907
|
-
|
|
908
|
-
for (let level = 0; level < levels.length; level++) {
|
|
909
|
-
texture_write_level(device.queue, source, level, levels[level].rgba);
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
const lines = [];
|
|
913
|
-
|
|
914
|
-
/**
|
|
915
|
-
* Record, submit and wait, answering with the result and the wall time.
|
|
916
|
-
*
|
|
917
|
-
* @returns {Promise<{result: TextureEncodeResult, blocks_copy: GPUBuffer, wall_ms: number}>}
|
|
918
|
-
*/
|
|
919
|
-
async function encode_once() {
|
|
920
|
-
const cmd = ShadeGPUCommandContext.create(graphics, 'texture encode');
|
|
921
|
-
|
|
922
|
-
const started = performance.now();
|
|
923
|
-
|
|
924
|
-
const result = texture_encode(cmd, {
|
|
925
|
-
source,
|
|
926
|
-
format,
|
|
927
|
-
level_count: levels.length,
|
|
928
|
-
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC,
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
// the block buffer is transient and goes back to the pool at finish, so it is copied out now
|
|
932
|
-
const blocks_copy = device.createBuffer({
|
|
933
|
-
size: result.layout.byte_length,
|
|
934
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
935
|
-
});
|
|
936
|
-
|
|
937
|
-
cmd.copyBufferToBuffer(result.blocks, 0, blocks_copy, 0, result.layout.byte_length);
|
|
938
|
-
|
|
939
|
-
cmd.finish();
|
|
940
|
-
|
|
941
|
-
await device.queue.onSubmittedWorkDone();
|
|
942
|
-
|
|
943
|
-
return { result, blocks_copy, wall_ms: performance.now() - started };
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
device.pushErrorScope('validation');
|
|
947
|
-
|
|
948
|
-
// twice: the first run compiles the pipeline, the second is what a second texture costs
|
|
949
|
-
const first = await encode_once();
|
|
950
|
-
|
|
951
|
-
first.result.texture.destroy();
|
|
952
|
-
first.blocks_copy.destroy();
|
|
953
|
-
|
|
954
|
-
const { result, blocks_copy, wall_ms } = await encode_once();
|
|
955
|
-
|
|
956
|
-
const validation = await device.popErrorScope();
|
|
957
|
-
|
|
958
|
-
if (validation !== null) {
|
|
959
|
-
set_status(`validation error: ${validation.message}`, 'fail');
|
|
960
|
-
|
|
961
|
-
lines.push(validation.message);
|
|
962
|
-
report_element.textContent = lines.join('\n');
|
|
963
|
-
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
const texels = levels.reduce((sum, level) => sum + level.width * level.height, 0);
|
|
968
|
-
|
|
969
|
-
lines.push(`${format} ${base.width}x${base.height}, ${levels.length} levels, ${result.layout.block_count} blocks`);
|
|
970
|
-
lines.push(` wall time, record to done ${wall_ms.toFixed(2)} ms (${(texels / 1e6 / (wall_ms / 1000)).toFixed(1)} MP/s; first run with pipeline compile ${first.wall_ms.toFixed(1)} ms)`);
|
|
971
|
-
lines.push(` block buffer ${result.layout.byte_length} bytes, rows padded to ${result.layout.levels[0].bytes_per_row}`);
|
|
972
|
-
lines.push('');
|
|
973
|
-
lines.push(' level size PSNR dB max hw PSNR hw max texture==buffer hw decode vs CPU decode');
|
|
974
|
-
|
|
975
|
-
const blocks = await read_buffer(device, blocks_copy, result.layout.byte_length);
|
|
976
|
-
|
|
977
|
-
blocks_copy.destroy();
|
|
978
|
-
|
|
979
|
-
let all_texture_match = true;
|
|
980
|
-
let hardware_mismatch_total = 0;
|
|
981
|
-
let hardware_max = 0;
|
|
982
|
-
let hardware_psnr_gap = 0;
|
|
983
|
-
let flip_line = '';
|
|
984
|
-
|
|
985
|
-
for (const level of result.layout.levels) {
|
|
986
|
-
const texel_count = level.width * level.height;
|
|
987
|
-
|
|
988
|
-
const truth = info.hdr
|
|
989
|
-
? levels[level.level].hdr
|
|
990
|
-
: leading_channels(levels[level.level].rgba, 4, info.channels, texel_count, info.signed);
|
|
991
|
-
|
|
992
|
-
const level_blocks = texture_encode_level_blocks(result.layout, blocks, level.level);
|
|
993
|
-
|
|
994
|
-
const decoded = info.decode(level_blocks, level.width, level.height);
|
|
995
|
-
|
|
996
|
-
/*
|
|
997
|
-
What a sampler is measured over: bytes for an LDR format, and for BC6H the half-float
|
|
998
|
-
bit patterns, which is the domain its decoder interpolates in and a relative error in
|
|
999
|
-
value — with the largest finite half as the peak.
|
|
1000
|
-
*/
|
|
1001
|
-
const measure = samples => info.hdr ? half_bits_of(samples) : samples;
|
|
1002
|
-
const peak = info.hdr ? 0x7BFF : (info.signed ? 127 : 255);
|
|
1003
|
-
|
|
1004
|
-
// the PSNR of one level everywhere: no picture is credited as closer to the source than
|
|
1005
|
-
// a rounding, so a level one decoder reproduces exactly still has a ratio to take
|
|
1006
|
-
const rounding_psnr = 20 * Math.log10(peak);
|
|
1007
|
-
|
|
1008
|
-
const reference = new Sampler2D(measure(truth), info.channels, level.width, level.height);
|
|
1009
|
-
const test = new Sampler2D(measure(decoded), info.channels, level.width, level.height);
|
|
1010
|
-
|
|
1011
|
-
const psnr = sampler2d_psnr(reference, test, info.channels, peak);
|
|
1012
|
-
const max = sampler2d_max_absolute_difference(reference, test);
|
|
1013
|
-
|
|
1014
|
-
// 2. the texture holds the buffer's blocks
|
|
1015
|
-
const from_texture = await read_texture_level(device, result.texture, level, result.layout.bytes_per_block);
|
|
1016
|
-
|
|
1017
|
-
let texture_match = from_texture.length === level_blocks.length;
|
|
1018
|
-
|
|
1019
|
-
for (let i = 0; texture_match && i < level_blocks.length; i++) {
|
|
1020
|
-
texture_match = from_texture[i] === level_blocks[i];
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
all_texture_match = all_texture_match && texture_match;
|
|
1024
|
-
|
|
1025
|
-
// 3. the hardware decodes what the test decoder predicts
|
|
1026
|
-
const hardware = leading_channels(
|
|
1027
|
-
await hardware_decode(device, result.texture, level.level, level.width, level.height, info.hdr ? 'float' : (info.signed ? 'signed' : 'bytes')),
|
|
1028
|
-
4, info.channels, texel_count, info.signed
|
|
1029
|
-
);
|
|
1030
|
-
|
|
1031
|
-
const hardware_measured = measure(hardware);
|
|
1032
|
-
const decoded_measured = measure(decoded);
|
|
1033
|
-
|
|
1034
|
-
let mismatches = 0;
|
|
1035
|
-
let worst = 0;
|
|
1036
|
-
|
|
1037
|
-
for (let i = 0; i < hardware_measured.length; i++) {
|
|
1038
|
-
const delta = Math.abs(hardware_measured[i] - decoded_measured[i]);
|
|
1039
|
-
|
|
1040
|
-
if (delta !== 0) {
|
|
1041
|
-
mismatches++;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
worst = Math.max(worst, delta);
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
hardware_mismatch_total += mismatches;
|
|
1048
|
-
hardware_max = Math.max(hardware_max, worst);
|
|
1049
|
-
|
|
1050
|
-
// what the GPU actually shows against the source — the number a user would see
|
|
1051
|
-
const hardware_sampler = new Sampler2D(hardware_measured, info.channels, level.width, level.height);
|
|
1052
|
-
|
|
1053
|
-
const hardware_psnr = sampler2d_psnr(reference, hardware_sampler, info.channels, peak);
|
|
1054
|
-
const hardware_worst = sampler2d_max_absolute_difference(reference, hardware_sampler);
|
|
1055
|
-
|
|
1056
|
-
/*
|
|
1057
|
-
The gap is the CPU decoder's reading minus the hardware's, where the hardware is worse,
|
|
1058
|
-
both held at the PSNR of a rounding. A tail level the CPU decoder reproduces exactly has
|
|
1059
|
-
infinite PSNR, and against a hardware texel one level off the ratio is whatever the
|
|
1060
|
-
ceiling makes it — a 200 dB ceiling read BC1's 1x1 level as a 150 dB gap. Under the
|
|
1061
|
-
rounding ceiling that level is no gap at all, while a misread block on it stays tens of
|
|
1062
|
-
decibels under.
|
|
1063
|
-
*/
|
|
1064
|
-
hardware_psnr_gap = Math.max(hardware_psnr_gap, Math.min(psnr, rounding_psnr) - Math.min(hardware_psnr, rounding_psnr));
|
|
1065
|
-
|
|
1066
|
-
lines.push(
|
|
1067
|
-
` ${String(level.level).padStart(5)} ${`${level.width}x${level.height}`.padStart(9)}`
|
|
1068
|
-
+ ` ${psnr.toFixed(2).padStart(7)} ${String(max).padStart(3)}`
|
|
1069
|
-
+ ` ${hardware_psnr.toFixed(2).padStart(7)} ${String(hardware_worst).padStart(6)}`
|
|
1070
|
-
+ ` ${texture_match ? 'yes' : 'NO '} `
|
|
1071
|
-
+ ` ${mismatches} samples differ, worst ${worst}`
|
|
1072
|
-
);
|
|
1073
|
-
|
|
1074
|
-
if (level.level === 0) {
|
|
1075
|
-
const shown_truth = info.hdr ? tonemap(truth) : truth;
|
|
1076
|
-
const shown_hardware = info.hdr ? tonemap(hardware) : hardware;
|
|
1077
|
-
|
|
1078
|
-
const truth_rgba = display_rgba(shown_truth, info.channels, texel_count, 1, info.signed);
|
|
1079
|
-
const hardware_rgba = display_rgba(shown_hardware, info.channels, texel_count, 1, info.signed);
|
|
1080
|
-
|
|
1081
|
-
panels.set_picture(PANEL_SOURCE, truth_rgba, level.width, level.height);
|
|
1082
|
-
panels.set_picture(PANEL_HARDWARE, hardware_rgba, level.width, level.height);
|
|
1083
|
-
|
|
1084
|
-
/*
|
|
1085
|
-
The third panel is FLIP: how likely a viewer flipping between the two pictures is
|
|
1086
|
-
to notice a difference, and where. A tint nobody would see scores low and a
|
|
1087
|
-
displaced edge scores high, which is what an amplified pixel difference gets
|
|
1088
|
-
backwards. It compares the displayed bytes — the tone-mapped picture for HDR, the
|
|
1089
|
-
mid-grey-centred one for a signed format — because that is what is seen. It is a
|
|
1090
|
-
CPU pass over the whole image, seconds at 2048x2048, so the status says what the
|
|
1091
|
-
page is doing and a frame goes by so that it shows.
|
|
1092
|
-
*/
|
|
1093
|
-
set_status(`encoded; FLIP over ${level.width}x${level.height}…`);
|
|
1094
|
-
|
|
1095
|
-
await painted();
|
|
1096
|
-
|
|
1097
|
-
const flip_started = performance.now();
|
|
1098
|
-
|
|
1099
|
-
const error = sampler2d_flip_ldr(
|
|
1100
|
-
new Sampler2D(truth_rgba, 4, level.width, level.height),
|
|
1101
|
-
new Sampler2D(hardware_rgba, 4, level.width, level.height)
|
|
1102
|
-
);
|
|
1103
|
-
|
|
1104
|
-
const flip_ms = performance.now() - flip_started;
|
|
1105
|
-
|
|
1106
|
-
// the readbacks of the other levels are awaited, so this one paints on its own
|
|
1107
|
-
set_status(`FLIP done in ${flip_ms.toFixed(0)} ms; the other levels, then the emulator over level 0…`);
|
|
1108
|
-
|
|
1109
|
-
const pooled = flip_pool(error);
|
|
1110
|
-
|
|
1111
|
-
panels.set_picture(PANEL_FLIP, display_rgba(sampler2d_channel_apply_lut(error, 0, MAGMA_LUT, 3).data, 3, texel_count), level.width, level.height);
|
|
1112
|
-
|
|
1113
|
-
// the bytes the panels show at a texel, in the format's own units — a signed
|
|
1114
|
-
// format's are shown about mid grey and an HDR one's tone-mapped — and FLIP there
|
|
1115
|
-
const shown_channels = Math.min(info.channels, 3);
|
|
1116
|
-
|
|
1117
|
-
describe_texel = (x, y) => {
|
|
1118
|
-
const i = y * level.width + x;
|
|
1119
|
-
|
|
1120
|
-
const bytes = rgba => Array.from(rgba.subarray(i * 4, i * 4 + shown_channels), byte => info.signed ? byte - 128 : byte).join(' ');
|
|
1121
|
-
|
|
1122
|
-
return `texel ${x}, ${y} source ${bytes(truth_rgba)} hardware ${bytes(hardware_rgba)}${info.hdr ? ' (tone-mapped bytes)' : ''} FLIP ${error.data[i].toFixed(4)}`;
|
|
1123
|
-
};
|
|
1124
|
-
|
|
1125
|
-
flip_line = ` FLIP, level 0, hardware picture against the source at ${new FlipParameters().pixels_per_degree.toFixed(1)} pixels per degree:`
|
|
1126
|
-
+ ` mean ${pooled.mean.toFixed(4)}, weighted median ${pooled.weighted_median.toFixed(4)}, max ${pooled.maximum.toFixed(4)}`
|
|
1127
|
-
+ ` (${flip_ms.toFixed(0)} ms)`;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
lines.push('');
|
|
1132
|
-
lines.push(flip_line);
|
|
1133
|
-
|
|
1134
|
-
// 4. the device against the emulator, block by block, on level 0 — the emulator runs a
|
|
1135
|
-
// block in a fraction of a millisecond, which is fine at 512x512 and a hang at 2048
|
|
1136
|
-
lines.push('');
|
|
1137
|
-
|
|
1138
|
-
const emulator_limit = 32768;
|
|
1139
|
-
|
|
1140
|
-
if (base.width * base.height / 16 > emulator_limit) {
|
|
1141
|
-
lines.push(` device against the emulator: skipped above ${emulator_limit} blocks; use the 512x512 source`);
|
|
1142
|
-
} else try {
|
|
1143
|
-
const encode_block = await info.emulate();
|
|
1144
|
-
|
|
1145
|
-
const truth = info.hdr
|
|
1146
|
-
? levels[0].hdr
|
|
1147
|
-
: leading_channels(levels[0].rgba, 4, info.channels, base.width * base.height, info.signed);
|
|
1148
|
-
|
|
1149
|
-
// the helper hands the encoder bytes; a signed encoder reads them as two's complement,
|
|
1150
|
-
// and an HDR level is encoded from its floats directly
|
|
1151
|
-
const emulated = info.hdr
|
|
1152
|
-
? encode_hdr_image(encode_block, truth, base.width, base.height)
|
|
1153
|
-
: encode_image_with_block_encoder(
|
|
1154
|
-
encode_block, info.bytes_per_block, new Uint8Array(truth.buffer), base.width, base.height, info.channels
|
|
1155
|
-
);
|
|
1156
|
-
|
|
1157
|
-
const gpu = texture_encode_level_blocks(result.layout, blocks, 0);
|
|
1158
|
-
|
|
1159
|
-
const bytes_per_block = info.bytes_per_block;
|
|
1160
|
-
const block_count = gpu.length / bytes_per_block;
|
|
1161
|
-
|
|
1162
|
-
let identical = 0;
|
|
1163
|
-
let gpu_better = 0;
|
|
1164
|
-
let gpu_worse = 0;
|
|
1165
|
-
let gpu_equal = 0;
|
|
1166
|
-
|
|
1167
|
-
const gpu_decoded = info.decode(gpu, base.width, base.height);
|
|
1168
|
-
const emulated_decoded = info.decode(emulated, base.width, base.height);
|
|
1169
|
-
|
|
1170
|
-
for (let block = 0; block < block_count; block++) {
|
|
1171
|
-
let same = true;
|
|
1172
|
-
|
|
1173
|
-
for (let i = 0; i < bytes_per_block; i++) {
|
|
1174
|
-
same = same && gpu[block * bytes_per_block + i] === emulated[block * bytes_per_block + i];
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
if (same) {
|
|
1178
|
-
identical++;
|
|
1179
|
-
|
|
1180
|
-
continue;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// a different block is fine if it is no worse; the device's f32 rounds differently
|
|
1184
|
-
const bx = block % Math.ceil(base.width / 4);
|
|
1185
|
-
const by = Math.floor(block / Math.ceil(base.width / 4));
|
|
1186
|
-
|
|
1187
|
-
let gpu_error = 0;
|
|
1188
|
-
let emulated_error = 0;
|
|
1189
|
-
|
|
1190
|
-
for (let y = by * 4; y < Math.min(by * 4 + 4, base.height); y++) {
|
|
1191
|
-
for (let x = bx * 4; x < Math.min(bx * 4 + 4, base.width); x++) {
|
|
1192
|
-
for (let c = 0; c < info.channels; c++) {
|
|
1193
|
-
const at = (y * base.width + x) * info.channels + c;
|
|
1194
|
-
|
|
1195
|
-
gpu_error += (gpu_decoded[at] - truth[at]) ** 2;
|
|
1196
|
-
emulated_error += (emulated_decoded[at] - truth[at]) ** 2;
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
if (gpu_error < emulated_error) {
|
|
1202
|
-
gpu_better++;
|
|
1203
|
-
} else if (gpu_error > emulated_error) {
|
|
1204
|
-
gpu_worse++;
|
|
1205
|
-
} else {
|
|
1206
|
-
gpu_equal++;
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
lines.push(` device against the emulator, level 0: ${identical} of ${block_count} blocks identical;`
|
|
1211
|
-
+ ` of the rest ${gpu_equal} equal in error, ${gpu_better} better on the device, ${gpu_worse} worse`);
|
|
1212
|
-
} catch (error) {
|
|
1213
|
-
lines.push(` emulator comparison skipped: ${error.message}`);
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
report_element.textContent = lines.join('\n');
|
|
1217
|
-
|
|
1218
|
-
/*
|
|
1219
|
-
The hardware's interpolation is not the specification's rounding — the probe below has the
|
|
1220
|
-
exact weights — so a few levels against the CPU decoder on a wide-span block is the
|
|
1221
|
-
decoder's arithmetic, not a wrong block. What a wrong block does is move whole texels by
|
|
1222
|
-
tens, which shows up as the hardware's picture being decibels worse than the CPU decoder's
|
|
1223
|
-
reading of the same bytes. So the verdict is that difference, per level, with nothing finer
|
|
1224
|
-
than a rounding counted.
|
|
1225
|
-
*/
|
|
1226
|
-
const verdict = all_texture_match && hardware_psnr_gap <= 1;
|
|
1227
|
-
|
|
1228
|
-
set_status(
|
|
1229
|
-
verdict
|
|
1230
|
-
? `ok — texture holds the shader's blocks on every level; the hardware's picture is within ${hardware_psnr_gap.toFixed(2)} dB of the CPU decoder's, a rounding forgiven; samples within ${hardware_max} (${hardware_mismatch_total} differ)`
|
|
1231
|
-
: `problem — texture matches buffer: ${all_texture_match}; the hardware's picture is ${hardware_psnr_gap.toFixed(2)} dB under the CPU decoder's, a rounding forgiven; samples up to ${hardware_max} apart`,
|
|
1232
|
-
verdict ? 'pass' : 'fail'
|
|
1233
|
-
);
|
|
1234
|
-
|
|
1235
|
-
globalThis.last_result = result;
|
|
1236
|
-
|
|
1237
|
-
source.destroy();
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
/**
|
|
1241
|
-
* What the hardware's BC4 decoder actually computes, found rather than assumed.
|
|
1242
|
-
*
|
|
1243
|
-
* A 1024x1024 BC4 texture holds every pair of endpoints as one block each, with texels 0..7
|
|
1244
|
-
* carrying indices 0..7 — so a hardware read of the texture is the whole decode table, 256 x
|
|
1245
|
-
* 256 x 8 entries, read back at the precision the sampler delivers rather than rounded to eight
|
|
1246
|
-
* bits. From that the interpolation weight at every index is fitted exactly, and the difference
|
|
1247
|
-
* from the CPU decoder the specs judge with is tabulated. The specification writes the
|
|
1248
|
-
* interpolation in real arithmetic and leaves the precision to the implementation; this is what
|
|
1249
|
-
* one implementation chose.
|
|
1250
|
-
*/
|
|
1251
|
-
async function probe_decoder() {
|
|
1252
|
-
// 65536 pairs at 256 blocks a row is 256 rows of blocks: 1024 texels tall, not 512
|
|
1253
|
-
const width = 1024;
|
|
1254
|
-
const height = 1024;
|
|
1255
|
-
|
|
1256
|
-
const blocks_x = width / 4;
|
|
1257
|
-
|
|
1258
|
-
const blocks = new Uint8Array(blocks_x * (height / 4) * 8);
|
|
1259
|
-
|
|
1260
|
-
for (let first = 0; first < 256; first++) {
|
|
1261
|
-
for (let second = 0; second < 256; second++) {
|
|
1262
|
-
const block = first * 256 + second;
|
|
1263
|
-
|
|
1264
|
-
const at = block * 8;
|
|
1265
|
-
|
|
1266
|
-
blocks[at] = first;
|
|
1267
|
-
blocks[at + 1] = second;
|
|
1268
|
-
|
|
1269
|
-
// texels 0..7 take indices 0..7 in order: eight three-bit fields, 24 bits, 3 bytes
|
|
1270
|
-
const word = 0 | (1 << 3) | (2 << 6) | (3 << 9) | (4 << 12) | (5 << 15) | (6 << 18) | (7 << 21);
|
|
1271
|
-
|
|
1272
|
-
blocks[at + 2] = word & 0xFF;
|
|
1273
|
-
blocks[at + 3] = (word >> 8) & 0xFF;
|
|
1274
|
-
blocks[at + 4] = (word >> 16) & 0xFF;
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
const texture = device.createTexture({
|
|
1279
|
-
label: 'decoder probe',
|
|
1280
|
-
size: [width, height, 1],
|
|
1281
|
-
format: 'bc4-r-unorm',
|
|
1282
|
-
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
|
|
1283
|
-
});
|
|
1284
|
-
|
|
1285
|
-
texture_write_level(device.queue, texture, 0, blocks);
|
|
1286
|
-
|
|
1287
|
-
const exact = await hardware_decode(device, texture, 0, width, height, 'exact_red');
|
|
1288
|
-
|
|
1289
|
-
texture.destroy();
|
|
1290
|
-
|
|
1291
|
-
/**
|
|
1292
|
-
* The hardware's value for one entry of one pair's palette, as the unorm it sampled.
|
|
1293
|
-
*
|
|
1294
|
-
* @param {number} first
|
|
1295
|
-
* @param {number} second
|
|
1296
|
-
* @param {number} index
|
|
1297
|
-
* @returns {number}
|
|
1298
|
-
*/
|
|
1299
|
-
const sampled = (first, second, index) => {
|
|
1300
|
-
const block = first * 256 + second;
|
|
1301
|
-
|
|
1302
|
-
const x = (block % blocks_x) * 4 + (index & 3);
|
|
1303
|
-
const y = Math.floor(block / blocks_x) * 4 + (index >> 2);
|
|
1304
|
-
|
|
1305
|
-
return exact[y * width + x];
|
|
1306
|
-
};
|
|
1307
|
-
|
|
1308
|
-
const lines = [
|
|
1309
|
-
'the hardware BC4 decoder, over every pair of endpoints:',
|
|
1310
|
-
'',
|
|
1311
|
-
' the weight of the second endpoint at each index, fitted from the unrounded samples as',
|
|
1312
|
-
' value = A + (B - A) * w over 16-bit endpoints A = 257a. An interval is the set of weights',
|
|
1313
|
-
' every pair agrees with; an empty one would mean no single weight does.',
|
|
1314
|
-
'',
|
|
1315
|
-
];
|
|
1316
|
-
|
|
1317
|
-
for (const [mode, indices] of [['eight-value (first > second)', [2, 3, 4, 5, 6, 7]], ['six-value (first <= second)', [2, 3, 4, 5]]]) {
|
|
1318
|
-
for (const index of indices) {
|
|
1319
|
-
let low = -Infinity;
|
|
1320
|
-
let high = Infinity;
|
|
1321
|
-
|
|
1322
|
-
for (let first = 0; first < 256; first += 3) {
|
|
1323
|
-
for (let second = 0; second < 256; second += 3) {
|
|
1324
|
-
const eight = first > second;
|
|
1325
|
-
const wanted_eight = indices.length === 6;
|
|
1326
|
-
|
|
1327
|
-
if (eight !== wanted_eight || Math.abs(first - second) < 32) {
|
|
1328
|
-
continue;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
const A = first * 257;
|
|
1332
|
-
const B = second * 257;
|
|
1333
|
-
|
|
1334
|
-
const w = (sampled(first, second, index) * 65535 - A) / (B - A);
|
|
1335
|
-
|
|
1336
|
-
// the 16-bit value is within half a unit of what the hardware computed
|
|
1337
|
-
const tolerance = 0.51 / Math.abs(B - A);
|
|
1338
|
-
|
|
1339
|
-
low = Math.max(low, w - tolerance);
|
|
1340
|
-
high = Math.min(high, w + tolerance);
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
const mid = (low + high) / 2;
|
|
1345
|
-
|
|
1346
|
-
lines.push(
|
|
1347
|
-
` ${mode} index ${index}: w = ${mid.toFixed(6)}`
|
|
1348
|
-
+ ` (x257 = ${(mid * 257).toFixed(2)}, the specification's ${(indices.length === 6 ? (index - 1) / 7 : (index - 1) / 5).toFixed(6)})`
|
|
1349
|
-
+ (high < low ? ' EMPTY' : '')
|
|
1350
|
-
);
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
// and, rounded to eight bits, how far that is from the CPU decoder the specs judge with
|
|
1355
|
-
const histogram = new Map();
|
|
1356
|
-
|
|
1357
|
-
for (let first = 0; first < 256; first++) {
|
|
1358
|
-
for (let second = 0; second < 256; second++) {
|
|
1359
|
-
const block = new Uint8Array(8);
|
|
1360
|
-
|
|
1361
|
-
block[0] = first;
|
|
1362
|
-
block[1] = second;
|
|
1363
|
-
block[2] = 0b10001000;
|
|
1364
|
-
block[3] = 0b11000110;
|
|
1365
|
-
block[4] = 0b11111010;
|
|
1366
|
-
|
|
1367
|
-
const reference = decode_bc4(block, 4, 2);
|
|
1368
|
-
|
|
1369
|
-
for (let index = 0; index < 8; index++) {
|
|
1370
|
-
const delta = Math.round(sampled(first, second, index) * 255) - reference[index];
|
|
1371
|
-
|
|
1372
|
-
histogram.set(delta, (histogram.get(delta) ?? 0) + 1);
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
const total = 65536 * 8;
|
|
1378
|
-
|
|
1379
|
-
lines.push('');
|
|
1380
|
-
lines.push(' hardware, rounded to eight bits, minus the CPU decoder, over every pair and index:');
|
|
1381
|
-
|
|
1382
|
-
for (const delta of [...histogram.keys()].sort((a, b) => a - b)) {
|
|
1383
|
-
lines.push(` ${String(delta).padStart(3)}: ${String(histogram.get(delta)).padStart(7)} (${(100 * histogram.get(delta) / total).toFixed(2)}%)`);
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
report_element.textContent = lines.join('\n');
|
|
1387
|
-
|
|
1388
|
-
set_status('decoder probe done', 'pass');
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
element('probe').addEventListener('click', () => {
|
|
1392
|
-
probe_decoder().catch(error => {
|
|
1393
|
-
set_status(error.message, 'fail');
|
|
1394
|
-
|
|
1395
|
-
console.error(error);
|
|
1396
|
-
});
|
|
1397
|
-
});
|
|
1398
|
-
|
|
1399
|
-
element('run').addEventListener('click', () => {
|
|
1400
|
-
const choice = element('source').value;
|
|
1401
|
-
|
|
1402
|
-
const kind = choice.startsWith('correlated') ? 'correlated'
|
|
1403
|
-
: choice.startsWith('normal') ? 'normal'
|
|
1404
|
-
: choice.startsWith('hdr') ? 'hdr'
|
|
1405
|
-
: 'independent';
|
|
1406
|
-
const signed = FORMATS[element('format').value]?.signed === true;
|
|
1407
|
-
|
|
1408
|
-
const base = uploaded_source ?? synthetic_source(Number(choice.replace(/[a-z]+/, '')), kind, signed);
|
|
1409
|
-
|
|
1410
|
-
run(base).catch(error => {
|
|
1411
|
-
set_status(error.message, 'fail');
|
|
1412
|
-
|
|
1413
|
-
console.error(error);
|
|
1414
|
-
});
|
|
1415
|
-
});
|
|
1416
|
-
|
|
1417
|
-
element('file').addEventListener('change', async event => {
|
|
1418
|
-
const file = event.target.files[0];
|
|
1419
|
-
|
|
1420
|
-
if (file === undefined) {
|
|
1421
|
-
return;
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
uploaded_source = await file_source(file);
|
|
1425
|
-
|
|
1426
|
-
set_status(`${file.name}: ${uploaded_source.width}x${uploaded_source.height} ready`, 'pass');
|
|
1427
|
-
});
|
|
1428
|
-
|
|
1429
|
-
set_status('ready', 'pass');
|
|
1430
|
-
}
|
|
1
|
+
import { MAGMA_LUT } from "../../../core/color/colormap/MAGMA_LUT.js";
|
|
2
|
+
import { FlipParameters } from "../../../engine/graphics/texture/sampler/compare/flip/FlipParameters.js";
|
|
3
|
+
import { flip_pool } from "../../../engine/graphics/texture/sampler/compare/flip/flip_pool.js";
|
|
4
|
+
import { sampler2d_flip_ldr } from "../../../engine/graphics/texture/sampler/compare/flip/sampler2d_flip_ldr.js";
|
|
5
|
+
import { Sampler2D } from "../../../engine/graphics/texture/sampler/Sampler2D.js";
|
|
6
|
+
import { sampler2d_channel_apply_lut } from "../../../engine/graphics/texture/sampler/sampler2d_channel_apply_lut.js";
|
|
7
|
+
import {
|
|
8
|
+
sampler2d_max_absolute_difference
|
|
9
|
+
} from "../../../engine/graphics/texture/sampler/compare/sampler2d_max_absolute_difference.js";
|
|
10
|
+
import { sampler2d_psnr } from "../../../engine/graphics/texture/sampler/compare/sampler2d_psnr.js";
|
|
11
|
+
import {
|
|
12
|
+
decode_astc_4x4,
|
|
13
|
+
decode_bc1,
|
|
14
|
+
decode_bc4,
|
|
15
|
+
decode_bc4_snorm,
|
|
16
|
+
decode_bc5,
|
|
17
|
+
decode_bc5_snorm,
|
|
18
|
+
decode_bc6h_ufloat,
|
|
19
|
+
decode_bc7
|
|
20
|
+
} from "../../../format/texture/basis/__fixtures__/decode_blocks.js";
|
|
21
|
+
import { ShadeGPUCommandContext } from "../../device/ShadeGPUCommandContext.js";
|
|
22
|
+
import { GraphicsContext } from "../../renderer/GraphicsContext.js";
|
|
23
|
+
import {
|
|
24
|
+
block_bytes,
|
|
25
|
+
encode_image_with_block_encoder
|
|
26
|
+
} from "../../renderer/texture/encode/__fixtures__/encode_image_with_block_encoder.js";
|
|
27
|
+
import {
|
|
28
|
+
color_correlated_image,
|
|
29
|
+
hdr_image,
|
|
30
|
+
mask_image,
|
|
31
|
+
normal_map_image,
|
|
32
|
+
rough_image,
|
|
33
|
+
smooth_image
|
|
34
|
+
} from "../../renderer/texture/encode/__fixtures__/encode_test_images.js";
|
|
35
|
+
import { chunk_astc_encode_block_4x4_rgb } from "../../renderer/texture/encode/chunk_astc_encode_block_4x4_rgb.js";
|
|
36
|
+
import { chunk_bc1_encode_block } from "../../renderer/texture/encode/chunk_bc1_encode_block.js";
|
|
37
|
+
import { chunk_bc4_encode_block } from "../../renderer/texture/encode/chunk_bc4_encode_block.js";
|
|
38
|
+
import { chunk_bc6h_encode_block_mode11 } from "../../renderer/texture/encode/chunk_bc6h_encode_block_mode11.js";
|
|
39
|
+
import { chunk_bc7_encode_block } from "../../renderer/texture/encode/chunk_bc7_encode_block.js";
|
|
40
|
+
import { texture_encode } from "../../renderer/texture/encode/texture_encode.js";
|
|
41
|
+
import { texture_encode_level_blocks } from "../../renderer/texture/encode/texture_encode_layout.js";
|
|
42
|
+
import { gpu_texture_compression_support } from "../../renderer/texture/format/gpu_texture_compression_support.js";
|
|
43
|
+
import { texture_write_level } from "../../renderer/texture/texture_write_level.js";
|
|
44
|
+
import { texture_mip_level_count } from "../../util/texture_mip_extent.js";
|
|
45
|
+
import { ComputeShaderEmulator } from "../../wgsl/emulator/ComputeShaderEmulator.js";
|
|
46
|
+
import { create_panel_group } from "./panel_view.js";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The GPU block encoder on a real device.
|
|
50
|
+
*
|
|
51
|
+
* **Drives the device directly, without the renderer.** The renderer's frame loop needs immediate
|
|
52
|
+
* data, which not every browser build offers, and nothing here needs a frame: the encoder is a
|
|
53
|
+
* dispatch and some copies recorded into a command context, and the proof is read back. What the
|
|
54
|
+
* device-free tiers established — the encoder's quality, the layout, the wiring — is taken as read;
|
|
55
|
+
* what this page adds is everything only hardware can answer:
|
|
56
|
+
*
|
|
57
|
+
* 1. `createTexture` accepts the block format, and `copyBufferToTexture` accepts the padded rows.
|
|
58
|
+
* 2. The blocks in the texture are the blocks the shader wrote — read back with
|
|
59
|
+
* `copyTextureToBuffer` and compared byte for byte with the block buffer.
|
|
60
|
+
* 3. The hardware decodes them to the texels the test decoder predicts — read back through a
|
|
61
|
+
* `textureLoad` pass, which is the GPU's own BC decoder speaking.
|
|
62
|
+
* 4. The device's f32 arithmetic lands on the blocks the f64 emulator chose, or on blocks no worse.
|
|
63
|
+
*
|
|
64
|
+
* Every number on the page is against the source image, through the quality harness the transcoders
|
|
65
|
+
* were held to.
|
|
66
|
+
*
|
|
67
|
+
* @author Alex Goldring
|
|
68
|
+
* @copyright Company Named Limited (c) 2026
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} id
|
|
73
|
+
* @returns {HTMLElement}
|
|
74
|
+
*/
|
|
75
|
+
function element(id) {
|
|
76
|
+
return document.getElementById(id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Side of each of the three panels, CSS pixels
|
|
81
|
+
* @type {number}
|
|
82
|
+
*/
|
|
83
|
+
const PANEL_SIZE = 384;
|
|
84
|
+
|
|
85
|
+
const PANEL_SOURCE = 0;
|
|
86
|
+
const PANEL_HARDWARE = 1;
|
|
87
|
+
const PANEL_FLIP = 2;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* How long to wait for a frame before going on without one, milliseconds. A document that is not
|
|
91
|
+
* shown runs no frames, and a run should not stand still until its tab is looked at.
|
|
92
|
+
*
|
|
93
|
+
* @type {number}
|
|
94
|
+
*/
|
|
95
|
+
const PAINT_FALLBACK_MS = 250;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolves once the page has painted, or once the fallback has passed. A status set right before a
|
|
99
|
+
* long synchronous pass is never seen unless a frame goes by first.
|
|
100
|
+
*
|
|
101
|
+
* @returns {Promise<void>}
|
|
102
|
+
*/
|
|
103
|
+
function painted() {
|
|
104
|
+
return new Promise(resolve => {
|
|
105
|
+
const fallback = setTimeout(resolve, PAINT_FALLBACK_MS);
|
|
106
|
+
|
|
107
|
+
requestAnimationFrame(() => setTimeout(() => {
|
|
108
|
+
clearTimeout(fallback);
|
|
109
|
+
resolve();
|
|
110
|
+
}, 0));
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* One level of a source image, as bytes and as the sampler the harness compares with.
|
|
116
|
+
*
|
|
117
|
+
* @typedef {Object} SourceLevel
|
|
118
|
+
* @property {number} width
|
|
119
|
+
* @property {number} height
|
|
120
|
+
* @property {Uint8Array} rgba `width * height * 4` bytes of the texel format — `rgba8unorm`,
|
|
121
|
+
* `rgba8snorm`, or the half floats of `rgba16float` for an HDR level, in which case it is
|
|
122
|
+
* `width * height * 8` bytes
|
|
123
|
+
* @property {Float32Array} [hdr] `width * height * 3` linear RGB floats, present for an HDR level
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A 2x2 box filter, per channel, rounded — the mip chain a source is given so every level of the
|
|
128
|
+
* encode has a truth to be judged against.
|
|
129
|
+
*
|
|
130
|
+
* @param {SourceLevel} level
|
|
131
|
+
* @returns {SourceLevel}
|
|
132
|
+
*/
|
|
133
|
+
function halve(level) {
|
|
134
|
+
const width = Math.max(1, level.width >> 1);
|
|
135
|
+
const height = Math.max(1, level.height >> 1);
|
|
136
|
+
|
|
137
|
+
if (level.hdr !== undefined) {
|
|
138
|
+
const hdr = new Float32Array(width * height * 3);
|
|
139
|
+
|
|
140
|
+
for (let y = 0; y < height; y++) {
|
|
141
|
+
for (let x = 0; x < width; x++) {
|
|
142
|
+
for (let c = 0; c < 3; c++) {
|
|
143
|
+
const x0 = Math.min(x * 2, level.width - 1);
|
|
144
|
+
const x1 = Math.min(x * 2 + 1, level.width - 1);
|
|
145
|
+
const y0 = Math.min(y * 2, level.height - 1);
|
|
146
|
+
const y1 = Math.min(y * 2 + 1, level.height - 1);
|
|
147
|
+
|
|
148
|
+
hdr[(y * width + x) * 3 + c] = (
|
|
149
|
+
level.hdr[(y0 * level.width + x0) * 3 + c]
|
|
150
|
+
+ level.hdr[(y0 * level.width + x1) * 3 + c]
|
|
151
|
+
+ level.hdr[(y1 * level.width + x0) * 3 + c]
|
|
152
|
+
+ level.hdr[(y1 * level.width + x1) * 3 + c]
|
|
153
|
+
) / 4;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { width, height, hdr, rgba: hdr_bytes(hdr, width * height) };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const rgba = new Uint8Array(width * height * 4);
|
|
162
|
+
|
|
163
|
+
for (let y = 0; y < height; y++) {
|
|
164
|
+
for (let x = 0; x < width; x++) {
|
|
165
|
+
for (let c = 0; c < 4; c++) {
|
|
166
|
+
const x0 = Math.min(x * 2, level.width - 1);
|
|
167
|
+
const x1 = Math.min(x * 2 + 1, level.width - 1);
|
|
168
|
+
const y0 = Math.min(y * 2, level.height - 1);
|
|
169
|
+
const y1 = Math.min(y * 2 + 1, level.height - 1);
|
|
170
|
+
|
|
171
|
+
const sum = level.rgba[(y0 * level.width + x0) * 4 + c]
|
|
172
|
+
+ level.rgba[(y0 * level.width + x1) * 4 + c]
|
|
173
|
+
+ level.rgba[(y1 * level.width + x0) * 4 + c]
|
|
174
|
+
+ level.rgba[(y1 * level.width + x1) * 4 + c];
|
|
175
|
+
|
|
176
|
+
rgba[(y * width + x) * 4 + c] = Math.round(sum / 4);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { width, height, rgba };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The bytes of an `rgba16float` level: each channel a half, alpha one.
|
|
186
|
+
*
|
|
187
|
+
* @param {Float32Array} hdr `texels * 3` linear floats
|
|
188
|
+
* @param {number} texels
|
|
189
|
+
* @returns {Uint8Array} `texels * 8`
|
|
190
|
+
*/
|
|
191
|
+
function hdr_bytes(hdr, texels) {
|
|
192
|
+
const halves = new Float16Array(texels * 4);
|
|
193
|
+
|
|
194
|
+
for (let i = 0; i < texels; i++) {
|
|
195
|
+
halves[i * 4] = hdr[i * 3];
|
|
196
|
+
halves[i * 4 + 1] = hdr[i * 3 + 1];
|
|
197
|
+
halves[i * 4 + 2] = hdr[i * 3 + 2];
|
|
198
|
+
halves[i * 4 + 3] = 1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return new Uint8Array(halves.buffer);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The half-float bit patterns of linear values, which is the domain BC6H's error lives in.
|
|
206
|
+
*
|
|
207
|
+
* @param {Float32Array} values
|
|
208
|
+
* @returns {Uint16Array}
|
|
209
|
+
*/
|
|
210
|
+
function half_bits_of(values) {
|
|
211
|
+
const halves = new Float16Array(values.length);
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < values.length; i++) {
|
|
214
|
+
halves[i] = Math.min(65504, Math.max(0, values[i]));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return new Uint16Array(halves.buffer);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @param {SourceLevel} base
|
|
222
|
+
* @returns {SourceLevel[]} the full chain, base first
|
|
223
|
+
*/
|
|
224
|
+
function mip_chain(base) {
|
|
225
|
+
const levels = [base];
|
|
226
|
+
|
|
227
|
+
const count = texture_mip_level_count(base.width, base.height, 1, "2d");
|
|
228
|
+
|
|
229
|
+
for (let level = 1; level < count; level++) {
|
|
230
|
+
levels.push(halve(levels[level - 1]));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return levels;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The three fixture images the specs measure with, one per channel, at a size worth timing —
|
|
238
|
+
* three channels with nothing in common, which is the hard case for a colour format. Or, with
|
|
239
|
+
* `correlated`, the colour fixture whose channels move together, which is what a photograph is. Or,
|
|
240
|
+
* with `normal`, a tangent-space normal map in red and green — as unsigned bytes, or as the signed
|
|
241
|
+
* bytes a signed target reads, so the picture is the same picture either way rather than a ramp
|
|
242
|
+
* wrapped into a cliff by a change of sign.
|
|
243
|
+
*
|
|
244
|
+
* @param {number} size
|
|
245
|
+
* @param {'independent'|'correlated'|'normal'} kind
|
|
246
|
+
* @param {boolean} signed
|
|
247
|
+
* @returns {SourceLevel}
|
|
248
|
+
*/
|
|
249
|
+
function synthetic_source(size, kind, signed) {
|
|
250
|
+
const rgba = new Uint8Array(size * size * 4);
|
|
251
|
+
|
|
252
|
+
if (kind === 'hdr') {
|
|
253
|
+
const hdr = hdr_image(size, size);
|
|
254
|
+
|
|
255
|
+
return { width: size, height: size, hdr, rgba: hdr_bytes(hdr, size * size) };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (kind === 'normal') {
|
|
259
|
+
const normals = normal_map_image(size, size, 3);
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < size * size; i++) {
|
|
262
|
+
for (let c = 0; c < 2; c++) {
|
|
263
|
+
const unorm = normals[i * 2 + c];
|
|
264
|
+
|
|
265
|
+
// the byte a signed format stores for the same direction: [-127, 127], wrapped
|
|
266
|
+
rgba[i * 4 + c] = signed ? (Math.round((unorm / 255) * 254 - 127) & 0xFF) : unorm;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
rgba[i * 4 + 2] = signed ? 127 : 255;
|
|
270
|
+
rgba[i * 4 + 3] = signed ? 127 : 255;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { width: size, height: size, rgba };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (kind === 'correlated') {
|
|
277
|
+
const rgb = color_correlated_image(size, size);
|
|
278
|
+
|
|
279
|
+
for (let i = 0; i < size * size; i++) {
|
|
280
|
+
rgba[i * 4] = rgb[i * 3];
|
|
281
|
+
rgba[i * 4 + 1] = rgb[i * 3 + 1];
|
|
282
|
+
rgba[i * 4 + 2] = rgb[i * 3 + 2];
|
|
283
|
+
rgba[i * 4 + 3] = 255;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { width: size, height: size, rgba };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const channels = [smooth_image(size, size), rough_image(size, size, 7), mask_image(size, size)];
|
|
290
|
+
|
|
291
|
+
for (let i = 0; i < size * size; i++) {
|
|
292
|
+
rgba[i * 4] = channels[0][i];
|
|
293
|
+
rgba[i * 4 + 1] = channels[1][i];
|
|
294
|
+
rgba[i * 4 + 2] = channels[2][i];
|
|
295
|
+
rgba[i * 4 + 3] = 255;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { width: size, height: size, rgba };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @param {File} file
|
|
303
|
+
* @returns {Promise<SourceLevel>}
|
|
304
|
+
*/
|
|
305
|
+
async function file_source(file) {
|
|
306
|
+
const bitmap = await createImageBitmap(file);
|
|
307
|
+
|
|
308
|
+
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
309
|
+
const context = canvas.getContext('2d');
|
|
310
|
+
|
|
311
|
+
context.drawImage(bitmap, 0, 0);
|
|
312
|
+
|
|
313
|
+
const data = context.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
314
|
+
|
|
315
|
+
return { width: bitmap.width, height: bitmap.height, rgba: new Uint8Array(data.data.buffer) };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The leading `count` channels of interleaved texels, re-interleaved on their own.
|
|
320
|
+
*
|
|
321
|
+
* @param {Uint8Array} data
|
|
322
|
+
* @param {number} stride channels per texel in `data`
|
|
323
|
+
* @param {number} count channels to keep
|
|
324
|
+
* @param {number} texels
|
|
325
|
+
* @returns {Uint8Array} `texels * count` bytes
|
|
326
|
+
*/
|
|
327
|
+
function leading_channels(data, stride, count, texels, signed = false) {
|
|
328
|
+
const out = data instanceof Float32Array
|
|
329
|
+
? new Float32Array(texels * count)
|
|
330
|
+
: signed ? new Int8Array(texels * count) : new Uint8Array(texels * count);
|
|
331
|
+
|
|
332
|
+
for (let i = 0; i < texels; i++) {
|
|
333
|
+
for (let c = 0; c < count; c++) {
|
|
334
|
+
const value = data[i * stride + c];
|
|
335
|
+
|
|
336
|
+
// a signed format's byte is two's complement, and -128 reads as -127
|
|
337
|
+
out[i * count + c] = signed ? Math.max(-127, (value << 24) >> 24) : value;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* What the page knows about each target it can encode to: how many of the source's channels the
|
|
346
|
+
* format carries, how the test decoder reads its blocks back to those channels, and how the same
|
|
347
|
+
* encoder runs in the emulator on the host.
|
|
348
|
+
*
|
|
349
|
+
* @type {Object<string, {channels: number, bytes_per_block: number, decode: (blocks: Uint8Array, width: number, height: number) => Uint8Array, emulate: () => Promise<(values: Uint8Array) => Uint8Array>}>}
|
|
350
|
+
*/
|
|
351
|
+
const FORMATS = {
|
|
352
|
+
'bc4-r-snorm': {
|
|
353
|
+
channels: 1,
|
|
354
|
+
bytes_per_block: 8,
|
|
355
|
+
signed: true,
|
|
356
|
+
decode: (blocks, width, height) => decode_bc4_snorm(blocks, width, height),
|
|
357
|
+
emulate: async () => {
|
|
358
|
+
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_bc4_encode_block);
|
|
359
|
+
|
|
360
|
+
return values => {
|
|
361
|
+
const snorm = new Float32Array(16);
|
|
362
|
+
|
|
363
|
+
for (let i = 0; i < 16; i++) {
|
|
364
|
+
snorm[i] = Math.max(-127, (values[i] << 24) >> 24) / 127;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return block_bytes(lib.bc4_encode_block_snorm(snorm));
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
'bc5-rg-snorm': {
|
|
372
|
+
channels: 2,
|
|
373
|
+
bytes_per_block: 16,
|
|
374
|
+
signed: true,
|
|
375
|
+
decode: (blocks, width, height) => decode_bc5_snorm(blocks, width, height),
|
|
376
|
+
emulate: async () => {
|
|
377
|
+
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_bc4_encode_block);
|
|
378
|
+
|
|
379
|
+
return values => {
|
|
380
|
+
const out = new Uint8Array(16);
|
|
381
|
+
|
|
382
|
+
for (let channel = 0; channel < 2; channel++) {
|
|
383
|
+
const snorm = new Float32Array(16);
|
|
384
|
+
|
|
385
|
+
for (let i = 0; i < 16; i++) {
|
|
386
|
+
snorm[i] = Math.max(-127, (values[i * 2 + channel] << 24) >> 24) / 127;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
out.set(block_bytes(lib.bc4_encode_block_snorm(snorm)), channel * 8);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return out;
|
|
393
|
+
};
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
'bc4-r-unorm': {
|
|
397
|
+
channels: 1,
|
|
398
|
+
bytes_per_block: 8,
|
|
399
|
+
signed: false,
|
|
400
|
+
decode: (blocks, width, height) => decode_bc4(blocks, width, height),
|
|
401
|
+
emulate: async () => {
|
|
402
|
+
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_bc4_encode_block);
|
|
403
|
+
|
|
404
|
+
return values => {
|
|
405
|
+
const unorm = new Float32Array(16);
|
|
406
|
+
|
|
407
|
+
for (let i = 0; i < 16; i++) {
|
|
408
|
+
unorm[i] = values[i] / 255;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return block_bytes(lib.bc4_encode_block(unorm));
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
'bc5-rg-unorm': {
|
|
416
|
+
channels: 2,
|
|
417
|
+
bytes_per_block: 16,
|
|
418
|
+
signed: false,
|
|
419
|
+
decode: (blocks, width, height) => leading_channels(decode_bc5(blocks, width, height), 4, 2, width * height),
|
|
420
|
+
emulate: async () => {
|
|
421
|
+
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_bc4_encode_block);
|
|
422
|
+
|
|
423
|
+
return values => {
|
|
424
|
+
const out = new Uint8Array(16);
|
|
425
|
+
|
|
426
|
+
for (let channel = 0; channel < 2; channel++) {
|
|
427
|
+
const unorm = new Float32Array(16);
|
|
428
|
+
|
|
429
|
+
for (let i = 0; i < 16; i++) {
|
|
430
|
+
unorm[i] = values[i * 2 + channel] / 255;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
out.set(block_bytes(lib.bc4_encode_block(unorm)), channel * 8);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return out;
|
|
437
|
+
};
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
'bc6h-rgb-ufloat': {
|
|
441
|
+
channels: 3,
|
|
442
|
+
bytes_per_block: 16,
|
|
443
|
+
signed: false,
|
|
444
|
+
hdr: true,
|
|
445
|
+
decode: (blocks, width, height) => decode_bc6h_ufloat(blocks, width, height),
|
|
446
|
+
emulate: async () => {
|
|
447
|
+
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_bc6h_encode_block_mode11);
|
|
448
|
+
|
|
449
|
+
// handed floats rather than bytes: an HDR level's truth is its linear values
|
|
450
|
+
return values => {
|
|
451
|
+
const texels = [];
|
|
452
|
+
|
|
453
|
+
for (let i = 0; i < 16; i++) {
|
|
454
|
+
texels.push(new Float32Array([values[i * 3], values[i * 3 + 1], values[i * 3 + 2]]));
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return block_bytes(lib.bc6h_encode_block_mode11(texels));
|
|
458
|
+
};
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
'bc7-rgba-unorm': {
|
|
462
|
+
channels: 4,
|
|
463
|
+
bytes_per_block: 16,
|
|
464
|
+
signed: false,
|
|
465
|
+
decode: (blocks, width, height) => decode_bc7(blocks, width, height),
|
|
466
|
+
emulate: async () => {
|
|
467
|
+
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_bc7_encode_block);
|
|
468
|
+
|
|
469
|
+
return values => {
|
|
470
|
+
const texels = [];
|
|
471
|
+
|
|
472
|
+
for (let i = 0; i < 16; i++) {
|
|
473
|
+
texels.push(new Float32Array([values[i * 4] / 255, values[i * 4 + 1] / 255, values[i * 4 + 2] / 255, values[i * 4 + 3] / 255]));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return block_bytes(lib.bc7_encode_block(texels));
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
'astc-4x4-unorm': {
|
|
481
|
+
channels: 3,
|
|
482
|
+
bytes_per_block: 16,
|
|
483
|
+
signed: false,
|
|
484
|
+
decode: (blocks, width, height) => {
|
|
485
|
+
const blocks_x = Math.ceil(width / 4);
|
|
486
|
+
const blocks_y = Math.ceil(height / 4);
|
|
487
|
+
|
|
488
|
+
const out = new Uint8Array(width * height * 3);
|
|
489
|
+
|
|
490
|
+
for (let by = 0; by < blocks_y; by++) {
|
|
491
|
+
for (let bx = 0; bx < blocks_x; bx++) {
|
|
492
|
+
const texels = decode_astc_4x4(blocks, (by * blocks_x + bx) * 16);
|
|
493
|
+
|
|
494
|
+
if (texels === null) {
|
|
495
|
+
throw new Error(`block ${by * blocks_x + bx} is not the shape the decoder reads`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
for (let i = 0; i < 16; i++) {
|
|
499
|
+
const x = bx * 4 + (i & 3);
|
|
500
|
+
const y = by * 4 + (i >> 2);
|
|
501
|
+
|
|
502
|
+
if (x < width && y < height) {
|
|
503
|
+
out.set(texels[i], (y * width + x) * 3);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return out;
|
|
510
|
+
},
|
|
511
|
+
emulate: async () => {
|
|
512
|
+
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_astc_encode_block_4x4_rgb);
|
|
513
|
+
|
|
514
|
+
return values => {
|
|
515
|
+
const texels = [];
|
|
516
|
+
|
|
517
|
+
for (let i = 0; i < 16; i++) {
|
|
518
|
+
texels.push(new Float32Array([values[i * 3] / 255, values[i * 3 + 1] / 255, values[i * 3 + 2] / 255]));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return block_bytes(lib.astc_encode_block_4x4_rgb(texels));
|
|
522
|
+
};
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
'bc1-rgba-unorm': {
|
|
526
|
+
channels: 3,
|
|
527
|
+
bytes_per_block: 8,
|
|
528
|
+
signed: false,
|
|
529
|
+
decode: (blocks, width, height) => leading_channels(decode_bc1(blocks, width, height), 4, 3, width * height),
|
|
530
|
+
emulate: async () => {
|
|
531
|
+
const lib = await ComputeShaderEmulator.fromCodeChunk(chunk_bc1_encode_block);
|
|
532
|
+
|
|
533
|
+
return values => {
|
|
534
|
+
const texels = [];
|
|
535
|
+
|
|
536
|
+
for (let i = 0; i < 16; i++) {
|
|
537
|
+
texels.push(new Float32Array([values[i * 3] / 255, values[i * 3 + 1] / 255, values[i * 3 + 2] / 255]));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return block_bytes(lib.bc1_encode_block(texels));
|
|
541
|
+
};
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Read one level of a block-compressed texture back through the hardware's decoder: a
|
|
548
|
+
* `textureLoad` per texel into a buffer of words.
|
|
549
|
+
*
|
|
550
|
+
* @param {GPUDevice} device
|
|
551
|
+
* @param {GPUTexture} texture
|
|
552
|
+
* @param {number} level
|
|
553
|
+
* @param {number} width
|
|
554
|
+
* @param {number} height
|
|
555
|
+
* @param {'bytes'|'signed'|'float'|'exact_red'} mode `bytes` answers every channel rounded to
|
|
556
|
+
* eight-bit levels, `signed` the same as signed bytes, `float` every channel as the f32 the
|
|
557
|
+
* sampler delivers, and `exact_red` the red channel alone as f32 — the decoder produces more
|
|
558
|
+
* precision than eight bits, and the probe wants it
|
|
559
|
+
* @returns {Promise<Uint8Array|Int8Array|Float32Array>} interleaved RGBA, `width * height * 4`
|
|
560
|
+
* samples; or the red channel's `width * height` floats
|
|
561
|
+
*/
|
|
562
|
+
async function hardware_decode(device, texture, level, width, height, mode) {
|
|
563
|
+
const per_texel = mode === 'float' ? 4 : 1;
|
|
564
|
+
|
|
565
|
+
const store = {
|
|
566
|
+
bytes: 'out[at] = pack4x8unorm(texel);',
|
|
567
|
+
signed: 'out[at] = pack4x8snorm(texel);',
|
|
568
|
+
exact_red: 'out[at] = bitcast<u32>(texel.r);',
|
|
569
|
+
float: 'out[at * 4u] = bitcast<u32>(texel.r); out[at * 4u + 1u] = bitcast<u32>(texel.g); out[at * 4u + 2u] = bitcast<u32>(texel.b); out[at * 4u + 3u] = bitcast<u32>(texel.a);',
|
|
570
|
+
}[mode];
|
|
571
|
+
|
|
572
|
+
const module = device.createShaderModule({
|
|
573
|
+
code: `
|
|
574
|
+
@group(0) @binding(0) var tex: texture_2d<f32>;
|
|
575
|
+
@group(0) @binding(1) var<storage, read_write> out: array<u32>;
|
|
576
|
+
@group(0) @binding(2) var<uniform> level: vec4<u32>;
|
|
577
|
+
|
|
578
|
+
@compute @workgroup_size(8, 8, 1)
|
|
579
|
+
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
580
|
+
let size = textureDimensions(tex, i32(level.x));
|
|
581
|
+
|
|
582
|
+
if (id.x >= size.x || id.y >= size.y) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let texel = textureLoad(tex, vec2<i32>(id.xy), i32(level.x));
|
|
587
|
+
let at = id.y * size.x + id.x;
|
|
588
|
+
|
|
589
|
+
${store}
|
|
590
|
+
}
|
|
591
|
+
`,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const pipeline = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'main' } });
|
|
595
|
+
|
|
596
|
+
const byte_length = width * height * 4 * per_texel;
|
|
597
|
+
|
|
598
|
+
const out = device.createBuffer({ size: byte_length, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC });
|
|
599
|
+
const uniform = device.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
|
600
|
+
const readback = device.createBuffer({ size: byte_length, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
|
|
601
|
+
|
|
602
|
+
device.queue.writeBuffer(uniform, 0, new Uint32Array([level, 0, 0, 0]));
|
|
603
|
+
|
|
604
|
+
const bind_group = device.createBindGroup({
|
|
605
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
606
|
+
entries: [
|
|
607
|
+
{ binding: 0, resource: texture.createView() },
|
|
608
|
+
{ binding: 1, resource: { buffer: out } },
|
|
609
|
+
{ binding: 2, resource: { buffer: uniform } },
|
|
610
|
+
],
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const encoder = device.createCommandEncoder();
|
|
614
|
+
|
|
615
|
+
const pass = encoder.beginComputePass();
|
|
616
|
+
|
|
617
|
+
pass.setPipeline(pipeline);
|
|
618
|
+
pass.setBindGroup(0, bind_group);
|
|
619
|
+
pass.dispatchWorkgroups(Math.ceil(width / 8), Math.ceil(height / 8), 1);
|
|
620
|
+
pass.end();
|
|
621
|
+
|
|
622
|
+
encoder.copyBufferToBuffer(out, 0, readback, 0, byte_length);
|
|
623
|
+
|
|
624
|
+
device.queue.submit([encoder.finish()]);
|
|
625
|
+
|
|
626
|
+
await readback.mapAsync(GPUMapMode.READ);
|
|
627
|
+
|
|
628
|
+
const range = readback.getMappedRange().slice(0);
|
|
629
|
+
|
|
630
|
+
readback.unmap();
|
|
631
|
+
|
|
632
|
+
out.destroy();
|
|
633
|
+
uniform.destroy();
|
|
634
|
+
readback.destroy();
|
|
635
|
+
|
|
636
|
+
// the packing puts red in the low byte, which is where a little-endian byte view starts
|
|
637
|
+
if (mode === 'bytes') {
|
|
638
|
+
return new Uint8Array(range);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (mode === 'signed') {
|
|
642
|
+
return new Int8Array(range);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return new Float32Array(range);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* @param {GPUDevice} device
|
|
650
|
+
* @param {GPUBuffer} source
|
|
651
|
+
* @param {number} byte_length
|
|
652
|
+
* @returns {Promise<Uint8Array>}
|
|
653
|
+
*/
|
|
654
|
+
async function read_buffer(device, source, byte_length) {
|
|
655
|
+
const readback = device.createBuffer({ size: byte_length, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
|
|
656
|
+
|
|
657
|
+
const encoder = device.createCommandEncoder();
|
|
658
|
+
|
|
659
|
+
encoder.copyBufferToBuffer(source, 0, readback, 0, byte_length);
|
|
660
|
+
|
|
661
|
+
device.queue.submit([encoder.finish()]);
|
|
662
|
+
|
|
663
|
+
await readback.mapAsync(GPUMapMode.READ);
|
|
664
|
+
|
|
665
|
+
const bytes = new Uint8Array(readback.getMappedRange().slice(0));
|
|
666
|
+
|
|
667
|
+
readback.unmap();
|
|
668
|
+
readback.destroy();
|
|
669
|
+
|
|
670
|
+
return bytes;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* The blocks of one level of a compressed texture, read back with the copy the encoder's copy is the
|
|
675
|
+
* inverse of.
|
|
676
|
+
*
|
|
677
|
+
* @param {GPUDevice} device
|
|
678
|
+
* @param {GPUTexture} texture
|
|
679
|
+
* @param {TextureEncodeLevel} level
|
|
680
|
+
* @param {number} bytes_per_block
|
|
681
|
+
* @returns {Promise<Uint8Array>} tightly packed
|
|
682
|
+
*/
|
|
683
|
+
async function read_texture_level(device, texture, level, bytes_per_block) {
|
|
684
|
+
const padded = level.bytes_per_row * level.blocks_y;
|
|
685
|
+
|
|
686
|
+
const readback = device.createBuffer({ size: padded, usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST });
|
|
687
|
+
|
|
688
|
+
const encoder = device.createCommandEncoder();
|
|
689
|
+
|
|
690
|
+
// sized in whole blocks, as the encoder's own copy is — the device refuses a 2x2 extent
|
|
691
|
+
encoder.copyTextureToBuffer(
|
|
692
|
+
{ texture, mipLevel: level.level },
|
|
693
|
+
{ buffer: readback, bytesPerRow: level.bytes_per_row, rowsPerImage: level.blocks_y },
|
|
694
|
+
[level.copy_width, level.copy_height, 1]
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
device.queue.submit([encoder.finish()]);
|
|
698
|
+
|
|
699
|
+
await readback.mapAsync(GPUMapMode.READ);
|
|
700
|
+
|
|
701
|
+
const bytes = new Uint8Array(readback.getMappedRange().slice(0));
|
|
702
|
+
|
|
703
|
+
readback.unmap();
|
|
704
|
+
readback.destroy();
|
|
705
|
+
|
|
706
|
+
const row_bytes = level.blocks_x * bytes_per_block;
|
|
707
|
+
|
|
708
|
+
const out = new Uint8Array(row_bytes * level.blocks_y);
|
|
709
|
+
|
|
710
|
+
for (let row = 0; row < level.blocks_y; row++) {
|
|
711
|
+
out.set(bytes.subarray(row * level.bytes_per_row, row * level.bytes_per_row + row_bytes), row * row_bytes);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return out;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Linear HDR values as display bytes: a Reinhard curve and a display gamma, so the sun clips and
|
|
719
|
+
* the shadows show.
|
|
720
|
+
*
|
|
721
|
+
* @param {Float32Array} values
|
|
722
|
+
* @returns {Uint8Array}
|
|
723
|
+
*/
|
|
724
|
+
function tonemap(values) {
|
|
725
|
+
const out = new Uint8Array(values.length);
|
|
726
|
+
|
|
727
|
+
for (let i = 0; i < values.length; i++) {
|
|
728
|
+
const v = Math.max(0, values[i]);
|
|
729
|
+
|
|
730
|
+
out[i] = Math.round(Math.min(1, (v / (1 + v)) ** (1 / 2.2)) * 255);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* An HDR image through a block encoder that takes forty-eight floats, edge texels repeated.
|
|
738
|
+
*
|
|
739
|
+
* @param {(values: Float32Array) => Uint8Array} encode_block
|
|
740
|
+
* @param {Float32Array} image `width * height * 3`
|
|
741
|
+
* @param {number} width
|
|
742
|
+
* @param {number} height
|
|
743
|
+
* @returns {Uint8Array} tightly packed blocks
|
|
744
|
+
*/
|
|
745
|
+
function encode_hdr_image(encode_block, image, width, height) {
|
|
746
|
+
const blocks_x = Math.ceil(width / 4);
|
|
747
|
+
const blocks_y = Math.ceil(height / 4);
|
|
748
|
+
|
|
749
|
+
const out = new Uint8Array(blocks_x * blocks_y * 16);
|
|
750
|
+
const values = new Float32Array(48);
|
|
751
|
+
|
|
752
|
+
for (let by = 0; by < blocks_y; by++) {
|
|
753
|
+
for (let bx = 0; bx < blocks_x; bx++) {
|
|
754
|
+
for (let i = 0; i < 16; i++) {
|
|
755
|
+
const x = Math.min(bx * 4 + (i & 3), width - 1);
|
|
756
|
+
const y = Math.min(by * 4 + (i >> 2), height - 1);
|
|
757
|
+
|
|
758
|
+
for (let c = 0; c < 3; c++) {
|
|
759
|
+
values[i * 3 + c] = image[(y * width + x) * 3 + c];
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
out.set(encode_block(values), (by * blocks_x + bx) * 16);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return out;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* The bytes a canvas shows for one, two or three interleaved channels: one as grey, two as red and
|
|
772
|
+
* green, three as colour — opaque RGBA, which is also what FLIP compares, since FLIP judges what is
|
|
773
|
+
* displayed.
|
|
774
|
+
*
|
|
775
|
+
* @param {ArrayLike<number>} data
|
|
776
|
+
* @param {number} channels
|
|
777
|
+
* @param {number} texels
|
|
778
|
+
* @param {number} gain
|
|
779
|
+
* @param {boolean} signed values in [-127, 127], shown with zero at mid grey
|
|
780
|
+
* @returns {Uint8Array} `texels * 4`
|
|
781
|
+
*/
|
|
782
|
+
function display_rgba(data, channels, texels, gain = 1, signed = false) {
|
|
783
|
+
const out = new Uint8Array(texels * 4);
|
|
784
|
+
|
|
785
|
+
for (let i = 0; i < texels; i++) {
|
|
786
|
+
for (let c = 0; c < 3; c++) {
|
|
787
|
+
const source = channels === 1 ? 0 : c;
|
|
788
|
+
|
|
789
|
+
const value = source < channels ? data[i * channels + source] : (signed ? -128 : 0);
|
|
790
|
+
|
|
791
|
+
out[i * 4 + c] = Math.min(255, (signed ? value + 128 : value) * gain);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
out[i * 4 + 3] = 255;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return out;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Stand the page up.
|
|
802
|
+
*
|
|
803
|
+
* @returns {Promise<void>}
|
|
804
|
+
*/
|
|
805
|
+
export async function start() {
|
|
806
|
+
const status_element = element('status');
|
|
807
|
+
const report_element = element('report');
|
|
808
|
+
const readout_element = element('readout');
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* What each panel holds at a texel, for the line under the panels; set by the run that drew
|
|
812
|
+
* them, null before the first.
|
|
813
|
+
*
|
|
814
|
+
* @type {?function(number, number): string}
|
|
815
|
+
*/
|
|
816
|
+
let describe_texel = null;
|
|
817
|
+
|
|
818
|
+
const panels = create_panel_group({
|
|
819
|
+
canvases: [element('source_view'), element('decoded_view'), element('difference_view')],
|
|
820
|
+
size: PANEL_SIZE,
|
|
821
|
+
on_cursor: texel => {
|
|
822
|
+
readout_element.textContent = texel === null || describe_texel === null ? '' : describe_texel(texel.x, texel.y);
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* @param {string} text
|
|
828
|
+
* @param {string} [kind]
|
|
829
|
+
*/
|
|
830
|
+
function set_status(text, kind = '') {
|
|
831
|
+
status_element.textContent = text;
|
|
832
|
+
status_element.className = kind;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (navigator.gpu === undefined) {
|
|
836
|
+
set_status('no WebGPU in this browser', 'fail');
|
|
837
|
+
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
842
|
+
|
|
843
|
+
if (adapter === null) {
|
|
844
|
+
set_status('no WebGPU adapter', 'fail');
|
|
845
|
+
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// every compression family the adapter offers is asked for, so the encoder can be handed any
|
|
850
|
+
// of its targets; the encoder itself needs no feature at all
|
|
851
|
+
const requiredFeatures = ['texture-compression-bc', 'texture-compression-etc2', 'texture-compression-astc']
|
|
852
|
+
.filter(feature => adapter.features.has(feature));
|
|
853
|
+
|
|
854
|
+
const device = await adapter.requestDevice({ requiredFeatures });
|
|
855
|
+
|
|
856
|
+
const support = gpu_texture_compression_support(device);
|
|
857
|
+
|
|
858
|
+
element('device').textContent = `${adapter.info?.vendor ?? '?'} ${adapter.info?.architecture ?? ''}`
|
|
859
|
+
+ ` — ${support.size} compressed formats sampleable, features: ${requiredFeatures.join(', ') || 'none'}`;
|
|
860
|
+
|
|
861
|
+
const graphics = new GraphicsContext(device);
|
|
862
|
+
|
|
863
|
+
/** @type {SourceLevel|null} */
|
|
864
|
+
let uploaded_source = null;
|
|
865
|
+
|
|
866
|
+
globalThis.device = device;
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* @param {SourceLevel} base
|
|
870
|
+
*/
|
|
871
|
+
async function run(base) {
|
|
872
|
+
const format = element('format').value;
|
|
873
|
+
|
|
874
|
+
const info = FORMATS[format];
|
|
875
|
+
|
|
876
|
+
if (info === undefined) {
|
|
877
|
+
set_status(`${format} is not a format this page knows how to judge`, 'fail');
|
|
878
|
+
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (!support.has(format)) {
|
|
883
|
+
set_status(`${format} cannot be sampled on this device, so the texture cannot be created`, 'fail');
|
|
884
|
+
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
set_status(`encoding ${base.width}x${base.height} to ${format}…`);
|
|
889
|
+
|
|
890
|
+
const levels = mip_chain(base);
|
|
891
|
+
|
|
892
|
+
if (info.hdr === true && base.hdr === undefined) {
|
|
893
|
+
set_status(`${format} wants an HDR source; pick the HDR synthetic one`, 'fail');
|
|
894
|
+
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// a signed target reads a signed source: the same bytes, meaning [-1, 1] to the sampler;
|
|
899
|
+
// an HDR target reads halves
|
|
900
|
+
const source = device.createTexture({
|
|
901
|
+
label: 'source',
|
|
902
|
+
size: [base.width, base.height, 1],
|
|
903
|
+
format: info.hdr ? 'rgba16float' : (info.signed ? 'rgba8snorm' : 'rgba8unorm'),
|
|
904
|
+
mipLevelCount: levels.length,
|
|
905
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
for (let level = 0; level < levels.length; level++) {
|
|
909
|
+
texture_write_level(device.queue, source, level, levels[level].rgba);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const lines = [];
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Record, submit and wait, answering with the result and the wall time.
|
|
916
|
+
*
|
|
917
|
+
* @returns {Promise<{result: TextureEncodeResult, blocks_copy: GPUBuffer, wall_ms: number}>}
|
|
918
|
+
*/
|
|
919
|
+
async function encode_once() {
|
|
920
|
+
const cmd = ShadeGPUCommandContext.create(graphics, 'texture encode');
|
|
921
|
+
|
|
922
|
+
const started = performance.now();
|
|
923
|
+
|
|
924
|
+
const result = texture_encode(cmd, {
|
|
925
|
+
source,
|
|
926
|
+
format,
|
|
927
|
+
level_count: levels.length,
|
|
928
|
+
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC,
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// the block buffer is transient and goes back to the pool at finish, so it is copied out now
|
|
932
|
+
const blocks_copy = device.createBuffer({
|
|
933
|
+
size: result.layout.byte_length,
|
|
934
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
cmd.copyBufferToBuffer(result.blocks, 0, blocks_copy, 0, result.layout.byte_length);
|
|
938
|
+
|
|
939
|
+
cmd.finish();
|
|
940
|
+
|
|
941
|
+
await device.queue.onSubmittedWorkDone();
|
|
942
|
+
|
|
943
|
+
return { result, blocks_copy, wall_ms: performance.now() - started };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
device.pushErrorScope('validation');
|
|
947
|
+
|
|
948
|
+
// twice: the first run compiles the pipeline, the second is what a second texture costs
|
|
949
|
+
const first = await encode_once();
|
|
950
|
+
|
|
951
|
+
first.result.texture.destroy();
|
|
952
|
+
first.blocks_copy.destroy();
|
|
953
|
+
|
|
954
|
+
const { result, blocks_copy, wall_ms } = await encode_once();
|
|
955
|
+
|
|
956
|
+
const validation = await device.popErrorScope();
|
|
957
|
+
|
|
958
|
+
if (validation !== null) {
|
|
959
|
+
set_status(`validation error: ${validation.message}`, 'fail');
|
|
960
|
+
|
|
961
|
+
lines.push(validation.message);
|
|
962
|
+
report_element.textContent = lines.join('\n');
|
|
963
|
+
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const texels = levels.reduce((sum, level) => sum + level.width * level.height, 0);
|
|
968
|
+
|
|
969
|
+
lines.push(`${format} ${base.width}x${base.height}, ${levels.length} levels, ${result.layout.block_count} blocks`);
|
|
970
|
+
lines.push(` wall time, record to done ${wall_ms.toFixed(2)} ms (${(texels / 1e6 / (wall_ms / 1000)).toFixed(1)} MP/s; first run with pipeline compile ${first.wall_ms.toFixed(1)} ms)`);
|
|
971
|
+
lines.push(` block buffer ${result.layout.byte_length} bytes, rows padded to ${result.layout.levels[0].bytes_per_row}`);
|
|
972
|
+
lines.push('');
|
|
973
|
+
lines.push(' level size PSNR dB max hw PSNR hw max texture==buffer hw decode vs CPU decode');
|
|
974
|
+
|
|
975
|
+
const blocks = await read_buffer(device, blocks_copy, result.layout.byte_length);
|
|
976
|
+
|
|
977
|
+
blocks_copy.destroy();
|
|
978
|
+
|
|
979
|
+
let all_texture_match = true;
|
|
980
|
+
let hardware_mismatch_total = 0;
|
|
981
|
+
let hardware_max = 0;
|
|
982
|
+
let hardware_psnr_gap = 0;
|
|
983
|
+
let flip_line = '';
|
|
984
|
+
|
|
985
|
+
for (const level of result.layout.levels) {
|
|
986
|
+
const texel_count = level.width * level.height;
|
|
987
|
+
|
|
988
|
+
const truth = info.hdr
|
|
989
|
+
? levels[level.level].hdr
|
|
990
|
+
: leading_channels(levels[level.level].rgba, 4, info.channels, texel_count, info.signed);
|
|
991
|
+
|
|
992
|
+
const level_blocks = texture_encode_level_blocks(result.layout, blocks, level.level);
|
|
993
|
+
|
|
994
|
+
const decoded = info.decode(level_blocks, level.width, level.height);
|
|
995
|
+
|
|
996
|
+
/*
|
|
997
|
+
What a sampler is measured over: bytes for an LDR format, and for BC6H the half-float
|
|
998
|
+
bit patterns, which is the domain its decoder interpolates in and a relative error in
|
|
999
|
+
value — with the largest finite half as the peak.
|
|
1000
|
+
*/
|
|
1001
|
+
const measure = samples => info.hdr ? half_bits_of(samples) : samples;
|
|
1002
|
+
const peak = info.hdr ? 0x7BFF : (info.signed ? 127 : 255);
|
|
1003
|
+
|
|
1004
|
+
// the PSNR of one level everywhere: no picture is credited as closer to the source than
|
|
1005
|
+
// a rounding, so a level one decoder reproduces exactly still has a ratio to take
|
|
1006
|
+
const rounding_psnr = 20 * Math.log10(peak);
|
|
1007
|
+
|
|
1008
|
+
const reference = new Sampler2D(measure(truth), info.channels, level.width, level.height);
|
|
1009
|
+
const test = new Sampler2D(measure(decoded), info.channels, level.width, level.height);
|
|
1010
|
+
|
|
1011
|
+
const psnr = sampler2d_psnr(reference, test, info.channels, peak);
|
|
1012
|
+
const max = sampler2d_max_absolute_difference(reference, test);
|
|
1013
|
+
|
|
1014
|
+
// 2. the texture holds the buffer's blocks
|
|
1015
|
+
const from_texture = await read_texture_level(device, result.texture, level, result.layout.bytes_per_block);
|
|
1016
|
+
|
|
1017
|
+
let texture_match = from_texture.length === level_blocks.length;
|
|
1018
|
+
|
|
1019
|
+
for (let i = 0; texture_match && i < level_blocks.length; i++) {
|
|
1020
|
+
texture_match = from_texture[i] === level_blocks[i];
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
all_texture_match = all_texture_match && texture_match;
|
|
1024
|
+
|
|
1025
|
+
// 3. the hardware decodes what the test decoder predicts
|
|
1026
|
+
const hardware = leading_channels(
|
|
1027
|
+
await hardware_decode(device, result.texture, level.level, level.width, level.height, info.hdr ? 'float' : (info.signed ? 'signed' : 'bytes')),
|
|
1028
|
+
4, info.channels, texel_count, info.signed
|
|
1029
|
+
);
|
|
1030
|
+
|
|
1031
|
+
const hardware_measured = measure(hardware);
|
|
1032
|
+
const decoded_measured = measure(decoded);
|
|
1033
|
+
|
|
1034
|
+
let mismatches = 0;
|
|
1035
|
+
let worst = 0;
|
|
1036
|
+
|
|
1037
|
+
for (let i = 0; i < hardware_measured.length; i++) {
|
|
1038
|
+
const delta = Math.abs(hardware_measured[i] - decoded_measured[i]);
|
|
1039
|
+
|
|
1040
|
+
if (delta !== 0) {
|
|
1041
|
+
mismatches++;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
worst = Math.max(worst, delta);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
hardware_mismatch_total += mismatches;
|
|
1048
|
+
hardware_max = Math.max(hardware_max, worst);
|
|
1049
|
+
|
|
1050
|
+
// what the GPU actually shows against the source — the number a user would see
|
|
1051
|
+
const hardware_sampler = new Sampler2D(hardware_measured, info.channels, level.width, level.height);
|
|
1052
|
+
|
|
1053
|
+
const hardware_psnr = sampler2d_psnr(reference, hardware_sampler, info.channels, peak);
|
|
1054
|
+
const hardware_worst = sampler2d_max_absolute_difference(reference, hardware_sampler);
|
|
1055
|
+
|
|
1056
|
+
/*
|
|
1057
|
+
The gap is the CPU decoder's reading minus the hardware's, where the hardware is worse,
|
|
1058
|
+
both held at the PSNR of a rounding. A tail level the CPU decoder reproduces exactly has
|
|
1059
|
+
infinite PSNR, and against a hardware texel one level off the ratio is whatever the
|
|
1060
|
+
ceiling makes it — a 200 dB ceiling read BC1's 1x1 level as a 150 dB gap. Under the
|
|
1061
|
+
rounding ceiling that level is no gap at all, while a misread block on it stays tens of
|
|
1062
|
+
decibels under.
|
|
1063
|
+
*/
|
|
1064
|
+
hardware_psnr_gap = Math.max(hardware_psnr_gap, Math.min(psnr, rounding_psnr) - Math.min(hardware_psnr, rounding_psnr));
|
|
1065
|
+
|
|
1066
|
+
lines.push(
|
|
1067
|
+
` ${String(level.level).padStart(5)} ${`${level.width}x${level.height}`.padStart(9)}`
|
|
1068
|
+
+ ` ${psnr.toFixed(2).padStart(7)} ${String(max).padStart(3)}`
|
|
1069
|
+
+ ` ${hardware_psnr.toFixed(2).padStart(7)} ${String(hardware_worst).padStart(6)}`
|
|
1070
|
+
+ ` ${texture_match ? 'yes' : 'NO '} `
|
|
1071
|
+
+ ` ${mismatches} samples differ, worst ${worst}`
|
|
1072
|
+
);
|
|
1073
|
+
|
|
1074
|
+
if (level.level === 0) {
|
|
1075
|
+
const shown_truth = info.hdr ? tonemap(truth) : truth;
|
|
1076
|
+
const shown_hardware = info.hdr ? tonemap(hardware) : hardware;
|
|
1077
|
+
|
|
1078
|
+
const truth_rgba = display_rgba(shown_truth, info.channels, texel_count, 1, info.signed);
|
|
1079
|
+
const hardware_rgba = display_rgba(shown_hardware, info.channels, texel_count, 1, info.signed);
|
|
1080
|
+
|
|
1081
|
+
panels.set_picture(PANEL_SOURCE, truth_rgba, level.width, level.height);
|
|
1082
|
+
panels.set_picture(PANEL_HARDWARE, hardware_rgba, level.width, level.height);
|
|
1083
|
+
|
|
1084
|
+
/*
|
|
1085
|
+
The third panel is FLIP: how likely a viewer flipping between the two pictures is
|
|
1086
|
+
to notice a difference, and where. A tint nobody would see scores low and a
|
|
1087
|
+
displaced edge scores high, which is what an amplified pixel difference gets
|
|
1088
|
+
backwards. It compares the displayed bytes — the tone-mapped picture for HDR, the
|
|
1089
|
+
mid-grey-centred one for a signed format — because that is what is seen. It is a
|
|
1090
|
+
CPU pass over the whole image, seconds at 2048x2048, so the status says what the
|
|
1091
|
+
page is doing and a frame goes by so that it shows.
|
|
1092
|
+
*/
|
|
1093
|
+
set_status(`encoded; FLIP over ${level.width}x${level.height}…`);
|
|
1094
|
+
|
|
1095
|
+
await painted();
|
|
1096
|
+
|
|
1097
|
+
const flip_started = performance.now();
|
|
1098
|
+
|
|
1099
|
+
const error = sampler2d_flip_ldr(
|
|
1100
|
+
new Sampler2D(truth_rgba, 4, level.width, level.height),
|
|
1101
|
+
new Sampler2D(hardware_rgba, 4, level.width, level.height)
|
|
1102
|
+
);
|
|
1103
|
+
|
|
1104
|
+
const flip_ms = performance.now() - flip_started;
|
|
1105
|
+
|
|
1106
|
+
// the readbacks of the other levels are awaited, so this one paints on its own
|
|
1107
|
+
set_status(`FLIP done in ${flip_ms.toFixed(0)} ms; the other levels, then the emulator over level 0…`);
|
|
1108
|
+
|
|
1109
|
+
const pooled = flip_pool(error);
|
|
1110
|
+
|
|
1111
|
+
panels.set_picture(PANEL_FLIP, display_rgba(sampler2d_channel_apply_lut(error, 0, MAGMA_LUT, 3).data, 3, texel_count), level.width, level.height);
|
|
1112
|
+
|
|
1113
|
+
// the bytes the panels show at a texel, in the format's own units — a signed
|
|
1114
|
+
// format's are shown about mid grey and an HDR one's tone-mapped — and FLIP there
|
|
1115
|
+
const shown_channels = Math.min(info.channels, 3);
|
|
1116
|
+
|
|
1117
|
+
describe_texel = (x, y) => {
|
|
1118
|
+
const i = y * level.width + x;
|
|
1119
|
+
|
|
1120
|
+
const bytes = rgba => Array.from(rgba.subarray(i * 4, i * 4 + shown_channels), byte => info.signed ? byte - 128 : byte).join(' ');
|
|
1121
|
+
|
|
1122
|
+
return `texel ${x}, ${y} source ${bytes(truth_rgba)} hardware ${bytes(hardware_rgba)}${info.hdr ? ' (tone-mapped bytes)' : ''} FLIP ${error.data[i].toFixed(4)}`;
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
flip_line = ` FLIP, level 0, hardware picture against the source at ${new FlipParameters().pixels_per_degree.toFixed(1)} pixels per degree:`
|
|
1126
|
+
+ ` mean ${pooled.mean.toFixed(4)}, weighted median ${pooled.weighted_median.toFixed(4)}, max ${pooled.maximum.toFixed(4)}`
|
|
1127
|
+
+ ` (${flip_ms.toFixed(0)} ms)`;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
lines.push('');
|
|
1132
|
+
lines.push(flip_line);
|
|
1133
|
+
|
|
1134
|
+
// 4. the device against the emulator, block by block, on level 0 — the emulator runs a
|
|
1135
|
+
// block in a fraction of a millisecond, which is fine at 512x512 and a hang at 2048
|
|
1136
|
+
lines.push('');
|
|
1137
|
+
|
|
1138
|
+
const emulator_limit = 32768;
|
|
1139
|
+
|
|
1140
|
+
if (base.width * base.height / 16 > emulator_limit) {
|
|
1141
|
+
lines.push(` device against the emulator: skipped above ${emulator_limit} blocks; use the 512x512 source`);
|
|
1142
|
+
} else try {
|
|
1143
|
+
const encode_block = await info.emulate();
|
|
1144
|
+
|
|
1145
|
+
const truth = info.hdr
|
|
1146
|
+
? levels[0].hdr
|
|
1147
|
+
: leading_channels(levels[0].rgba, 4, info.channels, base.width * base.height, info.signed);
|
|
1148
|
+
|
|
1149
|
+
// the helper hands the encoder bytes; a signed encoder reads them as two's complement,
|
|
1150
|
+
// and an HDR level is encoded from its floats directly
|
|
1151
|
+
const emulated = info.hdr
|
|
1152
|
+
? encode_hdr_image(encode_block, truth, base.width, base.height)
|
|
1153
|
+
: encode_image_with_block_encoder(
|
|
1154
|
+
encode_block, info.bytes_per_block, new Uint8Array(truth.buffer), base.width, base.height, info.channels
|
|
1155
|
+
);
|
|
1156
|
+
|
|
1157
|
+
const gpu = texture_encode_level_blocks(result.layout, blocks, 0);
|
|
1158
|
+
|
|
1159
|
+
const bytes_per_block = info.bytes_per_block;
|
|
1160
|
+
const block_count = gpu.length / bytes_per_block;
|
|
1161
|
+
|
|
1162
|
+
let identical = 0;
|
|
1163
|
+
let gpu_better = 0;
|
|
1164
|
+
let gpu_worse = 0;
|
|
1165
|
+
let gpu_equal = 0;
|
|
1166
|
+
|
|
1167
|
+
const gpu_decoded = info.decode(gpu, base.width, base.height);
|
|
1168
|
+
const emulated_decoded = info.decode(emulated, base.width, base.height);
|
|
1169
|
+
|
|
1170
|
+
for (let block = 0; block < block_count; block++) {
|
|
1171
|
+
let same = true;
|
|
1172
|
+
|
|
1173
|
+
for (let i = 0; i < bytes_per_block; i++) {
|
|
1174
|
+
same = same && gpu[block * bytes_per_block + i] === emulated[block * bytes_per_block + i];
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (same) {
|
|
1178
|
+
identical++;
|
|
1179
|
+
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// a different block is fine if it is no worse; the device's f32 rounds differently
|
|
1184
|
+
const bx = block % Math.ceil(base.width / 4);
|
|
1185
|
+
const by = Math.floor(block / Math.ceil(base.width / 4));
|
|
1186
|
+
|
|
1187
|
+
let gpu_error = 0;
|
|
1188
|
+
let emulated_error = 0;
|
|
1189
|
+
|
|
1190
|
+
for (let y = by * 4; y < Math.min(by * 4 + 4, base.height); y++) {
|
|
1191
|
+
for (let x = bx * 4; x < Math.min(bx * 4 + 4, base.width); x++) {
|
|
1192
|
+
for (let c = 0; c < info.channels; c++) {
|
|
1193
|
+
const at = (y * base.width + x) * info.channels + c;
|
|
1194
|
+
|
|
1195
|
+
gpu_error += (gpu_decoded[at] - truth[at]) ** 2;
|
|
1196
|
+
emulated_error += (emulated_decoded[at] - truth[at]) ** 2;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (gpu_error < emulated_error) {
|
|
1202
|
+
gpu_better++;
|
|
1203
|
+
} else if (gpu_error > emulated_error) {
|
|
1204
|
+
gpu_worse++;
|
|
1205
|
+
} else {
|
|
1206
|
+
gpu_equal++;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
lines.push(` device against the emulator, level 0: ${identical} of ${block_count} blocks identical;`
|
|
1211
|
+
+ ` of the rest ${gpu_equal} equal in error, ${gpu_better} better on the device, ${gpu_worse} worse`);
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
lines.push(` emulator comparison skipped: ${error.message}`);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
report_element.textContent = lines.join('\n');
|
|
1217
|
+
|
|
1218
|
+
/*
|
|
1219
|
+
The hardware's interpolation is not the specification's rounding — the probe below has the
|
|
1220
|
+
exact weights — so a few levels against the CPU decoder on a wide-span block is the
|
|
1221
|
+
decoder's arithmetic, not a wrong block. What a wrong block does is move whole texels by
|
|
1222
|
+
tens, which shows up as the hardware's picture being decibels worse than the CPU decoder's
|
|
1223
|
+
reading of the same bytes. So the verdict is that difference, per level, with nothing finer
|
|
1224
|
+
than a rounding counted.
|
|
1225
|
+
*/
|
|
1226
|
+
const verdict = all_texture_match && hardware_psnr_gap <= 1;
|
|
1227
|
+
|
|
1228
|
+
set_status(
|
|
1229
|
+
verdict
|
|
1230
|
+
? `ok — texture holds the shader's blocks on every level; the hardware's picture is within ${hardware_psnr_gap.toFixed(2)} dB of the CPU decoder's, a rounding forgiven; samples within ${hardware_max} (${hardware_mismatch_total} differ)`
|
|
1231
|
+
: `problem — texture matches buffer: ${all_texture_match}; the hardware's picture is ${hardware_psnr_gap.toFixed(2)} dB under the CPU decoder's, a rounding forgiven; samples up to ${hardware_max} apart`,
|
|
1232
|
+
verdict ? 'pass' : 'fail'
|
|
1233
|
+
);
|
|
1234
|
+
|
|
1235
|
+
globalThis.last_result = result;
|
|
1236
|
+
|
|
1237
|
+
source.destroy();
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* What the hardware's BC4 decoder actually computes, found rather than assumed.
|
|
1242
|
+
*
|
|
1243
|
+
* A 1024x1024 BC4 texture holds every pair of endpoints as one block each, with texels 0..7
|
|
1244
|
+
* carrying indices 0..7 — so a hardware read of the texture is the whole decode table, 256 x
|
|
1245
|
+
* 256 x 8 entries, read back at the precision the sampler delivers rather than rounded to eight
|
|
1246
|
+
* bits. From that the interpolation weight at every index is fitted exactly, and the difference
|
|
1247
|
+
* from the CPU decoder the specs judge with is tabulated. The specification writes the
|
|
1248
|
+
* interpolation in real arithmetic and leaves the precision to the implementation; this is what
|
|
1249
|
+
* one implementation chose.
|
|
1250
|
+
*/
|
|
1251
|
+
async function probe_decoder() {
|
|
1252
|
+
// 65536 pairs at 256 blocks a row is 256 rows of blocks: 1024 texels tall, not 512
|
|
1253
|
+
const width = 1024;
|
|
1254
|
+
const height = 1024;
|
|
1255
|
+
|
|
1256
|
+
const blocks_x = width / 4;
|
|
1257
|
+
|
|
1258
|
+
const blocks = new Uint8Array(blocks_x * (height / 4) * 8);
|
|
1259
|
+
|
|
1260
|
+
for (let first = 0; first < 256; first++) {
|
|
1261
|
+
for (let second = 0; second < 256; second++) {
|
|
1262
|
+
const block = first * 256 + second;
|
|
1263
|
+
|
|
1264
|
+
const at = block * 8;
|
|
1265
|
+
|
|
1266
|
+
blocks[at] = first;
|
|
1267
|
+
blocks[at + 1] = second;
|
|
1268
|
+
|
|
1269
|
+
// texels 0..7 take indices 0..7 in order: eight three-bit fields, 24 bits, 3 bytes
|
|
1270
|
+
const word = 0 | (1 << 3) | (2 << 6) | (3 << 9) | (4 << 12) | (5 << 15) | (6 << 18) | (7 << 21);
|
|
1271
|
+
|
|
1272
|
+
blocks[at + 2] = word & 0xFF;
|
|
1273
|
+
blocks[at + 3] = (word >> 8) & 0xFF;
|
|
1274
|
+
blocks[at + 4] = (word >> 16) & 0xFF;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const texture = device.createTexture({
|
|
1279
|
+
label: 'decoder probe',
|
|
1280
|
+
size: [width, height, 1],
|
|
1281
|
+
format: 'bc4-r-unorm',
|
|
1282
|
+
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
texture_write_level(device.queue, texture, 0, blocks);
|
|
1286
|
+
|
|
1287
|
+
const exact = await hardware_decode(device, texture, 0, width, height, 'exact_red');
|
|
1288
|
+
|
|
1289
|
+
texture.destroy();
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* The hardware's value for one entry of one pair's palette, as the unorm it sampled.
|
|
1293
|
+
*
|
|
1294
|
+
* @param {number} first
|
|
1295
|
+
* @param {number} second
|
|
1296
|
+
* @param {number} index
|
|
1297
|
+
* @returns {number}
|
|
1298
|
+
*/
|
|
1299
|
+
const sampled = (first, second, index) => {
|
|
1300
|
+
const block = first * 256 + second;
|
|
1301
|
+
|
|
1302
|
+
const x = (block % blocks_x) * 4 + (index & 3);
|
|
1303
|
+
const y = Math.floor(block / blocks_x) * 4 + (index >> 2);
|
|
1304
|
+
|
|
1305
|
+
return exact[y * width + x];
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
const lines = [
|
|
1309
|
+
'the hardware BC4 decoder, over every pair of endpoints:',
|
|
1310
|
+
'',
|
|
1311
|
+
' the weight of the second endpoint at each index, fitted from the unrounded samples as',
|
|
1312
|
+
' value = A + (B - A) * w over 16-bit endpoints A = 257a. An interval is the set of weights',
|
|
1313
|
+
' every pair agrees with; an empty one would mean no single weight does.',
|
|
1314
|
+
'',
|
|
1315
|
+
];
|
|
1316
|
+
|
|
1317
|
+
for (const [mode, indices] of [['eight-value (first > second)', [2, 3, 4, 5, 6, 7]], ['six-value (first <= second)', [2, 3, 4, 5]]]) {
|
|
1318
|
+
for (const index of indices) {
|
|
1319
|
+
let low = -Infinity;
|
|
1320
|
+
let high = Infinity;
|
|
1321
|
+
|
|
1322
|
+
for (let first = 0; first < 256; first += 3) {
|
|
1323
|
+
for (let second = 0; second < 256; second += 3) {
|
|
1324
|
+
const eight = first > second;
|
|
1325
|
+
const wanted_eight = indices.length === 6;
|
|
1326
|
+
|
|
1327
|
+
if (eight !== wanted_eight || Math.abs(first - second) < 32) {
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const A = first * 257;
|
|
1332
|
+
const B = second * 257;
|
|
1333
|
+
|
|
1334
|
+
const w = (sampled(first, second, index) * 65535 - A) / (B - A);
|
|
1335
|
+
|
|
1336
|
+
// the 16-bit value is within half a unit of what the hardware computed
|
|
1337
|
+
const tolerance = 0.51 / Math.abs(B - A);
|
|
1338
|
+
|
|
1339
|
+
low = Math.max(low, w - tolerance);
|
|
1340
|
+
high = Math.min(high, w + tolerance);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const mid = (low + high) / 2;
|
|
1345
|
+
|
|
1346
|
+
lines.push(
|
|
1347
|
+
` ${mode} index ${index}: w = ${mid.toFixed(6)}`
|
|
1348
|
+
+ ` (x257 = ${(mid * 257).toFixed(2)}, the specification's ${(indices.length === 6 ? (index - 1) / 7 : (index - 1) / 5).toFixed(6)})`
|
|
1349
|
+
+ (high < low ? ' EMPTY' : '')
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// and, rounded to eight bits, how far that is from the CPU decoder the specs judge with
|
|
1355
|
+
const histogram = new Map();
|
|
1356
|
+
|
|
1357
|
+
for (let first = 0; first < 256; first++) {
|
|
1358
|
+
for (let second = 0; second < 256; second++) {
|
|
1359
|
+
const block = new Uint8Array(8);
|
|
1360
|
+
|
|
1361
|
+
block[0] = first;
|
|
1362
|
+
block[1] = second;
|
|
1363
|
+
block[2] = 0b10001000;
|
|
1364
|
+
block[3] = 0b11000110;
|
|
1365
|
+
block[4] = 0b11111010;
|
|
1366
|
+
|
|
1367
|
+
const reference = decode_bc4(block, 4, 2);
|
|
1368
|
+
|
|
1369
|
+
for (let index = 0; index < 8; index++) {
|
|
1370
|
+
const delta = Math.round(sampled(first, second, index) * 255) - reference[index];
|
|
1371
|
+
|
|
1372
|
+
histogram.set(delta, (histogram.get(delta) ?? 0) + 1);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const total = 65536 * 8;
|
|
1378
|
+
|
|
1379
|
+
lines.push('');
|
|
1380
|
+
lines.push(' hardware, rounded to eight bits, minus the CPU decoder, over every pair and index:');
|
|
1381
|
+
|
|
1382
|
+
for (const delta of [...histogram.keys()].sort((a, b) => a - b)) {
|
|
1383
|
+
lines.push(` ${String(delta).padStart(3)}: ${String(histogram.get(delta)).padStart(7)} (${(100 * histogram.get(delta) / total).toFixed(2)}%)`);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
report_element.textContent = lines.join('\n');
|
|
1387
|
+
|
|
1388
|
+
set_status('decoder probe done', 'pass');
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
element('probe').addEventListener('click', () => {
|
|
1392
|
+
probe_decoder().catch(error => {
|
|
1393
|
+
set_status(error.message, 'fail');
|
|
1394
|
+
|
|
1395
|
+
console.error(error);
|
|
1396
|
+
});
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
element('run').addEventListener('click', () => {
|
|
1400
|
+
const choice = element('source').value;
|
|
1401
|
+
|
|
1402
|
+
const kind = choice.startsWith('correlated') ? 'correlated'
|
|
1403
|
+
: choice.startsWith('normal') ? 'normal'
|
|
1404
|
+
: choice.startsWith('hdr') ? 'hdr'
|
|
1405
|
+
: 'independent';
|
|
1406
|
+
const signed = FORMATS[element('format').value]?.signed === true;
|
|
1407
|
+
|
|
1408
|
+
const base = uploaded_source ?? synthetic_source(Number(choice.replace(/[a-z]+/, '')), kind, signed);
|
|
1409
|
+
|
|
1410
|
+
run(base).catch(error => {
|
|
1411
|
+
set_status(error.message, 'fail');
|
|
1412
|
+
|
|
1413
|
+
console.error(error);
|
|
1414
|
+
});
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
element('file').addEventListener('change', async event => {
|
|
1418
|
+
const file = event.target.files[0];
|
|
1419
|
+
|
|
1420
|
+
if (file === undefined) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
uploaded_source = await file_source(file);
|
|
1425
|
+
|
|
1426
|
+
set_status(`${file.name}: ${uploaded_source.width}x${uploaded_source.height} ready`, 'pass');
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
set_status('ready', 'pass');
|
|
1430
|
+
}
|