ebiten: add RunGameOptions.StrictContextRestration

This reverts commit a30f075896.

This change adds a new option StrictContextRestration to make the
restoration optional.

Closes #3083
This commit is contained in:
Hajime Hoshi 2024-09-06 16:30:20 +09:00
parent 935e7a6d5d
commit 35f4884a74
8 changed files with 149 additions and 56 deletions

View File

@ -25,10 +25,10 @@ import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;
import {{.JavaPkg}}.ebitenmobileview.RenderRequester;
import {{.JavaPkg}}.ebitenmobileview.Renderer;
import {{.JavaPkg}}.{{.PrefixLower}}.EbitenView;
class EbitenSurfaceView extends GLSurfaceView implements RenderRequester {
class EbitenSurfaceView extends GLSurfaceView implements Renderer {
private class EbitenRenderer implements GLSurfaceView.Renderer {
@ -63,6 +63,10 @@ class EbitenSurfaceView extends GLSurfaceView implements RenderRequester {
onceSurfaceCreated_ = true;
return;
}
if (hasStrictContextRestoration()) {
Ebitenmobileview.onContextLost();
return;
}
contextLost_ = true;
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
@ -77,6 +81,8 @@ class EbitenSurfaceView extends GLSurfaceView implements RenderRequester {
}
}
private boolean strictContextRestoration_ = false;
public EbitenSurfaceView(Context context) {
super(context);
initialize();
@ -90,9 +96,11 @@ class EbitenSurfaceView extends GLSurfaceView implements RenderRequester {
private void initialize() {
setEGLContextClientVersion(3);
setEGLConfigChooser(8, 8, 8, 8, 0, 0);
// setRenderer must be called before setRenderRequester.
// Or, the application crashes.
setRenderer(new EbitenRenderer());
setPreserveEGLContextOnPause(true);
Ebitenmobileview.setRenderRequester(this);
Ebitenmobileview.setRenderer(this);
}
private void onErrorOnGameUpdate(Exception e) {
@ -114,6 +122,16 @@ class EbitenSurfaceView extends GLSurfaceView implements RenderRequester {
}
}
@Override
public synchronized void setStrictContextRestoration(boolean strictContextRestoration) {
strictContextRestoration_ = strictContextRestoration;
setPreserveEGLContextOnPause(!strictContextRestoration);
}
private synchronized boolean hasStrictContextRestoration() {
return strictContextRestoration_;
}
@Override
public synchronized void requestRenderIfNeeded() {
if (getRenderMode() == RENDERMODE_WHEN_DIRTY) {

View File

@ -20,7 +20,7 @@
#import "Ebitenmobileview.objc.h"
@interface {{.PrefixUpper}}EbitenViewController : UIViewController<EbitenmobileviewRenderRequester, EbitenmobileviewSetGameNotifier>
@interface {{.PrefixUpper}}EbitenViewController : UIViewController<EbitenmobileviewRenderer, EbitenmobileviewSetGameNotifier>
@end
@implementation {{.PrefixUpper}}EbitenViewController {
@ -149,7 +149,7 @@
displayLink_ = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawFrame)];
[displayLink_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
EbitenmobileviewSetRenderRequester(self);
EbitenmobileviewSetRenderer(self);
// Run the loop. This will never return.
[[NSRunLoop currentRunLoop] run];
@ -364,6 +364,10 @@
}
}
- (void)setStrictContextRestoration:(BOOL)strictContextRestoration {
// Do nothing.
}
- (void)setExplicitRenderingMode:(BOOL)explicitRendering {
@synchronized(self) {
explicitRendering_ = explicitRendering;

View File

@ -20,6 +20,11 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
)
// EnableRestoringForTesting forces to enable restoring for testing.
func EnableRestoringForTesting() {
forceRestoring = true
}
func ResolveStaleImages(graphicsDriver graphicsdriver.Graphics) error {
return resolveStaleImages(graphicsDriver, false)
}

View File

@ -15,17 +15,42 @@
package restorable
import (
"image"
"runtime"
"sync"
"sync/atomic"
"github.com/hajimehoshi/ebiten/v2/internal/debug"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
)
// forceRestoring reports whether restoring forcely happens or not.
// forceRestoring reports whether restoring forcibly happens or not.
// This is used only for testing.
var forceRestoring = false
// disabled indicates that restoring is disabled or not.
// Restoring is enabled by default for some platforms like Android for safety.
// Before SetGame, it is not possible to determine whether restoring is needed or not.
var disabled atomic.Bool
var disabledOnce sync.Once
// Disable disables restoring.
func Disable() {
disabled.Store(true)
}
// needsRestoring reports whether restoring process works or not.
func needsRestoring() bool {
return forceRestoring
if forceRestoring {
return true
}
// TODO: If Vulkan is introduced, restoring might not be needed.
if runtime.GOOS == "android" {
return !disabled.Load()
}
return false
}
// AlwaysReadPixelsFromGPU reports whether ReadPixels always reads pixels from GPU or not.
@ -33,16 +58,12 @@ func AlwaysReadPixelsFromGPU() bool {
return !needsRestoring()
}
// EnableRestoringForTesting forces to enable restoring for testing.
func EnableRestoringForTesting() {
forceRestoring = true
}
// images is a set of Image objects.
type images struct {
images map[*Image]struct{}
shaders map[*Shader]struct{}
lastTarget *Image
images map[*Image]struct{}
shaders map[*Shader]struct{}
lastTarget *Image
contextLost atomic.Bool
}
// theImages represents the images for the current process.
@ -66,6 +87,15 @@ func SwapBuffers(graphicsDriver graphicsdriver.Graphics) error {
// resolveStaleImages flushes the queued draw commands and resolves all stale images.
// If endFrame is true, the current screen might be used to present when flushing the commands.
func resolveStaleImages(graphicsDriver graphicsdriver.Graphics, endFrame bool) error {
// When Disable is called, all the images data should be evicted once.
if disabled.Load() {
disabledOnce.Do(func() {
for img := range theImages.images {
img.makeStale(image.Rectangle{})
}
})
}
if err := graphicscommand.FlushCommands(graphicsDriver, endFrame); err != nil {
return err
}
@ -83,14 +113,8 @@ func RestoreIfNeeded(graphicsDriver graphicsdriver.Graphics) error {
return nil
}
if !forceRestoring {
var r bool
// TODO: Detect context lost explicitly on Android.
if !r {
return nil
}
if !forceRestoring && !theImages.contextLost.Load() {
return nil
}
if err := graphicscommand.ResetGraphicsDriverState(graphicsDriver); err != nil {
@ -243,6 +267,8 @@ func (i *images) restore(graphicsDriver graphicsdriver.Graphics) error {
}
}
i.contextLost.Store(false)
return nil
}
@ -258,3 +284,8 @@ func InitializeGraphicsDriverState(graphicsDriver graphicsdriver.Graphics) error
func MaxImageSize(graphicsDriver graphicsdriver.Graphics) int {
return graphicscommand.MaxImageSize(graphicsDriver)
}
// OnContextLost is called when the context lost is detected in an explicit way.
func OnContextLost() {
theImages.contextLost.Store(true)
}

View File

@ -172,15 +172,16 @@ func (u *UserInterface) dumpImages(dir string) (string, error) {
}
type RunOptions struct {
GraphicsLibrary GraphicsLibrary
InitUnfocused bool
ScreenTransparent bool
SkipTaskbar bool
SingleThread bool
DisableHiDPI bool
ColorSpace graphicsdriver.ColorSpace
X11ClassName string
X11InstanceName string
GraphicsLibrary GraphicsLibrary
InitUnfocused bool
ScreenTransparent bool
SkipTaskbar bool
SingleThread bool
DisableHiDPI bool
ColorSpace graphicsdriver.ColorSpace
X11ClassName string
X11InstanceName string
StrictContextRestoration bool
}
// InitialWindowPosition returns the position for centering the given second width/height pair within the first width/height pair.

View File

@ -28,6 +28,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/hook"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
)
var (
@ -97,8 +98,11 @@ type userInterfaceImpl struct {
inputState InputState
touches []TouchForInput
fpsMode atomic.Int32
renderRequester RenderRequester
fpsMode atomic.Int32
renderer Renderer
strictContextRestoration bool
strictContextRestorationOnce sync.Once
m sync.RWMutex
}
@ -152,6 +156,11 @@ func (u *UserInterface) runMobile(game Game, options *RunOptions) (err error) {
u.graphicsDriver = g
u.setGraphicsLibrary(lib)
close(u.graphicsLibraryInitCh)
u.strictContextRestoration = options.StrictContextRestoration
if !u.strictContextRestoration {
restorable.Disable()
}
u.renderer.SetStrictContextRestoration(u.strictContextRestoration)
for {
if err := u.update(); err != nil {
@ -239,10 +248,10 @@ func (u *UserInterface) SetFPSMode(mode FPSModeType) {
}
func (u *UserInterface) updateExplicitRenderingModeIfNeeded(fpsMode FPSModeType) {
if u.renderRequester == nil {
if u.renderer == nil {
return
}
u.renderRequester.SetExplicitRenderingMode(fpsMode == FPSModeVsyncOffMinimum)
u.renderer.SetExplicitRenderingMode(fpsMode == FPSModeVsyncOffMinimum)
}
func (u *UserInterface) readInputState(inputState *InputState) {
@ -297,23 +306,24 @@ func (u *UserInterface) Monitor() *Monitor {
func (u *UserInterface) UpdateInput(keys map[Key]struct{}, runes []rune, touches []TouchForInput) {
u.updateInputStateFromOutside(keys, runes, touches)
if FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum {
u.renderRequester.RequestRenderIfNeeded()
u.renderer.RequestRenderIfNeeded()
}
}
type RenderRequester interface {
type Renderer interface {
SetExplicitRenderingMode(explicitRendering bool)
SetStrictContextRestoration(strictContextRestoration bool)
RequestRenderIfNeeded()
}
func (u *UserInterface) SetRenderRequester(renderRequester RenderRequester) {
u.renderRequester = renderRequester
func (u *UserInterface) SetRenderer(renderer Renderer) {
u.renderer = renderer
u.updateExplicitRenderingModeIfNeeded(FPSModeType(u.fpsMode.Load()))
}
func (u *UserInterface) ScheduleFrame() {
if u.renderRequester != nil && FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum {
u.renderRequester.RequestRenderIfNeeded()
if u.renderer != nil && FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum {
u.renderer.RequestRenderIfNeeded()
}
}

View File

@ -30,6 +30,7 @@ import (
"sync"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
"github.com/hajimehoshi/ebiten/v2/internal/ui"
)
@ -112,16 +113,20 @@ func Resume() error {
return ui.Get().SetForeground(true)
}
func OnContextLost() {
restorable.OnContextLost()
}
func DeviceScale() float64 {
return ui.Get().Monitor().DeviceScaleFactor()
}
type RenderRequester interface {
ui.RenderRequester
type Renderer interface {
ui.Renderer
}
func SetRenderRequester(renderRequester RenderRequester) {
ui.Get().SetRenderRequester(renderRequester)
func SetRenderer(renderer Renderer) {
ui.Get().SetRenderer(renderer)
}
func SetSetGameNotifier(setGameNotifier SetGameNotifier) {

37
run.go
View File

@ -297,6 +297,24 @@ type RunGameOptions struct {
// X11InstanceName is an instance name in the ICCCM WM_CLASS window property.
X11InstanceName string
// StrictContextRestration indicates whether the context lost should be restored strictly by Ebitengine or not.
//
// StrictContextRestration is available only on Android. Otherwise, StrictContextRestration is ignored.
//
// When StrictContextRestration is false, Ebitengine tries to rely on the OS to restore the context.
// In Android, Ebitengien uses `GLSurfaceView`'s `setPreserveEGLContextOnPause(true)`.
// This works in most cases, but it is still possible that the context is lost in some minor cases.
// With StrictContextRestration false, the activity's launch mode should be singleInstance,
// or the activity no longer works correctly after the context is lost.
//
// When StrictContextRestration is true, Ebitengine tries to restore the context more strictly.
// This is useful when you want to restore the context in any case.
// However, this might cause a performance issue since Ebitengine tries to keep all the information
// to restore the context.
//
// The default (zero) value is false.
StrictContextRestration bool
}
// RunGameWithOptions starts the main loop and runs the game with the specified options.
@ -716,15 +734,16 @@ func toUIRunOptions(options *RunGameOptions) *ui.RunOptions {
options.X11InstanceName = defaultX11InstanceName
}
return &ui.RunOptions{
GraphicsLibrary: ui.GraphicsLibrary(options.GraphicsLibrary),
InitUnfocused: options.InitUnfocused,
ScreenTransparent: options.ScreenTransparent,
SkipTaskbar: options.SkipTaskbar,
SingleThread: options.SingleThread,
DisableHiDPI: options.DisableHiDPI,
ColorSpace: graphicsdriver.ColorSpace(options.ColorSpace),
X11ClassName: options.X11ClassName,
X11InstanceName: options.X11InstanceName,
GraphicsLibrary: ui.GraphicsLibrary(options.GraphicsLibrary),
InitUnfocused: options.InitUnfocused,
ScreenTransparent: options.ScreenTransparent,
SkipTaskbar: options.SkipTaskbar,
SingleThread: options.SingleThread,
DisableHiDPI: options.DisableHiDPI,
ColorSpace: graphicsdriver.ColorSpace(options.ColorSpace),
X11ClassName: options.X11ClassName,
X11InstanceName: options.X11InstanceName,
StrictContextRestoration: options.StrictContextRestration,
}
}