Skip to content

Error-driven GSplat budget allocation from streamed SOG LOD error metadata - #9157

Open
slimbuck wants to merge 5 commits into
playcanvas:mainfrom
slimbuck:dec-err
Open

Error-driven GSplat budget allocation from streamed SOG LOD error metadata#9157
slimbuck wants to merge 5 commits into
playcanvas:mainfrom
slimbuck:dec-err

Conversation

@slimbuck

@slimbuck slimbuck commented Aug 10, 2026

Copy link
Copy Markdown
Member

LOD selection for streamed SOG has been purely geometric: each node picks a level from camera distance bands, and the budget balancer redistributes by distance bucket. Distance is a poor proxy for how much a scene actually loses at a given level — a node full of near-duplicate splats and one carrying fine detail are treated alike.

This spends the scene's splat budget where it buys the most quality instead, using per-node approximation errors that splat-transform now writes into lod-meta.json (playcanvas/splat-transform#NNN). The two land together; this side falls back to the existing distance-bucket allocator, now _balanceDistance, whenever error metadata is absent.

Allocation. For each visible node, the renderable levels are sorted by ascending cost (with error as a tiebreak) and swept once, keeping a level only when it strictly improves on the cheapest error seen so far. That yields the Pareto frontier over (splat count, error) in O(L log L), and it comes out already ordered by cost. Every node is seeded at its cheapest frontier entry, and the remaining transitions are ranked by lodCoverage * benefit / cost — error reduction per additional splat, weighted by how much screen the node covers — then applied greedily in log-priority buckets until the budget is spent. Math.min(previousPriority, ...) keeps a node's marginal returns non-increasing, so a later upgrade on one node can never outrank an earlier one.

Requiring a strict improvement in that sweep matters beyond tidiness: it collapses levels identical in both cost and error, so consecutive frontier entries always differ in both and every transition's cost stays above zero. A zero-cost transition would carry a 0/0 priority, and because priorities accumulate through Math.min, that NaN propagates to every later transition on the node and demotes all of them to the lowest bucket regardless of what they are worth — the node then loses budget to genuinely lower-value upgrades elsewhere in the scene.

NodeInfo.lodCoverage is the view-dependent half of the ranking: squared projected radius including the FOV scale and behind-camera penalty, computed alongside the existing distance evaluation.

Error metadata. GSplatOctreeNodeLod gains error, read from each leaf's errors array by absolute LOD level. Whether that metadata is usable is settled once, in GSplatOctree, from the manifest's lodErrors header and confirmed against the values themselves inside the nodes.map loop the constructor already runs — so the balancer reads one boolean per instance rather than walking every node's levels each frame, and its choice of allocator is a property of the asset rather than of what the camera currently sees.

A level that can be rendered must supply an error that is finite and non-negative. Errors are magnitudes relative to the finest LOD, so a negative one is meaningless, and it is more dangerous than a non-finite one: it would pass a finiteness check, let a coarse level dominate every finer level on the frontier, and pin the node there at any budget. A manifest that declares the header without meeting that contract warns and falls back.

Note the validation covers all LOD levels rather than only the configured [rangeMin, rangeMax], which is what keeps the result independent of runtime state.

@slimbuck
slimbuck requested a lite review from Copilot August 10, 2026 09:34
@slimbuck slimbuck self-assigned this Aug 10, 2026
@slimbuck slimbuck added the area: graphics Graphics related issue label Aug 10, 2026

Copilot AI 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.

Pull request overview

This PR adds error-driven splat budget allocation for streamed SOG GSplat octrees when per-node/per-LOD approximation error metadata is available, with a fallback to the existing distance-bucket allocator when it isn’t.

Changes:

  • Parse and validate streamed SOG lodErrors/errors metadata at GSplatOctree construction time and expose a per-octree lodErrors capability flag.
  • Add view-dependent NodeInfo.lodCoverage and implement an error-benefit-per-cost budget balancer (_balanceErrors) that uses Pareto-frontier LOD transitions.
  • Add unit tests covering allocator fallback behavior and streamed error-metadata loading/validation.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/scene/gsplat-unified/gsplat-budget-balancer.test.mjs Adds tests for error-driven balancing vs. distance fallback and octree manifest error validation.
src/scene/gsplat-unified/gsplat-octree.js Loads errors[] from manifests, validates completeness/finite values, and exposes lodErrors with a fallback warning.
src/scene/gsplat-unified/gsplat-octree-node.js Extends GSplatOctreeNodeLod with an optional error field.
src/scene/gsplat-unified/gsplat-octree-instance.js Computes per-node projected coverage (lodCoverage) during LOD evaluation to weight error-based priorities.
src/scene/gsplat-unified/gsplat-budget-balancer.js Implements _balanceErrors allocator and keeps legacy _balanceDistance for missing/incomplete error metadata.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/scene/gsplat-unified/gsplat-budget-balancer.js Outdated
Comment thread src/scene/gsplat-unified/gsplat-budget-balancer.js Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/scene/gsplat-unified/gsplat-budget-balancer.js:172

  • This comment says a zero-cost transition would "stall" the node for the rest of the pass, but the real failure mode is that a 0/0 transition produces NaN priority and demotes the node's later upgrades into the lowest bucket (as described in the PR discussion). Updating this keeps the rationale for the strict-improvement frontier accurate.
                // differ in both. That keeps every transition's cost above zero: a
                // zero-cost transition would carry a 0/0 priority and, because
                // upgrades apply in chain order, would stall the node there for the
                // rest of the pass.

src/scene/gsplat-unified/gsplat-budget-balancer.js:19

  • The _buckets JSDoc says the buckets store NodeInfo references, but _balanceErrors uses the same buckets to store transition indices (numbers). This makes the comment/type description misleading for future maintainers.

This issue also appears on line 169 of the same file.

    /**
     * Buckets storing NodeInfo references.
     * @type {Array<Array>|null}
     * @private
     */

src/scene/gsplat-unified/gsplat-octree-node.js:12

  • GSplatOctreeNodeLod.error can be null (e.g. when metadata is missing/invalid, as exercised by the new tests), but the typedef only allows number|undefined. This makes generated typings inaccurate for consumers.
 * @property {number} count - The count of items
 * @property {number|undefined} error - Approximation error relative to the finest LOD
 */

test/scene/gsplat-unified/gsplat-budget-balancer.test.mjs:50

  • This test hard-codes 63 as the last distance bucket. That couples the test to the current NUM_BUCKETS = 64 value; if the constant changes, the test will fail for an unrelated reason. Prefer deriving this from NUM_BUCKETS - 1 (imported from src/scene/gsplat-unified/constants.js).
        inst.nodeInfos[0].budgetBucket = 0;
        inst.nodeInfos[1].budgetBucket = 63;

@slimbuck
slimbuck marked this pull request as ready for review August 10, 2026 10:22
@slimbuck
slimbuck requested a review from a team August 10, 2026 10:22
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.

3 participants