From 3608aac5cac6258e5cde20dedf3f302e9239ce32 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Thu, 21 Sep 2023 18:39:35 +0900 Subject: [PATCH] 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 --- .../_files/EbitenViewController.m | 66 ++++++++++++++++++- mobile/ebitenmobileview/mobile.go | 43 ++++++++++-- 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/cmd/ebitenmobile/_files/EbitenViewController.m b/cmd/ebitenmobile/_files/EbitenViewController.m index 928b60b12..bb1c50afd 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 { @@ -32,6 +32,28 @@ CADisplayLink* displayLink_; bool explicitRendering_; 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 { @@ -53,6 +75,18 @@ - (void)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_) { @synchronized(self) { active_ = true; @@ -122,6 +156,10 @@ } - (void)viewWillLayoutSubviews { + if (!started_) { + return; + } + NSError* err = nil; BOOL isGL = NO; EbitenmobileviewIsGL(&isGL, &err); @@ -143,6 +181,11 @@ - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; + + if (!started_) { + return; + } + CGRect viewRect = [[self view] frame]; EbitenmobileviewLayout(viewRect.size.width, viewRect.size.height); @@ -217,6 +260,10 @@ } - (void)updateTouches:(NSSet*)touches { + if (!started_) { + return; + } + NSError* err = nil; BOOL isGL = NO; EbitenmobileviewIsGL(&isGL, &err); @@ -260,7 +307,9 @@ } - (void)suspendGame { - NSAssert(started_, @"suspendGame must not be called before viewDidLoad is called"); + if (!started_) { + return; + } @synchronized(self) { active_ = false; @@ -274,7 +323,9 @@ } - (void)resumeGame { - NSAssert(started_, @"resumeGame must not be called before viewDidLoad is called"); + if (!started_) { + return; + } @synchronized(self) { active_ = true; @@ -306,4 +357,13 @@ } } +- (void)notifySetGame { + dispatch_async(dispatch_get_main_queue(), ^{ + gameSet_ = true; + if (viewDidLoad_ && gameSet_) { + [self initView]; + } + }); +} + @end diff --git a/mobile/ebitenmobileview/mobile.go b/mobile/ebitenmobileview/mobile.go index 692f1037d..8b8eedf83 100644 --- a/mobile/ebitenmobileview/mobile.go +++ b/mobile/ebitenmobileview/mobile.go @@ -27,7 +27,7 @@ import "C" import ( "runtime" - "sync/atomic" + "sync" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/internal/devicescale" @@ -35,18 +35,49 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/ui" ) +type SetGameNotifier interface { + NotifySetGame() +} + var theState state type state struct { - running int32 + running bool + setGameNotifier SetGameNotifier + + m sync.Mutex } 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() { - 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) { @@ -99,3 +130,7 @@ type RenderRequester interface { func SetRenderRequester(renderRequester RenderRequester) { ui.Get().SetRenderRequester(renderRequester) } + +func SetSetGameNotifier(setGameNotifier SetGameNotifier) { + theState.setSetGameNotifier(setGameNotifier) +}