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 (
"sync/atomic"
"github.com/hajimehoshi/ebiten/v2/internal/clock"
)
const DefaultTPS = 60
type Context interface {
UpdateFrame() error
ForceUpdateFrame() error
UpdateFrame(updateCount int) error
Layout(outsideWidth, outsideHeight float64)
// AdjustPosition can be called from a different goroutine from Update's or Layout's.
@ -30,21 +33,36 @@ type Context interface {
type contextImpl struct {
context Context
err atomic.Value
outsideWidth float64
outsideHeight float64
}
func newContextImpl(context Context) *contextImpl {
return &contextImpl{
context: context,
}
}
func (c *contextImpl) updateFrame() error {
if err, ok := c.err.Load().(error); ok && err != nil {
if err := theGlobalState.err(); err != nil {
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 {
if err, ok := c.err.Load().(error); ok && err != nil {
if err := theGlobalState.err(); err != nil {
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) {
@ -54,6 +72,8 @@ func (c *contextImpl) layout(outsideWidth, outsideHeight float64) {
return
}
c.outsideWidth = outsideWidth
c.outsideHeight = 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)
}
func (c *contextImpl) setError(err error) {
c.err.Store(err)
var theGlobalState = globalState{
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) {
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"
)
func (u *UserInterface) Run(uicontext Context) error {
u.context = &contextImpl{
context: uicontext,
}
func (u *UserInterface) Run(context Context) error {
u.context = newContextImpl(context)
// Initialize the main thread first so the thread is available at u.run (#809).
u.t = thread.NewOSThread()

View File

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

View File

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

View File

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

View File

@ -65,7 +65,7 @@ type UserInterface struct {
origPosX int
origPosY int
runnableOnUnfocused bool
fpsMode FPSMode
fpsMode FPSModeType
iconImages []image.Image
cursorShape CursorShape
windowClosingHandled bool
@ -536,7 +536,7 @@ func (u *UserInterface) IsRunnableOnUnfocused() bool {
return u.isRunnableOnUnfocused()
}
func (u *UserInterface) SetFPSMode(mode FPSMode) {
func (u *UserInterface) SetFPSMode(mode FPSModeType) {
if !u.isRunning() {
u.m.Lock()
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() {
if !u.isRunning() {
return
@ -943,7 +929,7 @@ func (u *UserInterface) updateSize() (float64, float64) {
}
// 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
u.fpsMode = fpsMode
u.fpsModeInited = true

View File

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

View File

@ -113,7 +113,7 @@ type UserInterface struct {
input Input
fpsMode FPSMode
fpsMode FPSModeType
renderRequester RenderRequester
t *thread.OSThread
@ -273,9 +273,7 @@ func (u *UserInterface) run(context Context, mainloop bool) (err error) {
u.sizeChanged = true
u.m.Unlock()
u.context = &contextImpl{
context: context,
}
u.context = newContextImpl(context)
if mainloop {
// 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
}
func (u *UserInterface) FPSMode() FPSMode {
return u.fpsMode
}
func (u *UserInterface) SetFPSMode(mode FPSMode) {
func (u *UserInterface) SetFPSMode(mode FPSModeType) {
u.fpsMode = mode
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.
const DefaultTPS = 60
const DefaultTPS = ui.DefaultTPS
// CurrentFPS returns the current number of FPS (frames per second), that represents
// how many swapping buffer happens per second.
@ -79,7 +79,6 @@ func CurrentFPS() float64 {
var (
isScreenClearedEveryFrame = int32(1)
isRunGameEnded_ = int32(0)
currentMaxTPS = int32(DefaultTPS)
)
// 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.
func IsVsyncEnabled() bool {
return ui.Get().FPSMode() == ui.FPSModeVsyncOn
return ui.FPSMode() == ui.FPSModeVsyncOn
}
// SetVsyncEnabled sets a boolean value indicating whether
@ -333,14 +332,14 @@ func IsVsyncEnabled() bool {
// Deprecated: as of v2.2. Use SetFPSMode instead.
func SetVsyncEnabled(enabled bool) {
if enabled {
ui.Get().SetFPSMode(ui.FPSModeVsyncOn)
ui.SetFPSMode(ui.FPSModeVsyncOn)
} else {
ui.Get().SetFPSMode(ui.FPSModeVsyncOffMaximum)
ui.SetFPSMode(ui.FPSModeVsyncOffMaximum)
}
}
// FPSModeType is a type of FPS modes.
type FPSModeType = ui.FPSMode
type FPSModeType = ui.FPSModeType
const (
// FPSModeVsyncOn indicates that the game tries to sync the display's refresh rate.
@ -371,7 +370,7 @@ const (
//
// FPSMode is concurrent-safe.
func FPSMode() FPSModeType {
return ui.Get().FPSMode()
return ui.FPSMode()
}
// SetFPSMode sets the FPS mode.
@ -379,7 +378,7 @@ func FPSMode() FPSModeType {
//
// SetFPSMode is concurrent-safe.
func SetFPSMode(mode FPSModeType) {
ui.Get().SetFPSMode(mode)
ui.SetFPSMode(mode)
}
// ScheduleFrame schedules a next frame when the current FPS mode is FPSModeVsyncOffMinimum.
@ -393,10 +392,7 @@ func ScheduleFrame() {
//
// MaxTPS is concurrent-safe.
func MaxTPS() int {
if FPSMode() == FPSModeVsyncOffMinimum {
return SyncWithFPS
}
return int(atomic.LoadInt32(&currentMaxTPS))
return ui.MaxTPS()
}
// CurrentTPS returns the current TPS (ticks per second),
@ -426,10 +422,7 @@ const UncappedTPS = SyncWithFPS
//
// SetMaxTPS is concurrent-safe.
func SetMaxTPS(tps int) {
if tps < 0 && tps != SyncWithFPS {
panic("ebiten: tps must be >= 0 or SyncWithFPS")
}
atomic.StoreInt32(&currentMaxTPS, int32(tps))
ui.SetMaxTPS(tps)
}
// IsScreenTransparent reports whether the window is transparent.

View File

@ -20,7 +20,6 @@ import (
"sync"
"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/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
@ -123,20 +122,7 @@ func (c *uiContext) offsets(deviceScaleFactor float64) (float64, float64) {
return x, y
}
func (c *uiContext) UpdateFrame() 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 {
func (c *uiContext) UpdateFrame(updateCount int) error {
debug.Logf("----\n")
if err := buffered.BeginFrame(); err != nil {