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"
@interface {{.PrefixUpper}}EbitenViewController : UIViewController
@interface {{.PrefixUpper}}EbitenViewController : UIViewController<EbitenmobileviewRenderRequester>
@end
@implementation {{.PrefixUpper}}EbitenViewController {
@ -173,6 +173,8 @@ const objcM = `// Code generated by ebitenmobile. DO NOT EDIT.
bool started_;
bool active_;
bool error_;
CADisplayLink* displayLink_;
bool explicitRendering_;
}
- (UIView*)metalView {
@ -214,8 +216,9 @@ const objcM = `// Code generated by ebitenmobile. DO NOT EDIT.
[EAGLContext setCurrentContext:context];
#endif
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
displayLink_ = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];
[displayLink_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
EbitenmobileviewSetRenderRequester(self);
}
- (void)viewWillLayoutSubviews {
@ -251,6 +254,10 @@ const objcM = `// Code generated by ebitenmobile. DO NOT EDIT.
#else
[[self glkView] setNeedsDisplay];
#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
`
@ -745,9 +771,10 @@ import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;
import {{.JavaPkg}}.ebitenmobileview.RenderRequester;
import {{.JavaPkg}}.{{.PrefixLower}}.EbitenView;
class EbitenSurfaceView extends GLSurfaceView {
class EbitenSurfaceView extends GLSurfaceView implements RenderRequester {
private class EbitenRenderer implements GLSurfaceView.Renderer {
@ -795,10 +822,27 @@ class EbitenSurfaceView extends GLSurfaceView {
setEGLContextClientVersion(2);
setEGLConfigChooser(8, 8, 8, 8, 0, 0);
setRenderer(new EbitenRenderer());
Ebitenmobileview.setRenderRequester(this);
}
private void onErrorOnGameUpdate(Exception 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()
runnableOnUnfocused := ebiten.IsRunnableOnUnfocused()
cursorMode := ebiten.CursorMode()
vsyncEnabled := ebiten.IsVsyncEnabled()
fpsMode := ebiten.FPSMode()
tps := ebiten.MaxTPS()
decorated := ebiten.IsWindowDecorated()
positionX, positionY := ebiten.WindowPosition()
@ -205,7 +205,14 @@ func (g *game) Update() error {
}
}
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) {
switch tps {
@ -249,10 +256,10 @@ func (g *game) Update() error {
ebiten.SetRunnableOnUnfocused(runnableOnUnfocused)
ebiten.SetCursorMode(cursorMode)
// Set vsync enabled only when this is needed.
// This makes a bug around vsync initialization more explicit (#1364).
if vsyncEnabled != ebiten.IsVsyncEnabled() {
ebiten.SetVsyncEnabled(vsyncEnabled)
// Set FPS mode enabled only when this is needed.
// This makes a bug around FPS mode initialization more explicit (#1364).
if fpsMode != ebiten.FPSMode() {
ebiten.SetFPSMode(fpsMode)
}
ebiten.SetMaxTPS(tps)
ebiten.SetWindowDecorated(decorated)
@ -323,7 +330,7 @@ func (g *game) Draw(screen *ebiten.Image) {
[U] Switch the runnable-on-unfocused state
[C] Switch the cursor mode (visible, hidden, or captured)
[I] Change the window icon (only for desktops)
[V] Switch vsync
[V] Switch the FPS mode
[T] Switch TPS (ticks per second)
[D] Switch the window decoration (only for desktops)
[L] Switch the window floating state (only for desktops)
@ -403,7 +410,9 @@ func main() {
ebiten.SetWindowResizable(true)
ebiten.MaximizeWindow()
}
ebiten.SetVsyncEnabled(*flagVsync)
if !*flagVsync {
ebiten.SetFPSMode(ebiten.FPSModeVsyncOffMaximum)
}
if *flagAutoAdjusting {
ebiten.SetWindowResizable(true)
}

View File

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

View File

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

View File

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

View File

@ -639,6 +639,15 @@ func (u *UserInterface) FPSMode() driver.FPSMode {
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 {
if !u.isRunning() {
return u.getInitCursorMode()
@ -964,8 +973,12 @@ func (u *UserInterface) update() (float64, float64, bool, error) {
outsideWidth, outsideHeight, outsideSizeChanged := u.updateSize()
if u.fpsMode != driver.FPSModeVsyncOffMinimum {
// TODO: Updating the input can be skipped when clock.Update returns 0 (#1367).
glfw.PollEvents()
} else {
glfw.WaitEvents()
}
u.input.update(u.window, u.context)
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):
i.updateTouchesFromEvent(e)
}
i.ui.forceUpdateOnMinimumFPSMode()
}
func (i *Input) setMouseCursorFromEvent(e js.Value) {

View File

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

View File

@ -112,6 +112,9 @@ type UserInterface struct {
input Input
fpsMode driver.FPSMode
renderRequester RenderRequester
t *thread.OSThread
m sync.RWMutex
@ -405,11 +408,19 @@ func (u *UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) {
}
func (u *UserInterface) FPSMode() driver.FPSMode {
return driver.FPSModeVsyncOn
return u.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 {
@ -459,4 +470,23 @@ type Gamepad struct {
func (u *UserInterface) UpdateInput(keys map[driver.Key]struct{}, runes []rune, touches []*Touch, gamepads []Gamepad) {
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 {
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
// the game uses the display's vsync.
//
// IsVsyncEnabled is concurrent-safe.
// Deprecated: as of v2.2. Use FPSMode instead.
func IsVsyncEnabled() bool {
return uiDriver().FPSMode() == driver.FPSModeVsyncOn
}
@ -320,17 +320,7 @@ func IsVsyncEnabled() bool {
// SetVsyncEnabled sets a boolean value indicating whether
// the game uses the display's vsync.
//
// If the given value is true, the game tries to sync the display's refresh rate.
// 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.
// Deprecated: as of v2.2. Use SetFPSMode instead.
func SetVsyncEnabled(enabled bool) {
if enabled {
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 is concurrent-safe.
func MaxTPS() int {
if FPSMode() == FPSModeVsyncOffMinimum {
return SyncWithFPS
}
return int(atomic.LoadInt32(&currentMaxTPS))
}