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.
Files changed (46) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/NOTICE +5 -6
  3. package/README.md +41 -7
  4. package/bin/three-blocks-benchmark.mjs +3 -1
  5. package/browser/harness.js +53 -16
  6. package/browser/supersplat-editor.js +118 -0
  7. package/browser/worker-sort.js +56 -0
  8. package/lib/run.mjs +61 -35
  9. package/package.json +14 -6
  10. package/vendor/README.md +26 -45
  11. package/vendor/supersplat-editor/LICENSE +19 -0
  12. package/vendor/supersplat-editor/manifest.json +34 -0
  13. package/vendor/supersplat-editor/src/color-grade.ts +206 -0
  14. package/vendor/supersplat-editor/src/color-palette.ts +126 -0
  15. package/vendor/supersplat-editor/src/data-processor/histogram-config.ts +22 -0
  16. package/vendor/supersplat-editor/src/editor-splat-resource.ts +368 -0
  17. package/vendor/supersplat-editor/src/events.ts +27 -0
  18. package/vendor/supersplat-editor/src/gaussian-instances.ts +464 -0
  19. package/vendor/supersplat-editor/src/index-ranges.ts +121 -0
  20. package/vendor/supersplat-editor/src/io/index.ts +20 -0
  21. package/vendor/supersplat-editor/src/io/read/file-systems.ts +146 -0
  22. package/vendor/supersplat-editor/src/io/read/index.ts +17 -0
  23. package/vendor/supersplat-editor/src/io/read/loader.ts +237 -0
  24. package/vendor/supersplat-editor/src/io/write/browser-file-system.ts +141 -0
  25. package/vendor/supersplat-editor/src/io/write/index.ts +12 -0
  26. package/vendor/supersplat-editor/src/io/write/writer.ts +118 -0
  27. package/vendor/supersplat-editor/src/projected-splat-renderer.ts +885 -0
  28. package/vendor/supersplat-editor/src/shaders/blit-shader.ts +114 -0
  29. package/vendor/supersplat-editor/src/shaders/color-grade-chunk.ts +55 -0
  30. package/vendor/supersplat-editor/src/shaders/footprint-intersect-shader.ts +117 -0
  31. package/vendor/supersplat-editor/src/shaders/palette-chunk.ts +33 -0
  32. package/vendor/supersplat-editor/src/shaders/projected-splat-indirect-args-shader.ts +65 -0
  33. package/vendor/supersplat-editor/src/shaders/projected-splat-projector-shader.ts +387 -0
  34. package/vendor/supersplat-editor/src/shaders/projected-splat-shader.ts +299 -0
  35. package/vendor/supersplat-editor/src/splat-state.ts +9 -0
  36. package/vendor/supersplat-editor/src/transform-palette.ts +120 -0
  37. package/vendor/supersplat-editor/src/utils/resolve.ts +9 -0
  38. package/vendor/supersplat-editor/src/utils/simple-render-pass.ts +84 -0
  39. package/vendor/three-dev/build/three.core.js +0 -60911
  40. package/vendor/three-dev/build/three.tsl.js +0 -672
  41. package/vendor/three-dev/build/three.webgpu.js +0 -90737
  42. package/vendor/three-gs/gpgpu/CountingSort.js +0 -271
  43. package/vendor/three-gs/loaders/GaussianSplatPLYLoader.js +0 -386
  44. package/vendor/three-gs/loaders/PLYLoader.js +0 -968
  45. package/vendor/three-gs/objects/GaussianSplat.js +0 -859
  46. 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/three-gs/ contains verbatim copies of three.js addon files from the
17
- unmerged pull request https://github.com/mrdoob/three.js/pull/33950, used to
18
- benchmark that implementation. They remain under the MIT License,
19
- Copyright (c) 2010-2026 three.js authors:
20
- https://github.com/mrdoob/three.js/blob/dev/LICENSE
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 `GaussianSplat`
4
- addon ([merged into `dev`][pr], vendored with the `dev` build it needs — see
5
- `vendor/README.md`), Spark, SuperSplat, GaussianSplats3D, and Babylon.js.
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 runs headless Chromium by default. On macOS it forces the Metal ANGLE
63
- backend, without which headless Chromium selects SwiftShader and captures blank
64
- frames. Use `--headed` to watch a run; the recorded metrics are the same.
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;
@@ -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' ) ?? 'float16';
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( '../vendor/three-gs/loaders/GaussianSplatPLYLoader.js' ),
234
- import( '../vendor/three-gs/objects/GaussianSplat.js' ),
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.finish();
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
- sphericalHarmonicsDegree: 3,
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
- // Babylon depth-sorts on a worker outside the frame (like GS3D and SuperSplat); the
504
- // timed frame is the raster. The 1x1 readback forces true GPU completion.
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
- await frameEnd;
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 !== 'three-blocks' ) {
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 === 'three-blocks' ) adapter = await createThreeBlocks( bytes );
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
- await adapter.render( benchmarkPose );
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
+ }