ebiten: Add FPSModeType, FPSMode, SetFPSMode, and ScheduleFrame

This change adds these APIs:

  * type FPSModeType
  * func FPSMode
  * func SetFPSMode
  * func ScheduleFrame

and deprecates these APIs:

  * func SetVsyncEnabled
  * func IsVsyncEnabled

Closes #1556
This commit is contained in:
Hajime Hoshi 2021-07-23 01:07:28 +09:00
parent cf8aa0a7b2
commit 1706d9436a
12 changed files with 245 additions and 41 deletions

View File

@ -164,7 +164,7 @@ const objcM = `// Code generated by ebitenmobile. DO NOT EDIT.
#import "Ebitenmobileview.objc.h" #import "Ebitenmobileview.objc.h"
@interface {{.PrefixUpper}}EbitenViewController : UIViewController @interface {{.PrefixUpper}}EbitenViewController : UIViewController<EbitenmobileviewRenderRequester>
@end @end
@implementation {{.PrefixUpper}}EbitenViewController { @implementation {{.PrefixUpper}}EbitenViewController {
@ -173,6 +173,8 @@ const objcM = `// Code generated by ebitenmobile. DO NOT EDIT.
bool started_; bool started_;
bool active_; bool active_;
bool error_; bool error_;
CADisplayLink* displayLink_;
bool explicitRendering_;
} }
- (UIView*)metalView { - (UIView*)metalView {
@ -214,8 +216,9 @@ const objcM = `// Code generated by ebitenmobile. DO NOT EDIT.
[EAGLContext setCurrentContext:context]; [EAGLContext setCurrentContext:context];
#endif #endif
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)]; displayLink_ = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [displayLink_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
EbitenmobileviewSetRenderRequester(self);
} }
- (void)viewWillLayoutSubviews { - (void)viewWillLayoutSubviews {
@ -251,6 +254,10 @@ const objcM = `// Code generated by ebitenmobile. DO NOT EDIT.
#else #else
[[self glkView] setNeedsDisplay]; [[self glkView] setNeedsDisplay];
#endif #endif
if (explicitRendering_) {
[displayLink_ setPaused:YES];
}
} }
} }
@ -336,6 +343,25 @@ const objcM = `// Code generated by ebitenmobile. DO NOT EDIT.
} }
} }
- (void)setExplicitRenderingMode:(BOOL)explicitRendering {
@synchronized(self) {
explicitRendering_ = explicitRendering;
if (explicitRendering_) {
[displayLink_ setPaused:YES];
}
}
}
- (void)requestRenderIfNeeded {
@synchronized(self) {
if (explicitRendering_) {
// Resume the callback temporarily.
// This is paused again soon in drawFrame.
[displayLink_ setPaused:NO];
}
}
}
@end @end
` `
@ -745,9 +771,10 @@ import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10; import javax.microedition.khronos.opengles.GL10;
import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview; import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;
import {{.JavaPkg}}.ebitenmobileview.RenderRequester;
import {{.JavaPkg}}.{{.PrefixLower}}.EbitenView; import {{.JavaPkg}}.{{.PrefixLower}}.EbitenView;
class EbitenSurfaceView extends GLSurfaceView { class EbitenSurfaceView extends GLSurfaceView implements RenderRequester {
private class EbitenRenderer implements GLSurfaceView.Renderer { private class EbitenRenderer implements GLSurfaceView.Renderer {
@ -795,10 +822,27 @@ class EbitenSurfaceView extends GLSurfaceView {
setEGLContextClientVersion(2); setEGLContextClientVersion(2);
setEGLConfigChooser(8, 8, 8, 8, 0, 0); setEGLConfigChooser(8, 8, 8, 8, 0, 0);
setRenderer(new EbitenRenderer()); setRenderer(new EbitenRenderer());
Ebitenmobileview.setRenderRequester(this);
} }
private void onErrorOnGameUpdate(Exception e) { private void onErrorOnGameUpdate(Exception e) {
((EbitenView)getParent()).onErrorOnGameUpdate(e); ((EbitenView)getParent()).onErrorOnGameUpdate(e);
} }
@Override
public synchronized void setExplicitRenderingMode(boolean explictRendering) {
if (explictRendering) {
setRenderMode(RENDERMODE_WHEN_DIRTY);
} else {
setRenderMode(RENDERMODE_CONTINUOUSLY);
}
}
@Override
public synchronized void requestRenderIfNeeded() {
if (getRenderMode() == RENDERMODE_WHEN_DIRTY) {
requestRender();
}
}
} }
` `

File diff suppressed because one or more lines are too long

View File

@ -124,7 +124,7 @@ func (g *game) Update() error {
fullscreen := ebiten.IsFullscreen() fullscreen := ebiten.IsFullscreen()
runnableOnUnfocused := ebiten.IsRunnableOnUnfocused() runnableOnUnfocused := ebiten.IsRunnableOnUnfocused()
cursorMode := ebiten.CursorMode() cursorMode := ebiten.CursorMode()
vsyncEnabled := ebiten.IsVsyncEnabled() fpsMode := ebiten.FPSMode()
tps := ebiten.MaxTPS() tps := ebiten.MaxTPS()
decorated := ebiten.IsWindowDecorated() decorated := ebiten.IsWindowDecorated()
positionX, positionY := ebiten.WindowPosition() positionX, positionY := ebiten.WindowPosition()
@ -205,7 +205,14 @@ func (g *game) Update() error {
} }
} }
if inpututil.IsKeyJustPressed(ebiten.KeyV) { if inpututil.IsKeyJustPressed(ebiten.KeyV) {
vsyncEnabled = !vsyncEnabled switch fpsMode {
case ebiten.FPSModeVsyncOn:
fpsMode = ebiten.FPSModeVsyncOffMaximum
case ebiten.FPSModeVsyncOffMaximum:
fpsMode = ebiten.FPSModeVsyncOffMinimum
case ebiten.FPSModeVsyncOffMinimum:
fpsMode = ebiten.FPSModeVsyncOn
}
} }
if inpututil.IsKeyJustPressed(ebiten.KeyT) { if inpututil.IsKeyJustPressed(ebiten.KeyT) {
switch tps { switch tps {
@ -249,10 +256,10 @@ func (g *game) Update() error {
ebiten.SetRunnableOnUnfocused(runnableOnUnfocused) ebiten.SetRunnableOnUnfocused(runnableOnUnfocused)
ebiten.SetCursorMode(cursorMode) ebiten.SetCursorMode(cursorMode)
// Set vsync enabled only when this is needed. // Set FPS mode enabled only when this is needed.
// This makes a bug around vsync initialization more explicit (#1364). // This makes a bug around FPS mode initialization more explicit (#1364).
if vsyncEnabled != ebiten.IsVsyncEnabled() { if fpsMode != ebiten.FPSMode() {
ebiten.SetVsyncEnabled(vsyncEnabled) ebiten.SetFPSMode(fpsMode)
} }
ebiten.SetMaxTPS(tps) ebiten.SetMaxTPS(tps)
ebiten.SetWindowDecorated(decorated) ebiten.SetWindowDecorated(decorated)
@ -323,7 +330,7 @@ func (g *game) Draw(screen *ebiten.Image) {
[U] Switch the runnable-on-unfocused state [U] Switch the runnable-on-unfocused state
[C] Switch the cursor mode (visible, hidden, or captured) [C] Switch the cursor mode (visible, hidden, or captured)
[I] Change the window icon (only for desktops) [I] Change the window icon (only for desktops)
[V] Switch vsync [V] Switch the FPS mode
[T] Switch TPS (ticks per second) [T] Switch TPS (ticks per second)
[D] Switch the window decoration (only for desktops) [D] Switch the window decoration (only for desktops)
[L] Switch the window floating state (only for desktops) [L] Switch the window floating state (only for desktops)
@ -403,7 +410,9 @@ func main() {
ebiten.SetWindowResizable(true) ebiten.SetWindowResizable(true)
ebiten.MaximizeWindow() ebiten.MaximizeWindow()
} }
ebiten.SetVsyncEnabled(*flagVsync) if !*flagVsync {
ebiten.SetFPSMode(ebiten.FPSModeVsyncOffMaximum)
}
if *flagAutoAdjusting { if *flagAutoAdjusting {
ebiten.SetWindowResizable(true) ebiten.SetWindowResizable(true)
} }

View File

@ -56,6 +56,7 @@ type UI interface {
FPSMode() FPSMode FPSMode() FPSMode
SetFPSMode(mode FPSMode) SetFPSMode(mode FPSMode)
ScheduleFrame()
IsScreenTransparent() bool IsScreenTransparent() bool
SetScreenTransparent(transparent bool) SetScreenTransparent(transparent bool)
@ -104,4 +105,5 @@ type FPSMode int
const ( const (
FPSModeVsyncOn FPSMode = iota FPSModeVsyncOn FPSMode = iota
FPSModeVsyncOffMaximum FPSModeVsyncOffMaximum
FPSModeVsyncOffMinimum
) )

View File

@ -334,6 +334,10 @@ func PollEvents() {
glfw.PollEvents() glfw.PollEvents()
} }
func PostEmptyEvent() {
glfw.PostEmptyEvent()
}
func SetMonitorCallback(cbfun func(monitor *Monitor, event PeripheralEvent)) { func SetMonitorCallback(cbfun func(monitor *Monitor, event PeripheralEvent)) {
var gcb func(monitor *glfw.Monitor, event glfw.PeripheralEvent) var gcb func(monitor *glfw.Monitor, event glfw.PeripheralEvent)
if cbfun != nil { if cbfun != nil {
@ -360,6 +364,10 @@ func UpdateGamepadMappings(mapping string) bool {
return glfw.UpdateGamepadMappings(mapping) return glfw.UpdateGamepadMappings(mapping)
} }
func WaitEvents() {
glfw.WaitEvents()
}
func WindowHint(target Hint, hint int) { func WindowHint(target Hint, hint int) {
glfw.WindowHint(glfw.Hint(target), hint) glfw.WindowHint(glfw.Hint(target), hint)
} }

View File

@ -517,6 +517,11 @@ func PollEvents() {
panicErrorExceptForInvalidValue() panicErrorExceptForInvalidValue()
} }
func PostEmptyEvent() {
glfwDLL.call("glfwPostEmptyEvent")
panicError()
}
func SetMonitorCallback(cbfun func(monitor *Monitor, event PeripheralEvent)) { func SetMonitorCallback(cbfun func(monitor *Monitor, event PeripheralEvent)) {
var gcb uintptr var gcb uintptr
if cbfun != nil { if cbfun != nil {
@ -554,6 +559,11 @@ func UpdateGamepadMappings(mapping string) bool {
return r == True return r == True
} }
func WaitEvents() {
glfwDLL.call("glfwWaitEvents")
panicError()
}
func WindowHint(target Hint, hint int) { func WindowHint(target Hint, hint int) {
glfwDLL.call("glfwWindowHint", uintptr(target), uintptr(hint)) glfwDLL.call("glfwWindowHint", uintptr(target), uintptr(hint))
panicError() panicError()

View File

@ -639,6 +639,15 @@ func (u *UserInterface) FPSMode() driver.FPSMode {
return v return v
} }
func (u *UserInterface) ScheduleFrame() {
if !u.isRunning() {
return
}
// As the main thread can be blocked, do not check the current FPS mode.
// PostEmptyEvent is concurrent safe.
glfw.PostEmptyEvent()
}
func (u *UserInterface) CursorMode() driver.CursorMode { func (u *UserInterface) CursorMode() driver.CursorMode {
if !u.isRunning() { if !u.isRunning() {
return u.getInitCursorMode() return u.getInitCursorMode()
@ -964,8 +973,12 @@ func (u *UserInterface) update() (float64, float64, bool, error) {
outsideWidth, outsideHeight, outsideSizeChanged := u.updateSize() outsideWidth, outsideHeight, outsideSizeChanged := u.updateSize()
if u.fpsMode != driver.FPSModeVsyncOffMinimum {
// TODO: Updating the input can be skipped when clock.Update returns 0 (#1367). // TODO: Updating the input can be skipped when clock.Update returns 0 (#1367).
glfw.PollEvents() glfw.PollEvents()
} else {
glfw.WaitEvents()
}
u.input.update(u.window, u.context) u.input.update(u.window, u.context)
for !u.isRunnableOnUnfocused() && u.window.GetAttrib(glfw.Focused) == 0 && !u.window.ShouldClose() { for !u.isRunnableOnUnfocused() && u.window.GetAttrib(glfw.Focused) == 0 && !u.window.ShouldClose() {

View File

@ -403,6 +403,8 @@ func (i *Input) updateFromEvent(e js.Value) {
case t.Equal(stringTouchstart) || t.Equal(stringTouchend) || t.Equal(stringTouchmove): case t.Equal(stringTouchstart) || t.Equal(stringTouchend) || t.Equal(stringTouchmove):
i.updateTouchesFromEvent(e) i.updateTouchesFromEvent(e)
} }
i.ui.forceUpdateOnMinimumFPSMode()
} }
func (i *Input) setMouseCursorFromEvent(e js.Value) { func (i *Input) setMouseCursorFromEvent(e js.Value) {

View File

@ -50,11 +50,13 @@ func driverCursorShapeToCSSCursor(cursor driver.CursorShape) string {
type UserInterface struct { type UserInterface struct {
runnableOnUnfocused bool runnableOnUnfocused bool
fpsMode driver.FPSMode fpsMode driver.FPSMode
renderingScheduled bool
running bool running bool
initFocused bool initFocused bool
cursorMode driver.CursorMode cursorMode driver.CursorMode
cursorPrevMode driver.CursorMode cursorPrevMode driver.CursorMode
cursorShape driver.CursorShape cursorShape driver.CursorShape
onceUpdateCalled bool
sizeChanged bool sizeChanged bool
@ -146,6 +148,10 @@ func (u *UserInterface) FPSMode() driver.FPSMode {
return u.fpsMode return u.fpsMode
} }
func (u *UserInterface) ScheduleFrame() {
u.renderingScheduled = true
}
func (u *UserInterface) CursorMode() driver.CursorMode { func (u *UserInterface) CursorMode() driver.CursorMode {
if !canvas.Truthy() { if !canvas.Truthy() {
return driver.CursorModeHidden return driver.CursorModeHidden
@ -281,6 +287,20 @@ func (u *UserInterface) updateImpl(force bool) error {
return nil return nil
} }
func (u *UserInterface) needsUpdate() bool {
if u.fpsMode != driver.FPSModeVsyncOffMinimum {
return true
}
if !u.onceUpdateCalled {
return true
}
if u.renderingScheduled {
return true
}
// TODO: Watch the gamepad state?
return false
}
func (u *UserInterface) loop(context driver.UIContext) <-chan error { func (u *UserInterface) loop(context driver.UIContext) <-chan error {
u.context = context u.context = context
@ -290,6 +310,9 @@ func (u *UserInterface) loop(context driver.UIContext) <-chan error {
var cf js.Func var cf js.Func
f := func() { f := func() {
if u.needsUpdate() {
u.onceUpdateCalled = true
u.renderingScheduled = false
if err := u.update(); err != nil { if err := u.update(); err != nil {
close(reqStopAudioCh) close(reqStopAudioCh)
<-resStopAudioCh <-resStopAudioCh
@ -297,10 +320,14 @@ func (u *UserInterface) loop(context driver.UIContext) <-chan error {
errCh <- err errCh <- err
return return
} }
if u.fpsMode == driver.FPSModeVsyncOn { }
switch u.fpsMode {
case driver.FPSModeVsyncOn:
requestAnimationFrame.Invoke(cf) requestAnimationFrame.Invoke(cf)
} else { case driver.FPSModeVsyncOffMaximum:
setTimeout.Invoke(cf, 0) setTimeout.Invoke(cf, 0)
case driver.FPSModeVsyncOffMinimum:
requestAnimationFrame.Invoke(cf)
} }
} }
@ -540,6 +567,13 @@ func setCanvasEventHandlers(v js.Value) {
})) }))
} }
func (u *UserInterface) forceUpdateOnMinimumFPSMode() {
if u.fpsMode != driver.FPSModeVsyncOffMinimum {
return
}
u.updateImpl(true)
}
func (u *UserInterface) Run(context driver.UIContext) error { func (u *UserInterface) Run(context driver.UIContext) error {
if u.initFocused && window.Truthy() { if u.initFocused && window.Truthy() {
// Do not focus the canvas when the current document is in an iframe. // Do not focus the canvas when the current document is in an iframe.

View File

@ -112,6 +112,9 @@ type UserInterface struct {
input Input input Input
fpsMode driver.FPSMode
renderRequester RenderRequester
t *thread.OSThread t *thread.OSThread
m sync.RWMutex m sync.RWMutex
@ -405,11 +408,19 @@ func (u *UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) {
} }
func (u *UserInterface) FPSMode() driver.FPSMode { func (u *UserInterface) FPSMode() driver.FPSMode {
return driver.FPSModeVsyncOn return u.fpsMode
} }
func (u *UserInterface) SetFPSMode(mode driver.FPSMode) { func (u *UserInterface) SetFPSMode(mode driver.FPSMode) {
// Do nothing u.fpsMode = mode
u.updateExplicitRenderingModeIfNeeded()
}
func (u *UserInterface) updateExplicitRenderingModeIfNeeded() {
if u.renderRequester == nil {
return
}
u.renderRequester.SetExplicitRenderingMode(u.fpsMode == driver.FPSModeVsyncOffMinimum)
} }
func (u *UserInterface) DeviceScaleFactor() float64 { func (u *UserInterface) DeviceScaleFactor() float64 {
@ -459,4 +470,23 @@ type Gamepad struct {
func (u *UserInterface) UpdateInput(keys map[driver.Key]struct{}, runes []rune, touches []*Touch, gamepads []Gamepad) { func (u *UserInterface) UpdateInput(keys map[driver.Key]struct{}, runes []rune, touches []*Touch, gamepads []Gamepad) {
u.input.update(keys, runes, touches, gamepads) u.input.update(keys, runes, touches, gamepads)
if u.fpsMode == driver.FPSModeVsyncOffMinimum {
u.renderRequester.RequestRenderIfNeeded()
}
}
type RenderRequester interface {
SetExplicitRenderingMode(explicitRendering bool)
RequestRenderIfNeeded()
}
func (u *UserInterface) SetRenderRequester(renderRequester RenderRequester) {
u.renderRequester = renderRequester
u.updateExplicitRenderingModeIfNeeded()
}
func (u *UserInterface) ScheduleFrame() {
if u.renderRequester != nil && u.fpsMode == driver.FPSModeVsyncOffMinimum {
u.renderRequester.RequestRenderIfNeeded()
}
} }

View File

@ -90,3 +90,12 @@ func OnContextLost() {
func DeviceScale() float64 { func DeviceScale() float64 {
return devicescale.GetAt(0, 0) return devicescale.GetAt(0, 0)
} }
type RenderRequester interface {
SetExplicitRenderingMode(explicitRendering bool)
RequestRenderIfNeeded()
}
func SetRenderRequester(renderRequester RenderRequester) {
mobile.Get().SetRenderRequester(renderRequester)
}

67
run.go
View File

@ -312,7 +312,7 @@ func DeviceScaleFactor() float64 {
// IsVsyncEnabled returns a boolean value indicating whether // IsVsyncEnabled returns a boolean value indicating whether
// the game uses the display's vsync. // the game uses the display's vsync.
// //
// IsVsyncEnabled is concurrent-safe. // Deprecated: as of v2.2. Use FPSMode instead.
func IsVsyncEnabled() bool { func IsVsyncEnabled() bool {
return uiDriver().FPSMode() == driver.FPSModeVsyncOn return uiDriver().FPSMode() == driver.FPSModeVsyncOn
} }
@ -320,17 +320,7 @@ func IsVsyncEnabled() bool {
// SetVsyncEnabled sets a boolean value indicating whether // SetVsyncEnabled sets a boolean value indicating whether
// the game uses the display's vsync. // the game uses the display's vsync.
// //
// If the given value is true, the game tries to sync the display's refresh rate. // Deprecated: as of v2.2. Use SetFPSMode instead.
// If false, the game ignores the display's refresh rate.
// The initial value is true.
// By disabling vsync, the game works more efficiently but consumes more CPU.
//
// Note that the state doesn't affect TPS (ticks per second, i.e. how many the run function is
// updated per second).
//
// SetVsyncEnabled does nothing on mobiles so far.
//
// SetVsyncEnabled is concurrent-safe.
func SetVsyncEnabled(enabled bool) { func SetVsyncEnabled(enabled bool) {
if enabled { if enabled {
uiDriver().SetFPSMode(driver.FPSModeVsyncOn) uiDriver().SetFPSMode(driver.FPSModeVsyncOn)
@ -339,10 +329,63 @@ func SetVsyncEnabled(enabled bool) {
} }
} }
// FPSModeType is a type of FPS modes.
type FPSModeType = driver.FPSMode
const (
// FPSModeVsyncOn indicates that the game tries to sync the display's refresh rate.
// FPSModeVsyncOn is the default mode.
FPSModeVsyncOn FPSModeType = driver.FPSModeVsyncOn
// FPSModeVsyncOffMaximum indicates that the game doesn't sync with vsycn, and
// the game is updated whenever possible.
//
// Be careful that FPSModeVsyncOffMaximum might consume a lot of battery power.
//
// In FPSModeVsyncOffMaximum, the game's Draw is called almost without sleeping.
// The game's Update is called based on the specified TPS.
FPSModeVsyncOffMaximum FPSModeType = driver.FPSModeVsyncOffMaximum
// FPSModeVsyncOffMinimum indicates that the game doesn't sync with vsycn, and
// the game is updated only when necessary.
//
// FPSModeVsyncOffMinimum is useful for relatively static applications to save battery power.
//
// In FPSModeVsyncOffMinimum, the game's Update and Draw are called only when
// 1) new inputting is detected, or 2) ScheduleFrame is called.
// In FPSModeVsyncOffMinimum, TPS is SyncWithFPS no matter what TPS is specified at SetMaxTPS.
FPSModeVsyncOffMinimum FPSModeType = driver.FPSModeVsyncOffMinimum
)
// FPSMode returns the current FPS mode.
//
// FPSMode is concurrent-safe.
func FPSMode() FPSModeType {
return uiDriver().FPSMode()
}
// SetFPSMode sets the FPS mode.
// The default FPS mode is FPSModeVsycnOn.
//
// SetFPSMode is concurrent-safe.
func SetFPSMode(mode FPSModeType) {
uiDriver().SetFPSMode(mode)
}
// ScheduleFrame schedules a next frame when the current FPS mode is FPSModeVsyncOffMinimum.
//
// ScheduleFrame is concurrent-safe.
func ScheduleFrame() {
uiDriver().ScheduleFrame()
}
// MaxTPS returns the current maximum TPS. // MaxTPS returns the current maximum TPS.
// //
// MaxTPS is concurrent-safe. // MaxTPS is concurrent-safe.
func MaxTPS() int { func MaxTPS() int {
if FPSMode() == FPSModeVsyncOffMinimum {
return SyncWithFPS
}
return int(atomic.LoadInt32(&currentMaxTPS)) return int(atomic.LoadInt32(&currentMaxTPS))
} }