stride3d/stride · error · InvalidOperationException

This code must be executed in the game thread.

Error message

This code must be executed in the game thread.

What it means

EnsureGameAccess(inGameThread: true) asserts that the current thread is the dedicated sceneGameThread that owns the game form/scene. Game-side objects are not thread-safe, so any access to them from another thread would corrupt state; the method throws InvalidOperationException when called from the wrong thread.

Solutions

  1. Wrap the access in Controller.InvokeAsync(() => ...) so it executes on the game thread.
  2. If running on the game thread is impossible, marshal the data out via InvokeAsync and operate on copies.
  3. Add EnsureGameAccess() calls early in your own game-side helpers to catch threading mistakes close to the source.

Example fix

// before
var part = controller.FindGameSidePart(partId); // wrong thread
// after
var part = await controller.InvokeAsync(() => controller.FindGameSidePart(partId));
Defensive patterns

Strategy: validation

Validate before calling

if (!controller.CheckGameAccess())
    await controller.InvokeAsync(() => DoGameSideWork());
else
    DoGameSideWork();

Try / catch

try { DoGameSideWork(); }
catch (InvalidOperationException ex) when (ex.Message.Contains("game thread")) { controller.InvokeAsync(DoGameSideWork); }

Prevention

When it happens

Trigger: Calling game-side APIs such as FindGameSidePart or EnsureAccess (anything that routes through EnsureGameAccess) directly from the UI thread or a worker thread without wrapping the call in Controller.InvokeAsync.

Common situations: Plugin code touching the live scene from a WPF event handler; background tasks that computed a change and then tried to apply it directly to game objects; forgetting the InvokeAsync indirection the change propagator normally uses.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/41dcf012409a89ae. Report an issue: GitHub.

Appendix: source

Thrown at sources/editor/Stride.Assets.Presentation/AssetEditors/GameEditor/Services/EditorGameController.cs:361

        /// <summary>
        /// Verifies that the current thread is the game thread.
        /// </summary>
        /// <returns><c>True</c> if the current thread is the game thread, <c>False</c> otherwise.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool CheckGameAccess()
        {
            return Thread.CurrentThread == sceneGameThread;
        }

        /// <summary>
        /// Ensures that the current thread is the game thread. This method will throw an exception if it is not the case.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void EnsureGameAccess(bool inGameThread = true)
        {
            if (inGameThread && Thread.CurrentThread != sceneGameThread)
                throw new InvalidOperationException("This code must be executed in the game thread.");
            if (!inGameThread && Thread.CurrentThread == sceneGameThread)
                throw new InvalidOperationException("This code must not be executed in the game thread.");
        }

        /// <inheritdoc/>
        bool IDispatcherService.CheckAccess() => CheckGameAccess();

        /// <inheritdoc/>
        void IDispatcherService.EnsureAccess(bool inDispatcherThread) => EnsureGameAccess(inDispatcherThread);

        /// <inheritdoc/>
        void IDispatcherService.Invoke(Action callback)
        {
            throw new NotSupportedException();
        }

        /// <inheritdoc/>
        TResult IDispatcherService.Invoke<TResult>(Func<TResult> callback)

View on GitHub (pinned to 96fad776d2)