From 4573883b03288e80cabcf383f556b715e621ed86 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Thu, 8 Jul 2021 04:21:20 +0900 Subject: [PATCH] internal/graphicsdriver/metal: Make FPS stable by 'presentsWithTransaction' Closes #1196 --- internal/graphicsdriver/metal/ca/ca.go | 14 ++++++++++++++ internal/graphicsdriver/metal/ca/ca.h | 2 ++ internal/graphicsdriver/metal/ca/ca.m | 16 ++++++++++++++++ internal/graphicsdriver/metal/graphics.go | 6 +++++- internal/graphicsdriver/metal/mtl/mtl.go | 7 +++++++ internal/graphicsdriver/metal/mtl/mtl.h | 1 + internal/graphicsdriver/metal/mtl/mtl.m | 4 ++++ internal/graphicsdriver/metal/view.go | 9 ++++++++- 8 files changed, 57 insertions(+), 2 deletions(-) diff --git a/internal/graphicsdriver/metal/ca/ca.go b/internal/graphicsdriver/metal/ca/ca.go index 008021acc..c8582ede7 100644 --- a/internal/graphicsdriver/metal/ca/ca.go +++ b/internal/graphicsdriver/metal/ca/ca.go @@ -147,6 +147,13 @@ func (ml MetalLayer) NextDrawable() (MetalDrawable, error) { return MetalDrawable{md}, nil } +// PresentsWithTransaction returns a Boolean value that determines whether the layer presents its content using a Core Animation transaction. +// +// Reference: https://developer.apple.com/documentation/quartzcore/cametallayer/1478157-presentswithtransaction +func (ml MetalLayer) PresentsWithTransaction() bool { + return C.MetalLayer_PresentsWithTransaction(ml.metalLayer) != 0 +} + // 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 @@ -175,3 +182,10 @@ func (md MetalDrawable) Drawable() unsafe.Pointer { return md.metalDrawable } func (md MetalDrawable) Texture() mtl.Texture { return mtl.NewTexture(C.MetalDrawable_Texture(md.metalDrawable)) } + +// Present presents the drawable onscreen as soon as possible. +// +// Reference: https://developer.apple.com/documentation/metal/mtldrawable/1470284-present. +func (md MetalDrawable) Present() { + C.MetalDrawable_Present(md.metalDrawable) +} diff --git a/internal/graphicsdriver/metal/ca/ca.h b/internal/graphicsdriver/metal/ca/ca.h index 9b5dab957..afa8f2add 100644 --- a/internal/graphicsdriver/metal/ca/ca.h +++ b/internal/graphicsdriver/metal/ca/ca.h @@ -31,5 +31,7 @@ void MetalLayer_SetDisplaySyncEnabled(void *metalLayer, void MetalLayer_SetDrawableSize(void *metalLayer, double width, double height); void *MetalLayer_NextDrawable(void *metalLayer); void MetalLayer_SetFramebufferOnly(void *metalLayer, uint8_t framebufferOnly); +uint8_t MetalLayer_PresentsWithTransaction(void *metalLayer); void *MetalDrawable_Texture(void *drawable); +void MetalDrawable_Present(void *drawable); diff --git a/internal/graphicsdriver/metal/ca/ca.m b/internal/graphicsdriver/metal/ca/ca.m index 0ceeb7eb2..11d13921e 100644 --- a/internal/graphicsdriver/metal/ca/ca.m +++ b/internal/graphicsdriver/metal/ca/ca.m @@ -85,6 +85,14 @@ void MetalLayer_SetDisplaySyncEnabled(void *metalLayer, [((CAMetalLayer *)metalLayer) setDisplaySyncEnabled:displaySyncEnabled]; } #endif + + // setting presentsWithTransaction YES makes the FPS stable (#1196). We're not + // sure why... + if (displaySyncEnabled) { + [((CAMetalLayer *)metalLayer) setPresentsWithTransaction:YES]; + } else { + [((CAMetalLayer *)metalLayer) setPresentsWithTransaction:NO]; + } } void MetalLayer_SetDrawableSize(void *metalLayer, double width, double height) { @@ -99,6 +107,14 @@ void *MetalDrawable_Texture(void *metalDrawable) { return ((id)metalDrawable).texture; } +void MetalDrawable_Present(void *metalDrawable) { + [((id)metalDrawable) present]; +} + void MetalLayer_SetFramebufferOnly(void *metalLayer, uint8_t framebufferOnly) { [((CAMetalLayer *)metalLayer) setFramebufferOnly:framebufferOnly]; } + +uint8_t MetalLayer_PresentsWithTransaction(void *metalLayer) { + return [((CAMetalLayer *)metalLayer) presentsWithTransaction]; +} diff --git a/internal/graphicsdriver/metal/graphics.go b/internal/graphicsdriver/metal/graphics.go index 709a24bd1..3f23a737b 100644 --- a/internal/graphicsdriver/metal/graphics.go +++ b/internal/graphicsdriver/metal/graphics.go @@ -467,10 +467,14 @@ func (g *Graphics) flushIfNeeded(present bool) { } g.flushRenderCommandEncoderIfNeeded() - if present && g.screenDrawable != (ca.MetalDrawable{}) { + if !g.view.presentsWithTransaction() && present && g.screenDrawable != (ca.MetalDrawable{}) { g.cb.PresentDrawable(g.screenDrawable) } g.cb.Commit() + if g.view.presentsWithTransaction() && present && g.screenDrawable != (ca.MetalDrawable{}) { + g.cb.WaitUntilScheduled() + g.screenDrawable.Present() + } for _, t := range g.tmpTextures { t.Release() diff --git a/internal/graphicsdriver/metal/mtl/mtl.go b/internal/graphicsdriver/metal/mtl/mtl.go index 6b9602489..427d92c66 100644 --- a/internal/graphicsdriver/metal/mtl/mtl.go +++ b/internal/graphicsdriver/metal/mtl/mtl.go @@ -670,6 +670,13 @@ func (cb CommandBuffer) WaitUntilCompleted() { C.CommandBuffer_WaitUntilCompleted(cb.commandBuffer) } +// WaitUntilScheduled blocks execution of the current thread until the command buffer is scheduled. +// +// Reference: https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443036-waituntilscheduled. +func (cb CommandBuffer) WaitUntilScheduled() { + C.CommandBuffer_WaitUntilScheduled(cb.commandBuffer) +} + // MakeRenderCommandEncoder creates an encoder object that can // encode graphics rendering commands into this command buffer. // diff --git a/internal/graphicsdriver/metal/mtl/mtl.h b/internal/graphicsdriver/metal/mtl/mtl.h index 52aee873b..58a576f28 100644 --- a/internal/graphicsdriver/metal/mtl/mtl.h +++ b/internal/graphicsdriver/metal/mtl/mtl.h @@ -153,6 +153,7 @@ uint8_t CommandBuffer_Status(void *commandBuffer); void CommandBuffer_PresentDrawable(void *commandBuffer, void *drawable); void CommandBuffer_Commit(void *commandBuffer); void CommandBuffer_WaitUntilCompleted(void *commandBuffer); +void CommandBuffer_WaitUntilScheduled(void *commandBuffer); void * CommandBuffer_MakeRenderCommandEncoder(void *commandBuffer, struct RenderPassDescriptor descriptor); diff --git a/internal/graphicsdriver/metal/mtl/mtl.m b/internal/graphicsdriver/metal/mtl/mtl.m index 94a11486c..a77490046 100644 --- a/internal/graphicsdriver/metal/mtl/mtl.m +++ b/internal/graphicsdriver/metal/mtl/mtl.m @@ -188,6 +188,10 @@ void CommandBuffer_WaitUntilCompleted(void *commandBuffer) { [(id)commandBuffer waitUntilCompleted]; } +void CommandBuffer_WaitUntilScheduled(void *commandBuffer) { + [(id)commandBuffer waitUntilScheduled]; +} + void * CommandBuffer_MakeRenderCommandEncoder(void *commandBuffer, struct RenderPassDescriptor descriptor) { diff --git a/internal/graphicsdriver/metal/view.go b/internal/graphicsdriver/metal/view.go index c221d12a2..381fc2323 100644 --- a/internal/graphicsdriver/metal/view.go +++ b/internal/graphicsdriver/metal/view.go @@ -69,7 +69,10 @@ func (v *view) reset() error { // MTLPixelFormatBGRA8Unorm_sRGB, MTLPixelFormatRGBA16Float, MTLPixelFormatBGRA10_XR, or // MTLPixelFormatBGRA10_XR_sRGB. v.ml.SetPixelFormat(mtl.PixelFormatBGRA8UNorm) - v.ml.SetMaximumDrawableCount(3) + + // When presentsWithTransaction is YES and triple buffering is enabled, nextDrawing returns immediately once every two times. + // This makes FPS doubled. To avoid this, disable the triple buffering. + v.ml.SetMaximumDrawableCount(2) // The vsync state might be reset. Set the state again (#1364). v.ml.SetDisplaySyncEnabled(v.vsync) @@ -86,3 +89,7 @@ func (v *view) nextDrawable() ca.MetalDrawable { } return d } + +func (v *view) presentsWithTransaction() bool { + return v.ml.PresentsWithTransaction() +}