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

View File

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

View File

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

View File

@ -15,17 +15,42 @@
package restorable package restorable
import ( import (
"image"
"runtime"
"sync"
"sync/atomic"
"github.com/hajimehoshi/ebiten/v2/internal/debug" "github.com/hajimehoshi/ebiten/v2/internal/debug"
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand" "github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "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 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. // needsRestoring reports whether restoring process works or not.
func needsRestoring() bool { 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. // AlwaysReadPixelsFromGPU reports whether ReadPixels always reads pixels from GPU or not.
@ -33,16 +58,12 @@ func AlwaysReadPixelsFromGPU() bool {
return !needsRestoring() return !needsRestoring()
} }
// EnableRestoringForTesting forces to enable restoring for testing.
func EnableRestoringForTesting() {
forceRestoring = true
}
// images is a set of Image objects. // images is a set of Image objects.
type images struct { type images struct {
images map[*Image]struct{} images map[*Image]struct{}
shaders map[*Shader]struct{} shaders map[*Shader]struct{}
lastTarget *Image lastTarget *Image
contextLost atomic.Bool
} }
// theImages represents the images for the current process. // 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. // 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. // If endFrame is true, the current screen might be used to present when flushing the commands.
func resolveStaleImages(graphicsDriver graphicsdriver.Graphics, endFrame bool) error { 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 { if err := graphicscommand.FlushCommands(graphicsDriver, endFrame); err != nil {
return err return err
} }
@ -83,14 +113,8 @@ func RestoreIfNeeded(graphicsDriver graphicsdriver.Graphics) error {
return nil return nil
} }
if !forceRestoring { if !forceRestoring && !theImages.contextLost.Load() {
var r bool return nil
// TODO: Detect context lost explicitly on Android.
if !r {
return nil
}
} }
if err := graphicscommand.ResetGraphicsDriverState(graphicsDriver); err != 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 return nil
} }
@ -258,3 +284,8 @@ func InitializeGraphicsDriverState(graphicsDriver graphicsdriver.Graphics) error
func MaxImageSize(graphicsDriver graphicsdriver.Graphics) int { func MaxImageSize(graphicsDriver graphicsdriver.Graphics) int {
return graphicscommand.MaxImageSize(graphicsDriver) 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 { type RunOptions struct {
GraphicsLibrary GraphicsLibrary GraphicsLibrary GraphicsLibrary
InitUnfocused bool InitUnfocused bool
ScreenTransparent bool ScreenTransparent bool
SkipTaskbar bool SkipTaskbar bool
SingleThread bool SingleThread bool
DisableHiDPI bool DisableHiDPI bool
ColorSpace graphicsdriver.ColorSpace ColorSpace graphicsdriver.ColorSpace
X11ClassName string X11ClassName string
X11InstanceName string X11InstanceName string
StrictContextRestoration bool
} }
// InitialWindowPosition returns the position for centering the given second width/height pair within the first width/height pair. // 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/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver" "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/hook" "github.com/hajimehoshi/ebiten/v2/internal/hook"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
) )
var ( var (
@ -97,8 +98,11 @@ type userInterfaceImpl struct {
inputState InputState inputState InputState
touches []TouchForInput touches []TouchForInput
fpsMode atomic.Int32 fpsMode atomic.Int32
renderRequester RenderRequester renderer Renderer
strictContextRestoration bool
strictContextRestorationOnce sync.Once
m sync.RWMutex m sync.RWMutex
} }
@ -152,6 +156,11 @@ func (u *UserInterface) runMobile(game Game, options *RunOptions) (err error) {
u.graphicsDriver = g u.graphicsDriver = g
u.setGraphicsLibrary(lib) u.setGraphicsLibrary(lib)
close(u.graphicsLibraryInitCh) close(u.graphicsLibraryInitCh)
u.strictContextRestoration = options.StrictContextRestoration
if !u.strictContextRestoration {
restorable.Disable()
}
u.renderer.SetStrictContextRestoration(u.strictContextRestoration)
for { for {
if err := u.update(); err != nil { if err := u.update(); err != nil {
@ -239,10 +248,10 @@ func (u *UserInterface) SetFPSMode(mode FPSModeType) {
} }
func (u *UserInterface) updateExplicitRenderingModeIfNeeded(fpsMode FPSModeType) { func (u *UserInterface) updateExplicitRenderingModeIfNeeded(fpsMode FPSModeType) {
if u.renderRequester == nil { if u.renderer == nil {
return return
} }
u.renderRequester.SetExplicitRenderingMode(fpsMode == FPSModeVsyncOffMinimum) u.renderer.SetExplicitRenderingMode(fpsMode == FPSModeVsyncOffMinimum)
} }
func (u *UserInterface) readInputState(inputState *InputState) { 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) { func (u *UserInterface) UpdateInput(keys map[Key]struct{}, runes []rune, touches []TouchForInput) {
u.updateInputStateFromOutside(keys, runes, touches) u.updateInputStateFromOutside(keys, runes, touches)
if FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum { if FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum {
u.renderRequester.RequestRenderIfNeeded() u.renderer.RequestRenderIfNeeded()
} }
} }
type RenderRequester interface { type Renderer interface {
SetExplicitRenderingMode(explicitRendering bool) SetExplicitRenderingMode(explicitRendering bool)
SetStrictContextRestoration(strictContextRestoration bool)
RequestRenderIfNeeded() RequestRenderIfNeeded()
} }
func (u *UserInterface) SetRenderRequester(renderRequester RenderRequester) { func (u *UserInterface) SetRenderer(renderer Renderer) {
u.renderRequester = renderRequester u.renderer = renderer
u.updateExplicitRenderingModeIfNeeded(FPSModeType(u.fpsMode.Load())) u.updateExplicitRenderingModeIfNeeded(FPSModeType(u.fpsMode.Load()))
} }
func (u *UserInterface) ScheduleFrame() { func (u *UserInterface) ScheduleFrame() {
if u.renderRequester != nil && FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum { if u.renderer != nil && FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum {
u.renderRequester.RequestRenderIfNeeded() u.renderer.RequestRenderIfNeeded()
} }
} }

View File

@ -30,6 +30,7 @@ import (
"sync" "sync"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
"github.com/hajimehoshi/ebiten/v2/internal/ui" "github.com/hajimehoshi/ebiten/v2/internal/ui"
) )
@ -112,16 +113,20 @@ func Resume() error {
return ui.Get().SetForeground(true) return ui.Get().SetForeground(true)
} }
func OnContextLost() {
restorable.OnContextLost()
}
func DeviceScale() float64 { func DeviceScale() float64 {
return ui.Get().Monitor().DeviceScaleFactor() return ui.Get().Monitor().DeviceScaleFactor()
} }
type RenderRequester interface { type Renderer interface {
ui.RenderRequester ui.Renderer
} }
func SetRenderRequester(renderRequester RenderRequester) { func SetRenderer(renderer Renderer) {
ui.Get().SetRenderRequester(renderRequester) ui.Get().SetRenderer(renderer)
} }
func SetSetGameNotifier(setGameNotifier SetGameNotifier) { 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 is an instance name in the ICCCM WM_CLASS window property.
X11InstanceName string 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. // 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 options.X11InstanceName = defaultX11InstanceName
} }
return &ui.RunOptions{ return &ui.RunOptions{
GraphicsLibrary: ui.GraphicsLibrary(options.GraphicsLibrary), GraphicsLibrary: ui.GraphicsLibrary(options.GraphicsLibrary),
InitUnfocused: options.InitUnfocused, InitUnfocused: options.InitUnfocused,
ScreenTransparent: options.ScreenTransparent, ScreenTransparent: options.ScreenTransparent,
SkipTaskbar: options.SkipTaskbar, SkipTaskbar: options.SkipTaskbar,
SingleThread: options.SingleThread, SingleThread: options.SingleThread,
DisableHiDPI: options.DisableHiDPI, DisableHiDPI: options.DisableHiDPI,
ColorSpace: graphicsdriver.ColorSpace(options.ColorSpace), ColorSpace: graphicsdriver.ColorSpace(options.ColorSpace),
X11ClassName: options.X11ClassName, X11ClassName: options.X11ClassName,
X11InstanceName: options.X11InstanceName, X11InstanceName: options.X11InstanceName,
StrictContextRestoration: options.StrictContextRestration,
} }
} }