Skip to content

Fix Android SIGSEGV: don't free CGImage textures against a destroyed GL context - #429

Open
tanarie wants to merge 2 commits into
masterfrom
fix/free-cgimage-after-gl-context-loss
Open

Fix Android SIGSEGV: don't free CGImage textures against a destroyed GL context#429
tanarie wants to merge 2 commits into
masterfrom
fix/free-cgimage-after-gl-context-loss

Conversation

@tanarie

@tanarie tanarie commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Type of change: bug fix

Motivation

Fixes the ongoing Android crash:
https://app.bugsnag.com/flowkey/mobile-app/errors/69644a5e4ef4ae6af7fd0f05

SIGSEGV in renderer_GL_common.inl:3923, inside GPU_FreeImage. Android only.
Last 30 days: 108 events, 106 users; 198 events / 192 users all time. First seen
2026-01-12, still happening on 2.113.2. It correlates with the app going to the
background and coming back.

Root cause

On Android, backgrounding tears down the UIScreen and its GL context
(UIScreen.main = nilUIScreen.deinitGPU_Quit()); resuming builds a
new UIScreen/context. A CGImage created under the old context can
then be freed after the new context exists, and CGImage.deinit's guard only
checked UIScreen.main != nil — which passes, because a (new) screen exists. So
GPU_FreeImage ran against an image whose context_target is freed memory →
crash. The guard asked "does a screen exist?", not "is the context that owns
this image still alive?".

Why it reliably lands after the resume: deinit frees via Task { @MainActor },
and Android pauses our render loop while backgrounded, so jobs queued during
teardown only drain on the first frame after resume — the stack trace shows the
free inside nativeProcessEventsAndRender.

Why sdl-gpu's own guard doesn't help: it compares
image->renderer == GPU_GetCurrentRenderer(), but the renderer is SDL_malloc'd
and SDL_free'd (renderer_GLES_2.c:32/:62), so the GPU_Init after
GPU_Quit usually gets the same address back and the comparison passes
spuriously.

Fix

Tag each CGImage with the UIScreen.contextGeneration it was created under,
and only call GPU_FreeImage when that generation is still the current one —
in both deinit and reloadFromSourceData(). The counter is bumped whenever a
GL context is created or destroyed, so "generation matches" means exactly
"the context that owns this image is still live", without depending on
UIScreen.main being nil for the whole teardown→reinit window.

Skipping the free doesn't leak texture memory (that goes with the context), but
it does leak the small GPU_Image/GPU_IMAGE_DATA structs — FreeImage's two
SDL_free calls sit outside the GL branch. ~100 bytes per live image per
background cycle. Freeing them properly needs a registry of live CGImages
torn down before GPU_Quit(); follow-up, not a blocker.

Notes / testing

  • Couldn't reproduce locally (POCO M3 / Adreno). Adreno keeps the GL context on
    background, so it rarely triggers; the crash mostly hits Samsung/Mali devices.
  • Not verified locally — please review carefully, and confirm via the Bugsnag
    count dropping after release.
  • Follow-up: bump submodules UIKit-SDL → NativePlayer → mobile-app once merged.

Please check if the PR fulfills these requirements

  • Self-review: I am confident this is the simplest and clearest way to achieve the expected behaviour
  • There are no dependencies on other PRs

@tanarie
tanarie force-pushed the fix/free-cgimage-after-gl-context-loss branch from 44f1735 to 9d7c69f Compare July 30, 2026 10:01
…GL context

On Android, backgrounding the app tears down the UIScreen and its GL
context (UIScreen.main = nil -> UIScreen.deinit); resuming builds a new
UIScreen/context. A CGImage created under the old context could then be
freed after the new context exists: CGImage.deinit's guard only checked
UIScreen.main != nil, which passes because a (new) screen exists, so
GPU_FreeImage ran against the old image whose context_target is now freed
memory -> SIGSEGV in renderer_GL_common.inl (GPU_FreeImage).

Tag each CGImage with the UIScreen.contextGeneration it was created under
and only free its GPU_Image when that generation still matches the live
context, in both deinit and reloadFromSourceData(). A stale image's
texture was already freed together with its context, so skipping the free
leaks nothing.
@tanarie
tanarie force-pushed the fix/free-cgimage-after-gl-context-loss branch from 9d7c69f to 757b772 Compare July 30, 2026 10:03
@tanarie
tanarie marked this pull request as ready for review July 30, 2026 10:17
@tanarie
tanarie requested a review from michaelknoch July 30, 2026 10:17
Makes the staleness check self-sufficient: `generation == contextGeneration`
now means exactly "the context that owns this image is still the live one",
rather than relying on `UIScreen.main` being nil for the whole
teardown -> reinit window. If an old UIScreen's deinit were ever deferred
past the new screen's creation, images from the new context would have
looked live while GPU_Quit tore down their renderer.

Also correct the claim about leaking: skipping GPU_FreeImage doesn't leak
texture memory (that goes away with the context), but the two SDL_free calls
at the end of FreeImage sit outside the GL branch, so the CPU-side
GPU_Image/GPU_IMAGE_DATA structs do leak.
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