#6483·fyne

Windows on ARM: desktop builds request OpenGL ES over WGL, with no way to select desktop GL or EGL

Author: conorarmstrongCreated Aug 17, 2026Updated Sep 4, 2026

Checklist

  • I have searched the issue tracker for open issues that relate to the same problem, before opening a new one.
  • This issue only relates to a single bug. I will open new issues for any other problems.

Describe the bug

On windows/arm64 the GLES painter is selected by architecture, not by platform, so a desktop Windows-on-ARM binary always asks GLFW for an OpenGL ES 2.0 context through WGL. Where the display driver does not expose WGL_EXT_create_context_es2_profile, window creation fails and the application has no way to recover.

Fyne error: window creation error
  Cause: VersionUnavailable: WGL: Failed to create OpenGL ES context

This is the Windows instance of #4782, which reports the same failure on the GLX side. I am opening it separately because that issue is framed around software rendering in a devcontainer, and the discussion there turned on whether that use case is in scope. Windows on ARM is not a niche setup: it is a shipping consumer platform, and the failure needs no container and no emulation to reach.

Why it happens. internal/painter/gl/gl_es.go is selected by:

//go:build (gles || arm || arm64) && !android && !ios && !mobile && !darwin && !wasm && !test_web_driver

and internal/painter/gl/gl_core.go carries an explicit !arm && !arm64. Between them, no combination of build tags selects desktop GL on arm64. internal/driver/glfw/glfw_es.go then sets:

go
glfw.WindowHint(glfw.ClientAPI, glfw.OpenGLESAPI)
glfw.WindowHint(glfw.ContextVersionMajor, 2)
glfw.WindowHint(glfw.ContextVersionMinor, 0)

Keying on architecture is right for ARM single-board computers and for mobile, where GLES is what exists. It is the wrong axis for Windows on ARM, which is a desktop platform that happens to be ARM. There, requesting GLES through WGL requires an extension that Windows drivers commonly do not expose; GLES on Windows normally arrives through ANGLE's EGL instead.

@Bluebugs reached this diagnosis in one line on #4782 in April 2024: "on ARM we are picking GLES, while we should pick GL". The build constraints above are what makes that unfixable from outside the toolkit.

An application cannot work around it. In internal/driver/glfw/window_desktop.go, initWindowHints() and glfw.CreateWindow are a few lines apart inside (*window).create, with nothing in between that calls out to application code, and GLFW hints are thread-local to the GLFW thread. There is no point at which an application can set a hint of its own.

Bundling ANGLE does not help either. Placing libEGL.dll and libGLESv2.dll beside the executable, the way Chromium ships them, changes nothing: the error still names WGL. GLFW does not consult EGL unless asked, and glfw_es.go sets only ClientAPI and the context version, never ContextCreationAPI.

One small thing that may have sent #4782 sideways: -tags egl was suggested there and reported not to help. There is no egl build tag in the desktop driver in v2.6.3 — every egl reference in the tree is under internal/driver/mobile, which is a different driver. So that suggestion could not have had any effect.

How to reproduce

  1. Cross-compile a minimal Fyne application for windows/arm64 (llvm-mingw works).
  2. Run it on a Windows-on-ARM system whose display driver does not expose WGL_EXT_create_context_es2_profile.
  3. No window appears; the error above is printed.

Screenshots

No response

Example code

go
func main() {
	a := app.New()
	w := a.NewWindow("Hello")
	w.ShowAndRun()
}

Fyne version

2.6.3

Go compiler version

1.25.1

Operating system and version

Windows 11 Pro ARM64, Build 28000

Additional Information

Suggested fix. Retry through EGL when the native context API refuses, rather than requesting EGL unconditionally. A fallback costs nothing on systems that work today, and it fixes the GLX case in #4782 by the same path. I have opened a PR that does this.

Requesting EGL unconditionally on Windows would be the wrong shape: a physical Snapdragon device with Adreno drivers may well expose the WGL extension, and forcing EGL there would break a working configuration and require every application to ship ANGLE. A fallback only runs once the platform has already refused, so it cannot regress anything.

glfw.ContextCreationAPI and glfw.EGLContextAPI already exist in the GLFW binding Fyne depends on, and egl_context.c is compiled into its Windows build and looks for libEGL.dll by name. So no new dependency is involved, and bundling ANGLE becomes a working answer for applications once the hint can be reached.

The limits of my evidence, stated plainly. I have not been able to test on a physical Windows-on-ARM device.

  • My own observation is from a Windows 11 ARM64 guest under QEMU TCG with no GPU (ramfb framebuffer only). That guest cannot distinguish a driver refusing a GLES context from the absence of any accelerated graphics stack.
  • Independently, netbirdio/netbird#4691 reports a shipping Fyne application failing on Windows 11 Pro ARM64 build 26100 under Parallels with APIUnavailable: WGL: OpenGL ES requested but WGL_ARB_create_context_es2_profile is unavailable. Parallels on Apple Silicon runs ARM64 natively, so the ARM code there is executing on real ARM hardware, though the GPU is still virtual.

So: two independent reports, neither on a physical Snapdragon. If a maintainer has such a device, the single question is whether a Fyne window opens on it at all.

The build-constraint finding is not subject to that caveat. It was read directly from v2.6.3 and holds regardless of any particular machine.

(For completeness, the extension names in the two error strings differ because of a quirk in GLFW itself: wgl_context.c checks for WGL_EXT_create_context_es2_profile but its error message says ARB. Same check.)

Secondary, and I will open it separately if you prefer. After the error is printed the process stays resident, with no window and no prompt returned to the console. The run loop appears to block with nothing to service. Since NewWindow returns no error, an application cannot detect this and exit cleanly on its own. A non-zero exit would let CI and scripted launches fail rather than hang.