git.y1.nz

SameBoy

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

Cocoa/Document.m

      1 #import <AVFoundation/AVFoundation.h>
      2 #import <CoreAudio/CoreAudio.h>
      3 #import <Core/gb.h>
      4 #import "GBAudioClient.h"
      5 #import "Document.h"
      6 #import "GBApp.h"
      7 #import "HexFiend/HexFiend.h"
      8 #import "GBMemoryByteArray.h"
      9 #import "GBWarningPopover.h"
     10 #import "GBCheatWindowController.h"
     11 #import "GBTerminalTextFieldCell.h"
     12 #import "BigSurToolbar.h"
     13 #import "GBPaletteEditorController.h"
     14 #import "GBCheatSearchController.h"
     15 #import "GBObjectView.h"
     16 #import "GBPaletteView.h"
     17 #import "GBHexStatusBarRepresenter.h"
     18 #import "NSObject+DefaultsObserver.h"
     19 
     20 #define likely(x)   GB_likely(x)
     21 #define unlikely(x) GB_unlikely(x)
     22 
     23 @implementation NSString (relativePath)
     24 
     25 - (NSString *)pathRelativeToDirectory:(NSString *)directory
     26 {
     27     NSMutableArray<NSString *> *baseComponents = [[directory pathComponents] mutableCopy];
     28     NSMutableArray<NSString *> *selfComponents = [[self pathComponents] mutableCopy];
     29     
     30     while (baseComponents.count) {
     31         if (![baseComponents.firstObject isEqualToString:selfComponents.firstObject]) {
     32             break;
     33         }
     34         
     35         [baseComponents removeObjectAtIndex:0];
     36         [selfComponents removeObjectAtIndex:0];
     37     }
     38     while (baseComponents.count) {
     39         [baseComponents removeObjectAtIndex:0];
     40         [selfComponents insertObject:@".." atIndex:0];
     41     }
     42     return [selfComponents componentsJoinedByString:@"/"];
     43 }
     44 
     45 @end
     46 
     47 #define GB_MODEL_PAL_BIT_OLD 0x1000
     48 
     49 /* Todo: The general Objective-C coding style conflicts with SameBoy's. This file needs a cleanup. */
     50 /* Todo: Split into category files! This is so messy!!! */
     51 
     52 
     53 @interface Document ()
     54 @property GBAudioClient *audioClient;
     55 @end
     56 
     57 @implementation Document
     58 {
     59     GB_gameboy_t _gb;
     60     volatile bool _running;
     61     volatile bool _stopping;
     62     NSConditionLock *_hasDebuggerInput;
     63     NSMutableArray *_debuggerInputQueue;
     64     
     65     NSMutableAttributedString *_pendingConsoleOutput;
     66     NSRecursiveLock *_consoleOutputLock;
     67     NSTimer *_consoleOutputTimer;
     68     NSTimer *_hexTimer;
     69     
     70     NSTimer *_batterySaveTimer;
     71     bool _dirtyBattery;
     72     
     73     bool _fullScreen;
     74     bool _inSyncInput;
     75     NSString *_debuggerCommandWhilePaused;
     76     HFController *_hexController;
     77     
     78     NSString *_lastConsoleInput;
     79     HFLineCountingRepresenter *_lineRep;
     80     GBHexStatusBarRepresenter *_statusRep;
     81     
     82     CVImageBufferRef _cameraImage;
     83     AVCaptureSession *_cameraSession;
     84     AVCaptureConnection *_cameraConnection;
     85     AVCaptureStillImageOutput *_cameraOutput;
     86     
     87     GB_oam_info_t _oamInfo[40];
     88     
     89     NSMutableData *_currentPrinterImageData;
     90     
     91     bool _romWarningIssued;
     92     
     93     NSMutableString *_capturedOutput;
     94     bool _logToSideView;
     95     bool _shouldClearSideView;
     96     enum model _currentModel;
     97     bool _usesAutoModel;
     98     
     99     bool _rewind;
    100     bool _modelsChanging;
    101     
    102     NSCondition *_audioLock;
    103     GB_sample_t *_audioBuffer;
    104     size_t _audioBufferSize;
    105     size_t _audioBufferPosition;
    106     size_t _audioBufferNeeded;
    107     double _volume;
    108     
    109     bool _borderModeChanged;
    110     
    111     /* Link cable*/
    112     Document *_master;
    113     Document *_slave;
    114     signed _linkOffset;
    115     bool _linkCableBit;
    116     
    117     NSSavePanel *_audioSavePanel;
    118     bool _isRecordingAudio;
    119     
    120     void (^ volatile _pendingAtomicBlock)();
    121     
    122     NSDate *_fileModificationTime;
    123     __weak NSThread *_emulationThread;
    124     
    125     GBCheatSearchController *_cheatSearchController;
    126     
    127     bool _romModified;
    128 }
    129 
    130 static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type)
    131 {
    132     Document *self = (__bridge Document *)GB_get_user_data(gb);
    133     [self loadBootROM: type];
    134 }
    135 
    136 static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type)
    137 {
    138     Document *self = (__bridge Document *)GB_get_user_data(gb);
    139     [self vblankWithType:type];
    140 }
    141 
    142 static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes)
    143 {
    144     Document *self = (__bridge Document *)GB_get_user_data(gb);
    145     [self log:string withAttributes: attributes];
    146 }
    147 
    148 static char *consoleInput(GB_gameboy_t *gb)
    149 {
    150     Document *self = (__bridge Document *)GB_get_user_data(gb);
    151     return [self getDebuggerInput];
    152 }
    153 
    154 static char *asyncConsoleInput(GB_gameboy_t *gb)
    155 {
    156     Document *self = (__bridge Document *)GB_get_user_data(gb);
    157     char *ret = [self getAsyncDebuggerInput];
    158     return ret;
    159 }
    160 
    161 static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b)
    162 {
    163     return (r << 0) | (g << 8) | (b << 16) | 0xFF000000;
    164 }
    165 
    166 static void cameraRequestUpdate(GB_gameboy_t *gb)
    167 {
    168     Document *self = (__bridge Document *)GB_get_user_data(gb);
    169     [self cameraRequestUpdate];
    170 }
    171 
    172 static uint8_t cameraGetPixel(GB_gameboy_t *gb, uint8_t x, uint8_t y)
    173 {
    174     Document *self = (__bridge Document *)GB_get_user_data(gb);
    175     return [self cameraGetPixelAtX:x andY:y];
    176 }
    177 
    178 static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height,
    179                        uint8_t top_margin, uint8_t bottom_margin, uint8_t exposure)
    180 {
    181     Document *self = (__bridge Document *)GB_get_user_data(gb);
    182     [self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure];
    183 }
    184 
    185 static void printDone(GB_gameboy_t *gb)
    186 {
    187     Document *self = (__bridge Document *)GB_get_user_data(gb);
    188     [self printDone];
    189 }
    190 
    191 static void setWorkboyTime(GB_gameboy_t *gb, time_t t)
    192 {
    193     [[NSUserDefaults standardUserDefaults] setInteger:time(NULL) - t forKey:@"GBWorkboyTimeOffset"];
    194 }
    195 
    196 static time_t getWorkboyTime(GB_gameboy_t *gb)
    197 {
    198     return time(NULL) - [[NSUserDefaults standardUserDefaults] integerForKey:@"GBWorkboyTimeOffset"];
    199 }
    200 
    201 static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample)
    202 {
    203     Document *self = (__bridge Document *)GB_get_user_data(gb);
    204     [self gotNewSample:sample];
    205 }
    206 
    207 static void rumbleCallback(GB_gameboy_t *gb, double amp)
    208 {
    209     Document *self = (__bridge Document *)GB_get_user_data(gb);
    210     [self rumbleChanged:amp];
    211 }
    212 
    213 
    214 static void _linkCableBitStart(GB_gameboy_t *gb, bool bit_to_send)
    215 {
    216     Document *self = (__bridge Document *)GB_get_user_data(gb);
    217     [self _linkCableBitStart:bit_to_send];
    218 }
    219 
    220 static bool _linkCableBitEnd(GB_gameboy_t *gb)
    221 {
    222     Document *self = (__bridge Document *)GB_get_user_data(gb);
    223     return [self _linkCableBitEnd];
    224 }
    225 
    226 static void infraredStateChanged(GB_gameboy_t *gb, bool on)
    227 {
    228     Document *self = (__bridge Document *)GB_get_user_data(gb);
    229     [self infraredStateChanged:on];
    230 }
    231 
    232 static void debuggerReloadCallback(GB_gameboy_t *gb)
    233 {
    234     Document *self = (__bridge Document *)GB_get_user_data(gb);
    235     dispatch_sync(dispatch_get_main_queue(), ^{
    236         bool wasRunning = self->_running;
    237         self->_running = false; // Hack for output capture
    238         [self loadROM];
    239         self->_running = wasRunning;
    240         GB_reset(gb);
    241     });
    242 }
    243 
    244 - (instancetype)init 
    245 {
    246     self = [super init];
    247     if (self) {
    248         _hasDebuggerInput = [[NSConditionLock alloc] initWithCondition:0];
    249         _debuggerInputQueue = [[NSMutableArray alloc] init];
    250         _consoleOutputLock = [[NSRecursiveLock alloc] init];
    251         _audioLock = [[NSCondition alloc] init];
    252         _volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"];
    253     }
    254     return self;
    255 }
    256 
    257 - (GB_model_t)internalModel
    258 {
    259     switch (_currentModel) {
    260         case MODEL_DMG:
    261             return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBDMGModel"];
    262             
    263         case MODEL_NONE:
    264         case MODEL_QUICK_RESET:
    265         case MODEL_AUTO:
    266         case MODEL_CGB:
    267             return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"];
    268             
    269         case MODEL_SGB: {
    270             GB_model_t model = (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"];
    271             if (model == (GB_MODEL_SGB | GB_MODEL_PAL_BIT_OLD)) {
    272                 model = GB_MODEL_SGB_PAL;
    273             }
    274             return model;
    275         }
    276         
    277         case MODEL_MGB:
    278             return GB_MODEL_MGB;
    279         
    280         case MODEL_AGB:
    281             return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBAGBModel"];
    282     }
    283 }
    284 
    285 - (void)updatePalette
    286 {
    287     GB_set_palette(&_gb, [GBPaletteEditorController userPalette]);
    288 }
    289 
    290 - (void)initCommon
    291 {
    292     GB_init(&_gb, [self internalModel]);
    293     GB_set_user_data(&_gb, (__bridge void *)(self));
    294     GB_set_boot_rom_load_callback(&_gb, (GB_boot_rom_load_callback_t)boot_rom_load);
    295     GB_set_vblank_callback(&_gb, (GB_vblank_callback_t) vblank);
    296     GB_set_enable_skipped_frame_vblank_callbacks(&_gb, true);
    297     GB_set_log_callback(&_gb, (GB_log_callback_t) consoleLog);
    298     GB_set_input_callback(&_gb, (GB_input_callback_t) consoleInput);
    299     GB_set_async_input_callback(&_gb, (GB_input_callback_t) asyncConsoleInput);
    300     [self updatePalette];
    301     GB_set_rgb_encode_callback(&_gb, rgbEncode);
    302     GB_set_camera_get_pixel_callback(&_gb, cameraGetPixel);
    303     GB_set_camera_update_request_callback(&_gb, cameraRequestUpdate);
    304     GB_apu_set_sample_callback(&_gb, audioCallback);
    305     GB_set_rumble_callback(&_gb, rumbleCallback);
    306     GB_set_infrared_callback(&_gb, infraredStateChanged);
    307     GB_debugger_set_reload_callback(&_gb, debuggerReloadCallback);
    308     
    309     GB_gameboy_t *gb = &_gb;
    310     __unsafe_unretained Document *weakSelf = self;
    311     
    312     [self observeStandardDefaultsKey:@"GBColorCorrection" withBlock:^(NSNumber *value) {
    313         GB_set_color_correction_mode(gb, value.unsignedIntValue);
    314     }];
    315     
    316     [self observeStandardDefaultsKey:@"GBLightTemperature" withBlock:^(NSNumber *value) {
    317         GB_set_light_temperature(gb, value.doubleValue);
    318     }];
    319     
    320     [self observeStandardDefaultsKey:@"GBInterferenceVolume" withBlock:^(NSNumber *value) {
    321         GB_set_interference_volume(gb, value.doubleValue);
    322     }];
    323     
    324     GB_set_border_mode(&_gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]);
    325     [self observeStandardDefaultsKey:@"GBBorderMode" withBlock:^(NSNumber *value) {
    326         weakSelf->_borderModeChanged = true;
    327     }];
    328     
    329     [self observeStandardDefaultsKey:@"GBHighpassFilter" withBlock:^(NSNumber *value) {
    330         GB_set_highpass_filter_mode(gb, value.unsignedIntValue);
    331     }];
    332     
    333     [self observeStandardDefaultsKey:@"GBRewindLength" withBlock:^(NSNumber *value) {
    334         [weakSelf performAtomicBlock:^{
    335             GB_set_rewind_length(gb, value.unsignedIntValue);
    336         }];
    337     }];
    338     
    339     [self observeStandardDefaultsKey:@"GBRTCMode" withBlock:^(NSNumber *value) {
    340         GB_set_rtc_mode(gb, value.unsignedIntValue);
    341     }];
    342     
    343     [self observeStandardDefaultsKey:@"GBRumbleMode" withBlock:^(NSNumber *value) {
    344         GB_set_rumble_mode(gb, value.unsignedIntValue);
    345     }];
    346     
    347     [self observeStandardDefaultsKey:@"GBDebuggerFont" withBlock:^(NSString *value) {
    348         [weakSelf updateFonts];
    349     }];
    350     
    351     [self observeStandardDefaultsKey:@"GBDebuggerFontSize" withBlock:^(NSString *value) {
    352         [weakSelf updateFonts];
    353     }];
    354     
    355     [self observeStandardDefaultsKey:@"GBTurboCap" withBlock:^(NSNumber *value) {
    356         if (!weakSelf->_master) {
    357             GB_set_turbo_cap(gb, value.doubleValue);
    358         }
    359     }];
    360 }
    361 
    362 - (void)updateMinSize
    363 {
    364     self.mainWindow.contentMinSize = NSMakeSize(GB_get_screen_width(&_gb), GB_get_screen_height(&_gb));
    365     if (self.mainWindow.contentView.bounds.size.width < GB_get_screen_width(&_gb) ||
    366         self.mainWindow.contentView.bounds.size.width < GB_get_screen_height(&_gb)) {
    367         [self.mainWindow zoom:nil];
    368     }
    369     self.osdView.usesSGBScale = GB_get_screen_width(&_gb) == 256;
    370 }
    371 
    372 - (void)vblankWithType:(GB_vblank_type_t)type
    373 {
    374     if (type == GB_VBLANK_TYPE_SKIPPED_FRAME) {
    375         double frameUsage = GB_debugger_get_frame_cpu_usage(&_gb);
    376         [_cpuView addSample:frameUsage];
    377         return;
    378     }
    379     
    380     if (_gbsVisualizer) {
    381         dispatch_async(dispatch_get_main_queue(), ^{
    382             [_gbsVisualizer setNeedsDisplay:true];
    383         });
    384     }
    385     
    386     double frameUsage = GB_debugger_get_frame_cpu_usage(&_gb);
    387     [_cpuView addSample:frameUsage];
    388     
    389     if (self.consoleWindow.visible) {
    390         double secondUsage = GB_debugger_get_second_cpu_usage(&_gb);
    391         dispatch_async(dispatch_get_main_queue(), ^{
    392             [_cpuView setNeedsDisplay:true];
    393             _cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100];
    394         });
    395     }
    396     
    397     if (type != GB_VBLANK_TYPE_REPEAT) {
    398         [self.view flip];
    399         if (_borderModeChanged) {
    400             dispatch_sync(dispatch_get_main_queue(), ^{
    401                 size_t previous_width = GB_get_screen_width(&_gb);
    402                 GB_set_border_mode(&_gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]);
    403                 if (GB_get_screen_width(&_gb) != previous_width) {
    404                     [self.view screenSizeChanged];
    405                     [self updateMinSize];
    406                 }
    407             });
    408             _borderModeChanged = false;
    409         }
    410         GB_set_pixels_output(&_gb, self.view.pixels);
    411     }
    412     
    413     if (self.vramWindow.isVisible) {
    414         dispatch_async(dispatch_get_main_queue(), ^{
    415             [self reloadVRAMData: nil];
    416         });
    417     }
    418     
    419     self.view.mouseHidingEnabled = (_mainWindow.styleMask & NSWindowStyleMaskFullScreen) && !_consoleWindow.visible && !_memoryWindow.visible && !_vramWindow.visible && !_printerFeedWindow.visible && !_cheatsWindow.visible;
    420 
    421     if (self.view.isRewinding) {
    422         _rewind = true;
    423         [self.osdView displayText:@"Rewinding…"];
    424     }
    425 }
    426 
    427 - (void)gotNewSample:(GB_sample_t *)sample
    428 {
    429     if (_gbsVisualizer) {
    430         [_gbsVisualizer addSample:sample];
    431     }
    432     [_audioLock lock];
    433     if (_audioClient.isPlaying) {
    434         if (_audioBufferPosition == _audioBufferSize) {
    435             if (_audioBufferSize >= 0x4000) {
    436                 _audioBufferPosition = 0;
    437                 [_audioLock unlock];
    438                 return;
    439             }
    440             
    441             if (_audioBufferSize == 0) {
    442                 _audioBufferSize = 512;
    443             }
    444             else {
    445                 _audioBufferSize += _audioBufferSize >> 2;
    446             }
    447             _audioBuffer = realloc(_audioBuffer, sizeof(*sample) * _audioBufferSize);
    448         }
    449         if (_volume != 1) {
    450             sample->left *= _volume;
    451             sample->right *= _volume;
    452         }
    453         _audioBuffer[_audioBufferPosition++] = *sample;
    454     }
    455     if (_audioBufferPosition == _audioBufferNeeded) {
    456         [_audioLock signal];
    457         _audioBufferNeeded = 0;
    458     }
    459     [_audioLock unlock];
    460 }
    461 
    462 - (void)rumbleChanged:(double)amp
    463 {
    464     [_view setRumble:amp];
    465 }
    466 
    467 - (void)preRun
    468 {
    469     GB_set_pixels_output(&_gb, self.view.pixels);
    470     GB_set_sample_rate(&_gb, 96000);
    471     _audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) {
    472         [_audioLock lock];
    473         
    474         if (_audioBufferPosition < nFrames) {
    475             _audioBufferNeeded = nFrames;
    476             [_audioLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:(double)(_audioBufferNeeded - _audioBufferPosition) / sampleRate]];
    477             _audioBufferNeeded = 0;
    478         }
    479         
    480         if (_stopping || GB_debugger_is_stopped(&_gb)) {
    481             memset(buffer, 0, nFrames * sizeof(*buffer));
    482             [_audioLock unlock];
    483             return;
    484         }
    485         
    486         if (_audioBufferPosition < nFrames) {
    487             // Not enough audio
    488             memset(buffer, 0, (nFrames - _audioBufferPosition) * sizeof(*buffer));
    489             memcpy(buffer, _audioBuffer, _audioBufferPosition * sizeof(*buffer));
    490             // Do not reset the audio position to avoid more underflows
    491         }
    492         else if (_audioBufferPosition < nFrames + 4800) {
    493             memcpy(buffer, _audioBuffer, nFrames * sizeof(*buffer));
    494             memmove(_audioBuffer, _audioBuffer + nFrames, (_audioBufferPosition - nFrames) * sizeof(*buffer));
    495             _audioBufferPosition = _audioBufferPosition - nFrames;
    496         }
    497         else {
    498             memcpy(buffer, _audioBuffer + (_audioBufferPosition - nFrames), nFrames * sizeof(*buffer));
    499             _audioBufferPosition = 0;
    500         }
    501         [_audioLock unlock];
    502     } andSampleRate:96000];
    503     if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) {
    504         [_audioClient start];
    505     }
    506     _hexTimer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:true];
    507     [[NSRunLoop mainRunLoop] addTimer:_hexTimer forMode:NSDefaultRunLoopMode];
    508     
    509     _batterySaveTimer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(batteryTimerExpired) userInfo:nil repeats:true];
    510     [[NSRunLoop mainRunLoop] addTimer:_batterySaveTimer forMode:NSDefaultRunLoopMode];
    511     
    512     /* Clear pending alarms, don't play alarms while playing */
    513     if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) {
    514         NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
    515         for (NSUserNotification *notification in [center scheduledNotifications]) {
    516             if ([notification.identifier isEqualToString:self.fileURL.path]) {
    517                 [center removeScheduledNotification:notification];
    518                 break;
    519             }
    520         }
    521         
    522         for (NSUserNotification *notification in [center deliveredNotifications]) {
    523             if ([notification.identifier isEqualToString:self.fileURL.path]) {
    524                 [center removeDeliveredNotification:notification];
    525                 break;
    526             }
    527         }
    528     }
    529 }
    530 
    531 static unsigned *multiplication_table_for_frequency(unsigned frequency)
    532 {
    533     unsigned *ret = malloc(sizeof(*ret) * 0x100);
    534     for (unsigned i = 0; i < 0x100; i++) {
    535         ret[i] = i * frequency;
    536     }
    537     return ret;
    538 }
    539 
    540 - (void)run
    541 {
    542     assert(!_master);
    543     [self preRun];
    544     if (_slave) {
    545         [_slave preRun];
    546         unsigned *masterTable = multiplication_table_for_frequency(GB_get_clock_rate(&_gb));
    547         unsigned *slaveTable = multiplication_table_for_frequency(GB_get_clock_rate(&_slave->_gb));
    548         while (_running) {
    549             if (_linkOffset <= 0) {
    550                 _linkOffset += slaveTable[GB_run(&_gb)];
    551             }
    552             else {
    553                 _linkOffset -= masterTable[GB_run(&_slave->_gb)];
    554             }
    555             if (unlikely(_pendingAtomicBlock)) {
    556                 _pendingAtomicBlock();
    557                 _pendingAtomicBlock = nil;
    558             }
    559         }
    560         free(masterTable);
    561         free(slaveTable);
    562         [_slave postRun];
    563     }
    564     else {
    565         while (_running) {
    566             if (_rewind) {
    567                 _rewind = false;
    568                 GB_rewind_pop(&_gb);
    569                 if (!GB_rewind_pop(&_gb)) {
    570                     _rewind = self.view.isRewinding;
    571                 }
    572             }
    573             else {
    574                 GB_run(&_gb);
    575             }
    576             if (unlikely(_pendingAtomicBlock)) {
    577                 _pendingAtomicBlock();
    578                 _pendingAtomicBlock = nil;
    579             }
    580         }
    581     }
    582     [self postRun];
    583     _stopping = false;
    584 }
    585 
    586 - (void)postRun
    587 {
    588     [_hexTimer invalidate];
    589     [_batterySaveTimer invalidate];
    590     [_audioLock lock];
    591     _audioBufferPosition = _audioBufferNeeded = 0;
    592     [_audioLock signal];
    593     [_audioLock unlock];
    594     [_audioClient stop];
    595     _audioClient = nil;
    596     self.view.mouseHidingEnabled = false;
    597     GB_save_battery(&_gb, self.savPath.UTF8String);
    598     GB_save_cheats(&_gb, self.chtPath.UTF8String);
    599     unsigned time_to_alarm = GB_time_to_alarm(&_gb);
    600     
    601     if (time_to_alarm) {
    602         [NSUserNotificationCenter defaultUserNotificationCenter].delegate = (id)[NSApp delegate];
    603         NSUserNotification *notification = [[NSUserNotification alloc] init];
    604         NSString *friendlyName = [[self.fileURL lastPathComponent] stringByDeletingPathExtension];
    605         NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\([^)]+\\)|\\[[^\\]]+\\]" options:0 error:nil];
    606         friendlyName = [regex stringByReplacingMatchesInString:friendlyName options:0 range:NSMakeRange(0, [friendlyName length]) withTemplate:@""];
    607         friendlyName = [friendlyName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
    608         
    609         notification.title = [NSString stringWithFormat:@"%@ Played an Alarm", friendlyName];
    610         notification.informativeText = [NSString stringWithFormat:@"%@ requested your attention by playing a scheduled alarm", friendlyName];
    611         notification.identifier = self.fileURL.path;
    612         notification.deliveryDate = [NSDate dateWithTimeIntervalSinceNow:time_to_alarm];
    613         notification.soundName = NSUserNotificationDefaultSoundName;
    614         [[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification];
    615         [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBNotificationsUsed"];
    616     }
    617     [_view setRumble:0];
    618 }
    619 
    620 - (void) start
    621 {
    622     if (!self.mainWindow) {
    623         NSLog(@"Bug, reference leak to Document %@", self);
    624         return;
    625     }
    626     dispatch_async(dispatch_get_main_queue(), ^{
    627         [self updateDebuggerButtons];
    628         [_slave updateDebuggerButtons];
    629     });
    630     self.gbsPlayPauseButton.state = true;
    631     self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0;
    632     if (_master) {
    633         [_master start];
    634         return;
    635     }
    636     if (_running) return;
    637     _running = true;
    638     NSThread *emulationThraed = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    639     _emulationThread = emulationThraed;
    640     [emulationThraed start];
    641 }
    642 
    643 - (void) stop
    644 {
    645     dispatch_async(dispatch_get_main_queue(), ^{
    646         [self updateDebuggerButtons];
    647         [_slave updateDebuggerButtons];
    648     });
    649     self.gbsPlayPauseButton.state = false;
    650     if (_master) {
    651         if (!_master->_running) return;
    652         GB_debugger_set_disabled(&_gb, true);
    653         if (GB_debugger_is_stopped(&_gb)) {
    654             [self interruptDebugInputRead];
    655         }
    656         [_master stop];
    657         GB_debugger_set_disabled(&_gb, false);
    658         return;
    659     }
    660     if (!_running) return;
    661     GB_debugger_set_disabled(&_gb, true);
    662     if (GB_debugger_is_stopped(&_gb)) {
    663         [self interruptDebugInputRead];
    664     }
    665     [_audioLock lock];
    666     _stopping = true;
    667     [_audioLock signal];
    668     [_audioLock unlock];
    669     _running = false;
    670     while (_stopping) {
    671         [_audioLock lock];
    672         [_audioLock signal];
    673         [_audioLock unlock];
    674     }
    675     GB_debugger_set_disabled(&_gb, false);
    676 }
    677 
    678 - (NSString *)bootROMPathForName:(NSString *)name
    679 {
    680     NSURL *url = [[NSUserDefaults standardUserDefaults] URLForKey:@"GBBootROMsFolder"];
    681     if (url) {
    682         NSString *path = [url path];
    683         path = [path stringByAppendingPathComponent:name];
    684         path = [path stringByAppendingPathExtension:@"bin"];
    685         if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
    686             return path;
    687         }
    688     }
    689     
    690     return [[NSBundle mainBundle] pathForResource:name ofType:@"bin"];
    691 }
    692 
    693 - (void)loadBootROM: (GB_boot_rom_t)type
    694 {
    695     static NSString *const names[] = {
    696         [GB_BOOT_ROM_DMG_0] = @"dmg0_boot",
    697         [GB_BOOT_ROM_DMG] = @"dmg_boot",
    698         [GB_BOOT_ROM_MGB] = @"mgb_boot",
    699         [GB_BOOT_ROM_SGB] = @"sgb_boot",
    700         [GB_BOOT_ROM_SGB2] = @"sgb2_boot",
    701         [GB_BOOT_ROM_CGB_0] = @"cgb0_boot",
    702         [GB_BOOT_ROM_CGB] = @"cgb_boot",
    703         [GB_BOOT_ROM_CGB_E] = @"cgbE_boot",
    704         [GB_BOOT_ROM_AGB_0] = @"agb0_boot",
    705         [GB_BOOT_ROM_AGB] = @"agb_boot",
    706     };
    707     NSString *name = names[type];
    708     NSString *path = [self bootROMPathForName:name];
    709     /* These boot types are not commonly available, and they are indentical
    710        from an emulator perspective, so fall back to the more common variants
    711        if they can't be found. */
    712     if (!path && type == GB_BOOT_ROM_CGB_E) {
    713         [self loadBootROM:GB_BOOT_ROM_CGB];
    714         return;
    715     }
    716     if (!path && type == GB_BOOT_ROM_AGB_0) {
    717         [self loadBootROM:GB_BOOT_ROM_AGB];
    718         return;
    719     }
    720     GB_load_boot_rom(&_gb, [path UTF8String]);
    721 }
    722 
    723 - (enum model)bestModelForROM
    724 {
    725     uint8_t *rom = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, NULL);
    726     if (!rom) return MODEL_CGB;
    727     
    728     if (rom[0x143] & 0x80) { // Has CGB features
    729         return MODEL_CGB;
    730     }
    731     if (rom[0x146] == 3) { // Has SGB features
    732         return MODEL_SGB;
    733     }
    734     
    735     if (rom[0x14B] == 1) { // Nintendo-licensed (most likely has boot ROM palettes)
    736         return MODEL_CGB;
    737     }
    738 
    739     if (rom[0x14B] == 0x33 &&
    740         rom[0x144] == '0' &&
    741         rom[0x145] == '1') { // Ditto
    742         return MODEL_CGB;
    743     }
    744     
    745     return MODEL_DMG;
    746 }
    747 
    748 - (IBAction)reset:(id)sender
    749 {
    750     [self stop];
    751     size_t old_width = GB_get_screen_width(&_gb);
    752     
    753     if ([sender tag] > MODEL_NONE) {
    754         /* User explictly selected a model, save the preference */
    755         _currentModel = (enum model)[sender tag];
    756         _usesAutoModel = _currentModel == MODEL_AUTO;
    757         [[NSUserDefaults standardUserDefaults] setInteger:_currentModel forKey:@"GBEmulatedModel"];
    758     }
    759     
    760     /* Reload the ROM, SAV and SYM files */
    761     [self loadROM];
    762 
    763     if ([sender tag] == MODEL_QUICK_RESET) {
    764         GB_quick_reset(&_gb);
    765     }
    766     else {
    767         GB_switch_model_and_reset(&_gb, [self internalModel]);
    768     }
    769     
    770     if (old_width != GB_get_screen_width(&_gb)) {
    771         [self.view screenSizeChanged];
    772     }
    773     [self updateMinSize];
    774     
    775     [self start];
    776     if (_gbsTracks) {
    777         [self changeGBSTrack:sender];
    778     }
    779 
    780     if (_hexController) {
    781         /* Verify bank sanity, especially when switching models. */
    782         [(GBMemoryByteArray *)(_hexController.byteArray) setSelectedBank:0];
    783         [self hexUpdateBank:self.memoryBankInput ignoreErrors:true];
    784     }
    785     
    786     char title[17];
    787     GB_get_rom_title(&_gb, title);
    788     [self.osdView displayText:[NSString stringWithFormat:@"SameBoy v" GB_VERSION "\n%s\n%08X", title, GB_get_rom_crc32(&_gb)]];
    789 }
    790 
    791 - (IBAction)togglePause:(id)sender
    792 {
    793     if (_master) {
    794         [_master togglePause:sender];
    795         return;
    796     }
    797     if (_running) {
    798         [self stop];
    799     }
    800     else {
    801         [self start];
    802     }
    803 }
    804 
    805 - (void)dealloc
    806 {
    807     [_cameraSession stopRunning];
    808     self.view.gb = NULL;
    809     GB_free(&_gb);
    810     if (_cameraImage) {
    811         CVBufferRelease(_cameraImage);
    812     }
    813     if (_audioBuffer) {
    814         free(_audioBuffer);
    815     }
    816 }
    817 
    818 - (void)batteryTimerExpired
    819 {
    820     if (_dirtyBattery && !GB_get_battery_dirty(&_gb)) {
    821         GB_save_battery(&_gb, self.savPath.UTF8String);
    822     }
    823     
    824     _dirtyBattery = GB_get_battery_dirty(&_gb);
    825     GB_clear_battery_dirty(&_gb);
    826 }
    827 
    828 - (NSFont *)debuggerFontOfSize:(unsigned)size
    829 {
    830     if (!size) {
    831         size = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBDebuggerFontSize"];
    832     }
    833     
    834     bool retry = false;
    835     
    836 again:;
    837     NSString *selectedFont = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBDebuggerFont"];
    838     if (@available(macOS 10.15, *)) {
    839         if ([selectedFont isEqual:@"SF Mono"]) {
    840             return [NSFont monospacedSystemFontOfSize:size weight:NSFontWeightRegular];
    841         }
    842     }
    843     
    844     NSFont *ret = [NSFont fontWithName:selectedFont size:size];
    845     if (ret) return ret;
    846     
    847     if (retry) {
    848         return [NSFont userFixedPitchFontOfSize:size];
    849     }
    850     [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"GBDebuggerFont"];
    851     retry = true;
    852     goto again;
    853 }
    854 
    855 - (void)updateFonts
    856 {
    857     _hexController.font = [self debuggerFontOfSize:12];
    858     [self.paletteView reloadData:self];
    859     [self.objectView reloadData:self];
    860     
    861     NSFont *newFont = [self debuggerFontOfSize:0];
    862     NSFont *newBoldFont = [[NSFontManager sharedFontManager] convertFont:newFont toHaveTrait:NSBoldFontMask];
    863     self.debuggerSideViewInput.font = newFont;
    864     
    865     unsigned inputHeight = MAX(ceil([@" " sizeWithAttributes:@{
    866         NSFontAttributeName: newFont
    867     }].height) + 6, 26);
    868     
    869     
    870     NSRect frame = _consoleInput.frame;
    871     unsigned oldHeight = frame.size.height;
    872     frame.size.height = inputHeight;
    873     _consoleInput.frame = frame;
    874     
    875     frame = _debugBar.frame;
    876     frame.origin.y += (signed)(inputHeight - oldHeight);
    877     _debugBar.frame = frame;
    878     
    879     frame = _debuggerScrollView.frame;
    880     frame.origin.y += (signed)(inputHeight - oldHeight);
    881     frame.size.height -= (signed)(inputHeight - oldHeight);
    882     _debuggerScrollView.frame = frame;
    883     
    884     _consoleInput.font = newFont;
    885     
    886     for (NSTextView *view in @[_debuggerSideView, _consoleOutput]) {
    887         NSMutableAttributedString *newString = view.attributedString.mutableCopy;
    888         [view.attributedString enumerateAttribute:NSFontAttributeName
    889                                           inRange:NSMakeRange(0, view.attributedString.length)
    890                                           options:0
    891                                        usingBlock:^(NSFont *value, NSRange range, BOOL *stop) {
    892             if ([[NSFontManager sharedFontManager] fontNamed:value.fontName hasTraits:NSBoldFontMask]) {
    893                 [newString addAttributes:@{
    894                     NSFontAttributeName: newBoldFont
    895                 } range:range];
    896             }
    897             else {
    898                 [newString addAttributes:@{
    899                     NSFontAttributeName: newFont
    900                 } range:range];
    901             }
    902         }];
    903         [view.textStorage setAttributedString:newString];
    904     }
    905     [_consoleOutput scrollToEndOfDocument:nil];
    906 }
    907 
    908 - (void)windowControllerDidLoadNib:(NSWindowController *)aController 
    909 {
    910     [super windowControllerDidLoadNib:aController];
    911     // Interface Builder bug?
    912     [self.consoleWindow setContentSize:self.consoleWindow.frame.size];
    913     /* Close Open Panels, if any */
    914     for (NSWindow *window in [[NSApplication sharedApplication] windows]) {
    915         if ([window isKindOfClass:[NSOpenPanel class]]) {
    916             [(NSOpenPanel *)window cancel:self];
    917         }
    918     }
    919     
    920     NSMutableParagraphStyle *paragraph_style = [[NSMutableParagraphStyle alloc] init];
    921     [paragraph_style setLineSpacing:2];
    922         
    923     self.debuggerSideViewInput.font = [self debuggerFontOfSize:0];
    924     self.debuggerSideViewInput.textColor = [NSColor whiteColor];
    925     self.debuggerSideViewInput.defaultParagraphStyle = paragraph_style;
    926     [self.debuggerSideViewInput setString:@"registers\nbacktrace\n"];
    927     ((GBTerminalTextFieldCell *)self.consoleInput.cell).gb = &_gb;
    928     [[NSNotificationCenter defaultCenter] addObserver:self
    929                                              selector:@selector(updateSideView)
    930                                                  name:NSTextDidChangeNotification
    931                                                object:self.debuggerSideViewInput];
    932     
    933     self.consoleOutput.textContainerInset = NSMakeSize(4, 4);
    934     [self.view becomeFirstResponder];
    935     self.view.frameBlendingMode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"];
    936     CGRect window_frame = self.mainWindow.frame;
    937     window_frame.size.width  = MAX([[NSUserDefaults standardUserDefaults] integerForKey:@"LastWindowWidth"],
    938                                   window_frame.size.width);
    939     window_frame.size.height = MAX([[NSUserDefaults standardUserDefaults] integerForKey:@"LastWindowHeight"],
    940                                    window_frame.size.height);
    941     [self.mainWindow setFrame:window_frame display:true];
    942     self.vramStatusLabel.cell.backgroundStyle = NSBackgroundStyleRaised;
    943         
    944     NSUInteger height_diff = self.vramWindow.frame.size.height - self.vramWindow.contentView.frame.size.height;
    945     CGRect vram_window_rect = self.vramWindow.frame;
    946     vram_window_rect.size.height = 384 + height_diff + 48;
    947     [self.vramWindow setFrame:vram_window_rect display:true animate:false];
    948     
    949     
    950     if (@available(macOS 11.0, *)) {
    951         self.consoleWindow.subtitle = [self.fileURL.path lastPathComponent];
    952         self.memoryWindow.subtitle = [self.fileURL.path lastPathComponent];
    953         self.vramWindow.subtitle = [self.fileURL.path lastPathComponent];
    954     }
    955     else {
    956         self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [self.fileURL.path lastPathComponent]];
    957         self.memoryWindow.title = [NSString stringWithFormat:@"Memory – %@", [self.fileURL.path lastPathComponent]];
    958         self.vramWindow.title = [NSString stringWithFormat:@"VRAM Viewer – %@", [self.fileURL.path lastPathComponent]];
    959     }
    960     
    961     self.consoleWindow.level = NSNormalWindowLevel;
    962     
    963     self.debuggerSplitView.dividerColor = self.debuggerVerticalLine.borderColor;
    964     [self.debuggerVerticalLine removeFromSuperview]; // No longer used, just there for the color
    965     if (@available(macOS 11.0, *)) {
    966         self.memoryWindow.toolbarStyle = NSWindowToolbarStyleExpanded;
    967         self.printerFeedWindow.toolbarStyle = NSWindowToolbarStyleUnifiedCompact;
    968         self.printerFeedWindow.toolbar.items[1].image =
    969             [NSImage imageWithSystemSymbolName:@"square.and.arrow.down"
    970                       accessibilityDescription:@"Save"];
    971         self.printerFeedWindow.toolbar.items[2].image =
    972             [NSImage imageWithSystemSymbolName:@"printer"
    973                       accessibilityDescription:@"Print"];
    974         self.printerFeedWindow.toolbar.items[1].bordered = false;
    975         self.printerFeedWindow.toolbar.items[2].bordered = false;
    976     }
    977     else {
    978         NSToolbarItem *spinner = self.printerFeedWindow.toolbar.items[0];
    979         [self.printerFeedWindow.toolbar removeItemAtIndex:0];
    980         [self.printerFeedWindow.toolbar insertItemWithItemIdentifier:spinner.itemIdentifier atIndex:2];
    981         [self.printerFeedWindow.toolbar removeItemAtIndex:1];
    982         [self.printerFeedWindow.toolbar insertItemWithItemIdentifier:NSToolbarPrintItemIdentifier
    983                                                              atIndex:1];
    984         [self.printerFeedWindow.toolbar insertItemWithItemIdentifier:NSToolbarFlexibleSpaceItemIdentifier
    985                                                              atIndex:2];
    986     }
    987     
    988     
    989     [[NSNotificationCenter defaultCenter] addObserver:self
    990                                              selector:@selector(updatePalette)
    991                                                  name:@"GBColorPaletteChanged"
    992                                                object:nil];
    993     
    994     __unsafe_unretained Document *weakSelf = self;
    995     [self observeStandardDefaultsKey:@"GBFrameBlendingMode"
    996                            withBlock:^(NSNumber *value) {
    997         weakSelf.view.frameBlendingMode = (GB_frame_blending_mode_t)value.unsignedIntValue;
    998     }];
    999     
   1000     [self observeStandardDefaultsKey:@"GBDMGModel" withBlock:^(id newValue) {
   1001         weakSelf->_modelsChanging = true;
   1002         if (weakSelf->_currentModel == MODEL_DMG) {
   1003             [weakSelf reset:nil];
   1004         }
   1005         weakSelf->_modelsChanging = false;
   1006     }];
   1007     
   1008     [self observeStandardDefaultsKey:@"GBSGBModel" withBlock:^(id newValue) {
   1009         weakSelf->_modelsChanging = true;
   1010         if (weakSelf->_currentModel == MODEL_SGB) {
   1011             [weakSelf reset:nil];
   1012         }
   1013         weakSelf->_modelsChanging = false;
   1014     }];
   1015     
   1016     [self observeStandardDefaultsKey:@"GBCGBModel" withBlock:^(id newValue) {
   1017         weakSelf->_modelsChanging = true;
   1018         if (weakSelf->_currentModel == MODEL_CGB) {
   1019             [weakSelf reset:nil];
   1020         }
   1021         weakSelf->_modelsChanging = false;
   1022     }];
   1023     
   1024     [self observeStandardDefaultsKey:@"GBAGBModel" withBlock:^(id newValue) {
   1025         weakSelf->_modelsChanging = true;
   1026         if (weakSelf->_currentModel == MODEL_AGB) {
   1027             [weakSelf reset:nil];
   1028         }
   1029         weakSelf->_modelsChanging = false;
   1030     }];
   1031     
   1032     
   1033     [self observeStandardDefaultsKey:@"GBVolume" withBlock:^(id newValue) {
   1034         weakSelf->_volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"];
   1035     }];
   1036     
   1037     
   1038     _currentModel = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBEmulatedModel"];
   1039     _usesAutoModel = _currentModel == MODEL_AUTO;
   1040     
   1041     [self initCommon];
   1042     self.view.gb = &_gb;
   1043     self.view.osdView = _osdView;
   1044     [self.view screenSizeChanged];
   1045     if ([self loadROM]) {
   1046         _mainWindow.alphaValue = 0; // Hack hack ugly hack
   1047         dispatch_async(dispatch_get_main_queue(), ^{
   1048             [self close];
   1049         });
   1050     }
   1051     else {
   1052         [self reset:nil];
   1053     }
   1054 }
   1055 
   1056 - (void)initMemoryView
   1057 {
   1058     _hexController = [[HFController alloc] init];
   1059     _hexController.font = [self debuggerFontOfSize:12];
   1060     [_hexController setBytesPerColumn:1];
   1061     [_hexController setEditMode:HFOverwriteMode];
   1062     
   1063     [_hexController setByteArray:[[GBMemoryByteArray alloc] initWithDocument:self]];
   1064 
   1065     /* Here we're going to make three representers - one for the hex, one for the ASCII, and one for the scrollbar.  To lay these all out properly, we'll use a fourth HFLayoutRepresenter. */
   1066     HFLayoutRepresenter *layoutRep = [[HFLayoutRepresenter alloc] init];
   1067     HFHexTextRepresenter *hexRep = [[HFHexTextRepresenter alloc] init];
   1068     HFStringEncodingTextRepresenter *asciiRep = [[HFStringEncodingTextRepresenter alloc] init];
   1069     HFVerticalScrollerRepresenter *scrollRep = [[HFVerticalScrollerRepresenter alloc] init];
   1070     _lineRep = [[HFLineCountingRepresenter alloc] init];
   1071     _statusRep = [[GBHexStatusBarRepresenter alloc] init];
   1072     _statusRep.gb = &_gb;
   1073     _statusRep.bankForDescription = -1;
   1074 
   1075     _lineRep.lineNumberFormat = HFLineNumberFormatHexadecimal;
   1076 
   1077     /* Add all our reps to the controller. */
   1078     [_hexController addRepresenter:layoutRep];
   1079     [_hexController addRepresenter:hexRep];
   1080     [_hexController addRepresenter:asciiRep];
   1081     [_hexController addRepresenter:scrollRep];
   1082     [_hexController addRepresenter:_lineRep];
   1083     [_hexController addRepresenter:_statusRep];
   1084 
   1085     /* Tell the layout rep which reps it should lay out. */
   1086     [layoutRep addRepresenter:hexRep];
   1087     [layoutRep addRepresenter:scrollRep];
   1088     [layoutRep addRepresenter:asciiRep];
   1089     [layoutRep addRepresenter:_lineRep];
   1090     [layoutRep addRepresenter:_statusRep];
   1091 
   1092 
   1093     [(NSView *)[hexRep view] setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
   1094 
   1095     /* Grab the layout rep's view and stick it into our container. */
   1096     NSView *layoutView = [layoutRep view];
   1097     NSRect layoutViewFrame = self.memoryView.frame;
   1098     [layoutView setFrame:layoutViewFrame];
   1099     [layoutView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable | NSViewMaxYMargin];
   1100     [self.memoryView addSubview:layoutView];
   1101     self.memoryView = layoutView;
   1102 
   1103     CGSize contentSize = _memoryWindow.contentView.frame.size;
   1104     while (_hexController.bytesPerLine < 16) {
   1105         contentSize.width += 4;
   1106         [_memoryWindow setContentSize:contentSize];
   1107     }
   1108     while (_hexController.bytesPerLine > 16) {
   1109         contentSize.width -= 4;
   1110         [_memoryWindow setContentSize:contentSize];
   1111     }
   1112     
   1113     self.memoryBankItem.enabled = false;
   1114 }
   1115 
   1116 - (NSString *)windowNibName 
   1117 {
   1118     // Override returning the nib file name of the document
   1119     // If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, you should remove this method and override -makeWindowControllers instead.
   1120     return @"Document";
   1121 }
   1122 
   1123 - (BOOL)readFromFile:(NSString *)fileName ofType:(NSString *)type
   1124 {
   1125     return true;
   1126 }
   1127 
   1128 - (IBAction)changeGBSTrack:(id)sender
   1129 {
   1130     if (!_running) {
   1131         [self start];
   1132     }
   1133     [self performAtomicBlock:^{
   1134         GB_gbs_switch_track(&_gb, self.gbsTracks.indexOfSelectedItem);
   1135     }];
   1136 }
   1137 - (IBAction)gbsNextPrevPushed:(id)sender
   1138 {
   1139     if (self.gbsNextPrevButton.selectedSegment == 0) {
   1140         // Previous
   1141         if (self.gbsTracks.indexOfSelectedItem == 0) {
   1142             [self.gbsTracks selectItemAtIndex:self.gbsTracks.numberOfItems - 1];
   1143         }
   1144         else {
   1145             [self.gbsTracks selectItemAtIndex:self.gbsTracks.indexOfSelectedItem - 1];
   1146         }
   1147     }
   1148     else {
   1149         // Next
   1150         if (self.gbsTracks.indexOfSelectedItem == self.gbsTracks.numberOfItems - 1) {
   1151             [self.gbsTracks selectItemAtIndex: 0];
   1152         }
   1153         else {
   1154             [self.gbsTracks selectItemAtIndex:self.gbsTracks.indexOfSelectedItem + 1];
   1155         }
   1156     }
   1157     [self changeGBSTrack:sender];
   1158 }
   1159 
   1160 - (void)prepareGBSInterface: (GB_gbs_info_t *)info
   1161 {
   1162     GB_set_rendering_disabled(&_gb, true);
   1163     _view = nil;
   1164     for (NSView *view in [_mainWindow.contentView.subviews copy]) {
   1165         [view removeFromSuperview];
   1166     }
   1167     if (@available(macOS 11, *)) {
   1168         [[NSBundle mainBundle] loadNibNamed:@"GBS11" owner:self topLevelObjects:nil];
   1169     }
   1170     else {
   1171         [[NSBundle mainBundle] loadNibNamed:@"GBS" owner:self topLevelObjects:nil];
   1172     }
   1173     [_mainWindow setContentSize:self.gbsPlayerView.bounds.size];
   1174     _mainWindow.styleMask &= ~NSWindowStyleMaskResizable;
   1175     dispatch_async(dispatch_get_main_queue(), ^{ // Cocoa is weird, no clue why it's needed
   1176         [_mainWindow standardWindowButton:NSWindowZoomButton].enabled = false;
   1177     });
   1178     [_mainWindow.contentView addSubview:self.gbsPlayerView];
   1179     _mainWindow.movableByWindowBackground = true;
   1180     [_mainWindow setContentBorderThickness:24 forEdge:NSRectEdgeMinY];
   1181 
   1182     self.gbsTitle.stringValue = [NSString stringWithCString:info->title encoding:NSISOLatin1StringEncoding] ?: @"GBS Player";
   1183     self.gbsAuthor.stringValue = [NSString stringWithCString:info->author encoding:NSISOLatin1StringEncoding] ?: @"Unknown Composer";
   1184     NSString *copyright = [NSString stringWithCString:info->copyright encoding:NSISOLatin1StringEncoding];
   1185     if (copyright) {
   1186         copyright = [@"©" stringByAppendingString:copyright];
   1187     }
   1188     self.gbsCopyright.stringValue = copyright ?: @"Missing copyright information";
   1189     for (unsigned i = 0; i < info->track_count; i++) {
   1190         [self.gbsTracks addItemWithTitle:[NSString stringWithFormat:@"Track %u", i + 1]];
   1191     }
   1192     [self.gbsTracks selectItemAtIndex:info->first_track];
   1193     self.gbsPlayPauseButton.image.template = true;
   1194     self.gbsPlayPauseButton.alternateImage.template = true;
   1195     self.gbsRewindButton.image.template = true;
   1196     for (unsigned i = 0; i < 2; i++) {
   1197         [self.gbsNextPrevButton imageForSegment:i].template = true;
   1198     }
   1199 
   1200     if (!_audioClient.isPlaying) {
   1201         [_audioClient start];
   1202     }
   1203     
   1204     if (@available(macOS 10.10, *)) {
   1205         _mainWindow.titlebarAppearsTransparent = true;
   1206     }
   1207     
   1208     if (@available(macOS 26.0, *)) {
   1209         // There's a new minimum width for segmented controls in Solarium
   1210         NSRect frame = _gbsNextPrevButton.frame;
   1211         frame.origin.x -= 16;
   1212         _gbsNextPrevButton.frame = frame;
   1213         
   1214         frame = _gbsTracks.frame;
   1215         frame.size.width -= 16;
   1216         _gbsTracks.frame = frame;
   1217     }
   1218 }
   1219 
   1220 - (bool)isCartContainer
   1221 {
   1222     return [self.fileName.pathExtension.lowercaseString isEqualToString:@"gbcart"];
   1223 }
   1224 
   1225 - (NSString *)savPath
   1226 {
   1227     if (self.isCartContainer) {
   1228         return [self.fileName stringByAppendingPathComponent:@"battery.sav"];
   1229     }
   1230     
   1231     return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"sav"].path;
   1232 }
   1233 
   1234 - (NSString *)chtPath
   1235 {
   1236     if (self.isCartContainer) {
   1237         return [self.fileName stringByAppendingPathComponent:@"cheats.cht"];
   1238     }
   1239     
   1240     return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"cht"].path;
   1241 }
   1242 
   1243 - (NSString *)saveStatePath:(unsigned)index
   1244 {
   1245     if (self.isCartContainer) {
   1246         return [self.fileName stringByAppendingPathComponent:[NSString stringWithFormat:@"state.s%u", index]];
   1247     }
   1248     return [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"s%u", index]].path;
   1249 }
   1250 
   1251 - (NSString *)romPath
   1252 {
   1253     NSString *fileName = self.fileName;
   1254     if (self.isCartContainer) {
   1255         NSArray *paths = [[NSString stringWithContentsOfFile:[fileName stringByAppendingPathComponent:@"rom.gbl"]
   1256                                                     encoding:NSUTF8StringEncoding
   1257                                                        error:nil] componentsSeparatedByString:@"\n"];
   1258         fileName = nil;
   1259         bool needsRebuild = false;
   1260         for (NSString *path in paths) {
   1261             NSURL *url = [NSURL URLWithString:path relativeToURL:self.fileURL];
   1262             if ([[NSFileManager defaultManager] fileExistsAtPath:url.path]) {
   1263                 if (fileName && ![fileName isEqualToString:url.path]) {
   1264                     needsRebuild = true;
   1265                     break;
   1266                 }
   1267                 fileName = url.path;
   1268             }
   1269             else {
   1270                 needsRebuild = true;
   1271             }
   1272         }
   1273         if (fileName && needsRebuild) {
   1274             [[NSString stringWithFormat:@"%@\n%@\n%@",
   1275               [fileName pathRelativeToDirectory:self.fileName],
   1276               fileName,
   1277               [[NSURL fileURLWithPath:fileName].fileReferenceURL.absoluteString substringFromIndex:strlen("file://")]]
   1278              writeToFile:[self.fileName stringByAppendingPathComponent:@"rom.gbl"]
   1279              atomically:false
   1280              encoding:NSUTF8StringEncoding
   1281              error:nil];
   1282         }
   1283     }
   1284     
   1285     return fileName;
   1286 }
   1287 
   1288 static bool is_path_writeable(const char *path)
   1289 {
   1290     if (!access(path, W_OK)) return true;
   1291     int fd = creat(path, 0644);
   1292     if (fd == -1) return false;
   1293     close(fd);
   1294     unlink(path);
   1295     return true;
   1296 }
   1297 
   1298 - (int)loadROM
   1299 {
   1300     __block int ret = 0;
   1301     NSString *fileName = self.romPath;
   1302     if (!fileName) {
   1303         NSAlert *alert = [[NSAlert alloc] init];
   1304         [alert setMessageText:@"Could not locate the ROM referenced by this Game Boy Cartridge"];
   1305         [alert setAlertStyle:NSAlertStyleCritical];
   1306         [alert runModal];
   1307         return 1;
   1308     }
   1309     
   1310     NSString *rom_warnings = [self captureOutputForBlock:^{
   1311         if (!_romModified) {
   1312             GB_debugger_clear_symbols(&_gb);
   1313             if ([[[fileName pathExtension] lowercaseString] isEqualToString:@"isx"]) {
   1314                 ret = GB_load_isx(&_gb, fileName.UTF8String);
   1315                 if (!self.isCartContainer) {
   1316                     GB_load_battery(&_gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"ram"].path.UTF8String);
   1317                 }
   1318             }
   1319             else if ([[[fileName pathExtension] lowercaseString] isEqualToString:@"gbs"]) {
   1320                 __block GB_gbs_info_t info;
   1321                 ret = GB_load_gbs(&_gb, fileName.UTF8String, &info);
   1322                 [self prepareGBSInterface:&info];
   1323             }
   1324             else {
   1325                 ret = GB_load_rom(&_gb, [fileName UTF8String]);
   1326             }
   1327         }
   1328         if (GB_save_battery_size(&_gb)) {
   1329             if (!is_path_writeable(self.savPath.UTF8String)) {
   1330                 GB_log(&_gb, "The save path for this ROM is not writeable, progress will not be saved.\n");
   1331             }
   1332         }
   1333         GB_load_battery(&_gb, self.savPath.UTF8String);
   1334         GB_load_cheats(&_gb, self.chtPath.UTF8String, true);
   1335         dispatch_async(dispatch_get_main_queue(), ^{
   1336             [self.cheatWindowController cheatsUpdated];
   1337         });
   1338         GB_debugger_load_symbol_file(&_gb, [[[NSBundle mainBundle] pathForResource:@"registers" ofType:@"sym"] UTF8String]);
   1339         GB_debugger_load_symbol_file(&_gb, [[fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sym"].UTF8String);
   1340     }];
   1341     if (ret) {
   1342         NSAlert *alert = [[NSAlert alloc] init];
   1343         [alert setMessageText:rom_warnings?: @"Could not load ROM"];
   1344         [alert setAlertStyle:NSAlertStyleCritical];
   1345         [alert runModal];
   1346     }
   1347     else if (rom_warnings && !_romWarningIssued) {
   1348         _romWarningIssued = true;
   1349         [GBWarningPopover popoverWithContents:rom_warnings onWindow:self.mainWindow];
   1350     }
   1351     _fileModificationTime = [[NSFileManager defaultManager] attributesOfItemAtPath:fileName error:nil][NSFileModificationDate];
   1352     if (_usesAutoModel) {
   1353         _currentModel = [self bestModelForROM];
   1354     }
   1355     return ret;
   1356 }
   1357 
   1358 - (void)showWindows
   1359 {
   1360     if (GB_is_inited(&_gb)) {
   1361         if (![_fileModificationTime isEqualToDate:[[NSFileManager defaultManager] attributesOfItemAtPath:self.fileName error:nil][NSFileModificationDate]]) {
   1362             [self reset:nil];
   1363         }
   1364     }
   1365     [super showWindows];
   1366 }
   1367 
   1368 - (void)close
   1369 {
   1370     [self disconnectLinkCable];
   1371     if (!self.gbsPlayerView) {
   1372         [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.width forKey:@"LastWindowWidth"];
   1373         [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.height forKey:@"LastWindowHeight"];
   1374     }
   1375     [self stop];
   1376     [_consoleOutputLock lock];
   1377     [_consoleOutputTimer invalidate];
   1378     [_consoleOutputLock unlock];
   1379     [_consoleWindow close];
   1380     _consoleWindow = nil;
   1381     [_memoryWindow close];
   1382     _memoryWindow = nil;
   1383     [_vramWindow close];
   1384     _vramWindow = nil;
   1385     [_printerFeedWindow close];
   1386     _printerFeedWindow = nil;
   1387     [_cheatsWindow close];
   1388     _cheatsWindow = nil;
   1389     [_cheatSearchController.window close];
   1390     _cheatSearchController.window = nil;
   1391     [super close];
   1392 }
   1393 
   1394 - (IBAction) interrupt:(id)sender
   1395 {
   1396     [self log:"^C\n"];
   1397     GB_debugger_break(&_gb);
   1398     [self start];
   1399     [self.consoleWindow makeKeyAndOrderFront:nil];
   1400     double secondUsage = GB_debugger_get_second_cpu_usage(&_gb);
   1401     _cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100];
   1402     [self.consoleInput becomeFirstResponder];
   1403 }
   1404 
   1405 - (IBAction)mute:(id)sender
   1406 {
   1407     if (_audioClient.isPlaying) {
   1408         [_audioClient stop];
   1409     }
   1410     else {
   1411         [_audioClient start];
   1412         if (_volume == 0) {
   1413             [GBWarningPopover popoverWithContents:@"Warning: Volume is set to to zero in the preferences panel" onWindow:self.mainWindow];
   1414         }
   1415     }
   1416     [[NSUserDefaults standardUserDefaults] setBool:!_audioClient.isPlaying forKey:@"Mute"];
   1417 }
   1418 
   1419 - (bool) isPaused
   1420 {
   1421     if (self.partner) {
   1422         return !self.partner->_running || GB_debugger_is_stopped(&_gb) || GB_debugger_is_stopped(&self.partner->_gb);
   1423     }
   1424     return (!_running) || GB_debugger_is_stopped(&_gb);
   1425 }
   1426 
   1427 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)anItem
   1428 {
   1429     if ([anItem action] == @selector(mute:)) {
   1430         if (_running) {
   1431             [(NSMenuItem *)anItem setState:!_audioClient.isPlaying];
   1432         }
   1433         else {
   1434             [(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]];
   1435         }
   1436     }
   1437     else if ([anItem action] == @selector(togglePause:)) {
   1438         [(NSMenuItem *)anItem setState:self.isPaused];
   1439         return !GB_debugger_is_stopped(&_gb);
   1440     }
   1441     else if ([anItem action] == @selector(reset:) && anItem.tag != MODEL_NONE && anItem.tag != MODEL_QUICK_RESET) {
   1442         [(NSMenuItem *)anItem setState:(anItem.tag == _currentModel) || (anItem.tag == MODEL_AUTO && _usesAutoModel)];
   1443     }
   1444     else if ([anItem action] == @selector(interrupt:)) {
   1445         if (![[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) {
   1446             return false;
   1447         }
   1448     }
   1449     else if ([anItem action] == @selector(disconnectAllAccessories:)) {
   1450         [(NSMenuItem *)anItem setState:GB_get_built_in_accessory(&_gb) == GB_ACCESSORY_NONE && !self.partner];
   1451     }
   1452     else if ([anItem action] == @selector(connectPrinter:)) {
   1453         [(NSMenuItem *)anItem setState:GB_get_built_in_accessory(&_gb) == GB_ACCESSORY_PRINTER];
   1454     }
   1455     else if ([anItem action] == @selector(connectWorkboy:)) {
   1456         [(NSMenuItem *)anItem setState:GB_get_built_in_accessory(&_gb) == GB_ACCESSORY_WORKBOY];
   1457     }
   1458     else if ([anItem action] == @selector(connectLinkCable:)) {
   1459         [(NSMenuItem *)anItem setState:[(NSMenuItem *)anItem representedObject] == _master ||
   1460                                        [(NSMenuItem *)anItem representedObject] == _slave];
   1461     }
   1462     else if ([anItem action] == @selector(toggleCheats:)) {
   1463         [(NSMenuItem *)anItem setState:GB_cheats_enabled(&_gb)];
   1464     }
   1465     else if ([anItem action] == @selector(toggleDisplayBackground:)) {
   1466         [(NSMenuItem *)anItem setState:!GB_is_background_rendering_disabled(&_gb)];
   1467     }
   1468     else if ([anItem action] == @selector(toggleDisplayObjects:)) {
   1469         [(NSMenuItem *)anItem setState:!GB_is_object_rendering_disabled(&_gb)];
   1470     }
   1471     else if ([anItem action] == @selector(toggleAudioRecording:)) {
   1472         [(NSMenuItem *)anItem setTitle:_isRecordingAudio? @"Stop Audio Recording" : @"Start Audio Recording…"];
   1473     }
   1474     else if ([anItem action] == @selector(toggleAudioChannel:)) {
   1475         [(NSMenuItem *)anItem setState:!GB_is_channel_muted(&_gb, [anItem tag])];
   1476     }
   1477     else if ([anItem action] == @selector(increaseWindowSize:)) {
   1478         return [self newRect:NULL forWindow:_mainWindow action:GBWindowResizeActionIncrease];
   1479     }
   1480     else if ([anItem action] == @selector(decreaseWindowSize:)) {
   1481         return [self newRect:NULL forWindow:_mainWindow action:GBWindowResizeActionDecrease];
   1482     }
   1483     else if ([anItem action] == @selector(reloadROM:)) {
   1484         return !_gbsTracks;
   1485     }
   1486     else if ([anItem action] == @selector(saveDocument:)) {
   1487         return _romModified;
   1488     }
   1489     else if ([anItem action] == @selector(saveDocumentAs:)) {
   1490         return _romModified && !self.isCartContainer;
   1491     }
   1492     
   1493     return [super validateUserInterfaceItem:anItem];
   1494 }
   1495 
   1496 
   1497 - (void) windowWillEnterFullScreen:(NSNotification *)notification
   1498 {
   1499     _fullScreen = true;
   1500     self.view.mouseHidingEnabled = _running;
   1501 }
   1502 
   1503 - (void) windowWillExitFullScreen:(NSNotification *)notification
   1504 {
   1505     _fullScreen = false;
   1506     self.view.mouseHidingEnabled = false;
   1507 }
   1508 
   1509 enum GBWindowResizeAction
   1510 {
   1511     GBWindowResizeActionZoom,
   1512     GBWindowResizeActionIncrease,
   1513     GBWindowResizeActionDecrease,
   1514 };
   1515 
   1516 - (bool)newRect:(NSRect *)rect forWindow:(NSWindow *)window action:(enum GBWindowResizeAction)action
   1517 {
   1518     if (_fullScreen) return false;
   1519     if (!rect) {
   1520         rect = alloca(sizeof(*rect));
   1521     }
   1522     
   1523     size_t width  = GB_get_screen_width(&_gb),
   1524     height = GB_get_screen_height(&_gb);
   1525     
   1526     *rect = window.contentView.frame;
   1527     
   1528     unsigned titlebarSize = window.contentView.superview.frame.size.height - rect->size.height;
   1529     
   1530     unsigned stepX = width / [[window screen] backingScaleFactor];
   1531     unsigned stepY = height / [[window screen] backingScaleFactor];
   1532     
   1533     if (action == GBWindowResizeActionDecrease) {
   1534         if (rect->size.width <= width || rect->size.height <= height) {
   1535             return false;
   1536         }
   1537     }
   1538     
   1539     typeof(floor) *roundFunc = action == GBWindowResizeActionDecrease? ceil : floor;
   1540     unsigned currentFactor = MIN(roundFunc(rect->size.width / stepX), roundFunc(rect->size.height / stepY));
   1541     
   1542     rect->size.width = currentFactor * stepX;
   1543     rect->size.height = currentFactor * stepY + titlebarSize;
   1544     
   1545     if (action == GBWindowResizeActionDecrease) {
   1546         rect->size.width -= stepX;
   1547         rect->size.height -= stepY;
   1548     }
   1549     else {
   1550         rect->size.width += stepX;
   1551         rect->size.height += stepY;
   1552     }
   1553     
   1554     NSRect maxRect = [_mainWindow screen].visibleFrame;
   1555     
   1556     if (rect->size.width > maxRect.size.width ||
   1557         rect->size.height > maxRect.size.height) {
   1558         if (action == GBWindowResizeActionIncrease) {
   1559             return false;
   1560         }
   1561         rect->size.width = width;
   1562         rect->size.height = height + titlebarSize;
   1563     }
   1564     
   1565     rect->origin = window.frame.origin;
   1566     if (action == GBWindowResizeActionZoom) {
   1567         rect->origin.y -= rect->size.height - window.frame.size.height;
   1568     }
   1569     else {
   1570         rect->origin.y -= (rect->size.height - window.frame.size.height) / 2;
   1571         rect->origin.x -= (rect->size.width - window.frame.size.width) / 2;
   1572     }
   1573     
   1574     if (rect->origin.x < maxRect.origin.x) {
   1575         rect->origin.x = maxRect.origin.x;
   1576     }
   1577     
   1578     if (rect->origin.y < maxRect.origin.y) {
   1579         rect->origin.y = maxRect.origin.y;
   1580     }
   1581     
   1582     if (rect->origin.x + rect->size.width > maxRect.origin.x + maxRect.size.width) {
   1583         rect->origin.x = maxRect.origin.x + maxRect.size.width - rect->size.width;
   1584     }
   1585     
   1586     if (rect->origin.y + rect->size.height > maxRect.origin.y + maxRect.size.height) {
   1587         rect->origin.y = maxRect.origin.y + maxRect.size.height - rect->size.height;
   1588     }
   1589     
   1590     return true;
   1591 }
   1592 
   1593 - (NSRect)windowWillUseStandardFrame:(NSWindow *)window defaultFrame:(NSRect)newFrame
   1594 {
   1595     if (_fullScreen) {
   1596         return newFrame;
   1597     }
   1598     [self newRect:&newFrame forWindow:window action:GBWindowResizeActionZoom];
   1599     return newFrame;
   1600 }
   1601 
   1602 
   1603 - (IBAction)increaseWindowSize:(id)sender
   1604 {
   1605     NSRect rect;
   1606     if ([self newRect:&rect forWindow:_mainWindow action:GBWindowResizeActionIncrease]) {
   1607         [_mainWindow setFrame:rect display:true animate:true];
   1608     }
   1609 }
   1610 
   1611 - (IBAction)decreaseWindowSize:(id)sender
   1612 {
   1613     NSRect rect;
   1614     if ([self newRect:&rect forWindow:_mainWindow action:GBWindowResizeActionDecrease]) {
   1615         [_mainWindow setFrame:rect display:true animate:true];
   1616     }
   1617 }
   1618 
   1619 - (void)appendPendingOutput
   1620 {
   1621     [_consoleOutputLock lock];
   1622     if (_shouldClearSideView) {
   1623         _shouldClearSideView = false;
   1624         [self.debuggerSideView setString:@""];
   1625     }
   1626     if (_pendingConsoleOutput) {
   1627         NSTextView *textView = _logToSideView? self.debuggerSideView : self.consoleOutput;
   1628         
   1629         [_hexController reloadData];
   1630         [self reloadVRAMData: nil];
   1631         
   1632         [textView.textStorage appendAttributedString:_pendingConsoleOutput];
   1633         if (!_logToSideView) {
   1634             [textView scrollToEndOfDocument:nil];
   1635         }
   1636         if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) {
   1637             [self.consoleWindow orderFront:nil];
   1638             /* Make sure mouse is not hidden while debugging */
   1639             self.view.mouseHidingEnabled = false;
   1640         }
   1641         _pendingConsoleOutput = nil;
   1642     }
   1643     [_consoleOutputLock unlock];
   1644 }
   1645 
   1646 - (void)log:(const char *)string withAttributes:(GB_log_attributes_t)attributes
   1647 {
   1648     NSString *nsstring = @(string); // For ref-counting
   1649     if (_capturedOutput) {
   1650         [_capturedOutput appendString:nsstring];
   1651         return;
   1652     }
   1653     
   1654     
   1655     NSFont *font = [self debuggerFontOfSize:0];
   1656     NSUnderlineStyle underline = NSUnderlineStyleNone;
   1657     if (attributes & GB_LOG_BOLD) {
   1658         font = [[NSFontManager sharedFontManager] convertFont:font toHaveTrait:NSBoldFontMask];
   1659     }
   1660     
   1661     if (attributes &  GB_LOG_UNDERLINE_MASK) {
   1662         underline = (attributes &  GB_LOG_UNDERLINE_MASK) == GB_LOG_DASHED_UNDERLINE? NSUnderlinePatternDot | NSUnderlineStyleSingle : NSUnderlineStyleSingle;
   1663     }
   1664     
   1665     NSMutableParagraphStyle *paragraph_style = [[NSMutableParagraphStyle alloc] init];
   1666     [paragraph_style setLineSpacing:2];
   1667     NSMutableAttributedString *attributed =
   1668     [[NSMutableAttributedString alloc] initWithString:nsstring
   1669                                            attributes:@{NSFontAttributeName: font,
   1670                                                         NSForegroundColorAttributeName: [NSColor whiteColor],
   1671                                                         NSUnderlineStyleAttributeName: @(underline),
   1672                                                         NSParagraphStyleAttributeName: paragraph_style}];
   1673     
   1674     [_consoleOutputLock lock];
   1675     if (!_pendingConsoleOutput) {
   1676         _pendingConsoleOutput = attributed;
   1677     }
   1678     else {
   1679         [_pendingConsoleOutput appendAttributedString:attributed];
   1680     }
   1681     
   1682     if (![_consoleOutputTimer isValid]) {
   1683         _consoleOutputTimer = [NSTimer timerWithTimeInterval:(NSTimeInterval)0.05 target:self selector:@selector(appendPendingOutput) userInfo:nil repeats:false];
   1684         [[NSRunLoop mainRunLoop] addTimer:_consoleOutputTimer forMode:NSDefaultRunLoopMode];
   1685     }
   1686     
   1687     [_consoleOutputLock unlock];
   1688 }
   1689 
   1690 - (IBAction)showConsoleWindow:(id)sender
   1691 {
   1692     [self.consoleWindow orderFront:nil];
   1693     double secondUsage = GB_debugger_get_second_cpu_usage(&_gb);
   1694     _cpuCounter.stringValue = [NSString stringWithFormat:@"%.2f%%", secondUsage * 100];
   1695 }
   1696 
   1697 - (void)queueDebuggerCommand:(NSString *)command
   1698 {
   1699     if (!_master && !_running && !GB_debugger_is_stopped(&_gb)) {
   1700         _debuggerCommandWhilePaused = command;
   1701         GB_debugger_break(&_gb);
   1702         [self start];
   1703         return;
   1704     }
   1705         
   1706     if (!_inSyncInput) {
   1707         [self log:">"];
   1708     }
   1709     [self log:[command UTF8String]];
   1710     [self log:"\n"];
   1711     [_hasDebuggerInput lock];
   1712     [_debuggerInputQueue addObject:command];
   1713     [_hasDebuggerInput unlockWithCondition:1];
   1714 }
   1715 
   1716 - (IBAction)consoleInput:(NSTextField *)sender 
   1717 {
   1718     NSString *line = [sender stringValue];
   1719     if ([line isEqualToString:@""] && _lastConsoleInput) {
   1720         line = _lastConsoleInput;
   1721     }
   1722     else if (line) {
   1723         _lastConsoleInput = line;
   1724     }
   1725     else {
   1726         line = @"";
   1727     }
   1728     
   1729     [self queueDebuggerCommand: line];
   1730 
   1731     [sender setStringValue:@""];
   1732 }
   1733 
   1734 - (void) interruptDebugInputRead
   1735 {
   1736     [_hasDebuggerInput lock];
   1737     [_debuggerInputQueue addObject:[NSNull null]];
   1738     [_hasDebuggerInput unlockWithCondition:1];
   1739 }
   1740 
   1741 - (void) updateSideView
   1742 {
   1743     if (!GB_debugger_is_stopped(&_gb)) {
   1744         return;
   1745     }
   1746     
   1747     if (![NSThread isMainThread]) {
   1748         dispatch_sync(dispatch_get_main_queue(), ^{
   1749             [self updateSideView];
   1750         });
   1751         return;
   1752     }
   1753     
   1754     [_consoleOutputLock lock];
   1755     _shouldClearSideView = true;
   1756     [self appendPendingOutput];
   1757     _logToSideView = true;
   1758     [_consoleOutputLock unlock];
   1759     
   1760     for (NSString *line in [self.debuggerSideViewInput.string componentsSeparatedByString:@"\n"]) {
   1761         NSString *stripped = [line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
   1762         if ([stripped length]) {
   1763             char *dupped = strdup([stripped UTF8String]);
   1764             GB_attributed_log(&_gb, GB_LOG_BOLD, "%s:\n", dupped);
   1765             GB_debugger_execute_command(&_gb, dupped);
   1766             GB_log(&_gb, "\n");
   1767             free(dupped);
   1768         }
   1769     }
   1770     
   1771     [_consoleOutputLock lock];
   1772     [self appendPendingOutput];
   1773     _logToSideView = false;
   1774     [_consoleOutputLock unlock];
   1775 }
   1776 
   1777 - (char *)getDebuggerInput
   1778 {
   1779     bool isPlaying = _audioClient.isPlaying;
   1780     if (isPlaying) {
   1781         [_audioClient stop];
   1782     }
   1783     [_audioLock lock];
   1784     [_audioLock signal];
   1785     [_audioLock unlock];
   1786     _inSyncInput = true;
   1787     [self updateSideView];
   1788     dispatch_async(dispatch_get_main_queue(), ^{
   1789         [self updateDebuggerButtons];
   1790     });
   1791     [self.partner updateDebuggerButtons];
   1792     [self log:">"];
   1793     if (_debuggerCommandWhilePaused) {
   1794         NSString *command = _debuggerCommandWhilePaused;
   1795         _debuggerCommandWhilePaused = nil;
   1796         dispatch_async(dispatch_get_main_queue(), ^{
   1797             [self queueDebuggerCommand:command];
   1798         });
   1799     }
   1800     [_hasDebuggerInput lockWhenCondition:1];
   1801     NSString *input = [_debuggerInputQueue firstObject];
   1802     [_debuggerInputQueue removeObjectAtIndex:0];
   1803     [_hasDebuggerInput unlockWithCondition:[_debuggerInputQueue count] != 0];
   1804     _inSyncInput = false;
   1805     _shouldClearSideView = true;
   1806     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC / 10)), dispatch_get_main_queue(), ^{
   1807         if (_shouldClearSideView) {
   1808             _shouldClearSideView = false;
   1809             [self.debuggerSideView setString:@""];
   1810         }
   1811         [self updateDebuggerButtons];
   1812         [self.partner updateDebuggerButtons];
   1813     });
   1814     if (isPlaying) {
   1815         [_audioClient start];
   1816     }
   1817     if ((id) input == [NSNull null]) {
   1818         return NULL;
   1819     }
   1820     return strdup([input UTF8String]);
   1821 }
   1822 
   1823 - (char *) getAsyncDebuggerInput
   1824 {
   1825     [_hasDebuggerInput lock];
   1826     NSString *input = [_debuggerInputQueue firstObject];
   1827     if (input) {
   1828         [_debuggerInputQueue removeObjectAtIndex:0];
   1829     }
   1830     [_hasDebuggerInput unlockWithCondition:[_debuggerInputQueue count] != 0];
   1831     if ((id)input == [NSNull null]) {
   1832         return NULL;
   1833     }
   1834     return input? strdup([input UTF8String]): NULL;
   1835 }
   1836 
   1837 - (IBAction)saveState:(id)sender
   1838 {
   1839     bool __block success = false;
   1840     [self performAtomicBlock:^{
   1841         success = GB_save_state(&_gb, [self saveStatePath:[sender tag]].UTF8String) == 0;
   1842     }];
   1843     
   1844     if (!success) {
   1845         [GBWarningPopover popoverWithContents:@"Failed to write save state." onWindow:self.mainWindow];
   1846         NSBeep();
   1847     }
   1848     else {
   1849         [self.osdView displayText:@"State saved"];
   1850     }
   1851 }
   1852 
   1853 - (int)loadStateFile:(const char *)path noErrorOnNotFound:(bool)noErrorOnFileNotFound;
   1854 {
   1855     int __block result = false;
   1856     NSString *error =
   1857     [self captureOutputForBlock:^{
   1858         result = GB_load_state(&_gb, path);
   1859     }];
   1860     
   1861     if (result == ENOENT && noErrorOnFileNotFound) {
   1862         return ENOENT;
   1863     }
   1864     
   1865     if (result) {
   1866         NSBeep();
   1867     }
   1868     else {
   1869         [self.osdView displayText:@"State loaded"];
   1870     }
   1871     if (error) {
   1872         [GBWarningPopover popoverWithContents:error onWindow:self.mainWindow];
   1873     }
   1874     return result;
   1875 }
   1876 
   1877 - (IBAction)loadState:(id)sender
   1878 {
   1879     int ret = [self loadStateFile:[self saveStatePath:[sender tag]].UTF8String noErrorOnNotFound:true];
   1880     if (ret == ENOENT && !self.isCartContainer) {
   1881         [self loadStateFile:[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"sn%ld", (long)[sender tag]]].path.UTF8String noErrorOnNotFound:false];
   1882     }
   1883 }
   1884 
   1885 - (IBAction)clearConsole:(id)sender
   1886 {
   1887     [self.consoleOutput setString:@""];
   1888 }
   1889 
   1890 - (void)log:(const char *)log
   1891 {
   1892     [self log:log withAttributes:0];
   1893 }
   1894 
   1895 - (void)performAtomicBlock: (void (^)())block
   1896 {
   1897     while (!GB_is_inited(&_gb));
   1898     bool isRunning = _running && !GB_debugger_is_stopped(&_gb);
   1899     if (_master) {
   1900         isRunning |= _master->_running;
   1901     }
   1902     if (!isRunning) {
   1903         block();
   1904         return;
   1905     }
   1906     
   1907     if (_master) {
   1908         [_master performAtomicBlock:block];
   1909         return;
   1910     }
   1911     
   1912     if ([NSThread currentThread] == _emulationThread) {
   1913         block();
   1914         return;
   1915     }
   1916     
   1917     _pendingAtomicBlock = block;
   1918     while (_pendingAtomicBlock);
   1919 }
   1920 
   1921 - (NSString *)captureOutputForBlock: (void (^)())block
   1922 {
   1923     _capturedOutput = [[NSMutableString alloc] init];
   1924     [self performAtomicBlock:block];
   1925     NSString *ret = [_capturedOutput stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];
   1926     _capturedOutput = nil;
   1927     return [ret length]? ret : nil;
   1928 }
   1929 
   1930 + (NSImage *) imageFromData:(NSData *)data width:(NSUInteger) width height:(NSUInteger) height scale:(double) scale
   1931 {
   1932     
   1933     NSImage *ret = [[NSImage alloc] initWithSize:NSMakeSize(width * scale, height * scale)];
   1934     NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
   1935                                                                     pixelsWide:width
   1936                                                                     pixelsHigh:height
   1937                                                                  bitsPerSample:8
   1938                                                                samplesPerPixel:3
   1939                                                                       hasAlpha:false
   1940                                                                       isPlanar:false
   1941                                                                 colorSpaceName:NSDeviceRGBColorSpace
   1942                                                                   bitmapFormat:0
   1943                                                                    bytesPerRow:4 * width
   1944                                                                   bitsPerPixel:32];
   1945     memcpy(rep.bitmapData, data.bytes, data.length);
   1946     [ret addRepresentation:rep];
   1947     return ret;
   1948 }
   1949 
   1950 - (void) reloadMemoryView
   1951 {
   1952     if (self.memoryWindow.isVisible) {
   1953         [_hexController reloadData];
   1954     }
   1955     if (_cheatSearchController.window.isVisible) {
   1956         if ([_cheatSearchController.tableView editedColumn] != 2) {
   1957             [_cheatSearchController.tableView reloadData];
   1958         }
   1959     }
   1960 }
   1961 
   1962 - (IBAction) reloadVRAMData: (id) sender
   1963 {
   1964     if (self.vramWindow.isVisible) {
   1965         uint8_t *io_regs = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_IO, NULL, NULL);
   1966         switch ([self.vramTabView.tabViewItems indexOfObject:self.vramTabView.selectedTabViewItem]) {
   1967             case 0:
   1968             /* Tileset */
   1969             {
   1970                 GB_palette_type_t palette_type = GB_PALETTE_NONE;
   1971                 NSUInteger palette_menu_index = self.tilesetPaletteButton.indexOfSelectedItem;
   1972                 if (palette_menu_index) {
   1973                     palette_type = palette_menu_index > 8? GB_PALETTE_OAM : GB_PALETTE_BACKGROUND;
   1974                 }
   1975                 size_t bufferLength = 256 * 192 * 4;
   1976                 NSMutableData *data = [NSMutableData dataWithCapacity:bufferLength];
   1977                 data.length = bufferLength;
   1978                 GB_draw_tileset(&_gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 1) & 7);
   1979                 
   1980                 self.tilesetImageView.image = [Document imageFromData:data width:256 height:192 scale:1.0];
   1981                 self.tilesetImageView.layer.magnificationFilter = kCAFilterNearest;
   1982             }
   1983             break;
   1984                 
   1985             case 1:
   1986             /* Tilemap */
   1987             {
   1988                 GB_palette_type_t palette_type = GB_PALETTE_NONE;
   1989                 NSUInteger palette_menu_index = self.tilemapPaletteButton.indexOfSelectedItem;
   1990                 if (palette_menu_index > 1) {
   1991                     palette_type = palette_menu_index > 9? GB_PALETTE_OAM : GB_PALETTE_BACKGROUND;
   1992                 }
   1993                 else if (palette_menu_index == 1) {
   1994                     palette_type = GB_PALETTE_AUTO;
   1995                 }
   1996                 
   1997                 size_t bufferLength = 256 * 256 * 4;
   1998                 NSMutableData *data = [NSMutableData dataWithCapacity:bufferLength];
   1999                 data.length = bufferLength;
   2000                 GB_draw_tilemap(&_gb, (uint32_t *)data.mutableBytes, palette_type, (palette_menu_index - 2) & 7,
   2001                                 (GB_map_type_t) self.tilemapMapButton.indexOfSelectedItem,
   2002                                 (GB_tileset_type_t) self.TilemapSetButton.indexOfSelectedItem);
   2003                 
   2004                 self.tilemapImageView.scrollRect = NSMakeRect(io_regs[GB_IO_SCX],
   2005                                                               io_regs[GB_IO_SCY],
   2006                                                               160, 144);
   2007                 self.tilemapImageView.image = [Document imageFromData:data width:256 height:256 scale:1.0];
   2008                 self.tilemapImageView.layer.magnificationFilter = kCAFilterNearest;
   2009             }
   2010             break;
   2011                 
   2012             case 2:
   2013             /* OAM */
   2014             {
   2015                 _oamCount = GB_get_oam_info(&_gb, _oamInfo, &_oamHeight);
   2016                 dispatch_async(dispatch_get_main_queue(), ^{
   2017                     [self.objectView reloadData:self];
   2018                 });
   2019             }
   2020             break;
   2021             
   2022             case 3:
   2023             /* Palettes */
   2024             {
   2025                 dispatch_async(dispatch_get_main_queue(), ^{
   2026                     [self.paletteView reloadData:self];
   2027                 });
   2028             }
   2029             break;
   2030         }
   2031     }
   2032 }
   2033 
   2034 - (IBAction) showMemory:(id)sender
   2035 {
   2036     if (!_hexController) {
   2037         [self initMemoryView];
   2038     }
   2039     [self.memoryWindow makeKeyAndOrderFront:sender];
   2040 }
   2041 
   2042 - (IBAction)hexGoTo:(id)sender
   2043 {
   2044     NSString *expression = [sender stringValue];
   2045     __block uint16_t addr = 0;
   2046     __block uint16_t bank = 0;
   2047     __block bool fail = false;
   2048     NSString *error = [self captureOutputForBlock:^{
   2049         if (GB_debugger_evaluate(&_gb, [expression UTF8String], &addr, &bank)) {
   2050             fail = true;
   2051         }
   2052     }];
   2053     
   2054     if (error) {
   2055         NSBeep();
   2056         [GBWarningPopover popoverWithContents:error onView:sender];
   2057     }
   2058     if (fail) return;
   2059     
   2060         
   2061     if (bank != (typeof(bank))-1) {
   2062         GB_memory_mode_t mode = [(GBMemoryByteArray *)(_hexController.byteArray) mode];
   2063         if (addr < 0x4000) {
   2064             if (bank == 0) {
   2065                 if (mode != GBMemoryROM && mode != GBMemoryEntireSpace) {
   2066                     mode = GBMemoryEntireSpace;
   2067                 }
   2068             }
   2069             else {
   2070                 addr |= 0x4000;
   2071                 mode = GBMemoryROM;
   2072             }
   2073         }
   2074         else if (addr < 0x8000) {
   2075             mode = GBMemoryROM;
   2076         }
   2077         else if (addr < 0xA000) {
   2078             mode = GBMemoryVRAM;
   2079         }
   2080         else if (addr < 0xC000) {
   2081             mode = GBMemoryExternalRAM;
   2082         }
   2083         else if (addr < 0xD000) {
   2084             if (mode != GBMemoryRAM && mode != GBMemoryEntireSpace) {
   2085                 mode = GBMemoryEntireSpace;
   2086             }
   2087         }
   2088         else if (addr < 0xE000) {
   2089             mode = GBMemoryRAM;
   2090         }
   2091         else {
   2092             mode = GBMemoryEntireSpace;
   2093         }
   2094         [_memorySpaceButton selectItemAtIndex:mode];
   2095         [self hexUpdateSpace:_memorySpaceButton.cell];
   2096         [_memoryBankInput setStringValue:[NSString stringWithFormat:@"$%02x", bank]];
   2097         [self hexUpdateBank:_memoryBankInput];
   2098     }
   2099     addr -= _lineRep.valueOffset;
   2100     if (addr >= _hexController.byteArray.length) {
   2101         GB_log(&_gb, "Value $%04x is out of range.\n", addr);
   2102         return;
   2103     }
   2104     
   2105     [_hexController setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(addr, 0)]]];
   2106     [_hexController _ensureVisibilityOfLocation:addr];
   2107     for (HFRepresenter *representer in _hexController.representers) {
   2108         if ([representer isKindOfClass:[HFHexTextRepresenter class]]) {
   2109             [self.memoryWindow makeFirstResponder:representer.view];
   2110             break;
   2111         }
   2112     }
   2113 }
   2114 
   2115 - (void)hexUpdateBank:(NSControl *)sender ignoreErrors: (bool)ignore_errors
   2116 {
   2117     NSString *expression = [sender stringValue];
   2118     __block uint16_t addr, bank;
   2119     __block bool fail = false;
   2120     NSString *error = [self captureOutputForBlock:^{
   2121         if (GB_debugger_evaluate(&_gb, [expression UTF8String], &addr, &bank)) {
   2122             fail = true;
   2123             return;
   2124         }
   2125     }];
   2126     
   2127     if (error && !ignore_errors) {
   2128         NSBeep();
   2129         [GBWarningPopover popoverWithContents:error onView:sender];
   2130     }
   2131     
   2132     if (fail) return;
   2133 
   2134     if (bank == (uint16_t) -1) {
   2135         bank = addr;
   2136     }
   2137 
   2138     uint16_t n_banks = 1;
   2139     switch ([(GBMemoryByteArray *)(_hexController.byteArray) mode]) {
   2140         case GBMemoryROM: {
   2141             size_t rom_size;
   2142             GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, &rom_size, NULL);
   2143             n_banks = rom_size / 0x4000;
   2144             break;
   2145         }
   2146         case GBMemoryVRAM:
   2147             n_banks = GB_is_cgb(&_gb) ? 2 : 1;
   2148             break;
   2149         case GBMemoryExternalRAM: {
   2150             size_t ram_size;
   2151             GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_CART_RAM, &ram_size, NULL);
   2152             n_banks = (ram_size + 0x1FFF) / 0x2000;
   2153             break;
   2154         }
   2155         case GBMemoryRAM:
   2156             n_banks = GB_is_cgb(&_gb) ? 8 : 1;
   2157             break;
   2158         case GBMemoryEntireSpace:
   2159             break;
   2160     }
   2161 
   2162     bank %= n_banks;
   2163 
   2164     [(GBMemoryByteArray *)(_hexController.byteArray) setSelectedBank:bank];
   2165     _statusRep.bankForDescription = bank;
   2166     [sender setStringValue:[NSString stringWithFormat:@"$%x", bank]];
   2167     [_hexController reloadData];
   2168 }
   2169 
   2170 - (IBAction)hexUpdateBank:(NSControl *)sender
   2171 {
   2172     [self hexUpdateBank:sender ignoreErrors:false];
   2173 }
   2174 
   2175 - (IBAction)hexUpdateSpace:(NSPopUpButtonCell *)sender
   2176 {
   2177     self.memoryBankItem.enabled = [sender indexOfSelectedItem] != GBMemoryEntireSpace;
   2178     [_hexController setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(0, 0)]]];
   2179     GBMemoryByteArray *byteArray = (GBMemoryByteArray *)(_hexController.byteArray);
   2180     [byteArray setMode:(GB_memory_mode_t)[sender indexOfSelectedItem]];
   2181     uint16_t bank = -1;
   2182     switch ((GB_memory_mode_t)[sender indexOfSelectedItem]) {
   2183         case GBMemoryEntireSpace:
   2184             _statusRep.baseAddress = _lineRep.valueOffset = 0;
   2185             break;
   2186         case GBMemoryROM:
   2187             _statusRep.baseAddress = _lineRep.valueOffset = 0;
   2188             GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, &bank);
   2189             break;
   2190         case GBMemoryVRAM:
   2191             _statusRep.baseAddress = _lineRep.valueOffset = 0x8000;
   2192             GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_VRAM, NULL, &bank);
   2193             break;
   2194         case GBMemoryExternalRAM:
   2195             _statusRep.baseAddress = _lineRep.valueOffset = 0xA000;
   2196             GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_CART_RAM, NULL, &bank);
   2197             break;
   2198         case GBMemoryRAM:
   2199             _statusRep.baseAddress = _lineRep.valueOffset = 0xC000;
   2200             GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_RAM, NULL, &bank);
   2201             break;
   2202     }
   2203     byteArray.selectedBank = bank;
   2204     _statusRep.bankForDescription = bank;
   2205     [self.memoryBankInput setStringValue:(bank == (uint16_t)-1)? @"" :
   2206                                                                  [NSString stringWithFormat:@"$%x", byteArray.selectedBank]];
   2207     
   2208     [_hexController reloadData];
   2209     for (NSView *view in self.memoryView.subviews) {
   2210         [view setNeedsDisplay:true];
   2211     }
   2212 }
   2213 
   2214 - (GB_gameboy_t *) gameboy
   2215 {
   2216     return &_gb;
   2217 }
   2218 
   2219 + (BOOL)canConcurrentlyReadDocumentsOfType:(NSString *)typeName
   2220 {
   2221     return true;
   2222 }
   2223 
   2224 - (void)cameraRequestUpdate
   2225 {
   2226     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
   2227         @try {
   2228             if (!_cameraSession) {
   2229                 if (@available(macOS 10.14, *)) {
   2230                     switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) {
   2231                         case AVAuthorizationStatusAuthorized:
   2232                             break;
   2233                         case AVAuthorizationStatusNotDetermined: {
   2234                             [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
   2235                                 [self cameraRequestUpdate];
   2236                             }];
   2237                             return;
   2238                         }
   2239                         case AVAuthorizationStatusDenied:
   2240                         case AVAuthorizationStatusRestricted:
   2241                             GB_camera_updated(&_gb);
   2242                             return;
   2243                     }
   2244                 }
   2245 
   2246                 NSError *error;
   2247                 AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
   2248                 AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice: device error: &error];
   2249                 CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions([[device activeFormat] formatDescription]);
   2250 
   2251                 if (!input) {
   2252                     GB_camera_updated(&_gb);
   2253                     return;
   2254                 }
   2255                 
   2256                 double ratio = MAX(130.0 / dimensions.width, 114.0 / dimensions.height);
   2257 
   2258                 _cameraOutput = [[AVCaptureStillImageOutput alloc] init];
   2259                 /* Greyscale is not widely supported, so we use YUV, whose first element is the brightness. */
   2260                 [_cameraOutput setOutputSettings: @{(id)kCVPixelBufferPixelFormatTypeKey: @(kYUVSPixelFormat),
   2261                                                    (id)kCVPixelBufferWidthKey: @(round(dimensions.width * ratio)),
   2262                                                    (id)kCVPixelBufferHeightKey: @(round(dimensions.height * ratio)),}];
   2263 
   2264 
   2265                 _cameraSession = [AVCaptureSession new];
   2266                 _cameraSession.sessionPreset = AVCaptureSessionPresetPhoto;
   2267 
   2268                 [_cameraSession addInput: input];
   2269                 [_cameraSession addOutput: _cameraOutput];
   2270                 [_cameraSession startRunning];
   2271                 _cameraConnection = [_cameraOutput connectionWithMediaType: AVMediaTypeVideo];
   2272             }
   2273 
   2274             [_cameraOutput captureStillImageAsynchronouslyFromConnection: _cameraConnection completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError *error) {
   2275                 if (error) {
   2276                     GB_camera_updated(&_gb);
   2277                 }
   2278                 else {
   2279                     if (_cameraImage) {
   2280                         CVBufferRelease(_cameraImage);
   2281                         _cameraImage = NULL;
   2282                     }
   2283                     _cameraImage = CVBufferRetain(CMSampleBufferGetImageBuffer(sampleBuffer));
   2284                     /* We only need the actual buffer, no need to ever unlock it. */
   2285                     CVPixelBufferLockBaseAddress(_cameraImage, 0);
   2286                 }
   2287                 
   2288                 GB_camera_updated(&_gb);
   2289             }];
   2290         }
   2291         @catch (NSException *exception) {
   2292             /* I have not tested camera support on many devices, so we catch exceptions just in case. */
   2293             GB_camera_updated(&_gb);
   2294         }
   2295     });
   2296 }
   2297 
   2298 - (uint8_t)cameraGetPixelAtX:(unsigned)x andY:(unsigned)y
   2299 {
   2300     if (!_cameraImage) {
   2301         return 0;
   2302     }
   2303 
   2304     uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(_cameraImage);
   2305     size_t bytesPerRow = CVPixelBufferGetBytesPerRow(_cameraImage);
   2306     unsigned offsetX = (CVPixelBufferGetWidth(_cameraImage) - 128) / 2;
   2307     unsigned offsetY = (CVPixelBufferGetHeight(_cameraImage) - 112) / 2;
   2308     uint8_t ret = baseAddress[(x + offsetX) * 2 + (y + offsetY) * bytesPerRow];
   2309 
   2310     return ret;
   2311 }
   2312 
   2313 - (IBAction)toggleTilesetGrid:(NSButton *)sender
   2314 {
   2315     if (sender.state) {
   2316         self.tilesetImageView.horizontalGrids = @[
   2317                                                   [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8],
   2318                                                   [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.5] size:128],
   2319                                                   
   2320         ];
   2321         self.tilesetImageView.verticalGrids = @[
   2322                                                   [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8],
   2323                                                   [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.5] size:64],
   2324         ];
   2325         self.tilemapImageView.horizontalGrids = @[
   2326                                                   [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8],
   2327                                                   ];
   2328         self.tilemapImageView.verticalGrids = @[
   2329                                                 [[GBImageViewGridConfiguration alloc] initWithColor:[NSColor colorWithCalibratedRed:0 green:0 blue:0 alpha:0.25] size:8],
   2330                                                 ];
   2331     }
   2332     else {
   2333         self.tilesetImageView.horizontalGrids = nil;
   2334         self.tilesetImageView.verticalGrids = nil;
   2335         self.tilemapImageView.horizontalGrids = nil;
   2336         self.tilemapImageView.verticalGrids = nil;
   2337     }
   2338 }
   2339 
   2340 - (IBAction)toggleScrollingDisplay:(NSButton *)sender
   2341 {
   2342     self.tilemapImageView.displayScrollRect = sender.state;
   2343 }
   2344 
   2345 - (IBAction)vramTabChanged:(NSSegmentedControl *)sender
   2346 {
   2347     [self.vramTabView selectTabViewItemAtIndex:[sender selectedSegment]];
   2348     [self reloadVRAMData:sender];
   2349     [self.vramTabView.selectedTabViewItem.view addSubview:self.gridButton];
   2350     self.gridButton.hidden = [sender selectedSegment] >= 2;
   2351 
   2352     NSUInteger height_diff = self.vramWindow.frame.size.height - self.vramWindow.contentView.frame.size.height;
   2353     CGRect window_rect = self.vramWindow.frame;
   2354     window_rect.origin.y += window_rect.size.height;
   2355     switch ([sender selectedSegment]) {
   2356         case 0:
   2357         case 2:
   2358             window_rect.size.height = 384 + height_diff + 48;
   2359             break;
   2360         case 1:
   2361             window_rect.size.height = 512 + height_diff + 48;
   2362             break;
   2363         case 3:
   2364             window_rect.size.height = 24 * 16 + height_diff;
   2365             break;
   2366             
   2367         default:
   2368             break;
   2369     }
   2370     window_rect.origin.y -= window_rect.size.height;
   2371     [self.vramWindow setFrame:window_rect display:true animate:true];
   2372 }
   2373 
   2374 - (void)mouseDidLeaveImageView:(GBImageView *)view
   2375 {
   2376     self.vramStatusLabel.stringValue = @"";
   2377 }
   2378 
   2379 - (void)imageView:(GBImageView *)view mouseMovedToX:(NSUInteger)x Y:(NSUInteger)y
   2380 {
   2381     if (view == self.tilesetImageView) {
   2382         uint8_t bank = x >= 128? 1 : 0;
   2383         x &= 127;
   2384         uint16_t tile = x / 8 + y / 8 * 16;
   2385         self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x at %d:$%04x", tile & 0xFF, bank, 0x8000 + tile * 0x10];
   2386     }
   2387     else if (view == self.tilemapImageView) {
   2388         uint16_t map_offset = x / 8 + y / 8 * 32;
   2389         uint16_t map_base = 0x1800;
   2390         GB_map_type_t map_type = (GB_map_type_t) self.tilemapMapButton.indexOfSelectedItem;
   2391         GB_tileset_type_t tileset_type = (GB_tileset_type_t) self.TilemapSetButton.indexOfSelectedItem;
   2392         uint8_t lcdc = ((uint8_t *)GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_IO, NULL, NULL))[GB_IO_LCDC];
   2393         uint8_t *vram = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_VRAM, NULL, NULL);
   2394         
   2395         if (map_type == GB_MAP_9C00 || (map_type == GB_MAP_AUTO && lcdc & GB_LCDC_BG_MAP)) {
   2396             map_base = 0x1C00;
   2397         }
   2398         
   2399         if (tileset_type == GB_TILESET_AUTO) {
   2400             tileset_type = (lcdc & GB_LCDC_TILE_SEL)? GB_TILESET_8800 : GB_TILESET_8000;
   2401         }
   2402         
   2403         uint8_t tile = vram[map_base + map_offset];
   2404         uint16_t tile_address = 0;
   2405         if (tileset_type == GB_TILESET_8000) {
   2406             tile_address = 0x8000 + tile * 0x10;
   2407         }
   2408         else {
   2409             tile_address = 0x9000 + (int8_t)tile * 0x10;
   2410         }
   2411         
   2412         if (GB_is_cgb(&_gb)) {
   2413             uint8_t attributes = vram[map_base + map_offset + 0x2000];
   2414             self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x (%d:$%04x) at map address $%04x (Attributes: %c%c%c%d%d)",
   2415                                                 tile,
   2416                                                 attributes & 0x8? 1 : 0,
   2417                                                 tile_address,
   2418                                                 0x8000 + map_base + map_offset,
   2419                                                 (attributes & 0x80) ? 'P' : '-',
   2420                                                 (attributes & 0x40) ? 'V' : '-',
   2421                                                 (attributes & 0x20) ? 'H' : '-',
   2422                                                 attributes & 0x8? 1 : 0,
   2423                                                 attributes & 0x7
   2424                                                 ];
   2425         }
   2426         else {
   2427             self.vramStatusLabel.stringValue = [NSString stringWithFormat:@"Tile number $%02x ($%04x) at map address $%04x",
   2428                                                 tile,
   2429                                                 tile_address,
   2430                                                 0x8000 + map_base + map_offset
   2431                                                 ];
   2432         }
   2433 
   2434     }
   2435 }
   2436 
   2437 - (GB_oam_info_t *)oamInfo
   2438 {
   2439     return _oamInfo;
   2440 }
   2441 
   2442 - (IBAction)showVRAMViewer:(id)sender
   2443 {
   2444     [self.vramWindow makeKeyAndOrderFront:sender];
   2445     [self reloadVRAMData: nil];
   2446 }
   2447 
   2448 - (void)printImage:(uint32_t *)imageBytes height:(unsigned) height
   2449          topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin
   2450           exposure:(unsigned) exposure
   2451 {
   2452     uint32_t paddedImage[160 * (topMargin + height + bottomMargin)];
   2453     memset(paddedImage, 0xFF, sizeof(paddedImage));
   2454     memcpy(paddedImage + (160 * topMargin), imageBytes, 160 * height * sizeof(imageBytes[0]));
   2455     if (!self.printerFeedWindow.isVisible) {
   2456         _currentPrinterImageData = [[NSMutableData alloc] init];
   2457     }
   2458     [_currentPrinterImageData appendBytes:paddedImage length:sizeof(paddedImage)];
   2459     /* UI related code must run on main thread. */
   2460     dispatch_async(dispatch_get_main_queue(), ^{
   2461         [_printerSpinner startAnimation:nil];
   2462         self.feedImageView.image = [Document imageFromData:_currentPrinterImageData
   2463                                                      width:160
   2464                                                     height:_currentPrinterImageData.length / 160 / sizeof(imageBytes[0])
   2465                                                      scale:2.0];
   2466         NSRect frame = self.printerFeedWindow.frame;
   2467         double oldHeight = frame.size.height;
   2468         frame.size = self.feedImageView.image.size;
   2469         [self.printerFeedWindow setContentMaxSize:frame.size];
   2470         frame.size.height += self.printerFeedWindow.frame.size.height - self.printerFeedWindow.contentView.frame.size.height;
   2471         frame.origin.y -= frame.size.height - oldHeight;
   2472         [self.printerFeedWindow setFrame:frame display:false animate: self.printerFeedWindow.isVisible];
   2473         [self.printerFeedWindow orderFront:NULL];
   2474     });
   2475     
   2476 }
   2477 
   2478 - (void)printDone
   2479 {
   2480     dispatch_async(dispatch_get_main_queue(), ^{
   2481         [_printerSpinner stopAnimation:nil];
   2482     });
   2483 }
   2484 
   2485 - (void)printDocument:(id)sender
   2486 {
   2487     if (self.feedImageView.image.size.height == 0) {
   2488         NSBeep(); return;
   2489     }
   2490     NSImageView *view = [[NSImageView alloc] initWithFrame:(NSRect){{0,0}, self.feedImageView.image.size}];
   2491     view.image = self.feedImageView.image;
   2492     [[NSPrintOperation printOperationWithView:view] runOperationModalForWindow:self.printerFeedWindow delegate:nil didRunSelector:NULL contextInfo:NULL];
   2493 }
   2494 
   2495 - (IBAction)savePrinterFeed:(id)sender
   2496 {
   2497     bool shouldResume = _running;
   2498     [self stop];
   2499     NSSavePanel *savePanel = [NSSavePanel savePanel];
   2500     [savePanel setAllowedFileTypes:@[@"png"]];
   2501     [savePanel beginSheetModalForWindow:self.printerFeedWindow completionHandler:^(NSInteger result) {
   2502         if (result == NSModalResponseOK) {
   2503             [savePanel orderOut:self];
   2504             CGImageRef cgRef = [self.feedImageView.image CGImageForProposedRect:NULL
   2505                                                                         context:nil
   2506                                                                           hints:nil];
   2507             NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef];
   2508             [imageRep setSize:(NSSize){160, self.feedImageView.image.size.height / 2}];
   2509             NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
   2510             [data writeToURL:savePanel.URL atomically:false];
   2511             [self.printerFeedWindow setIsVisible:false];
   2512         }
   2513         if (shouldResume) {
   2514             [self start];
   2515         }
   2516     }];
   2517 }
   2518 
   2519 - (IBAction)disconnectAllAccessories:(id)sender
   2520 {
   2521     [self disconnectLinkCable];
   2522     [self performAtomicBlock:^{
   2523         GB_disconnect_serial(&_gb);
   2524     }];
   2525 }
   2526 
   2527 - (IBAction)connectPrinter:(id)sender
   2528 {
   2529     [self disconnectLinkCable];
   2530     [self performAtomicBlock:^{
   2531         GB_connect_printer(&_gb, printImage, printDone);
   2532     }];
   2533 }
   2534 
   2535 - (IBAction)connectWorkboy:(id)sender
   2536 {
   2537     [self disconnectLinkCable];
   2538     [self performAtomicBlock:^{
   2539         GB_connect_workboy(&_gb, setWorkboyTime, getWorkboyTime);
   2540     }];
   2541 }
   2542 
   2543 - (void)setFileURL:(NSURL *)fileURL
   2544 {
   2545     [super setFileURL:fileURL];
   2546     if (@available(macOS 11.0, *)) {
   2547         self.consoleWindow.subtitle = [self.fileURL.path lastPathComponent];
   2548         self.memoryWindow.subtitle = [self.fileURL.path lastPathComponent];
   2549         self.vramWindow.subtitle = [self.fileURL.path lastPathComponent];
   2550     }
   2551     else {
   2552         self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [self.fileURL.path lastPathComponent]];
   2553         self.memoryWindow.title = [NSString stringWithFormat:@"Memory – %@", [self.fileURL.path lastPathComponent]];
   2554         self.vramWindow.title = [NSString stringWithFormat:@"VRAM Viewer – %@", [self.fileURL.path lastPathComponent]];
   2555     }
   2556 }
   2557 
   2558 - (BOOL)splitView:(GBSplitView *)splitView canCollapseSubview:(NSView *)subview;
   2559 {
   2560     if ([[splitView arrangedSubviews] lastObject] == subview) {
   2561         return true;
   2562     }
   2563     return false;
   2564 }
   2565 
   2566 - (CGFloat)splitView:(GBSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex
   2567 {
   2568     return 600;
   2569 }
   2570 
   2571 - (CGFloat)splitView:(GBSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex 
   2572 {
   2573     return splitView.frame.size.width - 321;
   2574 }
   2575 
   2576 - (BOOL)splitView:(GBSplitView *)splitView shouldAdjustSizeOfSubview:(NSView *)view 
   2577 {
   2578     if ([[splitView arrangedSubviews] lastObject] == view) {
   2579         return false;
   2580     }
   2581     return true;
   2582 }
   2583 
   2584 - (void)splitViewDidResizeSubviews:(NSNotification *)notification
   2585 {
   2586     GBSplitView *splitview = notification.object;
   2587     if ([[[splitview arrangedSubviews] firstObject] frame].size.width < 600) {
   2588         [splitview setPosition:600 ofDividerAtIndex:0];
   2589     }
   2590 }
   2591 
   2592 - (IBAction)showCheats:(id)sender
   2593 {
   2594     [self.cheatsWindow makeKeyAndOrderFront:nil];
   2595 }
   2596 
   2597 - (IBAction)showCheatSearch:(id)sender
   2598 {
   2599     if (!_cheatSearchController) {
   2600         _cheatSearchController = [GBCheatSearchController controllerWithDocument:self];
   2601     }
   2602     [_cheatSearchController.window makeKeyAndOrderFront:sender];
   2603 }
   2604 
   2605 - (IBAction)toggleCheats:(id)sender
   2606 {
   2607     GB_set_cheats_enabled(&_gb, !GB_cheats_enabled(&_gb));
   2608 }
   2609 
   2610 - (void)disconnectLinkCable
   2611 {
   2612     bool wasRunning = self->_running;
   2613     Document *partner = _master ?: _slave;
   2614     if (partner) {
   2615         wasRunning |= partner->_running;
   2616         [self stop];
   2617         partner->_master = nil;
   2618         partner->_slave = nil;
   2619         _master = nil;
   2620         _slave = nil;
   2621         if (wasRunning) {
   2622             [partner start];
   2623             [self start];
   2624         }
   2625         GB_set_turbo_mode(&_gb, false, false);
   2626         GB_set_turbo_mode(&partner->_gb, false, false);
   2627         GB_set_turbo_cap(&_gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]);
   2628         GB_set_turbo_cap(&partner->_gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboCap"]);
   2629     }
   2630 }
   2631 
   2632 - (void)connectLinkCable:(NSMenuItem *)sender
   2633 {
   2634     [self disconnectAllAccessories:sender];
   2635     Document *partner = [sender representedObject];
   2636     [partner disconnectAllAccessories:sender];
   2637     
   2638     bool wasRunning = self->_running;
   2639     [self stop];
   2640     [partner stop];
   2641     GB_set_turbo_mode(&partner->_gb, true, true);
   2642     _slave = partner;
   2643     partner->_master = self;
   2644     GB_set_turbo_cap(&partner->_gb, 0);
   2645     _linkOffset = 0;
   2646     GB_set_serial_transfer_bit_start_callback(&_gb, _linkCableBitStart);
   2647     GB_set_serial_transfer_bit_start_callback(&partner->_gb, _linkCableBitStart);
   2648     GB_set_serial_transfer_bit_end_callback(&_gb, _linkCableBitEnd);
   2649     GB_set_serial_transfer_bit_end_callback(&partner->_gb, _linkCableBitEnd);
   2650     if (wasRunning) {
   2651         [self start];
   2652     }
   2653 }
   2654 
   2655 - (void)_linkCableBitStart:(bool)bit
   2656 {
   2657     _linkCableBit = bit;
   2658 }
   2659 
   2660 -(bool)_linkCableBitEnd
   2661 {
   2662     bool ret = GB_serial_get_data_bit(&self.partner->_gb);
   2663     GB_serial_set_data_bit(&self.partner->_gb, _linkCableBit);
   2664     return ret;
   2665 }
   2666 
   2667 - (void)infraredStateChanged:(bool)state
   2668 {
   2669     if (self.partner) {
   2670         GB_set_infrared_input(&self.partner->_gb, state);
   2671     }
   2672 }
   2673 
   2674 -(Document *)partner
   2675 {
   2676     return _slave ?: _master;
   2677 }
   2678 
   2679 - (bool)isSlave
   2680 {
   2681     return _master;
   2682 }
   2683 
   2684 - (GB_gameboy_t *)gb
   2685 {
   2686     return &_gb;
   2687 }
   2688 
   2689 - (NSImage *)takeScreenshot
   2690 {
   2691     NSImage *ret = nil;
   2692     if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBFilterScreenshots"]) {
   2693         ret = [_view renderToImage];
   2694     }
   2695     if (!ret) {
   2696         ret = [Document imageFromData:[NSData dataWithBytesNoCopy:_view.currentBuffer
   2697                                                            length:GB_get_screen_width(&_gb) * GB_get_screen_height(&_gb) * 4
   2698                                                      freeWhenDone:false]
   2699                                 width:GB_get_screen_width(&_gb)
   2700                                height:GB_get_screen_height(&_gb)
   2701                                 scale:1.0];
   2702     }
   2703     return ret;
   2704 }
   2705 
   2706 - (NSString *)screenshotFilename
   2707 {
   2708     NSDate *date = [NSDate date];
   2709     NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
   2710     dateFormatter.dateStyle = NSDateFormatterLongStyle;
   2711     dateFormatter.timeStyle = NSDateFormatterMediumStyle;
   2712     return [[NSString stringWithFormat:@"%@ – %@.png",
   2713              self.fileURL.lastPathComponent.stringByDeletingPathExtension,
   2714              [dateFormatter stringFromDate:date]] stringByReplacingOccurrencesOfString:@":" withString:@"."]; // Gotta love Mac OS Classic
   2715 
   2716 }
   2717 
   2718 - (IBAction)saveScreenshot:(id)sender
   2719 {
   2720     NSString *folder = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBScreenshotFolder"];
   2721     BOOL isDirectory = false;
   2722     if (folder) {
   2723         [[NSFileManager defaultManager] fileExistsAtPath:folder isDirectory:&isDirectory];
   2724     }
   2725     if (!folder) {
   2726         bool shouldResume = _running;
   2727         [self stop];
   2728         NSOpenPanel *openPanel = [NSOpenPanel openPanel];
   2729         openPanel.canChooseFiles = false;
   2730         openPanel.canChooseDirectories = true;
   2731         openPanel.message = @"Choose a folder for screenshots";
   2732         [openPanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
   2733             if (result == NSModalResponseOK) {
   2734                 [[NSUserDefaults standardUserDefaults] setObject:openPanel.URL.path
   2735                                                           forKey:@"GBScreenshotFolder"];
   2736                 [self saveScreenshot:sender];
   2737             }
   2738             if (shouldResume) {
   2739                 [self start];
   2740             }
   2741             
   2742         }];
   2743         return;
   2744     }
   2745     NSImage *image = [self takeScreenshot];
   2746     NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
   2747     dateFormatter.dateStyle = NSDateFormatterLongStyle;
   2748     dateFormatter.timeStyle = NSDateFormatterMediumStyle;
   2749     NSString *filename = [self screenshotFilename];
   2750     filename = [folder stringByAppendingPathComponent:filename];
   2751     unsigned i = 2;
   2752     while ([[NSFileManager defaultManager] fileExistsAtPath:filename]) {
   2753         filename = [[filename stringByDeletingPathExtension] stringByAppendingFormat:@" %d.png", i++];
   2754     }
   2755     
   2756     NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject;
   2757     NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
   2758     [data writeToFile:filename atomically:false];
   2759     [self.osdView displayText:@"Screenshot saved"];
   2760 }
   2761 
   2762 - (IBAction)saveScreenshotAs:(id)sender
   2763 {
   2764     bool shouldResume = _running;
   2765     [self stop];
   2766     NSImage *image = [self takeScreenshot];
   2767     NSSavePanel *savePanel = [NSSavePanel savePanel];
   2768     [savePanel setNameFieldStringValue:[self screenshotFilename]];
   2769     [savePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
   2770         if (result == NSModalResponseOK) {
   2771             [savePanel orderOut:self];
   2772             NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject;
   2773             NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
   2774             [data writeToURL:savePanel.URL atomically:false];
   2775             [[NSUserDefaults standardUserDefaults] setObject:savePanel.URL.path.stringByDeletingLastPathComponent
   2776                                                       forKey:@"GBScreenshotFolder"];
   2777         }
   2778         if (shouldResume) {
   2779             [self start];
   2780         }
   2781     }];
   2782     [self.osdView displayText:@"Screenshot saved"];
   2783 }
   2784 
   2785 - (IBAction)copyScreenshot:(id)sender
   2786 {
   2787     NSImage *image = [self takeScreenshot];
   2788     [[NSPasteboard generalPasteboard] clearContents];
   2789     [[NSPasteboard generalPasteboard] writeObjects:@[image]];
   2790     [self.osdView displayText:@"Screenshot copied"];
   2791 }
   2792 
   2793 - (IBAction)toggleDisplayBackground:(id)sender
   2794 {
   2795     GB_set_background_rendering_disabled(&_gb, !GB_is_background_rendering_disabled(&_gb));
   2796 }
   2797 
   2798 - (IBAction)toggleDisplayObjects:(id)sender
   2799 {
   2800     GB_set_object_rendering_disabled(&_gb, !GB_is_object_rendering_disabled(&_gb));
   2801 }
   2802 
   2803 - (IBAction)newCartridgeInstance:(id)sender
   2804 {
   2805     bool shouldResume = _running;
   2806     [self stop];
   2807     NSSavePanel *savePanel = [NSSavePanel savePanel];
   2808     [savePanel setAllowedFileTypes:@[@"gbcart"]];
   2809     [savePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
   2810         if (result == NSModalResponseOK) {
   2811             [savePanel orderOut:self];
   2812             NSString *romPath = self.romPath;
   2813             [[NSFileManager defaultManager] trashItemAtURL:savePanel.URL resultingItemURL:nil error:nil];
   2814             [[NSFileManager defaultManager] createDirectoryAtURL:savePanel.URL withIntermediateDirectories:false attributes:nil error:nil];
   2815             [[NSString stringWithFormat:@"%@\n%@\n%@",
   2816               [romPath pathRelativeToDirectory:savePanel.URL.path],
   2817               romPath,
   2818               [[NSURL fileURLWithPath:romPath].fileReferenceURL.absoluteString substringFromIndex:strlen("file://")]
   2819             ] writeToURL:[savePanel.URL URLByAppendingPathComponent:@"rom.gbl"] atomically:false encoding:NSUTF8StringEncoding error:nil];
   2820             [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfURL:savePanel.URL display:true completionHandler:nil];
   2821         }
   2822         if (shouldResume) {
   2823             [self start];
   2824         }
   2825     }];
   2826 }
   2827 
   2828 - (IBAction)toggleAudioRecording:(id)sender
   2829 {
   2830 
   2831     bool shouldResume = _running;
   2832     [self stop];
   2833     if (_isRecordingAudio) {
   2834         _isRecordingAudio = false;
   2835         int error = GB_stop_audio_recording(&_gb);
   2836         if (error) {
   2837             NSAlert *alert = [[NSAlert alloc] init];
   2838             [alert setMessageText:[NSString stringWithFormat:@"Could not finalize recording: %s", strerror(error)]];
   2839             [alert setAlertStyle:NSAlertStyleCritical];
   2840             [alert runModal];
   2841         }
   2842         else {
   2843             [self.osdView displayText:@"Audio recording ended"];
   2844         }
   2845         if (shouldResume) {
   2846             [self start];
   2847         }
   2848         return;
   2849     }
   2850     _audioSavePanel = [NSSavePanel savePanel];
   2851     if (!self.audioRecordingAccessoryView) {
   2852         [[NSBundle mainBundle] loadNibNamed:@"AudioRecordingAccessoryView" owner:self topLevelObjects:nil];
   2853     }
   2854     _audioSavePanel.accessoryView = self.audioRecordingAccessoryView;
   2855     [self audioFormatChanged:self.audioFormatButton];
   2856     
   2857     [_audioSavePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
   2858         if (result == NSModalResponseOK) {
   2859             [_audioSavePanel orderOut:self];
   2860             int error = GB_start_audio_recording(&_gb, _audioSavePanel.URL.fileSystemRepresentation, self.audioFormatButton.selectedTag);
   2861             if (error) {
   2862                 NSAlert *alert = [[NSAlert alloc] init];
   2863                 [alert setMessageText:[NSString stringWithFormat:@"Could not start recording: %s", strerror(error)]];
   2864                 [alert setAlertStyle:NSAlertStyleCritical];
   2865                 [alert runModal];
   2866             }
   2867             else {
   2868                 [self.osdView displayText:@"Audio recording started"];
   2869                 _isRecordingAudio = true;
   2870             }
   2871         }
   2872         if (shouldResume) {
   2873             [self start];
   2874         }
   2875         _audioSavePanel = nil;
   2876     }];
   2877 }
   2878 
   2879 - (IBAction)audioFormatChanged:(NSPopUpButton *)sender
   2880 {
   2881     switch ((GB_audio_format_t)sender.selectedTag) {
   2882         case GB_AUDIO_FORMAT_RAW:
   2883             _audioSavePanel.allowedFileTypes = @[@"raw", @"pcm"];
   2884             break;
   2885         case GB_AUDIO_FORMAT_AIFF:
   2886             _audioSavePanel.allowedFileTypes = @[@"aiff", @"aif", @"aifc"];
   2887             break;
   2888         case GB_AUDIO_FORMAT_WAV:
   2889             _audioSavePanel.allowedFileTypes = @[@"wav"];
   2890             break;
   2891     }
   2892 }
   2893 
   2894 - (IBAction)toggleAudioChannel:(NSMenuItem *)sender
   2895 {
   2896     GB_set_channel_muted(&_gb, sender.tag, !GB_is_channel_muted(&_gb, sender.tag));
   2897 }
   2898 
   2899 - (IBAction)cartSwap:(id)sender
   2900 {
   2901     bool wasRunning = _running;
   2902     if (wasRunning) {
   2903         [self stop];
   2904     }
   2905     [[NSDocumentController sharedDocumentController] beginOpenPanelWithCompletionHandler:^(NSArray<NSURL *> *urls) {
   2906         if (urls.count == 1) {
   2907             bool ok = true;
   2908             for (Document *document in [NSDocumentController sharedDocumentController].documents) {
   2909                 if (document == self) continue;
   2910                 if ([document.fileURL isEqual:urls.firstObject]) {
   2911                     NSAlert *alert = [[NSAlert alloc] init];
   2912                     [alert setMessageText:[NSString stringWithFormat:@"‘%@’ is already open in another window. Close ‘%@’ before hot swapping it into this instance.",
   2913                                                                      urls.firstObject.lastPathComponent, urls.firstObject.lastPathComponent]];
   2914                     [alert setAlertStyle:NSAlertStyleCritical];
   2915                     [alert runModal];
   2916                     ok = false;
   2917                     break;
   2918                 }
   2919             }
   2920             if (ok) {
   2921                 GB_save_battery(&_gb, self.savPath.UTF8String);
   2922                 self.fileURL = urls.firstObject;
   2923                 [self loadROM];
   2924             }
   2925         }
   2926         if (wasRunning) {
   2927             [self start];
   2928         }
   2929     }];
   2930 }
   2931 
   2932 - (IBAction)reloadROM:(id)sender
   2933 {
   2934     bool wasRunning = _running;
   2935     if (wasRunning) {
   2936         [self stop];
   2937     }
   2938     
   2939     _romModified = false;
   2940     [self updateChangeCount:NSChangeCleared];
   2941     [self loadROM];
   2942 
   2943     if (wasRunning) {
   2944         [self start];
   2945     }
   2946 }
   2947 
   2948 - (void)updateDebuggerButtons
   2949 {
   2950     bool updateContinue = false;
   2951     if (@available(macOS 10.10, *)) {
   2952         if ([self.consoleInput.placeholderAttributedString.string isEqualToString:self.debuggerContinueButton.alternateTitle]) {
   2953             [self.debuggerContinueButton mouseExited:nil];
   2954             updateContinue = true;
   2955         }
   2956     }
   2957     if (self.isPaused) {
   2958         self.debuggerContinueButton.toolTip = self.debuggerContinueButton.title = @"Continue";
   2959         self.debuggerContinueButton.alternateTitle = @"continue";
   2960         self.debuggerContinueButton.imagePosition = NSImageOnly;
   2961         if (@available(macOS 10.14, *)) {
   2962             self.debuggerContinueButton.contentTintColor = nil;
   2963         }
   2964         self.debuggerContinueButton.image = [NSImage imageNamed:@"ContinueTemplate"];
   2965         
   2966         self.debuggerNextButton.enabled = true;
   2967         self.debuggerStepButton.enabled = true;
   2968         self.debuggerFinishButton.enabled = true;
   2969         self.debuggerBackstepButton.enabled = true;
   2970     }
   2971     else {
   2972         self.debuggerContinueButton.toolTip = self.debuggerContinueButton.title = @"Interrupt";
   2973         self.debuggerContinueButton.alternateTitle = @"interrupt";
   2974         self.debuggerContinueButton.imagePosition = NSImageOnly;
   2975         if (@available(macOS 10.14, *)) {
   2976             self.debuggerContinueButton.contentTintColor = [NSColor controlAccentColor];
   2977         }
   2978         self.debuggerContinueButton.image = [NSImage imageNamed:@"InterruptTemplate"];
   2979         
   2980         self.debuggerNextButton.enabled = false;
   2981         self.debuggerStepButton.enabled = false;
   2982         self.debuggerFinishButton.enabled = false;
   2983         self.debuggerBackstepButton.enabled = false;
   2984     }
   2985     if (updateContinue) {
   2986         [self.debuggerContinueButton mouseEntered:nil];
   2987     }
   2988 }
   2989 
   2990 - (IBAction)debuggerButtonPressed:(NSButton *)sender
   2991 {
   2992     [self queueDebuggerCommand:sender.alternateTitle];
   2993 }
   2994 
   2995 - (void)setROMModified
   2996 {
   2997     _romModified = true;
   2998     [self updateChangeCount:NSChangeDone];
   2999 }
   3000 
   3001 - (BOOL)writeToFile:(NSString *)path ofType:(NSString *)type
   3002 {
   3003     if ([type isEqualToString:@"Game Boy Cartridge"]) {
   3004         if (![[NSFileManager defaultManager] copyItemAtPath:self.fileName toPath:path error:nil]) return false;
   3005         path = self.romPath;
   3006         if (!path) return false;
   3007     }
   3008     size_t size;
   3009     uint8_t *data = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, &size, NULL);
   3010     return [[NSData dataWithBytesNoCopy:data length:size freeWhenDone:false] writeToFile:path atomically:true];
   3011 }
   3012 
   3013 + (NSArray<NSString *> *)readableTypes
   3014 {
   3015     NSMutableSet *set = [NSMutableSet setWithArray:[super readableTypes]];
   3016     for (NSString *type in @[@"gb", @"gbc", @"isx", @"gbs"]) {
   3017         [set addObject:(__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension,
   3018                                                                                            (__bridge CFStringRef)type,
   3019                                                                                            NULL)];
   3020     }
   3021     return [set allObjects];
   3022 }
   3023 
   3024 @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.