From 2db10b1e9c4be0ae183418301682bad7f0fe4088 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 5 Nov 2023 00:00:17 +0900 Subject: [PATCH] ebiten: add RunGameOptions.SingleThread and deprecate `ebitenginesinglethread` build tag Closes #2830 --- doc.go | 3 +- internal/ui/run_notsinglethread.go | 51 +---------------------- internal/ui/run_singlethread.go | 29 +------------ internal/ui/ui.go | 67 ++++++++++++++++++++++++++++++ internal/ui/ui_glfw.go | 5 ++- internal/ui/ui_js.go | 2 +- internal/ui/ui_nintendosdk.go | 5 ++- internal/ui/ui_playstation5.go | 5 ++- run.go | 15 +++++++ 9 files changed, 101 insertions(+), 81 deletions(-) diff --git a/doc.go b/doc.go index 7ceec1222..5a927c079 100644 --- a/doc.go +++ b/doc.go @@ -109,7 +109,8 @@ // `ebitenginesinglethread` disables Ebitengine's thread safety to unlock maximum performance. If you use this you will have // to manage threads yourself. Functions like IsKeyPressed will no longer be concurrent-safe with this build tag. // They must be called from the main thread or the same goroutine as the given game's callback functions like Update -// to RunGame. `ebitenginesinglethread` works only with desktops. +// `ebitenginesinglethread` works only with desktops. +// `ebitenginesinglethread` was deprecated as of v2.7. Use RunGameOptions.SingleThread instead. // // `microsoftgdk` is for Microsoft GDK (e.g. Xbox). // diff --git a/internal/ui/run_notsinglethread.go b/internal/ui/run_notsinglethread.go index 8e8bbbe8e..f775f76c6 100644 --- a/internal/ui/run_notsinglethread.go +++ b/internal/ui/run_notsinglethread.go @@ -12,55 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build !android && !ios && !js && !ebitenginesinglethread && !ebitensinglethread +//go:build !ebitenginesinglethread && !ebitensinglethread package ui -import ( - stdcontext "context" - - "golang.org/x/sync/errgroup" - - "github.com/hajimehoshi/ebiten/v2/internal/graphicscommand" - "github.com/hajimehoshi/ebiten/v2/internal/thread" -) - -func (u *UserInterface) run(game Game, options *RunOptions) error { - u.mainThread = thread.NewOSThread() - u.renderThread = thread.NewOSThread() - graphicscommand.SetRenderThread(u.renderThread) - - // Set the running state true after the main thread is set, and before initOnMainThread is called (#2742). - // TODO: As the existance of the main thread is the same as the value of `running`, this is redundant. - // Make `mainThread` atomic and remove `running` if possible. - u.setRunning(true) - defer u.setRunning(false) - - u.context = newContext(game) - - if err := u.initOnMainThread(options); err != nil { - return err - } - - ctx, cancel := stdcontext.WithCancel(stdcontext.Background()) - defer cancel() - - var wg errgroup.Group - - // Run the render thread. - wg.Go(func() error { - defer cancel() - _ = u.renderThread.Loop(ctx) - return nil - }) - - // Run the game thread. - wg.Go(func() error { - defer cancel() - return u.loopGame() - }) - - // Run the main thread. - _ = u.mainThread.Loop(ctx) - return wg.Wait() -} +const buildTagSingleThread = false diff --git a/internal/ui/run_singlethread.go b/internal/ui/run_singlethread.go index 0fc6cb29f..8d5d20811 100644 --- a/internal/ui/run_singlethread.go +++ b/internal/ui/run_singlethread.go @@ -12,33 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:build js || (!android && !ios && (ebitenginesinglethread || ebitensinglethread)) +//go:build ebitenginesinglethread || ebitensinglethread package ui -import ( - "github.com/hajimehoshi/ebiten/v2/internal/graphicscommand" - "github.com/hajimehoshi/ebiten/v2/internal/thread" -) - -func (u *UserInterface) run(game Game, options *RunOptions) error { - // Initialize the main thread first so the thread is available at u.run (#809). - u.mainThread = thread.NewNoopThread() - u.renderThread = thread.NewNoopThread() - graphicscommand.SetRenderThread(u.renderThread) - - u.setRunning(true) - defer u.setRunning(false) - - u.context = newContext(game) - - if err := u.initOnMainThread(options); err != nil { - return err - } - - if err := u.loopGame(); err != nil { - return err - } - - return nil -} +const buildTagSingleThread = true diff --git a/internal/ui/ui.go b/internal/ui/ui.go index b7fa0dd0f..fb32a9183 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -15,12 +15,16 @@ package ui import ( + stdcontext "context" "errors" "image" "sync" "sync/atomic" + "golang.org/x/sync/errgroup" + "github.com/hajimehoshi/ebiten/v2/internal/atlas" + "github.com/hajimehoshi/ebiten/v2/internal/graphicscommand" "github.com/hajimehoshi/ebiten/v2/internal/mipmap" "github.com/hajimehoshi/ebiten/v2/internal/thread" ) @@ -142,6 +146,7 @@ type RunOptions struct { InitUnfocused bool ScreenTransparent bool SkipTaskbar bool + SingleThread bool } // InitialWindowPosition returns the position for centering the given second width/height pair within the first width/height pair. @@ -202,3 +207,65 @@ func (u *UserInterface) isTerminated() bool { func (u *UserInterface) setTerminated() { atomic.StoreInt32(&u.terminated, 1) } + +func (u *UserInterface) runMultiThread(game Game, options *RunOptions) error { + u.mainThread = thread.NewOSThread() + u.renderThread = thread.NewOSThread() + graphicscommand.SetRenderThread(u.renderThread) + + // Set the running state true after the main thread is set, and before initOnMainThread is called (#2742). + // TODO: As the existance of the main thread is the same as the value of `running`, this is redundant. + // Make `mainThread` atomic and remove `running` if possible. + u.setRunning(true) + defer u.setRunning(false) + + u.context = newContext(game) + + if err := u.initOnMainThread(options); err != nil { + return err + } + + ctx, cancel := stdcontext.WithCancel(stdcontext.Background()) + defer cancel() + + var wg errgroup.Group + + // Run the render thread. + wg.Go(func() error { + defer cancel() + _ = u.renderThread.Loop(ctx) + return nil + }) + + // Run the game thread. + wg.Go(func() error { + defer cancel() + return u.loopGame() + }) + + // Run the main thread. + _ = u.mainThread.Loop(ctx) + return wg.Wait() +} + +func (u *UserInterface) runSingleThread(game Game, options *RunOptions) error { + // Initialize the main thread first so the thread is available at u.run (#809). + u.mainThread = thread.NewNoopThread() + u.renderThread = thread.NewNoopThread() + graphicscommand.SetRenderThread(u.renderThread) + + u.setRunning(true) + defer u.setRunning(false) + + u.context = newContext(game) + + if err := u.initOnMainThread(options); err != nil { + return err + } + + if err := u.loopGame(); err != nil { + return err + } + + return nil +} diff --git a/internal/ui/ui_glfw.go b/internal/ui/ui_glfw.go index 2cdad9ce6..13bf5135f 100644 --- a/internal/ui/ui_glfw.go +++ b/internal/ui/ui_glfw.go @@ -121,7 +121,10 @@ func init() { } func (u *UserInterface) Run(game Game, options *RunOptions) error { - return u.run(game, options) + if options.SingleThread || buildTagSingleThread { + return u.runSingleThread(game, options) + } + return u.runMultiThread(game, options) } func (u *UserInterface) init() error { diff --git a/internal/ui/ui_js.go b/internal/ui/ui_js.go index 08285167d..205e1979b 100644 --- a/internal/ui/ui_js.go +++ b/internal/ui/ui_js.go @@ -741,7 +741,7 @@ func (u *UserInterface) forceUpdateOnMinimumFPSMode() { } func (u *UserInterface) Run(game Game, options *RunOptions) error { - return u.run(game, options) + return u.runSingleThread(game, options) } func (u *UserInterface) initOnMainThread(options *RunOptions) error { diff --git a/internal/ui/ui_nintendosdk.go b/internal/ui/ui_nintendosdk.go index 98d5e8717..237b451d6 100644 --- a/internal/ui/ui_nintendosdk.go +++ b/internal/ui/ui_nintendosdk.go @@ -76,7 +76,10 @@ func (u *UserInterface) init() error { } func (u *UserInterface) Run(game Game, options *RunOptions) error { - return u.run(game, options) + if options.SingleThread || buildTagSingleThread { + return u.runSingleThread(game, options) + } + return u.runMultiThread(game, options) } func (u *UserInterface) initOnMainThread(options *RunOptions) error { diff --git a/internal/ui/ui_playstation5.go b/internal/ui/ui_playstation5.go index b33709a93..a45714c3a 100644 --- a/internal/ui/ui_playstation5.go +++ b/internal/ui/ui_playstation5.go @@ -70,7 +70,10 @@ func (u *UserInterface) init() error { } func (u *UserInterface) Run(game Game, options *RunOptions) error { - return u.run(game, options) + if options.SingleThread || buildTagSingleThread { + return u.runSingleThread(game, options) + } + return u.runMultiThread(game, options) } func (u *UserInterface) initOnMainThread(options *RunOptions) error { diff --git a/run.go b/run.go index f3c6f1160..d75c7e005 100644 --- a/run.go +++ b/run.go @@ -253,6 +253,20 @@ type RunGameOptions struct { // // The default (zero) value is false, which means that an icon is shown on a taskbar. SkipTaskbar bool + + // SingleThread indicates whether the single thread mode is used explicitly or not. + // The single thread mode disables Ebitengine's thread safety to unlock maximum performance. + // If you use this you will have to manage threads yourself. + // Functions like IsKeyPressed will no longer be concurrent-safe with this build tag. + // They must be called from the main thread or the same goroutine as the given game's callback functions like Update. + // + // SingleThread works only with desktops. + // + // If SingleThread is false, and if the build tag `ebitenginesinglethread` is specified, + // the single thread mode is used. + // + // The default (zero) value is false, which means that the single thread mode is disabled. + SingleThread bool } // RunGameWithOptions starts the main loop and runs the game with the specified options. @@ -672,6 +686,7 @@ func toUIRunOptions(options *RunGameOptions) *ui.RunOptions { InitUnfocused: options.InitUnfocused, ScreenTransparent: options.ScreenTransparent, SkipTaskbar: options.SkipTaskbar, + SingleThread: options.SingleThread, } }