@woosh/meep-engine 3.31.0 → 3.33.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.
Files changed (117) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/package.json +1 -1
  3. package/src/core/collection/table/RowFirstTable.d.ts +8 -0
  4. package/src/core/collection/table/RowFirstTable.d.ts.map +1 -1
  5. package/src/core/collection/table/RowFirstTable.js +40 -11
  6. package/src/engine/graphics/ecs/sprite/Sprite.d.ts +3 -0
  7. package/src/engine/graphics/ecs/sprite/Sprite.d.ts.map +1 -1
  8. package/src/engine/graphics/ecs/sprite/Sprite.js +3 -0
  9. package/src/engine/graphics/ecs/sprite/SpriteSystemPE.d.ts +3 -0
  10. package/src/engine/graphics/ecs/sprite/SpriteSystemPE.d.ts.map +1 -1
  11. package/src/engine/graphics/ecs/sprite/SpriteSystemPE.js +3 -0
  12. package/src/engine/graphics3/DebugDrawSystem.d.ts.map +1 -1
  13. package/src/engine/graphics3/DebugDrawSystem.js +6 -2
  14. package/src/engine/graphics3/PathDisplaySystem.d.ts.map +1 -1
  15. package/src/engine/graphics3/PathDisplaySystem.js +311 -307
  16. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  17. package/src/shade/RENDERER_CONTRACT.md +15 -2
  18. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +5 -0
  19. package/src/shade/device/ShadeGPUCommandContext.d.ts +15 -2
  20. package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
  21. package/src/shade/device/ShadeGPUCommandContext.js +154 -25
  22. package/src/shade/device/mock/SoftwareGPURenderPassEncoder.d.ts +20 -3
  23. package/src/shade/device/mock/SoftwareGPURenderPassEncoder.d.ts.map +1 -1
  24. package/src/shade/device/mock/SoftwareGPURenderPassEncoder.js +21 -3
  25. package/src/shade/renderer/GraphicsContext.d.ts +20 -1
  26. package/src/shade/renderer/GraphicsContext.d.ts.map +1 -1
  27. package/src/shade/renderer/GraphicsContext.js +105 -52
  28. package/src/shade/renderer/Renderer.d.ts +19 -4
  29. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  30. package/src/shade/renderer/Renderer.js +113 -2
  31. package/src/shade/renderer/animation/GPUAnimationManager.d.ts +10 -7
  32. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  33. package/src/shade/renderer/animation/GPUAnimationManager.js +147 -45
  34. package/src/shade/renderer/buffer/GPUBufferReadbackAllocator.d.ts +6 -5
  35. package/src/shade/renderer/buffer/GPUBufferReadbackAllocator.d.ts.map +1 -1
  36. package/src/shade/renderer/buffer/GPUBufferReadbackAllocator.js +241 -240
  37. package/src/shade/renderer/buffer/GPUBufferStagingAllocator.d.ts +62 -2
  38. package/src/shade/renderer/buffer/GPUBufferStagingAllocator.d.ts.map +1 -1
  39. package/src/shade/renderer/buffer/GPUBufferStagingAllocator.js +306 -38
  40. package/src/shade/renderer/buffer/GPUBufferUploadAllocator.d.ts +60 -0
  41. package/src/shade/renderer/buffer/GPUBufferUploadAllocator.d.ts.map +1 -0
  42. package/src/shade/renderer/buffer/GPUBufferUploadAllocator.js +202 -0
  43. package/src/shade/renderer/buffer/GPUUploadBlock.d.ts +68 -0
  44. package/src/shade/renderer/buffer/GPUUploadBlock.d.ts.map +1 -0
  45. package/src/shade/renderer/buffer/GPUUploadBlock.js +97 -0
  46. package/src/shade/renderer/dynamic/DynamicMeshAVBOITChannel.d.ts +2 -1
  47. package/src/shade/renderer/dynamic/DynamicMeshAVBOITChannel.d.ts.map +1 -1
  48. package/src/shade/renderer/dynamic/DynamicMeshAVBOITChannel.js +19 -17
  49. package/src/shade/renderer/dynamic/GPUDynamicMeshDrawList.d.ts +70 -0
  50. package/src/shade/renderer/dynamic/GPUDynamicMeshDrawList.d.ts.map +1 -0
  51. package/src/shade/renderer/dynamic/GPUDynamicMeshDrawList.js +257 -0
  52. package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.d.ts +20 -7
  53. package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.d.ts.map +1 -1
  54. package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.js +39 -41
  55. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.d.ts +6 -20
  56. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.d.ts.map +1 -1
  57. package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.js +42 -36
  58. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_avboit_vertex.d.ts +2 -1
  59. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_avboit_vertex.d.ts.map +1 -1
  60. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_avboit_vertex.js +4 -3
  61. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_vertex.d.ts +4 -3
  62. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_vertex.d.ts.map +1 -1
  63. package/src/shade/renderer/dynamic/chunk_dynamic_mesh_vertex.js +7 -4
  64. package/src/shade/renderer/dynamic/graph_dynamic_mesh_avboit.d.ts +14 -15
  65. package/src/shade/renderer/dynamic/graph_dynamic_mesh_avboit.d.ts.map +1 -1
  66. package/src/shade/renderer/dynamic/graph_dynamic_mesh_avboit.js +49 -83
  67. package/src/shade/renderer/dynamic/shader_dynamic_mesh.d.ts +19 -3
  68. package/src/shade/renderer/dynamic/shader_dynamic_mesh.d.ts.map +1 -1
  69. package/src/shade/renderer/dynamic/shader_dynamic_mesh.js +26 -6
  70. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_draw.d.ts +2 -1
  71. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_draw.d.ts.map +1 -1
  72. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_draw.js +12 -6
  73. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_splat.d.ts +3 -2
  74. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_splat.d.ts.map +1 -1
  75. package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_splat.js +21 -13
  76. package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.d.ts +5 -0
  77. package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.d.ts.map +1 -1
  78. package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.js +19 -0
  79. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  80. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  81. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts +3 -1
  82. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts.map +1 -1
  83. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.js +3 -1
  84. package/src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_rasterization_bucket_sort.d.ts.map +1 -1
  85. package/src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_rasterization_bucket_sort.js +134 -88
  86. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.d.ts.map +1 -1
  87. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.js +57 -26
  88. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  89. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +28 -20
  90. package/src/shade/renderer/rasterize/graph_clear_raster_targets.d.ts +38 -0
  91. package/src/shade/renderer/rasterize/graph_clear_raster_targets.d.ts.map +1 -0
  92. package/src/shade/renderer/rasterize/graph_clear_raster_targets.js +105 -0
  93. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +20 -0
  94. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts +1 -1
  95. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  96. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +21 -1
  97. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.d.ts.map +1 -1
  98. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.js +12 -20
  99. package/src/shade/renderer/shader/graph/graph_import_typed_buffer.d.ts +2 -1
  100. package/src/shade/renderer/shader/graph/graph_import_typed_buffer.d.ts.map +1 -1
  101. package/src/shade/renderer/shader/graph/graph_import_typed_buffer.js +15 -28
  102. package/src/shade/renderer/shader/type/serde/write_gpu_typed_buffer.d.ts.map +1 -1
  103. package/src/shade/renderer/shader/type/serde/write_gpu_typed_buffer.js +8 -1
  104. package/src/core/geom/3d/atlas/atlas_bench_lib.d.ts +0 -73
  105. package/src/core/geom/3d/atlas/atlas_bench_lib.d.ts.map +0 -1
  106. package/src/engine/graphics3/terrain/GPUTerrainSplatRenderer.d.ts +0 -59
  107. package/src/engine/graphics3/terrain/GPUTerrainSplatRenderer.d.ts.map +0 -1
  108. package/src/engine/graphics3/terrain/pack_terrain_row_table.d.ts +0 -22
  109. package/src/engine/graphics3/terrain/pack_terrain_row_table.d.ts.map +0 -1
  110. package/src/engine/graphics3/terrain/shader_terrain_splat.d.ts +0 -26
  111. package/src/engine/graphics3/terrain/shader_terrain_splat.d.ts.map +0 -1
  112. package/src/shade/renderer/animation/ANIMATION_BOUND_TRACK_GROUP_STRUCT.d.ts +0 -25
  113. package/src/shade/renderer/animation/ANIMATION_BOUND_TRACK_GROUP_STRUCT.d.ts.map +0 -1
  114. package/src/shade/renderer/animation/ANIMATION_KEYFRAME_BLOCK_STRUCT.d.ts +0 -26
  115. package/src/shade/renderer/animation/ANIMATION_KEYFRAME_BLOCK_STRUCT.d.ts.map +0 -1
  116. package/src/shade/renderer/animation/ANIMATION_SKIN_JOINT_BLOCK_STRUCT.d.ts +0 -29
  117. package/src/shade/renderer/animation/ANIMATION_SKIN_JOINT_BLOCK_STRUCT.d.ts.map +0 -1
@@ -1,89 +1,135 @@
1
- import { u32 } from "../../../../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
2
- import { WebGPUArray } from "../../../../shader/type/WebGPUArray.js";
3
- import { WebGPUStruct } from "../../../../shader/type/WebGPUStruct.js";
4
- import { COLLECTION_U32_STRUCT_READ } from "../../../../gpu_primitive/COLLECTION_U32_STRUCT_READ.js";
5
- import { MATERIAL_METADATA_STRUCT } from "../../../../material/MATERIAL_METADATA_STRUCT.js";
6
- import { chunk_scene_database_access } from "../../../../scene/chunk_scene_database_access.js";
7
- import { CodeChunk } from "../../../../shader/compiler/CodeChunk.js";
8
- import { COMPUTE_SHADER_DEFAULT_GROUP_SIZE } from "../../../../shader/COMPUTE_SHADER_DEFAULT_GROUP_SIZE.js";
9
- import { ComputeShader } from "../../../../shader/ComputeShader.js";
10
- import { ShaderResourceSetDescriptor } from "../../../../shader/resource/ShaderResourceSetDescriptor.js";
11
- import { chunk_rasterization_material_bucket } from "../chunk_rasterization_material_bucket.js";
12
- import { RASTERIZATION_BUCKET_LIMIT } from "../RASTERIZATION_BUCKET_LIMIT.js";
13
- import {
14
- RASTERIZATION_BUCKET_SORT_OUTPUT_HEADER_STRUCT_WRITE
15
- } from "../RASTERIZATION_BUCKET_SORT_OUTPUT_HEADER_STRUCT_WRITE.js";
16
-
17
-
18
- export const RASTERIZATION_BUCKET_SORT_OUTPUT_STRUCT_WRITE = WebGPUStruct.from({
19
- headers: RASTERIZATION_BUCKET_SORT_OUTPUT_HEADER_STRUCT_WRITE,
20
- elements: WebGPUArray.u32
21
- });
22
-
23
- const OFFSETS_TYPE = WebGPUArray.from(u32, RASTERIZATION_BUCKET_LIMIT);
24
-
25
- const resources = new ShaderResourceSetDescriptor();
26
- resources.createGroup()
27
- .addStorageBuffer("scene_database", WebGPUArray.from(u32))
28
- .addStorageBuffer("materials", WebGPUArray.from(MATERIAL_METADATA_STRUCT))
29
-
30
- resources.createGroup()
31
- .addStorageBuffer("input", COLLECTION_U32_STRUCT_READ)
32
- .addStorageBuffer("offsets", OFFSETS_TYPE)
33
- .addStorageBuffer("output", RASTERIZATION_BUCKET_SORT_OUTPUT_STRUCT_WRITE, true)
34
-
35
-
36
- const body = CodeChunk.from(
37
- //language=WGSL
38
- `
39
- @compute @workgroup_size(${COMPUTE_SHADER_DEFAULT_GROUP_SIZE})
40
- fn main(@builtin(global_invocation_id) global_id : vec3<u32>){
41
-
42
- let index = global_id.x;
43
-
44
- let input_size = min(input.count, arrayLength(&input.elements));
45
-
46
- if(index >= input_size){
47
- return;
48
- }
49
-
50
- let mesh_index = input.elements[index];
51
-
52
- let mesh = scene_read_mesh(&scene_database, mesh_index);
53
-
54
- let material = materials[mesh.material];
55
-
56
- let bucket_index = rasterization_material_bucket(
57
- material.transparency_mode,
58
- material.draw_mode,
59
- material.draw_side
60
- );
61
-
62
- let element_offset = atomicAdd(&output.headers[bucket_index].count, 1u);
63
-
64
- let bucket_offset = offsets[bucket_index];
65
-
66
- if(element_offset == 0u){
67
- // first element in bucket, write offset as well
68
- output.headers[bucket_index].offset = bucket_offset;
69
- }
70
-
71
- let output_offset = bucket_offset + element_offset;
72
-
73
- if(output_offset < arrayLength(&output.elements)){
74
- // guard against out-of-bounds access
75
- output.elements[output_offset] = mesh_index;
76
- }
77
-
78
- }
79
- `, [
80
- chunk_rasterization_material_bucket,
81
- chunk_scene_database_access,
82
- ]
83
- );
84
-
85
- export const shader_instances_rasterization_bucket_sort = ComputeShader.from({
86
- label: 'Rasterization bucket sort',
87
- resources,
88
- body,
1
+ import { assert } from "../../../../../../core/assert.js";
2
+ import { u32 } from "../../../../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
3
+ import { WebGPUArray } from "../../../../shader/type/WebGPUArray.js";
4
+ import { WebGPUStruct } from "../../../../shader/type/WebGPUStruct.js";
5
+ import { COLLECTION_U32_STRUCT_READ } from "../../../../gpu_primitive/COLLECTION_U32_STRUCT_READ.js";
6
+ import { MATERIAL_METADATA_STRUCT } from "../../../../material/MATERIAL_METADATA_STRUCT.js";
7
+ import { chunk_scene_database_access } from "../../../../scene/chunk_scene_database_access.js";
8
+ import { CodeChunk } from "../../../../shader/compiler/CodeChunk.js";
9
+ import { COMPUTE_SHADER_DEFAULT_GROUP_SIZE } from "../../../../shader/COMPUTE_SHADER_DEFAULT_GROUP_SIZE.js";
10
+ import { ComputeShader } from "../../../../shader/ComputeShader.js";
11
+ import { ShaderResourceSetDescriptor } from "../../../../shader/resource/ShaderResourceSetDescriptor.js";
12
+ import { chunk_rasterization_material_bucket } from "../chunk_rasterization_material_bucket.js";
13
+ import { RASTERIZATION_BUCKET_LIMIT } from "../RASTERIZATION_BUCKET_LIMIT.js";
14
+ import {
15
+ RASTERIZATION_BUCKET_SORT_OUTPUT_HEADER_STRUCT_WRITE
16
+ } from "../RASTERIZATION_BUCKET_SORT_OUTPUT_HEADER_STRUCT_WRITE.js";
17
+
18
+
19
+ export const RASTERIZATION_BUCKET_SORT_OUTPUT_STRUCT_WRITE = WebGPUStruct.from({
20
+ headers: RASTERIZATION_BUCKET_SORT_OUTPUT_HEADER_STRUCT_WRITE,
21
+ elements: WebGPUArray.u32
22
+ });
23
+
24
+ const OFFSETS_TYPE = WebGPUArray.from(u32, RASTERIZATION_BUCKET_LIMIT);
25
+
26
+ const resources = new ShaderResourceSetDescriptor();
27
+ resources.createGroup()
28
+ .addStorageBuffer("scene_database", WebGPUArray.from(u32))
29
+ .addStorageBuffer("materials", WebGPUArray.from(MATERIAL_METADATA_STRUCT))
30
+
31
+ resources.createGroup()
32
+ .addStorageBuffer("input", COLLECTION_U32_STRUCT_READ)
33
+ .addStorageBuffer("offsets", OFFSETS_TYPE)
34
+ .addStorageBuffer("output", RASTERIZATION_BUCKET_SORT_OUTPUT_STRUCT_WRITE, true)
35
+
36
+
37
+ const GROUP_SIZE = COMPUTE_SHADER_DEFAULT_GROUP_SIZE;
38
+
39
+ // one lane per bucket makes the workgroup's claims
40
+ assert.lessThanOrEqual(RASTERIZATION_BUCKET_LIMIT, GROUP_SIZE);
41
+
42
+ /**
43
+ * Every element used to claim its slot with an `atomicAdd(&output.headers[bucket].count, 1u)` of
44
+ * its own. The address comes from the material, so nothing aggregated it, and every survivor of a
45
+ * view queued on its bucket's one counter: 0.47-0.80 ns a survivor, the largest non-raster pass of
46
+ * a view.
47
+ *
48
+ * Instead each lane ranks itself within its workgroup, in workgroup memory, and one lane per bucket
49
+ * claims the workgroup's whole run with a single global atomic. Measured on a 4090 against
50
+ * aggregating per subgroup (a ballot per bit of the bucket index, one atomic per bucket a wave
51
+ * holds), at 2M rows: 62 against 63 us for 1.48M elements of one bucket, 13 against 22 us for
52
+ * the main view's 774K, and 62 against 107 us for the 1.48M with six buckets to a wave. Waves on
53
+ * mixed content hold several buckets, and a workgroup claims each of them a quarter as often.
54
+ */
55
+ const body = CodeChunk.from(
56
+ //language=WGSL
57
+ `
58
+ var<workgroup> wg_count: array<atomic<u32>, ${RASTERIZATION_BUCKET_LIMIT}>;
59
+ var<workgroup> wg_base: array<u32, ${RASTERIZATION_BUCKET_LIMIT}>;
60
+
61
+ @compute @workgroup_size(${GROUP_SIZE})
62
+ fn main(
63
+ @builtin(global_invocation_id) global_id : vec3<u32>,
64
+ @builtin(local_invocation_index) local_index : u32,
65
+ ){
66
+
67
+ let index = global_id.x;
68
+
69
+ let input_size = min(input.count, arrayLength(&input.elements));
70
+
71
+ // a lane past the input does not return: every lane of the workgroup has to reach the barriers
72
+ let valid = index < input_size;
73
+
74
+ var mesh_index = 0u;
75
+ var bucket_index = 0u;
76
+ var rank = 0u;
77
+
78
+ if(valid){
79
+ mesh_index = input.elements[index];
80
+
81
+ let mesh = scene_read_mesh(&scene_database, mesh_index);
82
+
83
+ let material = materials[mesh.material];
84
+
85
+ bucket_index = rasterization_material_bucket(
86
+ material.transparency_mode,
87
+ material.draw_mode,
88
+ material.draw_side
89
+ );
90
+
91
+ // this element's place in the workgroup's run of its bucket
92
+ rank = atomicAdd(&wg_count[bucket_index], 1u);
93
+ }
94
+
95
+ workgroupBarrier();
96
+
97
+ // one lane per bucket claims the workgroup's whole run with one atomic
98
+ if(local_index < ${RASTERIZATION_BUCKET_LIMIT}u){
99
+ let run_length = atomicLoad(&wg_count[local_index]);
100
+
101
+ if(run_length > 0u){
102
+ let run_offset = atomicAdd(&output.headers[local_index].count, run_length);
103
+
104
+ if(run_offset == 0u){
105
+ // first element in bucket, write offset as well
106
+ output.headers[local_index].offset = offsets[local_index];
107
+ }
108
+
109
+ wg_base[local_index] = run_offset;
110
+ }
111
+ }
112
+
113
+ workgroupBarrier();
114
+
115
+ if(valid){
116
+ let output_offset = offsets[bucket_index] + wg_base[bucket_index] + rank;
117
+
118
+ if(output_offset < arrayLength(&output.elements)){
119
+ // guard against out-of-bounds access
120
+ output.elements[output_offset] = mesh_index;
121
+ }
122
+ }
123
+
124
+ }
125
+ `, [
126
+ chunk_rasterization_material_bucket,
127
+ chunk_scene_database_access,
128
+ ]
129
+ );
130
+
131
+ export const shader_instances_rasterization_bucket_sort = ComputeShader.from({
132
+ label: 'Rasterization bucket sort',
133
+ resources,
134
+ body,
89
135
  })
@@ -1 +1 @@
1
- {"version":3,"file":"shader_meshlets_rasterization_bucket_sort.d.ts","sourceRoot":"","sources":["../../../../../../../../src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.js"],"names":[],"mappings":"AA2FA,sEAIE;8BAtF4B,qCAAqC"}
1
+ {"version":3,"file":"shader_meshlets_rasterization_bucket_sort.d.ts","sourceRoot":"","sources":["../../../../../../../../src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.js"],"names":[],"mappings":"AA0HA,sEAIE;8BApH4B,qCAAqC"}
@@ -1,3 +1,4 @@
1
+ import { assert } from "../../../../../../core/assert.js";
1
2
  import { u32 } from "../../../../shader/type/WEBGPU_WGSL_PRIMITIVE_TYPES.js";
2
3
  import { WebGPUArray } from "../../../../shader/type/WebGPUArray.js";
3
4
  import { WebGPUStruct } from "../../../../shader/type/WebGPUStruct.js";
@@ -33,52 +34,82 @@ resources.createGroup()
33
34
  .addStorageBuffer("output", RASTERIZATION_MESHLET_BUCKET_SORT_OUTPUT_STRUCT_WRITE, true)
34
35
 
35
36
 
37
+ const GROUP_SIZE = COMPUTE_SHADER_DEFAULT_GROUP_SIZE;
38
+
39
+ // one lane per bucket makes the workgroup's claims
40
+ assert.lessThanOrEqual(RASTERIZATION_BUCKET_LIMIT, GROUP_SIZE);
41
+
42
+ /**
43
+ * The meshlet twin of {@link shader_instances_rasterization_bucket_sort}: the same workgroup
44
+ * histogram, for the same reason.
45
+ */
36
46
  const body = CodeChunk.from(
37
47
  //language=WGSL
38
48
  `
39
- @compute @workgroup_size(${COMPUTE_SHADER_DEFAULT_GROUP_SIZE})
49
+ var<workgroup> wg_count: array<atomic<u32>, ${RASTERIZATION_BUCKET_LIMIT}>;
50
+ var<workgroup> wg_base: array<u32, ${RASTERIZATION_BUCKET_LIMIT}>;
51
+
52
+ @compute @workgroup_size(${GROUP_SIZE})
40
53
  fn main(
41
54
  @builtin(workgroup_id) workgroup_id : vec3<u32>,
42
55
  @builtin(local_invocation_index) local_index : u32,
43
56
  ){
44
-
57
+
45
58
  // folded dispatch — see IndirectComputeCommandBuildPass
46
- let index = folded_workgroup_index(workgroup_id) * ${COMPUTE_SHADER_DEFAULT_GROUP_SIZE}u + local_index;
59
+ let index = folded_workgroup_index(workgroup_id) * ${GROUP_SIZE}u + local_index;
47
60
 
48
61
  let input_size = min(input.count, arrayLength(&input.elements));
49
62
 
50
- if(index >= input_size){
51
- return;
63
+ // a lane past the input does not return: every lane of the workgroup has to reach the barriers
64
+ let valid = index < input_size;
65
+
66
+ var bucket_index = 0u;
67
+ var rank = 0u;
68
+
69
+ if(valid){
70
+ let mesh_index = input.elements[index].mesh;
71
+
72
+ let mesh = scene_read_mesh(&scene_database, mesh_index);
73
+
74
+ let material = materials[mesh.material];
75
+
76
+ bucket_index = rasterization_material_bucket(
77
+ material.transparency_mode,
78
+ material.draw_mode,
79
+ material.draw_side
80
+ );
81
+
82
+ // this element's place in the workgroup's run of its bucket
83
+ rank = atomicAdd(&wg_count[bucket_index], 1u);
52
84
  }
53
85
 
54
- let meshlet_def = input.elements[index];
55
-
56
- let mesh_index = meshlet_def.mesh;
57
-
58
- let mesh = scene_read_mesh(&scene_database, mesh_index);
59
-
60
- let material = materials[mesh.material];
86
+ workgroupBarrier();
61
87
 
62
- let bucket_index = rasterization_material_bucket(
63
- material.transparency_mode,
64
- material.draw_mode,
65
- material.draw_side
66
- );
88
+ // one lane per bucket claims the workgroup's whole run with one atomic
89
+ if(local_index < ${RASTERIZATION_BUCKET_LIMIT}u){
90
+ let run_length = atomicLoad(&wg_count[local_index]);
67
91
 
68
- let element_offset = atomicAdd(&output.headers[bucket_index].count, 1u);
92
+ if(run_length > 0u){
93
+ let run_offset = atomicAdd(&output.headers[local_index].count, run_length);
69
94
 
70
- let bucket_offset = offsets[bucket_index];
95
+ if(run_offset == 0u){
96
+ // first element in bucket, write offset as well
97
+ output.headers[local_index].offset = offsets[local_index];
98
+ }
71
99
 
72
- if(element_offset == 0u){
73
- // first element in bucket, write offset as well
74
- output.headers[bucket_index].offset = bucket_offset;
100
+ wg_base[local_index] = run_offset;
101
+ }
75
102
  }
76
103
 
77
- let output_offset = bucket_offset + element_offset;
104
+ workgroupBarrier();
105
+
106
+ if(valid){
107
+ let output_offset = offsets[bucket_index] + wg_base[bucket_index] + rank;
78
108
 
79
- if(output_offset < arrayLength(&output.elements)){
80
- // guard against out-of-bounds access
81
- output.elements[output_offset] = meshlet_def;
109
+ if(output_offset < arrayLength(&output.elements)){
110
+ // guard against out-of-bounds access
111
+ output.elements[output_offset] = input.elements[index];
112
+ }
82
113
  }
83
114
 
84
115
  }
@@ -1 +1 @@
1
- {"version":3,"file":"graph_rasterize_scene_fast.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js"],"names":[],"mappings":"AAoBA;;;;;;;;;;GAUG;AACH;;;;;;;IAFa;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAC,CA0J3D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH;IAJ+B,KAAK;IACC,GAAG;IACA,MAAM;SA8C7C"}
1
+ {"version":3,"file":"graph_rasterize_scene_fast.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js"],"names":[],"mappings":"AAoBA;;;;;;;;;;GAUG;AACH;;;;;;;IAFa;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAC,CAkK3D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH;IAJ+B,KAAK;IACC,GAAG;IACA,MAAM;SA8C7C"}
@@ -13,8 +13,8 @@ import { graph_vgeo_expand_if_enabled } from "../../geometry/virtual/runtime/gra
13
13
  import { RasterizationCollectionType } from "../expand/RasterizationCollectionType.js";
14
14
  import { construct_primitive_state } from "../bucket/construct_primitive_state.js";
15
15
  import { rasterization_opaque_buckets } from "../bucket/rasterization_opaque_buckets.js";
16
- import { rasterization_has_opaque_bucket } from "../bucket/rasterization_has_opaque_bucket.js";
17
16
  import { graph_mesh_frustum_cull } from "../compute/graph_mesh_frustum_cull.js";
17
+ import { graph_clear_raster_targets } from "../graph_clear_raster_targets.js";
18
18
  import { graph_rasterize_meshes_alpha_tested } from "../native/graph_rasterize_meshes_alpha_tested.js";
19
19
  import { graph_extract_meshes_from_bucket } from "./graph_extract_meshes_from_bucket.js";
20
20
 
@@ -122,29 +122,39 @@ export function graph_rasterize_scene_fast_raw({
122
122
  is_first_draw = false;
123
123
  }
124
124
 
125
- // alpha-tested: reuse the existing native pass but skip HZB filtering by passing hzb=-1
126
- if (job.alpha_tested_pass !== undefined) {
127
- /**
128
- * The opaque path's first draw is the only thing that clears the visibility-buffer colour
129
- * attachments and the depth attachment — the clear values live on the opaque pass descriptor
130
- * and reach the targets through its `loadOp`, and every pass after it is recorded with
131
- * `load`. When the scene has no opaque bucket that draw never happens, so the first pass that
132
- * does run owns the clear. The alpha-tested descriptor carries the same clear values
133
- * (`[0,0,0,0]`, VIZ_BUFFER_INVALID_MESH_VALUE, DEPTH_BACKGROUND_VALUE), so handing it over
134
- * changes nothing about what the targets are cleared to.
135
- *
136
- * Wherever an opaque bucket exists — nearly every scene — this starts true and every call
137
- * below asks for `false`, exactly as before.
138
- */
139
- let targets_cleared = rasterization_has_opaque_bucket(active_buckets);
125
+ if (is_first_draw) {
126
+ /*
127
+ No opaque bucket, so no draw above cleared the target, and the rectangle holds whatever was
128
+ there before: in the atlas, the shadow of whatever was in the scene then; in a point light's
129
+ cube, which is a pooled transient, whatever the pool's last user left. The clear goes in the
130
+ first draw's place, and everything after this loads.
131
+ */
132
+ const cleared = graph_clear_raster_targets({
133
+ graph,
134
+ depth: target_depth,
135
+ color_targets: targets_color,
136
+ pass_descriptor: job.opaque_pass,
137
+ viewport,
138
+ });
140
139
 
140
+ target_depth = cleared.depth;
141
+ targets_color = cleared.color_targets;
142
+ }
141
143
 
144
+ // alpha-tested: reuse the existing native pass but skip HZB filtering by passing hzb=-1
145
+ if (job.alpha_tested_pass !== undefined) {
146
+ /*
147
+ Both alpha-tested passes load, never clear: the targets were cleared above, by the first
148
+ opaque draw or in its place. The clear used to be handed to the first alpha-tested draw, which
149
+ left a scene with neither kind of draw — nothing in it, or only transparent surfaces — with
150
+ nothing clearing at all.
151
+ */
142
152
  const alpha_tested_front = graph_rasterize_meshes_alpha_tested({
143
153
  graph,
144
154
  buckets: gr_render_buckets,
145
155
  view_ctx: view_ctx,
146
156
  render_pass_descriptor: job.alpha_tested_pass,
147
- clear_target: !targets_cleared,
157
+ clear_target: false,
148
158
  side: ShadeDrawSide.Front,
149
159
  cull_policy: job.cull_policy,
150
160
  limits,
@@ -157,14 +167,13 @@ export function graph_rasterize_scene_fast_raw({
157
167
 
158
168
  target_depth = alpha_tested_front.depth;
159
169
  targets_color = alpha_tested_front.color_targets;
160
- targets_cleared = targets_cleared || alpha_tested_front.executed;
161
170
 
162
171
  const alpha_tested_double = graph_rasterize_meshes_alpha_tested({
163
172
  graph,
164
173
  buckets: gr_render_buckets,
165
174
  view_ctx: view_ctx,
166
175
  render_pass_descriptor: job.alpha_tested_pass,
167
- clear_target: !targets_cleared,
176
+ clear_target: false,
168
177
  side: ShadeDrawSide.Double,
169
178
  cull_policy: job.cull_policy,
170
179
  limits,
@@ -177,7 +186,6 @@ export function graph_rasterize_scene_fast_raw({
177
186
 
178
187
  target_depth = alpha_tested_double.depth;
179
188
  targets_color = alpha_tested_double.color_targets;
180
- targets_cleared = targets_cleared || alpha_tested_double.executed;
181
189
  }
182
190
 
183
191
  return {target_depth, targets_color};
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Clear a rasterization's targets to what its first draw would have cleared them to, for a
3
+ * rasterization that has no first draw.
4
+ *
5
+ * A rasterization clears its targets through the `loadOp` of its first draw, which is its first opaque
6
+ * bucket's; everything after that loads. A scene with no opaque bucket has no such draw, and a scene
7
+ * with nothing in it has no draw at all — and no target starts a frame empty, so the frame then reads
8
+ * whatever it last held. The renderer's own targets outlive the frame: a mesh taken out of the scene
9
+ * stays in them, and so does the depth an extension wrote over the scene after the rasterizer was done.
10
+ * A graph transient comes out of a pool holding whatever its last user left, which is what a point
11
+ * light's cube faces are. This takes that draw's place.
12
+ *
13
+ * The clear values are the pass descriptor's own, so what the targets hold afterwards is exactly what
14
+ * the draw's clear would have left. A viewport short of the whole target fills that rectangle alone —
15
+ * shadow maps share an atlas, and a clear load op would erase every other map in it.
16
+ *
17
+ * @param {object} options
18
+ * @param {FrameGraph} options.graph
19
+ * @param {number} options.depth depth texture resource ID
20
+ * @param {number[]} options.color_targets colour texture resource IDs, in the order of the pass
21
+ * descriptor's colour attachments
22
+ * @param {RenderPassDescriptor} options.pass_descriptor the pass whose first draw this stands in for;
23
+ * the clear values are read off it
24
+ * @param {number[]} [options.viewport] [x, y, width, height]; a width or height of -1 runs to the edge of
25
+ * the target, and [0, 0, -1, -1] is the whole target
26
+ * @returns {{depth: number, color_targets: number[]}} the depth and colour handles, cleared
27
+ */
28
+ export function graph_clear_raster_targets({ graph, depth, color_targets, pass_descriptor, viewport, }: {
29
+ graph: FrameGraph;
30
+ depth: number;
31
+ color_targets: number[];
32
+ pass_descriptor: RenderPassDescriptor;
33
+ viewport?: number[];
34
+ }): {
35
+ depth: number;
36
+ color_targets: number[];
37
+ };
38
+ //# sourceMappingURL=graph_clear_raster_targets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph_clear_raster_targets.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/rasterize/graph_clear_raster_targets.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH;IAV+B,KAAK;IACT,KAAK,EAArB,MAAM;IACY,aAAa,EAA/B,MAAM,EAAE;IAEsB,eAAe;IAE1B,QAAQ,GAA3B,MAAM,EAAE;IAEN;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAC,CA4EpD"}
@@ -0,0 +1,105 @@
1
+ import { assert } from "../../../core/assert.js";
2
+ import { texture_fill_rectangle } from "../texture/util/texture_fill_rectangle.js";
3
+
4
+ /**
5
+ * Clear a rasterization's targets to what its first draw would have cleared them to, for a
6
+ * rasterization that has no first draw.
7
+ *
8
+ * A rasterization clears its targets through the `loadOp` of its first draw, which is its first opaque
9
+ * bucket's; everything after that loads. A scene with no opaque bucket has no such draw, and a scene
10
+ * with nothing in it has no draw at all — and no target starts a frame empty, so the frame then reads
11
+ * whatever it last held. The renderer's own targets outlive the frame: a mesh taken out of the scene
12
+ * stays in them, and so does the depth an extension wrote over the scene after the rasterizer was done.
13
+ * A graph transient comes out of a pool holding whatever its last user left, which is what a point
14
+ * light's cube faces are. This takes that draw's place.
15
+ *
16
+ * The clear values are the pass descriptor's own, so what the targets hold afterwards is exactly what
17
+ * the draw's clear would have left. A viewport short of the whole target fills that rectangle alone —
18
+ * shadow maps share an atlas, and a clear load op would erase every other map in it.
19
+ *
20
+ * @param {object} options
21
+ * @param {FrameGraph} options.graph
22
+ * @param {number} options.depth depth texture resource ID
23
+ * @param {number[]} options.color_targets colour texture resource IDs, in the order of the pass
24
+ * descriptor's colour attachments
25
+ * @param {RenderPassDescriptor} options.pass_descriptor the pass whose first draw this stands in for;
26
+ * the clear values are read off it
27
+ * @param {number[]} [options.viewport] [x, y, width, height]; a width or height of -1 runs to the edge of
28
+ * the target, and [0, 0, -1, -1] is the whole target
29
+ * @returns {{depth: number, color_targets: number[]}} the depth and colour handles, cleared
30
+ */
31
+ export function graph_clear_raster_targets({
32
+ graph,
33
+ depth,
34
+ color_targets,
35
+ pass_descriptor,
36
+ viewport = [0, 0, -1, -1],
37
+ }) {
38
+ assert.defined(graph, 'graph');
39
+ assert.isNonNegativeInteger(depth, 'depth');
40
+ assert.isArray(color_targets, 'color_targets');
41
+ assert.equal(pass_descriptor.isRenderPassDescriptor, true, "pass_descriptor must be a RenderPassDescriptor");
42
+ assert.equal(color_targets.length, pass_descriptor.colorAttachments.length, 'one colour target per colour attachment of the pass');
43
+
44
+ /**
45
+ * @param data
46
+ * @param {FramePassResources} resources
47
+ * @param {GraphicsExecutionContext} context
48
+ */
49
+ function execute(data, resources, context) {
50
+ const encoder = context.encoder;
51
+
52
+ /**
53
+ * @type {GPUTextureContext}
54
+ */
55
+ const depth_texture = resources.get(data.depth);
56
+
57
+ /**
58
+ * @type {GPUTextureContext[]}
59
+ */
60
+ const color_textures = data.color_targets.map(id => resources.get(id));
61
+
62
+ const x = viewport[0];
63
+ const y = viewport[1];
64
+ const width = viewport[2] >= 0 ? viewport[2] : depth_texture.width - x;
65
+ const height = viewport[3] >= 0 ? viewport[3] : depth_texture.height - y;
66
+
67
+ const is_whole_target = x === 0 && y === 0 && width === depth_texture.width && height === depth_texture.height;
68
+
69
+ if (is_whole_target) {
70
+ const gpu_descriptor = pass_descriptor.toGPU({
71
+ colorTargets: color_textures,
72
+ depthTarget: depth_texture,
73
+ loadOp: "clear",
74
+ storeOp: "store",
75
+ });
76
+
77
+ // not the draw's pipeline label: nothing is drawn
78
+ gpu_descriptor.label = "Clear rasterization targets";
79
+
80
+ encoder.beginRenderPass(gpu_descriptor).end();
81
+
82
+ return;
83
+ }
84
+
85
+ const rectangle = [x, y, width, height];
86
+
87
+ for (let i = 0; i < color_textures.length; i++) {
88
+ texture_fill_rectangle(encoder, color_textures[i], rectangle, pass_descriptor.colorAttachments[i].clearValue);
89
+ }
90
+
91
+ texture_fill_rectangle(encoder, depth_texture, rectangle, [pass_descriptor.depthStencilAttachment.depthClearValue]);
92
+ }
93
+
94
+ const data = {};
95
+
96
+ const pass = graph.add("clear raster targets", data, execute);
97
+
98
+ data.depth = pass.write(depth);
99
+ data.color_targets = color_targets.map(pass.write, pass);
100
+
101
+ return {
102
+ depth: data.depth,
103
+ color_targets: data.color_targets,
104
+ };
105
+ }
@@ -887,3 +887,23 @@ reach — the dynamic-mesh directory, the orchestrator run with a real channel,
887
887
  93 tests — passes, and the type check is at budget. On the Metal adapter, `@builtin(sample_mask)` and
888
888
  `@interpolate(perspective, centroid)` compile and the multisampled pipeline builds; no uncaptured device
889
889
  errors, including 240 frames of a live trail on a moving path, fading in and out.
890
+
891
+ ## 25. A trail's draw constants go up once a frame — 2026-09-27
892
+
893
+ Entry 23 left each live mesh's constants built twice — a transient uniform and a bind group of its own
894
+ for the splat, and again for the draw — noted, not shaved. They are shaved here, as part of moving
895
+ every dynamic mesh's per-draw constants into a table (CHANGELOG, Unreleased, Rendering): the
896
+ occupancy, after its upload, writes the frame's `GPUDynamicMeshDrawList` — one transient storage
897
+ buffer and one write per 1,024 draws — and the splat and the draw both bind it and name their rows by
898
+ `first_instance`. The splat's layout is four groups now (constants, geometry, warp, volume), which is
899
+ WebGPU's default `maxBindGroups`.
900
+
901
+ **Measured** with 1,500 live meshes through the channel at 1280×720 on the RTX 4090, asserts
902
+ stripped, paired: 9.8–11.4 ms of CPU a frame to 2.0–2.5; 3,032 copies a frame to 34 and 7,624
903
+ `setBindGroup` to 3,131. The pictures after equal frame counts are bit-identical to before.
904
+
905
+ **Verified.** `DynamicMeshAVBOITChannel.draws.spec.js` checks the splat and the draw read each mesh's own
906
+ constants from one table, and fails with a draw pass that uploads its own copy (two tables).
907
+ `chunk_dynamic_mesh_vertex.spec.js` runs the shared AVBOIT vertex stage on the emulator and checks a
908
+ vertex is carried by the row its draw names. The other half of the item — the emptiness query and the
909
+ gather both walking the first live mesh's triangles — is left as it was.
@@ -12,7 +12,7 @@
12
12
  * @param {Set<number>} active_buckets IDs of buckets that have any materials that are active.
13
13
  * @param {number[]} [viewport] optional viewport definition, if not provided, will use the entire render target. Useful for drawing multiple views into the same render target.
14
14
  * @param {Readonly<Record<number, GPUCullMode>>} [cull_policy] side→cullMode mapping; defaults to {@link CullPolicy.Standard}
15
- * @returns {{instances_maybe:number, meshlets_maybe:number, depth:number, color_targets:number[]}} the "maybe visible" sets, and the depth and colour handles produced by the rasterization
15
+ * @returns {{instances_maybe:number, meshlets_maybe:number, depth:number, color_targets:number[]}} the "maybe visible" sets, and the depth and colour handles produced by the rasterization — cleared whether or not any bucket drew
16
16
  */
17
17
  export function graph_rasterize_partial_opaque({ graph, buckets, view, render_pass_descriptor, limits, depth, raster_color_targets, hzb, viewport, active_buckets, cull_policy, }: FrameGraph): {
18
18
  instances_maybe: number;
@@ -1 +1 @@
1
- {"version":3,"file":"graph_rasterize_partial_opaque.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js"],"names":[],"mappings":"AAwBA;;;;;;;;;;;;;;;GAeG;AACH,gMAFa;IAAC,eAAe,EAAC,MAAM,CAAC;IAAC,cAAc,EAAC,MAAM,CAAC;IAAC,KAAK,EAAC,MAAM,CAAC;IAAC,aAAa,EAAC,MAAM,EAAE,CAAA;CAAC,CAgMjG"}
1
+ {"version":3,"file":"graph_rasterize_partial_opaque.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js"],"names":[],"mappings":"AAyBA;;;;;;;;;;;;;;;GAeG;AACH,gMAFa;IAAC,eAAe,EAAC,MAAM,CAAC;IAAC,cAAc,EAAC,MAAM,CAAC;IAAC,KAAK,EAAC,MAAM,CAAC;IAAC,aAAa,EAAC,MAAM,EAAE,CAAA;CAAC,CAmNjG"}