rayzee 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +139 -8
  2. package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
  3. package/dist/{NeuralSuperRes-CQLCLpcC.js → NeuralSuperRes-CbNvQjCw.js} +3 -2
  4. package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
  5. package/dist/rayzee.es.js +10344 -5334
  6. package/dist/rayzee.umd.js +265 -265
  7. package/package.json +1 -1
  8. package/src/AssetConfig.js +4 -0
  9. package/src/EngineDefaults.js +38 -56
  10. package/src/EngineEvents.js +3 -0
  11. package/src/EngineIssues.js +6 -0
  12. package/src/Headless.js +3 -1
  13. package/src/Passes/AIUpscaler.js +2 -1
  14. package/src/Passes/OIDNDenoiser.js +3 -6
  15. package/src/PathTracerApp.js +290 -19
  16. package/src/Processor/ArchiveCache.js +120 -0
  17. package/src/Processor/ArchiveReader.js +179 -32
  18. package/src/Processor/AssetLoader.js +514 -76
  19. package/src/Processor/AtmosphereModel.js +465 -0
  20. package/src/Processor/BVHBuilder.js +4 -1
  21. package/src/Processor/BVHLeafFold.js +100 -0
  22. package/src/Processor/BVHRefitter.js +98 -214
  23. package/src/Processor/ChunkedRecords.js +189 -4
  24. package/src/Processor/EmissiveTriangleBuilder.js +8 -0
  25. package/src/Processor/GeometryExtractor.js +238 -40
  26. package/src/Processor/InstanceTable.js +55 -37
  27. package/src/Processor/PBRT/PBRTCurves.js +104 -27
  28. package/src/Processor/PBRT/PBRTParser.js +132 -22
  29. package/src/Processor/PBRT/PBRTSceneBuilder.js +192 -49
  30. package/src/Processor/PBRT/buffers.js +27 -0
  31. package/src/Processor/PBRT/index.js +55 -15
  32. package/src/Processor/PackedRayBuffer.js +10 -13
  33. package/src/Processor/ParallelBVHBuilder.js +36 -15
  34. package/src/Processor/PhysicalSky.js +308 -0
  35. package/src/Processor/QueueManager.js +1 -0
  36. package/src/Processor/SceneProcessor.js +1132 -181
  37. package/src/Processor/SplitBLAS.js +249 -0
  38. package/src/Processor/SunPosition.js +41 -0
  39. package/src/Processor/TextureCreator.js +21 -0
  40. package/src/Processor/Workers/AIUpscalerWorker.js +79 -52
  41. package/src/Processor/Workers/BVHWorker.js +17 -13
  42. package/src/Processor/ZipReader.js +207 -0
  43. package/src/RenderSettings.js +38 -0
  44. package/src/SceneState/SceneState.js +367 -0
  45. package/src/SceneState/portable.js +52 -0
  46. package/src/Stages/NormalDepth.js +7 -6
  47. package/src/Stages/PathTracer.js +108 -4
  48. package/src/Stages/PathTracerStage.js +251 -104
  49. package/src/Storage/BLASCache.js +190 -0
  50. package/src/Storage/CDFCache.js +77 -0
  51. package/src/Storage/DownloadCache.js +266 -0
  52. package/src/Storage/GeometrySpill.js +217 -0
  53. package/src/Storage/SceneGraphCodec.js +503 -0
  54. package/src/Storage/SpillStore.js +112 -0
  55. package/src/Storage/StorageManager.js +934 -0
  56. package/src/Storage/StorageOps.js +137 -0
  57. package/src/Storage/StorageWorker.js +37 -0
  58. package/src/Storage/events.js +29 -0
  59. package/src/Storage/identity.js +77 -0
  60. package/src/Storage/index.js +9 -0
  61. package/src/Storage/inlineTransport.js +24 -0
  62. package/src/Storage/locks.js +99 -0
  63. package/src/Storage/openStorage.js +87 -0
  64. package/src/Storage/sceneCachePolicy.js +12 -0
  65. package/src/Storage/shared.js +9 -0
  66. package/src/Storage/transport.js +62 -0
  67. package/src/TSL/Atmosphere.js +684 -0
  68. package/src/TSL/BVHTraversal.js +288 -239
  69. package/src/TSL/Common.js +11 -2
  70. package/src/TSL/Displacement.js +6 -6
  71. package/src/TSL/EmissiveSampling.js +5 -5
  72. package/src/TSL/EnvironmentCDF.js +176 -0
  73. package/src/TSL/GenerateKernel.js +3 -3
  74. package/src/TSL/HitFacet.js +4 -4
  75. package/src/TSL/LightsDirect.js +8 -8
  76. package/src/TSL/LightsSampling.js +55 -37
  77. package/src/TSL/Random.js +1 -0
  78. package/src/TSL/ShadeKernel.js +75 -19
  79. package/src/TSL/Sun.js +40 -0
  80. package/src/TSL/TextureSampling.js +6 -7
  81. package/src/index.js +18 -0
  82. package/src/managers/CameraManager.js +169 -1
  83. package/src/managers/EnvironmentManager.js +263 -33
  84. package/src/managers/GoboManager.js +19 -8
  85. package/src/managers/IESManager.js +3 -3
  86. package/src/managers/LightManager.js +98 -5
  87. package/src/managers/MaterialDataManager.js +22 -2
  88. package/src/managers/TransformManager.js +26 -0
  89. package/src/managers/UniformManager.js +5 -3
  90. package/src/managers/VideoRenderManager.js +3 -1
  91. package/src/managers/timeline/CameraTrack.js +22 -0
  92. package/src/managers/timeline/TimelineManager.js +14 -0
  93. package/src/version.js +3 -0
  94. package/src/Processor/ProceduralSky.js +0 -291
@@ -1,3 +1,4 @@
1
+
1
2
  /**
2
3
  * BVHRefitter — Fast O(N) bottom-up BVH AABB refit for animated geometry.
3
4
  *
@@ -8,6 +9,8 @@
8
9
  * Designed to run in both main thread and Web Worker contexts.
9
10
  */
10
11
 
12
+ import { isFoldedRef, foldedFirst, foldedCount } from './BVHLeafFold.js';
13
+
11
14
  // Inline copy of layout constants (source of truth: EngineDefaults.js).
12
15
  // Cannot import because this runs inside Web Workers where window is not defined.
13
16
  const TRIANGLE_DATA_LAYOUT = {
@@ -161,6 +164,98 @@ function transformBoundsToWorld( bvhData, nodeOff, src, srcOff, dst, dstOff ) {
161
164
  }
162
165
 
163
166
 
167
+ /** Bounds of triangles [first, first + count) into out[ off .. off + 6 ). */
168
+ function triangleBounds( acc, first, count, out, off ) {
169
+
170
+ const A = TRIANGLE_DATA_LAYOUT.POSITION_A_OFFSET;
171
+ const B = TRIANGLE_DATA_LAYOUT.POSITION_B_OFFSET;
172
+ const C = TRIANGLE_DATA_LAYOUT.POSITION_C_OFFSET;
173
+ let minX = Infinity, minY = Infinity, minZ = Infinity;
174
+ let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity;
175
+
176
+ for ( let t = 0; t < count; t ++ ) {
177
+
178
+ const gi = first + t;
179
+ const f = acc.chunked ? acc.f.chunkFor( gi ) : acc.f;
180
+ const o = acc.chunked ? acc.chunked.baseOf( gi ) : gi * FPT;
181
+ minX = Math.min( minX, f[ o + A ], f[ o + B ], f[ o + C ] );
182
+ minY = Math.min( minY, f[ o + A + 1 ], f[ o + B + 1 ], f[ o + C + 1 ] );
183
+ minZ = Math.min( minZ, f[ o + A + 2 ], f[ o + B + 2 ], f[ o + C + 2 ] );
184
+ maxX = Math.max( maxX, f[ o + A ], f[ o + B ], f[ o + C ] );
185
+ maxY = Math.max( maxY, f[ o + A + 1 ], f[ o + B + 1 ], f[ o + C + 1 ] );
186
+ maxZ = Math.max( maxZ, f[ o + A + 2 ], f[ o + B + 2 ], f[ o + C + 2 ] );
187
+
188
+ }
189
+
190
+ out[ off ] = minX; out[ off + 1 ] = minY; out[ off + 2 ] = minZ;
191
+ out[ off + 3 ] = maxX; out[ off + 4 ] = maxY; out[ off + 5 ] = maxZ;
192
+
193
+ }
194
+
195
+ const folded = new Float32Array( 12 );
196
+
197
+ /**
198
+ * One node of a bottom-up refit: its own bounds into `bounds[ ( i - base ) * 6 ]`, and an inner
199
+ * node's two child boxes rewritten from theirs. A leaf folded into its parent takes its box from
200
+ * its triangles.
201
+ */
202
+ function refitNode( nodes, i, acc, bounds, base ) {
203
+
204
+ const bvhF = nodeF( nodes, i );
205
+ const idx = nodeIdx( nodes, i );
206
+ const o = nodeBase( nodes, i );
207
+ const b = ( i - base ) * 6;
208
+ const marker = idx[ o + 3 ];
209
+
210
+ if ( marker === LEAF_MARKER ) {
211
+
212
+ triangleBounds( acc, idx[ o ], idx[ o + 1 ], bounds, b );
213
+ return;
214
+
215
+ }
216
+
217
+ if ( marker === BLAS_POINTER_MARKER ) {
218
+
219
+ // BLAS-pointer leaf (TLAS): the BLAS root's bounds are in the instance's own
220
+ // space, and the TLAS sorts on world bounds — so carry the box out through the
221
+ // leaf's transform. Slots 4..15 are the rows of world-to-object; the eight
222
+ // corners go back the other way through its inverse.
223
+ transformBoundsToWorld( bvhF, o, bounds, ( idx[ o ] - base ) * 6, bounds, b );
224
+ return;
225
+
226
+ }
227
+
228
+ const right = idx[ o + 7 ];
229
+ let ls = bounds, lb = ( marker - base ) * 6;
230
+ if ( isFoldedRef( marker ) ) {
231
+
232
+ triangleBounds( acc, foldedFirst( marker ), foldedCount( marker ), folded, 0 );
233
+ ls = folded; lb = 0;
234
+
235
+ }
236
+
237
+ let rs = bounds, rb = ( right - base ) * 6;
238
+ if ( isFoldedRef( right ) ) {
239
+
240
+ triangleBounds( acc, foldedFirst( right ), foldedCount( right ), folded, 6 );
241
+ rs = folded; rb = 6;
242
+
243
+ }
244
+
245
+ bvhF[ o ] = ls[ lb ]; bvhF[ o + 1 ] = ls[ lb + 1 ]; bvhF[ o + 2 ] = ls[ lb + 2 ];
246
+ bvhF[ o + 4 ] = ls[ lb + 3 ]; bvhF[ o + 5 ] = ls[ lb + 4 ]; bvhF[ o + 6 ] = ls[ lb + 5 ];
247
+ bvhF[ o + 8 ] = rs[ rb ]; bvhF[ o + 9 ] = rs[ rb + 1 ]; bvhF[ o + 10 ] = rs[ rb + 2 ];
248
+ bvhF[ o + 12 ] = rs[ rb + 3 ]; bvhF[ o + 13 ] = rs[ rb + 4 ]; bvhF[ o + 14 ] = rs[ rb + 5 ];
249
+
250
+ for ( let a = 0; a < 3; a ++ ) {
251
+
252
+ bounds[ b + a ] = Math.min( ls[ lb + a ], rs[ rb + a ] );
253
+ bounds[ b + 3 + a ] = Math.max( ls[ lb + 3 + a ], rs[ rb + 3 + a ] );
254
+
255
+ }
256
+
257
+ }
258
+
164
259
  export class BVHRefitter {
165
260
 
166
261
  constructor() {
@@ -254,102 +349,9 @@ export class BVHRefitter {
254
349
 
255
350
  }
256
351
 
257
- const bounds = this._bounds;
258
- const endNode = startNode + nodeCount;
259
352
  const nodes = nodeAccess( bvhData );
260
-
261
- for ( let i = endNode - 1; i >= startNode; i -- ) {
262
-
263
- const bvhF = nodeF( nodes, i );
264
- const idx = nodeIdx( nodes, i );
265
- const o = nodeBase( nodes, i );
266
- const b = ( i - startNode ) * 6; // bounds indexed relative to BLAS start
267
-
268
- if ( idx[ o + 3 ] === LEAF_MARKER ) {
269
-
270
- const triOffset = idx[ o ];
271
- const triCount = idx[ o + 1 ];
272
-
273
- let minX = Infinity, minY = Infinity, minZ = Infinity;
274
- let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity;
275
-
276
- for ( let t = 0; t < triCount; t ++ ) {
277
-
278
- const gi = triOffset + t;
279
- const triFloats = acc.chunked ? acc.f.chunkFor( gi ) : acc.f;
280
- const tOff = acc.chunked ? acc.chunked.baseOf( gi ) : gi * FPT;
281
- const ax = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_A_OFFSET ];
282
- const ay = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_A_OFFSET + 1 ];
283
- const az = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_A_OFFSET + 2 ];
284
- const bx = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_B_OFFSET ];
285
- const by = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_B_OFFSET + 1 ];
286
- const bz = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_B_OFFSET + 2 ];
287
- const cx = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_C_OFFSET ];
288
- const cy = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_C_OFFSET + 1 ];
289
- const cz = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_C_OFFSET + 2 ];
290
-
291
- minX = Math.min( minX, ax, bx, cx );
292
- minY = Math.min( minY, ay, by, cy );
293
- minZ = Math.min( minZ, az, bz, cz );
294
- maxX = Math.max( maxX, ax, bx, cx );
295
- maxY = Math.max( maxY, ay, by, cy );
296
- maxZ = Math.max( maxZ, az, bz, cz );
297
-
298
- }
299
-
300
- bounds[ b ] = minX;
301
- bounds[ b + 1 ] = minY;
302
- bounds[ b + 2 ] = minZ;
303
- bounds[ b + 3 ] = maxX;
304
- bounds[ b + 4 ] = maxY;
305
- bounds[ b + 5 ] = maxZ;
306
-
307
- } else {
308
-
309
- // Inner node — child indices are absolute, but bounds index relative to startNode
310
- const leftIdx = idx[ o + 3 ];
311
- const rightIdx = idx[ o + 7 ];
312
- const lb = ( leftIdx - startNode ) * 6;
313
- const rb = ( rightIdx - startNode ) * 6;
314
-
315
- const lMinX = bounds[ lb ];
316
- const lMinY = bounds[ lb + 1 ];
317
- const lMinZ = bounds[ lb + 2 ];
318
- const lMaxX = bounds[ lb + 3 ];
319
- const lMaxY = bounds[ lb + 4 ];
320
- const lMaxZ = bounds[ lb + 5 ];
321
-
322
- const rMinX = bounds[ rb ];
323
- const rMinY = bounds[ rb + 1 ];
324
- const rMinZ = bounds[ rb + 2 ];
325
- const rMaxX = bounds[ rb + 3 ];
326
- const rMaxY = bounds[ rb + 4 ];
327
- const rMaxZ = bounds[ rb + 5 ];
328
-
329
- bvhF[ o ] = lMinX;
330
- bvhF[ o + 1 ] = lMinY;
331
- bvhF[ o + 2 ] = lMinZ;
332
- bvhF[ o + 4 ] = lMaxX;
333
- bvhF[ o + 5 ] = lMaxY;
334
- bvhF[ o + 6 ] = lMaxZ;
335
-
336
- bvhF[ o + 8 ] = rMinX;
337
- bvhF[ o + 9 ] = rMinY;
338
- bvhF[ o + 10 ] = rMinZ;
339
- bvhF[ o + 12 ] = rMaxX;
340
- bvhF[ o + 13 ] = rMaxY;
341
- bvhF[ o + 14 ] = rMaxZ;
342
-
343
- bounds[ b ] = Math.min( lMinX, rMinX );
344
- bounds[ b + 1 ] = Math.min( lMinY, rMinY );
345
- bounds[ b + 2 ] = Math.min( lMinZ, rMinZ );
346
- bounds[ b + 3 ] = Math.max( lMaxX, rMaxX );
347
- bounds[ b + 4 ] = Math.max( lMaxY, rMaxY );
348
- bounds[ b + 5 ] = Math.max( lMaxZ, rMaxZ );
349
-
350
- }
351
-
352
- }
353
+ // Bounds indexed relative to the BLAS start; child indices are absolute.
354
+ for ( let i = startNode + nodeCount - 1; i >= startNode; i -- ) refitNode( nodes, i, acc, this._bounds, startNode );
353
355
 
354
356
  }
355
357
 
@@ -374,126 +376,8 @@ export class BVHRefitter {
374
376
 
375
377
  }
376
378
 
377
- const bounds = this._bounds;
378
379
  const nodes = nodeAccess( bvhData );
379
-
380
- // Reverse iteration: bottom-up in pre-order layout
381
- for ( let i = nodeCount - 1; i >= 0; i -- ) {
382
-
383
- const bvhF = nodeF( nodes, i );
384
- const idx = nodeIdx( nodes, i );
385
- const o = nodeBase( nodes, i );
386
- const b = i * 6;
387
-
388
- const marker = idx[ o + 3 ];
389
-
390
- if ( marker === LEAF_MARKER ) {
391
-
392
- // Triangle leaf: compute AABB from triangles
393
- const triOffset = idx[ o ];
394
- const triCount = idx[ o + 1 ];
395
-
396
- let minX = Infinity, minY = Infinity, minZ = Infinity;
397
- let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity;
398
-
399
- for ( let t = 0; t < triCount; t ++ ) {
400
-
401
- const gi = triOffset + t;
402
- const triFloats = acc.chunked ? acc.f.chunkFor( gi ) : acc.f;
403
- const tOff = acc.chunked ? acc.chunked.baseOf( gi ) : gi * FPT;
404
-
405
- // Position A
406
- const ax = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_A_OFFSET ];
407
- const ay = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_A_OFFSET + 1 ];
408
- const az = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_A_OFFSET + 2 ];
409
- // Position B
410
- const bx = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_B_OFFSET ];
411
- const by = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_B_OFFSET + 1 ];
412
- const bz = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_B_OFFSET + 2 ];
413
- // Position C
414
- const cx = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_C_OFFSET ];
415
- const cy = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_C_OFFSET + 1 ];
416
- const cz = triFloats[ tOff + TRIANGLE_DATA_LAYOUT.POSITION_C_OFFSET + 2 ];
417
-
418
- minX = Math.min( minX, ax, bx, cx );
419
- minY = Math.min( minY, ay, by, cy );
420
- minZ = Math.min( minZ, az, bz, cz );
421
- maxX = Math.max( maxX, ax, bx, cx );
422
- maxY = Math.max( maxY, ay, by, cy );
423
- maxZ = Math.max( maxZ, az, bz, cz );
424
-
425
- }
426
-
427
- bounds[ b ] = minX;
428
- bounds[ b + 1 ] = minY;
429
- bounds[ b + 2 ] = minZ;
430
- bounds[ b + 3 ] = maxX;
431
- bounds[ b + 4 ] = maxY;
432
- bounds[ b + 5 ] = maxZ;
433
-
434
- } else if ( marker === BLAS_POINTER_MARKER ) {
435
-
436
- // BLAS-pointer leaf (TLAS): the BLAS root's bounds are in the instance's own
437
- // space, and the TLAS sorts on world bounds — so carry the box out through the
438
- // leaf's transform. Slots 4..15 are the rows of world-to-object; the eight
439
- // corners go back the other way through its inverse.
440
- const blasRoot = idx[ o ];
441
- const br = blasRoot * 6;
442
- transformBoundsToWorld( bvhF, o, bounds, br, bounds, b );
443
-
444
- } else {
445
-
446
- // Inner node: union children bounds (already computed since we iterate in reverse)
447
- const leftIdx = idx[ o + 3 ];
448
- const rightIdx = idx[ o + 7 ];
449
- const lb = leftIdx * 6;
450
- const rb = rightIdx * 6;
451
-
452
- const lMinX = bounds[ lb ];
453
- const lMinY = bounds[ lb + 1 ];
454
- const lMinZ = bounds[ lb + 2 ];
455
- const lMaxX = bounds[ lb + 3 ];
456
- const lMaxY = bounds[ lb + 4 ];
457
- const lMaxZ = bounds[ lb + 5 ];
458
-
459
- const rMinX = bounds[ rb ];
460
- const rMinY = bounds[ rb + 1 ];
461
- const rMinZ = bounds[ rb + 2 ];
462
- const rMaxX = bounds[ rb + 3 ];
463
- const rMaxY = bounds[ rb + 4 ];
464
- const rMaxZ = bounds[ rb + 5 ];
465
-
466
- // Write left child AABB into bvhData
467
- bvhF[ o ] = lMinX;
468
- bvhF[ o + 1 ] = lMinY;
469
- bvhF[ o + 2 ] = lMinZ;
470
- // o+3 = leftChildIdx (preserved)
471
- bvhF[ o + 4 ] = lMaxX;
472
- bvhF[ o + 5 ] = lMaxY;
473
- bvhF[ o + 6 ] = lMaxZ;
474
- // o+7 = rightChildIdx (preserved)
475
-
476
- // Write right child AABB into bvhData
477
- bvhF[ o + 8 ] = rMinX;
478
- bvhF[ o + 9 ] = rMinY;
479
- bvhF[ o + 10 ] = rMinZ;
480
- // o+11 = 0 padding
481
- bvhF[ o + 12 ] = rMaxX;
482
- bvhF[ o + 13 ] = rMaxY;
483
- bvhF[ o + 14 ] = rMaxZ;
484
- // o+15 = 0 padding
485
-
486
- // Store this node's bounds as union of children
487
- bounds[ b ] = Math.min( lMinX, rMinX );
488
- bounds[ b + 1 ] = Math.min( lMinY, rMinY );
489
- bounds[ b + 2 ] = Math.min( lMinZ, rMinZ );
490
- bounds[ b + 3 ] = Math.max( lMaxX, rMaxX );
491
- bounds[ b + 4 ] = Math.max( lMaxY, rMaxY );
492
- bounds[ b + 5 ] = Math.max( lMaxZ, rMaxZ );
493
-
494
- }
495
-
496
- }
380
+ for ( let i = nodeCount - 1; i >= 0; i -- ) refitNode( nodes, i, acc, this._bounds, 0 );
497
381
 
498
382
  }
499
383
 
@@ -134,7 +134,7 @@ export class ChunkedRecords {
134
134
  const lanes = Math.min( this.recordsPerChunk, this.recordCount - from ) * this.lanesPerRecord;
135
135
  const chunk = this.chunks[ k ] = allocChunk( this.LaneType, lanes, this.shared );
136
136
 
137
- for ( const v of this._views ) {
137
+ for ( const v of this._liveViews() ) {
138
138
 
139
139
  v.chunks[ k ] = new v.LaneType( chunk.buffer, chunk.byteOffset, chunk.byteLength / v.LaneType.BYTES_PER_ELEMENT );
140
140
 
@@ -147,7 +147,7 @@ export class ChunkedRecords {
147
147
  /** Allocate whatever a lazy fill never touched, so the result behaves like an eager one. */
148
148
  materializeAll() {
149
149
 
150
- for ( let k = 0; k < this.chunks.length; k ++ ) if ( ! this.chunks[ k ] ) this._materialize( k );
150
+ for ( let k = 0; k < this.chunks.length; k ++ ) if ( ! this.chunks[ k ] && ! this._spilled?.has( k ) ) this._materialize( k );
151
151
  return this;
152
152
 
153
153
  }
@@ -177,7 +177,104 @@ export class ChunkedRecords {
177
177
  chunkFor( record ) {
178
178
 
179
179
  const k = ( record / this.recordsPerChunk ) | 0;
180
- return this.chunks[ k ] ?? this._materialize( k );
180
+ const chunk = this.chunks[ k ];
181
+ if ( chunk ) return chunk;
182
+ if ( this._spilled?.has( k ) ) throw new Error( `ChunkedRecords: chunk ${k} is on disk — await ensureResident() before touching record ${record}` );
183
+ return this._materialize( k );
184
+
185
+ }
186
+
187
+ /**
188
+ * Writes every resident chunk to `store` and lets go of it; {@link ensureResident} reads it
189
+ * back. A spilled chunk's accessors throw rather than read zeros. Every other holder of a chunk
190
+ * (a worker, a view) keeps it alive, so spill only what nothing else holds.
191
+ * @param {{write: function(number, ArrayBufferView): Promise<void>, read: function(number, number): Promise<ArrayBuffer>}} store
192
+ * @param {{keep?: function(number): boolean}} [options] - chunks that stay in memory
193
+ * @returns {Promise<number>} bytes let go
194
+ */
195
+ async spill( store, { keep = () => false } = {} ) {
196
+
197
+ if ( this._owner ) throw new Error( 'ChunkedRecords: spill the storage, not a view of it' );
198
+ this._store = store;
199
+ this._spilled ??= new Set();
200
+ let bytes = 0;
201
+
202
+ for ( let k = 0; k < this.chunks.length; k ++ ) {
203
+
204
+ const chunk = this.chunks[ k ];
205
+ if ( ! chunk || keep( k ) ) continue;
206
+ await store.write( k, chunk );
207
+ this.chunks[ k ] = null;
208
+ for ( const v of this._liveViews() ) v.chunks[ k ] = null;
209
+ this._spilled.add( k );
210
+ bytes += chunk.byteLength;
211
+
212
+ }
213
+
214
+ return bytes;
215
+
216
+ }
217
+
218
+ /** Writes one chunk to `store` and lets go of it — a build hands chunks over as it finishes them. */
219
+ async spillChunk( k, store ) {
220
+
221
+ const chunk = this.chunks[ k ];
222
+ if ( ! chunk ) return 0;
223
+ this._store = store;
224
+ this._spilled ??= new Set();
225
+ await store.write( k, chunk );
226
+ this.chunks[ k ] = null;
227
+ for ( const v of this._liveViews() ) v.chunks[ k ] = null;
228
+ this._spilled.add( k );
229
+ return chunk.byteLength;
230
+
231
+ }
232
+
233
+ /** Reads the spilled chunks under records [start, start+count) back into memory. */
234
+ async ensureResident( start = 0, count = this.recordCount ) {
235
+
236
+ if ( ! this._spilled?.size || count <= 0 ) return;
237
+ const first = ( start / this.recordsPerChunk ) | 0;
238
+ const last = ( ( start + count - 1 ) / this.recordsPerChunk ) | 0;
239
+
240
+ for ( let k = first; k <= last; k ++ ) {
241
+
242
+ if ( ! this._spilled.has( k ) ) continue;
243
+ const lanes = Math.min( this.recordsPerChunk, this.recordCount - k * this.recordsPerChunk ) * this.lanesPerRecord;
244
+ const bytes = await this._store.read( k, lanes * this.LaneType.BYTES_PER_ELEMENT );
245
+ const chunk = allocChunk( this.LaneType, lanes, this.shared );
246
+ new Uint8Array( chunk.buffer, chunk.byteOffset, chunk.byteLength ).set( new Uint8Array( bytes ) );
247
+ this.chunks[ k ] = chunk;
248
+ for ( const v of this._liveViews() ) v.chunks[ k ] = new v.LaneType( chunk.buffer, chunk.byteOffset, chunk.byteLength / v.LaneType.BYTES_PER_ELEMENT );
249
+ this._spilled.delete( k );
250
+
251
+ }
252
+
253
+ }
254
+
255
+ /** @private */
256
+ _liveViews() {
257
+
258
+ if ( this._weakViews ) this._weakViews = this._weakViews.filter( ref => ref.deref() );
259
+ return [ ...( this._views ?? [] ), ...( this._weakViews ?? [] ).map( ref => ref.deref() ) ];
260
+
261
+ }
262
+
263
+ /** Whether records [start, start+count) are all in memory. */
264
+ isResident( start = 0, count = this.recordCount ) {
265
+
266
+ if ( ! this._spilled?.size || count <= 0 ) return true;
267
+ const first = ( start / this.recordsPerChunk ) | 0;
268
+ const last = ( ( start + count - 1 ) / this.recordsPerChunk ) | 0;
269
+ for ( let k = first; k <= last; k ++ ) if ( this._spilled.has( k ) ) return false;
270
+ return true;
271
+
272
+ }
273
+
274
+ /** Chunks currently on disk. */
275
+ get spilledChunks() {
276
+
277
+ return this._spilled?.size ?? 0;
181
278
 
182
279
  }
183
280
 
@@ -278,6 +375,87 @@ export class ChunkedRecords {
278
375
 
279
376
  }
280
377
 
378
+ /**
379
+ * A lazy store taking more records without moving the ones it has: new chunks are added and
380
+ * only a short last chunk is reallocated. A build streaming chunks to disk cannot copy the
381
+ * store, since earlier chunks may already be gone.
382
+ */
383
+ grow( recordCount ) {
384
+
385
+ if ( recordCount <= this.recordCount ) return;
386
+ const last = this.chunks.length - 1;
387
+ const lanesFor = ( k, count ) => Math.min( this.recordsPerChunk, count - k * this.recordsPerChunk ) * this.lanesPerRecord;
388
+ const old = this.chunks[ last ];
389
+
390
+ this.recordCount = recordCount;
391
+ for ( const v of this._liveViews() ) v.recordCount = recordCount;
392
+ if ( this.chunks.length * this.recordsPerChunk < recordCount ) {
393
+
394
+ this.chunks.length = Math.ceil( recordCount / this.recordsPerChunk );
395
+ for ( const v of this._liveViews() ) v.chunks.length = this.chunks.length;
396
+
397
+ }
398
+
399
+ if ( old && old.length < lanesFor( last, recordCount ) ) {
400
+
401
+ const chunk = allocChunk( this.LaneType, lanesFor( last, recordCount ), this.shared );
402
+ chunk.set( old );
403
+ this.chunks[ last ] = chunk;
404
+ for ( const v of this._liveViews() ) v.chunks[ last ] = new v.LaneType( chunk.buffer, chunk.byteOffset, chunk.byteLength / v.LaneType.BYTES_PER_ELEMENT );
405
+
406
+ } else if ( ! old && this._spilled?.has( last ) ) {
407
+
408
+ throw new Error( `ChunkedRecords: cannot grow past chunk ${last}, which is on disk` );
409
+
410
+ }
411
+
412
+ }
413
+
414
+ /**
415
+ * Lets go of the chunks lying wholly inside records [start, start+count) without writing them
416
+ * anywhere: their contents are about to be rewritten, and the next access allocates them again.
417
+ * @returns {number} bytes let go
418
+ */
419
+ release( start, count ) {
420
+
421
+ const rpc = this.recordsPerChunk;
422
+ const first = Math.ceil( start / rpc );
423
+ const end = Math.floor( ( start + count ) / rpc );
424
+ let bytes = 0;
425
+ for ( let k = first; k < end; k ++ ) {
426
+
427
+ const chunk = this.chunks[ k ];
428
+ if ( ! chunk ) continue;
429
+ bytes += chunk.byteLength;
430
+ this.chunks[ k ] = undefined;
431
+ for ( const v of this._liveViews() ) v.chunks[ k ] = undefined;
432
+
433
+ }
434
+
435
+ return bytes;
436
+
437
+ }
438
+
439
+ /** {@link trimTo} on this store itself, keeping what it has spilled. */
440
+ trimInPlace( recordCount ) {
441
+
442
+ const needed = Math.max( 1, Math.ceil( recordCount / this.recordsPerChunk ) );
443
+ const lanes = Math.max( 0, ( recordCount - ( needed - 1 ) * this.recordsPerChunk ) * this.lanesPerRecord );
444
+ const stores = [ this, ...this._liveViews() ];
445
+ for ( const s of stores ) {
446
+
447
+ s.chunks.length = needed;
448
+ const last = s.chunks[ needed - 1 ];
449
+ if ( last && last.length > lanes ) s.chunks[ needed - 1 ] = last.subarray( 0, lanes );
450
+ s.recordCount = recordCount;
451
+
452
+ }
453
+
454
+ if ( this._spilled ) for ( const k of this._spilled ) if ( k >= needed ) this._spilled.delete( k );
455
+ return this;
456
+
457
+ }
458
+
281
459
  /** A parallel set of views of another lane type over the same memory (u32 records read as f32). */
282
460
  viewAs( LaneType ) {
283
461
 
@@ -287,14 +465,21 @@ export class ChunkedRecords {
287
465
  this.recordCount, this.lanesPerRecord, this.recordsPerChunk
288
466
  );
289
467
  v.LaneType = LaneType;
468
+ // A view never allocates for itself: a chunk it finds missing is the storage's to create.
469
+ v._owner = this;
290
470
 
291
471
  if ( this._views ) {
292
472
 
293
- v._owner = this;
294
473
  this._views.push( v );
295
474
 
475
+ } else {
476
+
477
+ // Weakly: callers take short-lived views, and a spill must still reach the lasting ones.
478
+ ( this._weakViews ??= [] ).push( new WeakRef( v ) );
479
+
296
480
  }
297
481
 
482
+ v._spilled = this._spilled ??= new Set();
298
483
  return v;
299
484
 
300
485
  }
@@ -90,6 +90,14 @@ export class EmissiveTriangleBuilder {
90
90
 
91
91
  for ( let i = 0; i < triangleCount; i ++ ) {
92
92
 
93
+ // A chunk a build spilled holds no emitters: that is why it could be spilled.
94
+ if ( chunked?.spilledChunks && ! chunked.isResident( i, 1 ) ) {
95
+
96
+ i = ( ( ( i / chunked.recordsPerChunk ) | 0 ) + 1 ) * chunked.recordsPerChunk - 1;
97
+ continue;
98
+
99
+ }
100
+
93
101
  const triU = chunked ? chunked.chunkFor( i ) : flatU;
94
102
  const triFloats = chunked ? chunkedF.chunkFor( i ) : flatF;
95
103
  const baseOffset = chunked ? chunked.baseOf( i ) : i * FLOATS_PER_TRIANGLE;