#include #include "GBAudioClient.h" #include "Document.h" #include "AppDelegate.h" #include "gb.h" @interface Document () { /* NSTextViews freeze the entire app if they're modified too often and too quickly. We use this bool to tune down the write speed. Let me know if there's a more reasonable alternative to this. */ unsigned long pendingLogLines; bool tooMuchLogs; bool fullScreen; NSString *lastConsoleInput; } @property GBAudioClient *audioClient; - (void) vblank; - (void) log: (const char *) log withAttributes: (GB_log_attributes) attributes; - (const char *) getDebuggerInput; @end static void vblank(GB_gameboy_t *gb) { Document *self = (__bridge Document *)(gb->user_data); [self vblank]; } static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) { Document *self = (__bridge Document *)(gb->user_data); [self log:string withAttributes: attributes]; } static char *consoleInput(GB_gameboy_t *gb) { Document *self = (__bridge Document *)(gb->user_data); return strdup([self getDebuggerInput]); } static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) { return (r << 0) | (g << 8) | (b << 16); } @implementation Document { GB_gameboy_t gb; volatile bool running; volatile bool stopping; NSConditionLock *has_debugger_input; NSMutableArray *debugger_input_queue; bool is_inited; } - (instancetype)init { self = [super init]; if (self) { has_debugger_input = [[NSConditionLock alloc] initWithCondition:0]; debugger_input_queue = [[NSMutableArray alloc] init]; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateDMG"]) { [self initDMG]; } else { [self initCGB]; } } return self; } - (void) initDMG { GB_init(&gb); GB_load_boot_rom(&gb, [[[NSBundle mainBundle] pathForResource:@"dmg_boot" ofType:@"bin"] UTF8String]); GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank); GB_set_log_callback(&gb, (GB_log_callback_t) consoleLog); GB_set_input_callback(&gb, (GB_input_callback_t) consoleInput); GB_set_rgb_encode_callback(&gb, rgbEncode); gb.user_data = (__bridge void *)(self); } - (void) initCGB { GB_init_cgb(&gb); GB_load_boot_rom(&gb, [[[NSBundle mainBundle] pathForResource:@"cgb_boot" ofType:@"bin"] UTF8String]); GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank); GB_set_log_callback(&gb, (GB_log_callback_t) consoleLog); GB_set_input_callback(&gb, (GB_input_callback_t) consoleInput); GB_set_rgb_encode_callback(&gb, rgbEncode); gb.user_data = (__bridge void *)(self); } - (void) vblank { self.view.mouseHidingEnabled = YES; [self.view flip]; GB_set_pixels_output(&gb, self.view.pixels); } - (void) run { running = true; GB_set_pixels_output(&gb, self.view.pixels); self.view.gb = &gb; GB_set_sample_rate(&gb, 96000); self.audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) { GB_apu_copy_buffer(&gb, buffer, nFrames); } andSampleRate:96000]; self.view.mouseHidingEnabled = YES; [self.audioClient start]; while (running) { GB_run(&gb); } [self.audioClient stop]; self.view.mouseHidingEnabled = NO; GB_save_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]); stopping = false; } - (void) start { if (running) return; [[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start]; } - (void) stop { if (!running) return; if (gb.debug_stopped) { gb.debug_stopped = false; [self consoleInput:nil]; } stopping = true; running = false; while (stopping); } - (IBAction)reset:(id)sender { bool was_cgb = gb.is_cgb; [self stop]; GB_free(&gb); is_inited = false; if (([sender tag] == 0 && was_cgb) || [sender tag] == 2) { [self initCGB]; } else { [self initDMG]; } if ([sender tag] != 0) { /* User explictly selected a model, save the preference */ [[NSUserDefaults standardUserDefaults] setBool:!gb.is_cgb forKey:@"EmulateDMG"]; } [self readFromFile:self.fileName ofType:@"gb"]; [self start]; } - (IBAction)togglePause:(id)sender { if (running) { [self stop]; } else { [self start]; } } - (void)dealloc { GB_free(&gb); } - (void)windowControllerDidLoadNib:(NSWindowController *)aController { [super windowControllerDidLoadNib:aController]; self.consoleOutput.textContainerInset = NSMakeSize(4, 4); [self.view becomeFirstResponder]; self.view.shouldBlendFrameWithPrevious = ![[NSUserDefaults standardUserDefaults] boolForKey:@"DisableFrameBlending"]; CGRect window_frame = self.mainWindow.frame; window_frame.size.width = MAX([[NSUserDefaults standardUserDefaults] integerForKey:@"LastWindowWidth"], window_frame.size.width); window_frame.size.height = MAX([[NSUserDefaults standardUserDefaults] integerForKey:@"LastWindowHeight"], window_frame.size.height); [self.mainWindow setFrame:window_frame display:YES]; [self start]; } + (BOOL)autosavesInPlace { return YES; } - (NSString *)windowNibName { // Override returning the nib file name of the document // 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. return @"Document"; } - (BOOL)readFromFile:(NSString *)fileName ofType:(NSString *)type { if (is_inited++) { return YES; } GB_load_rom(&gb, [fileName UTF8String]); GB_load_battery(&gb, [[[fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]); return YES; } - (void)close { [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.width forKey:@"LastWindowWidth"]; [[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.height forKey:@"LastWindowHeight"]; [self stop]; [self.consoleWindow close]; [super close]; } - (IBAction) interrupt:(id)sender { [self log:"^C\n"]; gb.debug_stopped = true; if (!running) { [self start]; } [self.consoleInput becomeFirstResponder]; } - (IBAction)mute:(id)sender { if (self.audioClient.isPlaying) { [self.audioClient stop]; } else { [self.audioClient start]; } } - (IBAction)toggleBlend:(id)sender { self.view.shouldBlendFrameWithPrevious ^= YES; [[NSUserDefaults standardUserDefaults] setBool:!self.view.shouldBlendFrameWithPrevious forKey:@"DisableFrameBlending"]; } - (BOOL)validateUserInterfaceItem:(id)anItem { if([anItem action] == @selector(mute:)) { [(NSMenuItem*)anItem setState:!self.audioClient.isPlaying]; } else if ([anItem action] == @selector(togglePause:)) { [(NSMenuItem*)anItem setState:(!running) || (gb.debug_stopped)]; return !gb.debug_stopped; } else if ([anItem action] == @selector(reset:) && anItem.tag != 0) { [(NSMenuItem*)anItem setState:(anItem.tag == 1 && !gb.is_cgb) || (anItem.tag == 2 && gb.is_cgb)]; } else if ([anItem action] == @selector(toggleBlend:)) { [(NSMenuItem*)anItem setState:self.view.shouldBlendFrameWithPrevious]; } else if ([anItem action] == @selector(interrupt:)) { if (![[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) { return false; } } return [super validateUserInterfaceItem:anItem]; } - (void) windowWillEnterFullScreen:(NSNotification *)notification { fullScreen = true; } - (void) windowWillExitFullScreen:(NSNotification *)notification { fullScreen = false; } - (NSRect)windowWillUseStandardFrame:(NSWindow *)window defaultFrame:(NSRect)newFrame { if (fullScreen) { return newFrame; } NSRect rect = window.contentView.frame; int titlebarSize = window.contentView.superview.frame.size.height - rect.size.height; int step = 160 / [[window screen] backingScaleFactor]; rect.size.width = floor(rect.size.width / step) * step + step; rect.size.height = rect.size.width / 10 * 9 + titlebarSize; if (rect.size.width > newFrame.size.width) { rect.size.width = 160; rect.size.height = 144 + titlebarSize; } else if (rect.size.height > newFrame.size.height) { rect.size.width = 160; rect.size.height = 144 + titlebarSize; } rect.origin = window.frame.origin; rect.origin.y -= rect.size.height - window.frame.size.height; return rect; } - (void) log: (const char *) string withAttributes: (GB_log_attributes) attributes { if (pendingLogLines > 128) { /* The ROM causes so many errors in such a short time, and we can't handle it. */ tooMuchLogs = true; return; } pendingLogLines++; /* Make sure mouse is not hidden while debugging */ self.view.mouseHidingEnabled = NO; NSString *nsstring = @(string); // For ref-counting dispatch_async(dispatch_get_main_queue(), ^{ NSFont *font = [NSFont userFixedPitchFontOfSize:12]; NSUnderlineStyle underline = NSUnderlineStyleNone; if (attributes & GB_LOG_BOLD) { font = [[NSFontManager sharedFontManager] convertFont:font toHaveTrait:NSBoldFontMask]; } if (attributes & GB_LOG_UNDERLINE_MASK) { underline = (attributes & GB_LOG_UNDERLINE_MASK) == GB_LOG_DASHED_UNDERLINE? NSUnderlinePatternDot | NSUnderlineStyleSingle : NSUnderlineStyleSingle; } NSMutableParagraphStyle *paragraph_style = [[NSMutableParagraphStyle alloc] init]; [paragraph_style setLineSpacing:2]; NSAttributedString *attributed = [[NSAttributedString alloc] initWithString:nsstring attributes:@{NSFontAttributeName: font, NSForegroundColorAttributeName: [NSColor whiteColor], NSUnderlineStyleAttributeName: @(underline), NSParagraphStyleAttributeName: paragraph_style}]; [self.consoleOutput.textStorage appendAttributedString:attributed]; if (pendingLogLines == 1) { if (tooMuchLogs) { tooMuchLogs = false; [self log:"[...]\n"]; } [self.consoleOutput scrollToEndOfDocument:nil]; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) { [self.consoleWindow orderBack:nil]; } } pendingLogLines--; }); } - (IBAction)showConsoleWindow:(id)sender { [self.consoleWindow orderBack:nil]; } - (IBAction)consoleInput:(NSTextField *)sender { NSString *line = [sender stringValue]; if ([line isEqualToString:@""] && lastConsoleInput) { line = lastConsoleInput; } else if (line) { lastConsoleInput = line; } else { line = @""; } [self log:[line UTF8String]]; [self log:"\n"]; [has_debugger_input lock]; [debugger_input_queue addObject:line]; [has_debugger_input unlockWithCondition:1]; [sender setStringValue:@""]; } - (const char *) getDebuggerInput { [self log:">"]; [has_debugger_input lockWhenCondition:1]; NSString *input = [debugger_input_queue firstObject]; [debugger_input_queue removeObjectAtIndex:0]; [has_debugger_input unlockWithCondition:[debugger_input_queue count] != 0]; return [input UTF8String]; } - (IBAction)saveState:(id)sender { bool was_running = running; if (!gb.debug_stopped) { [self stop]; } GB_save_state(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:[NSString stringWithFormat:@"s%ld", (long)[sender tag] ]] UTF8String]); if (was_running) { [self start]; } } - (IBAction)loadState:(id)sender { bool was_running = running; if (!gb.debug_stopped) { [self stop]; } GB_load_state(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:[NSString stringWithFormat:@"s%ld", (long)[sender tag] ]] UTF8String]); if (was_running) { [self start]; } } - (IBAction)clearConsole:(id)sender { [self.consoleOutput setString:@""]; } - (void)log:(const char *)log { [self log:log withAttributes:0]; } @end