ebiten: add RunOnMainThread(func()) (#1927)

Closes #1926
This commit is contained in:
Changkun Ou 2022-01-02 19:30:29 +01:00 committed by GitHub
parent 0e74b34705
commit 626c91e360
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 119 additions and 160 deletions

View File

@ -224,10 +224,11 @@ func (q *commandQueue) Enqueue(command command) {
}
// Flush flushes the command queue.
func (q *commandQueue) Flush() error {
return runOnMainThread(func() error {
return q.flush()
func (q *commandQueue) Flush() (err error) {
RunOnMainThread(func() {
err = q.flush()
})
return
}
// flush must be called the main thread.
@ -699,18 +700,19 @@ func (c *newShaderCommand) Exec(indexOffset int) error {
}
// InitializeGraphicsDriverState initialize the current graphics driver state.
func InitializeGraphicsDriverState() error {
return runOnMainThread(func() error {
return theGraphicsDriver.Initialize()
func InitializeGraphicsDriverState() (err error) {
RunOnMainThread(func() {
err = theGraphicsDriver.Initialize()
})
return
}
// ResetGraphicsDriverState resets the current graphics driver state.
// If the graphics driver doesn't have an API to reset, ResetGraphicsDriverState does nothing.
func ResetGraphicsDriverState() error {
func ResetGraphicsDriverState() (err error) {
if r, ok := theGraphicsDriver.(interface{ Reset() error }); ok {
return runOnMainThread(func() error {
return r.Reset()
RunOnMainThread(func() {
err = r.Reset()
})
}
return nil
@ -719,9 +721,8 @@ func ResetGraphicsDriverState() error {
// MaxImageSize returns the maximum size of an image.
func MaxImageSize() int {
var size int
_ = runOnMainThread(func() error {
RunOnMainThread(func() {
size = theGraphicsDriver.MaxImageSize()
return nil
})
return size
}

View File

@ -17,7 +17,7 @@ package graphicscommand
var theThread Thread
type Thread interface {
Call(f func() error) error
Call(f func())
}
// SetMainThread must be called from the main thread (i.e, the goroutine where the thread is created).
@ -25,12 +25,14 @@ func SetMainThread(thread Thread) {
theThread = thread
}
func runOnMainThread(f func() error) error {
// RunOnMainThread calls f on the main thread, and returns an error if any.
func RunOnMainThread(f func()) {
// The thread is nil when 1) GOOS=js or 2) using golang.org/x/mobile/gl.
// When golang.org/x/mobile/gl is used, all the GL functions are called via Context, which already runs on an
// appropriate thread.
if theThread == nil {
return f()
f()
return
}
return theThread.Call(f)
theThread.Call(f)
}

View File

@ -14,20 +14,18 @@
package thread
import (
"errors"
)
// Thread defines threading behavior in Ebiten.
type Thread interface {
Call(func() error) error
Call(func())
Loop()
Stop()
}
// OSThread represents an OS thread.
type OSThread struct {
funcs chan func() error
results chan error
funcs chan func()
done chan struct{}
terminate chan struct{}
}
// NewOSThread creates a new thread.
@ -35,38 +33,45 @@ type OSThread struct {
// It is assumed that the OS thread is fixed by runtime.LockOSThread when NewOSThread is called.
func NewOSThread() *OSThread {
return &OSThread{
funcs: make(chan func() error),
results: make(chan error),
funcs: make(chan func()),
done: make(chan struct{}),
terminate: make(chan struct{}),
}
}
// BreakLoop represents an termination of the loop.
var BreakLoop = errors.New("break loop")
// Loop starts the thread loop until a posted function returns BreakLoop.
// Loop starts the thread loop until Stop is called.
//
// Loop must be called on the thread.
func (t *OSThread) Loop() {
for f := range t.funcs {
err := f()
if err == BreakLoop {
t.results <- nil
for {
select {
case fn := <-t.funcs:
func() {
defer func() {
t.done <- struct{}{}
}()
fn()
}()
case <-t.terminate:
return
}
t.results <- err
}
}
// Stop stops the thread loop.
func (t *OSThread) Stop() {
close(t.terminate)
}
// Call calls f on the thread.
//
// Do not call this from the same thread. This would block forever.
//
// If f returns BreakLoop, Loop returns.
//
// Call blocks if Loop is not called.
func (t *OSThread) Call(f func() error) error {
func (t *OSThread) Call(f func()) {
t.funcs <- f
return <-t.results
<-t.done
}
// NoopThread is used to disable threading.
@ -81,6 +86,7 @@ func NewNoopThread() *NoopThread {
func (t *NoopThread) Loop() {}
// Call executes the func immediately
func (t *NoopThread) Call(f func() error) error {
return f()
}
func (t *NoopThread) Call(f func()) { f() }
// Stop does nothing
func (t *NoopThread) Stop() {}

View File

@ -32,16 +32,13 @@ func (u *UserInterface) Run(uicontext driver.UIContext) error {
ch := make(chan error, 1)
go func() {
defer func() {
_ = u.t.Call(func() error {
return thread.BreakLoop
})
}()
defer u.t.Stop()
defer close(ch)
if err := u.t.Call(func() error {
return u.init()
var err error
if u.t.Call(func() {
err = u.init()
}); err != nil {
ch <- err
return
@ -77,11 +74,7 @@ func (u *UserInterface) runOnAnotherThreadFromMainThread(f func() error) error {
var err error
go func() {
defer func() {
_ = u.t.Call(func() error {
return thread.BreakLoop
})
}()
defer u.t.Stop()
err = f()
}()
u.t.Loop()

View File

@ -484,12 +484,11 @@ func (u *UserInterface) ScreenSizeInFullscreen() (int, int) {
}
var w, h int
_ = u.t.Call(func() error {
u.t.Call(func() {
m := u.currentMonitor()
v := m.GetVideoMode()
w = int(u.dipFromGLFWMonitorPixel(float64(v.Width), m))
h = int(u.dipFromGLFWMonitorPixel(float64(v.Height), m))
return nil
})
return w, h
}
@ -507,9 +506,8 @@ func (u *UserInterface) IsFullscreen() bool {
return u.isInitFullscreen()
}
b := false
_ = u.t.Call(func() error {
u.t.Call(func() {
b = u.isFullscreen()
return nil
})
return b
}
@ -521,18 +519,16 @@ func (u *UserInterface) SetFullscreen(fullscreen bool) {
}
var update bool
_ = u.t.Call(func() error {
u.t.Call(func() {
update = u.isFullscreen() != fullscreen
return nil
})
if !update {
return
}
_ = u.t.Call(func() error {
u.t.Call(func() {
w, h := u.windowWidthInDIP, u.windowHeightInDIP
u.setWindowSizeInDIP(w, h, fullscreen)
return nil
})
}
@ -542,9 +538,8 @@ func (u *UserInterface) IsFocused() bool {
}
var focused bool
_ = u.t.Call(func() error {
u.t.Call(func() {
focused = u.window.GetAttrib(glfw.Focused) == glfw.True
return nil
})
return focused
}
@ -564,14 +559,13 @@ func (u *UserInterface) SetFPSMode(mode driver.FPSMode) {
u.m.Unlock()
return
}
_ = u.t.Call(func() error {
u.t.Call(func() {
if !u.fpsModeInited {
u.fpsMode = mode
return nil
return
}
u.setFPSMode(mode)
u.updateVsync()
return nil
})
}
@ -583,9 +577,8 @@ func (u *UserInterface) FPSMode() driver.FPSMode {
return m
}
var v driver.FPSMode
_ = u.t.Call(func() error {
u.t.Call(func() {
v = u.fpsMode
return nil
})
return v
}
@ -603,21 +596,23 @@ func (u *UserInterface) CursorMode() driver.CursorMode {
if !u.isRunning() {
return u.getInitCursorMode()
}
var v driver.CursorMode
_ = u.t.Call(func() error {
mode := u.window.GetInputMode(glfw.CursorMode)
switch mode {
case glfw.CursorNormal:
v = driver.CursorModeVisible
case glfw.CursorHidden:
v = driver.CursorModeHidden
case glfw.CursorDisabled:
v = driver.CursorModeCaptured
default:
panic(fmt.Sprintf("glfw: invalid GLFW cursor mode: %d", mode))
}
return nil
var mode int
u.t.Call(func() {
mode = u.window.GetInputMode(glfw.CursorMode)
})
var v driver.CursorMode
switch mode {
case glfw.CursorNormal:
v = driver.CursorModeVisible
case glfw.CursorHidden:
v = driver.CursorModeHidden
case glfw.CursorDisabled:
v = driver.CursorModeCaptured
default:
panic(fmt.Sprintf("glfw: invalid GLFW cursor mode: %d", mode))
}
return v
}
@ -626,9 +621,8 @@ func (u *UserInterface) SetCursorMode(mode driver.CursorMode) {
u.setInitCursorMode(mode)
return
}
_ = u.t.Call(func() error {
u.t.Call(func() {
u.window.SetInputMode(glfw.CursorMode, driverCursorModeToGLFWCursorMode(mode))
return nil
})
}
@ -644,9 +638,8 @@ func (u *UserInterface) SetCursorShape(shape driver.CursorShape) {
if !u.isRunning() {
return
}
_ = u.t.Call(func() error {
u.t.Call(func() {
u.setNativeCursor(shape)
return nil
})
}
@ -657,9 +650,8 @@ func (u *UserInterface) DeviceScaleFactor() float64 {
}
f := 0.0
_ = u.t.Call(func() error {
u.t.Call(func() {
f = u.deviceScaleFactor(u.currentMonitor())
return nil
})
return f
}
@ -745,7 +737,7 @@ func (u *UserInterface) registerWindowSetSizeCallback() {
var outsideWidth, outsideHeight float64
_ = u.t.Call(func() error {
u.t.Call(func() {
if width != 0 || height != 0 {
w := int(u.dipFromGLFWPixel(float64(width), u.currentMonitor()))
h := int(u.dipFromGLFWPixel(float64(height), u.currentMonitor()))
@ -753,16 +745,14 @@ func (u *UserInterface) registerWindowSetSizeCallback() {
}
outsideWidth, outsideHeight = u.updateSize()
return nil
})
u.context.Layout(outsideWidth, outsideHeight)
if err := u.context.ForceUpdateFrame(); err != nil {
return err
}
if u.Graphics().IsGL() {
_ = u.t.Call(func() error {
u.t.Call(func() {
u.swapBuffers()
return nil
})
}
return nil
@ -1023,12 +1013,7 @@ func (u *UserInterface) update() (float64, float64, error) {
}
func (u *UserInterface) loop() error {
defer func() {
_ = u.t.Call(func() error {
glfw.Terminate()
return nil
})
}()
defer u.t.Call(glfw.Terminate)
for {
var unfocused bool
@ -1047,10 +1032,9 @@ func (u *UserInterface) loop() error {
}
var outsideWidth, outsideHeight float64
if err := u.t.Call(func() error {
var err error
var err error
if u.t.Call(func() {
outsideWidth, outsideHeight, err = u.update()
return err
}); err != nil {
return err
}
@ -1084,14 +1068,13 @@ func (u *UserInterface) loop() error {
newImgs[i] = rgba
}
_ = u.t.Call(func() error {
u.t.Call(func() {
// In the fullscreen mode, reset the icon images and try again later.
if u.isFullscreen() {
u.setIconImages(imgs)
return nil
return
}
u.window.SetIcon(newImgs)
return nil
})
}()
}
@ -1100,10 +1083,7 @@ func (u *UserInterface) loop() error {
// However, (*thread).Call is not good for performance due to channels.
// Let's avoid this whenever possible (#1367).
if u.Graphics().IsGL() {
_ = u.t.Call(func() error {
u.swapBuffers()
return nil
})
u.t.Call(u.swapBuffers)
}
if unfocused {
@ -1399,9 +1379,8 @@ func (u *UserInterface) IsScreenTransparent() bool {
return u.isInitScreenTransparent()
}
val := false
_ = u.t.Call(func() error {
u.t.Call(func() {
val = u.window.GetAttrib(glfw.TransparentFramebuffer) == glfw.True
return nil
})
return val
}
@ -1409,9 +1388,8 @@ func (u *UserInterface) IsScreenTransparent() bool {
func (u *UserInterface) ResetForFrame() {
// The offscreens must be updated every frame (#490).
var w, h float64
_ = u.t.Call(func() error {
u.t.Call(func() {
w, h = u.updateSize()
return nil
})
u.context.Layout(w, h)
u.input.resetForFrame()

View File

@ -32,9 +32,8 @@ func (w *window) IsDecorated() bool {
return w.ui.isInitWindowDecorated()
}
v := false
_ = w.ui.t.Call(func() error {
w.ui.t.Call(func() {
v = w.ui.window.GetAttrib(glfw.Decorated) == glfw.True
return nil
})
return v
}
@ -45,13 +44,12 @@ func (w *window) SetDecorated(decorated bool) {
return
}
_ = w.ui.t.Call(func() error {
w.ui.t.Call(func() {
if w.ui.isNativeFullscreen() {
return nil
return
}
w.ui.setWindowDecorated(decorated)
return nil
})
}
@ -60,9 +58,8 @@ func (w *window) IsResizable() bool {
return w.ui.isInitWindowResizable()
}
v := false
_ = w.ui.t.Call(func() error {
w.ui.t.Call(func() {
v = w.ui.window.GetAttrib(glfw.Resizable) == glfw.True
return nil
})
return v
}
@ -72,12 +69,11 @@ func (w *window) SetResizable(resizable bool) {
w.ui.setInitWindowResizable(resizable)
return
}
_ = w.ui.t.Call(func() error {
w.ui.t.Call(func() {
if w.ui.isNativeFullscreen() {
return nil
return
}
w.ui.setWindowResizable(resizable)
return nil
})
}
@ -86,9 +82,8 @@ func (w *window) IsFloating() bool {
return w.ui.isInitWindowFloating()
}
var v bool
_ = w.ui.t.Call(func() error {
w.ui.t.Call(func() {
v = w.ui.window.GetAttrib(glfw.Floating) == glfw.True
return nil
})
return v
}
@ -98,12 +93,11 @@ func (w *window) SetFloating(floating bool) {
w.ui.setInitWindowFloating(floating)
return
}
_ = w.ui.t.Call(func() error {
w.ui.t.Call(func() {
if w.ui.isNativeFullscreen() {
return nil
return
}
w.ui.setWindowFloating(floating)
return nil
})
}
@ -112,9 +106,8 @@ func (w *window) IsMaximized() bool {
return w.ui.isInitWindowMaximized()
}
var v bool
_ = w.ui.t.Call(func() error {
w.ui.t.Call(func() {
v = w.ui.window.GetAttrib(glfw.Maximized) == glfw.True
return nil
})
return v
}
@ -127,10 +120,7 @@ func (w *window) Maximize() {
w.ui.setInitWindowMaximized(true)
return
}
_ = w.ui.t.Call(func() error {
w.ui.maximizeWindow()
return nil
})
w.ui.t.Call(w.ui.maximizeWindow)
}
func (w *window) IsMinimized() bool {
@ -138,9 +128,8 @@ func (w *window) IsMinimized() bool {
return false
}
var v bool
_ = w.ui.t.Call(func() error {
w.ui.t.Call(func() {
v = w.ui.window.GetAttrib(glfw.Iconified) == glfw.True
return nil
})
return v
}
@ -150,10 +139,7 @@ func (w *window) Minimize() {
// Do nothing
return
}
_ = w.ui.t.Call(func() error {
w.ui.iconifyWindow()
return nil
})
w.ui.t.Call(w.ui.iconifyWindow)
}
func (w *window) Restore() {
@ -161,10 +147,7 @@ func (w *window) Restore() {
// Do nothing
return
}
_ = w.ui.t.Call(func() error {
w.ui.restoreWindow()
return nil
})
w.ui.t.Call(w.ui.restoreWindow)
}
func (w *window) Position() (int, int) {
@ -172,7 +155,7 @@ func (w *window) Position() (int, int) {
panic("glfw: WindowPosition can't be called before the main loop starts")
}
x, y := 0, 0
_ = w.ui.t.Call(func() error {
w.ui.t.Call(func() {
var wx, wy int
if w.ui.isFullscreen() && !w.ui.isNativeFullscreenAvailable() {
wx, wy = w.ui.origPos()
@ -186,7 +169,6 @@ func (w *window) Position() (int, int) {
xf := w.ui.dipFromGLFWPixel(float64(wx), m)
yf := w.ui.dipFromGLFWPixel(float64(wy), m)
x, y = int(xf), int(yf)
return nil
})
return x, y
}
@ -196,9 +178,8 @@ func (w *window) SetPosition(x, y int) {
w.ui.setInitWindowPositionInDIP(x, y)
return
}
_ = w.ui.t.Call(func() error {
w.ui.t.Call(func() {
w.ui.setWindowPositionInDIP(x, y, w.ui.currentMonitor())
return nil
})
}
@ -208,10 +189,9 @@ func (w *window) Size() (int, int) {
return w.ui.adjustWindowSizeBasedOnSizeLimitsInDIP(ww, wh)
}
ww, wh := 0, 0
_ = w.ui.t.Call(func() error {
w.ui.t.Call(func() {
ww = w.ui.windowWidthInDIP
wh = w.ui.windowHeightInDIP
return nil
})
return ww, wh
}
@ -221,15 +201,14 @@ func (w *window) SetSize(width, height int) {
w.ui.setInitWindowSizeInDIP(width, height)
return
}
_ = w.ui.t.Call(func() error {
w.ui.t.Call(func() {
// When a window is a native fullscreen, forcing to resize the window might leave unexpected image lags.
// Forbid this.
if w.ui.isNativeFullscreen() {
return nil
return
}
w.ui.setWindowSizeInDIP(width, height, w.ui.isFullscreen())
return nil
})
}
@ -245,10 +224,7 @@ func (w *window) SetSizeLimits(minw, minh, maxw, maxh int) {
return
}
_ = w.ui.t.Call(func() error {
w.ui.updateWindowSizeLimits()
return nil
})
w.ui.t.Call(w.ui.updateWindowSizeLimits)
}
func (w *window) SetIcon(iconImages []image.Image) {
@ -264,9 +240,8 @@ func (w *window) SetTitle(title string) {
return
}
w.ui.title = title
_ = w.ui.t.Call(func() error {
w.ui.t.Call(func() {
w.ui.setWindowTitle(title)
return nil
})
}

View File

@ -88,9 +88,7 @@ func (u *UserInterface) Update() error {
renderCh <- struct{}{}
go func() {
<-renderEndCh
u.t.Call(func() error {
return thread.BreakLoop
})
u.t.Stop()
}()
u.t.Loop()
return nil

6
run.go
View File

@ -19,6 +19,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/clock"
"github.com/hajimehoshi/ebiten/v2/internal/driver"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
)
// Game defines necessary functions for a game.
@ -168,6 +169,11 @@ func RunGame(game Game) error {
return nil
}
// RunOnMainThread calls the given f on the main thread, and blocks until f returns.
func RunOnMainThread(f func()) {
graphicscommand.RunOnMainThread(f)
}
func isRunGameEnded() bool {
return atomic.LoadInt32(&isRunGameEnded_) != 0
}