rayzee 7.22.2 → 7.22.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.22.2",
3
+ "version": "7.22.3",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -199,9 +199,10 @@ export const MAX_RESERVABLE_RENDER_SIZE = 4096;
199
199
  // Reserved render size: every per-resolution compute StorageTexture + aux buffer is pre-allocated at
200
200
  // this SQUARE size and never resized at runtime (works around three.js StorageTexture-resize bugs —
201
201
  // see TSL/patches history). Render resolution must not exceed it; the engine warns + ignores larger.
202
- // It is a LIVE binding (mutable) so the host can raise it (e.g. for 4K) BEFORE the pipeline/stages are
203
- // constructed via setReservedRenderSize(); consumers read it inside their constructors. Default 2048
204
- // (zero VRAM regression). See docs/internal/specs/wavefront-chunked-pool.md.
202
+ // It is a LIVE binding (mutable) so it can be raised (e.g. for 4K) via setReservedRenderSize(); consumers
203
+ // read it inside their constructors, and stages already built at a lower value are re-initialised in place
204
+ // by PathTracerApp.setReservedRenderResolution() — which is the device-gated API hosts should call, at any
205
+ // point in the lifecycle. Default 2048 (zero VRAM regression). See docs/internal/specs/wavefront-chunked-pool.md.
205
206
  export let MAX_STORAGE_TEXTURE_SIZE = 2048;
206
207
 
207
208
  // Set the reserved render size. MUST be called before pipeline construction (stages pre-allocate at
@@ -114,6 +114,8 @@ export class PathTracerApp extends EventDispatcher {
114
114
  // ── Pipeline & stages ──
115
115
  this.pipeline = null;
116
116
 
117
+ this._pendingReservedRenderSize = null;
118
+
117
119
  /**
118
120
  * Named access to all pipeline stages.
119
121
  * Advanced consumers can reach into stages for fine-grained control.
@@ -220,6 +222,7 @@ export class PathTracerApp extends EventDispatcher {
220
222
  async init() {
221
223
 
222
224
  await this._initRenderer();
225
+ this._applyPendingReservedRenderSize();
223
226
  this._initCameraManager();
224
227
  this._initScenes();
225
228
  this._initAssetPipeline();
@@ -1233,34 +1236,53 @@ export class PathTracerApp extends EventDispatcher {
1233
1236
  * is device-capped: 4K reservation (~1.5 GB of MRT textures) is only granted on GPUs with ample VRAM +
1234
1237
  * a large storage-buffer binding limit; weaker devices clamp to 2048.
1235
1238
  *
1236
- * MUST be called before the pipeline/stages are constructed (they pre-allocate at this value and cannot
1237
- * resize). Calling it after init only takes effect on the next full pipeline (re)build.
1239
+ * Callable at any point in the lifecycle:
1240
+ * - before init(): recorded, then applied during init() before the stages are constructed, so they
1241
+ * pre-allocate at the raised size directly. The device gate cannot run until the device exists, so
1242
+ * the value returned here is the request, not the verdict — read getReservedRenderResolution() after
1243
+ * init(), or listen for `reserved_render_size_changed`.
1244
+ * - after init(): applied immediately, re-initialising the reserved GPU storage in place.
1238
1245
  * @param {number} requestedPx desired reserved size (longest edge)
1239
1246
  * @param {Object} [opts]
1240
1247
  * @param {boolean} [opts.allowLower=false] permit lowering, paying a rebuild, to reclaim VRAM
1241
- * @returns {number} the applied reserved size
1248
+ * @returns {number} the applied reserved size (the pending request when called before init)
1242
1249
  */
1243
1250
  setReservedRenderResolution( requestedPx, { allowLower = false } = {} ) {
1244
1251
 
1245
- const limits = this.renderer?.backend?.device?.limits;
1246
- const maxBinding = limits?.maxStorageBufferBindingSize || ( 128 * 1024 * 1024 );
1247
- const deviceMemGB = ( typeof navigator !== 'undefined' && navigator.deviceMemory ) || 4;
1248
- // 4K reserve pins the accum MRT (~1.5 GB) + aux; only grant it on clearly-capable GPUs.
1249
- const deviceSafeMax = ( deviceMemGB >= 8 && maxBinding >= 1024 * 1024 * 1024 )
1250
- ? MAX_RESERVABLE_RENDER_SIZE : 2048;
1251
1252
  const prev = MAX_STORAGE_TEXTURE_SIZE;
1252
1253
 
1253
1254
  // Monotonic up: UI-driven callers request whatever the current view needs, so honouring decreases made
1254
1255
  // the reserve oscillate on every preview↔render switch and paid a full kernel rebuild each time.
1255
1256
  const target = allowLower ? requestedPx : Math.max( requestedPx, prev );
1257
+
1258
+ // No device yet: the gate below has nothing to interrogate and would clamp a 4K-capable GPU to 2048.
1259
+ // init() replays the request once the device exists, still before the stages allocate.
1260
+ if ( ! this.renderer ) {
1261
+
1262
+ this._pendingReservedRenderSize = { requestedPx, allowLower };
1263
+ return Math.max( 256, Math.min( MAX_RESERVABLE_RENDER_SIZE, Math.floor( target ) || 256 ) );
1264
+
1265
+ }
1266
+
1267
+ const limits = this.renderer.backend?.device?.limits;
1268
+ const maxBinding = limits?.maxStorageBufferBindingSize || ( 128 * 1024 * 1024 );
1269
+ const deviceMemGB = ( typeof navigator !== 'undefined' && navigator.deviceMemory ) || 4;
1270
+ // 4K reserve pins the accum MRT (~1.5 GB) + aux; only grant it on clearly-capable GPUs.
1271
+ const deviceSafeMax = ( deviceMemGB >= 8 && maxBinding >= 1024 * 1024 * 1024 )
1272
+ ? MAX_RESERVABLE_RENDER_SIZE : 2048;
1256
1273
  const applied = setReservedRenderSize( Math.min( target, deviceSafeMax ) );
1257
1274
 
1258
- // If the pipeline is already built and the reserved size changed, re-init the reserved GPU storage in
1259
- // place: each stage recreates its pre-allocated StorageTextures at the new size + rebuilds its compute
1260
- // pipelines. Stage OBJECTS, manager refs and event wiring are preserved (so no re-subscription needed);
1261
- // scene geometry buffers are resolution-independent and reused. Rendering is paused across the swap so
1262
- // no in-flight dispatch references a disposed texture.
1263
- if ( applied !== prev && this.stages?.pathTracer?._packedBuffers ) {
1275
+ // Gate the realloc on the textures that EXIST, not on how the binding moved: a raise applied while
1276
+ // nothing was allocated leaves `applied === prev` for every later call, so keying off that let the
1277
+ // first ineffective call poison every effective one after it (issue #9). The path tracer's write MRT
1278
+ // witnesses the reserve the stages were last built at.
1279
+ const allocated = this.stages?.pathTracer?.storageTextures?.writeColor?.image?.width ?? applied;
1280
+
1281
+ // Re-init the reserved GPU storage in place: each stage recreates its pre-allocated StorageTextures at
1282
+ // the new size + rebuilds its compute pipelines. Stage OBJECTS, manager refs and event wiring are
1283
+ // preserved (so no re-subscription needed); scene geometry buffers are resolution-independent and
1284
+ // reused. Rendering is paused across the swap so no in-flight dispatch references a disposed texture.
1285
+ if ( allocated !== applied ) {
1264
1286
 
1265
1287
  const wasPaused = this.pauseRendering;
1266
1288
  this.pauseRendering = true;
@@ -1294,6 +1316,31 @@ export class PathTracerApp extends EventDispatcher {
1294
1316
 
1295
1317
  }
1296
1318
 
1319
+ /**
1320
+ * Replay a setReservedRenderResolution() call made before init(): after the device exists, so the gate is
1321
+ * evaluated against the real GPU, and before _initPipeline() constructs the stages, which read the reserve
1322
+ * in their constructors. The event carries the device's verdict — the only authoritative answer a pre-init
1323
+ * caller can get.
1324
+ */
1325
+ _applyPendingReservedRenderSize() {
1326
+
1327
+ const pending = this._pendingReservedRenderSize;
1328
+ if ( ! pending ) return;
1329
+
1330
+ this._pendingReservedRenderSize = null;
1331
+
1332
+ const applied = this.setReservedRenderResolution( pending.requestedPx, { allowLower: pending.allowLower } );
1333
+
1334
+ if ( applied < pending.requestedPx ) {
1335
+
1336
+ log.warn( `reserved render size ${fmt.n( pending.requestedPx )}px was capped to ${fmt.n( applied )}px by this device's limits — renders above ${fmt.n( applied )}px will be declined.` );
1337
+
1338
+ }
1339
+
1340
+ this.dispatchEvent( { type: 'reserved_render_size_changed', size: applied } );
1341
+
1342
+ }
1343
+
1297
1344
  /**
1298
1345
  * The current reserved (pre-allocated) square render size in px.
1299
1346
  * @returns {number}
@@ -1314,7 +1361,7 @@ export class PathTracerApp extends EventDispatcher {
1314
1361
 
1315
1362
  if ( width > MAX_STORAGE_TEXTURE_SIZE || height > MAX_STORAGE_TEXTURE_SIZE ) {
1316
1363
 
1317
- 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}.` );
1364
+ log.warn( `render resolution ${width}×${height} exceeds the ${MAX_STORAGE_TEXTURE_SIZE}px reserve (compute storage textures are pre-allocated at ${MAX_STORAGE_TEXTURE_SIZE}px). Ignoring resize — raise the reserve with setReservedRenderResolution( ${Math.max( width, height )} ) first, or use a resolution ≤ ${MAX_STORAGE_TEXTURE_SIZE}.` );
1318
1365
  return false;
1319
1366
 
1320
1367
  }
@@ -1360,10 +1407,19 @@ export class PathTracerApp extends EventDispatcher {
1360
1407
 
1361
1408
  }
1362
1409
 
1410
+ /**
1411
+ * Set the render resolution in pixels, applied immediately (unlike the debounced onResize()).
1412
+ * @param {number} width
1413
+ * @param {number} height
1414
+ * @returns {{width: number, height: number}|null} the size now in effect, or null if the request was
1415
+ * declined — zero, or above the reserved render size (raise it with setReservedRenderResolution).
1416
+ * A declined request leaves the previous size in place, so ignoring this return renders at the
1417
+ * wrong resolution with nothing but a warning to show for it.
1418
+ */
1363
1419
  setCanvasSize( width, height ) {
1364
1420
 
1365
- if ( width === 0 || height === 0 ) return;
1366
- if ( ! this._isRenderSizeSupported( width, height ) ) return;
1421
+ if ( width === 0 || height === 0 ) return null;
1422
+ if ( ! this._isRenderSizeSupported( width, height ) ) return null;
1367
1423
 
1368
1424
  this.renderer.setPixelRatio( 1.0 );
1369
1425
  this.renderer.setSize( width, height, false );
@@ -1373,6 +1429,8 @@ export class PathTracerApp extends EventDispatcher {
1373
1429
  clearTimeout( this._resizeDebounceTimer );
1374
1430
  this._applyRenderResize( width, height );
1375
1431
 
1432
+ return { width, height };
1433
+
1376
1434
  }
1377
1435
 
1378
1436
  // ═══════════════════════════════════════════════════════════════
@@ -761,10 +761,10 @@ export class PathTracer extends PathTracerStage {
761
761
  // MAX_STORAGE_TEXTURE_SIZE and can't be resized, so recreate the pool at the (new) live reserved size and
762
762
  // rebuild the wavefront kernels in place. The stage OBJECT is unchanged, so manager/event refs stay valid;
763
763
  // only GPU textures + compute pipelines are rebuilt. Scene buffers (BVH/tri/material) are resolution-
764
- // independent and untouched. Caller must have rendering paused. No-op before the first build.
764
+ // independent and untouched. Caller must have rendering paused.
765
765
  reallocateReservedStorage() {
766
766
 
767
- if ( ! this._packedBuffers || ! ( this.materialData?.materialCount > 0 ) ) return;
767
+ if ( ! this.storageTextures.writeColor ) return;
768
768
 
769
769
  // Clamp into the (possibly just-lowered) reserve: create() sizes the write StorageTextures at
770
770
  // MAX_STORAGE_TEXTURE_SIZE but the readTarget at what's passed here, and the tracked render size still
@@ -777,6 +777,11 @@ export class PathTracer extends PathTracerStage {
777
777
  this.storageTextures.create( w, h );
778
778
  this.resolution.value.set( w, h );
779
779
 
780
+ // No kernels before the first scene with materials — nothing to rebuild, and a build here would compile
781
+ // against 0 materials. The pool above is recreated regardless: it is allocated at construction, so a
782
+ // reserve raised before the first model load must still reach it or every later copy overflows it.
783
+ if ( ! this._packedBuffers || ! ( this.materialData?.materialCount > 0 ) ) return;
784
+
780
785
  // Rebuild kernels: re-references the fresh write textures + recreates the per-pixel aux buffers
781
786
  // (m2/streak/frozenMask) at the new maxPixels. Prev-frame nodes are repointed per-frame in render().
782
787
  if ( this._kernelManager ) this._kernelManager.dispose();
package/src/index.js CHANGED
@@ -36,6 +36,7 @@ export {
36
36
  MEMORY_CONSTANTS,
37
37
  PRODUCTION_RENDER_CONFIG,
38
38
  INTERACTIVE_RENDER_CONFIG,
39
+ MAX_RESERVABLE_RENDER_SIZE,
39
40
  } from './EngineDefaults.js';
40
41
 
41
42
  // Settings & managers (for advanced consumers)