rayzee 11.2.1 → 11.3.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 (62) hide show
  1. package/README.md +58 -29
  2. package/dist/ArchiveImporter-D735xX-4.js +7860 -0
  3. package/dist/BidirectionalIntegrator-M89c0AlP.js +954 -0
  4. package/dist/{BufferLayout-D_IBo2ci.js → BufferLayout-xPRH9jYD.js} +41 -5
  5. package/dist/{NeuralRetouch-Cr-otSW8.js → NeuralRetouch-DiBH63kz.js} +1 -1
  6. package/dist/{NeuralSuperRes-B7E3_M0t.js → NeuralSuperRes-CZNQP13W.js} +1 -1
  7. package/dist/{PathTracer-D6nbknj5.js → PathTracer-BXCCVrPK.js} +4371 -4612
  8. package/dist/{PhysicalSky-Dq5VSiCQ.js → PhysicalSky-DqmaS0lb.js} +2 -2
  9. package/dist/{RayzeeRenderer-DbjqO6v-.js → RayzeeRenderer-DUSMFhmc.js} +426 -363
  10. package/dist/SolarLimb-bZE6y63S.js +762 -0
  11. package/dist/addons/archives.es.js +1 -1
  12. package/dist/addons/bidirectional.es.js +1 -1
  13. package/dist/addons/physical-sky.es.js +1 -1
  14. package/dist/rayzee-core.es.js +7 -7
  15. package/dist/rayzee.es.js +472 -468
  16. package/dist/rayzee.umd.js +106 -105
  17. package/dist/sceneCachePolicy-BAib5fCp.js +1602 -0
  18. package/package.json +1 -1
  19. package/src/PathTracerApp.js +4 -3
  20. package/src/Processor/ArchiveImporter.js +173 -94
  21. package/src/Processor/ArchiveReader.js +20 -23
  22. package/src/Processor/AssetLoader.js +48 -1
  23. package/src/Processor/BVHRefitter.js +109 -13
  24. package/src/Processor/BufferLayout.js +115 -0
  25. package/src/Processor/ChunkedRecords.js +3 -2
  26. package/src/Processor/EmissiveTriangleBuilder.js +6 -6
  27. package/src/Processor/GeometryExtractor.js +189 -79
  28. package/src/Processor/HostMemory.js +39 -9
  29. package/src/Processor/InstanceTable.js +714 -42
  30. package/src/Processor/PBRT/PBRTCurves.js +224 -6
  31. package/src/Processor/PBRT/PBRTMaterials.js +3 -0
  32. package/src/Processor/PBRT/PBRTSceneBuilder.js +82 -25
  33. package/src/Processor/PackedRayBuffer.js +5 -2
  34. package/src/Processor/SceneProcessor.js +747 -141
  35. package/src/Processor/TLASBuilder.js +32 -17
  36. package/src/Processor/USD/USDLayer.js +1149 -0
  37. package/src/Processor/USD/USDScene.js +1655 -0
  38. package/src/Processor/USD/USDStage.js +682 -0
  39. package/src/Processor/USD/USDText.js +674 -0
  40. package/src/Processor/USD/index.js +138 -0
  41. package/src/Processor/Workers/BVHRefitWorker.js +3 -3
  42. package/src/RayzeeRenderer.js +167 -26
  43. package/src/Stages/NormalDepth.js +7 -12
  44. package/src/Stages/PathTracer.js +12 -17
  45. package/src/Stages/PathTracerStage.js +273 -96
  46. package/src/Storage/GeometrySpill.js +46 -14
  47. package/src/Storage/SceneGraphCodec.js +17 -3
  48. package/src/Storage/SpillStore.js +9 -0
  49. package/src/TSL/BVHTraversal.js +183 -7
  50. package/src/TSL/Common.js +57 -13
  51. package/src/TSL/ShadeKernel.js +3 -2
  52. package/src/TSL/TextureSampling.js +11 -8
  53. package/src/TSL/patches.js +84 -0
  54. package/src/integrators/BidirectionalIntegrator.js +20 -2
  55. package/src/managers/DenoisingManager.js +4 -4
  56. package/src/managers/InteractionManager.js +19 -6
  57. package/src/managers/MaterialDataManager.js +22 -5
  58. package/src/managers/UniformManager.js +3 -0
  59. package/dist/ArchiveImporter-D_x863yu.js +0 -5198
  60. package/dist/BidirectionalIntegrator-DmkxxEOR.js +0 -946
  61. package/dist/SolarLimb-CPaeZVwz.js +0 -721
  62. package/dist/sceneCachePolicy-C4-4qwt6.js +0 -485
@@ -0,0 +1,138 @@
1
+ /**
2
+ * USD scene loader for multi-file scenes in a folder or archive: layers are read only as composition reaches them, and
3
+ * the stage is built by the pbrt importer's builder, so instancing, budgets and curve tessellation are shared.
4
+ *
5
+ * const files = new USDFiles( paths, read );
6
+ * const { group, environment, cameras } = await loadUSDScene( { files, entryPath, resolveImage, resolveEnvironment } );
7
+ */
8
+
9
+ import { PBRTSceneBuilder } from '../PBRT/PBRTSceneBuilder.js';
10
+ import { USDStage } from './USDStage.js';
11
+ import { USDSceneReader } from './USDScene.js';
12
+
13
+ export { USDFiles, USDStage } from './USDStage.js';
14
+
15
+ export const USD_LAYER = /\.(usd|usda|usdc)$/i;
16
+
17
+ /** USD layers that could be a scene's root, best first: the shallowest, then the shortest name. */
18
+ export function listUSDRootLayers( paths ) {
19
+
20
+ const layers = paths.filter( p => USD_LAYER.test( p ) && ! p.split( '/' ).pop().startsWith( '._' ) );
21
+ if ( layers.length === 0 ) return [];
22
+ const depth = p => p.split( '/' ).length;
23
+ const top = Math.min( ...layers.map( depth ) );
24
+ return layers.filter( p => depth( p ) === top ).sort( ( a, b ) => {
25
+
26
+ const na = a.split( '/' ).pop(), nb = b.split( '/' ).pop();
27
+ return na.length - nb.length || ( na < nb ? - 1 : na > nb ? 1 : 0 );
28
+
29
+ } );
30
+
31
+ }
32
+
33
+ /** The parts a large scene loads by: prims under the root's top prims that bring in sibling directories (Moana's elements/). */
34
+ export async function listUSDParts( stage, listing ) {
35
+
36
+ const parts = new Map();
37
+ const root = stage.pseudoRoot;
38
+ const rootDir = stage.root.id.includes( '/' ) ? stage.root.id.slice( 0, stage.root.id.lastIndexOf( '/' ) ) : '';
39
+ for ( const top of root.childNames() ) {
40
+
41
+ for ( const [ , spec, layer ] of ( await stage.child( root, top ) )?.sites ?? [] ) {
42
+
43
+ for ( const [ name, child ] of spec.children ?? [] ) {
44
+
45
+ for ( const key of [ 'references', 'payload' ] ) {
46
+
47
+ const op = child.arcs?.[ key ];
48
+ for ( const ref of [ ...( op?.explicit ?? [] ), ...( op?.prepend ?? [] ), ...( op?.append ?? [] ), ...( op?.add ?? [] ) ] ) {
49
+
50
+ const file = stage.files.resolve( ref.asset, layer.path );
51
+ if ( ! file?.includes( '/' ) ) continue;
52
+ const prefix = file.slice( 0, file.lastIndexOf( '/' ) );
53
+ if ( prefix === rootDir || ! prefix.startsWith( rootDir ) ) continue;
54
+ if ( ! parts.has( prefix ) ) parts.set( prefix, { name, prefix, files: 0, bytes: 0, scenes: 1 } );
55
+
56
+ }
57
+
58
+ }
59
+
60
+ }
61
+
62
+ }
63
+
64
+ }
65
+
66
+ // A library referenced on its own beside the parts is not one.
67
+ const byParent = new Map();
68
+ for ( const part of parts.values() ) {
69
+
70
+ const parent = part.prefix.slice( 0, part.prefix.lastIndexOf( '/' ) );
71
+ if ( ! byParent.has( parent ) ) byParent.set( parent, [] );
72
+ byParent.get( parent ).push( part );
73
+
74
+ }
75
+
76
+ const siblings = [ ...byParent.values() ].sort( ( a, b ) => b.length - a.length )[ 0 ] ?? [];
77
+ for ( const part of siblings ) {
78
+
79
+ for ( const entry of listing ) {
80
+
81
+ if ( ! entry.path.startsWith( `${part.prefix}/` ) || ! USD_LAYER.test( entry.path ) ) continue;
82
+ part.files ++;
83
+ part.bytes += entry.size;
84
+
85
+ }
86
+
87
+ }
88
+
89
+ return siblings.sort( ( a, b ) => a.bytes - b.bytes );
90
+
91
+ }
92
+
93
+ export async function loadUSDScene( args ) {
94
+
95
+ const warnings = [];
96
+ const warn = message => warnings.push( message );
97
+ const builder = new PBRTSceneBuilder( {
98
+ maxTriangles: args.maxTriangles, maxPlacements: args.maxPlacements, mergeShapesAbove: args.mergeShapesAbove,
99
+ curveSteps: args.curveSteps, curveSides: args.curveSides, curveTolerance: args.curveTolerance,
100
+ } );
101
+
102
+ const parseStart = performance.now();
103
+ const stage = new USDStage( args.files, { warn } );
104
+ await stage.open( args.entryPath );
105
+ const reader = new USDSceneReader( stage, {
106
+ maxTriangles: builder.maxTriangles, maxPlacements: builder.maxPlacements, curveSteps: builder.curveSteps,
107
+ resolveImage: args.resolveImage, warn,
108
+ } );
109
+ const read = await reader.read();
110
+ if ( stage.omittedArcs > 0 ) warn( `${stage.omittedArcs} reference(s) into the parts left out of this load skipped` );
111
+ const parseMs = performance.now() - parseStart;
112
+
113
+ const buildStart = performance.now();
114
+ const built = await builder.build( read.ir );
115
+ for ( const object of [ ...read.cameras, ...read.lights ] ) built.group.add( object );
116
+
117
+ let environment = null;
118
+ if ( read.dome ) {
119
+
120
+ const texture = await args.resolveEnvironment( read.dome.file );
121
+ if ( texture ) environment = { texture, intensity: read.dome.intensity, rotation: read.dome.rotation };
122
+ else warn( `USD dome light texture "${read.dome.file}" could not be read` );
123
+
124
+ }
125
+
126
+ return {
127
+ ...built,
128
+ environment,
129
+ cameras: read.cameras,
130
+ lights: read.lights,
131
+ cost: read.cost,
132
+ fitNote: reader.fitNote,
133
+ warnings: [ ...warnings, ...built.warnings ],
134
+ parseMs,
135
+ buildMs: performance.now() - buildStart,
136
+ };
137
+
138
+ }
@@ -46,9 +46,9 @@ self.onmessage = function ( e ) {
46
46
 
47
47
  const startTime = performance.now();
48
48
 
49
- const { blasRanges, tlasNodeCount } = e.data;
50
- if ( blasRanges ) refitter.refitPartial( bvhData, triData, blasRanges, tlasNodeCount );
51
- else refitter.refit( bvhData, triData, nodeCount );
49
+ const { blasRanges, tlasNodeCount, groupRange, treeNodeCount, recordNodeStart = - 1 } = e.data;
50
+ if ( blasRanges ) refitter.refitPartial( bvhData, triData, blasRanges, tlasNodeCount, groupRange, recordNodeStart );
51
+ else refitter.refit( bvhData, triData, treeNodeCount ?? nodeCount, recordNodeStart );
52
52
 
53
53
  self.postMessage( {
54
54
  type: 'refitComplete',
@@ -11,6 +11,7 @@ import { RenderPipeline } from './Pipeline/RenderPipeline.js';
11
11
  import { CompletionTracker } from './Pipeline/CompletionTracker.js';
12
12
  import { ENGINE_DEFAULTS as DEFAULT_STATE } from './EngineDefaults.js';
13
13
  import { TRIANGLE_DATA_LAYOUT, BVH_LEAF_MARKERS } from './Processor/BufferLayout.js';
14
+ import { usesTextureCoordinates } from './Processor/GeometryExtractor.js';
14
15
  import { MAX_STORAGE_TEXTURE_SIZE, MAX_RESERVABLE_RENDER_SIZE, setReservedRenderSize } from './Processor/StorageTexturePool.js';
15
16
  import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples, disposeObjectFromMemory, disposeRenderer } from './Processor/utils.js';
16
17
  import { BuildTimer } from './Processor/BuildTimer.js';
@@ -28,9 +29,9 @@ import { PackedToneMapper } from './Processor/ToneMapGPU.js';
28
29
  import { BasicColor } from './Color/BasicColor.js';
29
30
  import { setActiveColorManagement } from './Color/ActiveColor.js';
30
31
  import { getViewTransform, DEFAULT_VIEW } from './Color/ViewTransforms.js';
31
- import { AssetLoader } from './Processor/AssetLoader.js';
32
+ import { AssetLoader, limitEnvironmentWidth } from './Processor/AssetLoader.js';
32
33
  import { SceneProcessor } from './Processor/SceneProcessor.js';
33
- import { deviceMemoryGB, SPILL_TRIANGLE_BUDGET, SPILL_PLACEMENT_BUDGET } from './Processor/HostMemory.js';
34
+ import { deviceMemoryGB, SPILL_TRIANGLE_BUDGET, SPILL_PLACEMENT_BUDGET, SPILL_ENVIRONMENT_WIDTH } from './Processor/HostMemory.js';
34
35
  import { createHeadlessCanvas } from './HeadlessCanvas.js';
35
36
 
36
37
  // Managers
@@ -52,6 +53,8 @@ const partOf = ( { element, pbrtEntry } = {} ) => ( {
52
53
  // One app per canvas — auto-dispose a prior owner if the caller double-
53
54
  // instantiates (StrictMode, HMR, etc.) so its rAF loop can't burn CPU.
54
55
  const _appsByCanvas = new WeakMap();
56
+ // Quiet time after an edit before memory spill puts back on disk what the edit read back.
57
+ const RESPILL_AFTER_MS = 30_000;
55
58
 
56
59
  const KNOWN_EVENTS = new Set( [ ...Object.values( EngineEvents ), ...Object.values( LEGACY_EVENT_NAMES ) ] );
57
60
  const _warnedEvents = new Set();
@@ -165,9 +168,11 @@ export class RayzeeRenderer extends EventDispatcher {
165
168
  * @param {false|'auto'|Object} [options.storage] - on-disk storage; defaults to
166
169
  * `configureAssets( { storage } )`, or off under `strict` unless that was set. 'auto' opens it through
167
170
  * {@link setStorageOpener} (rayzee/addons/storage); a host-supplied StorageManager stays the host's to dispose.
168
- * @param {boolean} [options.memorySpill=false] - experimental: once a large static scene is
169
- * on the GPU, move its triangle records and BLAS nodes to disk (see
170
- * {@link ensureSceneResident}). Needs storage; skipped for animated scenes.
171
+ * @param {'auto'|boolean} [options.memorySpill='auto'] - build a static scene through disk: its triangle records,
172
+ * BLAS nodes and three.js geometry leave memory as they are built (see {@link ensureSceneResident}), scene archives
173
+ * load past the default budgets, and an environment wider than SPILL_ENVIRONMENT_WIDTH is box-filtered down.
174
+ * 'auto' does so only for a scene whose in-memory estimate passes SAFE_SCENE_BYTES; true for every scene, false
175
+ * never. Needs storage; skipped for animated scenes. See {@link setMemorySpill}.
171
176
  *
172
177
  * The engine dispatches `EngineEvents.FRAME` after each animate() iteration so hosts can
173
178
  * tick external instrumentation (e.g. a stats panel) without coupling the engine to it.
@@ -198,7 +203,8 @@ export class RayzeeRenderer extends EventDispatcher {
198
203
  this._hostMemoryGB = options.hostMemoryGB;
199
204
  this._storageOption = options.storage;
200
205
  this._storageOpener = null;
201
- this._memorySpill = options.memorySpill === true;
206
+ this._memorySpill = options.memorySpill === true || options.memorySpill === false ? options.memorySpill : 'auto';
207
+ this._spillPlan = null;
202
208
  /** @type {?Object} on-disk storage (a StorageManager), null when off or unavailable */
203
209
  this.storage = null;
204
210
  this._storageRelease = null;
@@ -708,6 +714,8 @@ export class RayzeeRenderer extends EventDispatcher {
708
714
  this.stopAnimation();
709
715
  clearTimeout( this._resizeDebounceTimer );
710
716
  this._resizeDebounceTimer = null;
717
+ clearTimeout( this._respillTimer );
718
+ this._respillTimer = null;
711
719
 
712
720
  this._removeTrackedListeners();
713
721
  setStatusCallback( null );
@@ -1175,6 +1183,8 @@ export class RayzeeRenderer extends EventDispatcher {
1175
1183
  if ( this._loadingInProgress ) throw this._busyError( 'PathTracerApp' );
1176
1184
 
1177
1185
  this._loadingInProgress = true;
1186
+ // The previous scene's sky width must not narrow this one's while it loads.
1187
+ if ( this.assetLoader ) this.assetLoader.maxEnvironmentWidth = this._memorySpill === true ? SPILL_ENVIRONMENT_WIDTH : 0;
1178
1188
 
1179
1189
  try {
1180
1190
 
@@ -1189,6 +1199,8 @@ export class RayzeeRenderer extends EventDispatcher {
1189
1199
  // (The old primary was already released by releaseTargetModel() in loadFn.)
1190
1200
  this._clearAppendedModels();
1191
1201
  this._modelReplaced();
1202
+ // Before the scene's own environment starts loading: a spilling scene's is narrowed as it decodes.
1203
+ this._planSpill();
1192
1204
  await this.loadSceneData( { pendingEnvironment: this._beginSceneMetadataEnvironment() } );
1193
1205
  this._maybeSpill();
1194
1206
  this.pipeline?.eventBus.emit( 'pipeline:lightingChanged' );
@@ -1215,6 +1227,7 @@ export class RayzeeRenderer extends EventDispatcher {
1215
1227
  } finally {
1216
1228
 
1217
1229
  this._loadingInProgress = false;
1230
+ this._spillPlan = null;
1218
1231
 
1219
1232
  }
1220
1233
 
@@ -1445,11 +1458,22 @@ export class RayzeeRenderer extends EventDispatcher {
1445
1458
  updateLoading( { status: "Transferring data to GPU...", progress: 86 } );
1446
1459
  await new Promise( r => setTimeout( r, 0 ) );
1447
1460
  timer.start( 'GPU data transfer' );
1461
+ await this._sdf.uploadChunks( this._chunkUploader() );
1448
1462
 
1449
1463
  // Re-read rather than reusing the snapshot above: a pendingEnvironment may have landed
1450
1464
  // during the BVH build, and the snapshot is then a disposed texture.
1451
1465
  if ( ! this._sdf.uploadToPathTracer( this.stages.pathTracer, this.lightManager, this.meshScene, hdri(), { keepUserLights } ) ) return false;
1452
1466
 
1467
+ // A bucket at a time, each awaited (PathTracerStage#drainUploads): left to the first frame, three.js sends every
1468
+ // layer at once, and Chrome keeps a pool that size (724 MB for 24155522.glb).
1469
+ for ( const texture of [ ...( this._sdf.srgbBucketTextures ?? [] ), ...( this._sdf.linearBucketTextures ?? [] ) ] ) {
1470
+
1471
+ if ( ! texture ) continue;
1472
+ this.renderer.initTexture( texture );
1473
+ await this.stages.pathTracer.drainUploads();
1474
+
1475
+ }
1476
+
1453
1477
  // Patch per-mesh visibility into the TLAS leaves we just uploaded
1454
1478
  this.stages.pathTracer._meshRefs = this.stages.pathTracer._collectMeshRefs( this.meshScene );
1455
1479
  this.stages.pathTracer.setMeshVisibilityData( this.stages.pathTracer._meshRefs );
@@ -1724,43 +1748,124 @@ export class RayzeeRenderer extends EventDispatcher {
1724
1748
  // Dynamic cameras (add / remove)
1725
1749
  // ═══════════════════════════════════════════════════════════════
1726
1750
 
1727
- /** Triangle and placement budgets for a scene archive: raised when the build can spill. @private */
1751
+ /** How scenes use the disk: 'auto', true or false (see the constructor's `memorySpill`). */
1752
+ get memorySpill() {
1753
+
1754
+ return this._memorySpill;
1755
+
1756
+ }
1757
+
1758
+ /** Sets {@link memorySpill}; the next load or rebuild follows it. */
1759
+ setMemorySpill( mode ) {
1760
+
1761
+ this._memorySpill = mode === true || mode === false ? mode : 'auto';
1762
+
1763
+ }
1764
+
1765
+ /** Whether the current scene was built through disk. */
1766
+ get sceneSpilled() {
1767
+
1768
+ return !! this._sdf?.spilling;
1769
+
1770
+ }
1771
+
1772
+ /** Whether the model's three.js geometry is on disk, so picking skips it until {@link ensureSceneResident}. */
1773
+ get geometryOnDisk() {
1774
+
1775
+ return this._sdf?.geometryOnDisk === true;
1776
+
1777
+ }
1778
+
1779
+ /**
1780
+ * Triangle and placement budgets for a scene archive: raised whenever a build could spill, since the preflight
1781
+ * refuses what cannot fit either way. @private
1782
+ */
1728
1783
  _sceneBudgets( options ) {
1729
1784
 
1730
- if ( ! this._memorySpill || ! this.storage ) return options;
1785
+ if ( this._memorySpill === false || ! this.storage ) return options;
1731
1786
  return { maxTriangles: SPILL_TRIANGLE_BUDGET, maxPlacements: SPILL_PLACEMENT_BUDGET, ...options };
1732
1787
 
1733
1788
  }
1734
1789
 
1735
1790
  /**
1736
- * Experimental memory spill, during the build: static scenes only, since a deforming clip
1737
- * refits every frame. @private
1791
+ * Decides whether the scene about to be built spills: static scenes only, since a deforming clip refits every
1792
+ * frame. Held for the build that follows ({@link _progressiveSpill}). @private
1738
1793
  */
1794
+ _planSpill() {
1795
+
1796
+ const offered = this._memorySpill !== false && !! this.storage && ( this.assetLoader?.animations?.length ?? 0 ) === 0;
1797
+ const spill = offered && ( this._memorySpill === true || this._sdf.needsSpill( this.meshScene ) );
1798
+ if ( this.assetLoader ) this.assetLoader.maxEnvironmentWidth = spill ? SPILL_ENVIRONMENT_WIDTH : 0;
1799
+ // An importer installs its own sky at the end of its read, before this.
1800
+ const sky = spill ? this.meshScene?.environment : null;
1801
+ const image = sky?.image;
1802
+ if ( image ) limitEnvironmentWidth( sky, SPILL_ENVIRONMENT_WIDTH );
1803
+ if ( sky && sky.image !== image ) {
1804
+
1805
+ // three.js keeps an uploaded texture's size, so the old one goes.
1806
+ sky.dispose();
1807
+ sky.needsUpdate = true;
1808
+
1809
+ }
1810
+
1811
+ if ( spill && this._memorySpill === 'auto' ) log.info( 'scene is past the in-memory line: building it through disk' );
1812
+ this._spillPlan = spill;
1813
+ return spill;
1814
+
1815
+ }
1816
+
1817
+ /** @private */
1739
1818
  _progressiveSpill() {
1740
1819
 
1741
- if ( ! this._memorySpill || ! this.storage || ( this.assetLoader?.animations?.length ?? 0 ) > 0 ) return null;
1820
+ const spill = this._spillPlan ?? this._planSpill();
1821
+ this._spillPlan = null;
1822
+ return spill ? { storage: this.storage, uploader: this._chunkUploader() } : null;
1823
+
1824
+ }
1825
+
1826
+ /** @private */
1827
+ _chunkUploader() {
1828
+
1742
1829
  const stage = this.stages.pathTracer;
1743
- return { storage: this.storage, uploader: ( records, kind ) => kind === 'triangles' ? stage.createTriangleChunkUploader( records ) : stage.createChunkUploader( records ) };
1830
+ return ( records, kind, options ) => kind === 'triangles' ? stage.createTriangleChunkUploader( records, options ) : stage.createChunkUploader( records );
1744
1831
 
1745
1832
  }
1746
1833
 
1747
- /** Experimental memory spill for a static scene, in the background. @private */
1834
+ /** The rest of a spilling scene's spill, once it is on the GPU, in the background. @private */
1748
1835
  _maybeSpill() {
1749
1836
 
1750
- if ( ! this._memorySpill || ! this.storage || ( this.assetLoader?.animations?.length ?? 0 ) > 0 ) return;
1751
- this._sdf?.spillToDisk( this.storage )
1837
+ if ( ! this._sdf?.spilling || ! this.storage ) return;
1838
+ this._sdf.spillAfterLoad().catch( error => this._issues.warn( ISSUE_CODES.STORAGE_WRITE_FAILED, `memory spill failed: ${error.message}` ) );
1839
+ this._sdf.spillToDisk( this.storage )
1752
1840
  .then( result => result && this.dispatchEvent( { type: EngineEvents.SCENE_SPILLED, ...result } ) )
1753
1841
  .catch( error => this._issues.warn( ISSUE_CODES.STORAGE_WRITE_FAILED, `memory spill failed: ${error.message}` ) );
1754
1842
 
1755
1843
  }
1756
1844
 
1845
+ /** Once edits have been quiet for {@link RESPILL_AFTER_MS}, puts back on disk what they read back. @private */
1846
+ _scheduleRespill() {
1847
+
1848
+ if ( ! this._sdf?.spilling ) return;
1849
+ clearTimeout( this._respillTimer );
1850
+ this._respillTimer = setTimeout( () => {
1851
+
1852
+ this._respillTimer = null;
1853
+ this._sdf?.respill().catch( error => this._issues.warn( ISSUE_CODES.STORAGE_WRITE_FAILED, `memory spill failed: ${error.message}` ) );
1854
+
1855
+ }, RESPILL_AFTER_MS );
1856
+ this._respillTimer.unref?.();
1857
+
1858
+ }
1859
+
1757
1860
  /**
1758
- * Reads back what {@link options.memorySpill} moved to disk. The engine does this itself before
1759
- * its own readers; a host calling {@link refitBLASes} on a spilled scene awaits it first.
1861
+ * Reads back what {@link options.memorySpill} moved to disk: triangle records, BLAS nodes and the three.js geometry
1862
+ * (whose arrays are empty until then). The engine does this itself before its own readers; a host calling
1863
+ * {@link refitBLASes}, or reading the model's geometry, on a spilled scene awaits it first.
1760
1864
  */
1761
1865
  async ensureSceneResident() {
1762
1866
 
1763
1867
  if ( this._sdf?.spilled ) await this._sdf.ensureResident();
1868
+ await this._sdf?.ensureGeometryResident();
1764
1869
 
1765
1870
  }
1766
1871
 
@@ -1885,15 +1990,26 @@ export class RayzeeRenderer extends EventDispatcher {
1885
1990
  * drags along any other object sharing the same geometry.
1886
1991
  *
1887
1992
  * @param {number[]} meshIndices - indices into {@link sceneMeshes}
1888
- * @returns {{ refitTimeMs: number, placements: number }}
1993
+ * @returns {?{ refitTimeMs: number, placements: number }} null when the move waits for a spilled scene's data
1889
1994
  */
1890
1995
  updateMeshTransforms( meshIndices ) {
1891
1996
 
1997
+ // A spilled scene's matrix lists, TLAS or copy records are on disk: the move lands once they are back.
1998
+ if ( this._sdf?.needsPageInForMove( meshIndices ) ) {
1999
+
2000
+ this._sdf.ensureMovable( meshIndices )
2001
+ .then( () => this.updateMeshTransforms( meshIndices ) )
2002
+ .catch( error => this._issues.warn( ISSUE_CODES.STORAGE_READ_FAILED, `reading instance matrices back failed: ${error.message}` ) );
2003
+ return null;
2004
+
2005
+ }
2006
+
1892
2007
  this._notePlacementsMoving( meshIndices );
1893
2008
  const result = this._sdf.updateMeshTransforms( meshIndices );
1894
2009
 
1895
- this.stages.pathTracer.updateBufferRanges( [], [ this._sdf.computeTLASDirtyRange() ] );
2010
+ this.stages.pathTracer.updateBufferRanges( [], this._sdf.takeMoveRanges() );
1896
2011
  this.reset( false, { motion: true } );
2012
+ this._scheduleRespill();
1897
2013
 
1898
2014
  return result;
1899
2015
 
@@ -3198,9 +3314,7 @@ export class RayzeeRenderer extends EventDispatcher {
3198
3314
  */
3199
3315
  setMeshVisibility( meshIndex, visible ) {
3200
3316
 
3201
- this.stages.pathTracer?.updateMeshVisibility( meshIndex, visible );
3202
- this._refreshEmissiveForVisibility();
3203
- this.reset();
3317
+ this._whenTLASEditable( () => this.stages.pathTracer?.updateMeshVisibility( meshIndex, visible ) );
3204
3318
 
3205
3319
  }
3206
3320
 
@@ -3210,9 +3324,25 @@ export class RayzeeRenderer extends EventDispatcher {
3210
3324
  */
3211
3325
  updateAllMeshVisibility() {
3212
3326
 
3213
- this.stages.pathTracer?.updateAllMeshVisibility();
3214
- this._refreshEmissiveForVisibility();
3215
- this.reset();
3327
+ this._whenTLASEditable( () => this.stages.pathTracer?.updateAllMeshVisibility() );
3328
+
3329
+ }
3330
+
3331
+ /** Applies a visibility edit, then resets — once a spilled TLAS is back in memory. @private */
3332
+ _whenTLASEditable( edit ) {
3333
+
3334
+ const apply = () => {
3335
+
3336
+ edit();
3337
+ this._refreshEmissiveForVisibility();
3338
+ this.reset();
3339
+ this._scheduleRespill();
3340
+
3341
+ };
3342
+
3343
+ const ready = this._sdf?.whenTLASEditable();
3344
+ if ( ! ready ) return apply();
3345
+ ready.then( apply ).catch( error => this._issues.warn( ISSUE_CODES.STORAGE_READ_FAILED, `reading the TLAS back failed: ${error.message}` ) );
3216
3346
 
3217
3347
  }
3218
3348
 
@@ -3472,7 +3602,18 @@ export class RayzeeRenderer extends EventDispatcher {
3472
3602
  async rebuildMaterials( scene ) {
3473
3603
 
3474
3604
  await this.ensureSceneResident();
3475
- await this.stages.pathTracer?.rebuildMaterials( scene || this.meshScene );
3605
+ const stage = this.stages.pathTracer;
3606
+ await stage?.rebuildMaterials( scene || this.meshScene );
3607
+
3608
+ // Loaded without texture coordinates, and a material has a texture now: they go up again from the records.
3609
+ if ( stage?.triangleStorageNode?.withoutUV && this._sdf?.triangles && usesTextureCoordinates( scene || this.meshScene ) ) {
3610
+
3611
+ this._sdf.textureCoordinates = true;
3612
+ stage.setTriangleData( this._sdf.triangles, this._sdf.triangleCount, { textureCoordinates: true } );
3613
+ stage.setupMaterial();
3614
+
3615
+ }
3616
+
3476
3617
  this.reset();
3477
3618
 
3478
3619
  }
@@ -169,21 +169,16 @@ export class NormalDepth extends RenderStage {
169
169
 
170
170
  }
171
171
 
172
- if ( pt.triangleGeoAttr && ! this._triStorageNode ) {
172
+ if ( ! this._triStorageNode || ! this._bvhStorageNode ) {
173
173
 
174
- // uvec4, matching PathTracerStage: packed lanes must keep their exact bit pattern.
175
- this._triStorageNode = {
176
- geo: storage( pt.triangleGeoAttr, 'uvec4', pt.triangleGeoAttr.count ).toReadOnly(),
177
- shade: storage( pt.triangleShadeAttr, 'uvec4', pt.triangleShadeAttr.count ).toReadOnly(),
178
- };
174
+ // The path tracer's layout, split into parts as it is: uvec4 triangles keep packed lanes' exact bits.
175
+ const nodes = pt.sceneStorageNodes?.();
176
+ if ( nodes ) {
179
177
 
180
- }
181
-
182
- if ( pt.bvhStorageAttr && ! this._bvhStorageNode ) {
178
+ this._triStorageNode = nodes.triangles;
179
+ this._bvhStorageNode = nodes.bvh;
183
180
 
184
- this._bvhStorageNode = storage(
185
- pt.bvhStorageAttr, 'vec4', pt.bvhStorageAttr.count
186
- ).toReadOnly();
181
+ }
187
182
 
188
183
  }
189
184
 
@@ -57,7 +57,6 @@ export class PathTracer extends PathTracerStage {
57
57
  this._packedBuffers = null;
58
58
  this._queueManager = null;
59
59
  this._kernelManager = null;
60
- this._gBufferAttr = null; // per-pixel first-hit MRT (ND + albedo); see _buildWavefrontKernels
61
60
  this._m2Attr = null; // per-pixel running mean of luminance² for the Tier-1 convergence early-stop
62
61
  this._streakAttr = null; // Tier-2: per-pixel freeze-candidate streak (u32); frozen := streak >= K
63
62
  // Tier-2 dilation: per-pixel frozen mask (1=skip), written race-free in buildActivePixels; a frozen pixel
@@ -209,7 +208,6 @@ export class PathTracer extends PathTracerStage {
209
208
  t.register( 'gbuffer', () => {
210
209
 
211
210
  const a = [];
212
- if ( this._gBufferAttr ) a.push( this._gBufferAttr );
213
211
  if ( this._m2Attr ) a.push( this._m2Attr );
214
212
  if ( this._streakAttr ) a.push( this._streakAttr );
215
213
  if ( this._frozenMaskAttr ) a.push( this._frozenMaskAttr );
@@ -226,7 +224,8 @@ export class PathTracer extends PathTracerStage {
226
224
  } );
227
225
 
228
226
  // Scene geometry (triangle data, two-level BVH, light BVH + emissive)
229
- t.register( 'geometry', () => [ this.triangleGeoAttr, this.triangleShadeAttr, this.bvhStorageAttr, this.lightStorageAttr ] );
227
+ // The light data shares the materials' buffer and is counted with them.
228
+ t.register( 'geometry', () => [ ...( this._geoParts?.attrs ?? [] ), ...( this._shadeParts?.attrs ?? [] ), ...( this._bvhParts?.attrs ?? [ this.bvhStorageAttr ] ) ] );
230
229
 
231
230
  // Material storage buffer + per-property texture arrays
232
231
  t.register( 'materials', () => {
@@ -1369,8 +1368,9 @@ export class PathTracer extends PathTracerStage {
1369
1368
  _computePathBudget() {
1370
1369
 
1371
1370
  const RAY_BYTES = RAY_STRIDE * 16;
1372
- const HIT_BYTES = ( this._integrator?.hitStride ?? HIT_STRIDE ) * 16;
1373
- const bytesPerPath = RAY_BYTES + HIT_BYTES + GBUFFER_STRIDE * 16
1371
+ // The hit buffer carries the G-buffer as its last region.
1372
+ const HIT_BYTES = ( ( this._integrator?.hitStride ?? HIT_STRIDE ) + GBUFFER_STRIDE ) * 16;
1373
+ const bytesPerPath = RAY_BYTES + HIT_BYTES
1374
1374
  + 4 + 4 /* activeIndices A/B */ + 4;
1375
1375
 
1376
1376
  const limits = this.renderer?.backend?.device?.limits;
@@ -1493,15 +1493,12 @@ export class PathTracer extends PathTracerStage {
1493
1493
 
1494
1494
  }
1495
1495
 
1496
- // Per-CHUNK first-hit G-buffer (LOCAL slot r, size B), 1 uvec4/slot half-packed (pack2x16). Written by
1497
- // Generate + Shade(bounce 0), read by Shade + FinalWrite within the SAME chunk, so per-chunk suffices.
1498
- // uint (not f32): packed lanes can hit the NaN exponent range (snorm 1.0 → 0x7FFF) that an f32 store may
1499
- // canonicalize; u32 stores the bits verbatim.
1500
- const gBufferVec4s = B * GBUFFER_STRIDE;
1501
- freeStorageAttribute( this.renderer, this._gBufferAttr );
1502
- this._gBufferAttr = gpuOnlyStorageAttribute( gBufferVec4s, 4, Uint32Array );
1503
- const gBufferRW = storage( this._gBufferAttr, 'uvec4' );
1504
- const gBufferRO = storage( this._gBufferAttr, 'uvec4' ).toReadOnly();
1496
+ // Per-CHUNK first-hit G-buffer (LOCAL slot r, size B), 1 uvec4/slot half-packed (pack2x16), the hit buffer's
1497
+ // last region (PackedRayBuffer). Written by Generate + Shade(bounce 0), read by Shade + FinalWrite within the
1498
+ // SAME chunk, so per-chunk suffices. uint (not f32): packed lanes can hit the NaN exponent range (snorm 1.0 →
1499
+ // 0x7FFF) that an f32 store may canonicalize; u32 stores the bits verbatim.
1500
+ const gBufferRW = this._packedBuffers.hitBuffer.rw;
1501
+ const gBufferRO = this._packedBuffers.hitBuffer.ro;
1505
1502
 
1506
1503
  // Per-PIXEL persistent buffers (m2/streak/frozenMask) are GLOBAL-pixel-indexed (p = pixelBase + r) and
1507
1504
  // must span the whole frame across chunks AND persist across frames, so they're sized to the reserved
@@ -1842,7 +1839,7 @@ export class PathTracer extends PathTracerStage {
1842
1839
  const freshTri = this.triangleStorageNode;
1843
1840
  const freshMat = this.materialData.materialStorageNode;
1844
1841
  const freshEnvCDF = texture( this.environment.envCDFTexture ); // independent CDF texture node; refreshed in _refreshWfTextureNodes
1845
- const freshLight = this.lightStorageNode;
1842
+ const freshLight = this.lightDataNode;
1846
1843
  // Independent texture nodes (never compiled elsewhere) avoid Three.js TextureNode caching across pipelines; refreshed via _refreshWfTextureNodes.
1847
1844
  const _mat = this.materialData;
1848
1845
  const _env = this.environment;
@@ -2231,7 +2228,6 @@ export class PathTracer extends PathTracerStage {
2231
2228
  this._packedBuffers?.dispose();
2232
2229
  this._queueManager?.dispose();
2233
2230
  this._kernelManager?.dispose();
2234
- this._gBufferAttr?.dispose?.();
2235
2231
  this._m2Attr?.dispose?.();
2236
2232
  this._streakAttr?.dispose?.();
2237
2233
  this._frozenMaskAttr?.dispose?.();
@@ -2241,7 +2237,6 @@ export class PathTracer extends PathTracerStage {
2241
2237
  this._packedBuffers = null;
2242
2238
  this._queueManager = null;
2243
2239
  this._kernelManager = null;
2244
- this._gBufferAttr = null;
2245
2240
  this._m2Attr = null;
2246
2241
  this._streakAttr = null;
2247
2242
  this._frozenMaskAttr = null;