cmd/ebitenmobile: bug fix: accessing a view property caused deadlock on iOS

This change delays the initialization of the view until viewDidLoad is
called AND mobile.SetGame is called.

Closes #2768
This commit is contained in:
Hajime Hoshi 2023-09-21 18:39:35 +09:00
parent 0898906dfb
commit dd69387e15
2 changed files with 106 additions and 7 deletions

View File

@ -20,7 +20,7 @@
#import "Ebitenmobileview.objc.h" #import "Ebitenmobileview.objc.h"
@interface {{.PrefixUpper}}EbitenViewController : UIViewController<EbitenmobileviewRenderRequester> @interface {{.PrefixUpper}}EbitenViewController : UIViewController<EbitenmobileviewRenderRequester, EbitenmobileviewSetGameNotifier>
@end @end
@implementation {{.PrefixUpper}}EbitenViewController { @implementation {{.PrefixUpper}}EbitenViewController {
@ -32,6 +32,28 @@
CADisplayLink* displayLink_; CADisplayLink* displayLink_;
bool explicitRendering_; bool explicitRendering_;
NSThread* renderThread_; NSThread* renderThread_;
bool viewDidLoad_;
bool gameSet_;
}
- (id)initWithNibName:(NSString *)nibNameOrNil
bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil
bundle:nibBundleOrNil];
if (self) {
EbitenmobileviewSetSetGameNotifier(self);
}
return self;
}
- (id)initWithCoder:(NSCoder *)coder {
// Though initWithCoder might not be a designated initializer, this should be overwritten.
// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Archiving/Articles/codingobjects.html
self = [super initWithCoder:coder];
if (self) {
EbitenmobileviewSetSetGameNotifier(self);
}
return self;
} }
- (UIView*)metalView { - (UIView*)metalView {
@ -53,6 +75,18 @@
- (void)viewDidLoad { - (void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
viewDidLoad_ = true;
if (viewDidLoad_ && gameSet_) {
[self initView];
}
}
- (void)initView {
// initView must be called only when viewDidLoad_, and gameSet_ are true i.e. mobile.SetGame is called.
// Or, EbitenmobileviewIsGL causes a dead lock (#2768).
// A game is requried to determine a graphics driver, and EbitenmobileviewIsGL cannot return a value without a game.
NSAssert(viewDidLoad_ && gameSet_, @"viewDidLoad must be called and a game must be set at initView");
if (!started_) { if (!started_) {
@synchronized(self) { @synchronized(self) {
active_ = true; active_ = true;
@ -122,6 +156,10 @@
} }
- (void)viewWillLayoutSubviews { - (void)viewWillLayoutSubviews {
if (!started_) {
return;
}
NSError* err = nil; NSError* err = nil;
BOOL isGL = NO; BOOL isGL = NO;
EbitenmobileviewIsGL(&isGL, &err); EbitenmobileviewIsGL(&isGL, &err);
@ -143,6 +181,11 @@
- (void)viewDidLayoutSubviews { - (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews]; [super viewDidLayoutSubviews];
if (!started_) {
return;
}
CGRect viewRect = [[self view] frame]; CGRect viewRect = [[self view] frame];
EbitenmobileviewLayout(viewRect.size.width, viewRect.size.height); EbitenmobileviewLayout(viewRect.size.width, viewRect.size.height);
@ -217,6 +260,10 @@
} }
- (void)updateTouches:(NSSet*)touches { - (void)updateTouches:(NSSet*)touches {
if (!started_) {
return;
}
NSError* err = nil; NSError* err = nil;
BOOL isGL = NO; BOOL isGL = NO;
EbitenmobileviewIsGL(&isGL, &err); EbitenmobileviewIsGL(&isGL, &err);
@ -260,6 +307,10 @@
} }
- (void)updatePresses:(NSSet<UIPress *> *)presses { - (void)updatePresses:(NSSet<UIPress *> *)presses {
if (!started_) {
return;
}
if (@available(iOS 13.4, *)) { if (@available(iOS 13.4, *)) {
// Note: before iOS 13.4, this just can return UIPressType, which is // Note: before iOS 13.4, this just can return UIPressType, which is
// insufficient for games. // insufficient for games.
@ -282,7 +333,9 @@
} }
- (void)suspendGame { - (void)suspendGame {
NSAssert(started_, @"suspendGame must not be called before viewDidLoad is called"); if (!started_) {
return;
}
@synchronized(self) { @synchronized(self) {
active_ = false; active_ = false;
@ -296,7 +349,9 @@
} }
- (void)resumeGame { - (void)resumeGame {
NSAssert(started_, @"resumeGame must not be called before viewDidLoad is called"); if (!started_) {
return;
}
@synchronized(self) { @synchronized(self) {
active_ = true; active_ = true;
@ -328,4 +383,13 @@
} }
} }
- (void)notifySetGame {
dispatch_async(dispatch_get_main_queue(), ^{
gameSet_ = true;
if (viewDidLoad_ && gameSet_) {
[self initView];
}
});
}
@end @end

View File

@ -27,7 +27,7 @@ import "C"
import ( import (
"runtime" "runtime"
"sync/atomic" "sync"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/internal/devicescale" "github.com/hajimehoshi/ebiten/v2/internal/devicescale"
@ -35,18 +35,49 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/ui" "github.com/hajimehoshi/ebiten/v2/internal/ui"
) )
type SetGameNotifier interface {
NotifySetGame()
}
var theState state var theState state
type state struct { type state struct {
running int32 running bool
setGameNotifier SetGameNotifier
m sync.Mutex
} }
func (s *state) isRunning() bool { func (s *state) isRunning() bool {
return atomic.LoadInt32(&s.running) != 0 s.m.Lock()
defer s.m.Unlock()
return s.running
} }
func (s *state) run() { func (s *state) run() {
atomic.StoreInt32(&s.running, 1) s.m.Lock()
s.running = true
n := s.setGameNotifier
s.setGameNotifier = nil
s.m.Unlock()
if n != nil {
n.NotifySetGame()
}
}
func (s *state) setSetGameNotifier(setGameNotifier SetGameNotifier) {
s.m.Lock()
r := s.running
if !r {
s.setGameNotifier = setGameNotifier
}
s.m.Unlock()
// If SetGame is already called, notify this immediately.
if r {
setGameNotifier.NotifySetGame()
}
} }
func SetGame(game ebiten.Game, options *ebiten.RunGameOptions) { func SetGame(game ebiten.Game, options *ebiten.RunGameOptions) {
@ -99,3 +130,7 @@ type RenderRequester interface {
func SetRenderRequester(renderRequester RenderRequester) { func SetRenderRequester(renderRequester RenderRequester) {
ui.Get().SetRenderRequester(renderRequester) ui.Get().SetRenderRequester(renderRequester)
} }
func SetSetGameNotifier(setGameNotifier SetGameNotifier) {
theState.setSetGameNotifier(setGameNotifier)
}