ebiten: add RunGameWithOptions to specify graphics library

This also adds mobile.SetGameWithOptions.

Updates #2378
This commit is contained in:
Hajime Hoshi 2022-12-09 14:07:04 +09:00
parent 032f55d19a
commit bb68ebfcad
18 changed files with 198 additions and 65 deletions

View File

@ -26,6 +26,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/hajimehoshi/ebiten/v2"
@ -35,18 +36,19 @@ import (
)
var (
flagFullscreen = flag.Bool("fullscreen", false, "fullscreen")
flagResizable = flag.Bool("resizable", false, "make the window resizable")
flagWindowPosition = flag.String("windowposition", "", "window position (e.g., 100,200)")
flagTransparent = flag.Bool("transparent", false, "screen transparent")
flagAutoAdjusting = flag.Bool("autoadjusting", false, "make the game screen auto-adjusting")
flagFloating = flag.Bool("floating", false, "make the window floating")
flagMaximize = flag.Bool("maximize", false, "maximize the window")
flagVsync = flag.Bool("vsync", true, "enable vsync")
flagAutoRestore = flag.Bool("autorestore", false, "restore the window automatically")
flagInitFocused = flag.Bool("initfocused", true, "whether the window is focused on start")
flagMinWindowSize = flag.String("minwindowsize", "", "minimum window size (e.g., 100x200)")
flagMaxWindowSize = flag.String("maxwindowsize", "", "maximium window size (e.g., 1920x1080)")
flagFullscreen = flag.Bool("fullscreen", false, "fullscreen")
flagResizable = flag.Bool("resizable", false, "make the window resizable")
flagWindowPosition = flag.String("windowposition", "", "window position (e.g., 100,200)")
flagTransparent = flag.Bool("transparent", false, "screen transparent")
flagAutoAdjusting = flag.Bool("autoadjusting", false, "make the game screen auto-adjusting")
flagFloating = flag.Bool("floating", false, "make the window floating")
flagMaximize = flag.Bool("maximize", false, "maximize the window")
flagVsync = flag.Bool("vsync", true, "enable vsync")
flagAutoRestore = flag.Bool("autorestore", false, "restore the window automatically")
flagInitFocused = flag.Bool("initfocused", true, "whether the window is focused on start")
flagMinWindowSize = flag.String("minwindowsize", "", "minimum window size (e.g., 100x200)")
flagMaxWindowSize = flag.String("maxwindowsize", "", "maximium window size (e.g., 1920x1080)")
flagGraphicsLibrary = flag.String("graphicslibrary", "", "graphics library (e.g. opengl)")
)
func init() {
@ -93,6 +95,8 @@ type game struct {
width float64
height float64
transparent bool
logOnce sync.Once
}
func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) {
@ -111,6 +115,12 @@ func (g *game) LayoutF(outsideWidth, outsideHeight float64) (float64, float64) {
}
func (g *game) Update() error {
g.logOnce.Do(func() {
var debug ebiten.DebugInfo
ebiten.ReadDebugInfo(&debug)
fmt.Printf("Graphics library: %s\n", debug.GraphicsLibrary)
})
var (
screenWidth float64
screenHeight float64
@ -441,12 +451,26 @@ func main() {
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
}
op := &ebiten.RunGameOptions{}
switch *flagGraphicsLibrary {
case "":
op.GraphicsLibrary = ebiten.GraphicsLibraryAuto
case "opengl":
op.GraphicsLibrary = ebiten.GraphicsLibraryOpenGL
case "directx":
op.GraphicsLibrary = ebiten.GraphicsLibraryDirectX
case "metal":
op.GraphicsLibrary = ebiten.GraphicsLibraryMetal
default:
log.Fatalf("unexpected graphics library: %s", *flagGraphicsLibrary)
}
const title = "Window Size (Ebitengine Demo)"
ww := int(float64(g.width) * initScreenScale)
wh := int(float64(g.height) * initScreenScale)
ebiten.SetWindowSize(ww, wh)
ebiten.SetWindowTitle(title)
if err := ebiten.RunGame(g); err != nil {
if err := ebiten.RunGameWithOptions(g, op); err != nil {
log.Fatal(err)
}
}

View File

@ -127,23 +127,33 @@ func (c CompositeMode) blend() Blend {
}
// GraphicsLibrary represets graphics libraries supported by the engine.
type GraphicsLibrary = ui.GraphicsLibrary
type GraphicsLibrary int
const (
GraphicsLibraryAuto GraphicsLibrary = GraphicsLibrary(ui.GraphicsLibraryAuto)
// GraphicsLibraryUnknown represents the state at which graphics library cannot be determined,
// e.g. hasn't loaded yet or failed to initialize.
GraphicsLibraryUnknown GraphicsLibrary = ui.GraphicsLibraryUnknown
GraphicsLibraryUnknown GraphicsLibrary = GraphicsLibrary(ui.GraphicsLibraryUnknown)
// GraphicsLibraryOpenGL represents the graphics library OpenGL.
GraphicsLibraryOpenGL GraphicsLibrary = ui.GraphicsLibraryOpenGL
GraphicsLibraryOpenGL GraphicsLibrary = GraphicsLibrary(ui.GraphicsLibraryOpenGL)
// GraphicsLibraryDirectX represents the graphics library Microsoft DirectX.
GraphicsLibraryDirectX GraphicsLibrary = ui.GraphicsLibraryDirectX
GraphicsLibraryDirectX GraphicsLibrary = GraphicsLibrary(ui.GraphicsLibraryDirectX)
// GraphicsLibraryMetal represents the graphics library Apple's Metal.
GraphicsLibraryMetal GraphicsLibrary = ui.GraphicsLibraryMetal
GraphicsLibraryMetal GraphicsLibrary = GraphicsLibrary(ui.GraphicsLibraryMetal)
)
// String returns a string representing the graphics library.
func (g GraphicsLibrary) String() string {
return ui.GraphicsLibrary(g).String()
}
// Ensures GraphicsLibraryAuto is zero (the default value for RunOptions).
var _ [GraphicsLibraryAuto]int = [0]int{}
// DebugInfo is a struct to store debug info about the graphics.
type DebugInfo struct {
// GraphicsLibrary represents the graphics library currently in use.
@ -152,5 +162,5 @@ type DebugInfo struct {
// ReadDebugInfo writes debug info (e.g. current graphics library) into a provided struct.
func ReadDebugInfo(d *DebugInfo) {
d.GraphicsLibrary = ui.GetGraphicsLibrary()
d.GraphicsLibrary = GraphicsLibrary(ui.GetGraphicsLibrary())
}

View File

@ -543,7 +543,7 @@ type DrawTrianglesShaderOptions struct {
}
// Check the number of images.
var _ [len(DrawTrianglesShaderOptions{}.Images)]struct{} = [graphics.ShaderImageCount]struct{}{}
var _ [len(DrawTrianglesShaderOptions{}.Images) - graphics.ShaderImageCount]struct{} = [0]struct{}{}
// DrawTrianglesShader draws triangles with the specified vertices and their indices with the specified shader.
//

View File

@ -21,6 +21,7 @@ import (
var theGlobalState = globalState{
isScreenClearedEveryFrame_: 1,
graphicsLibrary_: int32(GraphicsLibraryUnknown),
}
// globalState represents a global state in this package.

View File

@ -28,7 +28,7 @@ type graphicsDriverCreator interface {
newMetal() (graphicsdriver.Graphics, error)
}
func newGraphicsDriver(creator graphicsDriverCreator) (graphicsdriver.Graphics, error) {
func newGraphicsDriver(creator graphicsDriverCreator, graphicsLibrary GraphicsLibrary) (graphicsdriver.Graphics, error) {
envName := "EBITENGINE_GRAPHICS_LIBRARY"
env := os.Getenv(envName)
if env == "" {
@ -39,6 +39,20 @@ func newGraphicsDriver(creator graphicsDriverCreator) (graphicsdriver.Graphics,
switch env {
case "", "auto":
// Use the specified graphics library.
// Otherwise, prefer the environment variable.
case "opengl":
graphicsLibrary = GraphicsLibraryOpenGL
case "directx":
graphicsLibrary = GraphicsLibraryDirectX
case "metal":
graphicsLibrary = GraphicsLibraryMetal
default:
return nil, fmt.Errorf("ui: an unsupported graphics library is specified by the environment variable: %s", env)
}
switch graphicsLibrary {
case GraphicsLibraryAuto:
g, lib, err := creator.newAuto()
if err != nil {
return nil, err
@ -48,41 +62,68 @@ func newGraphicsDriver(creator graphicsDriverCreator) (graphicsdriver.Graphics,
}
theGlobalState.setGraphicsLibrary(lib)
return g, nil
case "opengl":
case GraphicsLibraryOpenGL:
g, err := creator.newOpenGL()
if err != nil {
return nil, err
}
if g == nil {
return nil, fmt.Errorf("ui: %s=%s is specified but OpenGL is not available", envName, env)
return nil, fmt.Errorf("ui: %s is specified but OpenGL is not available", graphicsLibrary)
}
theGlobalState.setGraphicsLibrary(GraphicsLibraryOpenGL)
return g, nil
case "directx":
case GraphicsLibraryDirectX:
g, err := creator.newDirectX()
if err != nil {
return nil, err
}
if g == nil {
return nil, fmt.Errorf("ui: %s=%s is specified but DirectX is not available.", envName, env)
return nil, fmt.Errorf("ui: %s is specified but DirectX is not available.", graphicsLibrary)
}
theGlobalState.setGraphicsLibrary(GraphicsLibraryDirectX)
return g, nil
case "metal":
case GraphicsLibraryMetal:
g, err := creator.newMetal()
if err != nil {
return nil, err
}
if g == nil {
return nil, fmt.Errorf("ui: %s=%s is specified but Metal is not available", envName, env)
return nil, fmt.Errorf("ui: %s is specified but Metal is not available", graphicsLibrary)
}
theGlobalState.setGraphicsLibrary(GraphicsLibraryMetal)
return g, nil
default:
return nil, fmt.Errorf("ui: an unsupported graphics library is specified: %s", env)
return nil, fmt.Errorf("ui: an unsupported graphics library is specified: %d", graphicsLibrary)
}
}
func GraphicsDriverForTesting() graphicsdriver.Graphics {
return theUI.graphicsDriver
}
type GraphicsLibrary int
const (
GraphicsLibraryAuto GraphicsLibrary = iota
GraphicsLibraryOpenGL
GraphicsLibraryDirectX
GraphicsLibraryMetal
GraphicsLibraryUnknown
)
func (g GraphicsLibrary) String() string {
switch g {
case GraphicsLibraryAuto:
return "Auto"
case GraphicsLibraryOpenGL:
return "OpenGL"
case GraphicsLibraryDirectX:
return "DirectX"
case GraphicsLibraryMetal:
return "Metal"
case GraphicsLibraryUnknown:
return "Unknown"
default:
return fmt.Sprintf("GraphicsLibrary(%d)", g)
}
}

View File

@ -21,7 +21,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/thread"
)
func (u *userInterfaceImpl) Run(game Game) error {
func (u *userInterfaceImpl) Run(game Game, options *RunOptions) error {
u.context = newContext(game)
// Initialize the main thread first so the thread is available at u.run (#809).
@ -36,7 +36,7 @@ func (u *userInterfaceImpl) Run(game Game) error {
var err error
if u.t.Call(func() {
err = u.init()
err = u.init(options)
}); err != nil {
ch <- err
return

View File

@ -21,7 +21,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/thread"
)
func (u *userInterfaceImpl) Run(game Game) error {
func (u *userInterfaceImpl) Run(game Game, options *RunOptions) error {
u.context = newContext(game)
// Initialize the main thread first so the thread is available at u.run (#809).
@ -30,7 +30,7 @@ func (u *userInterfaceImpl) Run(game Game) error {
u.setRunning(true)
if err := u.init(); err != nil {
if err := u.init(options); err != nil {
return err
}

View File

@ -73,15 +73,6 @@ const (
WindowResizingModeEnabled
)
type GraphicsLibrary int
const (
GraphicsLibraryUnknown GraphicsLibrary = iota
GraphicsLibraryOpenGL
GraphicsLibraryDirectX
GraphicsLibraryMetal
)
type UserInterface struct {
userInterfaceImpl
}
@ -104,3 +95,7 @@ func (u *UserInterface) dumpScreenshot(mipmap *mipmap.Mipmap, name string, black
func (u *UserInterface) dumpImages(dir string) (string, error) {
return atlas.DumpImages(u.graphicsDriver, dir)
}
type RunOptions struct {
GraphicsLibrary GraphicsLibrary
}

View File

@ -888,7 +888,7 @@ event:
u.framebufferSizeCallbackCh = nil
}
func (u *userInterfaceImpl) init() error {
func (u *userInterfaceImpl) init(options *RunOptions) error {
glfw.WindowHint(glfw.AutoIconify, glfw.False)
decorated := glfw.False
@ -906,7 +906,7 @@ func (u *userInterfaceImpl) init() error {
g, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
transparent: transparent,
})
}, options.GraphicsLibrary)
if err != nil {
return err
}

View File

@ -637,7 +637,7 @@ func (u *userInterfaceImpl) forceUpdateOnMinimumFPSMode() {
}()
}
func (u *userInterfaceImpl) Run(game Game) error {
func (u *userInterfaceImpl) Run(game Game, options *RunOptions) error {
if u.initFocused && window.Truthy() {
// Do not focus the canvas when the current document is in an iframe.
// Otherwise, the parent page tries to focus the iframe on every loading, which is annoying (#1373).
@ -649,7 +649,7 @@ func (u *userInterfaceImpl) Run(game Game) error {
u.running = true
g, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
canvas: canvas,
})
}, options.GraphicsLibrary)
if err != nil {
return err
}

View File

@ -235,10 +235,10 @@ func (u *userInterfaceImpl) SetForeground(foreground bool) error {
}
}
func (u *userInterfaceImpl) Run(game Game) error {
func (u *userInterfaceImpl) Run(game Game, options *RunOptions) error {
u.setGBuildSizeCh = make(chan struct{})
go func() {
if err := u.run(game, true); err != nil {
if err := u.run(game, true, options); err != nil {
// As mobile apps never ends, Loop can't return. Just panic here.
panic(err)
}
@ -247,19 +247,19 @@ func (u *userInterfaceImpl) Run(game Game) error {
return nil
}
func RunWithoutMainLoop(game Game) {
theUI.runWithoutMainLoop(game)
func RunWithoutMainLoop(game Game, options *RunOptions) {
theUI.runWithoutMainLoop(game, options)
}
func (u *userInterfaceImpl) runWithoutMainLoop(game Game) {
func (u *userInterfaceImpl) runWithoutMainLoop(game Game, options *RunOptions) {
go func() {
if err := u.run(game, false); err != nil {
if err := u.run(game, false, options); err != nil {
u.errCh <- err
}
}()
}
func (u *userInterfaceImpl) run(game Game, mainloop bool) (err error) {
func (u *userInterfaceImpl) run(game Game, mainloop bool, options *RunOptions) (err error) {
// Convert the panic to a regular error so that Java/Objective-C layer can treat this easily e.g., for
// Crashlytics. A panic is treated as SIGABRT, and there is no way to handle this on Java/Objective-C layer
// unfortunately.
@ -284,7 +284,7 @@ func (u *userInterfaceImpl) run(game Game, mainloop bool) (err error) {
g, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
gomobileContext: mgl,
})
}, options.GraphicsLibrary)
if err != nil {
return err
}

View File

@ -56,9 +56,9 @@ type userInterfaceImpl struct {
input Input
}
func (u *userInterfaceImpl) Run(game Game) error {
func (u *userInterfaceImpl) Run(game Game, options *RunOptions) error {
u.context = newContext(game)
g, err := newGraphicsDriver(&graphicsDriverCreatorImpl{})
g, err := newGraphicsDriver(&graphicsDriverCreatorImpl{}, options.GraphicsLibrary)
if err != nil {
return err
}

View File

@ -49,11 +49,11 @@ func (s *state) run() {
atomic.StoreInt32(&s.running, 1)
}
func SetGame(game ebiten.Game) {
func SetGame(game ebiten.Game, options *ebiten.RunGameOptions) {
if theState.isRunning() {
panic("ebitenmobileview: SetGame cannot be called twice or more")
}
ebiten.RunGameWithoutMainLoop(game)
ebiten.RunGameWithoutMainLoop(game, options)
theState.run()
}

View File

@ -21,6 +21,6 @@ import (
"github.com/hajimehoshi/ebiten/v2/mobile/ebitenmobileview"
)
func setGame(game ebiten.Game) {
ebitenmobileview.SetGame(game)
func setGame(game ebiten.Game, options *ebiten.RunGameOptions) {
ebitenmobileview.SetGame(game, options)
}

View File

@ -20,6 +20,6 @@ import (
"github.com/hajimehoshi/ebiten/v2"
)
func setGame(game ebiten.Game) {
func setGame(game ebiten.Game, options *ebiten.RunGameOptions) {
panic("mobile: setGame is not implemented in this environment")
}

View File

@ -29,5 +29,14 @@ import (
//
// SetGame can be called anytime. Until SetGame is called, the game does not start.
func SetGame(game ebiten.Game) {
setGame(game)
SetGameWithOptions(game, nil)
}
// SetGameWithOptions sets a mobile game with the specified options.
//
// SetGameWithOptions is expected to be called only once.
//
// SetGameWithOptions can be called anytime. Until SetGameWithOptions is called, the game does not start.
func SetGameWithOptions(game ebiten.Game, options *ebiten.RunGameOptions) {
setGame(game, options)
}

57
run.go
View File

@ -224,13 +224,57 @@ var Termination = ui.RegularTermination
//
// The size unit is device-independent pixel.
//
// Don't call RunGame twice or more in one process.
// Don't call RunGame or RunGameWithOptions twice or more in one process.
func RunGame(game Game) error {
return RunGameWithOptions(game, nil)
}
// RungameOptions represents options for RunGameWithOptions.
type RunGameOptions struct {
// GraphicsLibrary is a graphics library Ebitengine will use.
// The default (zero) value is GraphicsLibraryAuto, which lets Ebitengine choose the graphics library.
GraphicsLibrary GraphicsLibrary
}
// RunGameWithOptions starts the main loop and runs the game with the specified options.
// game's Update function is called every tick to update the game logic.
// game's Draw function is called every frame to draw the screen.
// game's Layout function is called when necessary, and you can specify the logical screen size by the function.
//
// options can be nil. In this case, the default options are used.
//
// If game implements FinalScreenDrawer, its DrawFinalScreen is called after Draw.
// The argument screen represents the final screen. The argument offscreen is an offscreen modified at Draw.
// If game does not implement FinalScreenDrawer, the dafault rendering for the final screen is used.
//
// game's functions are called on the same goroutine.
//
// On browsers, it is strongly recommended to use iframe if you embed an Ebitengine application in your website.
//
// RunGameWithOptions must be called on the main thread.
// Note that Ebitengine bounds the main goroutine to the main OS thread by runtime.LockOSThread.
//
// Ebitengine tries to call game's Update function 60 times a second by default. In other words,
// TPS (ticks per second) is 60 by default.
// This is not related to framerate (display's refresh rate).
//
// RunGameWithOptions returns error when 1) an error happens in the underlying graphics driver, 2) an audio error happens
// or 3) Update returns an error. In the case of 3), RunGameWithOptions returns the same error so far, but it is recommended to
// use errors.Is when you check the returned error is the error you want, rather than comparing the values
// with == or != directly.
//
// If you want to terminate a game on desktops, it is recommended to return Termination at Update, which will halt
// execution without returning an error value from RunGameWithOptions.
//
// The size unit is device-independent pixel.
//
// Don't call RunGame or RunGameWithOptions twice or more in one process.
func RunGameWithOptions(game Game, options *RunGameOptions) error {
defer atomic.StoreInt32(&isRunGameEnded_, 1)
initializeWindowPositionIfNeeded(WindowSize())
g := newGameForUI(game)
if err := ui.Get().Run(g); err != nil {
if err := ui.Get().Run(g, toUIRunOptions(options)); err != nil {
if errors.Is(err, Termination) {
return nil
}
@ -557,3 +601,12 @@ func SetScreenTransparent(transparent bool) {
func SetInitFocused(focused bool) {
ui.Get().SetInitFocused(focused)
}
func toUIRunOptions(options *RunGameOptions) *ui.RunOptions {
if options == nil {
return &ui.RunOptions{}
}
return &ui.RunOptions{
GraphicsLibrary: ui.GraphicsLibrary(options.GraphicsLibrary),
}
}

View File

@ -27,6 +27,6 @@ import (
// Instead, functions in github.com/hajimehoshi/ebiten/v2/mobile package calls this.
//
// TODO: Remove this. In order to remove this, the gameForUI should be in another package.
func RunGameWithoutMainLoop(game Game) {
ui.RunWithoutMainLoop(newGameForUI(game))
func RunGameWithoutMainLoop(game Game, options *RunGameOptions) {
ui.RunWithoutMainLoop(newGameForUI(game), toUIRunOptions(options))
}