@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.
- package/build/bundle-worker-terrain.js +1 -1
- package/package.json +1 -1
- package/src/engine/graphics3/DebugDrawSystem.d.ts.map +1 -1
- package/src/engine/graphics3/DebugDrawSystem.js +6 -2
- package/src/engine/graphics3/PathDisplaySystem.d.ts.map +1 -1
- package/src/engine/graphics3/PathDisplaySystem.js +311 -307
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/shade/RENDERER_CONTRACT.md +15 -2
- package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +5 -0
- package/src/shade/device/ShadeGPUCommandContext.d.ts +15 -2
- package/src/shade/device/ShadeGPUCommandContext.d.ts.map +1 -1
- package/src/shade/device/ShadeGPUCommandContext.js +154 -25
- package/src/shade/renderer/GraphicsContext.d.ts +9 -1
- package/src/shade/renderer/GraphicsContext.d.ts.map +1 -1
- package/src/shade/renderer/GraphicsContext.js +54 -15
- package/src/shade/renderer/Renderer.d.ts +19 -4
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +113 -2
- package/src/shade/renderer/animation/GPUAnimationManager.d.ts +10 -7
- package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
- package/src/shade/renderer/animation/GPUAnimationManager.js +147 -45
- package/src/shade/renderer/buffer/GPUBufferStagingAllocator.d.ts +11 -2
- package/src/shade/renderer/buffer/GPUBufferStagingAllocator.d.ts.map +1 -1
- package/src/shade/renderer/buffer/GPUBufferStagingAllocator.js +95 -13
- package/src/shade/renderer/buffer/GPUBufferUploadAllocator.d.ts +60 -0
- package/src/shade/renderer/buffer/GPUBufferUploadAllocator.d.ts.map +1 -0
- package/src/shade/renderer/buffer/GPUBufferUploadAllocator.js +202 -0
- package/src/shade/renderer/buffer/GPUUploadBlock.d.ts +68 -0
- package/src/shade/renderer/buffer/GPUUploadBlock.d.ts.map +1 -0
- package/src/shade/renderer/buffer/GPUUploadBlock.js +97 -0
- package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.d.ts +20 -7
- package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.d.ts.map +1 -1
- package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.js +20 -7
- package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.d.ts +5 -0
- package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.d.ts.map +1 -1
- package/src/shade/renderer/geometry/meshlet/build/meshoptimizer/geometry_to_meshlets_meshoptimizer.js +19 -0
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
- package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts +3 -1
- package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.js +3 -1
- package/src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_rasterization_bucket_sort.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/bucket/mesh/shader_instances_rasterization_bucket_sort.js +134 -88
- package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.js +57 -26
- package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +28 -20
- package/src/shade/renderer/rasterize/graph_clear_raster_targets.d.ts +38 -0
- package/src/shade/renderer/rasterize/graph_clear_raster_targets.d.ts.map +1 -0
- package/src/shade/renderer/rasterize/graph_clear_raster_targets.js +105 -0
- package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts +1 -1
- package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +21 -1
- package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.js +12 -20
- package/src/shade/renderer/shader/graph/graph_import_typed_buffer.d.ts +2 -1
- package/src/shade/renderer/shader/graph/graph_import_typed_buffer.d.ts.map +1 -1
- package/src/shade/renderer/shader/graph/graph_import_typed_buffer.js +15 -28
- package/src/core/geom/3d/atlas/atlas_bench_lib.d.ts +0 -73
- package/src/core/geom/3d/atlas/atlas_bench_lib.d.ts.map +0 -1
- package/src/engine/graphics3/terrain/GPUTerrainSplatRenderer.d.ts +0 -59
- package/src/engine/graphics3/terrain/GPUTerrainSplatRenderer.d.ts.map +0 -1
- package/src/engine/graphics3/terrain/pack_terrain_row_table.d.ts +0 -22
- package/src/engine/graphics3/terrain/pack_terrain_row_table.d.ts.map +0 -1
- package/src/engine/graphics3/terrain/shader_terrain_splat.d.ts +0 -26
- package/src/engine/graphics3/terrain/shader_terrain_splat.d.ts.map +0 -1
- package/src/shade/renderer/animation/ANIMATION_BOUND_TRACK_GROUP_STRUCT.d.ts +0 -25
- package/src/shade/renderer/animation/ANIMATION_BOUND_TRACK_GROUP_STRUCT.d.ts.map +0 -1
- package/src/shade/renderer/animation/ANIMATION_KEYFRAME_BLOCK_STRUCT.d.ts +0 -26
- package/src/shade/renderer/animation/ANIMATION_KEYFRAME_BLOCK_STRUCT.d.ts.map +0 -1
- package/src/shade/renderer/animation/ANIMATION_SKIN_JOINT_BLOCK_STRUCT.d.ts +0 -29
- 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
|
-
*
|
|
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
|
-
|
|
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
|
|
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.#
|
|
177
|
+
this.#take_back(buffer);
|
|
131
178
|
} else if (state === "unmapped") {
|
|
132
179
|
buffer.mapAsync(GPUMapMode.WRITE)
|
|
133
180
|
.then(
|
|
134
|
-
() => this.#
|
|
135
|
-
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
|
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
|
-
}):
|
|
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
|
|
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"}
|