rayzee 7.16.0 → 7.18.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 (47) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/BVHSubtreeWorker-D7KMbrQ-.js +2 -0
  3. package/dist/assets/BVHSubtreeWorker-D7KMbrQ-.js.map +1 -0
  4. package/dist/assets/BVHWorker-DOaGn6uN.js +2 -0
  5. package/dist/assets/BVHWorker-DOaGn6uN.js.map +1 -0
  6. package/dist/rayzee.es.js +2817 -2537
  7. package/dist/rayzee.es.js.map +1 -1
  8. package/dist/rayzee.umd.js +55 -55
  9. package/dist/rayzee.umd.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/EngineDefaults.js +27 -11
  12. package/src/Passes/OIDNDenoiser.js +17 -14
  13. package/src/PathTracerApp.js +86 -13
  14. package/src/Processor/BVHBuilder.js +11 -7
  15. package/src/Processor/BuildTimer.js +20 -6
  16. package/src/Processor/EmissiveTriangleBuilder.js +11 -7
  17. package/src/Processor/GeometryExtractor.js +11 -5
  18. package/src/Processor/KernelManager.js +48 -1
  19. package/src/Processor/PackedRayBuffer.js +69 -35
  20. package/src/Processor/ParallelBVHBuilder.js +9 -6
  21. package/src/Processor/QueueManager.js +6 -8
  22. package/src/Processor/SceneProcessor.js +47 -21
  23. package/src/Processor/ShaderBuilder.js +4 -1
  24. package/src/Processor/StorageTexturePool.js +4 -1
  25. package/src/Processor/Workers/BVHWorker.js +12 -6
  26. package/src/RenderSettings.js +1 -1
  27. package/src/Stages/ASVGF.js +38 -0
  28. package/src/Stages/BilateralFilter.js +23 -0
  29. package/src/Stages/EdgeFilter.js +18 -0
  30. package/src/Stages/MotionVector.js +23 -0
  31. package/src/Stages/NormalDepth.js +22 -0
  32. package/src/Stages/PathTracer.js +324 -169
  33. package/src/Stages/PathTracerStage.js +17 -14
  34. package/src/Stages/Variance.js +23 -0
  35. package/src/TSL/FinalWriteKernel.js +30 -18
  36. package/src/TSL/GenerateKernel.js +28 -17
  37. package/src/TSL/ShadeKernel.js +186 -87
  38. package/src/TSL/TextureSampling.js +12 -2
  39. package/src/index.js +3 -0
  40. package/src/managers/EnvironmentManager.js +12 -8
  41. package/src/managers/MaterialDataManager.js +12 -9
  42. package/src/managers/UniformManager.js +1 -1
  43. package/src/utils/Logger.js +291 -0
  44. package/dist/assets/BVHSubtreeWorker-sNzvxn66.js +0 -2
  45. package/dist/assets/BVHSubtreeWorker-sNzvxn66.js.map +0 -1
  46. package/dist/assets/BVHWorker-CiVdFrwe.js +0 -2
  47. package/dist/assets/BVHWorker-CiVdFrwe.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.16.0",
3
+ "version": "7.18.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -84,15 +84,16 @@ export const ENGINE_DEFAULTS = {
84
84
  transmissiveBounces: 5,
85
85
  maxSubsurfaceSteps: 8, // interactive default: low cap (bounded random-walk SSS)
86
86
 
87
+ maxTransparentBounces: 32, // guard: alpha skips are free bounces, else cutout foliage eats the loop budget
88
+
87
89
  // Adaptive sampling (Blender-style): stop the frame once enough pixels drop below the noise threshold.
88
90
  useAdaptiveSampling: true,
89
- noiseThreshold: 0.02, // per-pixel noise below which a pixel is converged
90
- darkNoiseFloor: 0.003, // extra absolute-noise floor so dark pixels can converge too
91
+ noiseThreshold: 0.02, // √-luminance-normalized per-pixel noise below which a pixel is converged
91
92
  adaptiveMinSamples: 8, // min samples before adaptive sampling can trigger
92
93
  adaptiveStopFraction: 0.95, // retire the frame once this fraction of pixels has converged
93
94
  // Per-pixel freeze: skip tracing pixels that individually converged (noise threshold only — no dark floor,
94
95
  // which would bake dim regions too dark). Naturally engages only on static/idle views.
95
- usePixelFreeze: false,
96
+ usePixelFreeze: true,
96
97
  pixelFreezeThreshold: 0.02, // per-pixel noise below which a pixel becomes a freeze candidate
97
98
  pixelFreezeStability: 8, // consecutive candidate frames before a pixel freezes
98
99
 
@@ -184,11 +185,26 @@ export const ENGINE_DEFAULTS = {
184
185
  // only round-trip exactly when both sides agree.
185
186
  export const ALBEDO_EPS = 0.01;
186
187
 
187
- // Per-resolution compute StorageTextures are pre-allocated at this size and never
188
- // resized (works around three.js r184 StorageTexture-resize bugs — see TSL/patches
189
- // history). Render resolution must not exceed this on either axis; the engine warns
190
- // and ignores larger requests.
191
- export const MAX_STORAGE_TEXTURE_SIZE = 2048;
188
+ // Hard ceiling the engine supports for the reserved (pre-allocated) render size. 4K (3840×2160)
189
+ // fits within 4096². Raising the reserved size to this pins ~1.5 GB of MRT textures — opt-in only.
190
+ export const MAX_RESERVABLE_RENDER_SIZE = 4096;
191
+
192
+ // Reserved render size: every per-resolution compute StorageTexture + aux buffer is pre-allocated at
193
+ // this SQUARE size and never resized at runtime (works around three.js StorageTexture-resize bugs —
194
+ // see TSL/patches history). Render resolution must not exceed it; the engine warns + ignores larger.
195
+ // It is a LIVE binding (mutable) so the host can raise it (e.g. for 4K) BEFORE the pipeline/stages are
196
+ // constructed via setReservedRenderSize(); consumers read it inside their constructors. Default 2048
197
+ // (zero VRAM regression). See docs/internal/specs/wavefront-chunked-pool.md.
198
+ export let MAX_STORAGE_TEXTURE_SIZE = 2048;
199
+
200
+ // Set the reserved render size. MUST be called before pipeline construction (stages pre-allocate at
201
+ // this value and cannot resize). Clamped to [256, MAX_RESERVABLE_RENDER_SIZE]. Returns the applied value.
202
+ export function setReservedRenderSize( px ) {
203
+
204
+ MAX_STORAGE_TEXTURE_SIZE = Math.max( 256, Math.min( MAX_RESERVABLE_RENDER_SIZE, Math.floor( px ) || 256 ) );
205
+ return MAX_STORAGE_TEXTURE_SIZE;
206
+
207
+ }
192
208
 
193
209
  export const ASVGF_QUALITY_PRESETS = {
194
210
  // phiColor / phiDepth are RELATIVE tolerances (fractions). Bigger = more
@@ -575,11 +591,11 @@ export const PRODUCTION_RENDER_CONFIG = {
575
591
  renderMode: 1, enableAlphaShadows: true,
576
592
  enableOIDN: true, oidnQuality: 'balance',
577
593
  interactionModeEnabled: false,
578
- // Looser thresholds + stop at 90%, leaning on OIDN to clean the residual tail.
594
+ // √-norm convergence reaches a true ~all-converged gate, so stop at 0.98 — Cycles-faithful, leaving
595
+ // the unbiased residual for OIDN.
579
596
  useAdaptiveSampling: true,
580
597
  noiseThreshold: 0.1,
581
- darkNoiseFloor: 0.01,
582
- adaptiveStopFraction: 0.9,
598
+ adaptiveStopFraction: 0.98,
583
599
  usePixelFreeze: true,
584
600
  };
585
601
 
@@ -1,4 +1,7 @@
1
1
  import { EventDispatcher, ACESFilmicToneMapping } from 'three';
2
+ import { createLogger, fmt } from '../utils/Logger.js';
3
+
4
+ const log = createLogger( 'oidn' );
2
5
 
3
6
  let _initUNetFromURL = null;
4
7
  let _tfEngine = null;
@@ -36,7 +39,7 @@ function removeOidnTfjsBackend() {
36
39
 
37
40
  } catch ( e ) {
38
41
 
39
- console.warn( 'OIDNDenoiser: failed to clear cached TFJS backend', e );
42
+ log.warn( 'failed to clear cached TFJS backend', e );
40
43
 
41
44
  }
42
45
 
@@ -149,7 +152,7 @@ export class OIDNDenoiser extends EventDispatcher {
149
152
  // Initialize asynchronously
150
153
  this._initialize().catch( error => {
151
154
 
152
- console.error( 'Failed to initialize OIDNDenoiser:', error );
155
+ log.error( 'init failed:', error );
153
156
  this.dispatchEvent( { type: 'error', error } );
154
157
 
155
158
  } );
@@ -249,11 +252,11 @@ export class OIDNDenoiser extends EventDispatcher {
249
252
 
250
253
  this.currentTZAUrl = tzaUrl;
251
254
  this.dispatchEvent( { type: 'loaded' } );
252
- console.log( 'UNet denoiser loaded successfully:', tzaUrl );
255
+ log.debug( 'UNet weights loaded:', tzaUrl );
253
256
 
254
257
  } catch ( error ) {
255
258
 
256
- console.error( 'Failed to load UNet denoiser:', error );
259
+ log.error( 'UNet weights failed to load:', error );
257
260
  this.dispatchEvent( { type: 'error', error: new Error( `Denoiser loading failed: ${error.message}` ) } );
258
261
 
259
262
  } finally {
@@ -340,7 +343,7 @@ export class OIDNDenoiser extends EventDispatcher {
340
343
  this.input.style.opacity = '0';
341
344
 
342
345
  const duration = performance.now() - startTime;
343
- console.log( `Denoising completed in ${duration.toFixed( 1 )}ms (quality: ${this.quality})` );
346
+ log.debug( `denoise complete in ${fmt.ms( duration )} · quality ${this.quality}` );
344
347
 
345
348
  }
346
349
 
@@ -367,11 +370,11 @@ export class OIDNDenoiser extends EventDispatcher {
367
370
 
368
371
  if ( error.name === 'AbortError' ) {
369
372
 
370
- console.log( 'Denoising was aborted' );
373
+ log.debug( 'denoise aborted' );
371
374
 
372
375
  } else {
373
376
 
374
- console.error( 'Denoising error:', error );
377
+ log.error( 'denoise error:', error );
375
378
 
376
379
  }
377
380
 
@@ -408,7 +411,7 @@ export class OIDNDenoiser extends EventDispatcher {
408
411
 
409
412
  if ( ! this.getGPUTextures ) {
410
413
 
411
- console.warn( 'OIDNDenoiser: GPU mode enabled but getGPUTextures not provided' );
414
+ log.warn( 'GPU mode enabled but getGPUTextures not provided' );
412
415
  return false;
413
416
 
414
417
  }
@@ -416,7 +419,7 @@ export class OIDNDenoiser extends EventDispatcher {
416
419
  const textures = this.getGPUTextures();
417
420
  if ( ! textures?.color ) {
418
421
 
419
- console.warn( 'OIDNDenoiser: GPU textures not ready yet' );
422
+ log.warn( 'GPU textures not ready yet' );
420
423
  return false;
421
424
 
422
425
  }
@@ -424,7 +427,7 @@ export class OIDNDenoiser extends EventDispatcher {
424
427
  const device = this.gpuDevice;
425
428
  if ( ! device ) {
426
429
 
427
- console.warn( 'OIDNDenoiser: gpuDevice not available' );
430
+ log.warn( 'gpuDevice not available' );
428
431
  return false;
429
432
 
430
433
  }
@@ -828,7 +831,7 @@ export class OIDNDenoiser extends EventDispatcher {
828
831
  const device = this.gpuDevice;
829
832
  if ( ! device ) {
830
833
 
831
- console.error( 'OIDNDenoiser: gpuDevice not available for output readback' );
834
+ log.error( 'gpuDevice not available for output readback' );
832
835
  return;
833
836
 
834
837
  }
@@ -909,7 +912,7 @@ export class OIDNDenoiser extends EventDispatcher {
909
912
  this.state.isDenoising = false;
910
913
  this.dispatchEvent( { type: 'end' } );
911
914
 
912
- console.log( 'Denoising aborted' );
915
+ log.debug( 'denoise aborted' );
913
916
 
914
917
  }
915
918
 
@@ -927,7 +930,7 @@ export class OIDNDenoiser extends EventDispatcher {
927
930
  // Reinitialize denoiser if tile size changes relative to image size
928
931
  this._setupUNetDenoiser().catch( error => {
929
932
 
930
- console.error( 'Failed to reinitialize denoiser after size change:', error );
933
+ log.error( 'reinitialize after size change failed:', error );
931
934
 
932
935
  } );
933
936
 
@@ -975,7 +978,7 @@ export class OIDNDenoiser extends EventDispatcher {
975
978
  // Remove all event listeners
976
979
  this.removeAllListeners?.();
977
980
 
978
- console.log( 'OIDNDenoiser disposed' );
981
+ log.debug( 'disposed' );
979
982
 
980
983
  }
981
984
 
@@ -16,9 +16,10 @@ import { AutoExposure } from './Stages/AutoExposure.js';
16
16
  import { Compositor } from './Stages/Compositor.js';
17
17
  import { RenderPipeline } from './Pipeline/RenderPipeline.js';
18
18
  import { CompletionTracker } from './Pipeline/CompletionTracker.js';
19
- import { ENGINE_DEFAULTS as DEFAULT_STATE, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, MAX_STORAGE_TEXTURE_SIZE } from './EngineDefaults.js';
19
+ import { ENGINE_DEFAULTS as DEFAULT_STATE, PRODUCTION_RENDER_CONFIG, INTERACTIVE_RENDER_CONFIG, MAX_STORAGE_TEXTURE_SIZE, MAX_RESERVABLE_RENDER_SIZE, setReservedRenderSize } from './EngineDefaults.js';
20
20
  import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples, disposeObjectFromMemory } from './Processor/utils.js';
21
21
  import { BuildTimer } from './Processor/BuildTimer.js';
22
+ import { createLogger, fmt } from './utils/Logger.js';
22
23
  import { InteractionManager } from './managers/InteractionManager.js';
23
24
  import { EngineEvents } from './EngineEvents.js';
24
25
  import { AssetLoader } from './Processor/AssetLoader.js';
@@ -57,6 +58,9 @@ const _appsByCanvas = new WeakMap();
57
58
  *
58
59
  * Extends EventDispatcher for event-driven communication with stores/UI.
59
60
  */
61
+
62
+ const log = createLogger( 'engine' );
63
+
60
64
  export class PathTracerApp extends EventDispatcher {
61
65
 
62
66
  /**
@@ -79,7 +83,7 @@ export class PathTracerApp extends EventDispatcher {
79
83
 
80
84
  } catch ( err ) {
81
85
 
82
- console.warn( 'PathTracerApp: prior canvas owner dispose failed', err );
86
+ log.warn( 'prior canvas owner dispose failed', err );
83
87
 
84
88
  }
85
89
 
@@ -188,7 +192,7 @@ export class PathTracerApp extends EventDispatcher {
188
192
 
189
193
  } catch ( err ) {
190
194
 
191
- console.warn( 'PathTracerApp: failed to remove listener', type, err );
195
+ log.warn( 'failed to remove listener', type, err );
192
196
 
193
197
  }
194
198
 
@@ -222,7 +226,7 @@ export class PathTracerApp extends EventDispatcher {
222
226
  this.stages.pathTracer.setupMaterial();
223
227
 
224
228
  this.isInitialized = true;
225
- console.log( 'WebGPU Path Tracer App initialized' );
229
+ log.debug( 'WebGPU path tracer app initialized' );
226
230
 
227
231
  return this;
228
232
 
@@ -256,7 +260,7 @@ export class PathTracerApp extends EventDispatcher {
256
260
 
257
261
  this._animRefitInFlight = true;
258
262
  this.refitBVH( positions )
259
- .catch( err => console.error( 'Animation refit error:', err ) )
263
+ .catch( err => log.error( 'animation refit error:', err ) )
260
264
  .finally( () => {
261
265
 
262
266
  this._animRefitInFlight = false;
@@ -384,7 +388,7 @@ export class PathTracerApp extends EventDispatcher {
384
388
 
385
389
  if ( this._deviceLost ) return;
386
390
  this._deviceLost = true;
387
- console.error( `WebGPU device lost (${info?.reason || 'unknown'}): ${info?.message || ''}` );
391
+ log.error( `WebGPU device lost (${info?.reason || 'unknown'}): ${info?.message || ''}` );
388
392
  this.stopAnimation();
389
393
  this.dispatchEvent( { type: EngineEvents.DEVICE_LOST, reason: info?.reason, message: info?.message } );
390
394
 
@@ -545,7 +549,7 @@ export class PathTracerApp extends EventDispatcher {
545
549
 
546
550
  } catch ( err ) {
547
551
 
548
- console.warn( 'PathTracerApp: failed to clear TSL texture singleton listeners', err );
552
+ log.warn( 'failed to clear TSL texture singleton listeners', err );
549
553
 
550
554
  }
551
555
 
@@ -803,7 +807,7 @@ export class PathTracerApp extends EventDispatcher {
803
807
  // Tag the primary (replace-loaded) model so it appears in the scene-object list.
804
808
  this._tagPrimarySceneObject();
805
809
 
806
- const timer = new BuildTimer( 'loadSceneData' );
810
+ const timer = new BuildTimer( '', { namespace: 'scene', level: 'info' } );
807
811
  const environmentTexture = this.meshScene.environment;
808
812
 
809
813
  // Environment CDF build in parallel with BVH
@@ -856,7 +860,7 @@ export class PathTracerApp extends EventDispatcher {
856
860
 
857
861
  } catch ( err ) {
858
862
 
859
- console.warn( 'PathTracerApp: raster fallback precompile failed', err );
863
+ log.warn( 'raster fallback precompile failed', err );
860
864
 
861
865
  }
862
866
 
@@ -896,7 +900,7 @@ export class PathTracerApp extends EventDispatcher {
896
900
  this.stages.compositor.setTransparentBackground( this.settings.get( 'transparentBackground' ) );
897
901
  timer.end( 'Apply settings' );
898
902
 
899
- timer.print();
903
+ timer.print( this._sceneSummaryParts() );
900
904
  resetLoading();
901
905
 
902
906
  this._initAnimationAndTransforms();
@@ -906,6 +910,23 @@ export class PathTracerApp extends EventDispatcher {
906
910
 
907
911
  }
908
912
 
913
+ /** Counts for the single `[scene]` summary line emitted after a scene build. */
914
+ _sceneSummaryParts() {
915
+
916
+ const pt = this.stages.pathTracer;
917
+ const meshes = this._sdf?.instanceTable?.entries?.filter( Boolean ).length ?? 0;
918
+ const maps = this._sdf?.geometryExtractor?.maps?.length ?? 0;
919
+
920
+ return [
921
+ fmt.count( pt.triangleCount, 'tri' ),
922
+ meshes ? fmt.count( meshes, 'mesh', 'meshes' ) : null,
923
+ fmt.count( pt.materialData.materialCount, 'material' ),
924
+ maps ? fmt.count( maps, 'map' ) : null,
925
+ fmt.count( pt.bvhNodeCount, 'BVH node' ),
926
+ ];
927
+
928
+ }
929
+
909
930
  // ═══════════════════════════════════════════════════════════════
910
931
  // Dynamic scene objects (add / remove / list / visibility)
911
932
  //
@@ -1202,6 +1223,56 @@ export class PathTracerApp extends EventDispatcher {
1202
1223
  // Resize
1203
1224
  // ═══════════════════════════════════════════════════════════════
1204
1225
 
1226
+ /**
1227
+ * Raise (or lower) the reserved render size — the square dimension every compute StorageTexture + aux
1228
+ * buffer is pre-allocated at. Needed to enable resolutions above the 2048 default (e.g. 4K). The request
1229
+ * is device-capped: 4K reservation (~1.5 GB of MRT textures) is only granted on GPUs with ample VRAM +
1230
+ * a large storage-buffer binding limit; weaker devices clamp to 2048.
1231
+ *
1232
+ * MUST be called before the pipeline/stages are constructed (they pre-allocate at this value and cannot
1233
+ * resize). Calling it after init only takes effect on the next full pipeline (re)build.
1234
+ * @param {number} requestedPx desired reserved size (longest edge)
1235
+ * @returns {number} the applied reserved size
1236
+ */
1237
+ setReservedRenderResolution( requestedPx ) {
1238
+
1239
+ const limits = this.renderer?.backend?.device?.limits;
1240
+ const maxBinding = limits?.maxStorageBufferBindingSize || ( 128 * 1024 * 1024 );
1241
+ const deviceMemGB = ( typeof navigator !== 'undefined' && navigator.deviceMemory ) || 4;
1242
+ // 4K reserve pins the accum MRT (~1.5 GB) + aux; only grant it on clearly-capable GPUs.
1243
+ const deviceSafeMax = ( deviceMemGB >= 8 && maxBinding >= 1024 * 1024 * 1024 )
1244
+ ? MAX_RESERVABLE_RENDER_SIZE : 2048;
1245
+ const prev = MAX_STORAGE_TEXTURE_SIZE;
1246
+ const applied = setReservedRenderSize( Math.min( requestedPx, deviceSafeMax ) );
1247
+
1248
+ // If the pipeline is already built and the reserved size changed, re-init the reserved GPU storage in
1249
+ // place: each stage recreates its pre-allocated StorageTextures at the new size + rebuilds its compute
1250
+ // pipelines. Stage OBJECTS, manager refs and event wiring are preserved (so no re-subscription needed);
1251
+ // scene geometry buffers are resolution-independent and reused. Rendering is paused across the swap so
1252
+ // no in-flight dispatch references a disposed texture.
1253
+ if ( applied !== prev && this.stages?.pathTracer?._packedBuffers ) {
1254
+
1255
+ const wasPaused = this.pauseRendering;
1256
+ this.pauseRendering = true;
1257
+ try {
1258
+
1259
+ for ( const stage of Object.values( this.stages ) ) stage?.reallocateReservedStorage?.();
1260
+
1261
+ } finally {
1262
+
1263
+ this.pauseRendering = wasPaused;
1264
+
1265
+ }
1266
+
1267
+ this.reset();
1268
+ this.dispatchEvent( { type: 'reserved_render_size_changed', size: applied } );
1269
+
1270
+ }
1271
+
1272
+ return applied;
1273
+
1274
+ }
1275
+
1205
1276
  /**
1206
1277
  * Guard against render resolutions the compute pipeline can't support.
1207
1278
  * Per-resolution StorageTextures are pre-allocated at MAX_STORAGE_TEXTURE_SIZE
@@ -1212,7 +1283,7 @@ export class PathTracerApp extends EventDispatcher {
1212
1283
 
1213
1284
  if ( width > MAX_STORAGE_TEXTURE_SIZE || height > MAX_STORAGE_TEXTURE_SIZE ) {
1214
1285
 
1215
- console.warn( `[Rayzee] Render resolution ${width}×${height} exceeds the ${MAX_STORAGE_TEXTURE_SIZE}px limit (compute storage textures are pre-allocated at ${MAX_STORAGE_TEXTURE_SIZE}px). Ignoring resize — use a resolution ≤ ${MAX_STORAGE_TEXTURE_SIZE}.` );
1286
+ log.warn( `render resolution ${width}×${height} exceeds the ${MAX_STORAGE_TEXTURE_SIZE}px limit (compute storage textures are pre-allocated at ${MAX_STORAGE_TEXTURE_SIZE}px). Ignoring resize — use a resolution ≤ ${MAX_STORAGE_TEXTURE_SIZE}.` );
1216
1287
  return false;
1217
1288
 
1218
1289
  }
@@ -1311,7 +1382,6 @@ export class PathTracerApp extends EventDispatcher {
1311
1382
  // engine defaults when a config doesn't override them.
1312
1383
  this.stages.pathTracer?.setUniform( 'useAdaptiveSampling', config.useAdaptiveSampling ?? false );
1313
1384
  this.stages.pathTracer?.setUniform( 'noiseThreshold', config.noiseThreshold ?? DEFAULT_STATE.noiseThreshold );
1314
- this.stages.pathTracer?.setUniform( 'darkNoiseFloor', config.darkNoiseFloor ?? DEFAULT_STATE.darkNoiseFloor );
1315
1385
  this.stages.pathTracer?.setUniform( 'adaptiveStopFraction', config.adaptiveStopFraction ?? DEFAULT_STATE.adaptiveStopFraction );
1316
1386
  this.stages.pathTracer?.setUniform( 'adaptiveMinSamples', config.adaptiveMinSamples ?? DEFAULT_STATE.adaptiveMinSamples );
1317
1387
 
@@ -1339,6 +1409,9 @@ export class PathTracerApp extends EventDispatcher {
1339
1409
 
1340
1410
  if ( options.canvasWidth && options.canvasHeight ) {
1341
1411
 
1412
+ // Raise the reserved storage first so a > 2048 final-render resolution (4K) fits (device-capped,
1413
+ // in-place re-init). No-op when the size already fits.
1414
+ this.setReservedRenderResolution( Math.max( options.canvasWidth, options.canvasHeight ) );
1342
1415
  this.setCanvasSize( options.canvasWidth, options.canvasHeight );
1343
1416
 
1344
1417
  }
@@ -1766,7 +1839,7 @@ export class PathTracerApp extends EventDispatcher {
1766
1839
  this._handleDeviceLost( info );
1767
1840
 
1768
1841
  } );
1769
- gpuDevice.onuncapturederror = ( event ) => console.error( 'WebGPU uncaptured error:', event.error );
1842
+ gpuDevice.onuncapturederror = ( event ) => log.error( 'WebGPU uncaptured error:', event.error );
1770
1843
 
1771
1844
  }
1772
1845
 
@@ -2,6 +2,10 @@ import { TreeletOptimizer } from './TreeletOptimizer.js';
2
2
  import { ReinsertionOptimizer } from './ReinsertionOptimizer.js';
3
3
  import { fetchAsWorker } from './Workers/fetchAsWorker.js';
4
4
  import BVH_WORKER_URL from './Workers/BVHWorker.js?worker&url';
5
+ // Logger is worker-safe (globalThis only, storage access guarded), unlike Constants.js below.
6
+ import { createLogger, fmt } from '../utils/Logger.js';
7
+
8
+ const log = createLogger( 'bvh' );
5
9
 
6
10
  // Inline copy of TRIANGLE_DATA_LAYOUT (mirrors Constants.js).
7
11
  // Cannot import Constants.js because BVHBuilder runs inside BVHWorker
@@ -403,7 +407,7 @@ export class BVHBuilder {
403
407
 
404
408
  const triangleCount = this.totalTriangles;
405
409
  const useShared = typeof SharedArrayBuffer !== 'undefined';
406
- console.log( `[BVHBuilder] SharedArrayBuffer: ${useShared ? 'enabled' : 'unavailable (using transfer fallback)'}` );
410
+ log.debug( `SharedArrayBuffer ${useShared ? 'enabled' : 'unavailable (using transfer fallback)'}` );
407
411
 
408
412
  // Pre-allocate SharedArrayBuffer for reordered output so worker
409
413
  // writes directly to shared memory (no transfer needed on return).
@@ -491,14 +495,14 @@ export class BVHBuilder {
491
495
 
492
496
  fetchAsWorker( BVH_WORKER_URL ).then( setupWorker ).catch( () => {
493
497
 
494
- console.warn( 'Worker fetch fallback failed, using synchronous build' );
498
+ log.warn( 'worker fetch fallback failed, using synchronous build' );
495
499
  resolve( this._buildSyncAndFlatten( triangles, depth, progressCallback ) );
496
500
 
497
501
  } );
498
502
 
499
503
  } else {
500
504
 
501
- console.warn( 'Worker creation failed, falling back to synchronous build:', error );
505
+ log.warn( 'worker creation failed, falling back to synchronous build:', error );
502
506
  resolve( this._buildSyncAndFlatten( triangles, depth, progressCallback ) );
503
507
 
504
508
  }
@@ -619,7 +623,7 @@ export class BVHBuilder {
619
623
 
620
624
  } catch ( error ) {
621
625
 
622
- console.error( `TreeletOptimizer: Error in pass ${pass + 1}:`, error );
626
+ log.error( `treelet optimizer failed in pass ${pass + 1}:`, error );
623
627
  break;
624
628
 
625
629
  }
@@ -663,7 +667,7 @@ export class BVHBuilder {
663
667
 
664
668
  } catch ( error ) {
665
669
 
666
- console.error( 'ReinsertionOptimizer: Error:', error );
670
+ log.error( 'reinsertion optimizer failed:', error );
667
671
 
668
672
  }
669
673
 
@@ -710,8 +714,8 @@ export class BVHBuilder {
710
714
 
711
715
  const total = this.splitStats.totalBuildTime;
712
716
  const s = this.splitStats;
713
- console.log(
714
- `[BVH] ${n.toLocaleString()} tris → ${this.totalNodes} nodes in ${Math.round( total )}ms` +
717
+ log.debug(
718
+ `${fmt.n( n )} tris → ${fmt.n( this.totalNodes )} nodes in ${fmt.ms( total )}` +
715
719
  ` | SAH ${s.sahSplits} objMed ${s.objectMedianSplits} spatMed ${s.spatialMedianSplits} failed ${s.failedSplits}` +
716
720
  ( s.treeletsProcessed ? ` | treelets ${s.treeletsImproved}/${s.treeletsProcessed} improved` : '' ) +
717
721
  ( s.reinsertionsApplied ? ` | reinsertions ${s.reinsertionsApplied}` : '' )
@@ -1,17 +1,25 @@
1
+ import { createLogger, fmt } from '../utils/Logger.js';
2
+
1
3
  /**
2
4
  * BuildTimer - Tracks and logs timing for all build pipeline steps.
5
+ *
6
+ * Nested timers default to `debug` so a normal load only shows the top-level
7
+ * summary; pass `{ level: 'info' }` for the one phase worth a visible line.
8
+ *
3
9
  * Usage:
4
- * const timer = new BuildTimer();
10
+ * const timer = new BuildTimer( '', { namespace: 'scene', level: 'info' } );
5
11
  * timer.start('stepName');
6
12
  * // ... work ...
7
13
  * timer.end('stepName');
8
- * timer.print(); // logs summary table
14
+ * timer.print( [ '18,016 tris' ] ); // one summary line, steps folded into a group
9
15
  */
10
16
  export class BuildTimer {
11
17
 
12
- constructor( label = 'Build' ) {
18
+ constructor( label = 'Build', { namespace = 'build', level = 'debug' } = {} ) {
13
19
 
14
20
  this.label = label;
21
+ this.level = level;
22
+ this.logger = createLogger( namespace );
15
23
  this.entries = new Map();
16
24
  this.order = [];
17
25
  this.totalStart = performance.now();
@@ -47,11 +55,14 @@ export class BuildTimer {
47
55
 
48
56
  }
49
57
 
50
- print() {
58
+ /**
59
+ * @param {string[]} [parts] - extra summary segments (counts, sizes) shown before the total
60
+ */
61
+ print( parts = [] ) {
51
62
 
52
63
  const totalDuration = performance.now() - this.totalStart;
53
64
 
54
- const parts = this.order
65
+ const steps = this.order
55
66
  .map( name => {
56
67
 
57
68
  const dur = this.entries.get( name )?.duration ?? 0;
@@ -60,7 +71,10 @@ export class BuildTimer {
60
71
  } )
61
72
  .filter( Boolean );
62
73
 
63
- console.log( `[${this.label}] ${Math.round( totalDuration )}ms` + ( parts.length ? ` | ${parts.join( ' · ' )}` : '' ) );
74
+ const headline = fmt.list( [ this.label, ...parts, fmt.ms( totalDuration ) ] );
75
+
76
+ if ( this.level === 'info' ) this.logger.summary( headline, steps );
77
+ else this.logger.debug( fmt.list( [ headline, steps.join( ' · ' ) ] ) );
64
78
 
65
79
  return { steps: Object.fromEntries( this.order.map( n => [ n, Math.round( this.entries.get( n )?.duration ?? 0 ) ] ) ), total: Math.round( totalDuration ) };
66
80
 
@@ -9,6 +9,9 @@
9
9
  import { DataTexture, RGBAFormat, FloatType, NearestFilter } from 'three';
10
10
  import { TRIANGLE_DATA_LAYOUT } from '../EngineDefaults.js';
11
11
  import { LightBVHBuilder } from './LightBVHBuilder.js';
12
+ import { createLogger, fmt } from '../utils/Logger.js';
13
+
14
+ const log = createLogger( 'emissive' );
12
15
 
13
16
  export class EmissiveTriangleBuilder {
14
17
 
@@ -41,8 +44,6 @@ export class EmissiveTriangleBuilder {
41
44
  */
42
45
  extractEmissiveTriangles( triangleData, materials, triangleCount ) {
43
46
 
44
- console.log( '[EmissiveTriangleBuilder] Extracting emissive triangles...' );
45
-
46
47
  this.emissiveTriangles = [];
47
48
  this.totalEmissivePower = 0;
48
49
  this._totalTriangleCount = triangleCount;
@@ -135,8 +136,11 @@ export class EmissiveTriangleBuilder {
135
136
 
136
137
  this.emissiveCount = this.emissiveTriangles.length;
137
138
 
138
- console.log( `[EmissiveTriangleBuilder] Found ${this.emissiveCount} emissive triangles (${( this.emissiveCount / triangleCount * 100 ).toFixed( 2 )}%)` );
139
- console.log( `[EmissiveTriangleBuilder] Total emissive power: ${this.totalEmissivePower.toFixed( 2 )}` );
139
+ if ( this.emissiveCount > 0 ) {
140
+
141
+ log.debug( `${fmt.n( this.emissiveCount )} emissive tris (${( this.emissiveCount / triangleCount * 100 ).toFixed( 2 )}%) · total power ${this.totalEmissivePower.toFixed( 2 )}` );
142
+
143
+ }
140
144
 
141
145
  // Build data arrays
142
146
  this._buildDataArrays();
@@ -333,7 +337,7 @@ export class EmissiveTriangleBuilder {
333
337
  texture.minFilter = NearestFilter;
334
338
  texture.magFilter = NearestFilter;
335
339
 
336
- console.log( `[EmissiveTriangleBuilder] Created ${width}x${height} emissive texture (${this.emissiveCount} emissives)` );
340
+ log.debug( `emissive texture ${fmt.px( width, height )} · ${fmt.n( this.emissiveCount )} entries` );
337
341
 
338
342
  return texture;
339
343
 
@@ -375,7 +379,7 @@ export class EmissiveTriangleBuilder {
375
379
 
376
380
  }
377
381
 
378
- console.log( `[EmissiveTriangleBuilder] Created emissive raw data: ${this.emissiveCount} entries (${data.byteLength} bytes)` );
382
+ log.debug( `emissive raw data ${fmt.n( this.emissiveCount )} entries (${fmt.mb( data.byteLength )})` );
379
383
 
380
384
  return data;
381
385
 
@@ -611,7 +615,7 @@ export class EmissiveTriangleBuilder {
611
615
  }
612
616
 
613
617
  this.emissiveTriangleData = data;
614
- console.log( `[EmissiveTriangleBuilder] Rebuilt sorted emissive data: ${n} entries` );
618
+ log.debug( `rebuilt sorted emissive data · ${fmt.n( n )} entries` );
615
619
 
616
620
  }
617
621
 
@@ -1,5 +1,8 @@
1
1
  import { Vector3, Vector2, Color, Matrix3, Matrix4, FrontSide, BackSide, DoubleSide, RGBAFormat } from "three";
2
2
  import { TRIANGLE_DATA_LAYOUT } from '../EngineDefaults.js';
3
+ import { createLogger, fmt, warnOnce } from '../utils/Logger.js';
4
+
5
+ const log = createLogger( 'geometry' );
3
6
 
4
7
  const MAX_TEXTURES_LIMIT = 128;
5
8
 
@@ -169,7 +172,8 @@ export class GeometryExtractor {
169
172
  if ( material.depthWrite === false ) {
170
173
 
171
174
  material.depthWrite = true;
172
- console.warn( "Depth write is disabled in material, enabling it for rastered rendering" );
175
+ // Fires per offending material; the fix is identical every time, so warn once.
176
+ warnOnce( log, 'depthWrite', 'material had depthWrite disabled — enabled for rastered rendering' );
173
177
 
174
178
  }
175
179
 
@@ -758,10 +762,12 @@ export class GeometryExtractor {
758
762
  logStats() {
759
763
 
760
764
  const usedBytes = this.currentTriangleIndex * TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE * 4;
761
- console.log( "materials:", this.materials.length );
762
- console.log( "triangles:", this.currentTriangleIndex );
763
- console.log( "triangle data size (MB):", ( usedBytes / ( 1024 * 1024 ) ).toFixed( 2 ) );
764
- console.log( "maps:", this.maps.length );
765
+
766
+ log.debug( fmt.list( [
767
+ `${fmt.count( this.currentTriangleIndex, 'tri' )} (${fmt.mb( usedBytes )})`,
768
+ fmt.count( this.materials.length, 'material' ),
769
+ fmt.count( this.maps.length, 'map' ),
770
+ ] ) );
765
771
 
766
772
  }
767
773