@woosh/meep-engine 3.60.5 → 3.60.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +1 -1
  3. package/src/shade/renderer/Renderer.d.ts +11 -0
  4. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  5. package/src/shade/renderer/Renderer.js +43 -1
  6. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +26 -43
  7. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts.map +1 -1
  8. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +109 -117
  9. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +20 -2
  10. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  11. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +66 -2
  12. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts +19 -0
  13. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts.map +1 -0
  14. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.js +25 -0
  15. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts +15 -0
  16. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -1
  17. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +38 -0
  18. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts +28 -59
  19. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts.map +1 -1
  20. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.js +84 -789
  21. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +2 -1
  22. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  23. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +2 -1
  24. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.d.ts +2 -2
  25. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.js +2 -2
  26. package/src/shade/renderer/material/GPUMaterialManager.d.ts +4 -1
  27. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  28. package/src/shade/renderer/material/GPUMaterialManager.js +5 -8
  29. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts +7 -3
  30. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts.map +1 -1
  31. package/src/shade/renderer/postprocess/gtao/GTAO.js +26 -13
  32. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts +14 -0
  33. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts.map +1 -1
  34. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.js +196 -68
  35. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts.map +1 -1
  36. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js +42 -28
  37. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts.map +1 -1
  38. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +429 -414
  39. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts +16 -0
  40. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts.map +1 -0
  41. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.js +156 -0
  42. package/src/shade/renderer/scene/GPUSceneContext.d.ts +2 -2
  43. package/src/shade/renderer/scene/GPUSceneContext.js +5 -5
  44. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts +0 -47
  45. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts.map +0 -1
  46. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.js +0 -80
  47. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts +0 -16
  48. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts.map +0 -1
  49. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.js +0 -67
  50. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts +0 -17
  51. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts.map +0 -1
  52. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.js +0 -145
  53. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts +0 -12
  54. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts.map +0 -1
  55. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.js +0 -105
@@ -1,181 +1,46 @@
1
1
  import { assert } from "../../../../../core/assert.js";
2
- import { BufferDescriptor } from "../../../../descriptor/buffer/BufferDescriptor.js";
3
- import { BufferResourceDescriptor } from "../../../../device/graph/BufferResourceDescriptor.js";
4
- import { graph_read_buffer } from "../../../buffer/graph_read_buffer.js";
5
- import { graph_build_indirect_dispatch_command } from "../../../gpu_primitive/indirect/IndirectComputeCommandBuildPass.js";
6
- import {
7
- graph_prefix_scan_csdldf,
8
- prefix_scan_csdldf_output_descriptor
9
- } from "../../../gpu_primitive/prefix_sum/v1/graph_prefix_scan_csdldf.js";
10
- import { graph_create_buffer } from "../../../shader/graph/graph_create_buffer.js";
11
- import { Attribute } from "../../Attribute.js";
12
- import { Geometry } from "../../Geometry.js";
13
- import { MeshoptClusterizer } from "../../meshlet/build/meshoptimizer/meshopt_clusterizer.js";
14
- import { meshlet_geometry_build_from_geometry } from "../../meshlet_geometry_build_from_geometry.js";
15
- import { MeshSDFState } from "../../sdf/MESH_SDF.js";
16
- import { MeshSDFLayout } from "../../sdf/MeshSDFLayout.js";
17
- import { StandardAttributes } from "../../StandardAttributes.js";
18
- import { vgeo_feedback_slot_stats } from "../runtime/cut/vgeo_cut_feedback_layout.js";
19
- import { graph_vgeo_cut_descent } from "../runtime/graph_vgeo_cut_descent.js";
20
- import { VGEO_CUT_STAT } from "../runtime/layout/VGEO_CUT_STAT.js";
21
- import { VGEO_CUT_STAT_COUNT } from "../runtime/layout/VGEO_CUT_STAT_COUNT.js";
22
- import { VGEO_GEOMETRY_ROW_NONE } from "../runtime/layout/VGEO_GEOMETRY_ROW_NONE.js";
23
- import { VGEO_INSTANCE_WORDS } from "../runtime/layout/VGEO_INSTANCE_WORDS.js";
24
- import { VGeoViewCut } from "../runtime/VGeoViewCut.js";
25
- import { shader_vgeo_sdf_collect, VGEO_SDF_COLLECT_WORKGROUP_SIZE } from "./shader_vgeo_sdf_collect.js";
26
- import { shader_vgeo_sdf_decode } from "./shader_vgeo_sdf_decode.js";
27
- import { shader_vgeo_sdf_traverse } from "./shader_vgeo_sdf_traverse.js";
28
- import {
29
- VGEO_SDF_CLUSTER_WORDS,
30
- VGEO_SDF_COUNTER,
31
- VGEO_SDF_COUNTER_WORDS,
32
- VGEO_SDF_JOB_STRUCT,
33
- vgeo_sdf_header_words
34
- } from "./VGEO_SDF.js";
35
2
 
36
3
  /**
37
- * @typedef {import("../../../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} FrameGraph
38
4
  * @typedef {import("../../GPUGeometryManager.js").GPUGeometryManager} GPUGeometryManager
39
5
  * @typedef {import("../../GPUGeometryMetadata.js").GPUGeometryMetadata} GPUGeometryMetadata
40
6
  * @typedef {import("../../MeshletGeometry.js").MeshletGeometry} MeshletGeometry
41
- * @typedef {import("../runtime/GPUVirtualGeometryRuntime.js").GPUVirtualGeometryRuntime} GPUVirtualGeometryRuntime
42
7
  */
43
8
 
44
- /** The slot holds no geometry. */
45
- const JOB_FREE = 0;
46
- /** Cut each frame, its counters read back. */
47
- const JOB_CUT = 1;
48
- /** Its last cut read back stood nothing in: the next frame it is cut, its mesh is written out too. */
49
- const JOB_READY = 2;
50
- /** Written out in a frame whose readback has not landed. */
51
- const JOB_DECODING = 3;
52
- /** Its mesh is read back: its proxy is built on the host. */
53
- const JOB_BUILD = 4;
54
- /** Its proxy is baked by the mesh path. */
55
- const JOB_BAKING = 5;
56
- /** Its proxy's cache is resident: given to the geometry at the next update. */
57
- const JOB_BAKED = 6;
58
-
59
- /**
60
- * Frames whose readback may be in flight before a frame's cut is skipped, as the bake's log: a mapping lands a frame or
61
- * two after its copy, and past this the device is behind.
62
- * @type {number}
63
- */
64
- const MAX_FRAMES_IN_FLIGHT = 3;
65
-
66
- /**
67
- * Geometries extracted at once: the jobs cut each frame until their cut stands nothing in. One whose proxy is built,
68
- * baking or waiting to be given its cache keeps its slot until then.
69
- * @type {number}
70
- */
71
- const DEFAULT_JOB_CAPACITY = 32;
72
-
73
- /**
74
- * Entries the cluster list starts with: a cut of a typical geometry at half its cache's voxel is under 128 clusters
75
- * (S8, step 0), for each of {@link DEFAULT_JOB_CAPACITY} jobs. It doubles past what a frame's cut took.
76
- * @type {number}
77
- */
78
- const INITIAL_CLUSTER_CAPACITY = DEFAULT_JOB_CAPACITY * 128;
79
-
80
- /**
81
- * One geometry being extracted.
82
- */
83
- class VGeoSDFJob {
84
- /**
85
- * Bumped each time the slot takes a geometry: a readback of a frame cut for an earlier one is not this one's.
86
- * @type {number}
87
- */
88
- generation = 0;
89
-
90
- phase = JOB_FREE;
91
-
92
- /**
93
- * The geometry, whose cache is asked for.
94
- * @type {GPUGeometryMetadata|null}
95
- */
96
- record = null;
97
-
98
- /**
99
- * What puts its error against a τ of 1: one over half its cache's level-0 voxel, in its own units.
100
- * @type {number}
101
- */
102
- projection_k = 0;
103
-
104
- /** What its last complete cut held. */
105
- vertex_count = 0;
106
- triangle_count = 0;
107
-
108
- /** @type {Float32Array|null} */
109
- positions = null;
110
-
111
- /** @type {Uint32Array|null} */
112
- indices = null;
113
-
114
- /** @type {MeshletGeometry|null} */
115
- proxy = null;
116
-
117
- /** @type {GPUGeometryMetadata|null} */
118
- proxy_record = null;
119
- }
120
-
121
9
  /**
122
- * The jobs one recorded frame cut, for its readback.
10
+ * A virtual geometry's stand-in, once applied.
123
11
  */
124
- class VGeoSDFFrame {
125
- busy = false;
126
-
127
- count = 0;
128
-
129
- /** @type {VGeoSDFJob[]} */
130
- jobs = [];
131
-
12
+ class VGeoProxyEntry {
132
13
  /**
133
- * @param {number} capacity
14
+ * @param {GPUGeometryMetadata} record the virtual geometry's
15
+ * @param {MeshletGeometry} proxy
134
16
  */
135
- constructor(capacity) {
136
- this.generations = new Uint32Array(capacity);
137
- this.decoding = new Uint8Array(capacity);
138
- this.vertex_bases = new Uint32Array(capacity);
139
- this.triangle_bases = new Uint32Array(capacity);
17
+ constructor(record, proxy) {
18
+ this.record = record;
19
+ this.proxy = proxy;
140
20
  }
141
-
142
- vertex_offset = 0;
143
- index_offset = 0;
144
21
  }
145
22
 
146
23
  /**
147
- * The geometry caches of virtual geometry (PROBE_CLIPMAP_PROPOSAL_2026_10_27.md §7.8, as S8 built it): a
148
- * {@link MeshSDFSource} of {@link GPUGeometrySDFManager}, which bakes a geometry's field from its tree, and a virtual
149
- * geometry has none. Each virtual geometry whose cache is asked for gets a proxy, the triangles of a cut of it, baked by
150
- * the mesh path as any geometry is, and its resident cache adopted into the geometry. Its instances are then composed
151
- * from it as a mesh's are.
24
+ * The geometry caches and cards of virtual geometry (PROBE_CLIPMAP_PROPOSAL_2026_10_27.md §7.8, §17).
25
+ * {@link GPUGeometrySDFManager} bakes a geometry's field from its tree and captures its cards through its meshlets, and
26
+ * a virtual geometry has neither.
152
27
  *
153
- * **The cut.** A geometry is cut whole by the camera cut's own descent (`shader_vgeo_sdf_traverse`), at an error under
154
- * half its cache's level-0 voxel, in its own space, under a feedback slot of the extraction's own: a cluster's page not
155
- * resident is wanted there, as a view's are, and since a want lasts one reading the job is cut again each frame until
156
- * its cut stands nothing in. Its counters are read back each frame.
28
+ * Each virtual geometry whose cache is asked for gets its proxy ({@link VirtualGeometry#get_proxy}): an ordinary
29
+ * geometry in the same local space, with its attributes. The proxy is added to the geometry manager and the virtual
30
+ * geometry aliased to it ({@link GPUGeometrySDFManager#alias}). From there the proxy is any geometry: the virtual
31
+ * geometry's holders hold its cache, baked from its tree, and the virtual geometry's card sets capture its layout,
32
+ * drawn through its meshlets with the virtual geometry's own materials.
157
33
  *
158
- * **The mesh.** The frame after a cut that stood nothing in, the job's clusters are written out too, its vertices as
159
- * stored and its triangles as indices into them (`shader_vgeo_sdf_decode`), in room the last cut's counts made, and
160
- * read back with that frame's counters. A frame that cut it otherwise (a page evicted since, the cut grown) is not
161
- * taken, and the job is cut again.
34
+ * **The proxy's detail is its own**, not the cache's voxel's: a proxy coarser than the voxel bakes a coarser field and
35
+ * coarser cards than the asset would. A known discrepancy, kept: a hit on virtual geometry has a material at all.
162
36
  *
163
- * **The proxy.** One a frame, on the host: an ordinary geometry built from the mesh
164
- * (`meshlet_geometry_build_from_geometry`, which costs 2.2 to 4.1 µs a triangle, measured), added to the geometry
165
- * manager, its cache held with the side the geometry's holders draw. When its bake turns resident the next update gives
166
- * it to the geometry ({@link GPUGeometrySDFManager#adopt}) and removes the proxy.
167
- *
168
- * A geometry no longer asked for when its turn comes is let go, and one whose holders ask again is extracted again.
169
- * Settings that change the caches' voxel restart every job.
37
+ * A proxy is applied as it arrives, which is between frames: adding a geometry grows the geometry manager's buffers,
38
+ * which a frame's passes must not find replaced under them. A virtual geometry the geometry manager lets go takes its
39
+ * proxy with it; one let go before its proxy arrives has none applied. A proxy that cannot be had leaves its geometry
40
+ * waiting, black.
170
41
  */
171
42
  export class VGeoSDFProxies {
172
43
 
173
- /** @type {GPUDevice} */
174
- #device;
175
-
176
- /** @type {GPUVirtualGeometryRuntime} */
177
- #runtime;
178
-
179
44
  /** @type {GPUGeometryManager} */
180
45
  #geometries;
181
46
 
@@ -183,700 +48,137 @@ export class VGeoSDFProxies {
183
48
  #sdf;
184
49
 
185
50
  /**
186
- * The extraction's cut: its slot, its params and its wavefront bound.
187
- * @type {VGeoViewCut}
51
+ * Virtual geometries whose proxy is asked for and has not arrived, by id.
52
+ * @type {Map<number, GPUGeometryMetadata>}
188
53
  */
189
- #cut;
190
-
191
- /** @type {VGeoSDFJob[]} */
192
- #jobs = [];
54
+ #asked = new Map();
193
55
 
194
56
  /**
195
- * Geometries asked for and not yet a job's, in the order asked; a geometry asked for again or let go since may be
196
- * here, and is passed over.
197
- * @type {GPUGeometryMetadata[]}
57
+ * Proxies applied, by the virtual geometry's id.
58
+ * @type {Map<number, VGeoProxyEntry>}
198
59
  */
199
- #requested = [];
200
-
201
- /** @type {VGeoSDFFrame[]} */
202
- #frames = [];
203
-
204
- #cluster_capacity = INITIAL_CLUSTER_CAPACITY;
205
-
206
- /**
207
- * The frame's jobs as the passes read them, `VGEO_SDF_JOB_STRUCT` each.
208
- * @type {GPUBuffer}
209
- */
210
- #jobs_buffer;
211
-
212
- #jobs_scratch;
213
-
214
- #settings = { job_count: 0, cluster_capacity: 0, vertex_offset: 0, index_offset: 0 };
215
-
216
- #collect_data = { instances: -1, claim_widths: -1 };
217
- #collect_bindings = {};
218
- #decode_data = { command: -1, clusters: -1, counters: -1, payload: -1 };
219
- #decode_bindings = {};
220
- #header_data = { counters: -1, feedback: -1, payload: -1 };
221
-
222
- /** Where the extraction's slot keeps its counters in the frame's feedback, for the frame being recorded. */
223
- #stats_base = 0;
60
+ #applied = new Map();
224
61
 
225
- #voxel_size;
226
- #max_bricks;
227
-
228
- #layout = new MeshSDFLayout();
229
-
230
- #clusterizer_ready = false;
231
-
232
- #extracted_count = 0;
233
- #adopted_count = 0;
234
- #triangles_extracted = 0;
62
+ #failed_count = 0;
235
63
 
236
64
  #destroyed = false;
237
65
 
238
66
  /**
239
67
  * @param {Object} options
240
- * @param {GPUDevice} options.device
241
- * @param {GPUVirtualGeometryRuntime} options.runtime whose geometries' caches this makes
242
68
  * @param {GPUGeometryManager} options.geometries the scene's, whose caches the virtual geometries' are
243
- * @param {number} [options.job_capacity] geometries extracted at once
244
69
  */
245
- constructor({ device, runtime, geometries, job_capacity = DEFAULT_JOB_CAPACITY }) {
246
- assert.defined(device, 'device');
247
- assert.defined(runtime, 'runtime');
70
+ constructor({ geometries }) {
248
71
  assert.defined(geometries, 'geometries');
249
- assert.isPositiveInteger(job_capacity, 'job_capacity');
250
72
 
251
- this.#device = device;
252
- this.#runtime = runtime;
253
73
  this.#geometries = geometries;
254
74
  this.#sdf = geometries.sdf;
255
- this.#cut = new VGeoViewCut({ device, label: 'vgeo sdf proxies' });
256
-
257
- this.#voxel_size = this.#sdf.voxel_size;
258
- this.#max_bricks = this.#sdf.max_bricks;
259
-
260
- for (let i = 0; i < job_capacity; i++) {
261
- this.#jobs.push(new VGeoSDFJob());
262
- }
263
-
264
- for (let i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
265
- this.#frames.push(new VGeoSDFFrame(job_capacity));
266
- }
267
-
268
- this.#jobs_buffer = device.createBuffer(BufferDescriptor.fromJSON({
269
- label: 'VGeoSDFProxies/jobs',
270
- size: job_capacity * VGEO_SDF_JOB_STRUCT.size,
271
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
272
- }));
273
-
274
- this.#jobs_scratch = new DataView(new ArrayBuffer(job_capacity * VGEO_SDF_JOB_STRUCT.size));
275
-
276
- const ready = MeshoptClusterizer.ready;
277
-
278
- if (ready === undefined) {
279
- this.#clusterizer_ready = true;
280
- } else {
281
- ready.then(() => {
282
- this.#clusterizer_ready = true;
283
- });
284
- }
285
-
286
- this.#sdf.onRequested.add(this.#requested_one, this);
287
- this.#sdf.onBaked.add(this.#baked, this);
288
- this.#sdf.add_source(this);
289
- }
290
75
 
291
- /**
292
- * The extraction's cut, whose `stats` say what the last reading of its slot counted.
293
- * @returns {VGeoViewCut}
294
- */
295
- get cut() {
296
- return this.#cut;
297
- }
298
-
299
- /**
300
- * Geometries being extracted.
301
- * @returns {number}
302
- */
303
- get job_count() {
304
- let count = 0;
305
-
306
- for (const job of this.#jobs) {
307
- if (job.phase !== JOB_FREE) {
308
- count++;
309
- }
310
- }
311
-
312
- return count;
313
- }
314
-
315
- /**
316
- * Geometries asked for that wait for a job, those let go since included.
317
- * @returns {number}
318
- */
319
- get requested_count() {
320
- return this.#requested.length;
321
- }
322
-
323
- /**
324
- * Proxies built so far.
325
- * @returns {number}
326
- */
327
- get extracted_count() {
328
- return this.#extracted_count;
76
+ this.#sdf.onRequested.add(this.#requested, this);
77
+ this.#sdf.onForgotten.add(this.#forgotten, this);
329
78
  }
330
79
 
331
80
  /**
332
- * Caches given to their geometries so far.
81
+ * Virtual geometries whose proxy is asked for and has not arrived.
333
82
  * @returns {number}
334
83
  */
335
- get adopted_count() {
336
- return this.#adopted_count;
84
+ get pending_count() {
85
+ return this.#asked.size;
337
86
  }
338
87
 
339
88
  /**
340
- * Triangles of the proxies built so far.
89
+ * Virtual geometries standing in through their proxy.
341
90
  * @returns {number}
342
91
  */
343
- get triangles_extracted() {
344
- return this.#triangles_extracted;
92
+ get applied_count() {
93
+ return this.#applied.size;
345
94
  }
346
95
 
347
96
  /**
348
- * Entries the cluster list holds.
97
+ * Proxies that could not be had.
349
98
  * @returns {number}
350
99
  */
351
- get cluster_capacity() {
352
- return this.#cluster_capacity;
353
- }
354
-
355
- /**
356
- * @param {GPUGeometryMetadata} record
357
- */
358
- #requested_one(record) {
359
- if (record.vg_row !== VGEO_GEOMETRY_ROW_NONE) {
360
- this.#requested.push(record);
361
- }
362
- }
363
-
364
- /**
365
- * @param {number} id
366
- */
367
- #baked(id) {
368
- for (const job of this.#jobs) {
369
- if (job.phase === JOB_BAKING && job.proxy_record.index === id) {
370
- job.phase = JOB_BAKED;
371
- }
372
- }
100
+ get failed_count() {
101
+ return this.#failed_count;
373
102
  }
374
103
 
375
104
  /**
376
- * The frame's work, ahead of the caches' bake: the caches baked given to their geometries, a proxy built, jobs
377
- * started, and the jobs' cut recorded into `graph` while the runtime's frame is open.
105
+ * The proxy a virtual geometry stands in through, once applied.
378
106
  *
379
- * @param {FrameGraph} graph
380
- */
381
- graph_update(graph) {
382
- assert.notOk(this.#destroyed, 'destroyed');
383
-
384
- const sdf = this.#sdf;
385
-
386
- if (sdf.voxel_size !== this.#voxel_size || sdf.max_bricks !== this.#max_bricks) {
387
- // a cut for the old voxel bakes a proxy finer or coarser than the cache it is for
388
- this.#restart();
389
-
390
- this.#voxel_size = sdf.voxel_size;
391
- this.#max_bricks = sdf.max_bricks;
392
- }
393
-
394
- this.#adopt();
395
- this.#build();
396
- this.#start();
397
-
398
- if (this.#runtime.frame_open) {
399
- this.#record(graph);
400
- }
401
- }
402
-
403
- /**
404
- * Give each geometry whose proxy is baked its cache, and remove the proxy.
107
+ * @param {GPUGeometryMetadata} record the virtual geometry's
108
+ * @returns {MeshletGeometry|undefined}
405
109
  */
406
- #adopt() {
407
- const sdf = this.#sdf;
408
-
409
- for (const job of this.#jobs) {
410
- if (job.phase !== JOB_BAKED) {
411
- continue;
412
- }
413
-
414
- if (sdf.adopt(job.record, job.proxy_record)) {
415
- this.#adopted_count++;
416
- } else if (sdf.is_requested(job.record)) {
417
- // the proxy's cache is not the one it was baked to: a proxy equal to another's is that one's record
418
- this.#requested.push(job.record);
419
- }
110
+ proxy_of(record) {
111
+ const entry = this.#applied.get(record.index);
420
112
 
421
- this.#free(job);
422
- }
113
+ return entry !== undefined && entry.record === record ? entry.proxy : undefined;
423
114
  }
424
115
 
425
116
  /**
426
- * Build the proxy of the first job whose mesh is read back, and hold its cache.
117
+ * @param {GPUGeometryMetadata} record
427
118
  */
428
- #build() {
429
- if (!this.#clusterizer_ready) {
430
- return;
431
- }
432
-
433
- const sdf = this.#sdf;
434
-
435
- for (const job of this.#jobs) {
436
- if (job.phase !== JOB_BUILD) {
437
- continue;
438
- }
439
-
440
- if (!sdf.is_requested(job.record)) {
441
- // let go since its cut was taken
442
- this.#free(job);
443
- continue;
444
- }
445
-
446
- const mesh = new Geometry();
447
-
448
- mesh.name = 'vgeo sdf proxy';
449
- mesh.index = Attribute.from(job.indices, 1, StandardAttributes.Index);
450
- mesh.addAttribute(Attribute.from(job.positions, 3, StandardAttributes.Position));
451
-
452
- const proxy = meshlet_geometry_build_from_geometry(mesh);
453
-
454
- this.#geometries.add(proxy);
455
-
456
- job.proxy = proxy;
457
- job.proxy_record = this.#geometries.get_metadata(proxy);
458
- job.positions = null;
459
- job.indices = null;
460
- job.phase = JOB_BAKING;
461
-
462
- this.#extracted_count++;
463
- this.#triangles_extracted += job.triangle_count;
464
-
465
- sdf.acquire(job.proxy_record, sdf.holds_two_sided(job.record));
466
-
467
- if (sdf.state_of(job.proxy_record) === MeshSDFState.Resident) {
468
- // a record a resident proxy of the same mesh already had
469
- job.phase = JOB_BAKED;
470
- }
119
+ #requested(record) {
120
+ const geometry = record.geometry;
471
121
 
122
+ if (geometry?.isVirtualGeometry !== true) {
472
123
  return;
473
124
  }
474
- }
475
-
476
- /**
477
- * Give the free slots the geometries asked for, in the order asked.
478
- */
479
- #start() {
480
- const sdf = this.#sdf;
481
- const requested = this.#requested;
482
- const layout = this.#layout;
483
-
484
- let taken = 0;
485
125
 
486
- for (const job of this.#jobs) {
487
- if (job.phase !== JOB_FREE) {
488
- continue;
489
- }
126
+ // asked again before it arrives, the proxy is the same promise's, and arrives twice: the second finds it applied
127
+ this.#asked.set(record.index, record);
490
128
 
491
- while (taken < requested.length) {
492
- const record = requested[taken++];
129
+ geometry.get_proxy().then(
130
+ proxy => this.#arrived(record, proxy),
131
+ error => {
132
+ if (this.#asked.get(record.index) === record) {
133
+ this.#asked.delete(record.index);
134
+ this.#failed_count++;
493
135
 
494
- if (!sdf.is_requested(record) || this.#extracting(record)) {
495
- continue;
136
+ console.warn('vgeo: no proxy, the geometry has no cache or cards', error);
496
137
  }
497
-
498
- layout.compute(record.geometry.bounding_box, sdf.voxel_size, sdf.max_bricks);
499
-
500
- job.generation++;
501
- job.record = record;
502
- job.projection_k = 1 / (0.5 * layout.voxel);
503
- job.phase = JOB_CUT;
504
-
505
- break;
506
138
  }
507
- }
508
-
509
- requested.copyWithin(0, taken);
510
- requested.length -= taken;
139
+ );
511
140
  }
512
141
 
513
142
  /**
514
143
  * @param {GPUGeometryMetadata} record
515
- * @returns {boolean} whether a job holds it
516
- */
517
- #extracting(record) {
518
- for (const job of this.#jobs) {
519
- if (job.phase !== JOB_FREE && job.record === record) {
520
- return true;
521
- }
522
- }
523
-
524
- return false;
525
- }
526
-
527
- /**
528
- * Let every job go, and ask again for the geometries still asked for.
529
- */
530
- #restart() {
531
- for (const job of this.#jobs) {
532
- if (job.phase === JOB_FREE) {
533
- continue;
534
- }
535
-
536
- if (this.#sdf.is_requested(job.record)) {
537
- this.#requested.push(job.record);
538
- }
539
-
540
- this.#free(job);
541
- }
542
- }
543
-
544
- /**
545
- * @param {VGeoSDFJob} job
546
- */
547
- #free(job) {
548
- if (job.proxy !== null) {
549
- this.#geometries.remove(job.proxy);
550
- }
551
-
552
- job.phase = JOB_FREE;
553
- job.record = null;
554
- job.positions = null;
555
- job.indices = null;
556
- job.proxy = null;
557
- job.proxy_record = null;
558
- }
559
-
560
- /**
561
- * @returns {VGeoSDFFrame|null}
562
- */
563
- #take_frame() {
564
- for (const frame of this.#frames) {
565
- if (!frame.busy) {
566
- return frame;
567
- }
568
- }
569
-
570
- return null;
571
- }
572
-
573
- /**
574
- * Record the cut of the jobs that cut, and the write-out of those whose last cut stood nothing in.
575
- *
576
- * @param {FrameGraph} graph
144
+ * @param {MeshletGeometry} proxy
577
145
  */
578
- #record(graph) {
579
- const frame = this.#take_frame();
580
-
581
- if (frame === null) {
582
- return;
583
- }
584
-
585
- const runtime = this.#runtime;
586
- const view = this.#jobs_scratch;
587
- const stride = VGEO_SDF_JOB_STRUCT.size;
588
-
589
- let count = 0;
590
- let vertices = 0;
591
- let triangles = 0;
592
-
593
- for (const job of this.#jobs) {
594
- if (job.phase !== JOB_CUT && job.phase !== JOB_READY) {
595
- continue;
596
- }
597
-
598
- if (!this.#sdf.is_requested(job.record)) {
599
- // let go since it was asked for: dropped, or forgotten
600
- this.#free(job);
601
- continue;
602
- }
603
-
604
- const decode = job.phase === JOB_READY;
605
- const at = count * stride;
606
-
607
- frame.jobs[count] = job;
608
- frame.generations[count] = job.generation;
609
- frame.decoding[count] = decode ? 1 : 0;
610
- frame.vertex_bases[count] = vertices;
611
- frame.triangle_bases[count] = triangles;
612
-
613
- view.setUint32(at + VGEO_SDF_JOB_STRUCT.get("vg_row").offset, job.record.vg_row, true);
614
- view.setFloat32(at + VGEO_SDF_JOB_STRUCT.get("projection_k").offset, job.projection_k, true);
615
- view.setUint32(at + VGEO_SDF_JOB_STRUCT.get("decode").offset, decode ? 1 : 0, true);
616
- view.setUint32(at + VGEO_SDF_JOB_STRUCT.get("vertex_base").offset, vertices, true);
617
- view.setUint32(at + VGEO_SDF_JOB_STRUCT.get("triangle_base").offset, triangles, true);
618
- view.setUint32(at + VGEO_SDF_JOB_STRUCT.get("vertex_count").offset, decode ? job.vertex_count : 0, true);
619
- view.setUint32(at + VGEO_SDF_JOB_STRUCT.get("triangle_count").offset, decode ? job.triangle_count : 0, true);
620
-
621
- if (decode) {
622
- vertices += job.vertex_count;
623
- triangles += job.triangle_count;
624
- job.phase = JOB_DECODING;
625
- }
626
-
627
- count++;
628
- }
629
-
630
- if (count === 0) {
146
+ #arrived(record, proxy) {
147
+ if (this.#asked.get(record.index) !== record) {
148
+ // let go since it was asked for, or applied already
631
149
  return;
632
150
  }
633
151
 
634
- frame.busy = true;
635
- frame.count = count;
636
-
637
- const header_words = vgeo_sdf_header_words(count);
638
-
639
- frame.vertex_offset = header_words;
640
- frame.index_offset = header_words + 3 * vertices;
641
-
642
- const payload_bytes = 4 * (frame.index_offset + 3 * triangles);
643
-
644
- this.#device.queue.writeBuffer(this.#jobs_buffer, 0, view.buffer, 0, count * stride);
645
-
646
- const settings = this.#settings;
647
-
648
- settings.job_count = count;
649
- settings.cluster_capacity = this.#cluster_capacity;
650
- settings.vertex_offset = frame.vertex_offset;
651
- settings.index_offset = frame.index_offset;
652
-
653
- // this frame's slot for the cut, and its params: a τ of 1, since its records carry the error's own scale
654
- const slot = runtime.slot_of(this.#cut);
655
-
656
- this.#cut.write_params({ runtime, slot, tau: 1 });
657
- this.#stats_base = vgeo_feedback_slot_stats(slot);
658
-
659
- // ----- the records: a job's geometry whole, then their claim widths scanned -----
660
-
661
- const collect_data = this.#collect_data;
662
- const collect = graph.add("VGeo SDF Collect", collect_data, this.#execute_collect);
152
+ this.#asked.delete(record.index);
663
153
 
664
- collect_data.instances = collect.create("vgeo sdf instances", BufferResourceDescriptor.from(
665
- count * VGEO_INSTANCE_WORDS * 4, GPUBufferUsage.STORAGE
666
- ));
667
- collect_data.claim_widths = collect.create("vgeo sdf claim widths", BufferResourceDescriptor.from(
668
- 16 + count * 4, GPUBufferUsage.STORAGE, 0, 16
669
- ));
154
+ this.#geometries.add(proxy);
155
+ this.#applied.set(record.index, new VGeoProxyEntry(record, proxy));
670
156
 
671
- const prefix_sums = graph_prefix_scan_csdldf({
672
- graph,
673
- input: collect_data.claim_widths,
674
- output: graph_create_buffer(graph, "vgeo sdf claim offsets", prefix_scan_csdldf_output_descriptor(count)),
675
- element_max_count: count,
676
- });
677
-
678
- // ----- the cut: the camera cut's own descent, its clusters onto the list -----
679
-
680
- const counters = graph_create_buffer(graph, "vgeo sdf counters", BufferResourceDescriptor.empty(
681
- count * VGEO_SDF_COUNTER_WORDS * 4, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
682
- ));
683
- const clusters = graph_create_buffer(graph, "vgeo sdf clusters", BufferResourceDescriptor.from(
684
- 16 + this.#cluster_capacity * VGEO_SDF_CLUSTER_WORDS * 4, GPUBufferUsage.STORAGE, 0, 16
685
- ));
686
-
687
- const descent = graph_vgeo_cut_descent({
688
- graph,
689
- runtime,
690
- cut: this.#cut,
691
- instances: collect_data.instances,
692
- prefix_sums,
693
- feedback: runtime.feedback_in(graph),
694
- traverse: shader_vgeo_sdf_traverse,
695
- bindings: { settings },
696
- outputs: { vgeo_sdf_counters: counters, vgeo_sdf_clusters: clusters },
697
- });
698
-
699
- runtime.publish_feedback(graph, descent.feedback);
700
-
701
- // ----- the write-out: a workgroup a listed cluster, every job's sizes and the ready jobs' meshes -----
702
-
703
- const command = graph_build_indirect_dispatch_command(graph, descent.outputs.vgeo_sdf_clusters, 1, true);
704
-
705
- const decode_data = this.#decode_data;
706
- const decode = graph.add("VGeo SDF Decode", decode_data, this.#execute_decode);
707
-
708
- decode_data.command = decode.read(command);
709
- decode_data.clusters = decode.read(descent.outputs.vgeo_sdf_clusters);
710
- decode_data.counters = decode.write(descent.outputs.vgeo_sdf_counters);
711
- decode_data.payload = decode.create("vgeo sdf payload", BufferResourceDescriptor.from(
712
- payload_bytes, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST
713
- ));
714
-
715
- // ----- the header: the jobs' counters and the cut's own, ahead of the meshes, then read back -----
716
-
717
- const header_data = this.#header_data;
718
- const header = graph.add("VGeo SDF Header", header_data, this.#execute_header);
719
-
720
- header_data.counters = header.read(decode_data.counters);
721
- header_data.feedback = header.read(descent.feedback);
722
- header_data.payload = header.write(decode_data.payload);
723
-
724
- graph_read_buffer({ graph, buffer: header_data.payload, offset: 0, size: payload_bytes })
725
- .then(reference => {
726
- try {
727
- if (!this.#destroyed) {
728
- this.#apply(new Uint32Array(reference.getValue()), frame);
729
- }
730
- } finally {
731
- reference.release();
732
- }
733
- })
734
- .catch(error => {
735
- if (!this.#destroyed) {
736
- console.warn('VGeoSDFProxies: readback failed', error);
737
- this.#lose(frame);
738
- }
739
- })
740
- .finally(() => {
741
- frame.busy = false;
742
- });
157
+ this.#sdf.alias(record, this.#geometries.get_metadata(proxy));
743
158
  }
744
159
 
745
- #execute_collect = (data, resources, execution) => {
746
- const b = this.#collect_bindings;
747
-
748
- b.settings = this.#settings;
749
- b.vgeo_sdf_jobs = this.#jobs_buffer;
750
- b.vg_geometries = this.#runtime.pool.buffer_geometries;
751
- b.instances = resources.get(data.instances);
752
- b.claim_widths = resources.get(data.claim_widths);
753
-
754
- shader_vgeo_sdf_collect.dispatch({
755
- encoder: execution.encoder,
756
- group_count_x: Math.ceil(this.#settings.job_count / VGEO_SDF_COLLECT_WORKGROUP_SIZE),
757
- bindings: b,
758
- });
759
- };
760
-
761
- #execute_decode = (data, resources, execution) => {
762
- const meshlets = this.#geometries.meshlets;
763
- const b = this.#decode_bindings;
764
-
765
- b.settings = this.#settings;
766
- b.vgeo_sdf_jobs = this.#jobs_buffer;
767
- b.vgeo_sdf_counters = resources.get(data.counters);
768
- b.vgeo_sdf_clusters = resources.get(data.clusters);
769
- b.meshlet_metadata = meshlets.buffer_metadata;
770
- b.meshlet_data = meshlets.buffer_data;
771
- b.vgeo_sdf_payload = resources.get(data.payload);
772
-
773
- shader_vgeo_sdf_decode.dispatchIndirect({
774
- encoder: execution.encoder,
775
- command: resources.get(data.command),
776
- command_offset: 0,
777
- bindings: b,
778
- });
779
- };
780
-
781
- #execute_header = (data, resources, execution) => {
782
- const encoder = execution.encoder;
783
- const payload = resources.get(data.payload);
784
- const counter_bytes = 4 * this.#settings.job_count * VGEO_SDF_COUNTER_WORDS;
785
-
786
- encoder.copyBufferToBuffer(resources.get(data.counters), 0, payload, 0, counter_bytes);
787
- encoder.copyBufferToBuffer(resources.get(data.feedback), 4 * this.#stats_base, payload, counter_bytes, 4 * VGEO_CUT_STAT_COUNT);
788
- };
789
-
790
160
  /**
791
- * A recorded frame's readback: each job's cut taken, its mesh too where the frame wrote it out.
792
- *
793
- * @param {Uint32Array} words
794
- * @param {VGeoSDFFrame} frame
161
+ * @param {GPUGeometryMetadata} record
795
162
  */
796
- #apply(words, frame) {
797
- const stats = frame.count * VGEO_SDF_COUNTER_WORDS;
798
-
799
- // a subtree the cut left undescended is missing from every job's list, and no counter of a job says which
800
- const holes = words[stats + VGEO_CUT_STAT.QUEUE_DROPPED] > 0
801
- || words[stats + VGEO_CUT_STAT.QUEUE_REMAINING] > 0
802
- || words[stats + VGEO_CUT_STAT.INSTANCES_DROPPED] > 0;
803
-
804
- let taken = 0;
805
- let dropped = false;
806
-
807
- for (let i = 0; i < frame.count; i++) {
808
- const job = frame.jobs[i];
809
- const c = i * VGEO_SDF_COUNTER_WORDS;
810
-
811
- taken += words[c + VGEO_SDF_COUNTER.TAKEN];
812
- dropped ||= words[c + VGEO_SDF_COUNTER.DROPPED] > 0;
813
-
814
- if (job.generation !== frame.generations[i]) {
815
- // the slot holds another geometry now
816
- continue;
817
- }
818
-
819
- const complete = !holes
820
- && words[c + VGEO_SDF_COUNTER.TAKEN] > 0
821
- && words[c + VGEO_SDF_COUNTER.STAND_INS] === 0
822
- && words[c + VGEO_SDF_COUNTER.DROPPED] === 0;
823
-
824
- if (frame.decoding[i] === 0) {
825
- if (job.phase === JOB_CUT && complete) {
826
- job.vertex_count = words[c + VGEO_SDF_COUNTER.VERTICES];
827
- job.triangle_count = words[c + VGEO_SDF_COUNTER.TRIANGLES];
828
- job.phase = JOB_READY;
829
- }
830
-
831
- continue;
832
- }
833
-
834
- if (job.phase !== JOB_DECODING) {
835
- continue;
836
- }
163
+ #forgotten(record) {
164
+ const id = record.index;
837
165
 
838
- const whole = complete
839
- && words[c + VGEO_SDF_COUNTER.VERTICES] === job.vertex_count
840
- && words[c + VGEO_SDF_COUNTER.TRIANGLES] === job.triangle_count
841
- && words[c + VGEO_SDF_COUNTER.VERTICES_WRITTEN] === job.vertex_count
842
- && words[c + VGEO_SDF_COUNTER.TRIANGLES_WRITTEN] === job.triangle_count;
843
-
844
- if (!whole) {
845
- // cut otherwise than the cut its room was made for
846
- job.phase = JOB_CUT;
847
- continue;
848
- }
849
-
850
- const first_vertex = frame.vertex_offset + 3 * frame.vertex_bases[i];
851
- const first_index = frame.index_offset + 3 * frame.triangle_bases[i];
852
-
853
- job.positions = new Float32Array(words.buffer, words.byteOffset + 4 * first_vertex, 3 * job.vertex_count).slice();
854
- job.indices = words.slice(first_index, first_index + 3 * job.triangle_count);
855
- job.phase = JOB_BUILD;
166
+ if (this.#asked.get(id) === record) {
167
+ this.#asked.delete(id);
856
168
  }
857
169
 
858
- if (dropped) {
859
- while (this.#cluster_capacity < taken) {
860
- this.#cluster_capacity *= 2;
861
- }
170
+ const entry = this.#applied.get(id);
171
+
172
+ if (entry !== undefined && entry.record === record) {
173
+ this.#applied.delete(id);
174
+ this.#geometries.remove(entry.proxy);
862
175
  }
863
176
  }
864
177
 
865
178
  /**
866
- * A frame whose readback was lost: the jobs it wrote out are cut again.
867
- *
868
- * @param {VGeoSDFFrame} frame
179
+ * Hear no more of the caches. The proxies applied stay with the geometry manager, as stand-ins their geometries
180
+ * are aliased to: removing one under its alias would leave its geometry's holders holding a geometry let go.
869
181
  */
870
- #lose(frame) {
871
- for (let i = 0; i < frame.count; i++) {
872
- const job = frame.jobs[i];
873
-
874
- if (frame.decoding[i] === 1 && job.generation === frame.generations[i] && job.phase === JOB_DECODING) {
875
- job.phase = JOB_CUT;
876
- }
877
- }
878
- }
879
-
880
182
  destroy() {
881
183
  if (this.#destroyed) {
882
184
  return;
@@ -886,17 +188,10 @@ export class VGeoSDFProxies {
886
188
 
887
189
  const sdf = this.#sdf;
888
190
 
889
- sdf.remove_source(this);
890
- sdf.onRequested.remove(this.#requested_one, this);
891
- sdf.onBaked.remove(this.#baked, this);
892
-
893
- for (const job of this.#jobs) {
894
- this.#free(job);
895
- }
896
-
897
- this.#requested.length = 0;
191
+ sdf.onRequested.remove(this.#requested, this);
192
+ sdf.onForgotten.remove(this.#forgotten, this);
898
193
 
899
- this.#cut.destroy();
900
- this.#jobs_buffer.destroy();
194
+ this.#applied.clear();
195
+ this.#asked.clear();
901
196
  }
902
197
  }