Skip to content

fix(misc): scattered null/dispose/cancellation robustness across auth, map, plugins, init#9378

Draft
eordano wants to merge 1 commit into
devfrom
pr/core-misc-robustness
Draft

fix(misc): scattered null/dispose/cancellation robustness across auth, map, plugins, init#9378
eordano wants to merge 1 commit into
devfrom
pr/core-misc-robustness

Conversation

@eordano

@eordano eordano commented Jul 14, 2026

Copy link
Copy Markdown
Member

Pull Request Description

What does this PR change?

A batch of small, scattered robustness fixes — no feature changes. Grouped by theme:

Lifecycle-scoped cancellation (replaces CancellationToken.None in fire-and-forget flows)

  • Auth screen views (LobbyForExistingAccount, LobbyForNewAccount, LoginSelection, ProfileFetching, VerificationDapp, VerificationOTP) now pass destroyCancellationToken to their show/hide animations.
  • InWorldCameraPlugin and LoadSmartWearablePreviewSceneSystem own a CTS cancelled+disposed on teardown; InWorldCameraController and ChatPlugin use their existing lifecycle tokens; plugins unsubscribe their identity-cache event handlers on dispose so no handler can outlive its token source. VoiceChatParticipantEntryPresenter deliberately keeps the passport popup on CancellationToken.None so it survives the pooled entry being recycled.

Null/dispose hardening in Unity teardown paths

  • ContextualImage.OnDisable/OnDestroy guard against already-destroyed image/asset.
  • AvatarBase.SetAnimatorFloat/Int/Trigger early-out when the animator is destroyed.
  • AnnouncementCreationCardView.OnDestroy guards destroyed serialized references and unsubscribes from the same ClipboardManager instance it subscribed to.
  • LoadPlayerAvatarStartupOperation now throws when the self profile resolves to null instead of adding a null profile to the world (which NRE'd every Profile-querying system per frame).
  • RealUserInAppInitializationFlow no longer logs shutdown cancellation as an authentication error.
  • ComputeShaderSkinning guards empty avatar mesh data (vertCount/boneCount == 0): the component is created in an explicit empty state (default buffers, pool-rented materials list) that ComputeSkinning/Dispose no-op on, and zero-length regions are excluded from FixedComputeBufferHandler rent/release and defragmentation remapping.
  • InputPlugin.Dispose disables DCLInput before Reset().

Diagnostics

  • IGetHash (hybrid scenes) logs the actual exception type/message instead of silently swallowing it.

Warning cleanup / Unity 6000.4 API modernization

  • GetInstanceID()GetEntityId(), FindObjectOfTypeFindAnyObjectByType.
  • async methods without awaitUniTask.CompletedTask (map layer controllers).
  • Documented #pragma warning disable CS0162 around the deliberate ENABLE_SDK_OBSERVABLES compile-time branch in SceneFactory.
  • DCLVersion.inEditorVersion returns null only in non-editor builds (#else instead of unreachable code).
  • Serialized fields initialized (null! / Array.Empty) in SkyboxRenderController, SettingsModuleBindings, DebugSettings; no-op event accessors in PrivateVoiceChatCallStatusServiceNull; removed unreachable code in EventListItemView and unused locals in AvatarShapeVisibilitySystemShould.

Test Instructions

Steps (standard run):

metaforge explorer run 9378

Expected result: Client boots, logs in, and runs with no new NREs or ObjectDisposedExceptions — especially during screen transitions and shutdown.

Steps (fresh account):

metaforge account create --clear
metaforge explorer run 9378

Expected result: Full new-account auth flow (login selection → OTP/dapp verification → lobby) shows and hides each screen cleanly; no errors logged when screens are dismissed mid-animation.

Automation (if applicable):

metaforge explorer test XXXX

Prerequisites

  • No special setup — a standard build is enough

Test Steps

  1. Launch and complete login with an existing account; watch the auth screens animate in/out without console errors.
  2. Open the map; toggle category, search-result, and live-event layers; hover the home marker.
  3. Open the in-world camera, toggle the shortcuts info panel, take a screenshot, close the camera.
  4. Open a community card, create an announcement (including pasting text), close the card.
  5. Quit the client and check the log tail: no NREs and no AUTHENTICATION errors caused by shutdown cancellation.

Additional Testing Notes

  • The empty-avatar skinning guard and the null self-profile failure only trigger on abnormal backend responses (profiles endpoint returning no avatars) — they are covered by the new log messages rather than a reproducible manual step.
  • Verify map layer toggling still tracks/untracks markers correctly (the asyncUniTask.CompletedTask conversions must not change behavior).

Quality Checklist

  • Changes have been tested locally
  • Documentation has been updated (if required)
  • Performance impact has been considered
  • For SDK features: Test scene is included

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

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

PR Review: fix(misc): scattered null/dispose/cancellation robustness across auth, map, plugins, init


STEP 2 — Root-cause check

This PR addresses multiple independent robustness issues: fire-and-forget async flows using CancellationToken.None (symptom of missing lifecycle-scoped cancellation), Unity teardown crashes from accessing destroyed objects, swallowed exceptions hiding diagnostic info, and compiler warnings from Unity 6000.4 API deprecations. Each fix targets the actual cause — properly scoping CTS to owner lifetimes, guarding against Unity's destroyed-object state, surfacing exception details, and modernizing API calls. PASS.

STEP 3 — Design & integration

CTS ownership: The two new CancellationTokenSource instances (InWorldCameraPlugin.pluginCts, LoadSmartWearablePreviewSceneSystem.systemCts) are correctly owned by their respective classes and cancelled+disposed in the matching Dispose()/OnDispose() lifecycle methods. The auth screen views correctly use destroyCancellationToken (MonoBehaviour lifecycle) rather than introducing a new CTS. PASS.

AnnouncementCreationCardView.subscribedClipboardManager: Correctly captures the ClipboardManager reference at subscription time so that OnDestroy unsubscribes from the same instance, avoiding the bug where ViewDependencies.ClipboardManager could return a different instance during teardown. PASS.

ComputeShaderSkinning empty guard: See P1 finding below — the returned component has a Buffers struct whose computeSkinningBufferContainer is never initialized, causing a deferred NRE on disposal. FAIL.

STEP 4 — Member audit

No new public properties or accessors added. The subscribedClipboardManager field in AnnouncementCreationCardView is private, single-use (captures the subscription target for matching unsubscribe). PASS.

STEP 5 — Line-level findings

See inline comments below.

STEP 6 — Complexity

Touches 37 files across auth flow, avatar rendering pipeline (compute shader skinning), async/UniTask patterns, plugin disposal, and map layer controllers. Multiple subsystems affected.

STEP 7 — QA assessment

Changes affect runtime code: auth screen transitions, avatar skinning, camera plugin, map layers, input system disposal, voice chat.

STEP 8 — Non-blocking warnings

No Main.unity changes detected.

Security review

No hardcoded secrets, credentials, or API keys found. No auth bypass risks — the CancellationToken.NonedestroyCancellationToken changes improve teardown safety. The exception message in LoadPlayerAvatarStartupOperation contains no sensitive data (no wallet address, no endpoint URL). No security issues found.


REVIEW_RESULT: FAIL ❌
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Touches avatar GPU skinning pipeline, auth screen async flows, plugin lifecycle/disposal, and map layer controllers across 37 files.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by Esteban Ordano (<@U0AJ91P1KBP>) via Slack

@eordano eordano closed this Jul 14, 2026
@eordano
eordano force-pushed the pr/core-misc-robustness branch from 4b280f8 to e15a8cc Compare July 14, 2026 22:37
@eordano eordano reopened this Jul 14, 2026
@eordano eordano closed this Jul 14, 2026
@eordano
eordano force-pushed the pr/core-misc-robustness branch from c93a652 to e15a8cc Compare July 14, 2026 22:50
@eordano eordano reopened this Jul 14, 2026

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

Re-review: fix(misc): scattered null/dispose/cancellation robustness across auth, map, plugins, init


Previous findings — status

  • [P1] ComputeShaderSkinning NRE on disposal ✅ Fixed in e0d6d1cc. Empty guard now uses default(Buffers) (no orphaned ComputeBuffer), DisposeBuffers() is null-safe, materials list is pool-rented via USED_SLOTS_POOL.Get(). Additionally, thorough pipeline guards added: ComputeSkinning early-outs on VertCount == 0, AvatarInstantiatorSystem skips Rent for zero VertCount, FixedComputeBufferHandler.Release guards zero-length slices, and MakeVertsOutBufferDefragmentationSystem skips defrag for zero-length regions. Well-handled.

  • [P2] AvatarBase inconsistent null guards ✅ Fixed in e0d6d1cc. All AvatarAnimator-accessing methods now have null guards: SetPointAtLayerWeight, SetRotationLayerWeight, IsAnimatorInTag, GetAnimatorCurrentStateTag, ResetAnimatorTrigger, ResetState, SetLayerWeight, GetAnimatorBool, SetAnimatorBool, GetAnimatorFloat, ReplaceEmoteAnimation, and StartMaskedLegacyEmote.

New changes in this revision

  • InWorldCameraPlugin.Dispose(): Now unsubscribes OnIdentityChanged before disposing, and uses SafeCancelAndDispose(). Correct ordering — prevents the handler from firing on a cancelled CTS. ✅
  • ChatPlugin.Dispose(): Now unsubscribes OnIdentityCleared and OnIdentityChanged (lines 196-197), matching subscriptions at lines 433-434. Fixes event leak. ✅
  • VoiceChatParticipantEntryPresenter.OnOpenPassport(): Correctly keeps CancellationToken.None with a comment explaining the passport UI must outlive the pooled list entry. ✅
  • LoadSmartWearablePreviewSceneSystem: Uses SafeCancelAndDispose() — consistent with project convention. ✅

STEP 2 — Root-cause check

All fixes target root causes. The empty-avatar pipeline guards are thorough — every touchpoint (skinning, buffer rent, defrag) is protected. PASS.

STEP 3 — Design & integration

CTS ownership is correct. Event subscription symmetry verified in both InWorldCameraPlugin and ChatPlugin. The VoiceChatParticipantEntryPresenter keeps CancellationToken.None with clear justification — the passport action must survive entry recycling. PASS.

STEP 5 — Line-level review

No blocking issues found. All changes are clean and well-commented.

Security review

No security issues found (unchanged from first review).


REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Touches avatar GPU skinning pipeline, auth screen async flows, plugin lifecycle/disposal, and map layer controllers across 40+ files.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by Esteban Ordano (<@U0AJ91P1KBP>) via Slack

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

PR Review — #9378 fix(misc): scattered null/dispose/cancellation robustness

STEP 2 — Root-cause check

PASS. The PR addresses genuine root causes across multiple subsystems:

  • Missing unsubscribes (ChatPlugin, InWorldCameraPlugin): event handlers outlived their owners → real leak.
  • CancellationToken.None in lifecycle-scoped flows: auth views, camera plugin, smart-wearable system all fired detached async that couldn't be cancelled on teardown → real resource waste / post-dispose callbacks.
  • Null self-profile poisoning ECS (LoadPlayerAvatarStartupOperation): profile! on a nullable result passed a null into every Profile-querying system → per-frame NRE.
  • Empty avatar mesh crashing skinning pipeline (ComputeShaderSkinning): zero verts/bones hit FixedComputeBufferHandler.Rent which throws on length 0.
  • Swallowed exceptions in IGetHash: exception type/message discarded → impossible to diagnose hybrid-scene fetch failures.

The AvatarBase null-checks are defensive against Unity's teardown behavior (serialized references become "Unity null" after Destroy). The behavior is real, but the fix is treating a symptom — see P2 finding below.

STEP 3 — Design & integration

PASS. No new long-lived units introduced. All changes modify existing classes at their existing lifecycle boundaries:

  • pluginCts in InWorldCameraPlugin / systemCts in LoadSmartWearablePreviewSceneSystem — scoped to the owning plugin/system, cancelled in Dispose()/OnDispose(). Correct lifecycle pattern.
  • Empty-avatar sentinel (VertCount == 0, default(Buffers)) — defense-in-depth across the GPU skinning pipeline. Guards are placed at every entry point that touches the buffer (ComputeSkinning, DisposeBuffers, Rent-skip, Release-skip, defragmentation-skip). The USED_SLOTS_POOL.Get() on the empty path keeps pool balance. Sound design for an edge case observed in production.
  • subscribedClipboardManager in AnnouncementCreationCardView — captures the exact instance subscribed to, ensuring OnDestroy unsubscribes from the same object even if the static accessor changes. Correct pattern.
  • ChatPlugin identity-cache unsubscribes — subscriptions at lines 430–431 (OnIdentityCleared, OnIdentityChanged) were the only two missing mirrors. Other subscriptions go through pluginScope (disposed) or are explicitly unsubscribed. Complete fix.

TEARDOWN TRACE:

  • InWorldCameraPlugin: ctor subscribes OnIdentityChanged += FetchCameraReelStorageDispose() now unsubscribes ✅ + cancels pluginCts ✅ + existing performed -= ✅.
  • ChatPlugin: InitializeAsync subscribes OnIdentityCleared += and OnIdentityChanged +=Dispose() now unsubscribes both ✅ + existing pluginCts.SafeCancelAndDispose() ✅.
  • LoadSmartWearablePreviewSceneSystem: systemCts created → OnDispose() calls SafeCancelAndDispose() ✅.
  • AnnouncementCreationCardView: Awake() subscribes OnPaste += on captured subscribedClipboardManagerOnDestroy() unsubscribes via same reference ✅.

STEP 4 — Member audit

No new public properties or accessors introduced. The ComputeSkinning early-return for VertCount == 0 is an existing public method gaining a guard — single consumer is the ECS query, count: 1. No single-use / absent-≠-false issues.

STEP 5 — Line-level findings

See inline comments. Summary of findings:

# Severity File Issue
1 P2 AvatarBase.cs Null-checks on non-nullable AvatarAnimator — declaration should be Animator? if null is a legitimate teardown state (CLAUDE.md §11)
2 P2 RealUserInAppInitializationFlow.cs Inconsistent null-safety: result.Error?.State (null-conditional) on L236 but result.Error.AsMessage() (non-conditional) on L239
3 P2 ContextualImage.cs Same pattern as AvatarBase: null-check on non-nullable image field

STEP 6 — Complexity

COMPLEX — touches avatar rendering pipeline (GPU skinning, compute shaders, buffer handlers), async/cancellation flows across auth/plugins/systems, 41 files across 10+ subsystems.

STEP 7 — QA assessment

YES — changes runtime code in auth views, avatar rendering, voice chat, map renderer, in-world camera, input handling, and startup flow.

STEP 8 — Non-blocking warnings

No Main Scene modified. No warnings.


REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Touches avatar GPU skinning pipeline, async/cancellation flows across auth/plugins/systems, and plugin disposal across 41 files.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by unknown (<@unknown>) via Slack

Comment thread Explorer/Assets/DCL/AssetsProvision/Contextual/ContextualImage.cs
@eordano eordano added the force-build Used to trigger a build on draft PR label Jul 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Warnings count reduced: 19134 => 19102

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

force-build Used to trigger a build on draft PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants