rayzee 9.1.2 → 9.2.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 +174 -15
- package/dist/{NeuralRetouch-BRquqfmZ.js → NeuralRetouch-DgnfldWO.js} +1 -1
- package/dist/{NeuralSuperRes-CbNvQjCw.js → NeuralSuperRes-Baa2w8VU.js} +72 -40
- package/dist/{ToneMapGPU-DpUZDMbQ.js → ToneMapGPU-CNgexVto.js} +75 -71
- package/dist/rayzee.es.js +4915 -4314
- package/dist/rayzee.umd.js +96 -96
- package/package.json +3 -2
- package/src/AssetConfig.js +10 -5
- package/src/EngineEvents.js +41 -1
- package/src/EngineIssues.js +3 -0
- package/src/Headless.js +23 -7
- package/src/HeadlessCanvas.js +104 -0
- package/src/Passes/OIDNDenoiser.js +29 -4
- package/src/Passes/OIDNTemporalHistory.js +7 -0
- package/src/PathTracerApp.js +432 -119
- package/src/Platform.js +76 -0
- package/src/Processor/AssetLoader.js +66 -4
- package/src/Processor/BVHBuilder.js +2 -1
- package/src/Processor/EquirectHDRInfo.js +2 -1
- package/src/Processor/HostMemory.js +14 -0
- package/src/Processor/PBRT/PBRTSceneBuilder.js +26 -0
- package/src/Processor/ParallelBVHBuilder.js +7 -6
- package/src/Processor/PlatformImageLoader.js +118 -0
- package/src/Processor/ResampleRGBA8.js +102 -0
- package/src/Processor/SceneProcessor.js +7 -6
- package/src/Processor/TextureCreator.js +162 -13
- package/src/Processor/Workers/PackWorker.js +28 -0
- package/src/Stages/Compositor.js +25 -6
- package/src/Stages/PathTracer.js +142 -25
- package/src/Stages/PathTracerStage.js +6 -96
- package/src/TSL/DebugKernel.js +1 -1
- package/src/TSL/GenerateKernel.js +2 -5
- package/src/TSL/Random.js +3 -120
- package/src/TSL/ShadeKernel.js +3 -3
- package/src/index.js +2 -1
- package/src/managers/CameraManager.js +2 -2
- package/src/managers/DenoisingManager.js +76 -7
- package/src/managers/GoboManager.js +8 -1
- package/src/managers/IESManager.js +13 -2
- package/src/managers/InteractionManager.js +1 -1
- package/src/managers/LightManager.js +31 -0
- package/src/managers/OverlayManager.js +11 -1
- package/src/managers/VideoRenderManager.js +8 -0
- package/src/node/NodeWorker.js +144 -0
- package/src/node/index.js +9 -0
- package/src/node/nodePlatform.js +86 -0
|
@@ -2,13 +2,37 @@ import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColor
|
|
|
2
2
|
import { alignBucketWidth, TEXTURE_CONSTANTS, MEMORY_CONSTANTS, MATERIAL_DATA_LAYOUT, BVH_LEAF_MARKERS, assertBVHIndexFits, bvhIndexView } from '../EngineDefaults.js';
|
|
3
3
|
import { packMaterial } from './MaterialPacking.js';
|
|
4
4
|
import TexturesWorker from './Workers/TexturesWorker.js?worker&inline';
|
|
5
|
-
import
|
|
5
|
+
import PackWorker from './Workers/PackWorker.js?worker&inline';
|
|
6
|
+
import { resampleRGBA8 } from './ResampleRGBA8.js';
|
|
7
|
+
import { ISSUE_CODES, EngineIssueError } from '../EngineIssues.js';
|
|
6
8
|
import { linearToSRGB } from './ToneMapCPU.js';
|
|
7
9
|
import { getActiveColorManagement } from '../Color/ColorManagement.js';
|
|
8
10
|
import { createLogger } from '../utils/Logger.js';
|
|
11
|
+
import { createWorker, hardwareThreads, hasWorkers } from '../Platform.js';
|
|
9
12
|
|
|
10
13
|
const log = createLogger( 'textures' );
|
|
11
14
|
|
|
15
|
+
// Below this a pack is cheaper than starting a worker for it.
|
|
16
|
+
const WORKER_PACK_BYTES = 8 * 2 ** 20;
|
|
17
|
+
|
|
18
|
+
// Buckets pack concurrently; this keeps their workers within the cores, beside the BVH pool's.
|
|
19
|
+
const packSlots = { busy: 0, waiting: [] };
|
|
20
|
+
|
|
21
|
+
async function acquirePackSlot() {
|
|
22
|
+
|
|
23
|
+
if ( packSlots.busy < Math.max( 1, hardwareThreads() - 1 ) ) packSlots.busy ++;
|
|
24
|
+
else await new Promise( ( resolve ) => packSlots.waiting.push( resolve ) );
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
|
|
28
|
+
const next = packSlots.waiting.shift();
|
|
29
|
+
if ( next ) next();
|
|
30
|
+
else packSlots.busy --;
|
|
31
|
+
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
// A bucket array carries ONE colorSpace and the shader reads each slot from a fixed pool, so a
|
|
13
37
|
// texture disagreeing with its pool cannot be re-routed — its pixels are re-encoded instead.
|
|
14
38
|
const TRANSFER_LUT = { toSRGB: null, toLinear: null };
|
|
@@ -529,6 +553,7 @@ export class TextureCreator {
|
|
|
529
553
|
/** A failed map array leaves those surfaces untextured — complete-looking and wrong. @private */
|
|
530
554
|
_reportTextureFailure( map, error ) {
|
|
531
555
|
|
|
556
|
+
if ( error instanceof EngineIssueError ) throw error;
|
|
532
557
|
this._issues?.record(
|
|
533
558
|
ISSUE_CODES.TEXTURE_BUILD_FAILED,
|
|
534
559
|
`${map} texture array failed to build — those surfaces render untextured`,
|
|
@@ -537,6 +562,17 @@ export class TextureCreator {
|
|
|
537
562
|
|
|
538
563
|
}
|
|
539
564
|
|
|
565
|
+
// Layers are addressed by position, so a texture that cannot be read keeps its slot.
|
|
566
|
+
_reportTextureLayer( layer, cause ) {
|
|
567
|
+
|
|
568
|
+
this._issues?.record(
|
|
569
|
+
ISSUE_CODES.TEXTURE_BUILD_FAILED,
|
|
570
|
+
`texture ${layer} could not be read — its surfaces show a white placeholder`,
|
|
571
|
+
{ layer, cause: String( cause?.message ?? cause ) }
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
}
|
|
575
|
+
|
|
540
576
|
_clampTextureSize( size ) {
|
|
541
577
|
|
|
542
578
|
const n = Math.floor( Number( size ) || TEXTURE_CONSTANTS.DEFAULT_MAX_TEXTURE_SIZE );
|
|
@@ -562,7 +598,7 @@ export class TextureCreator {
|
|
|
562
598
|
offscreenCanvas: typeof OffscreenCanvas !== 'undefined',
|
|
563
599
|
imageBitmap: typeof createImageBitmap !== 'undefined',
|
|
564
600
|
workers: typeof Worker !== 'undefined',
|
|
565
|
-
hardwareConcurrency:
|
|
601
|
+
hardwareConcurrency: hardwareThreads()
|
|
566
602
|
};
|
|
567
603
|
|
|
568
604
|
}
|
|
@@ -775,6 +811,16 @@ export class TextureCreator {
|
|
|
775
811
|
const cached = this.textureCache.get( cacheKey );
|
|
776
812
|
if ( cached ) return cached;
|
|
777
813
|
|
|
814
|
+
if ( typeof createImageBitmap === 'undefined' ) {
|
|
815
|
+
|
|
816
|
+
const packed = await this.processOnCPU( textures );
|
|
817
|
+
const reencoded = this._harmonizeTransfer( packed, textures, srgbPool );
|
|
818
|
+
this._applyColorManagement( packed, textures, srgbPool, reencoded );
|
|
819
|
+
this.textureCache.set( cacheKey, packed );
|
|
820
|
+
return packed;
|
|
821
|
+
|
|
822
|
+
}
|
|
823
|
+
|
|
778
824
|
// Normalize non-drawable images (KTX2 CompressedTexture RGBA, DataTexture)
|
|
779
825
|
const { normalized, bitmapsToClose } = await this._normalizeTexturesForProcessing( textures );
|
|
780
826
|
|
|
@@ -813,6 +859,7 @@ export class TextureCreator {
|
|
|
813
859
|
|
|
814
860
|
} catch ( error ) {
|
|
815
861
|
|
|
862
|
+
if ( error instanceof EngineIssueError ) throw error;
|
|
816
863
|
this._issues?.warn(
|
|
817
864
|
ISSUE_CODES.TEXTURE_PROCESSING_FALLBACK,
|
|
818
865
|
'worker texture processing failed — retrying on the main thread',
|
|
@@ -885,9 +932,9 @@ export class TextureCreator {
|
|
|
885
932
|
|
|
886
933
|
this.activeWorkers ++;
|
|
887
934
|
|
|
888
|
-
|
|
935
|
+
const worker = new TexturesWorker();
|
|
889
936
|
|
|
890
|
-
|
|
937
|
+
try {
|
|
891
938
|
|
|
892
939
|
// Prepare textures for worker with direct transfer
|
|
893
940
|
const texturesData = await this.prepareTexturesForWorkerDirect( textures );
|
|
@@ -934,11 +981,11 @@ export class TextureCreator {
|
|
|
934
981
|
|
|
935
982
|
} );
|
|
936
983
|
|
|
937
|
-
worker.terminate();
|
|
938
984
|
return this.createDataArrayTextureFromResult( result );
|
|
939
985
|
|
|
940
986
|
} finally {
|
|
941
987
|
|
|
988
|
+
worker.terminate();
|
|
942
989
|
this.activeWorkers --;
|
|
943
990
|
|
|
944
991
|
}
|
|
@@ -949,10 +996,18 @@ export class TextureCreator {
|
|
|
949
996
|
async prepareTexturesForWorkerDirect( textures ) {
|
|
950
997
|
|
|
951
998
|
const texturesData = [];
|
|
999
|
+
const placeholder = () => ( { data: new Uint8ClampedArray( [ 255, 255, 255, 255 ] ).buffer, width: 1, height: 1, isImageData: true } );
|
|
952
1000
|
|
|
953
|
-
for (
|
|
1001
|
+
for ( let layer = 0; layer < textures.length; layer ++ ) {
|
|
954
1002
|
|
|
955
|
-
|
|
1003
|
+
const texture = textures[ layer ];
|
|
1004
|
+
if ( ! texture?.image ) {
|
|
1005
|
+
|
|
1006
|
+
this._reportTextureLayer( layer, 'no image' );
|
|
1007
|
+
texturesData.push( placeholder() );
|
|
1008
|
+
continue;
|
|
1009
|
+
|
|
1010
|
+
}
|
|
956
1011
|
|
|
957
1012
|
const flipY = texture.flipY !== false;
|
|
958
1013
|
|
|
@@ -964,7 +1019,7 @@ export class TextureCreator {
|
|
|
964
1019
|
// getImageData off the main thread (Option 2 blocks it per texture).
|
|
965
1020
|
const img = texture.image;
|
|
966
1021
|
const canDirect = typeof createImageBitmap !== 'undefined' && (
|
|
967
|
-
img instanceof HTMLImageElement
|
|
1022
|
+
( typeof HTMLImageElement !== 'undefined' && img instanceof HTMLImageElement )
|
|
968
1023
|
|| ( typeof ImageBitmap !== 'undefined' && img instanceof ImageBitmap )
|
|
969
1024
|
|| ( typeof HTMLCanvasElement !== 'undefined' && img instanceof HTMLCanvasElement )
|
|
970
1025
|
|| ( typeof OffscreenCanvas !== 'undefined' && img instanceof OffscreenCanvas )
|
|
@@ -1008,7 +1063,8 @@ export class TextureCreator {
|
|
|
1008
1063
|
|
|
1009
1064
|
} catch ( error ) {
|
|
1010
1065
|
|
|
1011
|
-
|
|
1066
|
+
this._reportTextureLayer( layer, error );
|
|
1067
|
+
texturesData.push( placeholder() );
|
|
1012
1068
|
|
|
1013
1069
|
}
|
|
1014
1070
|
|
|
@@ -1018,6 +1074,93 @@ export class TextureCreator {
|
|
|
1018
1074
|
|
|
1019
1075
|
}
|
|
1020
1076
|
|
|
1077
|
+
/**
|
|
1078
|
+
* Packing without a browser (no createImageBitmap, no 2D canvas): each layer must already be
|
|
1079
|
+
* pixels — a DataTexture, or an image the platform's decodeImage decoded. A layer that matches its
|
|
1080
|
+
* bucket is copied exactly; one that does not is resampled bilinearly, where a browser's canvas
|
|
1081
|
+
* would use its own filter, so those texels can differ slightly from a browser render. A large
|
|
1082
|
+
* bucket packs in a worker: on the main thread it starved the BVH workers of their next task.
|
|
1083
|
+
*/
|
|
1084
|
+
async processOnCPU( textures ) {
|
|
1085
|
+
|
|
1086
|
+
const layers = textures.map( ( texture, layer ) => {
|
|
1087
|
+
|
|
1088
|
+
const image = texture?.image;
|
|
1089
|
+
const pixels = texture?.isCompressedTexture ? texture.mipmaps?.[ 0 ] : image;
|
|
1090
|
+
const data = pixels?.data;
|
|
1091
|
+
if ( data && pixels.width > 0 && pixels.height > 0 && data.byteLength >= pixels.width * pixels.height * 4 ) {
|
|
1092
|
+
|
|
1093
|
+
return { data: new Uint8Array( data.buffer, data.byteOffset, pixels.width * pixels.height * 4 ), width: pixels.width, height: pixels.height, flipY: texture.flipY !== false };
|
|
1094
|
+
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
this._reportTextureLayer( layer, image ? 'not decoded to pixels, and there is no browser to decode it' : 'no image' );
|
|
1098
|
+
return { data: new Uint8Array( [ 255, 255, 255, 255 ] ), width: 1, height: 1, flipY: false };
|
|
1099
|
+
|
|
1100
|
+
} );
|
|
1101
|
+
|
|
1102
|
+
const { maxWidth, maxHeight } = this.calculateOptimalDimensions( layers.map( ( image ) => ( { image } ) ) );
|
|
1103
|
+
const layerBytes = maxWidth * maxHeight * 4;
|
|
1104
|
+
|
|
1105
|
+
const packed = hasWorkers() && layerBytes * layers.length >= WORKER_PACK_BYTES
|
|
1106
|
+
? await this._packInWorker( layers, maxWidth, maxHeight )
|
|
1107
|
+
: null;
|
|
1108
|
+
if ( packed ) return this.createDataArrayTextureFromBuffer( packed, maxWidth, maxHeight, layers.length );
|
|
1109
|
+
|
|
1110
|
+
const data = this.bufferPool.getBuffer( layerBytes * layers.length, Uint8Array );
|
|
1111
|
+
layers.forEach( ( layer, i ) => resampleRGBA8( layer, data, i * layerBytes, maxWidth, maxHeight ) );
|
|
1112
|
+
|
|
1113
|
+
return this.createDataArrayTextureFromBuffer( data, maxWidth, maxHeight, layers.length );
|
|
1114
|
+
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/** The packed pixels, or null to pack on the main thread instead. */
|
|
1118
|
+
async _packInWorker( layers, width, height ) {
|
|
1119
|
+
|
|
1120
|
+
const release = await acquirePackSlot();
|
|
1121
|
+
let worker = null;
|
|
1122
|
+
|
|
1123
|
+
try {
|
|
1124
|
+
|
|
1125
|
+
// A SharedArrayBuffer view crosses as itself; anything else as a copy of just its bytes,
|
|
1126
|
+
// made only once this bucket has its slot so waiting buckets hold no copies.
|
|
1127
|
+
const transfer = [];
|
|
1128
|
+
const sent = layers.map( ( layer ) => {
|
|
1129
|
+
|
|
1130
|
+
if ( typeof SharedArrayBuffer !== 'undefined' && layer.data.buffer instanceof SharedArrayBuffer ) return layer;
|
|
1131
|
+
const data = layer.data.slice();
|
|
1132
|
+
transfer.push( data.buffer );
|
|
1133
|
+
return { ...layer, data };
|
|
1134
|
+
|
|
1135
|
+
} );
|
|
1136
|
+
|
|
1137
|
+
worker = createWorker( PackWorker );
|
|
1138
|
+
return await new Promise( ( resolve, reject ) => {
|
|
1139
|
+
|
|
1140
|
+
worker.onmessage = ( e ) => ( e.data.error ? reject( new Error( e.data.error ) ) : resolve( e.data.data ) );
|
|
1141
|
+
worker.onerror = ( e ) => reject( e.error ?? new Error( e.message ?? 'texture pack worker failed' ) );
|
|
1142
|
+
worker.postMessage( { layers: sent, width, height }, transfer );
|
|
1143
|
+
|
|
1144
|
+
} );
|
|
1145
|
+
|
|
1146
|
+
} catch ( error ) {
|
|
1147
|
+
|
|
1148
|
+
this._issues?.warn(
|
|
1149
|
+
ISSUE_CODES.TEXTURE_PROCESSING_FALLBACK,
|
|
1150
|
+
'worker texture packing failed — packing on the main thread',
|
|
1151
|
+
{ cause: String( error?.message ?? error ) }
|
|
1152
|
+
);
|
|
1153
|
+
return null;
|
|
1154
|
+
|
|
1155
|
+
} finally {
|
|
1156
|
+
|
|
1157
|
+
worker?.terminate();
|
|
1158
|
+
release();
|
|
1159
|
+
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1021
1164
|
async processOnMainThreadBatch( textures, batchSize ) {
|
|
1022
1165
|
|
|
1023
1166
|
const validTextures = textures.filter( tex => tex?.image );
|
|
@@ -1465,7 +1608,13 @@ export class TextureCreator {
|
|
|
1465
1608
|
|
|
1466
1609
|
for ( const tex of textures ) {
|
|
1467
1610
|
|
|
1468
|
-
if ( ! tex?.image )
|
|
1611
|
+
if ( ! tex?.image ) {
|
|
1612
|
+
|
|
1613
|
+
this._reportTextureLayer( normalized.length, 'no image' );
|
|
1614
|
+
normalized.push( null );
|
|
1615
|
+
continue;
|
|
1616
|
+
|
|
1617
|
+
}
|
|
1469
1618
|
|
|
1470
1619
|
// RGBA CompressedTexture (KTX2 Basis transcode wraps output as CompressedTexture)
|
|
1471
1620
|
if ( tex.isCompressedTexture && tex.format === RGBAFormat && tex.mipmaps?.[ 0 ]?.data ) {
|
|
@@ -1489,8 +1638,8 @@ export class TextureCreator {
|
|
|
1489
1638
|
}
|
|
1490
1639
|
|
|
1491
1640
|
// DataTexture with raw pixel array
|
|
1492
|
-
if ( tex.image.data && ! ( tex.image instanceof HTMLImageElement ) &&
|
|
1493
|
-
! ( tex.image instanceof HTMLCanvasElement ) &&
|
|
1641
|
+
if ( tex.image.data && ! ( typeof HTMLImageElement !== 'undefined' && tex.image instanceof HTMLImageElement ) &&
|
|
1642
|
+
! ( typeof HTMLCanvasElement !== 'undefined' && tex.image instanceof HTMLCanvasElement ) &&
|
|
1494
1643
|
! ( typeof ImageBitmap !== 'undefined' && tex.image instanceof ImageBitmap ) ) {
|
|
1495
1644
|
|
|
1496
1645
|
const idx = normalized.length;
|
|
@@ -1522,7 +1671,7 @@ export class TextureCreator {
|
|
|
1522
1671
|
|
|
1523
1672
|
} else {
|
|
1524
1673
|
|
|
1525
|
-
|
|
1674
|
+
this._reportTextureLayer( index, result.reason );
|
|
1526
1675
|
|
|
1527
1676
|
}
|
|
1528
1677
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Packs RGBA8 layers into one texture array off the main thread, for hosts without a browser, where
|
|
3
|
+
* TextureCreator.processOnCPU would otherwise hold up the BVH workers it feeds.
|
|
4
|
+
*
|
|
5
|
+
* Input: { layers: [{ data: Uint8Array (SharedArrayBuffer-backed), width, height, flipY }], width, height }
|
|
6
|
+
* Output: { data: Uint8Array } — its buffer transferred
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { resampleRGBA8 } from '../ResampleRGBA8.js';
|
|
10
|
+
|
|
11
|
+
self.onmessage = function ( e ) {
|
|
12
|
+
|
|
13
|
+
const { layers, width, height } = e.data;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
|
|
17
|
+
const layerBytes = width * height * 4;
|
|
18
|
+
const data = new Uint8Array( layerBytes * layers.length );
|
|
19
|
+
layers.forEach( ( layer, i ) => resampleRGBA8( layer, data, i * layerBytes, width, height ) );
|
|
20
|
+
self.postMessage( { data }, [ data.buffer ] );
|
|
21
|
+
|
|
22
|
+
} catch ( error ) {
|
|
23
|
+
|
|
24
|
+
self.postMessage( { error: error.message } );
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
};
|
package/src/Stages/Compositor.js
CHANGED
|
@@ -4,6 +4,16 @@ import { NoBlending, NoToneMapping } from 'three';
|
|
|
4
4
|
import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
5
5
|
import { REC709_LUMINANCE_COEFFICIENTS } from '../TSL/Common.js';
|
|
6
6
|
|
|
7
|
+
// In priority order: the first one published wins.
|
|
8
|
+
const LIGHT_SOURCES = [
|
|
9
|
+
'oidn:output',
|
|
10
|
+
'edgeFiltering:output',
|
|
11
|
+
'bilateralFiltering:output',
|
|
12
|
+
'asvgf:output',
|
|
13
|
+
'nrd:output',
|
|
14
|
+
'pathtracer:color',
|
|
15
|
+
];
|
|
16
|
+
|
|
7
17
|
/**
|
|
8
18
|
* Compositor — Terminal pipeline stage.
|
|
9
19
|
*
|
|
@@ -76,12 +86,21 @@ export class Compositor extends RenderStage {
|
|
|
76
86
|
*/
|
|
77
87
|
resolveLightTexture( context ) {
|
|
78
88
|
|
|
79
|
-
return
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
return this.resolveLightSource( context )?.texture ?? null;
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** {@link resolveLightTexture} plus the context key it came from. */
|
|
94
|
+
resolveLightSource( context ) {
|
|
95
|
+
|
|
96
|
+
for ( const key of LIGHT_SOURCES ) {
|
|
97
|
+
|
|
98
|
+
const texture = context.getTexture( key );
|
|
99
|
+
if ( texture ) return { key, texture };
|
|
100
|
+
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null;
|
|
85
104
|
|
|
86
105
|
}
|
|
87
106
|
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from '../TSL/SortGlobalKernels.js';
|
|
26
26
|
import { ENGINE_DEFAULTS, MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
|
|
27
27
|
import { createLogger, fmt } from '../utils/Logger.js';
|
|
28
|
+
import { deviceMemoryGB } from '../Processor/HostMemory.js';
|
|
28
29
|
import {
|
|
29
30
|
Fn, uint, int, atomicStore, atomicLoad, atomicAdd, instanceIndex, If, Return,
|
|
30
31
|
} from 'three/tsl';
|
|
@@ -113,6 +114,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
113
114
|
this._geometryPixelCount = 0;
|
|
114
115
|
this._convergedReadbackPending = false;
|
|
115
116
|
|
|
117
|
+
// Lockstep: readbacks issued on a fixed cadence and applied a fixed number of frames later,
|
|
118
|
+
// so when one lands cannot change the image. { due, read, apply } while one is in flight.
|
|
119
|
+
this._lockstep = false;
|
|
120
|
+
this._lockstepRead = null;
|
|
121
|
+
|
|
116
122
|
// Tier-2: last settled active-pixel count (maxRays − frozen), sizes next frame's bounce-0 grid.
|
|
117
123
|
// 0 until a settled readback lands (and on reset/camera-move/resize) → grid stays full-size until then.
|
|
118
124
|
this._lastActivePixelCount = 0;
|
|
@@ -131,6 +137,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
131
137
|
this._wfIsFirstChunk = uniform( 1, 'uint' ); // 1 → zero frame-scoped counters (CONVERGED/FROZEN)
|
|
132
138
|
this._pathBudget = 0; // B, paths-in-flight; computed once from device limits in _buildWavefrontKernels
|
|
133
139
|
this._pathBudgetOverride = 0; // test hook: force B (0 = derive from device)
|
|
140
|
+
this.hostMemoryGB = options.hostMemoryGB;
|
|
134
141
|
this._chunkRows = 0; // rows per chunk = floor(B / renderWidth), clamped ≥1
|
|
135
142
|
this._numChunks = 1; // ceil(renderHeight / chunkRows)
|
|
136
143
|
|
|
@@ -244,6 +251,15 @@ export class PathTracer extends PathTracerStage {
|
|
|
244
251
|
|
|
245
252
|
}
|
|
246
253
|
|
|
254
|
+
const lockstepRead = this._lockstepRead;
|
|
255
|
+
if ( lockstepRead && this.frameCount >= lockstepRead.due ) {
|
|
256
|
+
|
|
257
|
+
if ( ! lockstepRead.apply ) return; // see readbackWait()
|
|
258
|
+
this._lockstepRead = null;
|
|
259
|
+
lockstepRead.apply();
|
|
260
|
+
|
|
261
|
+
}
|
|
262
|
+
|
|
247
263
|
if ( this.isComplete || this.frameCount >= this.completionThreshold || this._isConvergedComplete() ) {
|
|
248
264
|
|
|
249
265
|
// Denoiser switched on after the render finished — spend one more frame to fill the aux MRT.
|
|
@@ -657,6 +673,54 @@ export class PathTracer extends PathTracerStage {
|
|
|
657
673
|
this._auxSamples = 0;
|
|
658
674
|
this._auxSeedPending = false;
|
|
659
675
|
|
|
676
|
+
if ( this._lockstep ) {
|
|
677
|
+
|
|
678
|
+
// Nothing measured by the previous render may steer this one.
|
|
679
|
+
this._lockstepRead = null;
|
|
680
|
+
this._readbackGeneration ++;
|
|
681
|
+
this._lastBounceCounts = null;
|
|
682
|
+
this._lastBounceEnergy = null;
|
|
683
|
+
this._lastBounceCountsBudget = - 1;
|
|
684
|
+
this._lastBounceCountsLoopBound = - 1;
|
|
685
|
+
this._curveSizingValid = false;
|
|
686
|
+
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Lockstep readbacks: the survivor curve and the convergence counters are read every N frames
|
|
693
|
+
* and applied exactly N frames later, waiting when they have not landed, and every reset starts
|
|
694
|
+
* from nothing measured and from seed 0. The same input then renders the same image however fast
|
|
695
|
+
* frames are submitted. Off by default: waiting costs a stall whenever the GPU falls N frames behind.
|
|
696
|
+
* @param {boolean} enabled
|
|
697
|
+
*/
|
|
698
|
+
setLockstepReadbacks( enabled ) {
|
|
699
|
+
|
|
700
|
+
enabled = !! enabled;
|
|
701
|
+
if ( enabled === this._lockstep ) return;
|
|
702
|
+
this._lockstep = enabled;
|
|
703
|
+
this._lockstepRead = null;
|
|
704
|
+
this._readbackGeneration ++;
|
|
705
|
+
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
get lockstepReadbacks() {
|
|
709
|
+
|
|
710
|
+
return this._lockstep;
|
|
711
|
+
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* While a lockstep readback is due and has not landed, render() traces nothing: await this
|
|
716
|
+
* before the next frame. Null otherwise.
|
|
717
|
+
* @returns {?Promise<void>}
|
|
718
|
+
*/
|
|
719
|
+
readbackWait() {
|
|
720
|
+
|
|
721
|
+
const pending = this._lockstepRead;
|
|
722
|
+
return pending && ! pending.apply && this.frameCount >= pending.due ? pending.read : null;
|
|
723
|
+
|
|
660
724
|
}
|
|
661
725
|
|
|
662
726
|
/**
|
|
@@ -805,6 +869,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
805
869
|
_willReadCountersThisFrame() {
|
|
806
870
|
|
|
807
871
|
if ( this.cameraChanged || this.cameraOptimizer?.isInInteractionMode() ) return false;
|
|
872
|
+
if ( this._lockstep ) return ! this._lockstepRead && this.frameCount % this._readbackEveryNFrames === 0;
|
|
808
873
|
if ( this._readbackPending || this._convergedReadbackPending ) return false;
|
|
809
874
|
return this._readbackFrameCounter + 1 >= this._readbackEveryNFrames;
|
|
810
875
|
|
|
@@ -819,6 +884,13 @@ export class PathTracer extends PathTracerStage {
|
|
|
819
884
|
// Async readback of the per-bounce snapshot every N frames; never awaited, so the early-exit uses past-frame data.
|
|
820
885
|
_maybeReadbackCounters() {
|
|
821
886
|
|
|
887
|
+
if ( this._lockstep ) {
|
|
888
|
+
|
|
889
|
+
this._issueLockstepReadback();
|
|
890
|
+
return;
|
|
891
|
+
|
|
892
|
+
}
|
|
893
|
+
|
|
822
894
|
// Never sample the survivor curve mid-motion, nor while the CameraOptimizer is holding maxBounces
|
|
823
895
|
// down to its interaction value (1): mid-motion counts belong to a pose we're leaving, and a curve
|
|
824
896
|
// measured at the interaction budget stores _lastBounceCountsBudget=1 — when the real (high) budget
|
|
@@ -864,16 +936,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
864
936
|
// Drop counts measured at a now-stale generation (a resize or camera move happened mid-flight).
|
|
865
937
|
// A surviving readback was initiated while settled (init is skipped mid-motion) and no motion
|
|
866
938
|
// happened before it resolved, so its counts match the current view — safe to size from.
|
|
867
|
-
if ( gen === this._readbackGeneration )
|
|
868
|
-
|
|
869
|
-
const all = new Uint32Array( buf.slice( 0 ) );
|
|
870
|
-
this._lastBounceCounts = all.subarray( 0, n );
|
|
871
|
-
this._lastBounceEnergy = all.subarray( n, 2 * n );
|
|
872
|
-
this._lastBounceCountsBudget = budget;
|
|
873
|
-
this._lastBounceCountsLoopBound = measuredLoopBound;
|
|
874
|
-
this._curveSizingValid = true;
|
|
875
|
-
|
|
876
|
-
}
|
|
939
|
+
if ( gen === this._readbackGeneration ) this._applyBounceCounts( buf, budget, measuredLoopBound, n );
|
|
877
940
|
|
|
878
941
|
this._readbackPending = false;
|
|
879
942
|
|
|
@@ -899,19 +962,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
899
962
|
const total = this._wfRenderWidth.value * this._wfRenderHeight.value;
|
|
900
963
|
this.renderer.getArrayBufferAsync( cAttr ).then( ( buf ) => {
|
|
901
964
|
|
|
902
|
-
if ( cgen === this._readbackGeneration && total > 0 )
|
|
903
|
-
|
|
904
|
-
const c = new Uint32Array( buf );
|
|
905
|
-
this._convergedFraction = c[ COUNTER.CONVERGED_COUNT ] / total;
|
|
906
|
-
// No geometry at all (pure environment) leaves nothing to gate on — 1 lets the
|
|
907
|
-
// whole-frame fraction decide alone.
|
|
908
|
-
const geo = c[ COUNTER.GEOMETRY_COUNT ];
|
|
909
|
-
this._geometryPixelCount = geo;
|
|
910
|
-
this._convergedGeometryFraction = geo > 0 ? c[ COUNTER.CONVERGED_GEOMETRY_COUNT ] / geo : 1;
|
|
911
|
-
// Tier-2: bounce-0 active-pixel count measured this settled frame → sizes next frame's grid.
|
|
912
|
-
this._lastActivePixelCount = c[ COUNTER.ACTIVE_PIXEL_COUNT ];
|
|
913
|
-
|
|
914
|
-
}
|
|
965
|
+
if ( cgen === this._readbackGeneration && total > 0 ) this._applyCounters( buf, total );
|
|
915
966
|
|
|
916
967
|
this._convergedReadbackPending = false;
|
|
917
968
|
|
|
@@ -927,6 +978,72 @@ export class PathTracer extends PathTracerStage {
|
|
|
927
978
|
|
|
928
979
|
}
|
|
929
980
|
|
|
981
|
+
_issueLockstepReadback() {
|
|
982
|
+
|
|
983
|
+
// One in flight, and none mid-motion: frames there do not count, so frameCount stands still.
|
|
984
|
+
if ( this._lockstepRead || this.frameCount % this._readbackEveryNFrames !== 0 ) return;
|
|
985
|
+
if ( this.cameraChanged || this.cameraOptimizer?.isInInteractionMode() ) return;
|
|
986
|
+
|
|
987
|
+
const bounceAttr = this._queueManager?.getBounceCountsAttribute();
|
|
988
|
+
const counterAttr = this._queueManager?.getCountersAttribute();
|
|
989
|
+
if ( ! bounceAttr || ! counterAttr ) return;
|
|
990
|
+
|
|
991
|
+
const gen = this._readbackGeneration;
|
|
992
|
+
const budget = this.maxBounces.value;
|
|
993
|
+
const loopBound = this._bounceLoopBound();
|
|
994
|
+
const n = this._queueManager.MAX_BOUNCE_SNAPSHOTS;
|
|
995
|
+
const total = this._wfRenderWidth.value * this._wfRenderHeight.value;
|
|
996
|
+
|
|
997
|
+
const entry = { due: this.frameCount + this._readbackEveryNFrames, read: null, apply: null };
|
|
998
|
+
entry.read = Promise.all( [
|
|
999
|
+
this.renderer.getArrayBufferAsync( bounceAttr ),
|
|
1000
|
+
this.renderer.getArrayBufferAsync( counterAttr ),
|
|
1001
|
+
] ).then( ( [ bounceBuf, counterBuf ] ) => {
|
|
1002
|
+
|
|
1003
|
+
entry.apply = () => {
|
|
1004
|
+
|
|
1005
|
+
if ( gen !== this._readbackGeneration ) return;
|
|
1006
|
+
this._applyBounceCounts( bounceBuf, budget, loopBound, n );
|
|
1007
|
+
if ( total > 0 ) this._applyCounters( counterBuf, total );
|
|
1008
|
+
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
}, ( e ) => {
|
|
1012
|
+
|
|
1013
|
+
log.warn( 'lockstep readback failed:', e );
|
|
1014
|
+
entry.apply = () => {};
|
|
1015
|
+
|
|
1016
|
+
} );
|
|
1017
|
+
|
|
1018
|
+
this._lockstepRead = entry;
|
|
1019
|
+
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
_applyBounceCounts( buf, budget, loopBound, n ) {
|
|
1023
|
+
|
|
1024
|
+
const all = new Uint32Array( buf.slice( 0 ) );
|
|
1025
|
+
this._lastBounceCounts = all.subarray( 0, n );
|
|
1026
|
+
this._lastBounceEnergy = all.subarray( n, 2 * n );
|
|
1027
|
+
this._lastBounceCountsBudget = budget;
|
|
1028
|
+
this._lastBounceCountsLoopBound = loopBound;
|
|
1029
|
+
this._curveSizingValid = true;
|
|
1030
|
+
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
_applyCounters( buf, total ) {
|
|
1034
|
+
|
|
1035
|
+
const c = new Uint32Array( buf );
|
|
1036
|
+
this._convergedFraction = c[ COUNTER.CONVERGED_COUNT ] / total;
|
|
1037
|
+
// No geometry at all (pure environment) leaves nothing to gate on — 1 lets the
|
|
1038
|
+
// whole-frame fraction decide alone.
|
|
1039
|
+
const geo = c[ COUNTER.GEOMETRY_COUNT ];
|
|
1040
|
+
this._geometryPixelCount = geo;
|
|
1041
|
+
this._convergedGeometryFraction = geo > 0 ? c[ COUNTER.CONVERGED_GEOMETRY_COUNT ] / geo : 1;
|
|
1042
|
+
// Tier-2: bounce-0 active-pixel count measured this settled frame → sizes next frame's grid.
|
|
1043
|
+
this._lastActivePixelCount = c[ COUNTER.ACTIVE_PIXEL_COUNT ];
|
|
1044
|
+
|
|
1045
|
+
}
|
|
1046
|
+
|
|
930
1047
|
// Sync wavefront's texture nodes with current env/material textures; only a changed ref triggers GPU rebind.
|
|
931
1048
|
_refreshWfTextureNodes() {
|
|
932
1049
|
|
|
@@ -1031,7 +1148,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
1031
1148
|
const maxBuffer = limits?.maxBufferSize || maxBinding;
|
|
1032
1149
|
|
|
1033
1150
|
const bByBinding = Math.floor( ( maxBinding * 0.9 ) / RAY_BYTES );
|
|
1034
|
-
const deviceMemBytes = (
|
|
1151
|
+
const deviceMemBytes = deviceMemoryGB( this.hostMemoryGB ).gb * 1024 * 1024 * 1024;
|
|
1035
1152
|
const poolVramBudget = Math.min( deviceMemBytes * 0.25, maxBuffer * 4 );
|
|
1036
1153
|
const bByVram = Math.floor( poolVramBudget / bytesPerPath );
|
|
1037
1154
|
|
|
@@ -1596,7 +1713,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
1596
1713
|
cameraViewMatrix: this.cameraViewMatrix,
|
|
1597
1714
|
fireflyThreshold: this.fireflyThreshold,
|
|
1598
1715
|
shadowTerminatorOffset: this.shadowTerminatorOffset,
|
|
1599
|
-
// RNG axis only
|
|
1716
|
+
// RNG axis only.
|
|
1600
1717
|
frame: this.seedFrame,
|
|
1601
1718
|
accumFrame: this.frame,
|
|
1602
1719
|
resolution: this.resolution,
|