rayzee 8.0.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "8.0.0",
3
+ "version": "8.1.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",
@@ -35,7 +35,7 @@
35
35
  "prepublishOnly": "npm run build"
36
36
  },
37
37
  "peerDependencies": {
38
- "three": ">=0.185.0"
38
+ "three": ">=0.186.0"
39
39
  },
40
40
  "optionalDependencies": {
41
41
  "oidn-web": ">=0.4.0",
@@ -2881,16 +2881,41 @@ export class PathTracerApp extends EventDispatcher {
2881
2881
 
2882
2882
  const adapterLimits = adapter.limits;
2883
2883
 
2884
+ // A limit that is NOT requested is granted at WebGPU's portable default, never at the
2885
+ // adapter's maximum — on Apple Metal-3 that is 16 sampled textures of 48, 4 storage
2886
+ // textures of 8, 256 workgroup invocations of 1024 and 256 array layers of 2048.
2887
+ // Requesting exactly what the adapter reports can never fail, so a weaker GPU simply
2888
+ // gets less and every consumer has to stay adaptive.
2889
+ const requiredLimits = { maxColorAttachmentBytesPerSample: 128 };
2890
+
2891
+ for ( const key of [
2892
+ 'maxBufferSize',
2893
+ 'maxStorageBufferBindingSize',
2894
+ 'maxSampledTexturesPerShaderStage',
2895
+ 'maxStorageTexturesPerShaderStage',
2896
+ 'maxTextureArrayLayers',
2897
+ 'maxTextureDimension2D',
2898
+ 'maxComputeInvocationsPerWorkgroup',
2899
+ 'maxComputeWorkgroupStorageSize',
2900
+ 'maxComputeWorkgroupSizeX',
2901
+ 'maxComputeWorkgroupSizeY',
2902
+ 'maxComputeWorkgroupSizeZ',
2903
+ ] ) {
2904
+
2905
+ const value = adapterLimits[ key ];
2906
+ if ( value !== undefined ) requiredLimits[ key ] = value;
2907
+
2908
+ }
2909
+
2910
+ // Shade binds exactly 10 and the kernels are written to that budget; asking for more
2911
+ // buys nothing, and 10 is already this adapter's ceiling.
2912
+ requiredLimits.maxStorageBuffersPerShaderStage = Math.min( adapterLimits.maxStorageBuffersPerShaderStage, 10 );
2913
+
2884
2914
  this.renderer = new WebGPURenderer( {
2885
2915
  canvas: this.canvas,
2886
2916
  alpha: true,
2887
2917
  powerPreference: 'high-performance',
2888
- requiredLimits: {
2889
- maxBufferSize: adapterLimits.maxBufferSize,
2890
- maxStorageBufferBindingSize: adapterLimits.maxStorageBufferBindingSize,
2891
- maxColorAttachmentBytesPerSample: 128,
2892
- maxStorageBuffersPerShaderStage: Math.min( adapterLimits.maxStorageBuffersPerShaderStage, 10 ),
2893
- }
2918
+ requiredLimits,
2894
2919
  } );
2895
2920
 
2896
2921
  await this.renderer.init();
@@ -965,7 +965,9 @@ export class GeometryExtractor {
965
965
  getTextureMatrix( texture ) {
966
966
 
967
967
  if ( ! texture ) return new Matrix3().elements;
968
- texture.updateMatrix();
968
+ // GLTFLoader writes `matrix` itself for a rotated KHR_texture_transform (glTF is
969
+ // T·R·S, three is T·S·R) and clears the flag; updateMatrix() ignores the flag.
970
+ if ( texture.matrixAutoUpdate ) texture.updateMatrix();
969
971
  return texture.matrix.elements;
970
972
 
971
973
  }
@@ -1,10 +1,6 @@
1
1
  import { SphereGeometry, Mesh, MeshPhysicalMaterial, Group, Object3D, Color, Vector3, ShaderMaterial, Vector2, Matrix4, GLSL3 } from 'three';
2
- import { QuadMesh } from 'three/webgpu';
3
2
  import { EngineEvents } from '../EngineEvents.js';
4
3
 
5
- /** Every QuadMesh in the realm shares this one geometry (module-level in three). */
6
- const _sharedQuadGeometry = /*@__PURE__*/ new QuadMesh( null ).geometry;
7
-
8
4
  let _statusCallback = null;
9
5
 
10
6
  /**
@@ -103,17 +99,18 @@ export function distroyBuffers( { material, geometry, children } ) {
103
99
  /**
104
100
  * Disposes a renderer without leaking it.
105
101
  *
106
- * Three.js 0.184–0.185: `Renderer.dispose()` does not remove the 'resize' listener
102
+ * Three.js 0.184–0.186: `Renderer.dispose()` does not remove the 'resize' listener
107
103
  * it installs on `_canvasTarget`. The bound handler closes over the renderer,
108
104
  * pinning the entire WebGPU graph (Backend, Nodes, Bindings, Pipelines, GPUDevice,
109
105
  * every TSL node) alive indefinitely — confirmed via heap-snapshot retainer
110
- * analysis. See three/src/renderers/common/Renderer.js:292 (attach) and :2503
111
- * (dispose — missing removal).
106
+ * analysis. See `Renderer.js` (attach in the constructor, still absent from
107
+ * `dispose()` in r186). Re-check on every upgrade; drop this when upstream fixes it.
112
108
  *
113
109
  * Renderers constructed with an external `device` do not destroy it here; three
114
110
  * only destroys a device it created itself.
115
111
  *
116
112
  * @param {import('three/webgpu').WebGPURenderer} renderer
113
+ * @returns {?Promise<void>} Settles once the backend is torn down.
117
114
  */
118
115
  export function disposeRenderer( renderer ) {
119
116
 
@@ -125,20 +122,11 @@ export function disposeRenderer( renderer ) {
125
122
 
126
123
  }
127
124
 
128
- renderer.dispose();
125
+ // r186+ `dispose()` is async and settles once the backend is torn down; anything that
126
+ // measures memory straight afterwards has to await the returned promise.
127
+ const disposed = renderer.dispose();
129
128
  renderer._canvasTarget = null;
130
-
131
- // Three.js 0.185: `RenderObjects.dispose()` drops its chain maps without disposing the
132
- // render objects, so each one leaves behind the 'dispose' listener it registered on its
133
- // geometry. Full-screen passes render a QuadMesh, and every QuadMesh in the realm shares
134
- // one module-level geometry — so that listener list grows by one entry per renderer and
135
- // keeps RenderObject → RenderObjects → Bindings → Backend → GPUDevice reachable for the
136
- // page's lifetime. The chain maps are WeakMaps, so this list is the only handle on them.
137
- //
138
- // Clearing it also drops entries belonging to live renderers, which is safe: the
139
- // singleton geometry is never disposed, so none of these listeners can ever fire.
140
- const quadDisposeListeners = _sharedQuadGeometry._listeners?.dispose;
141
- if ( quadDisposeListeners ) quadDisposeListeners.length = 0;
129
+ return disposed;
142
130
 
143
131
  }
144
132
 
@@ -21,11 +21,10 @@ import {
21
21
  Fn, uint,
22
22
  If, Loop,
23
23
  instanceIndex, localId,
24
- workgroupBarrier,
24
+ workgroupBarrier, workgroupArray,
25
25
  atomicAdd, atomicLoad, atomicStore,
26
26
  } from 'three/tsl';
27
27
 
28
- import { workgroupAtomicArray } from './patches.js';
29
28
  import { readHitMaterialIndex } from '../Processor/PackedRayBuffer.js';
30
29
  import { COUNTER } from '../Processor/QueueManager.js';
31
30
 
@@ -54,7 +53,7 @@ export function buildGlobalHistKernel( { hitBufferRO, activeIndicesReadRO, sortG
54
53
 
55
54
  const tid = instanceIndex;
56
55
  const lid = localId.x;
57
- const local = workgroupAtomicArray( 'uint', bins );
56
+ const local = workgroupArray( 'uint', bins ).toAtomic();
58
57
 
59
58
  If( lid.lessThan( uint( bins ) ), () => {
60
59
 
@@ -127,7 +126,7 @@ export function buildGlobalScatterKernel( { hitBufferRO, activeIndicesReadRO, so
127
126
 
128
127
  const tid = instanceIndex;
129
128
  const lid = localId.x;
130
- const local = workgroupAtomicArray( 'uint', bins );
129
+ const local = workgroupArray( 'uint', bins ).toAtomic();
131
130
 
132
131
  If( lid.lessThan( uint( bins ) ), () => {
133
132
 
@@ -7,12 +7,15 @@
7
7
  * loop) and `initTimestampQuery` (enlarges the stats-gl timestamp query pool so
8
8
  * the wavefront tracer's high per-frame compute-pass count doesn't overflow it).
9
9
  *
10
+ * Also overrides `ContextNode.prototype.setup` to undo an r186 codegen regression
11
+ * (§4) — that one is temporary and goes away at r187.
12
+ *
10
13
  * Exports: `struct()` — drop-in replacement for TSL's `struct()` returning
11
14
  * a proxy factory that supports GLSL-style dot-notation field access — and
12
15
  * `gpuOnlyStorageAttribute()`, a storage attribute with no CPU backing array.
13
16
  */
14
17
 
15
- import { StorageInstancedBufferAttribute, WebGPUBackend } from 'three/webgpu';
18
+ import { ContextNode, StorageInstancedBufferAttribute, WebGPUBackend } from 'three/webgpu';
16
19
  import { struct as _struct } from 'three/tsl';
17
20
 
18
21
  // ---------------------------------------------------------------------------
@@ -62,10 +65,6 @@ WebGPUBackend.prototype.createNodeBuilder = function ( object, renderer ) {
62
65
  configurable: true,
63
66
  } );
64
67
 
65
- // Install the workgroup-atomic-array codegen patch (section 4) lazily off the
66
- // first builder, so we don't need to import WGSLNodeBuilder directly.
67
- _installScopedArrayAtomicPatch( builder );
68
-
69
68
  return builder;
70
69
 
71
70
  };
@@ -351,87 +350,28 @@ export function struct( members, name = null ) {
351
350
  }
352
351
 
353
352
  // ---------------------------------------------------------------------------
354
- // 4. Workgroup-scoped atomic arrays — var<workgroup> array<atomic<T>, N>
353
+ // 4. r186 ContextNode.setup regression — DELETE WHEN THE three DEP REACHES r187
355
354
  // ---------------------------------------------------------------------------
356
- // TSL's `workgroupArray(type, count)` emits `var<workgroup> name: array<T, N>`
357
- // (`WGSLNodeBuilder.getScopedArrays`, r185 ~line 1773) with a NON-atomic element
358
- // type, so `atomicAdd(arr.element(i), v)` fails WGSL validation. AtomicFunctionNode
359
- // is itself address-space-agnostic — it emits `atomicAdd(&name[i], v)`, valid for
360
- // BOTH storage and workgroup pointers — so the ONLY missing piece is the
361
- // declaration. This patch adds `workgroupAtomicArray()` (a `workgroupArray` tagged
362
- // atomic) and overrides `getScopedArrays` to wrap the element type in `atomic<…>`
363
- // for tagged arrays, enabling true on-chip workgroup-shared atomics (e.g. fast
364
- // per-workgroup histograms) instead of the slow global-storage-atomic fallback.
365
- // Access tagged arrays ONLY via atomic ops (atomicAdd/atomicLoad/atomicStore).
366
-
367
- import { workgroupArray } from 'three/tsl';
368
-
369
- const _WorkgroupInfoNode = workgroupArray( 'uint', 1 ).constructor;
370
-
371
- // Tag the node so the patched getScopedArrays emits an atomic element type. The
372
- // array's WGSL name is only known after generate() runs, so we mark the builder's
373
- // scopedArrays entry there (idempotent across analyze/generate passes).
374
- const _origWorkgroupGenerate = _WorkgroupInfoNode.prototype.generate;
375
-
376
- _WorkgroupInfoNode.prototype.generate = function ( builder ) {
355
+ // r186 (#34251) made `ContextNode.setup()` return the node it had just built.
356
+ // Returning it stores the child's output node in the context node's own
357
+ // properties, so `Node.build()` then re-builds it generically OUTSIDE the
358
+ // context. `Loop()` wraps its body in a context (`fnCall.context({ nodeLoop })`),
359
+ // so every loop-heavy kernel is evaluated against the wrong one — measured on our
360
+ // corpus as 63x energy on furnace-clearcoat, 5.8x on furnace-sheen and NaN in the
361
+ // SSS random walk, all while the WGSL compiled without a single error.
362
+ //
363
+ // Upstream reverted it in #34531 (merged 2026-09-11, on dev, ships in r187); this
364
+ // body is that revert verbatim, and is also what r185 shipped, so it stays correct
365
+ // if the dep is rolled back. Verified: with it, all 27 quality scenes pass and the
366
+ // numbers are identical to r185. Drop this section and the `ContextNode` import
367
+ // once the dep is r187+.
377
368
 
378
- const name = _origWorkgroupGenerate.call( this, builder );
379
- if ( this.isAtomicArray === true ) {
369
+ ContextNode.prototype.setup = function ( builder ) {
380
370
 
381
- const entry = builder.scopedArrays && builder.scopedArrays.get( name );
382
- if ( entry ) entry.isAtomic = true;
371
+ const previousContext = builder.addContext( this.value );
383
372
 
384
- }
373
+ this.node.build( builder );
385
374
 
386
- return name;
375
+ builder.setContext( previousContext );
387
376
 
388
377
  };
389
-
390
- /**
391
- * Like `workgroupArray(type, count)` but declares the workgroup buffer with an
392
- * atomic element type: `var<workgroup> name: array<atomic<type>, count>`.
393
- * Elements MUST be accessed only via atomic ops (atomicAdd/atomicLoad/atomicStore).
394
- *
395
- * @param {string} type - Element type (e.g. 'uint').
396
- * @param {number} count - Number of elements.
397
- * @returns {WorkgroupInfoNode} The tagged workgroup array node.
398
- */
399
- export function workgroupAtomicArray( type, count ) {
400
-
401
- const node = workgroupArray( type, count );
402
- node.isAtomicArray = true;
403
- return node;
404
-
405
- }
406
-
407
- let _scopedArraysPatched = false;
408
-
409
- // Override getScopedArrays on the builder's prototype (compute-stage WGSL
410
- // assembly) to emit `atomic<T>` element types for tagged workgroup arrays.
411
- // Installed once, lazily, off the first node builder created.
412
- function _installScopedArrayAtomicPatch( builder ) {
413
-
414
- if ( _scopedArraysPatched ) return;
415
- const proto = Object.getPrototypeOf( builder );
416
- if ( ! proto || typeof proto.getScopedArrays !== 'function' ) return;
417
-
418
- proto.getScopedArrays = function ( shaderStage ) {
419
-
420
- if ( shaderStage !== 'compute' ) return;
421
-
422
- const snippets = [];
423
- for ( const { name, scope, bufferType, bufferCount, isAtomic } of this.scopedArrays.values() ) {
424
-
425
- const type = this.getType( bufferType );
426
- const elementType = ( isAtomic === true ) ? `atomic< ${ type } >` : type;
427
- snippets.push( `var<${ scope }> ${ name }: array< ${ elementType }, ${ bufferCount } >;` );
428
-
429
- }
430
-
431
- return snippets.join( '\n' );
432
-
433
- };
434
-
435
- _scopedArraysPatched = true;
436
-
437
- }