Skip to content

perf(context): skip redundant transform refreshes in semantic snapshots - #577

Merged
dli7319 merged 6 commits into
google:mainfrom
salmanmkc:salmanmkc-context-transform-reuse
Sep 15, 2026
Merged

dli7319 merged 6 commits into
google:mainfrom
salmanmkc:salmanmkc-context-transform-reuse

Conversation

@salmanmkc

@salmanmkc salmanmkc commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Description

buildSemanticTree already calls scene.updateMatrixWorld(true) once before it traverses the scene. Despite that, createSemanticNode called object.updateMatrixWorld(true) again for every node it visited, which forced a full recursive refresh of that node's subtree. In a deep hierarchy the same descendants get refreshed once per ancestor, so the cost grows with depth while the matrices are already current.

This removes that per-node call. The scene-wide refresh at the start of the snapshot stays exactly where it is, and object.getWorldPosition(tempPosition) stays too, since it performs the ancestor walk the node actually needs. Bounds are unaffected because getObjectBounds / getUIObjectBounds refresh their own subtrees, and Box3.expandByObject updates each object it visits.

Keeping getWorldPosition matters for correctness, not just style. UIElement overrides it so a UI node reports the world position of its registered uikit presentation object rather than its logical transform. Reading matrixWorld directly instead, as a first pass did, silently reports the wrong position for those elements once the logical and presentation transforms diverge.

There is no caching across frames or snapshots, no change to camera or XR pose timing, and no public API change.

On performance, this is a redundant-work cleanup rather than a headline win. An isolated synthetic benchmark over a deep semantic snapshot showed a small, repeatable reduction in per-snapshot build time. Semantic context polling defaults to 3000 ms, so this is not expected to move frame rate in a typical app.

Covered by a new colocated SemanticTreeBuilder.test.ts:

  • positions and bounds for objects beneath transformed, rotated, and scaled parents
  • fresh values on a later snapshot after objects move and rescale
  • no forced per-node subtree refresh during a snapshot
  • a custom getWorldPosition override remains authoritative
  • a real UICard / UIPanel registered through the presentation helper under a separate render root, where the logical and presentation transforms differ

Three of those cases fail against the behavior they guard, so they are live regressions rather than restatements.

Verified in Chromium as well as in unit tests: the same page was loaded against a build with and a build without this change, and the resulting semantic trees, visible-object contexts, and bounds were byte-identical across three snapshots taken before and after moving, rotating, and scaling both plain meshes and spatial UI.

Type of Change

  • Bug fix
  • New feature / enhancement
  • New demo or sample
  • Documentation update

Media / Screen Recordings & Screenshots (If Applicable)

  • Simulator Recording: not applicable, there is no visible behavior change. Verified in the desktop simulator by comparing semantic snapshots before and after the change.
  • Device Recording: not applicable.

Checklist

  • Tested in simulator & device: Verified in the desktop simulator. Not tested on physical hardware.
  • Large Assets ($\ge$ 1MB): Submitted separately to xrblocks/proprietary-assets via jsdelivr CDN.
  • SDK Dynamic Dependencies: All new SDK dependencies are dynamically loaded at runtime.
  • Security: Confirmed no hardcoded API keys or secrets are committed.

@salmanmkc

Copy link
Copy Markdown
Contributor Author

this is actually just removing one line with a bunch of tests

@salmanmkc salmanmkc changed the title skip redundant transform refreshes in semantic snapshots perf(context): skip redundant transform refreshes in semantic snapshots Sep 12, 2026
buildSemanticTree already calls scene.updateMatrixWorld(true) before it
traverses, so the extra object.updateMatrixWorld(true) inside
createSemanticNode re-walked each node's subtree again for every semantic
node. getWorldPosition already refreshes the ancestor chain it needs, and
bounds computation refreshes its own subtree, so the forced per-node
refresh was pure repeated work.

Keeps the scene-wide refresh at the start of the snapshot and leaves
getWorldPosition in place, so custom overrides such as UIElement's
presentation-aware getWorldPosition stay authoritative.

Adds a colocated test covering positions and bounds under transformed
parents, freshness after later movement and scale changes, the absence of
a forced per-node subtree refresh, and UI elements whose logical and
presentation transforms differ.
@salmanmkc
salmanmkc force-pushed the salmanmkc-context-transform-reuse branch from 5e87df8 to 46a9a52 Compare September 12, 2026 05:06
@dli7319

dli7319 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Can you fix the merge conflict?

@salmanmkc
salmanmkc force-pushed the salmanmkc-context-transform-reuse branch from ec6c096 to 71feaf6 Compare September 15, 2026 16:57
@salmanmkc

Copy link
Copy Markdown
Contributor Author

Can you fix the merge conflict?

Yup done!

@salmanmkc
salmanmkc force-pushed the salmanmkc-context-transform-reuse branch 2 times, most recently from 90be083 to 93ce92a Compare September 15, 2026 17:01
@dli7319

dli7319 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Thanks.

@dli7319
dli7319 merged commit efa5cb3 into google:main Sep 15, 2026
11 checks passed
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.

2 participants