rayzee 8.0.0 → 8.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/dist/rayzee.es.js +4413 -4411
- package/dist/rayzee.umd.js +80 -215
- package/package.json +2 -2
- package/src/Passes/OIDNDenoiser.js +196 -270
- package/src/PathTracerApp.js +73 -19
- package/src/Processor/GeometryExtractor.js +3 -1
- package/src/Processor/utils.js +8 -20
- package/src/Stages/Compositor.js +1 -0
- package/src/Stages/PathTracerStage.js +25 -0
- package/src/TSL/SortGlobalKernels.js +3 -4
- package/src/TSL/patches.js +22 -82
- package/src/managers/DenoisingManager.js +276 -58
- package/src/managers/VideoRenderManager.js +4 -0
- package/src/Processor/ToneMapWGSL.js +0 -162
package/src/PathTracerApp.js
CHANGED
|
@@ -362,8 +362,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
362
362
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
const cameraMoved = this.needsReset;
|
|
365
366
|
if ( this.needsReset ) {
|
|
366
367
|
|
|
368
|
+
// Before the reset, so the denoising manager's abort sees this frame as part of a move
|
|
369
|
+
// rather than deciding to hold on the first frame and dropping it on the second.
|
|
370
|
+
this.stages.pathTracer?.noteViewChanged();
|
|
367
371
|
this.reset( true );
|
|
368
372
|
this.needsReset = false;
|
|
369
373
|
|
|
@@ -404,6 +408,23 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
404
408
|
|
|
405
409
|
}
|
|
406
410
|
|
|
411
|
+
// A frame traced while a denoise is in flight is never seen: only denoised frames reach
|
|
412
|
+
// the canvas during a camera move, and the next denoise reads the newest frame anyway.
|
|
413
|
+
// Tracing it only takes the GPU away from the denoise the viewport is waiting on —
|
|
414
|
+
// inside a room that turned a 10 ms denoise into 185 ms of wall clock.
|
|
415
|
+
if ( this.denoisingManager?.skipsTrace() ) {
|
|
416
|
+
|
|
417
|
+
// The camera is still being dragged; without this the interaction timeout can
|
|
418
|
+
// expire inside a long denoise and drop the view out of interaction mode.
|
|
419
|
+
if ( cameraMoved ) this.stages.pathTracer.enterInteractionMode();
|
|
420
|
+
// Gizmos and outlines are drawn against the live camera, not the traced frame, so
|
|
421
|
+
// they would visibly lag the view if they only redrew on the frames that traced.
|
|
422
|
+
this._renderHelperOverlay();
|
|
423
|
+
this.dispatchEvent( { type: EngineEvents.FRAME } );
|
|
424
|
+
return;
|
|
425
|
+
|
|
426
|
+
}
|
|
427
|
+
|
|
407
428
|
this.pipeline.render();
|
|
408
429
|
|
|
409
430
|
if ( ! this.stages.pathTracer.isComplete ) {
|
|
@@ -540,7 +561,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
540
561
|
|
|
541
562
|
}
|
|
542
563
|
|
|
543
|
-
|
|
564
|
+
// Whatever is on screen stays until its replacement is ready, including while the camera
|
|
565
|
+
// moves: the denoising manager decides, since only it knows something is coming.
|
|
566
|
+
this._abortPostProcess( { keepDisplay: true } );
|
|
544
567
|
|
|
545
568
|
this.completion.reset();
|
|
546
569
|
this.wake();
|
|
@@ -713,6 +736,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
713
736
|
|
|
714
737
|
}
|
|
715
738
|
|
|
739
|
+
this.denoisingManager?.dropDisplay();
|
|
716
740
|
this.reset();
|
|
717
741
|
this.dispatchEvent( { type: 'SceneUnloaded' } );
|
|
718
742
|
|
|
@@ -962,6 +986,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
962
986
|
this._syncControlsAfterLoad();
|
|
963
987
|
await this.loadSceneData( { pendingEnvironment: this._beginSceneMetadataEnvironment() } );
|
|
964
988
|
this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
|
|
989
|
+
// Not held: the first denoise of a new scene lands ~1.1 s after the load (upload and
|
|
990
|
+
// shader compilation come first), and a second of the previous model reads as a bug.
|
|
991
|
+
this.denoisingManager?.dropDisplay();
|
|
965
992
|
this.reset();
|
|
966
993
|
this.cameraManager.currentCameraIndex = 0;
|
|
967
994
|
this.dispatchEvent( eventPayload );
|
|
@@ -994,6 +1021,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
994
1021
|
|
|
995
1022
|
this._clearAppendedModels();
|
|
996
1023
|
this.assetLoader?.releaseTargetModel();
|
|
1024
|
+
this.denoisingManager?.dropDisplay();
|
|
997
1025
|
this.reset();
|
|
998
1026
|
|
|
999
1027
|
} catch ( cleanupError ) {
|
|
@@ -1853,6 +1881,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1853
1881
|
const isProduction = mode === 'production';
|
|
1854
1882
|
const config = isProduction ? PRODUCTION_RENDER_CONFIG : INTERACTIVE_RENDER_CONFIG;
|
|
1855
1883
|
|
|
1884
|
+
// First, before anything below can wake the loop or resize the renderer: a live-view
|
|
1885
|
+
// refresh landing in the middle of that raced the renderer's own output pass.
|
|
1886
|
+
this.denoisingManager?.setCadenceSuspended( isProduction );
|
|
1887
|
+
|
|
1856
1888
|
this.cameraManager.controls.enabled = ! isProduction;
|
|
1857
1889
|
|
|
1858
1890
|
// Anything with a SETTING_ROUTES entry must go through settings, not setUniform: set() early-returns on
|
|
@@ -1935,9 +1967,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1935
1967
|
|
|
1936
1968
|
// Aborts any in-flight denoise/upscale and puts the denoiser canvas back at base resolution (the
|
|
1937
1969
|
// upscaler leaves it enlarged), so the live canvas is what's on screen again.
|
|
1938
|
-
_abortPostProcess() {
|
|
1970
|
+
_abortPostProcess( { keepDisplay = false } = {} ) {
|
|
1939
1971
|
|
|
1940
|
-
this.denoisingManager?.abort( this.canvas );
|
|
1972
|
+
this.denoisingManager?.abort( this.canvas, { keepDisplay } );
|
|
1941
1973
|
|
|
1942
1974
|
if ( this.denoisingManager?.restoreBaseResolution() ) {
|
|
1943
1975
|
|
|
@@ -2423,8 +2455,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2423
2455
|
// ═══════════════════════════════════════════════════════════════
|
|
2424
2456
|
|
|
2425
2457
|
/**
|
|
2426
|
-
* Returns the canvas element
|
|
2427
|
-
*
|
|
2458
|
+
* Returns the canvas element holding the image on screen — denoised, graded and tone-mapped,
|
|
2459
|
+
* whatever is currently showing. The upscaler is the one thing that paints elsewhere.
|
|
2428
2460
|
* @returns {HTMLCanvasElement|null}
|
|
2429
2461
|
*/
|
|
2430
2462
|
getCanvas() {
|
|
@@ -2432,13 +2464,13 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2432
2464
|
if ( ! this.renderer?.domElement ) return null;
|
|
2433
2465
|
|
|
2434
2466
|
const dm = this.denoisingManager;
|
|
2435
|
-
const
|
|
2436
|
-
&& dm
|
|
2437
|
-
&& this.stages.pathTracer?.isComplete;
|
|
2467
|
+
const upscaled = dm?.upscaler?.enabled && dm?.upscalerCanvas
|
|
2468
|
+
&& dm.upscalerCanvas.style.display !== 'none';
|
|
2438
2469
|
|
|
2439
|
-
if (
|
|
2470
|
+
if ( upscaled ) return dm.upscalerCanvas;
|
|
2440
2471
|
|
|
2441
|
-
//
|
|
2472
|
+
// A presented WebGPU canvas only reads back what was drawn immediately before, so draw.
|
|
2473
|
+
// This also puts the denoised picture on it: the Compositor prefers it over the raw render.
|
|
2442
2474
|
if ( this.stages.compositor && this.pipeline?.context ) {
|
|
2443
2475
|
|
|
2444
2476
|
this.stages.compositor.render( this.pipeline.context );
|
|
@@ -2881,16 +2913,41 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2881
2913
|
|
|
2882
2914
|
const adapterLimits = adapter.limits;
|
|
2883
2915
|
|
|
2916
|
+
// A limit that is NOT requested is granted at WebGPU's portable default, never at the
|
|
2917
|
+
// adapter's maximum — on Apple Metal-3 that is 16 sampled textures of 48, 4 storage
|
|
2918
|
+
// textures of 8, 256 workgroup invocations of 1024 and 256 array layers of 2048.
|
|
2919
|
+
// Requesting exactly what the adapter reports can never fail, so a weaker GPU simply
|
|
2920
|
+
// gets less and every consumer has to stay adaptive.
|
|
2921
|
+
const requiredLimits = { maxColorAttachmentBytesPerSample: 128 };
|
|
2922
|
+
|
|
2923
|
+
for ( const key of [
|
|
2924
|
+
'maxBufferSize',
|
|
2925
|
+
'maxStorageBufferBindingSize',
|
|
2926
|
+
'maxSampledTexturesPerShaderStage',
|
|
2927
|
+
'maxStorageTexturesPerShaderStage',
|
|
2928
|
+
'maxTextureArrayLayers',
|
|
2929
|
+
'maxTextureDimension2D',
|
|
2930
|
+
'maxComputeInvocationsPerWorkgroup',
|
|
2931
|
+
'maxComputeWorkgroupStorageSize',
|
|
2932
|
+
'maxComputeWorkgroupSizeX',
|
|
2933
|
+
'maxComputeWorkgroupSizeY',
|
|
2934
|
+
'maxComputeWorkgroupSizeZ',
|
|
2935
|
+
] ) {
|
|
2936
|
+
|
|
2937
|
+
const value = adapterLimits[ key ];
|
|
2938
|
+
if ( value !== undefined ) requiredLimits[ key ] = value;
|
|
2939
|
+
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
// Shade binds exactly 10 and the kernels are written to that budget; asking for more
|
|
2943
|
+
// buys nothing, and 10 is already this adapter's ceiling.
|
|
2944
|
+
requiredLimits.maxStorageBuffersPerShaderStage = Math.min( adapterLimits.maxStorageBuffersPerShaderStage, 10 );
|
|
2945
|
+
|
|
2884
2946
|
this.renderer = new WebGPURenderer( {
|
|
2885
2947
|
canvas: this.canvas,
|
|
2886
2948
|
alpha: true,
|
|
2887
2949
|
powerPreference: 'high-performance',
|
|
2888
|
-
requiredLimits
|
|
2889
|
-
maxBufferSize: adapterLimits.maxBufferSize,
|
|
2890
|
-
maxStorageBufferBindingSize: adapterLimits.maxStorageBufferBindingSize,
|
|
2891
|
-
maxColorAttachmentBytesPerSample: 128,
|
|
2892
|
-
maxStorageBuffersPerShaderStage: Math.min( adapterLimits.maxStorageBuffersPerShaderStage, 10 ),
|
|
2893
|
-
}
|
|
2950
|
+
requiredLimits,
|
|
2894
2951
|
} );
|
|
2895
2952
|
|
|
2896
2953
|
await this.renderer.init();
|
|
@@ -3236,8 +3293,6 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3236
3293
|
this.denoisingManager = new DenoisingManager( {
|
|
3237
3294
|
renderer: this.renderer,
|
|
3238
3295
|
mainCanvas: this.canvas,
|
|
3239
|
-
scene: this.scene,
|
|
3240
|
-
camera: this.cameraManager.camera,
|
|
3241
3296
|
stages: {
|
|
3242
3297
|
pathTracer: this.stages.pathTracer,
|
|
3243
3298
|
normalDepth: this.stages.normalDepth,
|
|
@@ -3253,7 +3308,6 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3253
3308
|
pipeline: this.pipeline,
|
|
3254
3309
|
getExposure: () => this.settings.get( 'exposure' ) ?? 1.0,
|
|
3255
3310
|
getSaturation: () => this.settings.get( 'saturation' ) ?? 1.0,
|
|
3256
|
-
getTransparentBg: () => this.settings.get( 'transparentBackground' ) ?? false,
|
|
3257
3311
|
} );
|
|
3258
3312
|
|
|
3259
3313
|
this.denoisingManager.setupDenoiser();
|
|
@@ -965,7 +965,9 @@ export class GeometryExtractor {
|
|
|
965
965
|
getTextureMatrix( texture ) {
|
|
966
966
|
|
|
967
967
|
if ( ! texture ) return new Matrix3().elements;
|
|
968
|
-
|
|
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
|
}
|
package/src/Processor/utils.js
CHANGED
|
@@ -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.
|
|
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
|
|
111
|
-
* (
|
|
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
|
-
|
|
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
|
|
package/src/Stages/Compositor.js
CHANGED
|
@@ -67,6 +67,7 @@ export class Compositor extends RenderStage {
|
|
|
67
67
|
_resolveSourceTexture( context ) {
|
|
68
68
|
|
|
69
69
|
return context.getTexture( 'bloom:output' )
|
|
70
|
+
|| context.getTexture( 'oidn:output' )
|
|
70
71
|
|| context.getTexture( 'edgeFiltering:output' )
|
|
71
72
|
|| context.getTexture( 'bilateralFiltering:output' )
|
|
72
73
|
|| context.getTexture( 'asvgf:output' )
|
|
@@ -30,6 +30,10 @@ import { createLogger, fmt } from '../utils/Logger.js';
|
|
|
30
30
|
|
|
31
31
|
const log = createLogger( 'pathtracer' );
|
|
32
32
|
|
|
33
|
+
// How long after the last move the view still counts as moving. Matches CameraOptimizer's own
|
|
34
|
+
// settle delay, so the answer does not change when interaction mode is switched off.
|
|
35
|
+
const VIEW_SETTLE_MS = 100;
|
|
36
|
+
|
|
33
37
|
/**
|
|
34
38
|
* Data layout constants
|
|
35
39
|
*/
|
|
@@ -163,6 +167,8 @@ export class PathTracerStage extends RenderStage {
|
|
|
163
167
|
|
|
164
168
|
// Track changes for event emission
|
|
165
169
|
this.cameraChanged = false;
|
|
170
|
+
// When the view was last moved, for viewIsChanging.
|
|
171
|
+
this._lastViewChangeAt = - Infinity;
|
|
166
172
|
|
|
167
173
|
// Update completion threshold
|
|
168
174
|
this.updateCompletionThreshold();
|
|
@@ -704,6 +710,25 @@ export class PathTracerStage extends RenderStage {
|
|
|
704
710
|
|
|
705
711
|
}
|
|
706
712
|
|
|
713
|
+
/**
|
|
714
|
+
* Whether the view is being moved right now — the camera or a gizmo — whether or not interaction
|
|
715
|
+
* mode is on. Interaction mode answers a narrower question (may quality be reduced) and is a
|
|
716
|
+
* switch a host can turn off; what a denoiser needs to know is only whether this frame is about
|
|
717
|
+
* to be thrown away by the next move.
|
|
718
|
+
*/
|
|
719
|
+
get viewIsChanging() {
|
|
720
|
+
|
|
721
|
+
return this.interactionMode || performance.now() - this._lastViewChangeAt < VIEW_SETTLE_MS;
|
|
722
|
+
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Called by the host loop on a frame whose reset came from the view moving.
|
|
726
|
+
noteViewChanged() {
|
|
727
|
+
|
|
728
|
+
this._lastViewChangeAt = performance.now();
|
|
729
|
+
|
|
730
|
+
}
|
|
731
|
+
|
|
707
732
|
// ===== TEXTURE SETTERS =====
|
|
708
733
|
|
|
709
734
|
/**
|
|
@@ -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 =
|
|
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 =
|
|
129
|
+
const local = workgroupArray( 'uint', bins ).toAtomic();
|
|
131
130
|
|
|
132
131
|
If( lid.lessThan( uint( bins ) ), () => {
|
|
133
132
|
|
package/src/TSL/patches.js
CHANGED
|
@@ -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.
|
|
353
|
+
// 4. r186 ContextNode.setup regression — DELETE WHEN THE three DEP REACHES r187
|
|
355
354
|
// ---------------------------------------------------------------------------
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
379
|
-
if ( this.isAtomicArray === true ) {
|
|
369
|
+
ContextNode.prototype.setup = function ( builder ) {
|
|
380
370
|
|
|
381
|
-
|
|
382
|
-
if ( entry ) entry.isAtomic = true;
|
|
371
|
+
const previousContext = builder.addContext( this.value );
|
|
383
372
|
|
|
384
|
-
|
|
373
|
+
this.node.build( builder );
|
|
385
374
|
|
|
386
|
-
|
|
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
|
-
}
|