@woosh/meep-engine 3.31.0 → 3.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/bundle-worker-terrain.js +1 -1
- package/package.json +1 -1
- package/src/core/collection/table/RowFirstTable.d.ts +8 -0
- package/src/core/collection/table/RowFirstTable.d.ts.map +1 -1
- package/src/core/collection/table/RowFirstTable.js +40 -11
- package/src/engine/graphics/ecs/sprite/Sprite.d.ts +3 -0
- package/src/engine/graphics/ecs/sprite/Sprite.d.ts.map +1 -1
- package/src/engine/graphics/ecs/sprite/Sprite.js +3 -0
- package/src/engine/graphics/ecs/sprite/SpriteSystemPE.d.ts +3 -0
- package/src/engine/graphics/ecs/sprite/SpriteSystemPE.d.ts.map +1 -1
- package/src/engine/graphics/ecs/sprite/SpriteSystemPE.js +3 -0
- 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/device/mock/SoftwareGPURenderPassEncoder.d.ts +20 -3
- package/src/shade/device/mock/SoftwareGPURenderPassEncoder.d.ts.map +1 -1
- package/src/shade/device/mock/SoftwareGPURenderPassEncoder.js +21 -3
- package/src/shade/renderer/GraphicsContext.d.ts +20 -1
- package/src/shade/renderer/GraphicsContext.d.ts.map +1 -1
- package/src/shade/renderer/GraphicsContext.js +105 -52
- 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/GPUBufferReadbackAllocator.d.ts +6 -5
- package/src/shade/renderer/buffer/GPUBufferReadbackAllocator.d.ts.map +1 -1
- package/src/shade/renderer/buffer/GPUBufferReadbackAllocator.js +241 -240
- package/src/shade/renderer/buffer/GPUBufferStagingAllocator.d.ts +62 -2
- package/src/shade/renderer/buffer/GPUBufferStagingAllocator.d.ts.map +1 -1
- package/src/shade/renderer/buffer/GPUBufferStagingAllocator.js +306 -38
- 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/DynamicMeshAVBOITChannel.d.ts +2 -1
- package/src/shade/renderer/dynamic/DynamicMeshAVBOITChannel.d.ts.map +1 -1
- package/src/shade/renderer/dynamic/DynamicMeshAVBOITChannel.js +19 -17
- package/src/shade/renderer/dynamic/GPUDynamicMeshDrawList.d.ts +70 -0
- package/src/shade/renderer/dynamic/GPUDynamicMeshDrawList.d.ts.map +1 -0
- package/src/shade/renderer/dynamic/GPUDynamicMeshDrawList.js +257 -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 +39 -41
- package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.d.ts +6 -20
- package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.d.ts.map +1 -1
- package/src/shade/renderer/dynamic/GPUDynamicMeshResidency.js +42 -36
- package/src/shade/renderer/dynamic/chunk_dynamic_mesh_avboit_vertex.d.ts +2 -1
- package/src/shade/renderer/dynamic/chunk_dynamic_mesh_avboit_vertex.d.ts.map +1 -1
- package/src/shade/renderer/dynamic/chunk_dynamic_mesh_avboit_vertex.js +4 -3
- package/src/shade/renderer/dynamic/chunk_dynamic_mesh_vertex.d.ts +4 -3
- package/src/shade/renderer/dynamic/chunk_dynamic_mesh_vertex.d.ts.map +1 -1
- package/src/shade/renderer/dynamic/chunk_dynamic_mesh_vertex.js +7 -4
- package/src/shade/renderer/dynamic/graph_dynamic_mesh_avboit.d.ts +14 -15
- package/src/shade/renderer/dynamic/graph_dynamic_mesh_avboit.d.ts.map +1 -1
- package/src/shade/renderer/dynamic/graph_dynamic_mesh_avboit.js +49 -83
- package/src/shade/renderer/dynamic/shader_dynamic_mesh.d.ts +19 -3
- package/src/shade/renderer/dynamic/shader_dynamic_mesh.d.ts.map +1 -1
- package/src/shade/renderer/dynamic/shader_dynamic_mesh.js +26 -6
- package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_draw.d.ts +2 -1
- package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_draw.d.ts.map +1 -1
- package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_draw.js +12 -6
- package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_splat.d.ts +3 -2
- package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_splat.d.ts.map +1 -1
- package/src/shade/renderer/dynamic/shader_dynamic_mesh_avboit_splat.js +21 -13
- 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/native/avboit/AVBOIT_LEDGER.md +20 -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/shade/renderer/shader/type/serde/write_gpu_typed_buffer.d.ts.map +1 -1
- package/src/shade/renderer/shader/type/serde/write_gpu_typed_buffer.js +8 -1
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assert } from "../../../core/assert.js";
|
|
2
|
-
import {
|
|
2
|
+
import { round_up } from "../../../core/math/round_up.js";
|
|
3
|
+
import { MEGABYTE } from "../../../core/science/units/memory/MEGABYTE.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
*
|
|
@@ -8,11 +9,140 @@ import { binarySearchHighIndex } from "../../../core/collection/array/binarySear
|
|
|
8
9
|
let id_count = 0;
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
+
* A pooled buffer is handed out only for a request it is at most this many times the size of.
|
|
12
13
|
*
|
|
14
|
+
* A buffer is mapped whole each time it comes back, and a mapping is paid for by the size of the
|
|
15
|
+
* buffer, not by what was written into it. With no bound, eight idle 16 MiB buffers left by a burst
|
|
16
|
+
* took the 80 KiB and 40 KiB draw-constant writes of a 1,500-DynamicMesh frame from then on: 32 MiB
|
|
17
|
+
* mapped a render for 120 KiB written, and 178–202 renders/s against 244–256 with no burst. The same
|
|
18
|
+
* 128 MiB mapped and idle outside the pool ran 246–259, so the cost was the handing out, not the
|
|
19
|
+
* memory (RTX 4090, Chrome 154, paired in one page, 2026-09-27).
|
|
20
|
+
*
|
|
21
|
+
* @type {number}
|
|
22
|
+
*/
|
|
23
|
+
const REUSE_LIMIT_SCALE = 2;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sizes a new staging buffer can be made at within each doubling, evenly spaced: with 4, a buffer
|
|
27
|
+
* made for a request of more than 16 bytes is never more than a quarter larger than it.
|
|
28
|
+
*
|
|
29
|
+
* @type {number}
|
|
30
|
+
*/
|
|
31
|
+
const SIZE_CLASSES_PER_DOUBLING = 4;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The size a new staging buffer for `bytes` is made at: the next of the
|
|
35
|
+
* {@link SIZE_CLASSES_PER_DOUBLING} sizes in the doubling `bytes` falls in.
|
|
36
|
+
*
|
|
37
|
+
* Made at the exact size, a write that grew a little every frame asked for a size no pooled buffer
|
|
38
|
+
* had, every frame, and got a new buffer every frame. Now one buffer serves it until it grows past a
|
|
39
|
+
* class.
|
|
40
|
+
*
|
|
41
|
+
* @param {number} bytes
|
|
42
|
+
* @returns {number} a multiple of 4, as a buffer mapped at creation must be
|
|
43
|
+
*/
|
|
44
|
+
function size_class(bytes) {
|
|
45
|
+
if (bytes <= 16) {
|
|
46
|
+
return round_up(bytes, 4);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// the largest power of two not above `bytes`
|
|
50
|
+
let doubling = 16;
|
|
51
|
+
|
|
52
|
+
while (doubling * 2 <= bytes) {
|
|
53
|
+
doubling *= 2;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return round_up(bytes, doubling / SIZE_CLASSES_PER_DOUBLING);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Where the first buffer of at least `size` bytes is, or would go, in `buffers` sorted by size.
|
|
61
|
+
*
|
|
62
|
+
* Both ends of the pool use it: a request takes the buffer there, the smallest that holds it, and a
|
|
63
|
+
* buffer given back goes in there, ahead of its equals — so of buffers of one size, the one given
|
|
64
|
+
* back last goes out first. A search that landed anywhere among equals moved a steady writer to
|
|
65
|
+
* another buffer whenever their count changed, and every buffer it had touched was kept for a
|
|
66
|
+
* backstop more: a burst of 40 left 3 alive past the backstop where the writer needed 1.
|
|
67
|
+
*
|
|
68
|
+
* @param {GPUBuffer[]} buffers
|
|
69
|
+
* @param {number} size
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
72
|
+
function first_at_least(buffers, size) {
|
|
73
|
+
let low = 0;
|
|
74
|
+
let high = buffers.length;
|
|
75
|
+
|
|
76
|
+
while (low < high) {
|
|
77
|
+
const middle = (low + high) >>> 1;
|
|
78
|
+
|
|
79
|
+
if (buffers[middle].size < size) {
|
|
80
|
+
low = middle + 1;
|
|
81
|
+
} else {
|
|
82
|
+
high = middle;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return low;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Mapped `MAP_WRITE | COPY_SRC` buffers for pushing data from the CPU to the GPU, one per upload.
|
|
91
|
+
*
|
|
92
|
+
* A buffer comes out mapped; its holder fills it, unmaps it, copies out of it and gives it back, and
|
|
93
|
+
* the pool maps it again — a round trip through the GPU — before it can come out again. For bytes
|
|
94
|
+
* that is the cheaper road; for thousands of small writes a frame the round trips are the cost, which
|
|
95
|
+
* is why a {@link ShadeGPUCommandContext} brings only its large writes here (see
|
|
96
|
+
* `MAPPED_UPLOAD_THRESHOLD`).
|
|
97
|
+
*
|
|
98
|
+
* A buffer is made up to a quarter larger than asked, so a size that drifts keeps its buffer, and is
|
|
99
|
+
* handed out again only to requests at least half its size ({@link REUSE_LIMIT_SCALE}), so a burst's
|
|
100
|
+
* large buffers do not end up mapped whole for every smaller write after it. Of buffers of one size
|
|
101
|
+
* the one given back last goes out first, so a steady writer keeps one and the rest go idle. What
|
|
102
|
+
* sits idle is destroyed by {@link update}, as {@link TransientResourcePool#update} does for the
|
|
103
|
+
* frame's transients: without that, a burst's buffers stayed until the device went.
|
|
13
104
|
*/
|
|
14
105
|
export class GPUBufferStagingAllocator {
|
|
15
106
|
|
|
107
|
+
// ----- reclaim tuning, as TransientResourcePool's -----
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Idle bytes above which buffers idle for {@link param_over_budget_idle_frames} are destroyed.
|
|
111
|
+
* The same as the upload blocks' budget.
|
|
112
|
+
*
|
|
113
|
+
* @type {number}
|
|
114
|
+
*/
|
|
115
|
+
param_soft_byte_budget = 16 * MEGABYTE;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Frames a buffer may sit idle while the pool is over its budget: long enough that a write made
|
|
119
|
+
* every few frames keeps its buffer, short enough that a burst's go within a second or so.
|
|
120
|
+
*
|
|
121
|
+
* @type {number}
|
|
122
|
+
*/
|
|
123
|
+
param_over_budget_idle_frames = 60;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* An idle buffer is destroyed after this many frames even with the pool under its budget.
|
|
127
|
+
*
|
|
128
|
+
* @type {number}
|
|
129
|
+
*/
|
|
130
|
+
param_backstop_frames = 600;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Most buffers destroyed by one {@link update}.
|
|
134
|
+
*
|
|
135
|
+
* @type {number}
|
|
136
|
+
*/
|
|
137
|
+
param_max_reclaim_per_frame = 8;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Most idle buffers one {@link update} looks at.
|
|
141
|
+
*
|
|
142
|
+
* @type {number}
|
|
143
|
+
*/
|
|
144
|
+
param_scan_budget = 16;
|
|
145
|
+
|
|
16
146
|
/**
|
|
17
147
|
* @type {GPUDevice}
|
|
18
148
|
*/
|
|
@@ -24,6 +154,60 @@ export class GPUBufferStagingAllocator {
|
|
|
24
154
|
*/
|
|
25
155
|
#ready = [];
|
|
26
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Bytes in {@link #ready}.
|
|
159
|
+
*
|
|
160
|
+
* @type {number}
|
|
161
|
+
*/
|
|
162
|
+
#ready_byte_size = 0;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Bytes in buffers handed out and not back in {@link #ready} yet: with their holder, or on their
|
|
166
|
+
* way back through a mapping. They are memory like any other, and a GPU running behind holds a
|
|
167
|
+
* frame's worth of them for every frame it is behind.
|
|
168
|
+
*
|
|
169
|
+
* @type {number}
|
|
170
|
+
*/
|
|
171
|
+
#held_byte_size = 0;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Buffers handed out and not given back yet. Only these are taken back: a buffer given back
|
|
175
|
+
* twice, or one this pool never made, would otherwise go into {@link #ready} twice and come out
|
|
176
|
+
* to two holders at once.
|
|
177
|
+
*
|
|
178
|
+
* @type {Set<GPUBuffer>}
|
|
179
|
+
*/
|
|
180
|
+
#held = new Set();
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Set by {@link destroy}. A buffer whose mapping comes back after that is destroyed, not pooled —
|
|
184
|
+
* the pool it would go into is gone.
|
|
185
|
+
*
|
|
186
|
+
* @type {boolean}
|
|
187
|
+
*/
|
|
188
|
+
#destroyed = false;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Frames counted by {@link increment_time}.
|
|
192
|
+
*
|
|
193
|
+
* @type {number}
|
|
194
|
+
*/
|
|
195
|
+
#frame = 0;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The frame each buffer in {@link #ready} came back in: how long it has been idle.
|
|
199
|
+
*
|
|
200
|
+
* @type {WeakMap<GPUBuffer, number>}
|
|
201
|
+
*/
|
|
202
|
+
#returned_at = new WeakMap();
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Where in {@link #ready} the next {@link update} starts looking.
|
|
206
|
+
*
|
|
207
|
+
* @type {number}
|
|
208
|
+
*/
|
|
209
|
+
#cursor = 0;
|
|
210
|
+
|
|
27
211
|
/**
|
|
28
212
|
*
|
|
29
213
|
* @param {GPUDevice} device
|
|
@@ -35,17 +219,13 @@ export class GPUBufferStagingAllocator {
|
|
|
35
219
|
}
|
|
36
220
|
|
|
37
221
|
/**
|
|
222
|
+
* Bytes in every buffer this pool has made and not destroyed: free, handed out, or on the way
|
|
223
|
+
* back.
|
|
38
224
|
*
|
|
39
225
|
* @returns {number}
|
|
40
226
|
*/
|
|
41
227
|
get gpu_memory_usage() {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for (const b of this.#ready) {
|
|
45
|
-
r += b.size;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return r;
|
|
228
|
+
return this.#ready_byte_size + this.#held_byte_size;
|
|
49
229
|
}
|
|
50
230
|
|
|
51
231
|
|
|
@@ -57,48 +237,45 @@ export class GPUBufferStagingAllocator {
|
|
|
57
237
|
get(min_size_bytes) {
|
|
58
238
|
assert.isNonNegativeInteger(min_size_bytes, 'min_size_bytes');
|
|
59
239
|
|
|
60
|
-
const index = this.#get_position(min_size_bytes);
|
|
61
|
-
|
|
62
240
|
const buffers = this.#ready;
|
|
63
241
|
|
|
242
|
+
// the smallest ready buffer that holds the request, unless even that one is too large for it
|
|
243
|
+
const index = first_at_least(buffers, min_size_bytes);
|
|
244
|
+
|
|
64
245
|
let result;
|
|
65
|
-
|
|
246
|
+
|
|
247
|
+
if (index < buffers.length && buffers[index].size <= min_size_bytes * REUSE_LIMIT_SCALE) {
|
|
66
248
|
result = buffers.splice(index, 1)[0];
|
|
67
|
-
}
|
|
68
249
|
|
|
69
|
-
|
|
250
|
+
this.#ready_byte_size -= result.size;
|
|
251
|
+
} else {
|
|
252
|
+
const device = this.#device;
|
|
253
|
+
|
|
254
|
+
// Never rounded past the device's limit for a request that is within it, which would
|
|
255
|
+
// refuse a write the exact size takes; and a whole number of words below the limit, which
|
|
256
|
+
// the spec does not require to be one, as a buffer mapped at creation must be.
|
|
257
|
+
const limit = device.limits.maxBufferSize;
|
|
258
|
+
const largest = limit - limit % 4;
|
|
70
259
|
|
|
71
|
-
// TODO align buffer size?
|
|
72
260
|
const id = id_count++;
|
|
73
261
|
|
|
74
|
-
result =
|
|
262
|
+
result = device.createBuffer({
|
|
75
263
|
label: `staging ${id}`,
|
|
76
|
-
size: min_size_bytes,
|
|
264
|
+
size: Math.max(min_size_bytes, Math.min(size_class(min_size_bytes), largest)),
|
|
77
265
|
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE,
|
|
78
266
|
mappedAtCreation: true,
|
|
79
267
|
});
|
|
80
|
-
|
|
81
268
|
}
|
|
82
269
|
|
|
270
|
+
this.#held_byte_size += result.size;
|
|
271
|
+
this.#held.add(result);
|
|
272
|
+
|
|
83
273
|
assert.greaterThanOrEqual(result.size, min_size_bytes, 'result.size < min_size_bytes');
|
|
84
274
|
assert.equal(result.mapState, "mapped", 'result.mapState !== "mapped"');
|
|
85
275
|
|
|
86
276
|
return result;
|
|
87
277
|
}
|
|
88
278
|
|
|
89
|
-
/**
|
|
90
|
-
*
|
|
91
|
-
* @param {number} size
|
|
92
|
-
* @return {number}
|
|
93
|
-
*/
|
|
94
|
-
#get_position(size) {
|
|
95
|
-
function by_size(a, b) {
|
|
96
|
-
return a.size - b.size;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return binarySearchHighIndex(this.#ready, { size }, by_size);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
279
|
/**
|
|
103
280
|
*
|
|
104
281
|
* @param {GPUBuffer} buffer
|
|
@@ -109,42 +286,133 @@ export class GPUBufferStagingAllocator {
|
|
|
109
286
|
assert.equal(buffer.mapState, "mapped", 'buffer.mapState !== "mapped"');
|
|
110
287
|
assert.equal(buffer.usage, GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE, 'buffer.usage !== GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE');
|
|
111
288
|
|
|
112
|
-
|
|
289
|
+
// ahead of its equals: the next request for this size takes it
|
|
290
|
+
const index = first_at_least(this.#ready, buffer.size);
|
|
113
291
|
|
|
114
292
|
this.#ready.splice(index, 0, buffer);
|
|
115
293
|
|
|
294
|
+
this.#ready_byte_size += buffer.size;
|
|
295
|
+
|
|
116
296
|
return true;
|
|
117
297
|
}
|
|
118
298
|
|
|
119
299
|
/**
|
|
120
300
|
*
|
|
121
301
|
* @param {GPUBuffer} buffer
|
|
302
|
+
* @returns {boolean} whether it was taken back: false for a buffer given back already, or one
|
|
303
|
+
* this pool did not hand out
|
|
122
304
|
*/
|
|
123
305
|
release(buffer) {
|
|
124
|
-
|
|
306
|
+
if (!this.#held.delete(buffer)) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
125
309
|
|
|
126
|
-
|
|
310
|
+
const state = buffer.mapState;
|
|
127
311
|
|
|
128
312
|
if (state === "mapped") {
|
|
129
313
|
// release directly
|
|
130
|
-
this.#
|
|
314
|
+
this.#take_back(buffer);
|
|
131
315
|
} else if (state === "unmapped") {
|
|
132
316
|
buffer.mapAsync(GPUMapMode.WRITE)
|
|
133
317
|
.then(
|
|
134
|
-
() => this.#
|
|
135
|
-
|
|
318
|
+
() => this.#take_back(buffer),
|
|
319
|
+
() => this.#give_up(buffer)
|
|
136
320
|
);
|
|
137
321
|
} else {
|
|
138
322
|
throw new Error(`Invalid map state: ${state}`);
|
|
139
323
|
}
|
|
324
|
+
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* A buffer back and mapped, to be handed out again.
|
|
330
|
+
*
|
|
331
|
+
* @param {GPUBuffer} buffer
|
|
332
|
+
*/
|
|
333
|
+
#take_back(buffer) {
|
|
334
|
+
this.#held_byte_size -= buffer.size;
|
|
335
|
+
|
|
336
|
+
if (this.#destroyed) {
|
|
337
|
+
buffer.destroy();
|
|
338
|
+
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
this.#add_to_ready(buffer);
|
|
343
|
+
|
|
344
|
+
this.#returned_at.set(buffer, this.#frame);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* A mapping that failed — the buffer or its device is gone — brings nothing back to reuse. It
|
|
349
|
+
* used to be logged and dropped: never destroyed, so whatever was left of it waited for the
|
|
350
|
+
* garbage collector.
|
|
351
|
+
*
|
|
352
|
+
* @param {GPUBuffer} buffer
|
|
353
|
+
*/
|
|
354
|
+
#give_up(buffer) {
|
|
355
|
+
this.#held_byte_size -= buffer.size;
|
|
356
|
+
|
|
357
|
+
buffer.destroy();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Advance the pool's frame clock; once a frame, with the other pools'.
|
|
362
|
+
*/
|
|
363
|
+
increment_time() {
|
|
364
|
+
this.#frame++;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Once-a-frame housekeeping: destroy the buffers nothing has asked for in a while — idle past
|
|
369
|
+
* {@link param_backstop_frames}, or, while more than {@link param_soft_byte_budget} bytes sit
|
|
370
|
+
* idle, past {@link param_over_budget_idle_frames}. A bounded sweep from where the last one
|
|
371
|
+
* stopped, at most {@link param_scan_budget} buffers looked at and
|
|
372
|
+
* {@link param_max_reclaim_per_frame} destroyed, as {@link TransientResourcePool#update}.
|
|
373
|
+
*/
|
|
374
|
+
update() {
|
|
375
|
+
const ready = this.#ready;
|
|
376
|
+
|
|
377
|
+
let count = ready.length;
|
|
378
|
+
|
|
379
|
+
let over_budget = this.#ready_byte_size - this.param_soft_byte_budget;
|
|
380
|
+
let reclaim_budget = this.param_max_reclaim_per_frame;
|
|
381
|
+
|
|
382
|
+
const sweep = Math.min(count, this.param_scan_budget);
|
|
383
|
+
|
|
384
|
+
for (let i = 0; i < sweep && count > 0 && reclaim_budget > 0; i++) {
|
|
385
|
+
const cursor = this.#cursor % count;
|
|
386
|
+
const buffer = ready[cursor];
|
|
387
|
+
|
|
388
|
+
const idle = this.#frame - this.#returned_at.get(buffer);
|
|
389
|
+
|
|
390
|
+
if (idle > this.param_backstop_frames || (over_budget > 0 && idle > this.param_over_budget_idle_frames)) {
|
|
391
|
+
ready.splice(cursor, 1);
|
|
392
|
+
count--;
|
|
393
|
+
|
|
394
|
+
this.#ready_byte_size -= buffer.size;
|
|
395
|
+
over_budget -= buffer.size;
|
|
396
|
+
reclaim_budget--;
|
|
397
|
+
|
|
398
|
+
buffer.destroy();
|
|
399
|
+
|
|
400
|
+
// the slot now holds the next buffer; the cursor stays where it is
|
|
401
|
+
} else {
|
|
402
|
+
this.#cursor = cursor + 1;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
140
405
|
}
|
|
141
406
|
|
|
142
407
|
destroy() {
|
|
408
|
+
this.#destroyed = true;
|
|
409
|
+
|
|
143
410
|
for (const b of this.#ready) {
|
|
144
411
|
b.destroy();
|
|
145
412
|
}
|
|
146
413
|
|
|
147
414
|
// drop data
|
|
148
415
|
this.#ready.splice(0, this.#ready.length);
|
|
416
|
+
this.#ready_byte_size = 0;
|
|
149
417
|
}
|
|
150
|
-
}
|
|
418
|
+
}
|
|
@@ -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
|
+
}
|