rayzee 9.0.0 → 9.1.1
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 +10353 -5337
- 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/KernelManager.js +4 -1
- 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
|
@@ -4,7 +4,7 @@ import { BVHRefitter } from './BVHRefitter.js';
|
|
|
4
4
|
import { buildBVHParallel, shouldUseParallelBuild } from './ParallelBVHBuilder.js';
|
|
5
5
|
import { TLASBuilder } from './TLASBuilder.js';
|
|
6
6
|
import { InstanceTable, isIdentity, multiplyAffine } from './InstanceTable.js';
|
|
7
|
-
import { ChunkedRecords, SHARED_MEMORY_AVAILABLE, setChunkObserver } from './ChunkedRecords.js';
|
|
7
|
+
import { ChunkedRecords, SHARED_MEMORY_AVAILABLE, setChunkObserver, DEFAULT_CHUNK_BYTES } from './ChunkedRecords.js';
|
|
8
8
|
import {
|
|
9
9
|
MemoryLedger, estimateSceneBytes, probeAddressSpace,
|
|
10
10
|
PREFLIGHT_MIN_BYTES, PREFLIGHT_SAFETY, SAFE_SCENE_BYTES, MAX_SCENE_BYTES,
|
|
@@ -18,8 +18,17 @@ import { createLogger, fmt, workerLogLevel } from '../utils/Logger.js';
|
|
|
18
18
|
import { SRGBColorSpace } from 'three';
|
|
19
19
|
import {
|
|
20
20
|
TRIANGLE_DATA_LAYOUT, TEXTURE_CONSTANTS, getTextureBucketId, packTextureIndex, planTextureBuckets,
|
|
21
|
-
packNormalOct, BVH_LEAF_MARKERS, assertBVHIndexFits, bvhIndexView, TLAS_PLACEMENT_MASK } from '../EngineDefaults.js';
|
|
21
|
+
packNormalOct, BVH_LEAF_MARKERS, BVH_FOLDED_FIRST_LIMIT, assertBVHIndexFits, bvhIndexView, TLAS_PLACEMENT_MASK, TRI_MATERIAL_MASK } from '../EngineDefaults.js';
|
|
22
22
|
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
23
|
+
import { BLAS_CACHE_FORMAT, openBLASCache, saveBLASCache, templateChecksums, readNodesInto, readOrder } from '../Storage/BLASCache.js';
|
|
24
|
+
import { sharedStorage } from '../Storage/shared.js';
|
|
25
|
+
import { SpillStore } from '../Storage/SpillStore.js';
|
|
26
|
+
import { GeometrySpill } from '../Storage/GeometrySpill.js';
|
|
27
|
+
import { partitionRange, joinPieces } from './SplitBLAS.js';
|
|
28
|
+
import { rebaseNodes } from './BVHLeafFold.js';
|
|
29
|
+
import { getAssetConfig } from '../AssetConfig.js';
|
|
30
|
+
import { VERSION } from '../version.js';
|
|
31
|
+
import { SCENE_CACHE_MIN_BUILD_MS } from '../Storage/sceneCachePolicy.js';
|
|
23
32
|
import BVHWorker from './Workers/BVHWorker.js?worker&inline';
|
|
24
33
|
import BVHRefitWorker from './Workers/BVHRefitWorker.js?worker&inline';
|
|
25
34
|
import TLASWorker from './Workers/TLASWorker.js?worker&inline';
|
|
@@ -27,8 +36,50 @@ import TLASWorker from './Workers/TLASWorker.js?worker&inline';
|
|
|
27
36
|
// Under this the TLAS build is a few ms; the worker round trip would cost more than it saves.
|
|
28
37
|
const TLAS_WORKER_MIN_ENTRIES = 50_000;
|
|
29
38
|
|
|
39
|
+
// Meshes past this build across every core, one at a time.
|
|
40
|
+
const LARGE_MESH_TRIANGLES = 200000;
|
|
41
|
+
// Past this a mesh is built as spatial pieces of at most SPLIT_PIECE_TRIANGLES, joined into one BLAS.
|
|
42
|
+
const SPLIT_MESH_TRIANGLES = 2_000_000;
|
|
43
|
+
const SPLIT_PIECE_TRIANGLES = 1 << 19;
|
|
44
|
+
// Past this many stored triangles, small leaves are folded into their parents: the BVH halves,
|
|
45
|
+
// at up to 4 % of traversal time, so only a scene whose BVH nears 2 GB pays it.
|
|
46
|
+
const FOLD_LEAVES_TRIANGLES = 40_000_000;
|
|
47
|
+
// A streamed build's extraction waits while more triangle records than this are in memory.
|
|
48
|
+
const STREAM_RESIDENT_BYTES = 1.5 * 1024 ** 3;
|
|
49
|
+
// Extraction waits while more three.js geometry than this is queued for disk.
|
|
50
|
+
const GEOMETRY_QUEUE_BYTES = 256 * 1024 * 1024;
|
|
51
|
+
|
|
30
52
|
const log = createLogger( 'scene' );
|
|
31
53
|
|
|
54
|
+
// A BLAS's nodes: one array, or a split mesh's parts in order — never one array that large.
|
|
55
|
+
const nodeParts = nodes => ( Array.isArray( nodes ) ? nodes : [ nodes ] );
|
|
56
|
+
const nodeBytes = nodes => nodeParts( nodes ).reduce( ( sum, part ) => sum + part.byteLength, 0 );
|
|
57
|
+
const READ_PART_BYTES = 64 * 1024 * 1024;
|
|
58
|
+
|
|
59
|
+
async function writeNodes( store, at, nodes ) {
|
|
60
|
+
|
|
61
|
+
for ( const part of nodeParts( nodes ) ) {
|
|
62
|
+
|
|
63
|
+
await store.writeAt( at, part );
|
|
64
|
+
at += part.byteLength;
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function largestMeshTriangles( object ) {
|
|
71
|
+
|
|
72
|
+
let largest = 0;
|
|
73
|
+
object.traverse( node => {
|
|
74
|
+
|
|
75
|
+
const g = node.isMesh ? node.geometry : null;
|
|
76
|
+
if ( g ) largest = Math.max( largest, Math.ceil( ( g.index ? g.index.count : ( g.attributes.position?.count ?? 0 ) ) / 3 ) );
|
|
77
|
+
|
|
78
|
+
} );
|
|
79
|
+
return largest;
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
32
83
|
/**
|
|
33
84
|
* SceneProcessor - Processes scene geometry into GPU-ready data:
|
|
34
85
|
* BVH acceleration, texture atlas, material buffers.
|
|
@@ -72,6 +123,9 @@ export class SceneProcessor {
|
|
|
72
123
|
treeletMinImprovement: 0.01, // Minimum SAH improvement threshold
|
|
73
124
|
// Above this triangle count the builder drops treelets to size 3.
|
|
74
125
|
treeletComplexityThreshold: 50000,
|
|
126
|
+
// Store the per-mesh BVHs of a scene whose build took at least this long (needs a sceneKey).
|
|
127
|
+
sceneCache: true,
|
|
128
|
+
sceneCacheMinBuildMs: SCENE_CACHE_MIN_BUILD_MS,
|
|
75
129
|
...options
|
|
76
130
|
};
|
|
77
131
|
|
|
@@ -234,6 +288,18 @@ export class SceneProcessor {
|
|
|
234
288
|
const maxBytes = this.config.maxSceneBytes ?? MAX_SCENE_BYTES;
|
|
235
289
|
const survey = this.geometryExtractor.surveyScene( object );
|
|
236
290
|
const estimate = estimateSceneBytes( survey );
|
|
291
|
+
// Spilled as it is built, the BVH is never resident as a whole.
|
|
292
|
+
if ( this._progressive ) estimate.total -= estimate.bvh;
|
|
293
|
+
// Streamed, neither are the triangles: what is held is the resident cap plus the largest
|
|
294
|
+
// mesh in the parallel builder — its copy and 44 B a triangle of scratch, the store's own
|
|
295
|
+
// chunks for it let go meanwhile.
|
|
296
|
+
if ( this._streaming ) {
|
|
297
|
+
|
|
298
|
+
const resident = STREAM_RESIDENT_BYTES + largestMeshTriangles( object ) * ( TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE * 4 + 44 );
|
|
299
|
+
estimate.total -= Math.max( 0, estimate.triangles - resident );
|
|
300
|
+
|
|
301
|
+
}
|
|
302
|
+
|
|
237
303
|
const report = {
|
|
238
304
|
...survey, estimate,
|
|
239
305
|
safeBytes: SAFE_SCENE_BYTES, maxBytes,
|
|
@@ -332,7 +398,7 @@ export class SceneProcessor {
|
|
|
332
398
|
|
|
333
399
|
if ( table ) {
|
|
334
400
|
|
|
335
|
-
for ( const blas of table.blasData.values() ) parts.blasScratch += blas
|
|
401
|
+
for ( const blas of table.blasData.values() ) parts.blasScratch += blas ? nodeBytes( blas ) : 0;
|
|
336
402
|
for ( const m of table.bvhToOriginal.values() ) parts.orderMaps += m?.byteLength ?? 0;
|
|
337
403
|
for ( const m of table.originalToBvhMap.values() ) parts.orderMaps += m?.byteLength ?? 0;
|
|
338
404
|
|
|
@@ -405,7 +471,11 @@ export class SceneProcessor {
|
|
|
405
471
|
* @param {Object3D} object - Three.js object to process
|
|
406
472
|
* @returns {Promise<SceneProcessor>} - This instance (for chaining)
|
|
407
473
|
*/
|
|
408
|
-
|
|
474
|
+
/**
|
|
475
|
+
* @param {Object3D} object
|
|
476
|
+
* @param {{sceneKey?: ?string}} [options] - what the scene was loaded from; enables the BLAS cache
|
|
477
|
+
*/
|
|
478
|
+
async buildBVH( object, { sceneKey = null, progressive = null } = {} ) {
|
|
409
479
|
|
|
410
480
|
if ( this.isProcessing ) {
|
|
411
481
|
|
|
@@ -426,6 +496,15 @@ export class SceneProcessor {
|
|
|
426
496
|
|
|
427
497
|
// Reset state before beginning
|
|
428
498
|
this._reset();
|
|
499
|
+
this._sceneKey = sceneKey;
|
|
500
|
+
this._blasKey = null;
|
|
501
|
+
this._blasRestored = false;
|
|
502
|
+
this._progressive = progressive?.storage && progressive.uploader ? progressive : null;
|
|
503
|
+
const stored = this.geometryExtractor._countStoredTriangles( object );
|
|
504
|
+
// A spilling build of more than one chunk extracts and builds its BLASes together.
|
|
505
|
+
this._streaming = !! this._progressive && stored > Math.floor( DEFAULT_CHUNK_BYTES / ( TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE * 4 ) );
|
|
506
|
+
this._foldLeaves = stored > FOLD_LEAVES_TRIANGLES;
|
|
507
|
+
this.bvhBuilder.foldLeaves = this._foldLeaves;
|
|
429
508
|
this._log( 'Starting scene processing' );
|
|
430
509
|
|
|
431
510
|
// Step 0: will this scene fit in the address space this process has left?
|
|
@@ -436,7 +515,8 @@ export class SceneProcessor {
|
|
|
436
515
|
this.processingStage = 'extraction';
|
|
437
516
|
this.memory.mark( 'extraction' );
|
|
438
517
|
timer.start( 'Geometry extraction' );
|
|
439
|
-
await this.
|
|
518
|
+
if ( this._streaming ) await this._extractStreaming( object );
|
|
519
|
+
else await this._extractGeometry( object );
|
|
440
520
|
timer.end( 'Geometry extraction' );
|
|
441
521
|
this.performanceMetrics.geometryExtractionTime = timer.getDuration( 'Geometry extraction' );
|
|
442
522
|
// The preflight figure was taken before `_compressAttributes` halved normals and
|
|
@@ -502,7 +582,14 @@ export class SceneProcessor {
|
|
|
502
582
|
timer.print();
|
|
503
583
|
|
|
504
584
|
this.processingStage = 'complete';
|
|
585
|
+
await this._restoreGeometry( object );
|
|
505
586
|
updateLoading( { status: "Scene data ready", progress: 85 } );
|
|
587
|
+
if ( this._blasKey && ! this._blasRestored && this.performanceMetrics.totalProcessingTime >= this.config.sceneCacheMinBuildMs ) {
|
|
588
|
+
|
|
589
|
+
this._blasStoring = this._storeBLASes( object.name ?? '' ).catch( ( error ) => log.debug( 'storing BLASes failed:', error ) );
|
|
590
|
+
|
|
591
|
+
}
|
|
592
|
+
|
|
506
593
|
return this;
|
|
507
594
|
|
|
508
595
|
} catch ( error ) {
|
|
@@ -518,6 +605,8 @@ export class SceneProcessor {
|
|
|
518
605
|
|
|
519
606
|
} finally {
|
|
520
607
|
|
|
608
|
+
// A failed build's model is discarded, so its geometry is not read back.
|
|
609
|
+
this._disposeGeometrySpill();
|
|
521
610
|
setChunkObserver( null );
|
|
522
611
|
this._sampleMemory( `at ${this.processingStage}` );
|
|
523
612
|
this._logMemoryReport();
|
|
@@ -548,46 +637,7 @@ export class SceneProcessor {
|
|
|
548
637
|
|
|
549
638
|
try {
|
|
550
639
|
|
|
551
|
-
|
|
552
|
-
const extractedData = this.geometryExtractor.extract( object );
|
|
553
|
-
|
|
554
|
-
this._setTriangleData( extractedData.triangleData );
|
|
555
|
-
this.triangleCount = assertBVHIndexFits( extractedData.triangleCount, 'triangle count' );
|
|
556
|
-
// Callers build refit buffers by walking meshes, which counts a shared geometry
|
|
557
|
-
// once per placement; storage counts it once.
|
|
558
|
-
this.expandedTriangleCount = extractedData.expandedTriangleCount ?? extractedData.triangleCount;
|
|
559
|
-
this.instanceSource = extractedData.instanceSource || null;
|
|
560
|
-
this.instanceMatrices = extractedData.instanceMatrices || null;
|
|
561
|
-
this.instanceCount = extractedData.instanceCount || 0;
|
|
562
|
-
this.bakeInverse = extractedData.bakeInverse || null;
|
|
563
|
-
|
|
564
|
-
this._log( `Using Float32Array format: ${this.triangleCount} triangles, ${( this.triangles.byteLength / ( 1024 * 1024 ) ).toFixed( 2 )}MB` );
|
|
565
|
-
|
|
566
|
-
// Store other extracted data
|
|
567
|
-
this.materials = extractedData.materials;
|
|
568
|
-
this.materialTriangleCounts = extractedData.materialTriangleCounts; // Per-material tri count for sort-bin remap
|
|
569
|
-
this.meshes = extractedData.meshes;
|
|
570
|
-
this.meshTriangleRanges = extractedData.meshTriangleRanges; // Per-mesh { start, count } for TLAS/BLAS
|
|
571
|
-
this.maps = extractedData.maps;
|
|
572
|
-
this.normalMaps = extractedData.normalMaps;
|
|
573
|
-
this.bumpMaps = extractedData.bumpMaps;
|
|
574
|
-
this.roughnessMaps = extractedData.roughnessMaps;
|
|
575
|
-
this.metalnessMaps = extractedData.metalnessMaps;
|
|
576
|
-
this.emissiveMaps = extractedData.emissiveMaps;
|
|
577
|
-
this.displacementMaps = extractedData.displacementMaps;
|
|
578
|
-
this.anisotropyMaps = extractedData.anisotropyMaps;
|
|
579
|
-
this.transmissionMaps = extractedData.transmissionMaps;
|
|
580
|
-
this.clearcoatMaps = extractedData.clearcoatMaps;
|
|
581
|
-
this.clearcoatRoughnessMaps = extractedData.clearcoatRoughnessMaps;
|
|
582
|
-
this.sheenColorMaps = extractedData.sheenColorMaps;
|
|
583
|
-
this.sheenRoughnessMaps = extractedData.sheenRoughnessMaps;
|
|
584
|
-
this.iridescenceMaps = extractedData.iridescenceMaps;
|
|
585
|
-
this.iridescenceThicknessMaps = extractedData.iridescenceThicknessMaps;
|
|
586
|
-
this.specularIntensityMaps = extractedData.specularIntensityMaps;
|
|
587
|
-
this.specularColorMaps = extractedData.specularColorMaps;
|
|
588
|
-
this.directionalLights = extractedData.directionalLights;
|
|
589
|
-
this.cameras = extractedData.cameras;
|
|
590
|
-
|
|
640
|
+
this._adoptExtraction( this.geometryExtractor.extract( object ) );
|
|
591
641
|
const duration = performance.now() - startTime;
|
|
592
642
|
this._log( `Geometry extraction complete (${duration.toFixed( 2 )}ms)`, {
|
|
593
643
|
triangleCount: this.triangleCount,
|
|
@@ -613,6 +663,224 @@ export class SceneProcessor {
|
|
|
613
663
|
|
|
614
664
|
}
|
|
615
665
|
|
|
666
|
+
/**
|
|
667
|
+
* Extraction and the BLAS builds together, for a spilling build of several chunks. Each
|
|
668
|
+
* stored range goes to a worker the moment it is written, each chunk whose BLASes are all
|
|
669
|
+
* built goes to the GPU and then to disk, and the extraction waits whenever more than
|
|
670
|
+
* STREAM_RESIDENT_BYTES of triangles are still in memory — so the records are never all
|
|
671
|
+
* resident at once, which they are at the start of the BLAS phase otherwise.
|
|
672
|
+
* @private
|
|
673
|
+
*/
|
|
674
|
+
async _extractStreaming( object ) {
|
|
675
|
+
|
|
676
|
+
updateLoading( { isLoading: true, title: 'Processing', status: 'Extracting geometry and building BVHs...', progress: 15 } );
|
|
677
|
+
await new Promise( r => setTimeout( r, 0 ) );
|
|
678
|
+
const startTime = performance.now();
|
|
679
|
+
|
|
680
|
+
const extractor = this.geometryExtractor;
|
|
681
|
+
const { storage, uploader } = this._progressive;
|
|
682
|
+
const key = `spill:${Date.now().toString( 36 )}:${Math.random().toString( 36 ).slice( 2 )}`;
|
|
683
|
+
const capacity = extractor._countStoredTriangles( object );
|
|
684
|
+
const probe = ChunkedRecords.lazy( capacity, TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE, Uint32Array );
|
|
685
|
+
const stride = probe.recordsPerChunk * probe.lanesPerRecord * 4;
|
|
686
|
+
|
|
687
|
+
const triStore = await SpillStore.create( storage, `${key}:tri`, stride, { label: 'Triangle records', expectedBytes: capacity * stride / probe.recordsPerChunk } );
|
|
688
|
+
const blasStore = triStore && await SpillStore.create( storage, `${key}:blas`, 0, { label: 'BLAS nodes (while building)' } );
|
|
689
|
+
if ( ! blasStore ) {
|
|
690
|
+
|
|
691
|
+
await triStore?.dispose();
|
|
692
|
+
this._streaming = false;
|
|
693
|
+
return this._extractGeometry( object );
|
|
694
|
+
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// The three.js geometry waits on disk from its last read until the build is done.
|
|
698
|
+
const geometry = await GeometrySpill.create( storage, `${key}:geo`, this.memoryPreflight?.geometryBytes ?? 0 );
|
|
699
|
+
this._geometrySpill = geometry;
|
|
700
|
+
|
|
701
|
+
const state = {
|
|
702
|
+
key, triStore, blasStore, bvhStore: null,
|
|
703
|
+
blasBytes: 0, blasAt: new Map(),
|
|
704
|
+
pending: [], extractedChunks: 0,
|
|
705
|
+
emitterChunks: new Set(),
|
|
706
|
+
isEmitter: materialIndex => extractor._emissiveMaterial( materialIndex ),
|
|
707
|
+
uploadTriangles: null,
|
|
708
|
+
chain: Promise.resolve(),
|
|
709
|
+
error: null,
|
|
710
|
+
};
|
|
711
|
+
this._spillStores = [ triStore ];
|
|
712
|
+
this._blasKey = null;
|
|
713
|
+
|
|
714
|
+
const templates = new Map();
|
|
715
|
+
const workerOpts = this._blasWorkerOpts();
|
|
716
|
+
let waiters = [];
|
|
717
|
+
const built = () => new Promise( resolve => waiters.push( resolve ) );
|
|
718
|
+
const wake = () => {
|
|
719
|
+
|
|
720
|
+
const w = waiters;
|
|
721
|
+
waiters = [];
|
|
722
|
+
for ( const resolve of w ) resolve();
|
|
723
|
+
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
let tri = null;
|
|
727
|
+
const onBuilt = ( t, range, result ) => {
|
|
728
|
+
|
|
729
|
+
const nodes = result.bvhData;
|
|
730
|
+
const aabb = new Float32Array( 6 );
|
|
731
|
+
InstanceTable.rootAABB( nodeParts( nodes )[ 0 ], tri, range.start, range.count, aabb, 0 );
|
|
732
|
+
templates.set( t, { result, aabb } );
|
|
733
|
+
|
|
734
|
+
const at = state.blasBytes;
|
|
735
|
+
state.blasBytes += nodeBytes( nodes );
|
|
736
|
+
state.blasAt.set( t, at );
|
|
737
|
+
result.nodeCount = nodeBytes( nodes ) / 64;
|
|
738
|
+
result.bvhData = null;
|
|
739
|
+
this._queueSpill( state, () => writeNodes( blasStore, at, nodes ) );
|
|
740
|
+
|
|
741
|
+
this._forEachChunk( tri, range.start, range.count, k => {
|
|
742
|
+
|
|
743
|
+
if ( -- state.pending[ k ] === 0 && k < state.extractedChunks ) this._finishTriangleChunk( state, k );
|
|
744
|
+
|
|
745
|
+
} );
|
|
746
|
+
wake();
|
|
747
|
+
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
let finished = 0;
|
|
751
|
+
const pool = this._blasPool( workerOpts, () => finished ++, ( t, range, result ) => onBuilt( t, range, result ) );
|
|
752
|
+
let large = Promise.resolve();
|
|
753
|
+
let largePending = 0;
|
|
754
|
+
const advance = ( written ) => {
|
|
755
|
+
|
|
756
|
+
const done = written >= tri.recordCount ? tri.chunks.length : Math.floor( written / tri.recordsPerChunk );
|
|
757
|
+
for ( let k = state.extractedChunks; k < done; k ++ ) if ( ! state.pending[ k ] ) this._finishTriangleChunk( state, k );
|
|
758
|
+
state.extractedChunks = Math.max( state.extractedChunks, done );
|
|
759
|
+
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
let extracted;
|
|
763
|
+
try {
|
|
764
|
+
|
|
765
|
+
extracted = await extractor.extractStreaming( object, {
|
|
766
|
+
|
|
767
|
+
onStore: store => {
|
|
768
|
+
|
|
769
|
+
tri = store;
|
|
770
|
+
this._setTriangleData( store );
|
|
771
|
+
state.uploadTriangles = uploader( store, 'triangles' );
|
|
772
|
+
|
|
773
|
+
},
|
|
774
|
+
|
|
775
|
+
onRange: ( t, range ) => {
|
|
776
|
+
|
|
777
|
+
this._forEachChunk( tri, range.start, range.count, k => state.pending[ k ] = ( state.pending[ k ] ?? 0 ) + 1 );
|
|
778
|
+
if ( this._isLargeMesh( range ) ) {
|
|
779
|
+
|
|
780
|
+
largePending ++;
|
|
781
|
+
large = large.then( async () => {
|
|
782
|
+
|
|
783
|
+
onBuilt( t, range, await this._buildLargeBLAS( range, workerOpts ) );
|
|
784
|
+
largePending --;
|
|
785
|
+
|
|
786
|
+
} );
|
|
787
|
+
large.catch( error => {
|
|
788
|
+
|
|
789
|
+
state.error ??= error;
|
|
790
|
+
wake();
|
|
791
|
+
|
|
792
|
+
} );
|
|
793
|
+
|
|
794
|
+
} else pool.submit( { m: t, range } );
|
|
795
|
+
|
|
796
|
+
},
|
|
797
|
+
|
|
798
|
+
releaseGeometry: geometry ? g => geometry.add( g ) : null,
|
|
799
|
+
|
|
800
|
+
pause: async ( written ) => {
|
|
801
|
+
|
|
802
|
+
advance( written );
|
|
803
|
+
if ( geometry?.queuedBytes > GEOMETRY_QUEUE_BYTES ) await geometry.written();
|
|
804
|
+
updateLoading( { status: `Extracted ${written.toLocaleString()} triangles, ${finished.toLocaleString()} BVHs built...`, progress: 15 + Math.floor( 20 * written / Math.max( 1, capacity ) ) } );
|
|
805
|
+
await new Promise( r => setTimeout( r, 0 ) );
|
|
806
|
+
// A timer, not the spill chain: an already-settled promise would spin this loop in
|
|
807
|
+
// microtasks and keep out the worker messages it is waiting for.
|
|
808
|
+
while ( tri.byteLength > STREAM_RESIDENT_BYTES && ( pool.pending > 0 || largePending > 0 ) && ! state.error && ! pool.error ) {
|
|
809
|
+
|
|
810
|
+
await Promise.race( [ built(), new Promise( r => setTimeout( r, 50 ) ) ] );
|
|
811
|
+
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if ( state.error || pool.error ) throw state.error ?? pool.error;
|
|
815
|
+
|
|
816
|
+
},
|
|
817
|
+
|
|
818
|
+
} );
|
|
819
|
+
|
|
820
|
+
advance( Infinity );
|
|
821
|
+
await Promise.all( [ pool.close(), large ] );
|
|
822
|
+
await state.chain;
|
|
823
|
+
if ( state.error ) throw state.error;
|
|
824
|
+
|
|
825
|
+
} catch ( error ) {
|
|
826
|
+
|
|
827
|
+
pool.abort( error );
|
|
828
|
+
log.error( 'streamed extraction failed:', error );
|
|
829
|
+
updateLoading( { status: `Extraction error: ${error.message}`, failed: true, progress: 25 } );
|
|
830
|
+
throw error;
|
|
831
|
+
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
this._adoptExtraction( extracted );
|
|
835
|
+
this._streamed = { state, templates };
|
|
836
|
+
this._log( `Geometry extracted and ${templates.size} BLASes built together (${( performance.now() - startTime ).toFixed( 0 )}ms)` );
|
|
837
|
+
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** Takes over what the extractor produced. @private */
|
|
841
|
+
_adoptExtraction( extractedData ) {
|
|
842
|
+
|
|
843
|
+
this._setTriangleData( extractedData.triangleData );
|
|
844
|
+
this.geometryExtractor.releaseTriangles();
|
|
845
|
+
this.triangleCount = assertBVHIndexFits( extractedData.triangleCount, 'triangle count' );
|
|
846
|
+
if ( this._foldLeaves && this.triangleCount > BVH_FOLDED_FIRST_LIMIT ) throw new RangeError( `${this.triangleCount.toLocaleString()} triangles is past the ${BVH_FOLDED_FIRST_LIMIT.toLocaleString()} a folded BVH leaf can address` );
|
|
847
|
+
// Callers build refit buffers by walking meshes, which counts a shared geometry
|
|
848
|
+
// once per placement; storage counts it once.
|
|
849
|
+
this.expandedTriangleCount = extractedData.expandedTriangleCount ?? extractedData.triangleCount;
|
|
850
|
+
this.instanceSource = extractedData.instanceSource || null;
|
|
851
|
+
this.instanceMatrices = extractedData.instanceMatrices || null;
|
|
852
|
+
this.instanceCount = extractedData.instanceCount || 0;
|
|
853
|
+
this.bakeInverse = extractedData.bakeInverse || null;
|
|
854
|
+
|
|
855
|
+
this._log( `Using Float32Array format: ${this.triangleCount} triangles, ${( this.triangles.byteLength / ( 1024 * 1024 ) ).toFixed( 2 )}MB` );
|
|
856
|
+
|
|
857
|
+
// Store other extracted data
|
|
858
|
+
this.materials = extractedData.materials;
|
|
859
|
+
this.materialTriangleCounts = extractedData.materialTriangleCounts; // Per-material tri count for sort-bin remap
|
|
860
|
+
this.meshes = extractedData.meshes;
|
|
861
|
+
this.meshTriangleRanges = extractedData.meshTriangleRanges; // Per-mesh { start, count } for TLAS/BLAS
|
|
862
|
+
this.maps = extractedData.maps;
|
|
863
|
+
this.normalMaps = extractedData.normalMaps;
|
|
864
|
+
this.bumpMaps = extractedData.bumpMaps;
|
|
865
|
+
this.roughnessMaps = extractedData.roughnessMaps;
|
|
866
|
+
this.metalnessMaps = extractedData.metalnessMaps;
|
|
867
|
+
this.emissiveMaps = extractedData.emissiveMaps;
|
|
868
|
+
this.displacementMaps = extractedData.displacementMaps;
|
|
869
|
+
this.anisotropyMaps = extractedData.anisotropyMaps;
|
|
870
|
+
this.transmissionMaps = extractedData.transmissionMaps;
|
|
871
|
+
this.clearcoatMaps = extractedData.clearcoatMaps;
|
|
872
|
+
this.clearcoatRoughnessMaps = extractedData.clearcoatRoughnessMaps;
|
|
873
|
+
this.sheenColorMaps = extractedData.sheenColorMaps;
|
|
874
|
+
this.sheenRoughnessMaps = extractedData.sheenRoughnessMaps;
|
|
875
|
+
this.iridescenceMaps = extractedData.iridescenceMaps;
|
|
876
|
+
this.iridescenceThicknessMaps = extractedData.iridescenceThicknessMaps;
|
|
877
|
+
this.specularIntensityMaps = extractedData.specularIntensityMaps;
|
|
878
|
+
this.specularColorMaps = extractedData.specularColorMaps;
|
|
879
|
+
this.directionalLights = extractedData.directionalLights;
|
|
880
|
+
this.cameras = extractedData.cameras;
|
|
881
|
+
|
|
882
|
+
}
|
|
883
|
+
|
|
616
884
|
/**
|
|
617
885
|
* Build two-level BVH (TLAS/BLAS): one BLAS per mesh, one TLAS over mesh AABBs.
|
|
618
886
|
* @private
|
|
@@ -659,12 +927,14 @@ export class SceneProcessor {
|
|
|
659
927
|
this.instanceTable.allocate( meshCount, ranges.length, worldPool, pooled ? instSource : null );
|
|
660
928
|
this.instanceTable.tplBakeInverse = this.bakeInverse;
|
|
661
929
|
|
|
662
|
-
|
|
663
|
-
const
|
|
930
|
+
// Built while the geometry was extracted (the streaming path): only adopt the results.
|
|
931
|
+
const streamed = this._streamed;
|
|
932
|
+
this._streamed = null;
|
|
664
933
|
|
|
665
934
|
// Separate into worker-pool tasks and multi-worker parallel tasks
|
|
666
935
|
const poolTasks = [];
|
|
667
936
|
const parallelTasks = [];
|
|
937
|
+
const streamedTasks = [];
|
|
668
938
|
|
|
669
939
|
// Placements that reuse another's triangles reuse its BLAS too — the whole point of
|
|
670
940
|
// storing geometry in object space. Only the first placement of a range is built.
|
|
@@ -686,41 +956,13 @@ export class SceneProcessor {
|
|
|
686
956
|
|
|
687
957
|
ownerOfRange.set( range.start, m );
|
|
688
958
|
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
} else {
|
|
694
|
-
|
|
695
|
-
poolTasks.push( { m, range } );
|
|
696
|
-
|
|
697
|
-
}
|
|
959
|
+
if ( streamed ) streamedTasks.push( { m, range } );
|
|
960
|
+
else if ( this._isLargeMesh( range ) ) parallelTasks.push( { m, range } );
|
|
961
|
+
else poolTasks.push( { m, range } );
|
|
698
962
|
|
|
699
963
|
}
|
|
700
964
|
|
|
701
|
-
|
|
702
|
-
const workerOpts = {
|
|
703
|
-
depth: this.config.bvhDepth,
|
|
704
|
-
treeletOptimization: {
|
|
705
|
-
enabled: originalTreeletEnabled !== false,
|
|
706
|
-
size: this.config.treeletSize,
|
|
707
|
-
passes: this.config.treeletOptimizationPasses,
|
|
708
|
-
minImprovement: this.config.treeletMinImprovement,
|
|
709
|
-
complexityThreshold: this.config.treeletComplexityThreshold
|
|
710
|
-
},
|
|
711
|
-
reinsertionOptimization: {
|
|
712
|
-
enabled: this.bvhBuilder.enableReinsertionOptimization,
|
|
713
|
-
batchSizeRatio: this.bvhBuilder.reinsertionBatchSizeRatio,
|
|
714
|
-
maxIterations: this.bvhBuilder.reinsertionMaxIterations
|
|
715
|
-
},
|
|
716
|
-
// BVH build params — previously omitted, so the pool path built at the
|
|
717
|
-
// BVHBuilder default (leaf 8) instead of the configured value.
|
|
718
|
-
maxLeafSize: this.bvhBuilder.maxLeafSize,
|
|
719
|
-
numBins: this.bvhBuilder.numBins,
|
|
720
|
-
maxBins: this.bvhBuilder.maxBins,
|
|
721
|
-
minBins: this.bvhBuilder.minBins,
|
|
722
|
-
logLevel: workerLogLevel(),
|
|
723
|
-
};
|
|
965
|
+
const workerOpts = this._blasWorkerOpts();
|
|
724
966
|
|
|
725
967
|
const totalTasks = poolTasks.length + parallelTasks.length;
|
|
726
968
|
let doneTasks = 0;
|
|
@@ -734,8 +976,35 @@ export class SceneProcessor {
|
|
|
734
976
|
|
|
735
977
|
};
|
|
736
978
|
|
|
979
|
+
const owners = [ ...poolTasks, ...parallelTasks ]
|
|
980
|
+
.map( ( { m, range } ) => ( { m, range, t: sourceOf( m ), triOffset: range.start, triCount: range.count } ) )
|
|
981
|
+
.sort( ( a, b ) => a.t - b.t );
|
|
982
|
+
const restored = streamed ? null : await this._restoreBLASes( owners, TLASBuilder.nodeCountFor( meshCount ), workerOpts );
|
|
983
|
+
const spill = streamed ? streamed.state
|
|
984
|
+
: ! restored && this._progressive && this.triangles.chunks.length > 1 ? await this._beginProgressiveSpill( owners ) : null;
|
|
985
|
+
const onBuilt = spill && ! streamed ? ( m, range, result ) => this._onBLASBuilt( spill, sourceOf( m ), range, result ) : null;
|
|
986
|
+
if ( streamed ) {
|
|
987
|
+
|
|
988
|
+
for ( const [ t, built ] of streamed.templates ) {
|
|
989
|
+
|
|
990
|
+
this.instanceTable.tplObjectAABB.set( built.aabb, t * 6 );
|
|
991
|
+
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if ( restored ) {
|
|
997
|
+
|
|
998
|
+
poolTasks.length = 0;
|
|
999
|
+
parallelTasks.length = 0;
|
|
1000
|
+
updateLoading( { status: 'Restored stored BVHs', progress: 68 } );
|
|
1001
|
+
|
|
1002
|
+
}
|
|
1003
|
+
|
|
737
1004
|
// Build all meshes via bounded worker pool (main thread stays free)
|
|
738
|
-
const poolPromise =
|
|
1005
|
+
const poolPromise = restored ? Promise.resolve( restored.results )
|
|
1006
|
+
: streamed ? Promise.resolve( streamedTasks.map( ( { m, range } ) => ( { m, range, result: streamed.templates.get( sourceOf( m ) ).result } ) ) )
|
|
1007
|
+
: this._buildBLASesWithPool( poolTasks, workerOpts, reportBLASProgress, onBuilt );
|
|
739
1008
|
|
|
740
1009
|
// One at a time: each build already spreads over every core and pins ~200 bytes per
|
|
741
1010
|
// triangle of SharedArrayBuffer until it finishes. This is a memory bound, not a core one.
|
|
@@ -744,23 +1013,8 @@ export class SceneProcessor {
|
|
|
744
1013
|
|
|
745
1014
|
for ( const { m, range } of parallelTasks ) {
|
|
746
1015
|
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
const result = await buildBVHParallel( meshTriData, this.config.bvhDepth, null, {
|
|
750
|
-
maxLeafSize: this.bvhBuilder.maxLeafSize,
|
|
751
|
-
numBins: this.bvhBuilder.numBins,
|
|
752
|
-
maxBins: this.bvhBuilder.maxBins,
|
|
753
|
-
minBins: this.bvhBuilder.minBins,
|
|
754
|
-
...workerOpts
|
|
755
|
-
} );
|
|
756
|
-
|
|
757
|
-
if ( result.reorderedTriangles ) {
|
|
758
|
-
|
|
759
|
-
this.triangles.setRecords( range.start, result.reorderedTriangles );
|
|
760
|
-
delete result.reorderedTriangles;
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
|
|
1016
|
+
const result = await this._buildLargeBLAS( range, workerOpts );
|
|
1017
|
+
onBuilt?.( m, range, result );
|
|
764
1018
|
parallelResults.push( { m, range, result } );
|
|
765
1019
|
reportBLASProgress();
|
|
766
1020
|
|
|
@@ -769,6 +1023,12 @@ export class SceneProcessor {
|
|
|
769
1023
|
} )();
|
|
770
1024
|
|
|
771
1025
|
const [ poolResults ] = await Promise.all( [ poolPromise, parallelPromise ] );
|
|
1026
|
+
if ( spill ) {
|
|
1027
|
+
|
|
1028
|
+
await spill.chain;
|
|
1029
|
+
if ( spill.error ) throw spill.error;
|
|
1030
|
+
|
|
1031
|
+
}
|
|
772
1032
|
|
|
773
1033
|
// Store all results, summing per-mesh split stats for one aggregate BVH line
|
|
774
1034
|
const blasStats = { sah: 0, objMed: 0, spatMed: 0, failed: 0, treeletsImproved: 0, treeletsProcessed: 0 };
|
|
@@ -790,7 +1050,7 @@ export class SceneProcessor {
|
|
|
790
1050
|
|
|
791
1051
|
this.instanceTable.setEntry( {
|
|
792
1052
|
meshIndex: m,
|
|
793
|
-
blasNodeCount: result.bvhData
|
|
1053
|
+
blasNodeCount: result.nodeCount ?? nodeBytes( result.bvhData ) / 64,
|
|
794
1054
|
triOffset: range.start,
|
|
795
1055
|
triCount: range.count,
|
|
796
1056
|
originalToBvhMap: result.originalToBvh || null,
|
|
@@ -832,7 +1092,7 @@ export class SceneProcessor {
|
|
|
832
1092
|
// carries packed per-mesh visibility in its slot [2]. The 1-node TLAS
|
|
833
1093
|
// overhead (one extra leaf fetch per ray) is negligible and eliminates
|
|
834
1094
|
// a dedicated visibility storage buffer binding.
|
|
835
|
-
this.instanceTable.computeAABBs( this.triangles );
|
|
1095
|
+
this.instanceTable.computeAABBs( this.triangles, { owners: ! spill } );
|
|
836
1096
|
|
|
837
1097
|
// Node count is exact up front (every leaf holds one entry), so BLAS offsets can be
|
|
838
1098
|
// assigned before the build and the TLAS is written in a single pass.
|
|
@@ -849,8 +1109,9 @@ export class SceneProcessor {
|
|
|
849
1109
|
// 33.4M nodes, which a large instanced scene reaches well before the GPU's 4 GB.
|
|
850
1110
|
// Lazy, and walked in template order so offsets ascend: chunks are allocated as the fill
|
|
851
1111
|
// reaches them and each BLAS is released as it lands, never both fully resident.
|
|
852
|
-
this._checkAssemblyHeadroom( totalNodes );
|
|
1112
|
+
if ( ! spill ) this._checkAssemblyHeadroom( totalNodes );
|
|
853
1113
|
this._setBVHData( ChunkedRecords.lazy( totalNodes, 16, Float32Array, undefined, SHARED_MEMORY_AVAILABLE ) );
|
|
1114
|
+
const flushBVH = spill ? await this._beginBVHSpill( spill, table.tlasNodeCount ) : null;
|
|
854
1115
|
this.bvh.setRecords( 0, tlasData );
|
|
855
1116
|
// Hundreds of megabytes at millions of placements, and the chunks below need the room
|
|
856
1117
|
// more than a later refit needs the cache.
|
|
@@ -862,23 +1123,57 @@ export class SceneProcessor {
|
|
|
862
1123
|
// as chunks are taken and BLASes released.
|
|
863
1124
|
const sampleEvery = Math.max( 1, Math.ceil( table.templateCount / 32 ) );
|
|
864
1125
|
|
|
865
|
-
|
|
1126
|
+
if ( restored ) {
|
|
1127
|
+
|
|
1128
|
+
// Stored already assembled: same template order, same offsets, indices already rebased.
|
|
1129
|
+
try {
|
|
866
1130
|
|
|
867
|
-
|
|
868
|
-
|
|
1131
|
+
const readStart = performance.now();
|
|
1132
|
+
await readNodesInto( restored.nodes, this.bvh, table.tlasNodeCount, totalNodes - table.tlasNodeCount );
|
|
1133
|
+
this.performanceMetrics.blasRestore.readMs = performance.now() - readStart;
|
|
869
1134
|
|
|
870
|
-
|
|
871
|
-
this.bvh.setRecords( blasOffset, blas );
|
|
872
|
-
this._offsetBLASInPlace( blasOffset, blas.length / 16, blasOffset, table.tplTriOffset[ t ] );
|
|
873
|
-
table.blasData.delete( t );
|
|
1135
|
+
} finally {
|
|
874
1136
|
|
|
875
|
-
|
|
1137
|
+
restored.release();
|
|
1138
|
+
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
table.blasData.clear();
|
|
1142
|
+
|
|
1143
|
+
} else {
|
|
1144
|
+
|
|
1145
|
+
for ( let t = 0; t < table.templateCount; t ++ ) {
|
|
1146
|
+
|
|
1147
|
+
// Only owning templates hold one; spilled, they are read back one at a time.
|
|
1148
|
+
const blas = spill ? await this._readSpilledBLAS( spill, t ) : table.blasData.get( t );
|
|
1149
|
+
if ( ! blas ) continue;
|
|
1150
|
+
|
|
1151
|
+
const blasOffset = table.tplBlasOffset[ t ];
|
|
1152
|
+
let end = blasOffset;
|
|
1153
|
+
for ( const part of nodeParts( blas ) ) {
|
|
1154
|
+
|
|
1155
|
+
this.bvh.setRecords( end, part );
|
|
1156
|
+
end += part.length / 16;
|
|
1157
|
+
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
this._offsetBLASInPlace( blasOffset, end - blasOffset, blasOffset, table.tplTriOffset[ t ] );
|
|
1161
|
+
table.blasData.delete( t );
|
|
1162
|
+
await flushBVH?.( end );
|
|
1163
|
+
|
|
1164
|
+
if ( t % sampleEvery === 0 ) this._sampleMemory( `assembling ${t}/${table.templateCount}` );
|
|
1165
|
+
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
await flushBVH?.( totalNodes, true );
|
|
1169
|
+
if ( spill ) await spill.blasStore.dispose();
|
|
876
1170
|
|
|
877
1171
|
}
|
|
878
1172
|
|
|
879
1173
|
this._setBVHData( this.bvh.materializeAll() );
|
|
880
1174
|
|
|
881
|
-
|
|
1175
|
+
if ( restored ) for ( const [ t, map ] of restored.orders ) table.bvhToOriginal.set( t, map );
|
|
1176
|
+
else this._buildBvhToOriginalMaps();
|
|
882
1177
|
this.performanceMetrics.bvhAssembleTime = performance.now() - assembleStart;
|
|
883
1178
|
|
|
884
1179
|
table.originalToBvhMap.clear();
|
|
@@ -917,31 +1212,486 @@ export class SceneProcessor {
|
|
|
917
1212
|
|
|
918
1213
|
}
|
|
919
1214
|
|
|
1215
|
+
/** Worker config shared by all BLAS builds. @private */
|
|
1216
|
+
_blasWorkerOpts() {
|
|
1217
|
+
|
|
1218
|
+
return {
|
|
1219
|
+
depth: this.config.bvhDepth,
|
|
1220
|
+
treeletOptimization: {
|
|
1221
|
+
enabled: this.config.enableTreeletOptimization !== false,
|
|
1222
|
+
size: this.config.treeletSize,
|
|
1223
|
+
passes: this.config.treeletOptimizationPasses,
|
|
1224
|
+
minImprovement: this.config.treeletMinImprovement,
|
|
1225
|
+
complexityThreshold: this.config.treeletComplexityThreshold
|
|
1226
|
+
},
|
|
1227
|
+
reinsertionOptimization: {
|
|
1228
|
+
enabled: this.bvhBuilder.enableReinsertionOptimization,
|
|
1229
|
+
batchSizeRatio: this.bvhBuilder.reinsertionBatchSizeRatio,
|
|
1230
|
+
maxIterations: this.bvhBuilder.reinsertionMaxIterations
|
|
1231
|
+
},
|
|
1232
|
+
// BVH build params — previously omitted, so the pool path built at the
|
|
1233
|
+
// BVHBuilder default (leaf 8) instead of the configured value.
|
|
1234
|
+
maxLeafSize: this.bvhBuilder.maxLeafSize,
|
|
1235
|
+
foldLeaves: this._foldLeaves === true,
|
|
1236
|
+
numBins: this.bvhBuilder.numBins,
|
|
1237
|
+
maxBins: this.bvhBuilder.maxBins,
|
|
1238
|
+
minBins: this.bvhBuilder.minBins,
|
|
1239
|
+
logLevel: workerLogLevel(),
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/** @private */
|
|
1245
|
+
_isLargeMesh( range ) {
|
|
1246
|
+
|
|
1247
|
+
return range.count >= LARGE_MESH_TRIANGLES && shouldUseParallelBuild( range.count );
|
|
1248
|
+
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* One mesh's BLAS across every core. The range is read straight into the SharedArrayBuffer
|
|
1253
|
+
* the builder works in, and its BVH order written back chunk by chunk, so the mesh is held
|
|
1254
|
+
* once rather than three times.
|
|
1255
|
+
* @private
|
|
1256
|
+
*/
|
|
1257
|
+
async _buildLargeBLAS( range, workerOpts ) {
|
|
1258
|
+
|
|
1259
|
+
if ( range.count > SPLIT_MESH_TRIANGLES ) return this._buildSplitBLAS( range, workerOpts );
|
|
1260
|
+
|
|
1261
|
+
const meshTriData = new Uint32Array( new SharedArrayBuffer( range.count * this.triangles.lanesPerRecord * 4 ) );
|
|
1262
|
+
this.triangles.readRecords( range.start, range.count, meshTriData );
|
|
1263
|
+
// The builder has its own copy and the order comes back into every record of the range, so
|
|
1264
|
+
// the chunks wholly inside it can go meanwhile — 1 GB at the build's peak for a 15M mesh.
|
|
1265
|
+
this.triangles.release( range.start, range.count );
|
|
1266
|
+
|
|
1267
|
+
const result = await buildBVHParallel( meshTriData, this.config.bvhDepth, null, workerOpts );
|
|
1268
|
+
|
|
1269
|
+
if ( result.reorderedTriangles ) {
|
|
1270
|
+
|
|
1271
|
+
this.triangles.setRecords( range.start, result.reorderedTriangles );
|
|
1272
|
+
delete result.reorderedTriangles;
|
|
1273
|
+
|
|
1274
|
+
} else if ( result.order ) {
|
|
1275
|
+
|
|
1276
|
+
this._writeInOrder( range.start, result.triangles, result.order );
|
|
1277
|
+
delete result.order;
|
|
1278
|
+
delete result.triangles;
|
|
1279
|
+
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
return result;
|
|
1283
|
+
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
/**
|
|
1287
|
+
* A mesh too large to build in one piece: its records are sorted in place into spatial pieces,
|
|
1288
|
+
* each piece built by a pool worker like any mesh, and the pieces joined under the halving that
|
|
1289
|
+
* made them. Held once, in the store, with a piece's copy per worker — where one build across
|
|
1290
|
+
* every core held a second copy of the whole mesh and 44 B a triangle of shared scratch.
|
|
1291
|
+
* @private
|
|
1292
|
+
*/
|
|
1293
|
+
async _buildSplitBLAS( range, workerOpts ) {
|
|
1294
|
+
|
|
1295
|
+
const tri = this.triangles;
|
|
1296
|
+
const { order, pieces, tree } = await partitionRange( tri, range.start, range.count, SPLIT_PIECE_TRIANGLES, { pause: () => new Promise( r => setTimeout( r, 0 ) ) } );
|
|
1297
|
+
const pool = this._blasPool( workerOpts, null, null, Math.min( navigator.hardwareConcurrency || 4, 8 ) );
|
|
1298
|
+
pieces.forEach( ( p, k ) => pool.submit( { m: k, range: { start: range.start + p.start, count: p.count } } ) );
|
|
1299
|
+
|
|
1300
|
+
const built = [];
|
|
1301
|
+
const splitStats = {};
|
|
1302
|
+
for ( const { m: k, range: r, result } of await pool.close() ) {
|
|
1303
|
+
|
|
1304
|
+
result.aabb = new Float32Array( 6 );
|
|
1305
|
+
InstanceTable.rootAABB( result.bvhData, tri, r.start, r.count, result.aabb, 0 );
|
|
1306
|
+
built[ k ] = result;
|
|
1307
|
+
for ( const [ key, value ] of Object.entries( result.splitStats ?? {} ) ) if ( typeof value === 'number' ) splitStats[ key ] = ( splitStats[ key ] ?? 0 ) + value;
|
|
1308
|
+
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
return { ...joinPieces( tree, pieces, built, order ), splitStats };
|
|
1312
|
+
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
_blasCacheKey( workerOpts ) {
|
|
1316
|
+
|
|
1317
|
+
const builder = { ...workerOpts };
|
|
1318
|
+
delete builder.logLevel;
|
|
1319
|
+
const cores = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : 0;
|
|
1320
|
+
return `blas:${BLAS_CACHE_FORMAT}:${VERSION}:${JSON.stringify( builder )}:${cores}:${this._sceneKey}`;
|
|
1321
|
+
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Reuses stored BLASes when every owning template matches what was stored — same ranges, same
|
|
1326
|
+
* node counts, same offsets, same triangles — and puts the triangles in their built order.
|
|
1327
|
+
* @returns {Promise<?{results: Array, orders: Map<number, Uint32Array>, nodes: File, release: function(): void}>}
|
|
1328
|
+
* @private
|
|
1329
|
+
*/
|
|
1330
|
+
async _restoreBLASes( owners, tlasNodeCount, workerOpts ) {
|
|
1331
|
+
|
|
1332
|
+
if ( ! this._sceneKey || this.config.sceneCache === false ) return null;
|
|
1333
|
+
const storage = sharedStorage( getAssetConfig().cacheNamespace );
|
|
1334
|
+
if ( ! storage ) return null;
|
|
1335
|
+
|
|
1336
|
+
this._blasKey = this._blasCacheKey( workerOpts );
|
|
1337
|
+
const cached = await openBLASCache( storage, this._blasKey );
|
|
1338
|
+
if ( ! cached ) return null;
|
|
1339
|
+
|
|
1340
|
+
const miss = ( reason ) => {
|
|
1341
|
+
|
|
1342
|
+
log.debug( `stored BLASes not used: ${reason}` );
|
|
1343
|
+
cached.release();
|
|
1344
|
+
return null;
|
|
1345
|
+
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
const { index } = cached;
|
|
1349
|
+
if ( index.tlasNodeCount !== tlasNodeCount || index.templates.length !== owners.length ) return miss( 'layout changed' );
|
|
1350
|
+
|
|
1351
|
+
let offset = tlasNodeCount;
|
|
1352
|
+
for ( let i = 0; i < owners.length; i ++ ) {
|
|
1353
|
+
|
|
1354
|
+
const [ t, triOffset, triCount, nodeCount, blasOffset ] = index.templates[ i ];
|
|
1355
|
+
const o = owners[ i ];
|
|
1356
|
+
if ( o.t !== t || o.triOffset !== triOffset || o.triCount !== triCount || blasOffset !== offset ) return miss( 'layout changed' );
|
|
1357
|
+
offset += nodeCount;
|
|
1358
|
+
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if ( offset !== index.totalNodes ) return miss( 'layout changed' );
|
|
1362
|
+
|
|
1363
|
+
const checkStart = performance.now();
|
|
1364
|
+
const sums = await templateChecksums( this.triangles, owners );
|
|
1365
|
+
for ( let i = 0; i < owners.length; i ++ ) if ( sums[ i ] !== index.templates[ i ][ 6 ] ) return miss( 'triangles changed' );
|
|
1366
|
+
const permuteStart = performance.now();
|
|
1367
|
+
|
|
1368
|
+
try {
|
|
1369
|
+
|
|
1370
|
+
const orders = new Map();
|
|
1371
|
+
const results = [];
|
|
1372
|
+
for ( let i = 0; i < owners.length; i ++ ) {
|
|
1373
|
+
|
|
1374
|
+
const [ t, triOffset, triCount, nodeCount, , orderOffset, , root ] = index.templates[ i ];
|
|
1375
|
+
const order = await readOrder( cached.order, orderOffset, triCount );
|
|
1376
|
+
this._permuteRange( triOffset, triCount, order );
|
|
1377
|
+
orders.set( t, order );
|
|
1378
|
+
results.push( {
|
|
1379
|
+
m: owners[ i ].m,
|
|
1380
|
+
range: owners[ i ].range,
|
|
1381
|
+
result: { bvhData: new Float32Array( Uint32Array.from( root ).buffer ), originalToBvh: null, nodeCount },
|
|
1382
|
+
} );
|
|
1383
|
+
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
this._blasRestored = true;
|
|
1387
|
+
this.performanceMetrics.blasRestore = { checksumMs: permuteStart - checkStart, permuteMs: performance.now() - permuteStart };
|
|
1388
|
+
log.debug( `restored ${fmt.n( owners.length )} stored BLASes` );
|
|
1389
|
+
return { results, orders, nodes: cached.nodes, release: cached.release };
|
|
1390
|
+
|
|
1391
|
+
} catch ( error ) {
|
|
1392
|
+
|
|
1393
|
+
cached.release();
|
|
1394
|
+
throw error;
|
|
1395
|
+
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/** Puts records [start, start + count) in built order: slot j takes caller's triangle order[ j ]. @private */
|
|
1401
|
+
/** Record `start + j` of the store becomes record `order[ j ]` of `src`, chunk by chunk. @private */
|
|
1402
|
+
_writeInOrder( start, src, order ) {
|
|
1403
|
+
|
|
1404
|
+
const store = this.triangles;
|
|
1405
|
+
const lanes = store.lanesPerRecord;
|
|
1406
|
+
for ( let j = 0, n = order.length; j < n; ) {
|
|
1407
|
+
|
|
1408
|
+
const dst = store.chunkFor( start + j );
|
|
1409
|
+
const end = Math.min( n, ( ( ( ( start + j ) / store.recordsPerChunk ) | 0 ) + 1 ) * store.recordsPerChunk - start );
|
|
1410
|
+
for ( let d = store.baseOf( start + j ); j < end; j ++, d += lanes ) {
|
|
1411
|
+
|
|
1412
|
+
const s = order[ j ] * lanes;
|
|
1413
|
+
for ( let l = 0; l < lanes; l ++ ) dst[ d + l ] = src[ s + l ];
|
|
1414
|
+
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
_permuteRange( start, count, order ) {
|
|
1422
|
+
|
|
1423
|
+
let identity = true;
|
|
1424
|
+
for ( let j = 0; j < count && identity; j ++ ) identity = order[ j ] === j;
|
|
1425
|
+
if ( identity ) return;
|
|
1426
|
+
|
|
1427
|
+
const lanes = this.triangles.lanesPerRecord;
|
|
1428
|
+
// A view when the range sits in one chunk: read from it, write a fresh buffer, copy back once.
|
|
1429
|
+
const src = this.triangles.slice( start, count );
|
|
1430
|
+
const out = new src.constructor( src.length );
|
|
1431
|
+
if ( lanes === 20 ) {
|
|
1432
|
+
|
|
1433
|
+
for ( let j = 0, d = 0; j < count; j ++, d += 20 ) {
|
|
1434
|
+
|
|
1435
|
+
const s = order[ j ] * 20;
|
|
1436
|
+
out[ d ] = src[ s ]; out[ d + 1 ] = src[ s + 1 ]; out[ d + 2 ] = src[ s + 2 ]; out[ d + 3 ] = src[ s + 3 ];
|
|
1437
|
+
out[ d + 4 ] = src[ s + 4 ]; out[ d + 5 ] = src[ s + 5 ]; out[ d + 6 ] = src[ s + 6 ]; out[ d + 7 ] = src[ s + 7 ];
|
|
1438
|
+
out[ d + 8 ] = src[ s + 8 ]; out[ d + 9 ] = src[ s + 9 ]; out[ d + 10 ] = src[ s + 10 ]; out[ d + 11 ] = src[ s + 11 ];
|
|
1439
|
+
out[ d + 12 ] = src[ s + 12 ]; out[ d + 13 ] = src[ s + 13 ]; out[ d + 14 ] = src[ s + 14 ]; out[ d + 15 ] = src[ s + 15 ];
|
|
1440
|
+
out[ d + 16 ] = src[ s + 16 ]; out[ d + 17 ] = src[ s + 17 ]; out[ d + 18 ] = src[ s + 18 ]; out[ d + 19 ] = src[ s + 19 ];
|
|
1441
|
+
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
} else {
|
|
1445
|
+
|
|
1446
|
+
for ( let j = 0; j < count; j ++ ) {
|
|
1447
|
+
|
|
1448
|
+
const s = order[ j ] * lanes;
|
|
1449
|
+
const d = j * lanes;
|
|
1450
|
+
for ( let l = 0; l < lanes; l ++ ) out[ d + l ] = src[ s + l ];
|
|
1451
|
+
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
this.triangles.setRecords( start, out );
|
|
1457
|
+
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/** Writes this build's BLASes to storage in the background; abandons it if the scene changes. @private */
|
|
1461
|
+
async _storeBLASes( label ) {
|
|
1462
|
+
|
|
1463
|
+
const storage = sharedStorage( getAssetConfig().cacheNamespace );
|
|
1464
|
+
const table = this.instanceTable;
|
|
1465
|
+
if ( ! storage || ! table || ! this.bvh ) return false;
|
|
1466
|
+
|
|
1467
|
+
const key = this._blasKey;
|
|
1468
|
+
const version = this._geometryVersion;
|
|
1469
|
+
const isStale = () => this._geometryVersion !== version || this._blasKey !== key;
|
|
1470
|
+
|
|
1471
|
+
const owners = [];
|
|
1472
|
+
for ( let t = 0; t < table.templateCount; t ++ ) {
|
|
1473
|
+
|
|
1474
|
+
const owner = table.tplOwner[ t ];
|
|
1475
|
+
if ( owner < 0 || table.sourceMesh[ owner ] !== t ) continue;
|
|
1476
|
+
owners.push( { t, triOffset: table.tplTriOffset[ t ], triCount: table.tplTriCount[ t ], nodeCount: table.tplNodeCount[ t ], blasOffset: table.tplBlasOffset[ t ] } );
|
|
1477
|
+
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
const sums = await templateChecksums( this.triangles, owners );
|
|
1481
|
+
if ( isStale() ) return false;
|
|
1482
|
+
|
|
1483
|
+
const rootBits = ( n ) => {
|
|
1484
|
+
|
|
1485
|
+
const chunk = this.bvhIndexChunks.chunkFor( n );
|
|
1486
|
+
const base = this.bvhIndexChunks.baseOf( n );
|
|
1487
|
+
return Array.from( chunk.subarray( base, base + 16 ) );
|
|
1488
|
+
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
let orderOffset = 0;
|
|
1492
|
+
const orders = [];
|
|
1493
|
+
const templates = owners.map( ( o, i ) => {
|
|
1494
|
+
|
|
1495
|
+
orders.push( table.bvhToOriginal.get( o.t ) ?? Uint32Array.from( { length: o.triCount }, ( _, j ) => j ) );
|
|
1496
|
+
const entry = [ o.t, o.triOffset, o.triCount, o.nodeCount, o.blasOffset, orderOffset, sums[ i ], rootBits( o.blasOffset ) ];
|
|
1497
|
+
orderOffset += o.triCount;
|
|
1498
|
+
return entry;
|
|
1499
|
+
|
|
1500
|
+
} );
|
|
1501
|
+
|
|
1502
|
+
const index = { v: BLAS_CACHE_FORMAT, tlasNodeCount: table.tlasNodeCount, totalNodes: table.totalNodeCount, templates };
|
|
1503
|
+
const stored = await saveBLASCache( storage, key, { index, bvh: this.bvh, orders, isStale, label } );
|
|
1504
|
+
if ( stored ) log.debug( `stored ${fmt.n( owners.length )} BLASes for the next load` );
|
|
1505
|
+
return stored;
|
|
1506
|
+
|
|
1507
|
+
}
|
|
1508
|
+
|
|
920
1509
|
/**
|
|
921
1510
|
* Adjust BLAS node indices in-place within the combined bvhData buffer.
|
|
922
1511
|
* @private
|
|
923
1512
|
*/
|
|
924
|
-
|
|
1513
|
+
/**
|
|
1514
|
+
* Memory spill during the build (8c): each BLAS goes to a scratch file as it arrives, and each
|
|
1515
|
+
* triangle chunk goes to the GPU and then to disk once every BLAS over it is built — so the
|
|
1516
|
+
* triangles and the BVH are never all in memory at once. Chunks holding emitters stay: the
|
|
1517
|
+
* emissive pass reads them. Null when storage cannot take it, and the build carries on as usual.
|
|
1518
|
+
* @private
|
|
1519
|
+
*/
|
|
1520
|
+
async _beginProgressiveSpill( owners ) {
|
|
925
1521
|
|
|
926
|
-
const
|
|
1522
|
+
const { storage, uploader } = this._progressive;
|
|
1523
|
+
const tri = this.triangles;
|
|
1524
|
+
const key = `spill:${Date.now().toString( 36 )}:${Math.random().toString( 36 ).slice( 2 )}`;
|
|
1525
|
+
const stride = tri.recordsPerChunk * tri.lanesPerRecord * tri.LaneType.BYTES_PER_ELEMENT;
|
|
927
1526
|
|
|
928
|
-
|
|
1527
|
+
const triStore = await SpillStore.create( storage, `${key}:tri`, stride, { label: 'Triangle records', expectedBytes: tri.byteLength } );
|
|
1528
|
+
const blasStore = triStore && await SpillStore.create( storage, `${key}:blas`, 0, { label: 'BLAS nodes (while building)' } );
|
|
1529
|
+
if ( ! blasStore ) {
|
|
929
1530
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1531
|
+
await triStore?.dispose();
|
|
1532
|
+
return null;
|
|
1533
|
+
|
|
1534
|
+
}
|
|
933
1535
|
|
|
934
|
-
|
|
1536
|
+
const state = {
|
|
1537
|
+
key, triStore, blasStore, bvhStore: null,
|
|
1538
|
+
blasBytes: 0, blasAt: new Map(),
|
|
1539
|
+
pending: new Int32Array( tri.chunks.length ),
|
|
1540
|
+
emitterChunks: new Set(),
|
|
1541
|
+
isEmitter: materialIndex => {
|
|
935
1542
|
|
|
936
|
-
|
|
1543
|
+
const m = this.materials[ materialIndex ];
|
|
1544
|
+
return ( m?.emissiveIntensity ?? 0 ) > 0 && !! m.emissive && ( m.emissive.r > 0 || m.emissive.g > 0 || m.emissive.b > 0 );
|
|
937
1545
|
|
|
938
|
-
}
|
|
1546
|
+
},
|
|
1547
|
+
uploadTriangles: uploader( tri, 'triangles' ),
|
|
1548
|
+
chain: Promise.resolve(),
|
|
1549
|
+
error: null,
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
this._spillStores = [ triStore ];
|
|
1553
|
+
this._blasKey = null;
|
|
1554
|
+
for ( const { triOffset, triCount } of owners ) this._forEachChunk( tri, triOffset, triCount, k => state.pending[ k ] ++ );
|
|
1555
|
+
for ( let k = 0; k < tri.chunks.length; k ++ ) if ( state.pending[ k ] === 0 ) this._finishTriangleChunk( state, k );
|
|
1556
|
+
return state;
|
|
1557
|
+
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/** @private */
|
|
1561
|
+
_forEachChunk( records, start, count, fn ) {
|
|
1562
|
+
|
|
1563
|
+
if ( count <= 0 ) return;
|
|
1564
|
+
const last = ( ( start + count - 1 ) / records.recordsPerChunk ) | 0;
|
|
1565
|
+
for ( let k = ( start / records.recordsPerChunk ) | 0; k <= last; k ++ ) fn( k );
|
|
1566
|
+
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/** A BLAS has landed: its bounds are taken now, its nodes go to scratch. @private */
|
|
1570
|
+
_onBLASBuilt( state, t, range, result ) {
|
|
1571
|
+
|
|
1572
|
+
const table = this.instanceTable;
|
|
1573
|
+
const nodes = result.bvhData;
|
|
1574
|
+
table.tplTriOffset[ t ] = range.start;
|
|
1575
|
+
table.tplTriCount[ t ] = range.count;
|
|
1576
|
+
table._readRootAABB( nodeParts( nodes )[ 0 ], t, this.triangles, table.tplObjectAABB, t * 6 );
|
|
1577
|
+
|
|
1578
|
+
const at = state.blasBytes;
|
|
1579
|
+
state.blasBytes += nodeBytes( nodes );
|
|
1580
|
+
state.blasAt.set( t, at );
|
|
1581
|
+
result.nodeCount = nodeBytes( nodes ) / 64;
|
|
1582
|
+
result.bvhData = null;
|
|
1583
|
+
|
|
1584
|
+
this._queueSpill( state, () => writeNodes( state.blasStore, at, nodes ) );
|
|
1585
|
+
this._forEachChunk( this.triangles, range.start, range.count, k => {
|
|
1586
|
+
|
|
1587
|
+
if ( -- state.pending[ k ] === 0 ) this._finishTriangleChunk( state, k );
|
|
1588
|
+
|
|
1589
|
+
} );
|
|
1590
|
+
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/** @private */
|
|
1594
|
+
_queueSpill( state, step ) {
|
|
1595
|
+
|
|
1596
|
+
state.chain = state.chain.then( step ).catch( error => {
|
|
1597
|
+
|
|
1598
|
+
state.error ??= error;
|
|
1599
|
+
|
|
1600
|
+
} );
|
|
1601
|
+
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/** Every BLAS over chunk `k` is built: upload it, and let it go unless it holds emitters. @private */
|
|
1605
|
+
_finishTriangleChunk( state, k ) {
|
|
1606
|
+
|
|
1607
|
+
this._queueSpill( state, async () => {
|
|
1608
|
+
|
|
1609
|
+
const tri = this.triangles;
|
|
1610
|
+
const chunk = tri.chunks[ k ];
|
|
1611
|
+
if ( ! chunk ) return;
|
|
1612
|
+
state.uploadTriangles( k );
|
|
1613
|
+
|
|
1614
|
+
const lanes = tri.lanesPerRecord;
|
|
1615
|
+
for ( let o = TRIANGLE_DATA_LAYOUT.MATERIAL_FLAGS_OFFSET; o < chunk.length; o += lanes ) {
|
|
1616
|
+
|
|
1617
|
+
if ( state.isEmitter( chunk[ o ] & TRI_MATERIAL_MASK ) ) {
|
|
1618
|
+
|
|
1619
|
+
state.emitterChunks.add( k );
|
|
1620
|
+
return;
|
|
1621
|
+
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
await tri.spillChunk( k, state.triStore );
|
|
1627
|
+
|
|
1628
|
+
} );
|
|
1629
|
+
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/** @private */
|
|
1633
|
+
async _readSpilledBLAS( state, t ) {
|
|
1634
|
+
|
|
1635
|
+
const at = state.blasAt.get( t );
|
|
1636
|
+
if ( at === undefined ) return null;
|
|
1637
|
+
const bytes = this.instanceTable.tplNodeCount[ t ] * 64;
|
|
1638
|
+
const parts = [];
|
|
1639
|
+
for ( let off = 0; off < bytes; off += READ_PART_BYTES ) parts.push( new Float32Array( await state.blasStore.readAt( at + off, Math.min( READ_PART_BYTES, bytes - off ) ) ) );
|
|
1640
|
+
return parts;
|
|
1641
|
+
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/**
|
|
1645
|
+
* The combined BVH is filled in ascending order, so every chunk the fill has passed is done:
|
|
1646
|
+
* upload it and let it go. The TLAS chunks stay, as they do after any spill.
|
|
1647
|
+
* @returns {Promise<?function(number, boolean=): Promise<void>>} call with the fill's end
|
|
1648
|
+
* @private
|
|
1649
|
+
*/
|
|
1650
|
+
async _beginBVHSpill( state, tlasNodeCount ) {
|
|
1651
|
+
|
|
1652
|
+
const { storage, uploader } = this._progressive;
|
|
1653
|
+
const bvh = this.bvh;
|
|
1654
|
+
const stride = bvh.recordsPerChunk * bvh.lanesPerRecord * bvh.LaneType.BYTES_PER_ELEMENT;
|
|
1655
|
+
const bvhStore = await SpillStore.create( storage, `${state.key}:bvh`, stride, { label: 'BLAS nodes', expectedBytes: bvh.recordCount * 64 } );
|
|
1656
|
+
const upload = uploader( bvh );
|
|
1657
|
+
const tlasChunks = Math.ceil( tlasNodeCount / bvh.recordsPerChunk );
|
|
1658
|
+
if ( bvhStore ) this._spillStores.push( bvhStore );
|
|
1659
|
+
let next = tlasChunks;
|
|
1660
|
+
|
|
1661
|
+
return async ( filledTo, last = false ) => {
|
|
1662
|
+
|
|
1663
|
+
while ( next < bvh.chunks.length && ( last || ( next + 1 ) * bvh.recordsPerChunk <= filledTo ) ) {
|
|
1664
|
+
|
|
1665
|
+
if ( ! bvh.chunks[ next ] ) bvh._materialize( next );
|
|
1666
|
+
upload( next );
|
|
1667
|
+
if ( bvhStore ) await bvh.spillChunk( next, bvhStore );
|
|
1668
|
+
next ++;
|
|
1669
|
+
|
|
1670
|
+
}
|
|
939
1671
|
|
|
940
|
-
|
|
941
|
-
|
|
1672
|
+
if ( last ) for ( let k = 0; k < Math.min( tlasChunks, bvh.chunks.length ); k ++ ) {
|
|
1673
|
+
|
|
1674
|
+
if ( ! bvh.chunks[ k ] ) bvh._materialize( k );
|
|
1675
|
+
upload( k );
|
|
942
1676
|
|
|
943
1677
|
}
|
|
944
1678
|
|
|
1679
|
+
};
|
|
1680
|
+
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
_offsetBLASInPlace( startNode, nodeCount, nodeOffset, triOffset ) {
|
|
1684
|
+
|
|
1685
|
+
const idx = this.bvhIndexChunks;
|
|
1686
|
+
|
|
1687
|
+
for ( let n = startNode, end = startNode + nodeCount; n < end; ) {
|
|
1688
|
+
|
|
1689
|
+
const chunk = idx.chunkFor( n );
|
|
1690
|
+
const stop = Math.min( end, ( ( ( n / idx.recordsPerChunk ) | 0 ) + 1 ) * idx.recordsPerChunk );
|
|
1691
|
+
const from = idx.baseOf( n );
|
|
1692
|
+
rebaseNodes( chunk, nodeOffset, triOffset, from, from + ( stop - n ) * 16 );
|
|
1693
|
+
n = stop;
|
|
1694
|
+
|
|
945
1695
|
}
|
|
946
1696
|
|
|
947
1697
|
}
|
|
@@ -956,32 +1706,63 @@ export class SceneProcessor {
|
|
|
956
1706
|
* @returns {Promise<Array<{m, range, result}>>}
|
|
957
1707
|
* @private
|
|
958
1708
|
*/
|
|
959
|
-
_buildBLASesWithPool( tasks, opts, onProgress ) {
|
|
1709
|
+
_buildBLASesWithPool( tasks, opts, onProgress, onResult = null ) {
|
|
960
1710
|
|
|
961
|
-
|
|
1711
|
+
const pool = this._blasPool( opts, onProgress, onResult );
|
|
1712
|
+
for ( const task of tasks ) pool.submit( task );
|
|
1713
|
+
return pool.close();
|
|
962
1714
|
|
|
963
|
-
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1718
|
+
* A BLAS worker pool that takes builds while it runs: `submit( { m, range } )` queues one,
|
|
1719
|
+
* `close()` resolves with every result once the queue has drained. Workers start as work
|
|
1720
|
+
* arrives, up to the pool size.
|
|
1721
|
+
* @private
|
|
1722
|
+
*/
|
|
1723
|
+
_blasPool( opts, onProgress, onResult = null, poolSize = this.config.maxConcurrentTextureTasks || 4 ) {
|
|
1724
|
+
|
|
1725
|
+
const queue = [];
|
|
964
1726
|
const results = [];
|
|
965
|
-
|
|
966
|
-
|
|
1727
|
+
const workers = [];
|
|
1728
|
+
const idle = [];
|
|
1729
|
+
let closed = false, running = 0, completed = 0, failure = null;
|
|
1730
|
+
let settle;
|
|
1731
|
+
const done = new Promise( ( resolve, reject ) => settle = { resolve, reject } );
|
|
1732
|
+
done.catch( () => {} );
|
|
967
1733
|
|
|
968
|
-
|
|
1734
|
+
const fail = ( error ) => {
|
|
1735
|
+
|
|
1736
|
+
if ( failure ) return;
|
|
1737
|
+
failure = error;
|
|
1738
|
+
workers.forEach( w => w.terminate() );
|
|
1739
|
+
settle.reject( error );
|
|
969
1740
|
|
|
970
|
-
|
|
1741
|
+
};
|
|
971
1742
|
|
|
972
|
-
|
|
1743
|
+
const finishIfDrained = () => {
|
|
973
1744
|
|
|
974
|
-
|
|
1745
|
+
if ( failure || ! closed || queue.length > 0 || running > 0 ) return;
|
|
1746
|
+
workers.forEach( w => w.terminate() );
|
|
1747
|
+
settle.resolve( results );
|
|
975
1748
|
|
|
976
|
-
|
|
977
|
-
worker.terminate();
|
|
978
|
-
workers.splice( workers.indexOf( worker ), 1 );
|
|
979
|
-
if ( workers.length === 0 ) resolve( results );
|
|
980
|
-
return;
|
|
1749
|
+
};
|
|
981
1750
|
|
|
982
|
-
|
|
1751
|
+
const dispatch = ( worker ) => {
|
|
1752
|
+
|
|
1753
|
+
const task = queue.shift();
|
|
1754
|
+
if ( ! task ) {
|
|
1755
|
+
|
|
1756
|
+
idle.push( worker );
|
|
1757
|
+
finishIfDrained();
|
|
1758
|
+
return;
|
|
1759
|
+
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
const { m, range } = task;
|
|
1763
|
+
running ++;
|
|
1764
|
+
try {
|
|
983
1765
|
|
|
984
|
-
const { m, range } = tasks[ nextTask ++ ];
|
|
985
1766
|
const meshTriData = this.triangles.copyOf( range.start, range.count );
|
|
986
1767
|
|
|
987
1768
|
// Disable treelet for tiny meshes
|
|
@@ -1002,84 +1783,122 @@ export class SceneProcessor {
|
|
|
1002
1783
|
treeletOptimization: treeletOpts,
|
|
1003
1784
|
reinsertionOptimization: opts.reinsertionOptimization,
|
|
1004
1785
|
maxLeafSize: opts.maxLeafSize,
|
|
1786
|
+
foldLeaves: opts.foldLeaves,
|
|
1005
1787
|
numBins: opts.numBins,
|
|
1006
1788
|
maxBins: opts.maxBins,
|
|
1007
1789
|
minBins: opts.minBins,
|
|
1008
1790
|
logLevel: opts.logLevel,
|
|
1009
1791
|
}, [ meshTriData.buffer ] );
|
|
1010
1792
|
|
|
1011
|
-
}
|
|
1793
|
+
} catch ( error ) {
|
|
1012
1794
|
|
|
1013
|
-
|
|
1795
|
+
fail( error );
|
|
1014
1796
|
|
|
1015
|
-
|
|
1797
|
+
}
|
|
1016
1798
|
|
|
1017
|
-
|
|
1799
|
+
};
|
|
1018
1800
|
|
|
1019
|
-
|
|
1020
|
-
reject( new Error( data.error ) );
|
|
1021
|
-
return;
|
|
1801
|
+
const onWorkerMessage = ( worker, e ) => {
|
|
1022
1802
|
|
|
1023
|
-
|
|
1803
|
+
const data = e.data;
|
|
1024
1804
|
|
|
1025
|
-
|
|
1805
|
+
if ( data.error ) {
|
|
1026
1806
|
|
|
1027
|
-
|
|
1807
|
+
fail( new Error( data.error ) );
|
|
1808
|
+
return;
|
|
1809
|
+
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
if ( data.progress !== undefined ) return; // Ignore progress messages
|
|
1813
|
+
|
|
1814
|
+
const { m, range } = worker._currentTask;
|
|
1815
|
+
running --;
|
|
1816
|
+
|
|
1817
|
+
try {
|
|
1028
1818
|
|
|
1029
1819
|
// Write back now: holding one copy per mesh until the pool drains is the whole
|
|
1030
1820
|
// triangle buffer over again, ~3 GB at 40M.
|
|
1031
1821
|
if ( data.triangles ) this.triangles.setRecords( range.start, data.triangles );
|
|
1032
1822
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
}
|
|
1041
|
-
} );
|
|
1823
|
+
const result = {
|
|
1824
|
+
bvhData: data.bvhData,
|
|
1825
|
+
originalToBvh: data.originalToBvh || null,
|
|
1826
|
+
splitStats: data.treeletStats || null,
|
|
1827
|
+
};
|
|
1828
|
+
onResult?.( m, range, result );
|
|
1829
|
+
results.push( { m, range, result } );
|
|
1042
1830
|
|
|
1043
|
-
|
|
1044
|
-
onProgress?.( completed );
|
|
1831
|
+
} catch ( error ) {
|
|
1045
1832
|
|
|
1046
|
-
|
|
1833
|
+
// Thrown here, it would stop the pool with the promise never settled.
|
|
1834
|
+
fail( error );
|
|
1835
|
+
return;
|
|
1047
1836
|
|
|
1048
|
-
}
|
|
1837
|
+
}
|
|
1049
1838
|
|
|
1050
|
-
|
|
1051
|
-
(
|
|
1839
|
+
completed ++;
|
|
1840
|
+
onProgress?.( completed );
|
|
1841
|
+
dispatch( worker );
|
|
1052
1842
|
|
|
1053
|
-
|
|
1843
|
+
};
|
|
1054
1844
|
|
|
1055
|
-
|
|
1056
|
-
try {
|
|
1845
|
+
const spawn = () => {
|
|
1057
1846
|
|
|
1058
|
-
|
|
1847
|
+
const worker = new BVHWorker();
|
|
1848
|
+
worker.onmessage = ( e ) => onWorkerMessage( worker, e );
|
|
1849
|
+
worker.onerror = ( err ) => fail( err );
|
|
1850
|
+
workers.push( worker );
|
|
1851
|
+
return worker;
|
|
1059
1852
|
|
|
1060
|
-
|
|
1853
|
+
};
|
|
1061
1854
|
|
|
1062
|
-
|
|
1063
|
-
return;
|
|
1855
|
+
return {
|
|
1064
1856
|
|
|
1065
|
-
|
|
1857
|
+
submit( task ) {
|
|
1066
1858
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1859
|
+
if ( failure ) return;
|
|
1860
|
+
queue.push( task );
|
|
1861
|
+
try {
|
|
1069
1862
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1863
|
+
const worker = idle.pop() ?? ( workers.length < poolSize ? spawn() : null );
|
|
1864
|
+
if ( worker ) dispatch( worker );
|
|
1072
1865
|
|
|
1073
|
-
|
|
1866
|
+
} catch ( error ) {
|
|
1074
1867
|
|
|
1075
|
-
|
|
1076
|
-
dispatchNext( worker );
|
|
1868
|
+
fail( error );
|
|
1077
1869
|
|
|
1078
1870
|
}
|
|
1079
1871
|
|
|
1080
|
-
}
|
|
1872
|
+
},
|
|
1081
1873
|
|
|
1082
|
-
|
|
1874
|
+
close() {
|
|
1875
|
+
|
|
1876
|
+
closed = true;
|
|
1877
|
+
finishIfDrained();
|
|
1878
|
+
return done;
|
|
1879
|
+
|
|
1880
|
+
},
|
|
1881
|
+
|
|
1882
|
+
abort( error ) {
|
|
1883
|
+
|
|
1884
|
+
fail( error ?? new Error( 'BLAS pool aborted' ) );
|
|
1885
|
+
|
|
1886
|
+
},
|
|
1887
|
+
|
|
1888
|
+
/** Builds queued or running. */
|
|
1889
|
+
get pending() {
|
|
1890
|
+
|
|
1891
|
+
return queue.length + running;
|
|
1892
|
+
|
|
1893
|
+
},
|
|
1894
|
+
|
|
1895
|
+
get error() {
|
|
1896
|
+
|
|
1897
|
+
return failure;
|
|
1898
|
+
|
|
1899
|
+
},
|
|
1900
|
+
|
|
1901
|
+
};
|
|
1083
1902
|
|
|
1084
1903
|
}
|
|
1085
1904
|
|
|
@@ -1415,6 +2234,10 @@ export class SceneProcessor {
|
|
|
1415
2234
|
*/
|
|
1416
2235
|
_reset() {
|
|
1417
2236
|
|
|
2237
|
+
this._geometryVersion = ( this._geometryVersion ?? 0 ) + 1;
|
|
2238
|
+
this._disposeSpill();
|
|
2239
|
+
this._disposeGeometrySpill();
|
|
2240
|
+
|
|
1418
2241
|
// First dispose any existing resources
|
|
1419
2242
|
this._disposeTextures();
|
|
1420
2243
|
|
|
@@ -1772,6 +2595,8 @@ export class SceneProcessor {
|
|
|
1772
2595
|
*/
|
|
1773
2596
|
async refitBVH( newPositions, newNormals ) {
|
|
1774
2597
|
|
|
2598
|
+
this._geometryVersion = ( this._geometryVersion ?? 0 ) + 1;
|
|
2599
|
+
|
|
1775
2600
|
if ( ! this.bvh || ! this.triangles || ! this.instanceTable ) {
|
|
1776
2601
|
|
|
1777
2602
|
throw new Error( 'No BVH data available for refit. Run buildBVH() first.' );
|
|
@@ -1909,6 +2734,8 @@ export class SceneProcessor {
|
|
|
1909
2734
|
|
|
1910
2735
|
refitBLASes( affectedMeshIndices, newPositions, newNormals ) {
|
|
1911
2736
|
|
|
2737
|
+
this._geometryVersion = ( this._geometryVersion ?? 0 ) + 1;
|
|
2738
|
+
|
|
1912
2739
|
if ( ! this.instanceTable || ! this.bvh || ! this.triangles ) {
|
|
1913
2740
|
|
|
1914
2741
|
throw new Error( 'No TLAS/BLAS data available. Run buildBVH() first.' );
|
|
@@ -2138,9 +2965,10 @@ export class SceneProcessor {
|
|
|
2138
2965
|
* @param {import('../managers/LightManager.js').LightManager} lightManager
|
|
2139
2966
|
* @param {import('three').Scene} meshScene
|
|
2140
2967
|
* @param {import('three').Texture|null} environmentTexture
|
|
2968
|
+
* @param {{keepUserLights?: boolean}} [options] - an incremental rebuild keeps the lights a host added
|
|
2141
2969
|
* @returns {boolean} false if critical data is missing
|
|
2142
2970
|
*/
|
|
2143
|
-
uploadToPathTracer( pathTracer, lightManager, meshScene, environmentTexture ) {
|
|
2971
|
+
uploadToPathTracer( pathTracer, lightManager, meshScene, environmentTexture, { keepUserLights = false } = {} ) {
|
|
2144
2972
|
|
|
2145
2973
|
if ( ! this.triangles ) {
|
|
2146
2974
|
|
|
@@ -2193,6 +3021,8 @@ export class SceneProcessor {
|
|
|
2193
3021
|
this.emissiveTotalPower,
|
|
2194
3022
|
this.emissiveBitTrailMap,
|
|
2195
3023
|
);
|
|
3024
|
+
// The light buffer holds the map from here; a rebuild makes a new one.
|
|
3025
|
+
this._releaseBitTrailMap();
|
|
2196
3026
|
|
|
2197
3027
|
}
|
|
2198
3028
|
|
|
@@ -2205,7 +3035,7 @@ export class SceneProcessor {
|
|
|
2205
3035
|
|
|
2206
3036
|
}
|
|
2207
3037
|
|
|
2208
|
-
lightManager.transferSceneLights( meshScene );
|
|
3038
|
+
lightManager.transferSceneLights( meshScene, { keepUserLights } );
|
|
2209
3039
|
return true;
|
|
2210
3040
|
|
|
2211
3041
|
}
|
|
@@ -2319,17 +3149,132 @@ export class SceneProcessor {
|
|
|
2319
3149
|
this.emissiveTriangleCount = this.emissiveTriangleBuilder.emissiveCount;
|
|
2320
3150
|
this.emissiveTotalPower = this.emissiveTriangleBuilder.totalEmissivePower;
|
|
2321
3151
|
|
|
3152
|
+
const bitTrailMap = this.emissiveBitTrailMap;
|
|
3153
|
+
this._releaseBitTrailMap();
|
|
3154
|
+
|
|
2322
3155
|
return {
|
|
2323
3156
|
rawData: this.emissiveTriangleData,
|
|
2324
3157
|
emissiveCount: this.emissiveTriangleCount,
|
|
2325
3158
|
totalPower: this.emissiveTotalPower,
|
|
2326
|
-
bitTrailMap
|
|
3159
|
+
bitTrailMap,
|
|
2327
3160
|
lightBVHNodeData: this.lightBVHNodeData,
|
|
2328
3161
|
lightBVHNodeCount: this.lightBVHNodeCount,
|
|
2329
3162
|
};
|
|
2330
3163
|
|
|
2331
3164
|
}
|
|
2332
3165
|
|
|
3166
|
+
/** Whether any triangle records or BLAS nodes are on disk (see {@link spillToDisk}). */
|
|
3167
|
+
get spilled() {
|
|
3168
|
+
|
|
3169
|
+
return !! ( this.triangles?.spilledChunks || this.bvh?.spilledChunks );
|
|
3170
|
+
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
/**
|
|
3174
|
+
* Experimental memory spill: once the scene is on the GPU, moves the triangle records and the
|
|
3175
|
+
* BLAS part of the BVH to disk. The TLAS stays, so visibility and rigid moves need nothing
|
|
3176
|
+
* back; anything that reads triangles or BLAS nodes awaits {@link ensureResident} first.
|
|
3177
|
+
* Multi-chunk scenes only — below a chunk the saving is small and the cost is not.
|
|
3178
|
+
* @param {import('../Storage/StorageManager.js').StorageManager} storage
|
|
3179
|
+
* @returns {Promise<?{bytes: number, ms: number}>} null when nothing was spilled
|
|
3180
|
+
*/
|
|
3181
|
+
async spillToDisk( storage ) {
|
|
3182
|
+
|
|
3183
|
+
const triangles = this.triangles;
|
|
3184
|
+
const bvh = this.bvh;
|
|
3185
|
+
if ( ! storage || this.spilled || this._spillStores || ! triangles?.chunks || triangles.chunks.length < 2 || ! bvh?.chunks ) return null;
|
|
3186
|
+
|
|
3187
|
+
await this._blasStoring;
|
|
3188
|
+
const version = this._geometryVersion;
|
|
3189
|
+
const started = performance.now();
|
|
3190
|
+
const key = `spill:${Date.now().toString( 36 )}:${Math.random().toString( 36 ).slice( 2 )}`;
|
|
3191
|
+
const stride = records => records.recordsPerChunk * records.lanesPerRecord * records.LaneType.BYTES_PER_ELEMENT;
|
|
3192
|
+
|
|
3193
|
+
const triStore = await SpillStore.create( storage, `${key}:tri`, stride( triangles ), { label: 'Triangle records', expectedBytes: triangles.byteLength } );
|
|
3194
|
+
const bvhStore = triStore && await SpillStore.create( storage, `${key}:bvh`, stride( bvh ), { label: 'BLAS nodes', expectedBytes: bvh.byteLength } );
|
|
3195
|
+
if ( ! triStore || ! bvhStore ) {
|
|
3196
|
+
|
|
3197
|
+
await triStore?.dispose();
|
|
3198
|
+
return null;
|
|
3199
|
+
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
this._spillStores = [ triStore, bvhStore ];
|
|
3203
|
+
const tlasChunks = Math.ceil( ( this.instanceTable?.tlasNodeCount ?? 1 ) / bvh.recordsPerChunk );
|
|
3204
|
+
|
|
3205
|
+
let bytes = await triangles.spill( triStore );
|
|
3206
|
+
bytes += await bvh.spill( bvhStore, { keep: k => k < tlasChunks } );
|
|
3207
|
+
await triStore.flush();
|
|
3208
|
+
await bvhStore.flush();
|
|
3209
|
+
|
|
3210
|
+
if ( version !== this._geometryVersion ) return null;
|
|
3211
|
+
const ms = performance.now() - started;
|
|
3212
|
+
log.info( `spilled ${( bytes / 1048576 ).toFixed( 0 )} MB of triangles and BLAS nodes to disk in ${ms.toFixed( 0 )} ms` );
|
|
3213
|
+
return { bytes, ms };
|
|
3214
|
+
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
/** Reads every spilled triangle record and BLAS node back, then drops the scratch files. */
|
|
3218
|
+
ensureResident() {
|
|
3219
|
+
|
|
3220
|
+
this._paging ??= ( async () => {
|
|
3221
|
+
|
|
3222
|
+
await this.triangles?.ensureResident?.();
|
|
3223
|
+
await this.bvh?.ensureResident?.();
|
|
3224
|
+
if ( ! this.spilled ) this._disposeSpill();
|
|
3225
|
+
|
|
3226
|
+
} )().finally( () => {
|
|
3227
|
+
|
|
3228
|
+
this._paging = null;
|
|
3229
|
+
|
|
3230
|
+
} );
|
|
3231
|
+
|
|
3232
|
+
return this._paging;
|
|
3233
|
+
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
/** Reads the three.js geometry a streamed build moved to disk back into place. @private */
|
|
3237
|
+
async _restoreGeometry( object ) {
|
|
3238
|
+
|
|
3239
|
+
const spill = this._geometrySpill;
|
|
3240
|
+
if ( ! spill?.bytes ) return;
|
|
3241
|
+
updateLoading( { status: 'Reading scene geometry back...', progress: 84 } );
|
|
3242
|
+
const started = performance.now();
|
|
3243
|
+
const bytes = spill.bytes;
|
|
3244
|
+
await spill.restore();
|
|
3245
|
+
this._disposeGeometrySpill();
|
|
3246
|
+
this._geometryBytes = geometryBytesOf( object );
|
|
3247
|
+
log.info( `read ${fmt.mb( bytes )} of scene geometry back from disk in ${fmt.ms( performance.now() - started )}` );
|
|
3248
|
+
if ( spill.error ) log.warn( 'some scene geometry stayed in memory, its spill write failed:', spill.error );
|
|
3249
|
+
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
/** @private */
|
|
3253
|
+
_disposeGeometrySpill() {
|
|
3254
|
+
|
|
3255
|
+
const spill = this._geometrySpill;
|
|
3256
|
+
this._geometrySpill = null;
|
|
3257
|
+
spill?.dispose().catch( () => {} );
|
|
3258
|
+
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
/** @private */
|
|
3262
|
+
_disposeSpill() {
|
|
3263
|
+
|
|
3264
|
+
const stores = this._spillStores;
|
|
3265
|
+
this._spillStores = null;
|
|
3266
|
+
for ( const store of stores ?? [] ) store.dispose().catch( () => {} );
|
|
3267
|
+
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
/** The map goes to the path tracer's light buffer and is not kept here as well. */
|
|
3271
|
+
_releaseBitTrailMap() {
|
|
3272
|
+
|
|
3273
|
+
this.emissiveBitTrailMap = null;
|
|
3274
|
+
if ( this.emissiveTriangleBuilder ) this.emissiveTriangleBuilder.emissiveBitTrailMap = null;
|
|
3275
|
+
|
|
3276
|
+
}
|
|
3277
|
+
|
|
2333
3278
|
/**
|
|
2334
3279
|
* Copy world-space positions into `dst`, each mesh's range carried into its own space.
|
|
2335
3280
|
* @private
|
|
@@ -2417,6 +3362,8 @@ export class SceneProcessor {
|
|
|
2417
3362
|
: ChunkedRecords.adopt( [ data ], data.length / 16, 16, data.length / 16 ) );
|
|
2418
3363
|
|
|
2419
3364
|
this.bvh = records;
|
|
3365
|
+
// Read by the traversal when it is built: only a folded BVH gets that code.
|
|
3366
|
+
if ( records && this._foldLeaves ) records.foldedLeaves = true;
|
|
2420
3367
|
this.bvhIndexChunks = records ? records.viewAs( Uint32Array ) : null;
|
|
2421
3368
|
// Null once the BVH needs more than one chunk; hot paths use `bvh` / `bvhIndexChunks`.
|
|
2422
3369
|
this.bvhData = records ? records.single : null;
|
|
@@ -2771,6 +3718,8 @@ export class SceneProcessor {
|
|
|
2771
3718
|
*/
|
|
2772
3719
|
_swapBLAS( meshIdx, entry, workerData, onSwap ) {
|
|
2773
3720
|
|
|
3721
|
+
this._geometryVersion = ( this._geometryVersion ?? 0 ) + 1;
|
|
3722
|
+
|
|
2774
3723
|
const FPN = 16;
|
|
2775
3724
|
const newBvhData = workerData.bvhData;
|
|
2776
3725
|
const newNodeCount = newBvhData.length / FPN;
|
|
@@ -2885,6 +3834,8 @@ export class SceneProcessor {
|
|
|
2885
3834
|
*/
|
|
2886
3835
|
dispose() {
|
|
2887
3836
|
|
|
3837
|
+
this._geometryVersion = ( this._geometryVersion ?? 0 ) + 1;
|
|
3838
|
+
|
|
2888
3839
|
this._log( 'Disposing resources' );
|
|
2889
3840
|
|
|
2890
3841
|
// Dispose workers
|