dmitri.shuralyov.com/gpu/mtl/...

Add minimal API to support interactive rendering in a window.

The goal of this change is to make it possible to use package mtl
to render to a window at interactive framerates (e.g., at 60 FPS,
assuming a 60 Hz display with vsync on). It adds the minimal API
that is needed.

A new movingtriangle example is added as a demonstration of this
functionality. It opens a window and renders a triangle that follows
the mouse cursor.

Much of the needed API comes from Core Animation, AppKit frameworks,
rather than Metal. Avoid adding that to mtl package; instead create
separate packages. For now, they are hidden in internal to avoid
committing to a public API and import path. After gaining more
confidence in the approach, they can be factored out and made public.
dmitshur committed 5 years ago commit c4eb07ba2d711bc78bcd2606dd587d9267a61aa5
Collapse all
example/movingtriangle/internal/ca/ca.go
@@ -0,0 +1,137 @@
// +build darwin

// Package ca 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 the movingtriangle example.
package ca

import (
	"errors"
	"unsafe"

	"dmitri.shuralyov.com/gpu/mtl"
)

/*
#cgo LDFLAGS: -framework QuartzCore -framework Foundation
#include "ca.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) {
	switch enabled {
	case true:
		C.MetalLayer_SetDisplaySyncEnabled(ml.metalLayer, 1)
	case false:
		C.MetalLayer_SetDisplaySyncEnabled(ml.metalLayer, 0)
	}
}

// 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))
}
example/movingtriangle/internal/ca/ca.h
@@ -0,0 +1,17 @@
// +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);
example/movingtriangle/internal/ca/ca.m
@@ -0,0 +1,54 @@
// +build darwin

#import <QuartzCore/QuartzCore.h>
#include "ca.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;
}
example/movingtriangle/internal/ns/ns.go
@@ -0,0 +1,65 @@
// +build darwin

// Package ns 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 the movingtriangle example.
package ns

import (
	"unsafe"

	"dmitri.shuralyov.com/gpu/mtl/example/movingtriangle/internal/ca"
)

/*
#include "ns.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 ca.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) {
	switch wantsLayer {
	case true:
		C.View_SetWantsLayer(v.view, 1)
	case false:
		C.View_SetWantsLayer(v.view, 0)
	}
}
example/movingtriangle/internal/ns/ns.h
@@ -0,0 +1,8 @@
// +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);
example/movingtriangle/internal/ns/ns.m
@@ -0,0 +1,16 @@
// +build darwin

#import <Cocoa/Cocoa.h>
#include "ns.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;
}
example/movingtriangle/main.go
@@ -0,0 +1,198 @@
// +build darwin

// movingtriangle is an example Metal program that displays a moving triangle in a window.
// It opens a window and renders a triangle that follows the mouse cursor.
package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"runtime"
	"time"
	"unsafe"

	"dmitri.shuralyov.com/gpu/mtl"
	"dmitri.shuralyov.com/gpu/mtl/example/movingtriangle/internal/ca"
	"dmitri.shuralyov.com/gpu/mtl/example/movingtriangle/internal/ns"
	"github.com/go-gl/glfw/v3.2/glfw"
	"golang.org/x/image/math/f32"
)

func init() {
	runtime.LockOSThread()
}

func main() {
	flag.Usage = func() {
		fmt.Fprintln(os.Stderr, "Usage: movingtriangle")
		flag.PrintDefaults()
	}
	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()

	ml := ca.MakeMetalLayer()
	ml.SetDevice(device)
	ml.SetPixelFormat(mtl.PixelFormatBGRA8UNorm)
	ml.SetDrawableSize(window.GetFramebufferSize())
	ml.SetMaximumDrawableCount(3)
	ml.SetDisplaySyncEnabled(true)
	cocoaWindow := ns.NewWindow(unsafe.Pointer(window.GetCocoaWindow()))
	cocoaWindow.ContentView().SetLayer(ml)
	cocoaWindow.ContentView().SetWantsLayer(true)

	// Set callbacks.
	window.SetFramebufferSizeCallback(func(_ *glfw.Window, width, height int) {
		ml.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 = ml.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 := ml.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.PresentDrawable(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
}
mtl.go
@@ -191,10 +191,11 @@ const (
// Resource represents a memory allocation for storing specialized data
// that is accessible to the GPU.
//
// Reference: https://developer.apple.com/documentation/metal/mtlresource.
type Resource interface {
	// resource returns the underlying id<MTLResource> pointer.
	resource() unsafe.Pointer
}

// RenderPipelineDescriptor configures new RenderPipelineState objects.
//
@@ -325,10 +326,13 @@ func CopyAllDevices() []Device {
		ds[i].Name = C.GoString(d.Name)
	}
	return ds
}

// Device returns the underlying id<MTLDevice> pointer.
func (d Device) Device() unsafe.Pointer { return d.device }

// SupportsFeatureSet reports whether device d supports feature set fs.
//
// Reference: https://developer.apple.com/documentation/metal/mtldevice/1433418-supportsfeatureset.
func (d Device) SupportsFeatureSet(fs FeatureSet) bool {
	return C.Device_SupportsFeatureSet(d.device, C.uint16_t(fs)) != 0
@@ -403,10 +407,18 @@ func (d Device) MakeTexture(td TextureDescriptor) Texture {
// Reference: https://developer.apple.com/documentation/metal/mtlcompileoptions.
type CompileOptions struct {
	// TODO.
}

// Drawable is a displayable resource that can be rendered or written to.
//
// Reference: https://developer.apple.com/documentation/metal/mtldrawable.
type Drawable interface {
	// Drawable returns the underlying id<MTLDrawable> pointer.
	Drawable() unsafe.Pointer
}

// CommandQueue is a queue that organizes the order
// in which command buffers are executed by the GPU.
//
// Reference: https://developer.apple.com/documentation/metal/mtlcommandqueue.
type CommandQueue struct {
@@ -426,10 +438,17 @@ func (cq CommandQueue) MakeCommandBuffer() CommandBuffer {
// Reference: https://developer.apple.com/documentation/metal/mtlcommandbuffer.
type CommandBuffer struct {
	commandBuffer unsafe.Pointer
}

// PresentDrawable registers a drawable presentation to occur as soon as possible.
//
// Reference: https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443029-presentdrawable.
func (cb CommandBuffer) PresentDrawable(d Drawable) {
	C.CommandBuffer_PresentDrawable(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.
func (cb CommandBuffer) Commit() {
	C.CommandBuffer_Commit(cb.commandBuffer)
@@ -562,17 +581,25 @@ func (l Library) MakeFunction(name string) (Function, error) {
//
// Reference: https://developer.apple.com/documentation/metal/mtltexture.
type Texture struct {
	texture unsafe.Pointer

	// TODO: Change these fields into methods.

	// Width is the width of the texture image for the base level mipmap, in pixels.
	Width int

	// Height is the height of the texture image for the base level mipmap, in pixels.
	Height int
}

// NewTexture returns a Texture that wraps an existing id<MTLTexture> pointer.
func NewTexture(texture unsafe.Pointer) Texture {
	return Texture{texture: texture}
}

// resource implements the Resource interface.
func (t Texture) resource() unsafe.Pointer { return t.texture }

// GetBytes copies a block of pixels from the storage allocation of texture
// slice zero into system memory at a specified address.
//
mtl.h
@@ -84,10 +84,11 @@ struct RenderPipelineState Device_MakeRenderPipelineState(void * device, struct
void *                     Device_MakeBuffer(void * device, const void * bytes, size_t length, uint16_t options);
void *                     Device_MakeTexture(void * device, struct TextureDescriptor descriptor);

void * CommandQueue_MakeCommandBuffer(void * commandQueue);

void   CommandBuffer_PresentDrawable(void * commandBuffer, void * drawable);
void   CommandBuffer_Commit(void * commandBuffer);
void   CommandBuffer_WaitUntilCompleted(void * commandBuffer);
void * CommandBuffer_MakeRenderCommandEncoder(void * commandBuffer, struct RenderPassDescriptor descriptor);
void * CommandBuffer_MakeBlitCommandEncoder(void * commandBuffer);

mtl.m
@@ -98,10 +98,14 @@ void * Device_MakeTexture(void * device, struct TextureDescriptor descriptor) {

void * CommandQueue_MakeCommandBuffer(void * commandQueue) {
	return [(id<MTLCommandQueue>)commandQueue commandBuffer];
}

void CommandBuffer_PresentDrawable(void * commandBuffer, void * drawable) {
	[(id<MTLCommandBuffer>)commandBuffer presentDrawable:(id<MTLDrawable>)drawable];
}

void CommandBuffer_Commit(void * commandBuffer) {
	[(id<MTLCommandBuffer>)commandBuffer commit];
}

void CommandBuffer_WaitUntilCompleted(void * commandBuffer) {