@woosh/meep-engine 3.32.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/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- 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 +11 -0
- package/src/shade/renderer/GraphicsContext.d.ts.map +1 -1
- package/src/shade/renderer/GraphicsContext.js +52 -38
- 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 +51 -0
- package/src/shade/renderer/buffer/GPUBufferStagingAllocator.d.ts.map +1 -1
- package/src/shade/renderer/buffer/GPUBufferStagingAllocator.js +214 -28
- 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.map +1 -1
- package/src/shade/renderer/dynamic/GPUDynamicMeshRenderer.js +19 -34
- 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/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
- package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +20 -0
- 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
|
@@ -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
|
*
|
|
@@ -7,6 +8,84 @@ import { binarySearchHighIndex } from "../../../core/collection/array/binarySear
|
|
|
7
8
|
*/
|
|
8
9
|
let id_count = 0;
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* A pooled buffer is handed out only for a request it is at most this many times the size of.
|
|
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
|
+
|
|
10
89
|
/**
|
|
11
90
|
* Mapped `MAP_WRITE | COPY_SRC` buffers for pushing data from the CPU to the GPU, one per upload.
|
|
12
91
|
*
|
|
@@ -15,9 +94,55 @@ let id_count = 0;
|
|
|
15
94
|
* that is the cheaper road; for thousands of small writes a frame the round trips are the cost, which
|
|
16
95
|
* is why a {@link ShadeGPUCommandContext} brings only its large writes here (see
|
|
17
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.
|
|
18
104
|
*/
|
|
19
105
|
export class GPUBufferStagingAllocator {
|
|
20
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
|
+
|
|
21
146
|
/**
|
|
22
147
|
* @type {GPUDevice}
|
|
23
148
|
*/
|
|
@@ -62,6 +187,27 @@ export class GPUBufferStagingAllocator {
|
|
|
62
187
|
*/
|
|
63
188
|
#destroyed = false;
|
|
64
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
|
+
|
|
65
211
|
/**
|
|
66
212
|
*
|
|
67
213
|
* @param {GPUDevice} device
|
|
@@ -91,29 +237,34 @@ export class GPUBufferStagingAllocator {
|
|
|
91
237
|
get(min_size_bytes) {
|
|
92
238
|
assert.isNonNegativeInteger(min_size_bytes, 'min_size_bytes');
|
|
93
239
|
|
|
94
|
-
const index = this.#get_position(min_size_bytes);
|
|
95
|
-
|
|
96
240
|
const buffers = this.#ready;
|
|
97
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
|
+
|
|
98
245
|
let result;
|
|
99
|
-
|
|
246
|
+
|
|
247
|
+
if (index < buffers.length && buffers[index].size <= min_size_bytes * REUSE_LIMIT_SCALE) {
|
|
100
248
|
result = buffers.splice(index, 1)[0];
|
|
101
|
-
}
|
|
102
249
|
|
|
103
|
-
|
|
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;
|
|
104
259
|
|
|
105
|
-
// TODO align buffer size?
|
|
106
260
|
const id = id_count++;
|
|
107
261
|
|
|
108
|
-
result =
|
|
262
|
+
result = device.createBuffer({
|
|
109
263
|
label: `staging ${id}`,
|
|
110
|
-
size: min_size_bytes,
|
|
264
|
+
size: Math.max(min_size_bytes, Math.min(size_class(min_size_bytes), largest)),
|
|
111
265
|
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE,
|
|
112
266
|
mappedAtCreation: true,
|
|
113
267
|
});
|
|
114
|
-
|
|
115
|
-
} else {
|
|
116
|
-
this.#ready_byte_size -= result.size;
|
|
117
268
|
}
|
|
118
269
|
|
|
119
270
|
this.#held_byte_size += result.size;
|
|
@@ -125,19 +276,6 @@ export class GPUBufferStagingAllocator {
|
|
|
125
276
|
return result;
|
|
126
277
|
}
|
|
127
278
|
|
|
128
|
-
/**
|
|
129
|
-
*
|
|
130
|
-
* @param {number} size
|
|
131
|
-
* @return {number}
|
|
132
|
-
*/
|
|
133
|
-
#get_position(size) {
|
|
134
|
-
function by_size(a, b) {
|
|
135
|
-
return a.size - b.size;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return binarySearchHighIndex(this.#ready, { size }, by_size);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
279
|
/**
|
|
142
280
|
*
|
|
143
281
|
* @param {GPUBuffer} buffer
|
|
@@ -148,7 +286,8 @@ export class GPUBufferStagingAllocator {
|
|
|
148
286
|
assert.equal(buffer.mapState, "mapped", 'buffer.mapState !== "mapped"');
|
|
149
287
|
assert.equal(buffer.usage, GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE, 'buffer.usage !== GPUBufferUsage.COPY_SRC | GPUBufferUsage.MAP_WRITE');
|
|
150
288
|
|
|
151
|
-
|
|
289
|
+
// ahead of its equals: the next request for this size takes it
|
|
290
|
+
const index = first_at_least(this.#ready, buffer.size);
|
|
152
291
|
|
|
153
292
|
this.#ready.splice(index, 0, buffer);
|
|
154
293
|
|
|
@@ -170,8 +309,6 @@ export class GPUBufferStagingAllocator {
|
|
|
170
309
|
|
|
171
310
|
const state = buffer.mapState;
|
|
172
311
|
|
|
173
|
-
// TODO the ready list never shrinks: a burst's buffers stay until destroy()
|
|
174
|
-
|
|
175
312
|
if (state === "mapped") {
|
|
176
313
|
// release directly
|
|
177
314
|
this.#take_back(buffer);
|
|
@@ -203,6 +340,8 @@ export class GPUBufferStagingAllocator {
|
|
|
203
340
|
}
|
|
204
341
|
|
|
205
342
|
this.#add_to_ready(buffer);
|
|
343
|
+
|
|
344
|
+
this.#returned_at.set(buffer, this.#frame);
|
|
206
345
|
}
|
|
207
346
|
|
|
208
347
|
/**
|
|
@@ -218,6 +357,53 @@ export class GPUBufferStagingAllocator {
|
|
|
218
357
|
buffer.destroy();
|
|
219
358
|
}
|
|
220
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
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
221
407
|
destroy() {
|
|
222
408
|
this.#destroyed = true;
|
|
223
409
|
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - **occupancy** — one world box per mesh, over the triangles that can still put a fragment on
|
|
9
9
|
* screen (`dynamic_mesh_live_bounds`), marked on the GPU through the particles' own chunks. The
|
|
10
|
-
* upload of the batch's vertices happens here too, being the first of
|
|
10
|
+
* upload of the batch's vertices and of its draws' constants happens here too, being the first of
|
|
11
|
+
* the three at execution time; the splat and the draw read the one upload.
|
|
11
12
|
* - **splat** — the meshes at the volume's resolution, coverage as the vertex and mesh alpha give it.
|
|
12
13
|
* - **draw** — the meshes at internal resolution into the accumulators, straight alpha premultiplied,
|
|
13
14
|
* fogged, weighted by the transmittance in front, routed by `avboit_premultiplied_contribution`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DynamicMeshAVBOITChannel.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/dynamic/DynamicMeshAVBOITChannel.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DynamicMeshAVBOITChannel.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/dynamic/DynamicMeshAVBOITChannel.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH;IA2BI;;OAEG;IACH,qCAKC;IAED;;;;;;;OAOG;IACH,mBAFa,OAAO,CAcnB;IAED;;;;;;;;;;OAUG;IACH;QANwB,KAAK,EAAlB,MAAM;QACO,MAAM,EAAnB,MAAM;QACkB,kBAAkB,EAA1C,UAAU,MAAM,CAAC;QACJ,SAAS,EAAtB,MAAM;QACJ,MAAM,CAuBlB;IAED;;;;;;;;;;OAUG;IACH;QAPwB,KAAK,EAAlB,MAAM;QACO,MAAM,EAAnB,MAAM;QACO,IAAI,EAAjB,MAAM;QACS,gBAAgB,EAA/B,MAAM,EAAE;QACoD,MAAM,EAAlE;YAAC,UAAU,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAC;QACnD;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAiBjE;IAED;;;;;;;;;;;;;;OAcG;IACH;QAXwB,KAAK,EAAlB,MAAM;QACmD,QAAQ,EAAjE,OAAO,2BAA2B,EAAE,cAAc;QACrC,MAAM,EAAnB,MAAM;QACO,aAAa,EAA1B,MAAM;QACO,IAAI,EAAjB,MAAM;QACO,QAAQ,EAArB,MAAM;QACO,eAAe,EAA5B,MAAM;QACO,oBAAoB,EAAjC,MAAM;QACoE,YAAY,EAAtF;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAC;QACjE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAkC/E;IAED;;;;;;OAMG;IACH,6BAFa,OAAO,CAInB;IAED;;;OAGG;IACH,gBAIC;;CAoDJ"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assert } from "../../../core/assert.js";
|
|
2
2
|
import { aabb3_matrix4_project } from "../../../core/geom/3d/aabb/aabb3_matrix4_project.js";
|
|
3
3
|
import { dynamic_mesh_live_bounds } from "./dynamic_mesh_live_bounds.js";
|
|
4
|
+
import { GPUDynamicMeshDrawList } from "./GPUDynamicMeshDrawList.js";
|
|
4
5
|
import {
|
|
5
6
|
graph_dynamic_mesh_avboit_draw,
|
|
6
7
|
graph_dynamic_mesh_avboit_occupancy,
|
|
@@ -25,7 +26,8 @@ const scratch_world = new Float64Array(6);
|
|
|
25
26
|
*
|
|
26
27
|
* - **occupancy** — one world box per mesh, over the triangles that can still put a fragment on
|
|
27
28
|
* screen (`dynamic_mesh_live_bounds`), marked on the GPU through the particles' own chunks. The
|
|
28
|
-
* upload of the batch's vertices happens here too, being the first of
|
|
29
|
+
* upload of the batch's vertices and of its draws' constants happens here too, being the first of
|
|
30
|
+
* the three at execution time; the splat and the draw read the one upload.
|
|
29
31
|
* - **splat** — the meshes at the volume's resolution, coverage as the vertex and mesh alpha give it.
|
|
30
32
|
* - **draw** — the meshes at internal resolution into the accumulators, straight alpha premultiplied,
|
|
31
33
|
* fogged, weighted by the transmittance in front, routed by `avboit_premultiplied_contribution`.
|
|
@@ -55,16 +57,17 @@ export class DynamicMeshAVBOITChannel {
|
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
59
|
* What {@link avboit_occupancy} decided for the graph it was asked in — the graph, the live
|
|
58
|
-
* meshes,
|
|
59
|
-
*
|
|
60
|
+
* meshes, a world box for each ({@link DYNAMIC_MESH_AVBOIT_BOX_FLOATS} floats apiece), and the
|
|
61
|
+
* list their draws are written into when the occupancy executes — for the splat and the draw of
|
|
62
|
+
* the same graph to take.
|
|
60
63
|
*
|
|
61
64
|
* **Dropped as soon as the draw is recorded.** The passes carry their own references to the list
|
|
62
65
|
* and the boxes into execution, so the channel keeps nothing of a frame past its recording: a
|
|
63
66
|
* channel that stops being asked — its last mesh faded, its renderer replaced — does not hold a
|
|
64
|
-
* whole frame graph, and every pass closure in it, reachable. And since each frame's
|
|
67
|
+
* whole frame graph, and every pass closure in it, reachable. And since each frame's lists and
|
|
65
68
|
* boxes are its own, two graphs recorded before either executes cannot see each other's.
|
|
66
69
|
*
|
|
67
|
-
* @type {{graph: object, live: DynamicMesh[], boxes: Float32Array}|null}
|
|
70
|
+
* @type {{graph: object, live: DynamicMesh[], boxes: Float32Array, draws: GPUDynamicMeshDrawList}|null}
|
|
68
71
|
*/
|
|
69
72
|
#frame = null;
|
|
70
73
|
|
|
@@ -130,6 +133,7 @@ export class DynamicMeshAVBOITChannel {
|
|
|
130
133
|
residency: this.#residency,
|
|
131
134
|
meshes: frame.live,
|
|
132
135
|
boxes: frame.boxes,
|
|
136
|
+
draws: frame.draws,
|
|
133
137
|
});
|
|
134
138
|
}
|
|
135
139
|
|
|
@@ -145,9 +149,9 @@ export class DynamicMeshAVBOITChannel {
|
|
|
145
149
|
* @returns {{extinction: number, overflow: number, dummy: number}} the volume after the write
|
|
146
150
|
*/
|
|
147
151
|
avboit_splat({ graph, camera, warp, voxel_resolution, volume }) {
|
|
148
|
-
const
|
|
152
|
+
const frame = this.#frame_for(graph);
|
|
149
153
|
|
|
150
|
-
if (
|
|
154
|
+
if (frame.live.length === 0) {
|
|
151
155
|
return volume;
|
|
152
156
|
}
|
|
153
157
|
|
|
@@ -157,8 +161,7 @@ export class DynamicMeshAVBOITChannel {
|
|
|
157
161
|
warp,
|
|
158
162
|
voxel_resolution,
|
|
159
163
|
volume,
|
|
160
|
-
|
|
161
|
-
meshes,
|
|
164
|
+
draws: frame.draws,
|
|
162
165
|
});
|
|
163
166
|
}
|
|
164
167
|
|
|
@@ -188,12 +191,12 @@ export class DynamicMeshAVBOITChannel {
|
|
|
188
191
|
volumetrics_metadata,
|
|
189
192
|
accumulators
|
|
190
193
|
}) {
|
|
191
|
-
const
|
|
194
|
+
const frame = this.#frame_for(graph);
|
|
192
195
|
|
|
193
196
|
// the last of the three: the frame is the passes' from here on, and none of the channel's
|
|
194
197
|
this.#frame = null;
|
|
195
198
|
|
|
196
|
-
if (
|
|
199
|
+
if (frame.live.length === 0) {
|
|
197
200
|
return accumulators;
|
|
198
201
|
}
|
|
199
202
|
|
|
@@ -207,8 +210,7 @@ export class DynamicMeshAVBOITChannel {
|
|
|
207
210
|
volumetrics_lut,
|
|
208
211
|
volumetrics_metadata,
|
|
209
212
|
accumulators,
|
|
210
|
-
|
|
211
|
-
meshes,
|
|
213
|
+
draws: frame.draws,
|
|
212
214
|
});
|
|
213
215
|
}
|
|
214
216
|
|
|
@@ -239,7 +241,7 @@ export class DynamicMeshAVBOITChannel {
|
|
|
239
241
|
* would upload nothing and draw some other frame's list, so it is refused.
|
|
240
242
|
*
|
|
241
243
|
* @param {object} graph
|
|
242
|
-
* @returns {{graph: object, live: DynamicMesh[], boxes: Float32Array}}
|
|
244
|
+
* @returns {{graph: object, live: DynamicMesh[], boxes: Float32Array, draws: GPUDynamicMeshDrawList}}
|
|
243
245
|
*/
|
|
244
246
|
#frame_for(graph) {
|
|
245
247
|
const frame = this.#frame;
|
|
@@ -251,10 +253,10 @@ export class DynamicMeshAVBOITChannel {
|
|
|
251
253
|
|
|
252
254
|
/**
|
|
253
255
|
* Find the batch's live meshes, and a world box for each — into a list and a buffer of this
|
|
254
|
-
* frame's own.
|
|
256
|
+
* frame's own, beside the draw list the occupancy will write their draws into.
|
|
255
257
|
*
|
|
256
258
|
* @param {object} graph
|
|
257
|
-
* @returns {{graph: object, live: DynamicMesh[], boxes: Float32Array}}
|
|
259
|
+
* @returns {{graph: object, live: DynamicMesh[], boxes: Float32Array, draws: GPUDynamicMeshDrawList}}
|
|
258
260
|
*/
|
|
259
261
|
#gather(graph) {
|
|
260
262
|
const meshes = this.#batch.meshes;
|
|
@@ -281,7 +283,7 @@ export class DynamicMeshAVBOITChannel {
|
|
|
281
283
|
live.push(mesh);
|
|
282
284
|
}
|
|
283
285
|
|
|
284
|
-
return { graph, live, boxes };
|
|
286
|
+
return { graph, live, boxes, draws: new GPUDynamicMeshDrawList() };
|
|
285
287
|
}
|
|
286
288
|
}
|
|
287
289
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The most draws one chunk of a frame's constants holds — and so the most draws one bind of the
|
|
3
|
+
* constants' group serves.
|
|
4
|
+
*
|
|
5
|
+
* Bounded rather than one table for every mesh the renderer draws: a chunk is one transient buffer
|
|
6
|
+
* and one write of at most 80 KiB of constants, whatever the frame's mesh count, so the write never
|
|
7
|
+
* grows with the scene and a chunk's buffer is the pool's ordinary business.
|
|
8
|
+
*
|
|
9
|
+
* @type {number}
|
|
10
|
+
*/
|
|
11
|
+
export const DYNAMIC_MESH_DRAWS_PER_CHUNK: number;
|
|
12
|
+
/**
|
|
13
|
+
* One frame's draws of a set of {@link DynamicMesh}es: which geometry each draws, and its per-draw
|
|
14
|
+
* constants ({@link DYNAMIC_MESH_DRAW_STRUCT}), written in chunks of up to
|
|
15
|
+
* {@link DYNAMIC_MESH_DRAWS_PER_CHUNK}.
|
|
16
|
+
*
|
|
17
|
+
* **The constants are all written anew every frame**, into transient buffers of the frame's own: a
|
|
18
|
+
* dynamic mesh's transform and colour are as free to change as its vertices, so nothing is kept for
|
|
19
|
+
* a mesh from one frame to the next. What the table buys is that the frame pays per *chunk* for the
|
|
20
|
+
* buffer, the copy and the bind group, where it used to pay per mesh for each. Per draw, what is
|
|
21
|
+
* left is the geometry's bind group — the same one every frame, while its buffers keep their size —
|
|
22
|
+
* and the draw, which names its row of the chunk through its `first_instance`.
|
|
23
|
+
*
|
|
24
|
+
* Draws keep the order of the meshes they came from, across chunks: the pass over the scene blends,
|
|
25
|
+
* and composites in that order.
|
|
26
|
+
*
|
|
27
|
+
* {@link write} before a render pass is opened on the encoder — the constants go up through copies,
|
|
28
|
+
* which a command encoder refuses while a pass is open on it — and then {@link draw} into as many
|
|
29
|
+
* passes of the frame as draw these meshes. The AVBOIT splat and draw read one write.
|
|
30
|
+
*/
|
|
31
|
+
export class GPUDynamicMeshDrawList {
|
|
32
|
+
/**
|
|
33
|
+
* How many draws the last {@link write} laid out.
|
|
34
|
+
*
|
|
35
|
+
* @returns {number}
|
|
36
|
+
*/
|
|
37
|
+
get count(): number;
|
|
38
|
+
/**
|
|
39
|
+
* Lay out a draw for every visible mesh of `meshes` with triangles resident in `residency`, and
|
|
40
|
+
* send their constants — as they are now — in one write per chunk. Replaces whatever the list
|
|
41
|
+
* held.
|
|
42
|
+
*
|
|
43
|
+
* The buffers are the encoder's transients, so the list is good for as long as that encoder is
|
|
44
|
+
* recording and no longer.
|
|
45
|
+
*
|
|
46
|
+
* @param {ShadeGPUCommandContext} encoder
|
|
47
|
+
* @param {GPUDynamicMeshResidencyTable} residency brought up to date for `meshes` this frame
|
|
48
|
+
* @param {DynamicMesh[]} meshes
|
|
49
|
+
*/
|
|
50
|
+
write(encoder: ShadeGPUCommandContext, residency: GPUDynamicMeshResidencyTable, meshes: DynamicMesh[]): void;
|
|
51
|
+
/**
|
|
52
|
+
* Let go of the frame: the geometries it drew and the transient buffers it wrote, which the
|
|
53
|
+
* encoder hands back when it finishes. For a list that outlives its frame, once its last pass is
|
|
54
|
+
* recorded.
|
|
55
|
+
*/
|
|
56
|
+
clear(): void;
|
|
57
|
+
/**
|
|
58
|
+
* Record every draw into `render_pass`: the constants' group once per chunk, then per draw the
|
|
59
|
+
* geometry's group and the draw at its row. The pass's other groups are the caller's to set.
|
|
60
|
+
*
|
|
61
|
+
* @param {ShadeGPURenderPassEncoder} render_pass running `pipeline`
|
|
62
|
+
* @param {GraphicsContext} graphics
|
|
63
|
+
* @param {RenderPipelineDescriptor} pipeline one of the dynamic-mesh pipelines, whose layout opens
|
|
64
|
+
* with `DYNAMIC_MESH_RESOURCES`' groups
|
|
65
|
+
* @param {GPUBuffer} camera
|
|
66
|
+
*/
|
|
67
|
+
draw(render_pass: ShadeGPURenderPassEncoder, graphics: GraphicsContext, pipeline: RenderPipelineDescriptor, camera: GPUBuffer): void;
|
|
68
|
+
#private;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=GPUDynamicMeshDrawList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GPUDynamicMeshDrawList.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/dynamic/GPUDynamicMeshDrawList.js"],"names":[],"mappings":"AASA;;;;;;;;;GASG;AACH,2CAFU,MAAM,CAEiC;AAsEjD;;;;;;;;;;;;;;;;;;GAkBG;AACH;IAgBI;;;;OAIG;IACH,oBAEC;IAED;;;;;;;;;;;OAWG;IACH,wFAFW,aAAa,QAoEvB;IAED;;;;OAIG;IACH,cAGC;IAED;;;;;;;;;OASG;IACH,qIAsBC;;CACJ"}
|