gpu,app/internal/wm: add Metal port
The OpenGL (ES) implementations on Apple platforms are deprecated and
don't support GPU compute programs. This change adds support for the
replacement, the Metal GPU API.
Signed-off-by: Elias Naur <[email protected]>
M app/internal/wm/gl_macos.go
M app/internal/wm/gl_macos.m
A app/internal/wm/metal_darwin.go
A app/internal/wm/metal_ios.go
A app/internal/wm/metal_macos.go
M app/internal/wm/os_ios.go
M app/internal/wm/os_ios.m
M app/internal/wm/os_macos.go
M app/internal/wm/os_macos.m
M go.mod
M go.sum
M gpu/api.go
M gpu/gpu.go
M gpu/headless/headless_darwin.go
D gpu/headless/headless_ios.m
D gpu/headless/headless_macos.m
M gpu/internal/driver/api.go
A gpu/internal/metal/metal.go
A gpu/internal/metal/metal_darwin.go
#include <CoreFoundation/CoreFoundation.h>
#include <OpenGLES/ES2/gl.h>
#include <OpenGLES/ES2/glext.h>
@@ 15,6 17,14 @@ __attribute__ ((visibility ("hidden"))) int gio_presentRenderbuffer(CFTypeRef ct
__attribute__ ((visibility ("hidden"))) int gio_makeCurrent(CFTypeRef ctx);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createContext(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createGLLayer(void);
+static CFTypeRef getViewLayer(CFTypeRef viewRef) {
+ @autoreleasepool {
+ UIView *view = (__bridge UIView *)viewRef;
+ return CFBridgingRetain(view.layer);
import "C"
@@ 36,12 46,6 @@ type context struct {
colorBuffer gl.Renderbuffer
-func init() {
- layerFactory = func() uintptr {
- return uintptr(C.gio_createGLLayer())
func newContext(w *window) (*context, error) {
ctx := C.gio_createContext()
if ctx == 0 {
@@ 55,7 59,7 @@ func newContext(w *window) (*context, error) {
c := &context{
ctx: ctx,
owner: w,
- layer: C.CFTypeRef(w.contextLayer()),
+ layer: C.getViewLayer(w.contextView()),
c: f,
return c, nil
@@ 116,7 120,7 @@ func (c *context) Refresh() error {
c.frameBuffer = c.c.CreateFramebuffer()
c.colorBuffer = c.c.CreateRenderbuffer()
- if !c.owner.isVisible() {
+ if !c.owner.visible {
// Make sure any in-flight GL commands are complete.
c.c.Finish()
return nil
int gio_renderbufferStorage(CFTypeRef ctxRef, CFTypeRef layerRef, GLenum buffer) {
EAGLContext *ctx = (__bridge EAGLContext *)ctxRef;
CAEAGLLayer *layer = (__bridge CAEAGLLayer *)layerRef;
+static CFTypeRef createMetalDevice(void) {
+ @autoreleasepool {
+ id<MTLDevice> dev = MTLCreateSystemDefaultDevice();
+ return CFBridgingRetain(dev);
+static void setupLayer(CFTypeRef layerRef, CFTypeRef devRef) {
+ @autoreleasepool {
+ CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef;
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)devRef;
+ layer.device = dev;
+ // Package gpu assumes an sRGB-encoded framebuffer.
+ layer.pixelFormat = MTLPixelFormatBGRA8Unorm_sRGB;
+ if (@available(iOS 11.0, *)) {
+ // Never let nextDrawable time out and return nil.
+ layer.allowsNextDrawableTimeout = NO;
+ if (@available(iOS 11.2, *)) {
+ // Minimize latency from input to window and save a bit of memory.
+ layer.maximumDrawableCount = 2;
+static CFTypeRef nextDrawable(CFTypeRef layerRef) {
+ @autoreleasepool {
+ CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef;
+ return CFBridgingRetain([layer nextDrawable]);
+static CFTypeRef drawableTexture(CFTypeRef drawableRef) {
+ @autoreleasepool {
+ id<CAMetalDrawable> drawable = (__bridge id<CAMetalDrawable>)drawableRef;
+ return CFBridgingRetain(drawable.texture);
+static void presentDrawable(CFTypeRef queueRef, CFTypeRef drawableRef) {
+ @autoreleasepool {
+ id<MTLDrawable> drawable = (__bridge id<MTLDrawable>)drawableRef;
+ id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)queueRef;
+ id<MTLCommandBuffer> cmdBuffer = [queue commandBuffer];
+ [cmdBuffer presentDrawable:drawable];
+ [cmdBuffer commit];
+ [cmdBuffer waitUntilCompleted];
+static CFTypeRef newCommandQueue(CFTypeRef devRef) {
+ @autoreleasepool {
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)devRef;
+ return CFBridgingRetain([dev newCommandQueue]);
+import "C"
+type mtlContext struct {
+ dev C.CFTypeRef
+ view C.CFTypeRef
+ layer C.CFTypeRef
+ queue C.CFTypeRef
+ drawable C.CFTypeRef
+ texture C.CFTypeRef
+func newMtlContext(w *window) (*mtlContext, error) {
+ dev := C.createMetalDevice()
+ if dev == 0 {
+ return nil, errors.New("metal: MTLCreateSystemDefaultDevice failed")
+ view := w.contextView()
+ layer := getMetalLayer(view)
+ if layer == 0 {
+ C.CFRelease(dev)
+ return nil, errors.New("metal: CAMetalLayer construction failed")
+ queue := C.newCommandQueue(dev)
+ if layer == 0 {
+ C.CFRelease(dev)
+ C.CFRelease(layer)
+ return nil, errors.New("metal: [MTLDevice newCommandQueue] failed")
+ C.setupLayer(layer, dev)
+ c := &mtlContext{
+ dev: dev,
+ view: view,
+ layer: layer,
+ queue: queue,
+ return c, nil
+func (c *mtlContext) RenderTarget() gpu.RenderTarget {
+ if c.drawable != 0 || c.texture != 0 {
+ panic("a previous RenderTarget wasn't Presented")
+ c.drawable = C.nextDrawable(c.layer)
+ if c.drawable == 0 {
+ panic("metal: [CAMetalLayer nextDrawable] failed")
+ c.texture = C.drawableTexture(c.drawable)
+ if c.texture == 0 {
+ panic("metal: CADrawable.texture is nil")
+ return gpu.MetalRenderTarget{
+ Texture: unsafe.Pointer(c.texture),
+func (c *mtlContext) API() gpu.API {
+ return gpu.Metal{
+ Device: unsafe.Pointer(c.dev),
+ Queue: unsafe.Pointer(c.queue),
+ PixelFormat: int(C.MTLPixelFormatBGRA8Unorm_sRGB),
+func (c *mtlContext) Release() {
+ C.CFRelease(c.queue)
+ C.CFRelease(c.dev)
+ C.CFRelease(c.layer)
+ if c.drawable != 0 {
+ C.CFRelease(c.drawable)
+ if c.texture != 0 {
+ C.CFRelease(c.texture)
+ *c = mtlContext{}
+func (c *mtlContext) Present() error {
+ C.CFRelease(c.texture)
+ c.texture = 0
+ C.presentDrawable(c.queue, c.drawable)
+ C.CFRelease(c.drawable)
+ c.drawable = 0
+ return nil
+func (c *mtlContext) Lock() error {
+ return nil
+func (c *mtlContext) Unlock() {}
+func (c *mtlContext) Refresh() error {
+ resizeDrawable(c.view, c.layer)
+ return nil
+func (w *window) NewContext() (Context, error) {
+ return newMtlContext(w)
+static CFTypeRef getMetalLayer(CFTypeRef viewRef) {
+ @autoreleasepool {
+ UIView *view = (__bridge UIView *)viewRef;
+ return CFBridgingRetain(view.layer);
+static void resizeDrawable(CFTypeRef viewRef, CFTypeRef layerRef) {
+ @autoreleasepool {
+ UIView *view = (__bridge UIView *)viewRef;
+ CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef;
+ layer.contentsScale = view.contentScaleFactor;
+ CGSize size = layer.bounds.size;
+ size.width *= layer.contentsScale;
+ size.height *= layer.contentsScale;
+ layer.drawableSize = size;
+import "C"
+func getMetalLayer(view C.CFTypeRef) C.CFTypeRef {
+ return C.getMetalLayer(view)
+func resizeDrawable(view, layer C.CFTypeRef) {
+ C.resizeDrawable(view, layer)
+static CFTypeRef getMetalLayer(CFTypeRef viewRef) {
+ @autoreleasepool {
+ NSView *view = (__bridge NSView *)viewRef;
+ return CFBridgingRetain(view.layer);
+static void resizeDrawable(CFTypeRef viewRef, CFTypeRef layerRef) {
+ @autoreleasepool {
+ NSView *view = (__bridge NSView *)viewRef;
+ CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef;
+ CGSize size = layer.bounds.size;
+ size.width *= layer.contentsScale;
+ size.height *= layer.contentsScale;
+ layer.drawableSize = size;
+import "C"
+func getMetalLayer(view C.CFTypeRef) C.CFTypeRef {
+ return C.getMetalLayer(view)
+func resizeDrawable(view, layer C.CFTypeRef) {
+ C.resizeDrawable(view, layer)
@@ 46,24 46,6 @@ static void hideTextInput(CFTypeRef viewRef) {
[view resignFirstResponder];
-static void addLayerToView(CFTypeRef viewRef, CFTypeRef layerRef) {
- UIView *view = (__bridge UIView *)viewRef;
- CALayer *layer = (__bridge CALayer *)layerRef;
- [view.layer addSublayer:layer];
-static void updateView(CFTypeRef viewRef, CFTypeRef layerRef) {
- UIView *view = (__bridge UIView *)viewRef;
- CAEAGLLayer *layer = (__bridge CAEAGLLayer *)layerRef;
- layer.contentsScale = view.contentScaleFactor;
- layer.bounds = view.bounds;
-static void removeLayer(CFTypeRef layerRef) {
- CALayer *layer = (__bridge CALayer *)layerRef;
- [layer removeFromSuperlayer];
static struct drawParams viewDrawParams(CFTypeRef viewRef) {
UIView *v = (__bridge UIView *)viewRef;
struct drawParams params;
@@ 92,7 74,6 @@ import (
"image"
"runtime"
"runtime/debug"
- "sync/atomic"
"time"
"unicode/utf16"
"unsafe"
@@ 112,8 93,7 @@ type window struct {
w Callbacks
displayLink *displayLink
- layer C.CFTypeRef
- visible atomic.Value
+ visible bool
cursor pointer.CursorName
pointerMap []C.CFTypeRef
@@ 121,8 101,6 @@ type window struct {
var mainWindow = newWindowRendezvous()
-var layerFactory func() uintptr
var views = make(map[C.CFTypeRef]*window)
func init() {
@@ 145,9 123,6 @@ func onCreate(view C.CFTypeRef) {
wopts := <-mainWindow.out
w.w = wopts.window
w.w.SetDriver(w)
- w.visible.Store(false)
- w.layer = C.CFTypeRef(layerFactory())
- C.addLayerToView(view, w.layer)
views[view] = w
w.w.Event(system.StageEvent{Stage: system.StagePaused})
@@ 163,9 138,8 @@ func (w *window) draw(sync bool) {
if params.width == 0 || params.height == 0 {
return
- wasVisible := w.isVisible()
- w.visible.Store(true)
- C.updateView(w.view, w.layer)
+ wasVisible := w.visible
+ w.visible = true
if !wasVisible {
w.w.Event(system.StageEvent{Stage: system.StageRunning})
@@ 195,7 169,7 @@ func (w *window) draw(sync bool) {
//export onStop
func onStop(view C.CFTypeRef) {
w := views[view]
- w.visible.Store(false)
+ w.visible = false
w.w.Event(system.StageEvent{Stage: system.StagePaused})
@@ 205,9 179,6 @@ func onDestroy(view C.CFTypeRef) {
delete(views, view)
w.w.Event(system.DestroyEvent{})
w.displayLink.Close()
- C.removeLayer(w.layer)
- C.CFRelease(w.layer)
- w.layer = 0
w.view = 0
@@ 341,12 312,8 @@ func (w *window) lookupTouch(last bool, touch C.CFTypeRef) pointer.ID {
return pointer.ID(id)
-func (w *window) contextLayer() uintptr {
- return uintptr(w.layer)
-func (w *window) isVisible() bool {
- return w.visible.Load().(bool)
+func (w *window) contextView() C.CFTypeRef {
+ return w.view
func (w *window) ShowTextInput(show bool) {
#include "framework_ios.h"
+__attribute__ ((visibility ("hidden"))) Class gio_layerClass(void);
@interface GioView: UIView <UIKeyInput>
@@ 125,6 127,9 @@ NSArray<UIKeyCommand *> *_keyCommands;
gio_onFrameCallback((__bridge CFTypeRef)link);
++ (Class)layerClass {
+ return gio_layerClass();
- (void)willMoveToWindow:(UIWindow *)newWindow {
if (self.window != nil) {
[[NSNotificationCenter defaultCenter] removeObserver:self
@@ 157,9 162,6 @@ NSArray<UIKeyCommand *> *_keyCommands;
-- (void)dealloc {
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
handleTouches(0, self, touches, event);
@@ 423,11 423,9 @@ func gio_onClose(view C.CFTypeRef) {
deleteView(view)
w.w.Event(system.DestroyEvent{})
w.displayLink.Close()
- w.displayLink = nil
C.CFRelease(w.view)
- w.view = 0
C.CFRelease(w.window)
- w.window = 0
+ *w = window{}
//export gio_onHide
#include "_cgo_export.h"
+__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
@interface GioAppDelegate : NSObject<NSApplicationDelegate>
@@ 54,13 56,24 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
gio_onMouse((__bridge CFTypeRef)view, typ, [NSEvent pressedMouseButtons], p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
-@interface GioView : NSView
+@interface GioView : NSView <CALayerDelegate>
@implementation GioView
+// drawRect is called when OpenGL is used, displayLayer otherwise.
+// Don't know why.
- (void)drawRect:(NSRect)r {
gio_onDraw((__bridge CFTypeRef)self);
+- (void)displayLayer:(CALayer *)layer {
+ layer.contentsScale = self.window.backingScaleFactor;
+ gio_onDraw((__bridge CFTypeRef)self);
+- (CALayer *)makeBackingLayer {
+ CALayer *layer = gio_layerFactory();
+ layer.delegate = self;
+ return layer;
- (void)mouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0);
@@ 221,6 234,7 @@ CFTypeRef gio_createView(void) {
NSRect frame = NSMakeRect(0, 0, 0, 0);
GioView* view = [[GioView alloc] initWithFrame:frame];
[view setWantsLayer:YES];
+ view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
return CFBridgingRetain(view);
require (
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
- gioui.org/shader v0.0.0-20210820085839-484f7c0305de
+ gioui.org/shader v0.0.0-20210821062758-133bba6632c7
@@ 4,8 4,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
-gioui.org/shader v0.0.0-20210820085839-484f7c0305de h1:fkcZh0i9T0SzMlqiDm2GBVqXZsbX6bevHcJHfR0llcg=
-gioui.org/shader v0.0.0-20210820085839-484f7c0305de/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
+gioui.org/shader v0.0.0-20210821062758-133bba6632c7 h1:DzpPjbWzoI5cg+WBXWSF2007lzdViHKYZR5NUOKD55k=
+gioui.org/shader v0.0.0-20210821062758-133bba6632c7/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@@ 17,8 17,14 @@ type OpenGLRenderTarget = driver.OpenGLRenderTarget
// Direct3D11RenderTarget is a render target suitable for the Direct3D 11 backend.
type Direct3D11RenderTarget = driver.Direct3D11RenderTarget
+// MetalRenderTarget is a render target suitable for the Metal backend.
+type MetalRenderTarget = driver.MetalRenderTarget
// OpenGL denotes the OpenGL or OpenGL ES API.
type OpenGL = driver.OpenGL
// Direct3D11 denotes the Direct3D API.
type Direct3D11 = driver.Direct3D11
+// Metal denotes the Apple Metal API.
+type Metal = driver.Metal
_ "gioui.org/gpu/internal/d3d11"
+ _ "gioui.org/gpu/internal/metal"
_ "gioui.org/gpu/internal/opengl"
@@ 135,7 136,7 @@ type imageOp struct {
// shaderModuleVersion is the exact version of gioui.org/shader expected by
// this package. Shader programs are not backwards or forwards compatible.
-const shaderModuleVersion = "v0.0.0-20210820085839-484f7c0305de"
+const shaderModuleVersion = "v0.0.0-20210821062758-133bba6632c7"
func decodeStrokeOp(data []byte) clip.StrokeStyle {
_ = data[4]
-#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c
+#cgo CFLAGS: -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c
+#cgo LDFLAGS: -framework CoreGraphics
+@import Metal;
-#include <CoreFoundation/CoreFoundation.h>
+static CFTypeRef createDevice(void) {
+ @autoreleasepool {
+ id dev = MTLCreateSystemDefaultDevice();
+ return CFBridgingRetain(dev);
-__attribute__ ((visibility ("hidden"))) CFTypeRef gio_headless_newContext(void);
-__attribute__ ((visibility ("hidden"))) void gio_headless_clearCurrentContext(CFTypeRef ctxRef);
-__attribute__ ((visibility ("hidden"))) void gio_headless_makeCurrentContext(CFTypeRef ctxRef);
+static CFTypeRef newCommandQueue(CFTypeRef devRef) {
+ @autoreleasepool {
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)devRef;
+ return CFBridgingRetain([dev newCommandQueue]);
import "C"
-type nsContext struct {
- ctx C.CFTypeRef
+type mtlContext struct {
+ dev C.CFTypeRef
+ queue C.CFTypeRef
func newContext() (context, error) {
- ctx := C.gio_headless_newContext()
- return &nsContext{ctx: ctx}, nil
+ dev := C.createDevice()
+ if dev == 0 {
+ return nil, errors.New("headless: failed to create Metal device")
+ queue := C.newCommandQueue(dev)
+ if queue == 0 {
+ C.CFRelease(dev)
+ return nil, errors.New("headless: failed to create MTLQueue")
+ return &mtlContext{dev: dev, queue: queue}, nil
-func (c *nsContext) API() gpu.API {
- return gpu.OpenGL{}
+func (c *mtlContext) API() gpu.API {
+ return gpu.Metal{
+ Device: unsafe.Pointer(c.dev),
+ Queue: unsafe.Pointer(c.queue),
+ PixelFormat: int(C.MTLPixelFormatRGBA8Unorm_sRGB),
-func (c *nsContext) MakeCurrent() error {
- C.gio_headless_makeCurrentContext(c.ctx)
+func (c *mtlContext) MakeCurrent() error {
return nil
-func (c *nsContext) ReleaseCurrent() {
- C.gio_headless_clearCurrentContext(c.ctx)
+func (c *mtlContext) ReleaseCurrent() {}
-func (d *nsContext) Release() {
- if d.ctx != 0 {
- C.CFRelease(d.ctx)
- d.ctx = 0
+func (d *mtlContext) Release() {
+ C.CFRelease(d.dev)
+ C.CFRelease(d.queue)
+ *d = mtlContext{}
-CFTypeRef gio_headless_newContext(void) {
- EAGLContext *ctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
- if (ctx == nil) {
- return nil;
- return CFBridgingRetain(ctx);
-void gio_headless_clearCurrentContext(CFTypeRef ctxRef) {
- [EAGLContext setCurrentContext:nil];
-void gio_headless_makeCurrentContext(CFTypeRef ctxRef) {
- EAGLContext *ctx = (__bridge EAGLContext *)ctxRef;
- [EAGLContext setCurrentContext:ctx];
-CFTypeRef gio_headless_newContext(void) {
- NSOpenGLPixelFormatAttribute attr[] = {
- NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
- NSOpenGLPFAColorSize, 24,
- NSOpenGLPFAAccelerated,
- // Opt-in to automatic GPU switching. CGL-only property.
- kCGLPFASupportsAutomaticGraphicsSwitching,
- NSOpenGLPFAAllowOfflineRenderers,
- NSOpenGLPixelFormat *pixFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attr];
- if (pixFormat == nil) {
- return NULL;
- NSOpenGLContext *ctx = [[NSOpenGLContext alloc] initWithFormat:pixFormat shareContext:nil];
- return CFBridgingRetain(ctx);
-void gio_headless_clearCurrentContext(CFTypeRef ctxRef) {
- NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef;
- CGLUnlockContext([ctx CGLContextObj]);
- [NSOpenGLContext clearCurrentContext];
-void gio_headless_makeCurrentContext(CFTypeRef ctxRef) {
- NSOpenGLContext *ctx = (__bridge NSOpenGLContext *)ctxRef;
- [ctx makeCurrentContext];
- CGLLockContext([ctx CGLContextObj]);
type OpenGL struct {
// ES forces the use of ANGLE OpenGL ES libraries on macOS. It is
// ignored on all other platforms.
@@ 41,10 46,20 @@ type Direct3D11 struct {
Device unsafe.Pointer
+type Metal struct {
+ // Device is an MTLDevice.
+ Device unsafe.Pointer
+ // Queue is a MTLCommandQueue.
+ Queue unsafe.Pointer
+ // PixelFormat is the MTLPixelFormat of the default framebuffer.
+ PixelFormat int
// API specific device constructors.
var (
NewOpenGLDevice func(api OpenGL) (Device, error)
NewDirect3D11Device func(api Direct3D11) (Device, error)
+ NewMetalDevice func(api Metal) (Device, error)
// NewDevice creates a new Device given the api.
@@ 62,11 77,17 @@ func NewDevice(api API) (Device, error) {
if NewDirect3D11Device != nil {
return NewDirect3D11Device(api)
+ case Metal:
+ if NewMetalDevice != nil {
+ return NewMetalDevice(api)
return nil, fmt.Errorf("driver: no driver available for the API %T", api)
func (OpenGL) implementsAPI() {}
func (Direct3D11) implementsAPI() {}
+func (Metal) implementsAPI() {}
func (OpenGLRenderTarget) ImplementsRenderTarget() {}
func (Direct3D11RenderTarget) ImplementsRenderTarget() {}
+func (MetalRenderTarget) ImplementsRenderTarget() {}
+// SPDX-License-Identifier: Unlicense OR MIT
+// This file exists so this package builds on non-Darwin platforms.
+package metal
+#cgo CFLAGS: -Werror -xobjective-c -fmodules -fobjc-arc
+#cgo LDFLAGS: -framework CoreGraphics
+@import Metal;
+#include <CoreFoundation/CoreFoundation.h>
+#include <Metal/Metal.h>
+static CFTypeRef queueNewBuffer(CFTypeRef queueRef) {
+ @autoreleasepool {
+ id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)queueRef;
+ return CFBridgingRetain([queue commandBuffer]);
+static void cmdBufferCommit(CFTypeRef cmdBufRef) {
+ @autoreleasepool {
+ id<MTLCommandBuffer> cmdBuf = (__bridge id<MTLCommandBuffer>)cmdBufRef;
+ [cmdBuf commit];
+static void cmdBufferWaitUntilCompleted(CFTypeRef cmdBufRef) {
+ @autoreleasepool {
+ id<MTLCommandBuffer> cmdBuf = (__bridge id<MTLCommandBuffer>)cmdBufRef;
+ [cmdBuf waitUntilCompleted];
+static CFTypeRef cmdBufferRenderEncoder(CFTypeRef cmdBufRef, CFTypeRef textureRef, MTLLoadAction act, float r, float g, float b, float a) {
+ @autoreleasepool {
+ id<MTLCommandBuffer> cmdBuf = (__bridge id<MTLCommandBuffer>)cmdBufRef;
+ MTLRenderPassDescriptor *desc = [MTLRenderPassDescriptor new];
+ desc.colorAttachments[0].texture = (__bridge id<MTLTexture>)textureRef;
+ desc.colorAttachments[0].loadAction = act;
+ desc.colorAttachments[0].clearColor = MTLClearColorMake(r, g, b, a);
+ return CFBridgingRetain([cmdBuf renderCommandEncoderWithDescriptor:desc]);
+static CFTypeRef cmdBufferComputeEncoder(CFTypeRef cmdBufRef) {
+ @autoreleasepool {
+ id<MTLCommandBuffer> cmdBuf = (__bridge id<MTLCommandBuffer>)cmdBufRef;
+ return CFBridgingRetain([cmdBuf computeCommandEncoder]);
+static CFTypeRef cmdBufferBlitEncoder(CFTypeRef cmdBufRef) {
+ @autoreleasepool {
+ id<MTLCommandBuffer> cmdBuf = (__bridge id<MTLCommandBuffer>)cmdBufRef;
+ return CFBridgingRetain([cmdBuf blitCommandEncoder]);
+static void renderEncEnd(CFTypeRef renderEncRef) {
+ @autoreleasepool {
+ id<MTLRenderCommandEncoder> enc = (__bridge id<MTLRenderCommandEncoder>)renderEncRef;
+ [enc endEncoding];
+static void renderEncViewport(CFTypeRef renderEncRef, MTLViewport viewport) {
+ @autoreleasepool {
+ id<MTLRenderCommandEncoder> enc = (__bridge id<MTLRenderCommandEncoder>)renderEncRef;
+ [enc setViewport:viewport];
+static void renderEncSetFragmentTexture(CFTypeRef renderEncRef, NSUInteger index, CFTypeRef texRef) {
+ @autoreleasepool {
+ id<MTLRenderCommandEncoder> enc = (__bridge id<MTLRenderCommandEncoder>)renderEncRef;
+ id<MTLTexture> tex = (__bridge id<MTLTexture>)texRef;
+ [enc setFragmentTexture:tex atIndex:index];
+static void renderEncSetFragmentSamplerState(CFTypeRef renderEncRef, NSUInteger index, CFTypeRef samplerRef) {
+ @autoreleasepool {
+ id<MTLRenderCommandEncoder> enc = (__bridge id<MTLRenderCommandEncoder>)renderEncRef;
+ id<MTLSamplerState> sampler = (__bridge id<MTLSamplerState>)samplerRef;
+ [enc setFragmentSamplerState:sampler atIndex:index];
+static void renderEncSetVertexBuffer(CFTypeRef renderEncRef, CFTypeRef bufRef, NSUInteger idx, NSUInteger offset) {
+ @autoreleasepool {
+ id<MTLRenderCommandEncoder> enc = (__bridge id<MTLRenderCommandEncoder>)renderEncRef;
+ id<MTLBuffer> buf = (__bridge id<MTLBuffer>)bufRef;
+ [enc setVertexBuffer:buf offset:offset atIndex:idx];
+static void renderEncSetFragmentBuffer(CFTypeRef renderEncRef, CFTypeRef bufRef, NSUInteger idx, NSUInteger offset) {
+ @autoreleasepool {
+ id<MTLRenderCommandEncoder> enc = (__bridge id<MTLRenderCommandEncoder>)renderEncRef;
+ id<MTLBuffer> buf = (__bridge id<MTLBuffer>)bufRef;
+ [enc setFragmentBuffer:buf offset:offset atIndex:idx];
+static void renderEncSetFragmentBytes(CFTypeRef renderEncRef, const void *bytes, NSUInteger length, NSUInteger idx) {
+ @autoreleasepool {
+ id<MTLRenderCommandEncoder> enc = (__bridge id<MTLRenderCommandEncoder>)renderEncRef;
+ [enc setFragmentBytes:bytes length:length atIndex:idx];
+static void renderEncSetVertexBytes(CFTypeRef renderEncRef, const void *bytes, NSUInteger length, NSUInteger idx) {
+ @autoreleasepool {
+ id<MTLRenderCommandEncoder> enc = (__bridge id<MTLRenderCommandEncoder>)renderEncRef;
+ [enc setVertexBytes:bytes length:length atIndex:idx];
+static void renderEncSetRenderPipelineState(CFTypeRef renderEncRef, CFTypeRef pipeRef) {
+ @autoreleasepool {
+ id<MTLRenderCommandEncoder> enc = (__bridge id<MTLRenderCommandEncoder>)renderEncRef;
+ id<MTLRenderPipelineState> pipe = (__bridge id<MTLRenderPipelineState>)pipeRef;
+ [enc setRenderPipelineState:pipe];
+static void renderEncDrawPrimitives(CFTypeRef renderEncRef, MTLPrimitiveType type, NSUInteger start, NSUInteger count) {
+ @autoreleasepool {
+ id<MTLRenderCommandEncoder> enc = (__bridge id<MTLRenderCommandEncoder>)renderEncRef;
+ [enc drawPrimitives:type vertexStart:start vertexCount:count];
+static void renderEncDrawIndexedPrimitives(CFTypeRef renderEncRef, MTLPrimitiveType type, CFTypeRef bufRef, NSUInteger offset, NSUInteger count) {
+ @autoreleasepool {
+ id<MTLRenderCommandEncoder> enc = (__bridge id<MTLRenderCommandEncoder>)renderEncRef;
+ id<MTLBuffer> buf = (__bridge id<MTLBuffer>)bufRef;
+ [enc drawIndexedPrimitives:type indexCount:count indexType:MTLIndexTypeUInt16 indexBuffer:buf indexBufferOffset:offset];
+static void computeEncSetPipeline(CFTypeRef computeEncRef, CFTypeRef pipeRef) {
+ @autoreleasepool {
+ id<MTLComputeCommandEncoder> enc = (__bridge id<MTLComputeCommandEncoder>)computeEncRef;
+ id<MTLComputePipelineState> pipe = (__bridge id<MTLComputePipelineState>)pipeRef;
+ [enc setComputePipelineState:pipe];
+static void computeEncSetTexture(CFTypeRef computeEncRef, NSUInteger index, CFTypeRef texRef) {
+ @autoreleasepool {
+ id<MTLComputeCommandEncoder> enc = (__bridge id<MTLComputeCommandEncoder>)computeEncRef;
+ id<MTLTexture> tex = (__bridge id<MTLTexture>)texRef;
+ [enc setTexture:tex atIndex:index];
+static void computeEncEnd(CFTypeRef computeEncRef) {
+ @autoreleasepool {
+ id<MTLComputeCommandEncoder> enc = (__bridge id<MTLComputeCommandEncoder>)computeEncRef;
+ [enc endEncoding];
+static void computeEncSetBuffer(CFTypeRef computeEncRef, NSUInteger index, CFTypeRef bufRef) {
+ @autoreleasepool {
+ id<MTLComputeCommandEncoder> enc = (__bridge id<MTLComputeCommandEncoder>)computeEncRef;
+ id<MTLBuffer> buf = (__bridge id<MTLBuffer>)bufRef;
+ [enc setBuffer:buf offset:0 atIndex:index];
+static void computeEncDispatch(CFTypeRef computeEncRef, MTLSize threadgroupsPerGrid, MTLSize threadsPerThreadgroup) {
+ @autoreleasepool {
+ id<MTLComputeCommandEncoder> enc = (__bridge id<MTLComputeCommandEncoder>)computeEncRef;
+ [enc dispatchThreadgroups:threadgroupsPerGrid threadsPerThreadgroup:threadsPerThreadgroup];
+static void computeEncSetBytes(CFTypeRef computeEncRef, const void *bytes, NSUInteger length, NSUInteger index) {
+ @autoreleasepool {
+ id<MTLComputeCommandEncoder> enc = (__bridge id<MTLComputeCommandEncoder>)computeEncRef;
+ [enc setBytes:bytes length:length atIndex:index];
+static void blitEncEnd(CFTypeRef blitEncRef) {
+ @autoreleasepool {
+ id<MTLBlitCommandEncoder> enc = (__bridge id<MTLBlitCommandEncoder>)blitEncRef;
+ [enc endEncoding];
+static void blitEncCopyFromTexture(CFTypeRef blitEncRef, CFTypeRef srcRef, MTLOrigin srcOrig, MTLSize srcSize, CFTypeRef dstRef, MTLOrigin dstOrig) {
+ @autoreleasepool {
+ id<MTLBlitCommandEncoder> enc = (__bridge id<MTLBlitCommandEncoder>)blitEncRef;
+ id<MTLTexture> src = (__bridge id<MTLTexture>)srcRef;
+ id<MTLTexture> dst = (__bridge id<MTLTexture>)dstRef;
+ [enc copyFromTexture:src
+ sourceSlice:0
+ sourceLevel:0
+ sourceOrigin:srcOrig
+ sourceSize:srcSize
+ toTexture:dst
+ destinationSlice:0
+ destinationLevel:0
+ destinationOrigin:dstOrig];
+static void blitEncCopyBufferToTexture(CFTypeRef blitEncRef, CFTypeRef bufRef, CFTypeRef texRef, NSUInteger offset, NSUInteger stride, NSUInteger length, MTLSize dims, MTLOrigin orig) {
+ @autoreleasepool {
+ id<MTLBlitCommandEncoder> enc = (__bridge id<MTLBlitCommandEncoder>)blitEncRef;
+ id<MTLBuffer> src = (__bridge id<MTLBuffer>)bufRef;
+ id<MTLTexture> dst = (__bridge id<MTLTexture>)texRef;
+ [enc copyFromBuffer:src
+ sourceOffset:offset
+ sourceBytesPerRow:stride
+ sourceBytesPerImage:length
+ sourceSize:dims
+ toTexture:dst
+ destinationSlice:0
+ destinationLevel:0
+ destinationOrigin:orig];
+static void blitEncCopyTextureToBuffer(CFTypeRef blitEncRef, CFTypeRef texRef, CFTypeRef bufRef, NSUInteger offset, NSUInteger stride, NSUInteger length, MTLSize dims, MTLOrigin orig) {
+ @autoreleasepool {
+ id<MTLBlitCommandEncoder> enc = (__bridge id<MTLBlitCommandEncoder>)blitEncRef;
+ id<MTLTexture> src = (__bridge id<MTLTexture>)texRef;
+ id<MTLBuffer> dst = (__bridge id<MTLBuffer>)bufRef;
+ [enc copyFromTexture:src
+ sourceSlice:0
+ sourceLevel:0
+ sourceOrigin:orig
+ sourceSize:dims
+ toBuffer:dst
+ destinationOffset:offset
+ destinationBytesPerRow:stride
+ destinationBytesPerImage:length];
+static void blitEncCopyBufferToBuffer(CFTypeRef blitEncRef, CFTypeRef srcRef, CFTypeRef dstRef, NSUInteger srcOff, NSUInteger dstOff, NSUInteger size) {
+ @autoreleasepool {
+ id<MTLBlitCommandEncoder> enc = (__bridge id<MTLBlitCommandEncoder>)blitEncRef;
+ id<MTLBuffer> src = (__bridge id<MTLBuffer>)srcRef;
+ id<MTLBuffer> dst = (__bridge id<MTLBuffer>)dstRef;
+ [enc copyFromBuffer:src
+ sourceOffset:srcOff
+ toBuffer:dst
+ destinationOffset:dstOff
+ size:size];
+static CFTypeRef newTexture(CFTypeRef devRef, NSUInteger width, NSUInteger height, MTLPixelFormat format, MTLTextureUsage usage) {
+ @autoreleasepool {
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)devRef;
+ MTLTextureDescriptor *mtlDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat: format
+ width: width
+ height: height
+ mipmapped: NO];
+ mtlDesc.usage = usage;
+ mtlDesc.storageMode = MTLStorageModePrivate;
+ return CFBridgingRetain([dev newTextureWithDescriptor:mtlDesc]);
+static CFTypeRef newSampler(CFTypeRef devRef, MTLSamplerMinMagFilter minFilter, MTLSamplerMinMagFilter magFilter) {
+ @autoreleasepool {
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)devRef;
+ MTLSamplerDescriptor *desc = [MTLSamplerDescriptor new];
+ desc.minFilter = minFilter;
+ desc.magFilter = magFilter;
+ return CFBridgingRetain([dev newSamplerStateWithDescriptor:desc]);
+static CFTypeRef newBuffer(CFTypeRef devRef, NSUInteger size, MTLResourceOptions opts) {
+ @autoreleasepool {
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)devRef;
+ id<MTLBuffer> buf = [dev newBufferWithLength:size
+ options:opts];
+ return CFBridgingRetain(buf);
+static void *bufferAddress(CFTypeRef bufRef) {
+ @autoreleasepool {
+ id<MTLBuffer> buf = (__bridge id<MTLBuffer>)bufRef;
+ return [buf contents];
+static NSUInteger bufferLength(CFTypeRef bufRef) {
+ @autoreleasepool {
+ id<MTLBuffer> buf = (__bridge id<MTLBuffer>)bufRef;
+ return [buf length];
+static CFTypeRef newLibrary(CFTypeRef devRef, char *name, void *mtllib, size_t size) {
+ @autoreleasepool {
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)devRef;
+ dispatch_data_t data = dispatch_data_create(mtllib, size, DISPATCH_TARGET_QUEUE_DEFAULT, DISPATCH_DATA_DESTRUCTOR_DEFAULT);
+ id<MTLLibrary> lib = [dev newLibraryWithData:data error:nil];
+ lib.label = [NSString stringWithUTF8String:name];
+ return CFBridgingRetain(lib);
+static CFTypeRef libraryNewFunction(CFTypeRef libRef, char *funcName) {
+ @autoreleasepool {
+ id<MTLLibrary> lib = (__bridge id<MTLLibrary>)libRef;
+ NSString *name = [NSString stringWithUTF8String:funcName];
+ return CFBridgingRetain([lib newFunctionWithName:name]);
+static CFTypeRef newComputePipeline(CFTypeRef devRef, CFTypeRef funcRef) {
+ @autoreleasepool {
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)devRef;
+ id<MTLFunction> func = (__bridge id<MTLFunction>)funcRef;
+ return CFBridgingRetain([dev newComputePipelineStateWithFunction:func error:nil]);
+static CFTypeRef newRenderPipeline(CFTypeRef devRef, CFTypeRef vertFunc, CFTypeRef fragFunc, MTLPixelFormat pixelFormat, NSUInteger bufIdx, NSUInteger nverts, MTLVertexFormat *fmts, NSUInteger *offsets, NSUInteger stride, int blend, MTLBlendFactor srcFactor, MTLBlendFactor dstFactor, NSUInteger nvertBufs, NSUInteger nfragBufs) {
+ @autoreleasepool {
+ id<MTLDevice> dev = (__bridge id<MTLDevice>)devRef;
+ id<MTLFunction> vfunc = (__bridge id<MTLFunction>)vertFunc;
+ id<MTLFunction> ffunc = (__bridge id<MTLFunction>)fragFunc;
+ MTLVertexDescriptor *vdesc = [MTLVertexDescriptor vertexDescriptor];
+ vdesc.layouts[bufIdx].stride = stride;
+ for (NSUInteger i = 0; i < nverts; i++) {
+ vdesc.attributes[i].format = fmts[i];
+ vdesc.attributes[i].offset = offsets[i];
+ vdesc.attributes[i].bufferIndex = bufIdx;
+ MTLRenderPipelineDescriptor *desc = [MTLRenderPipelineDescriptor new];
+ desc.vertexFunction = vfunc;
+ desc.fragmentFunction = ffunc;
+ desc.vertexDescriptor = vdesc;
+ for (NSUInteger i = 0; i < nvertBufs; i++) {
+ if (@available(iOS 11.0, *)) {
+ desc.vertexBuffers[i].mutability = MTLMutabilityImmutable;
+ for (NSUInteger i = 0; i < nfragBufs; i++) {
+ if (@available(iOS 11.0, *)) {
+ desc.fragmentBuffers[i].mutability = MTLMutabilityImmutable;
+ desc.colorAttachments[0].pixelFormat = pixelFormat;
+ desc.colorAttachments[0].blendingEnabled = blend ? YES : NO;
+ desc.colorAttachments[0].sourceAlphaBlendFactor = srcFactor;
+ desc.colorAttachments[0].sourceRGBBlendFactor = srcFactor;
+ desc.colorAttachments[0].destinationAlphaBlendFactor = dstFactor;
+ desc.colorAttachments[0].destinationRGBBlendFactor = dstFactor;
+ return CFBridgingRetain([dev newRenderPipelineStateWithDescriptor:desc
+ error:nil]);
+import "C"
+type Backend struct {
+ dev C.CFTypeRef
+ queue C.CFTypeRef
+ pixelFmt C.MTLPixelFormat
+ cmdBuffer C.CFTypeRef
+ lastCmdBuffer C.CFTypeRef
+ renderEnc C.CFTypeRef
+ computeEnc C.CFTypeRef
+ blitEnc C.CFTypeRef
+ stagingBuf C.CFTypeRef
+ stagingOff int
+ oldStagingBuf C.CFTypeRef
+ indexBuf *Buffer
+ state state
+ newState state
+ // bufSizes is scratch space for filling out the spvBufferSizeConstants
+ // that spirv-cross generates for emulating buffer.length expressions in
+ // shaders.
+ bufSizes []uint32
+type state struct {
+ renderPass struct {
+ framebuffer *Framebuffer
+ loadAction driver.LoadAction
+ clearColor [4]float32
+ pipeline *Pipeline
+ program *Program
+ vertices struct {
+ buffer *Buffer
+ offset int
+ buffers [bufferUnits]*Buffer
+ vertUniforms *Buffer
+ fragUniforms *Buffer
+ textures [texUnits]*Texture
+ viewport C.MTLViewport
+type Texture struct {
+ backend *Backend
+ texture C.CFTypeRef
+ sampler C.CFTypeRef
+ width int
+ height int
+type Shader struct {
+ function C.CFTypeRef
+ inputs []shader.InputLocation
+type Program struct {
+ pipeline C.CFTypeRef
+ groupSize [3]int
+type Pipeline struct {
+ pipeline C.CFTypeRef
+type Framebuffer struct {
+ backend *Backend
+ texture C.CFTypeRef
+ foreign bool
+type Buffer struct {
+ backend *Backend
+ size int
+ buffer C.CFTypeRef
+ // store is the buffer contents For buffers not allocated on the GPU.
+ store []byte
+const (
+ uniformBufferIndex = 0
+ attributeBufferIndex = 1
+ spvBufferSizeConstantsBinding = 25
+const (
+ texUnits = 4
+ bufferUnits = 4
+func init() {
+ driver.NewMetalDevice = newMetalDevice
+func newMetalDevice(api driver.Metal) (driver.Device, error) {
+ dev := C.CFTypeRef(api.Device)
+ C.CFRetain(dev)
+ queue := C.CFTypeRef(api.Queue)
+ C.CFRetain(queue)
+ b := &Backend{
+ dev: dev,
+ queue: queue,
+ pixelFmt: C.MTLPixelFormat(api.PixelFormat),
+ bufSizes: make([]uint32, bufferUnits),
+ return b, nil
+func (b *Backend) BeginFrame(target driver.RenderTarget, clear bool, viewport image.Point) driver.Framebuffer {
+ if b.lastCmdBuffer != 0 {
+ C.cmdBufferWaitUntilCompleted(b.lastCmdBuffer)
+ b.oldStagingBuf, b.stagingBuf = b.stagingBuf, b.oldStagingBuf
+ b.stagingOff = 0
+ if target == nil {
+ return nil
+ var texture C.CFTypeRef
+ switch t := target.(type) {
+ case driver.MetalRenderTarget:
+ texture = C.CFTypeRef(t.Texture)
+ return &Framebuffer{texture: texture, foreign: true}
+ case *Framebuffer:
+ texture = C.CFTypeRef(t.texture)
+ return t
+ default:
+ panic(fmt.Sprintf("metal: unsupported render target type: %T", t))
+func (b *Backend) startBlit() C.CFTypeRef {
+ if b.blitEnc != 0 {
+ return b.blitEnc
+ b.endEncoder()
+ b.ensureCmdBuffer()
+ b.blitEnc = C.cmdBufferBlitEncoder(b.cmdBuffer)
+ if b.blitEnc == 0 {
+ panic("metal: [MTLCommandBuffer blitCommandEncoder:] failed")
+ b.state = state{}
+ return b.blitEnc
+func (b *Backend) BlitFramebuffer(dst, src driver.Framebuffer, srect image.Rectangle, dorig image.Point) {
+ enc := b.startBlit()
+ dstTex := dst.(*Framebuffer).texture
+ srcTex := src.(*Framebuffer).texture
+ ssz := srect.Size()
+ C.blitEncCopyFromTexture(
+ enc,
+ srcTex,
+ C.MTLOrigin{
+ x: C.NSUInteger(srect.Min.X),
+ y: C.NSUInteger(srect.Min.Y),
+ C.MTLSize{
+ width: C.NSUInteger(ssz.X),
+ height: C.NSUInteger(ssz.Y),
+ depth: 1,
+ dstTex,
+ C.MTLOrigin{
+ x: C.NSUInteger(dorig.X),
+ y: C.NSUInteger(dorig.Y),
+func (b *Backend) EndFrame() {
+ b.endCmdBuffer(false)
+func (b *Backend) endCmdBuffer(wait bool) {
+ b.endEncoder()
+ if b.cmdBuffer == 0 {
+ return
+ C.cmdBufferCommit(b.cmdBuffer)
+ if wait {
+ C.cmdBufferWaitUntilCompleted(b.cmdBuffer)
+ if b.lastCmdBuffer != 0 {
+ C.CFRelease(b.lastCmdBuffer)
+ b.lastCmdBuffer = b.cmdBuffer
+ b.cmdBuffer = 0
+func (b *Backend) Caps() driver.Caps {
+ return driver.Caps{
+ MaxTextureSize: 8192,
+ Features: driver.FeatureSRGB | driver.FeatureCompute | driver.FeatureFloatRenderTargets,
+func (b *Backend) NewTimer() driver.Timer {
+ panic("timers not supported")
+func (b *Backend) IsTimeContinuous() bool {
+ panic("timers not supported")
+func (b *Backend) Release() {
+ if b.cmdBuffer != 0 {
+ C.CFRelease(b.cmdBuffer)
+ if b.lastCmdBuffer != 0 {
+ C.CFRelease(b.lastCmdBuffer)
+ if b.stagingBuf != 0 {
+ C.CFRelease(b.stagingBuf)
+ if b.oldStagingBuf != 0 {
+ C.CFRelease(b.oldStagingBuf)
+ C.CFRelease(b.queue)
+ C.CFRelease(b.dev)
+ *b = Backend{}
+func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, minFilter, magFilter driver.TextureFilter, bindings driver.BufferBinding) (driver.Texture, error) {
+ mformat := pixelFormatFor(format)
+ var usage C.MTLTextureUsage
+ if bindings&(driver.BufferBindingTexture|driver.BufferBindingShaderStorageRead) != 0 {
+ usage |= C.MTLTextureUsageShaderRead
+ if bindings&driver.BufferBindingFramebuffer != 0 {
+ usage |= C.MTLTextureUsageRenderTarget
+ if bindings&driver.BufferBindingShaderStorageWrite != 0 {
+ usage |= C.MTLTextureUsageShaderWrite
+ tex := C.newTexture(b.dev, C.NSUInteger(width), C.NSUInteger(height), mformat, usage)
+ if tex == 0 {
+ return nil, errors.New("metal: [MTLDevice newTextureWithDescriptor:] failed")
+ min := samplerFilterFor(minFilter)
+ max := samplerFilterFor(magFilter)
+ s := C.newSampler(b.dev, min, max)
+ if s == 0 {
+ C.CFRelease(tex)
+ return nil, errors.New("metal: [MTLDevice newSamplerStateWithDescriptor:] failed")
+ return &Texture{backend: b, texture: tex, sampler: s, width: width, height: height}, nil
+func samplerFilterFor(f driver.TextureFilter) C.MTLSamplerMinMagFilter {
+ switch f {
+ case driver.FilterNearest:
+ return C.MTLSamplerMinMagFilterNearest
+ case driver.FilterLinear:
+ return C.MTLSamplerMinMagFilterLinear
+ default:
+ panic("invalid texture filter")
+func (b *Backend) NewPipeline(desc driver.PipelineDesc) (driver.Pipeline, error) {
+ vsh, fsh := desc.VertexShader.(*Shader), desc.FragmentShader.(*Shader)
+ layout := desc.VertexLayout.Inputs
+ if got, exp := len(layout), len(vsh.inputs); got != exp {
+ return nil, fmt.Errorf("metal: number of input descriptors (%d) doesn't match number of inputs (%d)", got, exp)
+ formats := make([]C.MTLVertexFormat, len(layout))
+ offsets := make([]C.NSUInteger, len(layout))
+ for i, inp := range layout {
+ index := vsh.inputs[i].Location
+ formats[index] = vertFormatFor(vsh.inputs[i])
+ offsets[index] = C.NSUInteger(inp.Offset)
+ var (
+ fmtPtr *C.MTLVertexFormat
+ offPtr *C.NSUInteger
+ if len(layout) > 0 {
+ fmtPtr = &formats[0]
+ offPtr = &offsets[0]
+ srcFactor := blendFactorFor(desc.BlendDesc.SrcFactor)
+ dstFactor := blendFactorFor(desc.BlendDesc.DstFactor)
+ blend := C.int(0)
+ if desc.BlendDesc.Enable {
+ blend = 1
+ pf := b.pixelFmt
+ if f := desc.PixelFormat; f != driver.TextureFormatOutput {
+ pf = pixelFormatFor(f)
+ pipe := C.newRenderPipeline(
+ b.dev,
+ vsh.function,
+ fsh.function,
+ pf,
+ attributeBufferIndex,
+ C.NSUInteger(len(layout)), fmtPtr, offPtr,
+ C.NSUInteger(desc.VertexLayout.Stride),
+ blend, srcFactor, dstFactor,
+ 2, // Number of vertex buffers.
+ 1, // Number of fragment buffers.
+ if pipe == 0 {
+ return nil, errors.New("metal: pipeline construction failed")
+ return &Pipeline{pipeline: pipe}, nil
+func dataTypeSize(d shader.DataType) int {
+ switch d {
+ case shader.DataTypeFloat:
+ return 4
+ default:
+ panic("unsupported data type")
+func blendFactorFor(f driver.BlendFactor) C.MTLBlendFactor {
+ switch f {
+ case driver.BlendFactorZero:
+ return C.MTLBlendFactorZero
+ case driver.BlendFactorOne:
+ return C.MTLBlendFactorOne
+ case driver.BlendFactorOneMinusSrcAlpha:
+ return C.MTLBlendFactorOneMinusSourceAlpha
+ case driver.BlendFactorDstColor:
+ return C.MTLBlendFactorDestinationColor
+ default:
+ panic("unsupported blend factor")
+func vertFormatFor(f shader.InputLocation) C.MTLVertexFormat {
+ t := f.Type
+ s := f.Size
+ switch {
+ case t == shader.DataTypeFloat && s == 1:
+ return C.MTLVertexFormatFloat
+ case t == shader.DataTypeFloat && s == 2:
+ return C.MTLVertexFormatFloat2
+ case t == shader.DataTypeFloat && s == 3:
+ return C.MTLVertexFormatFloat3
+ case t == shader.DataTypeFloat && s == 4:
+ return C.MTLVertexFormatFloat4
+ default:
+ panic("unsupported data type")
+func pixelFormatFor(f driver.TextureFormat) C.MTLPixelFormat {
+ switch f {
+ case driver.TextureFormatFloat:
+ return C.MTLPixelFormatR16Float
+ case driver.TextureFormatRGBA8:
+ return C.MTLPixelFormatRGBA8Unorm
+ case driver.TextureFormatSRGBA:
+ return C.MTLPixelFormatRGBA8Unorm_sRGB
+ default:
+ panic("unsupported pixel format")
+func (b *Backend) NewFramebuffer(tex driver.Texture) (driver.Framebuffer, error) {
+ t := tex.(*Texture)
+ C.CFRetain(t.texture)
+ fbo := &Framebuffer{backend: b, texture: t.texture}
+ return fbo, nil
+func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer, error) {
+ // Transfer buffer contents in command encoders on every use for
+ // smaller buffers. The advantage is that buffer re-use during a frame
+ // won't occur a GPU wait.
+ // We can't do this for buffers written to by the GPU and read by the client,
+ // and Metal doesn't require a buffer for indexed draws.
+ if size <= 4096 && typ&(driver.BufferBindingShaderStorageWrite|driver.BufferBindingIndices) == 0 {
+ return &Buffer{size: size, store: make([]byte, size)}, nil
+ buf := C.newBuffer(b.dev, C.NSUInteger(size), C.MTLResourceStorageModePrivate)
+ return &Buffer{backend: b, size: size, buffer: buf}, nil
+func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding, data []byte) (driver.Buffer, error) {
+ buf, err := b.NewBuffer(typ, len(data))
+ if err != nil {
+ return nil, err
+ buf.Upload(data)
+ return buf, nil
+func (b *Backend) NewComputeProgram(src shader.Sources) (driver.Program, error) {
+ sh, err := b.newShader(src)
+ if err != nil {
+ return nil, err
+ defer sh.Release()
+ pipe := C.newComputePipeline(b.dev, sh.function)
+ if pipe == 0 {
+ return nil, fmt.Errorf("metal: compute program %q load failed", src.Name)
+ return &Program{pipeline: pipe, groupSize: src.WorkgroupSize}, nil
+func (b *Backend) NewVertexShader(src shader.Sources) (driver.VertexShader, error) {
+ return b.newShader(src)
+func (b *Backend) NewFragmentShader(src shader.Sources) (driver.FragmentShader, error) {
+ return b.newShader(src)
+func (b *Backend) newShader(src shader.Sources) (*Shader, error) {
+ vsrc := []byte(src.MetalLib)
+ cname := C.CString(src.Name)
+ defer C.free(unsafe.Pointer(cname))
+ vlib := C.newLibrary(b.dev, cname, unsafe.Pointer(&vsrc[0]), C.size_t(len(vsrc)))
+ if vlib == 0 {
+ return nil, fmt.Errorf("metal: vertex shader %q load failed", src.Name)
+ defer C.CFRelease(vlib)
+ funcName := C.CString("main0")
+ defer C.free(unsafe.Pointer(funcName))
+ f := C.libraryNewFunction(vlib, funcName)
+ if f == 0 {
+ return nil, fmt.Errorf("metal: main function not found in %q", src.Name)
+ return &Shader{function: f, inputs: src.Inputs}, nil
+func (b *Backend) Viewport(x, y, width, height int) {
+ b.newState.viewport = C.MTLViewport{
+ originX: C.double(x),
+ originY: C.double(y),
+ width: C.double(width),
+ height: C.double(height),
+ znear: 0.0,
+ zfar: 1.0,
+func (b *Backend) DrawArrays(mode driver.DrawMode, off, count int) {
+ enc := b.encodeState()
+ t := primitiveFor(mode)
+ C.renderEncDrawPrimitives(enc, t, C.NSUInteger(off), C.NSUInteger(count))
+func (b *Backend) DrawElements(mode driver.DrawMode, off, count int) {
+ enc := b.encodeState()
+ t := primitiveFor(mode)
+ C.renderEncDrawIndexedPrimitives(enc, t, b.indexBuf.buffer, C.NSUInteger(off), C.NSUInteger(count))
+func primitiveFor(mode driver.DrawMode) C.MTLPrimitiveType {
+ switch mode {
+ case driver.DrawModeTriangles:
+ return C.MTLPrimitiveTypeTriangle
+ case driver.DrawModeTriangleStrip:
+ return C.MTLPrimitiveTypeTriangleStrip
+ default:
+ panic("metal: unknown draw mode")
+func (b *Backend) BindImageTexture(unit int, tex driver.Texture, access driver.AccessBits, f driver.TextureFormat) {
+ b.newState.textures[unit] = tex.(*Texture)
+func (b *Backend) MemoryBarrier() {}
+func (b *Backend) startCompute() C.CFTypeRef {
+ if b.computeEnc != 0 {
+ return b.computeEnc
+ b.endEncoder()
+ b.ensureCmdBuffer()
+ b.computeEnc = C.cmdBufferComputeEncoder(b.cmdBuffer)
+ if b.computeEnc == 0 {
+ panic("metal: [MTLCommandBuffer computeCommandEncoder:] failed")
+ b.state = state{}
+ return b.computeEnc
+func (b *Backend) DispatchCompute(x, y, z int) {
+ enc := b.startCompute()
+ p := b.newState.program
+ if p != b.state.program {
+ C.computeEncSetPipeline(enc, p.pipeline)
+ for i, t := range b.newState.textures {
+ current := b.state.textures[i]
+ if t != current {
+ C.computeEncSetTexture(enc, C.NSUInteger(i), t.texture)
+ for i, buf := range b.newState.buffers {
+ b.bufSizes[i] = uint32(buf.size)
+ current := b.state.buffers[i]
+ if buf.buffer != 0 {
+ if buf != current {
+ C.computeEncSetBuffer(enc, C.NSUInteger(i), buf.buffer)
+ } else if buf.size > 0 {
+ C.computeEncSetBytes(enc, unsafe.Pointer(&buf.store[0]), C.NSUInteger(buf.size), C.NSUInteger(i))
+ if n := len(b.newState.buffers); n > 0 {
+ C.computeEncSetBytes(enc, unsafe.Pointer(&b.bufSizes[0]), C.NSUInteger(n*4), spvBufferSizeConstantsBinding)
+ threadgroupsPerGrid := C.MTLSize{
+ width: C.NSUInteger(x), height: C.NSUInteger(y), depth: C.NSUInteger(z),
+ threadsPerThreadgroup := C.MTLSize{
+ width: C.NSUInteger(p.groupSize[0]), height: C.NSUInteger(p.groupSize[1]), depth: C.NSUInteger(p.groupSize[2]),
+ C.computeEncDispatch(enc, threadgroupsPerGrid, threadsPerThreadgroup)
+ b.state = b.newState
+func (b *Backend) stagingBuffer(size int) (C.CFTypeRef, int) {
+ if b.stagingBuf == 0 || b.stagingOff+size > len(bufferStore(b.stagingBuf)) {
+ if b.stagingBuf != 0 {
+ C.CFRelease(b.stagingBuf)
+ cap := 2 * (b.stagingOff + size)
+ b.stagingBuf = C.newBuffer(b.dev, C.NSUInteger(cap), C.MTLResourceStorageModeShared)
+ if b.stagingBuf == 0 {
+ panic(fmt.Errorf("metal: failed to allocate %d bytes of staging buffer", cap))
+ b.stagingOff = 0
+ off := b.stagingOff
+ b.stagingOff += size
+ return b.stagingBuf, off
+func (t *Texture) Upload(offset, size image.Point, pixels []byte, stride int) {
+ if len(pixels) == 0 {
+ return
+ if stride == 0 {
+ stride = size.X * 4
+ buf, off := t.backend.stagingBuffer(len(pixels))
+ store := bufferSlice(buf, off, len(pixels))
+ copy(store, pixels)
+ enc := t.backend.startBlit()
+ orig := C.MTLOrigin{
+ x: C.NSUInteger(offset.X),
+ y: C.NSUInteger(offset.Y),
+ msize := C.MTLSize{
+ width: C.NSUInteger(size.X),
+ height: C.NSUInteger(size.Y),
+ depth: 1,
+ C.blitEncCopyBufferToTexture(enc, buf, t.texture, C.NSUInteger(off), C.NSUInteger(stride), C.NSUInteger(len(store)), msize, orig)
+func (t *Texture) Release() {
+ C.CFRelease(t.texture)
+ C.CFRelease(t.sampler)
+ *t = Texture{}
+func (p *Pipeline) Release() {
+ C.CFRelease(p.pipeline)
+ *p = Pipeline{}
+func (b *Backend) BindTexture(unit int, tex driver.Texture) {
+ t := tex.(*Texture)
+ b.newState.textures[unit] = t
+func (b *Backend) beginPass() C.CFTypeRef {
+ r := b.newState.renderPass
+ if r == b.state.renderPass {
+ return b.renderEnc
+ b.endEncoder()
+ var act C.MTLLoadAction
+ switch r.loadAction {
+ case driver.LoadActionKeep:
+ act = C.MTLLoadActionLoad
+ case driver.LoadActionClear:
+ act = C.MTLLoadActionClear
+ case driver.LoadActionInvalidate:
+ act = C.MTLLoadActionDontCare
+ b.ensureCmdBuffer()
+ c := r.clearColor
+ b.renderEnc = C.cmdBufferRenderEncoder(b.cmdBuffer, r.framebuffer.texture, act, C.float(c[0]), C.float(c[1]), C.float(c[2]), C.float(c[3]))
+ if b.renderEnc == 0 {
+ panic("metal: [MTLCommandBuffer renderCommandEncoderWithDescriptor:] failed")
+ r.loadAction = driver.LoadActionKeep
+ b.newState.renderPass = r
+ b.state.renderPass = r
+ return b.renderEnc
+func (b *Backend) ensureCmdBuffer() {
+ if b.cmdBuffer != 0 {
+ return
+ b.cmdBuffer = C.queueNewBuffer(b.queue)
+ if b.cmdBuffer == 0 {
+ panic("metal: [MTLCommandQueue cmdBuffer] failed")
+func (b *Backend) encodeState() C.CFTypeRef {
+ enc := b.beginPass()
+ for i, t := range b.newState.textures {
+ current := b.state.textures[i]
+ if t != current {
+ C.renderEncSetFragmentTexture(enc, C.NSUInteger(i), t.texture)
+ C.renderEncSetFragmentSamplerState(enc, C.NSUInteger(i), t.sampler)
+ if p := b.newState.pipeline; p != b.state.pipeline {
+ C.renderEncSetRenderPipelineState(enc, p.pipeline)
+ if bf := b.newState.fragUniforms; bf != nil {
+ if bf.buffer != 0 {
+ if bf != b.state.fragUniforms {
+ C.renderEncSetFragmentBuffer(enc, bf.buffer, uniformBufferIndex, 0)
+ } else if bf.size > 0 {
+ C.renderEncSetFragmentBytes(enc, unsafe.Pointer(&bf.store[0]), C.NSUInteger(bf.size), uniformBufferIndex)
+ if bf := b.newState.vertUniforms; bf != nil {
+ if bf.buffer != 0 {
+ if bf != b.state.vertUniforms {
+ C.renderEncSetVertexBuffer(enc, bf.buffer, uniformBufferIndex, 0)
+ } else if bf.size > 0 {
+ C.renderEncSetVertexBytes(enc, unsafe.Pointer(&bf.store[0]), C.NSUInteger(bf.size), uniformBufferIndex)
+ if v := b.newState.vertices; v.buffer != nil {
+ if v.buffer.buffer != 0 {
+ if v != b.state.vertices {
+ C.renderEncSetVertexBuffer(enc, v.buffer.buffer, attributeBufferIndex, C.NSUInteger(v.offset))
+ } else if n := v.buffer.size - v.offset; n > 0 {
+ C.renderEncSetVertexBytes(enc, unsafe.Pointer(&v.buffer.store[v.offset]), C.NSUInteger(n), attributeBufferIndex)
+ if vp := b.newState.viewport; vp != b.state.viewport {
+ C.renderEncViewport(enc, vp)
+ b.state = b.newState
+ return enc
+func (b *Backend) BindPipeline(pipe driver.Pipeline) {
+ p := pipe.(*Pipeline)
+ b.newState.pipeline = p
+func (b *Backend) BindProgram(prog driver.Program) {
+ b.newState.program = prog.(*Program)
+func (s *Shader) Release() {
+ C.CFRelease(s.function)
+ *s = Shader{}
+func (p *Program) Release() {
+ C.CFRelease(p.pipeline)
+ *p = Program{}
+func (b *Backend) BindStorageBuffer(binding int, buffer driver.Buffer) {
+ b.newState.buffers[binding] = buffer.(*Buffer)
+func (b *Backend) BindVertexUniforms(buf driver.Buffer) {
+ bf := buf.(*Buffer)
+ b.newState.vertUniforms = bf
+func (b *Backend) BindFragmentUniforms(buf driver.Buffer) {
+ bf := buf.(*Buffer)
+ b.newState.fragUniforms = bf
+func (b *Backend) BindVertexBuffer(buf driver.Buffer, offset int) {
+ bf := buf.(*Buffer)
+ b.newState.vertices.buffer = bf
+ b.newState.vertices.offset = offset
+func (b *Backend) BindIndexBuffer(buf driver.Buffer) {
+ b.indexBuf = buf.(*Buffer)
+func (b *Buffer) Download(data []byte) error {
+ if len(data) > b.size {
+ panic(fmt.Errorf("len(data) (%d) larger than len(content) (%d)", len(data), b.size))
+ buf, off := b.backend.stagingBuffer(len(data))
+ enc := b.backend.startBlit()
+ C.blitEncCopyBufferToBuffer(enc, b.buffer, buf, 0, C.NSUInteger(off), C.NSUInteger(len(data)))
+ b.backend.endCmdBuffer(true)
+ store := bufferSlice(buf, off, len(data))
+ copy(data, store)
+ return nil
+func (b *Buffer) Upload(data []byte) {
+ if len(data) > b.size {
+ panic(fmt.Errorf("len(data) (%d) larger than len(content) (%d)", len(data), b.size))
+ if b.buffer == 0 {
+ copy(b.store, data)
+ return
+ buf, off := b.backend.stagingBuffer(len(data))
+ store := bufferSlice(buf, off, len(data))
+ copy(store, data)
+ enc := b.backend.startBlit()
+ C.blitEncCopyBufferToBuffer(enc, buf, b.buffer, C.NSUInteger(off), 0, C.NSUInteger(len(store)))
+func bufferStore(buf C.CFTypeRef) []byte {
+ addr := C.bufferAddress(buf)
+ n := C.bufferLength(buf)
+ return (*(*[1 << 30]byte)(addr))[:n:n]
+func bufferSlice(buf C.CFTypeRef, off, len int) []byte {
+ store := bufferStore(buf)
+ return store[off : off+len]
+func (b *Buffer) Release() {
+ if b.buffer != 0 {
+ C.CFRelease(b.buffer)
+ *b = Buffer{}
+func (f *Framebuffer) ReadPixels(src image.Rectangle, pixels []byte) error {
+ if len(pixels) == 0 {
+ return nil
+ sz := src.Size()
+ orig := C.MTLOrigin{
+ x: C.NSUInteger(src.Min.X),
+ y: C.NSUInteger(src.Min.Y),
+ msize := C.MTLSize{
+ width: C.NSUInteger(sz.X),
+ height: C.NSUInteger(sz.Y),
+ depth: 1,
+ stride := 4 * sz.X
+ buf, off := f.backend.stagingBuffer(len(pixels))
+ enc := f.backend.startBlit()
+ C.blitEncCopyTextureToBuffer(enc, f.texture, buf, C.NSUInteger(off), C.NSUInteger(stride), C.NSUInteger(len(pixels)), msize, orig)
+ f.backend.endCmdBuffer(true)
+ store := bufferSlice(buf, off, len(pixels))
+ copy(pixels, store)
+ return nil
+func (b *Backend) BindFramebuffer(fbo driver.Framebuffer, desc driver.LoadDesc) {
+ b.newState.renderPass.framebuffer = fbo.(*Framebuffer)
+ b.newState.renderPass.loadAction = desc.Action
+ c := desc.ClearColor
+ b.newState.renderPass.clearColor = [4]float32{c.R, c.G, c.B, c.A}
+ b.beginPass()
+func (b *Backend) endEncoder() {
+ if b.renderEnc != 0 {
+ C.renderEncEnd(b.renderEnc)
+ C.CFRelease(b.renderEnc)
+ b.renderEnc = 0
+ b.state = state{}
+ if b.computeEnc != 0 {
+ C.computeEncEnd(b.computeEnc)
+ C.CFRelease(b.computeEnc)
+ b.computeEnc = 0
+ b.state = state{}
+ if b.blitEnc != 0 {
+ C.blitEncEnd(b.blitEnc)
+ C.CFRelease(b.blitEnc)
+ b.blitEnc = 0
+ b.state = state{}
+func (f *Framebuffer) Release() {
+ if f.foreign {
+ panic("metal: invalid release of external framebuffer")
+ C.CFRelease(f.texture)
+ *f = Framebuffer{}
+func (f *Framebuffer) ImplementsRenderTarget() {}