Skip to content

fix: stop GPU Instancer blocking the main thread on startup - #10111

Merged
lorux0 merged 2 commits into
devfrom
fix/anr-gpu-instancer
Sep 16, 2026
Merged

lorux0 merged 2 commits into
devfrom
fix/anr-gpu-instancer

Conversation

@lorux0

@lorux0 lorux0 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes a pre-in-world ANR cluster — 6 of 200 sampled events
(UNITY-EXPLORER-PBX).

Problem

The first GPUICoreAPI.RegisterRenderer call creates GPUIRenderingSystem, and its Awake reads
GPUIShaderBindings.Instance. That getter loads the asset with Addressables
WaitForCompletion() — the one Addressables API that is explicitly synchronous.

WaitForCompletion() can't wait for just its own asset. It makes Unity finish every async load
already in flight before returning, so this one small request freezes the main thread for as long
as the slowest unrelated load takes.

Fix

Load the asset ourselves and hand it to GPUI before the rendering system exists:

With GPUIShaderBindings.Instance already set, that code never runs — the load isn't made faster, it doesn't happen.

Testing

  • Landscape trees render as before, same density and LOD distances.
  • No GPUI warnings about missing shader bindings or replacement materials in the log.

@lorux0
lorux0 requested review from a team as code owners September 15, 2026 18:48
@github-actions
github-actions Bot requested a review from anicalbano September 15, 2026 18:48
@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

🚦 CI Status

Build

Windows and Mac built successfully in Unity Cloud.

Name Links & timing
Build b030d66 · Logs · built 2026-09-15T20:31:28Z
Windows GitHub job · Unity Cloud #2 · Unity log · ⏱ 57m 19s build + 9m 6s queue · Download .zip · .zip via S3
Mac GitHub job · Unity Cloud #2 · Unity log · ⏱ 1h 28m build + 3m 1s queue · Download .zip · .zip via S3

Lint

Warnings count reduced: 11910 => 11903

Warnings/errors in files changed by this PR (12)
Assets/DCL/PluginSystem/Global/LandscapeSettings.cs:16  CSharpWarnings::CS8618  Non-nullable field 'gpuiShaderBindings' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/PluginSystem/Global/LandscapeSettings.cs:15  CSharpWarnings::CS8618  Non-nullable field 'landscapeData' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/PluginSystem/Global/LandscapeSettings.cs:14  CSharpWarnings::CS8618  Non-nullable field 'realmPartitionSettings' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/PluginSystem/Global/LandscapePlugin.cs:43  CSharpWarnings::CS8618  Non-nullable field 'realmPartitionSettings' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/PluginSystem/Global/LandscapeSettings.cs:16  InconsistentNaming  Name 'gpuiShaderBindings' does not match rule 'members_should_be_pascal_case'. Suggested name is 'GpuiShaderBindings'.
Assets/DCL/PluginSystem/Global/LandscapeSettings.cs:15  InconsistentNaming  Name 'landscapeData' does not match rule 'members_should_be_pascal_case'. Suggested name is 'LandscapeData'.
Assets/DCL/PluginSystem/Global/LandscapeSettings.cs:14  InconsistentNaming  Name 'realmPartitionSettings' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RealmPartitionSettings'.
Assets/DCL/PluginSystem/Global/LandscapeSettings.cs:1  RedundantUsingDirective  Using directive is not required by the code and can be safely removed
Assets/DCL/PluginSystem/Global/LandscapeSettings.cs:6  RedundantUsingDirective  Using directive is not required by the code and can be safely removed
Assets/DCL/Landscape/Settings/LandscapeData.cs:23  UnusedAutoPropertyAccessor.Local  Auto-property accessor 'GrassIndirectRenderer.set' is never used
Assets/DCL/Landscape/Settings/LandscapeData.cs:21  UnusedAutoPropertyAccessor.Local  Auto-property accessor 'GroundMaterial.set' is never used
Assets/DCL/Landscape/Settings/LandscapeData.cs:54  UnusedMember.Local  Method 'OnEnable' is never used

Lint run · full InspectCode report · took 23m 51s

Tests

All Unity tests passed ✅

TESTS SUITE Result Passed Failed Skipped Tests time Job time
EditMode ✅ Passed 25962 0 13 4m 35s 32m 1s
PlayMode ✅ Passed 248 0 37 45s 9m 56s

Tests time sums the test cases; Job time is the job's wall clock including checkout, licensing and asset import.

Slowest tests
  • [editmode] 24.7s DCL.Tests.Editor.ValidationTests.CheckUnityObjectsForMissingReferences
  • [editmode] 16.5s DCL.AuthenticationScreenFlow.Tests.ProfileFetchingAuthStateShould.CancelStalledFetchOnTimeout
  • [editmode] 10.0s DCL.Notifications.Tests.NotificationsRequestControllerShould.ReuseSingleListInstanceAcrossPollIterations
  • [editmode] 9.7s DCL.Tests.Editor.ValidationTests.CheckForDebugUsage
  • [editmode] 7.8s DCL.Tests.Editor.ValidationTests.SettingsAreValid
  • [editmode] 5.0s DCL.Friends.Tests.FriendsConnectivityStatusTrackerShould.RaiseOnlineEventWhenSameStatusIsRebroadcastAfterReset
  • [editmode] 5.0s CrdtEcsBridge.WorldSynchronizer.Tests.CrdtWorldSynchronizerShould.ThrowIfSyncBufferIsAlreadyRented
  • [editmode] 4.2s SceneRunner.Tests.SceneFacadeShould.ContinueUpdateLoopOnBackgroundThread(20,4000)
  • [editmode] 4.1s SceneRunner.Tests.SceneFacadeShould.ContinueUpdateLoopOnBackgroundThread(180,4000)
  • [editmode] 4.1s SceneRunner.Tests.SceneFacadeShould.ContinueUpdateLoopOnBackgroundThread(10,4000)
  • [playmode] 4.4s Global.Tests.PlayMode.CubeWaveSceneShould.EmitECSComponents
  • [playmode] 3.1s DCL.SDKComponents.Tween.Tests.TweenUpdaterSystemShould.ContinuousTweensRunIndefinitelyWhenDurationIsZero
  • [playmode] 2.4s DCL.SDKComponents.Tween.Tests.TweenUpdaterSystemShould.RotateContinuousCompletesAfterDuration
  • [playmode] 2.3s DCL.SDKComponents.Tween.Tests.TweenSequenceSystemShould.TextureMoveSequenceUpdatesMaterial
  • [playmode] 2.3s DCL.SDKComponents.Tween.Tests.TweenUpdaterSystemShould.MoveContinuousMovesAndCompletesAfterDuration
  • [playmode] 2.3s DCL.SDKComponents.Tween.Tests.TweenSequenceSystemShould.TweenSequenceWithoutLoopCompletesOnce
  • [playmode] 2.2s DCL.SDKComponents.Tween.Tests.TweenUpdaterSystemShould.TextureMoveContinuousOffsetCompletesAndUpdatesMaterial
  • [playmode] 1.7s DCL.SDKComponents.Tween.Tests.TweenSequenceSystemShould.TweenSequenceWithMoveRotateScaleWithOmittedScale_ResolvesScaleFromCurrentTransform
  • [playmode] 1.6s DCL.SDKComponents.Tween.Tests.TweenUpdaterSystemShould.RotateContinuousAroundXAxisRotatesAroundXNotZ
  • [playmode] 1.6s DCL.SDKComponents.Tween.Tests.TweenSequenceSystemShould.TweenSequenceWithMultipleTweens

Full report: run summary · results + editor logs: editmode · playmode

Performance

🏁 Bare-metal benchmark finished — run #35020229630.

Full report

PR #10111, run #35020229630

Overall: 🟢 GPU 1% worst improved on Intel Core i5

Builds: Windows change, Windows baseline, macOS change, macOS baseline

How to read this table
  • Each build is measured 3 times, interleaved with the other build (change, baseline, change, baseline, ...) in the same session, so both see the same world content and machine state. The values are the median, and (min–max) is the lowest and highest of those runs.
  • Δ is Change minus Baseline (a negative Δ means Change is faster).
  • 🟢 faster / 🔴 slower — a difference that passed every check: the runs are fully separated (every run of one build faster than every run of the other), and the median difference is at least 3% and at least 0.5 ms.
  • ⚪ within noise — the builds' runs overlap, or the difference is tiny; it cannot be told apart from random variation. Treat it as no change.
  • — informational — the 0.1% worst metrics average only the few worst frames of a run, so a single OS hiccup swings them by a lot; they are shown for context and never earn a verdict.
  • ⚠️ no verdict — the two builds' sessions were not comparable (very different sample counts, or too few usable runs), so no conclusion is drawn from them.
  • Exceptions per run — the average number of exceptions in a run's log, not counting teardown ones logged while the app quits. Flagged only on a difference of at least 2 per run and 1.5× the other build; exception kinds the baseline never threw are called out under the table. The Exception breakdown groups all of them by the explorer's report category and exception type (as totals across the runs).
  • A run that logged unusually many exceptions (at least 10 and 5× the median of its build's runs — e.g. a service was down during it) is excluded from all numbers and called out under the table.
  • The Overall line at the top only reacts to a metric that moved on two or more machines, or by 10% or more on one — a single modest 🟢/🔴 cell can still be a statistical fluke.

Apple M1

Metric Baseline Change Δ Result
Samples 3955 (×3) 4051 (×3)
CPU average 22.6 ms (22.5–23.3) 22.2 ms (21.6–22.4) -0.5 ms ⚪ within noise
CPU 1% worst 232.3 ms (231.7–234.2) 230.6 ms (229.7–231.9) -1.8 ms ⚪ within noise
CPU 0.1% worst 239.5 ms (236.9–241.7) 237.4 ms (236.9–239.2) -2.1 ms — informational
GPU average 35.9 ms (34.6–35.9) 34.2 ms (33.7–36.1) -1.6 ms ⚪ within noise
GPU 1% worst 45.5 ms (44.4–47.0) 44.2 ms (43.0–45.9) -1.2 ms ⚪ within noise
GPU 0.1% worst 46.4 ms (45.6–47.6) 45.0 ms (43.7–46.8) -1.4 ms — informational
Exceptions per run 0 0 0 ⚪ no significant change

Intel Core i5

Metric Baseline Change Δ Result
Samples 4163 (×3) 4312 (×3)
CPU average 21.4 ms (20.0–22.6) 20.7 ms (15.6–21.0) -0.7 ms ⚪ within noise
CPU 1% worst 390.6 ms (335.3–432.4) 347.7 ms (234.9–358.8) -42.9 ms ⚪ within noise
CPU 0.1% worst 425.1 ms (386.8–465.4) 398.7 ms (283.6–448.2) -26.4 ms — informational
GPU average 14.1 ms (12.9–14.6) 13.3 ms (8.9–13.5) -0.9 ms ⚪ within noise
GPU 1% worst 282.5 ms (247.4–287.2) 197.9 ms (107.7–229.7) -84.6 ms 🟢 30% faster
GPU 0.1% worst 423.8 ms (387.6–467.5) 399.3 ms (276.8–436.1) -24.5 ms — informational
Exceptions per run 0 0 0 ⚪ no significant change

Automation

On demand — comment /visual-tests on this PR to run the visual regression suite against its build.

@decentraland-bot
decentraland-bot self-requested a review September 15, 2026 18:48

@decentraland-bot decentraland-bot 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.

Summary

What: Pre-loads GPUIShaderBindings asynchronously via the project's assetsProvisioner and assigns it to GPUIShaderBindings.Instance before the GPUI rendering system is created, eliminating a synchronous WaitForCompletion() call that blocks the main thread during startup.


Step 2 — Root-cause check: PASS ✅

The fix addresses the actual root cause. The ANR occurs because GPUIShaderBindings.Instance getter internally calls Addressables.LoadAssetAsync<T>().WaitForCompletion(), which synchronously completes ALL in-flight async loads — not just the requested asset. By pre-loading and assigning the asset before any code reads Instance, the synchronous path never executes. This is a genuine bypass of the blocking load, not a symptom relocation.

Step 3 — Design & integration: PASS ✅

Owner search: GPUICoreAPI.RegisterRenderer — the call that triggers GPUIRenderingSystem.Awake() → GPUIShaderBindings.Instance getter — is called only in LandscapePlugin.InitializeAsync() (line 97). All other GPUICoreAPI usage is in TreeData.cs for buffer/instance-count operations that don't create the rendering system. GPUIRenderingSystem has zero hits outside the vendored GPUI package. GPUI is exclusively Landscape-scoped. LandscapePlugin is the natural and sole owner of this preload.

enableLandscape guard: The fix is placed after if (!enableLandscape) return; (line 79). When landscape is disabled, RegisterRenderer is also never called, so no code path triggers the synchronous load. Correct.

Static singleton write: GPUIShaderBindings.Instance is a third-party library static (GPUInstancerPro), not a project container. CLAUDE.md's "plugins read from containers, don't write" rule targets the project's own StaticContainer/DynamicWorldContainer graph. Setting a vendor singleton that the vendor's own init reads is an acceptable pragmatic workaround — there is no project container to hoist it into.

No new long-lived units introduced. The change adds a field to an existing plugin, a typed asset reference (standard pattern), and a serialized settings field.

Step 4 — Member audit

No new public properties or accessors on existing types. GPUIShaderBindingsRef is a new public type — a trivial AssetReferenceT<GPUIShaderBindings> subclass following the established LandscapeDataRef pattern in the same file. Used only by Unity serialization.

Step 5 — Findings

One inline finding (P2, see inline comment).

Categories checked clean:

  • R1 (alloc): No hot-path allocations — runs once during plugin init ✅
  • R2 (LINQ): No LINQ ✅
  • R3 (struct/class): No struct-class mixing ✅
  • R4 (ECS): No ECS queries or systems modified ✅
  • R5 (Entity by-ref): N/A ✅
  • R6 (teardown): gpuiShaderBindings follows the same undisposed ProvidedAsset<T> pattern as landscapeData — pre-existing, not introduced by this PR ✅
  • R7 (nullability): Annotations consistent ✅
  • R8 (root cause): Genuine fix, not symptom relocation ✅
  • R9 (logging): No new logging ✅
  • R10 (catch-scope): No try/catch ✅
  • R11 (cancellation): CancellationToken properly threaded through ProvideMainAssetAsync ✅
  • R12 (abstraction): No new interfaces or wrappers ✅
  • R13 (reuse): Uses existing assetsProvisioner and ProvidedAsset<T> patterns ✅
  • R14–R15 (contract/dead weight): N/A ✅
  • R16–R17 (naming): Names follow conventions (GPUIShaderBindingsRef matches the *Ref family) ✅
  • R18 (magic numbers): None ✅
  • R19 (file hygiene): GPUIShaderBindingsRef co-located with LandscapeDataRef follows existing convention ✅
  • R20 (.NET idioms): Field can't be readonly — assigned in async init, not constructor ✅
  • R21 (MVC): N/A ✅
  • R22 (tests): No regression test. The ANR depends on timing of concurrent Addressables loads — inherently non-deterministic. LandscapePlugin has no existing test coverage. Manual test plan covers functional verification ✅
  • R24 (scope): All files directly related to the stated change ✅

Lint pre-flight: scripts/lint/custom-rules.sh not present on this branch — rules checked by hand.

Step 6 — Complexity: SIMPLE

Touches 4 files with ~20 lines of meaningful changes. Adds an async asset preload to an existing plugin's initialization — follows existing patterns exactly. Does not modify ECS systems, async flow patterns, or plugin registration.

Step 7 — QA: YES

Changes runtime code that ships in the build. Affects GPUI tree rendering — could impact what the user sees.

Step 8 — Non-blocking warnings

None. Main scene not modified.

Merge gates

  • R25 (comments): No outstanding review comments from prior rounds. This is the first review.
  • R26 (QA): Awaiting QA. PR affects rendering (landscape trees). Test instructions in PR body: verify trees render with same density/LOD, verify no GPUI warnings in log. Needs verification on both Windows and Mac.

REVIEW_RESULT: PASS ✅
COMPLEXITY: SIMPLE
COMPLEXITY_REASON: Adds an async asset preload to LandscapePlugin's existing initialization flow — no ECS, no new systems, no architectural changes.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub

Comment thread Explorer/Assets/DCL/PluginSystem/Global/LandscapePlugin.cs
@decentraland-bot
decentraland-bot self-requested a review September 15, 2026 18:55

@decentraland-bot decentraland-bot 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.

Summary

What: Pre-loads GPUIShaderBindings asynchronously via the project's assetsProvisioner and assigns it to GPUIShaderBindings.Instance before the GPUI rendering system is created, eliminating a synchronous WaitForCompletion() call that blocks the main thread during startup.

Cleanup (commit 2): member reordering in LandscapeData, removal of unused OnDetailDistanceChanged event, [UsedImplicitly] on enum values, null! initializers on serialized fields.


Step 2 — Root-cause check: PASS ✅

The fix addresses the actual root cause. The ANR occurs because GPUIShaderBindings.Instance getter calls Addressables.LoadAssetAsync<T>().WaitForCompletion(), which synchronously completes ALL in-flight async loads. By pre-loading and assigning the asset before GPUICoreAPI.RegisterRenderer triggers the getter, the synchronous path never executes. This is a genuine root-cause fix, not a symptom relocation.

Step 3 — Design & integration: PASS ✅

Owner search: GPUICoreAPI.RegisterRenderer — the call that triggers GPUIRenderingSystem.Awake() → GPUIShaderBindings.Instance getter — is called first in LandscapePlugin.InitializeAsync() (line 97). GPUInstancingPlugin does not call RegisterRenderer; it only passes LandscapeData to GPUInstancingService. LandscapePlugin is the correct and sole owner of this preload.

enableLandscape guard: The fix is placed after if (!enableLandscape) return; (line 79). When landscape is disabled, RegisterRenderer is never called, so the synchronous load never triggers. Correct placement.

Static singleton write: GPUIShaderBindings.Instance is a third-party library static (GPUInstancerPro), not a project container. CLAUDE.md's "plugins read from containers, don't write" rule targets project containers. Setting a vendor singleton is an acceptable pragmatic workaround.

No new long-lived units introduced. The change adds a field to an existing plugin, a typed asset reference (standard *Ref pattern), and a serialized settings field.

Teardown trace: gpuiShaderBindings stores a ProvidedAsset<GPUIShaderBindings> holding an Addressables handle. The existing landscapeData field of the same type is also never explicitly disposed — both are "main assets" that outlive any single plugin. assetsProvisioner manages their lifecycle. Consistent with the established pattern.

Step 4 — Member audit

  • GpuiShaderBindingsRef — typed Addressable reference following LandscapeDataRef pattern. Used by Unity serialization. No audit issue.
  • gpuiShaderBindings (field) — .Value used once to set the static Instance, but the field must persist to keep the Addressables handle alive (prevents asset unloading). Follows the existing landscapeData pattern.
  • OnDetailDistanceChanged removed — confirmed zero consumers via repo-wide search. Dead code removal.

Step 5 — Findings

One inline finding (P1 R23). See inline comment.

Categories checked clean:

  • R1 (alloc): No hot-path allocations — runs once during plugin init ✅
  • R2 (LINQ): No LINQ ✅
  • R4 (ECS): No ECS queries or systems modified ✅
  • R6 (teardown): gpuiShaderBindings follows the same undisposed ProvidedAsset<T> pattern as landscapeData — pre-existing convention, not introduced by this PR ✅
  • R7 (nullability): null! initializers on serialized fields are correct (Unity-managed). Annotations consistent ✅
  • R8 (root cause): Genuine fix, not symptom relocation ✅
  • R10 (catch-scope): No try/catch ✅
  • R11 (cancellation): CancellationToken properly threaded through ProvideMainAssetAsync ✅
  • R12 (abstraction): No new interfaces or wrappers ✅
  • R13 (reuse): Uses existing assetsProvisioner and ProvidedAsset<T> patterns ✅
  • R15 (dead weight): Commented-out code (lines 41, 61, 119, 126) is pre-existing, not added by this PR ✅
  • R17 (naming families): GpuiShaderBindingsRef follows *Ref suffix convention (LandscapeDataRef) ✅
  • R18 (magic numbers): None ✅
  • R19 (file hygiene): GpuiShaderBindingsRef co-located with LandscapeDataRef — follows existing convention ✅
  • R20 (.NET idioms): Field can't be readonly — assigned in async init, not constructor ✅
  • R22 (tests): No regression test. LandscapePlugin has zero existing test coverage. The ANR depends on timing of concurrent Addressables loads — inherently non-deterministic. The fix is two sequential lines with trivially correct ordering ✅
  • R24 (scope): Lint cleanup isolated in its own commit, same files, non-behavioral. Acceptable ✅

Lint pre-flight: scripts/lint/custom-rules.sh not present on this branch — rules checked by hand above.

Merge gates

  • R25 (comments): 1 open inline thread from the prior review round (P1 R23 at LandscapePlugin.cs:85 — comment narrates external behavior). Must be addressed before merge.
  • R26 (QA): Awaiting QA. PR affects rendering (landscape trees via GPUI). Test instructions in PR body cover tree rendering and GPUI log warnings. Needs verification on both Windows and Mac.

REVIEW_RESULT: PASS ✅
COMPLEXITY: SIMPLE
COMPLEXITY_REASON: Adds an async asset preload to LandscapePlugin's existing initialization flow — no ECS, no new systems, no architectural changes.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub

Comment thread Explorer/Assets/DCL/PluginSystem/Global/LandscapePlugin.cs

@github-actions github-actions Bot 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.

Auto-approved based on Jarvis review — simple fix/chore with no blocking issues. QA approval is still required.

@Ludmilafantaniella Ludmilafantaniella 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.

QA Update

Verified on Windows and Mac. Checked landscape/tree rendering around Genesis Plaza, at the Genesis City borders, Casa Roustan and in ryumi.dcl.eth, rowan.dcl.eth and cozyfarm.dcl.eth worlds.

Trees render with the same density and LOD distances as before, no missing shader bindings or replacement material warnings in the log.

Approved from QA side. ✅

10111-evi.mp4

✅Smoke test performed:

  • ✔️ Backpack and wearables in world
  • ✔️ Emotes in world and in backpack
  • ✔️ Teleport with map/coordinates/Jump In
  • ✔️ Chat and multiplayer
  • ✔️ Profile card
  • ✔️ Skybox

@lorux0
lorux0 merged commit a6de62e into dev Sep 16, 2026
38 of 46 checks passed
@lorux0
lorux0 deleted the fix/anr-gpu-instancer branch September 16, 2026 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants