rayzee 10.0.0 → 11.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 (173) hide show
  1. package/README.md +252 -44
  2. package/dist/ActiveColor-YvuQq44O.js +13 -0
  3. package/dist/ArchiveImporter-BRQvLggb.js +4300 -0
  4. package/dist/AssetConfig-DSEntE65.js +30 -0
  5. package/dist/BidirectionalIntegrator-9lZQxFVH.js +934 -0
  6. package/dist/BufferLayout-D_IBo2ci.js +150 -0
  7. package/dist/ColorManagement-oeEk7oSu.js +1488 -0
  8. package/dist/EngineEvents-Ci8dVWCP.js +73 -0
  9. package/dist/EngineIssues-CIZnXHsF.js +72 -0
  10. package/dist/FileFormats-sPGJeCRc.js +102 -0
  11. package/dist/Logger-dDDGj9Av.js +97 -0
  12. package/dist/{NeuralRetouch-CYgkQY8Z.js → NeuralRetouch-BCXylThH.js} +8 -6
  13. package/dist/{NeuralSuperRes-PkgDLQcD.js → NeuralSuperRes-sZAeS142.js} +55 -120
  14. package/dist/PathTracer-CQPW06Mm.js +29971 -0
  15. package/dist/PhysicalSky-BvqCw_dy.js +1219 -0
  16. package/dist/RayzeeRenderer-COpwotpU.js +3551 -0
  17. package/dist/SolarLimb-CPaeZVwz.js +721 -0
  18. package/dist/ToneMapGPU-CjU3osDd.js +177 -0
  19. package/dist/ViewTransforms-CYN7bq2w.js +255 -0
  20. package/dist/WorkingMatrix-C6y233hT.js +27 -0
  21. package/dist/ZipReader-gDSWr0I5.js +98 -0
  22. package/dist/addons/archives.es.js +3 -0
  23. package/dist/addons/bidirectional.es.js +2 -0
  24. package/dist/addons/color.es.js +3 -0
  25. package/dist/addons/formats.es.js +2 -0
  26. package/dist/addons/physical-sky.es.js +2 -0
  27. package/dist/addons/storage.es.js +3 -0
  28. package/dist/archiveFormats-BZw7TiDp.js +75 -0
  29. package/dist/identity-DrQsGT2o.js +39 -0
  30. package/dist/openStorage-DwcBRUj7.js +820 -0
  31. package/dist/rayzee-core.es.js +12 -0
  32. package/dist/rayzee.es.js +2551 -44024
  33. package/dist/rayzee.umd.js +528 -487
  34. package/dist/sceneCachePolicy-BSSsKY98.js +443 -0
  35. package/dist/shared-BGBcSxBQ.js +17 -0
  36. package/package.json +23 -2
  37. package/src/Color/ActiveColor.js +34 -0
  38. package/src/Color/BasicColor.js +147 -0
  39. package/src/Color/ColorManagement.js +7 -31
  40. package/src/Color/ViewTransforms.js +5 -1
  41. package/src/EngineDefaults.js +62 -1029
  42. package/src/EngineEvents.js +2 -0
  43. package/src/EngineIssues.js +2 -0
  44. package/src/Headless.js +6 -5
  45. package/src/Passes/AIUpscaler.js +58 -141
  46. package/src/Passes/OIDNDenoiser.js +92 -65
  47. package/src/Passes/OIDNTemporalHistory.js +1 -1
  48. package/src/PathTracerApp.js +620 -4026
  49. package/src/Pipeline/PipelineContext.js +0 -4
  50. package/src/Platform.js +36 -0
  51. package/src/Processor/ArchiveCache.js +1 -1
  52. package/src/Processor/ArchiveImporter.js +1259 -0
  53. package/src/Processor/AssetLoader.js +327 -1763
  54. package/src/Processor/AtmosphereModel.js +1 -2
  55. package/src/Processor/BVHBuilder.js +14 -126
  56. package/src/Processor/BVHLeafFold.js +4 -6
  57. package/src/Processor/BVHRefitter.js +67 -46
  58. package/src/Processor/BufferLayout.js +291 -0
  59. package/src/Processor/EmissiveTriangleBuilder.js +1 -1
  60. package/src/Processor/EnvironmentExactTable.js +187 -0
  61. package/src/Processor/EquirectHDRInfo.js +30 -223
  62. package/src/Processor/FileFormats.js +116 -0
  63. package/src/Processor/GLTFDecoders.js +85 -0
  64. package/src/Processor/GLTFDiffuseTransmission.js +63 -0
  65. package/src/Processor/GeometryExtractor.js +15 -5
  66. package/src/Processor/HostMemory.js +7 -1
  67. package/src/Processor/IESParser.js +47 -35
  68. package/src/Processor/InstanceTable.js +1 -1
  69. package/src/Processor/KernelManager.js +35 -2
  70. package/src/Processor/MaterialPacking.js +10 -8
  71. package/src/Processor/PBRT/PBRTMaterials.js +129 -41
  72. package/src/Processor/PBRT/PBRTParser.js +30 -17
  73. package/src/Processor/PBRT/PBRTSceneBuilder.js +548 -37
  74. package/src/Processor/PBRT/PBRTTextureBake.js +154 -0
  75. package/src/Processor/PBRT/PFM.js +59 -0
  76. package/src/Processor/PackedRayBuffer.js +116 -9
  77. package/src/Processor/ParallelBVHBuilder.js +3 -6
  78. package/src/Processor/PhysicalSky.js +5 -4
  79. package/src/Processor/PlatformImageLoader.js +50 -6
  80. package/src/Processor/QueueManager.js +39 -5
  81. package/src/Processor/ReinsertionOptimizer.js +2 -2
  82. package/src/Processor/SceneProcessor.js +53 -62
  83. package/src/Processor/ShaderBuilder.js +6 -16
  84. package/src/Processor/SimpleSky.js +1 -62
  85. package/src/Processor/SolarLimb.js +3 -0
  86. package/src/Processor/SplitBLAS.js +1 -1
  87. package/src/Processor/StorageTexturePool.js +22 -1
  88. package/src/Processor/TLASBuilder.js +1 -3
  89. package/src/Processor/TextureBuckets.js +237 -0
  90. package/src/Processor/TextureCreator.js +181 -8
  91. package/src/Processor/ToneMapGPU.js +64 -15
  92. package/src/Processor/Workers/BVHRefitWorker.js +5 -2
  93. package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
  94. package/src/Processor/Workers/BVHWorker.js +5 -18
  95. package/src/Processor/Workers/CDFWorker.js +6 -183
  96. package/src/Processor/Workers/TexturesWorker.js +103 -18
  97. package/src/Processor/archiveFormats.js +7 -0
  98. package/src/Processor/utils.js +2 -1
  99. package/src/RayzeeRenderer.js +3899 -0
  100. package/src/RenderSettings.js +68 -62
  101. package/src/Stages/ASVGF.js +13 -14
  102. package/src/Stages/AutoExposure.js +17 -17
  103. package/src/Stages/BilateralFilter.js +2 -1
  104. package/src/Stages/Compositor.js +8 -23
  105. package/src/Stages/DenoiserSettings.js +182 -0
  106. package/src/Stages/EdgeFilter.js +2 -1
  107. package/src/Stages/MotionVector.js +1 -1
  108. package/src/Stages/NRD.js +11 -5
  109. package/src/Stages/NormalDepth.js +12 -8
  110. package/src/Stages/PathTracer.js +338 -43
  111. package/src/Stages/PathTracerStage.js +69 -66
  112. package/src/Stages/Variance.js +1 -1
  113. package/src/Storage/BLASCache.js +1 -1
  114. package/src/Storage/CDFCache.js +18 -17
  115. package/src/Storage/DownloadCache.js +1 -1
  116. package/src/Storage/SceneGraphCodec.js +46 -3
  117. package/src/Storage/SpillStore.js +1 -1
  118. package/src/Storage/StorageManager.js +45 -9
  119. package/src/Storage/areas.js +4 -0
  120. package/src/TSL/BVHTraversal.js +50 -25
  121. package/src/TSL/Bidirectional.js +204 -0
  122. package/src/TSL/BidirectionalLamps.js +132 -0
  123. package/src/TSL/Clearcoat.js +15 -6
  124. package/src/TSL/Common.js +20 -15
  125. package/src/TSL/ConnectKernel.js +147 -0
  126. package/src/TSL/Debugger.js +1 -1
  127. package/src/TSL/Displacement.js +4 -3
  128. package/src/TSL/EmissiveSampling.js +65 -18
  129. package/src/TSL/Environment.js +77 -106
  130. package/src/TSL/EnvironmentCDF.js +85 -39
  131. package/src/TSL/ExtendKernel.js +8 -3
  132. package/src/TSL/FinalWriteKernel.js +4 -4
  133. package/src/TSL/GenerateKernel.js +18 -4
  134. package/src/TSL/HitFacet.js +35 -5
  135. package/src/TSL/LightBVHSampling.js +40 -9
  136. package/src/TSL/LightGenerateKernel.js +293 -0
  137. package/src/TSL/LightGuide.js +198 -0
  138. package/src/TSL/LightSplatKernel.js +165 -0
  139. package/src/TSL/LightVisibility.js +111 -0
  140. package/src/TSL/LightsCore.js +13 -38
  141. package/src/TSL/LightsDirect.js +28 -29
  142. package/src/TSL/LightsIndirect.js +3 -1
  143. package/src/TSL/LightsSampling.js +239 -51
  144. package/src/TSL/MaterialEvaluation.js +113 -31
  145. package/src/TSL/MaterialProperties.js +44 -17
  146. package/src/TSL/MaterialTransmission.js +52 -47
  147. package/src/TSL/MergeKernel.js +218 -0
  148. package/src/TSL/PathTracerCore.js +65 -29
  149. package/src/TSL/Random.js +5 -1
  150. package/src/TSL/SceneResources.js +51 -0
  151. package/src/TSL/ShadeKernel.js +964 -101
  152. package/src/TSL/Struct.js +10 -0
  153. package/src/TSL/Sun.js +1 -1
  154. package/src/TSL/TextureSampling.js +66 -61
  155. package/src/addons/archives.js +14 -0
  156. package/src/addons/bidirectional.js +13 -0
  157. package/src/addons/color.js +32 -0
  158. package/src/addons/formats.js +16 -0
  159. package/src/addons/physicalSky.js +14 -0
  160. package/src/addons/storage.js +19 -0
  161. package/src/core.js +31 -0
  162. package/src/index.js +13 -23
  163. package/src/integrators/BidirectionalIntegrator.js +833 -0
  164. package/src/managers/AnimationManager.js +88 -6
  165. package/src/managers/CameraManager.js +103 -46
  166. package/src/managers/DenoisingManager.js +14 -10
  167. package/src/managers/EnvironmentManager.js +125 -103
  168. package/src/managers/GoboManager.js +9 -0
  169. package/src/managers/MaterialDataManager.js +48 -37
  170. package/src/managers/UniformManager.js +32 -10
  171. package/dist/ToneMapGPU-B6XIe3DU.js +0 -544
  172. package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
  173. package/src/Processor/TreeletOptimizer.js +0 -574
@@ -8,6 +8,8 @@ export const EngineEvents = {
8
8
  RENDER_COMPLETE: 'engine:renderComplete',
9
9
  RENDER_RESET: 'engine:renderReset',
10
10
  FRAME: 'engine:frame',
11
+ // The path tracer compiles its shaders in the background ({ compiling }); the canvas keeps its last frame meanwhile.
12
+ SHADERS_COMPILING: 'engine:shadersCompiling',
11
13
 
12
14
  // Denoiser
13
15
  DENOISING_START: 'engine:denoisingStart',
@@ -32,7 +32,9 @@ export const ISSUE_CODES = Object.freeze( {
32
32
  STORAGE_CACHE_MISMATCH: 'storage.cache_mismatch',
33
33
  DENOISER_UNAVAILABLE: 'denoiser.unavailable',
34
34
  OUTPUT_SOURCE_FALLBACK: 'output.source_fallback',
35
+ OUTPUT_TONEMAP_FALLBACK: 'output.tonemap_fallback',
35
36
  LIGHT_PLACEHOLDER_SKIPPED: 'light.placeholder_skipped',
37
+ CAPABILITY_MISSING: 'capability.missing',
36
38
  } );
37
39
 
38
40
  /** Strict throws on ERROR only. */
package/src/Headless.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The supported way to render without a person watching. Defaults are the batch renderer's —
3
- * strict, physical profile, deterministic — all reversible, none reversible by accident.
3
+ * strict, deterministic — all reversible, none reversible by accident.
4
4
  *
5
5
  * @example
6
6
  * const shot = await renderHeadless( { model: url, width: 1920, height: 1080, samples: 256 } );
@@ -20,11 +20,10 @@ import { PathTracerApp } from './PathTracerApp.js';
20
20
  * @param {number} [options.samples=64] - samples to accumulate
21
21
  * @param {'linear'|'srgb'} [options.colorSpace='srgb']
22
22
  * @param {boolean} [options.strict=true]
23
- * @param {string} [options.profile='physical'] - see RENDER_PROFILES
24
23
  * @param {boolean} [options.deterministic=true]
25
24
  * @param {boolean} [options.allowEarlyRetire=false] - needs `deterministic: false` to be reachable
26
25
  * @param {boolean} [options.denoise=false] - one final OIDN pass, read back instead of the raw accumulation
27
- * @param {Object} [options.settings] - applied after the model loads
26
+ * @param {Object} [options.settings] - applied before the model loads and again after it
28
27
  * @param {function(number): void} [options.onProgress] - running sample count
29
28
  * @returns {Promise<{data: Float32Array|Uint8ClampedArray, width: number, height: number,
30
29
  * colorSpace: string, source: string, samples: number, retiredBy: string, issues: Object[], adapter: Object,
@@ -95,7 +94,6 @@ export async function openHeadless( {
95
94
  width = 1920,
96
95
  height = 1080,
97
96
  strict = true,
98
- profile = 'physical',
99
97
  deterministic = true,
100
98
  settings = null,
101
99
  storage = false,
@@ -103,7 +101,7 @@ export async function openHeadless( {
103
101
  } = {} ) {
104
102
 
105
103
  // Off unless asked for: cached state from an earlier run must not change what a batch renders.
106
- const app = new PathTracerApp( canvas ?? null, { autoResize: false, strict, profile, storage, hostMemoryGB } );
104
+ const app = new PathTracerApp( canvas ?? null, { autoResize: false, strict, storage, hostMemoryGB } );
107
105
 
108
106
  try {
109
107
 
@@ -114,7 +112,10 @@ export async function openHeadless( {
114
112
  await app.init();
115
113
  app.setCanvasSize( width, height );
116
114
 
115
+ // Before the load as well: some are read while a model loads (areaLightIntensityScale).
116
+ if ( settings ) app.settings.setMany( settings, { silent: true } );
117
117
  if ( model ) await app.loadModel( model );
118
+ // Again after it, over anything the model's metadata set.
118
119
  if ( settings ) app.settings.setMany( settings, { silent: true } );
119
120
 
120
121
  // After the model: scene metadata can turn adaptive sampling back on.
@@ -1,15 +1,8 @@
1
1
  import { EventDispatcher, ACESFilmicToneMapping } from 'three';
2
- import { TONE_MAP_FNS, SRGB_GAMMA, applySaturation, effectiveExposure, isOutputEncoded } from '../Processor/ToneMapCPU.js';
2
+ import { PackedToneMapper } from '../Processor/ToneMapGPU.js';
3
3
  import { getAssetConfig } from '../AssetConfig.js';
4
4
  import AIUpscalerWorker from '../Processor/Workers/AIUpscalerWorker.js?worker&inline';
5
5
 
6
-
7
- // An OCIO view already returned colour encoded for its display, so the gamma step below would
8
- // encode it twice. A built-in curve returns linear and does want it.
9
- const displayEncoder = toneMapping => isOutputEncoded( toneMapping )
10
- ? ( c => ( c > 1 ? 1 : c < 0 ? 0 : c ) )
11
- : ( c => Math.pow( c, SRGB_GAMMA ) );
12
-
13
6
  // ─── Model Configuration ───────────────────────────────────────────────────────
14
7
  // Quality presets reference relative paths against asset-config `upscalerModelBaseUrl`.
15
8
 
@@ -140,13 +133,8 @@ export class AIUpscaler extends EventDispatcher {
140
133
  this._baseWidth = output.width;
141
134
  this._baseHeight = output.height;
142
135
 
143
- // Pooled HDR readback staging buffer — reused across _captureSourceHDR calls.
144
- // Rebuilt only when the source texture dimensions change (same spirit as
145
- // r184's ReadbackBuffer; we can't use renderer.getArrayBufferAsync directly
146
- // because our source is a raw GPUTexture, not a Three.js BufferAttribute).
147
- this._hdrStagingBuffer = null;
148
- this._hdrStagingWidth = 0;
149
- this._hdrStagingHeight = 0;
136
+ // Tone-maps the HDR source on the card; its buffers are released after each capture.
137
+ this._toneMapper = null;
150
138
 
151
139
  }
152
140
 
@@ -283,7 +271,7 @@ export class AIUpscaler extends EventDispatcher {
283
271
  // so we must grab the pixels before awaiting model load.
284
272
  if ( this.hdr && this.getGPUTextures ) {
285
273
 
286
- // HDR path: read float32 from GPU texture, tonemap happens after upscale
274
+ // HDR path: float planes tone-mapped on the GPU, no 8-bit step before the network
287
275
  this._capturedSource = await this._captureSourceHDR();
288
276
 
289
277
  } else {
@@ -403,17 +391,6 @@ export class AIUpscaler extends EventDispatcher {
403
391
  // The SR model outputs RGB only — alpha would be lost without this.
404
392
  this._cacheUpscaledAlpha( sourceImageData, srcW * scale, srcH * scale );
405
393
 
406
- // Cache HDR tonemapping state for tile extraction (avoids per-pixel lookups)
407
- if ( sourceImageData.isHDR ) {
408
-
409
- this._hdrToneMapFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
410
- this._hdrEncode = displayEncoder( this.getToneMapping() );
411
- this._hdrExposure = effectiveExposure( this.getExposure(), this.getToneMapping() );
412
- this._hdrSaturation = this.getSaturation();
413
- this._tmOut = new Float32Array( 3 );
414
-
415
- }
416
-
417
394
  // Tile-based inference
418
395
  const overlap = MODEL_CONFIG.TILE_OVERLAP;
419
396
  const tileSize = this.tileSize;
@@ -503,121 +480,72 @@ export class AIUpscaler extends EventDispatcher {
503
480
  }
504
481
 
505
482
  /**
506
- * HDR capture: reads float32 color data directly from the path tracer's GPU texture.
507
- * Returns an ImageData-like object with float32 RGBA data and dimensions.
483
+ * HDR capture: tone-maps the path tracer's float colour on the card and reads back float
484
+ * planes — r, g, b, a — so the network gets the curve without 8-bit quantization.
508
485
  */
509
486
  async _captureSourceHDR() {
510
487
 
511
488
  const gpuTextures = this.getGPUTextures();
512
489
  if ( ! gpuTextures?.color ) throw new Error( 'No GPU color texture available for HDR capture' );
513
490
 
514
- const device = this.renderer.backend.device;
515
491
  const colorTexture = gpuTextures.color;
516
- const width = colorTexture.width;
517
- const height = colorTexture.height;
518
-
519
- // GPU texture → pooled staging buffer → CPU readback.
520
- // The staging buffer is kept alive between calls (unmap, don't destroy)
521
- // and only re-created when texture dimensions change.
522
- const bytesPerRow = Math.ceil( width * 16 / 256 ) * 256; // rgba32float=16 bytes, aligned to 256
523
- const bufferSize = bytesPerRow * height;
524
-
525
- if ( this._hdrStagingWidth !== width || this._hdrStagingHeight !== height ) {
526
-
527
- this._hdrStagingBuffer?.destroy();
528
- this._hdrStagingBuffer = device.createBuffer( {
529
- label: 'aiupscaler-hdr-readback',
530
- size: bufferSize,
531
- usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
532
- } );
533
- this._hdrStagingWidth = width;
534
- this._hdrStagingHeight = height;
535
-
536
- }
537
-
538
- const stagingBuffer = this._hdrStagingBuffer;
539
-
540
- const encoder = device.createCommandEncoder();
541
- encoder.copyTextureToBuffer(
542
- { texture: colorTexture },
543
- { buffer: stagingBuffer, bytesPerRow, rowsPerImage: height },
544
- { width, height, depthOrArrayLayers: 1 }
545
- );
546
- device.queue.submit( [ encoder.finish() ] );
547
-
548
- await stagingBuffer.mapAsync( GPUMapMode.READ );
549
- const mappedData = new Float32Array( stagingBuffer.getMappedRange() );
550
-
551
- // Copy to ImageData-like structure (handle row alignment padding)
552
- const pixelFloats = width * 4;
553
- const rowFloats = bytesPerRow / 4;
554
- const data = new Float32Array( width * height * 4 );
555
-
556
- for ( let y = 0; y < height; y ++ ) {
557
-
558
- const srcOffset = y * rowFloats;
559
- const dstOffset = y * pixelFloats;
560
- data.set( mappedData.subarray( srcOffset, srcOffset + pixelFloats ), dstOffset );
561
-
562
- }
563
-
564
- stagingBuffer.unmap();
492
+ const { width, height } = colorTexture;
493
+
494
+ this._toneMapper ??= new PackedToneMapper( this.renderer.backend.device, 'aiupscaler-tonemap', { input: 'texture', output: 'planar' } );
495
+ this._toneMapper.ensureSize( width, height );
496
+ const planes = await this._toneMapper.toPlanar( colorTexture, {
497
+ exposure: this.getExposure(),
498
+ toneMapping: this.getToneMapping() ?? ACESFilmicToneMapping,
499
+ saturation: this.getSaturation(),
500
+ } );
501
+ this._toneMapper.release();
565
502
 
566
- // Mark as HDR so _extractTile and _tensorToImageData handle it correctly
567
- return { data, width, height, isHDR: true };
503
+ return { planes, width, height, isHDR: true };
568
504
 
569
505
  }
570
506
 
571
507
  /**
572
508
  * Extracts a tile region from the source image.
573
509
  * Returns a Float32Array in NCHW format [1, 3, H, W] with values in [0, 1].
574
- * Handles both LDR (uint8 ImageData) and HDR (float32) sources.
510
+ * Handles both LDR (uint8 ImageData) and HDR (tone-mapped float planes) sources.
575
511
  */
576
512
  _extractTile( sourceImageData, x, y, w, h ) {
577
513
 
578
- const { data, width } = sourceImageData;
579
- const isHDR = sourceImageData.isHDR;
514
+ const { width } = sourceImageData;
580
515
  const pixelCount = w * h;
581
516
  const floats = new Float32Array( 3 * pixelCount );
582
517
 
583
- for ( let row = 0; row < h; row ++ ) {
518
+ if ( sourceImageData.isHDR ) {
584
519
 
585
- for ( let col = 0; col < w; col ++ ) {
520
+ const { planes } = sourceImageData;
521
+ const plane = width * sourceImageData.height;
522
+ for ( let c = 0; c < 3; c ++ ) {
586
523
 
587
- const srcIdx = ( ( y + row ) * width + ( x + col ) ) * 4;
588
- const dstIdx = row * w + col;
524
+ for ( let row = 0; row < h; row ++ ) {
589
525
 
590
- if ( isHDR ) {
526
+ const from = c * plane + ( y + row ) * width + x;
527
+ floats.set( planes.subarray( from, from + w ), c * pixelCount + row * w );
591
528
 
592
- // HDR: exposure + saturation + tonemap + gamma to sRGB [0,1] at float32 precision.
593
- // The SR model expects sRGB-range input — we tonemap before the model
594
- // but keep float32 precision (no uint8 quantization bottleneck).
595
- const tmFn = this._hdrToneMapFn;
596
- const exposure = this._hdrExposure;
597
- const saturation = this._hdrSaturation;
598
- let er = data[ srcIdx ] * exposure, eg = data[ srcIdx + 1 ] * exposure, eb = data[ srcIdx + 2 ] * exposure;
599
- if ( saturation !== 1.0 ) {
529
+ }
600
530
 
601
- this._tmOut[ 0 ] = er; this._tmOut[ 1 ] = eg; this._tmOut[ 2 ] = eb;
602
- applySaturation( this._tmOut, saturation );
603
- er = this._tmOut[ 0 ]; eg = this._tmOut[ 1 ]; eb = this._tmOut[ 2 ];
531
+ }
604
532
 
605
- }
533
+ return floats;
534
+
535
+ }
606
536
 
607
- tmFn( er, eg, eb, 1.0, this._tmOut );
608
- const encode = this._hdrEncode;
609
- floats[ dstIdx ] = encode( this._tmOut[ 0 ] );
610
- floats[ pixelCount + dstIdx ] = encode( this._tmOut[ 1 ] );
611
- floats[ 2 * pixelCount + dstIdx ] = encode( this._tmOut[ 2 ] );
537
+ const { data } = sourceImageData;
538
+ for ( let row = 0; row < h; row ++ ) {
612
539
 
613
- } else {
540
+ for ( let col = 0; col < w; col ++ ) {
614
541
 
615
- // LDR: normalize uint8 [0,255] to [0,1]
616
- floats[ dstIdx ] = data[ srcIdx ] / 255;
617
- floats[ pixelCount + dstIdx ] = data[ srcIdx + 1 ] / 255;
618
- floats[ 2 * pixelCount + dstIdx ] = data[ srcIdx + 2 ] / 255;
542
+ const srcIdx = ( ( y + row ) * width + ( x + col ) ) * 4;
543
+ const dstIdx = row * w + col;
619
544
 
620
- }
545
+ // LDR: normalize uint8 [0,255] to [0,1]
546
+ floats[ dstIdx ] = data[ srcIdx ] / 255;
547
+ floats[ pixelCount + dstIdx ] = data[ srcIdx + 1 ] / 255;
548
+ floats[ 2 * pixelCount + dstIdx ] = data[ srcIdx + 2 ] / 255;
621
549
 
622
550
  }
623
551
 
@@ -715,14 +643,19 @@ export class AIUpscaler extends EventDispatcher {
715
643
  */
716
644
  _cacheUpscaledAlpha( sourceImageData, outW, outH ) {
717
645
 
718
- const { data, width, height } = sourceImageData;
646
+ const { width, height } = sourceImageData;
647
+ const count = width * height;
719
648
 
649
+ // HDR alpha is the fourth plane, LDR every fourth byte.
720
650
  const isHDR = sourceImageData.isHDR;
651
+ const data = isHDR ? sourceImageData.planes.subarray( 3 * count ) : sourceImageData.data;
652
+ const stride = isHDR ? 1 : 4;
653
+ const offset = isHDR ? 0 : 3;
721
654
  const opaqueVal = isHDR ? 1.0 : 255;
722
655
 
723
656
  // Check if source has any non-opaque pixels
724
657
  let hasAlpha = false;
725
- for ( let i = 3; i < data.length; i += 4 ) {
658
+ for ( let i = offset; i < data.length; i += stride ) {
726
659
 
727
660
  if ( data[ i ] < opaqueVal ) {
728
661
 
@@ -748,10 +681,10 @@ export class AIUpscaler extends EventDispatcher {
748
681
  const srcCtx = srcCanvas.getContext( '2d' );
749
682
  const alphaImage = srcCtx.createImageData( width, height );
750
683
 
751
- for ( let i = 0, len = width * height; i < len; i ++ ) {
684
+ for ( let i = 0; i < count; i ++ ) {
752
685
 
753
686
  const a = isHDR
754
- ? Math.min( Math.max( data[ i * 4 + 3 ] * 255, 0 ), 255 ) | 0
687
+ ? Math.min( Math.max( data[ i ] * 255, 0 ), 255 ) | 0
755
688
  : data[ i * 4 + 3 ];
756
689
  alphaImage.data[ i * 4 ] = a;
757
690
  alphaImage.data[ i * 4 + 1 ] = a;
@@ -797,32 +730,18 @@ export class AIUpscaler extends EventDispatcher {
797
730
 
798
731
  if ( sourceImageData.isHDR ) {
799
732
 
800
- // HDR source: tonemap to LDR for the backup canvas display
801
- const { data, width, height } = sourceImageData;
733
+ // Already tone-mapped and encoded on the card; only quantized here.
734
+ const { planes, width, height } = sourceImageData;
735
+ const count = width * height;
802
736
  const imageData = ctx.createImageData( width, height );
803
737
  const pixels = imageData.data;
804
- const tmFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
805
- const exposure = effectiveExposure( this.getExposure(), this.getToneMapping() );
806
- const encode = displayEncoder( this.getToneMapping() );
807
- const saturation = this.getSaturation();
808
- const out = new Float32Array( 3 );
809
738
 
810
- for ( let i = 0, len = width * height; i < len; i ++ ) {
739
+ for ( let i = 0; i < count; i ++ ) {
811
740
 
812
741
  const si = i * 4;
813
- let er = data[ si ] * exposure, eg = data[ si + 1 ] * exposure, eb = data[ si + 2 ] * exposure;
814
- if ( saturation !== 1.0 ) {
815
-
816
- out[ 0 ] = er; out[ 1 ] = eg; out[ 2 ] = eb;
817
- applySaturation( out, saturation );
818
- er = out[ 0 ]; eg = out[ 1 ]; eb = out[ 2 ];
819
-
820
- }
821
-
822
- tmFn( er, eg, eb, 1.0, out );
823
- pixels[ si ] = ( encode( out[ 0 ] ) * 255 + 0.5 ) | 0;
824
- pixels[ si + 1 ] = ( encode( out[ 1 ] ) * 255 + 0.5 ) | 0;
825
- pixels[ si + 2 ] = ( encode( out[ 2 ] ) * 255 + 0.5 ) | 0;
742
+ pixels[ si ] = ( planes[ i ] * 255 + 0.5 ) | 0;
743
+ pixels[ si + 1 ] = ( planes[ count + i ] * 255 + 0.5 ) | 0;
744
+ pixels[ si + 2 ] = ( planes[ 2 * count + i ] * 255 + 0.5 ) | 0;
826
745
  pixels[ si + 3 ] = 255;
827
746
 
828
747
  }
@@ -927,10 +846,8 @@ export class AIUpscaler extends EventDispatcher {
927
846
  this._upscaledAlpha = null;
928
847
  this.state.abortController = null;
929
848
 
930
- this._hdrStagingBuffer?.destroy();
931
- this._hdrStagingBuffer = null;
932
- this._hdrStagingWidth = 0;
933
- this._hdrStagingHeight = 0;
849
+ this._toneMapper?.dispose();
850
+ this._toneMapper = null;
934
851
 
935
852
  console.log( 'AIUpscaler disposed' );
936
853
 
@@ -182,6 +182,20 @@ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
182
182
  }
183
183
  `;
184
184
 
185
+ const PACK_WG_SIZE = 8;
186
+ const PACK_WGSL = /* wgsl */`
187
+ @group(0) @binding(0) var src: texture_2d<f32>;
188
+ @group(0) @binding(1) var<storage, read_write> dst: array<vec4<f32>>;
189
+ @group(0) @binding(2) var<uniform> size: vec2<u32>;
190
+
191
+ @compute @workgroup_size(${PACK_WG_SIZE}, ${PACK_WG_SIZE})
192
+ fn main( @builtin(global_invocation_id) gid: vec3<u32> ) {
193
+
194
+ if ( gid.x >= size.x || gid.y >= size.y ) { return; }
195
+ dst[ gid.y * size.x + gid.x ] = textureLoad( src, gid.xy, 0 );
196
+ }
197
+ `;
198
+
185
199
  export class OIDNDenoiser extends EventDispatcher {
186
200
 
187
201
  /**
@@ -210,11 +224,10 @@ export class OIDNDenoiser extends EventDispatcher {
210
224
  // Cached GPU storage buffers for texture→buffer copies (reused across denoise calls)
211
225
  this._gpuInputBuffers = { color: null, albedo: null, normal: null };
212
226
  this._gpuInputBufferSize = { width: 0, height: 0 };
213
- // Shared pad-strip buffer for non-256-aligned widths. Reused across
214
- // color/albedo/normal copies within the same encoder (WebGPU command
215
- // order guarantees the overwrites are serialized).
216
- this._gpuInputPadBuffer = null;
217
- this._gpuInputPaddedRowBytes = 0;
227
+ // Packs widths copyTextureToBuffer cannot take; see _beginPack.
228
+ this._packPipeline = null;
229
+ this._packLayout = null;
230
+ this._packParams = null;
218
231
  // The autoexposure scale never leaves the GPU — _applyColorScale and the output pack
219
232
  // both read _inputScaleBuffer directly.
220
233
  this._lumReducePipeline = null;
@@ -671,42 +684,62 @@ export class OIDNDenoiser extends EventDispatcher {
671
684
  // Ensure storage buffers are sized correctly (recreate on resolution change)
672
685
  this._ensureGPUInputBuffers( width, height );
673
686
 
674
- // Copy render target textures → tightly packed GPU storage buffers for oidn-web.
675
- // copyTextureToBuffer requires bytesPerRow to be a multiple of 256. When the tight
676
- // row size (width * 16) isn't aligned, copy via a shared pre-allocated padded buffer
677
- // (see _ensureGPUInputBuffers) then strip padding row-by-row. The pad buffer is
678
- // reused across color/albedo/normal — safe because WebGPU serializes commands
679
- // within a single encoder.
687
+ this._copyInputs( device, textures, width, height );
688
+
689
+ // Autoexposure and the pre-multiply it drives, both on the GPU and both queued behind the
690
+ // 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 );
693
+
694
+ // A final denoise is shown tile by tile, so the tiles still to come must show this render
695
+ // rather than the blank or previous-view picture the output holds between runs.
696
+ if ( ! continuous && ! this._hasOutput ) {
697
+
698
+ this._unpackToTexture( this._gpuInputBuffers.color, width, { x: 0, y: 0, width, height }, false );
699
+
700
+ }
701
+
702
+ // Pass GPU storage buffers to oidn-web (GPUBuffer path, well-tested)
703
+ 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 }
707
+ };
708
+
709
+ return this._executeWithAbortGPU( config, continuous );
710
+
711
+ }
712
+
713
+ /** Copies the render's colour, albedo and normal into the tightly packed buffers oidn-web reads. @private */
714
+ _copyInputs( device, textures, width, height ) {
715
+
716
+ // copyTextureToBuffer takes only 256-byte-aligned rows; any other width packs on the GPU.
680
717
  const encoder = device.createCommandEncoder( { label: 'oidn-tex-to-buf' } );
681
718
  const tightRowBytes = width * 16; // rgba32float
682
- const paddedRowBytes = this._gpuInputPaddedRowBytes;
683
- const needsPadStrip = paddedRowBytes > tightRowBytes;
684
- const padBuf = this._gpuInputPadBuffer;
719
+ const pass = tightRowBytes % 256 === 0 ? null : this._beginPack( device, encoder, width, height );
685
720
 
686
721
  const copyTex = ( tex, tightBuf ) => {
687
722
 
688
- if ( ! needsPadStrip ) {
723
+ if ( pass ) {
689
724
 
690
- encoder.copyTextureToBuffer(
691
- { texture: tex, mipLevel: 0 },
692
- { buffer: tightBuf, offset: 0, bytesPerRow: tightRowBytes, rowsPerImage: height },
693
- { width, height, depthOrArrayLayers: 1 }
694
- );
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 ) );
695
734
 
696
735
  } else {
697
736
 
698
737
  encoder.copyTextureToBuffer(
699
738
  { texture: tex, mipLevel: 0 },
700
- { buffer: padBuf, offset: 0, bytesPerRow: paddedRowBytes, rowsPerImage: height },
739
+ { buffer: tightBuf, offset: 0, bytesPerRow: tightRowBytes, rowsPerImage: height },
701
740
  { width, height, depthOrArrayLayers: 1 }
702
741
  );
703
742
 
704
- for ( let row = 0; row < height; row ++ ) {
705
-
706
- encoder.copyBufferToBuffer( padBuf, row * paddedRowBytes, tightBuf, row * tightRowBytes, tightRowBytes );
707
-
708
- }
709
-
710
743
  }
711
744
 
712
745
  };
@@ -719,30 +752,9 @@ export class OIDNDenoiser extends EventDispatcher {
719
752
  // measured 0.878 -> 2.439 denoise ratio at 64 spp. Do not "fix" it.
720
753
  copyTex( textures.normal, this._gpuInputBuffers.normal );
721
754
 
755
+ pass?.end();
722
756
  device.queue.submit( [ encoder.finish() ] );
723
757
 
724
- // Autoexposure and the pre-multiply it drives, both on the GPU and both queued behind the
725
- // copies above. Nothing is awaited here — an await would drain the queue before the UNet.
726
- this._computeInputScale( device, width * height );
727
- this._applyColorScale( device, width * height );
728
-
729
- // A final denoise is shown tile by tile, so the tiles still to come must show this render
730
- // rather than the blank or previous-view picture the output holds between runs.
731
- if ( ! continuous && ! this._hasOutput ) {
732
-
733
- this._unpackToTexture( this._gpuInputBuffers.color, width, { x: 0, y: 0, width, height }, false );
734
-
735
- }
736
-
737
- // Pass GPU storage buffers to oidn-web (GPUBuffer path, well-tested)
738
- const config = {
739
- color: { data: this._gpuInputBuffers.color, width, height },
740
- albedo: { data: this._gpuInputBuffers.albedo, width, height },
741
- normal: { data: this._gpuInputBuffers.normal, width, height }
742
- };
743
-
744
- return this._executeWithAbortGPU( config, continuous );
745
-
746
758
  }
747
759
 
748
760
  /**
@@ -767,25 +779,41 @@ export class OIDNDenoiser extends EventDispatcher {
767
779
  this._gpuInputBuffers.normal = device.createBuffer( { label: 'oidn-in-normal', size: byteSize, usage } );
768
780
  this._gpuInputBufferSize = { width, height };
769
781
 
770
- // Pre-allocate the row-pad staging buffer when width * 16 isn't 256-aligned.
771
- // Shared across the three texture copies; recreated only on resolution change.
772
- const tightRowBytes = width * 16;
773
- const paddedRowBytes = Math.ceil( tightRowBytes / 256 ) * 256;
774
- if ( paddedRowBytes !== tightRowBytes ) {
782
+ }
783
+
784
+ /** Opens the compute pass that packs unaligned rows; one dispatch per texture follows. @private */
785
+ _beginPack( device, encoder, width, height ) {
786
+
787
+ if ( ! this._packPipeline ) {
775
788
 
776
- this._gpuInputPadBuffer = device.createBuffer( {
777
- label: 'oidn-in-pad',
778
- size: paddedRowBytes * height,
779
- usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
789
+ // Explicit: rgba32float is unfilterable, which an auto layout would not declare.
790
+ this._packLayout = device.createBindGroupLayout( {
791
+ label: 'oidn-pack',
792
+ entries: [
793
+ { binding: 0, visibility: GPUShaderStage.COMPUTE, texture: { sampleType: 'unfilterable-float' } },
794
+ { binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
795
+ { binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
796
+ ],
780
797
  } );
781
- this._gpuInputPaddedRowBytes = paddedRowBytes;
798
+ this._packPipeline = device.createComputePipeline( {
799
+ label: 'oidn-pack',
800
+ layout: device.createPipelineLayout( { bindGroupLayouts: [ this._packLayout ] } ),
801
+ compute: { module: device.createShaderModule( { label: 'oidn-pack', code: PACK_WGSL } ), entryPoint: 'main' },
802
+ } );
803
+
804
+ }
782
805
 
783
- } else {
806
+ if ( ! this._packParams ) {
784
807
 
785
- this._gpuInputPaddedRowBytes = tightRowBytes;
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 ] ) );
786
810
 
787
811
  }
788
812
 
813
+ const pass = encoder.beginComputePass( { label: 'oidn-pack' } );
814
+ pass.setPipeline( this._packPipeline );
815
+ return pass;
816
+
789
817
  }
790
818
 
791
819
  _destroyGPUInputBuffers() {
@@ -793,7 +821,7 @@ export class OIDNDenoiser extends EventDispatcher {
793
821
  this._gpuInputBuffers.color?.destroy();
794
822
  this._gpuInputBuffers.albedo?.destroy();
795
823
  this._gpuInputBuffers.normal?.destroy();
796
- this._gpuInputPadBuffer?.destroy();
824
+ this._packParams?.destroy();
797
825
 
798
826
  // Bind groups hold the destroyed buffers; pipelines are device-scoped and survive.
799
827
  this._colorScaleBindGroup = null;
@@ -805,8 +833,7 @@ export class OIDNDenoiser extends EventDispatcher {
805
833
  this._inputScaleBuffer?.destroy();
806
834
  this._inputScaleBuffer = null;
807
835
  this._gpuInputBuffers = { color: null, albedo: null, normal: null };
808
- this._gpuInputPadBuffer = null;
809
- this._gpuInputPaddedRowBytes = 0;
836
+ this._packParams = null;
810
837
  this._gpuInputBufferSize = { width: 0, height: 0 };
811
838
 
812
839
  }
@@ -994,7 +1021,7 @@ export class OIDNDenoiser extends EventDispatcher {
994
1021
 
995
1022
  const { color, albedo, normal } = this._gpuInputBuffers;
996
1023
  const out = this._outGPUTexture ? this._outTexSize.width * this._outTexSize.height * 8 : 0; // rgba16float
997
- return ( color?.size ?? 0 ) + ( albedo?.size ?? 0 ) + ( normal?.size ?? 0 ) + ( this._gpuInputPadBuffer?.size ?? 0 ) + out;
1024
+ return ( color?.size ?? 0 ) + ( albedo?.size ?? 0 ) + ( normal?.size ?? 0 ) + out;
998
1025
 
999
1026
  }
1000
1027
 
@@ -1,4 +1,4 @@
1
- import { GBUFFER_MISS_THRESHOLD } from '../EngineDefaults.js';
1
+ import { GBUFFER_MISS_THRESHOLD } from '../Stages/DenoiserSettings.js';
2
2
 
3
3
  /**
4
4
  * History of the frames the path tracer restarted while the view moved, reprojected into the