Skip to content

Make GSplat scene parameters tree-shakeable - #9328

Merged
mvaligursky merged 2 commits into
mainfrom
codex/nullable-gsplat-params
Sep 8, 2026
Merged

Make GSplat scene parameters tree-shakeable#9328
mvaligursky merged 2 commits into
mainfrom
codex/nullable-gsplat-params

Conversation

@mvaligursky

Copy link
Copy Markdown
Contributor

Move scene-wide GSplat parameters behind GSplatComponentSystem so applications that omit the system can fully tree-shake the GSplat implementation.

Changes:

  • Create and destroy scene-wide GSplat parameters with GSplatComponentSystem.
  • Pass the initialized parameters directly through GSplat rendering internals.
  • Handle absent GSplat parameters in shared rendering paths.
  • Add lifecycle coverage and strengthen the AppBase tree-shaking test.

API Changes:

  • Scene#gsplat changes from GSplatParams to GSplatParams | null.
  • It is initialized synchronously when GSplatComponentSystem is included and is otherwise null.

Before:

const renderer = app.scene.gsplat.renderer;

For applications without GSplatComponentSystem:

const renderer = app.scene.gsplat?.renderer;

Performance:

  • Tree-shaken AppBase applications without GSplatComponentSystem no longer retain GSplat formats, varyings, resource implementations, or GLSL/WGSL shader chunks.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Build size report

This PR changes the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2414.4 KB (+0.2 KB, +0.01%) 622.5 KB (−0.3 KB, −0.05%) 483.2 KB (−0.1 KB, −0.02%)
playcanvas.min.mjs 2411.8 KB (+0.2 KB, +0.01%) 621.4 KB (−0.3 KB, −0.05%) 482.4 KB (−0.2 KB, −0.05%)

@mvaligursky mvaligursky left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Automated PR review — posted on my behalf by Claude Code (Opus 4.8). Not a human review. The points below are suggestions to weigh as possible improvements, not changes that necessarily need to be addressed.


The injection is thorough and the lifecycle holds up. Things I checked and found clean:

  • No unguarded consumer left behind. I swept every scene.gsplat reference in src/ at this head — the only remaining ones are JSDoc text and deprecation strings. All live reads are either guarded with ?. or converted to the injected this.gsplat.
  • Teardown ordering is safe. GSplatComponentSystem.destroy() touches app.scene and app.renderer, and AppBase.destroy runs systems.destroy() (1905) well before renderer.destroy() (1948) and scene.destroy() (1961), so neither is null yet. Renderer.destroy() also destroys the director, but both paths null the field and use ?., so there's no double-destroy in either order.
  • The tree-shaking claim holds and the strengthened filter (now covering /scene/gsplat/, /scene/gsplat-unified/, /chunks/gsplat/ and /gsplat-chunks-) is a real improvement over the four-file list it replaces. 84 tests pass, lint clean.

One point on the API change worth weighing before this lands, plus two minor notes — all inline.

Minor, and not inline-able since the file isn't in this diff: Renderer.destroy() (renderer.js:293-294) also destroys and nulls the director. That line is now dead in every normal teardown, since systems.destroy() runs first and nulls the field, so the ?. always short-circuits. Harmless, but the director's ownership is now expressed in two places - worth either dropping it (the system is the sole creator, so the sole owner) or noting that it's a backstop for a renderer torn down without its systems, so the next reader knows which is authoritative.

Comment thread src/scene/scene.js Outdated
* @type {GSplatParams}
* Returns null when the application does not include {@link GSplatComponentSystem}.
*
* @type {GSplatParams|null}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Worth weighing whether the declared type needs to become nullable, separately from the runtime value.

The migration example in the description shows a read (app.scene.gsplat?.renderer), but the dominant use of this getter is configuration, and optional chaining is a SyntaxError on an assignment target — app.scene.gsplat?.splatBudget = 4e6 does not parse. So a downstream user with strictNullChecks on (the default under strict, and the norm for new TS projects) can't reach for ?. at all; every write has to be restructured into a guard or a non-null assertion. In this repo's own examples that's billions (12 sites), downtown (13), clipping (8), benchmark (5), and so on across roughly twenty gaussian-splatting examples.

And the guard is noise for all of them: an app touching these params has necessarily included GSplatComponentSystem, which is exactly the condition under which the value is never null. So the nullability is unobservable to every legitimate consumer while being mandatory in their types.

An alternative that keeps the tree-shaking win — scene.js still doesn't import GSplatParams, which is the whole mechanism — is to leave the declared type as GSplatParams and put the honesty in the getter instead:

get gsplat() {
    Debug.assert(this._gsplatParams, 'Scene#gsplat requires GSplatComponentSystem to be included in the app.');
    return this._gsplatParams;
}

Zero consumer churn, TS users unaffected, and an app that wrongly reaches for it gets a clear message instead of a Cannot read properties of null. The cost is that the type is optimistic for apps without the system — which have no reason to read it.

If you'd rather keep | null as the honest signature, the description's migration section is worth extending to cover the write case, since that's where essentially all the churn is.

(For what it's worth this won't fail CI as it stands: examples/tsconfig.examples.json sets checkJs: false and the base config doesn't enable strictNullChecks, so the un-updated examples aren't checked. That also means the repo has no signal that its own sample code now models a pattern strict-mode consumers can't copy.)

// Own the scene-wide parameters here so apps that omit this system also tree-shake the
// GSplat formats, varyings and shader chunks imported by GSplatParams.
const gsplatParams = new GSplatParams(app.graphicsDevice);
app.scene._gsplatParams = gsplatParams;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This writes a _-prefixed private of another class from a different module, and Scene exposes no setter for it — so the field name is now part of a cross-module contract while still reading as private. A rename inside Scene would break this silently, and there's nothing at the Scene end pointing at the fact that a component system owns the value.

An @ignored internal setter (or a small _setGsplatParams(params)) would make the coupling declared at both ends and give the ownership comment somewhere to live next to the field.

@mvaligursky

Copy link
Copy Markdown
Contributor Author

Tree-shaking size comparison using the unchanged misc/hello-world AppBase example (spinning cube, camera, and one directional light).

Method: bundle from src/index.js with esbuild 0.28.1 using bundle: true, minify: true, ESM output, target: es2022, and no legal comments. examples/context was replaced by a one-line deviceType = "webgl2" module. The example source has the same SHA-256 at every measured revision. Compressed columns use gzip level 9 and Brotli quality 11.

Revision Minified Gzip Brotli Retained GSplat modules GSplat bytes in output
Before all three PRs (1c36a730a, parent of #9322) 1,394,633 B 378,342 B 278,937 B 40 57,884 B
After #9322 (16127cac6) 1,380,346 B 374,502 B 276,128 B 35 43,597 B
After #9327 (cb79e7ac8) 1,366,835 B 369,904 B 272,499 B 29 29,689 B
This PR (11719fd17) 1,336,612 B 362,497 B 267,055 B 0 0 B

Combined result from before #9322 to this PR:

  • Minified: -58,021 B (-4.16%)
  • Gzip: -15,845 B (-4.19%)
  • Brotli: -11,882 B (-4.26%)
  • Retained GSplat modules: 40 → 0

Exact parent-to-head minified reductions were 14,287 B in #9322, 13,511 B in #9327, and 30,223 B in this PR.

@mvaligursky
mvaligursky merged commit e13ffb7 into main Sep 8, 2026
10 checks passed
@mvaligursky
mvaligursky deleted the codex/nullable-gsplat-params branch September 8, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant