diff --git a/AUTHORS b/AUTHORS index 23d429b13..ba2af4501 100644 --- a/AUTHORS +++ b/AUTHORS @@ -2,6 +2,7 @@ Andrew Gerrand Ben Echols Brett Chalupa +Dmitri Shuralyov Evan Leis Floppy gonutz diff --git a/graphicscontext.go b/graphicscontext.go index 514c31bec..726486b6c 100644 --- a/graphicscontext.go +++ b/graphicscontext.go @@ -18,6 +18,8 @@ import ( "math" "github.com/hajimehoshi/ebiten/internal/clock" + "github.com/hajimehoshi/ebiten/internal/graphicscommand" + "github.com/hajimehoshi/ebiten/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/internal/hooks" "github.com/hajimehoshi/ebiten/internal/shareable" "github.com/hajimehoshi/ebiten/internal/ui" @@ -122,13 +124,21 @@ func (c *graphicsContext) Update(afterFrameUpdate func()) error { } op := &DrawImageOptions{} - // c.screen is special: its Y axis is down to up, - // and the origin point is lower left. - op.GeoM.Scale(c.screenScale, -c.screenScale) - // Make the screen height an even number to fit the upper side of the screen (#662). - op.GeoM.Translate(0, float64((c.screenHeight+1)/2*2)) - op.GeoM.Translate(c.offsetX, c.offsetY) + switch graphicscommand.Driver().VDirection() { + case graphicsdriver.VDownward: + // c.screen is special: its Y axis is down to up, + // and the origin point is lower left. + op.GeoM.Scale(c.screenScale, -c.screenScale) + // Make the screen height an even number to fit the upper side of the screen (#662). + op.GeoM.Translate(0, float64((c.screenHeight+1)/2*2)) + case graphicsdriver.VUpward: + op.GeoM.Scale(c.screenScale, c.screenScale) + default: + panic("not reached") + } + + op.GeoM.Translate(c.offsetX, c.offsetY) op.CompositeMode = CompositeModeCopy // filterScreen works with >=1 scale, but does not well with <1 scale. diff --git a/internal/graphics/compositemode.go b/internal/graphics/compositemode.go index 728ee8b61..c649aa966 100644 --- a/internal/graphics/compositemode.go +++ b/internal/graphics/compositemode.go @@ -31,6 +31,8 @@ const ( CompositeModeXor CompositeModeLighter CompositeModeUnknown + + CompositeModeMax = CompositeModeLighter ) type Operation int diff --git a/internal/graphicscommand/command.go b/internal/graphicscommand/command.go index 1321ecc4a..f77700a4c 100644 --- a/internal/graphicscommand/command.go +++ b/internal/graphicscommand/command.go @@ -165,7 +165,7 @@ func (q *commandQueue) Flush() { nc++ } if 0 < ne { - driver().SetVertices(vs[:nv], es[:ne]) + Driver().SetVertices(vs[:nv], es[:ne]) es = es[ne:] vs = vs[nv:] } @@ -185,7 +185,7 @@ func (q *commandQueue) Flush() { } if 0 < nc { // Call glFlush to prevent black flicking (especially on Android (#226) and iOS). - driver().Flush() + Driver().Flush() } q.commands = q.commands[nc:] } @@ -230,7 +230,7 @@ func (c *drawImageCommand) Exec(indexOffset int) error { c.dst.image.SetAsDestination() c.src.image.SetAsSource() - if err := driver().Draw(c.nindices, indexOffset, c.mode, c.color, c.filter); err != nil { + if err := Driver().Draw(c.nindices, indexOffset, c.mode, c.color, c.filter); err != nil { return err } return nil @@ -394,7 +394,7 @@ func (c *newImageCommand) String() string { // Exec executes a newImageCommand. func (c *newImageCommand) Exec(indexOffset int) error { - i, err := driver().NewImage(c.width, c.height) + i, err := Driver().NewImage(c.width, c.height) if err != nil { return err } @@ -434,7 +434,7 @@ func (c *newScreenFramebufferImageCommand) String() string { // Exec executes a newScreenFramebufferImageCommand. func (c *newScreenFramebufferImageCommand) Exec(indexOffset int) error { var err error - c.result.image, err = driver().NewScreenFramebufferImage(c.width, c.height) + c.result.image, err = Driver().NewScreenFramebufferImage(c.width, c.height) return err } @@ -458,5 +458,5 @@ func (c *newScreenFramebufferImageCommand) CanMerge(dst, src *Image, color *affi // ResetGraphicsDriverState resets or initializes the current graphics driver state. func ResetGraphicsDriverState() error { - return driver().Reset() + return Driver().Reset() } diff --git a/internal/graphicscommand/driver_mac.go b/internal/graphicscommand/driver_mac.go new file mode 100644 index 000000000..21a0560dd --- /dev/null +++ b/internal/graphicscommand/driver_mac.go @@ -0,0 +1,27 @@ +// Copyright 2018 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build darwin,!ios +// +build !js + +package graphicscommand + +import ( + "github.com/hajimehoshi/ebiten/internal/graphicsdriver" + "github.com/hajimehoshi/ebiten/internal/graphicsdriver/metal" +) + +func Driver() graphicsdriver.GraphicsDriver { + return metal.Get() +} diff --git a/internal/graphicscommand/driver.go b/internal/graphicscommand/driver_notmac.go similarity index 91% rename from internal/graphicscommand/driver.go rename to internal/graphicscommand/driver_notmac.go index 57fa8e1f0..dcb7f7464 100644 --- a/internal/graphicscommand/driver.go +++ b/internal/graphicscommand/driver_notmac.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +// +build !darwin ios js + package graphicscommand import ( @@ -19,6 +21,6 @@ import ( "github.com/hajimehoshi/ebiten/internal/graphicsdriver/opengl" ) -func driver() graphicsdriver.GraphicsDriver { +func Driver() graphicsdriver.GraphicsDriver { return opengl.Get() } diff --git a/internal/graphicsdriver/graphicsdriver.go b/internal/graphicsdriver/graphicsdriver.go index 69b8b53c9..a3f8dc210 100644 --- a/internal/graphicsdriver/graphicsdriver.go +++ b/internal/graphicsdriver/graphicsdriver.go @@ -20,12 +20,16 @@ import ( ) type GraphicsDriver interface { + SetWindow(window uintptr) SetVertices(vertices []float32, indices []uint16) Flush() NewImage(width, height int) (Image, error) NewScreenFramebufferImage(width, height int) (Image, error) Reset() error Draw(indexLen int, indexOffset int, mode graphics.CompositeMode, colorM *affine.ColorM, filter graphics.Filter) error + SetVsyncEnabled(enabled bool) + VDirection() VDirection + IsGL() bool } type Image interface { @@ -36,3 +40,10 @@ type Image interface { SetAsSource() ReplacePixels(pixels []byte, x, y, width, height int) } + +type VDirection int + +const ( + VUpward VDirection = iota + VDownward +) diff --git a/internal/graphicsdriver/metal/README.md b/internal/graphicsdriver/metal/README.md new file mode 100644 index 000000000..b543f6538 --- /dev/null +++ b/internal/graphicsdriver/metal/README.md @@ -0,0 +1,5 @@ +These packages are copied from Dmitri Shuralyov's mtl packages and edited with Dmitri's permission: + +* `github.com/hajimehoshi/ebiten/internal/graphicsdriver/metal/ca` (copied from `dmitri.shuralyov.com/gpu/mtl/example/movingtriangle/internal/ca`) +* `github.com/hajimehoshi/ebiten/internal/graphicsdriver/metal/mtl` (copied from `dmitri.shuralyov.com/gpu/mtl`) +* `github.com/hajimehoshi/ebiten/internal/graphicsdriver/metal/ns` (copied from `dmitri.shuralyov.com/gpu/mtl/example/movingtriangle/internal/ns`) diff --git a/internal/graphicsdriver/metal/ca/ca.go b/internal/graphicsdriver/metal/ca/ca.go new file mode 100644 index 000000000..401797eee --- /dev/null +++ b/internal/graphicsdriver/metal/ca/ca.go @@ -0,0 +1,150 @@ +// Copyright 2018 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +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" + + "github.com/hajimehoshi/ebiten/internal/graphicsdriver/metal/mtl" +) + +// #cgo LDFLAGS: -framework QuartzCore -framework Foundation -framework CoreGraphics +// +// #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)) +} diff --git a/internal/graphicsdriver/metal/ca/ca.h b/internal/graphicsdriver/metal/ca/ca.h new file mode 100644 index 000000000..4190a41f2 --- /dev/null +++ b/internal/graphicsdriver/metal/ca/ca.h @@ -0,0 +1,33 @@ +// Copyright 2018 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +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/internal/graphicsdriver/metal/ca/ca.m b/internal/graphicsdriver/metal/ca/ca.m new file mode 100644 index 000000000..2c1bf44f5 --- /dev/null +++ b/internal/graphicsdriver/metal/ca/ca.m @@ -0,0 +1,75 @@ +// Copyright 2018 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build darwin + +#include "ca.h" +#import + +void *MakeMetalLayer() { + CAMetalLayer *layer = [[CAMetalLayer alloc] init]; + // TODO: Expose a function to set color space. + CGColorSpaceRef colorspace = + CGColorSpaceCreateWithName(kCGColorSpaceDisplayP3); + layer.colorspace = colorspace; + CGColorSpaceRelease(colorspace); + return layer; +} + +uint16_t MetalLayer_PixelFormat(void *metalLayer) { + return ((CAMetalLayer *)metalLayer).pixelFormat; +} + +void MetalLayer_SetDevice(void *metalLayer, void *device) { + ((CAMetalLayer *)metalLayer).device = (id)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)metalDrawable).texture; +} diff --git a/internal/graphicsdriver/metal/driver.go b/internal/graphicsdriver/metal/driver.go new file mode 100644 index 000000000..5faff090c --- /dev/null +++ b/internal/graphicsdriver/metal/driver.go @@ -0,0 +1,613 @@ +// Copyright 2018 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build darwin + +package metal + +import ( + "fmt" + "strings" + "unsafe" + + "github.com/hajimehoshi/ebiten/internal/affine" + "github.com/hajimehoshi/ebiten/internal/graphics" + "github.com/hajimehoshi/ebiten/internal/graphicsdriver" + "github.com/hajimehoshi/ebiten/internal/graphicsdriver/metal/ca" + "github.com/hajimehoshi/ebiten/internal/graphicsdriver/metal/mtl" + "github.com/hajimehoshi/ebiten/internal/graphicsdriver/metal/ns" + "github.com/hajimehoshi/ebiten/internal/mainthread" +) + +const source = `#include + +#define FILTER_NEAREST ({{.FilterNearest}}) +#define FILTER_LINEAR ({{.FilterLinear}}) +#define FILTER_SCREEN ({{.FilterScreen}}) + +using namespace metal; + +struct VertexIn { + packed_float2 position; + packed_float4 tex; + packed_float4 color; +}; + +struct VertexOut { + float4 position [[position]]; + float2 tex; + float2 tex_min; + float2 tex_max; + float4 color; +}; + +vertex VertexOut VertexShader( + uint vid [[vertex_id]], + device VertexIn* vertices [[buffer(0)]], + constant float2& viewport_size [[buffer(1)]] +) { + float4x4 projectionMatrix = float4x4( + float4(2.0 / viewport_size.x, 0, 0, 0), + float4(0, -2.0 / viewport_size.y, 0, 0), + float4(0, 0, 1, 0), + float4(-1, 1, 0, 1) + ); + + VertexIn in = vertices[vid]; + + VertexOut out = { + .position = projectionMatrix * float4(in.position, 0, 1), + .tex = float2(in.tex[0], in.tex[1]), + .color = in.color, + }; + + if (in.tex[2] >= 0 && in.tex[3] >= 0) { + out.tex_min = float2(min(in.tex[0], in.tex[2]), min(in.tex[1], in.tex[3])); + out.tex_max = float2(max(in.tex[0], in.tex[2]), max(in.tex[1], in.tex[3])); + } else { + out.tex_min = float2(0); + out.tex_max = float2(1); + } + + return out; +} + +fragment float4 FragmentShader(VertexOut v [[stage_in]], + texture2d texture [[texture(0)]], + constant float4x4& color_matrix_body [[buffer(2)]], + constant float4& color_matrix_translation [[buffer(3)]], + constant uint8_t& filter [[buffer(4)]], + constant float& scale [[buffer(5)]]) { + constexpr sampler texture_sampler(filter::nearest); + float2 source_size = 1; + while (source_size.x < texture.get_width()) { + source_size.x *= 2; + } + while (source_size.y < texture.get_height()) { + source_size.y *= 2; + } + const float2 texel_size = 1 / source_size; + + float4 c; + + switch (filter) { + case FILTER_NEAREST: { + c = texture.sample(texture_sampler, v.tex); + if (v.tex.x < v.tex_min.x || + v.tex.y < v.tex_min.y || + (v.tex_max.x - texel_size.x / 512.0) <= v.tex.x || + (v.tex_max.y - texel_size.y / 512.0) <= v.tex.y) { + c = 0; + } + break; + } + + case FILTER_LINEAR: { + float2 p0 = v.tex - texel_size / 2.0; + float2 p1 = v.tex + texel_size / 2.0; + + float4 c0 = texture.sample(texture_sampler, p0); + float4 c1 = texture.sample(texture_sampler, float2(p1.x, p0.y)); + float4 c2 = texture.sample(texture_sampler, float2(p0.x, p1.y)); + float4 c3 = texture.sample(texture_sampler, p1); + + if (p0.x < v.tex_min.x) { + c0 = 0; + c2 = 0; + } + if (p0.y < v.tex_min.y) { + c0 = 0; + c1 = 0; + } + if ((v.tex_max.x - texel_size.x / 512.0) <= p1.x) { + c1 = 0; + c3 = 0; + } + if ((v.tex_max.y - texel_size.y / 512.0) <= p1.y) { + c2 = 0; + c3 = 0; + } + + float2 rate = fract(p0 * source_size); + c = mix(mix(c0, c1, rate.x), mix(c2, c3, rate.x), rate.y); + break; + } + + case FILTER_SCREEN: { + float2 p0 = v.tex - texel_size / 2.0 / scale; + float2 p1 = v.tex + texel_size / 2.0 / scale; + + float4 c0 = texture.sample(texture_sampler, p0); + float4 c1 = texture.sample(texture_sampler, float2(p1.x, p0.y)); + float4 c2 = texture.sample(texture_sampler, float2(p0.x, p1.y)); + float4 c3 = texture.sample(texture_sampler, p1); + + float2 rate_center = float2(1.0, 1.0) - texel_size / 2.0 / scale; + float2 rate = clamp(((fract(p0 * source_size) - rate_center) * scale) + rate_center, 0.0, 1.0); + c = mix(mix(c0, c1, rate.x), mix(c2, c3, rate.x), rate.y); + break; + } + + default: + // Not reached. + discard_fragment(); + return float4(0); + } + + if (0 < c.a) { + c.rgb /= c.a; + } + c = (color_matrix_body * c) + color_matrix_translation; + c *= v.color; + c = clamp(c, 0, 1); + c.rgb *= c.a; + return c; +} +` + +type Driver struct { + window uintptr + + device mtl.Device + ml ca.MetalLayer + screenRPS mtl.RenderPipelineState + rpss map[graphics.CompositeMode]mtl.RenderPipelineState + cq mtl.CommandQueue + cb mtl.CommandBuffer + + screenDrawable ca.MetalDrawable + + vb mtl.Buffer + ib mtl.Buffer + + src *Image + dst *Image + + maxImageSize int +} + +var theDriver Driver + +func Get() *Driver { + return &theDriver +} + +func (d *Driver) SetWindow(window uintptr) { + // Note that [NSApp mainWindow] returns nil when the window is borderless. + // Then the window is needed to be given. + d.window = window +} + +func (d *Driver) SetVertices(vertices []float32, indices []uint16) { + mainthread.Run(func() error { + if d.vb != (mtl.Buffer{}) { + d.vb.Release() + } + if d.ib != (mtl.Buffer{}) { + d.ib.Release() + } + d.vb = d.device.MakeBuffer(unsafe.Pointer(&vertices[0]), unsafe.Sizeof(vertices[0])*uintptr(len(vertices)), mtl.ResourceStorageModeManaged) + d.ib = d.device.MakeBuffer(unsafe.Pointer(&indices[0]), unsafe.Sizeof(indices[0])*uintptr(len(indices)), mtl.ResourceStorageModeManaged) + return nil + }) +} + +func (d *Driver) Flush() { + mainthread.Run(func() error { + if d.cb == (mtl.CommandBuffer{}) { + return nil + } + + if d.screenDrawable != (ca.MetalDrawable{}) { + d.cb.PresentDrawable(d.screenDrawable) + } + d.cb.Commit() + d.cb.WaitUntilCompleted() + + d.cb = mtl.CommandBuffer{} + d.screenDrawable = ca.MetalDrawable{} + + return nil + }) +} + +func (d *Driver) checkSize(width, height int) { + m := 0 + mainthread.Run(func() error { + if d.maxImageSize == 0 { + d.maxImageSize = 4096 + // https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf + switch { + case d.device.SupportsFeatureSet(mtl.FeatureSet_iOS_GPUFamily5_v1): + d.maxImageSize = 16384 + case d.device.SupportsFeatureSet(mtl.FeatureSet_iOS_GPUFamily4_v1): + d.maxImageSize = 16384 + case d.device.SupportsFeatureSet(mtl.FeatureSet_iOS_GPUFamily3_v1): + d.maxImageSize = 16384 + case d.device.SupportsFeatureSet(mtl.FeatureSet_iOS_GPUFamily2_v2): + d.maxImageSize = 8192 + case d.device.SupportsFeatureSet(mtl.FeatureSet_iOS_GPUFamily2_v1): + d.maxImageSize = 4096 + case d.device.SupportsFeatureSet(mtl.FeatureSet_iOS_GPUFamily1_v2): + d.maxImageSize = 8192 + case d.device.SupportsFeatureSet(mtl.FeatureSet_iOS_GPUFamily1_v1): + d.maxImageSize = 4096 + case d.device.SupportsFeatureSet(mtl.FeatureSet_tvOS_GPUFamily2_v1): + d.maxImageSize = 16384 + case d.device.SupportsFeatureSet(mtl.FeatureSet_tvOS_GPUFamily1_v1): + d.maxImageSize = 8192 + case d.device.SupportsFeatureSet(mtl.FeatureSet_macOS_GPUFamily1_v1): + d.maxImageSize = 16384 + default: + panic("metal: there is no supported feature set") + } + } + m = d.maxImageSize + return nil + }) + + if width < 1 { + panic(fmt.Sprintf("metal: width (%d) must be equal or more than 1", width)) + } + if height < 1 { + panic(fmt.Sprintf("metal: height (%d) must be equal or more than 1", height)) + } + if width > m { + panic(fmt.Sprintf("metal: width (%d) must be less than or equal to %d", width, m)) + } + if height > m { + panic(fmt.Sprintf("metal: height (%d) must be less than or equal to %d", height, m)) + } +} + +func (d *Driver) NewImage(width, height int) (graphicsdriver.Image, error) { + d.checkSize(width, height) + td := mtl.TextureDescriptor{ + PixelFormat: mtl.PixelFormatRGBA8UNorm, + Width: graphics.NextPowerOf2Int(width), + Height: graphics.NextPowerOf2Int(height), + StorageMode: mtl.StorageModeManaged, + + // MTLTextureUsageRenderTarget might cause a problematic render result. Not sure the reason. + // Usage: mtl.TextureUsageShaderRead | mtl.TextureUsageRenderTarget + Usage: mtl.TextureUsageShaderRead, + } + var t mtl.Texture + mainthread.Run(func() error { + t = d.device.MakeTexture(td) + return nil + }) + return &Image{ + driver: d, + width: width, + height: height, + texture: t, + }, nil +} + +func (d *Driver) NewScreenFramebufferImage(width, height int) (graphicsdriver.Image, error) { + mainthread.Run(func() error { + d.ml.SetDrawableSize(width, height) + return nil + }) + return &Image{ + driver: d, + width: width, + height: height, + screen: true, + }, nil +} + +func (d *Driver) Reset() error { + if err := mainthread.Run(func() error { + if d.cq != (mtl.CommandQueue{}) { + d.cq.Release() + d.cq = mtl.CommandQueue{} + } + + // TODO: Release existing rpss + if d.rpss == nil { + d.rpss = map[graphics.CompositeMode]mtl.RenderPipelineState{} + } + + var err error + d.device, err = mtl.CreateSystemDefaultDevice() + if err != nil { + return err + } + + d.ml = ca.MakeMetalLayer() + d.ml.SetDevice(d.device) + // https://developer.apple.com/documentation/quartzcore/cametallayer/1478155-pixelformat + // + // The pixel format for a Metal layer must be MTLPixelFormatBGRA8Unorm, + // MTLPixelFormatBGRA8Unorm_sRGB, MTLPixelFormatRGBA16Float, MTLPixelFormatBGRA10_XR, or + // MTLPixelFormatBGRA10_XR_sRGB. + d.ml.SetPixelFormat(mtl.PixelFormatBGRA8UNorm) + d.ml.SetMaximumDrawableCount(3) + d.ml.SetDisplaySyncEnabled(true) + + replaces := map[string]string{ + "{{.FilterNearest}}": fmt.Sprintf("%d", graphics.FilterNearest), + "{{.FilterLinear}}": fmt.Sprintf("%d", graphics.FilterLinear), + "{{.FilterScreen}}": fmt.Sprintf("%d", graphics.FilterScreen), + } + src := source + for k, v := range replaces { + src = strings.Replace(src, k, v, -1) + } + + lib, err := d.device.MakeLibrary(src, 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 + } + rpld := mtl.RenderPipelineDescriptor{ + VertexFunction: vs, + FragmentFunction: fs, + } + rpld.ColorAttachments[0].PixelFormat = d.ml.PixelFormat() + rpld.ColorAttachments[0].BlendingEnabled = true + rpld.ColorAttachments[0].DestinationAlphaBlendFactor = mtl.BlendFactorZero + rpld.ColorAttachments[0].DestinationRGBBlendFactor = mtl.BlendFactorZero + rpld.ColorAttachments[0].SourceAlphaBlendFactor = mtl.BlendFactorOne + rpld.ColorAttachments[0].SourceRGBBlendFactor = mtl.BlendFactorOne + rps, err := d.device.MakeRenderPipelineState(rpld) + if err != nil { + return err + } + d.screenRPS = rps + + conv := func(c graphics.Operation) mtl.BlendFactor { + switch c { + case graphics.Zero: + return mtl.BlendFactorZero + case graphics.One: + return mtl.BlendFactorOne + case graphics.SrcAlpha: + return mtl.BlendFactorSourceAlpha + case graphics.DstAlpha: + return mtl.BlendFactorDestinationAlpha + case graphics.OneMinusSrcAlpha: + return mtl.BlendFactorOneMinusSourceAlpha + case graphics.OneMinusDstAlpha: + return mtl.BlendFactorOneMinusDestinationAlpha + default: + panic("not reached") + } + } + + for c := graphics.CompositeModeSourceOver; c <= graphics.CompositeModeMax; c++ { + rpld := mtl.RenderPipelineDescriptor{ + VertexFunction: vs, + FragmentFunction: fs, + } + rpld.ColorAttachments[0].PixelFormat = mtl.PixelFormatRGBA8UNorm + rpld.ColorAttachments[0].BlendingEnabled = true + + src, dst := c.Operations() + rpld.ColorAttachments[0].DestinationAlphaBlendFactor = conv(dst) + rpld.ColorAttachments[0].DestinationRGBBlendFactor = conv(dst) + rpld.ColorAttachments[0].SourceAlphaBlendFactor = conv(src) + rpld.ColorAttachments[0].SourceRGBBlendFactor = conv(src) + rps, err := d.device.MakeRenderPipelineState(rpld) + if err != nil { + return err + } + d.rpss[c] = rps + } + + d.cq = d.device.MakeCommandQueue() + return nil + }); err != nil { + return err + } + + return nil +} + +func (d *Driver) Draw(indexLen int, indexOffset int, mode graphics.CompositeMode, colorM *affine.ColorM, filter graphics.Filter) error { + if err := mainthread.Run(func() error { + // NSView can be changed anytime (probably). Set this everyframe. + cocoaWindow := ns.NewWindow(unsafe.Pointer(d.window)) + cocoaWindow.ContentView().SetLayer(d.ml) + cocoaWindow.ContentView().SetWantsLayer(true) + + rpd := mtl.RenderPassDescriptor{} + if d.dst.screen { + rpd.ColorAttachments[0].LoadAction = mtl.LoadActionDontCare + rpd.ColorAttachments[0].StoreAction = mtl.StoreActionStore + } else { + rpd.ColorAttachments[0].LoadAction = mtl.LoadActionLoad + rpd.ColorAttachments[0].StoreAction = mtl.StoreActionStore + } + var t mtl.Texture + if d.dst.screen { + if d.screenDrawable == (ca.MetalDrawable{}) { + drawable, err := d.ml.NextDrawable() + if err != nil { + return err + } + d.screenDrawable = drawable + } + t = d.screenDrawable.Texture() + } else { + d.screenDrawable = ca.MetalDrawable{} + t = d.dst.texture + } + rpd.ColorAttachments[0].Texture = t + rpd.ColorAttachments[0].ClearColor = mtl.ClearColor{} + + w, h := d.dst.viewportSize() + + if d.cb == (mtl.CommandBuffer{}) { + d.cb = d.cq.MakeCommandBuffer() + } + rce := d.cb.MakeRenderCommandEncoder(rpd) + + if d.dst.screen { + rce.SetRenderPipelineState(d.screenRPS) + } else { + rce.SetRenderPipelineState(d.rpss[mode]) + } + rce.SetViewport(mtl.Viewport{0, 0, float64(w), float64(h), -1, 1}) + rce.SetVertexBuffer(d.vb, 0, 0) + + viewportSize := [...]float32{float32(w), float32(h)} + rce.SetVertexBytes(unsafe.Pointer(&viewportSize[0]), unsafe.Sizeof(viewportSize), 1) + esBody, esTranslate := colorM.UnsafeElements() + + rce.SetFragmentBytes(unsafe.Pointer(&esBody[0]), unsafe.Sizeof(esBody[0])*uintptr(len(esBody)), 2) + rce.SetFragmentBytes(unsafe.Pointer(&esTranslate[0]), unsafe.Sizeof(esTranslate[0])*uintptr(len(esTranslate)), 3) + + f := uint8(filter) + rce.SetFragmentBytes(unsafe.Pointer(&f), 1, 4) + + scale := float32(d.dst.width) / float32(d.src.width) + rce.SetFragmentBytes(unsafe.Pointer(&scale), unsafe.Sizeof(scale), 5) + + if d.src != nil { + rce.SetFragmentTexture(d.src.texture, 0) + } else { + rce.SetFragmentTexture(mtl.Texture{}, 0) + } + rce.DrawIndexedPrimitives(mtl.PrimitiveTypeTriangle, indexLen, mtl.IndexTypeUInt16, d.ib, indexOffset*2) + rce.EndEncoding() + + return nil + }); err != nil { + return err + } + + return nil +} + +func (d *Driver) ResetSource() { + d.src = nil +} + +func (d *Driver) SetVsyncEnabled(enabled bool) { + d.ml.SetDisplaySyncEnabled(enabled) +} + +func (d *Driver) VDirection() graphicsdriver.VDirection { + return graphicsdriver.VUpward +} + +func (d *Driver) IsGL() bool { + return false +} + +type Image struct { + driver *Driver + width int + height int + screen bool + texture mtl.Texture +} + +func (i *Image) viewportSize() (int, int) { + if i.screen { + return i.width, i.height + } + return graphics.NextPowerOf2Int(i.width), graphics.NextPowerOf2Int(i.height) +} + +func (i *Image) Dispose() { + i.texture.Release() +} + +func (i *Image) IsInvalidated() bool { + // TODO: Does Metal cause context lost? + // https://developer.apple.com/documentation/metal/mtlresource/1515898-setpurgeablestate + // https://developer.apple.com/documentation/metal/mtldevicenotificationhandler + return false +} + +func (i *Image) syncTexture() { + mainthread.Run(func() error { + if i.driver.cb != (mtl.CommandBuffer{}) { + panic("not reached") + } + + cb := i.driver.cq.MakeCommandBuffer() + bce := cb.MakeBlitCommandEncoder() + bce.SynchronizeTexture(i.texture, 0, 0) + bce.EndEncoding() + cb.Commit() + cb.WaitUntilCompleted() + return nil + }) +} + +func (i *Image) Pixels() ([]byte, error) { + i.driver.Flush() + i.syncTexture() + + b := make([]byte, 4*i.width*i.height) + mainthread.Run(func() error { + i.texture.GetBytes(&b[0], uintptr(4*i.width), mtl.Region{ + Size: mtl.Size{i.width, i.height, 1}, + }, 0) + return nil + }) + return b, nil +} + +func (i *Image) SetAsDestination() { + i.driver.dst = i +} + +func (i *Image) SetAsSource() { + i.driver.src = i +} + +func (i *Image) ReplacePixels(pixels []byte, x, y, width, height int) { + i.driver.Flush() + + mainthread.Run(func() error { + i.texture.ReplaceRegion(mtl.Region{ + Origin: mtl.Origin{x, y, 0}, + Size: mtl.Size{width, height, 1}, + }, 0, unsafe.Pointer(&pixels[0]), 4*width) + return nil + }) +} diff --git a/internal/graphicsdriver/metal/mtl/example_test.go b/internal/graphicsdriver/metal/mtl/example_test.go new file mode 100644 index 000000000..738687f5f --- /dev/null +++ b/internal/graphicsdriver/metal/mtl/example_test.go @@ -0,0 +1,231 @@ +// Copyright 2018 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build darwin + +package mtl_test + +import ( + "encoding/json" + "fmt" + "image" + "image/color" + "log" + "os" + "unsafe" + + "golang.org/x/image/math/f32" + + "github.com/hajimehoshi/ebiten/internal/graphicsdriver/metal/mtl" +) + +func Example_listDevices() { + allDevices := mtl.CopyAllDevices() + printJSON("all Metal devices in the system = ", allDevices) + + device, err := mtl.CreateSystemDefaultDevice() + if err != nil { + log.Fatalln(err) + } + printJSON("preferred system default Metal device = ", device) + + fmt.Println("device supports the macOS GPU family 1, version 1 feature set:", device.SupportsFeatureSet(mtl.MacOSGPUFamily1V1)) + fmt.Println("device supports the macOS GPU family 1, version 2 feature set:", device.SupportsFeatureSet(mtl.MacOSGPUFamily1V2)) + fmt.Println("device supports the macOS read-write texture, tier 2 feature set:", device.SupportsFeatureSet(mtl.MacOSReadWriteTextureTier2)) + fmt.Println("device supports the macOS GPU family 1, version 3 feature set:", device.SupportsFeatureSet(mtl.MacOSGPUFamily1V3)) + fmt.Println("device supports the macOS GPU family 1, version 4 feature set:", device.SupportsFeatureSet(mtl.MacOSGPUFamily1V4)) + fmt.Println("device supports the macOS GPU family 2, version 1 feature set:", device.SupportsFeatureSet(mtl.MacOSGPUFamily2V1)) + + // Sample output: + // all Metal devices in the system = [ + // { + // "Headless": false, + // "LowPower": true, + // "Removable": false, + // "RegistryID": 4294968287, + // "Name": "Intel Iris Pro Graphics" + // }, + // { + // "Headless": false, + // "LowPower": false, + // "Removable": false, + // "RegistryID": 4294968322, + // "Name": "AMD Radeon R9 M370X" + // } + // ] + // preferred system default Metal device = { + // "Headless": false, + // "LowPower": false, + // "Removable": false, + // "RegistryID": 4294968322, + // "Name": "AMD Radeon R9 M370X" + // } + // device supports the macOS GPU family 1, version 1 feature set: true + // device supports the macOS GPU family 1, version 2 feature set: true + // device supports the macOS read-write texture, tier 2 feature set: true + // device supports the macOS GPU family 1, version 3 feature set: true + // device supports the macOS GPU family 1, version 4 feature set: true + // device supports the macOS GPU family 2, version 1 feature set: true +} + +// printJSON prints label, then v as JSON encoded with indent to stdout. It panics on any error. +// It's meant to be used by examples to print the output. +func printJSON(label string, v interface{}) { + fmt.Print(label) + w := json.NewEncoder(os.Stdout) + w.SetIndent("", "\t") + err := w.Encode(v) + if err != nil { + panic(err) + } +} + +func Example_renderTriangle() { + device, err := mtl.CreateSystemDefaultDevice() + if err != nil { + log.Fatalln(err) + } + + // Create a render pipeline state. + const source = `#include + +using namespace metal; + +struct Vertex { + float4 position [[position]]; + float4 color; +}; + +vertex Vertex VertexShader( + uint vertexID [[vertex_id]], + device Vertex * vertices [[buffer(0)]] +) { + return vertices[vertexID]; +} + +fragment float4 FragmentShader(Vertex in [[stage_in]]) { + return in.color; +} +` + lib, err := device.MakeLibrary(source, mtl.CompileOptions{}) + if err != nil { + log.Fatalln(err) + } + vs, err := lib.MakeFunction("VertexShader") + if err != nil { + log.Fatalln(err) + } + fs, err := lib.MakeFunction("FragmentShader") + if err != nil { + log.Fatalln(err) + } + var rpld mtl.RenderPipelineDescriptor + rpld.VertexFunction = vs + rpld.FragmentFunction = fs + rpld.ColorAttachments[0].PixelFormat = mtl.PixelFormatRGBA8UNorm + rps, err := device.MakeRenderPipelineState(rpld) + if err != nil { + log.Fatalln(err) + } + + // Create a vertex buffer. + type Vertex struct { + Position f32.Vec4 + Color f32.Vec4 + } + vertexData := [...]Vertex{ + {f32.Vec4{+0.00, +0.75, 0, 1}, f32.Vec4{1, 1, 1, 1}}, + {f32.Vec4{-0.75, -0.75, 0, 1}, f32.Vec4{1, 1, 1, 1}}, + {f32.Vec4{+0.75, -0.75, 0, 1}, f32.Vec4{0, 0, 0, 1}}, + } + vertexBuffer := device.MakeBuffer(unsafe.Pointer(&vertexData[0]), unsafe.Sizeof(vertexData), mtl.ResourceStorageModeManaged) + + // Create an output texture to render into. + td := mtl.TextureDescriptor{ + PixelFormat: mtl.PixelFormatRGBA8UNorm, + Width: 80, + Height: 20, + StorageMode: mtl.StorageModeManaged, + } + texture := device.MakeTexture(td) + + cq := device.MakeCommandQueue() + 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, Green: 0, Blue: 0, Alpha: 1} + rpd.ColorAttachments[0].Texture = texture + rce := cb.MakeRenderCommandEncoder(rpd) + rce.SetRenderPipelineState(rps) + rce.SetVertexBuffer(vertexBuffer, 0, 0) + rce.DrawPrimitives(mtl.PrimitiveTypeTriangle, 0, 3) + rce.EndEncoding() + + // Encode all blit commands. + bce := cb.MakeBlitCommandEncoder() + bce.Synchronize(texture) + bce.EndEncoding() + + cb.Commit() + cb.WaitUntilCompleted() + + // Read pixels from output texture into an image. + img := image.NewNRGBA(image.Rect(0, 0, texture.Width, texture.Height)) + bytesPerRow := 4 * texture.Width + region := mtl.RegionMake2D(0, 0, texture.Width, texture.Height) + texture.GetBytes(&img.Pix[0], uintptr(bytesPerRow), region, 0) + + // Output image to stdout as grayscale ASCII art. + levels := []struct { + MinY uint8 + Shade string + }{{220, " "}, {170, "░"}, {85, "▒"}, {35, "▓"}, {0, "█"}} + for y := img.Bounds().Min.Y; y < img.Bounds().Max.Y; y++ { + for x := img.Bounds().Min.X; x < img.Bounds().Max.X; x++ { + c := color.GrayModel.Convert(img.At(x, y)).(color.Gray) + for _, l := range levels { + if c.Y >= l.MinY { + fmt.Print(l.Shade) + break + } + } + } + fmt.Println() + } + + // Output: + // ████████████████████████████████████████████████████████████████████████████████ + // ████████████████████████████████████████████████████████████████████████████████ + // ████████████████████████████████████████████████████████████████████████████████ + // ██████████████████████████████████████ ██████████████████████████████████████ + // ████████████████████████████████████ ████████████████████████████████████ + // ██████████████████████████████████ ░░░░██████████████████████████████████ + // ████████████████████████████████ ░░░░░░░░████████████████████████████████ + // ██████████████████████████████ ░░░░░░░░░░░░██████████████████████████████ + // ████████████████████████████ ░░░░░░░░░░░░▒▒▒▒████████████████████████████ + // ██████████████████████████ ░░░░░░░░░░░░▒▒▒▒▒▒▒▒██████████████████████████ + // ████████████████████████ ░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒████████████████████████ + // ██████████████████████ ░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒██████████████████████ + // ████████████████████ ░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒████████████████████ + // ██████████████████ ░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓██████████████████ + // ████████████████ ░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓████████████████ + // ██████████████ ░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓██████████████ + // ████████████ ░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓████████████████ + // ████████████████████████████████████████████████████████████████████████████████ + // ████████████████████████████████████████████████████████████████████████████████ + // ████████████████████████████████████████████████████████████████████████████████ +} diff --git a/internal/graphicsdriver/metal/mtl/mtl.go b/internal/graphicsdriver/metal/mtl/mtl.go new file mode 100644 index 000000000..68e3370fb --- /dev/null +++ b/internal/graphicsdriver/metal/mtl/mtl.go @@ -0,0 +1,867 @@ +// Copyright 2018 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build darwin + +// Package mtl provides access to Apple's Metal API (https://developer.apple.com/documentation/metal). +// +// Package mtl requires macOS version 10.13 or newer. +// +// This package is in very early stages of development. +// The API will change when opportunities for improvement are discovered; it is not yet frozen. +// Less than 20% of the Metal API surface is implemented. +// Current functionality is sufficient to render very basic geometry. +package mtl + +import ( + "errors" + "fmt" + "unsafe" +) + +// #cgo LDFLAGS: -framework Metal -framework Foundation +// +// #include +// #include "mtl.h" +// struct Library Go_Device_MakeLibrary(void * device, _GoString_ source) { +// return Device_MakeLibrary(device, _GoStringPtr(source), _GoStringLen(source)); +// } +import "C" + +// FeatureSet defines a specific platform, hardware, and software configuration. +// +// Reference: https://developer.apple.com/documentation/metal/mtlfeatureset. +type FeatureSet uint16 + +// The device feature sets that define specific platform, hardware, and software configurations. +const ( + MacOSGPUFamily1V1 FeatureSet = 10000 // The GPU family 1, version 1 feature set for macOS. + MacOSGPUFamily1V2 FeatureSet = 10001 // The GPU family 1, version 2 feature set for macOS. + MacOSReadWriteTextureTier2 FeatureSet = 10002 // The read-write texture, tier 2 feature set for macOS. + MacOSGPUFamily1V3 FeatureSet = 10003 // The GPU family 1, version 3 feature set for macOS. + MacOSGPUFamily1V4 FeatureSet = 10004 // The GPU family 1, version 4 feature set for macOS. + MacOSGPUFamily2V1 FeatureSet = 10005 // The GPU family 2, version 1 feature set for macOS. +) + +const ( + FeatureSet_iOS_GPUFamily1_v1 FeatureSet = 0 + FeatureSet_iOS_GPUFamily1_v2 FeatureSet = 2 + FeatureSet_iOS_GPUFamily1_v3 FeatureSet = 5 + FeatureSet_iOS_GPUFamily1_v4 FeatureSet = 8 + FeatureSet_iOS_GPUFamily1_v5 FeatureSet = 12 + FeatureSet_iOS_GPUFamily2_v1 FeatureSet = 1 + FeatureSet_iOS_GPUFamily2_v2 FeatureSet = 3 + FeatureSet_iOS_GPUFamily2_v3 FeatureSet = 6 + FeatureSet_iOS_GPUFamily2_v4 FeatureSet = 9 + FeatureSet_iOS_GPUFamily2_v5 FeatureSet = 13 + FeatureSet_iOS_GPUFamily3_v1 FeatureSet = 4 + FeatureSet_iOS_GPUFamily3_v2 FeatureSet = 7 + FeatureSet_iOS_GPUFamily3_v3 FeatureSet = 10 + FeatureSet_iOS_GPUFamily3_v4 FeatureSet = 14 + FeatureSet_iOS_GPUFamily4_v1 FeatureSet = 11 + FeatureSet_iOS_GPUFamily4_v2 FeatureSet = 15 + FeatureSet_iOS_GPUFamily5_v1 FeatureSet = 16 + FeatureSet_tvOS_GPUFamily1_v1 FeatureSet = 30000 + FeatureSet_tvOS_GPUFamily1_v2 FeatureSet = 30001 + FeatureSet_tvOS_GPUFamily1_v3 FeatureSet = 30002 + FeatureSet_tvOS_GPUFamily1_v4 FeatureSet = 30004 + FeatureSet_tvOS_GPUFamily2_v1 FeatureSet = 30003 + FeatureSet_tvOS_GPUFamily2_v2 FeatureSet = 30005 + FeatureSet_macOS_GPUFamily1_v1 FeatureSet = 10000 + FeatureSet_macOS_GPUFamily1_v2 FeatureSet = 10001 + FeatureSet_macOS_GPUFamily1_v3 FeatureSet = 10003 + FeatureSet_macOS_GPUFamily1_v4 FeatureSet = 10004 + FeatureSet_macOS_GPUFamily2_v1 FeatureSet = 10005 + FeatureSet_macOS_ReadWriteTextureTier2 FeatureSet = 10002 +) + +// PixelFormat defines data formats that describe the organization +// and characteristics of individual pixels in a texture. +// +// Reference: https://developer.apple.com/documentation/metal/mtlpixelformat. +type PixelFormat uint16 + +// 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. + PixelFormatRGBA8UNormSRGB PixelFormat = 71 // Ordinary format with four 8-bit normalized unsigned integer components in RGBA order with conversion between sRGB and linear space. + 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. +// +// Reference: https://developer.apple.com/documentation/metal/mtlprimitivetype. +type PrimitiveType uint8 + +// Geometric primitive types for drawing commands. +const ( + PrimitiveTypePoint PrimitiveType = 0 + PrimitiveTypeLine PrimitiveType = 1 + PrimitiveTypeLineStrip PrimitiveType = 2 + PrimitiveTypeTriangle PrimitiveType = 3 + PrimitiveTypeTriangleStrip PrimitiveType = 4 +) + +// LoadAction defines actions performed at the start of a rendering pass +// for a render command encoder. +// +// Reference: https://developer.apple.com/documentation/metal/mtlloadaction. +type LoadAction uint8 + +// Actions performed at the start of a rendering pass for a render command encoder. +const ( + LoadActionDontCare LoadAction = 0 + LoadActionLoad LoadAction = 1 + LoadActionClear LoadAction = 2 +) + +// StoreAction defines actions performed at the end of a rendering pass +// for a render command encoder. +// +// Reference: https://developer.apple.com/documentation/metal/mtlstoreaction. +type StoreAction uint8 + +// Actions performed at the end of a rendering pass for a render command encoder. +const ( + StoreActionDontCare StoreAction = 0 + StoreActionStore StoreAction = 1 + StoreActionMultisampleResolve StoreAction = 2 + StoreActionStoreAndMultisampleResolve StoreAction = 3 + StoreActionUnknown StoreAction = 4 + StoreActionCustomSampleDepthStore StoreAction = 5 +) + +// StorageMode defines defines the memory location and access permissions of a resource. +// +// Reference: https://developer.apple.com/documentation/metal/mtlstoragemode. +type StorageMode uint8 + +const ( + // StorageModeShared indicates that the resource is stored in system memory + // accessible to both the CPU and the GPU. + StorageModeShared StorageMode = 0 + + // StorageModeManaged indicates that the resource exists as a synchronized + // memory pair with one copy stored in system memory accessible to the CPU + // and another copy stored in video memory accessible to the GPU. + StorageModeManaged StorageMode = 1 + + // StorageModePrivate indicates that the resource is stored in memory + // only accessible to the GPU. In iOS and tvOS, the resource is stored in + // system memory. In macOS, the resource is stored in video memory. + StorageModePrivate StorageMode = 2 + + // StorageModeMemoryless indicates that the resource is stored in on-tile memory, + // without CPU or GPU memory backing. The contents of the on-tile memory are undefined + // and do not persist; the only way to populate the resource is to render into it. + // Memoryless resources are limited to temporary render targets (i.e., Textures configured + // with a TextureDescriptor and used with a RenderPassAttachmentDescriptor). + StorageModeMemoryless StorageMode = 3 +) + +// ResourceOptions defines optional arguments used to create +// and influence behavior of buffer and texture objects. +// +// Reference: https://developer.apple.com/documentation/metal/mtlresourceoptions. +type ResourceOptions uint16 + +const ( + // ResourceCPUCacheModeDefaultCache is the default CPU cache mode for the resource. + // Guarantees that read and write operations are executed in the expected order. + ResourceCPUCacheModeDefaultCache ResourceOptions = ResourceOptions(CPUCacheModeDefaultCache) << resourceCPUCacheModeShift + + // ResourceCPUCacheModeWriteCombined is a write-combined CPU cache mode for the resource. + // Optimized for resources that the CPU will write into, but never read. + ResourceCPUCacheModeWriteCombined ResourceOptions = ResourceOptions(CPUCacheModeWriteCombined) << resourceCPUCacheModeShift + + // ResourceStorageModeShared indicates that the resource is stored in system memory + // accessible to both the CPU and the GPU. + ResourceStorageModeShared ResourceOptions = ResourceOptions(StorageModeShared) << resourceStorageModeShift + + // ResourceStorageModeManaged indicates that the resource exists as a synchronized + // memory pair with one copy stored in system memory accessible to the CPU + // and another copy stored in video memory accessible to the GPU. + ResourceStorageModeManaged ResourceOptions = ResourceOptions(StorageModeManaged) << resourceStorageModeShift + + // ResourceStorageModePrivate indicates that the resource is stored in memory + // only accessible to the GPU. In iOS and tvOS, the resource is stored + // in system memory. In macOS, the resource is stored in video memory. + ResourceStorageModePrivate ResourceOptions = ResourceOptions(StorageModePrivate) << resourceStorageModeShift + + // ResourceStorageModeMemoryless indicates that the resource is stored in on-tile memory, + // without CPU or GPU memory backing. The contents of the on-tile memory are undefined + // and do not persist; the only way to populate the resource is to render into it. + // Memoryless resources are limited to temporary render targets (i.e., Textures configured + // with a TextureDescriptor and used with a RenderPassAttachmentDescriptor). + ResourceStorageModeMemoryless ResourceOptions = ResourceOptions(StorageModeMemoryless) << resourceStorageModeShift + + // ResourceHazardTrackingModeUntracked indicates that the command encoder dependencies + // for this resource are tracked manually with Fence objects. This value is always set + // for resources sub-allocated from a Heap object and may optionally be specified for + // non-heap resources. + ResourceHazardTrackingModeUntracked ResourceOptions = 1 << resourceHazardTrackingModeShift +) + +const ( + resourceCPUCacheModeShift = 0 + resourceStorageModeShift = 4 + resourceHazardTrackingModeShift = 8 +) + +// CPUCacheMode is the CPU cache mode that defines the CPU mapping of a resource. +// +// Reference: https://developer.apple.com/documentation/metal/mtlcpucachemode. +type CPUCacheMode uint8 + +const ( + // CPUCacheModeDefaultCache is the default CPU cache mode for the resource. + // Guarantees that read and write operations are executed in the expected order. + CPUCacheModeDefaultCache CPUCacheMode = 0 + + // CPUCacheModeWriteCombined is a write-combined CPU cache mode for the resource. + // Optimized for resources that the CPU will write into, but never read. + CPUCacheModeWriteCombined CPUCacheMode = 1 +) + +// IndexType is the index type for an index buffer that references vertices of geometric primitives. +// +// Reference: https://developer.apple.com/documentation/metal/mtlstoragemode +type IndexType uint8 + +const ( + // IndexTypeUInt16 is a 16-bit unsigned integer used as a primitive index. + IndexTypeUInt16 IndexType = 0 + + // IndexTypeUInt32 is a 32-bit unsigned integer used as a primitive index. + IndexTypeUInt32 IndexType = 1 +) + +type TextureUsage uint8 + +const ( + TextureUsageUnknown TextureUsage = 0x0000 + TextureUsageShaderRead TextureUsage = 0x0001 + TextureUsageShaderWrite TextureUsage = 0x0002 + TextureUsageRenderTarget TextureUsage = 0x0004 + TextureUsagePixelFormatView TextureUsage = 0x0008 +) + +type BlendFactor uint8 + +const ( + BlendFactorZero BlendFactor = 0 + BlendFactorOne BlendFactor = 1 + BlendFactorSourceColor BlendFactor = 2 + BlendFactorOneMinusSourceColor BlendFactor = 3 + BlendFactorSourceAlpha BlendFactor = 4 + BlendFactorOneMinusSourceAlpha BlendFactor = 5 + BlendFactorDestinationColor BlendFactor = 6 + BlendFactorOneMinusDestinationColor BlendFactor = 7 + BlendFactorDestinationAlpha BlendFactor = 8 + BlendFactorOneMinusDestinationAlpha BlendFactor = 9 + BlendFactorSourceAlphaSaturated BlendFactor = 10 + BlendFactorBlendColor BlendFactor = 11 + BlendFactorOneMinusBlendColor BlendFactor = 12 + BlendFactorBlendAlpha BlendFactor = 13 + BlendFactorOneMinusBlendAlpha BlendFactor = 14 + BlendFactorSource1Color BlendFactor = 15 + BlendFactorOneMinusSource1Color BlendFactor = 16 + BlendFactorSource1Alpha BlendFactor = 17 + BlendFactorOneMinusSource1Alpha BlendFactor = 18 +) + +// 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 pointer. + resource() unsafe.Pointer +} + +// RenderPipelineDescriptor configures new RenderPipelineState objects. +// +// Reference: https://developer.apple.com/documentation/metal/mtlrenderpipelinedescriptor. +type RenderPipelineDescriptor struct { + // VertexFunction is a programmable function that processes individual vertices in a rendering pass. + VertexFunction Function + + // FragmentFunction is a programmable function that processes individual fragments in a rendering pass. + FragmentFunction Function + + // ColorAttachments is an array of attachments that store color data. + ColorAttachments [1]RenderPipelineColorAttachmentDescriptor +} + +// RenderPipelineColorAttachmentDescriptor describes a color render target that specifies +// the color configuration and color operations associated with a render pipeline. +// +// Reference: https://developer.apple.com/documentation/metal/mtlrenderpipelinecolorattachmentdescriptor. +type RenderPipelineColorAttachmentDescriptor struct { + // PixelFormat is the pixel format of the color attachment's texture. + PixelFormat PixelFormat + + BlendingEnabled bool + + DestinationAlphaBlendFactor BlendFactor + DestinationRGBBlendFactor BlendFactor + SourceAlphaBlendFactor BlendFactor + SourceRGBBlendFactor BlendFactor +} + +// RenderPassDescriptor describes a group of render targets that serve as +// the output destination for pixels generated by a render pass. +// +// Reference: https://developer.apple.com/documentation/metal/mtlrenderpassdescriptor. +type RenderPassDescriptor struct { + // ColorAttachments is array of state information for attachments that store color data. + ColorAttachments [1]RenderPassColorAttachmentDescriptor +} + +// RenderPassColorAttachmentDescriptor describes a color render target that serves +// as the output destination for color pixels generated by a render pass. +// +// Reference: https://developer.apple.com/documentation/metal/mtlrenderpasscolorattachmentdescriptor. +type RenderPassColorAttachmentDescriptor struct { + RenderPassAttachmentDescriptor + ClearColor ClearColor +} + +// RenderPassAttachmentDescriptor describes a render target that serves +// as the output destination for pixels generated by a render pass. +// +// Reference: https://developer.apple.com/documentation/metal/mtlrenderpassattachmentdescriptor. +type RenderPassAttachmentDescriptor struct { + LoadAction LoadAction + StoreAction StoreAction + Texture Texture +} + +// ClearColor is an RGBA value used for a color pixel. +// +// Reference: https://developer.apple.com/documentation/metal/mtlclearcolor. +type ClearColor struct { + Red, Green, Blue, Alpha float64 +} + +// TextureDescriptor configures new Texture objects. +// +// Reference: https://developer.apple.com/documentation/metal/mtltexturedescriptor. +type TextureDescriptor struct { + PixelFormat PixelFormat + Width int + Height int + StorageMode StorageMode + Usage TextureUsage +} + +// Device is abstract representation of the GPU that +// serves as the primary interface for a Metal app. +// +// Reference: https://developer.apple.com/documentation/metal/mtldevice. +type Device struct { + device unsafe.Pointer + + // Headless indicates whether a device is configured as headless. + Headless bool + + // LowPower indicates whether a device is low-power. + LowPower bool + + // Removable determines whether or not a GPU is removable. + Removable bool + + // RegistryID is the registry ID value for the device. + RegistryID uint64 + + // Name is the name of the device. + Name string +} + +// CreateSystemDefaultDevice returns the preferred system default Metal device. +// +// Reference: https://developer.apple.com/documentation/metal/1433401-mtlcreatesystemdefaultdevice. +func CreateSystemDefaultDevice() (Device, error) { + d := C.CreateSystemDefaultDevice() + if d.Device == nil { + return Device{}, errors.New("Metal is not supported on this system") + } + + return Device{ + device: d.Device, + Headless: d.Headless != 0, + LowPower: d.LowPower != 0, + Removable: d.Removable != 0, + RegistryID: uint64(d.RegistryID), + Name: C.GoString(d.Name), + }, nil +} + +// CopyAllDevices returns all Metal devices in the system. +// +// Reference: https://developer.apple.com/documentation/metal/1433367-mtlcopyalldevices. +func CopyAllDevices() []Device { + d := C.CopyAllDevices() + defer C.free(unsafe.Pointer(d.Devices)) + + ds := make([]Device, d.Length) + for i := 0; i < len(ds); i++ { + d := (*C.struct_Device)(unsafe.Pointer(uintptr(unsafe.Pointer(d.Devices)) + uintptr(i)*C.sizeof_struct_Device)) + + ds[i].device = d.Device + ds[i].Headless = d.Headless != 0 + ds[i].LowPower = d.LowPower != 0 + ds[i].Removable = d.Removable != 0 + ds[i].RegistryID = uint64(d.RegistryID) + ds[i].Name = C.GoString(d.Name) + } + return ds +} + +// Device returns the underlying id 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 +} + +// MakeCommandQueue creates a serial command submission queue. +// +// Reference: https://developer.apple.com/documentation/metal/mtldevice/1433388-makecommandqueue. +func (d Device) MakeCommandQueue() CommandQueue { + return CommandQueue{C.Device_MakeCommandQueue(d.device)} +} + +// MakeLibrary creates a new library that contains +// the functions stored in the specified source string. +// +// Reference: https://developer.apple.com/documentation/metal/mtldevice/1433431-makelibrary. +func (d Device) MakeLibrary(source string, opt CompileOptions) (Library, error) { + l := C.Go_Device_MakeLibrary(d.device, source) // TODO: opt. + if l.Library == nil { + return Library{}, errors.New(C.GoString(l.Error)) + } + + return Library{l.Library}, nil +} + +// MakeRenderPipelineState creates a render pipeline state object. +// +// Reference: https://developer.apple.com/documentation/metal/mtldevice/1433369-makerenderpipelinestate. +func (d Device) MakeRenderPipelineState(rpd RenderPipelineDescriptor) (RenderPipelineState, error) { + blendingEnabled := C.BOOL(0) + if rpd.ColorAttachments[0].BlendingEnabled { + blendingEnabled = C.BOOL(1) + } + c := &rpd.ColorAttachments[0] + descriptor := C.struct_RenderPipelineDescriptor{ + VertexFunction: rpd.VertexFunction.function, + FragmentFunction: rpd.FragmentFunction.function, + ColorAttachment0PixelFormat: C.uint16_t(c.PixelFormat), + ColorAttachment0BlendingEnabled: C.BOOL(blendingEnabled), + ColorAttachment0DestinationAlphaBlendFactor: C.uint8_t(c.DestinationAlphaBlendFactor), + ColorAttachment0DestinationRGBBlendFactor: C.uint8_t(c.DestinationRGBBlendFactor), + ColorAttachment0SourceAlphaBlendFactor: C.uint8_t(c.SourceAlphaBlendFactor), + ColorAttachment0SourceRGBBlendFactor: C.uint8_t(c.SourceRGBBlendFactor), + } + rps := C.Device_MakeRenderPipelineState(d.device, descriptor) + if rps.RenderPipelineState == nil { + return RenderPipelineState{}, errors.New(C.GoString(rps.Error)) + } + + return RenderPipelineState{rps.RenderPipelineState}, nil +} + +// MakeBuffer allocates a new buffer of a given length +// and initializes its contents by copying existing data into it. +// +// Reference: https://developer.apple.com/documentation/metal/mtldevice/1433429-makebuffer. +func (d Device) MakeBuffer(bytes unsafe.Pointer, length uintptr, opt ResourceOptions) Buffer { + return Buffer{C.Device_MakeBuffer(d.device, bytes, C.size_t(length), C.uint16_t(opt))} +} + +// MakeTexture creates a texture object with privately owned storage +// that contains texture state. +// +// Reference: https://developer.apple.com/documentation/metal/mtldevice/1433425-maketexture. +func (d Device) MakeTexture(td TextureDescriptor) Texture { + descriptor := C.struct_TextureDescriptor{ + PixelFormat: C.uint16_t(td.PixelFormat), + Width: C.uint_t(td.Width), + Height: C.uint_t(td.Height), + StorageMode: C.uint8_t(td.StorageMode), + Usage: C.uint8_t(td.Usage), + } + return Texture{ + texture: C.Device_MakeTexture(d.device, descriptor), + Width: td.Width, // TODO: Fetch dimensions of actually created texture. + Height: td.Height, // TODO: Fetch dimensions of actually created texture. + } +} + +// CompileOptions specifies optional compilation settings for +// the graphics or compute functions within a library. +// +// 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 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 { + commandQueue unsafe.Pointer +} + +func (c CommandQueue) Release() { + C.CommandQueue_Release(c.commandQueue) +} + +// MakeCommandBuffer creates a command buffer. +// +// Reference: https://developer.apple.com/documentation/metal/mtlcommandqueue/1508686-makecommandbuffer. +func (cq CommandQueue) MakeCommandBuffer() CommandBuffer { + return CommandBuffer{C.CommandQueue_MakeCommandBuffer(cq.commandQueue)} +} + +// CommandBuffer is a container that stores encoded commands +// that are committed to and executed by the GPU. +// +// 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) +} + +// WaitUntilCompleted waits for the execution of this command buffer to complete. +// +// Reference: https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443039-waituntilcompleted. +func (cb CommandBuffer) WaitUntilCompleted() { + C.CommandBuffer_WaitUntilCompleted(cb.commandBuffer) +} + +// MakeRenderCommandEncoder creates an encoder object that can +// encode graphics rendering commands into this command buffer. +// +// Reference: https://developer.apple.com/documentation/metal/mtlcommandbuffer/1442999-makerendercommandencoder. +func (cb CommandBuffer) MakeRenderCommandEncoder(rpd RenderPassDescriptor) RenderCommandEncoder { + descriptor := C.struct_RenderPassDescriptor{ + ColorAttachment0LoadAction: C.uint8_t(rpd.ColorAttachments[0].LoadAction), + ColorAttachment0StoreAction: C.uint8_t(rpd.ColorAttachments[0].StoreAction), + ColorAttachment0ClearColor: C.struct_ClearColor{ + Red: C.double(rpd.ColorAttachments[0].ClearColor.Red), + Green: C.double(rpd.ColorAttachments[0].ClearColor.Green), + Blue: C.double(rpd.ColorAttachments[0].ClearColor.Blue), + Alpha: C.double(rpd.ColorAttachments[0].ClearColor.Alpha), + }, + ColorAttachment0Texture: rpd.ColorAttachments[0].Texture.texture, + } + return RenderCommandEncoder{CommandEncoder{C.CommandBuffer_MakeRenderCommandEncoder(cb.commandBuffer, descriptor)}} +} + +// MakeBlitCommandEncoder creates an encoder object that can encode +// memory operation (blit) commands into this command buffer. +// +// Reference: https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443001-makeblitcommandencoder. +func (cb CommandBuffer) MakeBlitCommandEncoder() BlitCommandEncoder { + return BlitCommandEncoder{CommandEncoder{C.CommandBuffer_MakeBlitCommandEncoder(cb.commandBuffer)}} +} + +// CommandEncoder is an encoder that writes sequential GPU commands +// into a command buffer. +// +// Reference: https://developer.apple.com/documentation/metal/mtlcommandencoder. +type CommandEncoder struct { + commandEncoder unsafe.Pointer +} + +// EndEncoding declares that all command generation from this encoder is completed. +// +// Reference: https://developer.apple.com/documentation/metal/mtlcommandencoder/1458038-endencoding. +func (ce CommandEncoder) EndEncoding() { + C.CommandEncoder_EndEncoding(ce.commandEncoder) +} + +// RenderCommandEncoder is an encoder that specifies graphics-rendering commands +// and executes graphics functions. +// +// Reference: https://developer.apple.com/documentation/metal/mtlrendercommandencoder. +type RenderCommandEncoder struct { + CommandEncoder +} + +func (rce RenderCommandEncoder) Release() { + C.RenderCommandEncoder_Release(rce.commandEncoder) +} + +// SetRenderPipelineState sets the current render pipeline state object. +// +// Reference: https://developer.apple.com/documentation/metal/mtlrendercommandencoder/1515811-setrenderpipelinestate. +func (rce RenderCommandEncoder) SetRenderPipelineState(rps RenderPipelineState) { + C.RenderCommandEncoder_SetRenderPipelineState(rce.commandEncoder, rps.renderPipelineState) +} + +func (rce RenderCommandEncoder) SetViewport(viewport Viewport) { + C.RenderCommandEncoder_SetViewport(rce.commandEncoder, viewport.c()) +} + +// SetVertexBuffer sets a buffer for the vertex shader function at an index +// in the buffer argument table with an offset that specifies the start of the data. +// +// Reference: https://developer.apple.com/documentation/metal/mtlrendercommandencoder/1515829-setvertexbuffer. +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)) +} + +func (rce RenderCommandEncoder) SetFragmentBytes(bytes unsafe.Pointer, length uintptr, index int) { + C.RenderCommandEncoder_SetFragmentBytes(rce.commandEncoder, bytes, C.size_t(length), C.uint_t(index)) +} + +// SetFragmentTexture sets a texture for the fragment function at an index in the texture argument table. +// +// Reference: https://developer.apple.com/documentation/metal/mtlrendercommandencoder/1515390-setfragmenttexture +func (rce RenderCommandEncoder) SetFragmentTexture(texture Texture, index int) { + C.RenderCommandEncoder_SetFragmentTexture(rce.commandEncoder, texture.texture, C.uint_t(index)) +} + +func (rce RenderCommandEncoder) SetBlendColor(red, green, blue, alpha float32) { + C.RenderCommandEncoder_SetBlendColor(rce.commandEncoder, C.float(red), C.float(green), C.float(blue), C.float(alpha)) +} + +// DrawPrimitives renders one instance of primitives using vertex data +// in contiguous array elements. +// +// Reference: https://developer.apple.com/documentation/metal/mtlrendercommandencoder/1516326-drawprimitives. +func (rce RenderCommandEncoder) DrawPrimitives(typ PrimitiveType, vertexStart, vertexCount int) { + C.RenderCommandEncoder_DrawPrimitives(rce.commandEncoder, C.uint8_t(typ), C.uint_t(vertexStart), C.uint_t(vertexCount)) +} + +// DrawIndexedPrimitives encodes a command to render one instance of primitives using an index list specified in a buffer. +// +// Reference: https://developer.apple.com/documentation/metal/mtlrendercommandencoder/1515542-drawindexedprimitives +func (rce RenderCommandEncoder) DrawIndexedPrimitives(typ PrimitiveType, indexCount int, indexType IndexType, indexBuffer Buffer, indexBufferOffset int) { + C.RenderCommandEncoder_DrawIndexedPrimitives(rce.commandEncoder, C.uint8_t(typ), C.uint_t(indexCount), C.uint8_t(indexType), indexBuffer.buffer, C.uint_t(indexBufferOffset)) +} + +// BlitCommandEncoder is an encoder that specifies resource copy +// and resource synchronization commands. +// +// Reference: https://developer.apple.com/documentation/metal/mtlblitcommandencoder. +type BlitCommandEncoder struct { + CommandEncoder +} + +// Synchronize flushes any copy of the specified resource from its corresponding +// Device caches and, if needed, invalidates any CPU caches. +// +// Reference: https://developer.apple.com/documentation/metal/mtlblitcommandencoder/1400775-synchronize. +func (bce BlitCommandEncoder) Synchronize(resource Resource) { + C.BlitCommandEncoder_Synchronize(bce.commandEncoder, resource.resource()) +} + +func (bce BlitCommandEncoder) SynchronizeTexture(texture Texture, slice int, level int) { + C.BlitCommandEncoder_SynchronizeTexture(bce.commandEncoder, texture.texture, C.uint_t(slice), C.uint_t(level)) +} + +// Library is a collection of compiled graphics or compute functions. +// +// Reference: https://developer.apple.com/documentation/metal/mtllibrary. +type Library struct { + library unsafe.Pointer +} + +// MakeFunction returns a pre-compiled, non-specialized function. +// +// Reference: https://developer.apple.com/documentation/metal/mtllibrary/1515524-makefunction. +func (l Library) MakeFunction(name string) (Function, error) { + f := C.Library_MakeFunction(l.library, C.CString(name)) + if f == nil { + return Function{}, fmt.Errorf("function %q not found", name) + } + + return Function{f}, nil +} + +// Texture is a memory allocation for storing formatted +// image data that is accessible to the GPU. +// +// 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 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 } + +func (t Texture) Release() { + C.Texture_Release(t.texture) +} + +// GetBytes copies a block of pixels from the storage allocation of texture +// slice zero into system memory at a specified address. +// +// Reference: https://developer.apple.com/documentation/metal/mtltexture/1515751-getbytes. +func (t Texture) GetBytes(pixelBytes *byte, bytesPerRow uintptr, region Region, level int) { + r := region.c() + C.Texture_GetBytes(t.texture, unsafe.Pointer(pixelBytes), C.size_t(bytesPerRow), r, C.uint_t(level)) +} + +// ReplaceRegion copies a block of pixels from the caller's pointer into the storage allocation for slice 0 of a texture. +// +// Reference: https://developer.apple.com/documentation/metal/mtltexture/1515464-replaceregion +func (t Texture) ReplaceRegion(region Region, level int, pixelBytes unsafe.Pointer, bytesPerRow int) { + r := region.c() + C.Texture_ReplaceRegion(t.texture, r, C.uint_t(level), pixelBytes, C.uint_t(bytesPerRow)) +} + +// Buffer is a memory allocation for storing unformatted data +// that is accessible to the GPU. +// +// Reference: https://developer.apple.com/documentation/metal/mtlbuffer. +type Buffer struct { + buffer unsafe.Pointer +} + +func (b Buffer) Release() { + C.Buffer_Release(b.buffer) +} + +// Function represents a programmable graphics or compute function executed by the GPU. +// +// Reference: https://developer.apple.com/documentation/metal/mtlfunction. +type Function struct { + function unsafe.Pointer +} + +// RenderPipelineState contains the graphics functions +// and configuration state used in a render pass. +// +// Reference: https://developer.apple.com/documentation/metal/mtlrenderpipelinestate. +type RenderPipelineState struct { + renderPipelineState unsafe.Pointer +} + +// Region is a rectangular block of pixels in an image or texture, +// defined by its upper-left corner and its size. +// +// Reference: https://developer.apple.com/documentation/metal/mtlregion. +type Region struct { + Origin Origin // The location of the upper-left corner of the block. + Size Size // The size of the block. +} + +func (r *Region) c() C.struct_Region { + return C.struct_Region{ + Origin: C.struct_Origin{ + X: C.uint_t(r.Origin.X), + Y: C.uint_t(r.Origin.Y), + Z: C.uint_t(r.Origin.Z), + }, + Size: C.struct_Size{ + Width: C.uint_t(r.Size.Width), + Height: C.uint_t(r.Size.Height), + Depth: C.uint_t(r.Size.Depth), + }, + } +} + +// Origin represents the location of a pixel in an image or texture relative +// to the upper-left corner, whose coordinates are (0, 0). +// +// Reference: https://developer.apple.com/documentation/metal/mtlorigin. +type Origin struct{ X, Y, Z int } + +// Size represents the set of dimensions that declare the size of an object, +// such as an image, texture, threadgroup, or grid. +// +// Reference: https://developer.apple.com/documentation/metal/mtlsize. +type Size struct{ Width, Height, Depth int } + +// RegionMake2D returns a 2D, rectangular region for image or texture data. +// +// Reference: https://developer.apple.com/documentation/metal/1515675-mtlregionmake2d. +func RegionMake2D(x, y, width, height int) Region { + return Region{ + Origin: Origin{x, y, 0}, + Size: Size{width, height, 1}, + } +} + +type Viewport struct { + OriginX float64 + OriginY float64 + Width float64 + Height float64 + ZNear float64 + ZFar float64 +} + +func (v *Viewport) c() C.struct_Viewport { + return C.struct_Viewport{ + OriginX: C.double(v.OriginX), + OriginY: C.double(v.OriginY), + Width: C.double(v.Width), + Height: C.double(v.Height), + ZNear: C.double(v.ZNear), + ZFar: C.double(v.ZFar), + } +} diff --git a/internal/graphicsdriver/metal/mtl/mtl.h b/internal/graphicsdriver/metal/mtl/mtl.h new file mode 100644 index 000000000..d93b26687 --- /dev/null +++ b/internal/graphicsdriver/metal/mtl/mtl.h @@ -0,0 +1,175 @@ +// Copyright 2018 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build darwin + +#include + +typedef signed char BOOL; +typedef unsigned long uint_t; +typedef unsigned char uint8_t; +typedef unsigned short uint16_t; +typedef unsigned long long uint64_t; + +struct Device { + void *Device; + BOOL Headless; + BOOL LowPower; + BOOL Removable; + uint64_t RegistryID; + const char *Name; +}; + +struct Devices { + struct Device *Devices; + int Length; +}; + +struct Library { + void *Library; + const char *Error; +}; + +struct RenderPipelineDescriptor { + void *VertexFunction; + void *FragmentFunction; + uint16_t ColorAttachment0PixelFormat; + BOOL ColorAttachment0BlendingEnabled; + uint8_t ColorAttachment0DestinationAlphaBlendFactor; + uint8_t ColorAttachment0DestinationRGBBlendFactor; + uint8_t ColorAttachment0SourceAlphaBlendFactor; + uint8_t ColorAttachment0SourceRGBBlendFactor; +}; + +struct RenderPipelineState { + void *RenderPipelineState; + const char *Error; +}; + +struct ClearColor { + double Red; + double Green; + double Blue; + double Alpha; +}; + +struct RenderPassDescriptor { + uint8_t ColorAttachment0LoadAction; + uint8_t ColorAttachment0StoreAction; + struct ClearColor ColorAttachment0ClearColor; + void *ColorAttachment0Texture; +}; + +struct TextureDescriptor { + uint16_t PixelFormat; + uint_t Width; + uint_t Height; + uint8_t StorageMode; + uint8_t Usage; +}; + +struct Origin { + uint_t X; + uint_t Y; + uint_t Z; +}; + +struct Size { + uint_t Width; + uint_t Height; + uint_t Depth; +}; + +struct Region { + struct Origin Origin; + struct Size Size; +}; + +struct Viewport { + double OriginX; + double OriginY; + double Width; + double Height; + double ZNear; + double ZFar; +}; + +struct Device CreateSystemDefaultDevice(); +struct Devices CopyAllDevices(); + +BOOL Device_SupportsFeatureSet(void *device, uint16_t featureSet); +void *Device_MakeCommandQueue(void *device); +struct Library Device_MakeLibrary(void *device, const char *source, + size_t sourceLength); +struct RenderPipelineState +Device_MakeRenderPipelineState(void *device, + struct RenderPipelineDescriptor descriptor); +void *Device_MakeBuffer(void *device, const void *bytes, size_t length, + uint16_t options); +void *Device_MakeTexture(void *device, struct TextureDescriptor descriptor); + +void CommandQueue_Release(void *commandQueue); +void *CommandQueue_MakeCommandBuffer(void *commandQueue); + +void CommandBuffer_Release(void *commandBuffer); +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); + +void CommandEncoder_EndEncoding(void *commandEncoder); + +void RenderCommandEncoder_Release(void *renderCommandEncoder); +void RenderCommandEncoder_SetRenderPipelineState(void *renderCommandEncoder, + void *renderPipelineState); +void RenderCommandEncoder_SetViewport(void *renderCommandEncoder, + struct Viewport viewport); +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_SetFragmentBytes(void *renderCommandEncoder, + const void *bytes, size_t length, + uint_t index); +void RenderCommandEncoder_SetBlendColor(void *renderCommandEncoder, float red, + float green, float blue, float alpha); +void RenderCommandEncoder_SetFragmentTexture(void *renderCommandEncoder, + void *texture, uint_t index); +void RenderCommandEncoder_DrawPrimitives(void *renderCommandEncoder, + uint8_t primitiveType, + uint_t vertexStart, + uint_t vertexCount); +void RenderCommandEncoder_DrawIndexedPrimitives( + void *renderCommandEncoder, uint8_t primitiveType, uint_t indexCount, + uint8_t indexType, void *indexBuffer, uint_t indexBufferOffset); + +void BlitCommandEncoder_Synchronize(void *blitCommandEncoder, void *resource); +void BlitCommandEncoder_SynchronizeTexture(void *blitCommandEncoder, + void *texture, uint_t slice, + uint_t level); + +void *Library_MakeFunction(void *library, const char *name); + +void Texture_Release(void *texture); +void Texture_GetBytes(void *texture, void *pixelBytes, size_t bytesPerRow, + struct Region region, uint_t level); +void Texture_ReplaceRegion(void *texture, struct Region region, uint_t level, + void *pixelBytes, uint_t bytesPerRow); + +void Buffer_Release(void *buffer); diff --git a/internal/graphicsdriver/metal/mtl/mtl.m b/internal/graphicsdriver/metal/mtl/mtl.m new file mode 100644 index 000000000..3afb7239f --- /dev/null +++ b/internal/graphicsdriver/metal/mtl/mtl.m @@ -0,0 +1,318 @@ +// Copyright 2018 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build darwin + +#include "mtl.h" +#import +#include + +struct Device CreateSystemDefaultDevice() { + id device = MTLCreateSystemDefaultDevice(); + if (!device) { + struct Device d; + d.Device = NULL; + return d; + } + + struct Device d; + d.Device = device; + d.Headless = device.headless; + d.LowPower = device.lowPower; + d.Removable = device.removable; + d.RegistryID = device.registryID; + d.Name = device.name.UTF8String; + return d; +} + +// Caller must call free(d.devices). +struct Devices CopyAllDevices() { + NSArray> *devices = MTLCopyAllDevices(); + + struct Devices d; + d.Devices = malloc(devices.count * sizeof(struct Device)); + for (int i = 0; i < devices.count; i++) { + d.Devices[i].Device = devices[i]; + d.Devices[i].Headless = devices[i].headless; + d.Devices[i].LowPower = devices[i].lowPower; + d.Devices[i].Removable = devices[i].removable; + d.Devices[i].RegistryID = devices[i].registryID; + d.Devices[i].Name = devices[i].name.UTF8String; + } + d.Length = devices.count; + return d; +} + +BOOL Device_SupportsFeatureSet(void *device, uint16_t featureSet) { + return [(id)device supportsFeatureSet:featureSet]; +} + +void *Device_MakeCommandQueue(void *device) { + return [(id)device newCommandQueue]; +} + +struct Library Device_MakeLibrary(void *device, const char *source, + size_t sourceLength) { + NSError *error; + id library = [(id)device + newLibraryWithSource:[[NSString alloc] initWithBytes:source + length:sourceLength + encoding:NSUTF8StringEncoding] + options:NULL // TODO. + error:&error]; + + struct Library l; + l.Library = library; + if (!library) { + l.Error = error.localizedDescription.UTF8String; + } + return l; +} + +struct RenderPipelineState +Device_MakeRenderPipelineState(void *device, + struct RenderPipelineDescriptor descriptor) { + MTLRenderPipelineDescriptor *renderPipelineDescriptor = + [[MTLRenderPipelineDescriptor alloc] init]; + renderPipelineDescriptor.vertexFunction = descriptor.VertexFunction; + renderPipelineDescriptor.fragmentFunction = descriptor.FragmentFunction; + renderPipelineDescriptor.colorAttachments[0].pixelFormat = + descriptor.ColorAttachment0PixelFormat; + renderPipelineDescriptor.colorAttachments[0].blendingEnabled = + descriptor.ColorAttachment0BlendingEnabled; + renderPipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = + descriptor.ColorAttachment0DestinationAlphaBlendFactor; + renderPipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = + descriptor.ColorAttachment0DestinationRGBBlendFactor; + renderPipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = + descriptor.ColorAttachment0SourceAlphaBlendFactor; + renderPipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = + descriptor.ColorAttachment0SourceRGBBlendFactor; + NSError *error; + id renderPipelineState = [(id)device + newRenderPipelineStateWithDescriptor:renderPipelineDescriptor + error:&error]; + [renderPipelineDescriptor release]; + struct RenderPipelineState rps; + rps.RenderPipelineState = renderPipelineState; + if (!renderPipelineState) { + rps.Error = error.localizedDescription.UTF8String; + } + return rps; +} + +void *Device_MakeBuffer(void *device, const void *bytes, size_t length, + uint16_t options) { + return [(id)device newBufferWithBytes:(const void *)bytes + length:(NSUInteger)length + options:(MTLResourceOptions)options]; +} + +void *Device_MakeTexture(void *device, struct TextureDescriptor descriptor) { + MTLTextureDescriptor *textureDescriptor = [[MTLTextureDescriptor alloc] init]; + textureDescriptor.pixelFormat = descriptor.PixelFormat; + textureDescriptor.width = descriptor.Width; + textureDescriptor.height = descriptor.Height; + textureDescriptor.storageMode = descriptor.StorageMode; + textureDescriptor.usage = descriptor.Usage; + id texture = + [(id)device newTextureWithDescriptor:textureDescriptor]; + [textureDescriptor release]; + return texture; +} + +void CommandQueue_Release(void *commandQueue) { + [(id)commandQueue release]; +} + +void *CommandQueue_MakeCommandBuffer(void *commandQueue) { + return [(id)commandQueue commandBuffer]; +} + +void CommandBuffer_PresentDrawable(void *commandBuffer, void *drawable) { + [(id)commandBuffer + presentDrawable:(id)drawable]; +} + +void CommandBuffer_Commit(void *commandBuffer) { + [(id)commandBuffer commit]; +} + +void CommandBuffer_WaitUntilCompleted(void *commandBuffer) { + [(id)commandBuffer waitUntilCompleted]; +} + +void * +CommandBuffer_MakeRenderCommandEncoder(void *commandBuffer, + struct RenderPassDescriptor descriptor) { + MTLRenderPassDescriptor *renderPassDescriptor = + [[MTLRenderPassDescriptor alloc] init]; + renderPassDescriptor.colorAttachments[0].loadAction = + descriptor.ColorAttachment0LoadAction; + renderPassDescriptor.colorAttachments[0].storeAction = + descriptor.ColorAttachment0StoreAction; + renderPassDescriptor.colorAttachments[0].clearColor = + MTLClearColorMake(descriptor.ColorAttachment0ClearColor.Red, + descriptor.ColorAttachment0ClearColor.Green, + descriptor.ColorAttachment0ClearColor.Blue, + descriptor.ColorAttachment0ClearColor.Alpha); + renderPassDescriptor.colorAttachments[0].texture = + (id)descriptor.ColorAttachment0Texture; + id rce = [(id)commandBuffer + renderCommandEncoderWithDescriptor:renderPassDescriptor]; + [renderPassDescriptor release]; + return rce; +} + +void *CommandBuffer_MakeBlitCommandEncoder(void *commandBuffer) { + return [(id)commandBuffer blitCommandEncoder]; +} + +void CommandEncoder_EndEncoding(void *commandEncoder) { + [(id)commandEncoder endEncoding]; +} + +void RenderCommandEncoder_Release(void *renderCommandEncoder) { + [(id)renderCommandEncoder release]; +} + +void RenderCommandEncoder_SetRenderPipelineState(void *renderCommandEncoder, + void *renderPipelineState) { + [(id)renderCommandEncoder + setRenderPipelineState:(id)renderPipelineState]; +} + +void RenderCommandEncoder_SetViewport(void *renderCommandEncoder, + struct Viewport viewport) { + [(id)renderCommandEncoder + setViewport:(MTLViewport){ + viewport.OriginX, + viewport.OriginY, + viewport.Width, + viewport.Height, + viewport.ZNear, + viewport.ZFar, + }]; +} + +void RenderCommandEncoder_SetVertexBuffer(void *renderCommandEncoder, + void *buffer, uint_t offset, + uint_t index) { + [(id)renderCommandEncoder + setVertexBuffer:(id)buffer + offset:(NSUInteger)offset + atIndex:(NSUInteger)index]; +} + +void RenderCommandEncoder_SetVertexBytes(void *renderCommandEncoder, + const void *bytes, size_t length, + uint_t index) { + [(id)renderCommandEncoder + setVertexBytes:bytes + length:(NSUInteger)length + atIndex:(NSUInteger)index]; +} + +void RenderCommandEncoder_SetFragmentBytes(void *renderCommandEncoder, + const void *bytes, size_t length, + uint_t index) { + [(id)renderCommandEncoder + setFragmentBytes:bytes + length:(NSUInteger)length + atIndex:(NSUInteger)index]; +} + +void RenderCommandEncoder_SetFragmentTexture(void *renderCommandEncoder, + void *texture, uint_t index) { + [(id)renderCommandEncoder + setFragmentTexture:(id)texture + atIndex:(NSUInteger)index]; +} + +void RenderCommandEncoder_SetBlendColor(void *renderCommandEncoder, float red, + float green, float blue, float alpha) { + [(id)renderCommandEncoder setBlendColorRed:red + green:green + blue:blue + alpha:alpha]; +} + +void RenderCommandEncoder_DrawPrimitives(void *renderCommandEncoder, + uint8_t primitiveType, + uint_t vertexStart, + uint_t vertexCount) { + [(id)renderCommandEncoder + drawPrimitives:(MTLPrimitiveType)primitiveType + vertexStart:(NSUInteger)vertexStart + vertexCount:(NSUInteger)vertexCount]; +} + +void RenderCommandEncoder_DrawIndexedPrimitives( + void *renderCommandEncoder, uint8_t primitiveType, uint_t indexCount, + uint8_t indexType, void *indexBuffer, uint_t indexBufferOffset) { + [(id)renderCommandEncoder + drawIndexedPrimitives:(MTLPrimitiveType)primitiveType + indexCount:(NSUInteger)indexCount + indexType:(MTLIndexType)indexType + indexBuffer:(id)indexBuffer + indexBufferOffset:(NSUInteger)indexBufferOffset]; +} + +void BlitCommandEncoder_Synchronize(void *blitCommandEncoder, void *resource) { + [(id)blitCommandEncoder + synchronizeResource:(id)resource]; +} + +void BlitCommandEncoder_SynchronizeTexture(void *blitCommandEncoder, + void *texture, uint_t slice, + uint_t level) { + [(id)blitCommandEncoder + synchronizeTexture:(id)texture + slice:(NSUInteger)slice + level:(NSUInteger)level]; +} + +void *Library_MakeFunction(void *library, const char *name) { + return [(id)library + newFunctionWithName:[NSString stringWithUTF8String:name]]; +} + +void Texture_Release(void *texture) { [(id)texture release]; } + +void Texture_GetBytes(void *texture, void *pixelBytes, size_t bytesPerRow, + struct Region region, uint_t level) { + [(id)texture + getBytes:(void *)pixelBytes + bytesPerRow:(NSUInteger)bytesPerRow + fromRegion:(MTLRegion) { + {region.Origin.X, region.Origin.Y, region.Origin.Z}, { + region.Size.Width, region.Size.Height, region.Size.Depth + } + } + mipmapLevel:(NSUInteger)level]; +} + +void Texture_ReplaceRegion(void *texture, struct Region region, uint_t level, + void *bytes, uint_t bytesPerRow) { + [(id)texture replaceRegion:(MTLRegion) { + {region.Origin.X, region.Origin.Y, region.Origin.Z}, { + region.Size.Width, region.Size.Height, region.Size.Depth + } + } + mipmapLevel:(NSUInteger)level + withBytes:bytes + bytesPerRow:(NSUInteger)bytesPerRow]; +} + +void Buffer_Release(void *buffer) { [(id)buffer release]; } diff --git a/internal/graphicsdriver/metal/mtl/mtl_test.go b/internal/graphicsdriver/metal/mtl/mtl_test.go new file mode 100644 index 000000000..ac370b521 --- /dev/null +++ b/internal/graphicsdriver/metal/mtl/mtl_test.go @@ -0,0 +1,179 @@ +// Copyright 2018 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build darwin + +package mtl_test + +import ( + "fmt" + "image" + "image/color" + "image/png" + "os" + "path/filepath" + "testing" + "unsafe" + + "golang.org/x/image/math/f32" + + "github.com/hajimehoshi/ebiten/internal/graphicsdriver/metal/mtl" +) + +func Disabled_TestRenderTriangle(t *testing.T) { + device, err := mtl.CreateSystemDefaultDevice() + if err != nil { + t.Fatal(err) + } + + // Create a render pipeline state. + const source = `#include + +using namespace metal; + +struct Vertex { + float4 position [[position]]; + float4 color; +}; + +vertex Vertex VertexShader( + uint vertexID [[vertex_id]], + device Vertex * vertices [[buffer(0)]] +) { + return vertices[vertexID]; +} + +fragment float4 FragmentShader(Vertex in [[stage_in]]) { + return in.color; +} +` + lib, err := device.MakeLibrary(source, mtl.CompileOptions{}) + if err != nil { + t.Fatal(err) + } + vs, err := lib.MakeFunction("VertexShader") + if err != nil { + t.Fatal(err) + } + fs, err := lib.MakeFunction("FragmentShader") + if err != nil { + t.Fatal(err) + } + var rpld mtl.RenderPipelineDescriptor + rpld.VertexFunction = vs + rpld.FragmentFunction = fs + rpld.ColorAttachments[0].PixelFormat = mtl.PixelFormatRGBA8UNorm + rps, err := device.MakeRenderPipelineState(rpld) + if err != nil { + t.Fatal(err) + } + + // Create a vertex buffer. + type Vertex struct { + Position f32.Vec4 + Color f32.Vec4 + } + vertexData := [...]Vertex{ + {f32.Vec4{+0.00, +0.75, 0, 1}, f32.Vec4{1, 0, 0, 1}}, + {f32.Vec4{-0.75, -0.75, 0, 1}, f32.Vec4{0, 1, 0, 1}}, + {f32.Vec4{+0.75, -0.75, 0, 1}, f32.Vec4{0, 0, 1, 1}}, + } + vertexBuffer := device.MakeBuffer(unsafe.Pointer(&vertexData[0]), unsafe.Sizeof(vertexData), mtl.ResourceStorageModeManaged) + + // Create an output texture to render into. + td := mtl.TextureDescriptor{ + PixelFormat: mtl.PixelFormatRGBA8UNorm, + Width: 512, + Height: 512, + StorageMode: mtl.StorageModeManaged, + } + texture := device.MakeTexture(td) + + cq := device.MakeCommandQueue() + 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 = texture + rce := cb.MakeRenderCommandEncoder(rpd) + rce.SetRenderPipelineState(rps) + rce.SetVertexBuffer(vertexBuffer, 0, 0) + rce.DrawPrimitives(mtl.PrimitiveTypeTriangle, 0, 3) + rce.EndEncoding() + + // Encode all blit commands. + bce := cb.MakeBlitCommandEncoder() + bce.Synchronize(texture) + bce.EndEncoding() + + cb.Commit() + cb.WaitUntilCompleted() + + // Read pixels from output texture into an image. + got := image.NewNRGBA(image.Rect(0, 0, texture.Width, texture.Height)) + bytesPerRow := 4 * texture.Width + region := mtl.RegionMake2D(0, 0, texture.Width, texture.Height) + texture.GetBytes(&got.Pix[0], uintptr(bytesPerRow), region, 0) + + // TODO: Embed this file? + want, err := readPNG(filepath.Join("testdata", "triangle.png")) + if err != nil { + t.Fatal(err) + } + + if err := imageEq(got, want); err != nil { + t.Errorf("got image != want: %v", err) + } +} + +func readPNG(name string) (image.Image, error) { + f, err := os.Open(name) + if err != nil { + return nil, err + } + defer f.Close() + return png.Decode(f) +} + +// imageEq reports whether images m, n are considered equivalent. Two images are considered +// equivalent if they have same bounds, and all pixel colors are within a small margin. +func imageEq(m, n image.Image) error { + if m.Bounds() != n.Bounds() { + return fmt.Errorf("bounds don't match: %v != %v", m.Bounds(), n.Bounds()) + } + for y := m.Bounds().Min.Y; y < m.Bounds().Max.Y; y++ { + for x := m.Bounds().Min.X; x < m.Bounds().Max.X; x++ { + c := color.NRGBAModel.Convert(m.At(x, y)).(color.NRGBA) + d := color.NRGBAModel.Convert(n.At(x, y)).(color.NRGBA) + if !colorEq(c, d) { + return fmt.Errorf("pixel (%v, %v) doesn't match: %+v != %+v", x, y, c, d) + } + } + } + return nil +} + +// colorEq reports whether colors c, d are considered equivalent, i.e., within a small margin. +func colorEq(c, d color.NRGBA) bool { + return eqEpsilon(c.R, d.R) && eqEpsilon(c.G, d.G) && eqEpsilon(c.B, d.B) && eqEpsilon(c.A, d.A) +} + +// eqEpsilon reports whether a and b are within epsilon of each other. +func eqEpsilon(a, b uint8) bool { + const epsilon = 1 + return uint16(a)-uint16(b) <= epsilon || uint16(b)-uint16(a) <= epsilon +} diff --git a/internal/graphicsdriver/metal/ns/ns.go b/internal/graphicsdriver/metal/ns/ns.go new file mode 100644 index 000000000..85b167a8e --- /dev/null +++ b/internal/graphicsdriver/metal/ns/ns.go @@ -0,0 +1,76 @@ +// Copyright 2018 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +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" + + "github.com/hajimehoshi/ebiten/internal/graphicsdriver/metal/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) { + if wantsLayer { + C.View_SetWantsLayer(v.view, 1) + } else { + C.View_SetWantsLayer(v.view, 0) + } +} diff --git a/internal/shareable/size_notmacos.go b/internal/graphicsdriver/metal/ns/ns.h similarity index 76% rename from internal/shareable/size_notmacos.go rename to internal/graphicsdriver/metal/ns/ns.h index 421b9bb0c..06242aacc 100644 --- a/internal/shareable/size_notmacos.go +++ b/internal/graphicsdriver/metal/ns/ns.h @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -// +build !darwin ios js +// +build darwin -package shareable +typedef signed char BOOL; -const ( - initSize = 1024 - maxSize = 4096 -) +void *Window_ContentView(void *window); + +void View_SetLayer(void *view, void *layer); +void View_SetWantsLayer(void *view, BOOL wantsLayer); diff --git a/internal/shareable/size_macos.go b/internal/graphicsdriver/metal/ns/ns.m similarity index 63% rename from internal/shareable/size_macos.go rename to internal/graphicsdriver/metal/ns/ns.m index ae265e9e1..c4a820bcf 100644 --- a/internal/shareable/size_macos.go +++ b/internal/graphicsdriver/metal/ns/ns.m @@ -13,17 +13,18 @@ // limitations under the License. // +build darwin -// +build !ios -// +build !js -package shareable +#include "ns.h" +#import -// On MacBook Pro 2013 (Late), there is a bug in texture rendering and -// extending shareable textures sometimes fail (#593). This is due to -// a bug in the grahics driver, and there is nothing we can do. Let's -// not extend shareable textures in such environment. +void *Window_ContentView(void *window) { + return ((NSWindow *)window).contentView; +} -const ( - initSize = 4096 - maxSize = 4096 -) +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/internal/graphicsdriver/opengl/driver.go b/internal/graphicsdriver/opengl/driver.go index dbde4f836..494b0433a 100644 --- a/internal/graphicsdriver/opengl/driver.go +++ b/internal/graphicsdriver/opengl/driver.go @@ -33,6 +33,10 @@ type Driver struct { context context } +func (d *Driver) SetWindow(window uintptr) { + // Do nothing. +} + func (d *Driver) checkSize(width, height int) { if width < 1 { panic(fmt.Sprintf("opengl: width (%d) must be equal or more than 1", width)) @@ -105,3 +109,15 @@ func (d *Driver) Draw(indexLen int, indexOffset int, mode graphics.CompositeMode func (d *Driver) Flush() { d.context.flush() } + +func (d *Driver) SetVsyncEnabled(enabled bool) { + // Do nothing +} + +func (d *Driver) VDirection() graphicsdriver.VDirection { + return graphicsdriver.VDownward +} + +func (d *Driver) IsGL() bool { + return true +} diff --git a/internal/restorable/image.go b/internal/restorable/image.go index 7c2dc56ae..05bf9a49a 100644 --- a/internal/restorable/image.go +++ b/internal/restorable/image.go @@ -105,6 +105,11 @@ func (i *Image) BasePixelsForTesting() []byte { return i.basePixels } +func (i *Image) Pixels() []byte { + i.readPixelsFromGPUIfNeeded() + return i.basePixels +} + // Size returns the image's size. func (i *Image) Size() (int, int) { return i.image.Size() @@ -245,6 +250,15 @@ func (i *Image) appendDrawImageHistory(image *Image, vertices []float32, indices i.drawImageHistory = append(i.drawImageHistory, item) } +func (i *Image) readPixelsFromGPUIfNeeded() { + if i.basePixels == nil || i.drawImageHistory != nil || i.stale { + graphicscommand.FlushCommands() + i.readPixelsFromGPU() + i.drawImageHistory = nil + i.stale = false + } +} + // At returns a color value at (x, y). // // Note that this must not be called until context is available. @@ -254,12 +268,7 @@ func (i *Image) At(x, y int) color.RGBA { return color.RGBA{} } - if i.basePixels == nil || i.drawImageHistory != nil || i.stale { - graphicscommand.FlushCommands() - i.readPixelsFromGPU() - i.drawImageHistory = nil - i.stale = false - } + i.readPixelsFromGPUIfNeeded() // Even after readPixelsFromGPU, basePixels might be nil when OpenGL error happens. if i.basePixels == nil { diff --git a/internal/shareable/shareable.go b/internal/shareable/shareable.go index 6f0e352dc..bfddace35 100644 --- a/internal/shareable/shareable.go +++ b/internal/shareable/shareable.go @@ -26,6 +26,11 @@ import ( "github.com/hajimehoshi/ebiten/internal/restorable" ) +const ( + initSize = 1024 + maxSize = 4096 +) + type backend struct { restorable *restorable.Image diff --git a/internal/ui/ui_glfw.go b/internal/ui/ui_glfw.go index a1315b5c7..0ba4a5161 100644 --- a/internal/ui/ui_glfw.go +++ b/internal/ui/ui_glfw.go @@ -29,6 +29,7 @@ import ( "github.com/go-gl/glfw/v3.2/glfw" "github.com/hajimehoshi/ebiten/internal/devicescale" + "github.com/hajimehoshi/ebiten/internal/graphicscommand" "github.com/hajimehoshi/ebiten/internal/hooks" "github.com/hajimehoshi/ebiten/internal/input" "github.com/hajimehoshi/ebiten/internal/mainthread" @@ -90,6 +91,9 @@ func initialize() error { if err := glfw.Init(); err != nil { return err } + if !graphicscommand.Driver().IsGL() { + glfw.WindowHint(glfw.ClientAPI, glfw.NoAPI) + } glfw.WindowHint(glfw.Visible, glfw.False) glfw.WindowHint(glfw.Resizable, glfw.False) @@ -542,7 +546,9 @@ func Run(width, height int, scale float64, title string, g GraphicsContext, main } u.window = window - u.window.MakeContextCurrent() + if graphicscommand.Driver().IsGL() { + u.window.MakeContextCurrent() + } u.window.SetInputMode(glfw.StickyMouseButtonsMode, glfw.True) u.window.SetInputMode(glfw.StickyKeysMode, glfw.True) @@ -589,6 +595,8 @@ func Run(width, height int, scale float64, title string, g GraphicsContext, main y := my + (v.Height-h)/3 x, y = adjustWindowPosition(x, y) u.window.SetPos(x, y) + + u.setWindowToDriver() return nil }) return u.loop(g) @@ -726,18 +734,7 @@ func (u *userInterface) loop(g GraphicsContext) error { u.swapBuffers() return nil } - - n1 := time.Now().UnixNano() u.swapBuffers() - n2 := time.Now().UnixNano() - d := time.Duration(n2 - n1) - - // On macOS Mojave, vsync might not work (#692). - // As a tempoarry fix, just wait for a while not to consume CPU too much. - const threshold = 4 * time.Millisecond // 250 [Hz] - if d < threshold { - time.Sleep(threshold - d) - } return nil }) } @@ -745,7 +742,9 @@ func (u *userInterface) loop(g GraphicsContext) error { // swapBuffers must be called from the main thread. func (u *userInterface) swapBuffers() { - u.window.SwapBuffers() + if graphicscommand.Driver().IsGL() { + u.window.SwapBuffers() + } } // setScreenSize must be called from the main thread. @@ -831,18 +830,21 @@ func (u *userInterface) forceSetScreenSize(width, height int, scale float64, ful u.window.SetTitle(u.title) } - // SwapInterval is affected by the current monitor of the window. - // This needs to be called at least after SetMonitor. - // Without SwapInterval after SetMonitor, vsynch doesn't work (#375). - // - // TODO: (#405) If triple buffering is needed, SwapInterval(0) should be called, - // but is this correct? If glfw.SwapInterval(0) and the driver doesn't support triple - // buffering, what will happen? - if u.vsync { - glfw.SwapInterval(1) - } else { - glfw.SwapInterval(0) + if graphicscommand.Driver().IsGL() { + // SwapInterval is affected by the current monitor of the window. + // This needs to be called at least after SetMonitor. + // Without SwapInterval after SetMonitor, vsynch doesn't work (#375). + // + // TODO: (#405) If triple buffering is needed, SwapInterval(0) should be called, + // but is this correct? If glfw.SwapInterval(0) and the driver doesn't support triple + // buffering, what will happen? + if u.vsync { + glfw.SwapInterval(1) + } else { + glfw.SwapInterval(0) + } } + graphicscommand.Driver().SetVsyncEnabled(vsync) u.toChangeSize = true } diff --git a/internal/ui/ui_mac.go b/internal/ui/ui_mac.go index 77ccc7e0a..d8f40e4b3 100644 --- a/internal/ui/ui_mac.go +++ b/internal/ui/ui_mac.go @@ -46,6 +46,8 @@ import ( "unsafe" "github.com/go-gl/glfw/v3.2/glfw" + + "github.com/hajimehoshi/ebiten/internal/graphicscommand" ) func glfwScale() float64 { @@ -70,3 +72,7 @@ func (u *userInterface) currentMonitorImpl() *glfw.Monitor { } return glfw.GetPrimaryMonitor() } + +func (u *userInterface) setWindowToDriver() { + graphicscommand.Driver().SetWindow(u.window.GetCocoaWindow()) +} diff --git a/internal/ui/ui_unix.go b/internal/ui/ui_unix.go index bc44bb14f..c80220111 100644 --- a/internal/ui/ui_unix.go +++ b/internal/ui/ui_unix.go @@ -44,3 +44,7 @@ func (u *userInterface) currentMonitorImpl() *glfw.Monitor { } return glfw.GetPrimaryMonitor() } + +func (u *userInterface) setWindowToDriver() { + // Do nothing +} diff --git a/internal/ui/ui_windows.go b/internal/ui/ui_windows.go index 39ec29471..08ed59899 100644 --- a/internal/ui/ui_windows.go +++ b/internal/ui/ui_windows.go @@ -164,3 +164,7 @@ func (u *userInterface) currentMonitorImpl() *glfw.Monitor { } return glfw.GetPrimaryMonitor() } + +func (u *userInterface) setWindowToDriver() { + // Do nothing +}