@woosh/meep-engine 3.31.0 → 3.32.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 (72) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/package.json +1 -1
  3. package/src/engine/graphics3/DebugDrawSystem.d.ts.map +1 -1
  4. package/src/engine/graphics3/DebugDrawSystem.js +6 -2
  5. package/src/engine/graphics3/PathDisplaySystem.d.ts.map +1 -1
  6. package/src/engine/graphics3/PathDisplaySystem.js +311 -307
  7. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  8. package/src/shade/RENDERER_CONTRACT.md +15 -2
  9. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +5 -0
  10. package/src/shade/device/ShadeGPUCommandContext.d.ts +15 -2
  11. package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
  12. package/src/shade/device/ShadeGPUCommandContext.js +154 -25
  13. package/src/shade/renderer/GraphicsContext.d.ts +9 -1
  14. package/src/shade/renderer/GraphicsContext.d.ts.map +1 -1
  15. package/src/shade/renderer/GraphicsContext.js +54 -15
  16. package/src/shade/renderer/Renderer.d.ts +19 -4
  17. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  18. package/src/shade/renderer/Renderer.js +113 -2
  19. package/src/shade/renderer/animation/GPUAnimationManager.d.ts +10 -7
  20. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  21. package/src/shade/renderer/animation/GPUAnimationManager.js +147 -45
  22. package/src/shade/renderer/buffer/GPUBufferStagingAllocator.d.ts +11 -2
  23. package/src/shade/renderer/buffer/GPUBufferStagingAllocator.d.ts.map +1 -1
  24. package/src/shade/renderer/buffer/GPUBufferStagingAllocator.js +95 -13
  25. package/src/shade/renderer/buffer/GPUBufferUploadAllocator.d.ts +60 -0
  26. package/src/shade/renderer/buffer/GPUBufferUploadAllocator.d.ts.map +1 -0
  27. package/src/shade/renderer/buffer/GPUBufferUploadAllocator.js +202 -0
  28. package/src/shade/renderer/buffer/GPUUploadBlock.d.ts +68 -0
  29. package/src/shade/renderer/buffer/GPUUploadBlock.d.ts.map +1 -0
  30. package/src/shade/renderer/buffer/GPUUploadBlock.js +97 -0
  31. package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.d.ts +20 -7
  32. package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.d.ts.map +1 -1
  33. package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.js +20 -7
  34. package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.d.ts +5 -0
  35. package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.d.ts.map +1 -1
  36. package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.js +19 -0
  37. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  38. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  39. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts +3 -1
  40. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts.map +1 -1
  41. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.js +3 -1
  42. package/src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_rasterization_bucket_sort.d.ts.map +1 -1
  43. package/src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_rasterization_bucket_sort.js +134 -88
  44. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.d.ts.map +1 -1
  45. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.js +57 -26
  46. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  47. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +28 -20
  48. package/src/shade/renderer/rasterize/graph_clear_raster_targets.d.ts +38 -0
  49. package/src/shade/renderer/rasterize/graph_clear_raster_targets.d.ts.map +1 -0
  50. package/src/shade/renderer/rasterize/graph_clear_raster_targets.js +105 -0
  51. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts +1 -1
  52. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  53. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +21 -1
  54. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.d.ts.map +1 -1
  55. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.js +12 -20
  56. package/src/shade/renderer/shader/graph/graph_import_typed_buffer.d.ts +2 -1
  57. package/src/shade/renderer/shader/graph/graph_import_typed_buffer.d.ts.map +1 -1
  58. package/src/shade/renderer/shader/graph/graph_import_typed_buffer.js +15 -28
  59. package/src/core/geom/3d/atlas/atlas_bench_lib.d.ts +0 -73
  60. package/src/core/geom/3d/atlas/atlas_bench_lib.d.ts.map +0 -1
  61. package/src/engine/graphics3/terrain/GPUTerrainSplatRenderer.d.ts +0 -59
  62. package/src/engine/graphics3/terrain/GPUTerrainSplatRenderer.d.ts.map +0 -1
  63. package/src/engine/graphics3/terrain/pack_terrain_row_table.d.ts +0 -22
  64. package/src/engine/graphics3/terrain/pack_terrain_row_table.d.ts.map +0 -1
  65. package/src/engine/graphics3/terrain/shader_terrain_splat.d.ts +0 -26
  66. package/src/engine/graphics3/terrain/shader_terrain_splat.d.ts.map +0 -1
  67. package/src/shade/renderer/animation/ANIMATION_BOUND_TRACK_GROUP_STRUCT.d.ts +0 -25
  68. package/src/shade/renderer/animation/ANIMATION_BOUND_TRACK_GROUP_STRUCT.d.ts.map +0 -1
  69. package/src/shade/renderer/animation/ANIMATION_KEYFRAME_BLOCK_STRUCT.d.ts +0 -26
  70. package/src/shade/renderer/animation/ANIMATION_KEYFRAME_BLOCK_STRUCT.d.ts.map +0 -1
  71. package/src/shade/renderer/animation/ANIMATION_SKIN_JOINT_BLOCK_STRUCT.d.ts +0 -29
  72. package/src/shade/renderer/animation/ANIMATION_SKIN_JOINT_BLOCK_STRUCT.d.ts.map +0 -1
@@ -30,6 +30,11 @@ export class GPUDynamicMeshRenderer {
30
30
  * Set before the first draw. Changing it later rebuilds the pipeline, which is cheap but not
31
31
  * free, and the whole batch moves together — this is a property of the pass, not of a mesh.
32
32
  *
33
+ * Solid content belongs at `FramePhase.AfterLighting`, with the depth {@link graph_draw} hands
34
+ * back published: transparency runs after that phase and tests against that depth, so glass in
35
+ * front of the batch covers it. At `AfterTransparency` the glass is already resolved, and the
36
+ * batch is drawn over it.
37
+ *
33
38
  * @type {boolean}
34
39
  */
35
40
  opaque = false;
@@ -52,11 +57,16 @@ export class GPUDynamicMeshRenderer {
52
57
  /**
53
58
  * Record the draws into `graph`, over the colour it is given.
54
59
  *
55
- * When {@link opaque} is set the pass writes the depth it is given. That write lands in the
56
- * same physical texture the handle names — a written handle is a renamed clone of one resource,
57
- * not a second one — so a later pass reading the handle the caller still holds sees the depth
58
- * this pass left. Only the colour is threaded back, because that is what an injected pass is
59
- * allowed to replace.
60
+ * **Both handles come back, and the caller publishes both** — into `SceneColor` and
61
+ * `ViewTextures` from an extension. The colour is always replaced. The depth is replaced when
62
+ * {@link opaque} is set, since the pass writes it; otherwise it comes back as it was given.
63
+ *
64
+ * A written handle is a renamed clone of the same texture, so a pass that goes on reading the
65
+ * depth handle it held before this draw is reading a version this pass has superseded.
66
+ * `FrameGraph.validate` refuses that, and the frame fails to compile in a development build.
67
+ * A stripped build reads the right contents only because passes run in the order they were
68
+ * recorded. So every reader of the scene depth after this draw has to be handed the new handle:
69
+ * depth of field, motion blur, the NSS upscaler, the next extension along.
60
70
  *
61
71
  * @param {object} args
62
72
  * @param {FrameGraph} args.graph
@@ -64,7 +74,7 @@ export class GPUDynamicMeshRenderer {
64
74
  * @param {number} args.color colour resource handle to draw over
65
75
  * @param {number} args.depth scene depth, written when {@link opaque} and otherwise only tested
66
76
  * @param {GPUBuffer} args.camera
67
- * @returns {number} the colour handle after the draws
77
+ * @returns {{color: number, depth: number}} the colour and depth handles after the draws
68
78
  */
69
79
  graph_draw({ graph, batch, color, depth, camera }) {
70
80
  assert.defined(graph, 'graph');
@@ -86,7 +96,10 @@ export class GPUDynamicMeshRenderer {
86
96
  data.depth = data.opaque ? pass.write(depth) : pass.read(depth);
87
97
  data.color = pass.write(pass.read(color));
88
98
 
89
- return data.color;
99
+ return {
100
+ color: data.color,
101
+ depth: data.depth,
102
+ };
90
103
  }
91
104
 
92
105
  /**
@@ -1,4 +1,9 @@
1
1
  /**
2
+ * Needs meshoptimizer's WASM, which instantiates asynchronously at import. A build before it has
3
+ * fails inside the vendored module, on an instance that is not there yet, with a message about
4
+ * `exports` that names nothing the caller did — so await `MeshoptClusterizer.ready` before the
5
+ * first call.
6
+ *
2
7
  * @param {RowFirstTable} result_metadata
3
8
  * @param {BinaryBuffer} result_data
4
9
  * @param {number[]} input_index Index array
@@ -1 +1 @@
1
- {"version":3,"file":"geometry_to_meshlets_meshoptimizer.d.ts","sourceRoot":"","sources":["../../../../../../../../src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.js"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,2HAPW,MAAM,EAAE,qBACR,MAAM,gBACN,MAAM,oBACN,YAAY,yBACZ,MAAM,2BACN,MAAM,QAkDhB"}
1
+ {"version":3,"file":"geometry_to_meshlets_meshoptimizer.d.ts","sourceRoot":"","sources":["../../../../../../../../src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.js"],"names":[],"mappings":"AAgBA;;;;;;;;;;;;;;GAcG;AACH,2HAPW,MAAM,EAAE,qBACR,MAAM,gBACN,MAAM,oBACN,YAAY,yBACZ,MAAM,2BACN,MAAM,QAmDhB"}
@@ -1,7 +1,25 @@
1
+ import { assert } from "../../../../../../core/assert.js";
1
2
  import { MeshoptClusterizer } from "./meshopt_clusterizer.js";
2
3
  import { meshopt_optimizeMeshlet } from "./meshopt_optimizeMeshlet.js";
3
4
 
4
5
  /**
6
+ * Whether meshoptimizer's WASM has landed. The vendored module instantiates it asynchronously at
7
+ * import and exposes only the promise, so this holds the promise's outcome for the assertion below.
8
+ *
9
+ * @type {boolean}
10
+ */
11
+ let clusterizer_ready = false;
12
+
13
+ MeshoptClusterizer.ready?.then(() => {
14
+ clusterizer_ready = true;
15
+ });
16
+
17
+ /**
18
+ * Needs meshoptimizer's WASM, which instantiates asynchronously at import. A build before it has
19
+ * fails inside the vendored module, on an instance that is not there yet, with a message about
20
+ * `exports` that names nothing the caller did — so await `MeshoptClusterizer.ready` before the
21
+ * first call.
22
+ *
5
23
  * @param {RowFirstTable} result_metadata
6
24
  * @param {BinaryBuffer} result_data
7
25
  * @param {number[]} input_index Index array
@@ -21,6 +39,7 @@ export function geometry_to_meshlets_meshoptimizer(
21
39
  meshlet_vertex_limit = 64,
22
40
  meshlet_triangle_limit = 64,
23
41
  ) {
42
+ assert.ok(clusterizer_ready, 'meshoptimizer is not ready; await MeshoptClusterizer.ready');
24
43
 
25
44
  const meshlets = MeshoptClusterizer.buildMeshlets(input_index, vertex_positions, 3, meshlet_vertex_limit, meshlet_triangle_limit, 0.0);
26
45
 
@@ -13,7 +13,7 @@ export class TinyGltf {
13
13
  * @readonly
14
14
  * @type {TextureFormat}
15
15
  */
16
- static readonly "__#250@#UNCOMPRESSED_TARGET": TextureFormat;
16
+ static readonly "__#251@#UNCOMPRESSED_TARGET": TextureFormat;
17
17
  /**
18
18
  * @param {AssetManager} assetManager
19
19
  * @param {AssetRequestScope} [scope]
@@ -53,7 +53,7 @@ export class NSS {
53
53
  * @returns {number}
54
54
  */
55
55
  static recommended_jitter_sequence_size(upscale_ratio: number): number;
56
- static "__#184@#preprocess_tensor_descriptor"(render_w: any, render_h: any): TextureResourceDescriptor;
56
+ static "__#185@#preprocess_tensor_descriptor"(render_w: any, render_h: any): TextureResourceDescriptor;
57
57
  /**
58
58
  * @param {GraphicsContext} graphics
59
59
  */
@@ -9,7 +9,9 @@
9
9
  * and every pixel the alpha-tested pass did not cover keeping last frame's mesh and triangle ids for
10
10
  * the deferred shading to resolve as real surfaces.
11
11
  *
12
- * This is what lets the composer hand the clear to the first pass that will actually run.
12
+ * The composers used this to hand the clear to the first alpha-tested draw, which left a scene with
13
+ * no draw of either kind uncleared. They no longer ask: the opaque loop knows whether it drew, and
14
+ * when it did not, `graph_clear_raster_targets` clears in its first draw's place.
13
15
  *
14
16
  * @param {Set<number>} active_buckets bucket ids any material in the scene asks for
15
17
  * @returns {boolean}
@@ -1 +1 @@
1
- {"version":3,"file":"rasterization_has_opaque_bucket.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,gEANW,IAAI,MAAM,CAAC,GACT,OAAO,CAWnB"}
1
+ {"version":3,"file":"rasterization_has_opaque_bucket.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,gEANW,IAAI,MAAM,CAAC,GACT,OAAO,CAWnB"}
@@ -11,7 +11,9 @@ import { rasterization_opaque_buckets } from "./rasterization_opaque_buckets.js"
11
11
  * and every pixel the alpha-tested pass did not cover keeping last frame's mesh and triangle ids for
12
12
  * the deferred shading to resolve as real surfaces.
13
13
  *
14
- * This is what lets the composer hand the clear to the first pass that will actually run.
14
+ * The composers used this to hand the clear to the first alpha-tested draw, which left a scene with
15
+ * no draw of either kind uncleared. They no longer ask: the opaque loop knows whether it drew, and
16
+ * when it did not, `graph_clear_raster_targets` clears in its first draw's place.
15
17
  *
16
18
  * @param {Set<number>} active_buckets bucket ids any material in the scene asks for
17
19
  * @returns {boolean}
@@ -1 +1 @@
1
- {"version":3,"file":"shader_instances_rasterization_bucket_sort.d.ts","sourceRoot":"","sources":["../../../../../../../../src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_rasterization_bucket_sort.js"],"names":[],"mappings":"AAiBA,yEAGG;AAgEH,uEAIE;6BAtF2B,yCAAyC;8BAMxC,qCAAqC"}
1
+ {"version":3,"file":"shader_instances_rasterization_bucket_sort.d.ts","sourceRoot":"","sources":["../../../../../../../../src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_rasterization_bucket_sort.js"],"names":[],"mappings":"AAkBA,yEAGG;AA6GH,uEAIE;6BAnI2B,yCAAyC;8BAMxC,qCAAqC"}
@@ -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};