git.y1.nz

SameBoy

Accurate GB/GBC emulator
download: https://git.y1.nz/archives/sameboy.tar.gz
README | Files | Log | Refs | LICENSE

iOS/GBViewController.m

      1 #import "GBViewController.h"
      2 #import "GBHorizontalLayout.h"
      3 #import "GBVerticalLayout.h"
      4 #import "GBViewMetal.h"
      5 #import "GBAudioClient.h"
      6 #import "GBROMManager.h"
      7 #import "GBLibraryViewController.h"
      8 #import "GBBackgroundView.h"
      9 #import "GBHapticManager.h"
     10 #import "GBMenuViewController.h"
     11 #import "GBOptionViewController.h"
     12 #import "GBAboutController.h"
     13 #import "GBSettingsViewController.h"
     14 #import "GBPalettePicker.h"
     15 #import "GBStatesViewController.h"
     16 #import "GBCheckableAlertController.h"
     17 #import "GBPrinterFeedController.h"
     18 #import "GBCheatsController.h"
     19 #import "UILabel+LockFonts.h"
     20 #import <CommonCrypto/CommonCrypto.h>
     21 #include <sys/xattr.h>
     22 #import "GCControllerGetElements.h"
     23 #import "GBZipReader.h"
     24 #import <sys/stat.h>
     25 #import <CoreMotion/CoreMotion.h>
     26 #import <dlfcn.h>
     27 #import <objc/runtime.h>
     28 
     29 #if !__has_include(<UIKit/UISliderTrackConfiguration.h>)
     30 /* Building with older SDKs */
     31 
     32 typedef NS_ENUM(NSInteger, UIMenuSystemElementGroupPreference) {
     33     UIMenuSystemElementGroupPreferenceAutomatic = 0,
     34     UIMenuSystemElementGroupPreferenceRemoved,
     35     UIMenuSystemElementGroupPreferenceIncluded,
     36 };
     37 
     38 API_AVAILABLE(ios(19.0))
     39 @interface UIMainMenuSystemConfiguration : NSObject <NSCopying>
     40 @property (nonatomic, assign) UIMenuSystemElementGroupPreference newScenePreference;
     41 @property (nonatomic, assign) UIMenuSystemElementGroupPreference documentPreference;
     42 @property (nonatomic, assign) UIMenuSystemElementGroupPreference printingPreference;
     43 @property (nonatomic, assign) UIMenuSystemElementGroupPreference findingPreference;
     44 @property (nonatomic, assign) UIMenuSystemElementGroupPreference toolbarPreference;
     45 @property (nonatomic, assign) UIMenuSystemElementGroupPreference sidebarPreference;
     46 @property (nonatomic, assign) UIMenuSystemElementGroupPreference inspectorPreference;
     47 @property (nonatomic, assign) UIMenuSystemElementGroupPreference textFormattingPreference;
     48 @end
     49 
     50 API_AVAILABLE(ios(19.0))
     51 @interface UIMainMenuSystem : UIMenuSystem
     52 @property (class, nonatomic, readonly) UIMainMenuSystem *sharedSystem;
     53 - (void)setBuildConfiguration:(UIMainMenuSystemConfiguration *)configuration buildHandler:(void(^)(NSObject<UIMenuBuilder> *builder))buildHandler;
     54 @end
     55 
     56 API_AVAILABLE(ios(19.0))
     57 @interface NSObject(UIMenuBuilder)
     58 - (void)insertElements:(NSArray<UIMenuElement *> *)childElements atStartOfMenuForIdentifier:(UIMenuIdentifier)parentIdentifier;
     59 @end
     60 
     61 #endif
     62 
     63 
     64 static UIImage *CreateMenuImage(NSString *name)
     65 {
     66     static const unsigned size = 20;
     67     UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(size, size)];
     68     CGRect destRect = {0,};
     69     UIImage *source = [UIImage imageNamed:name];
     70     CGSize sourceSize = source.size;
     71     if (sourceSize.width > sourceSize.height) {
     72         destRect.size.width = size;
     73         destRect.size.height = sourceSize.height * size / sourceSize.width;
     74         destRect.origin.y = (size - destRect.size.height) / 2;
     75     }
     76     else {
     77         destRect.size.height = size;
     78         destRect.size.width = sourceSize.width * size / sourceSize.height;
     79         destRect.origin.x = (size - destRect.size.width) / 2;
     80     }
     81     UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *myContext) {
     82         [source drawInRect:destRect];
     83     }];
     84     return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
     85 }
     86 
     87 API_AVAILABLE(ios(13.0))
     88 @implementation UIKeyCommand (KeyCommandWithImage)
     89 
     90 + (instancetype)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action title:(NSString *)title image:(UIImage *)image
     91 {
     92     UIKeyCommand *ret = [self keyCommandWithInput:input modifierFlags:modifierFlags action:action];
     93     ret.title = title;
     94     ret.image = image;
     95     return ret;
     96 }
     97 
     98 @end
     99 
    100 @implementation GBViewController
    101 {
    102     GB_gameboy_t _gb;
    103     GBView *_gbView;
    104     dispatch_queue_t _runQueue;
    105     
    106     volatile bool _running;
    107     volatile bool _stopping;
    108     bool _rewind;
    109     bool _rewindOver;
    110     bool _romLoaded;
    111     bool _swappingROM;
    112     bool _skipAutoLoad;
    113     
    114     bool _rapidA, _rapidB;
    115     uint8_t _rapidACount, _rapidBCount;
    116     
    117     UIInterfaceOrientation _orientation;
    118     GBHorizontalLayout *_horizontalLayoutLeft;
    119     GBHorizontalLayout *_horizontalLayoutRight;
    120     GBVerticalLayout *_verticalLayout;
    121     GBBackgroundView *_backgroundView;
    122     
    123     NSCondition *_audioLock;
    124     GB_sample_t *_audioBuffer;
    125     size_t _audioBufferSize;
    126     size_t _audioBufferPosition;
    127     size_t _audioBufferNeeded;
    128     GBAudioClient *_audioClient;
    129     
    130     NSMutableSet *_defaultsObservers;
    131     GB_palette_t _palette;
    132     CMMotionManager *_motionManager;
    133     
    134     CVImageBufferRef _cameraImage;
    135     AVCaptureSession *_cameraSession;
    136     AVCaptureConnection *_cameraConnection;
    137     AVCaptureVideoDataOutput *_cameraOutput;
    138     bool _cameraNeedsUpdate;
    139     NSTimer *_disableCameraTimer;
    140     AVCaptureDevicePosition _cameraPosition;
    141     UIButton *_cameraPositionButton;
    142     UIButton *_changeCameraButton;
    143     AVCaptureDevice *_frontCaptureDevice;
    144     AVCaptureDevice *_backCaptureDevice;
    145     NSMutableArray<NSNumber *> *_zoomLevels;
    146     unsigned _currentZoomIndex;
    147     
    148     __weak GCController *_lastController;
    149     
    150     dispatch_queue_t _cameraQueue;
    151     
    152     bool _runModeFromController;
    153     
    154     UIWindow *_mirrorWindow;
    155     GBView *_mirrorView;
    156     
    157     bool _printerConnected;
    158     UIButton *_printerButton;
    159     UIActivityIndicatorView *_printerSpinner;
    160     NSMutableData *_currentPrinterImageData;
    161     
    162     NSString *_lastSavedROM;
    163     NSDate *_saveDate;
    164     
    165     unsigned _autosaveCountdown;
    166 }
    167 
    168 static void loadBootROM(GB_gameboy_t *gb, GB_boot_rom_t type)
    169 {
    170     GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
    171     [self loadBootROM:type];
    172 }
    173 
    174 static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type)
    175 {
    176     GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
    177     [self vblankWithType:type];
    178 }
    179 
    180 
    181 static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height,
    182                        uint8_t top_margin, uint8_t bottom_margin, uint8_t exposure)
    183 {
    184     GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
    185     [self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure];
    186 }
    187 
    188 static void printDone(GB_gameboy_t *gb)
    189 {
    190     GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
    191     [self printDone];
    192 }
    193 
    194 
    195 static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes)
    196 {
    197     static NSString *buffer = @"";
    198     buffer = [buffer stringByAppendingString:@(string)];
    199     if ([buffer containsString:@"\n"]) {
    200         NSLog(@"%@", buffer);
    201         buffer = @"";
    202     }
    203 }
    204 
    205 static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b)
    206 {
    207     return (r << 0) | (g << 8) | (b << 16) | 0xFF000000;
    208 }
    209 
    210 static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample)
    211 {
    212     GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
    213     [self gotNewSample:sample];
    214 }
    215 
    216 static void cameraRequestUpdate(GB_gameboy_t *gb)
    217 {
    218     GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
    219     [self cameraRequestUpdate];
    220 }
    221 
    222 static uint8_t cameraGetPixel(GB_gameboy_t *gb, uint8_t x, uint8_t y)
    223 {
    224     GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
    225     return [self cameraGetPixelAtX:x andY:y];
    226 }
    227 
    228 
    229 static void rumbleCallback(GB_gameboy_t *gb, double amp)
    230 {
    231     GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
    232     double strength = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBRumbleStrength"];
    233     if (strength != 1) {
    234         amp = pow(amp, strength) * strength;
    235     }
    236     [self rumbleChanged:amp];
    237 }
    238 
    239 - (void)initGameBoy
    240 {
    241     GB_gameboy_t *gb = &_gb;
    242     GB_init(gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]);
    243     GB_set_user_data(gb, (__bridge void *)(self));
    244     GB_set_boot_rom_load_callback(gb, (GB_boot_rom_load_callback_t)loadBootROM);
    245     GB_set_vblank_callback(gb, (GB_vblank_callback_t) vblank);
    246     GB_set_log_callback(gb, (GB_log_callback_t) consoleLog);
    247     GB_set_camera_get_pixel_callback(gb, cameraGetPixel);
    248     GB_set_camera_update_request_callback(gb, cameraRequestUpdate);
    249     [self addDefaultObserver:^(id newValue) {
    250         GB_set_color_correction_mode(gb, (GB_color_correction_mode_t)[newValue integerValue]);
    251     } forKey:@"GBColorCorrection"];
    252     [self addDefaultObserver:^(id newValue) {
    253         GB_set_light_temperature(gb, [newValue doubleValue]);
    254     } forKey:@"GBLightTemperature"];
    255     GB_set_border_mode(gb, GB_BORDER_NEVER);
    256     __weak typeof(self) weakSelf = self;
    257     [self addDefaultObserver:^(id newValue) {
    258         [weakSelf updatePalette];
    259     } forKey:@"GBCurrentTheme"];
    260     GB_set_rgb_encode_callback(gb, rgbEncode);
    261     [self addDefaultObserver:^(id newValue) {
    262         GB_set_highpass_filter_mode(gb, (GB_highpass_mode_t)[newValue integerValue]);
    263     } forKey:@"GBHighpassFilter"];
    264     [self addDefaultObserver:^(id newValue) {
    265         GB_set_rtc_mode(gb, [newValue integerValue]);
    266     } forKey:@"GBRTCMode"];
    267     GB_apu_set_sample_callback(gb, audioCallback);
    268     GB_set_rumble_callback(gb, rumbleCallback);
    269     [self addDefaultObserver:^(id newValue) {
    270         GB_set_rumble_mode(gb, [newValue integerValue]);
    271     } forKey:@"GBRumbleMode"];
    272     [self addDefaultObserver:^(id newValue) {
    273         GB_set_interference_volume(gb, [newValue doubleValue]);
    274     } forKey:@"GBInterferenceVolume"];
    275     [self addDefaultObserver:^(id newValue) {
    276         GB_set_rewind_length(gb, [newValue unsignedIntValue]);
    277     } forKey:@"GBRewindLength"];
    278     [self addDefaultObserver:^(id newValue) {
    279         GB_set_turbo_cap(gb, [newValue doubleValue]);
    280     } forKey:@"GBTurboCap"];
    281     [self addDefaultObserver:^(id newValue) {
    282         [[AVAudioSession sharedInstance] setCategory:[newValue isEqual:@"on"]? AVAudioSessionCategoryPlayback :  AVAudioSessionCategorySoloAmbient
    283                                                 mode:AVAudioSessionModeDefault
    284                                   routeSharingPolicy:AVAudioSessionRouteSharingPolicyDefault
    285                                              options:0
    286                                                error:nil];
    287     } forKey:@"GBAudioMode"];
    288 }
    289 
    290 - (void)addDefaultObserver:(void(^)(id newValue))block forKey:(NSString *)key
    291 {
    292     if (!_defaultsObservers) {
    293         _defaultsObservers = [NSMutableSet set];
    294     }
    295     block = [block copy];
    296     [_defaultsObservers addObject:block];
    297     [[NSUserDefaults standardUserDefaults] addObserver:self
    298                                             forKeyPath:key
    299                                                options:NSKeyValueObservingOptionNew
    300                                                context:(void *)block];
    301     block([[NSUserDefaults standardUserDefaults] objectForKey:key]);
    302 }
    303 
    304 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
    305 {
    306     ((__bridge void(^)(id))context)(change[NSKeyValueChangeNewKey]);
    307 }
    308 
    309 - (NSArray<NSNumber *> *)zoomFactorsForDevice:(AVCaptureDevice *)device
    310 {
    311     if (@available(iOS 13.0, *)) {
    312         return device.virtualDeviceSwitchOverVideoZoomFactors;
    313     }
    314     double factor = device.dualCameraSwitchOverVideoZoomFactor;
    315     if (factor == 1.0) {
    316         return @[];
    317     }
    318     return @[@(factor)];
    319 }
    320 
    321 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    322 {
    323     _window = [[UIWindow alloc] init];
    324     _window.rootViewController = self;
    325     [_window makeKeyAndVisible];
    326     
    327     _runQueue = dispatch_queue_create("SameBoy Emulation Queue", NULL);
    328     
    329 #pragma clang diagnostic push
    330 #pragma clang diagnostic ignored "-Warc-retain-cycles"
    331     [self addDefaultObserver:^(id newValue) {
    332         GBTheme *theme = [GBSettingsViewController themeNamed:newValue];
    333         _horizontalLayoutLeft = [[GBHorizontalLayout alloc] initWithTheme:theme cutoutOnRight:false];
    334         _horizontalLayoutRight = _horizontalLayoutLeft.cutout?
    335             [[GBHorizontalLayout alloc] initWithTheme:theme cutoutOnRight:true] :
    336             _horizontalLayoutLeft;
    337         _verticalLayout = [[GBVerticalLayout alloc] initWithTheme:theme];
    338         _printerSpinner.color = theme.buttonColor;
    339 
    340         [self willRotateToInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation
    341                                       duration:0];
    342         [_backgroundView reloadThemeImages];
    343         
    344         [self setNeedsStatusBarAppearanceUpdate];
    345     } forKey:@"GBInterfaceTheme"];
    346 #pragma clang diagnostic pop
    347     
    348     _backgroundView = [[GBBackgroundView alloc] initWithLayout:_verticalLayout];
    349     [_window addSubview:_backgroundView];
    350     self.view = _backgroundView;
    351     
    352     
    353     [self initGameBoy];
    354     _gbView = _backgroundView.gbView;
    355     _gbView.hidden = true;
    356     _gbView.gb = &_gb;
    357     [_gbView screenSizeChanged];
    358     
    359     [self addDefaultObserver:^(id newValue) {
    360         [[NSNotificationCenter defaultCenter] postNotificationName:@"GBFilterChanged" object:nil];
    361     } forKey:@"GBFilter"];
    362     
    363     __weak GBView *gbview = _gbView;
    364     [self addDefaultObserver:^(id newValue) {
    365         gbview.frameBlendingMode = [newValue integerValue];
    366     } forKey:@"GBFrameBlendingMode"];
    367     
    368     __weak GBBackgroundView *backgroundView = _backgroundView;
    369     [self addDefaultObserver:^(id newValue) {
    370         backgroundView.usesSwipePad = [newValue boolValue];
    371     } forKey:@"GBSwipeDpad"];
    372     
    373     
    374     [self willRotateToInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation
    375                                   duration:0];
    376     
    377     
    378     _audioLock = [[NSCondition alloc] init];
    379     
    380     [[NSNotificationCenter defaultCenter] addObserverForName:@"GBROMChanged"
    381                                                       object:nil
    382                                                        queue:nil
    383                                                   usingBlock:^(NSNotification *note) {
    384         _swappingROM = true;
    385         [self stop];
    386         [self start];
    387     }];
    388     
    389     _motionManager = [[CMMotionManager alloc] init];
    390     _cameraPosition = AVCaptureDevicePositionBack;
    391 
    392     // Back camera setup
    393     NSArray *deviceTypes = @[AVCaptureDeviceTypeBuiltInWideAngleCamera,
    394                              AVCaptureDeviceTypeBuiltInTelephotoCamera,
    395                              AVCaptureDeviceTypeBuiltInDualCamera];
    396     if (@available(iOS 13.0, *)) {
    397         // AVCaptureDeviceTypeBuiltInUltraWideCamera is only available in iOS 13+
    398         deviceTypes = @[AVCaptureDeviceTypeBuiltInWideAngleCamera,
    399                         AVCaptureDeviceTypeBuiltInUltraWideCamera,
    400                         AVCaptureDeviceTypeBuiltInTelephotoCamera,
    401                         AVCaptureDeviceTypeBuiltInTripleCamera,
    402                         AVCaptureDeviceTypeBuiltInDualWideCamera,
    403                         AVCaptureDeviceTypeBuiltInDualCamera];
    404     }
    405 
    406     // Use a discovery session to gather the capture devices (all back cameras as well as the front camera)
    407     AVCaptureDeviceDiscoverySession *cameraDiscoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
    408                                                                                                                      mediaType:AVMediaTypeVideo
    409                                                                                                                       position:AVCaptureDevicePositionUnspecified];
    410     for (AVCaptureDevice *device in cameraDiscoverySession.devices) {
    411         if ([device position] == AVCaptureDevicePositionBack) {
    412             if (!_backCaptureDevice ||
    413                 [self zoomFactorsForDevice:_backCaptureDevice].count < [self zoomFactorsForDevice:device].count) {
    414                 _backCaptureDevice = device;
    415             }
    416         }
    417         else if ([device position] == AVCaptureDevicePositionFront) {
    418             _frontCaptureDevice = device;
    419         }
    420     }
    421     
    422     _zoomLevels = [self zoomFactorsForDevice:_backCaptureDevice].mutableCopy;
    423     [_zoomLevels insertObject:@1 atIndex:0];
    424     if (_zoomLevels.count == 3 && _zoomLevels[2].doubleValue > 5.5 && _zoomLevels[1].doubleValue < 3.5) {
    425         [_zoomLevels insertObject:@4 atIndex:2];
    426     }
    427 
    428     _cameraPositionButton = [[UIButton alloc] init];
    429     [self didRotateFromInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation];
    430     if (@available(iOS 13.0, *)) {
    431         [_cameraPositionButton  setImage:[UIImage systemImageNamed:@"camera.rotate"
    432                                                  withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]]
    433                                 forState:UIControlStateNormal];
    434         _cameraPositionButton.backgroundColor = [UIColor systemBackgroundColor];
    435 
    436         // Configure the change camera button stacked on top of the camera position button
    437         _changeCameraButton = [[UIButton alloc] init];
    438         [_changeCameraButton  setImage:[UIImage systemImageNamed:@"camera.aperture"
    439                                                withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]]
    440                               forState:UIControlStateNormal];
    441         _changeCameraButton.backgroundColor = [UIColor systemBackgroundColor];
    442         _changeCameraButton.layer.cornerRadius = 6;
    443         _changeCameraButton.alpha = 0;
    444         [_changeCameraButton addTarget:self
    445                                 action:@selector(changeCamera)
    446                       forControlEvents:UIControlEventTouchUpInside];
    447         // Only show the change camera button if we have more than one back camera to swap between.
    448         if (_zoomLevels.count > 1) {
    449             [_backgroundView addSubview:_changeCameraButton];
    450         }
    451     }
    452     else {
    453         UIImage *rotateImage = [[UIImage imageNamed:@"CameraRotateTemplate"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    454         [_cameraPositionButton setImage:rotateImage
    455                                forState:UIControlStateNormal];
    456         _cameraPositionButton.backgroundColor = [UIColor whiteColor];
    457     }
    458 
    459     _cameraPositionButton.layer.cornerRadius = 6;
    460     _cameraPositionButton.alpha = 0;
    461     [_cameraPositionButton addTarget:self
    462                               action:@selector(rotateCamera)
    463                     forControlEvents:UIControlEventTouchUpInside];
    464 
    465     [_backgroundView addSubview:_cameraPositionButton];
    466 
    467     _cameraQueue = dispatch_queue_create("SameBoy Camera Queue", NULL);
    468     
    469     [UNUserNotificationCenter currentNotificationCenter].delegate = self;
    470     [self verifyEntitlements];
    471 
    472     [self setControllerHandlers];
    473     [[NSNotificationCenter defaultCenter] addObserver:self
    474                                              selector:@selector(setControllerHandlers)
    475                                                  name:GCControllerDidConnectNotification
    476                                                object:nil];
    477     
    478     [[NSNotificationCenter defaultCenter] addObserver:self
    479                                              selector:@selector(controllerDisconnected:)
    480                                                  name:GCControllerDidDisconnectNotification
    481                                                object:nil];
    482     
    483     for (NSString *name in @[UIScreenDidConnectNotification,
    484                              UIScreenDidDisconnectNotification,
    485                              UIScreenModeDidChangeNotification]) {
    486         [[NSNotificationCenter defaultCenter] addObserver:self
    487                                                  selector:@selector(updateMirrorWindow)
    488                                                      name:name
    489                                                    object:nil];
    490     }
    491     
    492     _printerButton = [[UIButton alloc] init];
    493     _printerSpinner = [[UIActivityIndicatorView alloc] init];
    494     _printerSpinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite;
    495     _printerSpinner.color = _verticalLayout.theme.buttonColor;
    496     [self didRotateFromInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation];
    497     
    498     if (@available(iOS 13.0, *)) {
    499         [_printerButton  setImage:[UIImage systemImageNamed:@"printer"
    500                                           withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]]
    501                          forState:UIControlStateNormal];
    502         _printerButton.backgroundColor = [UIColor systemBackgroundColor];
    503     }
    504     else {
    505         UIImage *rotateImage = [[UIImage imageNamed:@"PrinterTemplate"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    506         [_printerButton setImage:rotateImage
    507                         forState:UIControlStateNormal];
    508         _printerButton.backgroundColor = [UIColor whiteColor];
    509     }
    510     
    511     _printerButton.layer.cornerRadius = 6;
    512     _printerButton.alpha = 0;
    513     [_printerButton addTarget:self
    514                        action:@selector(showPrinterFeed)
    515              forControlEvents:UIControlEventTouchUpInside];
    516     
    517     
    518     [_backgroundView addSubview:_printerButton];
    519     [_backgroundView addSubview:_printerSpinner];
    520     
    521     [self updateMirrorWindow];
    522     
    523     if (@available(iOS 26.0, *)) {
    524         UIMainMenuSystemConfiguration *conf = [[objc_getClass("UIMainMenuSystemConfiguration") alloc] init];
    525         conf.newScenePreference = UIMenuSystemElementGroupPreferenceRemoved;
    526         conf.documentPreference = UIMenuSystemElementGroupPreferenceRemoved;
    527         conf.printingPreference = UIMenuSystemElementGroupPreferenceRemoved;
    528         conf.findingPreference = UIMenuSystemElementGroupPreferenceRemoved;
    529         conf.toolbarPreference = UIMenuSystemElementGroupPreferenceRemoved;
    530         conf.sidebarPreference = UIMenuSystemElementGroupPreferenceRemoved;
    531         conf.inspectorPreference = UIMenuSystemElementGroupPreferenceRemoved;
    532         conf.textFormattingPreference =  UIMenuSystemElementGroupPreferenceRemoved;
    533         
    534         UIMainMenuSystem *system = (id)[objc_getClass("UIMainMenuSystem") sharedSystem];
    535         [system setBuildConfiguration:conf
    536                          buildHandler:^(id<UIMenuBuilder> builder) {
    537             [builder removeMenuForIdentifier:UIMenuView]; // This menu's always empty
    538             [builder removeMenuForIdentifier:UIMenuOpenRecent]; // This will list files to re-import, bad
    539             
    540             [(id)builder insertElements:@[[UICommand commandWithTitle:@"About SameBoy"
    541                                                                 image:nil
    542                                                                action:@selector(showAbout)
    543                                                          propertyList:nil]]
    544              atStartOfMenuForIdentifier:UIMenuApplication];
    545             
    546             UIMenu *emulationMenu = [UIMenu menuWithTitle:@"Emulation" children:@[
    547                 [UIKeyCommand keyCommandWithInput:@"r" modifierFlags:UIKeyModifierCommand action:@selector(reset) title:@"Reset" image:[UIImage systemImageNamed:@"arrow.2.circlepath"]],
    548                 [UICommand commandWithTitle:@"Change Model…" image:CreateMenuImage(@"ModelTemplate") action:@selector(changeModel) propertyList:nil],
    549                 [UICommand commandWithTitle:@"Save States…" image:[UIImage systemImageNamed:@"square.stack"] action:@selector(openStates) propertyList:nil],
    550                 [UIMenu menuWithTitle:@"Cheats" image:CreateMenuImage(@"CheatsTemplate") identifier:nil options:0 children:@[
    551                     [UIKeyCommand keyCommandWithInput:@"c" modifierFlags:UIKeyModifierCommand | UIKeyModifierShift action:@selector(toggleCheats) title:@"Enable Cheats" image:nil],
    552                     [UICommand commandWithTitle:@"Show Cheats…" image:nil action:@selector(openCheats) propertyList:nil],
    553                 ]],
    554                 [UIMenu menuWithTitle:@"Connect" image:CreateMenuImage(@"LinkCableTemplate") identifier:nil options:0 children:@[
    555                     [UICommand commandWithTitle:@"None" image:nil action:@selector(disconnectCable) propertyList:nil],
    556                     [UICommand commandWithTitle:@"Printer" image:[UIImage systemImageNamed:@"printer"] action:@selector(connectPrinter) propertyList:nil],
    557                 ]],
    558             ]];
    559             [builder insertSiblingMenu:emulationMenu beforeMenuForIdentifier:UIMenuWindow];
    560         }];
    561     }
    562     
    563     return true;
    564 }
    565 
    566 - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
    567 {
    568     if (action == @selector(reset)) {
    569         if (self.presentedViewController || ![GBROMManager sharedManager].currentROM) return false;
    570     }
    571     if (action == @selector(openStates) || action == @selector(changeModel) || action == @selector(openCheats) ||
    572         action == @selector(toggleCheats) || action == @selector(disconnectCable) || action == @selector(connectPrinter)) {
    573         if (![GBROMManager sharedManager].currentROM) return false;
    574     }
    575     return [super canPerformAction:action withSender:sender];
    576 }
    577 
    578 - (void)validateCommand:(UICommand *)command
    579 {
    580     if (command.action == @selector(toggleCheats)) {
    581         command.state = GB_is_inited(&_gb) && GB_cheats_enabled(&_gb);
    582     }
    583     
    584     if (command.action == @selector(connectPrinter)) {
    585         command.state = _printerConnected;
    586     }
    587     
    588     if (command.action == @selector(disconnectCable)) {
    589         command.state = !_printerConnected;
    590     }
    591     
    592     [super validateCommand:command];
    593 }
    594 
    595 - (void)toggleCheats
    596 {
    597     GB_set_cheats_enabled(&_gb, !GB_cheats_enabled(&_gb));
    598 }
    599 
    600 - (void)orderFrontPreferencesPanel:(id)sender
    601 {
    602     [self openSettings];
    603 }
    604 
    605 - (void)open:(id)sender
    606 {
    607     [self openLibrary];
    608 }
    609 
    610 - (void)updateMirrorWindow
    611 {
    612     if ([UIScreen screens].count == 1) {
    613         _mirrorWindow = nil;
    614         _mirrorView = nil;
    615         return;
    616     }
    617     if (_mirrorWindow && ![[UIScreen screens] containsObject:_mirrorWindow.screen]) {
    618         _mirrorWindow = nil;
    619         _mirrorView = nil;
    620     }
    621     for (UIScreen *screen in [UIScreen screens]) {
    622         if (screen == UIScreen.mainScreen) continue;
    623         CGRect rect = screen.bounds;
    624         rect.size.height = floor(rect.size.height / 144) * 144;
    625         rect.size.width = rect.size.height / 144 * 160;
    626         rect.origin.x = (screen.bounds.size.width - rect.size.width) / 2;
    627         rect.origin.y = (screen.bounds.size.height - rect.size.height) / 2;
    628         _mirrorWindow = [[UIWindow alloc] initWithFrame:screen.bounds];
    629         _mirrorWindow.screen = screen;
    630         _mirrorView = [_gbView mirroredView];
    631         _mirrorView.frame = rect;
    632         _mirrorWindow.backgroundColor = [UIColor blackColor];
    633         [_mirrorWindow addSubview:_mirrorView];
    634         [_mirrorWindow setHidden:false];
    635         break;
    636     }
    637 }
    638 
    639 - (void)controllerDisconnected:(NSNotification *)notification
    640 {
    641     if (notification.object == _lastController) {
    642         _backgroundView.fullScreenMode = GBControllerFocusOff;
    643     }
    644 }
    645 
    646 - (void)setControllerHandlers
    647 {
    648     for (GCController *controller in [GCController controllers]) {
    649         __weak GCController *weakController = controller;
    650         NSDictionary <NSNumber *, GCControllerElement *> *elements = GCControllerGetElements(controller);
    651         if (!elements) continue; // Controller not supported
    652         [elements enumerateKeysAndObjectsUsingBlock:^(NSNumber *usage, GCControllerElement *element, BOOL *stop) {
    653             if ([element isKindOfClass:[GCControllerButtonInput class]]) {
    654                 [(GCControllerButtonInput *)element setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) {
    655                     [self controller:weakController buttonChanged:button usage:usage.unsignedIntValue];
    656                 }];
    657             }
    658             else if ([element isKindOfClass:[GCControllerDirectionPad class]]) {
    659                 NSMutableSet *childrenUsages = [NSMutableSet set];
    660                 [elements enumerateKeysAndObjectsUsingBlock:^(NSNumber *childUsage, GCControllerElement *child, BOOL *stop) {
    661                     if (child.collection == element) {
    662                         [childrenUsages addObject:childUsage];
    663                     }
    664                 }];
    665                 [(GCControllerDirectionPad *)element setValueChangedHandler:^(GCControllerDirectionPad *dpad, float xValue, float yValue) {
    666                     [self controller:weakController axisChanged:dpad usage:usage.unsignedIntValue childrenUsages:childrenUsages];
    667                 }];
    668             }
    669         }];
    670         
    671         if (controller.motion) {
    672             [controller.motion setValueChangedHandler:^(GCMotion *motion) {
    673                 [self controller:weakController motionChanged:motion];
    674             }];
    675         }
    676     }
    677 }
    678 
    679 - (void)updateLastController:(GCController *)controller
    680 {
    681     if (_lastController == controller) return;
    682     _lastController = controller;
    683     [GBHapticManager sharedManager].controller = controller;
    684 }
    685 
    686 - (void)controller:(GCController *)controller buttonChanged:(GCControllerButtonInput *)button usage:(GBControllerUsage)usage
    687 {
    688     [self updateLastController:controller];
    689     if (_running && button.value > 0.25) {
    690         _backgroundView.fullScreenMode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBControllersHideInterface"];
    691     }
    692     
    693     GBButton gbButton = [GBSettingsViewController controller:controller convertUsageToButton:usage];
    694     static const double analogThreshold = 0.0625;
    695     switch (gbButton) {
    696         case GBRight:
    697         case GBLeft:
    698         case GBUp:
    699         case GBDown:
    700             GB_set_use_faux_analog_inputs(&_gb, 0, false);
    701         case GBA:
    702         case GBB:
    703         case GBSelect:
    704         case GBStart:
    705             GB_set_key_state(&_gb, (GB_key_t)gbButton, button.value > 0.25);
    706             if (_runMode == GBRunModeRewind || _runMode == GBRunModePaused) {
    707                 self.runMode = GBRunModeNormal;
    708                 [_backgroundView fadeOverlayOut];
    709             }
    710             break;
    711         case GBRapidA:
    712             _rapidA = button.value > 0.25;
    713             _rapidACount = 0;
    714             break;
    715         case GBRapidB:
    716             _rapidB = button.value > 0.25;
    717             _rapidBCount = 0;
    718             break;
    719         case GBTurbo:
    720             if (button.value > analogThreshold) {
    721                 [self setRunMode:GBRunModeTurbo ignoreDynamicSpeed:!button.isAnalog];
    722                 if (button.isAnalog && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
    723                     GB_set_clock_multiplier(&_gb, (button.value - analogThreshold) / (1 - analogThreshold) * 3 + 1);
    724                 }
    725                 _runModeFromController = true;
    726                 [_backgroundView fadeOverlayOut];
    727             }
    728             else {
    729                 if (self.runMode == GBRunModeTurbo && _runModeFromController) {
    730                     [self setRunMode:GBRunModeNormal];
    731                     _runModeFromController = false;
    732                 }
    733             }
    734             break;
    735         case GBRewind:
    736             if (button.value > analogThreshold) {
    737                 [self setRunMode:GBRunModeRewind ignoreDynamicSpeed:!button.isAnalog];
    738                 if (button.isAnalog && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
    739                     GB_set_clock_multiplier(&_gb, (button.value - analogThreshold) / (1 - analogThreshold) * 4);
    740                 }
    741                 _runModeFromController = true;
    742                 [_backgroundView fadeOverlayOut];
    743             }
    744             else {
    745                 if ((self.runMode == GBRunModeRewind || self.runMode == GBRunModePaused) && _runModeFromController) {
    746                     [self setRunMode:GBRunModeNormal];
    747                     _runModeFromController = false;
    748                 }
    749             }
    750             break;
    751         case GBUnderclock:
    752             if (button.value > analogThreshold) {
    753                 [self setRunMode:GBRunModeUnderclock ignoreDynamicSpeed:!button.isAnalog];
    754                 if (button.isAnalog && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
    755                     GB_set_clock_multiplier(&_gb, 1 - ((button.value - analogThreshold) / (1 - analogThreshold) * 0.75));
    756                 }
    757                 _runModeFromController = true;
    758                 [_backgroundView fadeOverlayOut];
    759             }
    760             else {
    761                 if (self.runMode == GBRunModeUnderclock && _runModeFromController) {
    762                     [self setRunMode:GBRunModeNormal];
    763                     _runModeFromController = false;
    764                 }
    765             }
    766             break;
    767         case GBSaveState1:
    768             if (_romLoaded) {
    769                 [_backgroundView saveSwipeFromController:true];
    770             }
    771             break;
    772         case GBLoadState1:
    773             if (_romLoaded) {
    774                 [_backgroundView loadSwipeFromController:true];
    775             }
    776             break;
    777         case GBReset:
    778             if (_romLoaded) {
    779                 [self stop];
    780                 _skipAutoLoad = true;
    781                 GB_reset(&_gb);
    782                 [self start];
    783             }
    784             break;
    785         case GBOpenMenu:
    786             self.window.backgroundColor = nil;
    787             [self presentViewController:[GBMenuViewController menu] animated:true completion:nil];
    788             break;
    789         default: break;
    790     }
    791 }
    792 
    793 - (void)controller:(GCController *)controller axisChanged:(GCControllerDirectionPad *)axis usage:(GBControllerUsage)usage childrenUsages:(NSSet *)childrenUsages
    794 {
    795     [self updateLastController:controller];
    796     bool left = axis.left.value > 0.5;
    797     bool right = axis.right.value > 0.5;
    798     bool up = axis.up.value > 0.5;
    799     bool down = axis.down.value > 0.5;
    800     
    801     bool hasUnmappedChild = false;
    802     for (NSNumber *childUsage in childrenUsages) {
    803         if ([GBSettingsViewController controller:controller convertUsageToButton:childUsage.unsignedIntValue] != GBUnusedButton) {
    804             GCControllerButtonInput *child = (id)(GCControllerGetElements(controller)[childUsage]);
    805             if ([child isKindOfClass:[GCControllerButtonInput class]] && child.pressed) {
    806                 left = right = up = down = false;
    807             }
    808         }
    809         else {
    810             hasUnmappedChild = true;
    811         }
    812     }
    813     
    814     if (!hasUnmappedChild) return;
    815     
    816     if (_running && (left || right || up || down)) {
    817         _backgroundView.fullScreenMode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBControllersHideInterface"];
    818     }
    819     
    820     if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBFauxAnalogInputs"]) {
    821         GB_set_use_faux_analog_inputs(&_gb, 0, true);
    822         GB_set_faux_analog_inputs(&_gb, 0, axis.right.value - axis.left.value, axis.down.value - axis.up.value);
    823     }
    824     
    825     GB_set_key_state(&_gb, GB_KEY_LEFT, left);
    826     GB_set_key_state(&_gb, GB_KEY_RIGHT, right);
    827     GB_set_key_state(&_gb, GB_KEY_UP, up);
    828     GB_set_key_state(&_gb, GB_KEY_DOWN, down);
    829     if (_runMode == GBRunModeRewind || _runMode == GBRunModePaused) {
    830         self.runMode = GBRunModeNormal;
    831         [_backgroundView fadeOverlayOut];
    832     }
    833 }
    834 
    835 - (void)controller:(GCController *)controller motionChanged:(GCMotion *)motion
    836 {
    837     if (controller != _lastController) return;
    838     GCAcceleration gravity = {0,};
    839     GCAcceleration userAccel = {0,};
    840     if (@available(iOS 14.0, *)) {
    841         if (motion.hasGravityAndUserAcceleration) {
    842             gravity = motion.gravity;
    843             userAccel = motion.userAcceleration;
    844         }
    845         else {
    846             gravity = motion.acceleration;
    847         }
    848     }
    849     else {
    850         gravity = motion.gravity;
    851         userAccel = motion.userAcceleration;
    852     }
    853     GB_set_accelerometer_values(&_gb, -(gravity.x + userAccel.x), gravity.y + userAccel.y);
    854 }
    855 
    856 
    857 - (void)verifyEntitlements
    858 {
    859     /*
    860         Make sure SameBoy is properly signed. If the bundle identifier the Info.plist file does not match the bundle
    861         identifier in the application-identifier entitlement, iOS will not allow SameBoy to open files.
    862     */
    863     typedef void *xpc_object_t;
    864     void *libxpc = dlopen("/usr/lib/system/libxpc.dylib", RTLD_NOW);
    865     
    866     extern xpc_object_t xpc_copy_entitlements_for_self$(void);
    867     extern const char *xpc_dictionary_get_string$(xpc_object_t *object, const char *key);
    868     
    869     typeof(xpc_copy_entitlements_for_self$) *xpc_copy_entitlements_for_self = dlsym(libxpc, "xpc_copy_entitlements_for_self");
    870     typeof(xpc_dictionary_get_string$) *xpc_dictionary_get_string = dlsym(libxpc, "xpc_dictionary_get_string");
    871     
    872     if (!xpc_copy_entitlements_for_self || !xpc_dictionary_get_string) return;
    873     
    874     xpc_object_t entitlements = xpc_copy_entitlements_for_self();
    875     if (!entitlements) return;
    876     const char *_entIdentifier = xpc_dictionary_get_string(entitlements, "application-identifier");
    877     NSString *entIdentifier = _entIdentifier? @(_entIdentifier) : nil;
    878     
    879     const char *_teamIdentifier = xpc_dictionary_get_string(entitlements, "com.apple.developer.team-identifier");
    880     NSString *teamIdentifier = _teamIdentifier? @(_teamIdentifier) : nil;
    881     
    882     if (!entIdentifier) { // No identifier. Installed using a jailbreak, we're fine.
    883         return;
    884     }
    885 
    886     if (teamIdentifier && [entIdentifier hasPrefix:[teamIdentifier stringByAppendingString:@"."]]) {
    887         entIdentifier = [entIdentifier substringFromIndex:teamIdentifier.length + 1];
    888     }
    889     
    890     NSString *plistIdentifier = [NSBundle mainBundle].bundleIdentifier;
    891     
    892     if (![entIdentifier isEqual:plistIdentifier]) {
    893         UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"SameBoy is not properly signed and might not be able to open ROMs"
    894                                                                        message:[NSString stringWithFormat:@"The bundle identifier in the Info.plist file (“%@”) does not match the one in the entitlements (“%@”)", plistIdentifier, entIdentifier]
    895                                                                 preferredStyle:UIAlertControllerStyleAlert];
    896         [alert addAction:[UIAlertAction actionWithTitle:@"Close"
    897                                                   style:UIAlertActionStyleCancel
    898                                                 handler:nil]];
    899         [self presentViewController:alert animated:true completion:nil];
    900     }
    901 }
    902 
    903 - (void)saveStateToFile:(NSString *)file
    904 {
    905     NSString *tempPath = [file stringByAppendingPathExtension:@"tmp"];
    906     int error = GB_save_state(&_gb, tempPath.UTF8String);
    907     if (!error) {
    908         rename(tempPath.UTF8String, file.UTF8String);
    909         NSData *data = [NSData dataWithBytes:_gbView.previousBuffer
    910                                       length:GB_get_screen_width(&_gb) *
    911                         GB_get_screen_height(&_gb) *
    912                         sizeof(*_gbView.previousBuffer)];
    913         UIImage *screenshot = [self imageFromData:data width:GB_get_screen_width(&_gb) height:GB_get_screen_height(&_gb)];
    914         [UIImagePNGRepresentation(screenshot) writeToFile:[file stringByAppendingPathExtension:@"png"] atomically:false];
    915     }
    916     else {
    917         dispatch_async(dispatch_get_main_queue(), ^{
    918             UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Could not Save State"
    919                                                                            message:[NSString stringWithFormat:@"An error occured while attempting to save: %s", strerror(error)]
    920                                                                     preferredStyle:UIAlertControllerStyleAlert];
    921             [alert addAction:[UIAlertAction actionWithTitle:@"Close"
    922                                                       style:UIAlertActionStyleCancel
    923                                                     handler:nil]];
    924             UIViewController *top = self;
    925             while (top.presentedViewController) {
    926                 top = top.presentedViewController;
    927             }
    928             [top presentViewController:alert animated:true completion:nil];
    929         });
    930     }
    931 }
    932 
    933 - (bool)loadStateFromFile:(NSString *)file
    934 {
    935     _skipAutoLoad = true;
    936     GB_model_t model;
    937     if (!GB_get_state_model(file.fileSystemRepresentation, &model)) {
    938         if (GB_get_model(&_gb) != model) {
    939             GB_switch_model_and_reset(&_gb, model);
    940         }
    941         return GB_load_state(&_gb, file.fileSystemRepresentation) == 0;
    942     }
    943 
    944     return false;
    945 }
    946 
    947 - (void)loadROM
    948 {
    949     GBROMManager *romManager = [GBROMManager sharedManager];
    950     if (romManager.romFile) {
    951         if (!_skipAutoLoad) {
    952             // Todo: display errors and warnings
    953             bool needsStateLoad = false;
    954             if (![_lastSavedROM isEqual:[GBROMManager sharedManager].currentROM]) {
    955                 if ([romManager.romFile.pathExtension.lowercaseString isEqualToString:@"isx"]) {
    956                     _romLoaded = GB_load_isx(&_gb, romManager.romFile.fileSystemRepresentation) == 0;
    957                 }
    958                 else {
    959                     _romLoaded = GB_load_rom(&_gb, romManager.romFile.fileSystemRepresentation) == 0;
    960                 }
    961                 needsStateLoad = true;
    962                 if (@available(iOS 16.0, *)) {
    963                     dispatch_async(dispatch_get_main_queue(), ^{
    964                         [super setNeedsUpdateOfSupportedInterfaceOrientations];
    965                     });
    966                 }
    967             }
    968             else if (access(romManager.romFile.fileSystemRepresentation, R_OK)) {
    969                 _romLoaded = false;
    970             }
    971             if (!_romLoaded) {
    972                 dispatch_async(dispatch_get_main_queue(), ^{
    973                     romManager.currentROM = nil;
    974                 });
    975             }
    976             
    977             if (!needsStateLoad) {
    978                 NSDate *date = nil;
    979                 [[NSURL fileURLWithPath:[GBROMManager sharedManager].autosaveStateFile] getResourceValue:&date
    980                                                                                                   forKey:NSURLContentModificationDateKey
    981                                                                                                    error:nil];
    982                 if (![_saveDate isEqual:date]) {
    983                     needsStateLoad = true;
    984                 }
    985             }
    986             
    987             if (_romLoaded && needsStateLoad) {
    988                 GB_reset(&_gb);
    989                 GB_load_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation);
    990                 GB_remove_all_cheats(&_gb);
    991                 GB_load_cheats(&_gb, [GBROMManager sharedManager].cheatsFile.UTF8String, false);
    992                 if (![self loadStateFromFile:[GBROMManager sharedManager].autosaveStateFile]) {
    993                     if ([_lastSavedROM isEqual:[GBROMManager sharedManager].currentROM]) {
    994                         /* Something weird just happened: we didn't change a ROM, but we failed to load the
    995                            latest save state. Save over the existing file, it's probably corrupt in some
    996                            way. */
    997                         [self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile];
    998                     }
    999                     else {
   1000                         // Newly played ROM, pick the best model
   1001                         uint8_t *rom = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, NULL);
   1002                         
   1003                         if ((rom[0x143] & 0x80)) {
   1004                             if (!GB_is_cgb(&_gb)) {
   1005                                 GB_switch_model_and_reset(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]);
   1006                             }
   1007                         }
   1008                         else if ((rom[0x146]  == 3) && !GB_is_sgb(&_gb)) {
   1009                             GB_switch_model_and_reset(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"]);
   1010                         }
   1011                     }
   1012                 }
   1013                 GB_rewind_reset(&_gb);
   1014             }
   1015         }
   1016     }
   1017     else {
   1018         _romLoaded = false;
   1019     }
   1020     dispatch_async(dispatch_get_main_queue(), ^{
   1021         _gbView.hidden = !_romLoaded;
   1022     });
   1023     _swappingROM = false;
   1024     _skipAutoLoad = false;
   1025 }
   1026 
   1027 - (void)applicationDidBecomeActive:(UIApplication *)application
   1028 {
   1029     [self start];
   1030 }
   1031 
   1032 -(void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion
   1033 {
   1034     [self stop];
   1035     [super presentViewController:viewControllerToPresent
   1036                         animated:flag
   1037                       completion:completion];
   1038 }
   1039 
   1040 - (void)reset
   1041 {
   1042     UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad?
   1043     UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet;
   1044     UIAlertController *menu = [UIAlertController alertControllerWithTitle:@"Reset Emulation?"
   1045                                                                   message:@"Unsaved progress will be lost."
   1046                                                            preferredStyle:style];
   1047     [menu addAction:[UIAlertAction actionWithTitle:@"Reset"
   1048                                              style:UIAlertActionStyleDestructive
   1049                                            handler:^(UIAlertAction *action) {
   1050         [self stop];
   1051         _skipAutoLoad = true;
   1052         GB_reset(&_gb);
   1053         [self start];
   1054     }]];
   1055     [menu addAction:[UIAlertAction actionWithTitle:@"Cancel"
   1056                                              style:UIAlertActionStyleCancel
   1057                                            handler:nil]];
   1058     [self presentViewController:menu animated:true completion:nil];
   1059 }
   1060 
   1061 - (void)openLibrary
   1062 {
   1063     static __weak UIViewController *presentedController;
   1064     if (presentedController && self.presentedViewController == presentedController) return;
   1065     if (![self dismissViewControllerIfSafe]) return;
   1066     
   1067     UIViewController *controller = [[GBLibraryViewController alloc] init];
   1068     presentedController = controller;
   1069     
   1070     [self presentViewController:controller
   1071                        animated:true
   1072                      completion:nil];
   1073 }
   1074 
   1075 - (void)changeModel
   1076 {
   1077     static __weak UIViewController *presentedController;
   1078     if (presentedController && self.presentedViewController == presentedController) return;
   1079     if (![self dismissViewControllerIfSafe]) return;
   1080 
   1081     GBOptionViewController *controller = [[GBOptionViewController alloc] initWithHeader:@"Select a model to emulate"];
   1082     controller.footer = @"Changing the emulated model will reset the emulator";
   1083     presentedController = controller;
   1084     
   1085     GB_model_t currentModel = GB_get_model(&_gb);
   1086     struct {
   1087         NSString *title;
   1088         NSString *settingKey;
   1089         bool checked;
   1090     } items[] = {
   1091         {@"Game Boy", @"GBDMGModel", currentModel < GB_MODEL_SGB},
   1092         {@"Game Boy Pocket/Light", nil, currentModel == GB_MODEL_MGB},
   1093         {@"Super Game Boy", @"GBSGBModel", GB_is_sgb(&_gb)},
   1094         {@"Game Boy Color", @"GBCGBModel", GB_is_cgb(&_gb) && currentModel <= GB_MODEL_CGB_E},
   1095         {@"Game Boy Advance", @"GBAGBModel", currentModel > GB_MODEL_CGB_E},
   1096     };
   1097     
   1098     for (unsigned i = 0; i <  sizeof(items) / sizeof(items[0]); i++) {
   1099         GB_model_t model = GB_MODEL_MGB;
   1100         if (items[i].settingKey) {
   1101             model = [[NSUserDefaults standardUserDefaults] integerForKey:items[i].settingKey];
   1102         }
   1103         [controller addOption:items[i].title withCheckmark:items[i].checked action:^{
   1104             [self stop];
   1105             _skipAutoLoad = true;
   1106             GB_switch_model_and_reset(&_gb, model);
   1107             if (model > GB_MODEL_CGB_E && ![[NSUserDefaults standardUserDefaults] boolForKey:@"GBShownGBAWarning"]) {
   1108                 UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"SameBoy is not a Game Boy Advance Emulator"
   1109                                                                                message:@"SameBoy cannot play GBA games. Changing the model to Game Boy Advance lets you play Game Boy games as if on a Game Boy Advance in Game Boy Color mode."
   1110                                                                         preferredStyle:UIAlertControllerStyleAlert];
   1111                 [alert addAction:[UIAlertAction actionWithTitle:@"Close"
   1112                                                           style:UIAlertActionStyleCancel
   1113                                                         handler:^(UIAlertAction *action) {
   1114                     [self start];
   1115                     [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBShownGBAWarning"];
   1116                 }]];
   1117                 [self presentViewController:alert animated:true completion:nil];
   1118             }
   1119             else {
   1120                 [self start];
   1121             }
   1122         }];
   1123     }
   1124     controller.title = @"Change Model";
   1125     
   1126     UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:controller];
   1127     UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
   1128                                                               style:UIBarButtonItemStylePlain
   1129                                                              target:self
   1130                                                              action:@selector(dismissViewController)];
   1131     [navController.visibleViewController.navigationItem setLeftBarButtonItem:close];
   1132 
   1133     [self presentViewController:navController animated:true completion:nil];
   1134 }
   1135 
   1136 - (void)openStates
   1137 {
   1138     static __weak UIViewController *presentedController;
   1139     if (presentedController && self.presentedViewController == presentedController) return;
   1140     if (![self dismissViewControllerIfSafe]) return;
   1141     
   1142     UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBStatesViewController alloc] init]];
   1143     presentedController = controller;
   1144     UIVisualEffect *effect = [UIBlurEffect effectWithStyle:(UIBlurEffectStyle)UIBlurEffectStyleProminent];
   1145     UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
   1146     effectView.frame = controller.view.bounds;
   1147     effectView.autoresizingMask = UIViewAutoresizingFlexibleWidth |  UIViewAutoresizingFlexibleHeight;
   1148     [controller.view insertSubview:effectView atIndex:0];
   1149     UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
   1150                                                               style:UIBarButtonItemStylePlain
   1151                                                              target:self
   1152                                                              action:@selector(dismissViewController)];
   1153     [controller.visibleViewController.navigationItem setLeftBarButtonItem:close];
   1154     [self presentViewController:controller
   1155                        animated:true
   1156                      completion:nil];
   1157 }
   1158 
   1159 - (void)openSettings
   1160 {
   1161     static __weak UIViewController *presentedController;
   1162     if (presentedController && self.presentedViewController == presentedController) return;
   1163     if (![self dismissViewControllerIfSafe]) return;
   1164 
   1165     UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
   1166                                                               style:UIBarButtonItemStylePlain
   1167                                                              target:self
   1168                                                              action:@selector(dismissViewController)];
   1169     UIViewController *controller = [GBSettingsViewController settingsViewControllerWithLeftButton:close];
   1170     presentedController = controller;
   1171     [self presentViewController:controller
   1172                        animated:true
   1173                      completion:nil];
   1174 }
   1175 
   1176 - (void)showAbout
   1177 {
   1178     static __weak UIViewController *presentedController;
   1179     if (presentedController && self.presentedViewController == presentedController) return;
   1180     if (![self dismissViewControllerIfSafe]) return;
   1181 
   1182     UIViewController *controller = [[GBAboutController alloc] init];
   1183     presentedController = controller;
   1184 
   1185     [self presentViewController:controller animated:true completion:nil];
   1186 }
   1187 
   1188 - (void)openCheats
   1189 {
   1190     static __weak UIViewController *presentedController;
   1191     if (presentedController && self.presentedViewController == presentedController) return;
   1192     if (![self dismissViewControllerIfSafe]) return;
   1193 
   1194     UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBCheatsController alloc] initWithGameBoy:&_gb]];
   1195     presentedController = controller;
   1196     UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
   1197                                                               style:UIBarButtonItemStylePlain
   1198                                                              target:self
   1199                                                              action:@selector(dismissViewController)];
   1200     [controller.visibleViewController.navigationItem setLeftBarButtonItem:close];
   1201     [self presentViewController:controller
   1202                        animated:true
   1203                      completion:nil];
   1204 }
   1205 
   1206 - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
   1207 {
   1208     [super dismissViewControllerAnimated:flag completion:^() {
   1209         if (completion) {
   1210             completion();
   1211         }
   1212         dispatch_async(dispatch_get_main_queue(), ^{
   1213             [self start];
   1214         });
   1215     }];
   1216 }
   1217 
   1218 - (void)setNeedsUpdateOfSupportedInterfaceOrientations
   1219 {
   1220     /* Hack. Some view controllers dismiss without calling the method above. */
   1221     dispatch_async(dispatch_get_main_queue(), ^{
   1222         [self start];
   1223         [super setNeedsUpdateOfSupportedInterfaceOrientations];
   1224     });
   1225 }
   1226 
   1227 - (void)dismissViewController
   1228 {
   1229     [self dismissViewControllerAnimated:true completion:nil];
   1230 }
   1231 
   1232 - (bool)dismissViewControllerIfSafe
   1233 {
   1234     if (!self.presentedViewController) return true;
   1235     
   1236     if (![self.presentedViewController isKindOfClass:[UIAlertController class]]) {
   1237         [self dismissViewController];
   1238         return true;
   1239     }
   1240     
   1241     if ([self.presentedViewController isKindOfClass:[GBMenuViewController class]]) {
   1242         [self dismissViewController];
   1243         return true;
   1244     }
   1245     return false;
   1246 }
   1247 
   1248 - (void)applicationWillResignActive:(UIApplication *)application
   1249 {
   1250     [self stop];
   1251 }
   1252 
   1253 - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration
   1254 {
   1255     if (_orientation != UIInterfaceOrientationUnknown && !((1 << orientation) & self.supportedInterfaceOrientations)) return;
   1256     GBLayout *layout = nil;
   1257     _orientation = orientation;
   1258     switch (orientation) {
   1259         default:
   1260         case UIInterfaceOrientationUnknown:
   1261         case UIInterfaceOrientationPortrait:
   1262         case UIInterfaceOrientationPortraitUpsideDown:
   1263             layout = _verticalLayout;
   1264             break;
   1265         case UIInterfaceOrientationLandscapeRight:
   1266             layout = _horizontalLayoutLeft;
   1267             break;
   1268         case UIInterfaceOrientationLandscapeLeft:
   1269             layout = _horizontalLayoutRight;
   1270             break;
   1271     }
   1272     
   1273     _backgroundView.frame = [layout viewRectForOrientation:orientation];
   1274     _backgroundView.layout = layout;
   1275     if (!self.presentedViewController) {
   1276         _window.backgroundColor = _backgroundView.fullScreenMode? [UIColor blackColor] :
   1277                                                                   layout.theme.backgroundGradientBottom;
   1278     }
   1279 }
   1280 
   1281 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
   1282 {
   1283     UIEdgeInsets insets = self.window.safeAreaInsets;
   1284     bool landscape = true;
   1285     if (_orientation == UIInterfaceOrientationPortrait || _orientation == UIInterfaceOrientationPortraitUpsideDown) {
   1286         landscape = false;
   1287     }
   1288     
   1289     _cameraPositionButton.frame = CGRectMake(insets.left + 8,
   1290                                              _backgroundView.bounds.size.height - 8 - insets.bottom - 32,
   1291                                              32,
   1292                                              32);
   1293     if (_changeCameraButton) {
   1294         _changeCameraButton.frame = CGRectMake(insets.left + 8 +  (landscape? (32 + 8) : 0 ),
   1295                                                _backgroundView.bounds.size.height - 8 - insets.bottom - 32 - (landscape? 0 : (32 + 8)),
   1296                                                32,
   1297                                                32);
   1298     }
   1299     _printerButton.frame = CGRectMake(_backgroundView.bounds.size.width - 8 - insets.right - 32,
   1300                                       _backgroundView.bounds.size.height - 8 - insets.bottom - 32,
   1301                                       32,
   1302                                       32);
   1303     
   1304     _printerSpinner.frame = CGRectMake(_backgroundView.bounds.size.width - 8 - insets.right - 32 - (landscape? (32 + 4) : 0),
   1305                                        _backgroundView.bounds.size.height - 8 - insets.bottom - 32 - (landscape? 0 : (32 + 4)),
   1306                                        32,
   1307                                        32);
   1308 
   1309     [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
   1310 }
   1311 
   1312 - (UIInterfaceOrientationMask)supportedInterfaceOrientations
   1313 {
   1314     if (!self.shouldAutorotate && _orientation != UIInterfaceOrientationUnknown) {
   1315         return 1 << _orientation;
   1316     }
   1317     if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
   1318         return UIInterfaceOrientationMaskAll;
   1319     }
   1320     if (MAX([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width) <= 568) {
   1321         return UIInterfaceOrientationMaskLandscape;
   1322     }
   1323     return UIInterfaceOrientationMaskAllButUpsideDown;
   1324 }
   1325 
   1326 - (BOOL)shouldAutorotate
   1327 {
   1328     if (_running && GB_has_accelerometer(&_gb)) {
   1329         return false;
   1330     }
   1331     return true;
   1332 }
   1333 
   1334 - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures
   1335 {
   1336     return UIRectEdgeTop;
   1337 }
   1338 
   1339 - (BOOL)prefersStatusBarHidden
   1340 {
   1341     switch (_orientation) {
   1342         case UIInterfaceOrientationLandscapeRight:
   1343         case UIInterfaceOrientationLandscapeLeft:
   1344             return true;
   1345         default:
   1346             return false;
   1347     }
   1348 }
   1349 
   1350 - (UIStatusBarStyle)preferredStatusBarStyle
   1351 {
   1352     if (@available(iOS 13.0, *)) {
   1353         return (_verticalLayout.theme.isDark || _backgroundView.fullScreenMode)? UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent;
   1354     }
   1355     return (_verticalLayout.theme.isDark || _backgroundView.fullScreenMode)? UIStatusBarStyleLightContent : UIStatusBarStyleDefault;
   1356 }
   1357 
   1358 - (void)preRun
   1359 {
   1360     dispatch_async(dispatch_get_main_queue(), ^{
   1361         UIApplication.sharedApplication.idleTimerDisabled = true;
   1362     });
   1363     GB_set_pixels_output(&_gb, _gbView.pixels);
   1364     GB_set_sample_rate(&_gb, 96000);
   1365     if (![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBAudioMode"] isEqual:@"off"]) {
   1366         _audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) {
   1367             [_audioLock lock];
   1368             
   1369             if (_audioBufferPosition < nFrames) {
   1370                 _audioBufferNeeded = nFrames;
   1371                 [_audioLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:(double)(_audioBufferNeeded - _audioBufferPosition) / sampleRate]];
   1372                 _audioBufferNeeded = 0;
   1373             }
   1374             
   1375             if (_stopping) {
   1376                 memset(buffer, 0, nFrames * sizeof(*buffer));
   1377                 [_audioLock unlock];
   1378                 return;
   1379             }
   1380             
   1381             if (_audioBufferPosition < nFrames) {
   1382                 // Not enough audio
   1383                 memset(buffer, 0, (nFrames - _audioBufferPosition) * sizeof(*buffer));
   1384                 memcpy(buffer, _audioBuffer, _audioBufferPosition * sizeof(*buffer));
   1385                 // Do not reset the audio position to avoid more underflows
   1386             }
   1387             else if (_audioBufferPosition < nFrames + 4800) {
   1388                 memcpy(buffer, _audioBuffer, nFrames * sizeof(*buffer));
   1389                 memmove(_audioBuffer, _audioBuffer + nFrames, (_audioBufferPosition - nFrames) * sizeof(*buffer));
   1390                 _audioBufferPosition = _audioBufferPosition - nFrames;
   1391             }
   1392             else {
   1393                 memcpy(buffer, _audioBuffer + (_audioBufferPosition - nFrames), nFrames * sizeof(*buffer));
   1394                 _audioBufferPosition = 0;
   1395             }
   1396             [_audioLock unlock];
   1397         } andSampleRate:96000];
   1398     }
   1399     
   1400     [_audioClient start];
   1401     if (GB_has_accelerometer(&_gb)) {
   1402         if (@available(iOS 14.0, *)) {
   1403             for (GCController *controller in [GCController controllers]) {
   1404                 if (controller.motion.sensorsRequireManualActivation) {
   1405                     [controller.motion setSensorsActive:true];
   1406                 }
   1407             }
   1408         }
   1409         [_motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue]
   1410                                              withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
   1411             if (_lastController.motion) return;
   1412             CMAcceleration data = accelerometerData.acceleration;
   1413             UIInterfaceOrientation orientation = _orientation;
   1414             switch (orientation) {
   1415                 case UIInterfaceOrientationUnknown:
   1416                 case UIInterfaceOrientationPortrait:
   1417                     break;
   1418                 case UIInterfaceOrientationPortraitUpsideDown:
   1419                     data.x = -data.x;
   1420                     data.y = -data.y;
   1421                     break;
   1422                 case UIInterfaceOrientationLandscapeLeft: {
   1423                     double tempX = data.x;
   1424                     data.x = data.y;
   1425                     data.y = -tempX;
   1426                     break;
   1427                 }
   1428                 case UIInterfaceOrientationLandscapeRight:{
   1429                     double tempX = data.x;
   1430                     data.x = -data.y;
   1431                     data.y = tempX;
   1432                     break;
   1433                 }
   1434             }
   1435             GB_set_accelerometer_values(&_gb, -data.x, data.y);
   1436         }];
   1437     }
   1438     
   1439     /* Clear pending alarms, don't play alarms while playing */
   1440     if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) {
   1441         UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
   1442         [center removeDeliveredNotificationsWithIdentifiers:@[[GBROMManager sharedManager].romFile]];
   1443         [center removePendingNotificationRequestsWithIdentifiers:@[[GBROMManager sharedManager].romFile]];
   1444     }
   1445     
   1446     if (GB_rom_supports_alarms(&_gb)) {
   1447         UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
   1448         [center requestAuthorizationWithOptions:UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert completionHandler:nil];
   1449     }
   1450 }
   1451 
   1452 - (NSString *)formatDate:(NSDate *)date
   1453 {
   1454     
   1455     NSCalendar *calendar = [NSCalendar currentCalendar];
   1456     NSDate *now = [NSDate date];
   1457     
   1458     NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
   1459     formatter.locale = [NSLocale currentLocale];
   1460     formatter.timeZone = [NSTimeZone localTimeZone];
   1461     formatter.dateStyle = NSDateFormatterNoStyle;
   1462     formatter.timeStyle = NSDateFormatterMediumStyle;
   1463     NSString *time = [formatter stringFromDate:date];
   1464     
   1465     if ([calendar isDate:date inSameDayAsDate:now]) {
   1466         return time;
   1467     }
   1468     
   1469     if ([calendar component:NSCalendarUnitYear fromDate:date] == [calendar component:NSCalendarUnitYear fromDate:now]) {
   1470         formatter.dateFormat = [NSDateFormatter dateFormatFromTemplate:@"MMM d"
   1471                                                                options:0
   1472                                                                 locale:formatter.locale];
   1473         return [[formatter stringFromDate:date] stringByAppendingFormat:@", %@", time];
   1474     }
   1475     
   1476     formatter.dateFormat = [NSDateFormatter dateFormatFromTemplate:@"MMM d yyyy"
   1477                                                            options:0
   1478                                                             locale:formatter.locale];
   1479     return [[formatter stringFromDate:date] stringByAppendingFormat:@", %@", time];
   1480 }
   1481 
   1482 - (NSData *)batteryHash
   1483 {
   1484     NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:[GBROMManager sharedManager].batterySaveFile];
   1485     if (!file) return nil;
   1486     
   1487     CC_SHA256_CTX ctx;
   1488     CC_SHA256_Init(&ctx);
   1489     
   1490     while (true) {
   1491         @autoreleasepool {
   1492             NSData *chunk = [file readDataOfLength:0x4000];
   1493             if (chunk.length == 0) break;
   1494             CC_SHA256_Update(&ctx, chunk.bytes, (CC_LONG)chunk.length);
   1495         }
   1496     }
   1497     [file closeFile];
   1498     
   1499     unsigned char digest[CC_SHA256_DIGEST_LENGTH];
   1500     CC_SHA256_Final(digest, &ctx);
   1501     
   1502     return [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
   1503 }
   1504 
   1505 - (bool)areSavesInSync
   1506 {
   1507     if (!GBROMManager.sharedManager.currentROM) return true; // No ROM
   1508     NSString *saveState = [[GBROMManager sharedManager] autosaveStateFile];
   1509     NSString *batterySave = [[GBROMManager sharedManager] batterySaveFile];
   1510     
   1511     // There can't be a mismatch if one of the files is missing
   1512     if (access(saveState.UTF8String, F_OK) || access(batterySave.UTF8String, F_OK)) return true;
   1513     
   1514     uint8_t savedHash[CC_SHA256_DIGEST_LENGTH];
   1515     if (getxattr(saveState.UTF8String, "battery-hash", &savedHash, sizeof(savedHash), 0, 0) == sizeof(savedHash)) {
   1516         NSData *computedHash = [self batteryHash];
   1517         if (computedHash && memcmp(savedHash, computedHash.bytes, sizeof(savedHash)) == 0) {
   1518             return true; // Battery didn't change
   1519         }
   1520     }
   1521     
   1522     GB_gameboy_t *gb = NULL;
   1523     GB_model_t model;
   1524     if (GB_get_state_model(saveState.UTF8String, &model)) {
   1525         return true; // Not a valid state, ignore
   1526     }
   1527     gb = GB_init(GB_alloc(), model);
   1528     NSString *romFile = [[GBROMManager sharedManager] romFile];
   1529     if ([romFile.pathExtension.lowercaseString isEqualToString:@"isx"]) {
   1530         if (GB_load_isx(gb, romFile.fileSystemRepresentation)) {
   1531             // Can't load ROM
   1532             GB_dealloc(gb);
   1533             return true;
   1534         }
   1535     }
   1536     else {
   1537         if (GB_load_rom(gb, romFile.fileSystemRepresentation)) {
   1538             // Can't load ROM
   1539             GB_dealloc(gb);
   1540             return true;
   1541         }
   1542     }
   1543     if (GB_load_state(gb, saveState.UTF8String)) {
   1544         // Not a valid state again, ignore
   1545         GB_dealloc(gb);
   1546         return true;
   1547     }
   1548     
   1549     size_t ramSize = 0;
   1550     const uint8_t *ram = GB_get_direct_access(gb, GB_DIRECT_ACCESS_CART_RAM, &ramSize, NULL);
   1551     uint8_t *ramCopy = malloc(ramSize);
   1552     memcpy(ramCopy, ram, ramSize);
   1553     
   1554     GB_load_battery(gb, batterySave.UTF8String);
   1555     ram = GB_get_direct_access(gb, GB_DIRECT_ACCESS_CART_RAM, &ramSize, NULL);
   1556     bool matching = memcmp(ram, ramCopy, ramSize) == 0;
   1557     free(ramCopy);
   1558     GB_dealloc(gb);
   1559     return matching;
   1560 }
   1561 
   1562 - (bool)verifySavesInSync
   1563 {
   1564     if ([self areSavesInSync]) return true; // Saves are in sync
   1565     dispatch_semaphore_t sem = dispatch_semaphore_create(0);
   1566     __block bool ret = false;
   1567     dispatch_async(dispatch_get_main_queue(), ^{
   1568         UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Save Conflict Detected"
   1569                                                                        message:[NSString stringWithFormat: @"“%@” has both an automatic save state and a conflicting battery save file. The battery save may have newer progress that isn’t in the save state. Which one would you like to keep?", [GBROMManager sharedManager].currentROM.lastPathComponent]
   1570                                                                 preferredStyle:UIAlertControllerStyleAlert];
   1571         
   1572         NSString *saveState = [[GBROMManager sharedManager] autosaveStateFile];
   1573         NSString *batterySave = [[GBROMManager sharedManager] batterySaveFile];
   1574         
   1575         NSDate *date;
   1576         [[NSURL fileURLWithPath:saveState] getResourceValue:&date
   1577                                                      forKey:NSURLContentModificationDateKey
   1578                                                       error:nil];
   1579         
   1580         NSString *stateDateString = [self formatDate:date];
   1581         
   1582         UIAlertAction *stateAction = [UIAlertAction actionWithTitle:@"Save State\n"
   1583                                                               style:UIAlertActionStyleDefault
   1584                                                             handler:^(UIAlertAction *action) {
   1585             unlink(batterySave.UTF8String);
   1586             ret = true;
   1587             dispatch_semaphore_signal(sem);
   1588         }];
   1589 
   1590         [[NSURL fileURLWithPath:batterySave] getResourceValue:&date
   1591                                                        forKey:NSURLContentModificationDateKey
   1592                                                         error:nil];
   1593         
   1594         NSString *batteryDateString = [self formatDate:date];
   1595         
   1596         UIAlertAction *batteryAction = [UIAlertAction actionWithTitle:@"Battery Save\n"
   1597                                                                 style:UIAlertActionStyleDefault
   1598                                                               handler:^(UIAlertAction *action) {
   1599             unlink(saveState.UTF8String);
   1600             ret = true;
   1601             dispatch_semaphore_signal(sem);
   1602         }];
   1603         [alert addAction:stateAction];
   1604         [alert addAction:batteryAction];
   1605         [alert addAction:[UIAlertAction actionWithTitle:@"Close ROM"
   1606                                                   style:UIAlertActionStyleCancel
   1607                                                 handler:^(UIAlertAction *action) {
   1608             ret = false;
   1609             dispatch_semaphore_signal(sem);
   1610         }]];
   1611         
   1612         // hack
   1613         _running = false;
   1614         [self presentViewController:alert animated:true completion:nil];
   1615         dispatch_async(dispatch_get_main_queue(), ^{
   1616             NSMutableSet<UIView *> *views = [NSMutableSet setWithObject:alert.view];
   1617             
   1618             UIColor *secondaryLabelColor;
   1619             if (@available(iOS 13.0, *)) {
   1620                 secondaryLabelColor = [UIColor secondaryLabelColor];
   1621             }
   1622             else {
   1623                 secondaryLabelColor = [UIColor systemGrayColor];
   1624             }
   1625             
   1626             UILabel *view;
   1627             while ((view = (UILabel *)views.anyObject)) {
   1628                 [views removeObject:view];
   1629                 [views addObjectsFromArray:view.subviews];
   1630                 if ([view isKindOfClass:[UILabel class]]) {
   1631                     if ([view.text isEqualToString:stateAction.title]) {
   1632                         NSMutableAttributedString *text = [view.attributedText mutableCopy];
   1633                         [text appendAttributedString:[[NSAttributedString alloc] initWithString:stateDateString
   1634                                                                                      attributes:@{
   1635                             NSFontAttributeName: [UIFont systemFontOfSize:UIFont.smallSystemFontSize],
   1636                             NSForegroundColorAttributeName: secondaryLabelColor,
   1637                         }]];
   1638                         view.attributedText = text;
   1639                         view.numberOfLines = 2;
   1640                         view.locksFonts = true;
   1641                     }
   1642                     if ([view.text isEqualToString:batteryAction.title]) {
   1643                         NSMutableAttributedString *text = [view.attributedText mutableCopy];
   1644                         [text appendAttributedString:[[NSAttributedString alloc] initWithString:batteryDateString
   1645                                                                                      attributes:@{
   1646                             NSFontAttributeName: [UIFont systemFontOfSize:UIFont.smallSystemFontSize],
   1647                             NSForegroundColorAttributeName: secondaryLabelColor,
   1648                         }]];
   1649                         view.attributedText = text;
   1650                         view.numberOfLines = 2;
   1651                         view.locksFonts = true;
   1652                     }
   1653                 }
   1654             }
   1655         });
   1656     });
   1657     dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
   1658     _running = true;
   1659     return ret;
   1660 }
   1661 
   1662 - (void)run
   1663 {
   1664     if (![self verifySavesInSync]) {
   1665         _romLoaded = false;
   1666         _running = false;
   1667         _stopping = false;
   1668         GBROMManager.sharedManager.currentROM = nil;
   1669         return;
   1670     }
   1671 
   1672     [self loadROM];
   1673     if (!_romLoaded) {
   1674         _running = false;
   1675         _stopping = false;
   1676         return;
   1677     }
   1678     [self preRun];
   1679     const unsigned autosaveFrequency = 60 * 60;
   1680     _autosaveCountdown = autosaveFrequency;
   1681     while (_running) {
   1682         if (_rewind) {
   1683             _rewind = false;
   1684             GB_rewind_pop(&_gb);
   1685             if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
   1686                 if (!GB_rewind_pop(&_gb)) {
   1687                     dispatch_sync(dispatch_get_main_queue(), ^{
   1688                         if (_runMode == GBRunModeRewind) {
   1689                             self.runMode = GBRunModePaused;
   1690                         }
   1691                     });
   1692                     _rewindOver = true;
   1693                 }
   1694             }
   1695             else {
   1696                 for (unsigned i = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindSpeed"]; i--;)  {
   1697                     if (!GB_rewind_pop(&_gb)) {
   1698                         dispatch_sync(dispatch_get_main_queue(), ^{
   1699                             if (_runMode == GBRunModeRewind) {
   1700                                 self.runMode = GBRunModePaused;
   1701                             }
   1702                         });
   1703                         _rewindOver = true;
   1704                     }
   1705                 }
   1706             }
   1707         }
   1708         if (_runMode != GBRunModePaused) {
   1709             GB_run(&_gb);
   1710             if (!_autosaveCountdown) {
   1711                 _autosaveCountdown = autosaveFrequency;
   1712                 [self performAutosave];
   1713             }
   1714         }
   1715     }
   1716     [self postRun];
   1717     _stopping = false;
   1718 }
   1719 
   1720 - (void)userNotificationCenter:(UNUserNotificationCenter *)center
   1721 didReceiveNotificationResponse:(UNNotificationResponse *)response
   1722          withCompletionHandler:(void (^)(void))completionHandler
   1723 {
   1724     if (![response.notification.request.identifier isEqual:[GBROMManager sharedManager].currentROM]) {
   1725         [self application:[UIApplication sharedApplication]
   1726                   openURL:[NSURL fileURLWithPath:response.notification.request.identifier]
   1727                   options:@{}];
   1728     }
   1729     completionHandler();
   1730 }
   1731 
   1732 - (UIImage *)imageFromData:(NSData *)data width:(unsigned)width height:(unsigned)height
   1733 {
   1734     /* Convert the screenshot to a CGImageRef */
   1735     CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)data);
   1736     CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
   1737     CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
   1738     CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
   1739     
   1740     CGImageRef iref = CGImageCreate(width,
   1741                                     height,
   1742                                     8,
   1743                                     32,
   1744                                     4 * width,
   1745                                     colorSpaceRef,
   1746                                     bitmapInfo,
   1747                                     provider,
   1748                                     NULL,
   1749                                     true,
   1750                                     renderingIntent);
   1751 
   1752     UIImage *ret = [[UIImage alloc] initWithCGImage:iref];
   1753     CGColorSpaceRelease(colorSpaceRef);
   1754     CGDataProviderRelease(provider);
   1755     CGImageRelease(iref);
   1756     return ret;
   1757 }
   1758 
   1759 - (void)performAutosave
   1760 {
   1761     GB_save_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation);
   1762     [self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile];
   1763     // Assoicate the battery save with the save state via a hash xattr
   1764     NSData *batteryHash = [self batteryHash];
   1765     if (batteryHash) {
   1766         setxattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash",
   1767                  batteryHash.bytes, batteryHash.length, 0, 0);
   1768     }
   1769     else {
   1770         removexattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash", 0);
   1771     }
   1772 }
   1773 
   1774 - (void)postRun
   1775 {
   1776     [_audioLock lock];
   1777     _audioBufferPosition = _audioBufferNeeded = 0;
   1778     [_audioLock signal];
   1779     [_audioLock unlock];
   1780     [_audioClient stop];
   1781     _audioClient = nil;
   1782 
   1783     if (!_swappingROM) {
   1784         [self performAutosave];
   1785         
   1786         // Assoicate the battery save with the save state via a hash xattr
   1787         NSData *batteryHash = [self batteryHash];
   1788         if (batteryHash) {
   1789             setxattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash",
   1790                      batteryHash.bytes, batteryHash.length, 0, 0);
   1791         }
   1792         else {
   1793             removexattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash", 0);
   1794         }
   1795 
   1796         NSDate *date;
   1797         [[NSURL fileURLWithPath:[GBROMManager sharedManager].autosaveStateFile] getResourceValue:&date
   1798                                                                                           forKey:NSURLContentModificationDateKey
   1799                                                                                            error:nil];
   1800         _saveDate = date;
   1801         _lastSavedROM = [GBROMManager sharedManager].currentROM;
   1802     }
   1803     [[GBHapticManager sharedManager] setRumbleStrength:0];
   1804     if (@available(iOS 14.0, *)) {
   1805         for (GCController *controller in [GCController controllers]) {
   1806             if (controller.motion.sensorsRequireManualActivation) {
   1807                 [controller.motion setSensorsActive:false];
   1808             }
   1809         }
   1810     }
   1811     [_motionManager stopAccelerometerUpdates];
   1812     
   1813     unsigned timeToAlarm = GB_time_to_alarm(&_gb);
   1814     
   1815     if (timeToAlarm) {
   1816         UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
   1817                 
   1818         UNMutableNotificationContent *notificationContent = [[UNMutableNotificationContent alloc] init];
   1819         NSString *friendlyName = [[[GBROMManager sharedManager].romFile lastPathComponent] stringByDeletingPathExtension];
   1820         NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\([^)]+\\)|\\[[^\\]]+\\]" options:0 error:nil];
   1821         friendlyName = [regex stringByReplacingMatchesInString:friendlyName options:0 range:NSMakeRange(0, [friendlyName length]) withTemplate:@""];
   1822         friendlyName = [friendlyName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
   1823         
   1824         notificationContent.title = [NSString stringWithFormat:@"%@ Played an Alarm", friendlyName];
   1825         notificationContent.body = [NSString stringWithFormat:@"%@ requested your attention by playing a scheduled alarm", friendlyName];
   1826         notificationContent.sound = UNNotificationSound.defaultSound;
   1827         
   1828         UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:[GBROMManager sharedManager].romFile
   1829                                                                               content:notificationContent
   1830                                                                               trigger:[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timeToAlarm repeats:false]];
   1831         
   1832         
   1833         [center addNotificationRequest:request withCompletionHandler:nil];
   1834         [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBNotificationsUsed"];
   1835     }
   1836     
   1837     dispatch_async(dispatch_get_main_queue(), ^{
   1838         UIApplication.sharedApplication.idleTimerDisabled = false;
   1839     });
   1840 }
   1841 
   1842 - (void)start
   1843 {
   1844     if (_running) return;
   1845     if (self.presentedViewController) return;
   1846     _running = true;
   1847     dispatch_async(_runQueue, ^{
   1848         [self run];
   1849     });
   1850 }
   1851 
   1852 - (void)stop
   1853 {
   1854     if (!_running) return;
   1855     [_audioLock lock];
   1856     _stopping = true;
   1857     [_audioLock signal];
   1858     [_audioLock unlock];
   1859     _running = false;
   1860     while (_stopping) {
   1861         [_audioLock lock];
   1862         [_audioLock signal];
   1863         [_audioLock unlock];
   1864     }
   1865     dispatch_sync(_runQueue, ^{});
   1866     dispatch_async(dispatch_get_main_queue(), ^{
   1867         self.runMode = GBRunModeNormal;
   1868         [_backgroundView fadeOverlayOut];
   1869     });
   1870 }
   1871 
   1872 
   1873 - (NSString *)bootROMPathForName:(NSString *)name
   1874 {
   1875     if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBCustomBootROMs"]) {
   1876         NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0];
   1877         path = [path stringByAppendingPathComponent:@"Boot ROMs"];
   1878         path = [path stringByAppendingPathComponent:name];
   1879         path = [path stringByAppendingPathExtension:@"bin"];
   1880         if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
   1881             return path;
   1882         }
   1883     }
   1884     
   1885     return [[NSBundle mainBundle] pathForResource:name ofType:@"bin"];
   1886 }
   1887 
   1888 - (void)loadBootROM: (GB_boot_rom_t)type
   1889 {
   1890     static NSString *const names[] = {
   1891         [GB_BOOT_ROM_DMG_0] = @"dmg0_boot",
   1892         [GB_BOOT_ROM_DMG] = @"dmg_boot",
   1893         [GB_BOOT_ROM_MGB] = @"mgb_boot",
   1894         [GB_BOOT_ROM_SGB] = @"sgb_boot",
   1895         [GB_BOOT_ROM_SGB2] = @"sgb2_boot",
   1896         [GB_BOOT_ROM_CGB_0] = @"cgb0_boot",
   1897         [GB_BOOT_ROM_CGB] = @"cgb_boot",
   1898         [GB_BOOT_ROM_CGB_E] = @"cgbE_boot",
   1899         [GB_BOOT_ROM_AGB_0] = @"agb0_boot",
   1900         [GB_BOOT_ROM_AGB] = @"agb_boot",
   1901     };
   1902     NSString *name = names[type];
   1903     NSString *path = [self bootROMPathForName:name];
   1904     /* These boot types are not commonly available, and they are indentical
   1905      from an emulator perspective, so fall back to the more common variants
   1906      if they can't be found. */
   1907     if (!path && type == GB_BOOT_ROM_CGB_E) {
   1908         [self loadBootROM:GB_BOOT_ROM_CGB];
   1909         return;
   1910     }
   1911     if (!path && type == GB_BOOT_ROM_AGB_0) {
   1912         [self loadBootROM:GB_BOOT_ROM_AGB];
   1913         return;
   1914     }
   1915     GB_load_boot_rom(&_gb, [path UTF8String]);
   1916 }
   1917 
   1918 - (void)vblankWithType:(GB_vblank_type_t)type
   1919 {
   1920     if (type != GB_VBLANK_TYPE_REPEAT) {
   1921         [_gbView flip];
   1922         GB_set_pixels_output(&_gb, _gbView.pixels);
   1923     }
   1924     if (_rapidA) {
   1925         _rapidACount++;
   1926         GB_set_key_state(&_gb, GB_KEY_A, !(_rapidACount & 2));
   1927     }
   1928     if (_rapidB) {
   1929         _rapidBCount++;
   1930         GB_set_key_state(&_gb, GB_KEY_B, !(_rapidBCount & 2));
   1931     }
   1932     _autosaveCountdown--;
   1933     if (_rapidA || _rapidB) {
   1934         if (_runMode == GBRunModeRewind || _runMode == GBRunModePaused) {
   1935             self.runMode = GBRunModeNormal;
   1936             [_backgroundView fadeOverlayOut];
   1937         }
   1938     }
   1939     _rewind = _runMode == GBRunModeRewind;
   1940 }
   1941 
   1942 - (void)gotNewSample:(GB_sample_t *)sample
   1943 {
   1944     [_audioLock lock];
   1945     if (_audioClient.isPlaying) {
   1946         if (_audioBufferPosition == _audioBufferSize) {
   1947             if (_audioBufferSize >= 0x4000) {
   1948                 _audioBufferPosition = 0;
   1949                 [_audioLock unlock];
   1950                 return;
   1951             }
   1952             
   1953             if (_audioBufferSize == 0) {
   1954                 _audioBufferSize = 512;
   1955             }
   1956             else {
   1957                 _audioBufferSize += _audioBufferSize >> 2;
   1958             }
   1959             _audioBuffer = realloc(_audioBuffer, sizeof(*sample) * _audioBufferSize);
   1960         }
   1961         _audioBuffer[_audioBufferPosition++] = *sample;
   1962     }
   1963     if (_audioBufferPosition == _audioBufferNeeded) {
   1964         [_audioLock signal];
   1965         _audioBufferNeeded = 0;
   1966     }
   1967     [_audioLock unlock];
   1968 }
   1969 
   1970 - (void)rumbleChanged:(double)amp
   1971 {
   1972     [[GBHapticManager sharedManager] setRumbleStrength:amp];
   1973 }
   1974 
   1975 - (void)updatePalette
   1976 {
   1977     memcpy(&_palette,
   1978            [GBPalettePicker paletteForTheme:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"]],
   1979            sizeof(_palette));
   1980     GB_set_palette(&_gb, &_palette);
   1981 }
   1982 
   1983 - (bool)handleOpenURLs:(NSArray <NSURL *> *)urls
   1984            openInPlace:(bool)inPlace
   1985 {
   1986     NSMutableArray<NSURL *> *validURLs = [NSMutableArray array];
   1987     NSMutableArray<NSString *> *skippedBasenames = [NSMutableArray array];
   1988     NSMutableArray<NSString *> *unusedZips = [NSMutableArray array];
   1989     NSString *tempDir = NSTemporaryDirectory();
   1990     
   1991     __block unsigned tempIndex = 0;
   1992     for (NSURL *url in urls) {
   1993         if ([url.pathExtension.lowercaseString isEqualToString:@"zip"]) {
   1994             GBZipReader *reader = [[GBZipReader alloc] initWithPath:url.path];
   1995             if (!reader) {
   1996                 [skippedBasenames addObject:url.lastPathComponent];
   1997                 continue;
   1998             }
   1999             __block bool used = false;
   2000             [reader iterateFiles:^bool(NSString *filename, size_t uncompressedSize, bool (^getData)(void *), bool (^writeToPath)(NSString *)) {
   2001                 if ([@[@"gb", @"gbc", @"isx"] containsObject:filename.pathExtension.lowercaseString] && uncompressedSize <= 32 * 1024 * 1024) {
   2002                     tempIndex++;
   2003                     NSString *subDir = [tempDir stringByAppendingFormat:@"/%u", tempIndex];
   2004                     mkdir(subDir.UTF8String, 0755);
   2005                     NSString *dest = [subDir stringByAppendingPathComponent:filename.lastPathComponent];
   2006                     if (writeToPath(dest)) {
   2007                         [validURLs addObject:[NSURL fileURLWithPath:dest]];
   2008                         used = true;
   2009                     }
   2010                 }
   2011                 return true;
   2012             }];
   2013             if (!used) {
   2014                 [unusedZips addObject:url.lastPathComponent];
   2015             }
   2016         }
   2017         else if ([@[@"gb", @"gbc", @"isx"] containsObject:url.pathExtension.lowercaseString]) {
   2018             [validURLs addObject:url];
   2019         }
   2020         else {
   2021             [skippedBasenames addObject:url.lastPathComponent];
   2022         }
   2023     }
   2024     
   2025     if (unusedZips.count) {
   2026         UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"No ROMs in archive"
   2027                                                                        message:[NSString stringWithFormat:@"Could not find any Game Boy ROM files in the following archives:\n%@",
   2028                                                                                 [unusedZips componentsJoinedByString:@"\n"]]
   2029                                                                 preferredStyle:UIAlertControllerStyleAlert];
   2030         [alert addAction:[UIAlertAction actionWithTitle:@"Close"
   2031                                                   style:UIAlertActionStyleCancel
   2032                                                 handler:nil]];
   2033         [self presentViewController:alert animated:true completion:nil];
   2034     }
   2035     
   2036     if (skippedBasenames.count) {
   2037         UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Unsupported Files"
   2038                                                                        message:[NSString stringWithFormat:@"Could not import the following files because they're not supported:\n%@",
   2039                                                                                 [skippedBasenames componentsJoinedByString:@"\n"]]
   2040                                                                 preferredStyle:UIAlertControllerStyleAlert];
   2041         [alert addAction:[UIAlertAction actionWithTitle:@"Close"
   2042                                                   style:UIAlertActionStyleCancel
   2043                                                 handler:^(UIAlertAction *action) {
   2044             [[NSUserDefaults standardUserDefaults] setBool:false forKey:@"GBShownUTIWarning"]; // Somebody might need a reminder
   2045         }]];
   2046         [self presentViewController:alert animated:true completion:nil];
   2047     }
   2048     
   2049     
   2050     if (validURLs.count == 1 && urls.count == 1) {
   2051         NSURL *url = validURLs.firstObject;
   2052         NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
   2053         if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
   2054             [GBROMManager sharedManager].currentROM = potentialROM;
   2055         }
   2056         else {
   2057             [url startAccessingSecurityScopedResource];
   2058             [GBROMManager sharedManager].currentROM =
   2059             [[GBROMManager sharedManager] importROM:url.path
   2060                                        keepOriginal:![url.path hasPrefix:tempDir] && inPlace];
   2061             [url stopAccessingSecurityScopedResource];
   2062         }
   2063         return true;
   2064     }
   2065     for (NSURL *url in validURLs) {
   2066         NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
   2067         if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
   2068             // That's an already imported ROM
   2069             continue;
   2070         }
   2071         [url startAccessingSecurityScopedResource];
   2072         [[GBROMManager sharedManager] importROM:url.path
   2073                                    keepOriginal:![url.path hasPrefix:tempDir] && inPlace];
   2074         [url stopAccessingSecurityScopedResource];
   2075     }
   2076     [self openLibrary];
   2077     
   2078     return validURLs.count;
   2079 }
   2080 
   2081 - (void)doImportedPaletteNotification
   2082 {
   2083     UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleProminent]];
   2084     effectView.layer.cornerRadius = 8;
   2085     effectView.layer.masksToBounds = true;
   2086     [self.view addSubview:effectView];
   2087     UILabel *tipLabel = [[UILabel alloc] init];
   2088     tipLabel.text = [NSString stringWithFormat:@"Imported palette “%@”", [[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"]];
   2089     if (@available(iOS 13.0, *)) {
   2090         tipLabel.textColor = [UIColor labelColor];
   2091     }
   2092     tipLabel.font = [UIFont systemFontOfSize:16];
   2093     tipLabel.alpha = 0.8;
   2094     [effectView.contentView addSubview:tipLabel];
   2095     
   2096     UIView *view = self.view;
   2097     CGSize outerSize = view.frame.size;
   2098     CGSize size = [tipLabel textRectForBounds:(CGRect){{0, 0},
   2099         {outerSize.width - 32,
   2100             outerSize.height - 32}}
   2101                        limitedToNumberOfLines:1].size;
   2102     size.width = ceil(size.width);
   2103     tipLabel.frame = (CGRect){{8, 8}, size};
   2104     CGRect finalFrame = (CGRect) {
   2105         {round((outerSize.width - size.width - 16) / 2), view.window.safeAreaInsets.top + 12},
   2106         {size.width + 16, size.height + 16}
   2107     };
   2108     
   2109     CGRect initFrame = finalFrame;
   2110     initFrame.origin.y = -initFrame.size.height;
   2111     effectView.frame = initFrame;
   2112     
   2113     effectView.alpha = 0;
   2114     [UIView animateWithDuration:0.5 animations:^{
   2115         effectView.alpha = 1.0;
   2116         effectView.frame = finalFrame;
   2117     }];
   2118     
   2119     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * 1.5)), dispatch_get_main_queue(), ^{
   2120         [UIView animateWithDuration:0.5 animations:^{
   2121             effectView.alpha = 0.0;
   2122             effectView.frame = initFrame;
   2123         } completion:^(BOOL finished) {
   2124             if (finished) {
   2125                 [effectView removeFromSuperview];
   2126             }
   2127         }];
   2128     });
   2129 }
   2130 
   2131 - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
   2132 {
   2133     if (self.presentedViewController && ![self.presentedViewController isKindOfClass:[UIAlertController class]]) {
   2134         [self dismissViewController];
   2135     }
   2136     if ([url.pathExtension.lowercaseString isEqual:@"sbp"]) {
   2137         [url startAccessingSecurityScopedResource];
   2138         bool success = [GBPalettePicker importPalette:url.path];
   2139         [url stopAccessingSecurityScopedResource];
   2140         if (!success) {
   2141             UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Palette Import Failed"
   2142                                                                                      message:@"The imported palette file is invalid."
   2143                                                                               preferredStyle:UIAlertControllerStyleAlert];
   2144             [alertController addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]];
   2145             [self presentViewController:alertController animated:true completion:nil];
   2146         }
   2147         else {
   2148             [self doImportedPaletteNotification];
   2149         }
   2150         return success;
   2151     }
   2152     NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
   2153     if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
   2154         [self stop];
   2155         [GBROMManager sharedManager].currentROM = potentialROM;
   2156         [self start];
   2157         return [GBROMManager sharedManager].currentROM != nil;
   2158     }
   2159     return [self handleOpenURLs:@[url]
   2160                     openInPlace:[options[UIApplicationOpenURLOptionsOpenInPlaceKey] boolValue]];
   2161 }
   2162 
   2163 - (void)setRunMode:(GBRunMode)runMode ignoreDynamicSpeed:(bool)ignoreDynamicSpeed
   2164 {
   2165     if (runMode == GBRunModeRewind && _rewindOver) {
   2166         runMode = GBRunModePaused;
   2167     }
   2168     if (runMode == _runMode) return;
   2169     if (_runMode == GBRunModePaused) {
   2170         [_audioClient start];
   2171     }
   2172     _runMode = runMode;
   2173     if (_runMode == GBRunModePaused) {
   2174         [_audioClient stop];
   2175     }
   2176     
   2177     if (_runMode == GBRunModeNormal || _runMode == GBRunModeTurbo || _runMode == GBRunModeUnderclock) {
   2178         _rewindOver = false;
   2179     }
   2180     
   2181     if (_runMode == GBRunModeNormal  || _runMode == GBRunModeUnderclock || !([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"] && !ignoreDynamicSpeed)) {
   2182         if (_runMode == GBRunModeTurbo) {
   2183             GB_set_turbo_mode(&_gb, true, false);
   2184         }
   2185         else if (_runMode == GBRunModeUnderclock) {
   2186             GB_set_clock_multiplier(&_gb, 0.5);
   2187         }
   2188         else {
   2189             GB_set_turbo_mode(&_gb, false, false);
   2190             GB_set_clock_multiplier(&_gb, 1.0);
   2191         }
   2192     }
   2193 }
   2194 
   2195 - (void)setRunMode:(GBRunMode)runMode
   2196 {
   2197     [self setRunMode:runMode ignoreDynamicSpeed:false];
   2198 }
   2199 
   2200 - (AVCaptureDevice *)captureDevice
   2201 {
   2202     if (_cameraPosition == AVCaptureDevicePositionFront) {
   2203         return _frontCaptureDevice ?: [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
   2204     }
   2205     return _backCaptureDevice ?: [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
   2206 }
   2207 
   2208 - (void)cameraRequestUpdate
   2209 {
   2210     dispatch_async(dispatch_get_main_queue(), ^{
   2211         if (!_cameraSession) {
   2212             dispatch_async(_cameraQueue, ^{
   2213                 @try {
   2214                     if (!_cameraSession) {
   2215                         NSError *error;
   2216                         AVCaptureDevice *device = [self captureDevice];
   2217                         AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice: device error: &error];
   2218                         
   2219                         if (!input) {
   2220                             GB_camera_updated(&_gb);
   2221                             return;
   2222                         }
   2223                         
   2224                         _cameraOutput = [[AVCaptureVideoDataOutput alloc] init];
   2225                         [_cameraOutput setVideoSettings: @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)}];
   2226                         [_cameraOutput setSampleBufferDelegate:self
   2227                                                          queue:_cameraQueue];
   2228                         
   2229                         
   2230                         
   2231                         _cameraSession = [AVCaptureSession new];
   2232                         if ([device supportsAVCaptureSessionPreset:AVCaptureSessionPreset352x288]) {
   2233                             _cameraSession.sessionPreset = AVCaptureSessionPreset352x288;
   2234                         }
   2235                         else if ([device supportsAVCaptureSessionPreset:AVCaptureSessionPresetMedium]) {
   2236                             _cameraSession.sessionPreset = AVCaptureSessionPresetMedium;
   2237                         }
   2238                         
   2239                         [_cameraSession addInput: input];
   2240                         [_cameraSession addOutput: _cameraOutput];
   2241                         _cameraConnection = [_cameraOutput connectionWithMediaType: AVMediaTypeVideo];
   2242                         _cameraConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
   2243                         [_cameraSession startRunning];
   2244                     }
   2245                 }
   2246                 @catch (NSException *exception) {
   2247                     /* I have not tested camera support on many devices, so we catch exceptions just in case. */
   2248                     GB_camera_updated(&_gb);
   2249                     _cameraSession = nil;
   2250                 }
   2251             });
   2252         }
   2253         
   2254         _cameraNeedsUpdate = true;
   2255         [_disableCameraTimer invalidate];
   2256         if (!_cameraPositionButton.alpha) {
   2257             [UIView animateWithDuration:0.25 animations:^{
   2258                 _cameraPositionButton.alpha = 1;
   2259             }];
   2260         }
   2261         if (_changeCameraButton) {
   2262             // The change camera button is only available when we are using a capture device on the back of the device
   2263             double changeCameraButtonAlpha = (_cameraPosition == AVCaptureDevicePositionFront) ? 0 : 1;
   2264             if (changeCameraButtonAlpha != _changeCameraButton.alpha) {
   2265                 [UIView animateWithDuration:0.25 animations:^{
   2266                     _changeCameraButton.alpha = changeCameraButtonAlpha;
   2267                 }];
   2268             }
   2269         }
   2270 
   2271         _disableCameraTimer = [NSTimer scheduledTimerWithTimeInterval:1
   2272                                                               repeats:false
   2273                                                                 block:^(NSTimer *timer) {
   2274             if (_cameraPositionButton.alpha) {
   2275                 [UIView animateWithDuration:0.25 animations:^{
   2276                     _cameraPositionButton.alpha = 0;
   2277                 }];
   2278             }
   2279             if (_changeCameraButton.alpha) {
   2280                 [UIView animateWithDuration:0.25 animations:^{
   2281                     _changeCameraButton.alpha = 0;
   2282                 }];
   2283             }
   2284             dispatch_async(_cameraQueue, ^{
   2285                 [_cameraSession stopRunning];
   2286                 _cameraSession = nil;
   2287                 _cameraConnection = nil;
   2288                 _cameraOutput = nil;
   2289             });
   2290         }];
   2291     });
   2292 }
   2293 
   2294 - (uint8_t)cameraGetPixelAtX:(uint8_t)x andY:(uint8_t)y
   2295 {
   2296     if (!_cameraImage) {
   2297         return 0;
   2298     }
   2299     if (_cameraNeedsUpdate) {
   2300         return 0;
   2301     }
   2302     
   2303     y += 8; // Equalize X and Y for rotation as a 128x128 image
   2304     
   2305     if (_cameraPosition == AVCaptureDevicePositionFront) {
   2306         x = 127 - x;
   2307     }
   2308     
   2309     switch (_orientation) {
   2310         case UIInterfaceOrientationUnknown:
   2311         case UIInterfaceOrientationPortrait:
   2312             break;
   2313         case UIInterfaceOrientationPortraitUpsideDown:
   2314             x = 127 - x;
   2315             y = 127 - y;
   2316             break;
   2317         case UIInterfaceOrientationLandscapeLeft: {
   2318             uint8_t tempX = x;
   2319             x = y;
   2320             y = 127 - tempX;
   2321             break;
   2322         }
   2323         case UIInterfaceOrientationLandscapeRight:{
   2324             uint8_t tempX = x;
   2325             x = 127 - y;
   2326             y = tempX;
   2327             break;
   2328         }
   2329     }
   2330     
   2331     if (_cameraPosition == AVCaptureDevicePositionFront) {
   2332         x = 127 - x;
   2333     }
   2334     
   2335     // Center the 128*128 image on the 130*XXX (or XXX*130) captured image
   2336     unsigned offsetX = (CVPixelBufferGetWidth(_cameraImage) - 128) / 2;
   2337     unsigned offsetY = (CVPixelBufferGetHeight(_cameraImage) - 112) / 2;
   2338     
   2339     uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(_cameraImage);
   2340     size_t bytesPerRow = CVPixelBufferGetBytesPerRow(_cameraImage);
   2341     uint8_t ret = baseAddress[(x + offsetX) * 4 + (y + offsetY) * bytesPerRow + 2];
   2342     
   2343     return ret;
   2344 }
   2345 
   2346 - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
   2347 {
   2348     if (!_cameraNeedsUpdate) return;
   2349     CVImageBufferRef buffer = CMSampleBufferGetImageBuffer(sampleBuffer);
   2350     CIImage *image = [CIImage imageWithCVPixelBuffer:buffer
   2351                                              options:[NSDictionary dictionaryWithObjectsAndKeys:[NSNull null], kCIImageColorSpace, nil]];
   2352     double scale = MAX(130.0 / CVPixelBufferGetWidth(buffer), 130.0 / CVPixelBufferGetHeight(buffer));
   2353     image = [image imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
   2354     if (_cameraImage) {
   2355         CVPixelBufferUnlockBaseAddress(_cameraImage, 0);
   2356         CVBufferRelease(_cameraImage);
   2357         _cameraImage = NULL;
   2358     }
   2359     CGSize size = image.extent.size;
   2360     CVPixelBufferCreate(kCFAllocatorDefault,
   2361                         size.width,
   2362                         size.height,
   2363                         kCVPixelFormatType_32BGRA,
   2364                         NULL,
   2365                         &_cameraImage);
   2366     [[[CIContext alloc] init] render:image toCVPixelBuffer:_cameraImage];
   2367     CVPixelBufferLockBaseAddress(_cameraImage, 0);
   2368     
   2369     GB_camera_updated(&_gb);
   2370     
   2371     _cameraNeedsUpdate = false;
   2372 }
   2373 
   2374 - (void)rotateCamera
   2375 {
   2376     dispatch_async(_cameraQueue, ^{
   2377         _cameraPosition ^= AVCaptureDevicePositionBack ^ AVCaptureDevicePositionFront;
   2378         [_cameraSession stopRunning];
   2379         _cameraSession = nil;
   2380         _cameraConnection = nil;
   2381         _cameraOutput = nil;
   2382         if (_cameraNeedsUpdate) {
   2383             _cameraNeedsUpdate = false;
   2384             GB_camera_updated(&_gb);
   2385         }
   2386     });
   2387 }
   2388 
   2389 - (void)changeCamera
   2390 {
   2391     dispatch_async(_cameraQueue, ^{
   2392         if (![_backCaptureDevice lockForConfiguration:nil]) return;
   2393         _currentZoomIndex++;
   2394         if (_currentZoomIndex == _zoomLevels.count) {
   2395             _currentZoomIndex = 0;
   2396         }
   2397         [_backCaptureDevice rampToVideoZoomFactor:_zoomLevels[_currentZoomIndex].doubleValue withRate:2];
   2398         [_backCaptureDevice unlockForConfiguration];
   2399     });
   2400 }
   2401 
   2402 - (void)disconnectCable
   2403 {
   2404     if (!_printerConnected) return;
   2405     _printerConnected = false;
   2406     _currentPrinterImageData = nil;
   2407     [UIView animateWithDuration:0.25 animations:^{
   2408         _printerButton.alpha = 0;
   2409     }];
   2410     [_printerSpinner stopAnimating];
   2411     GB_disconnect_serial(&_gb);
   2412 }
   2413 
   2414 - (void)connectPrinter
   2415 {
   2416     if (_printerConnected) return;
   2417     _printerConnected = true;
   2418     GB_connect_printer(&_gb, printImage, printDone);
   2419 }
   2420 
   2421 - (void)openConnectMenu
   2422 {
   2423     UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad?
   2424     UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet;
   2425     GBCheckableAlertController *menu = [GBCheckableAlertController alertControllerWithTitle:@"Connect Accessory"
   2426                                                                                     message:@"Choose an accessory to connect."
   2427                                                                              preferredStyle:style];
   2428     [menu addAction:[UIAlertAction actionWithTitle:@"None"
   2429                                              style:UIAlertActionStyleDefault
   2430                                            handler:^(UIAlertAction *action) {
   2431         [self disconnectCable];
   2432     }]];
   2433     [menu addAction:[UIAlertAction actionWithTitle:@"Game Boy Printer"
   2434                                              style:UIAlertActionStyleDefault
   2435                                            handler:^(UIAlertAction *action) {
   2436         [self connectPrinter];
   2437     }]];
   2438     menu.selectedAction = menu.actions[_printerConnected];
   2439     [menu addAction:[UIAlertAction actionWithTitle:@"Cancel"
   2440                                              style:UIAlertActionStyleCancel
   2441                                            handler:nil]];
   2442     [self presentViewController:menu animated:true completion:nil];
   2443 }
   2444 
   2445 - (void)printImage:(uint32_t *)imageBytes height:(unsigned) height
   2446          topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin
   2447           exposure:(unsigned) exposure
   2448 {
   2449     uint32_t paddedImage[160 * (topMargin + height + bottomMargin)];
   2450     memset(paddedImage, 0xFF, sizeof(paddedImage));
   2451     memcpy(paddedImage + (160 * topMargin), imageBytes, 160 * height * sizeof(imageBytes[0]));
   2452     if (!_currentPrinterImageData) {
   2453         _currentPrinterImageData = [[NSMutableData alloc] init];
   2454     }
   2455     [_currentPrinterImageData appendBytes:paddedImage length:sizeof(paddedImage)];
   2456 
   2457     dispatch_async(dispatch_get_main_queue(), ^{
   2458         [UIView animateWithDuration:0.25 animations:^{
   2459             _printerButton.alpha = 1;
   2460         }];
   2461         [_printerSpinner startAnimating];
   2462     });
   2463     
   2464 }
   2465 
   2466 - (void)printDone
   2467 {
   2468     dispatch_async(dispatch_get_main_queue(), ^{
   2469         [_printerSpinner stopAnimating];
   2470     });
   2471 }
   2472 
   2473 - (void)showPrinterFeed
   2474 {
   2475     UIImage *image = [self imageFromData:_currentPrinterImageData
   2476                                    width:160
   2477                                   height:_currentPrinterImageData.length / 160 / sizeof(uint32_t)];
   2478 
   2479     _window.backgroundColor = [UIColor blackColor];
   2480     [self presentViewController:[[GBPrinterFeedController alloc] initWithImage:image]
   2481                        animated:true
   2482                      completion:nil];
   2483     
   2484 }
   2485 
   2486 - (void)emptyPrinterFeed
   2487 {
   2488     _currentPrinterImageData = nil;
   2489     [UIView animateWithDuration:0.25 animations:^{
   2490         _printerButton.alpha = 0;
   2491     }];
   2492     [_printerSpinner stopAnimating];
   2493     [self dismissViewController];
   2494 }
   2495 
   2496 @end
   2497 
   2498 /* +[UIColor labelColor] is broken in some contexts in iOS 26 and despite being such a critical method
   2499    Apple isn't going to fix this in time. */
   2500 API_AVAILABLE(ios(19.0))
   2501 @implementation UIColor(SolariumBugs)
   2502 + (UIColor *)_labelColor
   2503 {
   2504     return [UIColor colorWithDynamicProvider:^UIColor *(UITraitCollection *traitCollection) {
   2505         switch (traitCollection.userInterfaceStyle) {
   2506                 
   2507             case UIUserInterfaceStyleUnspecified:
   2508             case UIUserInterfaceStyleLight:
   2509                 return [UIColor blackColor];
   2510             case UIUserInterfaceStyleDark:
   2511                 return [UIColor whiteColor];
   2512         }
   2513     }];
   2514 }
   2515 
   2516 + (void)load
   2517 {
   2518     if (@available(iOS 19.0, *)) {
   2519         method_setImplementation(class_getClassMethod(self, @selector(labelColor)),
   2520                                  [self methodForSelector:@selector(_labelColor)]);
   2521     }
   2522 }
   2523 
   2524 @end

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.