dmitri.shuralyov.com/service/change/...

fs: Add an open change for mtl package.

Still hard-coded mock data for now.

Update shurcool user to dmitshur.
dmitshur committed 11 months ago commit f0c4aee46f480a95d2b38fca1cde98f16466e592
fs/fs.go
@@ -15,39 +15,39 @@ type Service struct {
 	// Reactions, if not nil, is temporarily used as a place to store reactions.
 	Reactions reactions.Service
 }
 
 var s = struct {
-	changes []struct {
+	changes map[string][]struct {
 		change.Change
 		Timeline []interface{}
 		Commits  []change.Commit
 		Diffs    map[string][]byte // Key is commit SHA. "all" is diff of all commits combined.
 	}
 }{
-	changes: []struct {
+	changes: map[string][]struct {
 		change.Change
 		Timeline []interface{}
 		Commits  []change.Commit
 		Diffs    map[string][]byte
 	}{
-		{
+		"dmitri.shuralyov.com/font/woff2": {{
 			Change: change.Change{
 				ID:           1,
 				State:        change.MergedState,
 				Title:        "Initial implementation of woff2.",
 				Labels:       nil,
-				Author:       shurcool,
+				Author:       dmitshur,
 				CreatedAt:    time.Date(2018, 2, 12, 0, 9, 19, 621031866, time.UTC),
 				Replies:      1,
 				Commits:      3,
 				ChangedFiles: 5,
 			},
 			Timeline: []interface{}{
 				change.Comment{
 					ID:        "0",
-					User:      shurcool,
+					User:      dmitshur,
 					CreatedAt: time.Date(2018, 2, 12, 0, 9, 19, 621031866, time.UTC),
 					Body: `Add initial parser implementation.
 
 This is an initial implementation of a parser for the WOFF2 font
 packaging format.
@@ -71,18 +71,18 @@ Helps https://github.com/ConradIrwin/font/issues/1.
 
 For convenience, a ` + "`" + `godoc` + "`" + ` view of this change can be seen [here](https://redpen.io/rk9a75c358f45654a8).`,
 				},
 				change.Review{
 					ID:        "1",
-					User:      shurcool,
+					User:      dmitshur,
 					CreatedAt: time.Date(2018, 2, 20, 21, 49, 35, 536092503, time.UTC),
 					State:     change.Approved,
 					Body:      "There have been some out-of-band review comments that I've addressed. This will do for the initial version.\n\nLGTM.",
 				},
 				change.TimelineItem{
 					ID:        "2",
-					Actor:     shurcool,
+					Actor:     dmitshur,
 					CreatedAt: time.Date(2018, 2, 20, 21, 57, 47, 537746502, time.UTC),
 					Payload: change.MergedEvent{
 						CommitID:      "957792cbbdabb084d484a7dcfd1e5b1a739a0ced",
 						CommitHTMLURL: "https://dmitri.shuralyov.com/font/woff2/...$commit/957792cbbdabb084d484a7dcfd1e5b1a739a0ced",
 						RefName:       "master",
@@ -110,36 +110,87 @@ from the Go font source .ttf files located at
 https://go.googlesource.com/image/+/master/font/gofont/ttfs/.
 
 Add basic test coverage.
 
 Helps https://github.com/ConradIrwin/font/issues/1.`,
-				Author:     shurcool,
+				Author:     dmitshur,
 				AuthorTime: time.Date(2018, 2, 11, 20, 10, 28, 0, time.UTC),
 			}, {
 				SHA: "61339d441b319cd6ca35d952522f86cc42ad4b6e",
 				Message: `Update test for new API.
 
 The API had changed earlier, the test wasn't updated for it. This
 change fixes that, allowing tests to pass.`,
-				Author:     shurcool,
+				Author:     dmitshur,
 				AuthorTime: time.Date(2018, 2, 12, 20, 22, 29, 674711268, time.UTC),
 			}, {
 				SHA: "3b528c98b05508322be465a207f5ffd8258b8a96",
 				Message: `Add comment describing null-padding check.
 
 Also add a TODO comment to improve this check. It's not compliant with
 spec as is. Addressing this will be a part of future changes.`,
-				Author:     shurcool,
+				Author:     dmitshur,
 				AuthorTime: time.Date(2018, 2, 20, 21, 39, 17, 660912242, time.UTC),
 			}},
 			Diffs: map[string][]byte{
 				"all": []byte(diffAll),
 				"d2568fb6f10921b2d0c84d58bad14b2fadb88aa7": []byte(diffCommit1),
 				"61339d441b319cd6ca35d952522f86cc42ad4b6e": []byte(diffCommit2),
 				"3b528c98b05508322be465a207f5ffd8258b8a96": []byte(diffCommit3),
 			},
-		},
+		}},
+		"dmitri.shuralyov.com/gpu/mtl": {{
+			Change: change.Change{
+				ID:           1,
+				State:        change.OpenState,
+				Title:        "WIP: Add minimal API to support rendering to a window at 60 FPS.",
+				Labels:       nil,
+				Author:       dmitshur,
+				CreatedAt:    time.Date(2018, 10, 17, 2, 9, 9, 583606000, time.UTC),
+				Replies:      0,
+				Commits:      1,
+				ChangedFiles: 4,
+			},
+			Timeline: []interface{}{
+				change.Comment{
+					ID:        "0",
+					User:      dmitshur,
+					CreatedAt: time.Date(2018, 10, 17, 2, 9, 9, 583606000, time.UTC),
+					Body: `The goal of this change is to make it possible to use package mtl
+to render to a window at 60 FPS. It tries to add the minimum viable
+API that is needed.
+
+A new movingtriangle example is added as a demonstration of this
+functionality. It renders a triangle that follows the mouse cursor.
+
+**TODO:** A lot of the newly added API comes from Core Animation, AppKit
+frameworks, rather than Metal. As a result, they likely do not belong
+in this package and should be factored out.`,
+				},
+			},
+			Commits: []change.Commit{{
+				SHA: "fc76fa8984fb4a28ff383895e55e635e06bd32f0",
+				Message: `WIP: Add minimal API to support rendering to a window at 60 FPS.
+
+The goal of this change is to make it possible to use package mtl
+to render to a window at 60 FPS. It tries to add the minimum viable
+API that is needed.
+
+A new movingtriangle example is added as a demonstration of this
+functionality. It renders a triangle that follows the mouse cursor.
+
+TODO: A lot of the newly added API comes from Core Animation, AppKit
+frameworks, rather than Metal. As a result, they likely do not belong
+in this package and should be factored out.`,
+				Author:     dmitshur,
+				AuthorTime: time.Date(2018, 10, 17, 2, 9, 9, 583606000, time.UTC),
+			}},
+			Diffs: map[string][]byte{
+				"all": []byte(diffMtlAll),
+				"fc76fa8984fb4a28ff383895e55e635e06bd32f0": []byte(diffMtlCommit1),
+			},
+		}},
 	},
 }
 
 // List changes.
 func (*Service) List(ctx context.Context, repo string, opt change.ListOptions) ([]change.Change, error) {
@@ -153,15 +204,12 @@ func (*Service) List(ctx context.Context, repo string, opt change.ListOptions) (
 		counts = func(s change.State) bool { return true }
 	default:
 		// TODO: Map to 400 Bad Request HTTP error.
 		return nil, fmt.Errorf("invalid change.ListOptions.Filter value: %q", opt.Filter)
 	}
-	if repo != "dmitri.shuralyov.com/font/woff2" {
-		return nil, nil
-	}
 	var cs []change.Change
-	for _, c := range s.changes {
+	for _, c := range s.changes[repo] {
 		if !counts(c.State) {
 			continue
 		}
 		cs = append(cs, c.Change)
 	}
@@ -180,81 +228,87 @@ func (*Service) Count(ctx context.Context, repo string, opt change.ListOptions)
 		counts = func(s change.State) bool { return true }
 	default:
 		// TODO: Map to 400 Bad Request HTTP error.
 		return 0, fmt.Errorf("invalid change.ListOptions.Filter value: %q", opt.Filter)
 	}
-	if repo != "dmitri.shuralyov.com/font/woff2" {
-		return 0, nil
-	}
 	var count uint64
-	for _, c := range s.changes {
+	for _, c := range s.changes[repo] {
 		if !counts(c.State) {
 			continue
 		}
 		count++
 	}
 	return count, nil
 }
 
 // Get a change.
 func (*Service) Get(ctx context.Context, repo string, id uint64) (change.Change, error) {
-	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
+	if !hasChange(repo, id) {
 		return change.Change{}, os.ErrNotExist
 	}
-	return s.changes[0].Change, nil
+	return s.changes[repo][id-1].Change, nil
 }
 
 // ListTimeline lists timeline items (change.Comment, change.Review, change.TimelineItem) for specified change id.
 func (svc *Service) ListTimeline(ctx context.Context, repo string, id uint64, opt *change.ListTimelineOptions) ([]interface{}, error) {
-	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
+	if !hasChange(repo, id) {
 		return nil, os.ErrNotExist
 	}
 	if svc.Reactions == nil {
-		return s.changes[0].Timeline, nil
+		return s.changes[repo][id-1].Timeline, nil
 	}
 	reactions, err := svc.Reactions.List(ctx, repo)
 	if err != nil {
 		return nil, fmt.Errorf("ListTimeline: Reactions.List: %v", err)
 	}
-	timeline := make([]interface{}, len(s.changes[0].Timeline))
-	copy(timeline, s.changes[0].Timeline)
-	{
-		t := timeline[0].(change.Comment)
-		t.Reactions = reactions[t.ID]
-		timeline[0] = t
-	}
-	{
-		t := timeline[1].(change.Review)
-		t.Reactions = reactions[t.ID]
-		timeline[1] = t
+	timeline := make([]interface{}, len(s.changes[repo][id-1].Timeline))
+	copy(timeline, s.changes[repo][id-1].Timeline)
+	switch {
+	case repo == "dmitri.shuralyov.com/font/woff2" && id == 1:
+		{
+			t := timeline[0].(change.Comment)
+			t.Reactions = reactions[t.ID]
+			timeline[0] = t
+		}
+		{
+			t := timeline[1].(change.Review)
+			t.Reactions = reactions[t.ID]
+			timeline[1] = t
+		}
+	case repo == "dmitri.shuralyov.com/gpu/mtl" && id == 1:
+		{
+			t := timeline[0].(change.Comment)
+			t.Reactions = reactions[t.ID]
+			timeline[0] = t
+		}
 	}
 	return timeline, nil
 }
 
 // ListCommits lists change commits.
 func (*Service) ListCommits(ctx context.Context, repo string, id uint64) ([]change.Commit, error) {
-	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
+	if !hasChange(repo, id) {
 		return nil, os.ErrNotExist
 	}
-	return s.changes[0].Commits, nil
+	return s.changes[repo][id-1].Commits, nil
 }
 
 // Get a change diff.
 func (*Service) GetDiff(ctx context.Context, repo string, id uint64, opt *change.GetDiffOptions) ([]byte, error) {
-	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
+	if !hasChange(repo, id) {
 		return nil, os.ErrNotExist
 	}
 	switch opt {
 	case nil:
-		return s.changes[0].Diffs["all"], nil
+		return s.changes[repo][id-1].Diffs["all"], nil
 	default:
-		return s.changes[0].Diffs[opt.Commit], nil
+		return s.changes[repo][id-1].Diffs[opt.Commit], nil
 	}
 }
 
 func (s *Service) EditComment(ctx context.Context, repo string, id uint64, cr change.CommentRequest) (change.Comment, error) {
-	if repo != "dmitri.shuralyov.com/font/woff2" || id != 1 {
+	if !hasChange(repo, id) {
 		return change.Comment{}, os.ErrNotExist
 	}
 	if s.Reactions == nil {
 		return change.Comment{}, fmt.Errorf("no place on backend to store reactions")
 	}
@@ -267,22 +321,26 @@ func (s *Service) EditComment(ctx context.Context, repo string, id uint64, cr ch
 		comment.Reactions = reactions
 	}
 	return comment, nil
 }
 
+func hasChange(repo string, id uint64) bool {
+	return 1 <= id && id <= uint64(len(s.changes[repo]))
+}
+
 // threadType is the notifications thread type for this service.
 const threadType = "Change"
 
 // ThreadType returns the notifications thread type for this service.
 func (*Service) ThreadType(repo string) string { return threadType }
 
-var shurcool = users.User{
+var dmitshur = users.User{
 	UserSpec: users.UserSpec{
 		ID:     1924134,
 		Domain: "github.com",
 	},
-	Login:     "shurcooL",
+	Login:     "dmitshur",
 	Name:      "Dmitri Shuralyov",
 	Email:     "dmitri@shuralyov.com",
 	AvatarURL: "https://dmitri.shuralyov.com/avatar.jpg",
 	HTMLURL:   "https://dmitri.shuralyov.com",
 	SiteAdmin: true,
@@ -1696,5 +1754,595 @@ index 498a4a8..0004d27 100644
 +	//       and made more precise (i.e., the beginning of those blocks must be 4-byte aligned, etc.).
  	n, err := io.Copy(discardZeroes{}, r)
  	if err != nil {
  		return File{}, fmt.Errorf("Parse: %v", err)
 `
+
+const diffMtlAll = `diff --git a/Commit Message b/Commit Message
+new file mode 100644
+index 0000000..dfb31fe
+--- /dev/null
++++ b/Commit Message
+@@ -0,0 +1,27 @@
++Parent:     0cf138a8 (cmd/mtlinfo: Add a tool to list all Metal devices, supported feature sets.)
++Author:     Dmitri Shuralyov <dmitri@shuralyov.com>
++AuthorDate: Sat Jun 23 01:07:53 2018 -0400
++Commit:     Dmitri Shuralyov <dmitri@shuralyov.com>
++CommitDate: Tue Oct 16 21:39:22 2018 -0400
++
++WIP: Add minimal API to support rendering to a window at 60 FPS.
++
++The goal of this change is to make it possible to use package mtl
++to render to a window at 60 FPS. It tries to add the minimum viable
++API that is needed.
++
++A new movingtriangle example is added as a demonstration of this
++functionality. It renders a triangle that follows the mouse cursor.
++
++TODO: A lot of the newly added API comes from Core Animation, AppKit
++frameworks, rather than Metal. As a result, they likely do not belong
++in this package and should be factored out.
+` + diffMtlCommit1
+
+const diffMtlCommit1 = `commit fc76fa8984fb4a28ff383895e55e635e06bd32f0
+Author: Dmitri Shuralyov <dmitri@shuralyov.com>
+Date:   Sat Jun 23 01:07:53 2018 -0400
+
+    WIP: Add minimal API to support rendering to a window at 60 FPS.
+
+    The goal of this change is to make it possible to use package mtl
+    to render to a window at 60 FPS. It tries to add the minimum viable
+    API that is needed.
+
+    A new movingtriangle example is added as a demonstration of this
+    functionality. It renders a triangle that follows the mouse cursor.
+
+    TODO: A lot of the newly added API comes from Core Animation, AppKit
+    frameworks, rather than Metal. As a result, they likely do not belong
+    in this package and should be factored out.
+
+diff --git a/example/movingtriangle/main.go b/example/movingtriangle/main.go
+new file mode 100644
+index 0000000..18b5e03
+--- /dev/null
++++ b/example/movingtriangle/main.go
+@@ -0,0 +1,196 @@
++// +build darwin
++
++// movingtriangle is an example Metal program that displays a moving triangle in a window.
++package main
++
++import (
++	"flag"
++	"fmt"
++	"log"
++	"os"
++	"runtime"
++	"time"
++	"unsafe"
++
++	"dmitri.shuralyov.com/gpu/mtl"
++	"github.com/go-gl/glfw/v3.2/glfw"
++	"golang.org/x/image/math/f32"
++)
++
++func usage() {
++	fmt.Fprintln(os.Stderr, "Usage: movingtriangle")
++	flag.PrintDefaults()
++}
++
++func init() {
++	runtime.LockOSThread()
++}
++
++func main() {
++	flag.Usage = usage
++	flag.Parse()
++
++	err := run()
++	if err != nil {
++		log.Fatalln(err)
++	}
++}
++
++func run() error {
++	device, err := mtl.CreateSystemDefaultDevice()
++	if err != nil {
++		return err
++	}
++	fmt.Println("Metal device:", device.Name)
++
++	err = glfw.Init()
++	if err != nil {
++		return err
++	}
++	defer glfw.Terminate()
++
++	glfw.WindowHint(glfw.ClientAPI, glfw.NoAPI)
++	window, err := glfw.CreateWindow(640, 480, "Metal Example", nil, nil)
++	if err != nil {
++		return err
++	}
++	defer window.Destroy()
++
++	layer := mtl.MakeLayer()
++	layer.SetDevice(device)
++	layer.SetPixelFormat(mtl.PixelFormatBGRA8UNorm)
++	layer.SetDrawableSize(window.GetFramebufferSize())
++	layer.SetMaximumDrawableCount(3)
++	layer.SetDisplaySyncEnabled(true)
++	mtl.SetWindowContentViewLayer(window.GetCocoaWindow(), layer)
++	mtl.SetWindowContentViewWantsLayer(window.GetCocoaWindow(), true)
++
++	// Set callbacks.
++	window.SetFramebufferSizeCallback(func(_ *glfw.Window, width, height int) {
++		layer.SetDrawableSize(width, height)
++	})
++	var windowSize = [2]int32{640, 480}
++	window.SetSizeCallback(func(_ *glfw.Window, width, height int) {
++		windowSize[0], windowSize[1] = int32(width), int32(height)
++	})
++	var pos [2]float32
++	window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
++		pos[0], pos[1] = float32(x), float32(y)
++	})
++
++	// Create a render pipeline state.
++	const source = ` + "`" + `#include <metal_stdlib>
++
++using namespace metal;
++
++struct Vertex {
++	float4 position [[position]];
++	float4 color;
++};
++
++vertex Vertex VertexShader(
++	uint vertexID [[vertex_id]],
++	device Vertex * vertices [[buffer(0)]],
++	constant int2 * windowSize [[buffer(1)]],
++	constant float2 * pos [[buffer(2)]]
++) {
++	Vertex out = vertices[vertexID];
++	out.position.xy += *pos;
++	float2 viewportSize = float2(*windowSize);
++	out.position.xy = float2(-1 + out.position.x / (0.5 * viewportSize.x),
++	                          1 - out.position.y / (0.5 * viewportSize.y));
++	return out;
++}
++
++fragment float4 FragmentShader(Vertex in [[stage_in]]) {
++	return in.color;
++}
++` + "`" + `
++	lib, err := device.MakeLibrary(source, mtl.CompileOptions{})
++	if err != nil {
++		return err
++	}
++	vs, err := lib.MakeFunction("VertexShader")
++	if err != nil {
++		return err
++	}
++	fs, err := lib.MakeFunction("FragmentShader")
++	if err != nil {
++		return err
++	}
++	var rpld mtl.RenderPipelineDescriptor
++	rpld.VertexFunction = vs
++	rpld.FragmentFunction = fs
++	rpld.ColorAttachments[0].PixelFormat = layer.PixelFormat()
++	rps, err := device.MakeRenderPipelineState(rpld)
++	if err != nil {
++		return err
++	}
++
++	// Create a vertex buffer.
++	type Vertex struct {
++		Position f32.Vec4
++		Color    f32.Vec4
++	}
++	vertexData := [...]Vertex{
++		{f32.Vec4{0, 0, 0, 1}, f32.Vec4{1, 0, 0, 1}},
++		{f32.Vec4{300, 100, 0, 1}, f32.Vec4{0, 1, 0, 1}},
++		{f32.Vec4{0, 100, 0, 1}, f32.Vec4{0, 0, 1, 1}},
++	}
++	vertexBuffer := device.MakeBuffer(unsafe.Pointer(&vertexData[0]), unsafe.Sizeof(vertexData), mtl.ResourceStorageModeManaged)
++
++	cq := device.MakeCommandQueue()
++
++	frame := startFPSCounter()
++
++	for !window.ShouldClose() {
++		glfw.PollEvents()
++
++		// Create a drawable to render into.
++		drawable, err := layer.NextDrawable()
++		if err != nil {
++			return err
++		}
++
++		cb := cq.MakeCommandBuffer()
++
++		// Encode all render commands.
++		var rpd mtl.RenderPassDescriptor
++		rpd.ColorAttachments[0].LoadAction = mtl.LoadActionClear
++		rpd.ColorAttachments[0].StoreAction = mtl.StoreActionStore
++		rpd.ColorAttachments[0].ClearColor = mtl.ClearColor{Red: 0.35, Green: 0.65, Blue: 0.85, Alpha: 1}
++		rpd.ColorAttachments[0].Texture = drawable.Texture()
++		rce := cb.MakeRenderCommandEncoder(rpd)
++		rce.SetRenderPipelineState(rps)
++		rce.SetVertexBuffer(vertexBuffer, 0, 0)
++		rce.SetVertexBytes(unsafe.Pointer(&windowSize[0]), unsafe.Sizeof(windowSize), 1)
++		rce.SetVertexBytes(unsafe.Pointer(&pos[0]), unsafe.Sizeof(pos), 2)
++		rce.DrawPrimitives(mtl.PrimitiveTypeTriangle, 0, 3)
++		rce.EndEncoding()
++
++		cb.Present(drawable)
++		cb.Commit()
++
++		frame <- struct{}{}
++	}
++
++	return nil
++}
++
++func startFPSCounter() chan struct{} {
++	frame := make(chan struct{}, 4)
++	go func() {
++		second := time.Tick(time.Second)
++		frames := 0
++		for {
++			select {
++			case <-second:
++				fmt.Println("fps:", frames)
++				frames = 0
++			case <-frame:
++				frames++
++			}
++		}
++	}()
++	return frame
++}
+diff --git a/mtl.go b/mtl.go
+index feff4bb..5ff54c5 100644
+--- a/mtl.go
++++ b/mtl.go
+@@ -16,7 +16,7 @@ import (
+
+ /*
+ #cgo CFLAGS: -x objective-c
+-#cgo LDFLAGS: -framework Metal -framework Foundation
++#cgo LDFLAGS: -framework Metal -framework QuartzCore -framework Foundation
+ #include <stdlib.h>
+ #include "mtl.h"
+ struct Library Go_Device_MakeLibrary(void * device, _GoString_ source) {
+@@ -25,6 +25,124 @@ struct Library Go_Device_MakeLibrary(void * device, _GoString_ source) {
+ */
+ import "C"
+
++// Layer is a Core Animation Metal layer, a layer that manages a pool of Metal drawables.
++//
++// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer.
++type Layer struct {
++	layer unsafe.Pointer
++}
++
++// MakeLayer creates a new Core Animation Metal layer.
++//
++// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer.
++func MakeLayer() Layer {
++	return Layer{C.MakeLayer()}
++}
++
++// PixelFormat returns the pixel format of textures for rendering layer content.
++//
++// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer/1478155-pixelformat.
++func (l Layer) PixelFormat() PixelFormat {
++	return PixelFormat(C.Layer_PixelFormat(l.layer))
++}
++
++// SetDevice sets the Metal device responsible for the layer's drawable resources.
++//
++// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer/1478163-device.
++func (l Layer) SetDevice(device Device) {
++	C.Layer_SetDevice(l.layer, 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.
++func (l Layer) SetPixelFormat(pf PixelFormat) {
++	e := C.Layer_SetPixelFormat(l.layer, 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.
++func (l Layer) SetMaximumDrawableCount(count int) {
++	e := C.Layer_SetMaximumDrawableCount(l.layer, 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.
++func (l Layer) SetDisplaySyncEnabled(enabled bool) {
++	switch enabled {
++	case true:
++		C.Layer_SetDisplaySyncEnabled(l.layer, 1)
++	case false:
++		C.Layer_SetDisplaySyncEnabled(l.layer, 0)
++	}
++}
++
++// SetDrawableSize sets the size, in pixels, of textures for rendering layer content.
++//
++// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer/1478174-drawablesize.
++func (l Layer) SetDrawableSize(width, height int) {
++	C.Layer_SetDrawableSize(l.layer, C.double(width), C.double(height))
++}
++
++// NextDrawable returns a Metal drawable.
++//
++// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer/1478172-nextdrawable.
++func (l Layer) NextDrawable() (Drawable, error) {
++	d := C.Layer_NextDrawable(l.layer)
++	if d == nil {
++		return Drawable{}, errors.New("nextDrawable returned nil")
++	}
++
++	return Drawable{d}, nil
++}
++
++// Drawable is a displayable resource that can be rendered or written to.
++//
++// Reference: https://developer.apple.com/documentation/metal/mtldrawable.
++type Drawable struct {
++	drawable unsafe.Pointer
++}
++
++// Texture returns a Metal texture object representing the drawable object's content.
++//
++// Reference: https://developer.apple.com/documentation/quartzcore/cametaldrawable/1478159-texture.
++func (d Drawable) Texture() Texture {
++	return Texture{
++		texture: C.Drawable_Texture(d.drawable),
++		Width:   0, // TODO: Fetch dimensions of actually created texture.
++		Height:  0, // TODO: Fetch dimensions of actually created texture.
++	}
++}
++
++// SetWindowContentViewLayer sets cocoaWindow's contentView's layer to layer.
++//
++// Reference: https://developer.apple.com/documentation/appkit/nsview/1483298-layer.
++func SetWindowContentViewLayer(cocoaWindow uintptr, l Layer) {
++	C.SetWindowContentViewLayer(unsafe.Pointer(cocoaWindow), l.layer)
++}
++
++// SetWindowContentViewWantsLayer sets cocoaWindow's contentView's wantsLayer to wantsLayer.
++//
++// Reference: https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer.
++func SetWindowContentViewWantsLayer(cocoaWindow uintptr, wantsLayer bool) {
++	switch wantsLayer {
++	case true:
++		C.SetWindowContentViewWantsLayer(unsafe.Pointer(cocoaWindow), 1)
++	case false:
++		C.SetWindowContentViewWantsLayer(unsafe.Pointer(cocoaWindow), 0)
++	}
++}
++
+ // FeatureSet defines a specific platform, hardware, and software configuration.
+ //
+ // Reference: https://developer.apple.com/documentation/metal/mtlfeatureset.
+@@ -49,8 +167,9 @@ type PixelFormat uint8
+ // The data formats that describe the organization and characteristics
+ // of individual pixels in a texture.
+ const (
+-	PixelFormatRGBA8UNorm PixelFormat = 70 // Ordinary format with four 8-bit normalized unsigned integer components in RGBA order.
+-	PixelFormatBGRA8UNorm PixelFormat = 80 // Ordinary format with four 8-bit normalized unsigned integer components in BGRA order.
++	PixelFormatRGBA8UNorm     PixelFormat = 70 // Ordinary format with four 8-bit normalized unsigned integer components in RGBA order.
++	PixelFormatBGRA8UNorm     PixelFormat = 80 // Ordinary format with four 8-bit normalized unsigned integer components in BGRA order.
++	PixelFormatBGRA8UNormSRGB PixelFormat = 81 // Ordinary format with four 8-bit normalized unsigned integer components in BGRA order with conversion between sRGB and linear space.
+ )
+
+ // PrimitiveType defines geometric primitive types for drawing commands.
+@@ -215,7 +334,7 @@ type RenderPipelineDescriptor struct {
+ //
+ // Reference: https://developer.apple.com/documentation/metal/mtlrenderpipelinecolorattachmentdescriptor.
+ type RenderPipelineColorAttachmentDescriptor struct {
+-	// PixelFormat is the pixel format of the color attachment’s texture.
++	// PixelFormat is the pixel format of the color attachment's texture.
+ 	PixelFormat PixelFormat
+ }
+
+@@ -428,6 +547,13 @@ type CommandBuffer struct {
+ 	commandBuffer unsafe.Pointer
+ }
+
++// Present registers a drawable presentation to occur as soon as possible.
++//
++// Reference: https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443029-presentdrawable.
++func (cb CommandBuffer) Present(d Drawable) {
++	C.CommandBuffer_Present(cb.commandBuffer, d.drawable)
++}
++
+ // Commit commits this command buffer for execution as soon as possible.
+ //
+ // Reference: https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443003-commit.
+@@ -507,6 +633,13 @@ func (rce RenderCommandEncoder) SetVertexBuffer(buf Buffer, offset, index int) {
+ 	C.RenderCommandEncoder_SetVertexBuffer(rce.commandEncoder, buf.buffer, C.uint_t(offset), C.uint_t(index))
+ }
+
++// SetVertexBytes sets a block of data for the vertex function.
++//
++// Reference: https://developer.apple.com/documentation/metal/mtlrendercommandencoder/1515846-setvertexbytes.
++func (rce RenderCommandEncoder) SetVertexBytes(bytes unsafe.Pointer, length uintptr, index int) {
++	C.RenderCommandEncoder_SetVertexBytes(rce.commandEncoder, bytes, C.size_t(length), C.uint_t(index))
++}
++
+ // DrawPrimitives renders one instance of primitives using vertex data
+ // in contiguous array elements.
+ //
+diff --git a/mtl.h b/mtl.h
+index 6ac8b18..e8924ab 100644
+--- a/mtl.h
++++ b/mtl.h
+@@ -74,6 +74,21 @@ struct Region {
+ 	struct Size   Size;
+ };
+
++void * MakeLayer();
++
++uint16_t     Layer_PixelFormat(void * layer);
++void         Layer_SetDevice(void * layer, void * device);
++const char * Layer_SetPixelFormat(void * layer, uint16_t pixelFormat);
++const char * Layer_SetMaximumDrawableCount(void * layer, uint_t maximumDrawableCount);
++void         Layer_SetDisplaySyncEnabled(void * layer, BOOL displaySyncEnabled);
++void         Layer_SetDrawableSize(void * layer, double width, double height);
++void *       Layer_NextDrawable(void * layer);
++
++void * Drawable_Texture(void * drawable);
++
++void SetWindowContentViewLayer(void * cocoaWindow, void * layer);
++void SetWindowContentViewWantsLayer(void * cocoaWindow, BOOL wantsLayer);
++
+ struct Device CreateSystemDefaultDevice();
+ struct Devices CopyAllDevices();
+
+@@ -86,6 +101,7 @@ void *                     Device_MakeTexture(void * device, struct TextureDescr
+
+ void * CommandQueue_MakeCommandBuffer(void * commandQueue);
+
++void   CommandBuffer_Present(void * commandBuffer, void * drawable);
+ void   CommandBuffer_Commit(void * commandBuffer);
+ void   CommandBuffer_WaitUntilCompleted(void * commandBuffer);
+ void * CommandBuffer_MakeRenderCommandEncoder(void * commandBuffer, struct RenderPassDescriptor descriptor);
+@@ -95,6 +111,7 @@ void CommandEncoder_EndEncoding(void * commandEncoder);
+
+ void RenderCommandEncoder_SetRenderPipelineState(void * renderCommandEncoder, void * renderPipelineState);
+ void RenderCommandEncoder_SetVertexBuffer(void * renderCommandEncoder, void * buffer, uint_t offset, uint_t index);
++void RenderCommandEncoder_SetVertexBytes(void * renderCommandEncoder, const void * bytes, size_t length, uint_t index);
+ void RenderCommandEncoder_DrawPrimitives(void * renderCommandEncoder, uint8_t primitiveType, uint_t vertexStart, uint_t vertexCount);
+
+ void BlitCommandEncoder_Synchronize(void * blitCommandEncoder, void * resource);
+diff --git a/mtl.m b/mtl.m
+index b3126d6..7b0fd96 100644
+--- a/mtl.m
++++ b/mtl.m
+@@ -1,9 +1,71 @@
+ // +build darwin
+
+-#include <stdlib.h>
+ #import <Metal/Metal.h>
++#import <QuartzCore/QuartzCore.h>
++#import <Cocoa/Cocoa.h>
++
++#include <stdlib.h>
++
+ #include "mtl.h"
+
++void * MakeLayer() {
++	return [[CAMetalLayer alloc] init];
++}
++
++uint16_t Layer_PixelFormat(void * layer) {
++	return ((CAMetalLayer *)layer).pixelFormat;
++}
++
++void Layer_SetDevice(void * layer, void * device) {
++	((CAMetalLayer *)layer).device = (id<MTLDevice>)device;
++}
++
++const char * Layer_SetPixelFormat(void * layer, uint16_t pixelFormat) {
++	@try {
++		((CAMetalLayer *)layer).pixelFormat = (MTLPixelFormat)pixelFormat;
++	}
++	@catch (NSException * exception) {
++		return exception.reason.UTF8String;
++	}
++	return NULL;
++}
++
++const char * Layer_SetMaximumDrawableCount(void * layer, uint_t maximumDrawableCount) {
++	if (@available(macOS 10.13.2, *)) {
++		@try {
++			((CAMetalLayer *)layer).maximumDrawableCount = (NSUInteger)maximumDrawableCount;
++		}
++		@catch (NSException * exception) {
++			return exception.reason.UTF8String;
++		}
++	}
++	return NULL;
++}
++
++void Layer_SetDisplaySyncEnabled(void * layer, BOOL displaySyncEnabled) {
++	((CAMetalLayer *)layer).displaySyncEnabled = displaySyncEnabled;
++}
++
++void Layer_SetDrawableSize(void * layer, double width, double height) {
++	((CAMetalLayer *)layer).drawableSize = (CGSize){width, height};
++}
++
++void * Layer_NextDrawable(void * layer) {
++	return [(CAMetalLayer *)layer nextDrawable];
++}
++
++void * Drawable_Texture(void * drawable) {
++	return ((id<CAMetalDrawable>)drawable).texture;
++}
++
++void SetWindowContentViewLayer(void * cocoaWindow, void * layer) {
++	((NSWindow *)cocoaWindow).contentView.layer = (CAMetalLayer *)layer;
++}
++
++void SetWindowContentViewWantsLayer(void * cocoaWindow, BOOL wantsLayer) {
++	((NSWindow *)cocoaWindow).contentView.wantsLayer = wantsLayer;
++}
++
+ struct Device CreateSystemDefaultDevice() {
+ 	id<MTLDevice> device = MTLCreateSystemDefaultDevice();
+ 	if (!device) {
+@@ -100,6 +162,10 @@ struct RenderPipelineState Device_MakeRenderPipelineState(void * device, struct
+ 	return [(id<MTLCommandQueue>)commandQueue commandBuffer];
+ }
+
++void CommandBuffer_Present(void * commandBuffer, void * drawable) {
++	[(id<MTLCommandBuffer>)commandBuffer presentDrawable:(id<CAMetalDrawable>)drawable];
++}
++
+ void CommandBuffer_Commit(void * commandBuffer) {
+ 	[(id<MTLCommandBuffer>)commandBuffer commit];
+ }
+@@ -136,14 +202,20 @@ void RenderCommandEncoder_SetRenderPipelineState(void * renderCommandEncoder, vo
+
+ void RenderCommandEncoder_SetVertexBuffer(void * renderCommandEncoder, void * buffer, uint_t offset, uint_t index) {
+ 	[(id<MTLRenderCommandEncoder>)renderCommandEncoder setVertexBuffer:(id<MTLBuffer>)buffer
+-	                                                            offset:offset
+-	                                                           atIndex:index];
++	                                                            offset:(NSUInteger)offset
++	                                                           atIndex:(NSUInteger)index];
++}
++
++void RenderCommandEncoder_SetVertexBytes(void * renderCommandEncoder, const void * bytes, size_t length, uint_t index) {
++	[(id<MTLRenderCommandEncoder>)renderCommandEncoder setVertexBytes:bytes
++	                                                           length:(NSUInteger)length
++	                                                          atIndex:(NSUInteger)index];
+ }
+
+ void RenderCommandEncoder_DrawPrimitives(void * renderCommandEncoder, uint8_t primitiveType, uint_t vertexStart, uint_t vertexCount) {
+-	[(id<MTLRenderCommandEncoder>)renderCommandEncoder drawPrimitives:primitiveType
+-	                                                      vertexStart:vertexStart
+-	                                                      vertexCount:vertexCount];
++	[(id<MTLRenderCommandEncoder>)renderCommandEncoder drawPrimitives:(MTLPrimitiveType)primitiveType
++	                                                      vertexStart:(NSUInteger)vertexStart
++	                                                      vertexCount:(NSUInteger)vertexCount];
+ }
+
+ void BlitCommandEncoder_Synchronize(void * blitCommandEncoder, void * resource) {
+`