rayzee 7.19.1 → 7.20.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.
- package/README.md +22 -4
- package/dist/assets/{AIUpscalerWorker-AXN-lKWN.js.map → AIUpscalerWorker-ByQj53uK.js.map} +1 -1
- package/dist/assets/{BVHRefitWorker-GkmNJYvb.js.map → BVHRefitWorker-WezTgpI5.js.map} +1 -1
- package/dist/assets/BVHSubtreeWorker-B98NTgvc.js.map +1 -0
- package/dist/assets/BVHWorker-H923xA44.js.map +1 -0
- package/dist/assets/{CDFWorker-BFQUr3By.js.map → CDFWorker-CSxzmFhe.js.map} +1 -1
- package/dist/assets/{TexturesWorker-6mlBy4Dm.js.map → TexturesWorker-Dm6RnwKh.js.map} +1 -1
- package/dist/rayzee.es.js +1507 -1375
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +49 -48
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/Passes/AIUpscaler.js +2 -12
- package/src/PathTracerApp.js +270 -1
- package/src/Pipeline/RenderPipeline.js +6 -0
- package/src/Processor/BVHBuilder.js +4 -18
- package/src/Processor/EquirectHDRInfo.js +2 -12
- package/src/Processor/ParallelBVHBuilder.js +5 -36
- package/src/Processor/SceneProcessor.js +51 -32
- package/src/Processor/TextureCreator.js +2 -13
- package/src/Stages/PathTracerStage.js +23 -2
- package/dist/assets/AIUpscalerWorker-AXN-lKWN.js +0 -2
- package/dist/assets/BVHRefitWorker-GkmNJYvb.js +0 -2
- package/dist/assets/BVHSubtreeWorker-D7KMbrQ-.js +0 -2
- package/dist/assets/BVHSubtreeWorker-D7KMbrQ-.js.map +0 -1
- package/dist/assets/BVHWorker-DOaGn6uN.js +0 -2
- package/dist/assets/BVHWorker-DOaGn6uN.js.map +0 -1
- package/dist/assets/CDFWorker-BFQUr3By.js +0 -2
- package/dist/assets/TexturesWorker-6mlBy4Dm.js +0 -2
- package/src/Processor/Workers/fetchAsWorker.js +0 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rayzee",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.20.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Real-time WebGPU path tracing engine built on Three.js",
|
|
6
6
|
"main": "dist/rayzee.umd.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"author": "Atul Mourya",
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"scripts": {
|
|
33
|
-
"build": "vite build
|
|
33
|
+
"build": "vite build",
|
|
34
34
|
"prepublishOnly": "npm run build"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
package/src/Passes/AIUpscaler.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { EventDispatcher, ACESFilmicToneMapping } from 'three';
|
|
2
2
|
import { TONE_MAP_FNS, SRGB_GAMMA, applySaturation } from '../Processor/ToneMapCPU.js';
|
|
3
|
-
import { fetchAsWorker } from '../Processor/Workers/fetchAsWorker.js';
|
|
4
3
|
import { getAssetConfig } from '../AssetConfig.js';
|
|
5
|
-
import
|
|
4
|
+
import AIUpscalerWorker from '../Processor/Workers/AIUpscalerWorker.js?worker&inline';
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
// ─── Model Configuration ───────────────────────────────────────────────────────
|
|
@@ -166,16 +165,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
166
165
|
// Create worker on first use
|
|
167
166
|
if ( ! this._worker ) {
|
|
168
167
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this._worker = new Worker( AI_UPSCALER_WORKER_URL, { type: 'module' } );
|
|
172
|
-
|
|
173
|
-
} catch ( e ) {
|
|
174
|
-
|
|
175
|
-
if ( e.name !== 'SecurityError' ) throw e;
|
|
176
|
-
this._worker = await fetchAsWorker( AI_UPSCALER_WORKER_URL );
|
|
177
|
-
|
|
178
|
-
}
|
|
168
|
+
this._worker = new AIUpscalerWorker();
|
|
179
169
|
|
|
180
170
|
}
|
|
181
171
|
|
package/src/PathTracerApp.js
CHANGED
|
@@ -56,6 +56,7 @@ const _appsByCanvas = new WeakMap();
|
|
|
56
56
|
* - `app.environmentManager` — EnvironmentManager (HDRI, procedural sky, mode switching)
|
|
57
57
|
* - `app.settings` — {@link RenderSettings} (all render parameters)
|
|
58
58
|
* - `app.stages` — Named pipeline stages for advanced control
|
|
59
|
+
* - `app.sceneMeshes` — meshes backing the BVH, in buffer order (see {@link refitBVH})
|
|
59
60
|
*
|
|
60
61
|
* Extends EventDispatcher for event-driven communication with stores/UI.
|
|
61
62
|
*/
|
|
@@ -166,6 +167,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
166
167
|
this._disposed = false;
|
|
167
168
|
this._deviceLost = false;
|
|
168
169
|
|
|
170
|
+
// Deterministic-render mode — see setDeterministicMode()
|
|
171
|
+
this._deterministic = false;
|
|
172
|
+
this._dispatchPinned = false;
|
|
173
|
+
this._deterministicRestore = null;
|
|
174
|
+
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
/**
|
|
@@ -1149,6 +1155,24 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1149
1155
|
// BVH Refit (Animation)
|
|
1150
1156
|
// ═══════════════════════════════════════════════════════════════
|
|
1151
1157
|
|
|
1158
|
+
/**
|
|
1159
|
+
* The meshes backing the current acceleration structure, in the order their triangles
|
|
1160
|
+
* occupy the shared buffers — which is what "original mesh order" means in
|
|
1161
|
+
* {@link refitBVH} and {@link refitBLASes}, and what `meshIndex` indexes.
|
|
1162
|
+
*
|
|
1163
|
+
* Walking your own model instead is not equivalent: the list is a depth-first pre-order
|
|
1164
|
+
* traversal of the whole mesh scene, so it also contains engine-owned meshes (the hidden
|
|
1165
|
+
* ground-projection disk) and any mesh a multi-material split produced. A positions
|
|
1166
|
+
* buffer built from a different set is silently misaligned.
|
|
1167
|
+
*
|
|
1168
|
+
* @returns {import('three').Mesh[]} Live reference — do not mutate.
|
|
1169
|
+
*/
|
|
1170
|
+
get sceneMeshes() {
|
|
1171
|
+
|
|
1172
|
+
return this._sdf?.meshes ?? [];
|
|
1173
|
+
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1152
1176
|
/**
|
|
1153
1177
|
* Update vertex positions for animation without full BVH rebuild.
|
|
1154
1178
|
* O(N) bottom-up AABB refit instead of O(N log N) SAH rebuild.
|
|
@@ -1156,7 +1180,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1156
1180
|
* Topology must stay the same (same triangle count and connectivity).
|
|
1157
1181
|
* Call this per-frame for skeletal/morph-target animation.
|
|
1158
1182
|
*
|
|
1159
|
-
* @param {Float32Array} newPositions - 9 floats per triangle (ax,ay,az, bx,by,bz, cx,cy,cz) in
|
|
1183
|
+
* @param {Float32Array} newPositions - 9 floats per triangle (ax,ay,az, bx,by,bz, cx,cy,cz) for every triangle in the scene, meshes in {@link sceneMeshes} order and triangles in index order
|
|
1160
1184
|
* @param {Float32Array} [newNormals] - Optional 9 floats per triangle smooth normals. If omitted, face normals are computed from positions.
|
|
1161
1185
|
* @returns {Promise<{ refitTimeMs: number }>}
|
|
1162
1186
|
*/
|
|
@@ -1446,6 +1470,251 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1446
1470
|
|
|
1447
1471
|
}
|
|
1448
1472
|
|
|
1473
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1474
|
+
// Deterministic / headless control
|
|
1475
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* Pins every wall-clock- and readback-dependent input so that N samples from a
|
|
1479
|
+
* fresh `reset()` reproduce bit-for-bit.
|
|
1480
|
+
*
|
|
1481
|
+
* The image is always a pure function of (pixel, frame, uniforms) — no shader reads
|
|
1482
|
+
* a clock and there is no `Math.random()` in the render path. What varies run to run
|
|
1483
|
+
* is *which* uniforms and dispatch grids are live on frame k:
|
|
1484
|
+
*
|
|
1485
|
+
* - `useAdaptiveSampling` retires the whole frame off an async CONVERGED_COUNT
|
|
1486
|
+
* readback, so two runs accumulate different sample totals for one `maxSamples`.
|
|
1487
|
+
* - `usePixelFreeze` sizes the bounce-0 grid from a stale active-pixel readback.
|
|
1488
|
+
* - `_bounceEarlyExitThreshold` / `_useDynamicDispatch` both consume the async
|
|
1489
|
+
* survivor curve. Kernels bind on ENTERING_COUNT, so an under-sized grid silently
|
|
1490
|
+
* drops rays, and the frame a readback lands on is GPU-scheduled.
|
|
1491
|
+
* - `interactionModeEnabled` is a 100 ms timer that clamps bounces to 1, disables
|
|
1492
|
+
* accumulation and freezes frameCount; it engages on the very first frame.
|
|
1493
|
+
* - auto-focus raycasts per frame and auto-exposure adapts off `performance.now()`.
|
|
1494
|
+
*
|
|
1495
|
+
* Leaves the rAF loop stopped — drive rendering with {@link PathTracerApp#renderFrames}.
|
|
1496
|
+
* Idempotent; pass `false` to restore the previous configuration.
|
|
1497
|
+
*
|
|
1498
|
+
* `pinDispatch: false` keeps the two readback-driven dispatch heuristics
|
|
1499
|
+
* (`_useDynamicDispatch` and the per-bounce early exit) ACTIVE while still pinning the
|
|
1500
|
+
* render loop, sample count and timers. Output is then no longer bit-reproducible, so
|
|
1501
|
+
* this is only for performance measurement — it exists because those heuristics are
|
|
1502
|
+
* real shipping behaviour, and benchmarking with them disabled measures a configuration
|
|
1503
|
+
* production never runs.
|
|
1504
|
+
*
|
|
1505
|
+
* @param {boolean} [enabled=true]
|
|
1506
|
+
* @param {Object} [options]
|
|
1507
|
+
* @param {boolean} [options.pinDispatch=true] - false to keep production dispatch heuristics
|
|
1508
|
+
* @returns {boolean} whether deterministic mode is now active
|
|
1509
|
+
*/
|
|
1510
|
+
setDeterministicMode( enabled = true, { pinDispatch = true } = {} ) {
|
|
1511
|
+
|
|
1512
|
+
const stage = this.stages.pathTracer;
|
|
1513
|
+
if ( ! stage ) return false;
|
|
1514
|
+
|
|
1515
|
+
if ( enabled ) {
|
|
1516
|
+
|
|
1517
|
+
if ( ! this._deterministicRestore ) {
|
|
1518
|
+
|
|
1519
|
+
this._deterministicRestore = {
|
|
1520
|
+
settings: {
|
|
1521
|
+
useAdaptiveSampling: this.settings.get( 'useAdaptiveSampling' ),
|
|
1522
|
+
usePixelFreeze: this.settings.get( 'usePixelFreeze' ),
|
|
1523
|
+
interactionModeEnabled: this.settings.get( 'interactionModeEnabled' ),
|
|
1524
|
+
renderLimitMode: this.settings.get( 'renderLimitMode' ),
|
|
1525
|
+
renderTimeLimit: this.settings.get( 'renderTimeLimit' ),
|
|
1526
|
+
},
|
|
1527
|
+
bounceEarlyExit: stage._bounceEarlyExitThreshold,
|
|
1528
|
+
dynamicDispatch: stage._useDynamicDispatch,
|
|
1529
|
+
autoFocusMode: this.cameraManager?.autoFocusMode,
|
|
1530
|
+
autoExposure: this.stages.autoExposure?.enabled,
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Cleared together on purpose: the freeze streak is stamped inside the
|
|
1536
|
+
// convergence block, so freeze-on/adaptive-off would run the freeze path
|
|
1537
|
+
// against a streak buffer nothing writes or clears.
|
|
1538
|
+
this.settings.setMany( {
|
|
1539
|
+
useAdaptiveSampling: false,
|
|
1540
|
+
usePixelFreeze: false,
|
|
1541
|
+
interactionModeEnabled: false,
|
|
1542
|
+
renderLimitMode: 'frames',
|
|
1543
|
+
renderTimeLimit: 0,
|
|
1544
|
+
}, { silent: true } );
|
|
1545
|
+
|
|
1546
|
+
if ( pinDispatch ) {
|
|
1547
|
+
|
|
1548
|
+
// -1 is unreachable by a uint survivor count, and both _buildWavefrontKernels()
|
|
1549
|
+
// and _resizeWavefrontInPlace() preserve the sentinel across rebuilds.
|
|
1550
|
+
stage._bounceEarlyExitThreshold = - 1;
|
|
1551
|
+
stage._useDynamicDispatch = false;
|
|
1552
|
+
|
|
1553
|
+
} else {
|
|
1554
|
+
|
|
1555
|
+
// Restore the values captured on first enable, so a perf pass measures the
|
|
1556
|
+
// same dispatch behaviour a real render uses.
|
|
1557
|
+
stage._bounceEarlyExitThreshold = this._deterministicRestore.bounceEarlyExit;
|
|
1558
|
+
stage._useDynamicDispatch = this._deterministicRestore.dynamicDispatch;
|
|
1559
|
+
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
this.cameraManager?.setAutoFocusMode( 'manual' );
|
|
1563
|
+
if ( this.stages.autoExposure ) this.stages.autoExposure.enabled = false;
|
|
1564
|
+
|
|
1565
|
+
this._deterministic = true;
|
|
1566
|
+
this._dispatchPinned = pinDispatch;
|
|
1567
|
+
|
|
1568
|
+
} else if ( this._deterministicRestore ) {
|
|
1569
|
+
|
|
1570
|
+
const prev = this._deterministicRestore;
|
|
1571
|
+
|
|
1572
|
+
this.settings.setMany( prev.settings, { silent: true } );
|
|
1573
|
+
stage._bounceEarlyExitThreshold = prev.bounceEarlyExit;
|
|
1574
|
+
stage._useDynamicDispatch = prev.dynamicDispatch;
|
|
1575
|
+
|
|
1576
|
+
if ( prev.autoFocusMode !== undefined ) this.cameraManager?.setAutoFocusMode( prev.autoFocusMode );
|
|
1577
|
+
if ( this.stages.autoExposure && prev.autoExposure !== undefined ) {
|
|
1578
|
+
|
|
1579
|
+
this.stages.autoExposure.enabled = prev.autoExposure;
|
|
1580
|
+
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
this._deterministicRestore = null;
|
|
1584
|
+
this._deterministic = false;
|
|
1585
|
+
this._dispatchPinned = false;
|
|
1586
|
+
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
this.reset();
|
|
1590
|
+
this.stopAnimation(); // reset() calls wake(); a manual render loop must not race rAF
|
|
1591
|
+
|
|
1592
|
+
return this._deterministic;
|
|
1593
|
+
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Whether output is currently bit-reproducible. False when the dispatch heuristics
|
|
1598
|
+
* were left active via `pinDispatch: false`, since those consume async readbacks.
|
|
1599
|
+
*/
|
|
1600
|
+
get isDeterministic() {
|
|
1601
|
+
|
|
1602
|
+
return this._deterministic && this._dispatchPinned;
|
|
1603
|
+
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/**
|
|
1607
|
+
* Accumulates exactly `count` samples synchronously, bypassing the rAF loop.
|
|
1608
|
+
*
|
|
1609
|
+
* Awaits the STBN atlases first — until they land the sampler reads a constant-0.5
|
|
1610
|
+
* placeholder that gets baked permanently into the accumulation buffer.
|
|
1611
|
+
*
|
|
1612
|
+
* @param {number} count - samples to accumulate
|
|
1613
|
+
* @param {Object} [options]
|
|
1614
|
+
* @param {boolean} [options.reset=true] - restart accumulation from sample 0 first
|
|
1615
|
+
* @param {number} [options.yieldEvery=8] - yield to the event loop every N passes (0 disables)
|
|
1616
|
+
* @param {function(number): void} [options.onProgress] - called with the running sample count
|
|
1617
|
+
* @returns {Promise<number>} the final accumulated sample count
|
|
1618
|
+
*/
|
|
1619
|
+
async renderFrames( count, { reset = true, yieldEvery = 8, onProgress } = {} ) {
|
|
1620
|
+
|
|
1621
|
+
const stage = this.stages.pathTracer;
|
|
1622
|
+
if ( ! stage ) throw new Error( 'renderFrames: app is not initialized' );
|
|
1623
|
+
if ( ! ( count > 0 ) ) throw new Error( `renderFrames: count must be positive, got ${count}` );
|
|
1624
|
+
|
|
1625
|
+
await stage.blueNoiseReady;
|
|
1626
|
+
|
|
1627
|
+
const target = ( reset ? 0 : stage.frameCount ) + count;
|
|
1628
|
+
|
|
1629
|
+
// completionThreshold is a cached JS number derived from maxSamples, so this must
|
|
1630
|
+
// go through the settings handler — writing the uniform alone would not move it.
|
|
1631
|
+
if ( this.settings.get( 'maxSamples' ) < target ) {
|
|
1632
|
+
|
|
1633
|
+
this.settings.set( 'maxSamples', target, { silent: true, reset: false } );
|
|
1634
|
+
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
if ( reset ) this.reset();
|
|
1638
|
+
this.stopAnimation();
|
|
1639
|
+
|
|
1640
|
+
const maxPasses = count + 64;
|
|
1641
|
+
let passes = 0;
|
|
1642
|
+
|
|
1643
|
+
while ( stage.frameCount < target && passes < maxPasses ) {
|
|
1644
|
+
|
|
1645
|
+
if ( this._deviceLost ) throw new Error( 'renderFrames: WebGPU device lost' );
|
|
1646
|
+
if ( ! stage.isReady ) throw new Error( 'renderFrames: path tracer stage is not ready' );
|
|
1647
|
+
|
|
1648
|
+
this.pipeline.render();
|
|
1649
|
+
passes ++;
|
|
1650
|
+
|
|
1651
|
+
onProgress?.( stage.frameCount );
|
|
1652
|
+
|
|
1653
|
+
if ( yieldEvery > 0 && passes % yieldEvery === 0 ) {
|
|
1654
|
+
|
|
1655
|
+
await new Promise( ( resolve ) => setTimeout( resolve, 0 ) );
|
|
1656
|
+
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
if ( stage.frameCount < target ) {
|
|
1662
|
+
|
|
1663
|
+
throw new Error(
|
|
1664
|
+
`renderFrames: stalled at ${stage.frameCount}/${target} samples after ${passes} passes — ` +
|
|
1665
|
+
'something retired the render (maxSamples, a stray reset, or a canvas resize)'
|
|
1666
|
+
);
|
|
1667
|
+
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
return stage.frameCount;
|
|
1671
|
+
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
/**
|
|
1675
|
+
* Enables WebGPU timestamp queries so {@link PathTracerApp#getGPUTimings} reports real
|
|
1676
|
+
* GPU time. Off by default because the queries themselves cost time. No-ops when the
|
|
1677
|
+
* device lacks the `timestamp-query` feature.
|
|
1678
|
+
*
|
|
1679
|
+
* @param {boolean} [enabled=true]
|
|
1680
|
+
* @returns {boolean} whether timestamp tracking is now active
|
|
1681
|
+
*/
|
|
1682
|
+
enableGPUTiming( enabled = true ) {
|
|
1683
|
+
|
|
1684
|
+
const backend = this.renderer?.backend;
|
|
1685
|
+
if ( ! backend ) return false;
|
|
1686
|
+
if ( enabled && backend.hasFeature?.( 'timestamp-query' ) !== true ) return false;
|
|
1687
|
+
|
|
1688
|
+
backend.trackTimestamp = enabled;
|
|
1689
|
+
return backend.trackTimestamp === enabled;
|
|
1690
|
+
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Real GPU milliseconds for the last resolved frame, from WebGPU timestamp queries.
|
|
1695
|
+
* Returns null unless {@link PathTracerApp#enableGPUTiming} was called.
|
|
1696
|
+
*
|
|
1697
|
+
* This is the only true GPU metric available: `pipeline.getStats()` times
|
|
1698
|
+
* `performance.now()` around each stage's render(), which is command *encoding*
|
|
1699
|
+
* time and stays flat while GPU cost doubles.
|
|
1700
|
+
*
|
|
1701
|
+
* @returns {Promise<{ compute: number, render: number, total: number }|null>}
|
|
1702
|
+
*/
|
|
1703
|
+
async getGPUTimings() {
|
|
1704
|
+
|
|
1705
|
+
const renderer = this.renderer;
|
|
1706
|
+
if ( ! renderer?.backend?.trackTimestamp ) return null;
|
|
1707
|
+
|
|
1708
|
+
await renderer.resolveTimestampsAsync( 'compute' );
|
|
1709
|
+
await renderer.resolveTimestampsAsync( 'render' );
|
|
1710
|
+
|
|
1711
|
+
const compute = renderer.info.compute.timestamp || 0;
|
|
1712
|
+
const render = renderer.info.render.timestamp || 0;
|
|
1713
|
+
|
|
1714
|
+
return { compute, render, total: compute + render };
|
|
1715
|
+
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1449
1718
|
// ═══════════════════════════════════════════════════════════════
|
|
1450
1719
|
// Output (absorbed from OutputAPI)
|
|
1451
1720
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -375,6 +375,12 @@ export class RenderPipeline {
|
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
// ===== PERFORMANCE TRACKING =====
|
|
378
|
+
//
|
|
379
|
+
// NOTE: these timings wrap stage.render() in performance.now(), which measures
|
|
380
|
+
// command *encoding* on the CPU — not GPU execution. On a compute-heavy pipeline
|
|
381
|
+
// they stay flat while GPU cost doubles, so they are useful for spotting CPU-side
|
|
382
|
+
// stalls only. For real GPU milliseconds use PathTracerApp.enableGPUTiming() +
|
|
383
|
+
// getGPUTimings(), which read WebGPU timestamp queries.
|
|
378
384
|
|
|
379
385
|
/**
|
|
380
386
|
* Enable performance tracking
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { TreeletOptimizer } from './TreeletOptimizer.js';
|
|
2
2
|
import { ReinsertionOptimizer } from './ReinsertionOptimizer.js';
|
|
3
|
-
import
|
|
4
|
-
import BVH_WORKER_URL from './Workers/BVHWorker.js?worker&url';
|
|
3
|
+
import BVHWorker from './Workers/BVHWorker.js?worker&inline';
|
|
5
4
|
// Logger is worker-safe (globalThis only, storage access guarded), unlike Constants.js below.
|
|
6
5
|
import { createLogger, fmt } from '../utils/Logger.js';
|
|
7
6
|
|
|
@@ -487,25 +486,12 @@ export class BVHBuilder {
|
|
|
487
486
|
|
|
488
487
|
try {
|
|
489
488
|
|
|
490
|
-
setupWorker( new
|
|
489
|
+
setupWorker( new BVHWorker() );
|
|
491
490
|
|
|
492
491
|
} catch ( error ) {
|
|
493
492
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
fetchAsWorker( BVH_WORKER_URL ).then( setupWorker ).catch( () => {
|
|
497
|
-
|
|
498
|
-
log.warn( 'worker fetch fallback failed, using synchronous build' );
|
|
499
|
-
resolve( this._buildSyncAndFlatten( triangles, depth, progressCallback ) );
|
|
500
|
-
|
|
501
|
-
} );
|
|
502
|
-
|
|
503
|
-
} else {
|
|
504
|
-
|
|
505
|
-
log.warn( 'worker creation failed, falling back to synchronous build:', error );
|
|
506
|
-
resolve( this._buildSyncAndFlatten( triangles, depth, progressCallback ) );
|
|
507
|
-
|
|
508
|
-
}
|
|
493
|
+
log.warn( 'worker creation failed, falling back to synchronous build:', error );
|
|
494
|
+
resolve( this._buildSyncAndFlatten( triangles, depth, progressCallback ) );
|
|
509
495
|
|
|
510
496
|
}
|
|
511
497
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { DataUtils, HalfFloatType, FloatType, SRGBColorSpace } from 'three';
|
|
2
|
-
import
|
|
3
|
-
import CDF_WORKER_URL from './Workers/CDFWorker.js?worker&url';
|
|
2
|
+
import CDFWorker from './Workers/CDFWorker.js?worker&inline';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Binary search to find the closest index
|
|
@@ -223,16 +222,7 @@ export class EquirectHDRInfo {
|
|
|
223
222
|
const { floatData, width, height } = extractFloatData( hdr );
|
|
224
223
|
|
|
225
224
|
// Fresh worker per call — terminated in finally to avoid ~30 MB residency.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this._worker = new Worker( CDF_WORKER_URL, { type: 'module' } );
|
|
229
|
-
|
|
230
|
-
} catch ( e ) {
|
|
231
|
-
|
|
232
|
-
if ( e.name !== 'SecurityError' ) throw e;
|
|
233
|
-
this._worker = await fetchAsWorker( CDF_WORKER_URL );
|
|
234
|
-
|
|
235
|
-
}
|
|
225
|
+
this._worker = new CDFWorker();
|
|
236
226
|
|
|
237
227
|
try {
|
|
238
228
|
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* a cycle that Vite cannot resolve.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import BVH_SUBTREE_WORKER_URL from './Workers/BVHSubtreeWorker.js?worker&url';
|
|
10
|
+
import BVHWorker from './Workers/BVHWorker.js?worker&inline';
|
|
11
|
+
import BVHSubtreeWorker from './Workers/BVHSubtreeWorker.js?worker&inline';
|
|
13
12
|
import { createLogger, fmt } from '../utils/Logger.js';
|
|
14
13
|
|
|
15
14
|
const log = createLogger( 'bvh' );
|
|
@@ -55,17 +54,7 @@ export function buildBVHParallel( triangles, depth, progressCallback, config ) {
|
|
|
55
54
|
const sharedReorderBuffer = new SharedArrayBuffer( triangleCount * FPT * 4 );
|
|
56
55
|
|
|
57
56
|
// Phase 1: Coordinator worker
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
|
|
61
|
-
coordinatorWorker = new Worker( BVH_WORKER_URL, { type: 'module' } );
|
|
62
|
-
|
|
63
|
-
} catch ( e ) {
|
|
64
|
-
|
|
65
|
-
if ( e.name !== 'SecurityError' ) throw e;
|
|
66
|
-
coordinatorWorker = await fetchAsWorker( BVH_WORKER_URL );
|
|
67
|
-
|
|
68
|
-
}
|
|
57
|
+
const coordinatorWorker = new BVHWorker();
|
|
69
58
|
|
|
70
59
|
let phase1Stats = null;
|
|
71
60
|
const allWorkers = [ coordinatorWorker ];
|
|
@@ -313,17 +302,7 @@ async function handlePhase2(
|
|
|
313
302
|
const bucket = workerTaskBuckets[ w ];
|
|
314
303
|
if ( bucket.length === 0 ) continue;
|
|
315
304
|
|
|
316
|
-
|
|
317
|
-
try {
|
|
318
|
-
|
|
319
|
-
subtreeWorker = new Worker( BVH_SUBTREE_WORKER_URL, { type: 'module' } );
|
|
320
|
-
|
|
321
|
-
} catch ( e ) {
|
|
322
|
-
|
|
323
|
-
if ( e.name !== 'SecurityError' ) throw e;
|
|
324
|
-
subtreeWorker = await fetchAsWorker( BVH_SUBTREE_WORKER_URL );
|
|
325
|
-
|
|
326
|
-
}
|
|
305
|
+
const subtreeWorker = new BVHSubtreeWorker();
|
|
327
306
|
|
|
328
307
|
allWorkers.push( subtreeWorker );
|
|
329
308
|
|
|
@@ -428,17 +407,7 @@ function buildSingleWorker( triangles, depth, progressCallback, config ) {
|
|
|
428
407
|
|
|
429
408
|
( async () => {
|
|
430
409
|
|
|
431
|
-
|
|
432
|
-
try {
|
|
433
|
-
|
|
434
|
-
worker = new Worker( BVH_WORKER_URL, { type: 'module' } );
|
|
435
|
-
|
|
436
|
-
} catch ( e ) {
|
|
437
|
-
|
|
438
|
-
if ( e.name !== 'SecurityError' ) throw e;
|
|
439
|
-
worker = await fetchAsWorker( BVH_WORKER_URL );
|
|
440
|
-
|
|
441
|
-
}
|
|
410
|
+
const worker = new BVHWorker();
|
|
442
411
|
|
|
443
412
|
const triangleCount = triangles.byteLength / ( FPT * 4 );
|
|
444
413
|
const useShared = typeof SharedArrayBuffer !== 'undefined';
|
|
@@ -12,9 +12,8 @@ import { BuildTimer } from './BuildTimer.js';
|
|
|
12
12
|
import { createLogger, fmt, workerLogLevel } from '../utils/Logger.js';
|
|
13
13
|
import { SRGBColorSpace } from 'three';
|
|
14
14
|
import { TRIANGLE_DATA_LAYOUT, TEXTURE_CONSTANTS, getTextureBucketId, packTextureIndex } from '../EngineDefaults.js';
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import BVH_REFIT_WORKER_URL from './Workers/BVHRefitWorker.js?worker&url';
|
|
15
|
+
import BVHWorker from './Workers/BVHWorker.js?worker&inline';
|
|
16
|
+
import BVHRefitWorker from './Workers/BVHRefitWorker.js?worker&inline';
|
|
18
17
|
|
|
19
18
|
const log = createLogger( 'scene' );
|
|
20
19
|
|
|
@@ -730,18 +729,12 @@ export class SceneProcessor {
|
|
|
730
729
|
let worker;
|
|
731
730
|
try {
|
|
732
731
|
|
|
733
|
-
worker = new
|
|
732
|
+
worker = new BVHWorker();
|
|
734
733
|
|
|
735
734
|
} catch ( e ) {
|
|
736
735
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
reject( e );
|
|
740
|
-
return;
|
|
741
|
-
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
worker = await fetchAsWorker( BVH_WORKER_URL );
|
|
736
|
+
reject( e );
|
|
737
|
+
return;
|
|
745
738
|
|
|
746
739
|
}
|
|
747
740
|
|
|
@@ -1303,19 +1296,35 @@ export class SceneProcessor {
|
|
|
1303
1296
|
|
|
1304
1297
|
}
|
|
1305
1298
|
|
|
1306
|
-
//
|
|
1307
|
-
|
|
1299
|
+
// The worker reads triCount * 9 floats unconditionally, and the shared position buffer
|
|
1300
|
+
// is sized from the FIRST call's argument — so a short array reads past its end and
|
|
1301
|
+
// writes NaN into triangleData and every AABB above it, with no error anywhere. A
|
|
1302
|
+
// caller that misses a mesh (the hidden ground disk is easy to miss) sees the scene
|
|
1303
|
+
// silently vanish instead of a thrown exception, so check the length up front.
|
|
1304
|
+
const expectedFloats = this.triangleCount * 9;
|
|
1308
1305
|
|
|
1309
|
-
|
|
1306
|
+
if ( newPositions?.length !== expectedFloats ) {
|
|
1310
1307
|
|
|
1311
|
-
|
|
1308
|
+
throw new Error(
|
|
1309
|
+
`SceneProcessor.refitBVH: expected ${expectedFloats} position floats ` +
|
|
1310
|
+
`(${this.triangleCount} triangles × 9), got ${newPositions?.length ?? 'none'}. ` +
|
|
1311
|
+
'Positions must cover every triangle in the scene, meshes in this.meshes order.'
|
|
1312
|
+
);
|
|
1312
1313
|
|
|
1313
|
-
|
|
1314
|
+
}
|
|
1314
1315
|
|
|
1315
|
-
|
|
1316
|
-
this._refitWorker = await fetchAsWorker( BVH_REFIT_WORKER_URL );
|
|
1316
|
+
if ( newNormals && newNormals.length !== expectedFloats ) {
|
|
1317
1317
|
|
|
1318
|
-
|
|
1318
|
+
throw new Error(
|
|
1319
|
+
`SceneProcessor.refitBVH: expected ${expectedFloats} normal floats, got ${newNormals.length}.`
|
|
1320
|
+
);
|
|
1321
|
+
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Lazy-create worker
|
|
1325
|
+
if ( ! this._refitWorker ) {
|
|
1326
|
+
|
|
1327
|
+
this._refitWorker = new BVHRefitWorker();
|
|
1319
1328
|
|
|
1320
1329
|
}
|
|
1321
1330
|
|
|
@@ -1428,6 +1437,27 @@ export class SceneProcessor {
|
|
|
1428
1437
|
|
|
1429
1438
|
}
|
|
1430
1439
|
|
|
1440
|
+
// Indexed by absolute triangle, so a short buffer reads undefined → NaN bounds for the
|
|
1441
|
+
// affected meshes with no error. Same silent-corruption trap as refitBVH.
|
|
1442
|
+
const expectedFloats = this.triangleCount * 9;
|
|
1443
|
+
|
|
1444
|
+
if ( newPositions?.length !== expectedFloats ) {
|
|
1445
|
+
|
|
1446
|
+
throw new Error(
|
|
1447
|
+
`SceneProcessor.refitBLASes: expected ${expectedFloats} position floats ` +
|
|
1448
|
+
`(${this.triangleCount} triangles × 9, full scene), got ${newPositions?.length ?? 'none'}.`
|
|
1449
|
+
);
|
|
1450
|
+
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
if ( newNormals && newNormals.length !== expectedFloats ) {
|
|
1454
|
+
|
|
1455
|
+
throw new Error(
|
|
1456
|
+
`SceneProcessor.refitBLASes: expected ${expectedFloats} normal floats, got ${newNormals.length}.`
|
|
1457
|
+
);
|
|
1458
|
+
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1431
1461
|
const start = performance.now();
|
|
1432
1462
|
|
|
1433
1463
|
// Lazy-create refitter instance
|
|
@@ -1943,18 +1973,7 @@ export class SceneProcessor {
|
|
|
1943
1973
|
const existing = this._pendingRebuilds.get( meshIdx );
|
|
1944
1974
|
if ( existing ) existing.terminate();
|
|
1945
1975
|
|
|
1946
|
-
|
|
1947
|
-
try {
|
|
1948
|
-
|
|
1949
|
-
worker = new Worker( BVH_WORKER_URL, { type: 'module' } );
|
|
1950
|
-
dispatchRebuild( meshIdx, entry, worker );
|
|
1951
|
-
|
|
1952
|
-
} catch ( e ) {
|
|
1953
|
-
|
|
1954
|
-
if ( e.name !== 'SecurityError' ) throw e;
|
|
1955
|
-
fetchAsWorker( BVH_WORKER_URL ).then( w => dispatchRebuild( meshIdx, entry, w ) );
|
|
1956
|
-
|
|
1957
|
-
}
|
|
1976
|
+
dispatchRebuild( meshIdx, entry, new BVHWorker() );
|
|
1958
1977
|
|
|
1959
1978
|
}
|
|
1960
1979
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { DataArrayTexture, RGBAFormat, LinearFilter, UnsignedByteType, SRGBColorSpace, RepeatWrapping } from "three";
|
|
2
2
|
import { TEXTURE_CONSTANTS, MEMORY_CONSTANTS, DEFAULT_TEXTURE_MATRIX, MATERIAL_DATA_LAYOUT } from '../EngineDefaults.js';
|
|
3
|
-
import
|
|
4
|
-
import TEXTURES_WORKER_URL from './Workers/TexturesWorker.js?worker&url';
|
|
3
|
+
import TexturesWorker from './Workers/TexturesWorker.js?worker&inline';
|
|
5
4
|
|
|
6
5
|
// Canvas pooling for efficient reuse of canvas elements
|
|
7
6
|
class CanvasPool {
|
|
@@ -641,17 +640,7 @@ export class TextureCreator {
|
|
|
641
640
|
|
|
642
641
|
try {
|
|
643
642
|
|
|
644
|
-
|
|
645
|
-
try {
|
|
646
|
-
|
|
647
|
-
worker = new Worker( TEXTURES_WORKER_URL, { type: 'module' } );
|
|
648
|
-
|
|
649
|
-
} catch ( e ) {
|
|
650
|
-
|
|
651
|
-
if ( e.name !== 'SecurityError' ) throw e;
|
|
652
|
-
worker = await fetchAsWorker( TEXTURES_WORKER_URL );
|
|
653
|
-
|
|
654
|
-
}
|
|
643
|
+
const worker = new TexturesWorker();
|
|
655
644
|
|
|
656
645
|
// Prepare textures for worker with direct transfer
|
|
657
646
|
const texturesData = await this.prepareTexturesForWorkerDirect( textures );
|