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.
- package/README.md +139 -8
- package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
- package/dist/{NeuralSuperRes-CQLCLpcC.js → NeuralSuperRes-CbNvQjCw.js} +3 -2
- package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
- package/dist/rayzee.es.js +10344 -5334
- package/dist/rayzee.umd.js +265 -265
- package/package.json +1 -1
- package/src/AssetConfig.js +4 -0
- package/src/EngineDefaults.js +38 -56
- package/src/EngineEvents.js +3 -0
- package/src/EngineIssues.js +6 -0
- package/src/Headless.js +3 -1
- package/src/Passes/AIUpscaler.js +2 -1
- package/src/Passes/OIDNDenoiser.js +3 -6
- package/src/PathTracerApp.js +290 -19
- package/src/Processor/ArchiveCache.js +120 -0
- package/src/Processor/ArchiveReader.js +179 -32
- package/src/Processor/AssetLoader.js +514 -76
- package/src/Processor/AtmosphereModel.js +465 -0
- package/src/Processor/BVHBuilder.js +4 -1
- package/src/Processor/BVHLeafFold.js +100 -0
- package/src/Processor/BVHRefitter.js +98 -214
- package/src/Processor/ChunkedRecords.js +189 -4
- package/src/Processor/EmissiveTriangleBuilder.js +8 -0
- package/src/Processor/GeometryExtractor.js +238 -40
- package/src/Processor/InstanceTable.js +55 -37
- package/src/Processor/PBRT/PBRTCurves.js +104 -27
- package/src/Processor/PBRT/PBRTParser.js +132 -22
- package/src/Processor/PBRT/PBRTSceneBuilder.js +192 -49
- package/src/Processor/PBRT/buffers.js +27 -0
- package/src/Processor/PBRT/index.js +55 -15
- package/src/Processor/PackedRayBuffer.js +10 -13
- package/src/Processor/ParallelBVHBuilder.js +36 -15
- package/src/Processor/PhysicalSky.js +308 -0
- package/src/Processor/QueueManager.js +1 -0
- package/src/Processor/SceneProcessor.js +1132 -181
- package/src/Processor/SplitBLAS.js +249 -0
- package/src/Processor/SunPosition.js +41 -0
- package/src/Processor/TextureCreator.js +21 -0
- package/src/Processor/Workers/AIUpscalerWorker.js +79 -52
- package/src/Processor/Workers/BVHWorker.js +17 -13
- package/src/Processor/ZipReader.js +207 -0
- package/src/RenderSettings.js +38 -0
- package/src/SceneState/SceneState.js +367 -0
- package/src/SceneState/portable.js +52 -0
- package/src/Stages/NormalDepth.js +7 -6
- package/src/Stages/PathTracer.js +108 -4
- package/src/Stages/PathTracerStage.js +251 -104
- package/src/Storage/BLASCache.js +190 -0
- package/src/Storage/CDFCache.js +77 -0
- package/src/Storage/DownloadCache.js +266 -0
- package/src/Storage/GeometrySpill.js +217 -0
- package/src/Storage/SceneGraphCodec.js +503 -0
- package/src/Storage/SpillStore.js +112 -0
- package/src/Storage/StorageManager.js +934 -0
- package/src/Storage/StorageOps.js +137 -0
- package/src/Storage/StorageWorker.js +37 -0
- package/src/Storage/events.js +29 -0
- package/src/Storage/identity.js +77 -0
- package/src/Storage/index.js +9 -0
- package/src/Storage/inlineTransport.js +24 -0
- package/src/Storage/locks.js +99 -0
- package/src/Storage/openStorage.js +87 -0
- package/src/Storage/sceneCachePolicy.js +12 -0
- package/src/Storage/shared.js +9 -0
- package/src/Storage/transport.js +62 -0
- package/src/TSL/Atmosphere.js +684 -0
- package/src/TSL/BVHTraversal.js +288 -239
- package/src/TSL/Common.js +11 -2
- package/src/TSL/Displacement.js +6 -6
- package/src/TSL/EmissiveSampling.js +5 -5
- package/src/TSL/EnvironmentCDF.js +176 -0
- package/src/TSL/GenerateKernel.js +3 -3
- package/src/TSL/HitFacet.js +4 -4
- package/src/TSL/LightsDirect.js +8 -8
- package/src/TSL/LightsSampling.js +55 -37
- package/src/TSL/Random.js +1 -0
- package/src/TSL/ShadeKernel.js +75 -19
- package/src/TSL/Sun.js +40 -0
- package/src/TSL/TextureSampling.js +6 -7
- package/src/index.js +18 -0
- package/src/managers/CameraManager.js +169 -1
- package/src/managers/EnvironmentManager.js +263 -33
- package/src/managers/GoboManager.js +19 -8
- package/src/managers/IESManager.js +3 -3
- package/src/managers/LightManager.js +98 -5
- package/src/managers/MaterialDataManager.js +22 -2
- package/src/managers/TransformManager.js +26 -0
- package/src/managers/UniformManager.js +5 -3
- package/src/managers/VideoRenderManager.js +3 -1
- package/src/managers/timeline/CameraTrack.js +22 -0
- package/src/managers/timeline/TimelineManager.js +14 -0
- package/src/version.js +3 -0
- package/src/Processor/ProceduralSky.js +0 -291
package/src/PathTracerApp.js
CHANGED
|
@@ -17,7 +17,7 @@ import { AutoExposure } from './Stages/AutoExposure.js';
|
|
|
17
17
|
import { Compositor } from './Stages/Compositor.js';
|
|
18
18
|
import { RenderPipeline } from './Pipeline/RenderPipeline.js';
|
|
19
19
|
import { CompletionTracker } from './Pipeline/CompletionTracker.js';
|
|
20
|
-
import { ENGINE_DEFAULTS as DEFAULT_STATE, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, modePresetSettings, MAX_STORAGE_TEXTURE_SIZE, MAX_RESERVABLE_RENDER_SIZE, setReservedRenderSize, getRenderProfile } from './EngineDefaults.js';
|
|
20
|
+
import { ENGINE_DEFAULTS as DEFAULT_STATE, TRIANGLE_DATA_LAYOUT, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, modePresetSettings, MAX_STORAGE_TEXTURE_SIZE, MAX_RESERVABLE_RENDER_SIZE, setReservedRenderSize, getRenderProfile } from './EngineDefaults.js';
|
|
21
21
|
import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples, disposeObjectFromMemory, disposeRenderer } from './Processor/utils.js';
|
|
22
22
|
import { BuildTimer } from './Processor/BuildTimer.js';
|
|
23
23
|
import { TextureReadback } from './Processor/TextureReadback.js';
|
|
@@ -25,11 +25,17 @@ import { createLogger, fmt } from './utils/Logger.js';
|
|
|
25
25
|
import { InteractionManager } from './managers/InteractionManager.js';
|
|
26
26
|
import { EngineEvents } from './EngineEvents.js';
|
|
27
27
|
import { IssueLog, ISSUE_CODES } from './EngineIssues.js';
|
|
28
|
+
import { getAssetConfig } from './AssetConfig.js';
|
|
29
|
+
import { StorageManager } from './Storage/StorageManager.js';
|
|
30
|
+
import { acquireSharedStorage } from './Storage/openStorage.js';
|
|
31
|
+
import { nameFromUrl } from './Storage/DownloadCache.js';
|
|
32
|
+
import { fileIdentity, identityKey } from './Storage/identity.js';
|
|
28
33
|
import { SETTING_SOURCE } from './RenderSettings.js';
|
|
29
34
|
import { toneMapToRGBA8 } from './Processor/ToneMapCPU.js';
|
|
30
35
|
import { ColorManagement, setActiveColorManagement } from './Color/ColorManagement.js';
|
|
31
36
|
import { getViewTransform } from './Color/ViewTransforms.js';
|
|
32
37
|
import { AssetLoader } from './Processor/AssetLoader.js';
|
|
38
|
+
import { SPILL_TRIANGLE_BUDGET, SPILL_PLACEMENT_BUDGET } from './Processor/PBRT/index.js';
|
|
33
39
|
import { SceneProcessor } from './Processor/SceneProcessor.js';
|
|
34
40
|
|
|
35
41
|
// Managers
|
|
@@ -44,6 +50,17 @@ import { OverlayManager } from './managers/OverlayManager.js';
|
|
|
44
50
|
import { AnimationManager } from './managers/AnimationManager.js';
|
|
45
51
|
import { TransformManager } from './managers/TransformManager.js';
|
|
46
52
|
import { TransformGizmoHelper } from './managers/helpers/TransformGizmoHelper.js';
|
|
53
|
+
import { captureSceneState, applySceneState } from './SceneState/SceneState.js';
|
|
54
|
+
import { TRIANGLE_PATCH_PROPERTIES } from './managers/MaterialDataManager.js';
|
|
55
|
+
import { VERSION } from './version.js';
|
|
56
|
+
|
|
57
|
+
export const RENDER_CHECKPOINT_VERSION = 1;
|
|
58
|
+
|
|
59
|
+
// The part of an archive a load chose, for sceneSource.
|
|
60
|
+
const partOf = ( { element, pbrtEntry } = {} ) => ( {
|
|
61
|
+
...( element !== undefined ? { element } : {} ),
|
|
62
|
+
...( pbrtEntry !== undefined ? { pbrtEntry } : {} ),
|
|
63
|
+
} );
|
|
47
64
|
|
|
48
65
|
// One app per canvas — auto-dispose a prior owner if the caller double-
|
|
49
66
|
// instantiates (StrictMode, HMR, etc.) so its rAF loop can't burn CPU.
|
|
@@ -129,6 +146,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
129
146
|
* @param {number} [options.maxSceneBytes] - refuse a scene whose estimated host memory is
|
|
130
147
|
* above this. The default refuses where the renderer process would be killed instead of
|
|
131
148
|
* throwing; raise it deliberately, on a fresh browser. See HostMemory.js.
|
|
149
|
+
* @param {false|'auto'|StorageManager} [options.storage] - on-disk storage; defaults to
|
|
150
|
+
* `configureAssets( { storage } )`. A host-supplied manager stays the host's to dispose.
|
|
151
|
+
* @param {boolean} [options.memorySpill=false] - experimental: once a large static scene is
|
|
152
|
+
* on the GPU, move its triangle records and BLAS nodes to disk (see
|
|
153
|
+
* {@link ensureSceneResident}). Needs storage; skipped for animated scenes.
|
|
132
154
|
*
|
|
133
155
|
* The engine dispatches `EngineEvents.FRAME` after each animate() iteration so hosts can
|
|
134
156
|
* tick external instrumentation (e.g. a stats panel) without coupling the engine to it.
|
|
@@ -153,6 +175,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
153
175
|
this._autoResize = options.autoResize !== false;
|
|
154
176
|
// A scene budget the host may raise; read where SceneProcessor is built, well after this.
|
|
155
177
|
this._maxSceneBytes = options.maxSceneBytes;
|
|
178
|
+
this._storageOption = options.storage;
|
|
179
|
+
this._memorySpill = options.memorySpill === true;
|
|
180
|
+
/** @type {?StorageManager} on-disk storage, null when off or unavailable */
|
|
181
|
+
this.storage = null;
|
|
182
|
+
this._storageRelease = null;
|
|
156
183
|
this._container = options.container || null;
|
|
157
184
|
// Apply the environment authored into a model file's metadata on load. See _beginSceneMetadataEnvironment().
|
|
158
185
|
this._applySceneMetadataEnabled = options.applySceneMetadata !== false;
|
|
@@ -324,6 +351,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
324
351
|
*/
|
|
325
352
|
async init() {
|
|
326
353
|
|
|
354
|
+
const storageReady = this._initStorage();
|
|
327
355
|
await this._initRenderer();
|
|
328
356
|
this._applyPendingReservedRenderSize();
|
|
329
357
|
this._initCameraManager();
|
|
@@ -332,9 +360,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
332
360
|
this._initPipeline();
|
|
333
361
|
await this._initManagers();
|
|
334
362
|
this._wireEvents();
|
|
363
|
+
await storageReady;
|
|
364
|
+
this._attachStorage();
|
|
335
365
|
|
|
336
366
|
// Seed path tracer with minimal empty scene data
|
|
337
|
-
this.stages.pathTracer.setTriangleData( new
|
|
367
|
+
this.stages.pathTracer.setTriangleData( new Uint32Array( TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE ), 0 );
|
|
338
368
|
this.stages.pathTracer.setBVHData( new Float32Array( 16 ) );
|
|
339
369
|
this.stages.pathTracer.materialData.setMaterialData( new Float32Array( 16 ) );
|
|
340
370
|
this.stages.pathTracer.setupMaterial();
|
|
@@ -346,6 +376,48 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
346
376
|
|
|
347
377
|
}
|
|
348
378
|
|
|
379
|
+
async _initStorage() {
|
|
380
|
+
|
|
381
|
+
const option = this._storageOption ?? getAssetConfig().storage;
|
|
382
|
+
if ( option === false ) return;
|
|
383
|
+
|
|
384
|
+
if ( option instanceof StorageManager ) {
|
|
385
|
+
|
|
386
|
+
this.storage = option;
|
|
387
|
+
|
|
388
|
+
} else {
|
|
389
|
+
|
|
390
|
+
const { storage, reason, release } = await acquireSharedStorage( getAssetConfig().cacheNamespace );
|
|
391
|
+
if ( this._disposed ) {
|
|
392
|
+
|
|
393
|
+
release();
|
|
394
|
+
return;
|
|
395
|
+
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if ( ! storage ) {
|
|
399
|
+
|
|
400
|
+
this._issues.warn( ISSUE_CODES.STORAGE_UNAVAILABLE, `on-disk storage is off: ${reason}`, { reason } );
|
|
401
|
+
return;
|
|
402
|
+
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
this.storage = storage;
|
|
406
|
+
this._storageRelease = release;
|
|
407
|
+
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this._addTrackedListener( this.storage, 'change', ( e ) => this.dispatchEvent( { type: EngineEvents.STORAGE_CHANGED, area: e.area } ) );
|
|
411
|
+
this._addTrackedListener( this.storage, 'issue', ( e ) => this._issues.warn( e.code, e.message, e.detail ) );
|
|
412
|
+
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
_attachStorage() {
|
|
416
|
+
|
|
417
|
+
if ( this.assetLoader ) this.assetLoader.storage = this.storage;
|
|
418
|
+
|
|
419
|
+
}
|
|
420
|
+
|
|
349
421
|
/**
|
|
350
422
|
* Starts the animation loop.
|
|
351
423
|
*/
|
|
@@ -624,6 +696,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
624
696
|
this._removeTrackedListeners();
|
|
625
697
|
setStatusCallback( null );
|
|
626
698
|
|
|
699
|
+
this._storageRelease?.();
|
|
700
|
+
this._storageRelease = null;
|
|
701
|
+
this.storage = null;
|
|
702
|
+
this._sceneSource = this._sceneSourceFile = null;
|
|
703
|
+
|
|
627
704
|
this._issues.detach(); // onIssue captures `this`; see IssueLog.detach()
|
|
628
705
|
|
|
629
706
|
// Holds the renderer and the issue log, both of which outlive it otherwise.
|
|
@@ -764,6 +841,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
764
841
|
this.transformManager?.detach?.();
|
|
765
842
|
|
|
766
843
|
this.assetLoader?.releaseTargetModel();
|
|
844
|
+
this._sceneSource = this._sceneSourceFile = null;
|
|
767
845
|
|
|
768
846
|
// Clear lights in the WebGPU light scene
|
|
769
847
|
this.lightManager?.clearLights?.();
|
|
@@ -771,7 +849,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
771
849
|
// Seed path tracer with empty data (matches the init-time seed)
|
|
772
850
|
if ( this.stages.pathTracer ) {
|
|
773
851
|
|
|
774
|
-
this.stages.pathTracer.setTriangleData( new
|
|
852
|
+
this.stages.pathTracer.setTriangleData( new Uint32Array( TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE ), 0 );
|
|
775
853
|
this.stages.pathTracer.setBVHData( new Float32Array( 16 ) );
|
|
776
854
|
this.stages.pathTracer.materialData.setMaterialData( new Float32Array( 16 ) );
|
|
777
855
|
this.stages.pathTracer.setEmissiveTriangleData?.( new Float32Array( 0 ), 0, 0 );
|
|
@@ -788,12 +866,14 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
788
866
|
/**
|
|
789
867
|
* Loads a model, builds BVH, and uploads scene data.
|
|
790
868
|
* @param {string} url - Model URL
|
|
869
|
+
* @param {{cacheKey?: string}} [options] - download-cache key for a URL that expires
|
|
791
870
|
*/
|
|
792
|
-
async loadModel( url ) {
|
|
871
|
+
async loadModel( url, options = {} ) {
|
|
793
872
|
|
|
794
873
|
await this._loadWithSceneRebuild(
|
|
795
|
-
() => this.assetLoader.loadModel( url ),
|
|
796
|
-
{ type: 'ModelLoaded', url }
|
|
874
|
+
() => this.assetLoader.loadModel( url, options ),
|
|
875
|
+
{ type: 'ModelLoaded', url },
|
|
876
|
+
{ kind: 'url', url, cacheKey: options.cacheKey ?? null }
|
|
797
877
|
);
|
|
798
878
|
|
|
799
879
|
}
|
|
@@ -817,7 +897,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
817
897
|
|
|
818
898
|
await this._loadWithSceneRebuild(
|
|
819
899
|
() => this.assetLoader.loadObject3D( object3d, name ),
|
|
820
|
-
{ type: 'Object3DLoaded', name }
|
|
900
|
+
{ type: 'Object3DLoaded', name },
|
|
901
|
+
{ kind: 'object3d', name }
|
|
821
902
|
);
|
|
822
903
|
|
|
823
904
|
}
|
|
@@ -891,22 +972,49 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
891
972
|
* tracer rendering buffers whose geometry has been freed. Here a concurrent call throws
|
|
892
973
|
* LOAD_IN_PROGRESS before anything is touched.
|
|
893
974
|
*
|
|
894
|
-
* @param {File} file
|
|
975
|
+
* @param {File|string} file - a File, or a URL to download (through the download cache)
|
|
895
976
|
* @param {object} [options] - forwarded to the archive loader: `element` to load one
|
|
896
|
-
* subtree of a multi-part scene, `pbrtEntry` to choose among several .pbrt scenes.
|
|
977
|
+
* subtree of a multi-part scene, `pbrtEntry` to choose among several .pbrt scenes. For a
|
|
978
|
+
* URL also `filename` and `cacheKey` (see AssetLoader.loadAssetFromUrl).
|
|
897
979
|
* @returns {Promise<void>}
|
|
898
980
|
*/
|
|
899
981
|
async loadFile( file, options = {} ) {
|
|
900
982
|
|
|
983
|
+
options = this._sceneBudgets( options );
|
|
984
|
+
|
|
985
|
+
if ( typeof file === 'string' ) {
|
|
986
|
+
|
|
987
|
+
const filename = options.filename ?? nameFromUrl( file );
|
|
988
|
+
const format = this.assetLoader?.getFileFormat( filename );
|
|
989
|
+
if ( ! format ) throw new Error( `Unsupported file format: ${filename}` );
|
|
990
|
+
if ( format.type === 'environment' || format.type === 'image' ) return this.loadEnvironment( file );
|
|
991
|
+
|
|
992
|
+
await this._loadWithSceneRebuild(
|
|
993
|
+
() => this.assetLoader.loadAssetFromUrl( file, { ...options, filename } ),
|
|
994
|
+
{ type: 'ModelLoaded', filename },
|
|
995
|
+
{ kind: 'url', url: file, filename, cacheKey: options.cacheKey ?? null, ...partOf( options ) }
|
|
996
|
+
);
|
|
997
|
+
return;
|
|
998
|
+
|
|
999
|
+
}
|
|
1000
|
+
|
|
901
1001
|
const format = this.assetLoader?.getFileFormat( file?.name || '' );
|
|
902
1002
|
if ( ! format ) throw new Error( `Unsupported file format: ${file?.name}` );
|
|
903
1003
|
|
|
904
1004
|
if ( format.type !== 'environment' && format.type !== 'image' ) {
|
|
905
1005
|
|
|
1006
|
+
const identity = fileIdentity( file ).catch( () => ( { name: file.name, size: file.size, lastModified: file.lastModified ?? 0, sample: null } ) );
|
|
906
1007
|
await this._loadWithSceneRebuild(
|
|
907
1008
|
() => this.assetLoader.loadAssetFromFile( file, options ),
|
|
908
|
-
{ type: 'ModelLoaded', filename: file.name }
|
|
1009
|
+
{ type: 'ModelLoaded', filename: file.name },
|
|
1010
|
+
async () => {
|
|
1011
|
+
|
|
1012
|
+
const id = await identity;
|
|
1013
|
+
return { kind: 'local-file', file: id, key: id.sample ? identityKey( id ) : null, ...partOf( options ) };
|
|
1014
|
+
|
|
1015
|
+
}
|
|
909
1016
|
);
|
|
1017
|
+
this._sceneSourceFile = file;
|
|
910
1018
|
return;
|
|
911
1019
|
|
|
912
1020
|
}
|
|
@@ -948,7 +1056,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
948
1056
|
|
|
949
1057
|
await this._loadWithSceneRebuild(
|
|
950
1058
|
() => this.assetLoader.loadExampleModels( index, modelFiles ),
|
|
951
|
-
{ type: 'ModelLoaded', index }
|
|
1059
|
+
{ type: 'ModelLoaded', index },
|
|
1060
|
+
{ kind: 'url', url: modelFiles[ index ]?.url ?? null, cacheKey: null }
|
|
952
1061
|
);
|
|
953
1062
|
|
|
954
1063
|
}
|
|
@@ -1009,8 +1118,28 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1009
1118
|
|
|
1010
1119
|
}
|
|
1011
1120
|
|
|
1121
|
+
/**
|
|
1122
|
+
* Where the model on screen came from, as plain data — what a saved session reopens:
|
|
1123
|
+
* `{ kind: 'url', url, cacheKey, filename?, element?, pbrtEntry? }`,
|
|
1124
|
+
* `{ kind: 'local-file', file: {name, size, lastModified, sample}, key, element?, pbrtEntry? }` or
|
|
1125
|
+
* `{ kind: 'object3d', name }`. Null before a load and after {@link unloadScene}.
|
|
1126
|
+
* @returns {?Object}
|
|
1127
|
+
*/
|
|
1128
|
+
get sceneSource() {
|
|
1129
|
+
|
|
1130
|
+
return this._sceneSource ?? null;
|
|
1131
|
+
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/** The File a local load came from, so a host can save a project that carries it; null otherwise. */
|
|
1135
|
+
get sceneSourceFile() {
|
|
1136
|
+
|
|
1137
|
+
return this._sceneSourceFile ?? null;
|
|
1138
|
+
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1012
1141
|
/** Shared pipeline: load asset → sync controls → build BVH → reset → dispatch events */
|
|
1013
|
-
async _loadWithSceneRebuild( loadFn, eventPayload ) {
|
|
1142
|
+
async _loadWithSceneRebuild( loadFn, eventPayload, source = null ) {
|
|
1014
1143
|
|
|
1015
1144
|
if ( this._loadingInProgress ) throw this._busyError( 'PathTracerApp' );
|
|
1016
1145
|
|
|
@@ -1019,6 +1148,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1019
1148
|
try {
|
|
1020
1149
|
|
|
1021
1150
|
await loadFn();
|
|
1151
|
+
this._sceneSource = typeof source === 'function' ? await source() : source;
|
|
1152
|
+
this._sceneSourceFile = null;
|
|
1022
1153
|
// A fresh model re-establishes the emissive-sampling auto-default (incremental
|
|
1023
1154
|
// rebuilds — add/remove object, texture reprocess — preserve the user's choice).
|
|
1024
1155
|
this._emissiveSamplingUserSet = false;
|
|
@@ -1028,6 +1159,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1028
1159
|
this._clearAppendedModels();
|
|
1029
1160
|
this._syncControlsAfterLoad();
|
|
1030
1161
|
await this.loadSceneData( { pendingEnvironment: this._beginSceneMetadataEnvironment() } );
|
|
1162
|
+
this._maybeSpill();
|
|
1031
1163
|
this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
|
|
1032
1164
|
// Not held: the first denoise of a new scene lands ~1.1 s after the load (upload and
|
|
1033
1165
|
// shader compilation come first), and a second of the previous model reads as a bug.
|
|
@@ -1060,6 +1192,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1060
1192
|
*/
|
|
1061
1193
|
_discardFailedLoad() {
|
|
1062
1194
|
|
|
1195
|
+
this._sceneSource = this._sceneSourceFile = null;
|
|
1196
|
+
|
|
1063
1197
|
try {
|
|
1064
1198
|
|
|
1065
1199
|
this._clearAppendedModels();
|
|
@@ -1172,7 +1306,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1172
1306
|
* CDF'd concurrently with the BVH build rather than before it.
|
|
1173
1307
|
* @returns {boolean}
|
|
1174
1308
|
*/
|
|
1175
|
-
async loadSceneData( { pendingEnvironment = null } = {} ) {
|
|
1309
|
+
async loadSceneData( { pendingEnvironment = null, keepUserLights = false } = {} ) {
|
|
1176
1310
|
|
|
1177
1311
|
// Clear selection before rebuilding — the old object leaves the scene graph.
|
|
1178
1312
|
// Skipped on the append path (addModel): the selected object persists, so its
|
|
@@ -1188,7 +1322,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1188
1322
|
this._tagPrimarySceneObject();
|
|
1189
1323
|
|
|
1190
1324
|
const timer = new BuildTimer( '', { namespace: 'scene', level: 'info' } );
|
|
1191
|
-
|
|
1325
|
+
// meshScene.environment is the HDRI slot: a sky, gradient or colour in use outlives the model.
|
|
1326
|
+
const hdri = () => this.stages.pathTracer.environment.envParams.mode === 'hdri' ? this.meshScene.environment : null;
|
|
1327
|
+
const environmentTexture = hdri();
|
|
1192
1328
|
|
|
1193
1329
|
// Environment CDF build in parallel with BVH
|
|
1194
1330
|
let cdfPromise = null;
|
|
@@ -1231,7 +1367,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1231
1367
|
// Build BVH
|
|
1232
1368
|
timer.start( 'BVH build (SceneProcessor)' );
|
|
1233
1369
|
this._sdf.setMaxTextureSize( this._maxTextureSize );
|
|
1234
|
-
await this._sdf.buildBVH( this.meshScene );
|
|
1370
|
+
await this._sdf.buildBVH( this.meshScene, { sceneKey: this.assetLoader?.sceneSourceKey ?? null, progressive: this._progressiveSpill() } );
|
|
1371
|
+
this.assetLoader?.flushPendingGraph( this._sdf.performanceMetrics.totalProcessingTime );
|
|
1235
1372
|
timer.end( 'BVH build (SceneProcessor)' );
|
|
1236
1373
|
|
|
1237
1374
|
// Transfer geometry, materials, and textures to GPU
|
|
@@ -1241,7 +1378,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1241
1378
|
|
|
1242
1379
|
// Re-read rather than reusing the snapshot above: a pendingEnvironment may have landed
|
|
1243
1380
|
// during the BVH build, and the snapshot is then a disposed texture.
|
|
1244
|
-
if ( ! this._sdf.uploadToPathTracer( this.stages.pathTracer, this.lightManager, this.meshScene,
|
|
1381
|
+
if ( ! this._sdf.uploadToPathTracer( this.stages.pathTracer, this.lightManager, this.meshScene, hdri(), { keepUserLights } ) ) return false;
|
|
1245
1382
|
|
|
1246
1383
|
// Patch per-mesh visibility into the TLAS leaves we just uploaded
|
|
1247
1384
|
this.stages.pathTracer._meshRefs = this.stages.pathTracer._collectMeshRefs( this.meshScene );
|
|
@@ -1369,7 +1506,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1369
1506
|
/** Reframe-free rebuild sequence. Assumes the _loadingInProgress guard is already held. */
|
|
1370
1507
|
async _finishRebuildNoReframe( eventPayload ) {
|
|
1371
1508
|
|
|
1372
|
-
await this.loadSceneData(); // emits 'SceneRebuild'
|
|
1509
|
+
await this.loadSceneData( { keepUserLights: true } ); // emits 'SceneRebuild'
|
|
1510
|
+
this._maybeSpill();
|
|
1373
1511
|
this._recalibrateControlLimits(); // scene bounds changed — retune zoom limits + near/far (no camera move)
|
|
1374
1512
|
this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
|
|
1375
1513
|
this.reset();
|
|
@@ -1385,7 +1523,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1385
1523
|
* @param {string} [opts.name] - Display name for the scene-object list.
|
|
1386
1524
|
* @returns {Promise<string>} the new object's id (Object3D uuid).
|
|
1387
1525
|
*/
|
|
1388
|
-
async addModel( url, { name } = {} ) {
|
|
1526
|
+
async addModel( url, { name, cacheKey } = {} ) {
|
|
1389
1527
|
|
|
1390
1528
|
if ( this._loadingInProgress ) {
|
|
1391
1529
|
|
|
@@ -1397,8 +1535,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1397
1535
|
this._preserveSelectionOnRebuild = true;
|
|
1398
1536
|
try {
|
|
1399
1537
|
|
|
1400
|
-
const { root } = await this.assetLoader.appendModel( url );
|
|
1538
|
+
const { root } = await this.assetLoader.appendModel( url, { cacheKey } );
|
|
1401
1539
|
root.userData.__rayzeeSceneObject = true;
|
|
1540
|
+
root.userData.__rayzeeSourceUrl = url;
|
|
1541
|
+
if ( cacheKey ) root.userData.__rayzeeCacheKey = cacheKey;
|
|
1402
1542
|
if ( name ) root.userData.__rayzeeName = name;
|
|
1403
1543
|
await this._finishRebuildNoReframe( { type: 'ModelAdded', url, id: root.uuid } );
|
|
1404
1544
|
return root.uuid;
|
|
@@ -1551,6 +1691,75 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1551
1691
|
|
|
1552
1692
|
}
|
|
1553
1693
|
|
|
1694
|
+
/**
|
|
1695
|
+
* Everything changed since the scene loaded — render settings, environment, colour, lights,
|
|
1696
|
+
* cameras, timeline keys, material edits, hidden and moved objects — as JSON-safe data. The
|
|
1697
|
+
* model is not in it: {@link importSceneState} applies it once the host has loaded that again.
|
|
1698
|
+
* @returns {Object}
|
|
1699
|
+
*/
|
|
1700
|
+
exportSceneState() {
|
|
1701
|
+
|
|
1702
|
+
return captureSceneState( this );
|
|
1703
|
+
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/**
|
|
1707
|
+
* Applies {@link exportSceneState}'s data to the scene now loaded. Objects, materials and
|
|
1708
|
+
* cameras are matched by position plus name; a scene that differs keeps its own objects and
|
|
1709
|
+
* materials and the rest still applies.
|
|
1710
|
+
* @param {Object} state
|
|
1711
|
+
* @param {Object} [options]
|
|
1712
|
+
* @param {function(Object): Promise<*>} [options.resolve] - supplies what the engine cannot
|
|
1713
|
+
* reach: `{ kind: 'environment', source }` → File | URL | null, `{ kind: 'colorConfig', config }`
|
|
1714
|
+
* → true once loaded. Without it, URL environments and built-in colour configs still restore.
|
|
1715
|
+
* @returns {Promise<{skipped: Array<{section: string, reason: string}>}>}
|
|
1716
|
+
*/
|
|
1717
|
+
async importSceneState( state, options ) {
|
|
1718
|
+
|
|
1719
|
+
return await applySceneState( this, state, options );
|
|
1720
|
+
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/** Triangle and placement budgets for a scene archive: raised when the build can spill. @private */
|
|
1724
|
+
_sceneBudgets( options ) {
|
|
1725
|
+
|
|
1726
|
+
if ( ! this._memorySpill || ! this.storage ) return options;
|
|
1727
|
+
return { maxTriangles: SPILL_TRIANGLE_BUDGET, maxPlacements: SPILL_PLACEMENT_BUDGET, ...options };
|
|
1728
|
+
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/**
|
|
1732
|
+
* Experimental memory spill, during the build: static scenes only, since a deforming clip
|
|
1733
|
+
* refits every frame. @private
|
|
1734
|
+
*/
|
|
1735
|
+
_progressiveSpill() {
|
|
1736
|
+
|
|
1737
|
+
if ( ! this._memorySpill || ! this.storage || ( this.assetLoader?.animations?.length ?? 0 ) > 0 ) return null;
|
|
1738
|
+
const stage = this.stages.pathTracer;
|
|
1739
|
+
return { storage: this.storage, uploader: ( records, kind ) => kind === 'triangles' ? stage.createTriangleChunkUploader( records ) : stage.createChunkUploader( records ) };
|
|
1740
|
+
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/** Experimental memory spill for a static scene, in the background. @private */
|
|
1744
|
+
_maybeSpill() {
|
|
1745
|
+
|
|
1746
|
+
if ( ! this._memorySpill || ! this.storage || this.animationManager?.hasAnimations ) return;
|
|
1747
|
+
this._sdf?.spillToDisk( this.storage )
|
|
1748
|
+
.then( result => result && this.dispatchEvent( { type: 'SceneSpilled', ...result } ) )
|
|
1749
|
+
.catch( error => this._issues.warn( ISSUE_CODES.STORAGE_WRITE_FAILED, `memory spill failed: ${error.message}` ) );
|
|
1750
|
+
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* Reads back what {@link options.memorySpill} moved to disk. The engine does this itself before
|
|
1755
|
+
* its own readers; a host calling {@link refitBLASes} on a spilled scene awaits it first.
|
|
1756
|
+
*/
|
|
1757
|
+
async ensureSceneResident() {
|
|
1758
|
+
|
|
1759
|
+
if ( this._sdf?.spilled ) await this._sdf.ensureResident();
|
|
1760
|
+
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1554
1763
|
/** Notify consumers that the camera list changed (names / count). */
|
|
1555
1764
|
_dispatchCamerasUpdated() {
|
|
1556
1765
|
|
|
@@ -1629,6 +1838,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1629
1838
|
*/
|
|
1630
1839
|
async refitBVH( newPositions, newNormals ) {
|
|
1631
1840
|
|
|
1841
|
+
await this.ensureSceneResident();
|
|
1632
1842
|
const result = await this._sdf.refitBVH( newPositions, newNormals );
|
|
1633
1843
|
|
|
1634
1844
|
this.stages.pathTracer.updateTriangleData( this._sdf.triangles );
|
|
@@ -1651,6 +1861,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1651
1861
|
*/
|
|
1652
1862
|
refitBLASes( affectedMeshIndices, newPositions, newNormals ) {
|
|
1653
1863
|
|
|
1864
|
+
if ( this._sdf?.spilled ) throw new Error( 'refitBLASes: the scene is spilled to disk — await app.ensureSceneResident() first' );
|
|
1654
1865
|
const result = this._sdf.refitBLASes( affectedMeshIndices, newPositions, newNormals );
|
|
1655
1866
|
|
|
1656
1867
|
const { triRanges, bvhRanges } = this._sdf.computeBLASDirtyRanges( affectedMeshIndices );
|
|
@@ -1731,6 +1942,13 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1731
1942
|
_refreshMovedEmitters() {
|
|
1732
1943
|
|
|
1733
1944
|
if ( ! this._emittersMoved ) return;
|
|
1945
|
+
if ( this._sdf?.spilled ) {
|
|
1946
|
+
|
|
1947
|
+
this.ensureSceneResident().then( () => this._refreshMovedEmitters() );
|
|
1948
|
+
return;
|
|
1949
|
+
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1734
1952
|
this._emittersMoved = false;
|
|
1735
1953
|
this._uploadEmissivePayload( this._sdf?.refreshEmissiveTransforms() ?? null );
|
|
1736
1954
|
|
|
@@ -2104,6 +2322,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2104
2322
|
// renderMode has no SETTING_ROUTES entry
|
|
2105
2323
|
this.stages.pathTracer?.setUniform( 'renderMode', parseInt( config.renderMode ) );
|
|
2106
2324
|
|
|
2325
|
+
// A move just before this left the moving-camera drop in force or queued; a final render
|
|
2326
|
+
// never runs at it.
|
|
2327
|
+
if ( isProduction ) this._applyRenderScale( 1 );
|
|
2328
|
+
|
|
2107
2329
|
this.stages.pathTracer?.updateCompletionThreshold?.();
|
|
2108
2330
|
|
|
2109
2331
|
const denoiser = this.denoisingManager?.denoiser;
|
|
@@ -2447,6 +2669,41 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2447
2669
|
|
|
2448
2670
|
}
|
|
2449
2671
|
|
|
2672
|
+
/**
|
|
2673
|
+
* A render in progress as data: the accumulated colour (and the aux buffers while they hold
|
|
2674
|
+
* samples), the per-pixel convergence buffers, and the counters that choose the next sample.
|
|
2675
|
+
* {@link restoreRenderCheckpoint} continues it — bit-for-bit in deterministic mode, since the
|
|
2676
|
+
* random sequence is a pure function of pixel, frame and seed tick. At 4K it is ~500 MB.
|
|
2677
|
+
* @returns {Promise<?Object>} null before the path tracer exists
|
|
2678
|
+
*/
|
|
2679
|
+
async captureRenderCheckpoint() {
|
|
2680
|
+
|
|
2681
|
+
const stage = this.stages.pathTracer;
|
|
2682
|
+
if ( ! stage ) return null;
|
|
2683
|
+
const accumulation = await stage.captureAccumulation();
|
|
2684
|
+
return { v: RENDER_CHECKPOINT_VERSION, engine: VERSION, savedAt: Date.now(), samples: accumulation.state.frameCount, ...accumulation };
|
|
2685
|
+
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
/**
|
|
2689
|
+
* Continues a {@link captureRenderCheckpoint}. The scene, settings and render size must be the
|
|
2690
|
+
* ones it was taken with (restore the scene state first), and the scene must have rendered a
|
|
2691
|
+
* frame at that size since it last changed. Anything that resets accumulation afterwards
|
|
2692
|
+
* discards it. Rendering is not restarted: call {@link wake} or {@link renderFrames}.
|
|
2693
|
+
* @param {Object} checkpoint
|
|
2694
|
+
*/
|
|
2695
|
+
restoreRenderCheckpoint( checkpoint ) {
|
|
2696
|
+
|
|
2697
|
+
if ( checkpoint?.v !== RENDER_CHECKPOINT_VERSION ) throw new Error( `render checkpoint version ${checkpoint?.v} is not ${RENDER_CHECKPOINT_VERSION}` );
|
|
2698
|
+
const stage = this.stages.pathTracer;
|
|
2699
|
+
if ( ! stage ) throw new Error( 'restoreRenderCheckpoint: app is not initialized' );
|
|
2700
|
+
|
|
2701
|
+
stage.restoreAccumulation( checkpoint );
|
|
2702
|
+
this.needsReset = false;
|
|
2703
|
+
this.dispatchEvent( { type: EngineEvents.RENDER_RESET, restored: checkpoint.samples } );
|
|
2704
|
+
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2450
2707
|
/**
|
|
2451
2708
|
* The path tracer's accumulation as pixels, read from its storage target rather than the
|
|
2452
2709
|
* canvas — so it works headless, works while the page is hidden, and cannot pick up a
|
|
@@ -2952,6 +3209,14 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2952
3209
|
|
|
2953
3210
|
setMaterialProperty( materialIndex, property, value ) {
|
|
2954
3211
|
|
|
3212
|
+
// These rewrite triangle records, which a spilled scene has to read back first.
|
|
3213
|
+
if ( this._sdf?.spilled && TRIANGLE_PATCH_PROPERTIES.has( property ) ) {
|
|
3214
|
+
|
|
3215
|
+
this.ensureSceneResident().then( () => this.setMaterialProperty( materialIndex, property, value ) );
|
|
3216
|
+
return;
|
|
3217
|
+
|
|
3218
|
+
}
|
|
3219
|
+
|
|
2955
3220
|
this.stages.pathTracer?.materialData.updateMaterialProperty( materialIndex, property, value );
|
|
2956
3221
|
|
|
2957
3222
|
// Keep the emissive-NEE structure in sync unconditionally (not gated on the
|
|
@@ -3255,6 +3520,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3255
3520
|
}
|
|
3256
3521
|
|
|
3257
3522
|
if ( envChanged ) await this.environmentManager?.buildEnvironmentCDF?.();
|
|
3523
|
+
// Its pixels never leave the GPU, so it is baked again in the new space rather than converted.
|
|
3524
|
+
if ( env?._isPhysicalSky ) await this.environmentManager.generateProceduralSkyTexture();
|
|
3525
|
+
this.environmentManager?.refreshSun?.();
|
|
3258
3526
|
|
|
3259
3527
|
this.reset();
|
|
3260
3528
|
|
|
@@ -3266,6 +3534,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3266
3534
|
*/
|
|
3267
3535
|
async rebuildMaterials( scene ) {
|
|
3268
3536
|
|
|
3537
|
+
await this.ensureSceneResident();
|
|
3269
3538
|
await this.stages.pathTracer?.rebuildMaterials( scene || this.meshScene );
|
|
3270
3539
|
this.reset();
|
|
3271
3540
|
|
|
@@ -3531,6 +3800,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3531
3800
|
// Expose environment manager (lives on pathTracer stage)
|
|
3532
3801
|
this.environmentManager = this.stages.pathTracer.environment;
|
|
3533
3802
|
this.environmentManager.callbacks.onAutoExposureReset = () => this.pipeline.eventBus.emit( 'autoexposure:resetHistory' );
|
|
3803
|
+
// A whole-app reset, not the stage's: a sky bake lands after its input, often once the loop is idle.
|
|
3804
|
+
this.environmentManager.callbacks.onReset = () => this.reset();
|
|
3534
3805
|
|
|
3535
3806
|
}
|
|
3536
3807
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createTarIndexer } from './ArchiveReader.js';
|
|
2
|
+
import { ENGINE_AREAS } from '../Storage/StorageManager.js';
|
|
3
|
+
import { fileIdentity, identityKey } from '../Storage/identity.js';
|
|
4
|
+
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
5
|
+
|
|
6
|
+
const INDEX_VERSION = 1;
|
|
7
|
+
// Scene text compresses 3-5×; reserving too little only means the write fails and falls back.
|
|
8
|
+
const GUESSED_EXPANSION = 3;
|
|
9
|
+
|
|
10
|
+
export async function archiveKey( kind, file ) {
|
|
11
|
+
|
|
12
|
+
return `${kind}:${identityKey( await fileIdentity( file ) )}`;
|
|
13
|
+
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Decompresses a .tar.gz into storage once, indexing it on the way, and hands back the unpacked
|
|
18
|
+
* .tar as a seekable File. A second call for the same file skips the work.
|
|
19
|
+
* @returns {Promise<?{file: File, index: {v:number, listing:Array}, release: function(): void, cached: boolean}>}
|
|
20
|
+
* null when storage cannot take it — the caller reads the archive the old way.
|
|
21
|
+
*/
|
|
22
|
+
export async function unpackTarGz( file, { storage, label = file.name, onProgress = null } ) {
|
|
23
|
+
|
|
24
|
+
if ( ! storage || typeof DecompressionStream === 'undefined' ) return null;
|
|
25
|
+
|
|
26
|
+
const area = storage.area( ENGINE_AREAS.ARCHIVES );
|
|
27
|
+
const key = await archiveKey( 'gunzip', file );
|
|
28
|
+
|
|
29
|
+
let entry = await area.open( key );
|
|
30
|
+
const cached = !! entry;
|
|
31
|
+
|
|
32
|
+
if ( ! entry ) {
|
|
33
|
+
|
|
34
|
+
const writer = await area.create( key, { label, expectedBytes: file.size * GUESSED_EXPANSION } );
|
|
35
|
+
if ( ! writer ) return null;
|
|
36
|
+
|
|
37
|
+
const indexer = createTarIndexer();
|
|
38
|
+
const tap = new TransformStream( {
|
|
39
|
+
transform( chunk, controller ) {
|
|
40
|
+
|
|
41
|
+
indexer.push( chunk );
|
|
42
|
+
controller.enqueue( chunk );
|
|
43
|
+
|
|
44
|
+
},
|
|
45
|
+
} );
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
|
|
49
|
+
const unpacked = file.stream().pipeThrough( new DecompressionStream( 'gzip' ) ).pipeThrough( tap );
|
|
50
|
+
const tarBytes = await writer.writeStream( 'data.tar', unpacked, { onProgress } );
|
|
51
|
+
await writer.writeJSON( 'index.json', { v: INDEX_VERSION, listing: indexer.finish() } );
|
|
52
|
+
await writer.commit( { compressedBytes: file.size, tarBytes } );
|
|
53
|
+
|
|
54
|
+
} catch ( error ) {
|
|
55
|
+
|
|
56
|
+
await writer.abort();
|
|
57
|
+
if ( error?.name !== 'QuotaExceededError' ) throw error;
|
|
58
|
+
storage._issue( ISSUE_CODES.STORAGE_QUOTA_EXCEEDED, `ran out of room unpacking ${label}; reading it in memory instead`, { area: ENGINE_AREAS.ARCHIVES } );
|
|
59
|
+
return null;
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
entry = await area.open( key, { wait: true } );
|
|
64
|
+
if ( ! entry ) return null;
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tar = await entry.file( 'data.tar' );
|
|
69
|
+
const index = await entry.json( 'index.json' );
|
|
70
|
+
if ( ! tar || index?.v !== INDEX_VERSION ) {
|
|
71
|
+
|
|
72
|
+
entry.release();
|
|
73
|
+
return null;
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { file: tar, index, release: () => entry.release(), cached };
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A plain .tar's saved header index, so reopening it skips even the header walk. */
|
|
82
|
+
export async function loadTarIndex( file, storage ) {
|
|
83
|
+
|
|
84
|
+
if ( ! storage ) return null;
|
|
85
|
+
const entry = await storage.area( ENGINE_AREAS.ARCHIVES ).open( await archiveKey( 'tarindex', file ) );
|
|
86
|
+
if ( ! entry ) return null;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
|
|
90
|
+
const index = await entry.json( 'index.json' );
|
|
91
|
+
return index?.v === INDEX_VERSION ? index : null;
|
|
92
|
+
|
|
93
|
+
} finally {
|
|
94
|
+
|
|
95
|
+
entry.release();
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function saveTarIndex( file, storage, index ) {
|
|
102
|
+
|
|
103
|
+
if ( ! storage || ! index ) return false;
|
|
104
|
+
const writer = await storage.area( ENGINE_AREAS.ARCHIVES ).create( await archiveKey( 'tarindex', file ), { label: `${file.name} (index)` } );
|
|
105
|
+
if ( ! writer ) return false;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
|
|
109
|
+
await writer.writeJSON( 'index.json', index );
|
|
110
|
+
await writer.commit();
|
|
111
|
+
return true;
|
|
112
|
+
|
|
113
|
+
} catch {
|
|
114
|
+
|
|
115
|
+
await writer.abort();
|
|
116
|
+
return false;
|
|
117
|
+
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
}
|