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
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,15 +113,9 @@ func RestoreIfNeeded(graphicsDriver graphicsdriver.Graphics) error {
return nil
}
if !forceRestoring {
var r bool
// TODO: Detect context lost explicitly on Android.
if !r {
if !forceRestoring && !theImages.contextLost.Load() {
return nil
}
}
if err := graphicscommand.ResetGraphicsDriverState(graphicsDriver); err != nil {
return err
@ -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

@ -181,6 +181,7 @@ type RunOptions struct {
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 (
@ -98,7 +99,10 @@ type userInterfaceImpl struct {
touches []TouchForInput
fpsMode atomic.Int32
renderRequester RenderRequester
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) {

19
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.
@ -725,6 +743,7 @@ func toUIRunOptions(options *RunGameOptions) *ui.RunOptions {
ColorSpace: graphicsdriver.ColorSpace(options.ColorSpace),
X11ClassName: options.X11ClassName,
X11InstanceName: options.X11InstanceName,
StrictContextRestoration: options.StrictContextRestration,
}
}