Skip to content

fix: off-main-thread null check in ReleaseMemoryTexture/ReleaseMemorySprite - #46

Open
niclas-bolle wants to merge 1 commit into
IvanMurzak:mainfrom
niclas-bolle:fix/offthread-release-null-check
Open

niclas-bolle wants to merge 1 commit into
IvanMurzak:mainfrom
niclas-bolle:fix/offthread-release-null-check

Conversation

@niclas-bolle

@niclas-bolle niclas-bolle commented Oct 16, 2026 •

Copy link
Copy Markdown

Problem

When a Reference<Texture2D> is garbage-collected without Dispose(), ~Reference() runs Dispose() → ImageLoader.ClearMemoryCache(url) on the GC finalizer thread. This throws:

InvalidOperationException: EnsureRunningOnMainThread can only be called from the main thread
UnityEngine.Object.EnsureRunningOnMainThread ()
UnityEngine.Object.GetInstanceID ()
UnityEngine.Object.IsNativeObjectAlive (UnityEngine.Object o)
UnityEngine.Object.CompareBaseObjects (UnityEngine.Object lhs, UnityEngine.Object rhs)
UnityEngine.Object.op_Equality (UnityEngine.Object x, UnityEngine.Object y)
Extensions.Unity.ImageLoader.InternalExtensions.IsNull (UnityEngine.Object obj)
Extensions.Unity.ImageLoader.FutureTexture.ReleaseMemoryTexture (UnityEngine.Texture2D obj, Extensions.Unity.ImageLoader.DebugLevel logLevel)
Extensions.Unity.ImageLoader.Safe.Run[T1,T2] (...)
Extensions.Unity.ImageLoader.Future`1:ClearMemoryCache(String, Action`2, DebugLevel)
Extensions.Unity.ImageLoader.ImageLoader:ClearMemoryCache(String)
Extensions.Unity.ImageLoader.Reference`1:Dispose()
Extensions.Unity.ImageLoader.Reference`1:Finalize()

ReleaseMemoryTexture already handles the off-main-thread case by posting DestroyImmediate to the main thread via UniTask.Post. But the pre-check right before the post calls IsNull(), which uses Unity's overloaded == and throws off the main thread. Safe.Run catches the exception. By then ClearMemoryCache has already removed the entry from the memory cache, so the texture is never destroyed and leaks. ReleaseMemorySprite has the same pre-check.

Fix

In the off-main-thread branch only, the pre-check now uses ReferenceEquals(obj, null). The UniTask.Post callback still runs the full IsNull() check on the main thread before destroying. The main-thread branch is unchanged.

Testing

We run this patch in a production Unity 6 (6000.3) project. Before it, the exception above showed up regularly in the Editor during Play Mode whenever a GC finalized a leaked Reference.

With the patch applied, I checked it deterministically: I created a Reference for a cached texture, dropped it without Dispose(), and forced GC.Collect(); GC.WaitForPendingFinalizers();. There was no exception. Right after the GC the cache entry was gone but the texture was still alive, which shows the destroy was posted from the finalizer thread. The texture was destroyed on the next main-thread frame.

…Sprite

When a Reference is finalized without Dispose(), its finalizer clears the
memory cache from the GC finalizer thread. The off-main-thread branch of
ReleaseMemoryTexture / ReleaseMemorySprite called IsNull(), whose Unity ==
throws "EnsureRunningOnMainThread can only be called from the main thread".
The cache entry was already removed, so the texture was never destroyed.

Use ReferenceEquals for the off-thread pre-check; the UniTask.Post callback
still does the full IsNull() check on the main thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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