rayzee 11.2.1 → 11.4.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 (76) hide show
  1. package/README.md +91 -34
  2. package/dist/ArchiveImporter-Cgyx5AF7.js +7860 -0
  3. package/dist/BidirectionalIntegrator-BKF-cwH2.js +954 -0
  4. package/dist/{BufferLayout-D_IBo2ci.js → BufferLayout-xPRH9jYD.js} +41 -5
  5. package/dist/{NeuralRetouch-Cr-otSW8.js → NeuralRetouch-C3Q2XqbF.js} +4 -3
  6. package/dist/{NeuralSuperRes-B7E3_M0t.js → NeuralSuperRes-DV5TtURP.js} +11 -5
  7. package/dist/{PathTracer-D6nbknj5.js → PathTracer-D3MvEaZK.js} +4384 -4618
  8. package/dist/{PhysicalSky-Dq5VSiCQ.js → PhysicalSky-CY4jiz6a.js} +2 -2
  9. package/dist/{RayzeeRenderer-DbjqO6v-.js → RayzeeRenderer-D23m6NqG.js} +486 -391
  10. package/dist/SolarLimb-bZE6y63S.js +762 -0
  11. package/dist/{ToneMapGPU-BOjpul7A.js → ToneMapGPU-CTDBvmmS.js} +31 -22
  12. package/dist/addons/archives.es.js +1 -1
  13. package/dist/addons/bidirectional.es.js +1 -1
  14. package/dist/addons/physical-sky.es.js +1 -1
  15. package/dist/rayzee-core.es.js +7 -7
  16. package/dist/rayzee.es.js +1327 -838
  17. package/dist/rayzee.umd.js +146 -134
  18. package/dist/sceneCachePolicy-AKOEu2lf.js +1602 -0
  19. package/package.json +1 -1
  20. package/src/EngineDefaults.js +2 -2
  21. package/src/Passes/AIUpscaler.js +2 -0
  22. package/src/PathTracerApp.js +126 -19
  23. package/src/Pipeline/CompletionTracker.js +7 -3
  24. package/src/Processor/ArchiveImporter.js +173 -94
  25. package/src/Processor/ArchiveReader.js +20 -23
  26. package/src/Processor/AssetLoader.js +48 -1
  27. package/src/Processor/BVHRefitter.js +109 -13
  28. package/src/Processor/BufferLayout.js +115 -0
  29. package/src/Processor/ChunkedRecords.js +3 -2
  30. package/src/Processor/EmissiveTriangleBuilder.js +6 -6
  31. package/src/Processor/GeometryExtractor.js +189 -79
  32. package/src/Processor/HostMemory.js +39 -9
  33. package/src/Processor/InstanceTable.js +714 -42
  34. package/src/Processor/PBRT/PBRTCurves.js +224 -6
  35. package/src/Processor/PBRT/PBRTMaterials.js +3 -0
  36. package/src/Processor/PBRT/PBRTSceneBuilder.js +82 -25
  37. package/src/Processor/PackedRayBuffer.js +5 -2
  38. package/src/Processor/SceneProcessor.js +747 -141
  39. package/src/Processor/TLASBuilder.js +32 -17
  40. package/src/Processor/ToneMapCPU.js +16 -4
  41. package/src/Processor/ToneMapGPU.js +30 -14
  42. package/src/Processor/USD/USDLayer.js +1149 -0
  43. package/src/Processor/USD/USDScene.js +1655 -0
  44. package/src/Processor/USD/USDStage.js +682 -0
  45. package/src/Processor/USD/USDText.js +674 -0
  46. package/src/Processor/USD/index.js +138 -0
  47. package/src/Processor/Workers/BVHRefitWorker.js +3 -3
  48. package/src/RayzeeRenderer.js +221 -33
  49. package/src/RenderSettings.js +4 -2
  50. package/src/Stages/AutoExposure.js +484 -494
  51. package/src/Stages/Compositor.js +27 -6
  52. package/src/Stages/LocalExposure.js +546 -0
  53. package/src/Stages/NormalDepth.js +7 -12
  54. package/src/Stages/PathTracer.js +18 -21
  55. package/src/Stages/PathTracerStage.js +282 -103
  56. package/src/Storage/GeometrySpill.js +46 -14
  57. package/src/Storage/SceneGraphCodec.js +17 -3
  58. package/src/Storage/SpillStore.js +9 -0
  59. package/src/TSL/BVHTraversal.js +183 -7
  60. package/src/TSL/Common.js +57 -13
  61. package/src/TSL/ShadeKernel.js +3 -2
  62. package/src/TSL/TextureSampling.js +11 -8
  63. package/src/TSL/patches.js +84 -0
  64. package/src/index.js +1 -0
  65. package/src/integrators/BidirectionalIntegrator.js +20 -2
  66. package/src/managers/DenoisingManager.js +15 -19
  67. package/src/managers/InteractionManager.js +19 -6
  68. package/src/managers/MaterialDataManager.js +22 -5
  69. package/src/managers/UniformManager.js +3 -0
  70. package/src/managers/VideoRenderManager.js +30 -4
  71. package/src/neural/NeuralRetouch.js +1 -0
  72. package/src/neural/NeuralSuperRes.js +6 -1
  73. package/dist/ArchiveImporter-D_x863yu.js +0 -5198
  74. package/dist/BidirectionalIntegrator-DmkxxEOR.js +0 -946
  75. package/dist/SolarLimb-CPaeZVwz.js +0 -721
  76. 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;
@@ -526,6 +532,13 @@ export class RayzeeRenderer extends EventDispatcher {
526
532
 
527
533
  }
528
534
 
535
+ if ( this._settling() ) {
536
+
537
+ this.dispatchEvent( { type: EngineEvents.FRAME } );
538
+ return;
539
+
540
+ }
541
+
529
542
  // Stop the loop to avoid constant CPU usage while idle
530
543
  this.stopAnimation();
531
544
  return;
@@ -552,12 +565,7 @@ export class RayzeeRenderer extends EventDispatcher {
552
565
 
553
566
  this._traceFrame( { liveDenoise: true } );
554
567
 
555
- if ( this.stages.pathTracer.isComplete && this.completion.markComplete() ) {
556
-
557
- this._renderCompleted();
558
- this.dispatchEvent( { type: EngineEvents.RENDER_COMPLETE, ...this._completionInfo() } );
559
-
560
- }
568
+ if ( this.stages.pathTracer.isComplete && this.completion.markComplete() ) this._announceComplete();
561
569
 
562
570
  }
563
571
 
@@ -566,6 +574,25 @@ export class RayzeeRenderer extends EventDispatcher {
566
574
 
567
575
  }
568
576
 
577
+ // The closing passes and RENDER_COMPLETE wait for _finishImage(), so both see the finished image's exposure.
578
+ _announceComplete() {
579
+
580
+ const info = this._completionInfo();
581
+ const restarts = this.stages.pathTracer.resetCount;
582
+ const announce = () => {
583
+
584
+ if ( ! this.completion.renderCompleteDispatched || this.stages.pathTracer?.resetCount !== restarts ) return;
585
+ this._renderCompleted();
586
+ this.dispatchEvent( { type: EngineEvents.RENDER_COMPLETE, ...info } );
587
+
588
+ };
589
+
590
+ const finishing = this._finishImage();
591
+ if ( finishing ) finishing.then( announce, announce );
592
+ else announce();
593
+
594
+ }
595
+
569
596
  // One frame of a render and its bookkeeping, shared by animate() and renderUntilComplete().
570
597
  _traceFrame( { liveDenoise = false } = {} ) {
571
598
 
@@ -708,6 +735,8 @@ export class RayzeeRenderer extends EventDispatcher {
708
735
  this.stopAnimation();
709
736
  clearTimeout( this._resizeDebounceTimer );
710
737
  this._resizeDebounceTimer = null;
738
+ clearTimeout( this._respillTimer );
739
+ this._respillTimer = null;
711
740
 
712
741
  this._removeTrackedListeners();
713
742
  setStatusCallback( null );
@@ -1175,6 +1204,8 @@ export class RayzeeRenderer extends EventDispatcher {
1175
1204
  if ( this._loadingInProgress ) throw this._busyError( 'PathTracerApp' );
1176
1205
 
1177
1206
  this._loadingInProgress = true;
1207
+ // The previous scene's sky width must not narrow this one's while it loads.
1208
+ if ( this.assetLoader ) this.assetLoader.maxEnvironmentWidth = this._memorySpill === true ? SPILL_ENVIRONMENT_WIDTH : 0;
1178
1209
 
1179
1210
  try {
1180
1211
 
@@ -1189,6 +1220,8 @@ export class RayzeeRenderer extends EventDispatcher {
1189
1220
  // (The old primary was already released by releaseTargetModel() in loadFn.)
1190
1221
  this._clearAppendedModels();
1191
1222
  this._modelReplaced();
1223
+ // Before the scene's own environment starts loading: a spilling scene's is narrowed as it decodes.
1224
+ this._planSpill();
1192
1225
  await this.loadSceneData( { pendingEnvironment: this._beginSceneMetadataEnvironment() } );
1193
1226
  this._maybeSpill();
1194
1227
  this.pipeline?.eventBus.emit( 'pipeline:lightingChanged' );
@@ -1215,6 +1248,7 @@ export class RayzeeRenderer extends EventDispatcher {
1215
1248
  } finally {
1216
1249
 
1217
1250
  this._loadingInProgress = false;
1251
+ this._spillPlan = null;
1218
1252
 
1219
1253
  }
1220
1254
 
@@ -1445,11 +1479,22 @@ export class RayzeeRenderer extends EventDispatcher {
1445
1479
  updateLoading( { status: "Transferring data to GPU...", progress: 86 } );
1446
1480
  await new Promise( r => setTimeout( r, 0 ) );
1447
1481
  timer.start( 'GPU data transfer' );
1482
+ await this._sdf.uploadChunks( this._chunkUploader() );
1448
1483
 
1449
1484
  // Re-read rather than reusing the snapshot above: a pendingEnvironment may have landed
1450
1485
  // during the BVH build, and the snapshot is then a disposed texture.
1451
1486
  if ( ! this._sdf.uploadToPathTracer( this.stages.pathTracer, this.lightManager, this.meshScene, hdri(), { keepUserLights } ) ) return false;
1452
1487
 
1488
+ // A bucket at a time, each awaited (PathTracerStage#drainUploads): left to the first frame, three.js sends every
1489
+ // layer at once, and Chrome keeps a pool that size (724 MB for 24155522.glb).
1490
+ for ( const texture of [ ...( this._sdf.srgbBucketTextures ?? [] ), ...( this._sdf.linearBucketTextures ?? [] ) ] ) {
1491
+
1492
+ if ( ! texture ) continue;
1493
+ this.renderer.initTexture( texture );
1494
+ await this.stages.pathTracer.drainUploads();
1495
+
1496
+ }
1497
+
1453
1498
  // Patch per-mesh visibility into the TLAS leaves we just uploaded
1454
1499
  this.stages.pathTracer._meshRefs = this.stages.pathTracer._collectMeshRefs( this.meshScene );
1455
1500
  this.stages.pathTracer.setMeshVisibilityData( this.stages.pathTracer._meshRefs );
@@ -1724,43 +1769,124 @@ export class RayzeeRenderer extends EventDispatcher {
1724
1769
  // Dynamic cameras (add / remove)
1725
1770
  // ═══════════════════════════════════════════════════════════════
1726
1771
 
1727
- /** Triangle and placement budgets for a scene archive: raised when the build can spill. @private */
1772
+ /** How scenes use the disk: 'auto', true or false (see the constructor's `memorySpill`). */
1773
+ get memorySpill() {
1774
+
1775
+ return this._memorySpill;
1776
+
1777
+ }
1778
+
1779
+ /** Sets {@link memorySpill}; the next load or rebuild follows it. */
1780
+ setMemorySpill( mode ) {
1781
+
1782
+ this._memorySpill = mode === true || mode === false ? mode : 'auto';
1783
+
1784
+ }
1785
+
1786
+ /** Whether the current scene was built through disk. */
1787
+ get sceneSpilled() {
1788
+
1789
+ return !! this._sdf?.spilling;
1790
+
1791
+ }
1792
+
1793
+ /** Whether the model's three.js geometry is on disk, so picking skips it until {@link ensureSceneResident}. */
1794
+ get geometryOnDisk() {
1795
+
1796
+ return this._sdf?.geometryOnDisk === true;
1797
+
1798
+ }
1799
+
1800
+ /**
1801
+ * Triangle and placement budgets for a scene archive: raised whenever a build could spill, since the preflight
1802
+ * refuses what cannot fit either way. @private
1803
+ */
1728
1804
  _sceneBudgets( options ) {
1729
1805
 
1730
- if ( ! this._memorySpill || ! this.storage ) return options;
1806
+ if ( this._memorySpill === false || ! this.storage ) return options;
1731
1807
  return { maxTriangles: SPILL_TRIANGLE_BUDGET, maxPlacements: SPILL_PLACEMENT_BUDGET, ...options };
1732
1808
 
1733
1809
  }
1734
1810
 
1735
1811
  /**
1736
- * Experimental memory spill, during the build: static scenes only, since a deforming clip
1737
- * refits every frame. @private
1812
+ * Decides whether the scene about to be built spills: static scenes only, since a deforming clip refits every
1813
+ * frame. Held for the build that follows ({@link _progressiveSpill}). @private
1738
1814
  */
1815
+ _planSpill() {
1816
+
1817
+ const offered = this._memorySpill !== false && !! this.storage && ( this.assetLoader?.animations?.length ?? 0 ) === 0;
1818
+ const spill = offered && ( this._memorySpill === true || this._sdf.needsSpill( this.meshScene ) );
1819
+ if ( this.assetLoader ) this.assetLoader.maxEnvironmentWidth = spill ? SPILL_ENVIRONMENT_WIDTH : 0;
1820
+ // An importer installs its own sky at the end of its read, before this.
1821
+ const sky = spill ? this.meshScene?.environment : null;
1822
+ const image = sky?.image;
1823
+ if ( image ) limitEnvironmentWidth( sky, SPILL_ENVIRONMENT_WIDTH );
1824
+ if ( sky && sky.image !== image ) {
1825
+
1826
+ // three.js keeps an uploaded texture's size, so the old one goes.
1827
+ sky.dispose();
1828
+ sky.needsUpdate = true;
1829
+
1830
+ }
1831
+
1832
+ if ( spill && this._memorySpill === 'auto' ) log.info( 'scene is past the in-memory line: building it through disk' );
1833
+ this._spillPlan = spill;
1834
+ return spill;
1835
+
1836
+ }
1837
+
1838
+ /** @private */
1739
1839
  _progressiveSpill() {
1740
1840
 
1741
- if ( ! this._memorySpill || ! this.storage || ( this.assetLoader?.animations?.length ?? 0 ) > 0 ) return null;
1841
+ const spill = this._spillPlan ?? this._planSpill();
1842
+ this._spillPlan = null;
1843
+ return spill ? { storage: this.storage, uploader: this._chunkUploader() } : null;
1844
+
1845
+ }
1846
+
1847
+ /** @private */
1848
+ _chunkUploader() {
1849
+
1742
1850
  const stage = this.stages.pathTracer;
1743
- return { storage: this.storage, uploader: ( records, kind ) => kind === 'triangles' ? stage.createTriangleChunkUploader( records ) : stage.createChunkUploader( records ) };
1851
+ return ( records, kind, options ) => kind === 'triangles' ? stage.createTriangleChunkUploader( records, options ) : stage.createChunkUploader( records );
1744
1852
 
1745
1853
  }
1746
1854
 
1747
- /** Experimental memory spill for a static scene, in the background. @private */
1855
+ /** The rest of a spilling scene's spill, once it is on the GPU, in the background. @private */
1748
1856
  _maybeSpill() {
1749
1857
 
1750
- if ( ! this._memorySpill || ! this.storage || ( this.assetLoader?.animations?.length ?? 0 ) > 0 ) return;
1751
- this._sdf?.spillToDisk( this.storage )
1858
+ if ( ! this._sdf?.spilling || ! this.storage ) return;
1859
+ this._sdf.spillAfterLoad().catch( error => this._issues.warn( ISSUE_CODES.STORAGE_WRITE_FAILED, `memory spill failed: ${error.message}` ) );
1860
+ this._sdf.spillToDisk( this.storage )
1752
1861
  .then( result => result && this.dispatchEvent( { type: EngineEvents.SCENE_SPILLED, ...result } ) )
1753
1862
  .catch( error => this._issues.warn( ISSUE_CODES.STORAGE_WRITE_FAILED, `memory spill failed: ${error.message}` ) );
1754
1863
 
1755
1864
  }
1756
1865
 
1866
+ /** Once edits have been quiet for {@link RESPILL_AFTER_MS}, puts back on disk what they read back. @private */
1867
+ _scheduleRespill() {
1868
+
1869
+ if ( ! this._sdf?.spilling ) return;
1870
+ clearTimeout( this._respillTimer );
1871
+ this._respillTimer = setTimeout( () => {
1872
+
1873
+ this._respillTimer = null;
1874
+ this._sdf?.respill().catch( error => this._issues.warn( ISSUE_CODES.STORAGE_WRITE_FAILED, `memory spill failed: ${error.message}` ) );
1875
+
1876
+ }, RESPILL_AFTER_MS );
1877
+ this._respillTimer.unref?.();
1878
+
1879
+ }
1880
+
1757
1881
  /**
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.
1882
+ * Reads back what {@link options.memorySpill} moved to disk: triangle records, BLAS nodes and the three.js geometry
1883
+ * (whose arrays are empty until then). The engine does this itself before its own readers; a host calling
1884
+ * {@link refitBLASes}, or reading the model's geometry, on a spilled scene awaits it first.
1760
1885
  */
1761
1886
  async ensureSceneResident() {
1762
1887
 
1763
1888
  if ( this._sdf?.spilled ) await this._sdf.ensureResident();
1889
+ await this._sdf?.ensureGeometryResident();
1764
1890
 
1765
1891
  }
1766
1892
 
@@ -1885,15 +2011,26 @@ export class RayzeeRenderer extends EventDispatcher {
1885
2011
  * drags along any other object sharing the same geometry.
1886
2012
  *
1887
2013
  * @param {number[]} meshIndices - indices into {@link sceneMeshes}
1888
- * @returns {{ refitTimeMs: number, placements: number }}
2014
+ * @returns {?{ refitTimeMs: number, placements: number }} null when the move waits for a spilled scene's data
1889
2015
  */
1890
2016
  updateMeshTransforms( meshIndices ) {
1891
2017
 
2018
+ // A spilled scene's matrix lists, TLAS or copy records are on disk: the move lands once they are back.
2019
+ if ( this._sdf?.needsPageInForMove( meshIndices ) ) {
2020
+
2021
+ this._sdf.ensureMovable( meshIndices )
2022
+ .then( () => this.updateMeshTransforms( meshIndices ) )
2023
+ .catch( error => this._issues.warn( ISSUE_CODES.STORAGE_READ_FAILED, `reading instance matrices back failed: ${error.message}` ) );
2024
+ return null;
2025
+
2026
+ }
2027
+
1892
2028
  this._notePlacementsMoving( meshIndices );
1893
2029
  const result = this._sdf.updateMeshTransforms( meshIndices );
1894
2030
 
1895
- this.stages.pathTracer.updateBufferRanges( [], [ this._sdf.computeTLASDirtyRange() ] );
2031
+ this.stages.pathTracer.updateBufferRanges( [], this._sdf.takeMoveRanges() );
1896
2032
  this.reset( false, { motion: true } );
2033
+ this._scheduleRespill();
1897
2034
 
1898
2035
  return result;
1899
2036
 
@@ -2383,6 +2520,7 @@ export class RayzeeRenderer extends EventDispatcher {
2383
2520
 
2384
2521
  }
2385
2522
 
2523
+ await this._finishImage();
2386
2524
  return stage.frameCount;
2387
2525
 
2388
2526
  }
@@ -2460,6 +2598,8 @@ export class RayzeeRenderer extends EventDispatcher {
2460
2598
  }
2461
2599
 
2462
2600
  const info = this._completionInfo();
2601
+ // Before it is announced: a listener reading pixels sees the finished image's exposure.
2602
+ await this._finishImage();
2463
2603
  if ( this.completion.markComplete() ) this.dispatchEvent( { type: EngineEvents.RENDER_COMPLETE, ...info } );
2464
2604
 
2465
2605
  const denoised = denoise ? await this._finalDenoise() : false;
@@ -2619,12 +2759,14 @@ export class RayzeeRenderer extends EventDispatcher {
2619
2759
  toneMapping: this.renderer.toneMapping,
2620
2760
  saturation: this.settings.get( 'saturation' ) ?? 1,
2621
2761
  preserveAlpha,
2762
+ gain: this._displayGain(),
2622
2763
  };
2623
2764
 
2624
2765
  const gpu = await this._toneMapOnGPU( shown?.texture ?? null, target, width, height, tone );
2766
+ const fn = ! gpu && tone.gain ? await tone.gain.cpu() : null;
2625
2767
  const data = gpu ?? toneMapToRGBA8( shown
2626
2768
  ? await this._readTexture( shown.texture, width, height )
2627
- : await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 ), tone );
2769
+ : await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 ), { ...tone, pixelGain: fn && { fn, width, height } } );
2628
2770
  return { data, width, height, colorSpace, source: shown?.source ?? 'accumulation', toneMappedOn: gpu ? 'gpu' : 'cpu' };
2629
2771
 
2630
2772
  }
@@ -3198,9 +3340,7 @@ export class RayzeeRenderer extends EventDispatcher {
3198
3340
  */
3199
3341
  setMeshVisibility( meshIndex, visible ) {
3200
3342
 
3201
- this.stages.pathTracer?.updateMeshVisibility( meshIndex, visible );
3202
- this._refreshEmissiveForVisibility();
3203
- this.reset();
3343
+ this._whenTLASEditable( () => this.stages.pathTracer?.updateMeshVisibility( meshIndex, visible ) );
3204
3344
 
3205
3345
  }
3206
3346
 
@@ -3210,9 +3350,25 @@ export class RayzeeRenderer extends EventDispatcher {
3210
3350
  */
3211
3351
  updateAllMeshVisibility() {
3212
3352
 
3213
- this.stages.pathTracer?.updateAllMeshVisibility();
3214
- this._refreshEmissiveForVisibility();
3215
- this.reset();
3353
+ this._whenTLASEditable( () => this.stages.pathTracer?.updateAllMeshVisibility() );
3354
+
3355
+ }
3356
+
3357
+ /** Applies a visibility edit, then resets — once a spilled TLAS is back in memory. @private */
3358
+ _whenTLASEditable( edit ) {
3359
+
3360
+ const apply = () => {
3361
+
3362
+ edit();
3363
+ this._refreshEmissiveForVisibility();
3364
+ this.reset();
3365
+ this._scheduleRespill();
3366
+
3367
+ };
3368
+
3369
+ const ready = this._sdf?.whenTLASEditable();
3370
+ if ( ! ready ) return apply();
3371
+ ready.then( apply ).catch( error => this._issues.warn( ISSUE_CODES.STORAGE_READ_FAILED, `reading the TLAS back failed: ${error.message}` ) );
3216
3372
 
3217
3373
  }
3218
3374
 
@@ -3472,7 +3628,18 @@ export class RayzeeRenderer extends EventDispatcher {
3472
3628
  async rebuildMaterials( scene ) {
3473
3629
 
3474
3630
  await this.ensureSceneResident();
3475
- await this.stages.pathTracer?.rebuildMaterials( scene || this.meshScene );
3631
+ const stage = this.stages.pathTracer;
3632
+ await stage?.rebuildMaterials( scene || this.meshScene );
3633
+
3634
+ // Loaded without texture coordinates, and a material has a texture now: they go up again from the records.
3635
+ if ( stage?.triangleStorageNode?.withoutUV && this._sdf?.triangles && usesTextureCoordinates( scene || this.meshScene ) ) {
3636
+
3637
+ this._sdf.textureCoordinates = true;
3638
+ stage.setTriangleData( this._sdf.triangles, this._sdf.triangleCount, { textureCoordinates: true } );
3639
+ stage.setupMaterial();
3640
+
3641
+ }
3642
+
3476
3643
  this.reset();
3477
3644
 
3478
3645
  }
@@ -3841,6 +4008,20 @@ export class RayzeeRenderer extends EventDispatcher {
3841
4008
  /** Once, on the loop frame the render completes. */
3842
4009
  _renderCompleted() {}
3843
4010
 
4011
+ /** True keeps the loop running over a finished render, to redraw what is still changing. */
4012
+ _settling() {
4013
+
4014
+ return false;
4015
+
4016
+ }
4017
+
4018
+ /** A render has its samples, before any closing pass or announcement: a promise to wait for, or null. */
4019
+ _finishImage() {
4020
+
4021
+ return null;
4022
+
4023
+ }
4024
+
3844
4025
  /** Draws what sits over the image. Here only brings the light scene's matrices up to date. */
3845
4026
  _renderHelperOverlay() {
3846
4027
 
@@ -3891,6 +4072,13 @@ export class RayzeeRenderer extends EventDispatcher {
3891
4072
 
3892
4073
  }
3893
4074
 
4075
+ /** A per-pixel gain the shown picture carries before the view (the compositor's), for tone-mapped readbacks; or null. */
4076
+ _displayGain() {
4077
+
4078
+ return null;
4079
+
4080
+ }
4081
+
3894
4082
  /** Whether a 'display' read should find a processed picture. */
3895
4083
  _denoiserInUse() {
3896
4084
 
@@ -258,7 +258,7 @@ export class RenderSettings extends EventDispatcher {
258
258
 
259
259
  handleRenderLimitMode: ( value ) => {
260
260
 
261
- stages.pathTracer?.setRenderLimitMode?.( value );
261
+ stages.pathTracer?.setRenderLimitMode?.( value, this.get( 'renderTimeLimit' ) );
262
262
  reconcileCompletion?.();
263
263
 
264
264
  },
@@ -271,8 +271,10 @@ export class RenderSettings extends EventDispatcher {
271
271
 
272
272
  },
273
273
 
274
- handleRenderTimeLimit: () => {
274
+ // Arming or disarming the deadline lifts or restores the ceiling in 'timeOnly'.
275
+ handleRenderTimeLimit: ( value ) => {
275
276
 
277
+ stages.pathTracer?.setRenderLimitMode?.( this.get( 'renderLimitMode' ), value );
276
278
  reconcileCompletion?.();
277
279
 
278
280
  },