shiny/driver/mtldriver: add start of a Metal API-based driver

The OpenGL API has been deprecated on macOS as of version 10.14.¹
The replacement for OpenGL on macOS is the Metal API.² This change adds
the start of a Metal API-based shiny driver. It can be activated by
specifying the -tags=metal build tag on macOS 10.13+. For example:

	go run -tags='example metal' golang.org/x/exp/shiny/example/icongallery

The goal of this CL has been to create an MVP. As a result, the focus is
on simplicity and correctness. Performance optimizations can come later.

The Metal API is currently used only to present the final pixels to
the screen. All rendering is performed on the CPU via the image/draw
algorithms (see https://blog.golang.org/go-imagedraw-package).
Future work is to use mtl.Buffer, mtl.Texture, etc., to do more of
the rendering work on the GPU.

From what I've observed so far, the Metal API is looking to be a great
fit for implementing the screen.Screen interface. But we'll learn more
when it's used to a greater extent of its full performance potential.

GLFW v3.2.1 is used for window creation, receiving input events, etc.
Package dmitri.shuralyov.com/gpu/mtl is used for Metal API access.
Helper packages for Apple's Core Animation and AppKit APIs are copied.

References:

¹ https://developer.apple.com/documentation/macos_release_notes/macos_mojave_10_14_release_notes?language=objc#3035786
² https://developer.apple.com/metal/

Change-Id: I0e02d660b776820ca499bfe7d67e47a9866d530c
Reviewed-on: https://go-review.googlesource.com/c/exp/+/171025
Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/go.mod b/go.mod
index cb9f99c..06739c5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,9 @@
 go 1.12
 
 require (
+	dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9
 	github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802
+	github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1
 	golang.org/x/image v0.0.0-20190802002840-cff245a6509b
 	golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028
 	golang.org/x/mod v0.1.0
diff --git a/go.sum b/go.sum
index 7f7395b..2a7c68a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,9 @@
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
diff --git a/shiny/driver/driver_darwin.go b/shiny/driver/driver_darwin.go
index 2af9550..ee59ac6 100644
--- a/shiny/driver/driver_darwin.go
+++ b/shiny/driver/driver_darwin.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// +build darwin
+// +build darwin,!metal
 
 package driver
 
diff --git a/shiny/driver/mtldriver/buffer.go b/shiny/driver/mtldriver/buffer.go
new file mode 100644
index 0000000..c9a10ed
--- /dev/null
+++ b/shiny/driver/mtldriver/buffer.go
@@ -0,0 +1,19 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+package mtldriver
+
+import "image"
+
+// bufferImpl implements screen.Buffer.
+type bufferImpl struct {
+	rgba *image.RGBA
+}
+
+func (*bufferImpl) Release()                  {}
+func (b *bufferImpl) Size() image.Point       { return b.rgba.Rect.Max }
+func (b *bufferImpl) Bounds() image.Rectangle { return b.rgba.Rect }
+func (b *bufferImpl) RGBA() *image.RGBA       { return b.rgba }
diff --git a/shiny/driver/mtldriver/internal/appkit/appkit.go b/shiny/driver/mtldriver/internal/appkit/appkit.go
new file mode 100644
index 0000000..f6f6d5d
--- /dev/null
+++ b/shiny/driver/mtldriver/internal/appkit/appkit.go
@@ -0,0 +1,74 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+// Package appkit provides access to Apple's AppKit API
+// (https://developer.apple.com/documentation/appkit).
+//
+// This package is in very early stages of development.
+// It's a minimal implementation with scope limited to
+// supporting mtldriver.
+//
+// It was copied from dmitri.shuralyov.com/gpu/mtl/example/movingtriangle/internal/ns.
+package appkit
+
+import (
+	"unsafe"
+
+	"golang.org/x/exp/shiny/driver/mtldriver/internal/coreanim"
+)
+
+/*
+#include "appkit.h"
+*/
+import "C"
+
+// Window is a window that an app displays on the screen.
+//
+// Reference: https://developer.apple.com/documentation/appkit/nswindow.
+type Window struct {
+	window unsafe.Pointer
+}
+
+// NewWindow returns a Window that wraps an existing NSWindow * pointer.
+func NewWindow(window unsafe.Pointer) Window {
+	return Window{window}
+}
+
+// ContentView returns the window's content view, the highest accessible View
+// in the window's view hierarchy.
+//
+// Reference: https://developer.apple.com/documentation/appkit/nswindow/1419160-contentview.
+func (w Window) ContentView() View {
+	return View{C.Window_ContentView(w.window)}
+}
+
+// View is the infrastructure for drawing, printing, and handling events in an app.
+//
+// Reference: https://developer.apple.com/documentation/appkit/nsview.
+type View struct {
+	view unsafe.Pointer
+}
+
+// SetLayer sets v.layer to l.
+//
+// Reference: https://developer.apple.com/documentation/appkit/nsview/1483298-layer.
+func (v View) SetLayer(l coreanim.Layer) {
+	C.View_SetLayer(v.view, l.Layer())
+}
+
+// SetWantsLayer sets v.wantsLayer to wantsLayer.
+//
+// Reference: https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer.
+func (v View) SetWantsLayer(wantsLayer bool) {
+	C.View_SetWantsLayer(v.view, toCBool(wantsLayer))
+}
+
+func toCBool(b bool) C.BOOL {
+	if b {
+		return 1
+	}
+	return 0
+}
diff --git a/shiny/driver/mtldriver/internal/appkit/appkit.h b/shiny/driver/mtldriver/internal/appkit/appkit.h
new file mode 100644
index 0000000..eb3a92c
--- /dev/null
+++ b/shiny/driver/mtldriver/internal/appkit/appkit.h
@@ -0,0 +1,12 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+typedef signed char BOOL;
+
+void * Window_ContentView(void * window);
+
+void View_SetLayer(void * view, void * layer);
+void View_SetWantsLayer(void * view, BOOL wantsLayer);
diff --git a/shiny/driver/mtldriver/internal/appkit/appkit.m b/shiny/driver/mtldriver/internal/appkit/appkit.m
new file mode 100644
index 0000000..8bb74e3
--- /dev/null
+++ b/shiny/driver/mtldriver/internal/appkit/appkit.m
@@ -0,0 +1,20 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+#import <Cocoa/Cocoa.h>
+#include "appkit.h"
+
+void * Window_ContentView(void * window) {
+	return ((NSWindow *)window).contentView;
+}
+
+void View_SetLayer(void * view, void * layer) {
+	((NSView *)view).layer = (CALayer *)layer;
+}
+
+void View_SetWantsLayer(void * view, BOOL wantsLayer) {
+	((NSView *)view).wantsLayer = wantsLayer;
+}
diff --git a/shiny/driver/mtldriver/internal/coreanim/coreanim.go b/shiny/driver/mtldriver/internal/coreanim/coreanim.go
new file mode 100644
index 0000000..857c5e1
--- /dev/null
+++ b/shiny/driver/mtldriver/internal/coreanim/coreanim.go
@@ -0,0 +1,146 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+// Package coreanim provides access to Apple's Core Animation API
+// (https://developer.apple.com/documentation/quartzcore).
+//
+// This package is in very early stages of development.
+// It's a minimal implementation with scope limited to
+// supporting mtldriver.
+//
+// It was copied from dmitri.shuralyov.com/gpu/mtl/example/movingtriangle/internal/ca.
+package coreanim
+
+import (
+	"errors"
+	"unsafe"
+
+	"dmitri.shuralyov.com/gpu/mtl"
+)
+
+/*
+#cgo LDFLAGS: -framework QuartzCore -framework Foundation
+#include "coreanim.h"
+*/
+import "C"
+
+// Layer is an object that manages image-based content and
+// allows you to perform animations on that content.
+//
+// Reference: https://developer.apple.com/documentation/quartzcore/calayer.
+type Layer interface {
+	// Layer returns the underlying CALayer * pointer.
+	Layer() unsafe.Pointer
+}
+
+// MetalLayer is a Core Animation Metal layer, a layer that manages a pool of Metal drawables.
+//
+// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer.
+type MetalLayer struct {
+	metalLayer unsafe.Pointer
+}
+
+// MakeMetalLayer creates a new Core Animation Metal layer.
+//
+// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer.
+func MakeMetalLayer() MetalLayer {
+	return MetalLayer{C.MakeMetalLayer()}
+}
+
+// Layer implements the Layer interface.
+func (ml MetalLayer) Layer() unsafe.Pointer { return ml.metalLayer }
+
+// PixelFormat returns the pixel format of textures for rendering layer content.
+//
+// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer/1478155-pixelformat.
+func (ml MetalLayer) PixelFormat() mtl.PixelFormat {
+	return mtl.PixelFormat(C.MetalLayer_PixelFormat(ml.metalLayer))
+}
+
+// SetDevice sets the Metal device responsible for the layer's drawable resources.
+//
+// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer/1478163-device.
+func (ml MetalLayer) SetDevice(device mtl.Device) {
+	C.MetalLayer_SetDevice(ml.metalLayer, device.Device())
+}
+
+// SetPixelFormat controls the pixel format of textures for rendering layer content.
+//
+// The pixel format for a Metal layer must be PixelFormatBGRA8UNorm, PixelFormatBGRA8UNormSRGB,
+// PixelFormatRGBA16Float, PixelFormatBGRA10XR, or PixelFormatBGRA10XRSRGB.
+// SetPixelFormat panics for other values.
+//
+// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer/1478155-pixelformat.
+func (ml MetalLayer) SetPixelFormat(pf mtl.PixelFormat) {
+	e := C.MetalLayer_SetPixelFormat(ml.metalLayer, C.uint16_t(pf))
+	if e != nil {
+		panic(errors.New(C.GoString(e)))
+	}
+}
+
+// SetMaximumDrawableCount controls the number of Metal drawables in the resource pool
+// managed by Core Animation.
+//
+// It can set to 2 or 3 only. SetMaximumDrawableCount panics for other values.
+//
+// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer/2938720-maximumdrawablecount.
+func (ml MetalLayer) SetMaximumDrawableCount(count int) {
+	e := C.MetalLayer_SetMaximumDrawableCount(ml.metalLayer, C.uint_t(count))
+	if e != nil {
+		panic(errors.New(C.GoString(e)))
+	}
+}
+
+// SetDisplaySyncEnabled controls whether the Metal layer and its drawables
+// are synchronized with the display's refresh rate.
+//
+// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer/2887087-displaysyncenabled.
+func (ml MetalLayer) SetDisplaySyncEnabled(enabled bool) {
+	C.MetalLayer_SetDisplaySyncEnabled(ml.metalLayer, toCBool(enabled))
+}
+
+// SetDrawableSize sets the size, in pixels, of textures for rendering layer content.
+//
+// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer/1478174-drawablesize.
+func (ml MetalLayer) SetDrawableSize(width, height int) {
+	C.MetalLayer_SetDrawableSize(ml.metalLayer, C.double(width), C.double(height))
+}
+
+// NextDrawable returns a Metal drawable.
+//
+// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer/1478172-nextdrawable.
+func (ml MetalLayer) NextDrawable() (MetalDrawable, error) {
+	md := C.MetalLayer_NextDrawable(ml.metalLayer)
+	if md == nil {
+		return MetalDrawable{}, errors.New("nextDrawable returned nil")
+	}
+
+	return MetalDrawable{md}, nil
+}
+
+// MetalDrawable is a displayable resource that can be rendered or written to by Metal.
+//
+// Reference: https://developer.apple.com/documentation/quartzcore/cametaldrawable.
+type MetalDrawable struct {
+	metalDrawable unsafe.Pointer
+}
+
+// Drawable implements the mtl.Drawable interface.
+func (md MetalDrawable) Drawable() unsafe.Pointer { return md.metalDrawable }
+
+// Texture returns a Metal texture object representing the drawable object's content.
+//
+// Reference: https://developer.apple.com/documentation/quartzcore/cametaldrawable/1478159-texture.
+func (md MetalDrawable) Texture() mtl.Texture {
+	return mtl.NewTexture(C.MetalDrawable_Texture(md.metalDrawable))
+}
+
+func toCBool(b bool) C.BOOL {
+	if b {
+		return 1
+	}
+	return 0
+}
diff --git a/shiny/driver/mtldriver/internal/coreanim/coreanim.h b/shiny/driver/mtldriver/internal/coreanim/coreanim.h
new file mode 100644
index 0000000..36cd06c
--- /dev/null
+++ b/shiny/driver/mtldriver/internal/coreanim/coreanim.h
@@ -0,0 +1,21 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+typedef signed char BOOL;
+typedef unsigned long uint_t;
+typedef unsigned short uint16_t;
+
+void * MakeMetalLayer();
+
+uint16_t     MetalLayer_PixelFormat(void * metalLayer);
+void         MetalLayer_SetDevice(void * metalLayer, void * device);
+const char * MetalLayer_SetPixelFormat(void * metalLayer, uint16_t pixelFormat);
+const char * MetalLayer_SetMaximumDrawableCount(void * metalLayer, uint_t maximumDrawableCount);
+void         MetalLayer_SetDisplaySyncEnabled(void * metalLayer, BOOL displaySyncEnabled);
+void         MetalLayer_SetDrawableSize(void * metalLayer, double width, double height);
+void *       MetalLayer_NextDrawable(void * metalLayer);
+
+void * MetalDrawable_Texture(void * drawable);
diff --git a/shiny/driver/mtldriver/internal/coreanim/coreanim.m b/shiny/driver/mtldriver/internal/coreanim/coreanim.m
new file mode 100644
index 0000000..7aecf83
--- /dev/null
+++ b/shiny/driver/mtldriver/internal/coreanim/coreanim.m
@@ -0,0 +1,58 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+#import <QuartzCore/QuartzCore.h>
+#include "coreanim.h"
+
+void * MakeMetalLayer() {
+	return [[CAMetalLayer alloc] init];
+}
+
+uint16_t MetalLayer_PixelFormat(void * metalLayer) {
+	return ((CAMetalLayer *)metalLayer).pixelFormat;
+}
+
+void MetalLayer_SetDevice(void * metalLayer, void * device) {
+	((CAMetalLayer *)metalLayer).device = (id<MTLDevice>)device;
+}
+
+const char * MetalLayer_SetPixelFormat(void * metalLayer, uint16_t pixelFormat) {
+	@try {
+		((CAMetalLayer *)metalLayer).pixelFormat = (MTLPixelFormat)pixelFormat;
+	}
+	@catch (NSException * exception) {
+		return exception.reason.UTF8String;
+	}
+	return NULL;
+}
+
+const char * MetalLayer_SetMaximumDrawableCount(void * metalLayer, uint_t maximumDrawableCount) {
+	if (@available(macOS 10.13.2, *)) {
+		@try {
+			((CAMetalLayer *)metalLayer).maximumDrawableCount = (NSUInteger)maximumDrawableCount;
+		}
+		@catch (NSException * exception) {
+			return exception.reason.UTF8String;
+		}
+	}
+	return NULL;
+}
+
+void MetalLayer_SetDisplaySyncEnabled(void * metalLayer, BOOL displaySyncEnabled) {
+	((CAMetalLayer *)metalLayer).displaySyncEnabled = displaySyncEnabled;
+}
+
+void MetalLayer_SetDrawableSize(void * metalLayer, double width, double height) {
+	((CAMetalLayer *)metalLayer).drawableSize = (CGSize){width, height};
+}
+
+void * MetalLayer_NextDrawable(void * metalLayer) {
+	return [(CAMetalLayer *)metalLayer nextDrawable];
+}
+
+void * MetalDrawable_Texture(void * metalDrawable) {
+	return ((id<CAMetalDrawable>)metalDrawable).texture;
+}
diff --git a/shiny/driver/mtldriver/mtldriver.go b/shiny/driver/mtldriver/mtldriver.go
new file mode 100644
index 0000000..505bffb
--- /dev/null
+++ b/shiny/driver/mtldriver/mtldriver.go
@@ -0,0 +1,246 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+// Package mtldriver provides a Metal driver for accessing a screen.
+//
+// At this time, the Metal API is used only to present the final pixels
+// to the screen. All rendering is performed on the CPU via the image/draw
+// algorithms. Future work is to use mtl.Buffer, mtl.Texture, etc., and
+// do more of the rendering work on the GPU.
+package mtldriver
+
+import (
+	"runtime"
+	"unsafe"
+
+	"dmitri.shuralyov.com/gpu/mtl"
+	"github.com/go-gl/glfw/v3.2/glfw"
+	"golang.org/x/exp/shiny/driver/internal/errscreen"
+	"golang.org/x/exp/shiny/driver/mtldriver/internal/appkit"
+	"golang.org/x/exp/shiny/driver/mtldriver/internal/coreanim"
+	"golang.org/x/exp/shiny/screen"
+	"golang.org/x/mobile/event/key"
+	"golang.org/x/mobile/event/mouse"
+	"golang.org/x/mobile/event/paint"
+	"golang.org/x/mobile/event/size"
+)
+
+func init() {
+	runtime.LockOSThread()
+}
+
+// Main is called by the program's main function to run the graphical
+// application.
+//
+// It calls f on the Screen, possibly in a separate goroutine, as some OS-
+// specific libraries require being on 'the main thread'. It returns when f
+// returns.
+func Main(f func(screen.Screen)) {
+	if err := main(f); err != nil {
+		f(errscreen.Stub(err))
+	}
+}
+
+func main(f func(screen.Screen)) error {
+	device, err := mtl.CreateSystemDefaultDevice()
+	if err != nil {
+		return err
+	}
+	err = glfw.Init()
+	if err != nil {
+		return err
+	}
+	defer glfw.Terminate()
+	glfw.WindowHint(glfw.ClientAPI, glfw.NoAPI)
+	var (
+		done            = make(chan struct{})
+		newWindowCh     = make(chan newWindowReq, 1)
+		releaseWindowCh = make(chan releaseWindowReq, 1)
+	)
+	go func() {
+		f(&screenImpl{
+			newWindowCh: newWindowCh,
+		})
+		close(done)
+		glfw.PostEmptyEvent() // Break main loop out of glfw.WaitEvents so it can receive on done.
+	}()
+	for {
+		select {
+		case <-done:
+			return nil
+		case req := <-newWindowCh:
+			w, err := newWindow(device, releaseWindowCh, req.opts)
+			req.respCh <- newWindowResp{w, err}
+		case req := <-releaseWindowCh:
+			req.window.Destroy()
+			req.respCh <- struct{}{}
+		default:
+			glfw.WaitEvents()
+		}
+	}
+}
+
+type newWindowReq struct {
+	opts   *screen.NewWindowOptions
+	respCh chan newWindowResp
+}
+
+type newWindowResp struct {
+	w   screen.Window
+	err error
+}
+
+type releaseWindowReq struct {
+	window *glfw.Window
+	respCh chan struct{}
+}
+
+// newWindow creates a new GLFW window.
+// It must be called on the main thread.
+func newWindow(device mtl.Device, releaseWindowCh chan releaseWindowReq, opts *screen.NewWindowOptions) (screen.Window, error) {
+	width, height := optsSize(opts)
+	window, err := glfw.CreateWindow(width, height, opts.GetTitle(), nil, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	ml := coreanim.MakeMetalLayer()
+	ml.SetDevice(device)
+	ml.SetPixelFormat(mtl.PixelFormatBGRA8UNorm)
+	ml.SetMaximumDrawableCount(3)
+	ml.SetDisplaySyncEnabled(true)
+	cv := appkit.NewWindow(unsafe.Pointer(window.GetCocoaWindow())).ContentView()
+	cv.SetLayer(ml)
+	cv.SetWantsLayer(true)
+
+	w := &windowImpl{
+		device:          device,
+		window:          window,
+		releaseWindowCh: releaseWindowCh,
+		ml:              ml,
+		cq:              device.MakeCommandQueue(),
+	}
+
+	// Set callbacks.
+	framebufferSizeCallback := func(_ *glfw.Window, width, height int) {
+		w.Send(size.Event{
+			WidthPx:  width,
+			HeightPx: height,
+			// TODO(dmitshur): ppp,
+		})
+		w.Send(paint.Event{External: true})
+	}
+	window.SetFramebufferSizeCallback(framebufferSizeCallback)
+	window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
+		const scale = 2 // TODO(dmitshur): compute dynamically
+		w.Send(mouse.Event{X: float32(x * scale), Y: float32(y * scale)})
+	})
+	window.SetMouseButtonCallback(func(_ *glfw.Window, b glfw.MouseButton, a glfw.Action, mods glfw.ModifierKey) {
+		btn := glfwMouseButton(b)
+		if btn == mouse.ButtonNone {
+			return
+		}
+		const scale = 2 // TODO(dmitshur): compute dynamically
+		x, y := window.GetCursorPos()
+		w.Send(mouse.Event{
+			X: float32(x * scale), Y: float32(y * scale),
+			Button:    btn,
+			Direction: glfwMouseDirection(a),
+			// TODO(dmitshur): set Modifiers
+		})
+	})
+	window.SetKeyCallback(func(_ *glfw.Window, k glfw.Key, _ int, a glfw.Action, mods glfw.ModifierKey) {
+		code := glfwKeyCode(k)
+		if code == key.CodeUnknown {
+			return
+		}
+		w.Send(key.Event{
+			Code:      code,
+			Direction: glfwKeyDirection(a),
+			// TODO(dmitshur): set Modifiers
+		})
+	})
+	// TODO(dmitshur): set CharModsCallback to catch text (runes) that are typed,
+	//                 and perhaps try to unify key pressed + character typed into single event
+	window.SetCloseCallback(func(*glfw.Window) {
+		w.lifecycler.SetDead(true)
+		w.lifecycler.SendEvent(w, nil)
+	})
+
+	// TODO(dmitshur): more fine-grained tracking of whether window is visible and/or focused
+	w.lifecycler.SetDead(false)
+	w.lifecycler.SetVisible(true)
+	w.lifecycler.SetFocused(true)
+	w.lifecycler.SendEvent(w, nil)
+
+	// Send the initial size and paint events.
+	width, height = window.GetFramebufferSize()
+	framebufferSizeCallback(window, width, height)
+
+	return w, nil
+}
+
+func optsSize(opts *screen.NewWindowOptions) (width, height int) {
+	width, height = 1024/2, 768/2
+	if opts != nil {
+		if opts.Width > 0 {
+			width = opts.Width
+		}
+		if opts.Height > 0 {
+			height = opts.Height
+		}
+	}
+	return width, height
+}
+
+func glfwMouseButton(button glfw.MouseButton) mouse.Button {
+	switch button {
+	case glfw.MouseButtonLeft:
+		return mouse.ButtonLeft
+	case glfw.MouseButtonRight:
+		return mouse.ButtonRight
+	case glfw.MouseButtonMiddle:
+		return mouse.ButtonMiddle
+	default:
+		return mouse.ButtonNone
+	}
+}
+
+func glfwMouseDirection(action glfw.Action) mouse.Direction {
+	switch action {
+	case glfw.Press:
+		return mouse.DirPress
+	case glfw.Release:
+		return mouse.DirRelease
+	default:
+		panic("unreachable")
+	}
+}
+
+func glfwKeyCode(k glfw.Key) key.Code {
+	// TODO(dmitshur): support more keys
+	switch k {
+	case glfw.KeyEnter:
+		return key.CodeReturnEnter
+	case glfw.KeyEscape:
+		return key.CodeEscape
+	default:
+		return key.CodeUnknown
+	}
+}
+
+func glfwKeyDirection(action glfw.Action) key.Direction {
+	switch action {
+	case glfw.Press:
+		return key.DirPress
+	case glfw.Release:
+		return key.DirRelease
+	case glfw.Repeat:
+		return key.DirNone
+	default:
+		panic("unreachable")
+	}
+}
diff --git a/shiny/driver/mtldriver/screen.go b/shiny/driver/mtldriver/screen.go
new file mode 100644
index 0000000..7abba42
--- /dev/null
+++ b/shiny/driver/mtldriver/screen.go
@@ -0,0 +1,42 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+package mtldriver
+
+import (
+	"image"
+
+	"github.com/go-gl/glfw/v3.2/glfw"
+	"golang.org/x/exp/shiny/screen"
+)
+
+// screenImpl implements screen.Screen.
+type screenImpl struct {
+	newWindowCh chan newWindowReq
+}
+
+func (*screenImpl) NewBuffer(size image.Point) (screen.Buffer, error) {
+	return &bufferImpl{
+		rgba: image.NewRGBA(image.Rectangle{Max: size}),
+	}, nil
+}
+
+func (*screenImpl) NewTexture(size image.Point) (screen.Texture, error) {
+	return &textureImpl{
+		rgba: image.NewRGBA(image.Rectangle{Max: size}),
+	}, nil
+}
+
+func (s *screenImpl) NewWindow(opts *screen.NewWindowOptions) (screen.Window, error) {
+	respCh := make(chan newWindowResp)
+	s.newWindowCh <- newWindowReq{
+		opts:   opts,
+		respCh: respCh,
+	}
+	glfw.PostEmptyEvent() // Break main loop out of glfw.WaitEvents so it can receive on newWindowCh.
+	resp := <-respCh
+	return resp.w, resp.err
+}
diff --git a/shiny/driver/mtldriver/texture.go b/shiny/driver/mtldriver/texture.go
new file mode 100644
index 0000000..6a60dfd
--- /dev/null
+++ b/shiny/driver/mtldriver/texture.go
@@ -0,0 +1,32 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+package mtldriver
+
+import (
+	"image"
+	"image/color"
+
+	"golang.org/x/exp/shiny/screen"
+	"golang.org/x/image/draw"
+)
+
+// textureImpl implements screen.Texture.
+type textureImpl struct {
+	rgba *image.RGBA
+}
+
+func (*textureImpl) Release()                  {}
+func (t *textureImpl) Size() image.Point       { return t.rgba.Rect.Max }
+func (t *textureImpl) Bounds() image.Rectangle { return t.rgba.Rect }
+
+func (t *textureImpl) Upload(dp image.Point, src screen.Buffer, sr image.Rectangle) {
+	draw.Draw(t.rgba, sr.Sub(sr.Min).Add(dp), src.RGBA(), sr.Min, draw.Src)
+}
+
+func (t *textureImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) {
+	draw.Draw(t.rgba, dr, &image.Uniform{src}, image.Point{}, op)
+}
diff --git a/shiny/driver/mtldriver/window.go b/shiny/driver/mtldriver/window.go
new file mode 100644
index 0000000..7ba0d73
--- /dev/null
+++ b/shiny/driver/mtldriver/window.go
@@ -0,0 +1,119 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin
+
+package mtldriver
+
+import (
+	"image"
+	"image/color"
+	"log"
+
+	"dmitri.shuralyov.com/gpu/mtl"
+	"github.com/go-gl/glfw/v3.2/glfw"
+	"golang.org/x/exp/shiny/driver/internal/drawer"
+	"golang.org/x/exp/shiny/driver/internal/event"
+	"golang.org/x/exp/shiny/driver/internal/lifecycler"
+	"golang.org/x/exp/shiny/driver/mtldriver/internal/coreanim"
+	"golang.org/x/exp/shiny/screen"
+	"golang.org/x/image/draw"
+	"golang.org/x/image/math/f64"
+	"golang.org/x/mobile/event/size"
+)
+
+// windowImpl implements screen.Window.
+type windowImpl struct {
+	device          mtl.Device
+	window          *glfw.Window
+	releaseWindowCh chan releaseWindowReq
+	ml              coreanim.MetalLayer
+	cq              mtl.CommandQueue
+
+	event.Deque
+	lifecycler lifecycler.State
+
+	rgba    *image.RGBA
+	texture mtl.Texture // Used in Publish.
+}
+
+func (w *windowImpl) Release() {
+	respCh := make(chan struct{})
+	w.releaseWindowCh <- releaseWindowReq{
+		window: w.window,
+		respCh: respCh,
+	}
+	glfw.PostEmptyEvent() // Break main loop out of glfw.WaitEvents so it can receive on releaseWindowCh.
+	<-respCh
+}
+
+func (w *windowImpl) NextEvent() interface{} {
+	e := w.Deque.NextEvent()
+	if sz, ok := e.(size.Event); ok {
+		// TODO(dmitshur): this is the best place/time/frequency to do this
+		//                 I've found so far, but see if it can be even better
+
+		// Set drawable size, create backing image and texture.
+		w.ml.SetDrawableSize(sz.WidthPx, sz.HeightPx)
+		w.rgba = image.NewRGBA(image.Rectangle{Max: image.Point{X: sz.WidthPx, Y: sz.HeightPx}})
+		w.texture = w.device.MakeTexture(mtl.TextureDescriptor{
+			PixelFormat: mtl.PixelFormatRGBA8UNorm,
+			Width:       sz.WidthPx,
+			Height:      sz.HeightPx,
+			StorageMode: mtl.StorageModeManaged,
+		})
+	}
+	return e
+}
+
+func (w *windowImpl) Publish() screen.PublishResult {
+	// Copy w.rgba pixels into a texture.
+	region := mtl.RegionMake2D(0, 0, w.texture.Width, w.texture.Height)
+	bytesPerRow := 4 * w.texture.Width
+	w.texture.ReplaceRegion(region, 0, &w.rgba.Pix[0], uintptr(bytesPerRow))
+
+	drawable, err := w.ml.NextDrawable()
+	if err != nil {
+		log.Println("Window.Publish: couldn't get the next drawable:", err)
+		return screen.PublishResult{}
+	}
+
+	cb := w.cq.MakeCommandBuffer()
+
+	// Copy the texture into the drawable.
+	bce := cb.MakeBlitCommandEncoder()
+	bce.CopyFromTexture(
+		w.texture, 0, 0, mtl.Origin{}, mtl.Size{w.texture.Width, w.texture.Height, 1},
+		drawable.Texture(), 0, 0, mtl.Origin{})
+	bce.EndEncoding()
+
+	cb.PresentDrawable(drawable)
+	cb.Commit()
+
+	return screen.PublishResult{}
+}
+
+func (w *windowImpl) Upload(dp image.Point, src screen.Buffer, sr image.Rectangle) {
+	draw.Draw(w.rgba, sr.Sub(sr.Min).Add(dp), src.RGBA(), sr.Min, draw.Src)
+}
+
+func (w *windowImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) {
+	draw.Draw(w.rgba, dr, &image.Uniform{src}, image.Point{}, op)
+}
+
+func (w *windowImpl) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op, _ *screen.DrawOptions) {
+	draw.NearestNeighbor.Transform(w.rgba, src2dst, src.(*textureImpl).rgba, sr, op, nil)
+}
+
+func (w *windowImpl) DrawUniform(src2dst f64.Aff3, src color.Color, sr image.Rectangle, op draw.Op, _ *screen.DrawOptions) {
+	draw.NearestNeighbor.Transform(w.rgba, src2dst, &image.Uniform{src}, sr, op, nil)
+}
+
+func (w *windowImpl) Copy(dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op, opts *screen.DrawOptions) {
+	drawer.Copy(w, dp, src, sr, op, opts)
+}
+
+func (w *windowImpl) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op, opts *screen.DrawOptions) {
+	drawer.Scale(w, dr, src, sr, op, opts)
+}
diff --git a/shiny/driver/mtldriver_darwin.go b/shiny/driver/mtldriver_darwin.go
new file mode 100644
index 0000000..d94e633
--- /dev/null
+++ b/shiny/driver/mtldriver_darwin.go
@@ -0,0 +1,16 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin,metal
+
+package driver
+
+import (
+	"golang.org/x/exp/shiny/driver/mtldriver"
+	"golang.org/x/exp/shiny/screen"
+)
+
+func main(f func(screen.Screen)) {
+	mtldriver.Main(f)
+}