rayzee 6.4.0 → 7.0.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 (58) hide show
  1. package/README.md +24 -5
  2. package/dist/rayzee.es.js +4953 -4225
  3. package/dist/rayzee.es.js.map +1 -1
  4. package/dist/rayzee.umd.js +157 -236
  5. package/dist/rayzee.umd.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/EngineDefaults.js +29 -13
  8. package/src/PathTracerApp.js +119 -26
  9. package/src/Pipeline/PipelineContext.js +1 -2
  10. package/src/Pipeline/RenderPipeline.js +1 -1
  11. package/src/Pipeline/RenderStage.js +1 -1
  12. package/src/Processor/CameraOptimizer.js +0 -5
  13. package/src/Processor/GeometryExtractor.js +22 -1
  14. package/src/Processor/KernelManager.js +277 -0
  15. package/src/Processor/PackedRayBuffer.js +265 -0
  16. package/src/Processor/QueueManager.js +173 -0
  17. package/src/Processor/SceneProcessor.js +1 -0
  18. package/src/Processor/ShaderBuilder.js +11 -316
  19. package/src/Processor/StorageTexturePool.js +29 -15
  20. package/src/Processor/TextureCreator.js +6 -0
  21. package/src/Processor/VRAMTracker.js +169 -0
  22. package/src/Processor/utils.js +11 -110
  23. package/src/RenderSettings.js +1 -3
  24. package/src/Stages/ASVGF.js +76 -20
  25. package/src/Stages/BilateralFilter.js +34 -10
  26. package/src/Stages/EdgeFilter.js +2 -3
  27. package/src/Stages/MotionVector.js +16 -9
  28. package/src/Stages/NormalDepth.js +17 -5
  29. package/src/Stages/PathTracer.js +671 -1456
  30. package/src/Stages/PathTracerStage.js +1451 -0
  31. package/src/Stages/SSRC.js +32 -15
  32. package/src/Stages/Variance.js +35 -12
  33. package/src/TSL/BVHTraversal.js +7 -1
  34. package/src/TSL/Common.js +12 -2
  35. package/src/TSL/CompactKernel.js +110 -0
  36. package/src/TSL/DebugKernel.js +98 -0
  37. package/src/TSL/Environment.js +13 -11
  38. package/src/TSL/ExtendKernel.js +75 -0
  39. package/src/TSL/FinalWriteKernel.js +121 -0
  40. package/src/TSL/GenerateKernel.js +109 -0
  41. package/src/TSL/LightsSampling.js +2 -2
  42. package/src/TSL/MaterialTransmission.js +32 -2
  43. package/src/TSL/PathTracerCore.js +43 -912
  44. package/src/TSL/ShadeKernel.js +873 -0
  45. package/src/TSL/Struct.js +5 -0
  46. package/src/TSL/Subsurface.js +232 -0
  47. package/src/TSL/patches.js +81 -4
  48. package/src/index.js +3 -0
  49. package/src/managers/CameraManager.js +1 -1
  50. package/src/managers/DenoisingManager.js +40 -75
  51. package/src/managers/EnvironmentManager.js +30 -39
  52. package/src/managers/MaterialDataManager.js +60 -1
  53. package/src/managers/OverlayManager.js +7 -22
  54. package/src/managers/UniformManager.js +1 -3
  55. package/src/managers/helpers/TileHelper.js +2 -2
  56. package/src/Stages/AdaptiveSampling.js +0 -483
  57. package/src/TSL/PathTracer.js +0 -384
  58. package/src/managers/TileManager.js +0 -298
@@ -297,6 +297,7 @@ export class SceneProcessor {
297
297
  // Store other extracted data
298
298
  this.materials = extractedData.materials;
299
299
  this.materialCount = this.materials.length; // Store material count for feature scanning
300
+ this.materialTriangleCounts = extractedData.materialTriangleCounts; // Per-material tri count for sort-bin remap
300
301
  this.meshes = extractedData.meshes;
301
302
  this.meshTriangleRanges = extractedData.meshTriangleRanges; // Per-mesh { start, count } for TLAS/BLAS
302
303
  this.maps = extractedData.maps;
@@ -1,110 +1,29 @@
1
1
  /**
2
- * ShaderBuilder.js
3
- * Owns TSL shader graph construction and compute node management
4
- * for the path tracing pipeline.
2
+ * ShaderBuilder.js — shared scene texture-node factory for the path tracer.
5
3
  *
6
- * "Copy approach": Single compute node writes to 3 write-only StorageTextures.
7
- * Previous-frame reads use texture() sampling from a MRT RenderTarget
8
- * (populated by copyTextureToTexture after each dispatch).
9
- *
10
- * Texture nodes are created once and updated in-place via .value mutation
11
- * to preserve compiled shader graph references.
4
+ * Creates the texture/storage nodes the wavefront kernels read (environment, material map
5
+ * arrays, previous-frame MRT, adaptive-sampling, gobo/IES) and configures the module-level
6
+ * shadow/alpha/gobo/IES shader state. Nodes are created once and updated in-place via
7
+ * .value mutation to preserve compiled shader-graph references.
12
8
  */
13
9
 
14
- import { Fn, texture, vec2, float, int, uniform, If,
15
- localId, workgroupId } from 'three/tsl';
16
- import { TextureNode } from 'three/webgpu';
10
+ import { texture } from 'three/tsl';
17
11
  import { LinearFilter, DataArrayTexture } from 'three';
18
- import { pathTracerMain } from '../TSL/PathTracer.js';
19
12
  import { setShadowAlbedoMaps, setAlphaShadowsUniform } from '../TSL/LightsDirect.js';
20
13
  import { setGoboMapsTexture, setIESProfilesTexture } from '../TSL/LightsCore.js';
21
- import { BuildTimer } from './BuildTimer.js';
22
-
23
- const WG_SIZE = 8;
24
14
 
25
15
  export class ShaderBuilder {
26
16
 
27
17
  constructor() {
28
18
 
29
- // Single compute node (no dual ping-pong — copy approach)
30
- this.computeNode = null;
31
-
32
19
  // Previous-frame texture nodes (sample from MRT RenderTarget)
33
20
  this.prevColorTexNode = null;
34
21
  this.prevNormalDepthTexNode = null;
35
22
  this.prevAlbedoTexNode = null;
36
23
 
37
- // Adaptive sampling texture (updated per-frame from context)
38
- this.adaptiveSamplingTexNode = null;
39
-
40
- // Tile offset uniforms — pixel origin of the active tile region
41
- // Dispatch covers only tile-sized workgroups; offset maps them to image space
42
- this.tileOffsetX = uniform( 0, 'int' );
43
- this.tileOffsetY = uniform( 0, 'int' );
44
-
45
- // Render dimensions for edge-workgroup bounds checking
46
- this.renderWidth = uniform( 1920, 'int' );
47
- this.renderHeight = uniform( 1080, 'int' );
48
-
49
- // Dispatch dimensions
50
- this._dispatchX = 0;
51
- this._dispatchY = 0;
52
-
53
- // Reused per-frame dispatchSize array — avoids GC pressure from
54
- // allocating [x,y,z] on every setFullScreenDispatch/setTileDispatch call.
55
- // WebGPUBackend only reads indices 0..2 of this array during compute dispatch.
56
- this._dispatchSize = [ 0, 0, 1 ];
57
-
58
24
  // Scene texture nodes cache (for in-place updates on model change)
59
25
  this._sceneTextureNodes = null;
60
26
 
61
- // Whether the GPU compute pipeline has been compiled (via a real dispatch).
62
- // Reset on setupCompute() rebuilds and on dispose().
63
- this._compiled = false;
64
-
65
- }
66
-
67
- /**
68
- * Creates the full compute shader graph from scratch.
69
- *
70
- * @param {Object} config
71
- * @param {Object} config.stage - PathTracer instance
72
- * @param {Object} config.storageTextures - StorageTexturePool
73
- */
74
- setupCompute( config ) {
75
-
76
- const { stage, storageTextures } = config;
77
-
78
- const timer = new BuildTimer( 'setupCompute' );
79
-
80
- timer.start( 'Create texture nodes' );
81
- const textureNodes = this._createTextureNodes( stage, storageTextures );
82
- timer.end( 'Create texture nodes' );
83
-
84
- timer.start( 'Build compute node (TSL)' );
85
-
86
- const width = storageTextures.renderWidth;
87
- const height = storageTextures.renderHeight;
88
- this._dispatchX = Math.ceil( width / WG_SIZE );
89
- this._dispatchY = Math.ceil( height / WG_SIZE );
90
-
91
- this.renderWidth.value = width;
92
- this.renderHeight.value = height;
93
-
94
- const writeTex = storageTextures.getWriteTextures();
95
-
96
- this.computeNode = this._buildComputeNode(
97
- stage, textureNodes,
98
- writeTex.color, writeTex.normalDepth, writeTex.albedo
99
- );
100
-
101
- // New compute node → needs a fresh GPU pipeline compile
102
- this._compiled = false;
103
-
104
- timer.end( 'Build compute node (TSL)' );
105
-
106
- timer.print();
107
-
108
27
  }
109
28
 
110
29
  updateSceneTextures( stage ) {
@@ -165,98 +84,10 @@ export class ShaderBuilder {
165
84
 
166
85
  }
167
86
 
168
- setSize( width, height ) {
169
-
170
- this._dispatchX = Math.ceil( width / WG_SIZE );
171
- this._dispatchY = Math.ceil( height / WG_SIZE );
172
-
173
- if ( this.computeNode ) {
174
-
175
- this._dispatchSize[ 0 ] = this._dispatchX;
176
- this._dispatchSize[ 1 ] = this._dispatchY;
177
- this.computeNode.dispatchSize = this._dispatchSize;
178
-
179
- }
180
-
181
- this.renderWidth.value = width;
182
- this.renderHeight.value = height;
183
-
184
- // Reset tile offset (full-screen)
185
- this.tileOffsetX.value = 0;
186
- this.tileOffsetY.value = 0;
187
-
188
- }
189
-
190
- /**
191
- * Set dispatch to cover only the active tile region.
192
- * Adjusts dispatch count and tile offset so threads map directly to tile pixels.
193
- * @param {number} offsetX - Tile origin X in pixels
194
- * @param {number} offsetY - Tile origin Y in pixels
195
- * @param {number} tileWidth - Tile width in pixels
196
- * @param {number} tileHeight - Tile height in pixels
197
- */
198
- setTileDispatch( offsetX, offsetY, tileWidth, tileHeight ) {
199
-
200
- this.tileOffsetX.value = offsetX;
201
- this.tileOffsetY.value = offsetY;
202
-
203
- const dispatchX = Math.ceil( tileWidth / WG_SIZE );
204
- const dispatchY = Math.ceil( tileHeight / WG_SIZE );
205
-
206
- if ( this.computeNode ) {
207
-
208
- this._dispatchSize[ 0 ] = dispatchX;
209
- this._dispatchSize[ 1 ] = dispatchY;
210
- this.computeNode.dispatchSize = this._dispatchSize;
211
-
212
- }
213
-
214
- }
215
-
216
- /**
217
- * Reset dispatch to full-screen (no tiling).
218
- */
219
- setFullScreenDispatch() {
220
-
221
- this.tileOffsetX.value = 0;
222
- this.tileOffsetY.value = 0;
223
-
224
- if ( this.computeNode ) {
225
-
226
- this._dispatchSize[ 0 ] = this._dispatchX;
227
- this._dispatchSize[ 1 ] = this._dispatchY;
228
- this.computeNode.dispatchSize = this._dispatchSize;
229
-
230
- }
231
-
232
- }
233
-
234
- /**
235
- * Front-load GPU compute pipeline creation via a single dispatch.
236
- *
237
- * Three.js WebGPU has no `createComputePipelineAsync` path — compute
238
- * pipelines always compile synchronously on first `renderer.compute(node)`.
239
- * Calling this at build time (while a "Compiling shaders…" status is
240
- * already visible) moves the stall off the first animate frame.
241
- *
242
- * The dispatch writes to ping-pong storage textures whose contents are
243
- * discarded by the subsequent `reset()` (frame counter back to 0 →
244
- * `hasPreviousAccumulated = 0` → prev textures are not read).
245
- *
246
- * @param {object} renderer - WebGPURenderer
247
- */
248
- forceCompile( renderer ) {
249
-
250
- if ( this._compiled || ! this.computeNode || ! renderer ) return;
251
-
252
- this._compiled = true;
253
- renderer.compute( this.computeNode );
254
-
255
- }
256
-
257
- // ===== PRIVATE =====
258
-
259
- _createTextureNodes( stage, storageTextures ) {
87
+ // Creates the shared scene texture nodes (env, material maps, prev-frame, adaptive, gobo, IES)
88
+ // + configures the module-level shadow/alpha/gobo/IES shader state read by the wavefront kernels.
89
+ // Call from setupMaterial before the kernels are built.
90
+ createSceneTextureNodes( stage, storageTextures ) {
260
91
 
261
92
  const triStorage = stage.triangleStorageNode;
262
93
  const bvhStorage = stage.bvhStorageNode;
@@ -270,14 +101,6 @@ export class ShaderBuilder {
270
101
 
271
102
  const envTex = texture( stage.environment.environmentTexture );
272
103
 
273
- // Adaptive sampling texture
274
- const adaptiveSamplingTex = new TextureNode();
275
- this.adaptiveSamplingTexNode = adaptiveSamplingTex;
276
-
277
- // Environment importance sampling CDF — packed storage buffer
278
- // Layout: [marginal (envResolution.y floats) | conditional (envResolution.x * envResolution.y floats)]
279
- const envCDFStorage = stage.environment.envCDFStorageNode;
280
-
281
104
  // Previous-frame texture nodes — initialized from readTarget textures
282
105
  const readTextures = storageTextures.getReadTextures();
283
106
  this.prevColorTexNode = texture( readTextures.color );
@@ -319,7 +142,7 @@ export class ShaderBuilder {
319
142
 
320
143
  const result = {
321
144
  triStorage, bvhStorage, matStorage, lightBufferStorage,
322
- envTex, adaptiveSamplingTex, envCDFStorage,
145
+ envTex,
323
146
  albedoMapsTex, normalMapsTex, bumpMapsTex,
324
147
  metalnessMapsTex, roughnessMapsTex, emissiveMapsTex, displacementMapsTex,
325
148
  goboMapsTex, iesProfilesTex,
@@ -330,140 +153,12 @@ export class ShaderBuilder {
330
153
 
331
154
  }
332
155
 
333
- /**
334
- * Build a single compute node.
335
- * Previous-frame reads use texture() nodes bound to MRT RenderTarget textures.
336
- */
337
- _buildComputeNode( stage, textureNodes,
338
- writeColorTex, writeNDTex, writeAlbedoTex ) {
339
-
340
- const {
341
- triStorage, bvhStorage, matStorage, lightBufferStorage,
342
- envTex, adaptiveSamplingTex, envCDFStorage,
343
- albedoMapsTex, normalMapsTex, bumpMapsTex,
344
- metalnessMapsTex, roughnessMapsTex, emissiveMapsTex, displacementMapsTex,
345
- } = textureNodes;
346
-
347
- const tileOffsetX = this.tileOffsetX;
348
- const tileOffsetY = this.tileOffsetY;
349
- const renderWidth = this.renderWidth;
350
- const renderHeight = this.renderHeight;
351
-
352
- const prevColorTexNode = this.prevColorTexNode;
353
- const prevNormalDepthTexNode = this.prevNormalDepthTexNode;
354
- const prevAlbedoTexNode = this.prevAlbedoTexNode;
355
-
356
- const computeFn = Fn( () => {
357
-
358
- // Map thread to image-space pixel via tile offset
359
- const gx = tileOffsetX.add( int( workgroupId.x ).mul( WG_SIZE ) ).add( int( localId.x ) );
360
- const gy = tileOffsetY.add( int( workgroupId.y ).mul( WG_SIZE ) ).add( int( localId.y ) );
361
-
362
- // Bounds check only needed for edge workgroups that overshoot render dimensions
363
- If( gx.lessThan( renderWidth ).and( gy.lessThan( renderHeight ) ), () => {
364
-
365
- const pixelCoord = vec2( float( gx ).add( 0.5 ), float( gy ).add( 0.5 ) );
366
-
367
- pathTracerMain( {
368
- pixelCoord,
369
- writeColorTex, writeNDTex, writeAlbedoTex,
370
- // Previous-frame textures from MRT RenderTarget (sampled via texture())
371
- prevAccumTexture: prevColorTexNode,
372
- prevNormalDepthTexture: prevNormalDepthTexNode,
373
- prevAlbedoTexture: prevAlbedoTexNode,
374
- resolution: stage.resolution,
375
- frame: stage.frame,
376
- samplesPerPixel: stage.samplesPerPixel,
377
- visMode: stage.visMode,
378
- cameraWorldMatrix: stage.cameraWorldMatrix,
379
- cameraProjectionMatrixInverse: stage.cameraProjectionMatrixInverse,
380
- cameraViewMatrix: stage.cameraViewMatrix,
381
- cameraProjectionMatrix: stage.cameraProjectionMatrix,
382
- bvhBuffer: bvhStorage,
383
- triangleBuffer: triStorage,
384
- materialBuffer: matStorage,
385
- albedoMaps: albedoMapsTex,
386
- normalMaps: normalMapsTex,
387
- bumpMaps: bumpMapsTex,
388
- metalnessMaps: metalnessMapsTex,
389
- roughnessMaps: roughnessMapsTex,
390
- emissiveMaps: emissiveMapsTex,
391
- displacementMaps: displacementMapsTex,
392
- directionalLightsBuffer: stage.directionalLightsBufferNode,
393
- numDirectionalLights: stage.numDirectionalLights,
394
- areaLightsBuffer: stage.areaLightsBufferNode,
395
- numAreaLights: stage.numAreaLights,
396
- pointLightsBuffer: stage.pointLightsBufferNode,
397
- numPointLights: stage.numPointLights,
398
- spotLightsBuffer: stage.spotLightsBufferNode,
399
- numSpotLights: stage.numSpotLights,
400
- envTexture: envTex,
401
- environmentIntensity: stage.environmentIntensity,
402
- envMatrix: stage.environmentMatrix,
403
- envCDFBuffer: envCDFStorage,
404
- envTotalSum: stage.envTotalSum,
405
- envCompensationDelta: stage.envCompensationDelta,
406
- envResolution: stage.envResolution,
407
- enableEnvironmentLight: stage.enableEnvironment,
408
- useEnvMapIS: stage.useEnvMapIS,
409
- groundProjectionEnabled: stage.groundProjectionEnabled,
410
- groundProjectionRadius: stage.groundProjectionRadius,
411
- groundProjectionHeight: stage.groundProjectionHeight,
412
- maxBounceCount: stage.maxBounces,
413
- transmissiveBounces: stage.transmissiveBounces,
414
- showBackground: stage.showBackground,
415
- transparentBackground: stage.transparentBackground,
416
- backgroundIntensity: stage.backgroundIntensity,
417
- fireflyThreshold: stage.fireflyThreshold,
418
- globalIlluminationIntensity: stage.globalIlluminationIntensity,
419
- enableEmissiveTriangleSampling: stage.enableEmissiveTriangleSampling,
420
- emissiveTriangleBuffer: lightBufferStorage,
421
- emissiveTriangleCount: stage.emissiveTriangleCount,
422
- emissiveTotalPower: stage.emissiveTotalPower,
423
- emissiveBoost: stage.emissiveBoost,
424
- emissiveVec4Offset: stage.emissiveVec4Offset,
425
- lightBVHBuffer: lightBufferStorage,
426
- lightBVHNodeCount: stage.lightBVHNodeCount,
427
- debugVisScale: stage.debugVisScale,
428
- enableAccumulation: stage.enableAccumulation,
429
- hasPreviousAccumulated: stage.hasPreviousAccumulated,
430
- accumulationAlpha: stage.accumulationAlpha,
431
- cameraIsMoving: stage.cameraIsMoving,
432
- useAdaptiveSampling: stage.useAdaptiveSampling,
433
- adaptiveSamplingTexture: adaptiveSamplingTex,
434
- adaptiveSamplingMin: stage.adaptiveSamplingMin,
435
- adaptiveSamplingMax: stage.adaptiveSamplingMax,
436
- enableDOF: stage.enableDOF,
437
- focalLength: stage.focalLength,
438
- aperture: stage.aperture,
439
- focusDistance: stage.focusDistance,
440
- sceneScale: stage.sceneScale,
441
- apertureScale: stage.apertureScale,
442
- anamorphicRatio: stage.anamorphicRatio,
443
- } );
444
-
445
- } );
446
-
447
- } );
448
-
449
- return computeFn().compute(
450
- [ this._dispatchX, this._dispatchY, 1 ],
451
- [ WG_SIZE, WG_SIZE, 1 ]
452
- );
453
-
454
- }
455
-
456
156
  dispose() {
457
157
 
458
- this.computeNode?.dispose();
459
-
460
- this.computeNode = null;
461
158
  this.prevColorTexNode = null;
462
159
  this.prevNormalDepthTexNode = null;
463
160
  this.prevAlbedoTexNode = null;
464
- this.adaptiveSamplingTexNode = null;
465
161
  this._sceneTextureNodes = null;
466
- this._compiled = false;
467
162
 
468
163
  }
469
164
 
@@ -13,11 +13,12 @@
13
13
  */
14
14
 
15
15
  import { StorageTexture, RenderTarget } from 'three/webgpu';
16
- import { RGBAFormat, FloatType, LinearFilter, NearestFilter } from 'three';
16
+ import { RGBAFormat, FloatType, LinearFilter, NearestFilter, Box2, Vector2 } from 'three';
17
+ import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
17
18
 
18
- function createWriteStorageTex( width, height ) {
19
+ function createWriteStorageTex() {
19
20
 
20
- const tex = new StorageTexture( width, height );
21
+ const tex = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
21
22
  tex.type = FloatType;
22
23
  tex.format = RGBAFormat;
23
24
  tex.minFilter = LinearFilter;
@@ -44,6 +45,9 @@ export class StorageTexturePool {
44
45
  this.renderWidth = 0;
45
46
  this.renderHeight = 0;
46
47
 
48
+ // Reused srcRegion for copies out of the over-allocated StorageTextures.
49
+ this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
50
+
47
51
  if ( width > 0 && height > 0 ) {
48
52
 
49
53
  this.create( width, height );
@@ -59,10 +63,10 @@ export class StorageTexturePool {
59
63
  this.renderWidth = width;
60
64
  this.renderHeight = height;
61
65
 
62
- // Write-only StorageTextures
63
- this.writeColor = createWriteStorageTex( width, height );
64
- this.writeNormalDepth = createWriteStorageTex( width, height );
65
- this.writeAlbedo = createWriteStorageTex( width, height );
66
+ // Write-only StorageTextures allocated at max — never resized (see resize crash fix).
67
+ this.writeColor = createWriteStorageTex();
68
+ this.writeNormalDepth = createWriteStorageTex();
69
+ this.writeAlbedo = createWriteStorageTex();
66
70
 
67
71
  // Readable MRT RenderTarget (3 color attachments, no depth/stencil)
68
72
  this.readTarget = new RenderTarget( width, height, {
@@ -85,13 +89,22 @@ export class StorageTexturePool {
85
89
 
86
90
  ensureSize( width, height ) {
87
91
 
88
- if ( this.renderWidth !== width || this.renderHeight !== height || ! this.writeColor ) {
92
+ if ( ! this.writeColor ) {
89
93
 
90
94
  this.create( width, height );
91
95
  return true;
92
96
 
93
97
  }
94
98
 
99
+ if ( this.renderWidth !== width || this.renderHeight !== height ) {
100
+
101
+ // Resize only the readTarget — never dispose/recreate the write StorageTextures
102
+ // (that destroys the texture the compute bind group holds → submit crash).
103
+ this.setSize( width, height );
104
+ return true;
105
+
106
+ }
107
+
95
108
  return false;
96
109
 
97
110
  }
@@ -131,9 +144,13 @@ export class StorageTexturePool {
131
144
  */
132
145
  copyToReadTargets( renderer ) {
133
146
 
134
- renderer.copyTextureToTexture( this.writeColor, this.readTarget.textures[ 0 ] );
135
- renderer.copyTextureToTexture( this.writeNormalDepth, this.readTarget.textures[ 1 ] );
136
- renderer.copyTextureToTexture( this.writeAlbedo, this.readTarget.textures[ 2 ] );
147
+ // Source write StorageTextures are over-allocated at the max size; the Box2 region
148
+ // restricts the copy to the active render size so source and destination extents match.
149
+ this._srcRegion.max.set( this.renderWidth, this.renderHeight );
150
+
151
+ renderer.copyTextureToTexture( this.writeColor, this.readTarget.textures[ 0 ], this._srcRegion );
152
+ renderer.copyTextureToTexture( this.writeNormalDepth, this.readTarget.textures[ 1 ], this._srcRegion );
153
+ renderer.copyTextureToTexture( this.writeAlbedo, this.readTarget.textures[ 2 ], this._srcRegion );
137
154
 
138
155
  }
139
156
 
@@ -163,10 +180,7 @@ export class StorageTexturePool {
163
180
  this.renderWidth = width;
164
181
  this.renderHeight = height;
165
182
 
166
- this.writeColor?.setSize( width, height );
167
- this.writeNormalDepth?.setSize( width, height );
168
- this.writeAlbedo?.setSize( width, height );
169
-
183
+ // Write StorageTextures stay at max allocation — never resized (see resize crash fix).
170
184
  if ( this.readTarget ) {
171
185
 
172
186
  this.readTarget.setSize( width, height );
@@ -965,6 +965,12 @@ export class TextureCreator {
965
965
  bumpMapMatrices[ 4 ], bumpMapMatrices[ 5 ], bumpMapMatrices[ 6 ], 1,
966
966
  displacementMapMatrices[ 0 ], displacementMapMatrices[ 1 ], displacementMapMatrices[ 2 ], displacementMapMatrices[ 3 ],
967
967
  displacementMapMatrices[ 4 ], displacementMapMatrices[ 5 ], displacementMapMatrices[ 6 ], 1,
968
+ // Slot 27: subsurface (subsurfaceColor.rgb, subsurface weight)
969
+ mat.subsurfaceColor?.r ?? 1, mat.subsurfaceColor?.g ?? 1, mat.subsurfaceColor?.b ?? 1, mat.subsurface ?? 0,
970
+ // Slot 28: subsurface (subsurfaceRadius.rgb, subsurfaceRadiusScale)
971
+ mat.subsurfaceRadius?.[ 0 ] ?? 1, mat.subsurfaceRadius?.[ 1 ] ?? 0.2, mat.subsurfaceRadius?.[ 2 ] ?? 0.1, mat.subsurfaceRadiusScale ?? 1,
972
+ // Slot 29: subsurface (anisotropy g, reserved)
973
+ mat.subsurfaceAnisotropy ?? 0, 0, 0, 0,
968
974
  ];
969
975
 
970
976
  data.set( materialData, stride );
@@ -0,0 +1,169 @@
1
+ /**
2
+ * VRAMTracker.js — current/peak GPU memory accounting.
3
+ *
4
+ * Measures ACTUAL live bytes (attribute.array.byteLength, texture dims × format/type)
5
+ * rather than re-deriving allocation formulas, so it never drifts when strides,
6
+ * capacity rounding, or layouts change. Providers are thunks that read current state,
7
+ * so they survive reallocation (resize, scene/material/env reload). A per-pass WeakSet
8
+ * dedupes by resource identity, so overlapping registrations never double-count.
9
+ */
10
+
11
+ import {
12
+ RGBAFormat, RGBFormat, RGFormat, RedFormat,
13
+ FloatType, HalfFloatType, UnsignedByteType, ByteType,
14
+ UnsignedShortType, ShortType, UnsignedIntType, IntType,
15
+ } from 'three';
16
+
17
+ const CHANNELS = { [ RGBAFormat ]: 4, [ RGBFormat ]: 3, [ RGFormat ]: 2, [ RedFormat ]: 1 };
18
+ const TYPE_BYTES = {
19
+ [ FloatType ]: 4, [ HalfFloatType ]: 2,
20
+ [ UnsignedByteType ]: 1, [ ByteType ]: 1,
21
+ [ UnsignedShortType ]: 2, [ ShortType ]: 2,
22
+ [ UnsignedIntType ]: 4, [ IntType ]: 4,
23
+ };
24
+
25
+ function texelBytes( tex ) {
26
+
27
+ return ( CHANNELS[ tex.format ] ?? 4 ) * ( TYPE_BYTES[ tex.type ] ?? 4 );
28
+
29
+ }
30
+
31
+ /** Exact byte size of a storage/buffer attribute's backing typed array. */
32
+ export function bufferBytes( attr ) {
33
+
34
+ return attr?.array?.byteLength || 0;
35
+
36
+ }
37
+
38
+ /** Estimated GPU byte size of a Texture/StorageTexture/DataArrayTexture/RenderTarget. */
39
+ export function textureBytes( tex ) {
40
+
41
+ if ( ! tex ) return 0;
42
+
43
+ if ( tex.isRenderTarget ) {
44
+
45
+ const list = tex.textures?.length ? tex.textures : [ tex.texture ];
46
+ const w = tex.width || 0, h = tex.height || 0, d = tex.depth || 1;
47
+ let sum = 0;
48
+ for ( const t of list ) if ( t ) sum += w * h * d * texelBytes( t );
49
+ return sum;
50
+
51
+ }
52
+
53
+ const img = tex.image || {};
54
+ const w = img.width ?? tex.width ?? 0;
55
+ const h = img.height ?? tex.height ?? 0;
56
+ const d = img.depth ?? 1;
57
+ return w * h * d * texelBytes( tex );
58
+
59
+ }
60
+
61
+ export class VRAMTracker {
62
+
63
+ constructor() {
64
+
65
+ this._providers = [];
66
+ this.current = 0;
67
+ this.peak = 0;
68
+ this.byCategory = {};
69
+
70
+ }
71
+
72
+ /**
73
+ * @param {string} category - grouping label in the report
74
+ * @param {Function} fn - returns a resource or array of resources: buffer
75
+ * attributes (`.array`), textures/render targets (`.isTexture`/`.isRenderTarget`),
76
+ * or synthetic `{ bytes }` for sizes with no inspectable object. Return falsy to skip.
77
+ */
78
+ register( category, fn ) {
79
+
80
+ this._providers.push( { category, fn } );
81
+
82
+ }
83
+
84
+ measure() {
85
+
86
+ const seen = new WeakSet();
87
+ const byCategory = {};
88
+ let total = 0;
89
+
90
+ for ( const { category, fn } of this._providers ) {
91
+
92
+ let resources;
93
+ try {
94
+
95
+ resources = fn();
96
+
97
+ } catch {
98
+
99
+ resources = null;
100
+
101
+ }
102
+
103
+ if ( ! resources ) continue;
104
+
105
+ let bytes = 0;
106
+ for ( const r of ( Array.isArray( resources ) ? resources : [ resources ] ) ) {
107
+
108
+ bytes += this._resourceBytes( r, seen );
109
+
110
+ }
111
+
112
+ byCategory[ category ] = ( byCategory[ category ] || 0 ) + bytes;
113
+ total += bytes;
114
+
115
+ }
116
+
117
+ this.byCategory = byCategory;
118
+ this.current = total;
119
+ if ( total > this.peak ) this.peak = total;
120
+
121
+ return { current: total, peak: this.peak, byCategory };
122
+
123
+ }
124
+
125
+ _resourceBytes( r, seen ) {
126
+
127
+ if ( ! r ) return 0;
128
+
129
+ // synthetic { bytes } (e.g. attributeArray-backed histograms)
130
+ if ( typeof r.bytes === 'number' && ! r.isTexture && ! r.isRenderTarget ) return r.bytes;
131
+
132
+ // buffer attribute — dedupe by backing array (rw/ro nodes share one buffer)
133
+ if ( r.array && r.array.byteLength != null ) {
134
+
135
+ if ( seen.has( r.array ) ) return 0;
136
+ seen.add( r.array );
137
+ return r.array.byteLength;
138
+
139
+ }
140
+
141
+ // texture / render target — dedupe by object identity
142
+ if ( r.isRenderTarget || r.isTexture ) {
143
+
144
+ if ( seen.has( r ) ) return 0;
145
+ seen.add( r );
146
+ return textureBytes( r );
147
+
148
+ }
149
+
150
+ return 0;
151
+
152
+ }
153
+
154
+ /** Drop the high-water mark to the current value (call when a new render begins). */
155
+ resetPeak() {
156
+
157
+ this.peak = this.current;
158
+
159
+ }
160
+
161
+ getReport() {
162
+
163
+ const mb = ( b ) => ( b / 1048576 ).toFixed( 1 );
164
+ const parts = Object.entries( this.byCategory ).map( ( [ k, v ] ) => `${k}=${mb( v )}` );
165
+ return `VRAM current=${mb( this.current )}MB peak=${mb( this.peak )}MB [${parts.join( ' ' )}]`;
166
+
167
+ }
168
+
169
+ }