rayzee 9.3.3 → 9.4.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 +8 -3
- package/dist/rayzee.es.js +3365 -3226
- package/dist/rayzee.umd.js +93 -93
- package/package.json +1 -1
- package/src/EngineIssues.js +1 -0
- package/src/PathTracerApp.js +25 -13
- package/src/Processor/TextureCreator.js +170 -6
- package/src/Processor/Workers/TexturesWorker.js +98 -12
- package/src/Stages/PathTracer.js +13 -2
- package/src/Stages/PathTracerStage.js +27 -42
- package/src/Storage/StorageManager.js +43 -5
- package/src/managers/CameraManager.js +55 -34
- package/src/managers/GoboManager.js +9 -0
- package/src/managers/UniformManager.js +30 -7
package/package.json
CHANGED
package/src/EngineIssues.js
CHANGED
|
@@ -32,6 +32,7 @@ export const ISSUE_CODES = Object.freeze( {
|
|
|
32
32
|
STORAGE_CACHE_MISMATCH: 'storage.cache_mismatch',
|
|
33
33
|
DENOISER_UNAVAILABLE: 'denoiser.unavailable',
|
|
34
34
|
OUTPUT_SOURCE_FALLBACK: 'output.source_fallback',
|
|
35
|
+
OUTPUT_TONEMAP_FALLBACK: 'output.tonemap_fallback',
|
|
35
36
|
LIGHT_PLACEHOLDER_SKIPPED: 'light.placeholder_skipped',
|
|
36
37
|
} );
|
|
37
38
|
|
package/src/PathTracerApp.js
CHANGED
|
@@ -24,7 +24,7 @@ import { TextureReadback } from './Processor/TextureReadback.js';
|
|
|
24
24
|
import { createLogger, fmt } from './utils/Logger.js';
|
|
25
25
|
import { InteractionManager } from './managers/InteractionManager.js';
|
|
26
26
|
import { EngineEvents, LEGACY_EVENT_NAMES } from './EngineEvents.js';
|
|
27
|
-
import { IssueLog, ISSUE_CODES } from './EngineIssues.js';
|
|
27
|
+
import { IssueLog, ISSUE_CODES, EngineIssueError } from './EngineIssues.js';
|
|
28
28
|
import { getAssetConfig, isAssetConfigured } from './AssetConfig.js';
|
|
29
29
|
import { StorageManager } from './Storage/StorageManager.js';
|
|
30
30
|
import { acquireSharedStorage } from './Storage/openStorage.js';
|
|
@@ -2903,7 +2903,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2903
2903
|
headless: this._headless,
|
|
2904
2904
|
strict: this._issues.strict,
|
|
2905
2905
|
deterministic: this.isDeterministic,
|
|
2906
|
-
lockstep: !! stage?.
|
|
2906
|
+
lockstep: !! stage?.accumulationLockstep,
|
|
2907
2907
|
},
|
|
2908
2908
|
render: stage ? {
|
|
2909
2909
|
width: stage.width,
|
|
@@ -2998,8 +2998,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2998
2998
|
* viewport is showing — denoised when a denoiser has run, without bloom — instead of the raw
|
|
2999
2999
|
* accumulation
|
|
3000
3000
|
* @returns {Promise<{data: Float32Array|Uint8ClampedArray, width: number, height: number, colorSpace: string,
|
|
3001
|
-
* source: string}>} `source` is what was read: `'accumulation'`, or the
|
|
3002
|
-
* it (`'oidn'`, `'asvgf'`, `'nrd'`, `'edgeFiltering'`, `'bilateralFiltering'`)
|
|
3001
|
+
* source: string, toneMappedOn?: 'gpu'|'cpu'}>} `source` is what was read: `'accumulation'`, or the
|
|
3002
|
+
* denoiser that published it (`'oidn'`, `'asvgf'`, `'nrd'`, `'edgeFiltering'`, `'bilateralFiltering'`).
|
|
3003
|
+
* `toneMappedOn` is set for `'srgb'` only.
|
|
3003
3004
|
*/
|
|
3004
3005
|
async renderToBuffer( { colorSpace = 'srgb', preserveAlpha = false, source = 'accumulation' } = {} ) {
|
|
3005
3006
|
|
|
@@ -3029,11 +3030,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3029
3030
|
preserveAlpha,
|
|
3030
3031
|
};
|
|
3031
3032
|
|
|
3032
|
-
const
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
return { data, width, height, colorSpace, source: shown?.source ?? 'accumulation' };
|
|
3033
|
+
const gpu = await this._toneMapOnGPU( shown?.texture ?? null, target, width, height, tone );
|
|
3034
|
+
const data = gpu ?? toneMapToRGBA8( shown
|
|
3035
|
+
? await this._readTexture( shown.texture, width, height )
|
|
3036
|
+
: await this.renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, 0 ), tone );
|
|
3037
|
+
return { data, width, height, colorSpace, source: shown?.source ?? 'accumulation', toneMappedOn: gpu ? 'gpu' : 'cpu' };
|
|
3037
3038
|
|
|
3038
3039
|
}
|
|
3039
3040
|
|
|
@@ -3065,13 +3066,13 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3065
3066
|
async _toneMapOnGPU( shown, accumulation, width, height, tone ) {
|
|
3066
3067
|
|
|
3067
3068
|
const backend = this.renderer?.backend;
|
|
3068
|
-
if ( ! backend?.device ) return
|
|
3069
|
+
if ( ! backend?.device ) return this._toneMapFallback( 'the renderer has no WebGPU device' );
|
|
3069
3070
|
|
|
3070
3071
|
try {
|
|
3071
3072
|
|
|
3072
3073
|
const texture = shown ? this._readbackPass().draw( shown, width, height ).texture : accumulation.textures[ 0 ];
|
|
3073
3074
|
const gpuTexture = backend.get( texture ).texture;
|
|
3074
|
-
if ( ! gpuTexture ) return
|
|
3075
|
+
if ( ! gpuTexture ) return this._toneMapFallback( 'the picture has no GPU texture yet' );
|
|
3075
3076
|
|
|
3076
3077
|
this._readbackToneMapper ??= new PackedToneMapper( backend.device, 'rayzee:readback-tonemap', { input: 'texture' } );
|
|
3077
3078
|
this._readbackToneMapper.ensureSize( width, height );
|
|
@@ -3079,8 +3080,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3079
3080
|
|
|
3080
3081
|
} catch ( error ) {
|
|
3081
3082
|
|
|
3082
|
-
|
|
3083
|
-
return
|
|
3083
|
+
if ( error instanceof EngineIssueError ) throw error;
|
|
3084
|
+
return this._toneMapFallback( 'the GPU pass threw', error );
|
|
3084
3085
|
|
|
3085
3086
|
} finally {
|
|
3086
3087
|
|
|
@@ -3091,6 +3092,17 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3091
3092
|
|
|
3092
3093
|
}
|
|
3093
3094
|
|
|
3095
|
+
/** A warning, so strict keeps going: the CPU bytes match within a level, only slower. @private */
|
|
3096
|
+
_toneMapFallback( reason, error = null ) {
|
|
3097
|
+
|
|
3098
|
+
const cause = error ? String( error?.message ?? error ) : null;
|
|
3099
|
+
const message = `renderToBuffer: tone mapped on the CPU — ${reason}${cause ? `: ${cause}` : ''}`;
|
|
3100
|
+
log.warn( message );
|
|
3101
|
+
this._issues.warn( ISSUE_CODES.OUTPUT_TONEMAP_FALLBACK, message, { reason, cause } );
|
|
3102
|
+
return null;
|
|
3103
|
+
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3094
3106
|
/**
|
|
3095
3107
|
* Enables WebGPU timestamp queries so {@link PathTracerApp#getGPUTimings} reports real
|
|
3096
3108
|
* GPU time. Off by default because the queries themselves cost time. No-ops when the
|
|
@@ -15,6 +15,13 @@ const log = createLogger( 'textures' );
|
|
|
15
15
|
// Below this a pack is cheaper than starting a worker for it.
|
|
16
16
|
const WORKER_PACK_BYTES = 8 * 2 ** 20;
|
|
17
17
|
|
|
18
|
+
// Main-thread work allowed between yields while streaming layers to a worker.
|
|
19
|
+
const STREAM_YIELD_MS = 16;
|
|
20
|
+
|
|
21
|
+
const SHARED_PACK_WORKERS = 4;
|
|
22
|
+
// Below about one 8K source a bucket packs fast enough alone, and extra workers only compete with the BVH build.
|
|
23
|
+
const SHARED_PACK_MIN_PIXELS = 8192 * 8192;
|
|
24
|
+
|
|
18
25
|
// Buckets pack concurrently; this keeps their workers within the cores, beside the BVH pool's.
|
|
19
26
|
const packSlots = { busy: 0, waiting: [] };
|
|
20
27
|
|
|
@@ -835,6 +842,9 @@ export class TextureCreator {
|
|
|
835
842
|
case 'worker-direct':
|
|
836
843
|
result = await this.processWithWorkerDirect( normalized );
|
|
837
844
|
break;
|
|
845
|
+
case 'worker-streaming':
|
|
846
|
+
result = await this.processInWorkerStreaming( normalized );
|
|
847
|
+
break;
|
|
838
848
|
case 'main-batch':
|
|
839
849
|
result = await this.processOnMainThreadBatch( normalized, strategy.batchSize );
|
|
840
850
|
break;
|
|
@@ -892,10 +902,7 @@ export class TextureCreator {
|
|
|
892
902
|
|
|
893
903
|
if ( this.capabilities.workers && estimatedMemory > MEMORY_CONSTANTS.MAX_TEXTURE_MEMORY ) {
|
|
894
904
|
|
|
895
|
-
|
|
896
|
-
// full array correctly). The former 'worker-chunked' path combined its chunks
|
|
897
|
-
// by returning only the first, silently dropping the rest — removed.
|
|
898
|
-
return { method: 'main-streaming' };
|
|
905
|
+
return { method: 'worker-streaming' };
|
|
899
906
|
|
|
900
907
|
} else if ( this.capabilities.workers && totalPixels > 2097152 ) {
|
|
901
908
|
|
|
@@ -931,14 +938,24 @@ export class TextureCreator {
|
|
|
931
938
|
}
|
|
932
939
|
|
|
933
940
|
this.activeWorkers ++;
|
|
934
|
-
|
|
935
|
-
|
|
941
|
+
let slots = 1;
|
|
942
|
+
let worker = null;
|
|
936
943
|
|
|
937
944
|
try {
|
|
938
945
|
|
|
939
946
|
// Prepare textures for worker with direct transfer
|
|
940
947
|
const texturesData = await this.prepareTexturesForWorkerDirect( textures );
|
|
941
948
|
|
|
949
|
+
const extra = this._sharedPackSlots( texturesData );
|
|
950
|
+
if ( extra > 0 ) {
|
|
951
|
+
|
|
952
|
+
slots += extra;
|
|
953
|
+
this.activeWorkers += extra;
|
|
954
|
+
return this.createDataArrayTextureFromResult( await this._packAcrossWorkers( texturesData, slots ) );
|
|
955
|
+
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
worker = new TexturesWorker();
|
|
942
959
|
const result = await new Promise( ( resolve, reject ) => {
|
|
943
960
|
|
|
944
961
|
worker.onmessage = ( e ) => {
|
|
@@ -983,6 +1000,153 @@ export class TextureCreator {
|
|
|
983
1000
|
|
|
984
1001
|
return this.createDataArrayTextureFromResult( result );
|
|
985
1002
|
|
|
1003
|
+
} finally {
|
|
1004
|
+
|
|
1005
|
+
worker?.terminate();
|
|
1006
|
+
this.activeWorkers -= slots;
|
|
1007
|
+
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/** Extra worker slots a bucket may take now, beyond its own. */
|
|
1013
|
+
_sharedPackSlots( texturesData ) {
|
|
1014
|
+
|
|
1015
|
+
if ( typeof SharedArrayBuffer === 'undefined' || ! globalThis.crossOriginIsolated ) return 0;
|
|
1016
|
+
if ( texturesData.reduce( ( sum, t ) => sum + t.width * t.height, 0 ) < SHARED_PACK_MIN_PIXELS ) return 0;
|
|
1017
|
+
const free = this.maxConcurrentWorkers - this.activeWorkers;
|
|
1018
|
+
return Math.max( 0, Math.min( texturesData.length, SHARED_PACK_WORKERS, free + 1 ) - 1 );
|
|
1019
|
+
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* The worker's direct path split across `count` workers, each drawing its layers straight into
|
|
1024
|
+
* one shared array. Large sources dominate (their downscale), so they are dealt out first.
|
|
1025
|
+
*/
|
|
1026
|
+
async _packAcrossWorkers( texturesData, count ) {
|
|
1027
|
+
|
|
1028
|
+
const { maxWidth: width, maxHeight: height } = this.calculateOptimalDimensions( texturesData.map( image => ( { image } ) ) );
|
|
1029
|
+
const depth = texturesData.length;
|
|
1030
|
+
const shared = new SharedArrayBuffer( width * height * depth * 4 );
|
|
1031
|
+
|
|
1032
|
+
const parts = Array.from( { length: count }, () => ( { cost: 0, layers: [], transfer: [] } ) );
|
|
1033
|
+
const order = texturesData.map( ( _, index ) => index );
|
|
1034
|
+
const cost = index => texturesData[ index ].width * texturesData[ index ].height;
|
|
1035
|
+
order.sort( ( a, b ) => cost( b ) - cost( a ) );
|
|
1036
|
+
|
|
1037
|
+
for ( const index of order ) {
|
|
1038
|
+
|
|
1039
|
+
const part = parts.reduce( ( least, p ) => ( p.cost < least.cost ? p : least ) );
|
|
1040
|
+
const texture = texturesData[ index ];
|
|
1041
|
+
part.layers.push( { index, texture } );
|
|
1042
|
+
part.cost += cost( index );
|
|
1043
|
+
if ( texture.data instanceof ArrayBuffer ) part.transfer.push( texture.data );
|
|
1044
|
+
else if ( texture.bitmap ) part.transfer.push( texture.bitmap );
|
|
1045
|
+
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const workers = [];
|
|
1049
|
+
|
|
1050
|
+
try {
|
|
1051
|
+
|
|
1052
|
+
await Promise.all( parts.filter( part => part.layers.length ).map( part => new Promise( ( resolve, reject ) => {
|
|
1053
|
+
|
|
1054
|
+
const worker = createWorker( TexturesWorker );
|
|
1055
|
+
workers.push( worker );
|
|
1056
|
+
worker.onmessage = ( { data } ) => ( data.error ? reject( new Error( data.error ) ) : resolve() );
|
|
1057
|
+
worker.onerror = ( event ) => reject( new Error( event.message || 'texture worker failed' ) );
|
|
1058
|
+
worker.postMessage( { shared, width, height, layers: part.layers, maxTextureSize: this.maxTextureSize }, part.transfer );
|
|
1059
|
+
|
|
1060
|
+
} ) ) );
|
|
1061
|
+
|
|
1062
|
+
} finally {
|
|
1063
|
+
|
|
1064
|
+
for ( const worker of workers ) worker.terminate();
|
|
1065
|
+
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
return { data: shared, width, height, depth };
|
|
1069
|
+
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* `processOnMainThreadStreaming` with the draw and read moved to a worker: each layer is
|
|
1074
|
+
* resized here exactly as there, so the array is byte-for-byte the same.
|
|
1075
|
+
*/
|
|
1076
|
+
async processInWorkerStreaming( textures ) {
|
|
1077
|
+
|
|
1078
|
+
const validTextures = textures.filter( tex => tex?.image );
|
|
1079
|
+
if ( validTextures.length === 0 ) return this.createFallbackTexture();
|
|
1080
|
+
|
|
1081
|
+
const { maxWidth, maxHeight } = this.calculateOptimalDimensions( validTextures );
|
|
1082
|
+
const depth = validTextures.length;
|
|
1083
|
+
|
|
1084
|
+
while ( this.activeWorkers >= this.maxConcurrentWorkers ) {
|
|
1085
|
+
|
|
1086
|
+
await new Promise( resolve => setTimeout( resolve, 10 ) );
|
|
1087
|
+
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
this.activeWorkers ++;
|
|
1091
|
+
const worker = createWorker( TexturesWorker );
|
|
1092
|
+
|
|
1093
|
+
try {
|
|
1094
|
+
|
|
1095
|
+
let acked = 0;
|
|
1096
|
+
let wake = null;
|
|
1097
|
+
let settle;
|
|
1098
|
+
const finished = new Promise( ( resolve, reject ) => settle = { resolve, reject } );
|
|
1099
|
+
finished.catch( () => {} );
|
|
1100
|
+
|
|
1101
|
+
worker.onmessage = ( { data } ) => {
|
|
1102
|
+
|
|
1103
|
+
if ( data.error ) settle.reject( new Error( data.error ) );
|
|
1104
|
+
else if ( data.stream === 'ack' ) acked ++;
|
|
1105
|
+
else settle.resolve( data );
|
|
1106
|
+
wake?.();
|
|
1107
|
+
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
worker.onerror = ( event ) => {
|
|
1111
|
+
|
|
1112
|
+
settle.reject( new Error( event.message || 'texture worker failed' ) );
|
|
1113
|
+
wake?.();
|
|
1114
|
+
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
worker.postMessage( { stream: 'begin', width: maxWidth, height: maxHeight, depth } );
|
|
1118
|
+
let lastYield = performance.now();
|
|
1119
|
+
|
|
1120
|
+
for ( let i = 0; i < depth; i ++ ) {
|
|
1121
|
+
|
|
1122
|
+
while ( i - acked >= MEMORY_CONSTANTS.STREAM_BATCH_SIZE ) {
|
|
1123
|
+
|
|
1124
|
+
await Promise.race( [ new Promise( resolve => wake = resolve ), finished ] );
|
|
1125
|
+
lastYield = performance.now();
|
|
1126
|
+
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const texture = validTextures[ i ];
|
|
1130
|
+
const bitmap = await createImageBitmap( texture.image, {
|
|
1131
|
+
resizeWidth: maxWidth,
|
|
1132
|
+
resizeHeight: maxHeight,
|
|
1133
|
+
resizeQuality: 'high',
|
|
1134
|
+
imageOrientation: texture.flipY !== false ? 'flipY' : 'none',
|
|
1135
|
+
} );
|
|
1136
|
+
worker.postMessage( { stream: 'layer', index: i, bitmap }, [ bitmap ] );
|
|
1137
|
+
|
|
1138
|
+
if ( performance.now() - lastYield > STREAM_YIELD_MS ) {
|
|
1139
|
+
|
|
1140
|
+
await new Promise( resolve => setTimeout( resolve, 0 ) );
|
|
1141
|
+
lastYield = performance.now();
|
|
1142
|
+
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
worker.postMessage( { stream: 'end' } );
|
|
1148
|
+
return this.createDataArrayTextureFromResult( await finished );
|
|
1149
|
+
|
|
986
1150
|
} finally {
|
|
987
1151
|
|
|
988
1152
|
worker.terminate();
|
|
@@ -2,15 +2,112 @@ let canvas, ctx;
|
|
|
2
2
|
|
|
3
3
|
// Memory limits and chunking configuration
|
|
4
4
|
const MEMORY_LIMITS = {
|
|
5
|
-
MAX_BYTES_PER_TEXTURE: 256 * 1024 * 1024, // 256MB per texture array
|
|
6
5
|
MAX_TEXTURE_DIMENSION: 8192, // Hardware ceiling (WebGPU maxTextureDimension2D guaranteed min); the per-scene maxTextureSize setting is the actual knob
|
|
7
6
|
CHUNK_SIZE: 8, // Optimized: Process textures in chunks of 8 for better memory locality
|
|
8
7
|
ADAPTIVE_CHUNK_SIZE: true, // Enable adaptive chunk sizing based on texture dimensions
|
|
9
8
|
MEMORY_SAFETY_FACTOR: 0.8 // Use only 80% of estimated available memory
|
|
10
9
|
};
|
|
11
10
|
|
|
11
|
+
let stream = null;
|
|
12
|
+
|
|
13
|
+
function handleStream( msg ) {
|
|
14
|
+
|
|
15
|
+
if ( msg.stream === 'begin' ) {
|
|
16
|
+
|
|
17
|
+
const { width, height, depth } = msg;
|
|
18
|
+
const target = new OffscreenCanvas( width, height );
|
|
19
|
+
const context = target.getContext( '2d', { willReadFrequently: true, alpha: true, desynchronized: true } );
|
|
20
|
+
stream = { width, height, depth, context, data: new Uint8Array( width * height * depth * 4 ) };
|
|
21
|
+
return;
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { width, height, depth, context, data } = stream;
|
|
26
|
+
|
|
27
|
+
if ( msg.stream === 'layer' ) {
|
|
28
|
+
|
|
29
|
+
context.clearRect( 0, 0, width, height );
|
|
30
|
+
context.drawImage( msg.bitmap, 0, 0 );
|
|
31
|
+
msg.bitmap.close();
|
|
32
|
+
data.set( context.getImageData( 0, 0, width, height ).data, width * height * 4 * msg.index );
|
|
33
|
+
self.postMessage( { stream: 'ack', index: msg.index } );
|
|
34
|
+
return;
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
stream = null;
|
|
39
|
+
self.postMessage( { data: data.buffer, width, height, depth }, [ data.buffer ] );
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function processIntoShared( { shared, width, height, layers } ) {
|
|
44
|
+
|
|
45
|
+
if ( canvas.width !== width || canvas.height !== height ) {
|
|
46
|
+
|
|
47
|
+
canvas.width = width;
|
|
48
|
+
canvas.height = height;
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
ctx.imageSmoothingEnabled = true;
|
|
53
|
+
ctx.imageSmoothingQuality = 'high';
|
|
54
|
+
|
|
55
|
+
const data = new Uint8Array( shared );
|
|
56
|
+
const bytes = width * height * 4;
|
|
57
|
+
|
|
58
|
+
for ( const { index, texture } of layers ) {
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
|
|
62
|
+
await processSingleTextureOptimized( texture, data, index * bytes, width, height );
|
|
63
|
+
|
|
64
|
+
} catch ( error ) {
|
|
65
|
+
|
|
66
|
+
console.warn( `Failed to process texture ${index}:`, error );
|
|
67
|
+
data.fill( 0, index * bytes, ( index + 1 ) * bytes );
|
|
68
|
+
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
}
|
|
74
|
+
|
|
12
75
|
self.onmessage = async function ( e ) {
|
|
13
76
|
|
|
77
|
+
if ( e.data.shared ) {
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
|
|
81
|
+
if ( ! canvas ) initializeWorker( e.data.maxTextureSize );
|
|
82
|
+
await processIntoShared( e.data );
|
|
83
|
+
self.postMessage( { done: true } );
|
|
84
|
+
|
|
85
|
+
} catch ( error ) {
|
|
86
|
+
|
|
87
|
+
self.postMessage( { error: error.message } );
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return;
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ( e.data.stream ) {
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
|
|
99
|
+
handleStream( e.data );
|
|
100
|
+
|
|
101
|
+
} catch ( error ) {
|
|
102
|
+
|
|
103
|
+
self.postMessage( { error: error.message } );
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return;
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
14
111
|
const { textures, maxTextureSize, method = 'direct-transfer' } = e.data;
|
|
15
112
|
|
|
16
113
|
try {
|
|
@@ -51,17 +148,6 @@ function initializeWorker( maxTextureSize ) {
|
|
|
51
148
|
|
|
52
149
|
async function processTextures( textures, maxTextureSize, method ) {
|
|
53
150
|
|
|
54
|
-
// Check if we need to use chunked processing
|
|
55
|
-
const dimensions = calculateOptimalDimensions( textures, maxTextureSize );
|
|
56
|
-
const estimatedBytes = dimensions.maxWidth * dimensions.maxHeight * textures.length * 4;
|
|
57
|
-
|
|
58
|
-
if ( estimatedBytes > MEMORY_LIMITS.MAX_BYTES_PER_TEXTURE ) {
|
|
59
|
-
|
|
60
|
-
console.log( `Large texture array detected (${( estimatedBytes / 1024 / 1024 ).toFixed( 2 )}MB), using chunked processing` );
|
|
61
|
-
return await processTexturesInChunks( textures, maxTextureSize, method );
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
151
|
switch ( method ) {
|
|
66
152
|
|
|
67
153
|
case 'direct-transfer':
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -118,6 +118,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
118
118
|
// so when one lands cannot change the image. { due, read, apply } while one is in flight.
|
|
119
119
|
this._lockstep = false;
|
|
120
120
|
this._lockstepRead = null;
|
|
121
|
+
this._accumulationLockstep = false;
|
|
121
122
|
|
|
122
123
|
// Tier-2: last settled active-pixel count (maxRays − frozen), sizes next frame's bounce-0 grid.
|
|
123
124
|
// 0 until a settled readback lands (and on reset/camera-move/resize) → grid stays full-size until then.
|
|
@@ -240,8 +241,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
240
241
|
// Kernels not built yet (first frame / mid-resize) — skip until ready.
|
|
241
242
|
if ( ! this.isReady || ! this._wavefrontReady ) return;
|
|
242
243
|
|
|
243
|
-
// The packed light buffer was grow-reallocated at runtime (emissive set grew) —
|
|
244
|
-
// the compiled kernels still bind the old
|
|
244
|
+
// The packed light buffer was grow-reallocated at runtime (emissive set grew) or a light list grew —
|
|
245
|
+
// the compiled kernels still bind the old one, so rebuild before rendering.
|
|
245
246
|
if ( this._lightBufferRealloc ) {
|
|
246
247
|
|
|
247
248
|
if ( this._kernelManager ) this._kernelManager.dispose();
|
|
@@ -320,6 +321,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
320
321
|
this.frame.value = frameValue;
|
|
321
322
|
this.seedFrame.value = this._seedTick ++;
|
|
322
323
|
this.tracedFrames ++;
|
|
324
|
+
if ( ! this._lockstep ) this._accumulationLockstep = false;
|
|
323
325
|
|
|
324
326
|
this._setWfDispatch();
|
|
325
327
|
|
|
@@ -661,6 +663,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
661
663
|
this._convergedFraction = state.convergedFraction ?? 0;
|
|
662
664
|
this._convergedGeometryFraction = state.convergedGeometryFraction ?? 0;
|
|
663
665
|
this.isComplete = false;
|
|
666
|
+
// The survivor curve is not in a checkpoint, so a continuation is not the lockstep render.
|
|
667
|
+
this._accumulationLockstep = false;
|
|
664
668
|
|
|
665
669
|
}
|
|
666
670
|
|
|
@@ -711,6 +715,13 @@ export class PathTracer extends PathTracerStage {
|
|
|
711
715
|
|
|
712
716
|
}
|
|
713
717
|
|
|
718
|
+
/** Whether the current image was traced in lockstep from a reset made in it; with no sample yet, the setting. */
|
|
719
|
+
get accumulationLockstep() {
|
|
720
|
+
|
|
721
|
+
return this.frameCount > 0 ? this._accumulationLockstep : this._lockstep;
|
|
722
|
+
|
|
723
|
+
}
|
|
724
|
+
|
|
714
725
|
/**
|
|
715
726
|
* While a lockstep readback is due and has not landed, render() traces nothing: await this
|
|
716
727
|
* before the next frame. Null otherwise.
|
|
@@ -10,7 +10,7 @@ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
|
10
10
|
import { CameraOptimizer } from '../Processor/CameraOptimizer.js';
|
|
11
11
|
import { createPerformanceMonitor, calculateAccumulationAlpha, updateCompletionThreshold } from '../Processor/utils.js';
|
|
12
12
|
import { StorageTexturePool } from '../Processor/StorageTexturePool.js';
|
|
13
|
-
import { UniformManager } from '../managers/UniformManager.js';
|
|
13
|
+
import { UniformManager, LIGHT_FLOATS, lightListCapacity } from '../managers/UniformManager.js';
|
|
14
14
|
import { MaterialDataManager } from '../managers/MaterialDataManager.js';
|
|
15
15
|
import { EnvironmentManager } from '../managers/EnvironmentManager.js';
|
|
16
16
|
import { ShaderBuilder } from '../Processor/ShaderBuilder.js';
|
|
@@ -218,8 +218,8 @@ export class PathTracerStage extends RenderStage {
|
|
|
218
218
|
// Initialized with dummy data so TSL compilation never sees null.
|
|
219
219
|
this.lightStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( 16 ), 4 );
|
|
220
220
|
this.lightStorageNode = storage( this.lightStorageAttr, 'vec4', 1 ).toReadOnly();
|
|
221
|
-
// Set when _rebuildLightBuffer had to grow-reallocate the attribute
|
|
222
|
-
// kernels still bind the old one and must be rebuilt to see the new data.
|
|
221
|
+
// Set when _rebuildLightBuffer had to grow-reallocate the attribute, or a light list grew
|
|
222
|
+
// (_writeLightList): compiled kernels still bind the old one and must be rebuilt to see the new data.
|
|
223
223
|
this._lightBufferRealloc = false;
|
|
224
224
|
|
|
225
225
|
// Cached CPU-side data — rebuilt into the packed buffer whenever any source changes.
|
|
@@ -492,53 +492,36 @@ export class PathTracerStage extends RenderStage {
|
|
|
492
492
|
*/
|
|
493
493
|
_updateLightBufferNodes() {
|
|
494
494
|
|
|
495
|
-
|
|
496
|
-
|
|
495
|
+
this._writeLightList( 'directional', this.directionalLightsData, this.numDirectionalLights );
|
|
496
|
+
this._writeLightList( 'area', this.areaLightsData, this.numAreaLights );
|
|
497
|
+
this._writeLightList( 'point', this.pointLightsData, this.numPointLights );
|
|
498
|
+
this._writeLightList( 'spot', this.spotLightsData, this.numSpotLights );
|
|
497
499
|
|
|
498
|
-
|
|
499
|
-
this.numDirectionalLights.value = Math.floor( this.directionalLightsData.length / 12 );
|
|
500
|
-
|
|
501
|
-
} else {
|
|
502
|
-
|
|
503
|
-
this.numDirectionalLights.value = 0;
|
|
504
|
-
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Area lights (16 floats per light — 13 base + normalize/spread/shape)
|
|
508
|
-
if ( this.areaLightsData && this.areaLightsData.length > 0 ) {
|
|
509
|
-
|
|
510
|
-
this.areaLightsBufferNode.array = Array.from( this.areaLightsData );
|
|
511
|
-
this.numAreaLights.value = Math.floor( this.areaLightsData.length / 16 );
|
|
512
|
-
|
|
513
|
-
} else {
|
|
514
|
-
|
|
515
|
-
this.numAreaLights.value = 0;
|
|
516
|
-
|
|
517
|
-
}
|
|
500
|
+
}
|
|
518
501
|
|
|
519
|
-
|
|
520
|
-
|
|
502
|
+
/**
|
|
503
|
+
* In place, because the shader bakes the list's length: sized per scene it compiled a program per
|
|
504
|
+
* light count, and three.js uploads only what fits the length it was built with, so a light added
|
|
505
|
+
* after a build was dropped. A list grows only past its capacity, and that rebuilds the kernels.
|
|
506
|
+
* @private
|
|
507
|
+
*/
|
|
508
|
+
_writeLightList( type, data, count ) {
|
|
521
509
|
|
|
522
|
-
|
|
523
|
-
|
|
510
|
+
const floats = LIGHT_FLOATS[ type ];
|
|
511
|
+
const node = this[ `${type}LightsBufferNode` ];
|
|
512
|
+
const lights = data ? Math.floor( data.length / floats ) : 0;
|
|
513
|
+
const used = lights * floats;
|
|
524
514
|
|
|
525
|
-
|
|
515
|
+
if ( used > node.array.length ) {
|
|
526
516
|
|
|
527
|
-
|
|
517
|
+
node.array = new Float32Array( lightListCapacity( type, lights ) * floats );
|
|
518
|
+
this._lightBufferRealloc = true;
|
|
528
519
|
|
|
529
520
|
}
|
|
530
521
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
this.spotLightsBufferNode.array = Array.from( this.spotLightsData );
|
|
535
|
-
this.numSpotLights.value = Math.floor( this.spotLightsData.length / 20 );
|
|
536
|
-
|
|
537
|
-
} else {
|
|
538
|
-
|
|
539
|
-
this.numSpotLights.value = 0;
|
|
540
|
-
|
|
541
|
-
}
|
|
522
|
+
if ( used > 0 ) node.array.set( data.subarray( 0, used ) );
|
|
523
|
+
node.array.fill( 0, used );
|
|
524
|
+
count.value = lights;
|
|
542
525
|
|
|
543
526
|
}
|
|
544
527
|
|
|
@@ -565,6 +548,7 @@ export class PathTracerStage extends RenderStage {
|
|
|
565
548
|
|
|
566
549
|
// Only deterministic mode and lockstep readbacks rewind the seed axis; see _pinSeedToFrame.
|
|
567
550
|
if ( this._pinSeedToFrame || this._lockstep ) this._seedTick = 0;
|
|
551
|
+
this._accumulationLockstep = !! this._lockstep;
|
|
568
552
|
|
|
569
553
|
}
|
|
570
554
|
|
|
@@ -1288,6 +1272,7 @@ export class PathTracerStage extends RenderStage {
|
|
|
1288
1272
|
|
|
1289
1273
|
this.createStorageTextures( width, height );
|
|
1290
1274
|
this.frameCount = 0;
|
|
1275
|
+
this._accumulationLockstep = false;
|
|
1291
1276
|
|
|
1292
1277
|
}
|
|
1293
1278
|
|