rayzee 7.15.2 → 7.17.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.
@@ -404,6 +404,29 @@ export class Variance extends RenderStage {
404
404
 
405
405
  }
406
406
 
407
+ // Reserved-storage change (e.g. 4K toggle): recreate the MAX-preallocated StorageTextures at the new live
408
+ // reserved size + rebuild the compute nodes in place. Same stage object, so refs/wiring stay valid.
409
+ reallocateReservedStorage() {
410
+
411
+ this._computeNodeA?.dispose();
412
+ this._computeNodeB?.dispose();
413
+ this._storageTexA?.dispose();
414
+ this._storageTexB?.dispose();
415
+ const mk = () => {
416
+
417
+ const t = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
418
+ t.type = FloatType; t.format = RGBAFormat; t.minFilter = LinearFilter; t.magFilter = LinearFilter;
419
+ return t;
420
+
421
+ };
422
+
423
+ this._storageTexA = mk();
424
+ this._storageTexB = mk();
425
+ this._buildCompute();
426
+ this._compiled = false;
427
+
428
+ }
429
+
407
430
  dispose() {
408
431
 
409
432
  this._computeNodeA?.dispose();
@@ -1,5 +1,10 @@
1
1
  /**
2
2
  * FinalWriteKernel.js — wavefront final output: temporal accumulation + MRT StorageTexture writes (16×16, 2D).
3
+ *
4
+ * Chunked path pool (docs/internal/specs/wavefront-chunked-pool.md): runs 2D over one row band
5
+ * (renderWidth × chunkRows). A thread's band-local row `localGy` maps to global row `gy = chunkRowBase + localGy`.
6
+ * Path state + the per-chunk G-buffer are read at the LOCAL slot `rayID = localGy·W + gx`; the persistent
7
+ * per-pixel buffers (m2/streak/frozenMask) and the output texture use the GLOBAL pixel `p = gy·W + gx`.
3
8
  */
4
9
 
5
10
  import {
@@ -35,7 +40,8 @@ export function buildFinalWriteKernel( params ) {
35
40
  enableAccumulation, hasPreviousAccumulated, accumulationAlpha, cameraIsMoving,
36
41
  transparentBackground,
37
42
  prevAccumTexture, prevAlbedoTexture, prevNormalDepthTexture,
38
- renderWidth, renderHeight,
43
+ renderWidth,
44
+ chunkRowBase, chunkRows, // row band offset (global first row) + row count for this chunk
39
45
  visMode,
40
46
  // Aux MRT (normalDepth + albedo) feeds only the denoiser/OIDN. Gated by a live uniform (1 = denoiser
41
47
  // on): when off, skip the G-buffer decode, the prev-frame aux mix, and the two aux stores.
@@ -59,12 +65,13 @@ export function buildFinalWriteKernel( params ) {
59
65
  const computeFn = Fn( () => {
60
66
 
61
67
  const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
62
- const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
68
+ const localGy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
63
69
 
64
- If( gx.lessThan( renderWidth ).and( gy.lessThan( renderHeight ) ), () => {
70
+ If( gx.lessThan( renderWidth ).and( localGy.lessThan( chunkRows ) ), () => {
65
71
 
66
- const pixelIndex = gy.mul( int( resolution.x ) ).add( gx );
67
- const rayID = uint( pixelIndex );
72
+ const gy = localGy.add( chunkRowBase ); // GLOBAL row
73
+ const rayID = uint( localGy.mul( renderWidth ).add( gx ) ); // LOCAL path/gBuffer slot
74
+ const pixelId = uint( gy.mul( renderWidth ).add( gx ) ); // GLOBAL pixel (persistent buffers + texture)
68
75
 
69
76
  const sampleColor = readRayRadiance( rayBufferRO, rayID );
70
77
  const finalColor = sampleColor.xyz.toVar();
@@ -73,9 +80,9 @@ export function buildFinalWriteKernel( params ) {
73
80
  // A pixel frozen in a prior frame was skipped by Generate, so its rayBuffer sample is stale — pass
74
81
  // the accumulated colour through unchanged (below) instead of mixing it. Inert unless adaptiveOn.
75
82
  const wasFrozen = adaptiveOn.and( frame.greaterThan( uint( 0 ) ) )
76
- .and( frozenMaskRO.element( rayID ).equal( uint( 1 ) ) ).toVar();
83
+ .and( frozenMaskRO.element( pixelId ).equal( uint( 1 ) ) ).toVar();
77
84
 
78
- // MRT comes from the per-pixel G-buffer (rayID == pixelIndex). Half-packed: decode.
85
+ // MRT comes from the per-chunk G-buffer (LOCAL slot). Half-packed: decode.
79
86
  // auxOn gates the decode + stores so a no-denoiser frame does no G-buffer read and no aux writes.
80
87
  const finalNormalDepth = vec4( 0.0 ).toVar();
81
88
  const finalAlbedo = vec4( 0.0 ).xyz.toVar();
@@ -140,9 +147,9 @@ export function buildFinalWriteKernel( params ) {
140
147
  If( convOn, () => {
141
148
 
142
149
  const sampleLum = luminance( sampleColor.xyz );
143
- const prevM2 = m2BufferRW.element( rayID ).toVar();
150
+ const prevM2 = m2BufferRW.element( pixelId ).toVar();
144
151
  const m2 = mix( prevM2, sampleLum.mul( sampleLum ), accumulationAlpha ).toVar();
145
- m2BufferRW.element( rayID ).assign( m2 );
152
+ m2BufferRW.element( pixelId ).assign( m2 );
146
153
 
147
154
  const meanLum = luminance( finalColor ).toVar();
148
155
  const sampleVar = m2.sub( meanLum.mul( meanLum ) ).max( float( 0 ) );
@@ -168,7 +175,7 @@ export function buildFinalWriteKernel( params ) {
168
175
  If( frame.equal( uint( 0 ) ), () => {
169
176
 
170
177
  // Frame 0 (post-reset/camera-move) clears the frozen state so each run re-freezes fresh.
171
- streakBufferRW.element( rayID ).assign( uint( 0 ) );
178
+ streakBufferRW.element( pixelId ).assign( uint( 0 ) );
172
179
 
173
180
  } );
174
181
 
@@ -183,8 +190,8 @@ export function buildFinalWriteKernel( params ) {
183
190
 
184
191
  const freezeCandidate = frame.greaterThanEqual( uint( adaptiveMinSamples ) )
185
192
  .and( relErr.lessThan( pixelFreezeThreshold ) );
186
- const newStreak = select( freezeCandidate, streakBufferRW.element( rayID ).add( uint( 1 ) ), uint( 0 ) ).toVar();
187
- streakBufferRW.element( rayID ).assign( newStreak );
193
+ const newStreak = select( freezeCandidate, streakBufferRW.element( pixelId ).add( uint( 1 ) ), uint( 0 ) ).toVar();
194
+ streakBufferRW.element( pixelId ).assign( newStreak );
188
195
  // Just reached K → frozen next frame; count it now so the frozen population is current.
189
196
  If( newStreak.greaterThanEqual( uint( pixelFreezeStability ) ), () => {
190
197
 
@@ -2,6 +2,12 @@
2
2
  * GenerateKernel.js — wavefront primary ray generation.
3
3
  * Two dispatch modes from one builder: 2D screen-space (default) and 1D list-driven over the active-pixel
4
4
  * list (Tier-2 per-pixel freeze).
5
+ *
6
+ * Chunked path pool (docs/internal/specs/wavefront-chunked-pool.md): the image is streamed through a fixed
7
+ * device-budget pool in row bands. A kernel thread maps to a LOCAL path slot `rayID` (∈ [0,B)); the global
8
+ * pixel is `pixelBase + rayID` where `pixelBase = chunkRowBase · renderWidth`. Path state / gBuffer are
9
+ * written at the LOCAL slot; camera ray + RNG seed use the GLOBAL pixel coord (so a pixel's sample sequence
10
+ * is stable across chunks and frames).
5
11
  */
6
12
 
7
13
  import {
@@ -22,7 +28,7 @@ import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
22
28
  import {
23
29
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf,
24
30
  writeRayRadiance, writeGBuffer,
25
- writeMediumStack,
31
+ writeMediumStack, writeFeatureThroughput,
26
32
  } from '../Processor/PackedRayBuffer.js';
27
33
 
28
34
  const WG_SIZE = 16;
@@ -34,23 +40,21 @@ export function buildGenerateKernel( params ) {
34
40
  resolution, frame,
35
41
  cameraWorldMatrix, cameraProjectionMatrixInverse,
36
42
  enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
37
- renderWidth, renderHeight,
43
+ renderWidth,
44
+ chunkRowBase, chunkRows, // row band offset (global first row) + row count for this chunk
38
45
  transmissiveBounces, // per-ray refraction budget (megakernel parity: PathTracerCore.js:606)
39
46
  transparentBackground, // alpha inits to 1 here (megakernel parity: PathTracerCore.js:554) — env-escape-without-opaque zeroes it in Shade
40
47
  auxGBufferEnabled, // live uniform: 1 = init the per-pixel G-buffer (denoiser on), 0 = skip it
41
- // listDriven: 1D dispatch over the active-pixel list (activeIndicesRO[tid] = pixelID) instead of 2D.
48
+ // listDriven: 1D dispatch over the active-pixel list (activeIndicesRO[tid] = LOCAL slot) instead of 2D.
42
49
  listDriven = false, activeIndicesRO = null, counters = null,
43
50
  } = params;
44
51
 
45
52
  const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
46
53
 
47
- // Shared ray-gen body for a pixel (gx, gy) — identical in both dispatch modes.
48
- const emitRay = ( gx, gy ) => {
54
+ // Shared ray-gen body. (gx, gy) is the GLOBAL pixel; rayID is the LOCAL path-slot (both dispatch modes).
55
+ const emitRay = ( gx, gy, rayID ) => {
49
56
 
50
57
  const pixelCoord = vec2( float( gx ).add( 0.5 ), float( gy ).add( 0.5 ) );
51
- const pixelIndex = gy.mul( int( resolution.x ) ).add( gx );
52
- // One ray per pixel: rayID is the pixel index.
53
- const rayID = uint( pixelIndex );
54
58
 
55
59
  const screenPosition = pixelCoord.div( resolution ).mul( 2.0 ).sub( 1.0 ).toVar();
56
60
  screenPosition.y.assign( screenPosition.y.negate() );
@@ -88,8 +92,10 @@ export function buildGenerateKernel( params ) {
88
92
 
89
93
  If( auxOn, () => {
90
94
 
91
- // default: normal +Z, depth 1 (far), black albedo (background/miss)
92
- writeGBuffer( gBufferRW, uint( pixelIndex ), vec3( 0.0, 0.0, 1.0 ), float( 1.0 ), vec3( 0.0 ) );
95
+ // default: normal +Z, depth 1 (far), black albedo (background/miss). Per-CHUNK G-buffer → LOCAL slot.
96
+ writeGBuffer( gBufferRW, rayID, vec3( 0.0, 0.0, 1.0 ), float( 1.0 ), vec3( 0.0 ) );
97
+ // DDFA: seed the see-through aux tint to white (no tint yet). RMW preserves slot-5 xyz (sigmaA).
98
+ writeFeatureThroughput( rayBufferRW, rayID, vec3( 1.0 ) );
93
99
 
94
100
  } );
95
101
 
@@ -107,21 +113,26 @@ export function buildGenerateKernel( params ) {
107
113
  // Grid is over-sized from a stale readback; bound on the live ENTERING_COUNT so extra threads no-op.
108
114
  If( tid.lessThan( atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) ) ), () => {
109
115
 
110
- const pixelIndex = int( activeIndicesRO.element( tid ) );
111
- const gx = pixelIndex.mod( int( resolution.x ) );
112
- const gy = pixelIndex.div( int( resolution.x ) );
113
- emitRay( gx, gy );
116
+ // active list holds LOCAL slots; global pixel = pixelBase + local slot.
117
+ const localSlot = int( activeIndicesRO.element( tid ) );
118
+ const globalPixel = localSlot.add( chunkRowBase.mul( renderWidth ) );
119
+ const gx = globalPixel.mod( renderWidth );
120
+ const gy = globalPixel.div( renderWidth );
121
+ emitRay( gx, gy, uint( localSlot ) );
114
122
 
115
123
  } );
116
124
 
117
125
  } else {
118
126
 
127
+ // 2D grid over (renderWidth × chunkRows). local_gy is the band-local row; global gy adds chunkRowBase.
119
128
  const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
120
- const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
129
+ const localGy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
121
130
 
122
- If( gx.lessThan( renderWidth ).and( gy.lessThan( renderHeight ) ), () => {
131
+ If( gx.lessThan( renderWidth ).and( localGy.lessThan( chunkRows ) ), () => {
123
132
 
124
- emitRay( gx, gy );
133
+ const gy = localGy.add( chunkRowBase );
134
+ const localRayID = uint( localGy.mul( renderWidth ).add( gx ) );
135
+ emitRay( gx, gy, localRayID );
125
136
 
126
137
  } );
127
138
 
@@ -7,7 +7,7 @@
7
7
  import {
8
8
  Fn, float, vec2, vec3, vec4, int, uint,
9
9
  bool as tslBool,
10
- If, Loop, normalize, max, exp, log, clamp, dot, length, select,
10
+ If, Loop, normalize, max, exp, log, clamp, dot, length, select, smoothstep, mix,
11
11
  instanceIndex,
12
12
  sampler,
13
13
  atomicAdd, atomicLoad, atomicStore, uintBitsToFloat,
@@ -27,7 +27,8 @@ import { sampleChromaticCollision, sampleHenyeyGreenstein, subsurfaceCoefficient
27
27
  import { calculateIndirectLighting } from './LightsIndirect.js';
28
28
  import { IndirectLightingResult, sampleCone } from './LightsCore.js';
29
29
  import { regularizePathContribution, generateSampledDirection, computeNDCDepth, handleRussianRoulette } from './PathTracerCore.js';
30
- import { getImportanceSamplingInfo } from './MaterialProperties.js';
30
+ import { getImportanceSamplingInfo, evaluateDFG } from './MaterialProperties.js';
31
+ import { dielectricF0 } from './Fresnel.js';
31
32
  import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
32
33
  import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
33
34
  import { calculateEmissiveTriangleContribution, calculateEmissiveLightPdf, EmissiveSample } from './EmissiveSampling.js';
@@ -44,6 +45,7 @@ import {
44
45
  BRDFWeights,
45
46
  MaterialCache,
46
47
  DirectLightingDual,
48
+ DFGResult,
47
49
  } from './Struct.js';
48
50
  import { RandomValue, getRandomSample } from './Random.js';
49
51
  import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
@@ -54,8 +56,9 @@ import {
54
56
  readHitDistance, readHitBarycentrics, readHitNormal,
55
57
  readHitMaterialIndex, readHitTriangleIndex,
56
58
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
57
- writeGBuffer, readGBuffer, gbDecodeNormalDepth, gbDecodeAlbedo,
59
+ writeGBuffer, readGBuffer, gbDecodeNormalDepth,
58
60
  readRayRadiance,
61
+ readFeatureThroughput, writeFeatureThroughput,
59
62
  } from '../Processor/PackedRayBuffer.js';
60
63
 
61
64
  const WG_SIZE = 256;
@@ -90,6 +93,7 @@ export function buildShadeKernel( params ) {
90
93
  globalIlluminationIntensity,
91
94
  cameraProjectionMatrix, cameraViewMatrix,
92
95
  fireflyThreshold, frame, resolution,
96
+ chunkRowBase, // chunked path pool: global pixel = chunkRowBase·W + localSlot (rayID). See spec.
93
97
  emissiveTriangleCount, emissiveVec4Offset, emissiveTotalPower,
94
98
  emissiveBoost, totalTriangleCount, enableEmissiveTriangleSampling,
95
99
  lightBVHNodeCount, reverseMapVec4Offset,
@@ -183,6 +187,11 @@ export function buildShadeKernel( params ) {
183
187
  const pixelIndex = rayID;
184
188
  const rngState = rngBufferRW.element( rayID ).toVar();
185
189
 
190
+ // DDFA see-through aux tint carried across smooth glass/mirror; committed into the OIDN/ASVGF
191
+ // albedo guide at the first diffuse-enough surface (or env). Mutated locally, persisted on every
192
+ // deferring continue (a missed persist = stale tint). 1 = untinted (direct hit → today's albedo).
193
+ const featCarry = readFeatureThroughput( rayBufferRW, rayID ).toVar();
194
+
186
195
  const hitDist = readHitDistance( hitBufferRO, rayID ).toVar();
187
196
  const hitNormal = readHitNormal( hitBufferRO, rayID ).toVar();
188
197
  // hitInfo.uv is the interpolated texture UV (not barycentrics)
@@ -309,7 +318,8 @@ export function buildShadeKernel( params ) {
309
318
  } );
310
319
 
311
320
  writeRayRadiance( rayBufferRW, rayID, vec4( outRgb, outAlpha ) );
312
- writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
321
+ // DDFA: the plane's aux (written above) is a committed surface — lock it (cosmetic; ray dies here).
322
+ writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ).bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
313
323
  rngBufferRW.element( rayID ).assign( rngState );
314
324
  Return();
315
325
 
@@ -333,9 +343,8 @@ export function buildShadeKernel( params ) {
333
343
  const wantBackdrop = isBackdropView.and( showBackground ); // draw env image as backdrop
334
344
  const wantEnvLight = isBackdropView.not().and( enableEnvironmentLight ); // env as light on redirected bounces
335
345
 
336
- // Fix ①: OIDN clean-aux for a redirected ray that escapes to the environment (e.g. smooth
337
- // glass over sky). Holds the clamped env colour this ray sees; written into the aux G-buffer
338
- // before the miss Return below, but only when the pixel still carries the black bounce-0 default.
346
+ // DDFA: env colour a redirected ray escaping to the environment sees (e.g. smooth glass over
347
+ // sky). Committed (tinted by featCarry) into the aux G-buffer before the miss Return below.
339
348
  const escapedAuxAlbedo = vec3( 0.0 ).toVar();
340
349
 
341
350
  If( wantBackdrop.or( wantEnvLight ), () => {
@@ -376,8 +385,8 @@ export function buildShadeKernel( params ) {
376
385
 
377
386
  } );
378
387
 
379
- // Fix ①: remember the (clamped) env colour this escaped ray sees, so a redirected ray
380
- // that never captured a surface aux can feed OIDN a meaningful albedo below.
388
+ // DDFA: remember the (clamped) env colour this escaped ray sees, for the redirected-escape
389
+ // aux commit below (tinted by the accumulated see-through throughput).
381
390
  escapedAuxAlbedo.assign( clamp( envColor, vec3( 0.0 ), vec3( 1.0 ) ) );
382
391
 
383
392
  // MIS weight for implicit env hit — prevents double-counting with NEE
@@ -447,22 +456,18 @@ export function buildShadeKernel( params ) {
447
456
 
448
457
  } );
449
458
 
450
- // Fix ①: fill the black bounce-0 default aux for a redirected ray that reached the
451
- // environment without ever locking a surface guide (smooth glass / specular transmission
452
- // over sky). Only when the stored albedo is still the black default (dot < 1e-4), so a real
453
- // specular-surface aux written upstream is never clobbered. Normal = the escaped ray
454
- // direction (varies per pixel → OIDN sees structure that tracks the refracted view);
455
- // albedo = the clamped env colour it sees; depth kept at the primary hit.
459
+ // DDFA: a redirected ray (specular chain — glass/mirror) that escaped to the environment without
460
+ // ever committing a surface guide commits the env colour it sees, tinted by the accumulated
461
+ // see-through throughput (red glass over sky → sky×red). Normal = the escaped direction (varies
462
+ // per pixel → OIDN sees structure tracking the refracted view); depth kept at the primary hit.
463
+ // AUX_LOCKED-clear alone means "never committed" — the old black-probe is redundant. A direct
464
+ // backdrop (REDIRECTED clear) keeps Generate's black/far default (regression-safe sky guide).
456
465
  If( auxOn
457
466
  .and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) )
458
467
  .and( flags.bitAnd( uint( RAY_FLAG.REDIRECTED ) ).notEqual( uint( 0 ) ) ), () => {
459
468
 
460
469
  const gPrev = readGBuffer( gBufferRW, pixelIndex );
461
- If( dot( gbDecodeAlbedo( gPrev ), vec3( 1.0 ) ).lessThan( float( 1e-4 ) ), () => {
462
-
463
- writeGBuffer( gBufferRW, pixelIndex, normalize( direction ), gbDecodeNormalDepth( gPrev ).w, escapedAuxAlbedo );
464
-
465
- } );
470
+ writeGBuffer( gBufferRW, pixelIndex, normalize( direction ), gbDecodeNormalDepth( gPrev ).w, clamp( escapedAuxAlbedo.mul( featCarry ), vec3( 0.0 ), vec3( 1.0 ) ) );
466
471
 
467
472
  } );
468
473
 
@@ -496,7 +501,14 @@ export function buildShadeKernel( params ) {
496
501
  If( max( max( mSigmaS.x, mSigmaS.y ), mSigmaS.z ).lessThanEqual( 0.0 ), () => {
497
502
 
498
503
  // glass: Beer-Lambert absorption
499
- throughput.mulAssign( exp( mSigmaA.mul( hitDist ).negate() ) );
504
+ const beer = exp( mSigmaA.mul( hitDist ).negate() ).toVar();
505
+ throughput.mulAssign( beer );
506
+ // DDFA: colored-glass volume tints the deferred aux guide by the same absorption.
507
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
508
+
509
+ featCarry.mulAssign( beer );
510
+
511
+ } );
500
512
 
501
513
  } ).Else( () => {
502
514
 
@@ -585,6 +597,10 @@ export function buildShadeKernel( params ) {
585
597
  material, samplingUV, N,
586
598
  ) ).toVar();
587
599
 
600
+ // DDFA: snapshot the UNCLAMPED roughness (before the min-0.05 clamp below) for the specular↔diffuse
601
+ // classification, so a perfect mirror reads 0 (fully specular → defers).
602
+ const rawRough = matSamples.roughness.toVar();
603
+
588
604
  // BRDF functions read material.color/metalness/roughness, so apply samples here
589
605
  material.color.assign( matSamples.albedo );
590
606
  material.metalness.assign( matSamples.metalness.clamp( 0.0, 1.0 ) );
@@ -635,7 +651,24 @@ export function buildShadeKernel( params ) {
635
651
 
636
652
  } );
637
653
 
638
- // first-hit MRT data (bounce 0 only)
654
+ // ─── DDFA classification (Cycles smoothstep-by-roughness, not a hard binary) ───
655
+ // nonspec fraction over a 2-pseudo-closure model: a Lambert closure (weight 1-metal, fully diffuse)
656
+ // + a glossy/glass closure (weight metal·(1-trans)+trans, classified by roughness). Commit the aux at
657
+ // the first surface with a meaningful diffuse response (nonspec≥0.25); smooth glass/mirror defers.
658
+ // featPrefix = the tint accumulated BEFORE this surface (read-only for this bounce's commits).
659
+ const metal = material.metalness.toVar();
660
+ const trans = material.transmission.toVar();
661
+ const roughFrac = smoothstep( float( 0.0 ), float( 0.15 ), rawRough ).toVar();
662
+ const auxCommit = clamp(
663
+ float( 1.0 ).sub( metal ).mul( float( 1.0 ).sub( trans ) )
664
+ .add( roughFrac.mul( metal.mul( float( 1.0 ).sub( trans ) ).add( trans ) ) ),
665
+ 0.0, 1.0,
666
+ ).greaterThanEqual( float( 0.25 ) ).toVar();
667
+ const featPrefix = featCarry.toVar();
668
+
669
+ // first-hit MRT data (bounce 0 only): write the primary DEPTH now with the default normal/albedo.
670
+ // The real normal/albedo are committed by the DDFA decision blocks below (which re-pack this depth);
671
+ // they may defer through smooth glass/mirror and commit at the first diffuse-enough surface or the env.
639
672
  If( bounceIndex.equal( 0 ).and( auxOn ), () => {
640
673
 
641
674
  const linearDepth = computeNDCDepth( {
@@ -643,28 +676,7 @@ export function buildShadeKernel( params ) {
643
676
  cameraProjectionMatrix,
644
677
  cameraViewMatrix,
645
678
  } );
646
- // G-buffer is per-pixel (rayID == pixelIndex). writeGBuffer half-packs (normal/depth/albedo).
647
- // Write the primary DEPTH now with the miss-default aux; the real normal/albedo are captured below
648
- // (aux-extend) and may extend through specular surfaces (gap #9). Glass rays Return at the transparency
649
- // block before that capture, so a glass-then-escape pixel keeps this default aux — megakernel parity
650
- // (objectNormal/objectColor stay at their init for transmissive-then-miss).
651
- //
652
- // Fix ③: BLEND (semi-transparent) glass instead LOCKS its OWN normal + albedo here. Its per-frame
653
- // stochastic alpha-skip-vs-opaque-shade outcome (MaterialTransmission.js:522-540) otherwise writes a
654
- // DIFFERENT surface into the aux each frame (wall-behind on skip vs glass on opaque); with the normal
655
- // not temporally averaged that becomes the speckle OIDN smears on — the visible artefact on
656
- // alpha-transparent glass. A deterministic glass-surface guide every frame removes it. MASK / opaque
657
- // materials keep the default + rely on the downstream aux-extend.
658
- If( material.alphaMode.equal( int( 2 ) ), () => {
659
-
660
- writeGBuffer( gBufferRW, pixelIndex, N, linearDepth, albedo.xyz );
661
- flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
662
-
663
- } ).Else( () => {
664
-
665
- writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
666
-
667
- } );
679
+ writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
668
680
 
669
681
  } );
670
682
 
@@ -698,6 +710,53 @@ export function buildShadeKernel( params ) {
698
710
  // persist any wavelength locked on a fresh dispersive transmission; identity write otherwise
699
711
  pathWavelength.assign( interaction.pathWavelength );
700
712
 
713
+ // ─── DDFA aux decision at a transmissive / alpha / SSS interaction. Runs for BOTH the continuing and
714
+ // the BLEND fall-through paths, so it sits before If(continueRay). If/ElseIf chaining so BLEND wins
715
+ // over the transmission branch (fixes the BLEND-glass per-frame flicker + the BLEND+transmission case). ───
716
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
717
+
718
+ const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
719
+ // N here is the mapped normal — faceforward it (a ray exiting glass sees a back-facing N).
720
+ const Nff = select( dot( N, direction.negate() ).lessThan( 0.0 ), N.negate(), N ).toVar();
721
+ const auxAlbedo = clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ).toVar();
722
+
723
+ If( material.alphaMode.equal( int( 2 ) ), () => {
724
+
725
+ // BLEND: deterministic own-surface commit on BOTH stochastic skip + shade frames (no flicker).
726
+ writeGBuffer( gBufferRW, pixelIndex, Nff, primaryDepth, auxAlbedo );
727
+ flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
728
+
729
+ } ).ElseIf( interaction.isAlphaSkip, () => {
730
+
731
+ // MASK cutout passthrough: keep deferring, tint unchanged (backdrop through the hole commits later).
732
+
733
+ } ).ElseIf( interaction.isSubsurface, () => {
734
+
735
+ // SSS boundary: force-commit the skin surface as diffuse.
736
+ writeGBuffer( gBufferRW, pixelIndex, Nff, primaryDepth, auxAlbedo );
737
+ flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
738
+
739
+ } ).ElseIf( interaction.isTransmissive, () => {
740
+
741
+ If( auxCommit.or( material.dispersion.greaterThan( 0.0 ) ), () => {
742
+
743
+ // frosted / rough / dispersive glass: commit its own surface (deterministic per frame).
744
+ writeGBuffer( gBufferRW, pixelIndex, Nff, primaryDepth, auxAlbedo );
745
+ flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
746
+
747
+ } ).Else( () => {
748
+
749
+ // smooth glass: DEFER — tint by the glass colour (matches the transmission throughput,
750
+ // which multiplies material.color at both reflect and refract; achromatic eta²/Fr excluded).
751
+ featCarry.mulAssign( clamp( albedo, vec3( 0.0 ), vec3( 1.0 ) ) );
752
+
753
+ } );
754
+
755
+ } );
756
+ // else (opaque non-skip): the opaque decision block below handles it (needs the viewer-facing N).
757
+
758
+ } );
759
+
701
760
  If( interaction.continueRay, () => {
702
761
 
703
762
  // update medium stack for transmission (not reflection/TIR)
@@ -801,26 +860,6 @@ export function buildShadeKernel( params ) {
801
860
 
802
861
  throughput.mulAssign( interaction.throughput );
803
862
 
804
- // Fix ②: OIDN clean-aux for STOCHASTIC glass (rough / frosted / dispersive), whose refracted
805
- // ray hits a different behind-surface every frame. Extending aux through it (the aux-extend on
806
- // the opaque-hit path) captures that per-frame surface — albedo then temporally averaged, normal
807
- // point-sampled — an inconsistent guide OIDN smears on. Instead lock the glass surface's OWN
808
- // normal + tint here (deterministic, spatially coherent). Smooth non-dispersive glass is left
809
- // alone: its refraction is a per-pixel delta, so the behind-surface aux captured downstream is
810
- // already stable. Gated to a genuine dielectric event (not alpha-cutout passthrough or SSS).
811
- If( auxOn
812
- .and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) )
813
- .and( interaction.isTransmissive )
814
- .and( interaction.isAlphaSkip.not() )
815
- .and( interaction.isSubsurface.not() )
816
- .and( material.roughness.greaterThan( 0.05 ).or( material.dispersion.greaterThan( 0.0 ) ) ), () => {
817
-
818
- const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
819
- writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, albedo.xyz );
820
- flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
821
-
822
- } );
823
-
824
863
  // reflection stays on same side, transmission pushes through
825
864
  const reflectOffsetDir = select( interaction.entering, N, N.negate() );
826
865
  const offsetDir = select( interaction.didReflect, reflectOffsetDir, direction );
@@ -844,6 +883,13 @@ export function buildShadeKernel( params ) {
844
883
  writeRayThroughputPdf( rayBufferRW, rayID, throughput, readRayPdf( rayBufferRW, rayID ) );
845
884
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
846
885
  writeMediumStack( rayBufferRW, rayID, uint( mediumStackDepth ), uint( transTraversals ), mediumStack_ior_1, mediumStack_ior_2, mediumStack_ior_3, uint( pathWavelength.add( 0.5 ) ) );
886
+ // DDFA: persist the (possibly tinted) see-through throughput for the next bounce. MUST run after
887
+ // the writeMediumSigmaA above — both RMW slot 5 (sigmaA.xyz / featTP.w). Gated AUX_LOCKED-clear.
888
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
889
+
890
+ writeFeatureThroughput( rayBufferRW, rayID, featCarry );
891
+
892
+ } );
847
893
  rngBufferRW.element( rayID ).assign( rngState );
848
894
  Return();
849
895
 
@@ -930,22 +976,29 @@ export function buildShadeKernel( params ) {
930
976
 
931
977
  } );
932
978
 
933
- // OIDN clean-aux (megakernel parity: PathTracerCore.js:1300): keep overwriting the per-pixel normal/
934
- // albedo through mirror/glass until the first non-specular hit, so aux describes what's actually
935
- // visible (the surface reflected in a mirror / seen behind glass), not the specular surface. Glass
936
- // Returns at the transparency block above, so its aux is replaced by the surface behind it. Depth
937
- // stays at the primary hit (read back + re-packed; the snorm depth re-pack is idempotent — no drift).
938
- If( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ).and( auxOn ), () => {
979
+ // ─── DDFA opaque aux decision: commit at the first diffuse-enough surface, defer through smooth
980
+ // mirror/metal so the guide describes what the mirror reflects, not the mirror itself. N is already
981
+ // viewer-facing (two-sided flip above); depth read back + re-packed (idempotent snorm — no drift). ───
982
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
939
983
 
940
984
  const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
941
- writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, albedo.xyz );
942
-
943
- const auxIsMirror = material.metalness.greaterThan( 0.7 ).and( material.roughness.lessThan( 0.3 ) );
944
- const auxIsTransmissive = material.transmission.greaterThan( 0.5 );
945
- If( auxIsMirror.or( auxIsTransmissive ).not(), () => {
985
+ If( auxCommit, () => {
946
986
 
987
+ // diffuse / glossy / rough-metal: commit the surface guide, tinted by the see-through prefix.
988
+ writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ) );
947
989
  flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
948
990
 
991
+ } ).Else( () => {
992
+
993
+ // smooth mirror / metal: DEFER — tint by the specular directional albedo (metal colour),
994
+ // matching the opaque throughput (≈ F at the near-delta mirror lobe). Achromatic energy excluded.
995
+ const NoV = max( dot( N, V ), float( 1e-3 ) );
996
+ const F0 = clamp(
997
+ mix( dielectricF0( material.ior ).mul( material.specularColor ), albedo, material.metalness ).mul( material.specularIntensity ),
998
+ vec3( 0.0 ), vec3( 1.0 ),
999
+ );
1000
+ featCarry.mulAssign( DFGResult.wrap( evaluateDFG( F0, NoV, max( rawRough, float( 0.02 ) ) ) ).E_total );
1001
+
949
1002
  } );
950
1003
 
951
1004
  } );
@@ -955,11 +1008,14 @@ export function buildShadeKernel( params ) {
955
1008
  material.clearcoat, material.emissive, material.subsurface,
956
1009
  ) ).toVar();
957
1010
 
958
- // STBN keyed on (pixel, bounceIndex, frame).
1011
+ // STBN keyed on (GLOBAL pixel, bounceIndex, frame). pixelIndex is the LOCAL path slot; the global pixel
1012
+ // = chunkRowBase·W + localSlot, so the blue-noise pattern stays spatially aligned across row-band chunks
1013
+ // (and matches Generate's per-pixel RNG seed). chunkRowBase is 0 in the single-chunk case.
959
1014
  const _resX = int( resolution.x ).toVar();
1015
+ const _globalPixel = int( pixelIndex ).add( chunkRowBase.mul( _resX ) );
960
1016
  const _pixelCoord = vec2(
961
- float( int( pixelIndex ).mod( _resX ) ).add( 0.5 ),
962
- float( int( pixelIndex ).div( _resX ) ).add( 0.5 ),
1017
+ float( _globalPixel.mod( _resX ) ).add( 0.5 ),
1018
+ float( _globalPixel.div( _resX ) ).add( 0.5 ),
963
1019
  );
964
1020
  const xi = getRandomSample( _pixelCoord, int( 0 ), bounceIndex, rngState, int( - 1 ), resolution, frame ).toVar();
965
1021
  const emptyWeights = BRDFWeights( {
@@ -1174,6 +1230,14 @@ export function buildShadeKernel( params ) {
1174
1230
  ).toVar();
1175
1231
  If( rrSurvival.lessThanEqual( 0.0 ), () => {
1176
1232
 
1233
+ // DDFA terminal fallback: a ray dying while still deferring (e.g. a mirror maze) commits its last
1234
+ // surface instead of leaving a black guide OIDN would demod-amplify. N is viewer-facing here.
1235
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1236
+
1237
+ const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
1238
+ writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ) );
1239
+
1240
+ } );
1177
1241
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
1178
1242
  writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
1179
1243
  rngBufferRW.element( rayID ).assign( rngState );
@@ -1185,6 +1249,13 @@ export function buildShadeKernel( params ) {
1185
1249
  // Terminate on CAMERA depth (opaque scatter count), not path length — glass/SSS free bounces no longer burn the maxBounces budget (gap #4).
1186
1250
  If( cameraDepth.greaterThanEqual( maxBounceCount ), () => {
1187
1251
 
1252
+ // DDFA terminal fallback (see RR-kill above): commit the last surface if still deferring.
1253
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1254
+
1255
+ const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
1256
+ writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ) );
1257
+
1258
+ } );
1188
1259
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
1189
1260
  writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
1190
1261
  rngBufferRW.element( rayID ).assign( rngState );
@@ -1200,6 +1271,12 @@ export function buildShadeKernel( params ) {
1200
1271
  writeRayThroughputPdf( rayBufferRW, rayID, throughput, bouncePdf );
1201
1272
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
1202
1273
  writeMediumStack( rayBufferRW, rayID, uint( mediumStackDepth ), uint( transTraversals ), mediumStack_ior_1, mediumStack_ior_2, mediumStack_ior_3, uint( pathWavelength.add( 0.5 ) ) );
1274
+ // DDFA: persist the (possibly mirror-tinted) see-through throughput for the next bounce.
1275
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1276
+
1277
+ writeFeatureThroughput( rayBufferRW, rayID, featCarry );
1278
+
1279
+ } );
1203
1280
  rngBufferRW.element( rayID ).assign( rngState );
1204
1281
 
1205
1282
  } );