Skip to content

fix: smart wearable crash - #9755

Merged
eordano merged 2 commits into
devfrom
fix/smart-wearable-crash
Aug 14, 2026
Merged

fix: smart wearable crash#9755
eordano merged 2 commits into
devfrom
fix/smart-wearable-crash

Conversation

@lorux0

@lorux0 lorux0 commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

What does this PR change?

Fixes a crash on startup where the game could hard-crash right after loading finished. Closes #9753 (Sentry UNITY-EXPLORER-PM0).

The Smart Wearable system starts the scenes for your equipped smart wearables as soon as loading completes. It assumed the player profile was always ready by then, which isn't guaranteed — when it wasn't, the game crashed.

Changes:

  • The system now checks the profile is available before running, and retries on the next completed loading instead of giving up for the session.
  • Smart wearable background work is now properly cancelled on logout and when the system shuts down, so it can't leak into the next session.
  • Changing scenes repeatedly no longer stacks up overlapping runs — the previous one is dropped.

Test Instructions

Expected result: The game loads into world without crashing, and equipped smart wearables start as usual.

Also worth checking: log out and back in, and walk across a few scene borders with a smart wearable equipped — no crashes, and the wearable's scene still loads.

@lorux0
lorux0 requested review from a team as code owners August 14, 2026 15:47
@decentraland-bot
decentraland-bot self-requested a review August 14, 2026 15:48
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

🚦 CI Status

Build

Windows and Mac build successful in Unity Cloud! You can find a link to the downloadable artifact below.

Name Link
Commit dbb4ec9
Logs https://github.com/decentraland/unity-explorer/actions/runs/31816259478
Download Windows https://github.com/decentraland/unity-explorer/suites/86315849823/artifacts/9226158371
Download Windows S3 https://explorer-artifacts.decentraland.org/@dcl/unity-explorer/branch/fix/smart-wearable-crash/pr-25152-dbb4ec9/Decentraland_windows64.zip
Download Mac https://github.com/decentraland/unity-explorer/suites/86315849823/artifacts/9226167093
Download Mac S3 https://explorer-artifacts.decentraland.org/@dcl/unity-explorer/branch/fix/smart-wearable-crash/pr-25152-dbb4ec9/Decentraland_macos.zip
Built on 2026-08-14T16:23:51Z

Lint

Warnings count reduced: 13156 => 13155

Warnings/errors in files changed by this PR (3)
Assets/DCL/SmartWearables/Systems/SmartWearableSystem.cs:151  CSharpWarnings::CS8602  Dereference of a possibly null reference
Assets/DCL/SmartWearables/Systems/SmartWearableSystem.cs:269  CSharpWarnings::CS8602  Dereference of a possibly null reference
Assets/DCL/SmartWearables/Systems/SmartWearableSystem.cs:337  CSharpWarnings::CS8602  Dereference of a possibly null reference

Tests

All Unity tests passed ✅

TESTS SUITE Result Passed Failed Skipped
EditMode ✅ Passed 24971 0 13
PlayMode ✅ Passed 236 0 36

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

Step 2 — Root-cause check

PASS. The crash (Sentry UNITY-EXPLORER-PM0, #9753) occurred because RunScenesForEquippedWearablesAsync called World.CachePlayer() which throws when the player entity does not exist yet. The player profile is not guaranteed to be resolved by the time LoadingStatus.LoadingStage.Completed fires. The fix introduces TryGetPlayerProfile to check availability before accessing the profile, and defers the start-up to the next completed-loading transition when the profile is absent. This addresses the root cause — the incorrect assumption that the profile was always available at loading completion.

Step 3 — Design & integration

PASS. No new long-lived units introduced. All changes are within the existing SmartWearableSystem — the system already owns the smart-wearable lifecycle (loading, unloading, authorization, scene-change handling) and these changes add proper guards and cancellation scoping to that lifecycle.

New members:

  • CachePlayerEntityOrNull (WorldExtensions.cs) — thin utility that complements the existing CachePlayer() method, using the existing GetSingleInstanceEntityOrNull overload with strict: false. Not a lifecycle manager; no ownership concerns.
  • sessionCts, runScenesCts — CancellationTokenSources scoped to the session and per-scene-run respectively, alongside the pre-existing outfitEquipCts. All three have distinct cancellation semantics and consolidating any pair would cause unintended cross-cancellation.

Teardown / consumption trace — all subscriptions and CTS accounted for:

Opener Teardown location
EquipWearableEvent += (L104) OnDispose L114
UnEquipWearableEvent += (L105) OnDispose L115
EquipOutfitEvent += (L106) OnDispose L116
PortableExperienceUnloaded += (L107) OnDispose L117
CurrentStage.OnUpdate += (L108) OnDispose L118 · OnLoadingStatusChanged L412 · OnIdentityCleared L500
CurrentScene.OnUpdate += (L413) OnDispose L119 · OnIdentityCleared L499
OnIdentityCleared += (L109) OnDispose L120
outfitEquipCts field OnDispose L122 · OnIdentityCleared L484
sessionCts field OnDispose L123 · OnIdentityCleared L485
runScenesCts field OnDispose L124 · OnIdentityCleared L486

All openers have corresponding teardown. The defensive -= before += in OnIdentityCleared (L499-501) correctly prevents double-subscription when OnLoadingStatusChanged was never consumed (profile-unavailable case). This also fixes a pre-existing bug where the old code always did += without a preceding -=, risking double-subscription if the handler had not yet unsubscribed itself.

Step 4 — Member audit

Member Consumers Verdict
CachePlayerEntityOrNull (WorldExtensions) TryGetPlayerProfile (1) Single-use today but natural complement to CachePlayer() in a shared utility; legitimate for reuse
TryGetPlayerProfile (private) HandleSceneChange (L377), OnLoadingStatusChanged (L405) 2 consumers; well-scoped helper
sessionCts (private field) OnEquipWearable, OnUnEquipWearable, OnDispose, OnIdentityCleared 4 sites; distinct session-scoped lifetime
runScenesCts (private field) HandleSceneChange, OnLoadingStatusChanged, OnDispose, OnIdentityCleared 4 sites; distinct per-run lifetime

No single-use-merge, absent≠false, re-derive, or redundant-guard issues.

Step 5 — Line-level review

No P0 or P1 issues found.

No P2 issues requiring suggestion blocks. Minor non-blocking observations:

  • Explicit ct.IsCancellationRequested checks after awaits (e.g. L139, L474): Some are technically redundant given the outer catch (OperationCanceledException). However, the checks at L160 (guarding World.Add after a thread hop) and L462 (guarding cache mutations after authorization) are genuinely justified — they prevent side effects that must not execute after session end. The pattern is applied consistently across the file and serves as documentation of cancellation intent. Not flagging.

  • Deferred retry gap (L399-416): If the loading stage reaches Completed but the profile is absent, the handler stays subscribed and waits for the next Completed transition. If the profile becomes available shortly after (but the stage stays at Completed), smart wearables won't start until the next scene change triggers HandleSceneChange. This is strictly better than the pre-existing crash, and the window is very short (the profile is expected to arrive around the same time as loading completion). Worth noting for future hardening — subscribing to the profile's own availability signal (if one exists) would close this gap — but not blocking.

Security review: No security issues found. No secrets, no injection surfaces, no auth/authz changes, no sensitive data in logs.

Step 6 — Complexity

COMPLEX.

Step 7 — QA assessment

QA_REQUIRED: YES. Changes affect runtime smart-wearable loading/unloading — user-facing behavior on login, logout, and scene transitions.

Step 8 — Non-blocking warnings

None. Main scene not modified.

Step 9 — Verdict

REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Modifies async/UniTask cancellation flows, CTS lifecycle management, and event subscription patterns in SmartWearableSystem
QA_REQUIRED: YES


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

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Jarvis reviewed this PR and found no blocking issues, but assessed it as complex — human DEV review is still required before merging.

@decentraland-bot

Copy link
Copy Markdown
Contributor

PR #9755, run #31819170782

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

How to read this table
  • Each build is measured 3 times. The values are the median, and (min–max) is the lowest and highest of those runs — a wide range means the metric is noisy and small differences are not trustworthy.
  • Δ is Change minus Baseline (a negative Δ means Change is faster).
  • 🟢 faster / 🔴 slower — a real difference: larger than both 3% and the run-to-run range.
  • ⚪ within noise — the difference is smaller than how much the build varies between its own runs, so it cannot be told apart from random variation. Treat it as no change.
  • Exceptions per run — the average number of exceptions in a run's log; more than the baseline is flagged 🔴 even when frame times look fine. The Exception breakdown under each table groups 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.

Intel Core i5

Metric Baseline Change Δ Result
Samples 2287 (×3) 2284 (×3)
CPU average 39.2 ms (36.8–39.3) 39.1 ms (38.6–39.8) -0.1 ms ⚪ within noise
CPU 1% worst 378.9 ms (366.5–380.5) 387.8 ms (358.0–389.1) 8.9 ms ⚪ within noise
CPU 0.1% worst 395.9 ms (389.0–396.8) 403.5 ms (375.5–410.9) 7.6 ms ⚪ within noise
GPU average 9.5 ms (9.4–9.6) 8.3 ms (8.3–8.4) -1.2 ms 🟢 12% faster
GPU 1% worst 40.2 ms (39.7–40.3) 20.2 ms (19.8–20.4) -20.0 ms 🟢 50% faster
GPU 0.1% worst 48.7 ms (44.9–49.0) 21.4 ms (20.4–21.8) -27.2 ms 🟢 56% faster
Exceptions per run 66 66 0 ⚪ none new
Exception breakdown
Exception Baseline (3 runs) Change (3 runs)
[UI] DllNotFoundException 192 192
[ENGINE] NullReferenceException 3 3
[ENGINE] ObjectDisposedException 3 3

Apple M1

Metric Baseline Change Δ Result
Samples 4021 (×3) 3886 (×3)
CPU average 22.3 ms (21.7–23.2) 23.0 ms (22.3–23.3) 0.7 ms ⚪ within noise
CPU 1% worst 230.5 ms (224.7–232.1) 217.9 ms (215.6–224.2) -12.6 ms 🟢 5% faster
CPU 0.1% worst 238.5 ms (234.0–239.6) 229.1 ms (224.3–229.5) -9.4 ms 🟢 4% faster
GPU average 6.9 ms (2.7–7.2) 5.2 ms (4.2–17.5) -1.8 ms ⚪ within noise
GPU 1% worst 35.3 ms (33.7–37.2) 36.3 ms (35.6–37.9) 1.0 ms ⚪ within noise
GPU 0.1% worst 36.3 ms (35.1–38.0) 38.4 ms (36.9–39.8) 2.1 ms ⚪ within noise
Exceptions per run 0 0 0 ⚪ none new

@balducciv

Copy link
Copy Markdown

PR reviewed and approved by QA on both platforms following the PR test instructions. ✅
Smoke test performed on Windows and Mac to verify the normal flow is working as expected. ✅

Build: v0.168.0-alpha-fix/smart-wearable-crash-dbb4ec9 (commit dbb4ec9, matches PR head)
OS: Windows 11 / macOS (M-series)

Test results:

  • Fresh login (profile-loading race path) — reaches Loading stage: Completed without crash, both platforms
  • Teleport across scenes/realms — reaches Completed without crash, no stacked/duplicate scene runs (Windows: 7 scene loads; Mac: 8 scene loads across 4 distinct scenes)
  • Realm change — reaches Completed without crash, both platforms
  • Equipped smart wearable's scene started as expected — confirmed visually in-game
  • No crashes on repeated scene-border crossings

Unrelated errors noted (do not affect verdict):

  • ObjectDisposedException: The CancellationTokenSource has been disposed — occurs during shutdown, after [ExitUtils] Exit requested, in SidebarController/MVCManager and LiveKitMovementMessageBus/DynamicWorldContainer disposal paths on both platforms — known shutdown-phase noise, unrelated to SmartWearableSystem

Verdict: PASS ✅

Player 9755.log

Player 9755.log
Player 9755 2.log

14.08.2026_15.12.55_REC.windows.9755.mp4
14.08.2026_15.33.28_REC.-.9755.mac.mp4

@eordano
eordano merged commit c08a72c into dev Aug 14, 2026
28 of 39 checks passed
@eordano
eordano deleted the fix/smart-wearable-crash branch August 14, 2026 20:51
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.

U3CRunScenesForEquippedWearablesAsyncU3Ed__31_MoveNext_m480660509868724A00763C74561237F8234DB548

4 participants