all: separate the rendering thread from the main thread

Updates #1704
Closes #2512
This commit is contained in:
Hajime Hoshi 2022-12-30 16:28:07 +09:00
parent 0fc21470e2
commit 34941ca083
8 changed files with 49 additions and 27 deletions

View File

@ -17,6 +17,7 @@ package metal
import (
"fmt"
"math"
"runtime"
"sort"
"unsafe"
@ -86,7 +87,15 @@ func NewGraphics() (graphicsdriver.Graphics, error) {
return nil, fmt.Errorf("metal: mtl.CreateSystemDefaultDevice failed")
}
return &Graphics{}, nil
g := &Graphics{}
if runtime.GOOS != "ios" {
// Initializing a Metal device and a layer must be done in the main thread on macOS.
if err := g.view.initialize(); err != nil {
return nil, err
}
}
return g, nil
}
func (g *Graphics) Begin() error {
@ -356,9 +365,12 @@ func (g *Graphics) Initialize() error {
g.dsss = map[stencilMode]mtl.DepthStencilState{}
}
if runtime.GOOS == "ios" {
// Initializing a Metal device and a layer must be done in the render thread on iOS.
if err := g.view.initialize(); err != nil {
return err
}
}
if g.transparent {
g.view.ml.SetOpaque(false)
}

View File

@ -36,6 +36,7 @@ func (v *view) update() {
return
}
// TODO: Should this be called on the main thread?
cocoaWindow := ns.NewWindow(v.window)
cocoaWindow.ContentView().SetLayer(v.ml)
cocoaWindow.ContentView().SetWantsLayer(true)

View File

@ -16,6 +16,7 @@ package thread
import (
"context"
"runtime"
)
// OSThread represents an OS thread.
@ -32,12 +33,13 @@ func NewOSThread() *OSThread {
}
}
// Loop starts the thread loop until Stop is called.
//
// It is assumed that an OS thread is fixed by runtime.LockOSThread when Loop is called.
// Loop starts the thread loop until Stop is called on the current OS thread.
//
// Loop must be called on the thread.
func (t *OSThread) Loop(ctx context.Context) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
for {
select {
case fn := <-t.funcs:

View File

@ -16,6 +16,7 @@ package ui
import (
"math"
"sync"
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/buffered"
@ -57,6 +58,8 @@ type context struct {
isOffscreenModified bool
skipCount int
setContextOnce sync.Once
}
func newContext(game Game) *context {

View File

@ -27,21 +27,24 @@ import (
func (u *userInterfaceImpl) Run(game Game, options *RunOptions) error {
u.context = newContext(game)
// Initialize the main thread first so the thread is available at u.run (#809).
u.mainThread = thread.NewOSThread()
graphicscommand.SetRenderThread(u.mainThread)
u.setRunning(true)
defer u.setRunning(false)
if err := u.init(options); err != nil {
if err := u.initOnMainThread(options); err != nil {
return err
}
u.mainThread = thread.NewOSThread()
u.renderThread = thread.NewOSThread()
graphicscommand.SetRenderThread(u.renderThread)
ctx, cancel := stdcontext.WithCancel(stdcontext.Background())
defer cancel()
go func() {
_ = u.renderThread.Loop(ctx)
}()
ch := make(chan error, 1)
go func() {
defer close(ch)

View File

@ -26,12 +26,13 @@ func (u *userInterfaceImpl) Run(game Game, options *RunOptions) error {
// Initialize the main thread first so the thread is available at u.run (#809).
u.mainThread = thread.NewNoopThread()
graphicscommand.SetRenderThread(u.mainThread)
u.renderThread = thread.NewNoopThread()
graphicscommand.SetRenderThread(u.renderThread)
u.setRunning(true)
defer u.setRunning(false)
if err := u.init(options); err != nil {
if err := u.initOnMainThread(options); err != nil {
return err
}

View File

@ -111,6 +111,7 @@ type userInterfaceImpl struct {
glContextSetOnce sync.Once
mainThread threadInterface
renderThread threadInterface
m sync.RWMutex
}
@ -796,8 +797,7 @@ event:
u.framebufferSizeCallbackCh = nil
}
// init must be called from the main thread.
func (u *userInterfaceImpl) init(options *RunOptions) error {
func (u *userInterfaceImpl) initOnMainThread(options *RunOptions) error {
glfw.WindowHint(glfw.AutoIconify, glfw.False)
decorated := glfw.False
@ -1004,7 +1004,8 @@ func (u *userInterfaceImpl) update() (float64, float64, error) {
func (u *userInterfaceImpl) loopGame() error {
defer u.mainThread.Call(func() {
u.window.Destroy()
// An explicit destorying a window tries to delete a GL context on the main thread on Windows (wglDeleteContext),
// but this causes an error unfortunately.
glfw.Terminate()
})
for {
@ -1026,7 +1027,7 @@ func (u *userInterfaceImpl) updateGame() error {
}
u.glContextSetOnce.Do(func() {
u.mainThread.Call(func() {
u.renderThread.Call(func() {
if u.graphicsDriver.IsGL() {
u.window.MakeContextCurrent()
}
@ -1037,13 +1038,13 @@ func (u *userInterfaceImpl) updateGame() error {
return err
}
u.mainThread.Call(func() {
u.renderThread.Call(func() {
// Call updateVsync even though fpsMode is not updated.
// When toggling to fullscreen, vsync state might be reset unexpectedly (#1787).
u.updateVsync()
u.updateVsyncOnRenderThread()
// This works only for OpenGL.
u.swapBuffers()
u.swapBuffersOnRenderThread()
})
return nil
@ -1083,8 +1084,7 @@ func (u *userInterfaceImpl) updateIconIfNeeded() {
})
}
// swapBuffers must be called from the main thread.
func (u *userInterfaceImpl) swapBuffers() {
func (u *userInterfaceImpl) swapBuffersOnRenderThread() {
if u.graphicsDriver.IsGL() {
u.window.SwapBuffers()
}
@ -1260,8 +1260,7 @@ func (u *userInterfaceImpl) minimumWindowWidth() int {
return 1
}
// updateVsync must be called on the main thread.
func (u *userInterfaceImpl) updateVsync() {
func (u *userInterfaceImpl) updateVsyncOnRenderThread() {
if u.graphicsDriver.IsGL() {
// SwapInterval is affected by the current monitor of the window.
// This needs to be called at least after SetMonitor.

View File

@ -67,6 +67,7 @@ func (u *userInterfaceImpl) Run(game Game, options *RunOptions) error {
u.graphicsDriver = g
nintendosdk.InitializeGame()
for {
// TODO: Make a separate thread for rendering (#2512).
nintendosdk.BeginFrame()
gamepad.Update()
u.updateInputState()