rive-app / rive-app/rive-android

RiveShutdownException on RiveFile dispose after EGL_CONTEXT_LOST (rememberRiveFile)

Open
#453 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Kotlin
Stars
538
Forks
66
PR merge metrics
No merged PRs in 30d

Description

Version

11.4.1 (also reproduces in 11.3.2)

Summary

RenderContextGL.dispose() treats eglDestroyContext returning false as fatal and throws RiveShutdownException. When the underlying EGL error is EGL_CONTEXT_LOST — a documented EGL state that occurs after a power-management / GPU-reset event — the context has already been reclaimed by the system, so there's nothing to destroy. Throwing here turns a normal system event into a user-visible fatal crash whenever a rememberRiveFile-owning composable leaves composition after an EGL context-loss event.

We've seen this in production on Android 15.

Stack trace
Fatal Exception: app.rive.RiveShutdownException: Unable to destroy EGL context
  at app.rive.core.RenderContextGL.dispose(RenderContext.kt:260)
  at app.rive.core.RenderContextGL$cppPointer$1.invoke(RenderContext.kt:245)
  at app.rive.core.UniquePointer$1.invoke(UniquePointer.kt:22)
  at app.rive.core.CloseOnce.close(CloseOnce.kt:34)
  at app.rive.core.UniquePointer.close(UniquePointer.kt:2)
  at app.rive.core.RenderContext.close(RenderContext.kt:40)
  at app.rive.core.CommandQueue.dispose(CommandQueue.kt:134)
  at app.rive.core.RCPointer.release(RCPointer.kt:115)
  at app.rive.core.CommandQueue.release(CommandQueue.kt:145)
  at app.rive.RiveFile$1.invoke(RiveFile.kt:45)
  at app.rive.core.CloseOnce.close(CloseOnce.kt:34)
  at app.rive.RiveFile.close(RiveFile.kt:2)
  at app.rive.RiveFileKt$rememberRiveFile$1$1.invoke(RiveFile.kt:199)
  at androidx.compose.runtime.ProduceStateScopeImpl.awaitDispose(ProduceState.kt:51)
  ...

Caused by java.lang.Throwable: EGL_CONTEXT_LOST
  ...same frames...
Reproduction
  1. Use rememberRiveFile in a composable (e.g. feed it to Rive(...)).
  2. After the device has experienced an EGL context-loss event — reliably triggered on many devices by sleep/wake, aggressive power-save transitions, or GPU driver resets — cause the composable to leave composition (nav away, conditionally swap it out, etc.).
  3. awaitDispose in rememberRiveFile calls RiveFile.close(), which chains into RenderContextGL.dispose() and throws RiveShutdownException, crashing the process.
Offending code (RenderContext.kt lines 254–272 in 11.4.1)
private fun dispose(address: Long) {
    val destroyed = EGL14.eglDestroyContext(display, context)
    if (!destroyed) {
        val error = EGLError.errorString(EGL14.eglGetError())
        throw RiveShutdownException(\"Unable to destroy EGL context\", Throwable(error))
    }
    val terminated = EGL14.eglTerminate(display)
    if (!terminated) {
        val error = EGLError.errorString(EGL14.eglGetError())
        throw RiveShutdownException(\"Unable to terminate EGL display\", Throwable(error))
    }
    cppDelete(address)
}

The eglTerminate branch has the same issue. RiveEGLSurface.dispose and RiveEGLPBufferSurface.dispose throw on eglDestroySurface failure and are likely affected by the same root cause along related teardown paths.

Suggested fix

Check eglGetError() and treat EGL_CONTEXT_LOST as a non-fatal, already-cleaned-up state — log and continue. Something like:

private fun dispose(address: Long) {
    val destroyed = EGL14.eglDestroyContext(display, context)
    if (!destroyed) {
        val errCode = EGL14.eglGetError()
        if (errCode == EGL14.EGL_CONTEXT_LOST) {
            RiveLog.w(TAG) { \"EGL context already lost; skipping destroy\" }
        } else {
            throw RiveShutdownException(
                \"Unable to destroy EGL context\",
                Throwable(EGLError.errorString(errCode))
            )
        }
    }
    // analogous handling for eglTerminate
    cppDelete(address)
}
Rationale

Per the EGL spec, EGL_CONTEXT_LOST indicates a power-management event caused the loss of the associated client context, and the app is expected to release its resources and reinitialise. Destroying an already-lost context is a no-op from the driver's perspective. Making this fatal turns a recoverable system event into a crash during transient composable teardown (which happens routinely on navigation, backgrounding, and power-save toggles).

Reference

eglDestroyContext — Khronos EGL reference

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in RenderContext.kt at RenderContextGL.dispose(), then inspect RiveEGLSurface.dispose and RiveEGLPBufferSurface.dispose for the related EGL teardown paths. Verify how eglGetError() is handled after failed destroy or terminate calls; done means EGL_CONTEXT_LOST no longer crashes disposal while other teardown errors remain fatal.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, kotlin
Domain
computer-graphics, mobile-dev
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.