Repository navigation
fix: Resolve equal gsplat GPU sort keys in the same order every frame - #9549
Open
simonbaronti wants to merge 1 commit into
Open
simonbaronti wants to merge 1 commit into
simonbaronti wants to merge 1 commit into
Conversation
The GPU-sort gsplat renderer's projector compacted visible splats with a per-thread atomicAdd inside each workgroup and a per-workgroup atomicAdd on the global counter, so the input order of the stable radix sort, and with it the blend order of splats with equal depth keys, changed every frame. With a static camera this shows as flicker; with on-demand rendering, as a pop on every re-render. Survivors are now ranked by thread index inside each workgroup (survivor bitmask + popcount), and the projector packs its workgroup index below the depth key, so ties between workgroups also resolve the same way every frame. The sort runs over the depth bits plus these tie bits, at most 32. Fixed playcanvas#9548
simonbaronti
force-pushed
the
fix/gsplat-gpu-sort-deterministic-ties
branch
from
September 25, 2026 16:42
2161c52 to
7193200
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes flicker in the WebGPU GPU-sort gsplat renderer (
GSPLAT_RENDERER_RASTER_GPU_SORT): splats with equal sort keys were blended in a different order every frame, visible as shimmer with a static camera and as a pop on every re-render with on-demand rendering.Fixes #9548
countOneBits, the same idea as the radix reorder shader), instead of by the order of their workgroupatomicAdd, which now only counts them. Each workgroup's output range is therefore in a fixed order.tieBitsprojector uniform).GSplatHybridRenderersizestieBitsto the maximum workgroup count, rounded up to whole radix passes and capped at a 32-bit key, and sorts overnumBits + tieBits.Technical details
The radix sort is stable, so equal keys keep their input order. Before this change that order came from two atomics (
wgCountper thread,renderCounterper workgroup) and changed every frame. Depth keys are at most 20 bits (clamp(round(log2(n / 4)), 10, 20)), so ties are common in dense scenes. The CPU worker sort receives its input in a fixed order, which is why WebGL2 was not affected.With more than
2^tieBitsprojector workgroups (over 1M candidate splats at 20 depth bits) the workgroup index wraps, so two tied splats at least 1M candidates apart could in principle still swap. Frames were bit-identical in every test below.Public API changes
None.
Performance
GPU timestamps (engine
gpuProfiler), medians of 120 frames, static camera, Apple M4 Pro / Chrome 153 / WebGPU, 1280×800. Before =main@ 25b638c, after = this branch:About +0.25 ms of GPU time per frame in total: the extra radix passes (which only run over the splats that survive projection) and the rank (computed by survivors only). Forward and whole-frame differences are within run-to-run noise (up to 0.5 ms between repeated runs of the same build). The forward pass is unaffected because the draw order within a depth bucket stays in candidate (roughly spatial) order. Letting the workgroup leader sum the mask instead of keeping the
wgCountatomic was measurably slower (projector +0.17 ms at 20M instead of +0.11 ms).Alternatives measured (on 2.22.4) and rejected:
Testing
npm run lint(changed files),npm test(3591 passing, rebased on main @ 211b142),npm run build:types+npm run test:types.Checklist