stride3d/stride · critical · GraphicsDeviceException

Unexpected error on Present

Error message

Unexpected error on Present (device status: {deviceStatus})

What it means

SwapChainGraphicsPresenter.Present throws GraphicsDeviceException when the DXGI Present call returns a failure HRESULT that is not a recognized device-removed/reset case. The message includes GraphicsDevice.GraphicsDeviceStatus at the moment of failure, and the original COM exception is attached as InnerException. It signals an unexpected presentation failure distinct from the handled device-lost flows.

Solutions

  1. Read the InnerException's HRESULT and deviceStatus in the message to identify the actual failure (e.g. DXGI_ERROR_DEVICE_REMOVED reason)
  2. Handle GraphicsDeviceException in the frame loop, check GraphicsDeviceStatus, and recreate the GraphicsDevice/swap chain on device removal
  3. Update GPU drivers and DirectX runtime; test without overclocking
  4. Check that the window handle is still valid and Present is not called after window/device teardown begins

Example fix

// before
presenter.Present(); // unguarded, crashes app on device removal
// after
try
{
    presenter.Present();
}
catch (GraphicsDeviceException ex)
{
    if (ex.DeviceStatus == GraphicsDeviceStatus.Removed)
        RecreateGraphicsDevice();
    else
        throw;
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (GraphicsDevice.GraphicsDeviceStatus != GraphicsDeviceStatus.Normal)
{
    RecreateGraphicsDevice();
    return; // skip Present this frame
}

Type guard

bool CanPresent(GraphicsDevice d) => d?.GraphicsDeviceStatus == GraphicsDeviceStatus.Normal;

Try / catch

try { presenter.Present(); }
catch (GraphicsDeviceException ex)
{
    logger.Warning($"Present failed: {ex.HResult} status={ex.DeviceStatus}");
    if (ex.DeviceStatus != GraphicsDeviceStatus.Normal) RecreateGraphicsDevice();
}

Prevention

When it happens

Trigger: Calling Present (each frame, or indirectly via Game/RenderContext loop) when IDXGISwapChain.Present returns an unexpected failing HRESULT — e.g. device removed in an unhandled form, DXGI_STATUS/ERROR codes, or driver errors not matching the expected device-status branches.

Common situations: GPU driver crashes/TDR mid-frame; presenting after the device was removed but before Stride's device-lost handling kicked in; multi-GPU/remote-desktop sessions where the swap chain is invalidated; overclocked/unstable GPUs; window being destroyed while presenting.

Related errors


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

Appendix: source

Thrown at sources/engine/Stride.Graphics/Direct3D/SwapChainGraphicsPresenter.Direct3D.cs:380

            var presentInterval = GraphicsDevice.Tags.Get(ForcedPresentInterval) ?? PresentInterval;

            // From https://learn.microsoft.com/en-us/windows/win32/direct3ddxgi/variable-refresh-rate-displays
            //   DXGI_PRESENT_ALLOW_TEARING can only be used with sync interval 0. It is recommended to always pass this
            //   tearing flag when using sync interval 0 if CheckFeatureSupport reports that tearing is supported and the
            //   app is in a windowed mode - including border-less fullscreen mode.

            var presentFlags = useFlipModel && tearingSupport && presentInterval == PresentInterval.Immediate && !Description.IsFullScreen
                ? DXGI.PresentAllowTearing
                : 0;

            HResult result = swapChain->Present((uint) presentInterval,  presentFlags);

            if (result.IsFailure)
            {
                var deviceStatus = GraphicsDevice.GraphicsDeviceStatus;

                var exception = Marshal.GetExceptionForHR(result);
                throw new GraphicsDeviceException($"Unexpected error on Present (device status: {deviceStatus})", exception, deviceStatus);
            }

#if STRIDE_GRAPHICS_API_DIRECT3D12
            // Manually swap the Back-Buffers

            // Gets the native Back-Buffer from the Swap-Chain.
            //   This increments the reference count of the COM object,
            //   so we need to Release() it when discarding or swapping it.
            bufferSwapIndex = (uint)((++bufferSwapIndex) % bufferCount);
            var nextBackBuffer = GetBackBuffer<BackBufferResourceType>(bufferSwapIndex);

            // TODO: Maybe we should have a lighter Texture.SwapImpl method for this?
            //       InitializeFromImpl() is quite heavy for just swapping the internal resource pointer.
            //       It recreates the internal description and other things that for presenting should not have changed.

            // Texture.InitializeFromImpl also increments the reference count when storing the COM pointer;
            // compensate with Release() to return the reference count to its previous value
            backBuffer.InitializeFromImpl(nextBackBuffer, Description.BackBufferFormat.IsSRgb);

View on GitHub (pinned to 96fad776d2)