mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2025-09-26 20:11:28 +08:00
internal/graphicsdriver/metal: use CAMetalDisplayLink for macOS
This should fix the flickering issue. Closes #3278
This commit is contained in:
@@ -21,15 +21,21 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
class_NSInvocation = objc.GetClass("NSInvocation")
|
||||
class_NSMethodSignature = objc.GetClass("NSMethodSignature")
|
||||
class_NSAutoreleasePool = objc.GetClass("NSAutoreleasePool")
|
||||
class_NSString = objc.GetClass("NSString")
|
||||
class_NSColor = objc.GetClass("NSColor")
|
||||
class_NSScreen = objc.GetClass("NSScreen")
|
||||
class_NSInvocation = objc.GetClass("NSInvocation")
|
||||
class_NSMethodSignature = objc.GetClass("NSMethodSignature")
|
||||
class_NSAutoreleasePool = objc.GetClass("NSAutoreleasePool")
|
||||
class_NSString = objc.GetClass("NSString")
|
||||
class_NSColor = objc.GetClass("NSColor")
|
||||
class_NSScreen = objc.GetClass("NSScreen")
|
||||
class_NSRunLoop = objc.GetClass("NSRunLoop")
|
||||
class_NSMachPort = objc.GetClass("NSMachPort")
|
||||
class_NSWorkspace = objc.GetClass("NSWorkspace")
|
||||
class_NSNotificationCenter = objc.GetClass("NSNotificationCenter")
|
||||
class_NSOperationQueue = objc.GetClass("NSOperationQueue")
|
||||
)
|
||||
|
||||
var (
|
||||
sel_retain = objc.RegisterName("retain")
|
||||
sel_alloc = objc.RegisterName("alloc")
|
||||
sel_new = objc.RegisterName("new")
|
||||
sel_release = objc.RegisterName("release")
|
||||
@@ -60,6 +66,17 @@ var (
|
||||
sel_unsignedIntValue = objc.RegisterName("unsignedIntValue")
|
||||
sel_setLayer = objc.RegisterName("setLayer:")
|
||||
sel_setWantsLayer = objc.RegisterName("setWantsLayer:")
|
||||
sel_mainRunLoop = objc.RegisterName("mainRunLoop")
|
||||
sel_currentRunLoop = objc.RegisterName("currentRunLoop")
|
||||
sel_run = objc.RegisterName("run")
|
||||
sel_performBlock = objc.RegisterName("performBlock:")
|
||||
sel_port = objc.RegisterName("port")
|
||||
sel_addPort = objc.RegisterName("addPort:forMode:")
|
||||
sel_sharedWorkspace = objc.RegisterName("sharedWorkspace")
|
||||
sel_notificationCenter = objc.RegisterName("notificationCenter")
|
||||
sel_addObserver = objc.RegisterName("addObserver:selector:name:object:")
|
||||
sel_addObserverForName = objc.RegisterName("addObserverForName:object:queue:usingBlock:")
|
||||
sel_mainQueue = objc.RegisterName("mainQueue")
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -95,6 +112,14 @@ type NSPoint = CGPoint
|
||||
type NSRect = CGRect
|
||||
type NSSize = CGSize
|
||||
|
||||
type NSObject struct {
|
||||
objc.ID
|
||||
}
|
||||
|
||||
func (n NSObject) Retain() {
|
||||
n.Send(sel_retain)
|
||||
}
|
||||
|
||||
type NSError struct {
|
||||
objc.ID
|
||||
}
|
||||
@@ -268,3 +293,75 @@ type NSNumber struct {
|
||||
func (n NSNumber) UnsignedIntValue() uint {
|
||||
return uint(n.Send(sel_unsignedIntValue))
|
||||
}
|
||||
|
||||
type NSRunLoop struct {
|
||||
objc.ID
|
||||
}
|
||||
|
||||
func NSRunLoop_mainRunLoop() NSRunLoop {
|
||||
return NSRunLoop{objc.ID(class_NSRunLoop).Send(sel_mainRunLoop)}
|
||||
}
|
||||
|
||||
func NSRunLoop_currentRunLoop() NSRunLoop {
|
||||
return NSRunLoop{objc.ID(class_NSRunLoop).Send(sel_currentRunLoop)}
|
||||
}
|
||||
|
||||
func (r NSRunLoop) AddPort(port NSMachPort, mode NSRunLoopMode) {
|
||||
r.Send(sel_addPort, port.ID, mode)
|
||||
}
|
||||
|
||||
func (r NSRunLoop) Run() {
|
||||
r.Send(sel_run)
|
||||
}
|
||||
|
||||
func (r NSRunLoop) PerformBlock(block objc.Block) {
|
||||
r.Send(sel_performBlock, block)
|
||||
}
|
||||
|
||||
type NSRunLoopMode NSString
|
||||
|
||||
var (
|
||||
NSRunLoopCommonModes = NSRunLoopMode(NSString_alloc().InitWithUTF8String("kCFRunLoopCommonModes"))
|
||||
NSDefaultRunLoopMode = NSRunLoopMode(NSString_alloc().InitWithUTF8String("kCFRunLoopDefaultMode"))
|
||||
)
|
||||
|
||||
type NSMachPort struct {
|
||||
objc.ID
|
||||
}
|
||||
|
||||
func NSMachPort_port() NSMachPort {
|
||||
return NSMachPort{objc.ID(class_NSMachPort).Send(sel_port)}
|
||||
}
|
||||
|
||||
type NSWorkspace struct {
|
||||
objc.ID
|
||||
}
|
||||
|
||||
func NSWorkspace_sharedWorkspace() NSWorkspace {
|
||||
return NSWorkspace{objc.ID(class_NSWorkspace).Send(sel_sharedWorkspace)}
|
||||
}
|
||||
|
||||
func (w NSWorkspace) NotificationCenter() NSNotificationCenter {
|
||||
return NSNotificationCenter{w.Send(sel_notificationCenter)}
|
||||
}
|
||||
|
||||
var (
|
||||
NSWorkspaceDidWakeNotification = NSString_alloc().InitWithUTF8String("NSWorkspaceDidWakeNotification")
|
||||
NSWorkspaceScreensDidWakeNotification = NSString_alloc().InitWithUTF8String("NSWorkspaceScreensDidWakeNotification")
|
||||
)
|
||||
|
||||
type NSNotificationCenter struct {
|
||||
objc.ID
|
||||
}
|
||||
|
||||
func (n NSNotificationCenter) AddObserverForName(name NSString, object objc.ID, queue NSOperationQueue, usingBlock objc.Block) objc.ID {
|
||||
return n.Send(sel_addObserverForName, name.ID, object, queue.ID, usingBlock)
|
||||
}
|
||||
|
||||
type NSOperationQueue struct {
|
||||
objc.ID
|
||||
}
|
||||
|
||||
func NSOperationQueue_mainQueue() NSOperationQueue {
|
||||
return NSOperationQueue{objc.ID(class_NSOperationQueue).Send(sel_mainQueue)}
|
||||
}
|
||||
|
@@ -33,7 +33,11 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal/mtl"
|
||||
)
|
||||
|
||||
var class_CAMetalLayer = objc.GetClass("CAMetalLayer")
|
||||
var (
|
||||
class_CAMetalLayer = objc.GetClass("CAMetalLayer")
|
||||
class_CAMetalDisplayLink = objc.GetClass("CAMetalDisplayLink")
|
||||
class_CAMetalDisplayLinkUpdate = objc.GetClass("CAMetalDisplayLinkUpdate")
|
||||
)
|
||||
|
||||
var (
|
||||
sel_pixelFormat = objc.RegisterName("pixelFormat")
|
||||
@@ -51,6 +55,14 @@ var (
|
||||
sel_setFramebufferOnly = objc.RegisterName("setFramebufferOnly:")
|
||||
sel_texture = objc.RegisterName("texture")
|
||||
sel_present = objc.RegisterName("present")
|
||||
sel_alloc = objc.RegisterName("alloc")
|
||||
sel_initWithMetalLayer = objc.RegisterName("initWithMetalLayer:")
|
||||
sel_setDelegate = objc.RegisterName("setDelegate:")
|
||||
sel_addToOneLoopForMode = objc.RegisterName("addToRunLoop:forMode:")
|
||||
sel_removeFromRunLoopForMode = objc.RegisterName("removeFromRunLoop:forMode:")
|
||||
sel_setPaused = objc.RegisterName("setPaused:")
|
||||
sel_drawable = objc.RegisterName("drawable")
|
||||
sel_release = objc.RegisterName("release")
|
||||
)
|
||||
|
||||
// Layer is an object that manages image-based content and
|
||||
@@ -217,7 +229,7 @@ func (ml MetalLayer) SetPresentsWithTransaction(presentsWithTransaction bool) {
|
||||
|
||||
// SetFramebufferOnly sets a Boolean value that determines whether the layer’s textures are used only for rendering.
|
||||
//
|
||||
// https://developer.apple.com/documentation/quartzcore/cametallayer/1478168-framebufferonly?language=objc
|
||||
// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer/1478168-framebufferonly?language=objc
|
||||
func (ml MetalLayer) SetFramebufferOnly(framebufferOnly bool) {
|
||||
ml.metalLayer.Send(sel_setFramebufferOnly, framebufferOnly)
|
||||
}
|
||||
@@ -247,3 +259,64 @@ func (md MetalDrawable) Texture() mtl.Texture {
|
||||
func (md MetalDrawable) Present() {
|
||||
md.metalDrawable.Send(sel_present)
|
||||
}
|
||||
|
||||
// MetalDisplayLink is a class your Metal app uses to register for callbacks to synchronize its animations for a display.
|
||||
//
|
||||
// Reference: https://developer.apple.com/documentation/quartzcore/cametaldisplaylink?language=objc
|
||||
type MetalDisplayLink struct {
|
||||
objc.ID
|
||||
}
|
||||
|
||||
// SetDelegate sets an instance of a type your app implements that responds to the system’s callbacks.
|
||||
//
|
||||
// Reference: https://developer.apple.com/documentation/quartzcore/cametaldisplaylink/delegate?language=objc
|
||||
func (m MetalDisplayLink) SetDelegate(delegate objc.ID) {
|
||||
m.Send(sel_setDelegate, delegate)
|
||||
}
|
||||
|
||||
// AddToRunLoop registers the display link with a run loop.
|
||||
//
|
||||
// Reference: https://developer.apple.com/documentation/quartzcore/cametaldisplaylink/add(to:formode:)?language=objc
|
||||
func (m MetalDisplayLink) AddToRunLoop(runLoop cocoa.NSRunLoop, mode cocoa.NSRunLoopMode) {
|
||||
m.Send(sel_addToOneLoopForMode, runLoop, mode)
|
||||
}
|
||||
|
||||
// RemoveFromRunLoop removes a mode’s display link from a run loop.
|
||||
//
|
||||
// Reference: https://developer.apple.com/documentation/quartzcore/cametaldisplaylink/remove(from:formode:)?language=objc
|
||||
func (m MetalDisplayLink) RemoveFromRunLoop(runLoop cocoa.NSRunLoop, mode cocoa.NSRunLoopMode) {
|
||||
m.Send(sel_removeFromRunLoopForMode, runLoop, mode)
|
||||
}
|
||||
|
||||
// SetPaused sets a Boolean value that indicates whether the system suspends the display link’s notifications to the target.
|
||||
//
|
||||
// https://developer.apple.com/documentation/quartzcore/cametaldisplaylink/ispaused?language=objc
|
||||
func (m MetalDisplayLink) SetPaused(paused bool) {
|
||||
m.Send(sel_setPaused, paused)
|
||||
}
|
||||
|
||||
func (m MetalDisplayLink) Release() {
|
||||
m.Send(sel_release)
|
||||
}
|
||||
|
||||
// NewMetalDisplayLink creates a display link for Metal from a Core Animation layer.
|
||||
//
|
||||
// Reference: https://developer.apple.com/documentation/quartzcore/cametaldisplaylink/init(metallayer:)?language=objc
|
||||
func NewMetalDisplayLink(metalLayer MetalLayer) MetalDisplayLink {
|
||||
displayLink := objc.ID(class_CAMetalDisplayLink).Send(sel_alloc).Send(sel_initWithMetalLayer, metalLayer.metalLayer)
|
||||
return MetalDisplayLink{displayLink}
|
||||
}
|
||||
|
||||
// MetalDisplayLinkUpdate stores information about a single update from a Metal display link instance.
|
||||
//
|
||||
// Reference: https://developer.apple.com/documentation/quartzcore/cametaldisplaylink/update?language=objc
|
||||
type MetalDisplayLinkUpdate struct {
|
||||
objc.ID
|
||||
}
|
||||
|
||||
// Drawable returns the Metal drawable your app uses to render the next frame.
|
||||
//
|
||||
// https://developer.apple.com/documentation/quartzcore/cametaldisplaylink/update/drawable?language=objc
|
||||
func (m MetalDisplayLinkUpdate) Drawable() MetalDrawable {
|
||||
return MetalDrawable{m.Send(sel_drawable)}
|
||||
}
|
||||
|
219
internal/graphicsdriver/metal/displaylink_macos.go
Normal file
219
internal/graphicsdriver/metal/displaylink_macos.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// Copyright 2025 The Ebitengine 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.
|
||||
|
||||
//go:build darwin && !ios
|
||||
|
||||
package metal
|
||||
|
||||
// #cgo CFLAGS: -x objective-c
|
||||
//
|
||||
// #include <Foundation/Foundation.h>
|
||||
// #include <CoreVideo/CVDisplayLink.h>
|
||||
// #if __has_include(<QuartzCore/CAMetalLayer.h>)
|
||||
// #include <QuartzCore/CAMetalLayer.h>
|
||||
// #endif
|
||||
//
|
||||
// static bool isCAMetalDisplayLinkAvailable() {
|
||||
// // TODO: Use PureGo if returning a struct is supported (ebitengine/purego#225).
|
||||
// // As operatingSystemVersion returns a struct, this cannot be written with PureGo.
|
||||
// NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion];
|
||||
// if (version.majorVersion >= 14) {
|
||||
// // Also check if the CAMetalDisplayLink class exists
|
||||
// return NSClassFromString(@"CAMetalDisplayLink") != nil;
|
||||
// }
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// int ebitengine_DisplayLinkOutputCallback(CVDisplayLinkRef displayLinkRef, CVTimeStamp* inNow, CVTimeStamp* inOutputTime, uint64_t flagsIn, uint64_t* flagsOut, void* displayLinkContext);
|
||||
import "C"
|
||||
import (
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ebitengine/purego/objc"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/cocoa"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal/ca"
|
||||
)
|
||||
|
||||
func (v *view) initDisplayLink() error {
|
||||
if C.isCAMetalDisplayLinkAvailable() {
|
||||
if err := v.initCAMetalDisplayLink(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := v.initCADisplayLink(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var class_EbitengineCAMetalDisplayLinkDelegate objc.Class
|
||||
|
||||
func (v *view) initCAMetalDisplayLink() error {
|
||||
v.drawableCh = make(chan ca.MetalDrawable)
|
||||
v.drawableDoneCh = make(chan struct{})
|
||||
v.metalDisplayLinkRunLoop = createThreadWithRunLoop()
|
||||
v.prevMetalDisplayLink = make(chan uintptr, 1)
|
||||
|
||||
c, err := objc.RegisterClass(
|
||||
"EbitengineCAMetalDisplayLinkDelegate",
|
||||
objc.GetClass("NSObject"),
|
||||
[]*objc.Protocol{objc.GetProtocol("CAMetalDisplayLinkDelegate")},
|
||||
nil,
|
||||
[]objc.MethodDef{
|
||||
{
|
||||
Cmd: objc.RegisterName("metalDisplayLink:needsUpdate:"),
|
||||
Fn: func(id objc.ID, cmd objc.SEL, metalDisplayLink objc.ID, needsUpdate objc.ID) {
|
||||
drawable := ca.MetalDisplayLinkUpdate{ID: needsUpdate}.Drawable()
|
||||
if drawable == (ca.MetalDrawable{}) {
|
||||
return
|
||||
}
|
||||
v.drawableCh <- drawable
|
||||
<-v.drawableDoneCh
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
class_EbitengineCAMetalDisplayLinkDelegate = c
|
||||
|
||||
v.createCAMetalDisplayLink()
|
||||
|
||||
// Recreate the display link when the app is recovered from sleep.
|
||||
// TODO: Recreation might be needed when the display is changed.
|
||||
nc := cocoa.NSWorkspace_sharedWorkspace().NotificationCenter()
|
||||
if v.meltaDisplayLinkRecreateBlock == 0 {
|
||||
v.meltaDisplayLinkRecreateBlock = objc.NewBlock(func(block objc.Block) {
|
||||
v.createCAMetalDisplayLink()
|
||||
})
|
||||
}
|
||||
mainQueue := cocoa.NSOperationQueue_mainQueue()
|
||||
v.notificatioObserver = nc.AddObserverForName(cocoa.NSWorkspaceDidWakeNotification, 0, mainQueue, v.meltaDisplayLinkRecreateBlock)
|
||||
cocoa.NSObject{ID: v.notificatioObserver}.Retain()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *view) createCAMetalDisplayLink() {
|
||||
// Release the previous display link if any.
|
||||
// This is done in the thread for the display link, so that the callback is not called during releasing.
|
||||
if v.metalDisplayLink != 0 {
|
||||
// Unfortunately, there is no blocking 'performBlock' for NSRunLoop, so use a channel to wait.
|
||||
if v.metalDisplayLinkReleaseBlock == 0 {
|
||||
v.metalDisplayLinkReleaseBlock = objc.NewBlock(func(block objc.Block) {
|
||||
dl := ca.MetalDisplayLink{ID: objc.ID(<-v.prevMetalDisplayLink)}
|
||||
dl.RemoveFromRunLoop(v.metalDisplayLinkRunLoop, cocoa.NSDefaultRunLoopMode)
|
||||
dl.Release()
|
||||
})
|
||||
}
|
||||
v.prevMetalDisplayLink <- v.metalDisplayLink
|
||||
v.metalDisplayLinkRunLoop.PerformBlock(v.metalDisplayLinkReleaseBlock)
|
||||
}
|
||||
|
||||
dl := ca.NewMetalDisplayLink(v.ml)
|
||||
dl.SetDelegate(objc.ID(class_EbitengineCAMetalDisplayLinkDelegate).Send(objc.RegisterName("new")))
|
||||
dl.AddToRunLoop(v.metalDisplayLinkRunLoop, cocoa.NSDefaultRunLoopMode)
|
||||
dl.SetPaused(false)
|
||||
v.metalDisplayLink = uintptr(dl.ID)
|
||||
}
|
||||
|
||||
func createThreadWithRunLoop() cocoa.NSRunLoop {
|
||||
ch := make(chan cocoa.NSRunLoop)
|
||||
go func() {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
runLoop := cocoa.NSRunLoop_currentRunLoop()
|
||||
ch <- runLoop
|
||||
close(ch)
|
||||
|
||||
// Add a dummy mach port to keep alive.
|
||||
port := cocoa.NSMachPort_port()
|
||||
runLoop.AddPort(port, cocoa.NSRunLoopCommonModes)
|
||||
|
||||
runLoop.Run()
|
||||
}()
|
||||
|
||||
runLoop := <-ch
|
||||
if runLoop.ID == 0 {
|
||||
panic("metal: runLoop must be initialized")
|
||||
}
|
||||
return runLoop
|
||||
}
|
||||
|
||||
func (v *view) initCADisplayLink() error {
|
||||
v.fence = newFence()
|
||||
|
||||
// TODO: CVDisplayLink APIs are deprecated in macOS 10.15 and later.
|
||||
// Use new APIs like NSView.displayLink(target:selector:).
|
||||
var displayLinkRef C.CVDisplayLinkRef
|
||||
if ret := C.CVDisplayLinkCreateWithActiveCGDisplays(&displayLinkRef); ret != kCVReturnSuccess {
|
||||
// Failed to get the display link, so proceed without it.
|
||||
return nil
|
||||
}
|
||||
v.handleToSelf = cgo.NewHandle(v)
|
||||
C.CVDisplayLinkSetOutputCallback(displayLinkRef, C.CVDisplayLinkOutputCallback(C.ebitengine_DisplayLinkOutputCallback), unsafe.Pointer(&v.handleToSelf))
|
||||
C.CVDisplayLinkStart(displayLinkRef)
|
||||
|
||||
v.caDisplayLink = uintptr(displayLinkRef)
|
||||
return nil
|
||||
}
|
||||
|
||||
//export ebitengine_DisplayLinkOutputCallback
|
||||
func ebitengine_DisplayLinkOutputCallback(displayLinkRef C.CVDisplayLinkRef, inNow, inOutputTime *C.CVTimeStamp, flagsIn C.uint64_t, flagsOut *C.uint64_t, displayLinkContext unsafe.Pointer) C.int {
|
||||
cgoHandle := (*cgo.Handle)(displayLinkContext)
|
||||
view := cgoHandle.Value().(*view)
|
||||
view.fence.advance()
|
||||
return 0
|
||||
}
|
||||
|
||||
func (v *view) nextDrawable() ca.MetalDrawable {
|
||||
if v.metalDisplayLink != 0 {
|
||||
if v.drawableTimer == nil {
|
||||
v.drawableTimer = time.NewTimer(time.Second)
|
||||
} else {
|
||||
v.drawableTimer.Reset(time.Second)
|
||||
}
|
||||
defer v.drawableTimer.Stop()
|
||||
select {
|
||||
case d := <-v.drawableCh:
|
||||
return d
|
||||
case <-v.drawableTimer.C:
|
||||
// This happens when the main thread needs to execute the notification observer callback.
|
||||
return ca.MetalDrawable{}
|
||||
}
|
||||
}
|
||||
|
||||
v.waitForDisplayLinkOutputCallback()
|
||||
|
||||
d, err := v.ml.NextDrawable()
|
||||
if err != nil {
|
||||
// Drawable is nil. This can happen at the initial state. Let's wait and see.
|
||||
return ca.MetalDrawable{}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (v *view) finishDrawableUsage() {
|
||||
if v.metalDisplayLink != 0 {
|
||||
v.drawableDoneCh <- struct{}{}
|
||||
return
|
||||
}
|
||||
}
|
@@ -265,9 +265,11 @@ func (g *Graphics) flushCommandBufferIfNeeded(present bool) {
|
||||
|
||||
g.flushRenderCommandEncoderIfNeeded()
|
||||
|
||||
var presented bool
|
||||
if present && g.screenDrawable != (ca.MetalDrawable{}) {
|
||||
g.cb.PresentDrawable(g.screenDrawable)
|
||||
g.screenDrawable = ca.MetalDrawable{}
|
||||
presented = true
|
||||
}
|
||||
|
||||
g.cb.Commit()
|
||||
@@ -278,6 +280,10 @@ func (g *Graphics) flushCommandBufferIfNeeded(present bool) {
|
||||
g.tmpTextures = g.tmpTextures[:0]
|
||||
|
||||
g.cb = mtl.CommandBuffer{}
|
||||
|
||||
if presented {
|
||||
g.view.finishDrawableUsage()
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Graphics) checkSize(width, height int) {
|
||||
@@ -868,6 +874,9 @@ func (i *Image) mtlTexture() mtl.Texture {
|
||||
// After nextDrawable, it is expected some command buffers are completed.
|
||||
g.gcBuffers()
|
||||
}
|
||||
if g.screenDrawable == (ca.MetalDrawable{}) {
|
||||
return mtl.Texture{}
|
||||
}
|
||||
return g.screenDrawable.Texture()
|
||||
}
|
||||
return i.texture
|
||||
|
@@ -17,7 +17,10 @@ package metal
|
||||
import (
|
||||
"runtime/cgo"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ebitengine/purego/objc"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/cocoa"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal/ca"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal/mtl"
|
||||
@@ -41,7 +44,20 @@ type view struct {
|
||||
|
||||
once sync.Once
|
||||
|
||||
displayLink uintptr
|
||||
caDisplayLink uintptr
|
||||
metalDisplayLink uintptr
|
||||
|
||||
// The following members are used only with CAMetalDisplayLink.
|
||||
drawableCh chan ca.MetalDrawable
|
||||
drawableDoneCh chan struct{}
|
||||
drawableTimer *time.Timer
|
||||
prevMetalDisplayLink chan uintptr
|
||||
notificatioObserver objc.ID
|
||||
metalDisplayLinkRunLoop cocoa.NSRunLoop
|
||||
metalDisplayLinkReleaseBlock objc.Block
|
||||
meltaDisplayLinkRecreateBlock objc.Block
|
||||
|
||||
// The following members are used only with CADisplayLink.
|
||||
handleToSelf cgo.Handle
|
||||
fence *fence
|
||||
}
|
||||
@@ -97,7 +113,9 @@ func (v *view) initialize(device mtl.Device, colorSpace graphicsdriver.ColorSpac
|
||||
|
||||
v.ml.SetMaximumDrawableCount(maximumDrawableCount)
|
||||
|
||||
v.initializeOS()
|
||||
if err := v.initializeOS(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -75,6 +75,11 @@ func (v *view) nextDrawable() ca.MetalDrawable {
|
||||
return d
|
||||
}
|
||||
|
||||
func (v *view) initializeOS() {
|
||||
func (v *view) finishDrawableUsage() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
func (v *view) initializeOS() error {
|
||||
// Do nothing.
|
||||
return nil
|
||||
}
|
||||
|
@@ -16,19 +16,10 @@
|
||||
|
||||
package metal
|
||||
|
||||
// #include <CoreVideo/CVDisplayLink.h>
|
||||
//
|
||||
// int ebitengine_DisplayLinkOutputCallback(CVDisplayLinkRef displayLinkRef, CVTimeStamp* inNow, CVTimeStamp* inOutputTime, uint64_t flagsIn, uint64_t* flagsOut, void* displayLinkContext);
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"runtime/cgo"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ebitengine/purego/objc"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/cocoa"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal/ca"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/metal/mtl"
|
||||
)
|
||||
|
||||
@@ -62,52 +53,20 @@ const (
|
||||
resourceStorageMode = mtl.ResourceStorageModeManaged
|
||||
)
|
||||
|
||||
func (v *view) initializeOS() {
|
||||
v.fence = newFence()
|
||||
|
||||
// TODO: CVDisplayLink APIs are deprecated in macOS 10.15 and later.
|
||||
// Use new APIs like NSView.displayLink(target:selector:).
|
||||
var displayLinkRef C.CVDisplayLinkRef
|
||||
if ret := C.CVDisplayLinkCreateWithActiveCGDisplays(&displayLinkRef); ret != kCVReturnSuccess {
|
||||
// Failed to get the display link, so proceed without it.
|
||||
return
|
||||
func (v *view) initializeOS() error {
|
||||
if err := v.initDisplayLink(); err != nil {
|
||||
return err
|
||||
}
|
||||
v.handleToSelf = cgo.NewHandle(v)
|
||||
C.CVDisplayLinkSetOutputCallback(displayLinkRef, C.CVDisplayLinkOutputCallback(C.ebitengine_DisplayLinkOutputCallback), unsafe.Pointer(&v.handleToSelf))
|
||||
C.CVDisplayLinkStart(displayLinkRef)
|
||||
|
||||
v.displayLink = uintptr(displayLinkRef)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *view) waitForDisplayLinkOutputCallback() {
|
||||
if v.displayLink == 0 {
|
||||
if v.caDisplayLink == 0 && v.metalDisplayLink == 0 {
|
||||
return
|
||||
}
|
||||
if v.vsyncDisabled {
|
||||
if v.caDisplayLink == 0 && v.vsyncDisabled {
|
||||
// TODO: nextDrawable still waits for the next drawable available, so this should be fixed not to wait.
|
||||
return
|
||||
}
|
||||
|
||||
v.fence.wait()
|
||||
}
|
||||
|
||||
//export ebitengine_DisplayLinkOutputCallback
|
||||
func ebitengine_DisplayLinkOutputCallback(displayLinkRef C.CVDisplayLinkRef, inNow, inOutputTime *C.CVTimeStamp, flagsIn C.uint64_t, flagsOut *C.uint64_t, displayLinkContext unsafe.Pointer) C.int {
|
||||
cgoHandle := (*cgo.Handle)(displayLinkContext)
|
||||
view := cgoHandle.Value().(*view)
|
||||
view.fence.advance()
|
||||
return 0
|
||||
}
|
||||
|
||||
func (v *view) nextDrawable() ca.MetalDrawable {
|
||||
// TODO: Use CAMetalDisplayLink if available.
|
||||
|
||||
v.waitForDisplayLinkOutputCallback()
|
||||
|
||||
d, err := v.ml.NextDrawable()
|
||||
if err != nil {
|
||||
// Drawable is nil. This can happen at the initial state. Let's wait and see.
|
||||
return ca.MetalDrawable{}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
Reference in New Issue
Block a user