rayzee 11.5.0 → 11.7.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 (41) hide show
  1. package/README.md +14 -14
  2. package/dist/{ArchiveImporter-29Nl1567.js → ArchiveImporter-DYcRXeKE.js} +2 -2
  3. package/dist/{BidirectionalIntegrator-CJay8tXK.js → BidirectionalIntegrator-BvjRpbTP.js} +289 -289
  4. package/dist/{EngineEvents-Ci8dVWCP.js → EngineEvents-CLckhYbL.js} +1 -0
  5. package/dist/{NeuralRetouch-KqGVPV7u.js → NeuralRetouch-ClaMiKYg.js} +1 -1
  6. package/dist/{NeuralSuperRes-BHxhYzlD.js → NeuralSuperRes-WNEXun9s.js} +2 -2
  7. package/dist/{PathTracer-Cldt0mXi.js → PathTracer-B9wZPPVL.js} +3197 -3167
  8. package/dist/{PhysicalSky-CUWrTcmT.js → PhysicalSky-DcjItqKi.js} +1 -1
  9. package/dist/{RayzeeRenderer-Dd-Dit0v.js → RayzeeRenderer-p-NeE6E2.js} +405 -437
  10. package/dist/addons/archives.es.js +1 -1
  11. package/dist/addons/bidirectional.es.js +1 -1
  12. package/dist/addons/physical-sky.es.js +1 -1
  13. package/dist/rayzee-core.es.js +5 -5
  14. package/dist/rayzee.es.js +1003 -1686
  15. package/dist/rayzee.umd.js +104 -143
  16. package/dist/{sceneCachePolicy--ta6moJu.js → sceneCachePolicy-B9w3BnaP.js} +2 -2
  17. package/package.json +2 -2
  18. package/src/EngineEvents.js +2 -0
  19. package/src/Passes/OIDNDenoiser.js +106 -100
  20. package/src/PathTracerApp.js +8 -19
  21. package/src/Pipeline/BindingAudit.js +2 -3
  22. package/src/Pipeline/PipelineContext.js +0 -1
  23. package/src/Pipeline/RenderPipeline.js +1 -2
  24. package/src/Pipeline/RenderStage.js +1 -1
  25. package/src/Processor/PackedRayBuffer.js +2 -2
  26. package/src/Processor/StorageTexturePool.js +1 -1
  27. package/src/Processor/TextureReadback.js +18 -1
  28. package/src/RayzeeRenderer.js +5 -5
  29. package/src/Stages/DenoiserSettings.js +5 -81
  30. package/src/Stages/MotionVector.js +21 -215
  31. package/src/Stages/NormalDepth.js +5 -5
  32. package/src/Stages/PathTracer.js +3 -11
  33. package/src/Stages/PathTracerStage.js +4 -17
  34. package/src/TSL/FinalWriteKernel.js +2 -2
  35. package/src/TSL/ShadeKernel.js +3 -3
  36. package/src/index.js +1 -1
  37. package/src/managers/DenoisingManager.js +93 -187
  38. package/src/Stages/ASVGF.js +0 -932
  39. package/src/Stages/BilateralFilter.js +0 -486
  40. package/src/Stages/EdgeFilter.js +0 -545
  41. package/src/Stages/Variance.js +0 -443
@@ -1,4 +1,4 @@
1
- import { t as e } from "./EngineEvents-Ci8dVWCP.js";
1
+ import { t as e } from "./EngineEvents-CLckhYbL.js";
2
2
  import { a as t, i as n, r } from "./Logger-dDDGj9Av.js";
3
3
  import { f as i, v as a, y as o } from "./BufferLayout-xPRH9jYD.js";
4
4
  import { n as s } from "./EngineIssues-CIZnXHsF.js";
@@ -1594,7 +1594,7 @@ var vt = class {
1594
1594
  samples: this.samples.slice()
1595
1595
  };
1596
1596
  }
1597
- }, yt = "11.5.0", $ = 1e4, bt = 15e5;
1597
+ }, yt = "11.7.0", $ = 1e4, bt = 15e5;
1598
1598
  function xt(e, t, n = $) {
1599
1599
  return e >= n && t / bt < e / 3;
1600
1600
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "11.5.0",
3
+ "version": "11.7.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",
@@ -60,7 +60,7 @@
60
60
  "three": ">=0.186.0"
61
61
  },
62
62
  "optionalDependencies": {
63
- "oidn-web": ">=0.4.0",
63
+ "oidn-web": ">=0.5.0",
64
64
  "onnxruntime-web": ">=1.0.0"
65
65
  }
66
66
  }
@@ -14,6 +14,8 @@ export const EngineEvents = {
14
14
  // Denoiser
15
15
  DENOISING_START: 'engine:denoisingStart',
16
16
  DENOISING_END: 'engine:denoisingEnd',
17
+ // The denoiser fetches and builds its network ({ loading }); a denoise asked for meanwhile waits for it.
18
+ DENOISER_LOADING: 'engine:denoiserLoading',
17
19
 
18
20
  // Upscaler
19
21
  UPSCALING_START: 'engine:upscalingStart',
@@ -4,9 +4,22 @@ import { createLogger, fmt } from '../utils/Logger.js';
4
4
 
5
5
  const log = createLogger( 'oidn' );
6
6
 
7
- // The tile alignment oidn-web's own fitTileDimension uses (its tileScheduler.js).
7
+ // The tile alignment oidn-web's own planner uses (its tileScheduler.js).
8
8
  const OIDN_TILE_ALIGNMENT = 16;
9
9
 
10
+ // Mirrored around the frame: at the network's own boundary (zero padding, or oidn-web's repeated
11
+ // edge pixel) the outer pixels come out worse than not denoising at all. A multiple of 16 keeps
12
+ // the frame on the network's pooling grid, so pixels away from the edges denoise as before.
13
+ const OIDN_BORDER = 16;
14
+
15
+ /** The buffer oidn-web reads: the frame inside a mirrored border, both sides a multiple of 16. */
16
+ function oidnInputSize( width, height ) {
17
+
18
+ const align = n => Math.ceil( ( n + 2 * OIDN_BORDER ) / OIDN_TILE_ALIGNMENT ) * OIDN_TILE_ALIGNMENT;
19
+ return { width: align( width ), height: align( height ) };
20
+
21
+ }
22
+
10
23
  let _initUNetFromBuffer = null;
11
24
  async function getInitUNetFromBuffer() {
12
25
 
@@ -42,11 +55,8 @@ const MODEL_CONFIG = {
42
55
  DEFAULT_OPTIONS: {
43
56
  enableOIDN: true,
44
57
  oidnQuality: 'fast',
45
- // A cap, not a fixed size — the effective tile is min( max( w, h ), this ), so a frame
46
- // that fits in one tile pays no overlap padding at all. Every tile otherwise runs at
47
- // tileSize + 2*overlap (96px, 112 for _large): at 1024²/high, 4x512 tiles measured
48
- // 422ms against 229ms for one 1024 tile. 1024 caps the one-time activation
49
- // allocation at ~430MB; beyond it larger frames tile and stay bounded.
58
+ // A cap on the frame, not a tile size: a frame no larger in area than this square is one
59
+ // tile, with no overlap. The area bounds the network's activation memory.
50
60
  tileSize: 1024
51
61
  }
52
62
  };
@@ -89,12 +99,13 @@ fn reduce( @builtin(global_invocation_id) gid: vec3<u32>,
89
99
  @builtin(local_invocation_id) lid: vec3<u32>,
90
100
  @builtin(workgroup_id) wid: vec3<u32> ) {
91
101
 
102
+ // x: frame pixels, y: frame width, z: buffer width, w: border. The border is not metered.
92
103
  let total = params.x;
93
104
  var sum = 0.0;
94
105
  var i = gid.x;
95
106
  loop {
96
107
  if ( i >= total ) { break; }
97
- let c = col[ i ].xyz;
108
+ let c = col[ ( i / params.y + params.w ) * params.z + i % params.y + params.w ].xyz;
98
109
  let lum = 0.212671 * c.r + 0.71516 * c.g + 0.072169 * c.b;
99
110
  sum = sum + log2( max( lum, 0.0 ) + 0.0001 );
100
111
  i = i + WG * GROUPS;
@@ -155,7 +166,7 @@ struct UnpackParams {
155
166
  tileY: u32,
156
167
  tileW: u32,
157
168
  tileH: u32,
158
- pad0: u32,
169
+ border: u32,
159
170
  pad1: u32,
160
171
  pad2: u32,
161
172
  };
@@ -173,7 +184,7 @@ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
173
184
 
174
185
  let px = P.tileX + gid.x;
175
186
  let py = P.tileY + gid.y;
176
- let si = py * P.srcWidth + px;
187
+ let si = ( py + P.border ) * P.srcWidth + px + P.border;
177
188
 
178
189
  // scaleBuf is the autoexposure factor the input was multiplied by; dividing it out returns
179
190
  // the renderer's units. Alpha rides through from the path tracer so a transparent background
@@ -186,13 +197,20 @@ const PACK_WG_SIZE = 8;
186
197
  const PACK_WGSL = /* wgsl */`
187
198
  @group(0) @binding(0) var src: texture_2d<f32>;
188
199
  @group(0) @binding(1) var<storage, read_write> dst: array<vec4<f32>>;
189
- @group(0) @binding(2) var<uniform> size: vec2<u32>;
200
+ @group(0) @binding(2) var<uniform> size: vec4<u32>; // buffer w, h; frame w, h
201
+
202
+ fn mirror( i: i32, n: i32 ) -> i32 {
203
+ let m = abs( i );
204
+ return clamp( select( m, 2 * n - 2 - m, m >= n ), 0, n - 1 );
205
+ }
190
206
 
191
207
  @compute @workgroup_size(${PACK_WG_SIZE}, ${PACK_WG_SIZE})
192
208
  fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
193
209
 
194
210
  if ( gid.x >= size.x || gid.y >= size.y ) { return; }
195
- dst[ gid.y * size.x + gid.x ] = textureLoad( src, gid.xy, 0 );
211
+ let p = vec2<i32>( gid.xy ) - ${OIDN_BORDER};
212
+ let s = vec2<i32>( mirror( p.x, i32( size.z ) ), mirror( p.y, i32( size.w ) ) );
213
+ dst[ gid.y * size.x + gid.x ] = textureLoad( src, s, 0 );
196
214
  }
197
215
  `;
198
216
 
@@ -223,8 +241,9 @@ export class OIDNDenoiser extends EventDispatcher {
223
241
 
224
242
  // Cached GPU storage buffers for texture→buffer copies (reused across denoise calls)
225
243
  this._gpuInputBuffers = { color: null, albedo: null, normal: null };
244
+ // The frame the buffers hold; they are allocated at oidnInputSize() of it.
226
245
  this._gpuInputBufferSize = { width: 0, height: 0 };
227
- // Packs widths copyTextureToBuffer cannot take; see _beginPack.
246
+ // Packs the frame and its mirrored border; see _beginPack.
228
247
  this._packPipeline = null;
229
248
  this._packLayout = null;
230
249
  this._packParams = null;
@@ -363,19 +382,15 @@ export class OIDNDenoiser extends EventDispatcher {
363
382
  */
364
383
  _resolveTileSize() {
365
384
 
366
- const longest = Math.max( this._renderWidth, this._renderHeight );
367
- if ( ! longest ) return this.maxTileSize;
385
+ if ( ! this._renderWidth || ! this._renderHeight ) return this.maxTileSize;
368
386
 
369
- // Rounded up to the model's 16-pixel alignment, not clamped to the exact frame. The
370
- // library's own fitTileDimension aligns the size up and then clamps it back to whatever
371
- // cap it is handed, so handing it the raw frame size hands it an unaligned tile — and an
372
- // unaligned tile measured 4x the cost of the whole denoise: 336 ms at 900x900 against
373
- // 86 ms once aligned, with 896x896 unaffected either way.
374
- const aligned = Math.ceil( longest / OIDN_TILE_ALIGNMENT ) * OIDN_TILE_ALIGNMENT;
375
- if ( aligned <= this.maxTileSize ) return aligned;
387
+ // One tile for any frame no larger in area than the square the cap allows: oidn-web's memory
388
+ // follows the tile's area, not its longest side, so a wide frame needs no split. The cap is on
389
+ // the frame, so the border never splits a frame that fits.
390
+ const input = oidnInputSize( this._renderWidth, this._renderHeight );
391
+ const largest = oidnInputSize( this.maxTileSize, this.maxTileSize );
392
+ if ( input.width * input.height <= largest.width * largest.height ) return Math.max( input.width, input.height );
376
393
 
377
- // The cap has to be aligned too, or a cap somebody picked by hand lands on the same slow
378
- // path. Rounded down, so it never rises above what the caller asked for.
379
394
  return Math.max( OIDN_TILE_ALIGNMENT, Math.floor( this.maxTileSize / OIDN_TILE_ALIGNMENT ) * OIDN_TILE_ALIGNMENT );
380
395
 
381
396
  }
@@ -470,7 +485,7 @@ export class OIDNDenoiser extends EventDispatcher {
470
485
 
471
486
  const params = this.backendParamsGetter();
472
487
  this.gpuDevice = params?.device ?? null;
473
- backendParams = params?.device ? params : undefined;
488
+ backendParams = params?.device ? { device: params.device } : undefined;
474
489
 
475
490
  }
476
491
 
@@ -648,8 +663,7 @@ export class OIDNDenoiser extends EventDispatcher {
648
663
 
649
664
  /**
650
665
  * GPU-native execution path. Copies render target textures into GPU storage buffers
651
- * via copyTextureToBuffer (GPU-only, no CPU roundtrip), then passes those buffers to
652
- * oidn-web's well-tested GPUBuffer path.
666
+ * (GPU-only, no CPU roundtrip), then passes those buffers to oidn-web's GPUBuffer path.
653
667
  *
654
668
  * Note: oidn-web's GPUTexture input path produces NaN outputs — using GPUBuffer instead.
655
669
  */
@@ -688,71 +702,55 @@ export class OIDNDenoiser extends EventDispatcher {
688
702
 
689
703
  // Autoexposure and the pre-multiply it drives, both on the GPU and both queued behind the
690
704
  // copies above. Nothing is awaited here — an await would drain the queue before the UNet.
691
- this._computeInputScale( device, width * height );
692
- this._applyColorScale( device, width * height );
705
+ const input = oidnInputSize( width, height );
706
+ this._computeInputScale( device, width, height );
707
+ this._applyColorScale( device, input.width * input.height );
693
708
 
694
709
  // A final denoise is shown tile by tile, so the tiles still to come must show this render
695
710
  // rather than the blank or previous-view picture the output holds between runs.
696
711
  if ( ! continuous && ! this._hasOutput ) {
697
712
 
698
- this._unpackToTexture( this._gpuInputBuffers.color, width, { x: 0, y: 0, width, height }, false );
713
+ this._unpackToTexture( this._gpuInputBuffers.color, { x: 0, y: 0, width, height }, false );
699
714
 
700
715
  }
701
716
 
702
- // Pass GPU storage buffers to oidn-web (GPUBuffer path, well-tested)
717
+ const image = data => ( { data, width: input.width, height: input.height } );
703
718
  const config = {
704
- color: { data: this._gpuInputBuffers.color, width, height },
705
- albedo: { data: this._gpuInputBuffers.albedo, width, height },
706
- normal: { data: this._gpuInputBuffers.normal, width, height }
719
+ color: image( this._gpuInputBuffers.color ),
720
+ albedo: image( this._gpuInputBuffers.albedo ),
721
+ normal: image( this._gpuInputBuffers.normal )
707
722
  };
708
723
 
709
724
  return this._executeWithAbortGPU( config, continuous );
710
725
 
711
726
  }
712
727
 
713
- /** Copies the render's colour, albedo and normal into the tightly packed buffers oidn-web reads. @private */
728
+ /** Copies the render's colour, albedo and normal into the buffers oidn-web reads, border included. @private */
714
729
  _copyInputs( device, textures, width, height ) {
715
730
 
716
- // copyTextureToBuffer takes only 256-byte-aligned rows; any other width packs on the GPU.
731
+ const input = oidnInputSize( width, height );
717
732
  const encoder = device.createCommandEncoder( { label: 'oidn-tex-to-buf' } );
718
- const tightRowBytes = width * 16; // rgba32float
719
- const pass = tightRowBytes % 256 === 0 ? null : this._beginPack( device, encoder, width, height );
720
-
721
- const copyTex = ( tex, tightBuf ) => {
733
+ const pass = this._beginPack( device, encoder, input, width, height );
722
734
 
723
- if ( pass ) {
724
-
725
- pass.setBindGroup( 0, device.createBindGroup( {
726
- layout: this._packLayout,
727
- entries: [
728
- { binding: 0, resource: tex.createView() },
729
- { binding: 1, resource: { buffer: tightBuf } },
730
- { binding: 2, resource: { buffer: this._packParams } },
731
- ],
732
- } ) );
733
- pass.dispatchWorkgroups( Math.ceil( width / PACK_WG_SIZE ), Math.ceil( height / PACK_WG_SIZE ) );
734
-
735
- } else {
736
-
737
- encoder.copyTextureToBuffer(
738
- { texture: tex, mipLevel: 0 },
739
- { buffer: tightBuf, offset: 0, bytesPerRow: tightRowBytes, rowsPerImage: height },
740
- { width, height, depthOrArrayLayers: 1 }
741
- );
742
-
743
- }
744
-
745
- };
746
-
747
- copyTex( textures.color, this._gpuInputBuffers.color );
748
- copyTex( textures.albedo, this._gpuInputBuffers.albedo );
749
735
  // The normal stays [0,1]-encoded. This contradicts OIDN's API ("must be in the [-1,1]
750
736
  // range") but is right for this port: OIDN's own getNormal (cpu_input_process.isph) does
751
737
  // `value*0.5+0.5` before the network, and oidn-web omits that remap. Decoding to [-1,1]
752
738
  // measured 0.878 -> 2.439 denoise ratio at 64 spp. Do not "fix" it.
753
- copyTex( textures.normal, this._gpuInputBuffers.normal );
739
+ for ( const name of [ 'color', 'albedo', 'normal' ] ) {
740
+
741
+ pass.setBindGroup( 0, device.createBindGroup( {
742
+ layout: this._packLayout,
743
+ entries: [
744
+ { binding: 0, resource: textures[ name ].createView() },
745
+ { binding: 1, resource: { buffer: this._gpuInputBuffers[ name ] } },
746
+ { binding: 2, resource: { buffer: this._packParams } },
747
+ ],
748
+ } ) );
749
+ pass.dispatchWorkgroups( Math.ceil( input.width / PACK_WG_SIZE ), Math.ceil( input.height / PACK_WG_SIZE ) );
754
750
 
755
- pass?.end();
751
+ }
752
+
753
+ pass.end();
756
754
  device.queue.submit( [ encoder.finish() ] );
757
755
 
758
756
  }
@@ -760,7 +758,6 @@ export class OIDNDenoiser extends EventDispatcher {
760
758
  /**
761
759
  * Creates or recreates the GPU storage buffers used as oidn-web inputs.
762
760
  * Reuses existing buffers if the resolution hasn't changed.
763
- * Usage: COPY_DST (for copyTextureToBuffer) | STORAGE (for oidn-web WGSL read) | COPY_SRC
764
761
  */
765
762
  _ensureGPUInputBuffers( width, height ) {
766
763
 
@@ -771,7 +768,8 @@ export class OIDNDenoiser extends EventDispatcher {
771
768
  this._destroyGPUInputBuffers();
772
769
 
773
770
  const device = this.gpuDevice;
774
- const byteSize = width * height * 16; // rgba32float, tightly packed for oidn-web
771
+ const input = oidnInputSize( width, height );
772
+ const byteSize = input.width * input.height * 16; // rgba32float
775
773
  const usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC;
776
774
 
777
775
  this._gpuInputBuffers.color = device.createBuffer( { label: 'oidn-in-color', size: byteSize, usage } );
@@ -781,8 +779,8 @@ export class OIDNDenoiser extends EventDispatcher {
781
779
 
782
780
  }
783
781
 
784
- /** Opens the compute pass that packs unaligned rows; one dispatch per texture follows. @private */
785
- _beginPack( device, encoder, width, height ) {
782
+ /** Opens the compute pass that packs a frame into its bordered buffer; one dispatch per texture follows. @private */
783
+ _beginPack( device, encoder, input, width, height ) {
786
784
 
787
785
  if ( ! this._packPipeline ) {
788
786
 
@@ -803,12 +801,8 @@ export class OIDNDenoiser extends EventDispatcher {
803
801
 
804
802
  }
805
803
 
806
- if ( ! this._packParams ) {
807
-
808
- this._packParams = device.createBuffer( { label: 'oidn-pack-size', size: 8, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST } );
809
- device.queue.writeBuffer( this._packParams, 0, new Uint32Array( [ width, height ] ) );
810
-
811
- }
804
+ this._packParams ??= device.createBuffer( { label: 'oidn-pack-size', size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST } );
805
+ device.queue.writeBuffer( this._packParams, 0, new Uint32Array( [ input.width, input.height, width, height ] ) );
812
806
 
813
807
  const pass = encoder.beginComputePass( { label: 'oidn-pack' } );
814
808
  pass.setPipeline( this._packPipeline );
@@ -953,7 +947,7 @@ export class OIDNDenoiser extends EventDispatcher {
953
947
  }
954
948
 
955
949
  /** Writes OIDN's autoexposure scale into `_inputScaleBuffer`. Never read back. */
956
- _computeInputScale( device, pixelCount ) {
950
+ _computeInputScale( device, width, height ) {
957
951
 
958
952
  if ( ! this._ensureScalePipelines( device ) ) return;
959
953
 
@@ -964,7 +958,8 @@ export class OIDNDenoiser extends EventDispatcher {
964
958
 
965
959
  }
966
960
 
967
- device.queue.writeBuffer( this._lumParams, 0, new Uint32Array( [ pixelCount, 0, 0, 0 ] ) );
961
+ const input = oidnInputSize( width, height );
962
+ device.queue.writeBuffer( this._lumParams, 0, new Uint32Array( [ width * height, width, input.width, OIDN_BORDER ] ) );
968
963
 
969
964
  const encoder = device.createCommandEncoder( { label: 'oidn-autoexposure' } );
970
965
 
@@ -1078,12 +1073,11 @@ export class OIDNDenoiser extends EventDispatcher {
1078
1073
  /**
1079
1074
  * Writes a rectangle of the denoised buffer into the output picture, on the card.
1080
1075
  *
1081
- * @param {GPUBuffer} src - rgba32float, full image, scaled by the input autoexposure
1082
- * @param {number} srcWidth
1083
- * @param {{x: number, y: number, width: number, height: number}} rect
1076
+ * @param {GPUBuffer} src - rgba32float, the bordered input size, scaled by the input autoexposure
1077
+ * @param {{x: number, y: number, width: number, height: number}} rect - in frame pixels
1084
1078
  * @param {boolean} [denoised=true] - false for the raw render laid under a tiled run
1085
1079
  */
1086
- _unpackToTexture( src, srcWidth, rect, denoised = true ) {
1080
+ _unpackToTexture( src, rect, denoised = true ) {
1087
1081
 
1088
1082
  const device = this.gpuDevice;
1089
1083
  if ( ! device || ! this._ensureScalePipelines( device ) ) return;
@@ -1101,7 +1095,8 @@ export class OIDNDenoiser extends EventDispatcher {
1101
1095
 
1102
1096
  try {
1103
1097
 
1104
- device.queue.writeBuffer( params, 0, new Uint32Array( [ srcWidth, x, y, width, height, 0, 0, 0 ] ) );
1098
+ const srcWidth = oidnInputSize( this._gpuInputBufferSize.width, this._gpuInputBufferSize.height ).width;
1099
+ device.queue.writeBuffer( params, 0, new Uint32Array( [ srcWidth, x, y, width, height, OIDN_BORDER, 0, 0 ] ) );
1105
1100
 
1106
1101
  const bindGroup = device.createBindGroup( {
1107
1102
  label: 'oidn-output-unpack',
@@ -1174,13 +1169,23 @@ export class OIDNDenoiser extends EventDispatcher {
1174
1169
 
1175
1170
  };
1176
1171
 
1177
- this.state.abortController.signal.addEventListener( 'abort', abortHandler, { once: true } );
1172
+ const signal = this.state.abortController.signal;
1173
+ signal.addEventListener( 'abort', abortHandler, { once: true } );
1178
1174
 
1179
1175
  abortDenoise = this.unet.tileExecute( {
1180
1176
  ...config,
1177
+ // Refreshes share the GPU with the render; the closing denoise has it to itself.
1178
+ scheduling: continuous ? 'animation-frame' : 'event-loop',
1179
+ error: ( reason ) => {
1180
+
1181
+ signal.removeEventListener( 'abort', abortHandler );
1182
+ abortDenoise = null;
1183
+ reject( reason );
1184
+
1185
+ },
1181
1186
  done: async ( output ) => {
1182
1187
 
1183
- this.state.abortController.signal.removeEventListener( 'abort', abortHandler );
1188
+ signal.removeEventListener( 'abort', abortHandler );
1184
1189
  abortDenoise = null;
1185
1190
 
1186
1191
  try {
@@ -1203,28 +1208,28 @@ export class OIDNDenoiser extends EventDispatcher {
1203
1208
  progress: ( outputData, _tileData, tile ) => {
1204
1209
 
1205
1210
  // oidn-web GPU path: tileData is null, but outputData holds the assembled
1206
- // full-image buffer updated after each tile. The pack pass reads that tile's
1211
+ // full-image buffer updated after each tile. The unpack pass reads that tile's
1207
1212
  // rectangle straight out of it, so there are no row-by-row buffer copies.
1208
1213
  if ( ! outputData?.data || ! tile ) return;
1209
1214
 
1210
- const fullWidth = outputData.width;
1211
- const fullHeight = outputData.height;
1212
-
1213
- // Clamp tile to image bounds (edge tiles may extend past the image)
1214
- const width = Math.min( tile.width, fullWidth - tile.x );
1215
- const height = Math.min( tile.height, fullHeight - tile.y );
1215
+ // The tile in frame pixels, without the border.
1216
+ const { width: frameWidth, height: frameHeight } = this._gpuInputBufferSize;
1217
+ const x = Math.max( tile.x - OIDN_BORDER, 0 );
1218
+ const y = Math.max( tile.y - OIDN_BORDER, 0 );
1219
+ const width = Math.min( tile.x + tile.width - OIDN_BORDER, frameWidth ) - x;
1220
+ const height = Math.min( tile.y + tile.height - OIDN_BORDER, frameHeight ) - y;
1216
1221
  if ( width <= 0 || height <= 0 ) return;
1217
1222
 
1218
- const rect = { x: tile.x, y: tile.y, width, height };
1223
+ const rect = { x, y, width, height };
1219
1224
 
1220
- this._unpackToTexture( outputData.data, fullWidth, rect );
1225
+ this._unpackToTexture( outputData.data, rect );
1221
1226
  this._tilesWritten ++;
1222
1227
 
1223
1228
  this.dispatchEvent( {
1224
1229
  type: 'tileProgress',
1225
1230
  tile: rect,
1226
- imageWidth: fullWidth,
1227
- imageHeight: fullHeight,
1231
+ imageWidth: frameWidth,
1232
+ imageHeight: frameHeight,
1228
1233
  continuous
1229
1234
  } );
1230
1235
 
@@ -1237,9 +1242,9 @@ export class OIDNDenoiser extends EventDispatcher {
1237
1242
 
1238
1243
  /**
1239
1244
  * Degenerate fallback when no per-tile progress was emitted: one authoritative full paint.
1240
- * @param {{ data: GPUBuffer, width: number, height: number }} output
1245
+ * @param {{ data: GPUBuffer }} output
1241
1246
  */
1242
- _displayGPUOutput( { data: gpuBuffer, width, height } ) {
1247
+ _displayGPUOutput( { data: gpuBuffer } ) {
1243
1248
 
1244
1249
  if ( ! this.gpuDevice ) {
1245
1250
 
@@ -1248,7 +1253,8 @@ export class OIDNDenoiser extends EventDispatcher {
1248
1253
 
1249
1254
  }
1250
1255
 
1251
- this._unpackToTexture( gpuBuffer, width, { x: 0, y: 0, width, height } );
1256
+ const { width, height } = this._gpuInputBufferSize;
1257
+ this._unpackToTexture( gpuBuffer, { x: 0, y: 0, width, height } );
1252
1258
 
1253
1259
  }
1254
1260
 
@@ -2,11 +2,7 @@ import { Box3, Vector3 } from 'three';
2
2
  import { RayzeeRenderer, describeAdapter, RENDER_CHECKPOINT_VERSION } from './RayzeeRenderer.js';
3
3
  import { NormalDepth } from './Stages/NormalDepth.js';
4
4
  import { MotionVector } from './Stages/MotionVector.js';
5
- import { ASVGF } from './Stages/ASVGF.js';
6
5
  import { NRD } from './Stages/NRD.js';
7
- import { Variance } from './Stages/Variance.js';
8
- import { BilateralFilter } from './Stages/BilateralFilter.js';
9
- import { EdgeFilter } from './Stages/EdgeFilter.js';
10
6
  import { AutoExposure, AUTO_EXPOSURE_DEFAULTS } from './Stages/AutoExposure.js';
11
7
  import { LocalExposure, LOCAL_EXPOSURE_DEFAULTS } from './Stages/LocalExposure.js';
12
8
  import { PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, modePresetSettings } from './EngineDefaults.js';
@@ -151,17 +147,12 @@ export class PathTracerApp extends RayzeeRenderer {
151
147
  const { renderer, stages } = this;
152
148
  stages.normalDepth = new NormalDepth( renderer, { pathTracer: stages.pathTracer } );
153
149
  stages.motionVector = new MotionVector( renderer, this.camera, { pathTracer: stages.pathTracer } );
154
- stages.asvgf = new ASVGF( renderer, { enabled: false } );
155
150
  stages.nrd = new NRD( renderer, { enabled: false, pathTracer: stages.pathTracer } );
156
- stages.variance = new Variance( renderer, { enabled: false } );
157
- stages.bilateralFilter = new BilateralFilter( renderer, { enabled: false } );
158
- stages.edgeFilter = new EdgeFilter( renderer, { enabled: false } );
159
151
  stages.autoExposure = new AutoExposure( renderer, { enabled: AUTO_EXPOSURE_DEFAULTS.autoExposure } );
160
152
  stages.localExposure = new LocalExposure( renderer, { enabled: LOCAL_EXPOSURE_DEFAULTS.localExposure } );
161
153
 
162
154
  return [
163
- stages.normalDepth, stages.motionVector, stages.nrd, stages.asvgf,
164
- stages.variance, stages.bilateralFilter, stages.edgeFilter, stages.autoExposure, stages.localExposure,
155
+ stages.normalDepth, stages.motionVector, stages.nrd, stages.autoExposure, stages.localExposure,
165
156
  ];
166
157
 
167
158
  }
@@ -252,7 +243,7 @@ export class PathTracerApp extends RayzeeRenderer {
252
243
  this._addTrackedListener( this.timeline, EngineEvents.TIMELINE_CHANGED, ( e ) => this.dispatchEvent( e ) );
253
244
 
254
245
  this._forwardEvents( this.denoisingManager, [
255
- EngineEvents.DENOISING_START, EngineEvents.DENOISING_END,
246
+ EngineEvents.DENOISING_START, EngineEvents.DENOISING_END, EngineEvents.DENOISER_LOADING,
256
247
  EngineEvents.UPSCALING_START, EngineEvents.UPSCALING_PROGRESS, EngineEvents.UPSCALING_END,
257
248
  EngineEvents.RESOLUTION_CHANGED,
258
249
  ] );
@@ -318,7 +309,7 @@ export class PathTracerApp extends RayzeeRenderer {
318
309
 
319
310
  _displaySources() {
320
311
 
321
- return [ 'oidn:output', 'edgeFiltering:output', 'bilateralFiltering:output', 'asvgf:output', 'nrd:output' ];
312
+ return [ 'oidn:output', 'nrd:output' ];
322
313
 
323
314
  }
324
315
 
@@ -338,8 +329,10 @@ export class PathTracerApp extends RayzeeRenderer {
338
329
 
339
330
  this.cameraManager.applyProjection( value );
340
331
  // MotionVector unprojects through projectionMatrixInverse, which is meaningless once every pixel is its
341
- // own direction: fall back to the spatial-only denoiser rather than leaving no strategy.
342
- if ( value === 'equirectangular' && this.denoisingManager?.requiresMotionVectors ) this.denoisingManager.setDenoiserStrategy( 'edgeaware' );
332
+ // own direction: hand the view to OIDN, which needs none, rather than leaving no denoiser.
333
+ if ( value === 'equirectangular' && this.denoisingManager?.requiresMotionVectors ) this.denoisingManager.setDenoiserStrategy( 'oidn' );
334
+ // OIDN's motion history is off in a panorama and back on outside one.
335
+ else this.denoisingManager?._syncGBufferStages?.();
343
336
 
344
337
  },
345
338
  };
@@ -844,7 +837,7 @@ export class PathTracerApp extends RayzeeRenderer {
844
837
 
845
838
  }
846
839
 
847
- // OIDN as the live denoiser rebuilds its network on every size change, so it keeps full size.
840
+ // Live OIDN keeps full size: a resize drops its motion history and shows the raw render until it denoises again.
848
841
  _interactionRenderScale() {
849
842
 
850
843
  if ( this.denoisingManager?.continuousDenoise ) return 1;
@@ -1106,11 +1099,7 @@ export class PathTracerApp extends RayzeeRenderer {
1106
1099
  pathTracer: this.stages.pathTracer,
1107
1100
  normalDepth: this.stages.normalDepth,
1108
1101
  motionVector: this.stages.motionVector,
1109
- asvgf: this.stages.asvgf,
1110
1102
  nrd: this.stages.nrd,
1111
- variance: this.stages.variance,
1112
- bilateralFilter: this.stages.bilateralFilter,
1113
- edgeFilter: this.stages.edgeFilter,
1114
1103
  autoExposure: this.stages.autoExposure,
1115
1104
  compositor: this.stages.compositor,
1116
1105
  },
@@ -3,13 +3,12 @@
3
3
  *
4
4
  * Two TextureNodes still holding the default EmptyTexture when a stage's kernel is first
5
5
  * compiled can share one GPU binding, which then resolves to whichever is assigned last.
6
- * Nothing throws — the aliased node reads a real texture, just not its own. BilateralFilter
7
- * shipped this way, reading ASVGF's history counter as its variance.
6
+ * Nothing throws — the aliased node reads a real texture, just not its own.
8
7
  *
9
8
  * Rule enforced: every TextureNode a kernel reads must hold its real texture before that
10
9
  * kernel is first dispatched. StorageTexture-typed nodes are exempt — `textureLoad` codegen
11
10
  * only emits the required `level` parameter while the node still holds EmptyTexture, so those
12
- * are deliberately bound after compile (see ASVGF.render).
11
+ * are deliberately bound after compile.
13
12
  *
14
13
  * Off by default; the bench harness turns it on.
15
14
  */
@@ -43,7 +43,6 @@ export class PipelineContext {
43
43
 
44
44
  // Render modes
45
45
  renderMode: 0, // 0 = interactive, 1 = production (full-frame in both)
46
- interactionMode: false,
47
46
  isComplete: false,
48
47
 
49
48
  // Tile rendering
@@ -23,8 +23,7 @@ import { ISSUE_CODES } from '../EngineIssues.js';
23
23
  *
24
24
  * // Add stages in execution order
25
25
  * pipeline.addStage(new PathTracer(...));
26
- * pipeline.addStage(new ASVGF(...));
27
- * pipeline.addStage(new EdgeFilter(...));
26
+ * pipeline.addStage(new NRD(...));
28
27
  *
29
28
  * // Render all stages
30
29
  * pipeline.render(writeBuffer);
@@ -12,7 +12,7 @@ export const StageExecutionMode = {
12
12
 
13
13
  /**
14
14
  * PER_CYCLE - Execute only when tile rendering cycle completes
15
- * Use for: Post-processing, denoisers, filters (ASVGF, EdgeAwareFiltering, OIDN)
15
+ * Use for: Post-processing, denoisers, filters (NRD, OIDN)
16
16
  *
17
17
  * Complete cycle means:
18
18
  * - Progressive mode (renderMode=0): Every frame
@@ -424,7 +424,7 @@ export const writeMediumStack = ( buf, id, stackDepth, transTraversals, ior1, io
424
424
  ), ior1, ior2, ior3 ) );
425
425
 
426
426
  // Region 7: Beer-Lambert sigmaA (.xyz) of the active medium + DDFA feature-throughput (.w).
427
- // .xyz absorption is gated on stackDepth>0; .w carries the OIDN/ASVGF see-through aux tint (see below).
427
+ // .xyz absorption is gated on stackDepth>0; .w carries the OIDN/NRD see-through aux tint (see below).
428
428
  // Both writers RMW-preserve the other half so sigmaA (live in glass) and featTP never clobber each other.
429
429
  export const readMediumSigmaA = ( buf, id ) => buf.element( soa( id, RAY.MEDIUM_SIGMA_A ) ).xyz;
430
430
 
@@ -436,7 +436,7 @@ export const writeMediumSigmaA = ( buf, id, sigmaA ) => {
436
436
  };
437
437
 
438
438
  // DDFA (Deferred Denoising-Feature Aux) feature-throughput: a clean per-ray tint carried through smooth
439
- // glass/mirror, committed into the OIDN/ASVGF albedo guide at the first diffuse-enough surface. Rides the
439
+ // glass/mirror, committed into the OIDN/NRD albedo guide at the first diffuse-enough surface. Rides the
440
440
  // otherwise-dead MEDIUM_SIGMA_A.w, packed 10/10/10 into 30 bits (top 2 bits zero ⇒ the float bit-pattern is
441
441
  // always finite — never NaN/Inf). LDR [0,1] per channel; final albedo re-quantizes to unorm16 anyway.
442
442
  const pack30 = ( tp ) => {
@@ -7,7 +7,7 @@
7
7
  * - 1 MRT RenderTarget (3 attachments) used as the readable copy
8
8
  * - After each compute dispatch, copyTextureToTexture() transfers data
9
9
  * from StorageTextures → RenderTarget textures
10
- * - Downstream stages (ASVGF, display, OIDN) read from the RenderTarget
10
+ * - Downstream stages (NRD, display, OIDN) read from the RenderTarget
11
11
  * - Previous-frame reads in the path tracer sample from RenderTarget textures
12
12
  * via texture() (not storageTexture().toReadOnly())
13
13
  */
@@ -14,6 +14,23 @@ import { RenderTarget, FloatType, RGBAFormat, NearestFilter } from 'three';
14
14
  import { QuadMesh, NodeMaterial } from 'three/webgpu';
15
15
  import { texture as tslTexture, screenCoordinate, ivec2 } from 'three/tsl';
16
16
 
17
+ /**
18
+ * An RGBA render target's pixels without the 256-byte row padding WebGPU copies them with.
19
+ * @returns {Promise<TypedArray>} `width * height * 4`, top row first
20
+ */
21
+ export async function readPixels( renderer, target, width, height, index = 0 ) {
22
+
23
+ const data = await renderer.readRenderTargetPixelsAsync( target, 0, 0, width, height, index );
24
+ const row = width * 4;
25
+ const stride = Math.ceil( row * data.BYTES_PER_ELEMENT / 256 ) * 256 / data.BYTES_PER_ELEMENT;
26
+ if ( stride === row ) return data;
27
+
28
+ const tight = new data.constructor( row * height );
29
+ for ( let y = 0; y < height; y ++ ) tight.set( data.subarray( y * stride, y * stride + row ), y * row );
30
+ return tight;
31
+
32
+ }
33
+
17
34
  export class TextureReadback {
18
35
 
19
36
  constructor( renderer ) {
@@ -48,7 +65,7 @@ export class TextureReadback {
48
65
 
49
66
  try {
50
67
 
51
- return await this.renderer.readRenderTargetPixelsAsync( this.draw( source, width, height ), 0, 0, width, height );
68
+ return await readPixels( this.renderer, this.draw( source, width, height ), width, height );
52
69
 
53
70
  } finally {
54
71