three-blocks-benchmark 0.5.0 → 0.5.1-alpha.390.ga7db0265ceed
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/CHANGELOG.md +22 -0
- package/NOTICE +5 -6
- package/README.md +41 -7
- package/bin/three-blocks-benchmark.mjs +3 -1
- package/browser/harness.js +53 -16
- package/browser/supersplat-editor.js +118 -0
- package/browser/worker-sort.js +56 -0
- package/lib/run.mjs +61 -35
- package/package.json +14 -6
- package/vendor/README.md +26 -45
- package/vendor/supersplat-editor/LICENSE +19 -0
- package/vendor/supersplat-editor/manifest.json +34 -0
- package/vendor/supersplat-editor/src/color-grade.ts +206 -0
- package/vendor/supersplat-editor/src/color-palette.ts +126 -0
- package/vendor/supersplat-editor/src/data-processor/histogram-config.ts +22 -0
- package/vendor/supersplat-editor/src/editor-splat-resource.ts +368 -0
- package/vendor/supersplat-editor/src/events.ts +27 -0
- package/vendor/supersplat-editor/src/gaussian-instances.ts +464 -0
- package/vendor/supersplat-editor/src/index-ranges.ts +121 -0
- package/vendor/supersplat-editor/src/io/index.ts +20 -0
- package/vendor/supersplat-editor/src/io/read/file-systems.ts +146 -0
- package/vendor/supersplat-editor/src/io/read/index.ts +17 -0
- package/vendor/supersplat-editor/src/io/read/loader.ts +237 -0
- package/vendor/supersplat-editor/src/io/write/browser-file-system.ts +141 -0
- package/vendor/supersplat-editor/src/io/write/index.ts +12 -0
- package/vendor/supersplat-editor/src/io/write/writer.ts +118 -0
- package/vendor/supersplat-editor/src/projected-splat-renderer.ts +885 -0
- package/vendor/supersplat-editor/src/shaders/blit-shader.ts +114 -0
- package/vendor/supersplat-editor/src/shaders/color-grade-chunk.ts +55 -0
- package/vendor/supersplat-editor/src/shaders/footprint-intersect-shader.ts +117 -0
- package/vendor/supersplat-editor/src/shaders/palette-chunk.ts +33 -0
- package/vendor/supersplat-editor/src/shaders/projected-splat-indirect-args-shader.ts +65 -0
- package/vendor/supersplat-editor/src/shaders/projected-splat-projector-shader.ts +387 -0
- package/vendor/supersplat-editor/src/shaders/projected-splat-shader.ts +299 -0
- package/vendor/supersplat-editor/src/splat-state.ts +9 -0
- package/vendor/supersplat-editor/src/transform-palette.ts +120 -0
- package/vendor/supersplat-editor/src/utils/resolve.ts +9 -0
- package/vendor/supersplat-editor/src/utils/simple-render-pass.ts +84 -0
- package/vendor/three-dev/build/three.core.js +0 -60911
- package/vendor/three-dev/build/three.tsl.js +0 -672
- package/vendor/three-dev/build/three.webgpu.js +0 -90737
- package/vendor/three-gs/gpgpu/CountingSort.js +0 -271
- package/vendor/three-gs/loaders/GaussianSplatPLYLoader.js +0 -386
- package/vendor/three-gs/loaders/PLYLoader.js +0 -968
- package/vendor/three-gs/objects/GaussianSplat.js +0 -859
- package/vendor/three-gs/utils/GaussianSplatUtils.js +0 -229
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# three-blocks-benchmark
|
|
2
2
|
|
|
3
|
+
## 0.5.1-alpha.390.ga7db0265ceed
|
|
4
|
+
|
|
5
|
+
- Packages: This alpha contains no user-facing changes.
|
|
6
|
+
|
|
7
|
+
- Packages: Built from `a7db0265ceed`.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## 0.5.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- aa1d655: Packages: Gaussian benchmarks no longer charge display-frame waits to SuperSplat.
|
|
15
|
+
- aa1d655: Packages: Gaussian splats now use one full-quality default in the library and benchmark.
|
|
16
|
+
- aa1d655: Packages: Splats use less memory, with full frame benchmarks for r186 and SuperSplat 3.
|
|
17
|
+
- Updated dependencies [aa1d655]
|
|
18
|
+
- Updated dependencies [aa1d655]
|
|
19
|
+
- Updated dependencies [aa1d655]
|
|
20
|
+
- Updated dependencies [aa1d655]
|
|
21
|
+
- Updated dependencies [aa1d655]
|
|
22
|
+
- Updated dependencies [aa1d655]
|
|
23
|
+
- three-blocks@0.12.0
|
|
24
|
+
|
|
3
25
|
## 0.5.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/NOTICE
CHANGED
|
@@ -13,9 +13,8 @@ https://threejs-blocks.com/license
|
|
|
13
13
|
Third-party code
|
|
14
14
|
================
|
|
15
15
|
|
|
16
|
-
vendor/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
https://github.com/
|
|
21
|
-
See vendor/three-gs/README.md for the pinned commit and file digests.
|
|
16
|
+
vendor/supersplat-editor/ contains verbatim sources from SuperSplat Editor v3.0.0.
|
|
17
|
+
They remain under the upstream MIT License, included at
|
|
18
|
+
vendor/supersplat-editor/LICENSE. See vendor/README.md and the accompanying
|
|
19
|
+
manifest.json for the pinned commit and file digests.
|
|
20
|
+
https://github.com/playcanvas/supersplat/tree/v3.0.0
|
package/README.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# three-blocks-benchmark
|
|
2
2
|
|
|
3
|
-
Reproducible browser benchmarks for Three Blocks, the three.js
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Reproducible browser benchmarks for Three Blocks, the official three.js r186
|
|
4
|
+
`GaussianSplat` addon, Spark, SuperSplat Viewer 1.31.2, SuperSplat Editor 3.0.0's
|
|
5
|
+
renderer, GaussianSplats3D, and Babylon.js.
|
|
6
6
|
|
|
7
|
-
[pr]: https://github.com/mrdoob/three.js/pull/33950
|
|
8
7
|
The default `standard` suite uses fixed held-out cameras from Mip-NeRF 360
|
|
9
8
|
Bonsai and a pinned NerfBaselines Graphdeco 30K artifact. The synthetic
|
|
10
9
|
`micro` suite is performance-only.
|
|
11
10
|
|
|
11
|
+
Three Blocks uses its default and only preset, `quality`, at native resolution.
|
|
12
|
+
It uses full-precision depth keys and wider Gaussian support cropped at the
|
|
13
|
+
fragment alpha threshold. Load, frame time, memory, and image quality all use
|
|
14
|
+
that same configuration.
|
|
15
|
+
|
|
12
16
|
Published results and methodology:
|
|
13
17
|
<https://threejs-blocks.com/benchmark/gaussian>
|
|
14
18
|
|
|
@@ -59,9 +63,39 @@ a micro scene.
|
|
|
59
63
|
`--revision <value>` and `--dirty` add optional repository metadata; neither
|
|
60
64
|
Git nor a checkout is required.
|
|
61
65
|
|
|
62
|
-
The CLI
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
The CLI uses full Chromium in headless mode, including the Metal ANGLE backend
|
|
67
|
+
on macOS. Publication runs reject software GPU adapters.
|
|
68
|
+
Memory sampling uses [Chromium's `ForceEagerMeasureMemory` test mode][eager-memory] after
|
|
69
|
+
forced GC, outside the load and frame clocks, to avoid waiting for idle GC.
|
|
70
|
+
|
|
71
|
+
[eager-memory]: https://chromium.googlesource.com/chromium/src/+/refs/tags/143.0.7458.0/third_party/blink/web_tests/VirtualTestSuites
|
|
72
|
+
|
|
73
|
+
Frame timing directly executes each renderer's frame, without waiting for a display
|
|
74
|
+
refresh, and cycles through the three held-out cameras. SuperSplat's complete viewer
|
|
75
|
+
tick is driven directly and uses its WebGPU GPU-sort pipeline. Every timed frame
|
|
76
|
+
completes its current-camera sort, index upload and GPU draw. GaussianSplats3D
|
|
77
|
+
forces a complete visible-set sort and waits for its applied worker result;
|
|
78
|
+
Babylon waits for the matching camera sort request to be applied. These are
|
|
79
|
+
serial camera-to-image latencies, not pipelined interactive FPS. Worker completion
|
|
80
|
+
counts are recorded, and a run fails if any timed frame lacks a complete sort.
|
|
81
|
+
Quality captures use a settled camera and are measured separately.
|
|
82
|
+
|
|
83
|
+
The September 9 timing correction replaces earlier draw-only figures: the tight
|
|
84
|
+
loop starved worker-result messages in GaussianSplats3D and Babylon.js. Those
|
|
85
|
+
old timings must not be compared with the current complete-frame measurements.
|
|
86
|
+
GaussianSplats3D supports SH2 maximum and therefore omits the third band of the
|
|
87
|
+
shared SH3 PLY; its effective degree is recorded and labeled on the website.
|
|
88
|
+
|
|
89
|
+
The Editor 3.0 column runs its verbatim projector, radix sorter and draw shaders
|
|
90
|
+
with sorted transparency and fp16 accumulation; it excludes editor UI and the
|
|
91
|
+
motion-adaptive stochastic mode. It consumes resident PLY bytes using the
|
|
92
|
+
loader's `MemoryReadFileSystem`, so page memory includes the retained source
|
|
93
|
+
PLY. This does not reproduce the editor's file-backed memory figures. See
|
|
94
|
+
`vendor/README.md` for provenance. The viewer is a separate product and column.
|
|
95
|
+
|
|
96
|
+
Use `--renderer <id>` for a focused check. A partial run is never labeled
|
|
97
|
+
publication evidence. All WebGL2 timings include a 1×1 GPU readback; WebGPU
|
|
98
|
+
timings wait for queue completion.
|
|
65
99
|
|
|
66
100
|
Standard reports contain neutral PSNR and dm_pix-compatible SSIM versus ground
|
|
67
101
|
truth. Metrics versus the pinned NerfBaselines Graphdeco 30K prediction are
|
|
@@ -19,6 +19,7 @@ Options:
|
|
|
19
19
|
--report <file> Write the JSON report to a different path
|
|
20
20
|
--capture-url-prefix <url> Prefix capture paths stored in the report
|
|
21
21
|
--scene <id> Select a micro scene
|
|
22
|
+
--renderer <id> Validate one renderer (not publication evidence)
|
|
22
23
|
--quick Use one view/scene and short measurements
|
|
23
24
|
--omit-captures Write metrics JSON without renderer captures
|
|
24
25
|
--headed Show Chromium
|
|
@@ -42,7 +43,7 @@ function parseArgs( rawArgs ) {
|
|
|
42
43
|
else if ( argument === '--headless' ) options.headed = false;
|
|
43
44
|
else if ( argument === '--dirty' ) options.workingTreeDirty = true;
|
|
44
45
|
else if ( argument === '--clean' ) options.workingTreeDirty = false;
|
|
45
|
-
else if ( [ '--suite', '--output', '--cache', '--report', '--capture-url-prefix', '--scene', '--revision' ].includes( argument ) ) {
|
|
46
|
+
else if ( [ '--suite', '--output', '--cache', '--report', '--capture-url-prefix', '--scene', '--renderer', '--revision' ].includes( argument ) ) {
|
|
46
47
|
|
|
47
48
|
const value = rawArgs[ ++ index ];
|
|
48
49
|
if ( ! value || value.startsWith( '--' ) ) throw new Error( `${argument} requires a value.` );
|
|
@@ -53,6 +54,7 @@ function parseArgs( rawArgs ) {
|
|
|
53
54
|
'--report': 'report',
|
|
54
55
|
'--capture-url-prefix': 'captureUrlPrefix',
|
|
55
56
|
'--scene': 'scene',
|
|
57
|
+
'--renderer': 'renderer',
|
|
56
58
|
'--revision': 'revision',
|
|
57
59
|
}[ argument ];
|
|
58
60
|
options[ key ] = value;
|
package/browser/harness.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
createScenePLY,
|
|
7
7
|
playCanvasEulerFromMatrix,
|
|
8
8
|
} from './scenes.js';
|
|
9
|
+
import { sortBabylon, sortGS3D } from './worker-sort.js';
|
|
9
10
|
|
|
10
11
|
const BACKGROUND = 0x070a0f;
|
|
11
12
|
const canvas = document.querySelector( '#viewport' );
|
|
@@ -13,6 +14,7 @@ const status = document.querySelector( '#status' );
|
|
|
13
14
|
const query = new URLSearchParams( location.search );
|
|
14
15
|
const rendererId = __BENCHMARK_RENDERER__;
|
|
15
16
|
const suiteId = query.get( 'suite' ) ?? 'micro';
|
|
17
|
+
let benchmarkPhase = 'load';
|
|
16
18
|
let scene = SCENES.find( candidate => candidate.id === query.get( 'scene' ) );
|
|
17
19
|
let benchmarkViewport = VIEWPORT;
|
|
18
20
|
let benchmarkPose = QUALITY_CAMERA;
|
|
@@ -90,17 +92,16 @@ async function createThreeBlocks( bytes ) {
|
|
|
90
92
|
// dilation, and reference view-Z ordering.
|
|
91
93
|
const standard = suiteId === 'standard';
|
|
92
94
|
const compositing = query.get( 'compositing' ) ?? ( standard ? 'display' : 'linear' );
|
|
93
|
-
const sortPrecision = query.get( 'sortprec' )
|
|
95
|
+
const sortPrecision = query.get( 'sortprec' );
|
|
94
96
|
const parseOptions = {
|
|
95
97
|
enableSH: standard,
|
|
96
|
-
shDegree: standard ? 3 : 0,
|
|
98
|
+
shDegree: query.has( 'shdegree' ) ? Number( query.get( 'shdegree' ) ) : ( standard ? 3 : 0 ),
|
|
97
99
|
coordinateSystem: 'threejs',
|
|
98
100
|
compositing,
|
|
99
|
-
sortPrecision,
|
|
101
|
+
...( sortPrecision ? { sortPrecision } : {} ),
|
|
100
102
|
sortMode: query.get( 'sortmode' ) ?? ( standard ? 'depth' : 'radial' ),
|
|
101
103
|
...( standard ? { preBlurAmount: 0.3, blurAmount: 0 } : {} ),
|
|
102
104
|
...( query.has( 'maxstddev' ) ? { maxStdDev: Number( query.get( 'maxstddev' ) ) } : {} ),
|
|
103
|
-
...( query.has( 'quality' ) ? { quality: query.get( 'quality' ) } : {} ),
|
|
104
105
|
...( query.has( 'fragclip' ) ? { fragmentAlphaClip: Number( query.get( 'fragclip' ) ) } : {} ),
|
|
105
106
|
...( query.has( 'shformat' ) ? { shFormat: query.get( 'shformat' ) } : {} ),
|
|
106
107
|
...( standard ? { maxBufferBytes: shBufferBytes } : {} ),
|
|
@@ -230,8 +231,8 @@ async function createThreeGS( bytes ) {
|
|
|
230
231
|
|
|
231
232
|
const [ THREE, { GaussianSplatPLYLoader }, { GaussianSplat } ] = await Promise.all( [
|
|
232
233
|
import( 'three/webgpu' ),
|
|
233
|
-
import( '
|
|
234
|
-
import( '
|
|
234
|
+
import( 'three-r186/addons/loaders/GaussianSplatPLYLoader.js' ),
|
|
235
|
+
import( 'three-r186/addons/objects/GaussianSplat.js' ),
|
|
235
236
|
] );
|
|
236
237
|
const renderer = new THREE.WebGPURenderer( {
|
|
237
238
|
canvas,
|
|
@@ -321,13 +322,14 @@ async function createSpark( bytes ) {
|
|
|
321
322
|
await splats.initialized;
|
|
322
323
|
|
|
323
324
|
const gl = renderer.getContext();
|
|
325
|
+
const fencePixel = new Uint8Array( 4 );
|
|
324
326
|
const render = async pose => {
|
|
325
327
|
|
|
326
328
|
setThreeCamera( camera, pose );
|
|
327
329
|
renderer.getDrawingBufferSize( spark.renderSize );
|
|
328
330
|
await spark.update( { scene: scene3d, camera } );
|
|
329
331
|
renderer.render( scene3d, camera );
|
|
330
|
-
gl.
|
|
332
|
+
gl.readPixels( 0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, fencePixel );
|
|
331
333
|
|
|
332
334
|
};
|
|
333
335
|
await render( benchmarkPose );
|
|
@@ -375,7 +377,8 @@ async function createGS3D( bytes ) {
|
|
|
375
377
|
camera,
|
|
376
378
|
useBuiltInControls: false,
|
|
377
379
|
sharedMemoryForWorkers,
|
|
378
|
-
|
|
380
|
+
// 0.4.7's PLY loader and shader support at most degree 2.
|
|
381
|
+
sphericalHarmonicsDegree: suiteId === 'standard' ? 2 : 0,
|
|
379
382
|
logLevel: 0,
|
|
380
383
|
// Faithful-viewing configuration: render the whole checkpoint immediately instead of
|
|
381
384
|
// the default progressive reveal animation, whose expanding visible region leaves
|
|
@@ -404,11 +407,14 @@ async function createGS3D( bytes ) {
|
|
|
404
407
|
await viewer.addSplatScene( sceneUrl, sceneOptions );
|
|
405
408
|
|
|
406
409
|
}
|
|
410
|
+
const sortCompletions = { load: 0, warmup: 0, measure: 0, capture: 0 };
|
|
407
411
|
const gl = renderer.getContext();
|
|
408
412
|
const fencePixel = new Uint8Array( 4 );
|
|
409
413
|
const render = async pose => {
|
|
410
414
|
|
|
411
415
|
setThreeCamera( camera, pose );
|
|
416
|
+
await sortGS3D( viewer );
|
|
417
|
+
sortCompletions[ benchmarkPhase ] ++;
|
|
412
418
|
viewer.update();
|
|
413
419
|
viewer.render();
|
|
414
420
|
// gl.finish() alone is not a reliable completion fence under ANGLE-Metal; a 1x1
|
|
@@ -425,6 +431,10 @@ async function createGS3D( bytes ) {
|
|
|
425
431
|
return {
|
|
426
432
|
reportedGpuBytes: null,
|
|
427
433
|
reportedGpuBytesScope: null,
|
|
434
|
+
shDegree: viewer.splatMesh.minSphericalHarmonicsDegree,
|
|
435
|
+
sortCompletions,
|
|
436
|
+
splatRenderCount: viewer.splatRenderCount,
|
|
437
|
+
lastWorkerSortMilliseconds: viewer.lastSortTime,
|
|
428
438
|
webgl: webGLInfo( gl ),
|
|
429
439
|
};
|
|
430
440
|
|
|
@@ -494,14 +504,18 @@ async function createBabylon( bytes ) {
|
|
|
494
504
|
splats.scaling.set( 1, 1, 1 );
|
|
495
505
|
splats.rotationQuaternion = new Quaternion( 1, 0, 0, 0 );
|
|
496
506
|
|
|
507
|
+
const sortCompletions = { load: 0, warmup: 0, measure: 0, capture: 0 };
|
|
497
508
|
const gl = canvas.getContext( 'webgl2' );
|
|
498
509
|
const fencePixel = new Uint8Array( 4 );
|
|
499
510
|
const render = async pose => {
|
|
500
511
|
|
|
501
512
|
setBabylonCamera( pose );
|
|
513
|
+
camera.getViewMatrix( true );
|
|
514
|
+
await sortBabylon( splats, camera );
|
|
515
|
+
sortCompletions[ benchmarkPhase ] ++;
|
|
502
516
|
scene3d.render();
|
|
503
|
-
|
|
504
|
-
// timed frame
|
|
517
|
+
if ( ! splats._isDepthSortSettled ) throw new Error( 'Babylon started an unexpected sort during draw.' );
|
|
518
|
+
// The timed frame includes worker sorting, index upload and rasterization.
|
|
505
519
|
gl.readPixels( 0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, fencePixel );
|
|
506
520
|
|
|
507
521
|
};
|
|
@@ -523,6 +537,7 @@ async function createBabylon( bytes ) {
|
|
|
523
537
|
reportedGpuBytes: null,
|
|
524
538
|
reportedGpuBytesScope: null,
|
|
525
539
|
shDegree: splats.shDegree,
|
|
540
|
+
sortCompletions,
|
|
526
541
|
webgl: webGLInfo( gl ),
|
|
527
542
|
};
|
|
528
543
|
|
|
@@ -641,9 +656,11 @@ async function createSuperSplat( bytes, requestedBackend ) {
|
|
|
641
656
|
const render = async pose => {
|
|
642
657
|
|
|
643
658
|
setSuperSplatCamera( viewer, pose );
|
|
644
|
-
const frameEnd = new Promise( resolve => app.once( 'frameend', resolve ) );
|
|
645
659
|
app.renderNextFrame = true;
|
|
646
|
-
|
|
660
|
+
// Drive the complete viewer tick directly, as the other adapters do. Waiting
|
|
661
|
+
// for its next rAF charges display scheduling to SuperSplat alone.
|
|
662
|
+
app.tick( performance.now() );
|
|
663
|
+
cancelAnimationFrame( app.frameRequestId );
|
|
647
664
|
const device = app.graphicsDevice;
|
|
648
665
|
await device.wgpu?.queue?.onSubmittedWorkDone?.();
|
|
649
666
|
device.gl?.finish?.();
|
|
@@ -696,7 +713,7 @@ async function measureBrowserMemory() {
|
|
|
696
713
|
async function run() {
|
|
697
714
|
|
|
698
715
|
if ( ! [ 'standard', 'micro' ].includes( suiteId ) ) throw new Error( `Unknown suite: ${suiteId}` );
|
|
699
|
-
if ( ! [ 'three-blocks', 'three-gs', 'spark', 'supersplat', 'gs3d', 'babylon' ].includes( rendererId ) ) {
|
|
716
|
+
if ( ! [ 'three-blocks', 'three-gs', 'spark', 'supersplat', 'supersplat-editor', 'gs3d', 'babylon' ].includes( rendererId ) ) {
|
|
700
717
|
|
|
701
718
|
throw new Error( `Unknown renderer: ${rendererId}` );
|
|
702
719
|
|
|
@@ -719,7 +736,7 @@ async function run() {
|
|
|
719
736
|
canvas.style.height = `${view.height}px`;
|
|
720
737
|
// Three Blocks streams the PLY read through its Response-based parse API inside the
|
|
721
738
|
// adapter (fetch and parse overlap); the other runtimes consume resident bytes.
|
|
722
|
-
if ( rendererId
|
|
739
|
+
if ( ! rendererId.startsWith( 'three-blocks' ) ) {
|
|
723
740
|
|
|
724
741
|
loadMarks.fetchStart = performance.now();
|
|
725
742
|
bytes = await fetch( '/standard/point_cloud.ply' ).then( response => response.arrayBuffer() );
|
|
@@ -734,10 +751,16 @@ async function run() {
|
|
|
734
751
|
|
|
735
752
|
}
|
|
736
753
|
let adapter;
|
|
737
|
-
if ( rendererId
|
|
754
|
+
if ( rendererId.startsWith( 'three-blocks' ) ) adapter = await createThreeBlocks( bytes );
|
|
738
755
|
if ( rendererId === 'three-gs' ) adapter = await createThreeGS( bytes );
|
|
739
756
|
if ( rendererId === 'spark' ) adapter = await createSpark( bytes );
|
|
740
757
|
if ( rendererId === 'supersplat' ) adapter = await createSuperSplat( bytes, 'webgpu' );
|
|
758
|
+
if ( rendererId === 'supersplat-editor' ) {
|
|
759
|
+
|
|
760
|
+
const { createSuperSplatEditor } = await import( './supersplat-editor.js' );
|
|
761
|
+
adapter = await createSuperSplatEditor( canvas, bytes, benchmarkFileName, benchmarkViewport, benchmarkPose, suiteId );
|
|
762
|
+
|
|
763
|
+
}
|
|
741
764
|
if ( rendererId === 'gs3d' ) adapter = await createGS3D( bytes );
|
|
742
765
|
if ( rendererId === 'babylon' ) adapter = await createBabylon( bytes );
|
|
743
766
|
const loadMilliseconds = performance.now();
|
|
@@ -745,11 +768,13 @@ async function run() {
|
|
|
745
768
|
? cameraPose( frame )
|
|
746
769
|
: benchmarkPoses[ frame % benchmarkPoses.length ];
|
|
747
770
|
|
|
771
|
+
benchmarkPhase = 'warmup';
|
|
748
772
|
for ( let frame = 0; frame < warmupFrames; frame ++ ) {
|
|
749
773
|
|
|
750
774
|
await adapter.render( measurementPose( frame ) );
|
|
751
775
|
|
|
752
776
|
}
|
|
777
|
+
benchmarkPhase = 'measure';
|
|
753
778
|
const frameSamplesMilliseconds = [];
|
|
754
779
|
for ( let frame = 0; frame < measureFrames; frame ++ ) {
|
|
755
780
|
|
|
@@ -758,11 +783,23 @@ async function run() {
|
|
|
758
783
|
frameSamplesMilliseconds.push( performance.now() - start );
|
|
759
784
|
|
|
760
785
|
}
|
|
761
|
-
|
|
786
|
+
// Keep the same stationary capture protocol for every renderer, outside timing.
|
|
787
|
+
benchmarkPhase = 'capture';
|
|
788
|
+
for ( let frame = 0; frame < warmupFrames; frame ++ ) {
|
|
789
|
+
|
|
790
|
+
await new Promise( resolve => requestAnimationFrame( resolve ) );
|
|
791
|
+
await adapter.render( benchmarkPose );
|
|
792
|
+
|
|
793
|
+
}
|
|
762
794
|
const [ memory, diagnostics ] = await Promise.all( [
|
|
763
795
|
measureBrowserMemory(),
|
|
764
796
|
adapter.diagnostics(),
|
|
765
797
|
] );
|
|
798
|
+
if ( [ 'gs3d', 'babylon' ].includes( rendererId ) && diagnostics.sortCompletions.measure !== measureFrames ) {
|
|
799
|
+
|
|
800
|
+
throw new Error( `${rendererId}: every measured frame must apply a complete camera sort.` );
|
|
801
|
+
|
|
802
|
+
}
|
|
766
803
|
const resources = performance.getEntriesByType( 'resource' );
|
|
767
804
|
return {
|
|
768
805
|
renderer: rendererId,
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import * as pc from 'playcanvas';
|
|
2
|
+
import { MemoryReadFileSystem } from '@playcanvas/splat-transform';
|
|
3
|
+
import { ProjectedSplatRenderer } from '../vendor/supersplat-editor/src/projected-splat-renderer.ts';
|
|
4
|
+
import { EditorSplatResource } from '../vendor/supersplat-editor/src/editor-splat-resource.ts';
|
|
5
|
+
import { GaussianInstances } from '../vendor/supersplat-editor/src/gaussian-instances.ts';
|
|
6
|
+
import { TransformPalette } from '../vendor/supersplat-editor/src/transform-palette.ts';
|
|
7
|
+
import { ColorPalette } from '../vendor/supersplat-editor/src/color-palette.ts';
|
|
8
|
+
import { Events } from '../vendor/supersplat-editor/src/events.ts';
|
|
9
|
+
import { loadSplatSource } from '../vendor/supersplat-editor/src/io/read/index.ts';
|
|
10
|
+
import { ShaderQuad, SimpleRenderPass } from '../vendor/supersplat-editor/src/utils/simple-render-pass.ts';
|
|
11
|
+
import { vertexShader, fragmentShader } from '../vendor/supersplat-editor/src/shaders/blit-shader.ts';
|
|
12
|
+
import { playCanvasEulerFromMatrix } from './scenes.js';
|
|
13
|
+
|
|
14
|
+
// The v3 editor's actual loader, GPU projector, radix sorter and draw shaders.
|
|
15
|
+
// This adapter supplies a neutral scene, with editing overlays disabled. It does
|
|
16
|
+
// not measure the editor UI or substitute the separately shipped viewer.
|
|
17
|
+
export async function createSuperSplatEditor( canvas, bytes, filename, viewport, initialPose, suite ) {
|
|
18
|
+
|
|
19
|
+
const device = await pc.createGraphicsDevice( canvas, {
|
|
20
|
+
deviceTypes: [ 'webgpu' ], antialias: false, alpha: false,
|
|
21
|
+
powerPreference: 'high-performance',
|
|
22
|
+
} );
|
|
23
|
+
const app = new pc.Application( canvas, { graphicsDevice: device } );
|
|
24
|
+
device.maxPixelRatio = viewport.dpr;
|
|
25
|
+
app.setCanvasResolution( pc.RESOLUTION_FIXED, viewport.width, viewport.height );
|
|
26
|
+
const mainCamera = new pc.Entity( 'benchmark-camera' );
|
|
27
|
+
mainCamera.addComponent( 'camera', {
|
|
28
|
+
nearClip: 0.01, farClip: 100,
|
|
29
|
+
// gsplatOutputVS passes display-space RGB unchanged with this combination.
|
|
30
|
+
toneMapping: pc.TONEMAP_NONE, gammaCorrection: pc.GAMMA_SRGB,
|
|
31
|
+
clearColor: suite === 'standard' ? new pc.Color( 0, 0, 0, 1 ) : new pc.Color( 7 / 255, 10 / 255, 15 / 255, 1 ),
|
|
32
|
+
} );
|
|
33
|
+
app.root.addChild( mainCamera );
|
|
34
|
+
const events = new Events();
|
|
35
|
+
const neutral = {
|
|
36
|
+
selection: null, selectedClr: pc.Color.WHITE, unselectedClr: pc.Color.WHITE, lockedClr: pc.Color.WHITE,
|
|
37
|
+
'view.bands': suite === 'standard' ? 3 : 0, 'view.minPixelSize': 0,
|
|
38
|
+
'colorPanel.pending': null, 'view.outlineSelection': false, 'view.editView': false,
|
|
39
|
+
'view.gaussians': true, 'view.selectionColor': false, 'view.rings': false,
|
|
40
|
+
'view.selectionRings': false, 'view.splatsColorBlend': 0, 'view.ringsColorBlend': 0,
|
|
41
|
+
};
|
|
42
|
+
for ( const [ name, value ] of Object.entries( neutral ) ) events.function( name, () => value );
|
|
43
|
+
const scene = {
|
|
44
|
+
app, graphicsDevice: device, events,
|
|
45
|
+
splatLayer: app.scene.layers.getLayerById( pc.LAYERID_WORLD ),
|
|
46
|
+
camera: { camera: mainCamera.camera, mainCamera, renderOverlays: false },
|
|
47
|
+
targetSize: viewport, movingRender: false,
|
|
48
|
+
};
|
|
49
|
+
const projected = new ProjectedSplatRenderer( scene );
|
|
50
|
+
// A resident PLY input, as in the other byte-based adapters. Its retained
|
|
51
|
+
// bytes count towards page memory; this is not the editor's file-backed heap.
|
|
52
|
+
const fileSystem = new MemoryReadFileSystem();
|
|
53
|
+
fileSystem.set( filename, new Uint8Array( bytes ) );
|
|
54
|
+
const { source } = await loadSplatSource( filename, fileSystem );
|
|
55
|
+
const resource = await EditorSplatResource.create( device, source );
|
|
56
|
+
const entity = new pc.Entity( 'benchmark-splats' );
|
|
57
|
+
entity.setLocalEulerAngles( 180, 0, 0 );
|
|
58
|
+
app.root.addChild( entity );
|
|
59
|
+
const instances = new GaussianInstances( device, resource.numRows, resource.initialState );
|
|
60
|
+
instances.flush();
|
|
61
|
+
projected.add( {
|
|
62
|
+
uid: 0, visible: true, entity, resource,
|
|
63
|
+
instances,
|
|
64
|
+
transformPalette: new TransformPalette( device ),
|
|
65
|
+
colorPalette: new ColorPalette( device ),
|
|
66
|
+
} );
|
|
67
|
+
// Match the editor's fp16 accumulation, second overlay attachment and native
|
|
68
|
+
// resolve. The sorted path uses the upstream blit's unfiltered mode.
|
|
69
|
+
const colorBuffer = new pc.Texture( device, {
|
|
70
|
+
name: 'benchmark-color', width: viewport.width, height: viewport.height,
|
|
71
|
+
format: pc.PIXELFORMAT_RGBA16F, mipmaps: false,
|
|
72
|
+
} );
|
|
73
|
+
const overlay = new pc.Texture( device, {
|
|
74
|
+
name: 'benchmark-overlay', width: viewport.width, height: viewport.height,
|
|
75
|
+
format: pc.PIXELFORMAT_RGBA8, mipmaps: false,
|
|
76
|
+
} );
|
|
77
|
+
mainCamera.camera.renderTarget = new pc.RenderTarget( { colorBuffers: [ colorBuffer, overlay ], depth: true } );
|
|
78
|
+
const resolve = new SimpleRenderPass( device, new ShaderQuad( device, vertexShader, fragmentShader, 'benchmark-resolve' ), {
|
|
79
|
+
vars: () => ( { srcTexture: colorBuffer, blitScale: [ 1, 1 ], quadResolve: 0 } ),
|
|
80
|
+
} );
|
|
81
|
+
resolve.init( null );
|
|
82
|
+
app.on( 'prerender', () => projected.render() );
|
|
83
|
+
app.on( 'postrender', () => resolve.render() );
|
|
84
|
+
const render = async pose => {
|
|
85
|
+
|
|
86
|
+
mainCamera.camera.fov = pose.fov;
|
|
87
|
+
if ( pose.matrix ) {
|
|
88
|
+
|
|
89
|
+
mainCamera.setPosition( pose.matrix[ 12 ], pose.matrix[ 13 ], pose.matrix[ 14 ] );
|
|
90
|
+
mainCamera.setEulerAngles( ...playCanvasEulerFromMatrix( pose.matrix ) );
|
|
91
|
+
|
|
92
|
+
} else {
|
|
93
|
+
|
|
94
|
+
mainCamera.setPosition( ...pose.position );
|
|
95
|
+
mainCamera.lookAt( new pc.Vec3( ...pose.target ) );
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
app.update( 0 );
|
|
99
|
+
app.render();
|
|
100
|
+
await device.wgpu.queue.onSubmittedWorkDone();
|
|
101
|
+
|
|
102
|
+
};
|
|
103
|
+
await render( initialPose );
|
|
104
|
+
return {
|
|
105
|
+
backend: device.deviceType === 'webgpu' ? 'WebGPU' : device.deviceType,
|
|
106
|
+
render,
|
|
107
|
+
async diagnostics() {
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
reportedGpuBytes: projected.stats.totalSplatGpuBytes,
|
|
111
|
+
reportedGpuBytesScope: 'SuperSplat Editor source, editing and estimated transient splat resources',
|
|
112
|
+
rendererMode: 'sorted', splatCount: resource.numRows, engineVersion: pc.version,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Version-specific benchmark integration: the frame clock ends only after the
|
|
2
|
+
// requested camera's full ordering is installed, including worker round trips.
|
|
3
|
+
export async function sortGS3D( viewer ) {
|
|
4
|
+
|
|
5
|
+
if ( viewer.sortRunning ) throw new Error( 'GaussianSplats3D still has a previous camera sort in flight.' );
|
|
6
|
+
// runSplatSort resolves after GPU distance readback and posting the worker;
|
|
7
|
+
// sortPromise resolves later, after updateRenderIndexes applies its result.
|
|
8
|
+
await viewer.runSplatSort( true, true );
|
|
9
|
+
await viewer.sortPromise;
|
|
10
|
+
if ( viewer.sortRunning || viewer.lastSplatSortCount !== viewer.splatRenderCount ) {
|
|
11
|
+
|
|
12
|
+
throw new Error( 'GaussianSplats3D did not apply a complete current-camera sort.' );
|
|
13
|
+
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function sortBabylon( splats, camera ) {
|
|
19
|
+
|
|
20
|
+
const worker = splats._worker;
|
|
21
|
+
if ( ! worker ) throw new Error( 'Babylon Gaussian sort worker is unavailable.' );
|
|
22
|
+
await new Promise( ( resolve, reject ) => {
|
|
23
|
+
|
|
24
|
+
let requestId;
|
|
25
|
+
const finish = error => {
|
|
26
|
+
|
|
27
|
+
clearTimeout( timeout );
|
|
28
|
+
worker.removeEventListener( 'message', advance );
|
|
29
|
+
worker.removeEventListener( 'error', failed );
|
|
30
|
+
if ( error ) reject( error ); else resolve();
|
|
31
|
+
|
|
32
|
+
};
|
|
33
|
+
const failed = () => finish( new Error( 'Babylon Gaussian sort worker failed.' ) );
|
|
34
|
+
const advance = () => {
|
|
35
|
+
|
|
36
|
+
if ( ! splats._canPostToWorker ) return;
|
|
37
|
+
if ( requestId !== undefined ) {
|
|
38
|
+
|
|
39
|
+
const applied = splats._cameraViewInfos.get( camera.uniqueId )?.sortAppliedId;
|
|
40
|
+
finish( applied === requestId ? null : new Error( 'Babylon applied a different camera sort.' ) );
|
|
41
|
+
return;
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
splats._postToWorker( true );
|
|
45
|
+
requestId = splats._cameraViewInfos.get( camera.uniqueId )?.sortRequestId;
|
|
46
|
+
if ( requestId === undefined || splats._canPostToWorker ) failed();
|
|
47
|
+
|
|
48
|
+
};
|
|
49
|
+
const timeout = setTimeout( failed, 30_000 );
|
|
50
|
+
worker.addEventListener( 'message', advance );
|
|
51
|
+
worker.addEventListener( 'error', failed );
|
|
52
|
+
advance();
|
|
53
|
+
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
}
|