From 35f4884a74643c9e725e4ab61d3bd6f70574d4fd Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Fri, 6 Sep 2024 16:30:20 +0900 Subject: [PATCH] ebiten: add RunGameOptions.StrictContextRestration This reverts commit a30f075896b76b2f6fb1536c490aead650e4157f. This change adds a new option StrictContextRestration to make the restoration optional. Closes #3083 --- .../_files/EbitenSurfaceView.java | 26 +++++-- .../_files/EbitenViewController.m | 8 ++- internal/restorable/export_test.go | 5 ++ internal/restorable/images.go | 67 ++++++++++++++----- internal/ui/ui.go | 19 +++--- internal/ui/ui_mobile.go | 30 ++++++--- mobile/ebitenmobileview/mobile.go | 13 ++-- run.go | 37 +++++++--- 8 files changed, 149 insertions(+), 56 deletions(-) diff --git a/cmd/ebitenmobile/_files/EbitenSurfaceView.java b/cmd/ebitenmobile/_files/EbitenSurfaceView.java index a4340a7f8..000e5f7db 100644 --- a/cmd/ebitenmobile/_files/EbitenSurfaceView.java +++ b/cmd/ebitenmobile/_files/EbitenSurfaceView.java @@ -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) { diff --git a/cmd/ebitenmobile/_files/EbitenViewController.m b/cmd/ebitenmobile/_files/EbitenViewController.m index 33d7ae18d..a5f5cf90c 100644 --- a/cmd/ebitenmobile/_files/EbitenViewController.m +++ b/cmd/ebitenmobile/_files/EbitenViewController.m @@ -20,7 +20,7 @@ #import "Ebitenmobileview.objc.h" -@interface {{.PrefixUpper}}EbitenViewController : UIViewController +@interface {{.PrefixUpper}}EbitenViewController : UIViewController @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; diff --git a/internal/restorable/export_test.go b/internal/restorable/export_test.go index 10071b879..655b7ed0c 100644 --- a/internal/restorable/export_test.go +++ b/internal/restorable/export_test.go @@ -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) } diff --git a/internal/restorable/images.go b/internal/restorable/images.go index c1a34c792..5bfdf8282 100644 --- a/internal/restorable/images.go +++ b/internal/restorable/images.go @@ -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) +} diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 7d1436a7a..ce7e48ee2 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -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. diff --git a/internal/ui/ui_mobile.go b/internal/ui/ui_mobile.go index c1dd426b8..be0ed62bb 100644 --- a/internal/ui/ui_mobile.go +++ b/internal/ui/ui_mobile.go @@ -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() } } diff --git a/mobile/ebitenmobileview/mobile.go b/mobile/ebitenmobileview/mobile.go index 4f00062fe..9c3ad5ff5 100644 --- a/mobile/ebitenmobileview/mobile.go +++ b/mobile/ebitenmobileview/mobile.go @@ -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) { diff --git a/run.go b/run.go index d1f5e3f1a..bb97c5c7f 100644 --- a/run.go +++ b/run.go @@ -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, } }