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. // Flush flushes the command queue.
func (q *commandQueue) Flush() error { func (q *commandQueue) Flush() (err error) {
return runOnMainThread(func() error { RunOnMainThread(func() {
return q.flush() err = q.flush()
}) })
return
} }
// flush must be called the main thread. // 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. // InitializeGraphicsDriverState initialize the current graphics driver state.
func InitializeGraphicsDriverState() error { func InitializeGraphicsDriverState() (err error) {
return runOnMainThread(func() error { RunOnMainThread(func() {
return theGraphicsDriver.Initialize() err = theGraphicsDriver.Initialize()
}) })
return
} }
// ResetGraphicsDriverState resets the current graphics driver state. // ResetGraphicsDriverState resets the current graphics driver state.
// If the graphics driver doesn't have an API to reset, ResetGraphicsDriverState does nothing. // 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 { if r, ok := theGraphicsDriver.(interface{ Reset() error }); ok {
return runOnMainThread(func() error { RunOnMainThread(func() {
return r.Reset() err = r.Reset()
}) })
} }
return nil return nil
@ -719,9 +721,8 @@ func ResetGraphicsDriverState() error {
// MaxImageSize returns the maximum size of an image. // MaxImageSize returns the maximum size of an image.
func MaxImageSize() int { func MaxImageSize() int {
var size int var size int
_ = runOnMainThread(func() error { RunOnMainThread(func() {
size = theGraphicsDriver.MaxImageSize() size = theGraphicsDriver.MaxImageSize()
return nil
}) })
return size return size
} }

View File

@ -17,7 +17,7 @@ package graphicscommand
var theThread Thread var theThread Thread
type Thread interface { 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). // 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 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. // 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 // When golang.org/x/mobile/gl is used, all the GL functions are called via Context, which already runs on an
// appropriate thread. // appropriate thread.
if theThread == nil { if theThread == nil {
return f() f()
return
} }
return theThread.Call(f) theThread.Call(f)
} }

View File

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

View File

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

View File

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

View File

@ -88,9 +88,7 @@ func (u *UserInterface) Update() error {
renderCh <- struct{}{} renderCh <- struct{}{}
go func() { go func() {
<-renderEndCh <-renderEndCh
u.t.Call(func() error { u.t.Stop()
return thread.BreakLoop
})
}() }()
u.t.Loop() u.t.Loop()
return nil 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/clock"
"github.com/hajimehoshi/ebiten/v2/internal/driver" "github.com/hajimehoshi/ebiten/v2/internal/driver"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
) )
// Game defines necessary functions for a game. // Game defines necessary functions for a game.
@ -168,6 +169,11 @@ func RunGame(game Game) error {
return nil 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 { func isRunGameEnded() bool {
return atomic.LoadInt32(&isRunGameEnded_) != 0 return atomic.LoadInt32(&isRunGameEnded_) != 0
} }