diff --git a/internal/graphicscommand/command.go b/internal/graphicscommand/command.go index bccdb240530f..ca67895d8d98 100644 --- a/internal/graphicscommand/command.go +++ b/internal/graphicscommand/command.go @@ -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 } diff --git a/internal/graphicscommand/thread.go b/internal/graphicscommand/thread.go index a3a604161cd3..6b6d8a4268ab 100644 --- a/internal/graphicscommand/thread.go +++ b/internal/graphicscommand/thread.go @@ -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) } diff --git a/internal/thread/thread.go b/internal/thread/thread.go index 9f7c73a8f980..3120645df6c4 100644 --- a/internal/thread/thread.go +++ b/internal/thread/thread.go @@ -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() {} diff --git a/internal/uidriver/glfw/run_notsinglethread.go b/internal/uidriver/glfw/run_notsinglethread.go index 9e2586564f26..5b0b19dbb3d1 100644 --- a/internal/uidriver/glfw/run_notsinglethread.go +++ b/internal/uidriver/glfw/run_notsinglethread.go @@ -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() diff --git a/internal/uidriver/glfw/ui.go b/internal/uidriver/glfw/ui.go index 77b9b8728829..b030003489d6 100644 --- a/internal/uidriver/glfw/ui.go +++ b/internal/uidriver/glfw/ui.go @@ -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() diff --git a/internal/uidriver/glfw/window.go b/internal/uidriver/glfw/window.go index fbc697f3bf0f..e90735c93db1 100644 --- a/internal/uidriver/glfw/window.go +++ b/internal/uidriver/glfw/window.go @@ -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 }) } diff --git a/internal/uidriver/mobile/ui.go b/internal/uidriver/mobile/ui.go index 3e8ca62a08e5..a6587628913a 100644 --- a/internal/uidriver/mobile/ui.go +++ b/internal/uidriver/mobile/ui.go @@ -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 diff --git a/run.go b/run.go index 596e7ff7be39..5b6b86d7a464 100644 --- a/run.go +++ b/run.go @@ -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 }