@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
@@ -8,8 +8,13 @@ import { binarySearchHighIndex } from "../../../core/collection/array/binarySear
8
8
  let id_count = 0;
9
9
 
10
10
  /**
11
- * Provides staging buffers for pushing data from CPU to GPU.
11
+ * Mapped `MAP_WRITE | COPY_SRC` buffers for pushing data from the CPU to the GPU, one per upload.
12
12
  *
13
+ * A buffer comes out mapped; its holder fills it, unmaps it, copies out of it and gives it back, and
14
+ * the pool maps it again — a round trip through the GPU — before it can come out again. For bytes
15
+ * that is the cheaper road; for thousands of small writes a frame the round trips are the cost, which
16
+ * is why a {@link ShadeGPUCommandContext} brings only its large writes here (see
17
+ * `MAPPED_UPLOAD_THRESHOLD`).
13
18
  */
14
19
  export class GPUBufferStagingAllocator {
15
20
 
@@ -24,6 +29,39 @@ export class GPUBufferStagingAllocator {
24
29
  */
25
30
  #ready = [];
26
31
 
32
+ /**
33
+ * Bytes in {@link #ready}.
34
+ *
35
+ * @type {number}
36
+ */
37
+ #ready_byte_size = 0;
38
+
39
+ /**
40
+ * Bytes in buffers handed out and not back in {@link #ready} yet: with their holder, or on their
41
+ * way back through a mapping. They are memory like any other, and a GPU running behind holds a
42
+ * frame's worth of them for every frame it is behind.
43
+ *
44
+ * @type {number}
45
+ */
46
+ #held_byte_size = 0;
47
+
48
+ /**
49
+ * Buffers handed out and not given back yet. Only these are taken back: a buffer given back
50
+ * twice, or one this pool never made, would otherwise go into {@link #ready} twice and come out
51
+ * to two holders at once.
52
+ *
53
+ * @type {Set<GPUBuffer>}
54
+ */
55
+ #held = new Set();
56
+
57
+ /**
58
+ * Set by {@link destroy}. A buffer whose mapping comes back after that is destroyed, not pooled —
59
+ * the pool it would go into is gone.
60
+ *
61
+ * @type {boolean}
62
+ */
63
+ #destroyed = false;
64
+
27
65
  /**
28
66
  *
29
67
  * @param {GPUDevice} device
@@ -35,17 +73,13 @@ export class GPUBufferStagingAllocator {
35
73
  }
36
74
 
37
75
  /**
76
+ * Bytes in every buffer this pool has made and not destroyed: free, handed out, or on the way
77
+ * back.
38
78
  *
39
79
  * @returns {number}
40
80
  */
41
81
  get gpu_memory_usage() {
42
- let r = 0;
43
-
44
- for (const b of this.#ready) {
45
- r += b.size;
46
- }
47
-
48
- return r;
82
+ return this.#ready_byte_size + this.#held_byte_size;
49
83
  }
50
84
 
51
85
 
@@ -78,8 +112,13 @@ export class GPUBufferStagingAllocator {
78
112
  mappedAtCreation: true,
79
113
  });
80
114
 
115
+ } else {
116
+ this.#ready_byte_size -= result.size;
81
117
  }
82
118
 
119
+ this.#held_byte_size += result.size;
120
+ this.#held.add(result);
121
+
83
122
  assert.greaterThanOrEqual(result.size, min_size_bytes, 'result.size < min_size_bytes');
84
123
  assert.equal(result.mapState, "mapped", 'result.mapState !== "mapped"');
85
124
 
@@ -113,38 +152,81 @@ export class GPUBufferStagingAllocator {
113
152
 
114
153
  this.#ready.splice(index, 0, buffer);
115
154
 
155
+ this.#ready_byte_size += buffer.size;
156
+
116
157
  return true;
117
158
  }
118
159
 
119
160
  /**
120
161
  *
121
162
  * @param {GPUBuffer} buffer
163
+ * @returns {boolean} whether it was taken back: false for a buffer given back already, or one
164
+ * this pool did not hand out
122
165
  */
123
166
  release(buffer) {
167
+ if (!this.#held.delete(buffer)) {
168
+ return false;
169
+ }
170
+
124
171
  const state = buffer.mapState;
125
172
 
126
- // TODO have a cleanup mechanism to prevent memory leak
173
+ // TODO the ready list never shrinks: a burst's buffers stay until destroy()
127
174
 
128
175
  if (state === "mapped") {
129
176
  // release directly
130
- this.#add_to_ready(buffer);
177
+ this.#take_back(buffer);
131
178
  } else if (state === "unmapped") {
132
179
  buffer.mapAsync(GPUMapMode.WRITE)
133
180
  .then(
134
- () => this.#add_to_ready(buffer),
135
- console.error
181
+ () => this.#take_back(buffer),
182
+ () => this.#give_up(buffer)
136
183
  );
137
184
  } else {
138
185
  throw new Error(`Invalid map state: ${state}`);
139
186
  }
187
+
188
+ return true;
189
+ }
190
+
191
+ /**
192
+ * A buffer back and mapped, to be handed out again.
193
+ *
194
+ * @param {GPUBuffer} buffer
195
+ */
196
+ #take_back(buffer) {
197
+ this.#held_byte_size -= buffer.size;
198
+
199
+ if (this.#destroyed) {
200
+ buffer.destroy();
201
+
202
+ return;
203
+ }
204
+
205
+ this.#add_to_ready(buffer);
206
+ }
207
+
208
+ /**
209
+ * A mapping that failed — the buffer or its device is gone — brings nothing back to reuse. It
210
+ * used to be logged and dropped: never destroyed, so whatever was left of it waited for the
211
+ * garbage collector.
212
+ *
213
+ * @param {GPUBuffer} buffer
214
+ */
215
+ #give_up(buffer) {
216
+ this.#held_byte_size -= buffer.size;
217
+
218
+ buffer.destroy();
140
219
  }
141
220
 
142
221
  destroy() {
222
+ this.#destroyed = true;
223
+
143
224
  for (const b of this.#ready) {
144
225
  b.destroy();
145
226
  }
146
227
 
147
228
  // drop data
148
229
  this.#ready.splice(0, this.#ready.length);
230
+ this.#ready_byte_size = 0;
149
231
  }
150
- }
232
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Uploads of this many bytes or more go through a mapped staging buffer of their own
3
+ * ({@link GraphicsContext#buffer_allocator_staging}); smaller ones through upload blocks.
4
+ *
5
+ * The two ways cost differently. A mapping is a round trip per upload, which thousands of small
6
+ * uniforms a frame cannot afford: 1 KiB uploads moved 8–10 MiB/s mapped against 430–470 MiB/s
7
+ * through blocks. Bytes, though, travel better mapped — filled in place and copied once, where a
8
+ * block's are copied into it, sent with a queue write and copied again on the GPU — and 1 MiB
9
+ * uploads moved 3,350–3,560 MiB/s mapped against 1,480–1,640 through blocks. The two crossed between
10
+ * 24 and 32 KiB (RTX 4090, Chrome 154, 4 MiB per submission in uploads of one size, 2026-09-26).
11
+ *
12
+ * @type {number}
13
+ */
14
+ export const MAPPED_UPLOAD_THRESHOLD: number;
15
+ /**
16
+ * Size of every upload block. Everything that comes here is smaller than
17
+ * {@link MAPPED_UPLOAD_THRESHOLD}, so any empty block holds any write, and a frame's uniforms — a
18
+ * few thousand at the heavy end, tens of bytes each — fill one or two.
19
+ *
20
+ * @type {number}
21
+ */
22
+ export const UPLOAD_BLOCK_SIZE: number;
23
+ /**
24
+ * Upload memory for {@link ShadeGPUCommandContext}'s small writes: pooled {@link GPUUploadBlock}s,
25
+ * all {@link UPLOAD_BLOCK_SIZE} bytes, each a `COPY_SRC | COPY_DST` buffer paired with CPU memory of
26
+ * the same size.
27
+ *
28
+ * A block is a transient in {@link TransientResourcePool}'s sense and is reused as soon as it is
29
+ * given back, for the reason `ShadeGPUCommandContext#send_uploads` sets out. It is a pool of its own,
30
+ * not {@link GraphicsContext#buffer_allocator_main}, because the frame graph aliases that pool's
31
+ * buffers within a submission, and a queue write — which lands ahead of the whole submission — into
32
+ * an aliased buffer would overwrite what an earlier pass in it still needs. Nothing aliases a block.
33
+ */
34
+ export class GPUBufferUploadAllocator extends TransientResourcePool {
35
+ /**
36
+ * @param {GPUDevice} device
37
+ */
38
+ constructor(device: GPUDevice);
39
+ /**
40
+ * An empty block. Give it back with {@link release}.
41
+ *
42
+ * @returns {GPUUploadBlock}
43
+ */
44
+ get(): GPUUploadBlock;
45
+ /**
46
+ * @param {GPUUploadBlock} block
47
+ * @returns {boolean}
48
+ */
49
+ release(block: GPUUploadBlock): boolean;
50
+ find(): number;
51
+ compare(a: any, b: any): number;
52
+ sizeOf(block: any): any;
53
+ make(): GPUUploadBlock;
54
+ destroyResource(block: any): void;
55
+ afterAcquire(block: any): void;
56
+ #private;
57
+ }
58
+ import { TransientResourcePool } from "../pool/TransientResourcePool.js";
59
+ import { GPUUploadBlock } from "./GPUUploadBlock.js";
60
+ //# sourceMappingURL=GPUBufferUploadAllocator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GPUBufferUploadAllocator.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/buffer/GPUBufferUploadAllocator.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;GAYG;AACH,sCAFU,MAAM,CAEqC;AAErD;;;;;;GAMG;AACH,gCAFU,MAAM,CAEgC;AAShD;;;;;;;;;;GAUG;AACH;IAeI;;OAEG;IACH,+BAMC;IAED;;;;OAIG;IACH,OAFa,cAAc,CAI1B;IAkBD;;;OAGG;IACH,eAHW,cAAc,GACZ,OAAO,CAMnB;IAED,eAYC;IAED,gCAIC;IAED,wBAEC;IAED,uBAkDC;IAED,kCAEC;IAED,+BAEC;;CASJ;sCArMqC,kCAAkC;+BACzC,qBAAqB"}
@@ -0,0 +1,202 @@
1
+ import { assert } from "../../../core/assert.js";
2
+ import { KILOBYTE } from "../../../core/science/units/memory/KILOBYTE.js";
3
+ import { MEGABYTE } from "../../../core/science/units/memory/MEGABYTE.js";
4
+ import { warn_limited } from "../../util/warn_limited.js";
5
+ import { TransientResourcePool } from "../pool/TransientResourcePool.js";
6
+ import { GPUUploadBlock } from "./GPUUploadBlock.js";
7
+
8
+ /**
9
+ * Uploads of this many bytes or more go through a mapped staging buffer of their own
10
+ * ({@link GraphicsContext#buffer_allocator_staging}); smaller ones through upload blocks.
11
+ *
12
+ * The two ways cost differently. A mapping is a round trip per upload, which thousands of small
13
+ * uniforms a frame cannot afford: 1 KiB uploads moved 8–10 MiB/s mapped against 430–470 MiB/s
14
+ * through blocks. Bytes, though, travel better mapped — filled in place and copied once, where a
15
+ * block's are copied into it, sent with a queue write and copied again on the GPU — and 1 MiB
16
+ * uploads moved 3,350–3,560 MiB/s mapped against 1,480–1,640 through blocks. The two crossed between
17
+ * 24 and 32 KiB (RTX 4090, Chrome 154, 4 MiB per submission in uploads of one size, 2026-09-26).
18
+ *
19
+ * @type {number}
20
+ */
21
+ export const MAPPED_UPLOAD_THRESHOLD = 32 * KILOBYTE;
22
+
23
+ /**
24
+ * Size of every upload block. Everything that comes here is smaller than
25
+ * {@link MAPPED_UPLOAD_THRESHOLD}, so any empty block holds any write, and a frame's uniforms — a
26
+ * few thousand at the heavy end, tens of bytes each — fill one or two.
27
+ *
28
+ * @type {number}
29
+ */
30
+ export const UPLOAD_BLOCK_SIZE = 256 * KILOBYTE;
31
+
32
+ assert.greaterThan(UPLOAD_BLOCK_SIZE, MAPPED_UPLOAD_THRESHOLD, 'a block must hold the largest write routed to it');
33
+
34
+ /**
35
+ * @type {number}
36
+ */
37
+ let id_counter = 0;
38
+
39
+ /**
40
+ * Upload memory for {@link ShadeGPUCommandContext}'s small writes: pooled {@link GPUUploadBlock}s,
41
+ * all {@link UPLOAD_BLOCK_SIZE} bytes, each a `COPY_SRC | COPY_DST` buffer paired with CPU memory of
42
+ * the same size.
43
+ *
44
+ * A block is a transient in {@link TransientResourcePool}'s sense and is reused as soon as it is
45
+ * given back, for the reason `ShadeGPUCommandContext#send_uploads` sets out. It is a pool of its own,
46
+ * not {@link GraphicsContext#buffer_allocator_main}, because the frame graph aliases that pool's
47
+ * buffers within a submission, and a queue write — which lands ahead of the whole submission — into
48
+ * an aliased buffer would overwrite what an earlier pass in it still needs. Nothing aliases a block.
49
+ */
50
+ export class GPUBufferUploadAllocator extends TransientResourcePool {
51
+ /**
52
+ * Pooled bytes above which idle blocks are reclaimed. Counted once, but a block holds its size
53
+ * twice — on the GPU, and on the CPU for as long as it lives — so this is kept lower than the
54
+ * general pool's.
55
+ *
56
+ * @type {number}
57
+ */
58
+ param_soft_byte_budget = 16 * MEGABYTE;
59
+
60
+ /**
61
+ * @type {GPUDevice|undefined}
62
+ */
63
+ #device;
64
+
65
+ /**
66
+ * @param {GPUDevice} device
67
+ */
68
+ constructor(device) {
69
+ super();
70
+
71
+ assert.defined(device, 'device');
72
+
73
+ this.#device = device;
74
+ }
75
+
76
+ /**
77
+ * An empty block. Give it back with {@link release}.
78
+ *
79
+ * @returns {GPUUploadBlock}
80
+ */
81
+ get() {
82
+ return super.get(UPLOAD_BLOCK_SIZE);
83
+ }
84
+
85
+ /**
86
+ * When each pooled block was given back, counting releases. The pool keeps its blocks in this
87
+ * order and hands out the latest, so a frame that needs one block gets the same one every time
88
+ * and the rest go idle long enough to be reclaimed. With no order at all, the pool's sorted
89
+ * insert put each block given back in the middle and the one handed out came from the end: after
90
+ * a burst, a one-block frame cycled through three blocks for good, none ever idle.
91
+ *
92
+ * @type {WeakMap<GPUUploadBlock, number>}
93
+ */
94
+ #given_back = new WeakMap();
95
+
96
+ /**
97
+ * @type {number}
98
+ */
99
+ #release_count = 0;
100
+
101
+ /**
102
+ * @param {GPUUploadBlock} block
103
+ * @returns {boolean}
104
+ */
105
+ release(block) {
106
+ this.#given_back.set(block, ++this.#release_count);
107
+
108
+ return super.release(block);
109
+ }
110
+
111
+ find() {
112
+ const records = this.records;
113
+
114
+ // the latest given back that the device did not refuse: a refused block is never handed out
115
+ // again, and sits idle until update() reclaims it like any other
116
+ for (let i = records.length - 1; i >= 0; i--) {
117
+ if (!records[i].resource.refused) {
118
+ return i;
119
+ }
120
+ }
121
+
122
+ return -1;
123
+ }
124
+
125
+ compare(a, b) {
126
+ const given_back = this.#given_back;
127
+
128
+ return given_back.get(a.resource) - given_back.get(b.resource);
129
+ }
130
+
131
+ sizeOf(block) {
132
+ return block.size;
133
+ }
134
+
135
+ make() {
136
+ // the CPU half first: it is the one that can throw, and a buffer made before it would leak
137
+ const data = new ArrayBuffer(UPLOAD_BLOCK_SIZE);
138
+
139
+ const device = this.#device;
140
+
141
+ /*
142
+ WebGPU does not throw for a buffer it cannot back. It reports the failure through the error
143
+ scopes, later, and hands back a buffer that fails everything it is used in — here, the whole
144
+ submission of every context that takes the block. The scopes are how the block finds out,
145
+ so the pool can stop handing it out; they also keep the error from reaching the page's
146
+ uncapturederror handler, so it is reported here instead — a few times, not once a frame
147
+ for as long as the device stays out of memory.
148
+ */
149
+
150
+ /**
151
+ * Assigned before either scope can answer: they answer on a later turn.
152
+ * @type {GPUUploadBlock|null}
153
+ */
154
+ let block = null;
155
+
156
+ const on_scope = error => {
157
+ if (error !== null && block !== null && !block.refused) {
158
+ block.refused = true;
159
+
160
+ warn_limited(`GPUBufferUploadAllocator: an upload block could not be created and will not be used again: ${error.message}`, 8);
161
+ }
162
+ };
163
+
164
+ device.pushErrorScope('out-of-memory');
165
+ device.pushErrorScope('validation');
166
+
167
+ let buffer;
168
+
169
+ try {
170
+ buffer = device.createBuffer({
171
+ label: `upload ${id_counter++}`,
172
+ size: UPLOAD_BLOCK_SIZE,
173
+ usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
174
+ });
175
+ } finally {
176
+ // popped whatever happens, innermost first: a scope left open would take every error
177
+ // the device raised from then on, for anyone
178
+ device.popErrorScope().then(on_scope, console.error);
179
+ device.popErrorScope().then(on_scope, console.error);
180
+ }
181
+
182
+ block = new GPUUploadBlock(buffer, data);
183
+
184
+ return block;
185
+ }
186
+
187
+ destroyResource(block) {
188
+ block.buffer.destroy();
189
+ }
190
+
191
+ afterAcquire(block) {
192
+ block.used = 0;
193
+ }
194
+
195
+ destroy() {
196
+ super.destroy();
197
+
198
+ // as GPUBufferAllocator does: a block asked for after teardown would otherwise be made, and
199
+ // nothing would ever destroy it
200
+ this.#device = undefined;
201
+ }
202
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * One piece of upload memory: a GPU buffer, and the bytes it is to be given, held on the CPU until
3
+ * they are sent.
4
+ *
5
+ * A {@link ShadeGPUCommandContext} fills regions of {@link data} as it records, and copies each
6
+ * region to its destination with a `copyBufferToBuffer` out of {@link buffer} recorded where the
7
+ * write was made; the part in use reaches {@link buffer} in one queue write just before the context
8
+ * submits. Why the block can go back to its pool as soon as the context finishes is set out once,
9
+ * at `ShadeGPUCommandContext#send_uploads`.
10
+ *
11
+ * Both halves live as long as the block does — the CPU half is not given up once sent — so a block
12
+ * costs its size twice for as long as its pool keeps it.
13
+ */
14
+ export class GPUUploadBlock {
15
+ /**
16
+ * @param {GPUBuffer} buffer
17
+ * @param {ArrayBuffer} data
18
+ */
19
+ constructor(buffer: GPUBuffer, data: ArrayBuffer);
20
+ /**
21
+ * `COPY_SRC | COPY_DST`, never mapped.
22
+ *
23
+ * @type {GPUBuffer}
24
+ */
25
+ buffer: GPUBuffer;
26
+ /**
27
+ * What {@link buffer} is to hold, the same size as it. Past {@link used} it holds whatever the
28
+ * last use left.
29
+ *
30
+ * @type {ArrayBuffer}
31
+ */
32
+ data: ArrayBuffer;
33
+ /**
34
+ * Bytes handed out, from the start. A multiple of 4, so every region starts on one: WebGPU's
35
+ * buffer copies work in whole 4-byte words, on both offsets and the size.
36
+ *
37
+ * @type {number}
38
+ */
39
+ used: number;
40
+ /**
41
+ * Whether the device refused to back {@link buffer} — out of memory, in practice.
42
+ *
43
+ * WebGPU says so through its error scopes, after the fact, and hands back a buffer object either
44
+ * way. By the time this is set the context that took the block has recorded copies out of an
45
+ * invalid buffer and its submission is lost with them; what this prevents is the same loss on
46
+ * every frame after, which is what a pool that kept handing the block out would cause.
47
+ *
48
+ * @type {boolean}
49
+ */
50
+ refused: boolean;
51
+ /**
52
+ * @returns {number} bytes
53
+ */
54
+ get size(): number;
55
+ /**
56
+ * @param {number} size bytes
57
+ * @returns {boolean} whether a region of `size` bytes is still free
58
+ */
59
+ fits(size: number): boolean;
60
+ /**
61
+ * Take the next `size` bytes.
62
+ *
63
+ * @param {number} size bytes; the region taken is rounded up to a whole number of 4-byte words
64
+ * @returns {number} byte offset of the region in {@link data} and {@link buffer}
65
+ */
66
+ allocate(size: number): number;
67
+ }
68
+ //# sourceMappingURL=GPUUploadBlock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GPUUploadBlock.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/buffer/GPUUploadBlock.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AACH;IAoCI;;;OAGG;IACH,qCAFW,WAAW,EASrB;IA9CD;;;;OAIG;IACH,kBAAO;IAEP;;;;;OAKG;IACH,MAFU,WAAW,CAEhB;IAEL;;;;;OAKG;IACH,MAFU,MAAM,CAEP;IAET;;;;;;;;;OASG;IACH,SAFU,OAAO,CAED;IAehB;;OAEG;IACH,mBAEC;IAED;;;OAGG;IACH,WAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;;;OAKG;IACH,eAHW,MAAM,GACJ,MAAM,CAWlB;CACJ"}
@@ -0,0 +1,97 @@
1
+ import { assert } from "../../../core/assert.js";
2
+ import { align_4 } from "../../../core/binary/align_4.js";
3
+
4
+ /**
5
+ * One piece of upload memory: a GPU buffer, and the bytes it is to be given, held on the CPU until
6
+ * they are sent.
7
+ *
8
+ * A {@link ShadeGPUCommandContext} fills regions of {@link data} as it records, and copies each
9
+ * region to its destination with a `copyBufferToBuffer` out of {@link buffer} recorded where the
10
+ * write was made; the part in use reaches {@link buffer} in one queue write just before the context
11
+ * submits. Why the block can go back to its pool as soon as the context finishes is set out once,
12
+ * at `ShadeGPUCommandContext#send_uploads`.
13
+ *
14
+ * Both halves live as long as the block does — the CPU half is not given up once sent — so a block
15
+ * costs its size twice for as long as its pool keeps it.
16
+ */
17
+ export class GPUUploadBlock {
18
+ /**
19
+ * `COPY_SRC | COPY_DST`, never mapped.
20
+ *
21
+ * @type {GPUBuffer}
22
+ */
23
+ buffer;
24
+
25
+ /**
26
+ * What {@link buffer} is to hold, the same size as it. Past {@link used} it holds whatever the
27
+ * last use left.
28
+ *
29
+ * @type {ArrayBuffer}
30
+ */
31
+ data;
32
+
33
+ /**
34
+ * Bytes handed out, from the start. A multiple of 4, so every region starts on one: WebGPU's
35
+ * buffer copies work in whole 4-byte words, on both offsets and the size.
36
+ *
37
+ * @type {number}
38
+ */
39
+ used = 0;
40
+
41
+ /**
42
+ * Whether the device refused to back {@link buffer} — out of memory, in practice.
43
+ *
44
+ * WebGPU says so through its error scopes, after the fact, and hands back a buffer object either
45
+ * way. By the time this is set the context that took the block has recorded copies out of an
46
+ * invalid buffer and its submission is lost with them; what this prevents is the same loss on
47
+ * every frame after, which is what a pool that kept handing the block out would cause.
48
+ *
49
+ * @type {boolean}
50
+ */
51
+ refused = false;
52
+
53
+ /**
54
+ * @param {GPUBuffer} buffer
55
+ * @param {ArrayBuffer} data
56
+ */
57
+ constructor(buffer, data) {
58
+ assert.defined(buffer, 'buffer');
59
+ assert.defined(data, 'data');
60
+ assert.equal(data.byteLength, buffer.size, 'data.byteLength !== buffer.size');
61
+
62
+ this.buffer = buffer;
63
+ this.data = data;
64
+ }
65
+
66
+ /**
67
+ * @returns {number} bytes
68
+ */
69
+ get size() {
70
+ return this.data.byteLength;
71
+ }
72
+
73
+ /**
74
+ * @param {number} size bytes
75
+ * @returns {boolean} whether a region of `size` bytes is still free
76
+ */
77
+ fits(size) {
78
+ return this.used + size <= this.data.byteLength;
79
+ }
80
+
81
+ /**
82
+ * Take the next `size` bytes.
83
+ *
84
+ * @param {number} size bytes; the region taken is rounded up to a whole number of 4-byte words
85
+ * @returns {number} byte offset of the region in {@link data} and {@link buffer}
86
+ */
87
+ allocate(size) {
88
+ assert.isNonNegativeInteger(size, 'size');
89
+ assert.ok(this.fits(size), 'does not fit');
90
+
91
+ const offset = this.used;
92
+
93
+ this.used = offset + align_4(size);
94
+
95
+ return offset;
96
+ }
97
+ }
@@ -24,17 +24,27 @@ export class GPUDynamicMeshRenderer {
24
24
  * Set before the first draw. Changing it later rebuilds the pipeline, which is cheap but not
25
25
  * free, and the whole batch moves together — this is a property of the pass, not of a mesh.
26
26
  *
27
+ * Solid content belongs at `FramePhase.AfterLighting`, with the depth {@link graph_draw} hands
28
+ * back published: transparency runs after that phase and tests against that depth, so glass in
29
+ * front of the batch covers it. At `AfterTransparency` the glass is already resolved, and the
30
+ * batch is drawn over it.
31
+ *
27
32
  * @type {boolean}
28
33
  */
29
34
  opaque: boolean;
30
35
  /**
31
36
  * Record the draws into `graph`, over the colour it is given.
32
37
  *
33
- * When {@link opaque} is set the pass writes the depth it is given. That write lands in the
34
- * same physical texture the handle names — a written handle is a renamed clone of one resource,
35
- * not a second one — so a later pass reading the handle the caller still holds sees the depth
36
- * this pass left. Only the colour is threaded back, because that is what an injected pass is
37
- * allowed to replace.
38
+ * **Both handles come back, and the caller publishes both** — into `SceneColor` and
39
+ * `ViewTextures` from an extension. The colour is always replaced. The depth is replaced when
40
+ * {@link opaque} is set, since the pass writes it; otherwise it comes back as it was given.
41
+ *
42
+ * A written handle is a renamed clone of the same texture, so a pass that goes on reading the
43
+ * depth handle it held before this draw is reading a version this pass has superseded.
44
+ * `FrameGraph.validate` refuses that, and the frame fails to compile in a development build.
45
+ * A stripped build reads the right contents only because passes run in the order they were
46
+ * recorded. So every reader of the scene depth after this draw has to be handed the new handle:
47
+ * depth of field, motion blur, the NSS upscaler, the next extension along.
38
48
  *
39
49
  * @param {object} args
40
50
  * @param {FrameGraph} args.graph
@@ -42,7 +52,7 @@ export class GPUDynamicMeshRenderer {
42
52
  * @param {number} args.color colour resource handle to draw over
43
53
  * @param {number} args.depth scene depth, written when {@link opaque} and otherwise only tested
44
54
  * @param {GPUBuffer} args.camera
45
- * @returns {number} the colour handle after the draws
55
+ * @returns {{color: number, depth: number}} the colour and depth handles after the draws
46
56
  */
47
57
  graph_draw({ graph, batch, color, depth, camera }: {
48
58
  graph: FrameGraph;
@@ -50,7 +60,10 @@ export class GPUDynamicMeshRenderer {
50
60
  color: number;
51
61
  depth: number;
52
62
  camera: GPUBuffer;
53
- }): number;
63
+ }): {
64
+ color: number;
65
+ depth: number;
66
+ };
54
67
  /**
55
68
  * Release the GPU buffers held for `geometry`. A dynamic mesh that is gone for good should say
56
69
  * so — residency deliberately survives removal from the batch, so nothing else will free it.
@@ -1 +1 @@
1
- {"version":3,"file":"GPUDynamicMeshRenderer.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/dynamic/GPUDynamicMeshRenderer.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;GAWG;AACH;IACI;;;;;;;;;;;;;;OAcG;IACH,QAFU,OAAO,CAEF;IAiBf;;;;;;;;;;;;;;;;OAgBG;IACH;QAP4B,KAAK;QACC,KAAK;QACf,KAAK,EAAlB,MAAM;QACO,KAAK,EAAlB,MAAM;QACU,MAAM;QACpB,MAAM,CAuBlB;IAqHD;;;;;;OAMG;IACH,6BAFa,OAAO,CAInB;IAED,gBAEC;;CACJ"}
1
+ {"version":3,"file":"GPUDynamicMeshRenderer.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/dynamic/GPUDynamicMeshRenderer.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;GAWG;AACH;IACI;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAFU,OAAO,CAEF;IAiBf;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH;QAP4B,KAAK;QACC,KAAK;QACf,KAAK,EAAlB,MAAM;QACO,KAAK,EAAlB,MAAM;QACU,MAAM;QACpB;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CA0B1C;IAqHD;;;;;;OAMG;IACH,6BAFa,OAAO,CAInB;IAED,gBAEC;;CACJ"}