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 ( import (
"fmt" "fmt"
"math" "math"
"runtime"
"sort" "sort"
"unsafe" "unsafe"
@ -86,7 +87,15 @@ func NewGraphics() (graphicsdriver.Graphics, error) {
return nil, fmt.Errorf("metal: mtl.CreateSystemDefaultDevice failed") 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 { func (g *Graphics) Begin() error {
@ -356,9 +365,12 @@ func (g *Graphics) Initialize() error {
g.dsss = map[stencilMode]mtl.DepthStencilState{} 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 { if err := g.view.initialize(); err != nil {
return err return err
} }
}
if g.transparent { if g.transparent {
g.view.ml.SetOpaque(false) g.view.ml.SetOpaque(false)
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -111,6 +111,7 @@ type userInterfaceImpl struct {
glContextSetOnce sync.Once glContextSetOnce sync.Once
mainThread threadInterface mainThread threadInterface
renderThread threadInterface
m sync.RWMutex m sync.RWMutex
} }
@ -796,8 +797,7 @@ event:
u.framebufferSizeCallbackCh = nil u.framebufferSizeCallbackCh = nil
} }
// init must be called from the main thread. func (u *userInterfaceImpl) initOnMainThread(options *RunOptions) error {
func (u *userInterfaceImpl) init(options *RunOptions) error {
glfw.WindowHint(glfw.AutoIconify, glfw.False) glfw.WindowHint(glfw.AutoIconify, glfw.False)
decorated := glfw.False decorated := glfw.False
@ -1004,7 +1004,8 @@ func (u *userInterfaceImpl) update() (float64, float64, error) {
func (u *userInterfaceImpl) loopGame() error { func (u *userInterfaceImpl) loopGame() error {
defer u.mainThread.Call(func() { 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() glfw.Terminate()
}) })
for { for {
@ -1026,7 +1027,7 @@ func (u *userInterfaceImpl) updateGame() error {
} }
u.glContextSetOnce.Do(func() { u.glContextSetOnce.Do(func() {
u.mainThread.Call(func() { u.renderThread.Call(func() {
if u.graphicsDriver.IsGL() { if u.graphicsDriver.IsGL() {
u.window.MakeContextCurrent() u.window.MakeContextCurrent()
} }
@ -1037,13 +1038,13 @@ func (u *userInterfaceImpl) updateGame() error {
return err return err
} }
u.mainThread.Call(func() { u.renderThread.Call(func() {
// Call updateVsync even though fpsMode is not updated. // Call updateVsync even though fpsMode is not updated.
// When toggling to fullscreen, vsync state might be reset unexpectedly (#1787). // When toggling to fullscreen, vsync state might be reset unexpectedly (#1787).
u.updateVsync() u.updateVsyncOnRenderThread()
// This works only for OpenGL. // This works only for OpenGL.
u.swapBuffers() u.swapBuffersOnRenderThread()
}) })
return nil return nil
@ -1083,8 +1084,7 @@ func (u *userInterfaceImpl) updateIconIfNeeded() {
}) })
} }
// swapBuffers must be called from the main thread. func (u *userInterfaceImpl) swapBuffersOnRenderThread() {
func (u *userInterfaceImpl) swapBuffers() {
if u.graphicsDriver.IsGL() { if u.graphicsDriver.IsGL() {
u.window.SwapBuffers() u.window.SwapBuffers()
} }
@ -1260,8 +1260,7 @@ func (u *userInterfaceImpl) minimumWindowWidth() int {
return 1 return 1
} }
// updateVsync must be called on the main thread. func (u *userInterfaceImpl) updateVsyncOnRenderThread() {
func (u *userInterfaceImpl) updateVsync() {
if u.graphicsDriver.IsGL() { if u.graphicsDriver.IsGL() {
// SwapInterval is affected by the current monitor of the window. // SwapInterval is affected by the current monitor of the window.
// This needs to be called at least after SetMonitor. // 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 u.graphicsDriver = g
nintendosdk.InitializeGame() nintendosdk.InitializeGame()
for { for {
// TODO: Make a separate thread for rendering (#2512).
nintendosdk.BeginFrame() nintendosdk.BeginFrame()
gamepad.Update() gamepad.Update()
u.updateInputState() u.updateInputState()