rayzee 7.7.0 → 7.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{BVHSubtreeWorker-D9GImjGj.js → BVHSubtreeWorker-sNzvxn66.js} +2 -2
  3. package/dist/assets/{BVHSubtreeWorker-D9GImjGj.js.map → BVHSubtreeWorker-sNzvxn66.js.map} +1 -1
  4. package/dist/assets/{BVHWorker-CNJ0UBQz.js → BVHWorker-CiVdFrwe.js} +2 -2
  5. package/dist/assets/{BVHWorker-CNJ0UBQz.js.map → BVHWorker-CiVdFrwe.js.map} +1 -1
  6. package/dist/rayzee.es.js +2057 -2035
  7. package/dist/rayzee.es.js.map +1 -1
  8. package/dist/rayzee.umd.js +55 -55
  9. package/dist/rayzee.umd.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/EngineDefaults.js +45 -1
  12. package/src/EngineEvents.js +1 -0
  13. package/src/Passes/OIDNDenoiser.js +25 -1
  14. package/src/PathTracerApp.js +36 -0
  15. package/src/Processor/AssetLoader.js +3 -0
  16. package/src/Processor/GeometryExtractor.js +35 -9
  17. package/src/Processor/PackedRayBuffer.js +15 -1
  18. package/src/Processor/QueueManager.js +13 -1
  19. package/src/Processor/SceneProcessor.js +176 -113
  20. package/src/Processor/ShaderBuilder.js +6 -24
  21. package/src/Processor/TextureCreator.js +16 -32
  22. package/src/Processor/Workers/BVHWorker.js +6 -1
  23. package/src/Stages/EdgeFilter.js +4 -3
  24. package/src/Stages/MotionVector.js +4 -4
  25. package/src/Stages/NormalDepth.js +20 -30
  26. package/src/Stages/PathTracer.js +20 -40
  27. package/src/TSL/DebugKernel.js +0 -2
  28. package/src/TSL/Debugger.js +1 -8
  29. package/src/TSL/Displacement.js +10 -10
  30. package/src/TSL/GenerateKernel.js +5 -1
  31. package/src/TSL/LightsDirect.js +8 -9
  32. package/src/TSL/LightsIndirect.js +15 -9
  33. package/src/TSL/LightsSampling.js +9 -19
  34. package/src/TSL/Random.js +11 -1
  35. package/src/TSL/ShadeKernel.js +1 -6
  36. package/src/TSL/TextureSampling.js +155 -34
  37. package/src/managers/EnvironmentManager.js +11 -0
  38. package/src/managers/MaterialDataManager.js +71 -39
  39. package/src/managers/RenderTargetManager.js +0 -522
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.7.0",
3
+ "version": "7.9.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -492,9 +492,53 @@ export const TEXTURE_CONSTANTS = {
492
492
  // guaranteed minimum). The configurable maxTextureSize setting is clamped to this.
493
493
  MAX_TEXTURE_SIZE: 8192,
494
494
  // Default cap applied when no maxTextureSize is supplied (engine standalone use).
495
- DEFAULT_MAX_TEXTURE_SIZE: 4096
495
+ DEFAULT_MAX_TEXTURE_SIZE: 4096,
496
+ // Max layers (textures) per bucket array. Also the packing stride for (bucket, layer).
497
+ MAX_TEXTURES_LIMIT: 128,
498
+ // Size buckets per colorSpace pool. Material maps are grouped into this many
499
+ // longest-edge size classes so a small map no longer pays a large neighbour's
500
+ // footprint. 4 → ~8 bound material arrays (4 sRGB + 4 linear).
501
+ MATERIAL_BUCKET_COUNT: 4,
502
+ // Packing stride: a map's stored index encodes bucketId * BUCKET_LAYER_STRIDE + layer.
503
+ // Also the per-bucket layer cap. Kept at the WebGPU portable maxTextureArrayLayers floor (256)
504
+ // so a consolidated bucket (which merges several map types of one size) stays portable.
505
+ BUCKET_LAYER_STRIDE: 256,
496
506
  };
497
507
 
508
+ // Longest-edge size ladder for a given cap, ascending.
509
+ // cap=4096, count=4 → [512, 1024, 2048, 4096].
510
+ export function getTextureBucketSizes( maxTextureSize, count = TEXTURE_CONSTANTS.MATERIAL_BUCKET_COUNT ) {
511
+
512
+ const sizes = [];
513
+ for ( let i = count - 1; i >= 0; i -- ) {
514
+
515
+ sizes.push( Math.max( TEXTURE_CONSTANTS.MIN_TEXTURE_WIDTH, Math.round( maxTextureSize / Math.pow( 2, i ) ) ) );
516
+
517
+ }
518
+
519
+ return sizes;
520
+
521
+ }
522
+
523
+ // Bucket index for a texture, by its power-of-2 longest edge vs the cap's ladder.
524
+ // Larger-than-cap maps land in the top bucket (downscaled to cap, as before).
525
+ export function getTextureBucketId( width, height, maxTextureSize, count = TEXTURE_CONSTANTS.MATERIAL_BUCKET_COUNT ) {
526
+
527
+ const longest = Math.max( width || 1, height || 1 );
528
+ const pot = Math.pow( 2, Math.ceil( Math.log2( longest ) ) );
529
+ const sizes = getTextureBucketSizes( maxTextureSize, count );
530
+ for ( let i = 0; i < sizes.length; i ++ ) if ( pot <= sizes[ i ] ) return i;
531
+ return sizes.length - 1;
532
+
533
+ }
534
+
535
+ // Pack (bucketId, layer) into the single int slot a material map index occupies.
536
+ export function packTextureIndex( bucketId, layer ) {
537
+
538
+ return bucketId * TEXTURE_CONSTANTS.BUCKET_LAYER_STRIDE + layer;
539
+
540
+ }
541
+
498
542
  // Default texture matrix for materials
499
543
  export const DEFAULT_TEXTURE_MATRIX = [ 0, 0, 1, 1, 0, 0, 0, 1 ];
500
544
 
@@ -54,4 +54,5 @@ export const EngineEvents = {
54
54
 
55
55
  // Lifecycle
56
56
  DISPOSE: 'engine:dispose',
57
+ DEVICE_LOST: 'engine:deviceLost',
57
58
  };
@@ -142,6 +142,9 @@ export class OIDNDenoiser extends EventDispatcher {
142
142
 
143
143
  this.currentTZAUrl = null;
144
144
  this.unet = null;
145
+ // A start() requested while the UNet is still loading is deferred here and fired
146
+ // once loading finishes, instead of being silently dropped.
147
+ this._pendingStart = false;
145
148
 
146
149
  // Initialize asynchronously
147
150
  this._initialize().catch( error => {
@@ -257,6 +260,15 @@ export class OIDNDenoiser extends EventDispatcher {
257
260
 
258
261
  this.state.isLoading = false;
259
262
 
263
+ // Fire a start() that arrived mid-load. Guard on unet+enabled so a failed load
264
+ // or a disable during loading doesn't kick off a denoise.
265
+ if ( this._pendingStart && this.unet && this.enabled ) {
266
+
267
+ this._pendingStart = false;
268
+ this.start();
269
+
270
+ }
271
+
260
272
  }
261
273
 
262
274
  }
@@ -302,8 +314,17 @@ export class OIDNDenoiser extends EventDispatcher {
302
314
 
303
315
  async start() {
304
316
 
305
- if ( ! this.enabled || this.state.isDenoising || this.state.isLoading ) {
317
+ if ( ! this.enabled || this.state.isDenoising ) {
318
+
319
+ return false;
320
+
321
+ }
306
322
 
323
+ // UNet weights still loading — defer the start (fired from _setupUNetDenoiser's
324
+ // finally) rather than silently dropping this frame's denoise request.
325
+ if ( this.state.isLoading ) {
326
+
327
+ this._pendingStart = true;
307
328
  return false;
308
329
 
309
330
  }
@@ -870,6 +891,9 @@ export class OIDNDenoiser extends EventDispatcher {
870
891
 
871
892
  abort() {
872
893
 
894
+ // Cancel any start deferred during loading so a reset supersedes it.
895
+ this._pendingStart = false;
896
+
873
897
  if ( ! this.enabled || ! this.state.isDenoising ) return;
874
898
 
875
899
  // Signal abort to current operation
@@ -157,6 +157,7 @@ export class PathTracerApp extends EventDispatcher {
157
157
  // Tracked listeners for clean dispose()
158
158
  this._trackedListeners = [];
159
159
  this._disposed = false;
160
+ this._deviceLost = false;
160
161
 
161
162
  }
162
163
 
@@ -230,6 +231,9 @@ export class PathTracerApp extends EventDispatcher {
230
231
  */
231
232
  animate() {
232
233
 
234
+ // Device lost: stop the loop rather than rescheduling render() on a dead device.
235
+ if ( this._deviceLost ) return;
236
+
233
237
  this.animationManagerId = requestAnimationFrame( () => this.animate() );
234
238
 
235
239
  if ( this._loadingInProgress || this._sdf?.isProcessing ) {
@@ -368,9 +372,25 @@ export class PathTracerApp extends EventDispatcher {
368
372
 
369
373
  }
370
374
 
375
+ /**
376
+ * Handle GPU device loss: halt the render loop and notify hosts so they can surface a
377
+ * "renderer lost — reload" prompt. Full auto-recovery would require rebuilding every GPU
378
+ * resource, so this deliberately stops cleanly rather than attempting to re-init.
379
+ */
380
+ _handleDeviceLost( info ) {
381
+
382
+ if ( this._deviceLost ) return;
383
+ this._deviceLost = true;
384
+ console.error( `WebGPU device lost (${info?.reason || 'unknown'}): ${info?.message || ''}` );
385
+ this.stopAnimation();
386
+ this.dispatchEvent( { type: EngineEvents.DEVICE_LOST, reason: info?.reason, message: info?.message } );
387
+
388
+ }
389
+
371
390
  /** Wakes the animation loop if it was stopped due to idle. */
372
391
  wake() {
373
392
 
393
+ if ( this._deviceLost ) return;
374
394
  if ( ! this.animationManagerId && this.isInitialized && ! this._paused ) this.animate();
375
395
 
376
396
  }
@@ -1385,6 +1405,22 @@ export class PathTracerApp extends EventDispatcher {
1385
1405
 
1386
1406
  await this.renderer.init();
1387
1407
 
1408
+ // Detect GPU device loss (dGPU/iGPU switch, driver reset, TDR watchdog on heavy
1409
+ // compute). Without this the rAF loop keeps calling render() on a dead device,
1410
+ // spewing errors forever. reason 'destroyed' during dispose() is intentional teardown.
1411
+ const gpuDevice = this.renderer.backend?.device;
1412
+ if ( gpuDevice?.lost ) {
1413
+
1414
+ gpuDevice.lost.then( ( info ) => {
1415
+
1416
+ if ( this._disposed ) return;
1417
+ this._handleDeviceLost( info );
1418
+
1419
+ } );
1420
+ gpuDevice.onuncapturederror = ( event ) => console.error( 'WebGPU uncaptured error:', event.error );
1421
+
1422
+ }
1423
+
1388
1424
  RectAreaLightNode.setLTC( RectAreaLightTexturesLib.init() );
1389
1425
 
1390
1426
  this.renderer.outputColorSpace = SRGBColorSpace;
@@ -292,6 +292,9 @@ export class AssetLoader extends EventDispatcher {
292
292
 
293
293
  if ( ! this.loaderCache.texture ) this.loaderCache.texture = new TextureLoader();
294
294
  texture = await this.loaderCache.texture.loadAsync( url );
295
+ // LDR env maps (jpg/png/webp) are authored in sRGB; tag them so the backend
296
+ // decodes to linear. HDR/EXR are already linear and keep the loader's setting.
297
+ texture.colorSpace = SRGBColorSpace;
295
298
 
296
299
  }
297
300
 
@@ -45,8 +45,12 @@ export class GeometryExtractor {
45
45
 
46
46
  this.resetArrays();
47
47
 
48
- // Single-pass: allocate with initial capacity, grow dynamically as needed
49
- this._triangleCapacity = 1024;
48
+ // Pre-count the exact triangle total so the buffer is allocated once.
49
+ // Doubling-with-copy rounded up to the next power of two (wasting ~1GB on
50
+ // an 8.5M-tri scene) and held old+new during the copy (~3GB transient),
51
+ // which overflowed the browser's ArrayBuffer allocator. Exact avoids both.
52
+ const totalTriangles = this._countTriangles( object );
53
+ this._triangleCapacity = Math.max( 1024, totalTriangles );
50
54
  this.triangleData = new Float32Array( this._triangleCapacity * TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE );
51
55
  this.currentTriangleIndex = 0;
52
56
 
@@ -58,19 +62,41 @@ export class GeometryExtractor {
58
62
 
59
63
  }
60
64
 
61
- // Ensure triangleData has capacity for at least `needed` triangles
65
+ // Sum the exact triangle count over every mesh extract() will process
66
+ // (mirrors traverseObject/processMesh guards) so we can allocate once.
67
+ _countTriangles( object ) {
68
+
69
+ let count = 0;
70
+
71
+ if ( object.isMesh && object.geometry && object.material ) {
72
+
73
+ const indices = object.geometry.index;
74
+ const positions = object.geometry.attributes.position;
75
+ if ( indices ) count += Math.ceil( indices.count / 3 );
76
+ else if ( positions ) count += Math.ceil( positions.count / 3 );
77
+
78
+ }
79
+
80
+ if ( object.children ) {
81
+
82
+ for ( const child of object.children ) count += this._countTriangles( child );
83
+
84
+ }
85
+
86
+ return count;
87
+
88
+ }
89
+
90
+ // Safety net only: extract() pre-counts and allocates exactly, so this should
91
+ // not fire. Grow to exactly `needed` (never double) to avoid a memory spike.
62
92
  _ensureCapacity( needed ) {
63
93
 
64
94
  if ( needed <= this._triangleCapacity ) return;
65
95
 
66
- // Double until sufficient
67
- let newCapacity = this._triangleCapacity;
68
- while ( newCapacity < needed ) newCapacity *= 2;
69
-
70
- const newData = new Float32Array( newCapacity * TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE );
96
+ const newData = new Float32Array( needed * TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE );
71
97
  newData.set( this.triangleData );
72
98
  this.triangleData = newData;
73
- this._triangleCapacity = newCapacity;
99
+ this._triangleCapacity = needed;
74
100
 
75
101
  }
76
102
 
@@ -38,6 +38,16 @@ let _cap = 0;
38
38
 
39
39
  const soa = ( id, slot ) => ( slot === 0 ? id : id.add( slot * _cap ) );
40
40
 
41
+ // Free a compute storage attribute's GPU buffer. Nulling the JS reference alone leaks it —
42
+ // these attributes hang off no geometry, so nothing triggers the renderer's attribute cleanup.
43
+ // Deleting from the renderer's attribute map calls backend.destroyAttribute(). Guarded no-op
44
+ // when the attribute was never uploaded or no renderer was provided (e.g. unit tests).
45
+ export function freeStorageAttribute( renderer, attr ) {
46
+
47
+ if ( attr ) renderer?._attributes?.delete?.( attr );
48
+
49
+ }
50
+
41
51
  export class PackedRayBuffer {
42
52
 
43
53
  // Capacity maxRays would allocate (mirrors allocate()/resize()). 1.25× headroom, NO pow2 rounding —
@@ -49,9 +59,10 @@ export class PackedRayBuffer {
49
59
 
50
60
  }
51
61
 
52
- constructor( maxRays = 0 ) {
62
+ constructor( maxRays = 0, renderer = null ) {
53
63
 
54
64
  this.capacity = 0;
65
+ this._renderer = renderer;
55
66
  this._attrs = {};
56
67
 
57
68
  // Each: { rw: StorageBufferNode, ro: StorageBufferNode } over one shared GPU buffer.
@@ -118,6 +129,9 @@ export class PackedRayBuffer {
118
129
 
119
130
  dispose() {
120
131
 
132
+ freeStorageAttribute( this._renderer, this._attrs.ray );
133
+ freeStorageAttribute( this._renderer, this._attrs.rng );
134
+ freeStorageAttribute( this._renderer, this._attrs.hit );
121
135
  this._attrs = {};
122
136
  this.rayBuffer = null;
123
137
  this.rngBuffer = null;
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { storage } from 'three/tsl';
6
6
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
7
+ import { freeStorageAttribute } from './PackedRayBuffer.js';
7
8
 
8
9
  /** Counter indices — must match ResetCounters kernel */
9
10
  export const COUNTER = {
@@ -33,8 +34,9 @@ export class QueueManager {
33
34
  /**
34
35
  * @param {number} maxRays - Maximum number of rays (typically width * height)
35
36
  */
36
- constructor( maxRays = 0 ) {
37
+ constructor( maxRays = 0, renderer = null ) {
37
38
 
39
+ this._renderer = renderer;
38
40
  this.capacity = 0;
39
41
  this.counters = null;
40
42
  // A/B alternate: one read by current bounce, other written by compaction
@@ -197,6 +199,16 @@ export class QueueManager {
197
199
 
198
200
  dispose() {
199
201
 
202
+ // Free the GPU buffers before dropping the node references (nulling alone leaks them).
203
+ freeStorageAttribute( this._renderer, this._countersAttr );
204
+ freeStorageAttribute( this._renderer, this._bounceCountsAttr );
205
+ freeStorageAttribute( this._renderer, this._attrA );
206
+ freeStorageAttribute( this._renderer, this._attrB );
207
+ freeStorageAttribute( this._renderer, this._sortAttr );
208
+ freeStorageAttribute( this._renderer, this._sortGlobalHistAttr );
209
+ this._countersAttr = this._bounceCountsAttr = null;
210
+ this._attrA = this._attrB = this._sortAttr = this._sortGlobalHistAttr = null;
211
+
200
212
  this.counters = null;
201
213
  this.activeIndices = null;
202
214
  this.activeIndicesRO = null;