rayzee 9.1.2 → 9.2.1

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 (49) hide show
  1. package/README.md +176 -16
  2. package/dist/{NeuralRetouch-BRquqfmZ.js → NeuralRetouch-CYgkQY8Z.js} +1 -1
  3. package/dist/{NeuralSuperRes-CbNvQjCw.js → NeuralSuperRes-PkgDLQcD.js} +72 -40
  4. package/dist/{ToneMapGPU-DpUZDMbQ.js → ToneMapGPU-B6XIe3DU.js} +102 -92
  5. package/dist/rayzee.es.js +6209 -5571
  6. package/dist/rayzee.umd.js +137 -126
  7. package/package.json +3 -2
  8. package/src/AssetConfig.js +10 -5
  9. package/src/EngineEvents.js +41 -1
  10. package/src/EngineIssues.js +3 -0
  11. package/src/Headless.js +23 -7
  12. package/src/HeadlessCanvas.js +104 -0
  13. package/src/Passes/OIDNDenoiser.js +29 -4
  14. package/src/Passes/OIDNTemporalHistory.js +7 -0
  15. package/src/PathTracerApp.js +511 -136
  16. package/src/Platform.js +76 -0
  17. package/src/Processor/AssetLoader.js +66 -4
  18. package/src/Processor/BVHBuilder.js +2 -1
  19. package/src/Processor/EquirectHDRInfo.js +2 -1
  20. package/src/Processor/HostMemory.js +14 -0
  21. package/src/Processor/PBRT/PBRTSceneBuilder.js +26 -0
  22. package/src/Processor/ParallelBVHBuilder.js +7 -6
  23. package/src/Processor/PlatformImageLoader.js +118 -0
  24. package/src/Processor/ResampleRGBA8.js +102 -0
  25. package/src/Processor/SceneProcessor.js +7 -6
  26. package/src/Processor/TextureCreator.js +162 -13
  27. package/src/Processor/TextureReadback.js +26 -10
  28. package/src/Processor/ToneMapGPU.js +45 -15
  29. package/src/Processor/Workers/PackWorker.js +28 -0
  30. package/src/Stages/Compositor.js +25 -6
  31. package/src/Stages/PathTracer.js +142 -25
  32. package/src/Stages/PathTracerStage.js +6 -96
  33. package/src/Storage/DownloadCache.js +30 -7
  34. package/src/TSL/DebugKernel.js +1 -1
  35. package/src/TSL/GenerateKernel.js +2 -5
  36. package/src/TSL/Random.js +3 -120
  37. package/src/TSL/ShadeKernel.js +3 -3
  38. package/src/index.js +2 -1
  39. package/src/managers/CameraManager.js +2 -2
  40. package/src/managers/DenoisingManager.js +76 -7
  41. package/src/managers/GoboManager.js +8 -1
  42. package/src/managers/IESManager.js +13 -2
  43. package/src/managers/InteractionManager.js +1 -1
  44. package/src/managers/LightManager.js +31 -0
  45. package/src/managers/OverlayManager.js +11 -1
  46. package/src/managers/VideoRenderManager.js +8 -0
  47. package/src/node/NodeWorker.js +144 -0
  48. package/src/node/index.js +9 -0
  49. package/src/node/nodePlatform.js +86 -0
package/README.md CHANGED
@@ -267,16 +267,12 @@ export default defineConfig({
267
267
 
268
268
  ### Configuring Assets (CDN URLs & cache namespace)
269
269
 
270
- By default, the engine loads STBN blue-noise atlases, GLTF Draco/KTX2 decoders, OIDN denoiser weights, ONNX upscaler models, and the onnxruntime-web bundle from upstream CDNs. If you're self-hosting, embedding the engine alongside a different consumer of the same caches, or operating offline, override them **once before constructing `PathTracerApp`**:
270
+ By default, the engine loads GLTF Draco/KTX2 decoders, OIDN denoiser weights, ONNX upscaler models, and the onnxruntime-web bundle from upstream CDNs. If you're self-hosting, embedding the engine alongside a different consumer of the same caches, or operating offline, override them **once before constructing `PathTracerApp`**:
271
271
 
272
272
  ```js
273
273
  import { configureAssets } from 'rayzee';
274
274
 
275
275
  configureAssets({
276
- // STBN atlases (PNG, decoded as Float textures)
277
- stbnScalarAtlas: '/assets/stbn_scalar_atlas.png',
278
- stbnVec2Atlas: '/assets/stbn_vec2_atlas.png',
279
-
280
276
  // onnxruntime-web (loaded by AI upscaler worker via dynamic import)
281
277
  ortRuntimeUrl: '/ort/ort.webgpu.bundle.min.mjs',
282
278
  ortWasmPaths: '/ort/',
@@ -319,12 +315,15 @@ const engine = new PathTracerApp(canvas, options?)
319
315
  | Parameter | Type | Description |
320
316
  |---|---|---|
321
317
  | `canvas` | `HTMLCanvasElement` | Rendering target |
322
- | `options.autoResize` | `boolean` | Auto-resize on window resize (default: `true`) |
318
+ | `canvas` may be `null` | | Headless: the engine makes its own canvas and runs no render loop — see [Running in Node](#running-in-node) |
319
+ | `options.headless` | `boolean` | Headless with a canvas of your own (default: `true` when `canvas` is `null`) |
320
+ | `options.autoResize` | `boolean` | Auto-resize on window resize (default: `true`; always off headless) |
323
321
  | `options.container` | `HTMLElement` | Single DOM parent the engine mounts auxiliary elements into — HUD overlay (tile borders, helpers) and denoiser canvas. Defaults to `canvas.parentNode`. |
324
322
  | `options.strict` | `boolean` | Throw an `EngineIssueError` where the engine would otherwise degrade and carry on (default: `false`). See [Degradation contract](#degradation-contract). |
325
323
  | `options.profile` | `string` | `'viewer'` (default) or `'physical'` — product tuning that is not a physical constant: area-light scale, environment rotation, tone mapping, saturation. An unknown name throws. |
326
324
  | `options.maxSceneBytes` | `number` | Raise or lower the CPU memory ceiling a scene may need before the engine refuses it (default 9,216 MB). See [Memory monitoring](#memory-monitoring). |
327
- | `options.storage` | `false \| 'auto' \| StorageManager` | On-disk storage (default: `configureAssets( { storage } )`). A manager you pass stays yours to dispose. See [On-disk storage](#on-disk-storage-opfs). |
325
+ | `options.hostMemoryGB` | `number` | The host's memory, for runtimes without Chrome's `navigator.deviceMemory` (which then read as 4 GB and cap the render reserve at 2048). Sizes the reserve and the path pool. |
326
+ | `options.storage` | `false \| 'auto' \| StorageManager` | On-disk storage (default: `configureAssets( { storage } )`; off under `strict` unless you set it there or here). A manager you pass stays yours to dispose. See [On-disk storage](#on-disk-storage-opfs). |
328
327
  | `options.memorySpill` | `boolean` | Experimental, default `false`: build a large static scene through disk — triangle records, BLAS nodes and the three.js geometry are written out as the build finishes with them — and raise the pbrt triangle and placement caps to 60M / 8M. See [On-disk storage](#on-disk-storage-opfs). |
329
328
 
330
329
  The engine creates and mounts everything it needs (denoiser canvas, tile/HUD overlay) into a single parent on `init()`. Performance HUDs (e.g. `stats-gl`) are not bundled — listen to `EngineEvents.FRAME` and tick your own panel.
@@ -552,10 +551,16 @@ engine.lightManager.add('PointLight') // Add a light (PointLight, SpotLigh
552
551
  engine.lightManager.remove(uuid) // Remove by UUID
553
552
  engine.lightManager.clear() // Remove all lights
554
553
  engine.lightManager.getAll() // Get all light descriptors
555
- engine.lightManager.sync() // Re-upload light data to GPU
554
+ engine.lightManager.setIntensity(uuid, 40) // Set one light's power and re-upload
555
+ engine.lightManager.getLight(uuid) // The traced three.js light, to edit other properties
556
+ engine.lightManager.sync() // Re-upload light data to GPU after editing a light
556
557
  engine.lightManager.showHelpers(true) // Toggle visual helpers
557
558
  ```
558
559
 
560
+ The path tracer traces **copies** of a model's lights, made when the model loads. Changing a light
561
+ inside the loaded model does nothing; change the copy — `getLight(uuid)` with a UUID from `getAll()` —
562
+ and call `sync()`, or use `setIntensity()`.
563
+
559
564
  Light `intensity` follows Blender: radiant power in watts for point, spot and area lights, irradiance in W/m² for directional. Dividing power by area only means something in metres, so the engine assumes **one world unit is one metre**; scenes authored in cm or mm must carry that scale in their node transforms, as glTF exporters do. glTF `RectAreaLightPlaceholder` nodes author `intensity` as three.js radiance (their `power` field is `intensity · width · height · π`); the importer converts it to power through the light's world area so the authored radiance is reproduced exactly, then applies the profile's `areaLightIntensityScale`.
560
565
 
561
566
  ### engine.animationManager
@@ -824,6 +829,12 @@ surface** — pin a version and branch on the strings; they are never renamed or
824
829
  | `scene.memory_budget` | the scene needs more CPU memory than is safe, or more than is possible |
825
830
  | `emissive.instances_collapsed` | an emissive instanced mesh was too large to expand, so its copies light the scene as one |
826
831
  | `refit.shared_geometry` | a deform was asked for on a mesh that shares its triangles, and was skipped |
832
+ | `denoiser.unavailable` | a requested denoise or upscale produced nothing — the denoiser was not built, OIDN was off while accumulating, or the pass needs a canvas in a document |
833
+ | `output.source_fallback` | `renderToBuffer( { source: 'display' } )` found no denoised picture and returned the raw accumulation |
834
+ | `light.placeholder_skipped` | a `RectAreaLightPlaceholder` node lacked `userData.name` or `userData.type: 'RectAreaLight'`, so no light was made for it |
835
+
836
+ `asset.unreachable` also covers what the engine fetches for itself: OIDN weights, IES profiles and
837
+ gobos (`detail.asset` says which).
827
838
 
828
839
  `settings.getEffective()` is the companion for the `setting.unknown_key` case: it returns every live
829
840
  setting as `{ value, source, routed }`, and `routed: false` means stored but reaching no stage.
@@ -868,7 +879,7 @@ engine.setReservedRenderResolution(2048, { allowLower: true }) // lower, payin
868
879
  engine.getReservedRenderResolution() // the reserve actually in force
869
880
  ```
870
881
 
871
- The request is **device-capped**: a 4096 reserve pins roughly 1.5 GB of MRT textures, so it is only granted on GPUs reporting ≥ 8 GB and a ≥ 1 GB `maxStorageBufferBindingSize`; weaker devices clamp to 2048. `MAX_RESERVABLE_RENDER_SIZE` (4096) is the ceiling on any request.
882
+ The request is **device-capped**: a 4096 reserve pins roughly 1.5 GB of MRT textures, so it is only granted on hosts reporting ≥ 8 GB and a ≥ 1 GB `maxStorageBufferBindingSize`; weaker devices clamp to 2048, recorded as `render.reserve_capped`. The memory figure is `options.hostMemoryGB`, else `navigator.deviceMemory`, else an assumed 4 — so outside Chrome, pass it. `MAX_RESERVABLE_RENDER_SIZE` (4096) is the ceiling on any request.
872
883
 
873
884
  Raises are monotonic unless you pass `allowLower` — UI-driven callers ask for whatever the current view needs, and honouring every decrease made the reserve oscillate across preview↔render switches, paying a full kernel rebuild each time.
874
885
 
@@ -877,10 +888,10 @@ Callable at any point in the lifecycle:
877
888
  - **Before `init()`** — recorded and applied during `init()`, after the device exists but before the stages are constructed, so they allocate at the raised size directly. The device gate cannot run without a device, so the return value here is the *request*, not the verdict.
878
889
  - **After `init()`** — applied immediately, re-initialising the reserved GPU storage in place.
879
890
 
880
- Either way the verdict arrives as a `reserved_render_size_changed` event (a plain string type, not an `EngineEvents` constant):
891
+ Either way the verdict arrives as `EngineEvents.RESERVED_RENDER_SIZE_CHANGED`:
881
892
 
882
893
  ```js
883
- engine.addEventListener('reserved_render_size_changed', e => console.log('reserve:', e.size));
894
+ engine.addEventListener(EngineEvents.RESERVED_RENDER_SIZE_CHANGED, e => console.log('reserve:', e.size));
884
895
  engine.setReservedRenderResolution(4096);
885
896
  await engine.init();
886
897
  console.log(engine.getReservedRenderResolution()); // 4096, or 2048 if the device declined
@@ -894,7 +905,7 @@ Track GPU (VRAM) usage across the whole pipeline. Sizes are measured from live G
894
905
 
895
906
  ```js
896
907
  const { current, peak, byCategory } = engine.getMemoryInfo(); // bytes
897
- // byCategory: { rays, queues, gbuffer, accum, geometry, materials, environment, stages }
908
+ // byCategory: { rays, queues, gbuffer, accum, geometry, materials, environment, stages, denoiser, canvas }
898
909
 
899
910
  engine.vram.resetPeak(); // reset the high-water mark to the current value
900
911
  engine.vram.getReport(); // formatted one-line summary string
@@ -902,6 +913,12 @@ engine.vram.getReport(); // formatted one-line summary string
902
913
 
903
914
  `peak` is a high-water mark, reset when a final render begins (`configureForMode('production')`). The engine's VRAM is largely monotonic — the ray pool only grows and the per-stage storage textures are fixed-size — so `peak` equals `current` during a steady render and only exceeds it after memory is released (lower resolution, a smaller scene, or removing the HDRI). The `stages` + `accum` categories (fixed 2048² storage textures) dominate the baseline.
904
915
 
916
+ `denoiser` is what the engine allocates for OIDN — its three float inputs, its half-float output and
917
+ the motion history. `canvas` is one image per presented surface (a browser may keep one or two more)
918
+ plus the buffer three.js's output pass tone-maps through. Not counted: oidn-web's own network weights
919
+ and activations, which it reports by count, not by size; and the neural passes, which run on a
920
+ `GPUDevice` of their own.
921
+
905
922
  The React app surfaces this as a `Memory: … | Peak: …` readout in the on-canvas stats overlay.
906
923
 
907
924
  #### CPU memory
@@ -974,7 +991,134 @@ The RNG is already pure — `hash(pixel, rayIndex, frame)`, no clock, no `Math.r
974
991
 
975
992
  - `engine.isDeterministic` — whether output is currently bit-reproducible.
976
993
  - `setDeterministicMode(true, { pinDispatch: false })` keeps the two readback-driven dispatch heuristics active. Output is then *not* reproducible; this exists so performance measurements reflect shipping behaviour rather than a configuration production never runs.
977
- - `renderFrames` awaits `engine.stages.pathTracer.blueNoiseReady` first — until the STBN atlases land the sampler reads a constant-0.5 placeholder that bakes permanently into the accumulation buffer. It raises `maxSamples` if needed, and throws if something retires the render early.
994
+ - `renderFrames` raises `maxSamples` if needed, and throws if something retires the render early.
995
+
996
+ #### Production renders, reproducibly
997
+
998
+ Deterministic mode buys reproducibility by turning adaptive sampling off. To render the way the
999
+ product does — adaptive sampling, pixel freeze and the per-bounce early exit all on — and still get
1000
+ the same image for the same input, drive the render with `renderUntilComplete`:
1001
+
1002
+ ```js
1003
+ engine.configureForMode('production');
1004
+ const { samples, retiredBy, denoised } = await engine.renderUntilComplete({ signal });
1005
+ // retiredBy: 'samples' | 'converged' | 'timeLimit'
1006
+ ```
1007
+
1008
+ It renders without `requestAnimationFrame` until the sample ceiling, convergence or the time limit —
1009
+ whichever comes first — then runs the final OIDN denoise once, and fires `RENDER_COMPLETE` as the
1010
+ loop does. Adaptive sampling decides from counts read back from the GPU; normally each is applied
1011
+ whenever it lands, so the frame that retires depends on how fast frames are submitted. For its
1012
+ duration `renderUntilComplete` runs those readbacks in **lockstep**: one issued at frame N is applied
1013
+ at frame N + 4 exactly, and the render waits if it has not landed; every reset also starts from seed
1014
+ 0 and from nothing the previous render measured. It turns interaction mode off meanwhile (a
1015
+ wall-clock mode for camera drags), and keeps submissions at most 2 × `drainEvery` frames ahead of
1016
+ the GPU. A time limit is wall-clock by nature, so that one stop is not reproducible; neither is
1017
+ auto-exposure, if you turn it on.
1018
+
1019
+ `engine.setLockstepReadbacks(true)` applies the same lockstep to the rAF loop and to `renderFrames`.
1020
+
1021
+ #### Batch rendering
1022
+
1023
+ The supported entry point for a render farm is `rayzee/src/Headless.js`, exported from the package.
1024
+ Its defaults are a batch renderer's — `strict`, `profile: 'physical'`, deterministic, storage off —
1025
+ so a degraded render throws instead of shipping:
1026
+
1027
+ ```js
1028
+ import { openHeadless, captureHeadless } from 'rayzee';
1029
+
1030
+ const app = await openHeadless({ canvas, model: url, width: 1920, height: 1080 });
1031
+ try {
1032
+ const frame = await captureHeadless(app, { samples: 256, denoise: true });
1033
+ // frame.data: RGBA8 (colorSpace 'srgb') or Float32 (colorSpace 'linear')
1034
+ // frame.source: what was read — 'oidn' here, 'accumulation' without denoise
1035
+ // frame.issues: everything the engine survived, when not strict
1036
+ } finally {
1037
+ app.dispose();
1038
+ }
1039
+ ```
1040
+
1041
+ `denoise: true` turns OIDN on before accumulating (it reads the albedo and normal buffers, which
1042
+ are written only while it is on), runs **one** final denoise with `app.runFinalDenoise()`, and reads
1043
+ it back with `renderToBuffer( { source: 'display' } )`. Driving it yourself is the same three calls.
1044
+ `renderToBuffer` reports the picture it read as `source`, and records `output.source_fallback` when
1045
+ `'display'` was asked for, a denoiser is in use, and nothing had published — so a strict host cannot
1046
+ ship a noisy image by mistake. Its `'srgb'` bytes are tone-mapped on the GPU — 9 ms at 4096×2160,
1047
+ where the CPU pass it replaced took 1.4 s on an M-series Mac and 10 s on a cloud host — and match the
1048
+ canvas to within one level (it rounds half a level up); `'linear'` is exact.
1049
+
1050
+ Constructing `PathTracerApp` yourself instead: pass `strict: true`; storage is then off unless you
1051
+ set it. Outside Chrome, pass `hostMemoryGB`.
1052
+
1053
+ **Provenance.** `engine.getProvenance()` — also `frame.provenance` from `captureHeadless` — is plain
1054
+ JSON naming what produced the image: engine and three.js versions, the profile by name *and* by
1055
+ value, the adapter, every live setting with its source, the colour pipeline, the render size and
1056
+ samples, and whether it ran headless, strict, deterministic or lockstepped. Store it beside each
1057
+ render and "what made this?" has an answer without rendering again.
1058
+
1059
+ **Pinning a look.** A change to what a render looks like when a host sets nothing — a default
1060
+ setting, a mode preset, a profile's values, light units — is released as a **major**, with the
1061
+ change described under BREAKING CHANGES in the release notes. Pin a major to pin a look; read the
1062
+ notes before moving to the next one.
1063
+
1064
+ #### Running in Node
1065
+
1066
+ The published build renders in plain Node on Dawn — the `webgpu` package, the WebGPU implementation
1067
+ inside Chrome — with no browser and no DOM shim:
1068
+
1069
+ ```js
1070
+ import { create, globals } from 'webgpu';
1071
+ import sharp from 'sharp';
1072
+ import { configurePlatform, openHeadless } from 'rayzee';
1073
+ import { nodePlatform } from 'rayzee/node';
1074
+
1075
+ Object.assign(globalThis, globals);
1076
+ const gpu = create([]); // keep a reference: Dawn crashes if it is collected
1077
+ Object.defineProperty(navigator, 'gpu', { value: gpu });
1078
+
1079
+ const decodeImage = async (bytes) => {
1080
+ const { data, info } = await sharp(bytes).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1081
+ return { data, width: info.width, height: info.height };
1082
+ };
1083
+ configurePlatform(nodePlatform({ decodeImage })); // after the webgpu globals, before any app
1084
+ const app = await openHeadless({ width: 1920, height: 1080, hostMemoryGB: 16 }); // no canvas: headless
1085
+ await app.loadModel('https://…/room.glb');
1086
+ const { samples } = await app.renderUntilComplete();
1087
+ const frame = await app.renderToBuffer({ source: 'display' });
1088
+ app.dispose();
1089
+ ```
1090
+
1091
+ - **No canvas means headless.** `new PathTracerApp(null)` — or `{ headless: true }` — makes its own
1092
+ canvas (a WebGPU context over a plain texture), runs no render loop (drive it with `renderFrames` or
1093
+ `renderUntilComplete`), and builds neither the overlay renderer nor the transform gizmo.
1094
+ - **`configurePlatform({ Worker, decodeImage })`** is where a host without a browser supplies what one
1095
+ would. `nodePlatform()` from `rayzee/node` fills it: `NodeWorker`, the Web Worker API over
1096
+ `worker_threads`, runs the engine's inlined workers; `decodeImage(bytes, mimeType)` is yours — PNG,
1097
+ JPEG or WebP to RGBA8, straight alpha, top row first — since the engine carries no decoder. It decodes
1098
+ every glTF image, embedded or not, and JPEG/PNG skies. PNGs with bytes after `IEND`, which browsers
1099
+ accept and strict decoders refuse, are trimmed first; a texture that still fails is recorded as
1100
+ `texture.build_failed` rather than dropped. Measured on a 91-texture model: `sharp` and
1101
+ `@napi-rs/canvas` (`await loadImage()`, then a 2D canvas) both decode in parallel, ~0.3 s against
1102
+ pngjs's 2.5 s serial. The canvas route premultiplies, which zeroes colour under alpha 0 and loses
1103
+ it under low alpha; `sharp` is exact. `jpeg-js` differs from libjpeg-turbo by up to 63 levels.
1104
+ - **`nodePlatform()` wraps the `webgpu` queue**, so install its globals first. dawn.node 0.6.1
1105
+ segfaults on `writeBuffer` / `writeTexture` from a `SharedArrayBuffer`, which WebGPU allows and the
1106
+ engine's triangle and BVH stores are once a scene is large; those uploads are copied out, 64 MB at
1107
+ a time.
1108
+ - **Textures are packed on the CPU** where there is no `createImageBitmap`: exact when a map fits its
1109
+ bucket, bilinear otherwise (a browser's canvas filter differs slightly there). A bucket over 8 MB
1110
+ packs in a worker, reading images decoded by `decodeImage` in place (they are kept in shared
1111
+ memory): on the main thread it held up the BVH workers, and a 1.9M-triangle, 91-texture model
1112
+ loaded in 4.1–4.9 s. It now loads in 2.1 s, against Chrome's 2.9 s on the same machine.
1113
+ - `nodePlatform()` also defines `ProgressEvent`, which three.js's `FileLoader` constructs while
1114
+ streaming; that is the only global it sets. three.js's own Draco and KTX2 workers call the global
1115
+ `Worker`, so a model using either also needs `globalThis.Worker = NodeWorker`.
1116
+
1117
+ Measured on this bench's corpus: all 29 scenes match the Chrome goldens (`npm run bench:node`, RMSE
1118
+ ≤ 0.0036, no pixel over 0.02 — the readback's tone map accounts for most of it), and a textured glTF with an
1119
+ HDR, a PNG sky or a gradient matches Chrome block for block within 0.05 of a level. Not available
1120
+ without a browser: on-disk storage (OPFS), gobo libraries (they draw on a 2D canvas), and the AI
1121
+ upscaler.
978
1122
 
979
1123
  #### GPU timing
980
1124
 
@@ -1005,7 +1149,7 @@ list. Both need `timestamp-query`, and `getDenoiseProfile()` returns `null` when
1005
1149
 
1006
1150
  `engine.storage` is a `StorageManager` over the browser's origin private file system, or `null` where
1007
1151
  there is none (a private window, Node) — everything works without it, only slower. The engine keeps
1008
- its caches there: downloads (models, skies, OIDN weights, STBN atlases — revalidated with `HEAD`),
1152
+ its caches there: downloads (models, skies, OIDN weights — revalidated at most daily with a 1-byte `Range` request, and served from the cache meanwhile),
1009
1153
  unpacked `.tar.gz` archives and archive indexes, built scenes (reopened without rebuilding BVHs when
1010
1154
  the first build took ≥ 10 s), and environment sampling tables. Caches share a budget of 30 % of the
1011
1155
  quota and are evicted least-recently-used; `engine.storage.usage()` reports each area.
@@ -1103,7 +1247,7 @@ engine.addEventListener(EngineEvents.RENDER_COMPLETE, (e) => {
1103
1247
  | `FRAME` | Fires once per `animate()` tick — hook external instrumentation (stats panels, telemetry) here |
1104
1248
  | `DENOISING_START` / `DENOISING_END` | Denoiser runs. `event.continuous` is `true` for a cadence denoise of the still-accumulating image, `false` for the one that ends a render |
1105
1249
  | `UPSCALING_START` / `UPSCALING_PROGRESS` / `UPSCALING_END` | AI upscaler runs |
1106
- | `LOADING_UPDATE` / `LOADING_RESET` | Asset loading progress |
1250
+ | `LOADING_UPDATE` / `LOADING_RESET` | Asset loading progress. A failed load ends with `failed: true` and the error as `status`; an archive that asks which parts to load ends with `LOADING_RESET` |
1107
1251
  | `STATS_UPDATE` | Performance stats updated |
1108
1252
  | `OBJECT_SELECTED` / `OBJECT_DESELECTED` | Object selection changes |
1109
1253
  | `OBJECT_DOUBLE_CLICKED` | Object double-clicked |
@@ -1119,6 +1263,18 @@ engine.addEventListener(EngineEvents.RENDER_COMPLETE, (e) => {
1119
1263
  | `VIDEO_RENDER_PROGRESS` / `VIDEO_RENDER_COMPLETE` | Video export progress |
1120
1264
  | `DEVICE_LOST` | The GPU device was lost (driver crash/reset) — rendering halts instead of throwing into a dead device |
1121
1265
  | `DISPOSE` | Engine is being disposed (fires before teardown begins, so listeners can release their own references) |
1266
+ | `ISSUE` | An issue was recorded — see [Degradation contract](#degradation-contract) |
1267
+ | `MODEL_LOADED` / `OBJECT3D_LOADED` / `MODEL_ADDED` / `SCENE_OBJECT_REMOVED` / `SCENE_UNLOADED` | Scene content changed |
1268
+ | `SCENE_REBUILD` / `SCENE_SPILLED` / `SCENE_METADATA_APPLIED` | The scene was rebuilt, moved to disk, or had its authored environment applied |
1269
+ | `ENVIRONMENT_LOADED` / `TEXTURES_REPROCESSED` | An environment or the texture arrays were (re)built |
1270
+ | `CAMERAS_UPDATED` / `CAMERA_SWITCHED` / `FOCUS_CHANGED` | Camera list, active camera, or focus distance changed |
1271
+ | `RESOLUTION_CHANGED` / `RESERVED_RENDER_SIZE_CHANGED` | The render size or the reserve changed |
1272
+ | `STORAGE_CHANGED` / `TIMELINE_CHANGED` | On-disk storage or timeline keys changed |
1273
+
1274
+ Several of these used to be plain strings (`'ModelLoaded'`, `'RenderComplete'`, `'resolution_changed'`, …).
1275
+ The engine still dispatches each under its old name as well, until the next major; `LEGACY_EVENT_NAMES`
1276
+ maps new to old. Listening for a name the engine never dispatches logs a warning once, since such a
1277
+ listener fails silently otherwise.
1122
1278
 
1123
1279
  ### Advanced: Custom Pipeline Stages
1124
1280
 
@@ -1144,7 +1300,11 @@ class MyCustomStage extends RenderStage {
1144
1300
 
1145
1301
  ```js
1146
1302
  // Core
1147
- import { PathTracerApp, EngineEvents } from 'rayzee';
1303
+ import { PathTracerApp, EngineEvents, LEGACY_EVENT_NAMES } from 'rayzee';
1304
+
1305
+ // Platform services for hosts without a browser (see Running in Node)
1306
+ import { configurePlatform, getPlatform } from 'rayzee';
1307
+ import { nodePlatform, NodeWorker } from 'rayzee/node';
1148
1308
 
1149
1309
  // Configuration & presets
1150
1310
  import {
@@ -1,5 +1,5 @@
1
1
  import { t as e } from "./rolldown-runtime-CAFD8bLK.js";
2
- import { S as t, t as n, y as r } from "./ToneMapGPU-DpUZDMbQ.js";
2
+ import { C as t, t as n, y as r } from "./ToneMapGPU-B6XIe3DU.js";
3
3
  //#region src/neural/NeuralRetouch.js
4
4
  var i = /* @__PURE__ */ e({
5
5
  NeuralRetouch: () => m,
@@ -1,5 +1,5 @@
1
1
  import { t as e } from "./rolldown-runtime-CAFD8bLK.js";
2
- import { S as t, t as n, y as r } from "./ToneMapGPU-DpUZDMbQ.js";
2
+ import { C as t, t as n, y as r } from "./ToneMapGPU-B6XIe3DU.js";
3
3
  //#region src/EngineEvents.js
4
4
  var i = {
5
5
  RENDER_COMPLETE: "engine:renderComplete",
@@ -20,6 +20,9 @@ var i = {
20
20
  OBJECT_TRANSFORM_START: "engine:objectTransformStart",
21
21
  OBJECT_TRANSFORM_END: "engine:objectTransformEnd",
22
22
  TRANSFORM_MODE_CHANGED: "engine:transformModeChanged",
23
+ CAMERA_SWITCHED: "engine:cameraSwitched",
24
+ CAMERAS_UPDATED: "engine:camerasUpdated",
25
+ FOCUS_CHANGED: "engine:focusChanged",
23
26
  AUTO_FOCUS_UPDATED: "engine:autoFocusUpdated",
24
27
  ORTHO_HEIGHT_UPDATED: "engine:orthoHeightUpdated",
25
28
  AUTO_EXPOSURE_UPDATED: "engine:autoExposureUpdated",
@@ -35,21 +38,50 @@ var i = {
35
38
  DISPOSE: "engine:dispose",
36
39
  DEVICE_LOST: "engine:deviceLost",
37
40
  ISSUE: "engine:issue",
41
+ MODEL_LOADED: "engine:modelLoaded",
42
+ OBJECT3D_LOADED: "engine:object3dLoaded",
43
+ MODEL_ADDED: "engine:modelAdded",
44
+ SCENE_OBJECT_REMOVED: "engine:sceneObjectRemoved",
45
+ SCENE_UNLOADED: "engine:sceneUnloaded",
46
+ SCENE_REBUILD: "engine:sceneRebuild",
47
+ SCENE_SPILLED: "engine:sceneSpilled",
38
48
  SCENE_METADATA_APPLIED: "engine:sceneMetadataApplied",
49
+ ENVIRONMENT_LOADED: "engine:environmentLoaded",
50
+ TEXTURES_REPROCESSED: "engine:texturesReprocessed",
51
+ RESOLUTION_CHANGED: "engine:resolutionChanged",
52
+ RESERVED_RENDER_SIZE_CHANGED: "engine:reservedRenderSizeChanged",
39
53
  STORAGE_CHANGED: "engine:storageChanged"
40
- }, a = /* @__PURE__ */ e({
41
- NeuralSuperRes: () => g,
42
- SR_MAX_INPUT: () => c,
54
+ }, a = Object.freeze({
55
+ [i.RENDER_COMPLETE]: "RenderComplete",
56
+ [i.RENDER_RESET]: "RenderReset",
57
+ [i.CAMERA_SWITCHED]: "CameraSwitched",
58
+ [i.CAMERAS_UPDATED]: "CamerasUpdated",
59
+ [i.FOCUS_CHANGED]: "focusChanged",
60
+ [i.MODEL_LOADED]: "ModelLoaded",
61
+ [i.OBJECT3D_LOADED]: "Object3DLoaded",
62
+ [i.MODEL_ADDED]: "ModelAdded",
63
+ [i.SCENE_OBJECT_REMOVED]: "SceneObjectRemoved",
64
+ [i.SCENE_UNLOADED]: "SceneUnloaded",
65
+ [i.SCENE_REBUILD]: "SceneRebuild",
66
+ [i.SCENE_SPILLED]: "SceneSpilled",
67
+ [i.SCENE_METADATA_APPLIED]: "SceneMetadataApplied",
68
+ [i.ENVIRONMENT_LOADED]: "EnvironmentLoaded",
69
+ [i.TEXTURES_REPROCESSED]: "TexturesReprocessed",
70
+ [i.RESOLUTION_CHANGED]: "resolution_changed",
71
+ [i.RESERVED_RENDER_SIZE_CHANGED]: "reserved_render_size_changed"
72
+ }), o = /* @__PURE__ */ e({
73
+ NeuralSuperRes: () => _,
74
+ SR_MAX_INPUT: () => l,
43
75
  SR_SCALE: () => 2,
44
- presentRGBA8: () => b,
45
- readDenoisedHalf: () => h,
46
- releaseDenoisedReader: () => m,
47
- renderUpscaled: () => v
48
- }), o = 2, s = t("neural"), c = 2048, l = null;
49
- function u() {
50
- if (l) return l;
76
+ presentRGBA8: () => x,
77
+ readDenoisedHalf: () => g,
78
+ releaseDenoisedReader: () => h,
79
+ renderUpscaled: () => y
80
+ }), s = 2, c = t("neural"), l = 2048, u = null;
81
+ function d() {
82
+ if (u) return u;
51
83
  let { neuralRuntimeUrl: e, neuralAssetBaseUrl: t } = r();
52
- return t ? (globalThis.__NEURAL_ASSET_BASE__ = t, l = new Promise((t, n) => {
84
+ return t ? (globalThis.__NEURAL_ASSET_BASE__ = t, u = new Promise((t, n) => {
53
85
  if (globalThis.NeuralRuntime?.getNativeSR) return t(globalThis.NeuralRuntime.getNativeSR());
54
86
  let r = document.createElement("script");
55
87
  r.src = e, r.onload = () => {
@@ -57,13 +89,13 @@ function u() {
57
89
  if (!e) return n(/* @__PURE__ */ Error("Neural runtime loaded but exposed no super-resolution entry"));
58
90
  t(e());
59
91
  }, r.onerror = () => n(/* @__PURE__ */ Error(`Failed to load the neural runtime from ${e}`)), document.head.appendChild(r);
60
- }), l) : Promise.reject(/* @__PURE__ */ Error("No neural model location: set neuralAssetBaseUrl with configureAssets()"));
92
+ }), u) : Promise.reject(/* @__PURE__ */ Error("No neural model location: set neuralAssetBaseUrl with configureAssets()"));
61
93
  }
62
- var d = "\n@group(0) @binding(0) var src: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n\n@compute @workgroup_size(8, 8)\nfn main( @builtin(global_invocation_id) gid: vec3<u32> ) {\n let size = textureDimensions( src );\n if ( gid.x >= size.x || gid.y >= size.y ) { return; }\n let c = textureLoad( src, vec2<i32>( gid.xy ), 0 ).rgb;\n let a = ( gid.y * size.x + gid.x ) * 2u;\n dst[ a ] = pack2x16float( c.rg );\n dst[ a + 1u ] = pack2x16float( vec2<f32>( c.b, 1.0 ) );\n}\n", f = null;
63
- function p(e, t, n) {
64
- if (f && (f.device !== e || f.width !== t || f.height !== n) && m(), f) return f;
94
+ var f = "\n@group(0) @binding(0) var src: texture_2d<f32>;\n@group(0) @binding(1) var<storage, read_write> dst: array<u32>;\n\n@compute @workgroup_size(8, 8)\nfn main( @builtin(global_invocation_id) gid: vec3<u32> ) {\n let size = textureDimensions( src );\n if ( gid.x >= size.x || gid.y >= size.y ) { return; }\n let c = textureLoad( src, vec2<i32>( gid.xy ), 0 ).rgb;\n let a = ( gid.y * size.x + gid.x ) * 2u;\n dst[ a ] = pack2x16float( c.rg );\n dst[ a + 1u ] = pack2x16float( vec2<f32>( c.b, 1.0 ) );\n}\n", p = null;
95
+ function m(e, t, n) {
96
+ if (p && (p.device !== e || p.width !== t || p.height !== n) && h(), p) return p;
65
97
  let r = t * n * 8;
66
- return f = {
98
+ return p = {
67
99
  device: e,
68
100
  width: t,
69
101
  height: n,
@@ -83,20 +115,20 @@ function p(e, t, n) {
83
115
  compute: {
84
116
  module: e.createShaderModule({
85
117
  label: "rayzee:neural-sr-pack",
86
- code: d
118
+ code: f
87
119
  }),
88
120
  entryPoint: "main"
89
121
  }
90
122
  })
91
- }, f;
123
+ }, p;
92
124
  }
93
- function m() {
94
- f?.storage.destroy(), f?.map.destroy(), f = null;
125
+ function h() {
126
+ p?.storage.destroy(), p?.map.destroy(), p = null;
95
127
  }
96
- async function h(e) {
128
+ async function g(e) {
97
129
  let t = e?._outGPUTexture;
98
130
  if (!t) throw Error("NeuralSuperRes: no denoised frame — enable OIDN and finish a render first");
99
- let n = e.gpuDevice, { width: r, height: i } = e._outTexSize, a = p(n, r, i), o = n.createBindGroup({
131
+ let n = e.gpuDevice, { width: r, height: i } = e._outTexSize, a = m(n, r, i), o = n.createBindGroup({
100
132
  layout: a.pipeline.getBindGroupLayout(0),
101
133
  entries: [{
102
134
  binding: 0,
@@ -114,7 +146,7 @@ async function h(e) {
114
146
  height: i
115
147
  };
116
148
  }
117
- var g = class e {
149
+ var _ = class e {
118
150
  constructor(e, t) {
119
151
  this.sr = e, this.canvas = t, this.slot = 0, this._color = null, this._depth = null, this._motion = null, this._toneMapper = null, this.disposed = !1;
120
152
  }
@@ -135,8 +167,8 @@ var g = class e {
135
167
  }
136
168
  static async create({ width: t, height: n, onProgress: r = () => {} } = {}) {
137
169
  if (!Number.isInteger(t) || !Number.isInteger(n) || t < 1 || n < 1) throw Error("NeuralSuperRes.create: width and height must be positive integers");
138
- if (t > 2048 || n > 2048) throw Error(`NeuralSuperRes.create: input capped at ${c}px per side (${c * 2}px output), got ${t}x${n}. Above that the runtime's history plane exceeds maxTextureDimension2D and it fails later with an unrelated bind-group error.`);
139
- let i = await u(), a = document.createElement("canvas");
170
+ if (t > 2048 || n > 2048) throw Error(`NeuralSuperRes.create: input capped at ${l}px per side (${l * 2}px output), got ${t}x${n}. Above that the runtime's history plane exceeds maxTextureDimension2D and it fails later with an unrelated bind-group error.`);
171
+ let i = await d(), a = document.createElement("canvas");
140
172
  a.style.cssText = "position:fixed;left:-10000px;top:0;pointer-events:none", document.body.appendChild(a);
141
173
  let o;
142
174
  try {
@@ -199,12 +231,12 @@ var g = class e {
199
231
  try {
200
232
  this.sr.destroy();
201
233
  } catch (e) {
202
- s.warn("super resolution teardown:", e);
234
+ c.warn("super resolution teardown:", e);
203
235
  }
204
236
  }
205
237
  }
206
238
  };
207
- function _(e, t, n) {
239
+ function v(e, t, n) {
208
240
  return new Promise((r, a) => {
209
241
  let o = !1, s = (t, n) => {
210
242
  o || (o = !0, e.removeEventListener(i.DENOISING_END, l), clearInterval(u), clearTimeout(d), t ? r() : a(n));
@@ -217,7 +249,7 @@ function _(e, t, n) {
217
249
  }, 200), d = setTimeout(() => s(!1, /* @__PURE__ */ Error("NeuralSuperRes: timed out waiting for the render to finish")), n);
218
250
  });
219
251
  }
220
- async function v(e, { outputWidth: t, outputHeight: n, samples: r = 64, upscaler: i = null, timeoutMs: a = 6e5, present: o = !0, onProgress: s = () => {} } = {}) {
252
+ async function y(e, { outputWidth: t, outputHeight: n, samples: r = 64, upscaler: i = null, timeoutMs: a = 6e5, present: o = !0, onProgress: s = () => {} } = {}) {
221
253
  if (t % 2 || n % 2) throw Error(`renderUpscaled: output must be divisible by 2, got ${t}x${n}`);
222
254
  let c = t / 2, l = n / 2, u = e.stages?.pathTracer, d = {
223
255
  width: u?.width,
@@ -229,7 +261,7 @@ async function v(e, { outputWidth: t, outputHeight: n, samples: r = 64, upscaler
229
261
  try {
230
262
  if (!p) {
231
263
  let e = performance.now();
232
- s("Loading the super-resolution model"), p = await g.create({
264
+ s("Loading the super-resolution model"), p = await _.create({
233
265
  width: c,
234
266
  height: l,
235
267
  onProgress: s
@@ -237,17 +269,17 @@ async function v(e, { outputWidth: t, outputHeight: n, samples: r = 64, upscaler
237
269
  } else if (p.inputWidth !== c || p.inputHeight !== l) throw Error(`renderUpscaled: upscaler is ${p.inputWidth}x${p.inputHeight}, need ${c}x${l}`);
238
270
  let i = performance.now();
239
271
  s(`Tracing ${c}x${l} at ${r} spp`), e.pauseRendering = !1, e.setCanvasSize(c, l), await new Promise((e) => setTimeout(e, 300)), e.denoisingManager.applyOIDNEnabled(!0), e.settings.set("maxSamples", r);
240
- let u = _(e, r, a);
272
+ let u = v(e, r, a);
241
273
  e.reset(), await u, e.pauseRendering = !0, m.render = performance.now() - i, s("Reading the denoised frame");
242
- let d = performance.now(), f = await h(e.denoisingManager?.denoiser);
274
+ let d = performance.now(), f = await g(e.denoisingManager?.denoiser);
243
275
  m.read = performance.now() - d, s(`Upscaling to ${t}x${n}`);
244
- let v = {
276
+ let h = {
245
277
  exposure: e.renderer.toneMappingExposure,
246
278
  toneMapping: e.renderer.toneMapping,
247
279
  saturation: e.settings.get("saturation") ?? 1
248
- }, { rgba8: b, width: x, height: S, ms: C, toneMs: w } = await p.upscaleToRGBA8(f, v);
249
- return m.upscale = C, m.tonemap = w, o && y(e.denoisingManager?.upscalerCanvas, b, x, S), {
250
- rgba8: b,
280
+ }, { rgba8: y, width: x, height: S, ms: C, toneMs: w } = await p.upscaleToRGBA8(f, h);
281
+ return m.upscale = C, m.tonemap = w, o && b(e.denoisingManager?.upscalerCanvas, y, x, S), {
282
+ rgba8: y,
251
283
  width: x,
252
284
  height: S,
253
285
  timings: m,
@@ -259,11 +291,11 @@ async function v(e, { outputWidth: t, outputHeight: n, samples: r = 64, upscaler
259
291
  d.width && d.height && e.setCanvasSize(d.width, d.height), d.maxSamples !== void 0 && e.settings?.set?.("maxSamples", d.maxSamples), d.finalDenoise !== void 0 && e.denoisingManager?.applyOIDNEnabled(d.finalDenoise), e.pauseRendering = d.paused;
260
292
  }
261
293
  }
262
- function y(e, t, n, r) {
294
+ function b(e, t, n, r) {
263
295
  return e ? (e.width = n, e.height = r, e.getContext("2d").putImageData(new ImageData(new Uint8ClampedArray(t.buffer ?? t), n, r), 0, 0), e.style.display = "block", !0) : !1;
264
296
  }
265
- function b(e, t, n, r) {
266
- return y(e, t, n, r);
297
+ function x(e, t, n, r) {
298
+ return b(e, t, n, r);
267
299
  }
268
300
  //#endregion
269
- export { i as a, v as i, c as n, o as r, a as t };
301
+ export { i as a, y as i, l as n, a as o, s as r, o as t };