Skip to content

fix(gles): keep surface-created EGL context alive past Surface.Destroy - #362

Merged
kolkov merged 4 commits into
gogpu:mainfrom
admin-else:fix/gles-adapter-context-ownership
Oct 18, 2026
Merged

kolkov merged 4 commits into
gogpu:mainfrom
admin-else:fix/gles-adapter-context-ownership

Conversation

@admin-else

Copy link
Copy Markdown
Contributor

Summary

On Linux GLES, a Surface that has to create its own EGL context (Wayland, or when the detected window kind does not match the real window) owned that context and freed it in Surface.Destroy. The Adapter, Device, and Queue created from that Surface share the same *AdapterContext and outlive the Surface, so closing the window freed the EGL context first and the later Device.Destroy locked a nil EGL context.

Reproduced signature:

ERROR gles: AdapterContext.Lock: nil egl context
panic: runtime error: invalid memory address or nil pointer dereference
... gles.(*Device).Destroy -> gl.(*Context).DeleteVertexArrays(0x0)

This crashes any app that closes a window under GLES when the context is Surface-owned.

Root cause

Instance.CreateSurface has two paths:

  • Path A (X11/headless): the Surface shares the Instance AdapterContext — ownsContext=false, so the Instance (released last) frees it.
  • Path B (Wayland / window-kind mismatch): the Surface created a new AdapterContext with ownsContext=true.

AdapterContext.Destroy sets eglCtx=nil and gl=nil. In Path B the Surface then destroyed the context that Device/Queue/Adapter still referenced. On shutdown gogpu destroys the Surface (window close), then the Device, then the Instance, so the Device hit a dead context: Lock() logged nil egl context, returned the nil gl table, and DeleteVertexArrays dereferenced nil.

Fix

CreateSurface now adopts the Path B context on the Instance. The Instance is released after the Device and Surface, so the shared context lives as long as everything that uses it. Path B now returns ownsContext=false; the Instance.ctx field is guarded by a mutex.

This mirrors the Windows/X11 model where the context is owned by the Instance, not the Surface.

Testing

  • go test ./core/ ./hal/... — pass
  • go test -tags integration ./hal/gles/ — pass, including a new regression test TestSurfaceDoesNotDestroyAdoptedContext that destroys a Surface sharing an adopted context, then the Device, then the Instance (the exact shutdown order that used to panic).
  • go build ./... — pass

Related

Follow-up to the GLES Linux AdapterContext work (FEAT-GLES-003, #332).

SloppaElse added 2 commits September 30, 2026 17:30
On Linux the AdapterContext that backs an EGL context may be created by the
Surface instead of the Instance (Wayland, or when the detected window kind
differs from the real window's kind, e.g. an X11 window under a Wayland session).
In that case Surface.SetFail owned the context and Surface.Destroy freed it.

The Adapter, Device and Queue created from that Surface hold the same
*AdapterContext and outlive the Surface. Closing the window freed the EGL
context first; the later Device.Destroy locked a nil EGL context ("gles:
AdapterContext.Lock: nil egl context") and dereferenced the nil *gl.Context in
DeleteVertexArrays, crashing during app shutdown:

  panic: runtime error: invalid memory address or nil pointer dereference
  ... gles.(*Device).Destroy -> gl.(*Context).DeleteVertexArrays(0x0)

Adopt ownership on the Instance when CreateSurface has to create the context.
The Instance is released after the Device and Surface, so the shared context now
lives as long as everything that uses it. Region Path B now always returns
ownsContext=false.

Adds an integration regression test that destroys a Surface sharing an adopted
context, then destroys the Device, then the Instance.
@admin-else
admin-else requested a review from kolkov as a code owner September 30, 2026 15:35
@codecov

codecov Bot commented Oct 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@kolkov kolkov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @admin-else. The diagnosis matches the code: the Path B context was owned by the Surface while the Device kept using it, and moving ownership to the Instance fixes the shutdown order.

I checked it merged with current main:

  • On Linux (Mesa, EGL) TestSurfaceDoesNotDestroyAdoptedContext passes, and so does the whole -tags integration ./hal/gles/... suite.
  • go test ./..., go mod tidy, golangci-lint (incl. -tags integration for linux) and the cross-builds are clean.
  • main moved since you opened this, so CHANGELOG.md conflicted: #363 and #359 both added [Unreleased] entries. I merged main into your branch keeping both sides (02f72aa) and added the PR number and your handle to your entry (b3cd482). Nothing needed from you.

Optional, a question about adoptContext: when the Instance already has its own context that doesn't match the surface's window kind (X11 or headless detection, then a surface of another kind), prev.Destroy() runs. If someone opened a Device from that instance context before creating the surface (EnumerateAdapters(nil), compute first, window later), that Device loses its context, which is the same class of crash this PR fixes. On Wayland the instance context is skipped, so the common case is fine. Keeping prev alive until Instance.Destroy would cover it. Fine as a follow-up if you'd rather keep this PR small.

Approving. Because of the merge from main, this goes in as one squashed commit with you as the author.

@kolkov
kolkov merged commit b12e951 into gogpu:main Oct 18, 2026
15 checks passed
@kolkov kolkov mentioned this pull request Oct 18, 2026
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.

2 participants