internal/ui: refactoring: move some logics to internal/ui

This commit is contained in:
Hajime Hoshi 2022-02-13 17:23:52 +09:00
parent b282b1805b
commit 2609d73a1a
10 changed files with 116 additions and 94 deletions

View File

@ -16,11 +16,14 @@ package ui
import ( import (
"sync/atomic" "sync/atomic"
"github.com/hajimehoshi/ebiten/v2/internal/clock"
) )
const DefaultTPS = 60
type Context interface { type Context interface {
UpdateFrame() error UpdateFrame(updateCount int) error
ForceUpdateFrame() error
Layout(outsideWidth, outsideHeight float64) Layout(outsideWidth, outsideHeight float64)
// AdjustPosition can be called from a different goroutine from Update's or Layout's. // AdjustPosition can be called from a different goroutine from Update's or Layout's.
@ -30,21 +33,36 @@ type Context interface {
type contextImpl struct { type contextImpl struct {
context Context context Context
err atomic.Value outsideWidth float64
outsideHeight float64
}
func newContextImpl(context Context) *contextImpl {
return &contextImpl{
context: context,
}
} }
func (c *contextImpl) updateFrame() error { func (c *contextImpl) updateFrame() error {
if err, ok := c.err.Load().(error); ok && err != nil { if err := theGlobalState.err(); err != nil {
return err return err
} }
return c.context.UpdateFrame()
// TODO: If updateCount is 0 and vsync is disabled, swapping buffers can be skipped.
return c.context.UpdateFrame(clock.Update(theGlobalState.maxTPS()))
} }
func (c *contextImpl) forceUpdateFrame() error { func (c *contextImpl) forceUpdateFrame() error {
if err, ok := c.err.Load().(error); ok && err != nil { if err := theGlobalState.err(); err != nil {
return err return err
} }
return c.context.ForceUpdateFrame()
// ForceUpdate can be invoked even if the context is not initialized yet (#1591).
if c.outsideWidth == 0 || c.outsideHeight == 0 {
return nil
}
return c.context.UpdateFrame(1)
} }
func (c *contextImpl) layout(outsideWidth, outsideHeight float64) { func (c *contextImpl) layout(outsideWidth, outsideHeight float64) {
@ -54,6 +72,8 @@ func (c *contextImpl) layout(outsideWidth, outsideHeight float64) {
return return
} }
c.outsideWidth = outsideWidth
c.outsideHeight = outsideHeight
c.context.Layout(outsideWidth, outsideHeight) c.context.Layout(outsideWidth, outsideHeight)
} }
@ -61,10 +81,69 @@ func (c *contextImpl) adjustPosition(x, y float64, deviceScaleFactor float64) (f
return c.context.AdjustPosition(x, y, deviceScaleFactor) return c.context.AdjustPosition(x, y, deviceScaleFactor)
} }
func (c *contextImpl) setError(err error) { var theGlobalState = globalState{
c.err.Store(err) currentMaxTPS: DefaultTPS,
}
// globalState represents a global state in this package.
// This is available even before the game loop starts.
type globalState struct {
currentErr atomic.Value
currentFPSMode int32
currentMaxTPS int32
}
func (g *globalState) err() error {
err, ok := g.currentErr.Load().(error)
if !ok {
return nil
}
return err
}
func (g *globalState) setError(err error) {
g.currentErr.Store(err)
}
func (g *globalState) fpsMode() FPSModeType {
return FPSModeType(atomic.LoadInt32(&g.currentFPSMode))
}
func (g *globalState) setFPSMode(fpsMode FPSModeType) {
atomic.StoreInt32(&g.currentFPSMode, int32(fpsMode))
}
func (g *globalState) maxTPS() int {
if g.fpsMode() == FPSModeVsyncOffMinimum {
return clock.SyncWithFPS
}
return int(atomic.LoadInt32(&g.currentMaxTPS))
}
func (g *globalState) setMaxTPS(tps int) {
if tps < 0 && tps != clock.SyncWithFPS {
panic("ebiten: tps must be >= 0 or SyncWithFPS")
}
atomic.StoreInt32(&g.currentMaxTPS, int32(tps))
} }
func SetError(err error) { func SetError(err error) {
Get().context.setError(err) theGlobalState.setError(err)
}
func FPSMode() FPSModeType {
return theGlobalState.fpsMode()
}
func SetFPSMode(fpsMode FPSModeType) {
theGlobalState.setFPSMode(fpsMode)
Get().SetFPSMode(fpsMode)
}
func MaxTPS() int {
return theGlobalState.maxTPS()
}
func SetMaxTPS(tps int) {
theGlobalState.setMaxTPS(tps)
} }

View File

@ -22,10 +22,8 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/thread" "github.com/hajimehoshi/ebiten/v2/internal/thread"
) )
func (u *UserInterface) Run(uicontext Context) error { func (u *UserInterface) Run(context Context) error {
u.context = &contextImpl{ u.context = newContextImpl(context)
context: uicontext,
}
// Initialize the main thread first so the thread is available at u.run (#809). // Initialize the main thread first so the thread is available at u.run (#809).
u.t = thread.NewOSThread() u.t = thread.NewOSThread()

View File

@ -22,10 +22,8 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/thread" "github.com/hajimehoshi/ebiten/v2/internal/thread"
) )
func (u *UserInterface) Run(uicontext Context) error { func (u *UserInterface) Run(context Context) error {
u.context = &contextImpl{ u.context = newContextImpl(context)
context: uicontext,
}
// Initialize the main thread first so the thread is available at u.run (#809). // Initialize the main thread first so the thread is available at u.run (#809).
u.t = thread.NewNoopThread() u.t = thread.NewNoopThread()

View File

@ -35,10 +35,10 @@ type TouchID int
// the game loop should be terminated as soon as possible. // the game loop should be terminated as soon as possible.
var RegularTermination = errors.New("regular termination") var RegularTermination = errors.New("regular termination")
type FPSMode int type FPSModeType int
const ( const (
FPSModeVsyncOn FPSMode = iota FPSModeVsyncOn FPSModeType = iota
FPSModeVsyncOffMaximum FPSModeVsyncOffMaximum
FPSModeVsyncOffMinimum FPSModeVsyncOffMinimum
) )

View File

@ -41,9 +41,7 @@ func Get() *UserInterface {
} }
func (u *UserInterface) Run(context Context) error { func (u *UserInterface) Run(context Context) error {
u.context = &contextImpl{ u.context = newContextImpl(context)
context: context,
}
cbackend.InitializeGame() cbackend.InitializeGame()
for { for {
w, h := cbackend.ScreenSize() w, h := cbackend.ScreenSize()
@ -107,11 +105,7 @@ func (*UserInterface) IsRunnableOnUnfocused() bool {
func (*UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) { func (*UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) {
} }
func (*UserInterface) FPSMode() FPSMode { func (*UserInterface) SetFPSMode(mode FPSModeType) {
return FPSModeVsyncOn
}
func (*UserInterface) SetFPSMode(mode FPSMode) {
} }
func (*UserInterface) ScheduleFrame() { func (*UserInterface) ScheduleFrame() {

View File

@ -65,7 +65,7 @@ type UserInterface struct {
origPosX int origPosX int
origPosY int origPosY int
runnableOnUnfocused bool runnableOnUnfocused bool
fpsMode FPSMode fpsMode FPSModeType
iconImages []image.Image iconImages []image.Image
cursorShape CursorShape cursorShape CursorShape
windowClosingHandled bool windowClosingHandled bool
@ -536,7 +536,7 @@ func (u *UserInterface) IsRunnableOnUnfocused() bool {
return u.isRunnableOnUnfocused() return u.isRunnableOnUnfocused()
} }
func (u *UserInterface) SetFPSMode(mode FPSMode) { func (u *UserInterface) SetFPSMode(mode FPSModeType) {
if !u.isRunning() { if !u.isRunning() {
u.m.Lock() u.m.Lock()
u.fpsMode = mode u.fpsMode = mode
@ -553,20 +553,6 @@ func (u *UserInterface) SetFPSMode(mode FPSMode) {
}) })
} }
func (u *UserInterface) FPSMode() FPSMode {
if !u.isRunning() {
u.m.Lock()
m := u.fpsMode
u.m.Unlock()
return m
}
var v FPSMode
u.t.Call(func() {
v = u.fpsMode
})
return v
}
func (u *UserInterface) ScheduleFrame() { func (u *UserInterface) ScheduleFrame() {
if !u.isRunning() { if !u.isRunning() {
return return
@ -943,7 +929,7 @@ func (u *UserInterface) updateSize() (float64, float64) {
} }
// setFPSMode must be called from the main thread. // setFPSMode must be called from the main thread.
func (u *UserInterface) setFPSMode(fpsMode FPSMode) { func (u *UserInterface) setFPSMode(fpsMode FPSModeType) {
needUpdate := u.fpsMode != fpsMode || !u.fpsModeInited needUpdate := u.fpsMode != fpsMode || !u.fpsModeInited
u.fpsMode = fpsMode u.fpsMode = fpsMode
u.fpsModeInited = true u.fpsModeInited = true

View File

@ -48,7 +48,7 @@ func driverCursorShapeToCSSCursor(cursor CursorShape) string {
type UserInterface struct { type UserInterface struct {
runnableOnUnfocused bool runnableOnUnfocused bool
fpsMode FPSMode fpsMode FPSModeType
renderingScheduled bool renderingScheduled bool
running bool running bool
initFocused bool initFocused bool
@ -152,14 +152,10 @@ func (u *UserInterface) IsRunnableOnUnfocused() bool {
return u.runnableOnUnfocused return u.runnableOnUnfocused
} }
func (u *UserInterface) SetFPSMode(mode FPSMode) { func (u *UserInterface) SetFPSMode(mode FPSModeType) {
u.fpsMode = mode u.fpsMode = mode
} }
func (u *UserInterface) FPSMode() FPSMode {
return u.fpsMode
}
func (u *UserInterface) ScheduleFrame() { func (u *UserInterface) ScheduleFrame() {
u.renderingScheduled = true u.renderingScheduled = true
} }
@ -319,9 +315,7 @@ func (u *UserInterface) needsUpdate() bool {
} }
func (u *UserInterface) loop(context Context) <-chan error { func (u *UserInterface) loop(context Context) <-chan error {
u.context = &contextImpl{ u.context = newContextImpl(context)
context: context,
}
errCh := make(chan error, 1) errCh := make(chan error, 1)
reqStopAudioCh := make(chan struct{}) reqStopAudioCh := make(chan struct{})

View File

@ -113,7 +113,7 @@ type UserInterface struct {
input Input input Input
fpsMode FPSMode fpsMode FPSModeType
renderRequester RenderRequester renderRequester RenderRequester
t *thread.OSThread t *thread.OSThread
@ -273,9 +273,7 @@ func (u *UserInterface) run(context Context, mainloop bool) (err error) {
u.sizeChanged = true u.sizeChanged = true
u.m.Unlock() u.m.Unlock()
u.context = &contextImpl{ u.context = newContextImpl(context)
context: context,
}
if mainloop { if mainloop {
// When mainloop is true, gomobile-build is used. In this case, GL functions must be called via // When mainloop is true, gomobile-build is used. In this case, GL functions must be called via
@ -410,11 +408,7 @@ func (u *UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) {
// Do nothing // Do nothing
} }
func (u *UserInterface) FPSMode() FPSMode { func (u *UserInterface) SetFPSMode(mode FPSModeType) {
return u.fpsMode
}
func (u *UserInterface) SetFPSMode(mode FPSMode) {
u.fpsMode = mode u.fpsMode = mode
u.updateExplicitRenderingModeIfNeeded() u.updateExplicitRenderingModeIfNeeded()
} }

25
run.go
View File

@ -61,7 +61,7 @@ type Game interface {
} }
// DefaultTPS represents a default ticks per second, that represents how many times game updating happens in a second. // DefaultTPS represents a default ticks per second, that represents how many times game updating happens in a second.
const DefaultTPS = 60 const DefaultTPS = ui.DefaultTPS
// CurrentFPS returns the current number of FPS (frames per second), that represents // CurrentFPS returns the current number of FPS (frames per second), that represents
// how many swapping buffer happens per second. // how many swapping buffer happens per second.
@ -79,7 +79,6 @@ func CurrentFPS() float64 {
var ( var (
isScreenClearedEveryFrame = int32(1) isScreenClearedEveryFrame = int32(1)
isRunGameEnded_ = int32(0) isRunGameEnded_ = int32(0)
currentMaxTPS = int32(DefaultTPS)
) )
// SetScreenClearedEveryFrame enables or disables the clearing of the screen at the beginning of each frame. // SetScreenClearedEveryFrame enables or disables the clearing of the screen at the beginning of each frame.
@ -324,7 +323,7 @@ func DeviceScaleFactor() float64 {
// //
// Deprecated: as of v2.2. Use FPSMode instead. // Deprecated: as of v2.2. Use FPSMode instead.
func IsVsyncEnabled() bool { func IsVsyncEnabled() bool {
return ui.Get().FPSMode() == ui.FPSModeVsyncOn return ui.FPSMode() == ui.FPSModeVsyncOn
} }
// SetVsyncEnabled sets a boolean value indicating whether // SetVsyncEnabled sets a boolean value indicating whether
@ -333,14 +332,14 @@ func IsVsyncEnabled() bool {
// Deprecated: as of v2.2. Use SetFPSMode instead. // Deprecated: as of v2.2. Use SetFPSMode instead.
func SetVsyncEnabled(enabled bool) { func SetVsyncEnabled(enabled bool) {
if enabled { if enabled {
ui.Get().SetFPSMode(ui.FPSModeVsyncOn) ui.SetFPSMode(ui.FPSModeVsyncOn)
} else { } else {
ui.Get().SetFPSMode(ui.FPSModeVsyncOffMaximum) ui.SetFPSMode(ui.FPSModeVsyncOffMaximum)
} }
} }
// FPSModeType is a type of FPS modes. // FPSModeType is a type of FPS modes.
type FPSModeType = ui.FPSMode type FPSModeType = ui.FPSModeType
const ( const (
// FPSModeVsyncOn indicates that the game tries to sync the display's refresh rate. // FPSModeVsyncOn indicates that the game tries to sync the display's refresh rate.
@ -371,7 +370,7 @@ const (
// //
// FPSMode is concurrent-safe. // FPSMode is concurrent-safe.
func FPSMode() FPSModeType { func FPSMode() FPSModeType {
return ui.Get().FPSMode() return ui.FPSMode()
} }
// SetFPSMode sets the FPS mode. // SetFPSMode sets the FPS mode.
@ -379,7 +378,7 @@ func FPSMode() FPSModeType {
// //
// SetFPSMode is concurrent-safe. // SetFPSMode is concurrent-safe.
func SetFPSMode(mode FPSModeType) { func SetFPSMode(mode FPSModeType) {
ui.Get().SetFPSMode(mode) ui.SetFPSMode(mode)
} }
// ScheduleFrame schedules a next frame when the current FPS mode is FPSModeVsyncOffMinimum. // ScheduleFrame schedules a next frame when the current FPS mode is FPSModeVsyncOffMinimum.
@ -393,10 +392,7 @@ func ScheduleFrame() {
// //
// MaxTPS is concurrent-safe. // MaxTPS is concurrent-safe.
func MaxTPS() int { func MaxTPS() int {
if FPSMode() == FPSModeVsyncOffMinimum { return ui.MaxTPS()
return SyncWithFPS
}
return int(atomic.LoadInt32(&currentMaxTPS))
} }
// CurrentTPS returns the current TPS (ticks per second), // CurrentTPS returns the current TPS (ticks per second),
@ -426,10 +422,7 @@ const UncappedTPS = SyncWithFPS
// //
// SetMaxTPS is concurrent-safe. // SetMaxTPS is concurrent-safe.
func SetMaxTPS(tps int) { func SetMaxTPS(tps int) {
if tps < 0 && tps != SyncWithFPS { ui.SetMaxTPS(tps)
panic("ebiten: tps must be >= 0 or SyncWithFPS")
}
atomic.StoreInt32(&currentMaxTPS, int32(tps))
} }
// IsScreenTransparent reports whether the window is transparent. // IsScreenTransparent reports whether the window is transparent.

View File

@ -20,7 +20,6 @@ import (
"sync" "sync"
"github.com/hajimehoshi/ebiten/v2/internal/buffered" "github.com/hajimehoshi/ebiten/v2/internal/buffered"
"github.com/hajimehoshi/ebiten/v2/internal/clock"
"github.com/hajimehoshi/ebiten/v2/internal/debug" "github.com/hajimehoshi/ebiten/v2/internal/debug"
"github.com/hajimehoshi/ebiten/v2/internal/graphics" "github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
@ -123,20 +122,7 @@ func (c *uiContext) offsets(deviceScaleFactor float64) (float64, float64) {
return x, y return x, y
} }
func (c *uiContext) UpdateFrame() error { func (c *uiContext) UpdateFrame(updateCount int) error {
// TODO: If updateCount is 0 and vsync is disabled, swapping buffers can be skipped.
return c.updateFrame(clock.Update(MaxTPS()))
}
func (c *uiContext) ForceUpdateFrame() error {
// ForceUpdate can be invoked even if uiContext it not initialized yet (#1591).
if c.outsideWidth == 0 || c.outsideHeight == 0 {
return nil
}
return c.updateFrame(1)
}
func (c *uiContext) updateFrame(updateCount int) error {
debug.Logf("----\n") debug.Logf("----\n")
if err := buffered.BeginFrame(); err != nil { if err := buffered.BeginFrame(); err != nil {