Ebiten v2.10.2 - macOS 13 Ventura Metal Crash


Ebitengine v2.10.2 was published on 13 September 2026. The patch makes applications run again on macOS 13 Ventura. The Metal driver was using a screen drawable after a flush that did not present had already freed it.

The full release notes and downloads are on the GitHub release page. The project changelog repeats that single fix under v2.10.2. GitHub does not mark the tag as a prerelease, release candidate, beta, or alpha.

Issue 3704 is the only item in the release note. The report is a segmentation fault on macOS Ventura 13.1 with Ebitengine v2.10.1. The module was fresh, with Ebitengine as the only direct requirement. The binary was the hello world sample. It ran for about one frame, then exited with status 2. The host was a 2021 14 inch MacBook Pro running go1.27.0 darwin/arm64.

The runtime text says the signal arrived during cgo execution, with sigcode=2 and fault address 0xa23d1a1c0760. The pasted trace has no C frame under that line. runtime.cgocall enters purego.syscall_SyscallN, then objc.ID.Send, then ca.MetalDrawable.Texture. The caller is (*Image).mtlTexture in internal/graphicsdriver/metal/graphics_darwin.go, from (*Graphics).draw and DrawTriangles while commandQueue.flush runs. That goroutine sits in a syscall, locked to the render thread from glfwBackend.runMultiThread.

The stack is the use of a dead object, not the free. Texture messages an Objective C id the process no longer owns. The release note stops at macOS 13 Ventura.

Each frame, Graphics.Begin allocates an NSAutoreleasePool. Graphics.End flushes the command buffer and then calls pool.Release. The screen image does not keep its own mtl.Texture. (*Image).mtlTexture calls nextDrawable when screenDrawable is still the zero ca.MetalDrawable, then stores the result. nextDrawable returns an autoreleased drawable. Before this tag, the cache held that id with no extra retain.

flushCommandBufferIfNeeded presents only when its argument is true and screenDrawable is set. Graphics.End passes true only for FlushModePresent. During a frame, (*Graphics).draw also calls flushCommandBufferIfNeeded(false) when the destination switches between the screen image and an offscreen image. That path commits a command buffer and leaves screenDrawable populated. A flush that drains the autorelease pool without presenting then frees the drawable. The Go field still holds the old id. The next Texture send is the SIGSEGV in the trace.

Assigning g.screenDrawable = ca.MetalDrawable{} does not send release. The previous present branch cleared the Go field and relied on the pool to drop the object. A frame that never presented skipped that assignment, so the id outlived the object.

v2.10.2 adds MetalDrawable.Retain and MetalDrawable.Release on the internal type in internal/graphicsdriver/metal/ca/ca_darwin.go. Each method is one objc.ID.Send, using selectors sel_retain and sel_release. Nothing in the public module API moves.

mtlTexture now calls drawable.Retain before g.screenDrawable = drawable. The comment on that call names issue 3704: keep the drawable alive across flushes that drain the autorelease pool without presenting. The extra retain survives pool.Release. The autorelease from nextDrawable is still dropped by the pool, so the counts still balance once the driver releases its own reference.

flushCommandBufferIfNeeded no longer clears screenDrawable before Commit. If shouldPresentWithTransaction is set, the id is stashed and presentDrawableWithTransaction runs after Commit. Otherwise presentDrawable runs on the current command buffer. Both paths set presented. Only then does the function call screenDrawable.Release, store the zero drawable, and call finishDrawableUsage. A flush that does not present leaves the retained drawable for the next draw in the frame.

The driver owns one extra reference from a successful nextDrawable until a present flush. Release has to run on that path, including the transaction present. Skipping it would leak the drawable for the life of the process. Releasing before presentDrawableWithTransaction would restore the fault there. Draw submission, shaders, audio, and input are untouched.

Pin github.com/hajimehoshi/ebiten/v2 to v2.10.2 and rebuild. Application source stays as it is. There is no new build tag, environment variable, or config key. The retain call is new in this tag. The filed crash is v2.10.1 on Ventura 13.1.

Any desktop binary that reaches a screen draw on macOS 13 is in range. The hello world sample hits RunGame, DrawTriangles, and mtlTexture. The path runs when the screen image asks for a Metal texture. The edited files are the darwin Metal driver. The published note documents macOS 13.

There is no migration list. After the bump, run a windowed binary on a Ventura 13 host and confirm drawing continues past the first frame. A SIGSEGV in ca.MetalDrawable.Texture on v2.10.1 is the signature this tag closes. The release commit is 221aa13.