2016-10-02 21:26:12 +00:00
|
|
|
#include <AVFoundation/AVFoundation.h>
|
2016-03-30 20:07:55 +00:00
|
|
|
#include <CoreAudio/CoreAudio.h>
|
2016-06-10 12:28:50 +00:00
|
|
|
#include "GBAudioClient.h"
|
2016-04-08 10:54:34 +00:00
|
|
|
#include "Document.h"
|
|
|
|
#include "AppDelegate.h"
|
2016-03-30 20:07:55 +00:00
|
|
|
#include "gb.h"
|
2016-07-13 20:00:50 +00:00
|
|
|
#include "debugger.h"
|
2016-08-12 19:49:17 +00:00
|
|
|
#include "memory.h"
|
2016-10-02 21:26:12 +00:00
|
|
|
#include "camera.h"
|
2016-08-12 19:49:17 +00:00
|
|
|
#include "HexFiend/HexFiend.h"
|
|
|
|
#include "GBMemoryByteArray.h"
|
2016-03-30 20:07:55 +00:00
|
|
|
|
2016-08-19 11:54:54 +00:00
|
|
|
/* Todo: The general Objective-C coding style conflicts with SameBoy's. This file needs a cleanup. */
|
|
|
|
|
2016-03-30 20:07:55 +00:00
|
|
|
@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;
|
2016-07-03 18:32:58 +00:00
|
|
|
bool fullScreen;
|
2016-07-17 21:39:43 +00:00
|
|
|
bool in_sync_input;
|
2016-08-12 19:49:17 +00:00
|
|
|
HFController *hex_controller;
|
2016-07-03 18:32:58 +00:00
|
|
|
|
2016-03-30 20:31:34 +00:00
|
|
|
NSString *lastConsoleInput;
|
2016-08-19 11:54:54 +00:00
|
|
|
HFLineCountingRepresenter *lineRep;
|
2016-10-02 21:26:12 +00:00
|
|
|
|
|
|
|
CVImageBufferRef cameraImage;
|
|
|
|
AVCaptureSession *cameraSession;
|
|
|
|
AVCaptureConnection *cameraConnection;
|
|
|
|
AVCaptureStillImageOutput *cameraOutput;
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
|
|
|
|
2016-06-10 12:28:50 +00:00
|
|
|
@property GBAudioClient *audioClient;
|
2016-03-30 20:07:55 +00:00
|
|
|
- (void) vblank;
|
2016-06-18 17:29:11 +00:00
|
|
|
- (void) log: (const char *) log withAttributes: (GB_log_attributes) attributes;
|
2016-03-30 20:07:55 +00:00
|
|
|
- (const char *) getDebuggerInput;
|
2016-07-17 21:39:43 +00:00
|
|
|
- (const char *) getAsyncDebuggerInput;
|
2016-10-02 21:26:12 +00:00
|
|
|
- (void) cameraRequestUpdate;
|
|
|
|
- (uint8_t) cameraGetPixelAtX:(uint8_t)x andY:(uint8_t)y;
|
2016-03-30 20:07:55 +00:00
|
|
|
@end
|
|
|
|
|
|
|
|
static void vblank(GB_gameboy_t *gb)
|
|
|
|
{
|
|
|
|
Document *self = (__bridge Document *)(gb->user_data);
|
|
|
|
[self vblank];
|
|
|
|
}
|
|
|
|
|
2016-06-18 17:29:11 +00:00
|
|
|
static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes)
|
2016-03-30 20:07:55 +00:00
|
|
|
{
|
|
|
|
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]);
|
|
|
|
}
|
|
|
|
|
2016-07-17 21:39:43 +00:00
|
|
|
static char *asyncConsoleInput(GB_gameboy_t *gb)
|
|
|
|
{
|
|
|
|
Document *self = (__bridge Document *)(gb->user_data);
|
|
|
|
const char *ret = [self getAsyncDebuggerInput];
|
|
|
|
return ret? strdup(ret) : NULL;
|
|
|
|
}
|
|
|
|
|
2016-06-18 17:29:11 +00:00
|
|
|
static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b)
|
2016-03-30 20:07:55 +00:00
|
|
|
{
|
2016-04-28 20:07:05 +00:00
|
|
|
return (r << 0) | (g << 8) | (b << 16);
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
|
|
|
|
2016-10-02 21:26:12 +00:00
|
|
|
static void cameraRequestUpdate(GB_gameboy_t *gb)
|
|
|
|
{
|
|
|
|
Document *self = (__bridge Document *)(gb->user_data);
|
|
|
|
[self cameraRequestUpdate];
|
|
|
|
}
|
|
|
|
|
|
|
|
static uint8_t cameraGetPixel(GB_gameboy_t *gb, uint8_t x, uint8_t y)
|
|
|
|
{
|
|
|
|
Document *self = (__bridge Document *)(gb->user_data);
|
|
|
|
return [self cameraGetPixelAtX:x andY:y];
|
|
|
|
}
|
|
|
|
|
2016-03-30 20:07:55 +00:00
|
|
|
@implementation Document
|
|
|
|
{
|
|
|
|
GB_gameboy_t gb;
|
|
|
|
volatile bool running;
|
|
|
|
volatile bool stopping;
|
|
|
|
NSConditionLock *has_debugger_input;
|
|
|
|
NSMutableArray *debugger_input_queue;
|
2016-08-12 19:49:17 +00:00
|
|
|
volatile bool is_inited;
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (instancetype)init {
|
|
|
|
self = [super init];
|
|
|
|
if (self) {
|
|
|
|
has_debugger_input = [[NSConditionLock alloc] initWithCondition:0];
|
|
|
|
debugger_input_queue = [[NSMutableArray alloc] init];
|
2016-04-08 10:10:01 +00:00
|
|
|
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"EmulateDMG"]) {
|
|
|
|
[self initDMG];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
[self initCGB];
|
|
|
|
}
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void) initDMG
|
|
|
|
{
|
2016-06-18 17:29:11 +00:00
|
|
|
GB_init(&gb);
|
|
|
|
GB_load_boot_rom(&gb, [[[NSBundle mainBundle] pathForResource:@"dmg_boot" ofType:@"bin"] UTF8String]);
|
2016-10-02 21:26:12 +00:00
|
|
|
[self initCommon];
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void) initCGB
|
|
|
|
{
|
2016-06-18 17:29:11 +00:00
|
|
|
GB_init_cgb(&gb);
|
|
|
|
GB_load_boot_rom(&gb, [[[NSBundle mainBundle] pathForResource:@"cgb_boot" ofType:@"bin"] UTF8String]);
|
2016-10-02 21:26:12 +00:00
|
|
|
[self initCommon];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void) initCommon
|
|
|
|
{
|
2016-06-18 17:29:11 +00:00
|
|
|
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);
|
2016-07-17 21:39:43 +00:00
|
|
|
GB_set_async_input_callback(&gb, (GB_input_callback_t) asyncConsoleInput);
|
2016-06-18 17:29:11 +00:00
|
|
|
GB_set_rgb_encode_callback(&gb, rgbEncode);
|
2016-10-02 21:26:12 +00:00
|
|
|
GB_set_camera_get_pixel_callback(&gb, cameraGetPixel);
|
|
|
|
GB_set_camera_update_request_callback(&gb, cameraRequestUpdate);
|
2016-03-30 20:07:55 +00:00
|
|
|
gb.user_data = (__bridge void *)(self);
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void) vblank
|
|
|
|
{
|
2016-08-20 21:38:26 +00:00
|
|
|
self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSFullScreenWindowMask) != 0;
|
2016-03-30 20:07:55 +00:00
|
|
|
[self.view flip];
|
2016-06-18 17:29:11 +00:00
|
|
|
GB_set_pixels_output(&gb, self.view.pixels);
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void) run
|
|
|
|
{
|
|
|
|
running = true;
|
2016-06-18 17:29:11 +00:00
|
|
|
GB_set_pixels_output(&gb, self.view.pixels);
|
2016-03-30 20:07:55 +00:00
|
|
|
self.view.gb = &gb;
|
2016-06-18 17:29:11 +00:00
|
|
|
GB_set_sample_rate(&gb, 96000);
|
2016-06-10 12:28:50 +00:00
|
|
|
self.audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) {
|
2016-06-18 17:29:11 +00:00
|
|
|
GB_apu_copy_buffer(&gb, buffer, nFrames);
|
2016-03-30 20:07:55 +00:00
|
|
|
} andSampleRate:96000];
|
2016-08-20 21:38:26 +00:00
|
|
|
self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSFullScreenWindowMask) != 0;
|
2016-08-21 18:58:33 +00:00
|
|
|
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) {
|
|
|
|
[self.audioClient start];
|
|
|
|
}
|
2016-08-12 19:49:17 +00:00
|
|
|
NSTimer *hex_timer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:YES];
|
|
|
|
[[NSRunLoop mainRunLoop] addTimer:hex_timer forMode:NSDefaultRunLoopMode];
|
2016-03-30 20:07:55 +00:00
|
|
|
while (running) {
|
2016-06-18 17:29:11 +00:00
|
|
|
GB_run(&gb);
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
2016-08-12 19:49:17 +00:00
|
|
|
[hex_timer invalidate];
|
2016-03-30 20:07:55 +00:00
|
|
|
[self.audioClient stop];
|
2016-07-20 20:26:54 +00:00
|
|
|
self.audioClient = nil;
|
2016-07-05 20:34:33 +00:00
|
|
|
self.view.mouseHidingEnabled = NO;
|
2016-06-18 17:29:11 +00:00
|
|
|
GB_save_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]);
|
2016-03-30 20:07:55 +00:00
|
|
|
stopping = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void) start
|
|
|
|
{
|
|
|
|
if (running) return;
|
2016-06-18 14:20:40 +00:00
|
|
|
[[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start];
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void) stop
|
|
|
|
{
|
|
|
|
if (!running) return;
|
2016-09-06 21:37:11 +00:00
|
|
|
gb.debug_disable = true;
|
2016-03-30 20:07:55 +00:00
|
|
|
if (gb.debug_stopped) {
|
|
|
|
gb.debug_stopped = false;
|
|
|
|
[self consoleInput:nil];
|
|
|
|
}
|
|
|
|
stopping = true;
|
|
|
|
running = false;
|
|
|
|
while (stopping);
|
2016-09-06 21:37:11 +00:00
|
|
|
gb.debug_disable = false;
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)reset:(id)sender
|
|
|
|
{
|
|
|
|
bool was_cgb = gb.is_cgb;
|
|
|
|
[self stop];
|
|
|
|
is_inited = false;
|
2016-09-06 21:44:00 +00:00
|
|
|
|
|
|
|
/* Back up user's breakpoints/watchpoints */
|
|
|
|
typeof(gb.breakpoints) breakpoints = gb.breakpoints;
|
|
|
|
typeof(gb.n_breakpoints) n_breakpoints = gb.n_breakpoints;
|
|
|
|
typeof(gb.watchpoints) watchpoints = gb.watchpoints;
|
|
|
|
typeof(gb.n_watchpoints) n_watchpoints = gb.n_watchpoints;
|
|
|
|
|
|
|
|
/* Reset them so they're not freed*/
|
|
|
|
gb.watchpoints = NULL;
|
|
|
|
gb.breakpoints = NULL;
|
|
|
|
gb.n_watchpoints = gb.n_breakpoints = 0;
|
|
|
|
|
2016-08-12 19:49:17 +00:00
|
|
|
GB_free(&gb);
|
2016-03-30 20:07:55 +00:00
|
|
|
if (([sender tag] == 0 && was_cgb) || [sender tag] == 2) {
|
|
|
|
[self initCGB];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
[self initDMG];
|
|
|
|
}
|
2016-09-06 21:44:00 +00:00
|
|
|
|
|
|
|
/* Restore backpoints/watchpoints */
|
|
|
|
gb.breakpoints = breakpoints;
|
|
|
|
gb.n_breakpoints = n_breakpoints;
|
|
|
|
gb.watchpoints = watchpoints;
|
|
|
|
gb.n_watchpoints = n_watchpoints;
|
|
|
|
|
2016-04-08 10:10:01 +00:00
|
|
|
if ([sender tag] != 0) {
|
|
|
|
/* User explictly selected a model, save the preference */
|
|
|
|
[[NSUserDefaults standardUserDefaults] setBool:!gb.is_cgb forKey:@"EmulateDMG"];
|
|
|
|
}
|
2016-03-30 20:07:55 +00:00
|
|
|
[self readFromFile:self.fileName ofType:@"gb"];
|
|
|
|
[self start];
|
2016-08-19 11:54:54 +00:00
|
|
|
|
|
|
|
if (hex_controller) {
|
|
|
|
/* Verify bank sanity, especially when switching models. */
|
|
|
|
[(GBMemoryByteArray *)(hex_controller.byteArray) setSelectedBank:0];
|
|
|
|
[self hexUpdateBank:self.memoryBankInput];
|
|
|
|
}
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)togglePause:(id)sender
|
|
|
|
{
|
|
|
|
if (running) {
|
|
|
|
[self stop];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
[self start];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)dealloc
|
|
|
|
{
|
2016-06-18 17:29:11 +00:00
|
|
|
GB_free(&gb);
|
2016-10-02 21:26:12 +00:00
|
|
|
if (cameraImage) {
|
|
|
|
CVBufferRelease(cameraImage);
|
|
|
|
}
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)windowControllerDidLoadNib:(NSWindowController *)aController {
|
|
|
|
[super windowControllerDidLoadNib:aController];
|
|
|
|
self.consoleOutput.textContainerInset = NSMakeSize(4, 4);
|
|
|
|
[self.view becomeFirstResponder];
|
2016-04-08 10:10:01 +00:00
|
|
|
self.view.shouldBlendFrameWithPrevious = ![[NSUserDefaults standardUserDefaults] boolForKey:@"DisableFrameBlending"];
|
2016-06-18 14:43:39 +00:00
|
|
|
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];
|
2016-03-30 20:07:55 +00:00
|
|
|
[self start];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2016-08-12 19:49:17 +00:00
|
|
|
- (void) initMemoryView
|
|
|
|
{
|
|
|
|
hex_controller = [[HFController alloc] init];
|
|
|
|
[hex_controller setBytesPerColumn:1];
|
|
|
|
[hex_controller setFont:[NSFont userFixedPitchFontOfSize:12]];
|
|
|
|
[hex_controller setEditMode:HFOverwriteMode];
|
|
|
|
|
|
|
|
[hex_controller setByteArray:[[GBMemoryByteArray alloc] initWithDocument:self]];
|
|
|
|
|
|
|
|
/* 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. */
|
|
|
|
HFLayoutRepresenter *layoutRep = [[HFLayoutRepresenter alloc] init];
|
|
|
|
HFHexTextRepresenter *hexRep = [[HFHexTextRepresenter alloc] init];
|
|
|
|
HFStringEncodingTextRepresenter *asciiRep = [[HFStringEncodingTextRepresenter alloc] init];
|
|
|
|
HFVerticalScrollerRepresenter *scrollRep = [[HFVerticalScrollerRepresenter alloc] init];
|
2016-08-19 11:54:54 +00:00
|
|
|
lineRep = [[HFLineCountingRepresenter alloc] init];
|
2016-08-12 19:49:17 +00:00
|
|
|
HFStatusBarRepresenter *statusRep = [[HFStatusBarRepresenter alloc] init];
|
|
|
|
|
|
|
|
lineRep.lineNumberFormat = HFLineNumberFormatHexadecimal;
|
|
|
|
|
|
|
|
/* Add all our reps to the controller. */
|
|
|
|
[hex_controller addRepresenter:layoutRep];
|
|
|
|
[hex_controller addRepresenter:hexRep];
|
|
|
|
[hex_controller addRepresenter:asciiRep];
|
|
|
|
[hex_controller addRepresenter:scrollRep];
|
|
|
|
[hex_controller addRepresenter:lineRep];
|
|
|
|
[hex_controller addRepresenter:statusRep];
|
|
|
|
|
|
|
|
/* Tell the layout rep which reps it should lay out. */
|
|
|
|
[layoutRep addRepresenter:hexRep];
|
|
|
|
[layoutRep addRepresenter:scrollRep];
|
|
|
|
[layoutRep addRepresenter:asciiRep];
|
|
|
|
[layoutRep addRepresenter:lineRep];
|
|
|
|
[layoutRep addRepresenter:statusRep];
|
|
|
|
|
|
|
|
|
|
|
|
[(NSView *)[hexRep view] setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
|
|
|
|
|
|
|
|
/* Grab the layout rep's view and stick it into our container. */
|
|
|
|
NSView *layoutView = [layoutRep view];
|
|
|
|
NSRect layoutViewFrame = self.memoryView.frame;
|
|
|
|
[layoutView setFrame:layoutViewFrame];
|
|
|
|
[layoutView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable | NSViewMaxYMargin];
|
|
|
|
[self.memoryView addSubview:layoutView];
|
2016-08-19 11:54:54 +00:00
|
|
|
|
|
|
|
self.memoryBankItem.enabled = false;
|
2016-08-12 19:49:17 +00:00
|
|
|
}
|
|
|
|
|
2016-03-30 20:07:55 +00:00
|
|
|
+ (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;
|
|
|
|
}
|
2016-06-18 17:29:11 +00:00
|
|
|
GB_load_rom(&gb, [fileName UTF8String]);
|
|
|
|
GB_load_battery(&gb, [[[fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]);
|
2016-07-13 20:00:50 +00:00
|
|
|
GB_debugger_load_symbol_file(&gb, [[[fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sym"] UTF8String]);
|
|
|
|
|
2016-03-30 20:07:55 +00:00
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)close
|
|
|
|
{
|
2016-06-18 14:43:39 +00:00
|
|
|
[[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.width forKey:@"LastWindowWidth"];
|
|
|
|
[[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.height forKey:@"LastWindowHeight"];
|
2016-03-30 20:07:55 +00:00
|
|
|
[self stop];
|
|
|
|
[self.consoleWindow close];
|
|
|
|
[super close];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction) interrupt:(id)sender
|
|
|
|
{
|
|
|
|
[self log:"^C\n"];
|
|
|
|
gb.debug_stopped = true;
|
2016-07-02 17:58:06 +00:00
|
|
|
if (!running) {
|
|
|
|
[self start];
|
|
|
|
}
|
2016-03-30 20:07:55 +00:00
|
|
|
[self.consoleInput becomeFirstResponder];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)mute:(id)sender
|
|
|
|
{
|
|
|
|
if (self.audioClient.isPlaying) {
|
|
|
|
[self.audioClient stop];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
[self.audioClient start];
|
|
|
|
}
|
2016-08-21 18:58:33 +00:00
|
|
|
[[NSUserDefaults standardUserDefaults] setBool:!self.audioClient.isPlaying forKey:@"Mute"];
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)toggleBlend:(id)sender
|
|
|
|
{
|
|
|
|
self.view.shouldBlendFrameWithPrevious ^= YES;
|
2016-04-08 10:10:01 +00:00
|
|
|
[[NSUserDefaults standardUserDefaults] setBool:!self.view.shouldBlendFrameWithPrevious forKey:@"DisableFrameBlending"];
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)anItem
|
|
|
|
{
|
|
|
|
if([anItem action] == @selector(mute:)) {
|
|
|
|
[(NSMenuItem*)anItem setState:!self.audioClient.isPlaying];
|
|
|
|
}
|
2016-04-08 10:54:34 +00:00
|
|
|
else if ([anItem action] == @selector(togglePause:)) {
|
2016-07-02 17:58:06 +00:00
|
|
|
[(NSMenuItem*)anItem setState:(!running) || (gb.debug_stopped)];
|
|
|
|
return !gb.debug_stopped;
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
2016-04-08 10:54:34 +00:00
|
|
|
else if ([anItem action] == @selector(reset:) && anItem.tag != 0) {
|
2016-03-30 20:07:55 +00:00
|
|
|
[(NSMenuItem*)anItem setState:(anItem.tag == 1 && !gb.is_cgb) || (anItem.tag == 2 && gb.is_cgb)];
|
|
|
|
}
|
2016-04-08 10:54:34 +00:00
|
|
|
else if ([anItem action] == @selector(toggleBlend:)) {
|
2016-03-30 20:07:55 +00:00
|
|
|
[(NSMenuItem*)anItem setState:self.view.shouldBlendFrameWithPrevious];
|
|
|
|
}
|
2016-04-08 10:54:34 +00:00
|
|
|
else if ([anItem action] == @selector(interrupt:)) {
|
|
|
|
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2016-03-30 20:07:55 +00:00
|
|
|
return [super validateUserInterfaceItem:anItem];
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-07-03 18:32:58 +00:00
|
|
|
- (void) windowWillEnterFullScreen:(NSNotification *)notification
|
|
|
|
{
|
|
|
|
fullScreen = true;
|
2016-08-20 21:38:26 +00:00
|
|
|
self.view.mouseHidingEnabled = running;
|
2016-07-03 18:32:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void) windowWillExitFullScreen:(NSNotification *)notification
|
|
|
|
{
|
|
|
|
fullScreen = false;
|
2016-08-20 21:38:26 +00:00
|
|
|
self.view.mouseHidingEnabled = NO;
|
2016-07-03 18:32:58 +00:00
|
|
|
}
|
|
|
|
|
2016-03-30 20:07:55 +00:00
|
|
|
- (NSRect)windowWillUseStandardFrame:(NSWindow *)window defaultFrame:(NSRect)newFrame
|
|
|
|
{
|
2016-07-03 18:32:58 +00:00
|
|
|
if (fullScreen) {
|
|
|
|
return newFrame;
|
|
|
|
}
|
2016-03-30 20:07:55 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2016-06-18 17:29:11 +00:00
|
|
|
- (void) log: (const char *) string withAttributes: (GB_log_attributes) attributes
|
2016-03-30 20:07:55 +00:00
|
|
|
{
|
|
|
|
if (pendingLogLines > 128) {
|
|
|
|
/* The ROM causes so many errors in such a short time, and we can't handle it. */
|
|
|
|
tooMuchLogs = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
pendingLogLines++;
|
2016-07-05 20:34:33 +00:00
|
|
|
|
|
|
|
/* Make sure mouse is not hidden while debugging */
|
|
|
|
self.view.mouseHidingEnabled = NO;
|
|
|
|
|
2016-03-30 20:07:55 +00:00
|
|
|
NSString *nsstring = @(string); // For ref-counting
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
2016-08-12 19:49:17 +00:00
|
|
|
[hex_controller reloadData];
|
|
|
|
|
2016-03-30 20:07:55 +00:00
|
|
|
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];
|
2016-04-08 10:54:34 +00:00
|
|
|
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]) {
|
|
|
|
[self.consoleWindow orderBack:nil];
|
|
|
|
}
|
2016-03-30 20:07:55 +00:00
|
|
|
}
|
|
|
|
pendingLogLines--;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)showConsoleWindow:(id)sender
|
|
|
|
{
|
|
|
|
[self.consoleWindow orderBack:nil];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)consoleInput:(NSTextField *)sender {
|
|
|
|
NSString *line = [sender stringValue];
|
2016-04-07 22:53:21 +00:00
|
|
|
if ([line isEqualToString:@""] && lastConsoleInput) {
|
2016-03-30 20:31:34 +00:00
|
|
|
line = lastConsoleInput;
|
|
|
|
}
|
|
|
|
else if (line) {
|
|
|
|
lastConsoleInput = line;
|
|
|
|
}
|
|
|
|
else {
|
2016-03-30 20:07:55 +00:00
|
|
|
line = @"";
|
|
|
|
}
|
2016-07-17 21:39:43 +00:00
|
|
|
|
|
|
|
if (!in_sync_input) {
|
|
|
|
[self log:">"];
|
|
|
|
}
|
2016-03-30 20:07:55 +00:00
|
|
|
[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:">"];
|
2016-07-17 21:39:43 +00:00
|
|
|
in_sync_input = true;
|
2016-03-30 20:07:55 +00:00
|
|
|
[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];
|
2016-07-17 21:39:43 +00:00
|
|
|
in_sync_input = false;
|
|
|
|
return [input UTF8String];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (const char *) getAsyncDebuggerInput
|
|
|
|
{
|
|
|
|
[has_debugger_input lock];
|
|
|
|
NSString *input = [debugger_input_queue firstObject];
|
|
|
|
if (input) {
|
|
|
|
[debugger_input_queue removeObjectAtIndex:0];
|
|
|
|
}
|
|
|
|
[has_debugger_input unlockWithCondition:[debugger_input_queue count] != 0];
|
2016-03-30 20:07:55 +00:00
|
|
|
return [input UTF8String];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)saveState:(id)sender
|
|
|
|
{
|
|
|
|
bool was_running = running;
|
|
|
|
if (!gb.debug_stopped) {
|
|
|
|
[self stop];
|
|
|
|
}
|
2016-06-18 17:29:11 +00:00
|
|
|
GB_save_state(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:[NSString stringWithFormat:@"s%ld", (long)[sender tag] ]] UTF8String]);
|
2016-03-30 20:07:55 +00:00
|
|
|
if (was_running) {
|
|
|
|
[self start];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)loadState:(id)sender
|
|
|
|
{
|
|
|
|
bool was_running = running;
|
|
|
|
if (!gb.debug_stopped) {
|
|
|
|
[self stop];
|
|
|
|
}
|
2016-06-18 17:29:11 +00:00
|
|
|
GB_load_state(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:[NSString stringWithFormat:@"s%ld", (long)[sender tag] ]] UTF8String]);
|
2016-03-30 20:07:55 +00:00
|
|
|
if (was_running) {
|
|
|
|
[self start];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)clearConsole:(id)sender
|
|
|
|
{
|
|
|
|
[self.consoleOutput setString:@""];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)log:(const char *)log
|
|
|
|
{
|
|
|
|
[self log:log withAttributes:0];
|
|
|
|
}
|
|
|
|
|
2016-08-12 19:49:17 +00:00
|
|
|
- (uint8_t) readMemory:(uint16_t)addr
|
|
|
|
{
|
|
|
|
while (!is_inited);
|
|
|
|
return GB_read_memory(&gb, addr);
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void) writeMemory:(uint16_t)addr value:(uint8_t)value
|
|
|
|
{
|
|
|
|
while (!is_inited);
|
|
|
|
GB_write_memory(&gb, addr, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void) performAtomicBlock: (void (^)())block
|
|
|
|
{
|
|
|
|
while (!is_inited);
|
2016-08-19 11:54:54 +00:00
|
|
|
bool was_running = running && !gb.debug_stopped;
|
2016-08-12 19:49:17 +00:00
|
|
|
if (was_running) {
|
|
|
|
[self stop];
|
|
|
|
}
|
|
|
|
block();
|
|
|
|
if (was_running) {
|
|
|
|
[self start];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void) reloadMemoryView
|
|
|
|
{
|
|
|
|
if (self.memoryWindow.isVisible) {
|
|
|
|
[hex_controller reloadData];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction) showMemory:(id)sender
|
|
|
|
{
|
|
|
|
if (!hex_controller) {
|
|
|
|
[self initMemoryView];
|
|
|
|
}
|
|
|
|
[self.memoryWindow makeKeyAndOrderFront:sender];
|
|
|
|
}
|
|
|
|
|
2016-08-19 11:54:54 +00:00
|
|
|
- (IBAction)hexGoTo:(id)sender
|
|
|
|
{
|
|
|
|
[self performAtomicBlock:^{
|
|
|
|
uint16_t addr;
|
|
|
|
if (GB_debugger_evaluate(&gb, [[sender stringValue] UTF8String], &addr, NULL)) {
|
|
|
|
NSBeep();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
addr -= lineRep.valueOffset;
|
|
|
|
if (addr >= hex_controller.byteArray.length) {
|
|
|
|
NSBeep();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
[hex_controller setSelectedContentsRanges:@[[HFRangeWrapper withRange:HFRangeMake(addr, 0)]]];
|
|
|
|
[hex_controller _ensureVisibilityOfLocation:addr];
|
|
|
|
[self.memoryWindow makeFirstResponder:self.memoryView.subviews[0].subviews[0]];
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)hexUpdateBank:(NSControl *)sender
|
|
|
|
{
|
|
|
|
[self performAtomicBlock:^{
|
|
|
|
uint16_t addr, bank;
|
|
|
|
if (GB_debugger_evaluate(&gb, [[sender stringValue] UTF8String], &addr, &bank)) {
|
|
|
|
NSBeep();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (bank == (uint16_t) -1) {
|
|
|
|
bank = addr;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint16_t n_banks = 1;
|
|
|
|
switch ([(GBMemoryByteArray *)(hex_controller.byteArray) mode]) {
|
|
|
|
case GBMemoryROM:
|
|
|
|
n_banks = gb.rom_size / 0x4000;
|
|
|
|
break;
|
|
|
|
case GBMemoryVRAM:
|
|
|
|
n_banks = gb.is_cgb ? 2 : 1;
|
|
|
|
break;
|
|
|
|
case GBMemoryExternalRAM:
|
2016-09-30 20:34:06 +00:00
|
|
|
n_banks = (gb.mbc_ram_size + 0x1FFF) / 0x2000;
|
2016-08-19 11:54:54 +00:00
|
|
|
break;
|
|
|
|
case GBMemoryRAM:
|
|
|
|
n_banks = gb.is_cgb ? 8 : 1;
|
|
|
|
break;
|
|
|
|
case GBMemoryEntireSpace:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
bank %= n_banks;
|
|
|
|
|
|
|
|
[sender setStringValue:[NSString stringWithFormat:@"$%x", bank]];
|
|
|
|
[(GBMemoryByteArray *)(hex_controller.byteArray) setSelectedBank:bank];
|
|
|
|
[hex_controller reloadData];
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)hexUpdateSpace:(NSPopUpButtonCell *)sender
|
|
|
|
{
|
|
|
|
self.memoryBankItem.enabled = [sender indexOfSelectedItem] != GBMemoryEntireSpace;
|
|
|
|
GBMemoryByteArray *byteArray = (GBMemoryByteArray *)(hex_controller.byteArray);
|
|
|
|
[byteArray setMode:(GB_memory_mode_t)[sender indexOfSelectedItem]];
|
|
|
|
switch ((GB_memory_mode_t)[sender indexOfSelectedItem]) {
|
|
|
|
case GBMemoryEntireSpace:
|
|
|
|
case GBMemoryROM:
|
|
|
|
lineRep.valueOffset = 0;
|
|
|
|
byteArray.selectedBank = gb.mbc_rom_bank;
|
|
|
|
break;
|
|
|
|
case GBMemoryVRAM:
|
|
|
|
lineRep.valueOffset = 0x8000;
|
|
|
|
byteArray.selectedBank = gb.cgb_vram_bank;
|
|
|
|
break;
|
|
|
|
case GBMemoryExternalRAM:
|
|
|
|
lineRep.valueOffset = 0xA000;
|
|
|
|
byteArray.selectedBank = gb.mbc_ram_bank;
|
|
|
|
break;
|
|
|
|
case GBMemoryRAM:
|
|
|
|
lineRep.valueOffset = 0xC000;
|
|
|
|
byteArray.selectedBank = gb.cgb_ram_bank;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
[self.memoryBankInput setStringValue:[NSString stringWithFormat:@"$%x", byteArray.selectedBank]];
|
|
|
|
[hex_controller reloadData];
|
|
|
|
[self.memoryView setNeedsDisplay:YES];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (GB_gameboy_t *) gameboy
|
|
|
|
{
|
|
|
|
return &gb;
|
|
|
|
}
|
|
|
|
|
2016-10-01 21:10:31 +00:00
|
|
|
+ (BOOL)canConcurrentlyReadDocumentsOfType:(NSString *)typeName
|
|
|
|
{
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
|
2016-10-02 21:26:12 +00:00
|
|
|
- (void)cameraRequestUpdate
|
|
|
|
{
|
|
|
|
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
|
@try {
|
|
|
|
if (!cameraSession) {
|
|
|
|
NSError *error;
|
|
|
|
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
|
|
|
|
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice: device error: &error];
|
|
|
|
CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions([[[device formats] firstObject] formatDescription]);
|
|
|
|
|
|
|
|
if (!input) {
|
|
|
|
GB_camera_updated(&gb);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
cameraOutput = [[AVCaptureStillImageOutput alloc] init];
|
|
|
|
/* Greyscale is not widely supported, so we use YUV, whose first element is the brightness. */
|
|
|
|
[cameraOutput setOutputSettings: @{(id)kCVPixelBufferPixelFormatTypeKey: @(kYUVSPixelFormat),
|
|
|
|
(id)kCVPixelBufferWidthKey: @(MAX(128, 112 * dimensions.width / dimensions.height)),
|
|
|
|
(id)kCVPixelBufferHeightKey: @(MAX(112, 128 * dimensions.height / dimensions.width)),}];
|
|
|
|
|
|
|
|
|
|
|
|
cameraSession = [AVCaptureSession new];
|
|
|
|
cameraSession.sessionPreset = AVCaptureSessionPresetPhoto;
|
|
|
|
|
|
|
|
[cameraSession addInput: input];
|
|
|
|
[cameraSession addOutput: cameraOutput];
|
|
|
|
/* ARC will stop the session when the window is closed. */
|
|
|
|
[cameraSession startRunning];
|
|
|
|
cameraConnection = [cameraOutput connectionWithMediaType: AVMediaTypeVideo];
|
|
|
|
}
|
|
|
|
|
|
|
|
[cameraOutput captureStillImageAsynchronouslyFromConnection: cameraConnection completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError *error) {
|
|
|
|
if (error) {
|
|
|
|
GB_camera_updated(&gb);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (cameraImage) {
|
|
|
|
CVBufferRelease(cameraImage);
|
|
|
|
cameraImage = NULL;
|
|
|
|
}
|
|
|
|
cameraImage = CVBufferRetain(CMSampleBufferGetImageBuffer(sampleBuffer));
|
|
|
|
/* We only need the actual buffer, no need to ever unlock it. */
|
|
|
|
CVPixelBufferLockBaseAddress(cameraImage, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
GB_camera_updated(&gb);
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
@catch (NSException *exception) {
|
|
|
|
/* I have not tested camera support on many devices, so we catch exceptions just in case. */
|
|
|
|
GB_camera_updated(&gb);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
- (uint8_t)cameraGetPixelAtX:(uint8_t)x andY:(uint8_t) y
|
|
|
|
{
|
|
|
|
if (!cameraImage) {
|
|
|
|
return rand();
|
|
|
|
}
|
|
|
|
|
|
|
|
uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(cameraImage);
|
|
|
|
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(cameraImage);
|
|
|
|
uint8_t offsetX = (CVPixelBufferGetWidth(cameraImage) - 128) / 2;
|
|
|
|
uint8_t offsetY = (CVPixelBufferGetHeight(cameraImage) - 112) / 2;
|
|
|
|
uint8_t ret = baseAddress[(x + offsetX) * 2 + (y + offsetY) * bytesPerRow];
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
2016-08-12 19:49:17 +00:00
|
|
|
@end
|