Skip to content

Space gsplat sort keys by log distance so stray far splats keep near precision - #9899

Merged
mvaligursky merged 1 commit into
playcanvas:mainfrom
slimbuck:gsplat-log-radial-sort
Nov 2, 2026
Merged

mvaligursky merged 1 commit into
playcanvas:mainfrom
slimbuck:gsplat-log-radial-sort

Conversation

@slimbuck

@slimbuck slimbuck commented Nov 1, 2026

Copy link
Copy Markdown
Member

Description

Gsplat sort keys were spread linearly over the distance range, in camera-weighted bins out to the farthest splat. A few stray splats far outside a scene stretch that range, so splats near the camera share keys and draw out of order. On superspl.at scene 0839ceea the CPU sort flickers across the whole scene. The GPU projector spaced its keys the same way.

Radial keys, and the depth keys of a perspective camera, now come from the bit pattern of the distance as a float, which follows log2. Every doubling of distance gets the same number of keys, so far splats cost no precision near the camera. Radial keys use the squared distance, and depth keys the depth in front of the camera. Keys start ten doublings inside the near clip, because splats closer than the near clip are still drawn, with their depth clamped. The GPU projector and the CPU worker compute the keys the same way. Orthographic and fisheye depth sorts keep the bins, since they can draw splats at or behind the camera.

Because log keys are spread over every doubling of distance, a few keys are not enough: a small scene far from the camera would have its splats share keys. So the GPU sort now takes at least 16 bits, where scenes under about 23k splats had 12. Larger scenes are unchanged, and the smaller ones get one more radix pass.

In the CPU worker:

  • It writes the distances as floats over the key buffer, then converts them in a second pass. Reading each float's bits back in the same loop measured slower.
  • Log keys always take 16 bits, whatever the splat count. A small scene far from the camera still gets thousands of keys per doubling of distance, and the counting sort's table stays at 256 KB.
  • Depth keys give splats behind the camera the last sixteenth of the keys, spaced the same way. A shader that moves those splats into view then finds them in the order of their signed depth, because the CPU sort reads the centers before any modifier.
  • The sign of each depth picks the key's scale, clamp and direction arithmetically. A branch on it ran the loop at half the speed, and slower still when splats in front and behind are interleaved.

In the manager:

  • Log depth keys are measured from the camera, so a directional CPU sort now re-sorts when the camera moves, not only when it turns.
  • A change of sort key mode (radial, log depth or bins, for example after a projection switch) re-sorts even when the camera is still.
  • A sort the worker turns away while one is in flight is no longer dropped. The request stays until it is sent, and the camera pose is recorded only then. Before, the last pose of a fast move could go unsorted until the camera moved again.

GSplatSortBinWeights.computeCameraBin loses its unused radial parameter.

Trade-off

The CPU path uses 16-bit keys, while the GPU sort uses up to 20. In 0839ceea a slight flicker remains on very fine detail (a bike) on the CPU path, and it goes away with 20-bit CPU keys. CPU keys stay at 16 bits for speed: at 20 bits the counting table is 4 MB.

Performance

CPU sort time for 3M splats, upstream → this PR, as medians of alternating runs. The layouts are:

  • in front: a 20 m cube 5–25 m ahead of the camera;
  • around: a 20 m cube centred on the camera, half of it behind;
  • stray: in front, plus 0.1% of the splats scattered out to 10 km.

Coherent means the splats are stored in depth order, which is upstream's best case. Random means shuffled.

Pixel 7 Pro, Chrome, in a web worker:

Layout Radial Depth
in front, coherent 39.8 → 30.3 ms 28.6 → 34.4 ms
in front, random 54.8 → 34.3 ms 79.7 → 39.8 ms
around, coherent 54.4 → 33.2 ms 28.6 → 30.7 ms
around, random 62.8 → 34.5 ms 90.1 → 35.5 ms
stray, coherent 36.3 → 30.2 ms 30.0 → 31.1 ms
stray, random 42.2 → 34.1 ms 37.7 → 35.4 ms

Apple M4 Max, Node 22:

Layout Radial Depth
in front, coherent 12.8 → 11.8 ms 9.9 → 14.9 ms
in front, random 13.4 → 12.3 ms 13.7 → 13.7 ms
around, coherent 14.1 → 12.1 ms 11.1 → 14.3 ms
around, random 14.3 → 12.3 ms 13.4 → 11.8 ms
stray, coherent 10.1 → 11.7 ms 11.9 → 14.5 ms
stray, random 11.4 → 12.5 ms 10.5 → 11.3 ms

Upstream is quicker with stray splats because its bins collapse: most splats land in a few bins, which is the precision loss this PR fixes.

On 0839ceea itself, on the Pixel 7 Pro in WebGL during the default camera animation:

  • Radial sorts take 51.7 → 45.7 ms.
  • Depth sorts take 42.7 → 46.1 ms. They now also run when the camera moves, at 9.8 → 11.4 sorts a second.
  • The median frame time is unchanged at 16.4 ms.

On the GPU path, 0839ceea on the Pixel 7 Pro in WebGPU takes 32.4 → 30.8 ms of GPU time per frame with radial sorting, and 32.9 → 30.9 ms with depth sorting (medians of 300 frames over three alternating rounds).

Testing

  • New unit tests for the sort worker cover:
    • radial and depth order;
    • a stray far splat;
    • splats closer than the near clip;
    • a small scene far from the camera;
    • splats behind the camera that a shader moves into view;
    • orthographic bins.
  • New unit tests for the manager cover re-sorting on a move or a turn in each mode and on a mode switch, plus GSplatUnifiedSorter#setSortParams reporting whether it sent the sort.
  • All unit tests pass.
  • Checked by eye in the SuperSplat viewer on 0839ceea, on WebGL and WebGPU, on a Mac and a Pixel 7 Pro.
  • A 10k-splat scene, which takes the new 16-bit GPU minimum, renders the same as upstream on WebGPU.

Checklist

  • I have read the contributing guidelines
  • My code follows the project's coding standards
  • This PR focuses on a single change

…near precision

Sort keys were spread linearly over the distance range, in camera-weighted bins out to the farthest splat. A few stray splats far away stretched the range, so splats near the camera shared keys and drew out of order (superspl.at scenes 0bb9b075 and 0839ceea).

Radial keys, and the depth keys of a perspective camera, now come from the bit pattern of the distance as a float, which follows log2: every doubling of distance gets the same number of keys. They start ten doublings inside the near clip, since the splats closer than it are drawn too, with their depth clamped. The GPU projector and the CPU worker compute them the same way. Orthographic and fisheye depth sorts keep the bins, since they can draw splats at or behind the camera.

The CPU worker writes the distances as floats first and converts them in a second pass, and caps log keys at 16 bits so the counting sort's table stays in cache. Its depth keys give the splats behind the camera the last sixteenth of the keys, spaced the same way, so a shader that moves them into view finds them in the order of their signed depth (the CPU sort reads the centers before any modifier).

Log depth keys are measured from the camera, so a directional CPU sort that uses them now re-sorts when the camera moves, not only when it turns. A switch of projection or sort mode re-sorts too, even with the camera still. And a sort the worker turns away while one is in flight is no longer dropped: the request stays until it is sent, and the camera pose is recorded only then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@slimbuck
slimbuck requested a review from mvaligursky November 1, 2026 20:05
@slimbuck slimbuck self-assigned this Nov 1, 2026
@slimbuck slimbuck added the area: graphics Graphics related issue label Nov 1, 2026

@mvaligursky mvaligursky left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All checks out, and the example with cpu sorting don't see any objectionable sorting issue from 16bit key, so approving!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: graphics Graphics related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants