// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // +build darwin // Package mtldriver provides a Metal driver for accessing a screen. // // At this time, the Metal API is used only to present the final pixels // to the screen. All rendering is performed on the CPU via the image/draw // algorithms. Future work is to use mtl.Buffer, mtl.Texture, etc., and // do more of the rendering work on the GPU. package mtldriver import ( "runtime" "unsafe" "dmitri.shuralyov.com/gpu/mtl" "github.com/go-gl/glfw/v3.3/glfw" "golang.org/x/exp/shiny/driver/internal/errscreen" "golang.org/x/exp/shiny/driver/mtldriver/internal/appkit" "golang.org/x/exp/shiny/driver/mtldriver/internal/coreanim" "golang.org/x/exp/shiny/screen" "golang.org/x/mobile/event/key" "golang.org/x/mobile/event/mouse" "golang.org/x/mobile/event/paint" "golang.org/x/mobile/event/size" ) func init() { runtime.LockOSThread() } // Main is called by the program's main function to run the graphical // application. // // It calls f on the Screen, possibly in a separate goroutine, as some OS- // specific libraries require being on 'the main thread'. It returns when f // returns. func Main(f func(screen.Screen)) { if err := main(f); err != nil { f(errscreen.Stub(err)) } } func main(f func(screen.Screen)) error { device, err := mtl.CreateSystemDefaultDevice() if err != nil { return err } err = glfw.Init() if err != nil { return err } defer glfw.Terminate() glfw.WindowHint(glfw.ClientAPI, glfw.NoAPI) var ( done = make(chan struct{}) newWindowCh = make(chan newWindowReq, 1) releaseWindowCh = make(chan releaseWindowReq, 1) ) go func() { f(&screenImpl{ newWindowCh: newWindowCh, }) close(done) glfw.PostEmptyEvent() // Break main loop out of glfw.WaitEvents so it can receive on done. }() select { // TODO(dmitshur): Delete this when https://github.com/go-gl/glfw/issues/262 is resolved. // Wait for first window request (or done) before entering main // loop to work around https://github.com/glfw/glfw/issues/1543. case w := <-newWindowCh: newWindowCh <- w case <-done: } for { select { case <-done: return nil case req := <-newWindowCh: w, err := newWindow(device, releaseWindowCh, req.opts) req.respCh <- newWindowResp{w, err} case req := <-releaseWindowCh: req.window.Destroy() req.respCh <- struct{}{} default: glfw.WaitEvents() } } } type newWindowReq struct { opts *screen.NewWindowOptions respCh chan newWindowResp } type newWindowResp struct { w screen.Window err error } type releaseWindowReq struct { window *glfw.Window respCh chan struct{} } // newWindow creates a new GLFW window. // It must be called on the main thread. func newWindow(device mtl.Device, releaseWindowCh chan releaseWindowReq, opts *screen.NewWindowOptions) (screen.Window, error) { width, height := optsSize(opts) window, err := glfw.CreateWindow(width, height, opts.GetTitle(), nil, nil) if err != nil { return nil, err } ml := coreanim.MakeMetalLayer() ml.SetDevice(device) ml.SetPixelFormat(mtl.PixelFormatBGRA8UNorm) ml.SetMaximumDrawableCount(3) ml.SetDisplaySyncEnabled(true) cv := appkit.NewWindow(unsafe.Pointer(window.GetCocoaWindow())).ContentView() cv.SetLayer(ml) cv.SetWantsLayer(true) w := &windowImpl{ device: device, window: window, releaseWindowCh: releaseWindowCh, ml: ml, cq: device.MakeCommandQueue(), } // Set callbacks. framebufferSizeCallback := func(_ *glfw.Window, width, height int) { w.Send(size.Event{ WidthPx: width, HeightPx: height, // TODO(dmitshur): ppp, }) w.Send(paint.Event{External: true}) } window.SetFramebufferSizeCallback(framebufferSizeCallback) window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) { const scale = 2 // TODO(dmitshur): compute dynamically w.Send(mouse.Event{X: float32(x * scale), Y: float32(y * scale)}) }) window.SetMouseButtonCallback(func(_ *glfw.Window, b glfw.MouseButton, a glfw.Action, mods glfw.ModifierKey) { btn := glfwMouseButton(b) if btn == mouse.ButtonNone { return } const scale = 2 // TODO(dmitshur): compute dynamically x, y := window.GetCursorPos() w.Send(mouse.Event{ X: float32(x * scale), Y: float32(y * scale), Button: btn, Direction: glfwMouseDirection(a), // TODO(dmitshur): set Modifiers }) }) window.SetKeyCallback(func(_ *glfw.Window, k glfw.Key, _ int, a glfw.Action, mods glfw.ModifierKey) { code := glfwKeyCode(k) if code == key.CodeUnknown { return } w.Send(key.Event{ Code: code, Direction: glfwKeyDirection(a), // TODO(dmitshur): set Modifiers }) }) // TODO(dmitshur): set CharModsCallback to catch text (runes) that are typed, // and perhaps try to unify key pressed + character typed into single event window.SetCloseCallback(func(*glfw.Window) { w.lifecycler.SetDead(true) w.lifecycler.SendEvent(w, nil) }) // TODO(dmitshur): more fine-grained tracking of whether window is visible and/or focused w.lifecycler.SetDead(false) w.lifecycler.SetVisible(true) w.lifecycler.SetFocused(true) w.lifecycler.SendEvent(w, nil) // Send the initial size and paint events. width, height = window.GetFramebufferSize() framebufferSizeCallback(window, width, height) return w, nil } func optsSize(opts *screen.NewWindowOptions) (width, height int) { width, height = 1024/2, 768/2 if opts != nil { if opts.Width > 0 { width = opts.Width } if opts.Height > 0 { height = opts.Height } } return width, height } func glfwMouseButton(button glfw.MouseButton) mouse.Button { switch button { case glfw.MouseButtonLeft: return mouse.ButtonLeft case glfw.MouseButtonRight: return mouse.ButtonRight case glfw.MouseButtonMiddle: return mouse.ButtonMiddle default: return mouse.ButtonNone } } func glfwMouseDirection(action glfw.Action) mouse.Direction { switch action { case glfw.Press: return mouse.DirPress case glfw.Release: return mouse.DirRelease default: panic("unreachable") } } func glfwKeyCode(k glfw.Key) key.Code { // TODO(dmitshur): support more keys switch k { case glfw.KeyEnter: return key.CodeReturnEnter case glfw.KeyEscape: return key.CodeEscape default: return key.CodeUnknown } } func glfwKeyDirection(action glfw.Action) key.Direction { switch action { case glfw.Press: return key.DirPress case glfw.Release: return key.DirRelease case glfw.Repeat: return key.DirNone default: panic("unreachable") } }