Merge commit 'de500cd397f6028e43574ecd36168bc4665e30bc' into update-subtrees

This commit is contained in:
Tim Allen 2021-08-01 09:46:40 +10:00
commit 56e359af28
126 changed files with 8490 additions and 1974 deletions

View File

@ -1,3 +1,7 @@
# Always use LF line endings for shaders
*.fsh text eol=lf
*.metal text eol=lf
HexFiend/* linguist-vendored HexFiend/* linguist-vendored
*.inc linguist-language=C *.inc linguist-language=C
Core/*.h linguist-language=C Core/*.h linguist-language=C

217
bsnes/gb/BESS.md Normal file
View File

@ -0,0 +1,217 @@
# BESS Best Effort Save State 1.0
## Motivation
BESS is a save state format specification designed to allow different emulators, as well as majorly different versions of the same emulator, to import save states from other BESS-compliant save states. BESS works by appending additional, implementation-agnostic information about the emulation state. This allows a single save state file to be read as both a fully-featured, implementation specific save state which includes detailed timing information; and as a portable "best effort" save state that represents a state accurately enough to be restored in casual use-cases.
## Specification
Every integer used in the BESS specification is stored in Little Endian encoding.
### BESS footer
BESS works by appending a detectable footer at the end of an existing save state format. The footer uses the following format:
| Offset from end of file | Content |
|-------------------------|-------------------------------------------------------|
| -8 | Offset to the first BESS Block, from the file's start |
| -4 | The ASCII string 'BESS' |
### BESS blocks
BESS uses a block format where each block contains the following header:
| Offset | Content |
|--------|---------------------------------------|
| 0 | A four-letter ASCII identifier |
| 4 | Length of the block, excluding header |
Every block is followed by another block, until the END block is reached. If an implementation encounters an unsupported block, it should be completely ignored (Should not have any effect and should not trigger a failure).
#### NAME block
The NAME block uses the `'NAME'` identifier, and is an optional block that contains the name of the emulator that created this save state. While optional, it is highly recommended to be included in every implementation it allows the user to know which emulator and version is compatible with the native save state format contained in this file. When used, this block should come first.
The length of the NAME block is variable, and it only contains the name and version of the originating emulator in ASCII.
#### INFO block
The INFO block uses the `'INFO'` identifier, and is an optional block that contains information about the ROM this save state originates from. When used, this block should come before `CORE` but after `NAME`. This block is 0x12 bytes long, and it follows this structure:
| Offset | Content |
|--------|--------------------------------------------------|
| 0x00 | Bytes 0x134-0x143 from the ROM (Title) |
| 0x10 | Bytes 0x14E-0x14F from the ROM (Global checksum) |
#### CORE block
The CORE block uses the `'CORE'` identifier, and is a required block that contains both core state information, as well as basic information about the BESS version used. This block must be the first block, unless the `NAME` or `INFO` blocks exist then it must come directly after them. An implementation should not enforce block order on blocks unknown to it for future compatibility.
The length of the CORE block is 0xD0 bytes, but implementations are expected to ignore any excess bytes. Following the BESS block header, the structure is as follows:
| Offset | Content |
|--------|----------------------------------------|
| 0x00 | Major BESS version as a 16-bit integer |
| 0x02 | Minor BESS version as a 16-bit integer |
Both major and minor versions should be 1. Implementations are expected to reject incompatible majors, but still attempt to read newer minor versions.
| Offset | Content |
|--------|----------------------------------------|
| 0x04 | A four-character ASCII model identifier |
BESS uses a four-character string to identify Game Boy models:
* The first letter represents mutually-incompatible families of models and is required. The allowed values are `'G'` for the original Game Boy family, `'S'` for the Super Game Boy family, and `'C'` for the Game Boy Color and Advance family.
* The second letter represents a specific model within the family, and is optional (If an implementation does not distinguish between specific models in a family, a space character may be used). The allowed values for family G are `'D'` for DMG and `'M'` for MGB; the allowed values for family S are `'N'` for NTSC, `'P'` for PAL, and `'2'` for SGB2; and the allowed values for family C are `'C'` for CGB, and `'A'` for the various GBA line models.
* The third letter represents a specific CPU revision within a model, and is optional (If an implementation does not distinguish between revisions, a space character may be used). The allowed values for model GD (DMG) are `'0'` and `'A'`, through `'C'`; the allowed values for model CC (CGB) are `'0'` and `'A'`, through `'E'`; the allowed values for model CA (AGB, AGS, GBP) are `'0'`, `'A'` and `'B'`; and for every other model this value must be a space character.
* The last character is used for padding and must be a space character.
For example; `'GD '` represents a DMG of an unspecified revision, `'S '` represents some model of the SGB family, and `'CCE '` represent a CGB using CPU revision E.
| Offset | Content |
|--------|--------------------------------------------------------|
| 0x08 | The value of the PC register |
| 0x0A | The value of the AF register |
| 0x0C | The value of the BC register |
| 0x0E | The value of the DE register |
| 0x10 | The value of the HL register |
| 0x12 | The value of the SP register |
| 0x14 | The value of IME (0 or 1) |
| 0x15 | The value of the IE register |
| 0x16 | Execution state (0 = running; 1 = halted; 2 = stopped) |
| 0x17 | Reserved, must be 0 |
| 0x18 | The values of every memory-mapped register (128 bytes) |
The values of memory-mapped registers should be written 'as-is' to memory as if the actual ROM wrote them, with the following exceptions and note:
* Unused registers have Don't-Care values which should be ignored
* Unused register bits have Don't-Care values which should be ignored
* If the model is CGB or newer, the value of KEY0 (FF4C) must be valid as it determines DMG mode
* Bit 2 determines DMG mode. A value of 0x04 usually denotes DMG mode, while a value of `0x80` usually denotes CGB mode.
* Sprite priority is derived from KEY0 (FF4C) instead of OPRI (FF6C) because OPRI can be modified after booting, but only the value of OPRI during boot ROM execution takes effect
* If a register doesn't exist on the emulated model (For example, KEY0 (FF4C) on a DMG), its value should be ignored.
* BANK (FF50) should be 0 if the boot ROM is still mapped, and 1 otherwise, and must be valid.
* Implementations should not start a serial transfer when writing the value of SB
* Similarly, no value of NRx4 should trigger a sound pulse on save state load
* And similarly again, implementations should not trigger DMA transfers when writing the values of DMA or HDMA5
* The value store for DIV will be used to set the internal divisor to `DIV << 8`
* Implementation should apply care when ordering the write operations (For example, writes to NR52 must come before writes to the other APU registers)
| Offset | Content |
|--------|--------------------------------------------------------------------|
| 0x98 | The size of RAM (32-bit integer) |
| 0x9C | The offset of RAM from file start (32-bit integer) |
| 0xA0 | The size of VRAM (32-bit integer) |
| 0xA4 | The offset of VRAM from file start (32-bit integer) |
| 0xA8 | The size of MBC RAM (32-bit integer) |
| 0xAC | The offset of MBC RAM from file start (32-bit integer) |
| 0xB0 | The size of OAM (=0xA0, 32-bit integer) |
| 0xB4 | The offset of OAM from file start (32-bit integer) |
| 0xB8 | The size of HRAM (=0x7F, 32-bit integer) |
| 0xBC | The offset of HRAM from file start (32-bit integer) |
| 0xC0 | The size of background palettes (=0x40 or 0, 32-bit integer) |
| 0xC4 | The offset of background palettes from file start (32-bit integer) |
| 0xC8 | The size of object palettes (=0x40 or 0, 32-bit integer) |
| 0xCC | The offset of object palettes from file start (32-bit integer) |
The contents of large buffers are stored outside of BESS structure so data from an implementation's native save state format can be reused. The offsets are absolute offsets from the save state file's start. Background and object palette sizes must be 0 for models prior to Game Boy Color.
An implementation needs handle size mismatches gracefully. For example, if too large MBC RAM size is specified, the superfluous data should be ignored. On the other hand, if a too small VRAM size is specified (For example, if it's a save state from an emulator emulating a CGB in DMG mode, and it didn't save the second CGB VRAM bank), the implementation is expected to set that extra bank to all zeros.
#### XOAM block
The XOAM block uses the `'XOAM'` identifier, and is an optional block that contains the data of extra OAM (addresses `0xFEA0-0xFEFF`). This block length must be `0x60`. Implementations that do not emulate this extra range are free to ignore the excess bytes, and to not create this block.
#### MBC block
The MBC block uses the `'MBC '` identifier, and is an optional block that is only used when saving states of ROMs that use an MBC. The length of this block is variable and must be divisible by 3.
This block contains an MBC-specific number of 3-byte-long pairs that represent the values of each MBC register. For example, for MBC5 the contents would look like:
| Offset | Content |
|--------|---------------------------------------|
| 0x0 | The value 0x0000 as a 16-bit integer |
| 0x2 | 0x0A if RAM is enabled, 0 otherwise |
| 0x3 | The value 0x2000 as a 16-bit integer |
| 0x5 | The lower 8 bits of the ROM bank |
| 0x6 | The value 0x3000 as a 16-bit integer |
| 0x8 | The bit 9 of the ROM bank |
| 0x9 | The value 0x4000 as a 16-bit integer |
| 0xB | The current RAM bank |
An implementation should parse this block as a series of writes to be made. Values outside the `0x0000-0x7FFF` and `0xA000-0xBFFF` ranges are not allowed. Implementations must perform the writes in order (i.e. not reverse, sorted, or any other transformation on their order)
#### RTC block
The RTC block uses the `'RTC '` identifier, and is an optional block that is used while emulating an MBC3 with an RTC. The contents of this block are identical to 64-bit RTC saves from VBA, which are also used by SameBoy and different emulators such as BGB.
The length of this block is 0x30 bytes long and it follows the following structure:
| Offset | Content |
|--------|------------------------------------------------------------------------|
| 0x00 | Current seconds (1 byte), followed by 3 bytes of padding |
| 0x04 | Current minutes (1 byte), followed by 3 bytes of padding |
| 0x08 | Current hours (1 byte), followed by 3 bytes of padding |
| 0x0C | Current days (1 byte), followed by 3 bytes of padding |
| 0x10 | Current high/overflow/running (1 byte), followed by 3 bytes of padding |
| 0x14 | Latched seconds (1 byte), followed by 3 bytes of padding |
| 0x18 | Latched minutes (1 byte), followed by 3 bytes of padding |
| 0x1C | Latched hours (1 byte), followed by 3 bytes of padding |
| 0x20 | Latched days (1 byte), followed by 3 bytes of padding |
| 0x24 | Latched high/overflow/running (1 byte), followed by 3 bytes of padding |
| 0x28 | UNIX timestamp at the time of the save state (64-bit) |
#### HUC3 block
The HUC3 block uses the `'HUC3'` identifier, and is an optional block that is used while emulating an HuC3 cartridge to store RTC and alarm information. The contents of this block are identical to HuC3 RTC saves from SameBoy.
The length of this block is 0x11 bytes long and it follows the following structure:
| Offset | Content |
|--------|-------------------------------------------------------|
| 0x00 | UNIX timestamp at the time of the save state (64-bit) |
| 0x08 | RTC minutes (16-bit) |
| 0x0A | RTC days (16-bit) |
| 0x0C | Scheduled alarm time minutes (16-bit) |
| 0x0E | Scheduled alarm time days (16-bit) |
| 0x10 | Alarm enabled flag (8-bits, either 0 or 1) |
#### SGB block
The SGB block uses the `'SGB '` identifier, and is an optional block that is only used while emulating an SGB or SGB2 *and* SGB commands enabled. Implementations must not save this block on other models or when SGB commands are disabled, and should assume SGB commands are disabled if this block is missing.
The length of this block is 0x39 bytes, but implementations should allow and ignore excess data in this block for extensions. The block follows the following structure:
| Offset | Content |
|--------|--------------------------------------------------------------------------------------------------------------------------|
| 0x00 | The size of the border tile data (=0x2000, 32-bit integer) |
| 0x04 | The offset of the border tile data (SNES tile format, 32-bit integer) |
| 0x08 | The size of the border tilemap (=0x800, 32-bit integer) |
| 0x0C | The offset of the border tilemap (LE 16-bit sequences, 32-bit integer) |
| 0x10 | The size of the border palettes (=0x80, 32-bit integer) |
| 0x14 | The offset of the border palettes (LE 16-bit sequences, 32-bit integer) |
| 0x18 | The size of active colorization palettes (=0x20, 32-bit integer) |
| 0x1C | The offset of the active colorization palettes (LE 16-bit sequences, 32-bit integer) |
| 0x20 | The size of RAM colorization palettes (=0x1000, 32-bit integer) |
| 0x24 | The offset of the RAM colorization palettes (LE 16-bit sequences, 32-bit integer) |
| 0x28 | The size of the attribute map (=0x168, 32-bit integer) |
| 0x2C | The offset of the attribute map (32-bit integer) |
| 0x30 | The size of the attribute files (=0xfd2, 32-bit integer) |
| 0x34 | The offset of the attribute files (32-bit integer) |
| 0x38 | Multiplayer status (1 byte); high nibble is player count (1, 2 or 4), low nibble is current player (Where Player 1 is 0) |
If only some of the size-offset pairs are available (for example, partial HLE SGB implementation), missing fields are allowed to have 0 as their size, and implementations are expected to fall back to a sane default.
#### END block
The END block uses the `'END '` identifier, and is a required block that marks the end of BESS data. Naturally, it must be the last block. The length of the END block must be 0.
## Validation and Failures
Other than previously specified required fail conditions, an implementation is free to decide what format errors should abort the loading of a save file. Structural errors (e.g. a block with an invalid length, a file offset that is outside the file's range, or a missing END block) should be considered as irrecoverable errors. Other errors that are considered fatal by SameBoy's implementation:
* Duplicate CORE block
* A known block, other than NAME, appearing before CORE
* An invalid length for the XOAM, RTC, SGB or HUC3 blocks
* An invalid length of MBC (not a multiple of 3)
* A write outside the $0000-$7FFF and $A000-$BFFF ranges in the MBC block
* An SGB block on a save state targeting another model
* An END block with non-zero length

View File

@ -271,7 +271,7 @@ TitleChecksums:
db $A2 ; STAR WARS-NOA db $A2 ; STAR WARS-NOA
db $49 ; db $49 ;
db $4E ; WAVERACE db $4E ; WAVERACE
db $43 | $80 ; db $43 ;
db $68 ; LOLO2 db $68 ; LOLO2
db $E0 ; YOSHI'S COOKIE db $E0 ; YOSHI'S COOKIE
db $8B ; MYSTIC QUEST db $8B ; MYSTIC QUEST
@ -330,7 +330,7 @@ ChecksumsEnd:
PalettePerChecksum: PalettePerChecksum:
palette_index: MACRO ; palette, flags palette_index: MACRO ; palette, flags
db ((\1) * 3) | (\2) ; | $80 means game requires DMG boot tilemap db ((\1)) | (\2) ; | $80 means game requires DMG boot tilemap
ENDM ENDM
palette_index 0, 0 ; Default Palette palette_index 0, 0 ; Default Palette
palette_index 4, 0 ; ALLEY WAY palette_index 4, 0 ; ALLEY WAY
@ -374,7 +374,7 @@ ENDM
palette_index 45, 0 ; STAR WARS-NOA palette_index 45, 0 ; STAR WARS-NOA
palette_index 36, 0 ; palette_index 36, 0 ;
palette_index 38, 0 ; WAVERACE palette_index 38, 0 ; WAVERACE
palette_index 26, 0 ; palette_index 26, $80 ;
palette_index 42, 0 ; LOLO2 palette_index 42, 0 ; LOLO2
palette_index 30, 0 ; YOSHI'S COOKIE palette_index 30, 0 ; YOSHI'S COOKIE
palette_index 41, 0 ; MYSTIC QUEST palette_index 41, 0 ; MYSTIC QUEST
@ -475,7 +475,7 @@ ENDM
palette_comb 17, 4, 13 palette_comb 17, 4, 13
raw_palette_comb 28 * 4 - 1, 0 * 4, 14 * 4 raw_palette_comb 28 * 4 - 1, 0 * 4, 14 * 4
raw_palette_comb 28 * 4 - 1, 4 * 4, 15 * 4 raw_palette_comb 28 * 4 - 1, 4 * 4, 15 * 4
palette_comb 19, 22, 9 raw_palette_comb 19 * 4, 23 * 4 - 1, 9 * 4
palette_comb 16, 28, 10 palette_comb 16, 28, 10
palette_comb 4, 23, 28 palette_comb 4, 23, 28
palette_comb 17, 22, 2 palette_comb 17, 22, 2
@ -918,7 +918,10 @@ EmulateDMG:
call GetPaletteIndex call GetPaletteIndex
bit 7, a bit 7, a
call nz, LoadDMGTilemap call nz, LoadDMGTilemap
and $7F res 7, a
ld b, a
add b
add b
ld b, a ld b, a
ldh a, [InputPalette] ldh a, [InputPalette]
and a and a
@ -978,7 +981,7 @@ GetPaletteIndex:
; We might have a match, Do duplicate/4th letter check ; We might have a match, Do duplicate/4th letter check
ld a, l ld a, l
sub FirstChecksumWithDuplicate - TitleChecksums sub FirstChecksumWithDuplicate - TitleChecksums + 1
jr c, .match ; Does not have a duplicate, must be a match! jr c, .match ; Does not have a duplicate, must be a match!
; Has a duplicate; check 4th letter ; Has a duplicate; check 4th letter
push hl push hl

View File

@ -1,15 +1,25 @@
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <WebKit/WebKit.h>
@interface AppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate> @interface AppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate, NSMenuDelegate, WebUIDelegate, WebPolicyDelegate, WebFrameLoadDelegate>
@property IBOutlet NSWindow *preferencesWindow; @property (nonatomic, strong) IBOutlet NSWindow *preferencesWindow;
@property (strong) IBOutlet NSView *graphicsTab; @property (nonatomic, strong) IBOutlet NSView *graphicsTab;
@property (strong) IBOutlet NSView *emulationTab; @property (nonatomic, strong) IBOutlet NSView *emulationTab;
@property (strong) IBOutlet NSView *audioTab; @property (nonatomic, strong) IBOutlet NSView *audioTab;
@property (strong) IBOutlet NSView *controlsTab; @property (nonatomic, strong) IBOutlet NSView *controlsTab;
@property (nonatomic, strong) IBOutlet NSView *updatesTab;
- (IBAction)showPreferences: (id) sender; - (IBAction)showPreferences: (id) sender;
- (IBAction)toggleDeveloperMode:(id)sender; - (IBAction)toggleDeveloperMode:(id)sender;
- (IBAction)switchPreferencesTab:(id)sender; - (IBAction)switchPreferencesTab:(id)sender;
@property (nonatomic, weak) IBOutlet NSMenuItem *linkCableMenuItem;
@property (nonatomic, strong) IBOutlet NSWindow *updateWindow;
@property (nonatomic, strong) IBOutlet WebView *updateChanges;
@property (nonatomic, strong) IBOutlet NSProgressIndicator *updatesSpinner;
@property (strong) IBOutlet NSButton *updatesButton;
@property (strong) IBOutlet NSTextField *updateProgressLabel;
@property (strong) IBOutlet NSButton *updateProgressButton;
@property (strong) IBOutlet NSWindow *updateProgressWindow;
@property (strong) IBOutlet NSProgressIndicator *updateProgressSpinner;
@end @end

View File

@ -4,11 +4,33 @@
#include <Core/gb.h> #include <Core/gb.h>
#import <Carbon/Carbon.h> #import <Carbon/Carbon.h>
#import <JoyKit/JoyKit.h> #import <JoyKit/JoyKit.h>
#import <WebKit/WebKit.h>
#define UPDATE_SERVER "https://sameboy.github.io"
static uint32_t color_to_int(NSColor *color)
{
color = [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]];
return (((unsigned)(color.redComponent * 0xFF)) << 16) |
(((unsigned)(color.greenComponent * 0xFF)) << 8) |
((unsigned)(color.blueComponent * 0xFF));
}
@implementation AppDelegate @implementation AppDelegate
{ {
NSWindow *preferences_window; NSWindow *preferences_window;
NSArray<NSView *> *preferences_tabs; NSArray<NSView *> *preferences_tabs;
NSString *_lastVersion;
NSString *_updateURL;
NSURLSessionDownloadTask *_updateTask;
enum {
UPDATE_DOWNLOADING,
UPDATE_EXTRACTING,
UPDATE_WAIT_INSTALL,
UPDATE_INSTALLING,
UPDATE_FAILED,
} _updateState;
NSString *_downloadDirectory;
} }
- (void) applicationDidFinishLaunching:(NSNotification *)notification - (void) applicationDidFinishLaunching:(NSNotification *)notification
@ -44,6 +66,8 @@
@"GBCGBModel": @(GB_MODEL_CGB_E), @"GBCGBModel": @(GB_MODEL_CGB_E),
@"GBSGBModel": @(GB_MODEL_SGB2), @"GBSGBModel": @(GB_MODEL_SGB2),
@"GBRumbleMode": @(GB_RUMBLE_CARTRIDGE_ONLY), @"GBRumbleMode": @(GB_RUMBLE_CARTRIDGE_ONLY),
@"GBVolume": @(1.0),
}]; }];
[JOYController startOnRunLoop:[NSRunLoop currentRunLoop] withOptions:@{ [JOYController startOnRunLoop:[NSRunLoop currentRunLoop] withOptions:@{
@ -54,6 +78,16 @@
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) {
[NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self;
} }
[self askAutoUpdates];
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBAutoUpdatesEnabled"]) {
[self checkForUpdates];
}
if ([[NSProcessInfo processInfo].arguments containsObject:@"--update-launch"]) {
[NSApp activateIgnoringOtherApps:YES];
}
} }
- (IBAction)toggleDeveloperMode:(id)sender - (IBAction)toggleDeveloperMode:(id)sender
@ -81,10 +115,29 @@
if ([anItem action] == @selector(toggleDeveloperMode:)) { if ([anItem action] == @selector(toggleDeveloperMode:)) {
[(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]]; [(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]];
} }
if (anItem == self.linkCableMenuItem) {
return [[NSDocumentController sharedDocumentController] documents].count > 1;
}
return true; return true;
} }
- (void)menuNeedsUpdate:(NSMenu *)menu
{
NSMutableArray *items = [NSMutableArray array];
NSDocument *currentDocument = [[NSDocumentController sharedDocumentController] currentDocument];
for (NSDocument *document in [[NSDocumentController sharedDocumentController] documents]) {
if (document == currentDocument) continue;
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:document.displayName action:@selector(connectLinkCable:) keyEquivalent:@""];
item.representedObject = document;
item.image = [[NSWorkspace sharedWorkspace] iconForFile:document.fileURL.path];
[item.image setSize:NSMakeSize(16, 16)];
[items addObject:item];
}
menu.itemArray = items;
}
- (IBAction) showPreferences: (id) sender - (IBAction) showPreferences: (id) sender
{ {
NSArray *objects; NSArray *objects;
@ -92,21 +145,305 @@
[[NSBundle mainBundle] loadNibNamed:@"Preferences" owner:self topLevelObjects:&objects]; [[NSBundle mainBundle] loadNibNamed:@"Preferences" owner:self topLevelObjects:&objects];
NSToolbarItem *first_toolbar_item = [_preferencesWindow.toolbar.items firstObject]; NSToolbarItem *first_toolbar_item = [_preferencesWindow.toolbar.items firstObject];
_preferencesWindow.toolbar.selectedItemIdentifier = [first_toolbar_item itemIdentifier]; _preferencesWindow.toolbar.selectedItemIdentifier = [first_toolbar_item itemIdentifier];
preferences_tabs = @[self.emulationTab, self.graphicsTab, self.audioTab, self.controlsTab]; preferences_tabs = @[self.emulationTab, self.graphicsTab, self.audioTab, self.controlsTab, self.updatesTab];
[self switchPreferencesTab:first_toolbar_item]; [self switchPreferencesTab:first_toolbar_item];
[_preferencesWindow center]; [_preferencesWindow center];
#ifndef UPDATE_SUPPORT
[_preferencesWindow.toolbar removeItemAtIndex:4];
#endif
} }
[_preferencesWindow makeKeyAndOrderFront:self]; [_preferencesWindow makeKeyAndOrderFront:self];
} }
- (BOOL)applicationOpenUntitledFile:(NSApplication *)sender - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
{ {
[self askAutoUpdates];
/* Bring an existing panel to the foreground */
for (NSWindow *window in [[NSApplication sharedApplication] windows]) {
if ([window isKindOfClass:[NSOpenPanel class]]) {
[(NSOpenPanel *)window makeKeyAndOrderFront:nil];
return true;
}
}
[[NSDocumentController sharedDocumentController] openDocument:self]; [[NSDocumentController sharedDocumentController] openDocument:self];
return YES; return true;
} }
- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification - (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification
{ {
[[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:YES]; [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:YES];
} }
- (void)updateFound
{
[[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/raw_changes"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSColor *linkColor = [NSColor colorWithRed:0.125 green:0.325 blue:1.0 alpha:1.0];
if (@available(macOS 10.10, *)) {
linkColor = [NSColor linkColor];
}
NSString *changes = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSRange cutoffRange = [changes rangeOfString:@"<!--(" GB_VERSION ")-->"];
if (cutoffRange.location != NSNotFound) {
changes = [changes substringToIndex:cutoffRange.location];
}
NSString *html = [NSString stringWithFormat:@"<!DOCTYPE html><html><head><title></title>"
"<style>html {background-color:transparent; color: #%06x; line-height:1.5} a:link, a:visited{color:#%06x; text-decoration:none}</style>"
"</head><body>%@</body></html>",
color_to_int([NSColor textColor]),
color_to_int(linkColor),
changes];
if ([(NSHTTPURLResponse *)response statusCode] == 200) {
dispatch_async(dispatch_get_main_queue(), ^{
NSArray *objects;
[[NSBundle mainBundle] loadNibNamed:@"UpdateWindow" owner:self topLevelObjects:&objects];
self.updateChanges.preferences.standardFontFamily = [NSFont systemFontOfSize:0].familyName;
self.updateChanges.preferences.fixedFontFamily = @"Menlo";
self.updateChanges.drawsBackground = false;
[self.updateChanges.mainFrame loadHTMLString:html baseURL:nil];
});
}
}] resume];
}
- (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
{
// Disable reload context menu
if ([defaultMenuItems count] <= 2) {
return nil;
}
return defaultMenuItems;
}
- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sender.mainFrame.frameView.documentView.enclosingScrollView.drawsBackground = true;
sender.mainFrame.frameView.documentView.enclosingScrollView.backgroundColor = [NSColor textBackgroundColor];
sender.policyDelegate = self;
[self.updateWindow center];
[self.updateWindow makeKeyAndOrderFront:nil];
});
}
- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener
{
[listener ignore];
[[NSWorkspace sharedWorkspace] openURL:[request URL]];
}
- (void)checkForUpdates
{
#ifdef UPDATE_SUPPORT
[[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/latest_version"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
[self.updatesSpinner stopAnimation:nil];
[self.updatesButton setEnabled:YES];
});
if ([(NSHTTPURLResponse *)response statusCode] == 200) {
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSArray <NSString *> *components = [string componentsSeparatedByString:@"|"];
if (components.count != 2) return;
_lastVersion = components[0];
_updateURL = components[1];
if (![@GB_VERSION isEqualToString:_lastVersion] &&
![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBSkippedVersion"] isEqualToString:_lastVersion]) {
[self updateFound];
}
}
}] resume];
#endif
}
- (IBAction)userCheckForUpdates:(id)sender
{
if (self.updateWindow) {
[self.updateWindow makeKeyAndOrderFront:sender];
}
else {
[[NSUserDefaults standardUserDefaults] setObject:nil forKey:@"GBSkippedVersion"];
[self checkForUpdates];
[sender setEnabled:false];
[self.updatesSpinner startAnimation:sender];
}
}
- (void)askAutoUpdates
{
#ifdef UPDATE_SUPPORT
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAskedAutoUpdates"]) {
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = @"Should SameBoy check for updates when launched?";
alert.informativeText = @"SameBoy is frequently updated with new features, accuracy improvements, and bug fixes. This setting can always be changed in the preferences window.";
[alert addButtonWithTitle:@"Check on Launch"];
[alert addButtonWithTitle:@"Don't Check on Launch"];
[[NSUserDefaults standardUserDefaults] setBool:[alert runModal] == NSAlertFirstButtonReturn forKey:@"GBAutoUpdatesEnabled"];
[[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBAskedAutoUpdates"];
}
#endif
}
- (IBAction)skipVersion:(id)sender
{
[[NSUserDefaults standardUserDefaults] setObject:_lastVersion forKey:@"GBSkippedVersion"];
[self.updateWindow performClose:sender];
}
- (IBAction)installUpdate:(id)sender
{
[self.updateProgressSpinner startAnimation:nil];
self.updateProgressButton.title = @"Cancel";
self.updateProgressButton.enabled = true;
self.updateProgressLabel.stringValue = @"Downloading update...";
_updateState = UPDATE_DOWNLOADING;
_updateTask = [[NSURLSession sharedSession] downloadTaskWithURL: [NSURL URLWithString:_updateURL] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
_updateTask = nil;
dispatch_sync(dispatch_get_main_queue(), ^{
self.updateProgressButton.enabled = false;
self.updateProgressLabel.stringValue = @"Extracting update...";
_updateState = UPDATE_EXTRACTING;
});
_downloadDirectory = [[[NSFileManager defaultManager] URLForDirectory:NSItemReplacementDirectory
inDomain:NSUserDomainMask
appropriateForURL:[[NSBundle mainBundle] bundleURL]
create:YES
error:nil] path];
NSTask *unzipTask;
if (!_downloadDirectory) {
dispatch_sync(dispatch_get_main_queue(), ^{
self.updateProgressButton.enabled = false;
self.updateProgressLabel.stringValue = @"Failed to extract update.";
_updateState = UPDATE_FAILED;
self.updateProgressButton.title = @"Close";
self.updateProgressButton.enabled = true;
[self.updateProgressSpinner stopAnimation:nil];
});
}
unzipTask = [[NSTask alloc] init];
unzipTask.launchPath = @"/usr/bin/unzip";
unzipTask.arguments = @[location.path, @"-d", _downloadDirectory];
[unzipTask launch];
[unzipTask waitUntilExit];
if (unzipTask.terminationStatus != 0 || unzipTask.terminationReason != NSTaskTerminationReasonExit) {
[[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
dispatch_sync(dispatch_get_main_queue(), ^{
self.updateProgressButton.enabled = false;
self.updateProgressLabel.stringValue = @"Failed to extract update.";
_updateState = UPDATE_FAILED;
self.updateProgressButton.title = @"Close";
self.updateProgressButton.enabled = true;
[self.updateProgressSpinner stopAnimation:nil];
});
return;
}
dispatch_sync(dispatch_get_main_queue(), ^{
self.updateProgressButton.enabled = false;
self.updateProgressLabel.stringValue = @"Update ready, save your game progress and click Install.";
_updateState = UPDATE_WAIT_INSTALL;
self.updateProgressButton.title = @"Install";
self.updateProgressButton.enabled = true;
[self.updateProgressSpinner stopAnimation:nil];
});
}];
[_updateTask resume];
self.updateProgressWindow.preventsApplicationTerminationWhenModal = false;
[self.updateWindow beginSheet:self.updateProgressWindow completionHandler:^(NSModalResponse returnCode) {
[self.updateWindow close];
}];
}
- (void)performUpgrade
{
self.updateProgressButton.enabled = false;
self.updateProgressLabel.stringValue = @"Instaling update...";
_updateState = UPDATE_INSTALLING;
self.updateProgressButton.enabled = false;
[self.updateProgressSpinner startAnimation:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *executablePath = [[NSBundle mainBundle] executablePath];
NSString *contentsPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Contents"];
NSString *contentsTempPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"TempContents"];
NSString *updateContentsPath = [_downloadDirectory stringByAppendingPathComponent:@"SameBoy.app/Contents"];
NSError *error = nil;
[[NSFileManager defaultManager] moveItemAtPath:contentsPath toPath:contentsTempPath error:&error];
if (error) {
[[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
_downloadDirectory = nil;
dispatch_sync(dispatch_get_main_queue(), ^{
self.updateProgressButton.enabled = false;
self.updateProgressLabel.stringValue = @"Failed to install update.";
_updateState = UPDATE_FAILED;
self.updateProgressButton.title = @"Close";
self.updateProgressButton.enabled = true;
[self.updateProgressSpinner stopAnimation:nil];
});
return;
}
[[NSFileManager defaultManager] moveItemAtPath:updateContentsPath toPath:contentsPath error:&error];
if (error) {
[[NSFileManager defaultManager] moveItemAtPath:contentsTempPath toPath:contentsPath error:nil];
[[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
_downloadDirectory = nil;
dispatch_sync(dispatch_get_main_queue(), ^{
self.updateProgressButton.enabled = false;
self.updateProgressLabel.stringValue = @"Failed to install update.";
_updateState = UPDATE_FAILED;
self.updateProgressButton.title = @"Close";
self.updateProgressButton.enabled = true;
[self.updateProgressSpinner stopAnimation:nil];
});
return;
}
[[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
[[NSFileManager defaultManager] removeItemAtPath:contentsTempPath error:nil];
_downloadDirectory = nil;
atexit_b(^{
execl(executablePath.UTF8String, executablePath.UTF8String, "--update-launch", NULL);
});
dispatch_async(dispatch_get_main_queue(), ^{
[NSApp terminate:nil];
});
});
}
- (IBAction)updateAction:(id)sender
{
switch (_updateState) {
case UPDATE_DOWNLOADING:
[_updateTask cancelByProducingResumeData:nil];
_updateTask = nil;
[self.updateProgressWindow close];
break;
case UPDATE_WAIT_INSTALL:
[self performUpgrade];
break;
case UPDATE_EXTRACTING:
case UPDATE_INSTALLING:
break;
case UPDATE_FAILED:
[self.updateProgressWindow close];
break;
}
}
- (void)dealloc
{
if (_downloadDirectory) {
[[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
}
}
- (IBAction)nop:(id)sender
{
}
@end @end

Binary file not shown.

View File

@ -18,7 +18,7 @@ typedef NS_ENUM(NSInteger, NSWindowToolbarStyle) {
} API_AVAILABLE(macos(11.0)); } API_AVAILABLE(macos(11.0));
@interface NSWindow (toolbarStyle) @interface NSWindow (toolbarStyle)
@property NSWindowToolbarStyle toolbarStyle API_AVAILABLE(macos(11.0)); @property (nonatomic) NSWindowToolbarStyle toolbarStyle API_AVAILABLE(macos(11.0));
@end @end
@interface NSImage (SFSymbols) @interface NSImage (SFSymbols)

View File

@ -2,44 +2,60 @@
#include "GBView.h" #include "GBView.h"
#include "GBImageView.h" #include "GBImageView.h"
#include "GBSplitView.h" #include "GBSplitView.h"
#include "GBVisualizerView.h"
#include "GBOSDView.h"
@class GBCheatWindowController; @class GBCheatWindowController;
@interface Document : NSDocument <NSWindowDelegate, GBImageViewDelegate, NSTableViewDataSource, NSTableViewDelegate, NSSplitViewDelegate> @interface Document : NSDocument <NSWindowDelegate, GBImageViewDelegate, NSTableViewDataSource, NSTableViewDelegate, NSSplitViewDelegate>
@property (strong) IBOutlet GBView *view; @property (nonatomic, readonly) GB_gameboy_t *gb;
@property (strong) IBOutlet NSTextView *consoleOutput; @property (nonatomic, strong) IBOutlet GBView *view;
@property (strong) IBOutlet NSPanel *consoleWindow; @property (nonatomic, strong) IBOutlet NSTextView *consoleOutput;
@property (strong) IBOutlet NSTextField *consoleInput; @property (nonatomic, strong) IBOutlet NSPanel *consoleWindow;
@property (strong) IBOutlet NSWindow *mainWindow; @property (nonatomic, strong) IBOutlet NSTextField *consoleInput;
@property (strong) IBOutlet NSView *memoryView; @property (nonatomic, strong) IBOutlet NSWindow *mainWindow;
@property (strong) IBOutlet NSPanel *memoryWindow; @property (nonatomic, strong) IBOutlet NSView *memoryView;
@property (readonly) GB_gameboy_t *gameboy; @property (nonatomic, strong) IBOutlet NSPanel *memoryWindow;
@property (strong) IBOutlet NSTextField *memoryBankInput; @property (nonatomic, readonly) GB_gameboy_t *gameboy;
@property (strong) IBOutlet NSToolbarItem *memoryBankItem; @property (nonatomic, strong) IBOutlet NSTextField *memoryBankInput;
@property (strong) IBOutlet GBImageView *tilesetImageView; @property (nonatomic, strong) IBOutlet NSToolbarItem *memoryBankItem;
@property (strong) IBOutlet NSPopUpButton *tilesetPaletteButton; @property (nonatomic, strong) IBOutlet GBImageView *tilesetImageView;
@property (strong) IBOutlet GBImageView *tilemapImageView; @property (nonatomic, strong) IBOutlet NSPopUpButton *tilesetPaletteButton;
@property (strong) IBOutlet NSPopUpButton *tilemapPaletteButton; @property (nonatomic, strong) IBOutlet GBImageView *tilemapImageView;
@property (strong) IBOutlet NSPopUpButton *tilemapMapButton; @property (nonatomic, strong) IBOutlet NSPopUpButton *tilemapPaletteButton;
@property (strong) IBOutlet NSPopUpButton *TilemapSetButton; @property (nonatomic, strong) IBOutlet NSPopUpButton *tilemapMapButton;
@property (strong) IBOutlet NSButton *gridButton; @property (nonatomic, strong) IBOutlet NSPopUpButton *TilemapSetButton;
@property (strong) IBOutlet NSTabView *vramTabView; @property (nonatomic, strong) IBOutlet NSButton *gridButton;
@property (strong) IBOutlet NSPanel *vramWindow; @property (nonatomic, strong) IBOutlet NSTabView *vramTabView;
@property (strong) IBOutlet NSTextField *vramStatusLabel; @property (nonatomic, strong) IBOutlet NSPanel *vramWindow;
@property (strong) IBOutlet NSTableView *paletteTableView; @property (nonatomic, strong) IBOutlet NSTextField *vramStatusLabel;
@property (strong) IBOutlet NSTableView *spritesTableView; @property (nonatomic, strong) IBOutlet NSTableView *paletteTableView;
@property (strong) IBOutlet NSPanel *printerFeedWindow; @property (nonatomic, strong) IBOutlet NSTableView *spritesTableView;
@property (strong) IBOutlet NSImageView *feedImageView; @property (nonatomic, strong) IBOutlet NSPanel *printerFeedWindow;
@property (strong) IBOutlet NSTextView *debuggerSideViewInput; @property (nonatomic, strong) IBOutlet NSImageView *feedImageView;
@property (strong) IBOutlet NSTextView *debuggerSideView; @property (nonatomic, strong) IBOutlet NSTextView *debuggerSideViewInput;
@property (strong) IBOutlet GBSplitView *debuggerSplitView; @property (nonatomic, strong) IBOutlet NSTextView *debuggerSideView;
@property (strong) IBOutlet NSBox *debuggerVerticalLine; @property (nonatomic, strong) IBOutlet GBSplitView *debuggerSplitView;
@property (strong) IBOutlet NSPanel *cheatsWindow; @property (nonatomic, strong) IBOutlet NSBox *debuggerVerticalLine;
@property (strong) IBOutlet GBCheatWindowController *cheatWindowController; @property (nonatomic, strong) IBOutlet NSPanel *cheatsWindow;
@property (nonatomic, strong) IBOutlet GBCheatWindowController *cheatWindowController;
@property (nonatomic, readonly) Document *partner;
@property (nonatomic, readonly) bool isSlave;
@property (strong) IBOutlet NSView *gbsPlayerView;
@property (strong) IBOutlet NSTextField *gbsTitle;
@property (strong) IBOutlet NSTextField *gbsAuthor;
@property (strong) IBOutlet NSTextField *gbsCopyright;
@property (strong) IBOutlet NSPopUpButton *gbsTracks;
@property (strong) IBOutlet NSButton *gbsPlayPauseButton;
@property (strong) IBOutlet NSButton *gbsRewindButton;
@property (strong) IBOutlet NSSegmentedControl *gbsNextPrevButton;
@property (strong) IBOutlet GBVisualizerView *gbsVisualizer;
@property (strong) IBOutlet GBOSDView *osdView;
-(uint8_t) readMemory:(uint16_t) addr; -(uint8_t) readMemory:(uint16_t) addr;
-(void) writeMemory:(uint16_t) addr value:(uint8_t)value; -(void) writeMemory:(uint16_t) addr value:(uint8_t)value;
-(void) performAtomicBlock: (void (^)())block; -(void) performAtomicBlock: (void (^)())block;
-(void) connectLinkCable:(NSMenuItem *)sender;
-(int)loadStateFile:(const char *)path noErrorOnNotFound:(bool)noErrorOnFileNotFound;
@end @end

View File

@ -28,6 +28,7 @@ enum model {
NSMutableAttributedString *pending_console_output; NSMutableAttributedString *pending_console_output;
NSRecursiveLock *console_output_lock; NSRecursiveLock *console_output_lock;
NSTimer *console_output_timer; NSTimer *console_output_timer;
NSTimer *hex_timer;
bool fullScreen; bool fullScreen;
bool in_sync_input; bool in_sync_input;
@ -47,7 +48,7 @@ enum model {
bool oamUpdating; bool oamUpdating;
NSMutableData *currentPrinterImageData; NSMutableData *currentPrinterImageData;
enum {GBAccessoryNone, GBAccessoryPrinter, GBAccessoryWorkboy} accessory; enum {GBAccessoryNone, GBAccessoryPrinter, GBAccessoryWorkboy, GBAccessoryLinkCable} accessory;
bool rom_warning_issued; bool rom_warning_issued;
@ -66,6 +67,12 @@ enum model {
size_t audioBufferNeeded; size_t audioBufferNeeded;
bool borderModeChanged; bool borderModeChanged;
/* Link cable*/
Document *master;
Document *slave;
signed linkOffset;
bool linkCableBit;
} }
@property GBAudioClient *audioClient; @property GBAudioClient *audioClient;
@ -81,6 +88,10 @@ enum model {
- (void) gotNewSample:(GB_sample_t *)sample; - (void) gotNewSample:(GB_sample_t *)sample;
- (void) rumbleChanged:(double)amp; - (void) rumbleChanged:(double)amp;
- (void) loadBootROM:(GB_boot_rom_t)type; - (void) loadBootROM:(GB_boot_rom_t)type;
- (void)linkCableBitStart:(bool)bit;
- (bool)linkCableBitEnd;
- (void)infraredStateChanged:(bool)state;
@end @end
static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type)
@ -160,6 +171,26 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[self rumbleChanged:amp]; [self rumbleChanged:amp];
} }
static void linkCableBitStart(GB_gameboy_t *gb, bool bit_to_send)
{
Document *self = (__bridge Document *)GB_get_user_data(gb);
[self linkCableBitStart:bit_to_send];
}
static bool linkCableBitEnd(GB_gameboy_t *gb)
{
Document *self = (__bridge Document *)GB_get_user_data(gb);
return [self linkCableBitEnd];
}
static void infraredStateChanged(GB_gameboy_t *gb, bool on)
{
Document *self = (__bridge Document *)GB_get_user_data(gb);
[self infraredStateChanged:on];
}
@implementation Document @implementation Document
{ {
GB_gameboy_t gb; GB_gameboy_t gb;
@ -206,8 +237,13 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
case MODEL_CGB: case MODEL_CGB:
return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]; return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"];
case MODEL_SGB: case MODEL_SGB: {
return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"]; GB_model_t model = (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"];
if (model == (GB_MODEL_SGB | GB_MODEL_PAL_BIT_OLD)) {
model = GB_MODEL_SGB_PAL;
}
return model;
}
case MODEL_AGB: case MODEL_AGB:
return GB_MODEL_AGB; return GB_MODEL_AGB;
@ -255,6 +291,8 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
GB_set_input_callback(&gb, (GB_input_callback_t) consoleInput); GB_set_input_callback(&gb, (GB_input_callback_t) consoleInput);
GB_set_async_input_callback(&gb, (GB_input_callback_t) asyncConsoleInput); GB_set_async_input_callback(&gb, (GB_input_callback_t) asyncConsoleInput);
GB_set_color_correction_mode(&gb, (GB_color_correction_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"]); GB_set_color_correction_mode(&gb, (GB_color_correction_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorCorrection"]);
GB_set_light_temperature(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBLightTemperature"]);
GB_set_interference_volume(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBInterferenceVolume"]);
GB_set_border_mode(&gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]); GB_set_border_mode(&gb, (GB_border_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBBorderMode"]);
[self updatePalette]; [self updatePalette];
GB_set_rgb_encode_callback(&gb, rgbEncode); GB_set_rgb_encode_callback(&gb, rgbEncode);
@ -262,8 +300,10 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
GB_set_camera_update_request_callback(&gb, cameraRequestUpdate); GB_set_camera_update_request_callback(&gb, cameraRequestUpdate);
GB_set_highpass_filter_mode(&gb, (GB_highpass_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"]); GB_set_highpass_filter_mode(&gb, (GB_highpass_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"]);
GB_set_rewind_length(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]); GB_set_rewind_length(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]);
GB_set_rtc_mode(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRTCMode"]);
GB_apu_set_sample_callback(&gb, audioCallback); GB_apu_set_sample_callback(&gb, audioCallback);
GB_set_rumble_callback(&gb, rumbleCallback); GB_set_rumble_callback(&gb, rumbleCallback);
GB_set_infrared_callback(&gb, infraredStateChanged);
[self updateRumbleMode]; [self updateRumbleMode];
} }
@ -274,10 +314,16 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
self.mainWindow.contentView.bounds.size.width < GB_get_screen_height(&gb)) { self.mainWindow.contentView.bounds.size.width < GB_get_screen_height(&gb)) {
[self.mainWindow zoom:nil]; [self.mainWindow zoom:nil];
} }
self.osdView.usesSGBScale = GB_get_screen_width(&gb) == 256;
} }
- (void) vblank - (void) vblank
{ {
if (_gbsVisualizer) {
dispatch_async(dispatch_get_main_queue(), ^{
[_gbsVisualizer setNeedsDisplay:YES];
});
}
[self.view flip]; [self.view flip];
if (borderModeChanged) { if (borderModeChanged) {
dispatch_sync(dispatch_get_main_queue(), ^{ dispatch_sync(dispatch_get_main_queue(), ^{
@ -293,17 +339,21 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
GB_set_pixels_output(&gb, self.view.pixels); GB_set_pixels_output(&gb, self.view.pixels);
if (self.vramWindow.isVisible) { if (self.vramWindow.isVisible) {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSFullScreenWindowMask) != 0; self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0;
[self reloadVRAMData: nil]; [self reloadVRAMData: nil];
}); });
} }
if (self.view.isRewinding) { if (self.view.isRewinding) {
rewind = true; rewind = true;
[self.osdView displayText:@"Rewinding..."];
} }
} }
- (void)gotNewSample:(GB_sample_t *)sample - (void)gotNewSample:(GB_sample_t *)sample
{ {
if (_gbsVisualizer) {
[_gbsVisualizer addSample:sample];
}
[audioLock lock]; [audioLock lock];
if (self.audioClient.isPlaying) { if (self.audioClient.isPlaying) {
if (audioBufferPosition == audioBufferSize) { if (audioBufferPosition == audioBufferSize) {
@ -321,6 +371,11 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
} }
audioBuffer = realloc(audioBuffer, sizeof(*sample) * audioBufferSize); audioBuffer = realloc(audioBuffer, sizeof(*sample) * audioBufferSize);
} }
double volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"];
if (volume != 1) {
sample->left *= volume;
sample->right *= volume;
}
audioBuffer[audioBufferPosition++] = *sample; audioBuffer[audioBufferPosition++] = *sample;
} }
if (audioBufferPosition == audioBufferNeeded) { if (audioBufferPosition == audioBufferNeeded) {
@ -335,9 +390,8 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[_view setRumble:amp]; [_view setRumble:amp];
} }
- (void) run - (void) preRun
{ {
running = true;
GB_set_pixels_output(&gb, self.view.pixels); GB_set_pixels_output(&gb, self.view.pixels);
GB_set_sample_rate(&gb, 96000); GB_set_sample_rate(&gb, 96000);
self.audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) { self.audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) {
@ -348,7 +402,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[audioLock wait]; [audioLock wait];
} }
if (stopping) { if (stopping || GB_debugger_is_stopped(&gb)) {
memset(buffer, 0, nFrames * sizeof(*buffer)); memset(buffer, 0, nFrames * sizeof(*buffer));
[audioLock unlock]; [audioLock unlock];
return; return;
@ -368,39 +422,78 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) { if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) {
[self.audioClient start]; [self.audioClient start];
} }
NSTimer *hex_timer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:YES]; hex_timer = [NSTimer timerWithTimeInterval:0.25 target:self selector:@selector(reloadMemoryView) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:hex_timer forMode:NSDefaultRunLoopMode]; [[NSRunLoop mainRunLoop] addTimer:hex_timer forMode:NSDefaultRunLoopMode];
/* Clear pending alarms, don't play alarms while playing */ /* Clear pending alarms, don't play alarms while playing */
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) {
NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter]; NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
for (NSUserNotification *notification in [center scheduledNotifications]) { for (NSUserNotification *notification in [center scheduledNotifications]) {
if ([notification.identifier isEqualToString:self.fileName]) { if ([notification.identifier isEqualToString:self.fileURL.path]) {
[center removeScheduledNotification:notification]; [center removeScheduledNotification:notification];
break; break;
} }
} }
for (NSUserNotification *notification in [center deliveredNotifications]) { for (NSUserNotification *notification in [center deliveredNotifications]) {
if ([notification.identifier isEqualToString:self.fileName]) { if ([notification.identifier isEqualToString:self.fileURL.path]) {
[center removeDeliveredNotification:notification]; [center removeDeliveredNotification:notification];
break; break;
} }
} }
} }
}
while (running) {
if (rewind) { static unsigned *multiplication_table_for_frequency(unsigned frequency)
rewind = false; {
GB_rewind_pop(&gb); unsigned *ret = malloc(sizeof(*ret) * 0x100);
if (!GB_rewind_pop(&gb)) { for (unsigned i = 0; i < 0x100; i++) {
rewind = self.view.isRewinding; ret[i] = i * frequency;
}
return ret;
}
- (void) run
{
assert(!master);
running = true;
[self preRun];
if (slave) {
[slave preRun];
unsigned *masterTable = multiplication_table_for_frequency(GB_get_clock_rate(&gb));
unsigned *slaveTable = multiplication_table_for_frequency(GB_get_clock_rate(&slave->gb));
while (running) {
if (linkOffset <= 0) {
linkOffset += slaveTable[GB_run(&gb)];
}
else {
linkOffset -= masterTable[GB_run(&slave->gb)];
} }
} }
else { free(masterTable);
GB_run(&gb); free(slaveTable);
[slave postRun];
}
else {
while (running) {
if (rewind) {
rewind = false;
GB_rewind_pop(&gb);
if (!GB_rewind_pop(&gb)) {
rewind = self.view.isRewinding;
}
}
else {
GB_run(&gb);
}
} }
} }
[self postRun];
stopping = false;
}
- (void)postRun
{
[hex_timer invalidate]; [hex_timer invalidate];
[audioLock lock]; [audioLock lock];
memset(audioBuffer, 0, (audioBufferSize - audioBufferPosition) * sizeof(*audioBuffer)); memset(audioBuffer, 0, (audioBufferSize - audioBufferPosition) * sizeof(*audioBuffer));
@ -410,39 +503,54 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[self.audioClient stop]; [self.audioClient stop];
self.audioClient = nil; self.audioClient = nil;
self.view.mouseHidingEnabled = NO; self.view.mouseHidingEnabled = NO;
GB_save_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]); GB_save_battery(&gb, [[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"sav"].path UTF8String]);
GB_save_cheats(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"cht"] UTF8String]); GB_save_cheats(&gb, [[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"cht"].path UTF8String]);
unsigned time_to_alarm = GB_time_to_alarm(&gb); unsigned time_to_alarm = GB_time_to_alarm(&gb);
if (time_to_alarm) { if (time_to_alarm) {
[NSUserNotificationCenter defaultUserNotificationCenter].delegate = (id)[NSApp delegate]; [NSUserNotificationCenter defaultUserNotificationCenter].delegate = (id)[NSApp delegate];
NSUserNotification *notification = [[NSUserNotification alloc] init]; NSUserNotification *notification = [[NSUserNotification alloc] init];
NSString *friendlyName = [[self.fileName lastPathComponent] stringByDeletingPathExtension]; NSString *friendlyName = [[self.fileURL lastPathComponent] stringByDeletingPathExtension];
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\([^)]+\\)|\\[[^\\]]+\\]" options:0 error:nil]; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\([^)]+\\)|\\[[^\\]]+\\]" options:0 error:nil];
friendlyName = [regex stringByReplacingMatchesInString:friendlyName options:0 range:NSMakeRange(0, [friendlyName length]) withTemplate:@""]; friendlyName = [regex stringByReplacingMatchesInString:friendlyName options:0 range:NSMakeRange(0, [friendlyName length]) withTemplate:@""];
friendlyName = [friendlyName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; friendlyName = [friendlyName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
notification.title = [NSString stringWithFormat:@"%@ Played an Alarm", friendlyName]; notification.title = [NSString stringWithFormat:@"%@ Played an Alarm", friendlyName];
notification.informativeText = [NSString stringWithFormat:@"%@ requested your attention by playing a scheduled alarm", friendlyName]; notification.informativeText = [NSString stringWithFormat:@"%@ requested your attention by playing a scheduled alarm", friendlyName];
notification.identifier = self.fileName; notification.identifier = self.fileURL.path;
notification.deliveryDate = [NSDate dateWithTimeIntervalSinceNow:time_to_alarm]; notification.deliveryDate = [NSDate dateWithTimeIntervalSinceNow:time_to_alarm];
notification.soundName = NSUserNotificationDefaultSoundName; notification.soundName = NSUserNotificationDefaultSoundName;
[[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification]; [[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification];
[[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBNotificationsUsed"]; [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBNotificationsUsed"];
} }
[_view setRumble:0]; [_view setRumble:0];
stopping = false;
} }
- (void) start - (void) start
{ {
self.gbsPlayPauseButton.state = true;
self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0;
if (master) {
[master start];
return;
}
if (running) return; if (running) return;
self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSFullScreenWindowMask) != 0;
[[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start]; [[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start];
} }
- (void) stop - (void) stop
{ {
self.gbsPlayPauseButton.state = false;
if (master) {
if (!master->running) return;
GB_debugger_set_disabled(&gb, true);
if (GB_debugger_is_stopped(&gb)) {
[self interruptDebugInputRead];
}
[master stop];
GB_debugger_set_disabled(&gb, false);
return;
}
if (!running) return; if (!running) return;
GB_debugger_set_disabled(&gb, true); GB_debugger_set_disabled(&gb, true);
if (GB_debugger_is_stopped(&gb)) { if (GB_debugger_is_stopped(&gb)) {
@ -515,10 +623,18 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[(GBMemoryByteArray *)(hex_controller.byteArray) setSelectedBank:0]; [(GBMemoryByteArray *)(hex_controller.byteArray) setSelectedBank:0];
[self hexUpdateBank:self.memoryBankInput ignoreErrors:true]; [self hexUpdateBank:self.memoryBankInput ignoreErrors:true];
} }
char title[17];
GB_get_rom_title(&gb, title);
[self.osdView displayText:[NSString stringWithFormat:@"SameBoy v" GB_VERSION "\n%s\n%08X", title, GB_get_rom_crc32(&gb)]];
} }
- (IBAction)togglePause:(id)sender - (IBAction)togglePause:(id)sender
{ {
if (master) {
[master togglePause:sender];
return;
}
if (running) { if (running) {
[self stop]; [self stop];
} }
@ -575,10 +691,14 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
window_frame.size.height); window_frame.size.height);
[self.mainWindow setFrame:window_frame display:YES]; [self.mainWindow setFrame:window_frame display:YES];
self.vramStatusLabel.cell.backgroundStyle = NSBackgroundStyleRaised; self.vramStatusLabel.cell.backgroundStyle = NSBackgroundStyleRaised;
NSUInteger height_diff = self.vramWindow.frame.size.height - self.vramWindow.contentView.frame.size.height;
CGRect vram_window_rect = self.vramWindow.frame;
vram_window_rect.size.height = 384 + height_diff + 48;
[self.vramWindow setFrame:vram_window_rect display:YES animate:NO];
self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console %@", [self.fileURL.path lastPathComponent]];
self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console %@", [[self.fileURL path] lastPathComponent]];
self.debuggerSplitView.dividerColor = [NSColor clearColor]; self.debuggerSplitView.dividerColor = [NSColor clearColor];
if (@available(macOS 11.0, *)) { if (@available(macOS 11.0, *)) {
self.memoryWindow.toolbarStyle = NSWindowToolbarStyleExpanded; self.memoryWindow.toolbarStyle = NSWindowToolbarStyleExpanded;
@ -602,6 +722,16 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
name:@"GBColorCorrectionChanged" name:@"GBColorCorrectionChanged"
object:nil]; object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateLightTemperature)
name:@"GBLightTemperatureChanged"
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateInterferenceVolume)
name:@"GBInterferenceVolumeChanged"
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self [[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateFrameBlendingMode) selector:@selector(updateFrameBlendingMode)
name:@"GBFrameBlendingModeChanged" name:@"GBFrameBlendingModeChanged"
@ -627,6 +757,12 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
name:@"GBRewindLengthChanged" name:@"GBRewindLengthChanged"
object:nil]; object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateRTCMode)
name:@"GBRTCModeChanged"
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self [[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(dmgModelChanged) selector:@selector(dmgModelChanged)
name:@"GBDMGModelChanged" name:@"GBDMGModelChanged"
@ -654,10 +790,17 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[self initCommon]; [self initCommon];
self.view.gb = &gb; self.view.gb = &gb;
self.view.osdView = _osdView;
[self.view screenSizeChanged]; [self.view screenSizeChanged];
[self loadROM]; if ([self loadROM]) {
[self reset:nil]; _mainWindow.alphaValue = 0; // Hack hack ugly hack
dispatch_async(dispatch_get_main_queue(), ^{
[self close];
});
}
else {
[self reset:nil];
}
} }
- (void) initMemoryView - (void) initMemoryView
@ -723,36 +866,131 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
return YES; return YES;
} }
- (void) loadROM - (IBAction)changeGBSTrack:(id)sender
{ {
NSString *rom_warnings = [self captureOutputForBlock:^{ if (!running) {
GB_debugger_clear_symbols(&gb); [self start];
if ([[self.fileType pathExtension] isEqualToString:@"isx"]) { }
GB_load_isx(&gb, [self.fileName UTF8String]); [self performAtomicBlock:^{
GB_load_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"ram"] UTF8String]); GB_gbs_switch_track(&gb, self.gbsTracks.indexOfSelectedItem);
}];
}
- (IBAction)gbsNextPrevPushed:(id)sender
{
if (self.gbsNextPrevButton.selectedSegment == 0) {
// Previous
if (self.gbsTracks.indexOfSelectedItem == 0) {
[self.gbsTracks selectItemAtIndex:self.gbsTracks.numberOfItems - 1];
} }
else { else {
GB_load_rom(&gb, [self.fileName UTF8String]); [self.gbsTracks selectItemAtIndex:self.gbsTracks.indexOfSelectedItem - 1];
} }
GB_load_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]); }
GB_load_cheats(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"cht"] UTF8String]); else {
// Next
if (self.gbsTracks.indexOfSelectedItem == self.gbsTracks.numberOfItems - 1) {
[self.gbsTracks selectItemAtIndex: 0];
}
else {
[self.gbsTracks selectItemAtIndex:self.gbsTracks.indexOfSelectedItem + 1];
}
}
[self changeGBSTrack:sender];
}
- (void)prepareGBSInterface: (GB_gbs_info_t *)info
{
GB_set_rendering_disabled(&gb, true);
_view = nil;
for (NSView *view in _mainWindow.contentView.subviews) {
[view removeFromSuperview];
}
[[NSBundle mainBundle] loadNibNamed:@"GBS" owner:self topLevelObjects:nil];
[_mainWindow setContentSize:self.gbsPlayerView.bounds.size];
_mainWindow.styleMask &= ~NSWindowStyleMaskResizable;
dispatch_async(dispatch_get_main_queue(), ^{ // Cocoa is weird, no clue why it's needed
[_mainWindow standardWindowButton:NSWindowZoomButton].enabled = false;
});
[_mainWindow.contentView addSubview:self.gbsPlayerView];
_mainWindow.movableByWindowBackground = true;
[_mainWindow setContentBorderThickness:24 forEdge:NSRectEdgeMinY];
self.gbsTitle.stringValue = [NSString stringWithCString:info->title encoding:NSISOLatin1StringEncoding] ?: @"GBS Player";
self.gbsAuthor.stringValue = [NSString stringWithCString:info->author encoding:NSISOLatin1StringEncoding] ?: @"Unknown Composer";
NSString *copyright = [NSString stringWithCString:info->copyright encoding:NSISOLatin1StringEncoding];
if (copyright) {
copyright = [@"©" stringByAppendingString:copyright];
}
self.gbsCopyright.stringValue = copyright ?: @"Missing copyright information";
for (unsigned i = 0; i < info->track_count; i++) {
[self.gbsTracks addItemWithTitle:[NSString stringWithFormat:@"Track %u", i + 1]];
}
[self.gbsTracks selectItemAtIndex:info->first_track];
self.gbsPlayPauseButton.image.template = true;
self.gbsPlayPauseButton.alternateImage.template = true;
self.gbsRewindButton.image.template = true;
for (unsigned i = 0; i < 2; i++) {
[self.gbsNextPrevButton imageForSegment:i].template = true;
}
if (!self.audioClient.isPlaying) {
[self.audioClient start];
}
if (@available(macOS 10.10, *)) {
_mainWindow.titlebarAppearsTransparent = true;
}
}
- (int) loadROM
{
__block int ret = 0;
NSString *rom_warnings = [self captureOutputForBlock:^{
GB_debugger_clear_symbols(&gb);
if ([[[self.fileType pathExtension] lowercaseString] isEqualToString:@"isx"]) {
ret = GB_load_isx(&gb, self.fileURL.path.UTF8String);
GB_load_battery(&gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"ram"].path.UTF8String);
}
else if ([[[self.fileType pathExtension] lowercaseString] isEqualToString:@"gbs"]) {
__block GB_gbs_info_t info;
ret = GB_load_gbs(&gb, self.fileURL.path.UTF8String, &info);
[self prepareGBSInterface:&info];
}
else {
ret = GB_load_rom(&gb, [self.fileURL.path UTF8String]);
}
GB_load_battery(&gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"sav"].path.UTF8String);
GB_load_cheats(&gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"cht"].path.UTF8String);
[self.cheatWindowController cheatsUpdated]; [self.cheatWindowController cheatsUpdated];
GB_debugger_load_symbol_file(&gb, [[[NSBundle mainBundle] pathForResource:@"registers" ofType:@"sym"] UTF8String]); GB_debugger_load_symbol_file(&gb, [[[NSBundle mainBundle] pathForResource:@"registers" ofType:@"sym"] UTF8String]);
GB_debugger_load_symbol_file(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sym"] UTF8String]); GB_debugger_load_symbol_file(&gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"sym"].path.UTF8String);
}]; }];
if (rom_warnings && !rom_warning_issued) { if (ret) {
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:rom_warnings?: @"Could not load ROM"];
[alert setAlertStyle:NSAlertStyleCritical];
[alert runModal];
}
else if (rom_warnings && !rom_warning_issued) {
rom_warning_issued = true; rom_warning_issued = true;
[GBWarningPopover popoverWithContents:rom_warnings onWindow:self.mainWindow]; [GBWarningPopover popoverWithContents:rom_warnings onWindow:self.mainWindow];
} }
return ret;
} }
- (void)close - (void)close
{ {
[[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.width forKey:@"LastWindowWidth"]; [self disconnectLinkCable];
[[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.height forKey:@"LastWindowHeight"]; if (!self.gbsPlayerView) {
[[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.width forKey:@"LastWindowWidth"];
[[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.height forKey:@"LastWindowHeight"];
}
[self stop]; [self stop];
[self.consoleWindow close]; [self.consoleWindow close];
[self.memoryWindow close];
[self.vramWindow close];
[self.printerFeedWindow close];
[self.cheatsWindow close];
[super close]; [super close];
} }
@ -760,9 +998,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
{ {
[self log:"^C\n"]; [self log:"^C\n"];
GB_debugger_break(&gb); GB_debugger_break(&gb);
if (!running) { [self start];
[self start];
}
[self.consoleWindow makeKeyAndOrderFront:nil]; [self.consoleWindow makeKeyAndOrderFront:nil];
[self.consoleInput becomeFirstResponder]; [self.consoleInput becomeFirstResponder];
} }
@ -774,6 +1010,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
} }
else { else {
[self.audioClient start]; [self.audioClient start];
if ([[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"] == 0) {
[GBWarningPopover popoverWithContents:@"Warning: Volume is set to to zero in the preferences panel" onWindow:self.mainWindow];
}
} }
[[NSUserDefaults standardUserDefaults] setBool:!self.audioClient.isPlaying forKey:@"Mute"]; [[NSUserDefaults standardUserDefaults] setBool:!self.audioClient.isPlaying forKey:@"Mute"];
} }
@ -781,10 +1020,13 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)anItem - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)anItem
{ {
if ([anItem action] == @selector(mute:)) { if ([anItem action] == @selector(mute:)) {
[(NSMenuItem*)anItem setState:!self.audioClient.isPlaying]; [(NSMenuItem *)anItem setState:!self.audioClient.isPlaying];
} }
else if ([anItem action] == @selector(togglePause:)) { else if ([anItem action] == @selector(togglePause:)) {
[(NSMenuItem*)anItem setState:(!running) || (GB_debugger_is_stopped(&gb))]; if (master) {
[(NSMenuItem *)anItem setState:(!master->running) || (GB_debugger_is_stopped(&gb)) || (GB_debugger_is_stopped(&gb))];
}
[(NSMenuItem *)anItem setState:(!running) || (GB_debugger_is_stopped(&gb))];
return !GB_debugger_is_stopped(&gb); return !GB_debugger_is_stopped(&gb);
} }
else if ([anItem action] == @selector(reset:) && anItem.tag != MODEL_NONE) { else if ([anItem action] == @selector(reset:) && anItem.tag != MODEL_NONE) {
@ -804,6 +1046,10 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
else if ([anItem action] == @selector(connectWorkboy:)) { else if ([anItem action] == @selector(connectWorkboy:)) {
[(NSMenuItem*)anItem setState:accessory == GBAccessoryWorkboy]; [(NSMenuItem*)anItem setState:accessory == GBAccessoryWorkboy];
} }
else if ([anItem action] == @selector(connectLinkCable:)) {
[(NSMenuItem*)anItem setState:[(NSMenuItem *)anItem representedObject] == master ||
[(NSMenuItem *)anItem representedObject] == slave];
}
else if ([anItem action] == @selector(toggleCheats:)) { else if ([anItem action] == @selector(toggleCheats:)) {
[(NSMenuItem*)anItem setState:GB_cheats_enabled(&gb)]; [(NSMenuItem*)anItem setState:GB_cheats_enabled(&gb)];
} }
@ -1000,6 +1246,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (char *) getDebuggerInput - (char *) getDebuggerInput
{ {
[audioLock lock];
[audioLock signal];
[audioLock unlock];
[self updateSideView]; [self updateSideView];
[self log:">"]; [self log:">"];
in_sync_input = true; in_sync_input = true;
@ -1039,28 +1288,47 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
{ {
bool __block success = false; bool __block success = false;
[self performAtomicBlock:^{ [self performAtomicBlock:^{
success = GB_save_state(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:[NSString stringWithFormat:@"s%ld", (long)[sender tag] ]] UTF8String]) == 0; success = GB_save_state(&gb, [[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"s%ld", (long)[sender tag] ]].path.UTF8String) == 0;
}]; }];
if (!success) { if (!success) {
[GBWarningPopover popoverWithContents:@"Failed to write save state." onWindow:self.mainWindow]; [GBWarningPopover popoverWithContents:@"Failed to write save state." onWindow:self.mainWindow];
NSBeep(); NSBeep();
} }
else {
[self.osdView displayText:@"State saved"];
}
}
- (int)loadStateFile:(const char *)path noErrorOnNotFound:(bool)noErrorOnFileNotFound;
{
int __block result = false;
NSString *error =
[self captureOutputForBlock:^{
result = GB_load_state(&gb, path);
}];
if (result == ENOENT && noErrorOnFileNotFound) {
return ENOENT;
}
if (result) {
NSBeep();
}
else {
[self.osdView displayText:@"State loaded"];
}
if (error) {
[GBWarningPopover popoverWithContents:error onWindow:self.mainWindow];
}
return result;
} }
- (IBAction)loadState:(id)sender - (IBAction)loadState:(id)sender
{ {
bool __block success = false; int ret = [self loadStateFile:[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"s%ld", (long)[sender tag]]].path.UTF8String noErrorOnNotFound:true];
NSString *error = if (ret == ENOENT) {
[self captureOutputForBlock:^{ [self loadStateFile:[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"sn%ld", (long)[sender tag]]].path.UTF8String noErrorOnNotFound:false];
success = GB_load_state(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:[NSString stringWithFormat:@"s%ld", (long)[sender tag] ]] UTF8String]) == 0;
}];
if (!success) {
if (error) {
[GBWarningPopover popoverWithContents:error onWindow:self.mainWindow];
}
NSBeep();
} }
} }
@ -1090,6 +1358,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
{ {
while (!GB_is_inited(&gb)); while (!GB_is_inited(&gb));
bool was_running = running && !GB_debugger_is_stopped(&gb); bool was_running = running && !GB_debugger_is_stopped(&gb);
if (master) {
was_running |= master->running;
}
if (was_running) { if (was_running) {
[self stop]; [self stop];
} }
@ -1347,7 +1618,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (void)cameraRequestUpdate - (void)cameraRequestUpdate
{ {
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@try { @try {
if (!cameraSession) { if (!cameraSession) {
if (@available(macOS 10.14, *)) { if (@available(macOS 10.14, *)) {
@ -1584,7 +1855,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
switch (columnIndex) { switch (columnIndex) {
case 0: case 0:
return [Document imageFromData:[NSData dataWithBytesNoCopy:oamInfo[row].image return [Document imageFromData:[NSData dataWithBytesNoCopy:oamInfo[row].image
length:64 * 4 length:64 * 4 * 2
freeWhenDone:NO] freeWhenDone:NO]
width:8 width:8
height:oamHeight height:oamHeight
@ -1681,14 +1952,14 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
NSSavePanel * savePanel = [NSSavePanel savePanel]; NSSavePanel * savePanel = [NSSavePanel savePanel];
[savePanel setAllowedFileTypes:@[@"png"]]; [savePanel setAllowedFileTypes:@[@"png"]];
[savePanel beginSheetModalForWindow:self.printerFeedWindow completionHandler:^(NSInteger result) { [savePanel beginSheetModalForWindow:self.printerFeedWindow completionHandler:^(NSInteger result) {
if (result == NSFileHandlingPanelOKButton) { if (result == NSModalResponseOK) {
[savePanel orderOut:self]; [savePanel orderOut:self];
CGImageRef cgRef = [self.feedImageView.image CGImageForProposedRect:NULL CGImageRef cgRef = [self.feedImageView.image CGImageForProposedRect:NULL
context:nil context:nil
hints:nil]; hints:nil];
NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef]; NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef];
[imageRep setSize:(NSSize){160, self.feedImageView.image.size.height / 2}]; [imageRep setSize:(NSSize){160, self.feedImageView.image.size.height / 2}];
NSData *data = [imageRep representationUsingType:NSPNGFileType properties:@{}]; NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
[data writeToURL:savePanel.URL atomically:NO]; [data writeToURL:savePanel.URL atomically:NO];
[self.printerFeedWindow setIsVisible:NO]; [self.printerFeedWindow setIsVisible:NO];
} }
@ -1700,6 +1971,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (IBAction)disconnectAllAccessories:(id)sender - (IBAction)disconnectAllAccessories:(id)sender
{ {
[self disconnectLinkCable];
[self performAtomicBlock:^{ [self performAtomicBlock:^{
accessory = GBAccessoryNone; accessory = GBAccessoryNone;
GB_disconnect_serial(&gb); GB_disconnect_serial(&gb);
@ -1708,6 +1980,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (IBAction)connectPrinter:(id)sender - (IBAction)connectPrinter:(id)sender
{ {
[self disconnectLinkCable];
[self performAtomicBlock:^{ [self performAtomicBlock:^{
accessory = GBAccessoryPrinter; accessory = GBAccessoryPrinter;
GB_connect_printer(&gb, printImage); GB_connect_printer(&gb, printImage);
@ -1716,6 +1989,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (IBAction)connectWorkboy:(id)sender - (IBAction)connectWorkboy:(id)sender
{ {
[self disconnectLinkCable];
[self performAtomicBlock:^{ [self performAtomicBlock:^{
accessory = GBAccessoryWorkboy; accessory = GBAccessoryWorkboy;
GB_connect_workboy(&gb, setWorkboyTime, getWorkboyTime); GB_connect_workboy(&gb, setWorkboyTime, getWorkboyTime);
@ -1736,6 +2010,20 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
} }
} }
- (void) updateLightTemperature
{
if (GB_is_inited(&gb)) {
GB_set_light_temperature(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBLightTemperature"]);
}
}
- (void) updateInterferenceVolume
{
if (GB_is_inited(&gb)) {
GB_set_interference_volume(&gb, [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBInterferenceVolume"]);
}
}
- (void) updateFrameBlendingMode - (void) updateFrameBlendingMode
{ {
self.view.frameBlendingMode = (GB_frame_blending_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"]; self.view.frameBlendingMode = (GB_frame_blending_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBFrameBlendingMode"];
@ -1750,6 +2038,13 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
}]; }];
} }
- (void) updateRTCMode
{
if (GB_is_inited(&gb)) {
GB_set_rtc_mode(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRTCMode"]);
}
}
- (void)dmgModelChanged - (void)dmgModelChanged
{ {
modelsChanging = true; modelsChanging = true;
@ -1832,4 +2127,83 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
{ {
GB_set_cheats_enabled(&gb, !GB_cheats_enabled(&gb)); GB_set_cheats_enabled(&gb, !GB_cheats_enabled(&gb));
} }
- (void)disconnectLinkCable
{
bool wasRunning = self->running;
Document *partner = master ?: slave;
if (partner) {
[self stop];
partner->master = nil;
partner->slave = nil;
master = nil;
slave = nil;
if (wasRunning) {
[partner start];
[self start];
}
GB_set_turbo_mode(&gb, false, false);
GB_set_turbo_mode(&partner->gb, false, false);
partner->accessory = GBAccessoryNone;
accessory = GBAccessoryNone;
}
}
- (void)connectLinkCable:(NSMenuItem *)sender
{
[self disconnectAllAccessories:sender];
Document *partner = [sender representedObject];
[partner disconnectAllAccessories:sender];
bool wasRunning = self->running;
[self stop];
[partner stop];
GB_set_turbo_mode(&partner->gb, true, true);
slave = partner;
partner->master = self;
linkOffset = 0;
partner->accessory = GBAccessoryLinkCable;
accessory = GBAccessoryLinkCable;
GB_set_serial_transfer_bit_start_callback(&gb, linkCableBitStart);
GB_set_serial_transfer_bit_start_callback(&partner->gb, linkCableBitStart);
GB_set_serial_transfer_bit_end_callback(&gb, linkCableBitEnd);
GB_set_serial_transfer_bit_end_callback(&partner->gb, linkCableBitEnd);
if (wasRunning) {
[self start];
}
}
- (void)linkCableBitStart:(bool)bit
{
linkCableBit = bit;
}
-(bool)linkCableBitEnd
{
bool ret = GB_serial_get_data_bit(&self.partner->gb);
GB_serial_set_data_bit(&self.partner->gb, linkCableBit);
return ret;
}
- (void)infraredStateChanged:(bool)state
{
if (self.partner) {
GB_set_infrared_input(&self.partner->gb, state);
}
}
-(Document *)partner
{
return slave ?: master;
}
- (bool)isSlave
{
return master;
}
- (GB_gameboy_t *)gb
{
return &gb;
}
@end @end

View File

@ -25,6 +25,7 @@
<outlet property="memoryBankItem" destination="bWC-FW-IYP" id="Lf2-dh-z32"/> <outlet property="memoryBankItem" destination="bWC-FW-IYP" id="Lf2-dh-z32"/>
<outlet property="memoryView" destination="8hr-8o-3rN" id="fF0-rh-8ND"/> <outlet property="memoryView" destination="8hr-8o-3rN" id="fF0-rh-8ND"/>
<outlet property="memoryWindow" destination="mRm-dL-mCj" id="VPR-lu-vtI"/> <outlet property="memoryWindow" destination="mRm-dL-mCj" id="VPR-lu-vtI"/>
<outlet property="osdView" destination="MX4-l2-7NE" id="Am7-fq-uvu"/>
<outlet property="paletteTableView" destination="gfC-d3-dmq" id="fTC-eL-Qg3"/> <outlet property="paletteTableView" destination="gfC-d3-dmq" id="fTC-eL-Qg3"/>
<outlet property="printerFeedWindow" destination="NdE-0B-WCf" id="yVK-cS-NOJ"/> <outlet property="printerFeedWindow" destination="NdE-0B-WCf" id="yVK-cS-NOJ"/>
<outlet property="spritesTableView" destination="TOc-XJ-w9w" id="O4R-4Z-9hU"/> <outlet property="spritesTableView" destination="TOc-XJ-w9w" id="O4R-4Z-9hU"/>
@ -59,9 +60,16 @@
<view fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uqf-pe-VAF" customClass="GBView"> <view fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uqf-pe-VAF" customClass="GBView">
<rect key="frame" x="0.0" y="0.0" width="160" height="144"/> <rect key="frame" x="0.0" y="0.0" width="160" height="144"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<connections>
<outlet property="document" destination="-2" id="Fvh-rD-z4r"/>
</connections>
</view> </view>
</subviews> </subviews>
</customView> </customView>
<customView fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="MX4-l2-7NE" customClass="GBOSDView">
<rect key="frame" x="0.0" y="0.0" width="160" height="144"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
</customView>
</subviews> </subviews>
</view> </view>
<connections> <connections>
@ -115,7 +123,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" copiesOnScroll="NO" id="EQe-Ad-L7S"> <clipView key="contentView" ambiguous="YES" drawsBackground="NO" copiesOnScroll="NO" id="EQe-Ad-L7S">
<rect key="frame" x="0.0" y="0.0" width="591" height="376"/> <rect key="frame" x="0.0" y="0.0" width="591" height="376"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<textView ambiguous="YES" editable="NO" drawsBackground="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" baseWritingDirection="leftToRight" findStyle="bar" allowsNonContiguousLayout="YES" id="doS-dM-hnl"> <textView ambiguous="YES" editable="NO" drawsBackground="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" baseWritingDirection="leftToRight" findStyle="bar" allowsNonContiguousLayout="YES" id="doS-dM-hnl">
<rect key="frame" x="0.0" y="0.0" width="591" height="376"/> <rect key="frame" x="0.0" y="0.0" width="591" height="376"/>
@ -152,7 +160,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" copiesOnScroll="NO" id="YHx-TM-zIC"> <clipView key="contentView" ambiguous="YES" drawsBackground="NO" copiesOnScroll="NO" id="YHx-TM-zIC">
<rect key="frame" x="0.0" y="0.0" width="329" height="47"/> <rect key="frame" x="0.0" y="0.0" width="329" height="47"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<textView ambiguous="YES" drawsBackground="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" allowsCharacterPickerTouchBarItem="NO" allowsUndo="YES" allowsNonContiguousLayout="YES" textCompletion="NO" spellingCorrection="YES" id="w0g-eK-jM4"> <textView ambiguous="YES" drawsBackground="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" allowsCharacterPickerTouchBarItem="NO" allowsUndo="YES" allowsNonContiguousLayout="YES" textCompletion="NO" spellingCorrection="YES" id="w0g-eK-jM4">
<rect key="frame" x="0.0" y="0.0" width="329" height="47"/> <rect key="frame" x="0.0" y="0.0" width="329" height="47"/>
@ -186,7 +194,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<clipView key="contentView" ambiguous="YES" drawsBackground="NO" copiesOnScroll="NO" id="Cs9-3x-ATg"> <clipView key="contentView" ambiguous="YES" drawsBackground="NO" copiesOnScroll="NO" id="Cs9-3x-ATg">
<rect key="frame" x="0.0" y="0.0" width="329" height="328"/> <rect key="frame" x="0.0" y="0.0" width="329" height="328"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<textView ambiguous="YES" editable="NO" drawsBackground="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" baseWritingDirection="leftToRight" findStyle="bar" allowsNonContiguousLayout="YES" spellingCorrection="YES" id="JgV-7E-iwp"> <textView ambiguous="YES" editable="NO" drawsBackground="NO" importsGraphics="NO" richText="NO" verticallyResizable="YES" baseWritingDirection="leftToRight" findStyle="bar" allowsNonContiguousLayout="YES" spellingCorrection="YES" id="JgV-7E-iwp">
<rect key="frame" x="0.0" y="0.0" width="329" height="328"/> <rect key="frame" x="0.0" y="0.0" width="329" height="328"/>

View File

@ -2,9 +2,9 @@
#import <Core/gb.h> #import <Core/gb.h>
@interface GBAudioClient : NSObject @interface GBAudioClient : NSObject
@property (strong) void (^renderBlock)(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer); @property (nonatomic, strong) void (^renderBlock)(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer);
@property (readonly) UInt32 rate; @property (nonatomic, readonly) UInt32 rate;
@property (readonly, getter=isPlaying) bool playing; @property (nonatomic, readonly, getter=isPlaying) bool playing;
-(void) start; -(void) start;
-(void) stop; -(void) stop;
-(id) initWithRendererBlock:(void (^)(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer)) block -(id) initWithRendererBlock:(void (^)(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer)) block

View File

@ -1,5 +1,5 @@
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
@interface GBCheatTextFieldCell : NSTextFieldCell @interface GBCheatTextFieldCell : NSTextFieldCell
@property bool usesAddressFormat; @property (nonatomic) bool usesAddressFormat;
@end @end

View File

@ -3,15 +3,14 @@
#import "Document.h" #import "Document.h"
@interface GBCheatWindowController : NSObject <NSTableViewDelegate, NSTableViewDataSource, NSTextFieldDelegate> @interface GBCheatWindowController : NSObject <NSTableViewDelegate, NSTableViewDataSource, NSTextFieldDelegate>
@property (weak) IBOutlet NSTableView *cheatsTable; @property (nonatomic, weak) IBOutlet NSTableView *cheatsTable;
@property (weak) IBOutlet NSTextField *addressField; @property (nonatomic, weak) IBOutlet NSTextField *addressField;
@property (weak) IBOutlet NSTextField *valueField; @property (nonatomic, weak) IBOutlet NSTextField *valueField;
@property (weak) IBOutlet NSTextField *oldValueField; @property (nonatomic, weak) IBOutlet NSTextField *oldValueField;
@property (weak) IBOutlet NSButton *oldValueCheckbox; @property (nonatomic, weak) IBOutlet NSButton *oldValueCheckbox;
@property (weak) IBOutlet NSTextField *descriptionField; @property (nonatomic, weak) IBOutlet NSTextField *descriptionField;
@property (weak) IBOutlet NSTextField *importCodeField; @property (nonatomic, weak) IBOutlet NSTextField *importCodeField;
@property (weak) IBOutlet NSTextField *importDescriptionField; @property (nonatomic, weak) IBOutlet NSTextField *importDescriptionField;
@property (weak) IBOutlet Document *document; @property (nonatomic, weak) IBOutlet Document *document;
- (void)cheatsUpdated; - (void)cheatsUpdated;
@end @end

View File

@ -3,17 +3,17 @@
@protocol GBImageViewDelegate; @protocol GBImageViewDelegate;
@interface GBImageViewGridConfiguration : NSObject @interface GBImageViewGridConfiguration : NSObject
@property NSColor *color; @property (nonatomic, strong) NSColor *color;
@property NSUInteger size; @property (nonatomic) NSUInteger size;
- (instancetype) initWithColor: (NSColor *) color size: (NSUInteger) size; - (instancetype) initWithColor: (NSColor *) color size: (NSUInteger) size;
@end @end
@interface GBImageView : NSImageView @interface GBImageView : NSImageView
@property (nonatomic) NSArray *horizontalGrids; @property (nonatomic, strong) NSArray<GBImageViewGridConfiguration *> *horizontalGrids;
@property (nonatomic) NSArray *verticalGrids; @property (nonatomic, strong) NSArray<GBImageViewGridConfiguration *> *verticalGrids;
@property (nonatomic) bool displayScrollRect; @property (nonatomic) bool displayScrollRect;
@property NSRect scrollRect; @property NSRect scrollRect;
@property (weak) IBOutlet id<GBImageViewDelegate> delegate; @property (nonatomic, weak) IBOutlet id<GBImageViewDelegate> delegate;
@end @end
@protocol GBImageViewDelegate <NSObject> @protocol GBImageViewDelegate <NSObject>

View File

@ -12,6 +12,6 @@ typedef enum {
@interface GBMemoryByteArray : HFByteArray @interface GBMemoryByteArray : HFByteArray
- (instancetype) initWithDocument:(Document *)document; - (instancetype) initWithDocument:(Document *)document;
@property uint16_t selectedBank; @property (nonatomic) uint16_t selectedBank;
@property GB_memory_mode_t mode; @property (nonatomic) GB_memory_mode_t mode;
@end @end

View File

@ -0,0 +1,6 @@
#import <Cocoa/Cocoa.h>
@interface GBOSDView : NSView
@property bool usesSGBScale;
- (void)displayText:(NSString *)text;
@end

104
bsnes/gb/Cocoa/GBOSDView.m Normal file
View File

@ -0,0 +1,104 @@
#import "GBOSDView.h"
@implementation GBOSDView
{
bool _usesSGBScale;
NSString *_text;
double _animation;
NSTimer *_timer;
}
- (void)setUsesSGBScale:(bool)usesSGBScale
{
_usesSGBScale = usesSGBScale;
[self setNeedsDisplay:true];
}
- (bool)usesSGBScale
{
return _usesSGBScale;
}
- (void)displayText:(NSString *)text
{
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBOSDEnabled"]) return;
dispatch_async(dispatch_get_main_queue(), ^{
if (![_text isEqualToString:text]) {
[self setNeedsDisplay:true];
}
_text = text;
self.alphaValue = 1.0;
_animation = 2.5;
// Longer strings should appear longer
if ([_text rangeOfString:@"\n"].location != NSNotFound) {
_animation += 4;
}
[_timer invalidate];
self.hidden = false;
_timer = [NSTimer scheduledTimerWithTimeInterval:0.025 target:self selector:@selector(animate) userInfo:nil repeats:true];
});
}
- (void)animate
{
_animation -= 0.1;
if (_animation < 1.0) {
self.alphaValue = _animation;
};
if (_animation == 0) {
self.hidden = true;
[_timer invalidate];
_text = nil;
}
}
- (void)drawRect:(NSRect)dirtyRect
{
[super drawRect:dirtyRect];
if (!_text.length) return;
double fontSize = 8;
NSSize size = self.frame.size;
if (_usesSGBScale) {
fontSize *= MIN(size.width / 256, size.height / 224);
}
else {
fontSize *= MIN(size.width / 160, size.height / 144);
}
NSFont *font = [NSFont boldSystemFontOfSize:fontSize];
/* The built in stroke attribute uses an inside stroke, which is typographically terrible.
We'll use a naïve manual stroke instead which looks better. */
NSDictionary *attributes = @{
NSFontAttributeName: font,
NSForegroundColorAttributeName: [NSColor blackColor],
};
NSAttributedString *text = [[NSAttributedString alloc] initWithString:_text attributes:attributes];
[text drawAtPoint:NSMakePoint(fontSize + 1, fontSize + 0)];
[text drawAtPoint:NSMakePoint(fontSize - 1, fontSize + 0)];
[text drawAtPoint:NSMakePoint(fontSize + 0, fontSize + 1)];
[text drawAtPoint:NSMakePoint(fontSize + 0, fontSize - 1)];
// The uses of sqrt(2)/2, which is more correct, results in severe ugly-looking rounding errors
if (self.window.screen.backingScaleFactor > 1) {
[text drawAtPoint:NSMakePoint(fontSize + 0.5, fontSize + 0.5)];
[text drawAtPoint:NSMakePoint(fontSize - 0.5, fontSize + 0.5)];
[text drawAtPoint:NSMakePoint(fontSize - 0.5, fontSize - 0.5)];
[text drawAtPoint:NSMakePoint(fontSize + 0.5, fontSize - 0.5)];
}
attributes = @{
NSFontAttributeName: font,
NSForegroundColorAttributeName: [NSColor whiteColor],
};
text = [[NSAttributedString alloc] initWithString:_text attributes:attributes];
[text drawAtPoint:NSMakePoint(fontSize, fontSize)];
}
@end

View File

@ -2,5 +2,5 @@
#import "GBGLShader.h" #import "GBGLShader.h"
@interface GBOpenGLView : NSOpenGLView @interface GBOpenGLView : NSOpenGLView
@property GBGLShader *shader; @property (nonatomic) GBGLShader *shader;
@end @end

View File

@ -2,26 +2,30 @@
#import <JoyKit/JoyKit.h> #import <JoyKit/JoyKit.h>
@interface GBPreferencesWindow : NSWindow <NSTableViewDelegate, NSTableViewDataSource, JOYListener> @interface GBPreferencesWindow : NSWindow <NSTableViewDelegate, NSTableViewDataSource, JOYListener>
@property IBOutlet NSTableView *controlsTableView; @property (nonatomic, strong) IBOutlet NSTableView *controlsTableView;
@property IBOutlet NSPopUpButton *graphicsFilterPopupButton; @property (nonatomic, strong) IBOutlet NSPopUpButton *graphicsFilterPopupButton;
@property (strong) IBOutlet NSButton *analogControlsCheckbox; @property (nonatomic, strong) IBOutlet NSButton *analogControlsCheckbox;
@property (strong) IBOutlet NSButton *aspectRatioCheckbox; @property (nonatomic, strong) IBOutlet NSButton *aspectRatioCheckbox;
@property (strong) IBOutlet NSPopUpButton *highpassFilterPopupButton; @property (nonatomic, strong) IBOutlet NSPopUpButton *highpassFilterPopupButton;
@property (strong) IBOutlet NSPopUpButton *colorCorrectionPopupButton; @property (nonatomic, strong) IBOutlet NSPopUpButton *colorCorrectionPopupButton;
@property (strong) IBOutlet NSPopUpButton *frameBlendingModePopupButton; @property (nonatomic, strong) IBOutlet NSPopUpButton *frameBlendingModePopupButton;
@property (strong) IBOutlet NSPopUpButton *colorPalettePopupButton; @property (nonatomic, strong) IBOutlet NSPopUpButton *colorPalettePopupButton;
@property (strong) IBOutlet NSPopUpButton *displayBorderPopupButton; @property (nonatomic, strong) IBOutlet NSPopUpButton *displayBorderPopupButton;
@property (strong) IBOutlet NSPopUpButton *rewindPopupButton; @property (nonatomic, strong) IBOutlet NSPopUpButton *rewindPopupButton;
@property (strong) IBOutlet NSButton *configureJoypadButton; @property (nonatomic, strong) IBOutlet NSPopUpButton *rtcPopupButton;
@property (strong) IBOutlet NSButton *skipButton; @property (nonatomic, strong) IBOutlet NSButton *configureJoypadButton;
@property (strong) IBOutlet NSMenuItem *bootROMsFolderItem; @property (nonatomic, strong) IBOutlet NSButton *skipButton;
@property (strong) IBOutlet NSPopUpButtonCell *bootROMsButton; @property (nonatomic, strong) IBOutlet NSMenuItem *bootROMsFolderItem;
@property (strong) IBOutlet NSPopUpButton *rumbleModePopupButton; @property (nonatomic, strong) IBOutlet NSPopUpButtonCell *bootROMsButton;
@property (nonatomic, strong) IBOutlet NSPopUpButton *rumbleModePopupButton;
@property (weak) IBOutlet NSPopUpButton *dmgPopupButton; @property (nonatomic, weak) IBOutlet NSSlider *temperatureSlider;
@property (weak) IBOutlet NSPopUpButton *sgbPopupButton; @property (nonatomic, weak) IBOutlet NSSlider *interferenceSlider;
@property (weak) IBOutlet NSPopUpButton *cgbPopupButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *dmgPopupButton;
@property (weak) IBOutlet NSPopUpButton *preferredJoypadButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *sgbPopupButton;
@property (weak) IBOutlet NSPopUpButton *playerListButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *cgbPopupButton;
@property (nonatomic, weak) IBOutlet NSPopUpButton *preferredJoypadButton;
@property (nonatomic, weak) IBOutlet NSPopUpButton *playerListButton;
@property (nonatomic, weak) IBOutlet NSButton *autoUpdatesCheckbox;
@property (weak) IBOutlet NSSlider *volumeSlider;
@property (weak) IBOutlet NSButton *OSDCheckbox;
@end @end

View File

@ -19,6 +19,7 @@
NSPopUpButton *_colorPalettePopupButton; NSPopUpButton *_colorPalettePopupButton;
NSPopUpButton *_displayBorderPopupButton; NSPopUpButton *_displayBorderPopupButton;
NSPopUpButton *_rewindPopupButton; NSPopUpButton *_rewindPopupButton;
NSPopUpButton *_rtcPopupButton;
NSButton *_aspectRatioCheckbox; NSButton *_aspectRatioCheckbox;
NSButton *_analogControlsCheckbox; NSButton *_analogControlsCheckbox;
NSEventModifierFlags previousModifiers; NSEventModifierFlags previousModifiers;
@ -26,6 +27,11 @@
NSPopUpButton *_dmgPopupButton, *_sgbPopupButton, *_cgbPopupButton; NSPopUpButton *_dmgPopupButton, *_sgbPopupButton, *_cgbPopupButton;
NSPopUpButton *_preferredJoypadButton; NSPopUpButton *_preferredJoypadButton;
NSPopUpButton *_rumbleModePopupButton; NSPopUpButton *_rumbleModePopupButton;
NSSlider *_temperatureSlider;
NSSlider *_interferenceSlider;
NSSlider *_volumeSlider;
NSButton *_autoUpdatesCheckbox;
NSButton *_OSDCheckbox;
} }
+ (NSArray *)filterList + (NSArray *)filterList
@ -91,11 +97,45 @@
[_colorCorrectionPopupButton selectItemAtIndex:mode]; [_colorCorrectionPopupButton selectItemAtIndex:mode];
} }
- (NSPopUpButton *)colorCorrectionPopupButton - (NSPopUpButton *)colorCorrectionPopupButton
{ {
return _colorCorrectionPopupButton; return _colorCorrectionPopupButton;
} }
- (void)setTemperatureSlider:(NSSlider *)temperatureSlider
{
_temperatureSlider = temperatureSlider;
[temperatureSlider setDoubleValue:[[NSUserDefaults standardUserDefaults] doubleForKey:@"GBLightTemperature"] * 256];
}
- (NSSlider *)temperatureSlider
{
return _temperatureSlider;
}
- (void)setInterferenceSlider:(NSSlider *)interferenceSlider
{
_interferenceSlider = interferenceSlider;
[interferenceSlider setDoubleValue:[[NSUserDefaults standardUserDefaults] doubleForKey:@"GBInterferenceVolume"] * 256];
}
- (NSSlider *)interferenceSlider
{
return _interferenceSlider;
}
- (void)setVolumeSlider:(NSSlider *)volumeSlider
{
_volumeSlider = volumeSlider;
[volumeSlider setDoubleValue:[[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"] * 256];
}
- (NSSlider *)volumeSlider
{
return _volumeSlider;
}
- (void)setFrameBlendingModePopupButton:(NSPopUpButton *)frameBlendingModePopupButton - (void)setFrameBlendingModePopupButton:(NSPopUpButton *)frameBlendingModePopupButton
{ {
_frameBlendingModePopupButton = frameBlendingModePopupButton; _frameBlendingModePopupButton = frameBlendingModePopupButton;
@ -156,6 +196,18 @@
return _rewindPopupButton; return _rewindPopupButton;
} }
- (NSPopUpButton *)rtcPopupButton
{
return _rtcPopupButton;
}
- (void)setRtcPopupButton:(NSPopUpButton *)rtcPopupButton
{
_rtcPopupButton = rtcPopupButton;
NSInteger mode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRTCMode"];
[_rtcPopupButton selectItemAtIndex:mode];
}
- (void)setHighpassFilterPopupButton:(NSPopUpButton *)highpassFilterPopupButton - (void)setHighpassFilterPopupButton:(NSPopUpButton *)highpassFilterPopupButton
{ {
_highpassFilterPopupButton = highpassFilterPopupButton; _highpassFilterPopupButton = highpassFilterPopupButton;
@ -284,6 +336,26 @@
[[NSNotificationCenter defaultCenter] postNotificationName:@"GBColorCorrectionChanged" object:nil]; [[NSNotificationCenter defaultCenter] postNotificationName:@"GBColorCorrectionChanged" object:nil];
} }
- (IBAction)lightTemperatureChanged:(id)sender
{
[[NSUserDefaults standardUserDefaults] setObject:@([sender doubleValue] / 256.0)
forKey:@"GBLightTemperature"];
[[NSNotificationCenter defaultCenter] postNotificationName:@"GBLightTemperatureChanged" object:nil];
}
- (IBAction)interferenceVolumeChanged:(id)sender
{
[[NSUserDefaults standardUserDefaults] setObject:@([sender doubleValue] / 256.0)
forKey:@"GBInterferenceVolume"];
[[NSNotificationCenter defaultCenter] postNotificationName:@"GBInterferenceVolumeChanged" object:nil];
}
- (IBAction)volumeChanged:(id)sender
{
[[NSUserDefaults standardUserDefaults] setObject:@([sender doubleValue] / 256.0)
forKey:@"GBVolume"];
}
- (IBAction)franeBlendingModeChanged:(id)sender - (IBAction)franeBlendingModeChanged:(id)sender
{ {
[[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem]) [[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem])
@ -320,6 +392,20 @@
[[NSNotificationCenter defaultCenter] postNotificationName:@"GBRewindLengthChanged" object:nil]; [[NSNotificationCenter defaultCenter] postNotificationName:@"GBRewindLengthChanged" object:nil];
} }
- (IBAction)rtcModeChanged:(id)sender
{
[[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem])
forKey:@"GBRTCMode"];
[[NSNotificationCenter defaultCenter] postNotificationName:@"GBRTCModeChanged" object:nil];
}
- (IBAction)changeAutoUpdates:(id)sender
{
[[NSUserDefaults standardUserDefaults] setBool: [(NSButton *)sender state] == NSOnState
forKey:@"GBAutoUpdatesEnabled"];
}
- (IBAction) configureJoypad:(id)sender - (IBAction) configureJoypad:(id)sender
{ {
[self.configureJoypadButton setEnabled:NO]; [self.configureJoypadButton setEnabled:NO];
@ -392,30 +478,37 @@
static const unsigned gb_to_joykit[] = { static const unsigned gb_to_joykit[] = {
[GBRight]=JOYButtonUsageDPadRight, [GBRight] = JOYButtonUsageDPadRight,
[GBLeft]=JOYButtonUsageDPadLeft, [GBLeft] = JOYButtonUsageDPadLeft,
[GBUp]=JOYButtonUsageDPadUp, [GBUp] = JOYButtonUsageDPadUp,
[GBDown]=JOYButtonUsageDPadDown, [GBDown] = JOYButtonUsageDPadDown,
[GBA]=JOYButtonUsageA, [GBA] = JOYButtonUsageA,
[GBB]=JOYButtonUsageB, [GBB] = JOYButtonUsageB,
[GBSelect]=JOYButtonUsageSelect, [GBSelect] = JOYButtonUsageSelect,
[GBStart]=JOYButtonUsageStart, [GBStart] = JOYButtonUsageStart,
[GBTurbo]=JOYButtonUsageL1, [GBTurbo] = JOYButtonUsageL1,
[GBRewind]=JOYButtonUsageL2, [GBRewind] = JOYButtonUsageL2,
[GBUnderclock]=JOYButtonUsageR1, [GBUnderclock] = JOYButtonUsageR1,
}; };
if (joystick_configuration_state == GBUnderclock) { if (joystick_configuration_state == GBUnderclock) {
mapping[@"AnalogUnderclock"] = nil;
double max = 0;
for (JOYAxis *axis in controller.axes) { for (JOYAxis *axis in controller.axes) {
if (axis.value > 0.5) { if ((axis.value > 0.5 || (axis.equivalentButtonUsage == button.usage)) && axis.value >= max) {
max = axis.value;
mapping[@"AnalogUnderclock"] = @(axis.uniqueID); mapping[@"AnalogUnderclock"] = @(axis.uniqueID);
break;
} }
} }
} }
if (joystick_configuration_state == GBTurbo) { if (joystick_configuration_state == GBTurbo) {
mapping[@"AnalogTurbo"] = nil;
double max = 0;
for (JOYAxis *axis in controller.axes) { for (JOYAxis *axis in controller.axes) {
if (axis.value > 0.5) { if ((axis.value > 0.5 || (axis.equivalentButtonUsage == button.usage)) && axis.value >= max) {
max = axis.value;
mapping[@"AnalogTurbo"] = @(axis.uniqueID); mapping[@"AnalogTurbo"] = @(axis.uniqueID);
} }
} }
@ -644,4 +737,33 @@
} }
[[NSUserDefaults standardUserDefaults] setObject:default_joypads forKey:@"JoyKitDefaultControllers"]; [[NSUserDefaults standardUserDefaults] setObject:default_joypads forKey:@"JoyKitDefaultControllers"];
} }
- (NSButton *)autoUpdatesCheckbox
{
return _autoUpdatesCheckbox;
}
- (void)setAutoUpdatesCheckbox:(NSButton *)autoUpdatesCheckbox
{
_autoUpdatesCheckbox = autoUpdatesCheckbox;
[_autoUpdatesCheckbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAutoUpdatesEnabled"]];
}
- (NSButton *)OSDCheckbox
{
return _OSDCheckbox;
}
- (void)setOSDCheckbox:(NSButton *)OSDCheckbox
{
_OSDCheckbox = OSDCheckbox;
[_OSDCheckbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBOSDEnabled"]];
}
- (IBAction)changeOSDEnabled:(id)sender
{
[[NSUserDefaults standardUserDefaults] setBool:[(NSButton *)sender state] == NSOnState
forKey:@"GBOSDEnabled"];
}
@end @end

128
bsnes/gb/Cocoa/GBS.xib Normal file
View File

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14868" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14868"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="Document">
<connections>
<outlet property="gbsAuthor" destination="gaD-ZH-Beh" id="2i7-BD-bJ2"/>
<outlet property="gbsCopyright" destination="2dl-dH-E3J" id="LnT-Vb-pN6"/>
<outlet property="gbsNextPrevButton" destination="SRS-M5-VVL" id="YEN-01-wRX"/>
<outlet property="gbsPlayPauseButton" destination="qxJ-pH-d0y" id="qk8-8I-9u5"/>
<outlet property="gbsPlayerView" destination="c22-O7-iKe" id="A1w-e5-EQE"/>
<outlet property="gbsRewindButton" destination="0yD-Sp-Ilo" id="FgR-xd-JW5"/>
<outlet property="gbsTitle" destination="H3v-X3-48q" id="DCl-wL-oy8"/>
<outlet property="gbsTracks" destination="I1T-VS-Vse" id="Vk4-GP-RjB"/>
<outlet property="gbsVisualizer" destination="Q3o-bK-DIN" id="1YC-C5-Je6"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="c22-O7-iKe">
<rect key="frame" x="0.0" y="0.0" width="332" height="221"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="H3v-X3-48q">
<rect key="frame" x="18" y="192" width="296" height="19"/>
<autoresizingMask key="autoresizingMask" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="Title" id="BwZ-Zj-sP6">
<font key="font" metaFont="systemBold" size="16"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="gaD-ZH-Beh">
<rect key="frame" x="18" y="166" width="296" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="Author" id="IgT-r1-T38">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qxJ-pH-d0y">
<rect key="frame" x="61.5" y="127" width="39" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="Play" imagePosition="only" alignment="center" alternateImage="Pause" state="on" borderStyle="border" focusRingType="none" inset="2" id="3ZK-br-UrS">
<behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="togglePause:" target="-2" id="AUe-I7-nOK"/>
</connections>
</button>
<button focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0yD-Sp-Ilo">
<rect key="frame" x="19.5" y="127" width="38" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" image="Rewind" imagePosition="only" alignment="center" state="on" borderStyle="border" focusRingType="none" inset="2" id="ZIF-TP-Fqn">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeGBSTrack:" target="-2" id="jug-AS-bW7"/>
</connections>
</button>
<popUpButton focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="I1T-VS-Vse">
<rect key="frame" x="106" y="127" width="131" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="roundTextured" bezelStyle="texturedRounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="border" focusRingType="none" imageScaling="proportionallyDown" inset="2" id="YJh-dI-A5D">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="Knp-Ok-Pb4"/>
</popUpButtonCell>
<connections>
<action selector="changeGBSTrack:" target="-2" id="HET-AT-CfQ"/>
</connections>
</popUpButton>
<segmentedControl verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="SRS-M5-VVL">
<rect key="frame" x="240.5" y="127" width="72" height="23"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<segmentedCell key="cell" borderStyle="border" alignment="left" style="texturedRounded" trackingMode="momentary" id="cmq-I8-cFL">
<font key="font" metaFont="system"/>
<segments>
<segment toolTip="Previous Track" image="Previous" width="33"/>
<segment toolTip="Next Track" image="Next" width="32" tag="1"/>
</segments>
</segmentedCell>
<connections>
<action selector="gbsNextPrevPushed:" target="-2" id="roN-Iy-tDQ"/>
</connections>
</segmentedControl>
<box verticalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="b9A-cd-ias">
<rect key="frame" x="0.0" y="117" width="332" height="5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</box>
<customView appearanceType="darkAqua" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tRy-Gw-IaG" customClass="GBOptionalVisualEffectView">
<rect key="frame" x="0.0" y="24" width="332" height="95"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<customView fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Q3o-bK-DIN" customClass="GBVisualizerView">
<rect key="frame" x="0.0" y="0.0" width="332" height="95"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</customView>
</subviews>
</customView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" id="2dl-dH-E3J">
<rect key="frame" x="18" y="5" width="296" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="clipping" alignment="center" title="Copyright" id="nM9-oF-OV9">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<point key="canvasLocation" x="67" y="292.5"/>
</customView>
</objects>
<resources>
<image name="Next" width="16" height="10"/>
<image name="Pause" width="10" height="10"/>
<image name="Play" width="10" height="10"/>
<image name="Previous" width="16" height="10"/>
<image name="Rewind" width="10" height="10"/>
</resources>
</document>

View File

@ -2,5 +2,5 @@
#include <Core/gb.h> #include <Core/gb.h>
@interface GBTerminalTextFieldCell : NSTextFieldCell @interface GBTerminalTextFieldCell : NSTextFieldCell
@property GB_gameboy_t *gb; @property (nonatomic) GB_gameboy_t *gb;
@end @end

View File

@ -1,6 +1,8 @@
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#include <Core/gb.h> #include <Core/gb.h>
#import <JoyKit/JoyKit.h> #import <JoyKit/JoyKit.h>
#import "GBOSDView.h"
@class Document;
typedef enum { typedef enum {
GB_FRAME_BLENDING_MODE_DISABLED, GB_FRAME_BLENDING_MODE_DISABLED,
@ -13,11 +15,13 @@ typedef enum {
@interface GBView : NSView<JOYListener> @interface GBView : NSView<JOYListener>
- (void) flip; - (void) flip;
- (uint32_t *) pixels; - (uint32_t *) pixels;
@property GB_gameboy_t *gb; @property (nonatomic, weak) IBOutlet Document *document;
@property (nonatomic) GB_gameboy_t *gb;
@property (nonatomic) GB_frame_blending_mode_t frameBlendingMode; @property (nonatomic) GB_frame_blending_mode_t frameBlendingMode;
@property (getter=isMouseHidingEnabled) BOOL mouseHidingEnabled; @property (nonatomic, getter=isMouseHidingEnabled) BOOL mouseHidingEnabled;
@property bool isRewinding; @property (nonatomic) bool isRewinding;
@property NSView *internalView; @property (nonatomic, strong) NSView *internalView;
@property (weak) GBOSDView *osdView;
- (void) createInternalView; - (void) createInternalView;
- (uint32_t *)currentBuffer; - (uint32_t *)currentBuffer;
- (uint32_t *)previousBuffer; - (uint32_t *)previousBuffer;

View File

@ -5,6 +5,7 @@
#import "GBViewMetal.h" #import "GBViewMetal.h"
#import "GBButtons.h" #import "GBButtons.h"
#import "NSString+StringForKey.h" #import "NSString+StringForKey.h"
#import "Document.h"
#define JOYSTICK_HIGH 0x4000 #define JOYSTICK_HIGH 0x4000
#define JOYSTICK_LOW 0x3800 #define JOYSTICK_LOW 0x3800
@ -116,6 +117,7 @@ static const uint8_t workboy_vk_to_key[] = {
NSEventModifierFlags previousModifiers; NSEventModifierFlags previousModifiers;
JOYController *lastController; JOYController *lastController;
GB_frame_blending_mode_t _frameBlendingMode; GB_frame_blending_mode_t _frameBlendingMode;
bool _turbo;
} }
+ (instancetype)alloc + (instancetype)alloc
@ -141,6 +143,8 @@ static const uint8_t workboy_vk_to_key[] = {
- (void) _init - (void) _init
{ {
[self registerForDraggedTypes:[NSArray arrayWithObjects: NSFilenamesPboardType, nil]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(ratioKeepingChanged) name:@"GBAspectChanged" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(ratioKeepingChanged) name:@"GBAspectChanged" object:nil];
tracking_area = [ [NSTrackingArea alloc] initWithRect:(NSRect){} tracking_area = [ [NSTrackingArea alloc] initWithRect:(NSRect){}
options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect
@ -256,21 +260,45 @@ static const uint8_t workboy_vk_to_key[] = {
- (void) flip - (void) flip
{ {
if (analogClockMultiplierValid && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAnalogControls"]) { if (analogClockMultiplierValid && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAnalogControls"]) {
clockMultiplier = 1.0;
GB_set_clock_multiplier(_gb, analogClockMultiplier); GB_set_clock_multiplier(_gb, analogClockMultiplier);
if (self.document.partner) {
GB_set_clock_multiplier(self.document.partner.gb, analogClockMultiplier);
}
if (analogClockMultiplier == 1.0) { if (analogClockMultiplier == 1.0) {
analogClockMultiplierValid = false; analogClockMultiplierValid = false;
} }
if (analogClockMultiplier < 2.0 && analogClockMultiplier > 1.0) {
GB_set_turbo_mode(_gb, false, false);
if (self.document.partner) {
GB_set_turbo_mode(self.document.partner.gb, false, false);
}
}
} }
else { else {
if (underclockKeyDown && clockMultiplier > 0.5) { if (underclockKeyDown && clockMultiplier > 0.5) {
clockMultiplier -= 1.0/16; clockMultiplier -= 1.0/16;
GB_set_clock_multiplier(_gb, clockMultiplier); GB_set_clock_multiplier(_gb, clockMultiplier);
if (self.document.partner) {
GB_set_clock_multiplier(self.document.partner.gb, clockMultiplier);
}
} }
if (!underclockKeyDown && clockMultiplier < 1.0) { if (!underclockKeyDown && clockMultiplier < 1.0) {
clockMultiplier += 1.0/16; clockMultiplier += 1.0/16;
GB_set_clock_multiplier(_gb, clockMultiplier); GB_set_clock_multiplier(_gb, clockMultiplier);
if (self.document.partner) {
GB_set_clock_multiplier(self.document.partner.gb, clockMultiplier);
}
} }
} }
if ((!analogClockMultiplierValid && clockMultiplier > 1) ||
_turbo || (analogClockMultiplierValid && analogClockMultiplier > 1)) {
[self.osdView displayText:@"Fast forwarding..."];
}
else if ((!analogClockMultiplierValid && clockMultiplier < 1) ||
(analogClockMultiplierValid && analogClockMultiplier < 1)) {
[self.osdView displayText:@"Slow motion..."];
}
current_buffer = (current_buffer + 1) % self.numberOfBuffers; current_buffer = (current_buffer + 1) % self.numberOfBuffers;
} }
@ -299,6 +327,9 @@ static const uint8_t workboy_vk_to_key[] = {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
unsigned player_count = GB_get_player_count(_gb); unsigned player_count = GB_get_player_count(_gb);
if (self.document.partner) {
player_count = 2;
}
for (unsigned player = 0; player < player_count; player++) { for (unsigned player = 0; player < player_count; player++) {
for (GBButton button = 0; button < GBButtonCount; button++) { for (GBButton button = 0; button < GBButtonCount; button++) {
NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)]; NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)];
@ -308,13 +339,22 @@ static const uint8_t workboy_vk_to_key[] = {
handled = true; handled = true;
switch (button) { switch (button) {
case GBTurbo: case GBTurbo:
GB_set_turbo_mode(_gb, true, self.isRewinding); if (self.document.isSlave) {
GB_set_turbo_mode(self.document.partner.gb, true, false);
}
else {
GB_set_turbo_mode(_gb, true, self.isRewinding);
}
_turbo = true;
analogClockMultiplierValid = false; analogClockMultiplierValid = false;
break; break;
case GBRewind: case GBRewind:
self.isRewinding = true; if (!self.document.partner) {
GB_set_turbo_mode(_gb, false, false); self.isRewinding = true;
GB_set_turbo_mode(_gb, false, false);
_turbo = false;
}
break; break;
case GBUnderclock: case GBUnderclock:
@ -323,7 +363,17 @@ static const uint8_t workboy_vk_to_key[] = {
break; break;
default: default:
GB_set_key_state_for_player(_gb, (GB_key_t)button, player, true); if (self.document.partner) {
if (player == 0) {
GB_set_key_state_for_player(_gb, (GB_key_t)button, 0, true);
}
else {
GB_set_key_state_for_player(self.document.partner.gb, (GB_key_t)button, 0, true);
}
}
else {
GB_set_key_state_for_player(_gb, (GB_key_t)button, player, true);
}
break; break;
} }
} }
@ -351,6 +401,9 @@ static const uint8_t workboy_vk_to_key[] = {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
unsigned player_count = GB_get_player_count(_gb); unsigned player_count = GB_get_player_count(_gb);
if (self.document.partner) {
player_count = 2;
}
for (unsigned player = 0; player < player_count; player++) { for (unsigned player = 0; player < player_count; player++) {
for (GBButton button = 0; button < GBButtonCount; button++) { for (GBButton button = 0; button < GBButtonCount; button++) {
NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)]; NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)];
@ -360,7 +413,13 @@ static const uint8_t workboy_vk_to_key[] = {
handled = true; handled = true;
switch (button) { switch (button) {
case GBTurbo: case GBTurbo:
GB_set_turbo_mode(_gb, false, false); if (self.document.isSlave) {
GB_set_turbo_mode(self.document.partner.gb, false, false);
}
else {
GB_set_turbo_mode(_gb, false, false);
}
_turbo = false;
analogClockMultiplierValid = false; analogClockMultiplierValid = false;
break; break;
@ -374,7 +433,17 @@ static const uint8_t workboy_vk_to_key[] = {
break; break;
default: default:
GB_set_key_state_for_player(_gb, (GB_key_t)button, player, false); if (self.document.partner) {
if (player == 0) {
GB_set_key_state_for_player(_gb, (GB_key_t)button, 0, false);
}
else {
GB_set_key_state_for_player(self.document.partner.gb, (GB_key_t)button, 0, false);
}
}
else {
GB_set_key_state_for_player(_gb, (GB_key_t)button, player, false);
}
break; break;
} }
} }
@ -401,13 +470,13 @@ static const uint8_t workboy_vk_to_key[] = {
if ((axis.usage == JOYAxisUsageR1 && !mapping) || if ((axis.usage == JOYAxisUsageR1 && !mapping) ||
axis.uniqueID == [mapping[@"AnalogUnderclock"] unsignedLongValue]){ axis.uniqueID == [mapping[@"AnalogUnderclock"] unsignedLongValue]){
analogClockMultiplier = MIN(MAX(1 - axis.value + 0.2, 1.0 / 3), 1.0); analogClockMultiplier = MIN(MAX(1 - axis.value + 0.05, 1.0 / 3), 1.0);
analogClockMultiplierValid = true; analogClockMultiplierValid = true;
} }
else if ((axis.usage == JOYAxisUsageL1 && !mapping) || else if ((axis.usage == JOYAxisUsageL1 && !mapping) ||
axis.uniqueID == [mapping[@"AnalogTurbo"] unsignedLongValue]){ axis.uniqueID == [mapping[@"AnalogTurbo"] unsignedLongValue]){
analogClockMultiplier = MIN(MAX(axis.value * 3 + 0.8, 1.0), 3.0); analogClockMultiplier = MIN(MAX(axis.value * 3 + 0.95, 1.0), 3.0);
analogClockMultiplierValid = true; analogClockMultiplierValid = true;
} }
} }
@ -415,13 +484,11 @@ static const uint8_t workboy_vk_to_key[] = {
- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button - (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button
{ {
if (![self.window isMainWindow]) return; if (![self.window isMainWindow]) return;
if (controller != lastController) {
[self setRumble:0];
lastController = controller;
}
unsigned player_count = GB_get_player_count(_gb); unsigned player_count = GB_get_player_count(_gb);
if (self.document.partner) {
player_count = 2;
}
IOPMAssertionID assertionID; IOPMAssertionID assertionID;
IOPMAssertionDeclareUserActivity(CFSTR(""), kIOPMUserActiveLocal, &assertionID); IOPMAssertionDeclareUserActivity(CFSTR(""), kIOPMUserActiveLocal, &assertionID);
@ -435,7 +502,7 @@ static const uint8_t workboy_vk_to_key[] = {
continue; continue;
} }
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
[controller setPlayerLEDs:1 << player]; [controller setPlayerLEDs:[controller LEDMaskForPlayer:player]];
}); });
NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID];
if (!mapping) { if (!mapping) {
@ -447,33 +514,68 @@ static const uint8_t workboy_vk_to_key[] = {
usage = (const JOYButtonUsage[]){JOYButtonUsageY, JOYButtonUsageA, JOYButtonUsageB, JOYButtonUsageX}[(usage - JOYButtonUsageGeneric0) & 3]; usage = (const JOYButtonUsage[]){JOYButtonUsageY, JOYButtonUsageA, JOYButtonUsageB, JOYButtonUsageX}[(usage - JOYButtonUsageGeneric0) & 3];
} }
GB_gameboy_t *effectiveGB = _gb;
unsigned effectivePlayer = player;
if (player && self.document.partner) {
effectiveGB = self.document.partner.gb;
effectivePlayer = 0;
if (controller != self.document.partner.view->lastController) {
[self setRumble:0];
self.document.partner.view->lastController = controller;
}
}
else {
if (controller != lastController) {
[self setRumble:0];
lastController = controller;
}
}
switch (usage) { switch (usage) {
case JOYButtonUsageNone: break; case JOYButtonUsageNone: break;
case JOYButtonUsageA: GB_set_key_state_for_player(_gb, GB_KEY_A, player, button.isPressed); break; case JOYButtonUsageA: GB_set_key_state_for_player(effectiveGB, GB_KEY_A, effectivePlayer, button.isPressed); break;
case JOYButtonUsageB: GB_set_key_state_for_player(_gb, GB_KEY_B, player, button.isPressed); break; case JOYButtonUsageB: GB_set_key_state_for_player(effectiveGB, GB_KEY_B, effectivePlayer, button.isPressed); break;
case JOYButtonUsageC: break; case JOYButtonUsageC: break;
case JOYButtonUsageStart: case JOYButtonUsageStart:
case JOYButtonUsageX: GB_set_key_state_for_player(_gb, GB_KEY_START, player, button.isPressed); break; case JOYButtonUsageX: GB_set_key_state_for_player(effectiveGB, GB_KEY_START, effectivePlayer, button.isPressed); break;
case JOYButtonUsageSelect: case JOYButtonUsageSelect:
case JOYButtonUsageY: GB_set_key_state_for_player(_gb, GB_KEY_SELECT, player, button.isPressed); break; case JOYButtonUsageY: GB_set_key_state_for_player(effectiveGB, GB_KEY_SELECT, effectivePlayer, button.isPressed); break;
case JOYButtonUsageR2: case JOYButtonUsageR2:
case JOYButtonUsageL2: case JOYButtonUsageL2:
case JOYButtonUsageZ: { case JOYButtonUsageZ: {
self.isRewinding = button.isPressed; self.isRewinding = button.isPressed;
if (button.isPressed) { if (button.isPressed) {
GB_set_turbo_mode(_gb, false, false); if (self.document.isSlave) {
GB_set_turbo_mode(self.document.partner.gb, false, false);
}
else {
GB_set_turbo_mode(_gb, false, false);
}
_turbo = false;
} }
break; break;
} }
case JOYButtonUsageL1: GB_set_turbo_mode(_gb, button.isPressed, button.isPressed && self.isRewinding); break; case JOYButtonUsageL1: {
if (!analogClockMultiplierValid || analogClockMultiplier == 1.0 || !button.isPressed) {
if (self.document.isSlave) {
GB_set_turbo_mode(self.document.partner.gb, button.isPressed, false);
}
else {
GB_set_turbo_mode(_gb, button.isPressed, button.isPressed && self.isRewinding);
}
_turbo = button.isPressed;
}
break;
}
case JOYButtonUsageR1: underclockKeyDown = button.isPressed; break; case JOYButtonUsageR1: underclockKeyDown = button.isPressed; break;
case JOYButtonUsageDPadLeft: GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, button.isPressed); break; case JOYButtonUsageDPadLeft: GB_set_key_state_for_player(effectiveGB, GB_KEY_LEFT, effectivePlayer, button.isPressed); break;
case JOYButtonUsageDPadRight: GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, button.isPressed); break; case JOYButtonUsageDPadRight: GB_set_key_state_for_player(effectiveGB, GB_KEY_RIGHT, effectivePlayer, button.isPressed); break;
case JOYButtonUsageDPadUp: GB_set_key_state_for_player(_gb, GB_KEY_UP, player, button.isPressed); break; case JOYButtonUsageDPadUp: GB_set_key_state_for_player(effectiveGB, GB_KEY_UP, effectivePlayer, button.isPressed); break;
case JOYButtonUsageDPadDown: GB_set_key_state_for_player(_gb, GB_KEY_DOWN, player, button.isPressed); break; case JOYButtonUsageDPadDown: GB_set_key_state_for_player(effectiveGB, GB_KEY_DOWN, effectivePlayer, button.isPressed); break;
default: default:
break; break;
@ -550,4 +652,29 @@ static const uint8_t workboy_vk_to_key[] = {
return image_buffers[(current_buffer + 2) % self.numberOfBuffers]; return image_buffers[(current_buffer + 2) % self.numberOfBuffers];
} }
-(NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
{
NSPasteboard *pboard = [sender draggingPasteboard];
if ( [[pboard types] containsObject:NSURLPboardType] ) {
NSURL *fileURL = [NSURL URLFromPasteboard:pboard];
if (GB_is_stave_state(fileURL.fileSystemRepresentation)) {
return NSDragOperationGeneric;
}
}
return NSDragOperationNone;
}
-(BOOL)performDragOperation:(id<NSDraggingInfo>)sender
{
NSPasteboard *pboard = [sender draggingPasteboard];
if ( [[pboard types] containsObject:NSURLPboardType] ) {
NSURL *fileURL = [NSURL URLFromPasteboard:pboard];
return [_document loadStateFile:fileURL.fileSystemRepresentation noErrorOnNotFound:false];
}
return false;
}
@end @end

View File

@ -123,7 +123,7 @@ static const vector_float2 rect[] =
command_queue = [device newCommandQueue]; command_queue = [device newCommandQueue];
} }
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size - (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size
{ {
output_resolution = (vector_float2){size.width, size.height}; output_resolution = (vector_float2){size.width, size.height};
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
@ -131,7 +131,7 @@ static const vector_float2 rect[] =
}); });
} }
- (void)drawInMTKView:(nonnull MTKView *)view - (void)drawInMTKView:(MTKView *)view
{ {
if (!(view.window.occlusionState & NSWindowOcclusionStateVisible)) return; if (!(view.window.occlusionState & NSWindowOcclusionStateVisible)) return;
if (!self.gb) return; if (!self.gb) return;

View File

@ -0,0 +1,6 @@
#import <Cocoa/Cocoa.h>
#include <Core/gb.h>
@interface GBVisualizerView : NSView
- (void)addSample:(GB_sample_t *)sample;
@end

View File

@ -0,0 +1,87 @@
#import "GBVisualizerView.h"
#include <Core/gb.h>
#define SAMPLE_COUNT 1024
static NSColor *color_to_effect_color(typeof(GB_PALETTE_DMG.colors[0]) color)
{
if (@available(macOS 10.10, *)) {
double tint = MAX(color.r, MAX(color.g, color.b)) + 64;
return [NSColor colorWithRed:color.r / tint
green:color.g / tint
blue:color.b / tint
alpha:tint/(255 + 64)];
}
return [NSColor colorWithRed:color.r / 255.0
green:color.g / 255.0
blue:color.b / 255.0
alpha:1.0];
}
@implementation GBVisualizerView
{
GB_sample_t _samples[SAMPLE_COUNT];
size_t _position;
}
- (void)drawRect:(NSRect)dirtyRect
{
const GB_palette_t *palette;
switch ([[NSUserDefaults standardUserDefaults] integerForKey:@"GBColorPalette"]) {
case 1:
palette = &GB_PALETTE_DMG;
break;
case 2:
palette = &GB_PALETTE_MGB;
break;
case 3:
palette = &GB_PALETTE_GBL;
break;
default:
palette = &GB_PALETTE_GREY;
break;
}
NSSize size = self.bounds.size;
[color_to_effect_color(palette->colors[0]) setFill];
NSRectFill(self.bounds);
NSBezierPath *line = [NSBezierPath bezierPath];
[line moveToPoint:NSMakePoint(0, size.height / 2)];
for (unsigned i = 0; i < SAMPLE_COUNT; i++) {
GB_sample_t *sample = _samples + ((i + _position) % SAMPLE_COUNT);
double volume = ((signed)sample->left + (signed)sample->right) / 32768.0;
[line lineToPoint:NSMakePoint(size.width * (i + 0.5) / SAMPLE_COUNT,
(volume + 1) * size.height / 2)];
}
[line lineToPoint:NSMakePoint(size.width, size.height / 2)];
[line setLineWidth:1.0];
[color_to_effect_color(palette->colors[2]) setFill];
[line fill];
[color_to_effect_color(palette->colors[1]) setFill];
NSRectFill(NSMakeRect(0, size.height / 2 - 0.5, size.width, 1));
[color_to_effect_color(palette->colors[3]) setStroke];
[line stroke];
[super drawRect:dirtyRect];
}
- (void)addSample:(GB_sample_t *)sample
{
_samples[_position++] = *sample;
if (_position == SAMPLE_COUNT) {
_position = 0;
}
}
@end

View File

@ -51,7 +51,7 @@
<dict> <dict>
<key>CFBundleTypeExtensions</key> <key>CFBundleTypeExtensions</key>
<array> <array>
<string>gbc</string> <string>isx</string>
</array> </array>
<key>CFBundleTypeIconFile</key> <key>CFBundleTypeIconFile</key>
<string>ColorCartridge</string> <string>ColorCartridge</string>
@ -68,6 +68,26 @@
<key>NSDocumentClass</key> <key>NSDocumentClass</key>
<string>Document</string> <string>Document</string>
</dict> </dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>gbs</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>ColorCartridge</string>
<key>CFBundleTypeName</key>
<string>Game Boy Sound File</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array>
<string>com.github.liji32.sameboy.gbs</string>
</array>
<key>LSTypeIsPackage</key>
<integer>0</integer>
<key>NSDocumentClass</key>
<string>Document</string>
</dict>
</array> </array>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>SameBoy</string> <string>SameBoy</string>
@ -92,7 +112,7 @@
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>10.9</string> <string>10.9</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>Copyright © 2015-2020 Lior Halphon</string> <string>Copyright © 2015-2021 Lior Halphon</string>
<key>NSMainNibFile</key> <key>NSMainNibFile</key>
<string>MainMenu</string> <string>MainMenu</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
@ -156,6 +176,25 @@
</array> </array>
</dict> </dict>
</dict> </dict>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>Game Boy Sound File</string>
<key>UTTypeIconFile</key>
<string>ColorCartridge</string>
<key>UTTypeIdentifier</key>
<string>com.github.liji32.sameboy.gbs</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>gbs</string>
</array>
</dict>
</dict>
</array> </array>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>SameBoy needs to access your camera to emulate the Game Boy Camera</string> <string>SameBoy needs to access your camera to emulate the Game Boy Camera</string>

View File

@ -30,7 +30,7 @@
<h1>SameBoy</h1> <h1>SameBoy</h1>
<h2>MIT License</h2> <h2>MIT License</h2>
<h3>Copyright © 2015-2020 Lior Halphon</h3> <h3>Copyright © 2015-2021 Lior Halphon</h3>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy <p>Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -12,7 +12,11 @@
</customObject> </customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/> <customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate"/> <customObject id="Voe-Tx-rLC" customClass="AppDelegate">
<connections>
<outlet property="linkCableMenuItem" destination="V4S-Fo-xJK" id="KL9-3K-64i"/>
</connections>
</customObject>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/> <customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6"> <menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items> <items>
@ -373,6 +377,17 @@
<action selector="disconnectAllAccessories:" target="-1" id="5hY-9U-nRn"/> <action selector="disconnectAllAccessories:" target="-1" id="5hY-9U-nRn"/>
</connections> </connections>
</menuItem> </menuItem>
<menuItem title="Game Link Cable &amp; Infrared" id="V4S-Fo-xJK">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Game Link Cable &amp; Infrared" id="6sJ-Wz-QLj">
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="PMY-5j-25T"/>
</connections>
</menu>
<connections>
<action selector="nop:" target="Voe-Tx-rLC" id="Bpa-0C-lkN"/>
</connections>
</menuItem>
<menuItem title="Game Boy Printer" id="zHR-Ha-pOR"> <menuItem title="Game Boy Printer" id="zHR-Ha-pOR">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<connections> <connections>

BIN
bsnes/gb/Cocoa/Next.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
bsnes/gb/Cocoa/Next@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
bsnes/gb/Cocoa/Pause.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
bsnes/gb/Cocoa/Pause@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
bsnes/gb/Cocoa/Play.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
bsnes/gb/Cocoa/Play@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -13,6 +13,9 @@
<outlet property="emulationTab" destination="ymk-46-SX7" id="ofG-ca-a5C"/> <outlet property="emulationTab" destination="ymk-46-SX7" id="ofG-ca-a5C"/>
<outlet property="graphicsTab" destination="sRK-wO-K6R" id="pfP-Di-i0Q"/> <outlet property="graphicsTab" destination="sRK-wO-K6R" id="pfP-Di-i0Q"/>
<outlet property="preferencesWindow" destination="QvC-M9-y7g" id="kBg-fq-rZh"/> <outlet property="preferencesWindow" destination="QvC-M9-y7g" id="kBg-fq-rZh"/>
<outlet property="updatesButton" destination="KnI-UA-Nlj" id="9Lw-d7-Qoj"/>
<outlet property="updatesSpinner" destination="fB8-sd-zrh" id="HbX-JT-8Su"/>
<outlet property="updatesTab" destination="ffn-ie-9C3" id="rti-eV-pIV"/>
</connections> </connections>
</customObject> </customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
@ -49,17 +52,25 @@
<action selector="switchPreferencesTab:" target="-2" id="Tio-D7-PaA"/> <action selector="switchPreferencesTab:" target="-2" id="Tio-D7-PaA"/>
</connections> </connections>
</toolbarItem> </toolbarItem>
<toolbarItem implicitItemIdentifier="EB7AB5BC-57B0-4639-A037-E28226866C80" label="Updates" paletteLabel="Updates" tag="4" image="AppIcon" autovalidates="NO" selectable="YES" id="lMV-68-ntz">
<connections>
<action selector="switchPreferencesTab:" target="-2" id="bfU-hc-FnN"/>
</connections>
</toolbarItem>
</allowedToolbarItems> </allowedToolbarItems>
<defaultToolbarItems> <defaultToolbarItems>
<toolbarItem reference="zdp-Z7-yZM"/> <toolbarItem reference="zdp-Z7-yZM"/>
<toolbarItem reference="fCd-4a-SKR"/> <toolbarItem reference="fCd-4a-SKR"/>
<toolbarItem reference="EMp-g1-eKu"/> <toolbarItem reference="EMp-g1-eKu"/>
<toolbarItem reference="uNZ-j1-Dfx"/> <toolbarItem reference="uNZ-j1-Dfx"/>
<toolbarItem reference="lMV-68-ntz"/>
</defaultToolbarItems> </defaultToolbarItems>
</toolbar> </toolbar>
<connections> <connections>
<outlet property="OSDCheckbox" destination="quK-gY-Z6h" id="gxE-fd-1g7"/>
<outlet property="analogControlsCheckbox" destination="RuW-Db-dzW" id="FRE-hI-mnU"/> <outlet property="analogControlsCheckbox" destination="RuW-Db-dzW" id="FRE-hI-mnU"/>
<outlet property="aspectRatioCheckbox" destination="Vfj-tg-7OP" id="Yw0-xS-DBr"/> <outlet property="aspectRatioCheckbox" destination="Vfj-tg-7OP" id="Yw0-xS-DBr"/>
<outlet property="autoUpdatesCheckbox" destination="ZVh-ob-6wl" id="qPS-53-3aW"/>
<outlet property="bootROMsButton" destination="T3Y-Ln-Onl" id="tdL-Yv-E2K"/> <outlet property="bootROMsButton" destination="T3Y-Ln-Onl" id="tdL-Yv-E2K"/>
<outlet property="bootROMsFolderItem" destination="Dzv-Gc-zoL" id="yhV-ZI-avD"/> <outlet property="bootROMsFolderItem" destination="Dzv-Gc-zoL" id="yhV-ZI-avD"/>
<outlet property="cgbPopupButton" destination="dlD-sk-SHO" id="4tg-SR-e17"/> <outlet property="cgbPopupButton" destination="dlD-sk-SHO" id="4tg-SR-e17"/>
@ -73,21 +84,25 @@
<outlet property="frameBlendingModePopupButton" destination="lxk-db-Sxv" id="wzt-uo-TE6"/> <outlet property="frameBlendingModePopupButton" destination="lxk-db-Sxv" id="wzt-uo-TE6"/>
<outlet property="graphicsFilterPopupButton" destination="6pP-kK-EEC" id="LS7-HY-kHC"/> <outlet property="graphicsFilterPopupButton" destination="6pP-kK-EEC" id="LS7-HY-kHC"/>
<outlet property="highpassFilterPopupButton" destination="T69-6N-dhT" id="0p6-4m-hb1"/> <outlet property="highpassFilterPopupButton" destination="T69-6N-dhT" id="0p6-4m-hb1"/>
<outlet property="interferenceSlider" destination="FpE-5i-j5L" id="hfH-e8-7cx"/>
<outlet property="playerListButton" destination="gWx-7h-0xq" id="zo6-82-JId"/> <outlet property="playerListButton" destination="gWx-7h-0xq" id="zo6-82-JId"/>
<outlet property="preferredJoypadButton" destination="0Az-0R-oNw" id="7JM-tw-BhK"/> <outlet property="preferredJoypadButton" destination="0Az-0R-oNw" id="7JM-tw-BhK"/>
<outlet property="rewindPopupButton" destination="7fg-Ww-JjR" id="Ka2-TP-B1x"/> <outlet property="rewindPopupButton" destination="7fg-Ww-JjR" id="Ka2-TP-B1x"/>
<outlet property="rtcPopupButton" destination="tFf-H1-XUL" id="zxb-4h-aqg"/>
<outlet property="rumbleModePopupButton" destination="Ogs-xG-b4b" id="vuw-VN-MTp"/> <outlet property="rumbleModePopupButton" destination="Ogs-xG-b4b" id="vuw-VN-MTp"/>
<outlet property="sgbPopupButton" destination="dza-T7-RkX" id="B0o-Nb-pIH"/> <outlet property="sgbPopupButton" destination="dza-T7-RkX" id="B0o-Nb-pIH"/>
<outlet property="skipButton" destination="d2I-jU-sLb" id="udX-8K-0sK"/> <outlet property="skipButton" destination="d2I-jU-sLb" id="udX-8K-0sK"/>
<outlet property="temperatureSlider" destination="NuA-mL-AJZ" id="w11-n7-Bmj"/>
<outlet property="volumeSlider" destination="LNs-v1-Eki" id="DU4-An-OrC"/>
</connections> </connections>
<point key="canvasLocation" x="183" y="354"/> <point key="canvasLocation" x="183" y="354"/>
</window> </window>
<customView id="sRK-wO-K6R"> <customView id="sRK-wO-K6R">
<rect key="frame" x="0.0" y="0.0" width="292" height="323"/> <rect key="frame" x="0.0" y="0.0" width="292" height="395"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews> <subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T91-rh-rRp"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T91-rh-rRp">
<rect key="frame" x="18" y="286" width="256" height="17"/> <rect key="frame" x="18" y="358" width="256" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Graphics filter:" id="pXg-WY-8Q5"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Graphics filter:" id="pXg-WY-8Q5">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -96,7 +111,7 @@
</textFieldCell> </textFieldCell>
</textField> </textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6pP-kK-EEC"> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6pP-kK-EEC">
<rect key="frame" x="30" y="254" width="234" height="26"/> <rect key="frame" x="30" y="325" width="234" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Nearest neighbor (Pixelated)" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="neN-eo-LA7" id="I1w-05-lGl"> <popUpButtonCell key="cell" type="push" title="Nearest neighbor (Pixelated)" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="neN-eo-LA7" id="I1w-05-lGl">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
@ -133,7 +148,7 @@
</connections> </connections>
</popUpButton> </popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Wc3-2K-6CD"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Wc3-2K-6CD">
<rect key="frame" x="18" y="232" width="256" height="17"/> <rect key="frame" x="18" y="303" width="256" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Color correction:" id="5Si-hz-EK3"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Color correction:" id="5Si-hz-EK3">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -142,7 +157,7 @@
</textFieldCell> </textFieldCell>
</textField> </textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="VEz-N4-uP6"> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="VEz-N4-uP6">
<rect key="frame" x="30" y="200" width="234" height="26"/> <rect key="frame" x="30" y="270" width="234" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Disabled" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="D2J-wV-1vu" id="fNJ-Fi-yOm"> <popUpButtonCell key="cell" type="push" title="Disabled" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="D2J-wV-1vu" id="fNJ-Fi-yOm">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
@ -156,6 +171,7 @@
<menuItem title="Emulate hardware" id="XBL-hS-7ke"/> <menuItem title="Emulate hardware" id="XBL-hS-7ke"/>
<menuItem title="Preserve brightness" id="tlx-WB-Bkw"/> <menuItem title="Preserve brightness" id="tlx-WB-Bkw"/>
<menuItem title="Reduce contrast" id="wuO-Xv-0mQ"/> <menuItem title="Reduce contrast" id="wuO-Xv-0mQ"/>
<menuItem title="Harsh reality (low contrast)" id="98I-hs-vP2"/>
</items> </items>
</menu> </menu>
</popUpButtonCell> </popUpButtonCell>
@ -164,7 +180,7 @@
</connections> </connections>
</popUpButton> </popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="MLC-Rx-FgO"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="MLC-Rx-FgO">
<rect key="frame" x="20" y="178" width="252" height="17"/> <rect key="frame" x="18" y="194" width="252" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Frame blending" id="UCa-EO-tzh"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Frame blending" id="UCa-EO-tzh">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -173,7 +189,7 @@
</textFieldCell> </textFieldCell>
</textField> </textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="lxk-db-Sxv"> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="lxk-db-Sxv">
<rect key="frame" x="32" y="149" width="229" height="22"/> <rect key="frame" x="30" y="165" width="234" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Disabled" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="iHP-Yz-fiH" id="aQ6-HN-7Aj"> <popUpButtonCell key="cell" type="push" title="Disabled" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="iHP-Yz-fiH" id="aQ6-HN-7Aj">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
@ -193,7 +209,7 @@
</connections> </connections>
</popUpButton> </popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8fG-zm-hpr"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8fG-zm-hpr">
<rect key="frame" x="18" y="122" width="252" height="17"/> <rect key="frame" x="18" y="143" width="252" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Color palette for monochrome models:" id="LAN-8Y-T7H"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Color palette for monochrome models:" id="LAN-8Y-T7H">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -202,7 +218,7 @@
</textFieldCell> </textFieldCell>
</textField> </textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Iwr-eI-SD1"> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Iwr-eI-SD1">
<rect key="frame" x="30" y="93" width="234" height="22"/> <rect key="frame" x="30" y="114" width="234" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Greyscale" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Ajr-5r-iIk" id="rEU-jh-m3j"> <popUpButtonCell key="cell" type="push" title="Greyscale" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Ajr-5r-iIk" id="rEU-jh-m3j">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
@ -223,7 +239,7 @@
</connections> </connections>
</popUpButton> </popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="3Kz-cf-5X6"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="3Kz-cf-5X6">
<rect key="frame" x="18" y="71" width="248" height="17"/> <rect key="frame" x="18" y="92" width="248" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Display border:" id="HZd-qi-yyk"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Display border:" id="HZd-qi-yyk">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -232,7 +248,7 @@
</textFieldCell> </textFieldCell>
</textField> </textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="R9D-FV-bpd"> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="R9D-FV-bpd">
<rect key="frame" x="30" y="39" width="234" height="25"/> <rect key="frame" x="30" y="60" width="234" height="25"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Never" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="1" imageScaling="proportionallyDown" inset="2" selectedItem="heL-AV-0az" id="DY9-2D-h1L"> <popUpButtonCell key="cell" type="push" title="Never" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="1" imageScaling="proportionallyDown" inset="2" selectedItem="heL-AV-0az" id="DY9-2D-h1L">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
@ -251,8 +267,25 @@
<action selector="displayBorderChanged:" target="QvC-M9-y7g" id="GoA-BU-v3h"/> <action selector="displayBorderChanged:" target="QvC-M9-y7g" id="GoA-BU-v3h"/>
</connections> </connections>
</popUpButton> </popUpButton>
<slider verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="NuA-mL-AJZ">
<rect key="frame" x="30" y="218" width="234" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<sliderCell key="cell" continuous="YES" state="on" alignment="left" minValue="-256" maxValue="256" tickMarkPosition="below" numberOfTickMarks="3" sliderType="linear" id="KX7-G9-k0O"/>
<connections>
<action selector="lightTemperatureChanged:" target="QvC-M9-y7g" id="he8-ib-I3Y"/>
</connections>
</slider>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cCm-Oa-FbN">
<rect key="frame" x="20" y="248" width="252" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Ambient light temperature:" id="Lso-GQ-pBl">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Vfj-tg-7OP"> <button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Vfj-tg-7OP">
<rect key="frame" x="18" y="18" width="256" height="18"/> <rect key="frame" x="18" y="38" width="256" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="Keep aspect ratio" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="lsj-rC-Eo6"> <buttonCell key="cell" type="check" title="Keep aspect ratio" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="lsj-rC-Eo6">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
@ -262,39 +295,26 @@
<action selector="changeAspectRatio:" target="QvC-M9-y7g" id="mQG-Ib-1jN"/> <action selector="changeAspectRatio:" target="QvC-M9-y7g" id="mQG-Ib-1jN"/>
</connections> </connections>
</button> </button>
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="quK-gY-Z6h">
<rect key="frame" x="18" y="18" width="252" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="On-screen display" bezelStyle="regularSquare" imagePosition="left" inset="2" id="oeT-cD-YRw">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeOSDEnabled:" target="QvC-M9-y7g" id="xUx-iN-jl6"/>
</connections>
</button>
</subviews> </subviews>
<point key="canvasLocation" x="-176" y="667.5"/> <point key="canvasLocation" x="-176" y="677.5"/>
</customView> </customView>
<customView id="ymk-46-SX7"> <customView id="ymk-46-SX7">
<rect key="frame" x="0.0" y="0.0" width="292" height="320"/> <rect key="frame" x="0.0" y="0.0" width="292" height="375"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews> <subviews>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7fg-Ww-JjR">
<rect key="frame" x="30" y="193" width="234" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Disabled" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="lxQ-4n-kEv" id="lvb-QF-0Ht">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="lbS-Lw-kQX">
<items>
<menuItem title="Disabled" state="on" id="lxQ-4n-kEv">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="10 Seconds" tag="10" id="bPU-vT-d5z"/>
<menuItem title="30 Seconds" tag="30" id="aR8-IU-fFh"/>
<menuItem title="1 Minute" tag="60" id="E0R-mf-Hdl"/>
<menuItem title="2 Minutes" tag="120" id="zb2-uh-lvj"/>
<menuItem title="5 Minutes" tag="300" id="6Jj-EI-f6k"/>
<menuItem title="10 Minutes" tag="600" id="DOL-qL-Caz"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="rewindLengthChanged:" target="QvC-M9-y7g" id="5NQ-1T-RNc"/>
</connections>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="w9w-yX-KxB"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="w9w-yX-KxB">
<rect key="frame" x="18" y="225" width="256" height="17"/> <rect key="frame" x="18" y="283" width="256" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Rewinding duration:" id="JaO-5h-ugl"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Rewinding duration:" id="JaO-5h-ugl">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -302,8 +322,17 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
</textField> </textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="o3Z-34-FJk">
<rect key="frame" x="18" y="228" width="252" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Real Time Clock emulation:" id="Qoi-ub-YtI">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="MI2-ql-f6M"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="MI2-ql-f6M">
<rect key="frame" x="18" y="283" width="256" height="17"/> <rect key="frame" x="18" y="338" width="256" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Boot ROMs location:" id="nj0-Cb-gEA"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Boot ROMs location:" id="nj0-Cb-gEA">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -311,8 +340,116 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
</textField> </textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Wg8-hJ-df9">
<rect key="frame" x="18" y="160" width="256" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Game Boy revision:" id="GIA-ep-SBi">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LFw-Uk-cPR">
<rect key="frame" x="30" y="127" width="234" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="DMG-CPU B" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="2" imageScaling="proportionallyDown" inset="2" autoenablesItems="NO" selectedItem="aXT-sE-m5Z" id="FuX-Hc-uO7">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" autoenablesItems="NO" id="R0h-k6-Q5l">
<items>
<menuItem title="DMG-CPU 0" enabled="NO" id="pvp-Mo-9S8"/>
<menuItem title="DMG-CPU A" tag="1" enabled="NO" id="QLn-5i-Vlg"/>
<menuItem title="DMG-CPU B" state="on" tag="2" id="aXT-sE-m5Z"/>
<menuItem title="DMG-CPU C" tag="3" enabled="NO" id="Ecl-YM-oAA"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="dmgModelChanged:" target="QvC-M9-y7g" id="FIe-Wz-aSc"/>
</connections>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="MAq-1X-Gpo">
<rect key="frame" x="16" y="50" width="256" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Game Boy Color revision:" id="edD-t7-vwk">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="dlD-sk-SHO">
<rect key="frame" x="28" y="17" width="234" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="CPU-CGB E" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="517" imageScaling="proportionallyDown" inset="2" autoenablesItems="NO" selectedItem="3lF-1Q-2SS" id="Edt-1S-dXz">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" autoenablesItems="NO" id="bbF-hB-Hv7">
<items>
<menuItem title="CPU-CGB 0" tag="512" enabled="NO" id="2Uk-u3-6Gw"/>
<menuItem title="CPU-CGB A" tag="513" enabled="NO" id="axv-yk-RWM"/>
<menuItem title="CPU-CGB B" tag="514" enabled="NO" id="NtJ-oo-IM2"/>
<menuItem title="CPU-CGB C (Experimental)" tag="515" id="9YL-u8-12z"/>
<menuItem title="CPU-CGB D" tag="516" enabled="NO" id="c76-oF-fkU"/>
<menuItem title="CPU-CGB E" state="on" tag="517" id="3lF-1Q-2SS"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="cgbModelChanged:" target="QvC-M9-y7g" id="SY1-oj-VWQ"/>
</connections>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tAa-0A-0fP">
<rect key="frame" x="18" y="105" width="256" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Super Game Boy model:" id="d0g-rk-FK0">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<box verticalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="mdm-eW-ia1">
<rect key="frame" x="12" y="183" width="268" height="5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</box>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="dza-T7-RkX">
<rect key="frame" x="28" y="72" width="234" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Super Game Boy (NTSC)" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="4" imageScaling="proportionallyDown" inset="2" autoenablesItems="NO" selectedItem="x5A-7f-ef9" id="2Mt-ci-bB0">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" autoenablesItems="NO" id="czw-Hi-jyM">
<items>
<menuItem title="Super Game Boy (NTSC)" state="on" tag="4" id="x5A-7f-ef9"/>
<menuItem title="Super Game Boy (PAL)" tag="68" id="Cix-n2-l4L"/>
<menuItem title="Super Game Boy 2" tag="257" id="gZG-1g-KF0"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="sgbModelChanged:" target="QvC-M9-y7g" id="ybk-EV-WPS"/>
</connections>
</popUpButton>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tFf-H1-XUL">
<rect key="frame" x="30" y="199" width="234" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Sync to system clock" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="arp-Qi-Xix" id="uRs-Ag-Sbw">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="yFf-TQ-bZc">
<items>
<menuItem title="Sync to system clock" state="on" id="arp-Qi-Xix">
<modifierMask key="keyEquivalentModifierMask"/>
</menuItem>
<menuItem title="Accurate" id="H7h-S9-hvM"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="rtcModeChanged:" target="QvC-M9-y7g" id="3fA-mo-MB1"/>
</connections>
</popUpButton>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wC4-aJ-mhQ"> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wC4-aJ-mhQ">
<rect key="frame" x="30" y="251" width="234" height="26"/> <rect key="frame" x="30" y="305" width="234" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Use built-in boot ROMs" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Tnm-SR-ZEm" id="T3Y-Ln-Onl"> <popUpButtonCell key="cell" type="push" title="Use built-in boot ROMs" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Tnm-SR-ZEm" id="T3Y-Ln-Onl">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
@ -335,104 +472,56 @@
</menu> </menu>
</popUpButtonCell> </popUpButtonCell>
</popUpButton> </popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Wg8-hJ-df9"> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7fg-Ww-JjR">
<rect key="frame" x="18" y="157" width="256" height="17"/> <rect key="frame" x="30" y="250" width="234" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Game Boy revision:" id="GIA-ep-SBi"> <popUpButtonCell key="cell" type="push" title="Disabled" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="lxQ-4n-kEv" id="lvb-QF-0Ht">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LFw-Uk-cPR">
<rect key="frame" x="30" y="125" width="234" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="DMG-CPU B" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="2" imageScaling="proportionallyDown" inset="2" autoenablesItems="NO" selectedItem="aXT-sE-m5Z" id="FuX-Hc-uO7">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/> <font key="font" metaFont="menu"/>
<menu key="menu" autoenablesItems="NO" id="R0h-k6-Q5l"> <menu key="menu" id="lbS-Lw-kQX">
<items> <items>
<menuItem title="DMG-CPU 0" enabled="NO" id="pvp-Mo-9S8"/> <menuItem title="Disabled" state="on" id="lxQ-4n-kEv">
<menuItem title="DMG-CPU A" tag="1" enabled="NO" id="QLn-5i-Vlg"/> <modifierMask key="keyEquivalentModifierMask"/>
<menuItem title="DMG-CPU B" state="on" tag="2" id="aXT-sE-m5Z"/> </menuItem>
<menuItem title="DMG-CPU C" tag="3" enabled="NO" id="Ecl-YM-oAA"/> <menuItem title="10 Seconds" tag="10" id="bPU-vT-d5z"/>
<menuItem title="30 Seconds" tag="30" id="aR8-IU-fFh"/>
<menuItem title="1 Minute" tag="60" id="E0R-mf-Hdl"/>
<menuItem title="2 Minutes" tag="120" id="zb2-uh-lvj"/>
<menuItem title="5 Minutes" tag="300" id="6Jj-EI-f6k"/>
<menuItem title="10 Minutes" tag="600" id="DOL-qL-Caz"/>
</items> </items>
</menu> </menu>
</popUpButtonCell> </popUpButtonCell>
<connections> <connections>
<action selector="dmgModelChanged:" target="QvC-M9-y7g" id="FIe-Wz-aSc"/> <action selector="rewindLengthChanged:" target="QvC-M9-y7g" id="5NQ-1T-RNc"/>
</connections>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="MAq-1X-Gpo">
<rect key="frame" x="18" y="49" width="256" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Game Boy Color revision:" id="edD-t7-vwk">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="dlD-sk-SHO">
<rect key="frame" x="30" y="17" width="234" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="CPU-CGB E" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="517" imageScaling="proportionallyDown" inset="2" autoenablesItems="NO" selectedItem="3lF-1Q-2SS" id="Edt-1S-dXz">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" autoenablesItems="NO" id="bbF-hB-Hv7">
<items>
<menuItem title="CPU-CGB 0" tag="512" enabled="NO" id="2Uk-u3-6Gw"/>
<menuItem title="CPU-CGB A" tag="513" enabled="NO" id="axv-yk-RWM"/>
<menuItem title="CPU-CGB B" tag="514" enabled="NO" id="NtJ-oo-IM2"/>
<menuItem title="CPU-CGB C (Experimental)" tag="515" id="9YL-u8-12z"/>
<menuItem title="CPU-CGB D" tag="516" enabled="NO" id="c76-oF-fkU"/>
<menuItem title="CPU-CGB E" state="on" tag="517" id="3lF-1Q-2SS"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="cgbModelChanged:" target="QvC-M9-y7g" id="SY1-oj-VWQ"/>
</connections>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="tAa-0A-0fP">
<rect key="frame" x="18" y="103" width="256" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Super Game Boy model:" id="d0g-rk-FK0">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<box verticalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="mdm-eW-ia1">
<rect key="frame" x="12" y="180" width="268" height="5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</box>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="dza-T7-RkX">
<rect key="frame" x="30" y="71" width="234" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Super Game Boy (NTSC)" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="4" imageScaling="proportionallyDown" inset="2" autoenablesItems="NO" selectedItem="x5A-7f-ef9" id="2Mt-ci-bB0">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" autoenablesItems="NO" id="czw-Hi-jyM">
<items>
<menuItem title="Super Game Boy (NTSC)" state="on" tag="4" id="x5A-7f-ef9"/>
<menuItem title="Super Game Boy (PAL)" tag="4100" id="Cix-n2-l4L"/>
<menuItem title="Super Game Boy 2" tag="257" id="gZG-1g-KF0"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="sgbModelChanged:" target="QvC-M9-y7g" id="ybk-EV-WPS"/>
</connections> </connections>
</popUpButton> </popUpButton>
</subviews> </subviews>
<point key="canvasLocation" x="-176" y="848"/> <point key="canvasLocation" x="-501" y="667.5"/>
</customView> </customView>
<customView id="Zn1-Y5-RbR"> <customView id="Zn1-Y5-RbR">
<rect key="frame" x="0.0" y="0.0" width="292" height="86"/> <rect key="frame" x="0.0" y="0.0" width="292" height="183"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews> <subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Dzd-uB-B6l">
<rect key="frame" x="18" y="146" width="252" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Volume:" id="IbP-7k-xsZ">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<slider verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LNs-v1-Eki">
<rect key="frame" x="40" y="121" width="230" height="19"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<sliderCell key="cell" continuous="YES" state="on" alignment="left" maxValue="256" doubleValue="256" tickMarkPosition="below" sliderType="linear" id="Xvk-Vj-NHx"/>
<connections>
<action selector="volumeChanged:" target="QvC-M9-y7g" id="4g3-NK-ay4"/>
</connections>
</slider>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T69-6N-dhT"> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T69-6N-dhT">
<rect key="frame" x="30" y="17" width="233" height="26"/> <rect key="frame" x="30" y="65" width="234" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Disabled (Keep DC offset)" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Fgo-0S-zUG" id="om2-Bn-43B"> <popUpButtonCell key="cell" type="push" title="Disabled (Keep DC offset)" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Fgo-0S-zUG" id="om2-Bn-43B">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
@ -452,7 +541,7 @@
</connections> </connections>
</popUpButton> </popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WU3-oV-KHO"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="WU3-oV-KHO">
<rect key="frame" x="18" y="49" width="256" height="17"/> <rect key="frame" x="18" y="98" width="256" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="High-pass filter:" id="YLF-RL-b2D"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="High-pass filter:" id="YLF-RL-b2D">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -460,15 +549,32 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
</textField> </textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="GPt-9I-QBh">
<rect key="frame" x="18" y="43" width="252" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Interference volume:" id="I2Q-6U-uIx">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<slider verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="FpE-5i-j5L">
<rect key="frame" x="30" y="18" width="234" height="19"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<sliderCell key="cell" continuous="YES" state="on" alignment="left" maxValue="256" tickMarkPosition="below" sliderType="linear" id="Rbx-DU-xYf"/>
<connections>
<action selector="interferenceVolumeChanged:" target="QvC-M9-y7g" id="HFU-0q-hj1"/>
</connections>
</slider>
</subviews> </subviews>
<point key="canvasLocation" x="-176" y="890"/> <point key="canvasLocation" x="-825" y="446.5"/>
</customView> </customView>
<customView id="8TU-6J-NCg"> <customView id="8TU-6J-NCg">
<rect key="frame" x="0.0" y="0.0" width="292" height="467"/> <rect key="frame" x="0.0" y="0.0" width="292" height="467"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews> <subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Utu-t4-cLx"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Utu-t4-cLx">
<rect key="frame" x="10" y="430" width="122" height="17"/> <rect key="frame" x="18" y="430" width="122" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Control settings for" id="YqW-Ds-VIC"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Control settings for" id="YqW-Ds-VIC">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -486,14 +592,14 @@
</textFieldCell> </textFieldCell>
</textField> </textField>
<scrollView focusRingType="none" fixedFrame="YES" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" hasHorizontalScroller="NO" hasVerticalScroller="NO" usesPredominantAxisScrolling="NO" horizontalScrollElasticity="none" verticalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="PBp-dj-EIa"> <scrollView focusRingType="none" fixedFrame="YES" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" hasHorizontalScroller="NO" hasVerticalScroller="NO" usesPredominantAxisScrolling="NO" horizontalScrollElasticity="none" verticalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="PBp-dj-EIa">
<rect key="frame" x="32" y="208" width="240" height="211"/> <rect key="frame" x="32" y="208" width="229" height="211"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<clipView key="contentView" focusRingType="none" ambiguous="YES" drawsBackground="NO" id="AMs-PO-nid"> <clipView key="contentView" focusRingType="none" ambiguous="YES" drawsBackground="NO" id="AMs-PO-nid">
<rect key="frame" x="1" y="1" width="238" height="209"/> <rect key="frame" x="1" y="1" width="227" height="209"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<tableView focusRingType="none" verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnReordering="NO" columnResizing="NO" multipleSelection="NO" emptySelection="NO" autosaveColumns="NO" typeSelect="NO" id="UDd-IJ-fxX"> <tableView focusRingType="none" verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnReordering="NO" columnResizing="NO" multipleSelection="NO" emptySelection="NO" autosaveColumns="NO" typeSelect="NO" id="UDd-IJ-fxX">
<rect key="frame" x="0.0" y="0.0" width="238" height="209"/> <rect key="frame" x="0.0" y="0.0" width="227" height="209"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<size key="intercellSpacing" width="3" height="2"/> <size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -512,7 +618,7 @@
</textFieldCell> </textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
</tableColumn> </tableColumn>
<tableColumn width="116" minWidth="40" maxWidth="1000" id="5VG-zV-WM6"> <tableColumn width="105" minWidth="40" maxWidth="1000" id="5VG-zV-WM6">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border"> <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<font key="font" metaFont="smallSystem"/> <font key="font" metaFont="smallSystem"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
@ -553,7 +659,7 @@
</textFieldCell> </textFieldCell>
</textField> </textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0Az-0R-oNw"> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0Az-0R-oNw">
<rect key="frame" x="42" y="152" width="208" height="26"/> <rect key="frame" x="42" y="150" width="222" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="None" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingMiddle" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="hy8-cr-RrE" id="uEC-vN-8Jq"> <popUpButtonCell key="cell" type="push" title="None" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingMiddle" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="hy8-cr-RrE" id="uEC-vN-8Jq">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
@ -573,7 +679,7 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</box> </box>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ReM-uo-H0r"> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ReM-uo-H0r">
<rect key="frame" x="215" y="430" width="8" height="17"/> <rect key="frame" x="227" y="431" width="8" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title=":" id="VhO-3T-glt"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title=":" id="VhO-3T-glt">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
@ -582,7 +688,7 @@
</textFieldCell> </textFieldCell>
</textField> </textField>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gWx-7h-0xq"> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gWx-7h-0xq">
<rect key="frame" x="131" y="423" width="87" height="26"/> <rect key="frame" x="140" y="425" width="87" height="26"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Player 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="TO3-R7-9HN" id="pbt-Lr-bU1"> <popUpButtonCell key="cell" type="push" title="Player 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="TO3-R7-9HN" id="pbt-Lr-bU1">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
@ -601,7 +707,7 @@
</connections> </connections>
</popUpButton> </popUpButton>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ogs-xG-b4b"> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ogs-xG-b4b">
<rect key="frame" x="30" y="58" width="245" height="22"/> <rect key="frame" x="30" y="58" width="234" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="push" title="Never" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="jki-7x-bnM" id="o9b-MH-8kd"> <popUpButtonCell key="cell" type="push" title="Never" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="jki-7x-bnM" id="o9b-MH-8kd">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
@ -630,9 +736,9 @@
</connections> </connections>
</button> </button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="d2I-jU-sLb"> <button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="d2I-jU-sLb">
<rect key="frame" x="206" y="13" width="72" height="32"/> <rect key="frame" x="198" y="13" width="67" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Clear" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="sug-xy-tbw"> <buttonCell key="cell" type="push" title="Skip" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="sug-xy-tbw">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
</buttonCell> </buttonCell>
@ -641,9 +747,9 @@
</connections> </connections>
</button> </button>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Qa7-Z7-yfO"> <button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Qa7-Z7-yfO">
<rect key="frame" x="18" y="13" width="188" height="32"/> <rect key="frame" x="26" y="13" width="173" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Configure a Controller" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="GdK-tQ-Wim"> <buttonCell key="cell" type="push" title="Configure a controller" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="GdK-tQ-Wim">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
</buttonCell> </buttonCell>
@ -654,8 +760,42 @@
</subviews> </subviews>
<point key="canvasLocation" x="-159" y="1161.5"/> <point key="canvasLocation" x="-159" y="1161.5"/>
</customView> </customView>
<customView id="ffn-ie-9C3">
<rect key="frame" x="0.0" y="0.0" width="292" height="95"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ZVh-ob-6wl">
<rect key="frame" x="18" y="59" width="256" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="Check for updates on launch" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="euw-4z-Urd">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="changeAutoUpdates:" target="QvC-M9-y7g" id="MrL-Zh-0Cc"/>
</connections>
</button>
<progressIndicator wantsLayer="YES" fixedFrame="YES" maxValue="100" displayedWhenStopped="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="fB8-sd-zrh">
<rect key="frame" x="257" y="23" width="16" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</progressIndicator>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="KnI-UA-Nlj">
<rect key="frame" x="14" y="13" width="240" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Check for updates now" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="j8a-EZ-Ef5">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="userCheckForUpdates:" target="-2" id="ACZ-SI-yTQ"/>
</connections>
</button>
</subviews>
<point key="canvasLocation" x="-825" y="699.5"/>
</customView>
</objects> </objects>
<resources> <resources>
<image name="AppIcon" width="128" height="128"/>
<image name="CPU" width="32" height="32"/> <image name="CPU" width="32" height="32"/>
<image name="Display" width="32" height="32"/> <image name="Display" width="32" height="32"/>
<image name="Joypad" width="32" height="32"/> <image name="Joypad" width="32" height="32"/>

BIN
bsnes/gb/Cocoa/Previous.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
bsnes/gb/Cocoa/Rewind.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14868" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14868"/>
<plugIn identifier="com.apple.WebKitIBPlugin" version="14868"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="AppDelegate">
<connections>
<outlet property="updateChanges" destination="ATt-VM-cb5" id="hJj-Nd-FBv"/>
<outlet property="updateProgressButton" destination="7nO-AA-WmG" id="wTa-9l-cOG"/>
<outlet property="updateProgressLabel" destination="wIm-GX-c6B" id="URp-JG-6wR"/>
<outlet property="updateProgressSpinner" destination="fqq-Nb-THz" id="4vC-m5-ysO"/>
<outlet property="updateProgressWindow" destination="2Gy-QG-FoA" id="RXw-50-DQh"/>
<outlet property="updateWindow" destination="QvC-M9-y7g" id="iwP-kC-tmG"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Update Available" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g">
<windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="480" height="360"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="480" height="360"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="xZt-UI-Wl2">
<rect key="frame" x="338" y="13" width="128" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="push" title="Install Update" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Mav-rY-sJo">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
DQ
</string>
</buttonCell>
<connections>
<action selector="installUpdate:" target="-2" id="jJc-CY-4vz"/>
</connections>
</button>
<button verticalHuggingPriority="750" id="b3S-YN-iDZ">
<rect key="frame" x="242" y="13" width="96" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="push" title="Not Now" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="QsJ-cv-hwF">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="performClose:" target="QvC-M9-y7g" id="8qO-ac-k7U"/>
</connections>
</button>
<button verticalHuggingPriority="750" id="uOb-4Q-S8o">
<rect key="frame" x="14" y="13" width="128" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<buttonCell key="cell" type="push" title="Skip Version" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Ai5-6n-dvH">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="skipVersion:" target="-2" id="Jf8-Qe-6X0"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Swh-S6-6XA">
<rect key="frame" x="18" y="313" width="444" height="27"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="A new version of SameBoy is available with the following changes:" id="WsO-pC-VO7">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<box verticalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="9Tu-Q3-l40">
<rect key="frame" x="0.0" y="302" width="480" height="5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
</box>
<box verticalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="dva-Ay-nnl">
<rect key="frame" x="0.0" y="58" width="480" height="4"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
</box>
<webView maintainsBackForwardList="NO" id="ATt-VM-cb5">
<rect key="frame" x="0.0" y="61" width="480" height="243"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<webPreferences key="preferences" defaultFontSize="13" defaultFixedFontSize="13" minimumFontSize="0" plugInsEnabled="NO" javaEnabled="NO" javaScriptEnabled="NO" javaScriptCanOpenWindowsAutomatically="NO" loadsImagesAutomatically="NO" allowsAnimatedImages="NO" allowsAnimatedImageLooping="NO">
<nil key="identifier"/>
</webPreferences>
<connections>
<outlet property="UIDelegate" destination="-2" id="xQ1-eY-1hu"/>
<outlet property="frameLoadDelegate" destination="-2" id="BOf-df-5LR"/>
</connections>
</webView>
</subviews>
</view>
<point key="canvasLocation" x="217" y="267"/>
</window>
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" restorable="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="2Gy-QG-FoA">
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="283" y="305" width="512" height="61"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
<view key="contentView" id="jjT-Z5-15Q">
<rect key="frame" x="0.0" y="0.0" width="512" height="61"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<progressIndicator wantsLayer="YES" fixedFrame="YES" maxValue="100" displayedWhenStopped="NO" indeterminate="YES" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="fqq-Nb-THz">
<rect key="frame" x="20" y="15" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</progressIndicator>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7nO-AA-WmG">
<rect key="frame" x="417" y="13" width="82" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6l6-qX-gsr">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="updateAction:" target="-2" id="geO-Gk-xrs"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wIm-GX-c6B">
<rect key="frame" x="58" y="15" width="359" height="24"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Downloading update..." id="qmF-X1-v5B">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
</view>
<point key="canvasLocation" x="11" y="-203.5"/>
</window>
</objects>
</document>

File diff suppressed because it is too large Load Diff

View File

@ -46,6 +46,13 @@ enum GB_CHANNELS {
GB_N_CHANNELS GB_N_CHANNELS
}; };
typedef struct
{
bool locked:1;
bool clock:1; // Represents FOSY on channel 4
unsigned padding:6;
} GB_envelope_clock_t;
typedef void (*GB_sample_callback_t)(GB_gameboy_t *gb, GB_sample_t *sample); typedef void (*GB_sample_callback_t)(GB_gameboy_t *gb, GB_sample_t *sample);
typedef struct typedef struct
@ -64,10 +71,10 @@ typedef struct
uint8_t square_sweep_countdown; // In 128Hz uint8_t square_sweep_countdown; // In 128Hz
uint8_t square_sweep_calculate_countdown; // In 2 MHz uint8_t square_sweep_calculate_countdown; // In 2 MHz
uint16_t new_sweep_sample_length; uint16_t sweep_length_addend;
uint16_t shadow_sweep_sample_length; uint16_t shadow_sweep_sample_length;
bool sweep_enabled; bool unshifted_sweep;
bool sweep_decreasing; bool enable_zombie_calculate_stepping;
struct { struct {
uint16_t pulse_length; // Reloaded from NRX1 (xorred), in 256Hz DIV ticks uint16_t pulse_length; // Reloaded from NRX1 (xorred), in 256Hz DIV ticks
@ -105,8 +112,9 @@ typedef struct
uint16_t lfsr; uint16_t lfsr;
bool narrow; bool narrow;
uint16_t sample_countdown; // in APU ticks (Reloaded from sample_length) uint8_t counter_countdown; // Counts from 0-7 to 0 to tick counter (Scaled from 512KHz to 2MHz)
uint16_t sample_length; // From NR43, in APU ticks uint8_t __padding;
uint16_t counter; // A bit from this 14-bit register ticks LFSR
bool length_enabled; // NR44 bool length_enabled; // NR44
uint8_t alignment; // If (NR43 & 7) != 0, samples are aligned to 512KHz clock instead of uint8_t alignment; // If (NR43 & 7) != 0, samples are aligned to 512KHz clock instead of
@ -120,6 +128,14 @@ typedef struct
uint8_t skip_div_event; uint8_t skip_div_event;
bool current_lfsr_sample; bool current_lfsr_sample;
uint8_t pcm_mask[2]; // For CGB-0 to CGB-C PCM read glitch uint8_t pcm_mask[2]; // For CGB-0 to CGB-C PCM read glitch
uint8_t channel_1_restart_hold;
int8_t channel_4_delta;
bool channel_4_countdown_reloaded;
uint8_t channel_4_dmg_delayed_start;
uint16_t channel1_completed_addend;
GB_envelope_clock_t square_envelope_clock[2];
GB_envelope_clock_t noise_envelope_clock;
} GB_apu_t; } GB_apu_t;
typedef enum { typedef enum {
@ -149,17 +165,22 @@ typedef struct {
GB_sample_callback_t sample_callback; GB_sample_callback_t sample_callback;
bool rate_set_in_clocks; bool rate_set_in_clocks;
double interference_volume;
double interference_highpass;
} GB_apu_output_t; } GB_apu_output_t;
void GB_set_sample_rate(GB_gameboy_t *gb, unsigned sample_rate); void GB_set_sample_rate(GB_gameboy_t *gb, unsigned sample_rate);
void GB_set_sample_rate_by_clocks(GB_gameboy_t *gb, double cycles_per_sample); /* Cycles are in 8MHz units */ void GB_set_sample_rate_by_clocks(GB_gameboy_t *gb, double cycles_per_sample); /* Cycles are in 8MHz units */
void GB_set_highpass_filter_mode(GB_gameboy_t *gb, GB_highpass_mode_t mode); void GB_set_highpass_filter_mode(GB_gameboy_t *gb, GB_highpass_mode_t mode);
void GB_set_interference_volume(GB_gameboy_t *gb, double volume);
void GB_apu_set_sample_callback(GB_gameboy_t *gb, GB_sample_callback_t callback); void GB_apu_set_sample_callback(GB_gameboy_t *gb, GB_sample_callback_t callback);
#ifdef GB_INTERNAL #ifdef GB_INTERNAL
bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, unsigned index); bool GB_apu_is_DAC_enabled(GB_gameboy_t *gb, unsigned index);
void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value); void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value);
uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg); uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg);
void GB_apu_div_event(GB_gameboy_t *gb); void GB_apu_div_event(GB_gameboy_t *gb);
void GB_apu_div_secondary_event(GB_gameboy_t *gb);
void GB_apu_init(GB_gameboy_t *gb); void GB_apu_init(GB_gameboy_t *gb);
void GB_apu_run(GB_gameboy_t *gb); void GB_apu_run(GB_gameboy_t *gb);
void GB_apu_update_cycles_per_sample(GB_gameboy_t *gb); void GB_apu_update_cycles_per_sample(GB_gameboy_t *gb);

View File

@ -1,26 +1,26 @@
#include "gb.h" #include "gb.h"
static signed noise_seed = 0; static uint32_t noise_seed = 0;
/* This is not a complete emulation of the camera chip. Only the features used by the GameBoy Camera ROMs are supported. /* This is not a complete emulation of the camera chip. Only the features used by the Game Boy Camera ROMs are supported.
We also do not emulate the timing of the real cart, as it might be actually faster than the webcam. */ We also do not emulate the timing of the real cart, as it might be actually faster than the webcam. */
static uint8_t generate_noise(uint8_t x, uint8_t y) static uint8_t generate_noise(uint8_t x, uint8_t y)
{ {
signed value = (x + y * 128 + noise_seed); uint32_t value = (x * 151 + y * 149) ^ noise_seed;
uint8_t *data = (uint8_t *) &value; uint32_t hash = 0;
unsigned hash = 0;
while ((signed *) data != &value + 1) { while (value) {
hash ^= (*data << 8);
if (hash & 0x8000) {
hash ^= 0x8a00;
hash ^= *data;
}
data++;
hash <<= 1; hash <<= 1;
if (hash & 0x100) {
hash ^= 0x101;
}
if (value & 0x80000000) {
hash ^= 0xA1;
}
value <<= 1;
} }
return (hash >> 8); return hash;
} }
static long get_processed_color(GB_gameboy_t *gb, uint8_t x, uint8_t y) static long get_processed_color(GB_gameboy_t *gb, uint8_t x, uint8_t y)

View File

@ -69,7 +69,7 @@ void GB_add_cheat(GB_gameboy_t *gb, const char *description, uint16_t address, u
cheat->enabled = enabled; cheat->enabled = enabled;
strncpy(cheat->description, description, sizeof(cheat->description)); strncpy(cheat->description, description, sizeof(cheat->description));
cheat->description[sizeof(cheat->description) - 1] = 0; cheat->description[sizeof(cheat->description) - 1] = 0;
gb->cheats = realloc(gb->cheats, (++gb->cheat_count) * sizeof(*cheat)); gb->cheats = realloc(gb->cheats, (++gb->cheat_count) * sizeof(gb->cheats[0]));
gb->cheats[gb->cheat_count - 1] = cheat; gb->cheats[gb->cheat_count - 1] = cheat;
GB_cheat_hash_t **hash = &gb->cheat_hash[hash_addr(address)]; GB_cheat_hash_t **hash = &gb->cheat_hash[hash_addr(address)];
@ -100,7 +100,7 @@ void GB_remove_cheat(GB_gameboy_t *gb, const GB_cheat_t *cheat)
gb->cheats = NULL; gb->cheats = NULL;
} }
else { else {
gb->cheats = realloc(gb->cheats, gb->cheat_count * sizeof(*cheat)); gb->cheats = realloc(gb->cheats, gb->cheat_count * sizeof(gb->cheats[0]));
} }
break; break;
} }
@ -109,7 +109,7 @@ void GB_remove_cheat(GB_gameboy_t *gb, const GB_cheat_t *cheat)
GB_cheat_hash_t **hash = &gb->cheat_hash[hash_addr(cheat->address)]; GB_cheat_hash_t **hash = &gb->cheat_hash[hash_addr(cheat->address)];
for (unsigned i = 0; i < (*hash)->size; i++) { for (unsigned i = 0; i < (*hash)->size; i++) {
if ((*hash)->cheats[i] == cheat) { if ((*hash)->cheats[i] == cheat) {
(*hash)->cheats[i] = (*hash)->cheats[(*hash)->size--]; (*hash)->cheats[i] = (*hash)->cheats[--(*hash)->size];
if ((*hash)->size == 0) { if ((*hash)->size == 0) {
free(*hash); free(*hash);
*hash = NULL; *hash = NULL;
@ -200,7 +200,7 @@ void GB_update_cheat(GB_gameboy_t *gb, const GB_cheat_t *_cheat, const char *des
GB_cheat_hash_t **hash = &gb->cheat_hash[hash_addr(cheat->address)]; GB_cheat_hash_t **hash = &gb->cheat_hash[hash_addr(cheat->address)];
for (unsigned i = 0; i < (*hash)->size; i++) { for (unsigned i = 0; i < (*hash)->size; i++) {
if ((*hash)->cheats[i] == cheat) { if ((*hash)->cheats[i] == cheat) {
(*hash)->cheats[i] = (*hash)->cheats[(*hash)->size--]; (*hash)->cheats[i] = (*hash)->cheats[--(*hash)->size];
if ((*hash)->size == 0) { if ((*hash)->size == 0) {
free(*hash); free(*hash);
*hash = NULL; *hash = NULL;
@ -222,7 +222,7 @@ void GB_update_cheat(GB_gameboy_t *gb, const GB_cheat_t *_cheat, const char *des
} }
else { else {
(*hash)->size++; (*hash)->size++;
*hash = malloc(sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size); *hash = realloc(*hash, sizeof(GB_cheat_hash_t) + sizeof(cheat) * (*hash)->size);
(*hash)->cheats[(*hash)->size - 1] = cheat; (*hash)->cheats[(*hash)->size - 1] = cheat;
} }
} }

View File

@ -131,30 +131,25 @@ static const char *value_to_string(GB_gameboy_t *gb, uint16_t value, bool prefer
symbol = NULL; symbol = NULL;
} }
/* Avoid overflow */
if (symbol && strlen(symbol->name) >= 240) {
symbol = NULL;
}
if (!symbol) { if (!symbol) {
sprintf(output, "$%04x", value); snprintf(output, sizeof(output), "$%04x", value);
} }
else if (symbol->addr == value) { else if (symbol->addr == value) {
if (prefer_name) { if (prefer_name) {
sprintf(output, "%s ($%04x)", symbol->name, value); snprintf(output, sizeof(output), "%s ($%04x)", symbol->name, value);
} }
else { else {
sprintf(output, "$%04x (%s)", value, symbol->name); snprintf(output, sizeof(output), "$%04x (%s)", value, symbol->name);
} }
} }
else { else {
if (prefer_name) { if (prefer_name) {
sprintf(output, "%s+$%03x ($%04x)", symbol->name, value - symbol->addr, value); snprintf(output, sizeof(output), "%s+$%03x ($%04x)", symbol->name, value - symbol->addr, value);
} }
else { else {
sprintf(output, "$%04x (%s+$%03x)", value, symbol->name, value - symbol->addr); snprintf(output, sizeof(output), "$%04x (%s+$%03x)", value, symbol->name, value - symbol->addr);
} }
} }
return output; return output;
@ -171,30 +166,25 @@ static const char *debugger_value_to_string(GB_gameboy_t *gb, value_t value, boo
symbol = NULL; symbol = NULL;
} }
/* Avoid overflow */
if (symbol && strlen(symbol->name) >= 240) {
symbol = NULL;
}
if (!symbol) { if (!symbol) {
sprintf(output, "$%02x:$%04x", value.bank, value.value); snprintf(output, sizeof(output), "$%02x:$%04x", value.bank, value.value);
} }
else if (symbol->addr == value.value) { else if (symbol->addr == value.value) {
if (prefer_name) { if (prefer_name) {
sprintf(output, "%s ($%02x:$%04x)", symbol->name, value.bank, value.value); snprintf(output, sizeof(output), "%s ($%02x:$%04x)", symbol->name, value.bank, value.value);
} }
else { else {
sprintf(output, "$%02x:$%04x (%s)", value.bank, value.value, symbol->name); snprintf(output, sizeof(output), "$%02x:$%04x (%s)", value.bank, value.value, symbol->name);
} }
} }
else { else {
if (prefer_name) { if (prefer_name) {
sprintf(output, "%s+$%03x ($%02x:$%04x)", symbol->name, value.value - symbol->addr, value.bank, value.value); snprintf(output, sizeof(output), "%s+$%03x ($%02x:$%04x)", symbol->name, value.value - symbol->addr, value.bank, value.value);
} }
else { else {
sprintf(output, "$%02x:$%04x (%s+$%03x)", value.bank, value.value, symbol->name, value.value - symbol->addr); snprintf(output, sizeof(output), "$%02x:$%04x (%s+$%03x)", value.bank, value.value, symbol->name, value.value - symbol->addr);
} }
} }
return output; return output;
@ -473,7 +463,7 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string,
size_t length, bool *error, size_t length, bool *error,
uint16_t *watchpoint_address, uint8_t *watchpoint_new_value) uint16_t *watchpoint_address, uint8_t *watchpoint_new_value)
{ {
/* Disable watchpoints while evaulating expressions */ /* Disable watchpoints while evaluating expressions */
uint16_t n_watchpoints = gb->n_watchpoints; uint16_t n_watchpoints = gb->n_watchpoints;
gb->n_watchpoints = 0; gb->n_watchpoints = 0;
@ -1533,7 +1523,9 @@ static bool mbc(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg
const GB_cartridge_t *cartridge = gb->cartridge_type; const GB_cartridge_t *cartridge = gb->cartridge_type;
if (cartridge->has_ram) { if (cartridge->has_ram) {
GB_log(gb, "Cartridge includes%s RAM: $%x bytes\n", cartridge->has_battery? " battery-backed": "", gb->mbc_ram_size); bool has_battery = gb->cartridge_type->has_battery &&
(gb->cartridge_type->mbc_type != GB_TPP1 || (gb->rom[0x153] & 8));
GB_log(gb, "Cartridge includes%s RAM: $%x bytes\n", has_battery? " battery-backed": "", gb->mbc_ram_size);
} }
else { else {
GB_log(gb, "No cartridge RAM\n"); GB_log(gb, "No cartridge RAM\n");
@ -1575,7 +1567,8 @@ static bool mbc(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg
GB_log(gb, "No MBC\n"); GB_log(gb, "No MBC\n");
} }
if (cartridge->has_rumble) { if (gb->cartridge_type->has_rumble &&
(gb->cartridge_type->mbc_type != GB_TPP1 || (gb->rom[0x153] & 1))) {
GB_log(gb, "Cart contains a Rumble Pak\n"); GB_log(gb, "Cart contains a Rumble Pak\n");
} }
@ -1613,8 +1606,12 @@ static bool ticks(GB_gameboy_t *gb, char *arguments, char *modifiers, const debu
return true; return true;
} }
GB_log(gb, "Ticks: %llu. (Resetting)\n", (unsigned long long)gb->debugger_ticks); GB_log(gb, "T-cycles: %llu\n", (unsigned long long)gb->debugger_ticks);
GB_log(gb, "M-cycles: %llu\n", (unsigned long long)gb->debugger_ticks / 4);
GB_log(gb, "Absolute 8MHz ticks: %llu\n", (unsigned long long)gb->absolute_debugger_ticks);
GB_log(gb, "Tick count reset.\n");
gb->debugger_ticks = 0; gb->debugger_ticks = 0;
gb->absolute_debugger_ticks = 0;
return true; return true;
} }
@ -1778,10 +1775,17 @@ static bool apu(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg
gb->apu.square_channels[channel].current_sample_index >> 7 ? " (suppressed)" : ""); gb->apu.square_channels[channel].current_sample_index >> 7 ? " (suppressed)" : "");
if (channel == GB_SQUARE_1) { if (channel == GB_SQUARE_1) {
GB_log(gb, " Frequency sweep %s and %s (next in %u APU ticks)\n", GB_log(gb, " Frequency sweep %s and %s\n",
gb->apu.sweep_enabled? "active" : "inactive", ((gb->io_registers[GB_IO_NR10] & 0x7) && (gb->io_registers[GB_IO_NR10] & 0x70))? "active" : "inactive",
gb->apu.sweep_decreasing? "decreasing" : "increasing", (gb->io_registers[GB_IO_NR10] & 0x8) ? "decreasing" : "increasing");
gb->apu.square_sweep_calculate_countdown); if (gb->apu.square_sweep_calculate_countdown) {
GB_log(gb, " On going frequency calculation will be ready in %u APU ticks\n",
gb->apu.square_sweep_calculate_countdown);
}
else {
GB_log(gb, " Shadow frequency register: 0x%03x\n", gb->apu.shadow_sweep_sample_length);
GB_log(gb, " Sweep addend register: 0x%03x\n", gb->apu.sweep_length_addend);
}
} }
if (gb->apu.square_channels[channel].length_enabled) { if (gb->apu.square_channels[channel].length_enabled) {
@ -1814,10 +1818,10 @@ static bool apu(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg
GB_log(gb, "\nCH4:\n"); GB_log(gb, "\nCH4:\n");
GB_log(gb, " Current volume: %u, current sample length: %u APU ticks (next in %u ticks)\n", GB_log(gb, " Current volume: %u, current internal counter: 0x%04x (next increase in %u ticks)\n",
gb->apu.noise_channel.current_volume, gb->apu.noise_channel.current_volume,
gb->apu.noise_channel.sample_length * 4 + 3, gb->apu.noise_channel.counter,
gb->apu.noise_channel.sample_countdown); gb->apu.noise_channel.counter_countdown);
GB_log(gb, " %u 256 Hz ticks till next volume %screase (out of %u)\n", GB_log(gb, " %u 256 Hz ticks till next volume %screase (out of %u)\n",
gb->apu.noise_channel.volume_countdown, gb->apu.noise_channel.volume_countdown,
@ -1889,6 +1893,29 @@ static bool wave(GB_gameboy_t *gb, char *arguments, char *modifiers, const debug
return true; return true;
} }
static bool undo(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command)
{
NO_MODIFIERS
if (strlen(lstrip(arguments))) {
print_usage(gb, command);
return true;
}
if (!gb->undo_label) {
GB_log(gb, "No undo state available\n");
return true;
}
uint16_t pc = gb->pc;
GB_load_state_from_buffer(gb, gb->undo_state, GB_get_save_state_size_no_bess(gb));
GB_log(gb, "Reverted a \"%s\" command.\n", gb->undo_label);
if (pc != gb->pc) {
GB_cpu_disassemble(gb, gb->pc, 5);
}
gb->undo_label = NULL;
return true;
}
static bool help(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command); static bool help(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugger_command_t *command);
#define HELP_NEWLINE "\n " #define HELP_NEWLINE "\n "
@ -1899,6 +1926,7 @@ static const debugger_command_t commands[] = {
{"next", 1, next, "Run the next instruction, skipping over function calls"}, {"next", 1, next, "Run the next instruction, skipping over function calls"},
{"step", 1, step, "Run the next instruction, stepping into function calls"}, {"step", 1, step, "Run the next instruction, stepping into function calls"},
{"finish", 1, finish, "Run until the current function returns"}, {"finish", 1, finish, "Run until the current function returns"},
{"undo", 1, undo, "Reverts the last command"},
{"backtrace", 2, backtrace, "Displays the current call stack"}, {"backtrace", 2, backtrace, "Displays the current call stack"},
{"bt", 2, }, /* Alias */ {"bt", 2, }, /* Alias */
{"sld", 3, stack_leak_detection, "Like finish, but stops if a stack leak is detected"}, {"sld", 3, stack_leak_detection, "Like finish, but stops if a stack leak is detected"},
@ -2184,7 +2212,30 @@ bool GB_debugger_execute_command(GB_gameboy_t *gb, char *input)
const debugger_command_t *command = find_command(command_string); const debugger_command_t *command = find_command(command_string);
if (command) { if (command) {
return command->implementation(gb, arguments, modifiers, command); uint8_t *old_state = malloc(GB_get_save_state_size_no_bess(gb));
GB_save_state_to_buffer_no_bess(gb, old_state);
bool ret = command->implementation(gb, arguments, modifiers, command);
if (!ret) { // Command continues, save state in any case
free(gb->undo_state);
gb->undo_state = old_state;
gb->undo_label = command->command;
}
else {
uint8_t *new_state = malloc(GB_get_save_state_size_no_bess(gb));
GB_save_state_to_buffer_no_bess(gb, new_state);
if (memcmp(new_state, old_state, GB_get_save_state_size_no_bess(gb)) != 0) {
// State changed, save the old state as the new undo state
free(gb->undo_state);
gb->undo_state = old_state;
gb->undo_label = command->command;
}
else {
// Nothing changed, just free the old state
free(old_state);
}
free(new_state);
}
return ret;
} }
else { else {
GB_log(gb, "%s: no such command.\n", command_string); GB_log(gb, "%s: no such command.\n", command_string);
@ -2260,6 +2311,11 @@ static jump_to_return_t test_jump_to_breakpoints(GB_gameboy_t *gb, uint16_t *add
void GB_debugger_run(GB_gameboy_t *gb) void GB_debugger_run(GB_gameboy_t *gb)
{ {
if (gb->debug_disable) return; if (gb->debug_disable) return;
if (!gb->undo_state) {
gb->undo_state = malloc(GB_get_save_state_size_no_bess(gb));
GB_save_state_to_buffer_no_bess(gb, gb->undo_state);
}
char *input = NULL; char *input = NULL;
if (gb->debug_next_command && gb->debug_call_depth <= 0 && !gb->halted) { if (gb->debug_next_command && gb->debug_call_depth <= 0 && !gb->halted) {
@ -2306,9 +2362,9 @@ next_command:
} }
else if (jump_to_result == JUMP_TO_NONTRIVIAL) { else if (jump_to_result == JUMP_TO_NONTRIVIAL) {
if (!gb->nontrivial_jump_state) { if (!gb->nontrivial_jump_state) {
gb->nontrivial_jump_state = malloc(GB_get_save_state_size(gb)); gb->nontrivial_jump_state = malloc(GB_get_save_state_size_no_bess(gb));
} }
GB_save_state_to_buffer(gb, gb->nontrivial_jump_state); GB_save_state_to_buffer_no_bess(gb, gb->nontrivial_jump_state);
gb->non_trivial_jump_breakpoint_occured = false; gb->non_trivial_jump_breakpoint_occured = false;
should_delete_state = false; should_delete_state = false;
} }

View File

@ -2,6 +2,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <assert.h> #include <assert.h>
#include <string.h> #include <string.h>
#include <math.h>
#include "gb.h" #include "gb.h"
/* FIFO functions */ /* FIFO functions */
@ -27,8 +28,7 @@ static GB_fifo_item_t *fifo_pop(GB_fifo_t *fifo)
static void fifo_push_bg_row(GB_fifo_t *fifo, uint8_t lower, uint8_t upper, uint8_t palette, bool bg_priority, bool flip_x) static void fifo_push_bg_row(GB_fifo_t *fifo, uint8_t lower, uint8_t upper, uint8_t palette, bool bg_priority, bool flip_x)
{ {
if (!flip_x) { if (!flip_x) {
UNROLL unrolled for (unsigned i = 8; i--;) {
for (unsigned i = 8; i--;) {
fifo->fifo[fifo->write_end] = (GB_fifo_item_t) { fifo->fifo[fifo->write_end] = (GB_fifo_item_t) {
(lower >> 7) | ((upper >> 7) << 1), (lower >> 7) | ((upper >> 7) << 1),
palette, palette,
@ -43,8 +43,7 @@ static void fifo_push_bg_row(GB_fifo_t *fifo, uint8_t lower, uint8_t upper, uint
} }
} }
else { else {
UNROLL unrolled for (unsigned i = 8; i--;) {
for (unsigned i = 8; i--;) {
fifo->fifo[fifo->write_end] = (GB_fifo_item_t) { fifo->fifo[fifo->write_end] = (GB_fifo_item_t) {
(lower & 1) | ((upper & 1) << 1), (lower & 1) | ((upper & 1) << 1),
palette, palette,
@ -70,8 +69,7 @@ static void fifo_overlay_object_row(GB_fifo_t *fifo, uint8_t lower, uint8_t uppe
uint8_t flip_xor = flip_x? 0: 0x7; uint8_t flip_xor = flip_x? 0: 0x7;
UNROLL unrolled for (unsigned i = 8; i--;) {
for (unsigned i = 8; i--;) {
uint8_t pixel = (lower >> 7) | ((upper >> 7) << 1); uint8_t pixel = (lower >> 7) | ((upper >> 7) << 1);
GB_fifo_item_t *target = &fifo->fifo[(fifo->read_end + (i ^ flip_xor)) & (GB_FIFO_LENGTH - 1)]; GB_fifo_item_t *target = &fifo->fifo[(fifo->read_end + (i ^ flip_xor)) & (GB_FIFO_LENGTH - 1)];
if (pixel != 0 && (target->pixel == 0 || target->priority > priority)) { if (pixel != 0 && (target->pixel == 0 || target->priority > priority)) {
@ -128,7 +126,7 @@ static void display_vblank(GB_gameboy_t *gb)
bool is_ppu_stopped = !GB_is_cgb(gb) && gb->stopped && gb->io_registers[GB_IO_LCDC] & 0x80; bool is_ppu_stopped = !GB_is_cgb(gb) && gb->stopped && gb->io_registers[GB_IO_LCDC] & 0x80;
if (!gb->disable_rendering && ((!(gb->io_registers[GB_IO_LCDC] & 0x80) || is_ppu_stopped) || gb->cgb_repeated_a_frame)) { if (!gb->disable_rendering && ((!(gb->io_registers[GB_IO_LCDC] & 0x80) || is_ppu_stopped) || gb->cgb_repeated_a_frame)) {
/* LCD is off, set screen to white or black (if LCD is on in stop mode) */ /* LCD is off, set screen to white or black (if LCD is on in stop mode) */
if (!GB_is_sgb(gb)) { if (!GB_is_sgb(gb)) {
uint32_t color = 0; uint32_t color = 0;
@ -155,25 +153,25 @@ static void display_vblank(GB_gameboy_t *gb)
} }
} }
if (gb->border_mode == GB_BORDER_ALWAYS && !GB_is_sgb(gb)) { if (!gb->disable_rendering && gb->border_mode == GB_BORDER_ALWAYS && !GB_is_sgb(gb)) {
GB_borrow_sgb_border(gb); GB_borrow_sgb_border(gb);
uint32_t border_colors[16 * 4]; uint32_t border_colors[16 * 4];
if (!gb->has_sgb_border && GB_is_cgb(gb) && gb->model != GB_MODEL_AGB) { if (!gb->has_sgb_border && GB_is_cgb(gb) && gb->model != GB_MODEL_AGB) {
static uint16_t colors[] = { uint16_t colors[] = {
0x2095, 0x5129, 0x1EAF, 0x1EBA, 0x4648, 0x2095, 0x5129, 0x1EAF, 0x1EBA, 0x4648,
0x30DA, 0x69AD, 0x2B57, 0x2B5D, 0x632C, 0x30DA, 0x69AD, 0x2B57, 0x2B5D, 0x632C,
0x1050, 0x3C84, 0x0E07, 0x0E18, 0x2964, 0x1050, 0x3C84, 0x0E07, 0x0E18, 0x2964,
}; };
unsigned index = gb->rom? gb->rom[0x14e] % 5 : 0; unsigned index = gb->rom? gb->rom[0x14e] % 5 : 0;
gb->borrowed_border.palette[0] = colors[index]; gb->borrowed_border.palette[0] = LE16(colors[index]);
gb->borrowed_border.palette[10] = colors[5 + index]; gb->borrowed_border.palette[10] = LE16(colors[5 + index]);
gb->borrowed_border.palette[14] = colors[10 + index]; gb->borrowed_border.palette[14] = LE16(colors[10 + index]);
} }
for (unsigned i = 0; i < 16 * 4; i++) { for (unsigned i = 0; i < 16 * 4; i++) {
border_colors[i] = GB_convert_rgb15(gb, gb->borrowed_border.palette[i], true); border_colors[i] = GB_convert_rgb15(gb, LE16(gb->borrowed_border.palette[i]), true);
} }
for (unsigned tile_y = 0; tile_y < 28; tile_y++) { for (unsigned tile_y = 0; tile_y < 28; tile_y++) {
@ -181,13 +179,18 @@ static void display_vblank(GB_gameboy_t *gb)
if (tile_x >= 6 && tile_x < 26 && tile_y >= 5 && tile_y < 23) { if (tile_x >= 6 && tile_x < 26 && tile_y >= 5 && tile_y < 23) {
continue; continue;
} }
uint16_t tile = gb->borrowed_border.map[tile_x + tile_y * 32]; uint16_t tile = LE16(gb->borrowed_border.map[tile_x + tile_y * 32]);
uint8_t flip_x = (tile & 0x4000)? 0x7 : 0; uint8_t flip_x = (tile & 0x4000)? 0:7;
uint8_t flip_y = (tile & 0x8000)? 0x7 : 0; uint8_t flip_y = (tile & 0x8000)? 7:0;
uint8_t palette = (tile >> 10) & 3; uint8_t palette = (tile >> 10) & 3;
for (unsigned y = 0; y < 8; y++) { for (unsigned y = 0; y < 8; y++) {
unsigned base = (tile & 0xFF) * 32 + (y ^ flip_y) * 2;
for (unsigned x = 0; x < 8; x++) { for (unsigned x = 0; x < 8; x++) {
uint8_t color = gb->borrowed_border.tiles[(tile & 0xFF) * 64 + (x ^ flip_x) + (y ^ flip_y) * 8] & 0xF; uint8_t bit = 1 << (x ^ flip_x);
uint8_t color = ((gb->borrowed_border.tiles[base] & bit) ? 1 : 0) |
((gb->borrowed_border.tiles[base + 1] & bit) ? 2 : 0) |
((gb->borrowed_border.tiles[base + 16] & bit) ? 4 : 0) |
((gb->borrowed_border.tiles[base + 17] & bit) ? 8 : 0);
uint32_t *output = gb->screen + tile_x * 8 + x + (tile_y * 8 + y) * 256; uint32_t *output = gb->screen + tile_x * 8 + x + (tile_y * 8 + y) * 256;
if (color == 0) { if (color == 0) {
*output = border_colors[0]; *output = border_colors[0];
@ -208,6 +211,26 @@ static void display_vblank(GB_gameboy_t *gb)
GB_timing_sync(gb); GB_timing_sync(gb);
} }
static inline void temperature_tint(double temperature, double *r, double *g, double *b)
{
if (temperature >= 0) {
*r = 1;
*g = pow(1 - temperature, 0.375);
if (temperature >= 0.75) {
*b = 0;
}
else {
*b = sqrt(0.75 - temperature);
}
}
else {
*b = 1;
double squared = pow(temperature, 2);
*g = 0.125 * squared + 0.3 * temperature + 1.0;
*r = 0.21875 * squared + 0.5 * temperature + 1.0;
}
}
static inline uint8_t scale_channel(uint8_t x) static inline uint8_t scale_channel(uint8_t x)
{ {
return (x << 3) | (x >> 2); return (x << 3) | (x >> 2);
@ -215,12 +238,12 @@ static inline uint8_t scale_channel(uint8_t x)
static inline uint8_t scale_channel_with_curve(uint8_t x) static inline uint8_t scale_channel_with_curve(uint8_t x)
{ {
return (uint8_t[]){0,5,8,11,16,22,28,36,43,51,59,67,77,87,97,107,119,130,141,153,166,177,188,200,209,221,230,238,245,249,252,255}[x]; return (uint8_t[]){0,6,12,20,28,36,45,56,66,76,88,100,113,125,137,149,161,172,182,192,202,210,218,225,232,238,243,247,250,252,254,255}[x];
} }
static inline uint8_t scale_channel_with_curve_agb(uint8_t x) static inline uint8_t scale_channel_with_curve_agb(uint8_t x)
{ {
return (uint8_t[]){0,2,5,10,15,20,26,32,38,45,52,60,68,76,84,92,101,110,119,128,138,148,158,168,178,189,199,210,221,232,244,255}[x]; return (uint8_t[]){0,3,8,14,20,26,33,40,47,54,62,70,78,86,94,103,112,120,129,138,147,157,166,176,185,195,205,215,225,235,245,255}[x];
} }
static inline uint8_t scale_channel_with_curve_sgb(uint8_t x) static inline uint8_t scale_channel_with_curve_sgb(uint8_t x)
@ -240,13 +263,12 @@ uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border)
g = scale_channel(g); g = scale_channel(g);
b = scale_channel(b); b = scale_channel(b);
} }
else if (GB_is_sgb(gb) || for_border) {
r = scale_channel_with_curve_sgb(r);
g = scale_channel_with_curve_sgb(g);
b = scale_channel_with_curve_sgb(b);
}
else { else {
if (GB_is_sgb(gb) || for_border) {
return gb->rgb_encode_callback(gb,
scale_channel_with_curve_sgb(r),
scale_channel_with_curve_sgb(g),
scale_channel_with_curve_sgb(b));
}
bool agb = gb->model == GB_MODEL_AGB; bool agb = gb->model == GB_MODEL_AGB;
r = agb? scale_channel_with_curve_agb(r) : scale_channel_with_curve(r); r = agb? scale_channel_with_curve_agb(r) : scale_channel_with_curve(r);
g = agb? scale_channel_with_curve_agb(g) : scale_channel_with_curve(g); g = agb? scale_channel_with_curve_agb(g) : scale_channel_with_curve(g);
@ -270,12 +292,24 @@ uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border)
new_r = new_r * 7 / 8 + ( g + b) / 16; new_r = new_r * 7 / 8 + ( g + b) / 16;
new_g = new_g * 7 / 8 + (r + b) / 16; new_g = new_g * 7 / 8 + (r + b) / 16;
new_b = new_b * 7 / 8 + (r + g ) / 16; new_b = new_b * 7 / 8 + (r + g ) / 16;
new_r = new_r * (224 - 32) / 255 + 32; new_r = new_r * (224 - 32) / 255 + 32;
new_g = new_g * (220 - 36) / 255 + 36; new_g = new_g * (220 - 36) / 255 + 36;
new_b = new_b * (216 - 40) / 255 + 40; new_b = new_b * (216 - 40) / 255 + 40;
} }
else if (gb->color_correction_mode == GB_COLOR_CORRECTION_LOW_CONTRAST) {
r = new_r;
g = new_r;
b = new_r;
new_r = new_r * 7 / 8 + ( g + b) / 16;
new_g = new_g * 7 / 8 + (r + b) / 16;
new_b = new_b * 7 / 8 + (r + g ) / 16;
new_r = new_r * (162 - 67) / 255 + 67;
new_g = new_g * (167 - 62) / 255 + 62;
new_b = new_b * (157 - 58) / 255 + 58;
}
else if (gb->color_correction_mode == GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS) { else if (gb->color_correction_mode == GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS) {
uint8_t old_max = MAX(r, MAX(g, b)); uint8_t old_max = MAX(r, MAX(g, b));
uint8_t new_max = MAX(new_r, MAX(new_g, new_b)); uint8_t new_max = MAX(new_r, MAX(new_g, new_b));
@ -301,6 +335,14 @@ uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border)
} }
} }
if (gb->light_temperature) {
double light_r, light_g, light_b;
temperature_tint(gb->light_temperature, &light_r, &light_g, &light_b);
r = round(light_r * r);
g = round(light_g * g);
b = round(light_b * b);
}
return gb->rgb_encode_callback(gb, r, g, b); return gb->rgb_encode_callback(gb, r, g, b);
} }
@ -324,6 +366,17 @@ void GB_set_color_correction_mode(GB_gameboy_t *gb, GB_color_correction_mode_t m
} }
} }
void GB_set_light_temperature(GB_gameboy_t *gb, double temperature)
{
gb->light_temperature = temperature;
if (GB_is_cgb(gb)) {
for (unsigned i = 0; i < 32; i++) {
GB_palette_changed(gb, false, i * 2);
GB_palette_changed(gb, true, i * 2);
}
}
}
/* /*
STAT interrupt is implemented based on this finding: STAT interrupt is implemented based on this finding:
http://board.byuu.org/phpbb3/viewtopic.php?p=25527#p25531 http://board.byuu.org/phpbb3/viewtopic.php?p=25527#p25531
@ -430,6 +483,23 @@ static void add_object_from_index(GB_gameboy_t *gb, unsigned index)
} }
} }
static uint8_t data_for_tile_sel_glitch(GB_gameboy_t *gb, bool *should_use)
{
/*
Based on Matt Currie's research here:
https://github.com/mattcurrie/mealybug-tearoom-tests/blob/master/the-comprehensive-game-boy-ppu-documentation.md#tile_sel-bit-4
*/
*should_use = true;
if (gb->io_registers[GB_IO_LCDC] & 0x10) {
*should_use = !(gb->current_tile & 0x80);
/* if (gb->model != GB_MODEL_CGB_D) */ return gb->current_tile;
// TODO: CGB D behaves differently
}
return gb->data_for_sel_glitch;
}
static void render_pixel_if_possible(GB_gameboy_t *gb) static void render_pixel_if_possible(GB_gameboy_t *gb)
{ {
GB_fifo_item_t *fifo_item = NULL; GB_fifo_item_t *fifo_item = NULL;
@ -521,7 +591,6 @@ static void render_pixel_if_possible(GB_gameboy_t *gb)
else if (gb->model & GB_MODEL_NO_SFC_BIT) { else if (gb->model & GB_MODEL_NO_SFC_BIT) {
if (gb->icd_pixel_callback) { if (gb->icd_pixel_callback) {
icd_pixel = pixel; icd_pixel = pixel;
//gb->icd_pixel_callback(gb, pixel);
} }
} }
else if (gb->cgb_palettes_ppu_blocked) { else if (gb->cgb_palettes_ppu_blocked) {
@ -621,6 +690,10 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb)
break; break;
case GB_FETCHER_GET_TILE_DATA_LOWER: { case GB_FETCHER_GET_TILE_DATA_LOWER: {
bool use_glitched = false;
if (gb->tile_sel_glitch) {
gb->current_tile_data[0] = data_for_tile_sel_glitch(gb, &use_glitched);
}
uint8_t y_flip = 0; uint8_t y_flip = 0;
uint16_t tile_address = 0; uint16_t tile_address = 0;
uint8_t y = gb->model > GB_MODEL_CGB_C ? gb->fetcher_y : fetcher_y(gb); uint8_t y = gb->model > GB_MODEL_CGB_C ? gb->fetcher_y : fetcher_y(gb);
@ -638,20 +711,32 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb)
if (gb->current_tile_attributes & 0x40) { if (gb->current_tile_attributes & 0x40) {
y_flip = 0x7; y_flip = 0x7;
} }
gb->current_tile_data[0] = if (!use_glitched) {
gb->current_tile_data[0] =
gb->vram[tile_address + ((y & 7) ^ y_flip) * 2];
if (gb->vram_ppu_blocked) {
gb->current_tile_data[0] = 0xFF;
}
}
else {
gb->data_for_sel_glitch =
gb->vram[tile_address + ((y & 7) ^ y_flip) * 2]; gb->vram[tile_address + ((y & 7) ^ y_flip) * 2];
if (gb->vram_ppu_blocked) { if (gb->vram_ppu_blocked) {
gb->current_tile_data[0] = 0xFF; gb->data_for_sel_glitch = 0xFF;
}
} }
} }
gb->fetcher_state++; gb->fetcher_state++;
break; break;
case GB_FETCHER_GET_TILE_DATA_HIGH: { case GB_FETCHER_GET_TILE_DATA_HIGH: {
/* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. /* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. */
Additionally, on CGB-D and newer mixing two tiles by changing the tileset
bit mid-fetching causes a glitched mixing of the two, in comparison to the bool use_glitched = false;
more logical DMG version. */ if (gb->tile_sel_glitch) {
gb->current_tile_data[1] = data_for_tile_sel_glitch(gb, &use_glitched);
}
uint16_t tile_address = 0; uint16_t tile_address = 0;
uint8_t y = gb->model > GB_MODEL_CGB_C ? gb->fetcher_y : fetcher_y(gb); uint8_t y = gb->model > GB_MODEL_CGB_C ? gb->fetcher_y : fetcher_y(gb);
@ -669,10 +754,20 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb)
y_flip = 0x7; y_flip = 0x7;
} }
gb->last_tile_data_address = tile_address + ((y & 7) ^ y_flip) * 2 + 1; gb->last_tile_data_address = tile_address + ((y & 7) ^ y_flip) * 2 + 1;
gb->current_tile_data[1] = if (!use_glitched) {
gb->vram[gb->last_tile_data_address]; gb->current_tile_data[1] =
if (gb->vram_ppu_blocked) { gb->vram[gb->last_tile_data_address];
gb->current_tile_data[1] = 0xFF; if (gb->vram_ppu_blocked) {
gb->current_tile_data[1] = 0xFF;
}
}
else {
if ((gb->io_registers[GB_IO_LCDC] & 0x10) && gb->tile_sel_glitch) {
gb->data_for_sel_glitch = gb->vram[gb->last_tile_data_address];
if (gb->vram_ppu_blocked) {
gb->data_for_sel_glitch = 0xFF;
}
}
} }
} }
if (gb->wx_triggered) { if (gb->wx_triggered) {
@ -771,14 +866,14 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles)
GB_STATE(gb, display, 21); GB_STATE(gb, display, 21);
GB_STATE(gb, display, 22); GB_STATE(gb, display, 22);
GB_STATE(gb, display, 23); GB_STATE(gb, display, 23);
// GB_STATE(gb, display, 24); GB_STATE(gb, display, 24);
GB_STATE(gb, display, 25); GB_STATE(gb, display, 25);
GB_STATE(gb, display, 26); GB_STATE(gb, display, 26);
GB_STATE(gb, display, 27); GB_STATE(gb, display, 27);
GB_STATE(gb, display, 28); GB_STATE(gb, display, 28);
GB_STATE(gb, display, 29); GB_STATE(gb, display, 29);
GB_STATE(gb, display, 30); GB_STATE(gb, display, 30);
// GB_STATE(gb, display, 31); GB_STATE(gb, display, 31);
GB_STATE(gb, display, 32); GB_STATE(gb, display, 32);
GB_STATE(gb, display, 33); GB_STATE(gb, display, 33);
GB_STATE(gb, display, 34); GB_STATE(gb, display, 34);
@ -810,13 +905,7 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles)
/* Handle mode 2 on the very first line 0 */ /* Handle mode 2 on the very first line 0 */
gb->current_line = 0; gb->current_line = 0;
gb->window_y = -1; gb->window_y = -1;
/* Todo: verify timings */ gb->wy_triggered = false;
if (gb->io_registers[GB_IO_WY] == 0) {
gb->wy_triggered = true;
}
else {
gb->wy_triggered = false;
}
gb->ly_for_comparison = 0; gb->ly_for_comparison = 0;
gb->io_registers[GB_IO_STAT] &= ~3; gb->io_registers[GB_IO_STAT] &= ~3;
@ -867,11 +956,6 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles)
/* Lines 0 - 143 */ /* Lines 0 - 143 */
gb->window_y = -1; gb->window_y = -1;
for (; gb->current_line < LINES; gb->current_line++) { for (; gb->current_line < LINES; gb->current_line++) {
/* Todo: verify timings */
if ((gb->io_registers[GB_IO_WY] == gb->current_line ||
(gb->current_line != 0 && gb->io_registers[GB_IO_WY] == gb->current_line - 1))) {
gb->wy_triggered = true;
}
gb->oam_write_blocked = GB_is_cgb(gb) && !gb->cgb_double_speed; gb->oam_write_blocked = GB_is_cgb(gb) && !gb->cgb_double_speed;
gb->accessed_oam_row = 0; gb->accessed_oam_row = 0;
@ -951,6 +1035,11 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles)
gb->cycles_for_line += 2; gb->cycles_for_line += 2;
GB_SLEEP(gb, display, 32, 2); GB_SLEEP(gb, display, 32, 2);
mode_3_start: mode_3_start:
/* TODO: Timing seems incorrect, might need an access conflict handling. */
if ((gb->io_registers[GB_IO_LCDC] & 0x20) &&
gb->io_registers[GB_IO_WY] == gb->current_line) {
gb->wy_triggered = true;
}
fifo_clear(&gb->bg_fifo); fifo_clear(&gb->bg_fifo);
fifo_clear(&gb->oam_fifo); fifo_clear(&gb->oam_fifo);
@ -1112,7 +1201,8 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles)
object->flags & 0x80, object->flags & 0x80,
gb->object_priority == GB_OBJECT_PRIORITY_INDEX? gb->visible_objs[gb->n_visible_objs - 1] : 0, gb->object_priority == GB_OBJECT_PRIORITY_INDEX? gb->visible_objs[gb->n_visible_objs - 1] : 0,
object->flags & 0x20); object->flags & 0x20);
gb->data_for_sel_glitch = gb->vram_ppu_blocked? 0xFF : gb->vram[line_address + 1];
gb->n_visible_objs--; gb->n_visible_objs--;
} }
@ -1128,6 +1218,14 @@ abort_fetching_object:
GB_SLEEP(gb, display, 21, 1); GB_SLEEP(gb, display, 21, 1);
} }
/* TODO: Verify */
if (gb->fetcher_state == 4 || gb->fetcher_state == 5) {
gb->data_for_sel_glitch = gb->current_tile_data[0];
}
else {
gb->data_for_sel_glitch = gb->current_tile_data[1];
}
while (gb->lcd_x != 160 && !gb->disable_rendering && gb->screen && !gb->sgb) { while (gb->lcd_x != 160 && !gb->disable_rendering && gb->screen && !gb->sgb) {
/* Oh no! The PPU and LCD desynced! Fill the rest of the line whith white. */ /* Oh no! The PPU and LCD desynced! Fill the rest of the line whith white. */
uint32_t *dest = NULL; uint32_t *dest = NULL;
@ -1191,8 +1289,19 @@ abort_fetching_object:
if (gb->hdma_on_hblank) { if (gb->hdma_on_hblank) {
gb->hdma_starting = true; gb->hdma_starting = true;
} }
GB_SLEEP(gb, display, 11, LINE_LENGTH - gb->cycles_for_line); GB_SLEEP(gb, display, 11, LINE_LENGTH - gb->cycles_for_line - 2);
gb->mode_for_interrupt = 2; /*
TODO: Verify double speed timing
TODO: Timing differs on a DMG
*/
if ((gb->io_registers[GB_IO_LCDC] & 0x20) &&
(gb->io_registers[GB_IO_WY] == gb->current_line)) {
gb->wy_triggered = true;
}
GB_SLEEP(gb, display, 31, 2);
if (gb->current_line != LINES - 1) {
gb->mode_for_interrupt = 2;
}
// Todo: unverified timing // Todo: unverified timing
gb->current_lcd_line++; gb->current_lcd_line++;
@ -1210,20 +1319,22 @@ abort_fetching_object:
gb->io_registers[GB_IO_LY] = gb->current_line; gb->io_registers[GB_IO_LY] = gb->current_line;
gb->ly_for_comparison = -1; gb->ly_for_comparison = -1;
GB_SLEEP(gb, display, 26, 2); GB_SLEEP(gb, display, 26, 2);
if (gb->current_line == LINES) { if (gb->current_line == LINES && !gb->stat_interrupt_line && (gb->io_registers[GB_IO_STAT] & 0x20)) {
gb->mode_for_interrupt = 2; gb->io_registers[GB_IO_IF] |= 2;
} }
GB_STAT_update(gb);
GB_SLEEP(gb, display, 12, 2); GB_SLEEP(gb, display, 12, 2);
gb->ly_for_comparison = gb->current_line; gb->ly_for_comparison = gb->current_line;
GB_STAT_update(gb);
GB_SLEEP(gb, display, 24, 1);
if (gb->current_line == LINES) { if (gb->current_line == LINES) {
/* Entering VBlank state triggers the OAM interrupt */ /* Entering VBlank state triggers the OAM interrupt */
gb->io_registers[GB_IO_STAT] &= ~3; gb->io_registers[GB_IO_STAT] &= ~3;
gb->io_registers[GB_IO_STAT] |= 1; gb->io_registers[GB_IO_STAT] |= 1;
gb->io_registers[GB_IO_IF] |= 1; gb->io_registers[GB_IO_IF] |= 1;
gb->mode_for_interrupt = 2; if (!gb->stat_interrupt_line && (gb->io_registers[GB_IO_STAT] & 0x20)) {
GB_STAT_update(gb); gb->io_registers[GB_IO_IF] |= 2;
}
gb->mode_for_interrupt = 1; gb->mode_for_interrupt = 1;
GB_STAT_update(gb); GB_STAT_update(gb);
@ -1255,8 +1366,7 @@ abort_fetching_object:
} }
} }
GB_STAT_update(gb); GB_SLEEP(gb, display, 13, LINE_LENGTH - 5);
GB_SLEEP(gb, display, 13, LINE_LENGTH - 4);
} }
/* TODO: Verified on SGB2 and CGB-E. Actual interrupt timings not tested. */ /* TODO: Verified on SGB2 and CGB-E. Actual interrupt timings not tested. */
@ -1285,14 +1395,7 @@ abort_fetching_object:
gb->current_line = 0; gb->current_line = 0;
/* Todo: verify timings */ gb->wy_triggered = false;
if ((gb->io_registers[GB_IO_LCDC] & 0x20) &&
(gb->io_registers[GB_IO_WY] == 0)) {
gb->wy_triggered = true;
}
else {
gb->wy_triggered = false;
}
// TODO: not the correct timing // TODO: not the correct timing
gb->current_lcd_line = 0; gb->current_lcd_line = 0;
@ -1464,8 +1567,7 @@ uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_h
} }
for (unsigned y = 0; y < *sprite_height; y++) { for (unsigned y = 0; y < *sprite_height; y++) {
UNROLL unrolled for (unsigned x = 0; x < 8; x++) {
for (unsigned x = 0; x < 8; x++) {
uint8_t color = (((gb->vram[vram_address ] >> ((~x)&7)) & 1 ) | uint8_t color = (((gb->vram[vram_address ] >> ((~x)&7)) & 1 ) |
((gb->vram[vram_address + 1] >> ((~x)&7)) & 1) << 1 ); ((gb->vram[vram_address + 1] >> ((~x)&7)) & 1) << 1 );

View File

@ -51,6 +51,7 @@ typedef enum {
GB_COLOR_CORRECTION_EMULATE_HARDWARE, GB_COLOR_CORRECTION_EMULATE_HARDWARE,
GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS, GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS,
GB_COLOR_CORRECTION_REDUCE_CONTRAST, GB_COLOR_CORRECTION_REDUCE_CONTRAST,
GB_COLOR_CORRECTION_LOW_CONTRAST,
} GB_color_correction_mode_t; } GB_color_correction_mode_t;
void GB_draw_tileset(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette_type, uint8_t palette_index); void GB_draw_tileset(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette_type, uint8_t palette_index);
@ -58,5 +59,6 @@ void GB_draw_tilemap(GB_gameboy_t *gb, uint32_t *dest, GB_palette_type_t palette
uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_height); uint8_t GB_get_oam_info(GB_gameboy_t *gb, GB_oam_info_t *dest, uint8_t *sprite_height);
uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border); uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border);
void GB_set_color_correction_mode(GB_gameboy_t *gb, GB_color_correction_mode_t mode); void GB_set_color_correction_mode(GB_gameboy_t *gb, GB_color_correction_mode_t mode);
void GB_set_light_temperature(GB_gameboy_t *gb, double temperature);
bool GB_is_odd_frame(GB_gameboy_t *gb); bool GB_is_odd_frame(GB_gameboy_t *gb);
#endif /* display_h */ #endif /* display_h */

View File

@ -19,12 +19,6 @@
#endif #endif
static inline uint32_t state_magic(void)
{
if (sizeof(bool) == 1) return 'SAME';
return 'S4ME';
}
void GB_attributed_logv(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, va_list args) void GB_attributed_logv(GB_gameboy_t *gb, GB_log_attributes attributes, const char *fmt, va_list args)
{ {
char *string = NULL; char *string = NULL;
@ -115,21 +109,18 @@ static void load_default_border(GB_gameboy_t *gb)
#define LOAD_BORDER() do { \ #define LOAD_BORDER() do { \
memcpy(gb->borrowed_border.map, tilemap, sizeof(tilemap));\ memcpy(gb->borrowed_border.map, tilemap, sizeof(tilemap));\
memcpy(gb->borrowed_border.palette, palette, sizeof(palette));\ memcpy(gb->borrowed_border.palette, palette, sizeof(palette));\
\ memcpy(gb->borrowed_border.tiles, tiles, sizeof(tiles));\
/* Expand tileset */\
for (unsigned tile = 0; tile < sizeof(tiles) / 32; tile++) {\
for (unsigned y = 0; y < 8; y++) {\
for (unsigned x = 0; x < 8; x++) {\
gb->borrowed_border.tiles[tile * 8 * 8 + y * 8 + x] =\
(tiles[tile * 32 + y * 2 + 0] & (1 << (7 ^ x)) ? 1 : 0) |\
(tiles[tile * 32 + y * 2 + 1] & (1 << (7 ^ x)) ? 2 : 0) |\
(tiles[tile * 32 + y * 2 + 16] & (1 << (7 ^ x)) ? 4 : 0) |\
(tiles[tile * 32 + y * 2 + 17] & (1 << (7 ^ x)) ? 8 : 0);\
}\
}\
}\
} while (false); } while (false);
#ifdef GB_BIG_ENDIAN
for (unsigned i = 0; i < sizeof(gb->borrowed_border.map) / 2; i++) {
gb->borrowed_border.map[i] = LE16(gb->borrowed_border.map[i]);
}
for (unsigned i = 0; i < sizeof(gb->borrowed_border.palette) / 2; i++) {
gb->borrowed_border.palette[i] = LE16(gb->borrowed_border.palette[i]);
}
#endif
if (gb->model == GB_MODEL_AGB) { if (gb->model == GB_MODEL_AGB) {
#include "graphics/agb_border.inc" #include "graphics/agb_border.inc"
LOAD_BORDER(); LOAD_BORDER();
@ -202,6 +193,9 @@ void GB_free(GB_gameboy_t *gb)
if (gb->nontrivial_jump_state) { if (gb->nontrivial_jump_state) {
free(gb->nontrivial_jump_state); free(gb->nontrivial_jump_state);
} }
if (gb->undo_state) {
free(gb->undo_state);
}
#ifndef GB_DISABLE_DEBUGGER #ifndef GB_DISABLE_DEBUGGER
GB_debugger_clear_symbols(gb); GB_debugger_clear_symbols(gb);
#endif #endif
@ -302,9 +296,183 @@ int GB_load_rom(GB_gameboy_t *gb, const char *path)
fread(gb->rom, 1, gb->rom_size, f); fread(gb->rom, 1, gb->rom_size, f);
fclose(f); fclose(f);
GB_configure_cart(gb); GB_configure_cart(gb);
gb->tried_loading_sgb_border = false;
gb->has_sgb_border = false;
load_default_border(gb);
return 0; return 0;
} }
#define GBS_ENTRY 0x61
#define GBS_ENTRY_SIZE 13
static void generate_gbs_entry(GB_gameboy_t *gb, uint8_t *data)
{
memcpy(data, (uint8_t[]) {
0xCD, // Call $XXXX
LE16(gb->gbs_header.init_address),
LE16(gb->gbs_header.init_address) >> 8,
0x76, // HALT
0x00, // NOP
0xAF, // XOR a
0xE0, // LDH [$FFXX], a
GB_IO_IF,
0xCD, // Call $XXXX
LE16(gb->gbs_header.play_address),
LE16(gb->gbs_header.play_address) >> 8,
0x18, // JR pc ± $XX
-10 // To HALT
}, GBS_ENTRY_SIZE);
}
void GB_gbs_switch_track(GB_gameboy_t *gb, uint8_t track)
{
GB_reset(gb);
GB_write_memory(gb, 0xFF00 + GB_IO_LCDC, 0x80);
GB_write_memory(gb, 0xFF00 + GB_IO_TAC, gb->gbs_header.TAC);
GB_write_memory(gb, 0xFF00 + GB_IO_TMA, gb->gbs_header.TMA);
GB_write_memory(gb, 0xFF00 + GB_IO_NR52, 0x80);
GB_write_memory(gb, 0xFF00 + GB_IO_NR51, 0xFF);
GB_write_memory(gb, 0xFF00 + GB_IO_NR50, 0x77);
memset(gb->ram, 0, gb->ram_size);
memset(gb->hram, 0, sizeof(gb->hram));
memset(gb->oam, 0, sizeof(gb->oam));
if (gb->gbs_header.TAC || gb->gbs_header.TMA) {
GB_write_memory(gb, 0xFFFF, 0x04);
}
else {
GB_write_memory(gb, 0xFFFF, 0x01);
}
if (gb->gbs_header.TAC & 0x80) {
gb->cgb_double_speed = true; // Might mean double speed mode on a DMG
}
if (gb->gbs_header.load_address) {
gb->sp = LE16(gb->gbs_header.sp);
gb->pc = GBS_ENTRY;
}
else {
gb->pc = gb->sp = LE16(gb->gbs_header.sp - GBS_ENTRY_SIZE);
uint8_t entry[GBS_ENTRY_SIZE];
generate_gbs_entry(gb, entry);
for (unsigned i = 0; i < sizeof(entry); i++) {
GB_write_memory(gb, gb->pc + i, entry[i]);
}
}
gb->boot_rom_finished = true;
gb->a = track;
if (gb->sgb) {
gb->sgb->intro_animation = GB_SGB_INTRO_ANIMATION_LENGTH;
gb->sgb->disable_commands = true;
}
if (gb->gbs_header.TAC & 0x40) {
gb->interrupt_enable = true;
}
}
int GB_load_gbs_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size, GB_gbs_info_t *info)
{
if (size < sizeof(gb->gbs_header)) {
GB_log(gb, "Not a valid GBS file.\n");
return -1;
}
memcpy(&gb->gbs_header, buffer, sizeof(gb->gbs_header));
if (gb->gbs_header.magic != BE32('GBS\x01') ||
((LE16(gb->gbs_header.load_address) < GBS_ENTRY + GBS_ENTRY_SIZE ||
LE16(gb->gbs_header.load_address) >= 0x8000) &&
LE16(gb->gbs_header.load_address) != 0)) {
GB_log(gb, "Not a valid GBS file.\n");
return -1;
}
size_t data_size = size - sizeof(gb->gbs_header);
gb->rom_size = (data_size + LE16(gb->gbs_header.load_address) + 0x3FFF) & ~0x3FFF; /* Round to bank */
/* And then round to a power of two */
while (gb->rom_size & (gb->rom_size - 1)) {
/* I promise this works. */
gb->rom_size |= gb->rom_size >> 1;
gb->rom_size++;
}
if (gb->rom_size == 0) {
gb->rom_size = 0x8000;
}
if (gb->rom) {
free(gb->rom);
}
gb->rom = malloc(gb->rom_size);
memset(gb->rom, 0xFF, gb->rom_size); /* Pad with 0xFFs */
memcpy(gb->rom + LE16(gb->gbs_header.load_address), buffer + sizeof(gb->gbs_header), data_size);
gb->cartridge_type = &GB_cart_defs[0x11];
if (gb->mbc_ram) {
free(gb->mbc_ram);
gb->mbc_ram = NULL;
}
if (gb->cartridge_type->has_ram) {
gb->mbc_ram_size = 0x2000;
gb->mbc_ram = malloc(gb->mbc_ram_size);
memset(gb->mbc_ram, 0xFF, gb->mbc_ram_size);
}
bool has_interrupts = gb->gbs_header.TAC & 0x40;
if (gb->gbs_header.load_address) {
// Generate interrupt handlers
for (unsigned i = 0; i <= (has_interrupts? 0x50 : 0x38); i += 8) {
gb->rom[i] = 0xc3; // jp $XXXX
gb->rom[i + 1] = (LE16(gb->gbs_header.load_address) + i);
gb->rom[i + 2] = (LE16(gb->gbs_header.load_address) + i) >> 8;
}
for (unsigned i = has_interrupts? 0x58 : 0x40; i <= 0x60; i += 8) {
gb->rom[i] = 0xc9; // ret
}
// Generate entry
generate_gbs_entry(gb, gb->rom + GBS_ENTRY);
}
GB_gbs_switch_track(gb, gb->gbs_header.first_track - 1);
if (info) {
memset(info, 0, sizeof(*info));
info->first_track = gb->gbs_header.first_track - 1;
info->track_count = gb->gbs_header.track_count;
memcpy(info->title, gb->gbs_header.title, sizeof(gb->gbs_header.title));
memcpy(info->author, gb->gbs_header.author, sizeof(gb->gbs_header.author));
memcpy(info->copyright, gb->gbs_header.copyright, sizeof(gb->gbs_header.copyright));
}
gb->tried_loading_sgb_border = true; // Don't even attempt on GBS files
gb->has_sgb_border = false;
load_default_border(gb);
return 0;
}
int GB_load_gbs(GB_gameboy_t *gb, const char *path, GB_gbs_info_t *info)
{
FILE *f = fopen(path, "rb");
if (!f) {
GB_log(gb, "Could not open GBS: %s.\n", strerror(errno));
return errno;
}
fseek(f, 0, SEEK_END);
size_t file_size = MIN(ftell(f), sizeof(GB_gbs_header_t) + 0x4000 * 0x100); // Cap with the maximum MBC3 ROM size + GBS header
fseek(f, 0, SEEK_SET);
uint8_t *file_data = malloc(file_size);
fread(file_data, 1, file_size, f);
fclose(f);
int r = GB_load_gbs_from_buffer(gb, file_data, file_size, info);
free(file_data);
return r;
}
int GB_load_isx(GB_gameboy_t *gb, const char *path) int GB_load_isx(GB_gameboy_t *gb, const char *path)
{ {
FILE *f = fopen(path, "rb"); FILE *f = fopen(path, "rb");
@ -534,6 +702,9 @@ error:
gb->rom_size = old_size; gb->rom_size = old_size;
} }
fclose(f); fclose(f);
gb->tried_loading_sgb_border = false;
gb->has_sgb_border = false;
load_default_border(gb);
return -1; return -1;
} }
@ -554,6 +725,9 @@ void GB_load_rom_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t siz
memset(gb->rom, 0xff, gb->rom_size); memset(gb->rom, 0xff, gb->rom_size);
memcpy(gb->rom, buffer, size); memcpy(gb->rom, buffer, size);
GB_configure_cart(gb); GB_configure_cart(gb);
gb->tried_loading_sgb_border = false;
gb->has_sgb_border = false;
load_default_border(gb);
} }
typedef struct { typedef struct {
@ -570,12 +744,13 @@ typedef struct {
} GB_vba_rtc_time_t; } GB_vba_rtc_time_t;
typedef struct __attribute__((packed)) { typedef struct __attribute__((packed)) {
uint32_t magic;
uint16_t version;
uint8_t mr4;
uint8_t reserved;
uint64_t last_rtc_second; uint64_t last_rtc_second;
uint16_t minutes; uint8_t rtc_data[4];
uint16_t days; } GB_tpp1_rtc_save_t;
uint16_t alarm_minutes, alarm_days;
uint8_t alarm_enabled;
} GB_huc3_rtc_time_t;
typedef union { typedef union {
struct __attribute__((packed)) { struct __attribute__((packed)) {
@ -594,14 +769,33 @@ typedef union {
} vba64; } vba64;
} GB_rtc_save_t; } GB_rtc_save_t;
static void GB_fill_tpp1_save_data(GB_gameboy_t *gb, GB_tpp1_rtc_save_t *data)
{
data->magic = BE32('TPP1');
data->version = BE16(0x100);
data->mr4 = gb->tpp1_mr4;
data->reserved = 0;
data->last_rtc_second = LE64(time(NULL));
unrolled for (unsigned i = 4; i--;) {
data->rtc_data[i] = gb->rtc_real.data[i ^ 3];
}
}
int GB_save_battery_size(GB_gameboy_t *gb) int GB_save_battery_size(GB_gameboy_t *gb)
{ {
if (!gb->cartridge_type->has_battery) return 0; // Nothing to save. if (!gb->cartridge_type->has_battery) return 0; // Nothing to save.
if (gb->cartridge_type->mbc_type == GB_TPP1 && !(gb->rom[0x153] & 8)) return 0; // Nothing to save.
if (gb->mbc_ram_size == 0 && !gb->cartridge_type->has_rtc) return 0; /* Claims to have battery, but has no RAM or RTC */ if (gb->mbc_ram_size == 0 && !gb->cartridge_type->has_rtc) return 0; /* Claims to have battery, but has no RAM or RTC */
if (gb->cartridge_type->mbc_type == GB_HUC3) { if (gb->cartridge_type->mbc_type == GB_HUC3) {
return gb->mbc_ram_size + sizeof(GB_huc3_rtc_time_t); return gb->mbc_ram_size + sizeof(GB_huc3_rtc_time_t);
} }
if (gb->cartridge_type->mbc_type == GB_TPP1) {
return gb->mbc_ram_size + sizeof(GB_tpp1_rtc_save_t);
}
GB_rtc_save_t rtc_save_size; GB_rtc_save_t rtc_save_size;
return gb->mbc_ram_size + (gb->cartridge_type->has_rtc ? sizeof(rtc_save_size.vba64) : 0); return gb->mbc_ram_size + (gb->cartridge_type->has_rtc ? sizeof(rtc_save_size.vba64) : 0);
} }
@ -609,13 +803,20 @@ int GB_save_battery_size(GB_gameboy_t *gb)
int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size) int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size)
{ {
if (!gb->cartridge_type->has_battery) return 0; // Nothing to save. if (!gb->cartridge_type->has_battery) return 0; // Nothing to save.
if (gb->cartridge_type->mbc_type == GB_TPP1 && !(gb->rom[0x153] & 8)) return 0; // Nothing to save.
if (gb->mbc_ram_size == 0 && !gb->cartridge_type->has_rtc) return 0; /* Claims to have battery, but has no RAM or RTC */ if (gb->mbc_ram_size == 0 && !gb->cartridge_type->has_rtc) return 0; /* Claims to have battery, but has no RAM or RTC */
if (size < GB_save_battery_size(gb)) return EIO; if (size < GB_save_battery_size(gb)) return EIO;
memcpy(buffer, gb->mbc_ram, gb->mbc_ram_size); memcpy(buffer, gb->mbc_ram, gb->mbc_ram_size);
if (gb->cartridge_type->mbc_type == GB_HUC3) { if (gb->cartridge_type->mbc_type == GB_TPP1) {
buffer += gb->mbc_ram_size;
GB_tpp1_rtc_save_t rtc_save;
GB_fill_tpp1_save_data(gb, &rtc_save);
memcpy(buffer, &rtc_save, sizeof(rtc_save));
}
else if (gb->cartridge_type->mbc_type == GB_HUC3) {
buffer += gb->mbc_ram_size; buffer += gb->mbc_ram_size;
#ifdef GB_BIG_ENDIAN #ifdef GB_BIG_ENDIAN
@ -652,9 +853,9 @@ int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size)
rtc_save.vba64.rtc_latched.days = gb->rtc_latched.days; rtc_save.vba64.rtc_latched.days = gb->rtc_latched.days;
rtc_save.vba64.rtc_latched.high = gb->rtc_latched.high; rtc_save.vba64.rtc_latched.high = gb->rtc_latched.high;
#ifdef GB_BIG_ENDIAN #ifdef GB_BIG_ENDIAN
rtc_save.vba64.last_rtc_second = __builtin_bswap64(gb->last_rtc_second); rtc_save.vba64.last_rtc_second = __builtin_bswap64(time(NULL));
#else #else
rtc_save.vba64.last_rtc_second = gb->last_rtc_second; rtc_save.vba64.last_rtc_second = time(NULL);
#endif #endif
memcpy(buffer + gb->mbc_ram_size, &rtc_save.vba64, sizeof(rtc_save.vba64)); memcpy(buffer + gb->mbc_ram_size, &rtc_save.vba64, sizeof(rtc_save.vba64));
} }
@ -666,6 +867,7 @@ int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size)
int GB_save_battery(GB_gameboy_t *gb, const char *path) int GB_save_battery(GB_gameboy_t *gb, const char *path)
{ {
if (!gb->cartridge_type->has_battery) return 0; // Nothing to save. if (!gb->cartridge_type->has_battery) return 0; // Nothing to save.
if (gb->cartridge_type->mbc_type == GB_TPP1 && !(gb->rom[0x153] & 8)) return 0; // Nothing to save.
if (gb->mbc_ram_size == 0 && !gb->cartridge_type->has_rtc) return 0; /* Claims to have battery, but has no RAM or RTC */ if (gb->mbc_ram_size == 0 && !gb->cartridge_type->has_rtc) return 0; /* Claims to have battery, but has no RAM or RTC */
FILE *f = fopen(path, "wb"); FILE *f = fopen(path, "wb");
if (!f) { if (!f) {
@ -677,7 +879,16 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path)
fclose(f); fclose(f);
return EIO; return EIO;
} }
if (gb->cartridge_type->mbc_type == GB_HUC3) { if (gb->cartridge_type->mbc_type == GB_TPP1) {
GB_tpp1_rtc_save_t rtc_save;
GB_fill_tpp1_save_data(gb, &rtc_save);
if (fwrite(&rtc_save, sizeof(rtc_save), 1, f) != 1) {
fclose(f);
return EIO;
}
}
else if (gb->cartridge_type->mbc_type == GB_HUC3) {
#ifdef GB_BIG_ENDIAN #ifdef GB_BIG_ENDIAN
GB_huc3_rtc_time_t rtc_save = { GB_huc3_rtc_time_t rtc_save = {
__builtin_bswap64(gb->last_rtc_second), __builtin_bswap64(gb->last_rtc_second),
@ -716,9 +927,9 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path)
rtc_save.vba64.rtc_latched.days = gb->rtc_latched.days; rtc_save.vba64.rtc_latched.days = gb->rtc_latched.days;
rtc_save.vba64.rtc_latched.high = gb->rtc_latched.high; rtc_save.vba64.rtc_latched.high = gb->rtc_latched.high;
#ifdef GB_BIG_ENDIAN #ifdef GB_BIG_ENDIAN
rtc_save.vba64.last_rtc_second = __builtin_bswap64(gb->last_rtc_second); rtc_save.vba64.last_rtc_second = __builtin_bswap64(time(NULL));
#else #else
rtc_save.vba64.last_rtc_second = gb->last_rtc_second; rtc_save.vba64.last_rtc_second = time(NULL);
#endif #endif
if (fwrite(&rtc_save.vba64, 1, sizeof(rtc_save.vba64), f) != sizeof(rtc_save.vba64)) { if (fwrite(&rtc_save.vba64, 1, sizeof(rtc_save.vba64), f) != sizeof(rtc_save.vba64)) {
fclose(f); fclose(f);
@ -732,6 +943,14 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path)
return errno; return errno;
} }
static void GB_load_tpp1_save_data(GB_gameboy_t *gb, const GB_tpp1_rtc_save_t *data)
{
gb->last_rtc_second = LE64(data->last_rtc_second);
unrolled for (unsigned i = 4; i--;) {
gb->rtc_real.data[i ^ 3] = data->rtc_data[i];
}
}
void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size) void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size)
{ {
memcpy(gb->mbc_ram, buffer, MIN(gb->mbc_ram_size, size)); memcpy(gb->mbc_ram, buffer, MIN(gb->mbc_ram_size, size));
@ -739,6 +958,22 @@ void GB_load_battery_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t
goto reset_rtc; goto reset_rtc;
} }
if (gb->cartridge_type->mbc_type == GB_TPP1) {
GB_tpp1_rtc_save_t rtc_save;
if (size - gb->mbc_ram_size < sizeof(rtc_save)) {
goto reset_rtc;
}
memcpy(&rtc_save, buffer + gb->mbc_ram_size, sizeof(rtc_save));
GB_load_tpp1_save_data(gb, &rtc_save);
if (gb->last_rtc_second > time(NULL)) {
/* We must reset RTC here, or it will not advance. */
goto reset_rtc;
}
return;
}
if (gb->cartridge_type->mbc_type == GB_HUC3) { if (gb->cartridge_type->mbc_type == GB_HUC3) {
GB_huc3_rtc_time_t rtc_save; GB_huc3_rtc_time_t rtc_save;
if (size - gb->mbc_ram_size < sizeof(rtc_save)) { if (size - gb->mbc_ram_size < sizeof(rtc_save)) {
@ -848,6 +1083,21 @@ void GB_load_battery(GB_gameboy_t *gb, const char *path)
goto reset_rtc; goto reset_rtc;
} }
if (gb->cartridge_type->mbc_type == GB_TPP1) {
GB_tpp1_rtc_save_t rtc_save;
if (fread(&rtc_save, sizeof(rtc_save), 1, f) != 1) {
goto reset_rtc;
}
GB_load_tpp1_save_data(gb, &rtc_save);
if (gb->last_rtc_second > time(NULL)) {
/* We must reset RTC here, or it will not advance. */
goto reset_rtc;
}
return;
}
if (gb->cartridge_type->mbc_type == GB_HUC3) { if (gb->cartridge_type->mbc_type == GB_HUC3) {
GB_huc3_rtc_time_t rtc_save; GB_huc3_rtc_time_t rtc_save;
if (fread(&rtc_save, sizeof(rtc_save), 1, f) != 1) { if (fread(&rtc_save, sizeof(rtc_save), 1, f) != 1) {
@ -964,7 +1214,6 @@ uint8_t GB_run(GB_gameboy_t *gb)
gb->cycles_since_run = 0; gb->cycles_since_run = 0;
GB_cpu_run(gb); GB_cpu_run(gb);
if (gb->vblank_just_occured) { if (gb->vblank_just_occured) {
GB_rtc_run(gb);
GB_debugger_handle_async_commands(gb); GB_debugger_handle_async_commands(gb);
GB_rewind_push(gb); GB_rewind_push(gb);
} }
@ -1073,17 +1322,6 @@ void GB_set_infrared_callback(GB_gameboy_t *gb, GB_infrared_callback_t callback)
void GB_set_infrared_input(GB_gameboy_t *gb, bool state) void GB_set_infrared_input(GB_gameboy_t *gb, bool state)
{ {
gb->infrared_input = state; gb->infrared_input = state;
gb->cycles_since_input_ir_change = 0;
gb->ir_queue_length = 0;
}
void GB_queue_infrared_input(GB_gameboy_t *gb, bool state, uint64_t cycles_after_previous_change)
{
if (gb->ir_queue_length == GB_MAX_IR_QUEUE) {
GB_log(gb, "IR Queue is full\n");
return;
}
gb->ir_queue[gb->ir_queue_length++] = (GB_ir_queue_item_t){state, cycles_after_previous_change};
} }
void GB_set_rumble_callback(GB_gameboy_t *gb, GB_rumble_callback_t callback) void GB_set_rumble_callback(GB_gameboy_t *gb, GB_rumble_callback_t callback)
@ -1110,6 +1348,7 @@ bool GB_serial_get_data_bit(GB_gameboy_t *gb)
} }
return gb->io_registers[GB_IO_SB] & 0x80; return gb->io_registers[GB_IO_SB] & 0x80;
} }
void GB_serial_set_data_bit(GB_gameboy_t *gb, bool data) void GB_serial_set_data_bit(GB_gameboy_t *gb, bool data)
{ {
if (gb->io_registers[GB_IO_SC] & 1) { if (gb->io_registers[GB_IO_SC] & 1) {
@ -1445,6 +1684,10 @@ void GB_switch_model_and_reset(GB_gameboy_t *gb, GB_model_t model)
gb->ram = realloc(gb->ram, gb->ram_size = 0x2000); gb->ram = realloc(gb->ram, gb->ram_size = 0x2000);
gb->vram = realloc(gb->vram, gb->vram_size = 0x2000); gb->vram = realloc(gb->vram, gb->vram_size = 0x2000);
} }
if (gb->undo_state) {
free(gb->undo_state);
gb->undo_state = NULL;
}
GB_rewind_free(gb); GB_rewind_free(gb);
GB_reset(gb); GB_reset(gb);
load_default_border(gb); load_default_border(gb);
@ -1524,15 +1767,20 @@ void GB_set_clock_multiplier(GB_gameboy_t *gb, double multiplier)
uint32_t GB_get_clock_rate(GB_gameboy_t *gb) uint32_t GB_get_clock_rate(GB_gameboy_t *gb)
{ {
if (gb->model & GB_MODEL_PAL_BIT) { return GB_get_unmultiplied_clock_rate(gb) * gb->clock_multiplier;
return SGB_PAL_FREQUENCY * gb->clock_multiplier;
}
if ((gb->model & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB) {
return SGB_NTSC_FREQUENCY * gb->clock_multiplier;
}
return CPU_FREQUENCY * gb->clock_multiplier;
} }
uint32_t GB_get_unmultiplied_clock_rate(GB_gameboy_t *gb)
{
if (gb->model & GB_MODEL_PAL_BIT) {
return SGB_PAL_FREQUENCY;
}
if ((gb->model & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB) {
return SGB_NTSC_FREQUENCY;
}
return CPU_FREQUENCY;
}
void GB_set_border_mode(GB_gameboy_t *gb, GB_border_mode_t border_mode) void GB_set_border_mode(GB_gameboy_t *gb, GB_border_mode_t border_mode)
{ {
if (gb->border_mode > GB_BORDER_ALWAYS) return; if (gb->border_mode > GB_BORDER_ALWAYS) return;
@ -1617,3 +1865,80 @@ unsigned GB_time_to_alarm(GB_gameboy_t *gb)
if (current_time > alarm_time) return 0; if (current_time > alarm_time) return 0;
return alarm_time - current_time; return alarm_time - current_time;
} }
void GB_set_rtc_mode(GB_gameboy_t *gb, GB_rtc_mode_t mode)
{
if (gb->rtc_mode != mode) {
gb->rtc_mode = mode;
gb->rtc_cycles = 0;
gb->last_rtc_second = time(NULL);
}
}
void GB_get_rom_title(GB_gameboy_t *gb, char *title)
{
memset(title, 0, 17);
if (gb->rom_size >= 0x4000) {
for (unsigned i = 0; i < 0x10; i++) {
if (gb->rom[0x134 + i] < 0x20 || gb->rom[0x134 + i] >= 0x80) break;
title[i] = gb->rom[0x134 + i];
}
}
}
uint32_t GB_get_rom_crc32(GB_gameboy_t *gb)
{
static const uint32_t table[] = {
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
};
const uint8_t *byte = gb->rom;
uint32_t size = gb->rom_size;
uint32_t ret = 0xFFFFFFFF;
while (size--) {
ret = table[(ret ^ *byte++) & 0xFF] ^ (ret >> 8);
}
return ~ret;
}

View File

@ -31,16 +31,19 @@
#define GB_MODEL_DMG_FAMILY 0x000 #define GB_MODEL_DMG_FAMILY 0x000
#define GB_MODEL_MGB_FAMILY 0x100 #define GB_MODEL_MGB_FAMILY 0x100
#define GB_MODEL_CGB_FAMILY 0x200 #define GB_MODEL_CGB_FAMILY 0x200
#define GB_MODEL_PAL_BIT 0x1000 #define GB_MODEL_PAL_BIT 0x40
#define GB_MODEL_NO_SFC_BIT 0x2000 #define GB_MODEL_NO_SFC_BIT 0x80
#define GB_MODEL_PAL_BIT_OLD 0x1000
#define GB_MODEL_NO_SFC_BIT_OLD 0x2000
#ifdef GB_INTERNAL #ifdef GB_INTERNAL
#if __clang__ #if __clang__
#define UNROLL _Pragma("unroll") #define unrolled _Pragma("unroll")
#elif __GNUC__ >= 8 #elif __GNUC__ >= 8
#define UNROLL _Pragma("GCC unroll 8") #define unrolled _Pragma("GCC unroll 8")
#else #else
#define UNROLL #define unrolled
#endif #endif
#endif #endif
@ -53,6 +56,25 @@
#error Unable to detect endianess #error Unable to detect endianess
#endif #endif
#ifdef GB_INTERNAL
/* Todo: similar macros are everywhere, clean this up and remove direct calls to bswap */
#ifdef GB_BIG_ENDIAN
#define LE16(x) __builtin_bswap16(x)
#define LE32(x) __builtin_bswap32(x)
#define LE64(x) __builtin_bswap64(x)
#define BE16(x) (x)
#define BE32(x) (x)
#define BE64(x) (x)
#else
#define LE16(x) (x)
#define LE32(x) (x)
#define LE64(x) (x)
#define BE16(x) __builtin_bswap16(x)
#define BE32(x) __builtin_bswap32(x)
#define BE64(x) __builtin_bswap64(x)
#endif
#endif
#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 8) #if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 8)
#define __builtin_bswap16(x) ({ typeof(x) _x = (x); _x >> 8 | _x << 8; }) #define __builtin_bswap16(x) ({ typeof(x) _x = (x); _x >> 8 | _x << 8; })
#endif #endif
@ -76,9 +98,24 @@ typedef union {
uint8_t days; uint8_t days;
uint8_t high; uint8_t high;
}; };
struct {
uint8_t seconds;
uint8_t minutes;
uint8_t hours:5;
uint8_t weekday:3;
uint8_t weeks;
} tpp1;
uint8_t data[5]; uint8_t data[5];
} GB_rtc_time_t; } GB_rtc_time_t;
typedef struct __attribute__((packed)) {
uint64_t last_rtc_second;
uint16_t minutes;
uint16_t days;
uint16_t alarm_minutes, alarm_days;
uint8_t alarm_enabled;
} GB_huc3_rtc_time_t;
typedef enum { typedef enum {
// GB_MODEL_DMG_0 = 0x000, // GB_MODEL_DMG_0 = 0x000,
// GB_MODEL_DMG_A = 0x001, // GB_MODEL_DMG_A = 0x001,
@ -125,8 +162,6 @@ typedef enum {
GB_BORDER_ALWAYS, GB_BORDER_ALWAYS,
} GB_border_mode_t; } GB_border_mode_t;
#define GB_MAX_IR_QUEUE 256
enum { enum {
/* Joypad and Serial */ /* Joypad and Serial */
GB_IO_JOYP = 0x00, // Joypad (R/W) GB_IO_JOYP = 0x00, // Joypad (R/W)
@ -251,13 +286,17 @@ typedef enum {
GB_BOOT_ROM_AGB, GB_BOOT_ROM_AGB,
} GB_boot_rom_t; } GB_boot_rom_t;
typedef enum {
GB_RTC_MODE_SYNC_TO_HOST,
GB_RTC_MODE_ACCURATE,
} GB_rtc_mode_t;
#ifdef GB_INTERNAL #ifdef GB_INTERNAL
#define LCDC_PERIOD 70224 #define LCDC_PERIOD 70224
#define CPU_FREQUENCY 0x400000 #define CPU_FREQUENCY 0x400000
#define SGB_NTSC_FREQUENCY (21477272 / 5) #define SGB_NTSC_FREQUENCY (21477272 / 5)
#define SGB_PAL_FREQUENCY (21281370 / 5) #define SGB_PAL_FREQUENCY (21281370 / 5)
#define DIV_CYCLES (0x100) #define DIV_CYCLES (0x100)
#define INTERNAL_DIV_CYCLES (0x40000)
#if !defined(MIN) #if !defined(MIN)
#define MIN(A, B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) #define MIN(A, B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })
@ -272,7 +311,7 @@ typedef void (*GB_vblank_callback_t)(GB_gameboy_t *gb);
typedef void (*GB_log_callback_t)(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes); typedef void (*GB_log_callback_t)(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes);
typedef char *(*GB_input_callback_t)(GB_gameboy_t *gb); typedef char *(*GB_input_callback_t)(GB_gameboy_t *gb);
typedef uint32_t (*GB_rgb_encode_callback_t)(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b); typedef uint32_t (*GB_rgb_encode_callback_t)(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b);
typedef void (*GB_infrared_callback_t)(GB_gameboy_t *gb, bool on, uint64_t cycles_since_last_update); typedef void (*GB_infrared_callback_t)(GB_gameboy_t *gb, bool on);
typedef void (*GB_rumble_callback_t)(GB_gameboy_t *gb, double rumble_amplitude); typedef void (*GB_rumble_callback_t)(GB_gameboy_t *gb, double rumble_amplitude);
typedef void (*GB_serial_transfer_bit_start_callback_t)(GB_gameboy_t *gb, bool bit_to_send); typedef void (*GB_serial_transfer_bit_start_callback_t)(GB_gameboy_t *gb, bool bit_to_send);
typedef bool (*GB_serial_transfer_bit_end_callback_t)(GB_gameboy_t *gb); typedef bool (*GB_serial_transfer_bit_end_callback_t)(GB_gameboy_t *gb);
@ -283,11 +322,6 @@ typedef void (*GB_icd_hreset_callback_t)(GB_gameboy_t *gb);
typedef void (*GB_icd_vreset_callback_t)(GB_gameboy_t *gb); typedef void (*GB_icd_vreset_callback_t)(GB_gameboy_t *gb);
typedef void (*GB_boot_rom_load_callback_t)(GB_gameboy_t *gb, GB_boot_rom_t type); typedef void (*GB_boot_rom_load_callback_t)(GB_gameboy_t *gb, GB_boot_rom_t type);
typedef struct {
bool state;
uint64_t delay;
} GB_ir_queue_item_t;
struct GB_breakpoint_s; struct GB_breakpoint_s;
struct GB_watchpoint_s; struct GB_watchpoint_s;
@ -305,6 +339,29 @@ typedef struct {
uint8_t write_end; uint8_t write_end;
} GB_fifo_t; } GB_fifo_t;
typedef struct {
uint32_t magic;
uint8_t track_count;
uint8_t first_track;
uint16_t load_address;
uint16_t init_address;
uint16_t play_address;
uint16_t sp;
uint8_t TMA;
uint8_t TAC;
char title[32];
char author[32];
char copyright[32];
} GB_gbs_header_t;
typedef struct {
uint8_t track_count;
uint8_t first_track;
char title[33];
char author[33];
char copyright[33];
} GB_gbs_info_t;
/* When state saving, each section is dumped independently of other sections. /* When state saving, each section is dumped independently of other sections.
This allows adding data to the end of the section without worrying about future compatibility. This allows adding data to the end of the section without worrying about future compatibility.
Some other changes might be "safe" as well. Some other changes might be "safe" as well.
@ -374,6 +431,9 @@ struct GB_gameboy_internal_s {
uint8_t extra_oam[0xff00 - 0xfea0]; uint8_t extra_oam[0xff00 - 0xfea0];
uint32_t ram_size; // Different between CGB and DMG uint32_t ram_size; // Different between CGB and DMG
GB_workboy_t workboy; GB_workboy_t workboy;
int32_t ir_sensor;
bool effective_ir_input;
); );
/* DMA and HDMA */ /* DMA and HDMA */
@ -437,10 +497,10 @@ struct GB_gameboy_internal_s {
uint16_t mbc_rom0_bank; /* For some MBC1 wirings. */ uint16_t mbc_rom0_bank; /* For some MBC1 wirings. */
bool camera_registers_mapped; bool camera_registers_mapped;
uint8_t camera_registers[0x36]; uint8_t camera_registers[0x36];
bool rumble_state; uint8_t rumble_strength;
bool cart_ir; bool cart_ir;
// TODO: move to huc3/mbc3 struct when breaking save compat // TODO: move to huc3/mbc3/tpp1 struct when breaking save compat
uint8_t huc3_mode; uint8_t huc3_mode;
uint8_t huc3_access_index; uint8_t huc3_access_index;
uint16_t huc3_minutes, huc3_days; uint16_t huc3_minutes, huc3_days;
@ -449,6 +509,9 @@ struct GB_gameboy_internal_s {
uint8_t huc3_read; uint8_t huc3_read;
uint8_t huc3_access_flags; uint8_t huc3_access_flags;
bool mbc3_rtc_mapped; bool mbc3_rtc_mapped;
uint16_t tpp1_rom_bank;
uint8_t tpp1_ram_bank;
uint8_t tpp1_mode;
); );
@ -468,6 +531,9 @@ struct GB_gameboy_internal_s {
uint16_t serial_length; uint16_t serial_length;
uint8_t double_speed_alignment; uint8_t double_speed_alignment;
uint8_t serial_count; uint8_t serial_count;
int32_t speed_switch_halt_countdown;
uint8_t speed_switch_countdown; // To compensate for the lack of pipeline emulation
uint8_t speed_switch_freeze; // Solely for realigning the PPU, should be removed when the odd modes are implemented
); );
/* APU */ /* APU */
@ -479,7 +545,9 @@ struct GB_gameboy_internal_s {
GB_SECTION(rtc, GB_SECTION(rtc,
GB_rtc_time_t rtc_real, rtc_latched; GB_rtc_time_t rtc_real, rtc_latched;
uint64_t last_rtc_second; uint64_t last_rtc_second;
bool rtc_latch; GB_PADDING(bool, rtc_latch);
uint32_t rtc_cycles;
uint8_t tpp1_mr4;
); );
/* Video Display */ /* Video Display */
@ -548,6 +616,7 @@ struct GB_gameboy_internal_s {
uint16_t last_tile_data_address; uint16_t last_tile_data_address;
uint16_t last_tile_index_address; uint16_t last_tile_index_address;
bool cgb_repeated_a_frame; bool cgb_repeated_a_frame;
uint8_t data_for_sel_glitch;
); );
/* Unsaved data. This includes all pointers, as well as everything that shouldn't be on a save state */ /* Unsaved data. This includes all pointers, as well as everything that shouldn't be on a save state */
@ -576,6 +645,7 @@ struct GB_gameboy_internal_s {
uint32_t sprite_palettes_rgb[0x20]; uint32_t sprite_palettes_rgb[0x20];
const GB_palette_t *dmg_palette; const GB_palette_t *dmg_palette;
GB_color_correction_mode_t color_correction_mode; GB_color_correction_mode_t color_correction_mode;
double light_temperature;
bool keys[4][GB_KEY_MAX]; bool keys[4][GB_KEY_MAX];
GB_border_mode_t border_mode; GB_border_mode_t border_mode;
GB_sgb_border_t borrowed_border; GB_sgb_border_t borrowed_border;
@ -585,6 +655,7 @@ struct GB_gameboy_internal_s {
/* Timing */ /* Timing */
uint64_t last_sync; uint64_t last_sync;
uint64_t cycles_since_last_sync; // In 8MHz units uint64_t cycles_since_last_sync; // In 8MHz units
GB_rtc_mode_t rtc_mode;
/* Audio */ /* Audio */
GB_apu_output_t apu_output; GB_apu_output_t apu_output;
@ -612,12 +683,6 @@ struct GB_gameboy_internal_s {
GB_print_image_callback_t printer_callback; GB_print_image_callback_t printer_callback;
GB_workboy_set_time_callback workboy_set_time_callback; GB_workboy_set_time_callback workboy_set_time_callback;
GB_workboy_get_time_callback workboy_get_time_callback; GB_workboy_get_time_callback workboy_get_time_callback;
/* IR */
uint64_t cycles_since_ir_change; // In 8MHz units
uint64_t cycles_since_input_ir_change; // In 8MHz units
GB_ir_queue_item_t ir_queue[GB_MAX_IR_QUEUE];
size_t ir_queue_length;
/*** Debugger ***/ /*** Debugger ***/
volatile bool debug_stopped, debug_disable; volatile bool debug_stopped, debug_disable;
@ -654,7 +719,12 @@ struct GB_gameboy_internal_s {
/* Ticks command */ /* Ticks command */
uint64_t debugger_ticks; uint64_t debugger_ticks;
uint64_t absolute_debugger_ticks;
/* Undo */
uint8_t *undo_state;
const char *undo_label;
/* Rewind */ /* Rewind */
#define GB_REWIND_FRAMES_PER_KEY 255 #define GB_REWIND_FRAMES_PER_KEY 255
size_t rewind_buffer_length; size_t rewind_buffer_length;
@ -692,6 +762,9 @@ struct GB_gameboy_internal_s {
/* Temporary state */ /* Temporary state */
bool wx_just_changed; bool wx_just_changed;
bool tile_sel_glitch;
GB_gbs_header_t gbs_header;
); );
}; };
@ -744,14 +817,15 @@ void *GB_get_direct_access(GB_gameboy_t *gb, GB_direct_access_t access, size_t *
void *GB_get_user_data(GB_gameboy_t *gb); void *GB_get_user_data(GB_gameboy_t *gb);
void GB_set_user_data(GB_gameboy_t *gb, void *data); void GB_set_user_data(GB_gameboy_t *gb, void *data);
int GB_load_boot_rom(GB_gameboy_t *gb, const char *path); int GB_load_boot_rom(GB_gameboy_t *gb, const char *path);
void GB_load_boot_rom_from_buffer(GB_gameboy_t *gb, const unsigned char *buffer, size_t size); void GB_load_boot_rom_from_buffer(GB_gameboy_t *gb, const unsigned char *buffer, size_t size);
int GB_load_rom(GB_gameboy_t *gb, const char *path); int GB_load_rom(GB_gameboy_t *gb, const char *path);
void GB_load_rom_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size); void GB_load_rom_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size);
int GB_load_isx(GB_gameboy_t *gb, const char *path); int GB_load_isx(GB_gameboy_t *gb, const char *path);
int GB_load_gbs_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t size, GB_gbs_info_t *info);
int GB_load_gbs(GB_gameboy_t *gb, const char *path, GB_gbs_info_t *info);
void GB_gbs_switch_track(GB_gameboy_t *gb, uint8_t track);
int GB_save_battery_size(GB_gameboy_t *gb); int GB_save_battery_size(GB_gameboy_t *gb);
int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size); int GB_save_battery_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size);
int GB_save_battery(GB_gameboy_t *gb, const char *path); int GB_save_battery(GB_gameboy_t *gb, const char *path);
@ -769,7 +843,6 @@ void GB_set_pixels_output(GB_gameboy_t *gb, uint32_t *output);
void GB_set_border_mode(GB_gameboy_t *gb, GB_border_mode_t border_mode); void GB_set_border_mode(GB_gameboy_t *gb, GB_border_mode_t border_mode);
void GB_set_infrared_input(GB_gameboy_t *gb, bool state); void GB_set_infrared_input(GB_gameboy_t *gb, bool state);
void GB_queue_infrared_input(GB_gameboy_t *gb, bool state, uint64_t cycles_after_previous_change); /* In 8MHz units*/
void GB_set_vblank_callback(GB_gameboy_t *gb, GB_vblank_callback_t callback); void GB_set_vblank_callback(GB_gameboy_t *gb, GB_vblank_callback_t callback);
void GB_set_log_callback(GB_gameboy_t *gb, GB_log_callback_t callback); void GB_set_log_callback(GB_gameboy_t *gb, GB_log_callback_t callback);
@ -797,15 +870,17 @@ void GB_disconnect_serial(GB_gameboy_t *gb);
/* For cartridges with an alarm clock */ /* For cartridges with an alarm clock */
unsigned GB_time_to_alarm(GB_gameboy_t *gb); // 0 if no alarm unsigned GB_time_to_alarm(GB_gameboy_t *gb); // 0 if no alarm
/* RTC emulation mode */
void GB_set_rtc_mode(GB_gameboy_t *gb, GB_rtc_mode_t mode);
/* For integration with SFC/SNES emulators */ /* For integration with SFC/SNES emulators */
void GB_set_joyp_write_callback(GB_gameboy_t *gb, GB_joyp_write_callback_t callback); void GB_set_joyp_write_callback(GB_gameboy_t *gb, GB_joyp_write_callback_t callback);
void GB_set_icd_pixel_callback(GB_gameboy_t *gb, GB_icd_pixel_callback_t callback); void GB_set_icd_pixel_callback(GB_gameboy_t *gb, GB_icd_pixel_callback_t callback);
void GB_set_icd_hreset_callback(GB_gameboy_t *gb, GB_icd_hreset_callback_t callback); void GB_set_icd_hreset_callback(GB_gameboy_t *gb, GB_icd_hreset_callback_t callback);
void GB_set_icd_vreset_callback(GB_gameboy_t *gb, GB_icd_vreset_callback_t callback); void GB_set_icd_vreset_callback(GB_gameboy_t *gb, GB_icd_vreset_callback_t callback);
#ifdef GB_INTERNAL
uint32_t GB_get_clock_rate(GB_gameboy_t *gb); uint32_t GB_get_clock_rate(GB_gameboy_t *gb);
#endif uint32_t GB_get_unmultiplied_clock_rate(GB_gameboy_t *gb);
void GB_set_clock_multiplier(GB_gameboy_t *gb, double multiplier); void GB_set_clock_multiplier(GB_gameboy_t *gb, double multiplier);
unsigned GB_get_screen_width(GB_gameboy_t *gb); unsigned GB_get_screen_width(GB_gameboy_t *gb);
@ -813,4 +888,9 @@ unsigned GB_get_screen_height(GB_gameboy_t *gb);
double GB_get_usual_frame_rate(GB_gameboy_t *gb); double GB_get_usual_frame_rate(GB_gameboy_t *gb);
unsigned GB_get_player_count(GB_gameboy_t *gb); unsigned GB_get_player_count(GB_gameboy_t *gb);
/* Handy ROM info APIs */
// `title` must be at least 17 bytes in size
void GB_get_rom_title(GB_gameboy_t *gb, char *title);
uint32_t GB_get_rom_crc32(GB_gameboy_t *gb);
#endif /* GB_h */ #endif /* GB_h */

View File

@ -111,12 +111,24 @@ void GB_update_mbc_mappings(GB_gameboy_t *gb)
gb->mbc_rom_bank = gb->huc3.rom_bank; gb->mbc_rom_bank = gb->huc3.rom_bank;
gb->mbc_ram_bank = gb->huc3.ram_bank; gb->mbc_ram_bank = gb->huc3.ram_bank;
break; break;
case GB_TPP1:
gb->mbc_rom_bank = gb->tpp1_rom_bank;
gb->mbc_ram_bank = gb->tpp1_ram_bank;
gb->mbc_ram_enable = (gb->tpp1_mode == 2) || (gb->tpp1_mode == 3);
break;
} }
} }
void GB_configure_cart(GB_gameboy_t *gb) void GB_configure_cart(GB_gameboy_t *gb)
{ {
gb->cartridge_type = &GB_cart_defs[gb->rom[0x147]]; gb->cartridge_type = &GB_cart_defs[gb->rom[0x147]];
if (gb->rom[0x147] == 0xbc &&
gb->rom[0x149] == 0xc1 &&
gb->rom[0x14a] == 0x65) {
static const GB_cartridge_t tpp1 = {GB_TPP1, GB_STANDARD_MBC, true, true, true, true};
gb->cartridge_type = &tpp1;
gb->tpp1_rom_bank = 1;
}
if (gb->rom[0x147] == 0 && gb->rom_size > 0x8000) { if (gb->rom[0x147] == 0 && gb->rom_size > 0x8000) {
GB_log(gb, "ROM header reports no MBC, but file size is over 32Kb. Assuming cartridge uses MBC3.\n"); GB_log(gb, "ROM header reports no MBC, but file size is over 32Kb. Assuming cartridge uses MBC3.\n");
@ -125,11 +137,21 @@ void GB_configure_cart(GB_gameboy_t *gb)
else if (gb->rom[0x147] != 0 && memcmp(gb->cartridge_type, &GB_cart_defs[0], sizeof(GB_cart_defs[0])) == 0) { else if (gb->rom[0x147] != 0 && memcmp(gb->cartridge_type, &GB_cart_defs[0], sizeof(GB_cart_defs[0])) == 0) {
GB_log(gb, "Cartridge type %02x is not yet supported.\n", gb->rom[0x147]); GB_log(gb, "Cartridge type %02x is not yet supported.\n", gb->rom[0x147]);
} }
if (gb->mbc_ram) {
free(gb->mbc_ram);
gb->mbc_ram = NULL;
}
if (gb->cartridge_type->has_ram) { if (gb->cartridge_type->has_ram) {
if (gb->cartridge_type->mbc_type == GB_MBC2) { if (gb->cartridge_type->mbc_type == GB_MBC2) {
gb->mbc_ram_size = 0x200; gb->mbc_ram_size = 0x200;
} }
else if (gb->cartridge_type->mbc_type == GB_TPP1) {
if (gb->rom[0x152] >= 1 && gb->rom[0x152] <= 9) {
gb->mbc_ram_size = 0x2000 << (gb->rom[0x152] - 1);
}
}
else { else {
static const unsigned ram_sizes[256] = {0, 0x800, 0x2000, 0x8000, 0x20000, 0x10000}; static const unsigned ram_sizes[256] = {0, 0x800, 0x2000, 0x8000, 0x20000, 0x10000};
gb->mbc_ram_size = ram_sizes[gb->rom[0x149]]; gb->mbc_ram_size = ram_sizes[gb->rom[0x149]];
@ -139,7 +161,7 @@ void GB_configure_cart(GB_gameboy_t *gb)
gb->mbc_ram = malloc(gb->mbc_ram_size); gb->mbc_ram = malloc(gb->mbc_ram_size);
} }
/* Todo: Some games assume unintialized MBC RAM is 0xFF. It this true for all cartridges types? */ /* Todo: Some games assume unintialized MBC RAM is 0xFF. It this true for all cartridge types? */
memset(gb->mbc_ram, 0xFF, gb->mbc_ram_size); memset(gb->mbc_ram, 0xFF, gb->mbc_ram_size);
} }

View File

@ -12,6 +12,7 @@ typedef struct {
GB_MBC5, GB_MBC5,
GB_HUC1, GB_HUC1,
GB_HUC3, GB_HUC3,
GB_TPP1,
} mbc_type; } mbc_type;
enum { enum {
GB_STANDARD_MBC, GB_STANDARD_MBC,

View File

@ -29,33 +29,80 @@ static GB_bus_t bus_for_addr(GB_gameboy_t *gb, uint16_t addr)
return GB_BUS_INTERNAL; return GB_BUS_INTERNAL;
} }
static uint8_t bitwise_glitch(uint8_t a, uint8_t b, uint8_t c) static uint16_t bitwise_glitch(uint16_t a, uint16_t b, uint16_t c)
{ {
return ((a ^ c) & (b ^ c)) ^ c; return ((a ^ c) & (b ^ c)) ^ c;
} }
static uint8_t bitwise_glitch_read(uint8_t a, uint8_t b, uint8_t c) static uint16_t bitwise_glitch_read(uint16_t a, uint16_t b, uint16_t c)
{ {
return b | (a & c); return b | (a & c);
} }
static uint8_t bitwise_glitch_read_increase(uint8_t a, uint8_t b, uint8_t c, uint8_t d) static uint16_t bitwise_glitch_read_secondary(uint16_t a, uint16_t b, uint16_t c, uint16_t d)
{ {
return (b & (a | c | d)) | (a & c & d); return (b & (a | c | d)) | (a & c & d);
} }
/*
Used on the MGB in some scenarios
static uint16_t bitwise_glitch_mgb(uint16_t a, uint16_t b, uint16_t c, uint16_t d, uint16_t e, bool variant)
{
return (c & (e | d | b | (variant? 0 : a))) | (b & d & (a | e));
}
*/
static uint16_t bitwise_glitch_tertiary_read_1(uint16_t a, uint16_t b, uint16_t c, uint16_t d, uint16_t e)
{
return c | (a & b & d & e);
}
static uint16_t bitwise_glitch_tertiary_read_2(uint16_t a, uint16_t b, uint16_t c, uint16_t d, uint16_t e)
{
return (c & (a | b | d | e)) | (a & b & d & e);
}
static uint16_t bitwise_glitch_tertiary_read_3(uint16_t a, uint16_t b, uint16_t c, uint16_t d, uint16_t e)
{
return (c & (a | b | d | e)) | (b & d & e);
}
static uint16_t bitwise_glitch_quaternary_read_dmg(uint16_t a, uint16_t b, uint16_t c, uint16_t d,
uint16_t e, uint16_t f, uint16_t g, uint16_t h)
{
/* On my DMG, some cases are non-deterministic, while on some other DMGs they yield constant zeros.
The non deterministic cases are affected by (on the row 40 case) 34, 36, 3e and 28, and potentially
others. For my own sanity I'm going to emulate the DMGs that output zeros. */
(void)a;
return (e & (h | g | (~d & f) | c | b)) | (c & g & h);
}
/*
// Oh my.
static uint16_t bitwise_glitch_quaternary_read_mgb(uint16_t a, uint16_t b, uint16_t c, uint16_t d,
uint16_t e, uint16_t f, uint16_t g, uint16_t h)
{
return (e & (h | g | c | (a & b))) | ((c & h) & (g & (~f | b | a | ~d) | (a & b & f)));
}
*/
static uint16_t bitwise_glitch_quaternary_read_sgb2(uint16_t a, uint16_t b, uint16_t c, uint16_t d,
uint16_t e, uint16_t f, uint16_t g, uint16_t h)
{
return (e & (h | g | c | (a & b))) | ((c & g & h) & (b | a | ~f));
}
void GB_trigger_oam_bug(GB_gameboy_t *gb, uint16_t address) void GB_trigger_oam_bug(GB_gameboy_t *gb, uint16_t address)
{ {
if (GB_is_cgb(gb)) return; if (GB_is_cgb(gb)) return;
if (address >= 0xFE00 && address < 0xFF00) { if (address >= 0xFE00 && address < 0xFF00) {
if (gb->accessed_oam_row != 0xff && gb->accessed_oam_row >= 8) { if (gb->accessed_oam_row != 0xff && gb->accessed_oam_row >= 8) {
gb->oam[gb->accessed_oam_row] = bitwise_glitch(gb->oam[gb->accessed_oam_row], uint16_t *base = (uint16_t *)(gb->oam + gb->accessed_oam_row);
gb->oam[gb->accessed_oam_row - 8], base[0] = bitwise_glitch(base[0],
gb->oam[gb->accessed_oam_row - 4]); base[-4],
gb->oam[gb->accessed_oam_row + 1] = bitwise_glitch(gb->oam[gb->accessed_oam_row + 1], base[-2]);
gb->oam[gb->accessed_oam_row - 7],
gb->oam[gb->accessed_oam_row - 3]);
for (unsigned i = 2; i < 8; i++) { for (unsigned i = 2; i < 8; i++) {
gb->oam[gb->accessed_oam_row + i] = gb->oam[gb->accessed_oam_row - 8 + i]; gb->oam[gb->accessed_oam_row + i] = gb->oam[gb->accessed_oam_row - 8 + i];
} }
@ -63,49 +110,140 @@ void GB_trigger_oam_bug(GB_gameboy_t *gb, uint16_t address)
} }
} }
static void oam_bug_secondary_read_corruption(GB_gameboy_t *gb)
{
if (gb->accessed_oam_row < 0x98) {
uint16_t *base = (uint16_t *)(gb->oam + gb->accessed_oam_row);
base[-4] = bitwise_glitch_read_secondary(base[-8],
base[-4],
base[0],
base[-2]);
for (unsigned i = 0; i < 8; i++) {
gb->oam[gb->accessed_oam_row - 0x10 + i] = gb->oam[gb->accessed_oam_row - 0x08 + i];
}
}
}
/*
static void oam_bug_tertiary_read_corruption_mgb(GB_gameboy_t *gb)
{
if (gb->accessed_oam_row < 0x98) {
uint16_t *base = (uint16_t *)(gb->oam + gb->accessed_oam_row);
uint16_t temp = bitwise_glitch_mgb(
base[0],
base[-2],
base[-4],
base[-8],
base[-16],
true);
base[-4] = bitwise_glitch_mgb(
base[0],
base[-2],
base[-4],
base[-8],
base[-16],
false);
for (unsigned i = 0; i < 8; i++) {
gb->oam[gb->accessed_oam_row - 0x10 + i] = gb->oam[gb->accessed_oam_row - 0x20 + i] = gb->oam[gb->accessed_oam_row - 0x08 + i];
}
base[-8] = temp;
base[-16] = temp;
}
}
*/
static void oam_bug_quaternary_read_corruption(GB_gameboy_t *gb, typeof(bitwise_glitch_quaternary_read_dmg) *bitwise_op)
{
if (gb->accessed_oam_row < 0x98) {
uint16_t *base = (uint16_t *)(gb->oam + gb->accessed_oam_row);
base[-4] = bitwise_op(*(uint16_t *)gb->oam,
base[0],
base[-2],
base[-3],
base[-4],
base[-7],
base[-8],
base[-16]);
for (unsigned i = 0; i < 8; i++) {
gb->oam[gb->accessed_oam_row - 0x10 + i] = gb->oam[gb->accessed_oam_row - 0x20 + i] = gb->oam[gb->accessed_oam_row - 0x08 + i];
}
}
}
static void oam_bug_tertiary_read_corruption(GB_gameboy_t *gb, typeof(bitwise_glitch_tertiary_read_1) *bitwise_op)
{
if (gb->accessed_oam_row < 0x98) {
uint16_t *base = (uint16_t *)(gb->oam + gb->accessed_oam_row);
/* On some instances, the corruption row is copied to the first row for some accessed row. On my DMG it happens
for row 80, and on my MGB it happens on row 60. Some instances only copy odd or even bytes. Additionally,
for some instances on accessed rows that do not affect the first row, the last two bytes of the preceeding
row are also corrupted in a non-deterministic probability. */
base[-4] = bitwise_op(
base[0],
base[-2],
base[-4],
base[-8],
base[-16]);
for (unsigned i = 0; i < 8; i++) {
gb->oam[gb->accessed_oam_row - 0x10 + i] = gb->oam[gb->accessed_oam_row - 0x20 + i] = gb->oam[gb->accessed_oam_row - 0x08 + i];
}
}
}
void GB_trigger_oam_bug_read(GB_gameboy_t *gb, uint16_t address) void GB_trigger_oam_bug_read(GB_gameboy_t *gb, uint16_t address)
{ {
if (GB_is_cgb(gb)) return; if (GB_is_cgb(gb)) return;
if (address >= 0xFE00 && address < 0xFF00) { if (address >= 0xFE00 && address < 0xFF00) {
if (gb->accessed_oam_row != 0xff && gb->accessed_oam_row >= 8) { if (gb->accessed_oam_row != 0xff && gb->accessed_oam_row >= 8) {
gb->oam[gb->accessed_oam_row - 8] = if ((gb->accessed_oam_row & 0x18) == 0x10) {
gb->oam[gb->accessed_oam_row] = bitwise_glitch_read(gb->oam[gb->accessed_oam_row], oam_bug_secondary_read_corruption(gb);
gb->oam[gb->accessed_oam_row - 8], }
gb->oam[gb->accessed_oam_row - 4]); else if ((gb->accessed_oam_row & 0x18) == 0x00) {
gb->oam[gb->accessed_oam_row - 7] = /* Everything in this specific case is *extremely* revision and instance specific. */
gb->oam[gb->accessed_oam_row + 1] = bitwise_glitch_read(gb->oam[gb->accessed_oam_row + 1], if (gb->accessed_oam_row == 0x40) {
gb->oam[gb->accessed_oam_row - 7], oam_bug_quaternary_read_corruption(gb,
gb->oam[gb->accessed_oam_row - 3]); ((gb->model & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB2)?
for (unsigned i = 2; i < 8; i++) { bitwise_glitch_quaternary_read_sgb2:
bitwise_glitch_quaternary_read_dmg);
}
else if ((gb->model & ~GB_MODEL_NO_SFC_BIT) != GB_MODEL_SGB2) {
if (gb->accessed_oam_row == 0x20) {
oam_bug_tertiary_read_corruption(gb, bitwise_glitch_tertiary_read_2);
}
else if (gb->accessed_oam_row == 0x60) {
oam_bug_tertiary_read_corruption(gb, bitwise_glitch_tertiary_read_3);
}
else {
oam_bug_tertiary_read_corruption(gb, bitwise_glitch_tertiary_read_1);
}
}
else {
oam_bug_tertiary_read_corruption(gb, bitwise_glitch_tertiary_read_2);
}
}
else {
uint16_t *base = (uint16_t *)(gb->oam + gb->accessed_oam_row);
base[-4] =
base[0] = bitwise_glitch_read(base[0],
base[-4],
base[-2]);
}
for (unsigned i = 0; i < 8; i++) {
gb->oam[gb->accessed_oam_row + i] = gb->oam[gb->accessed_oam_row - 8 + i]; gb->oam[gb->accessed_oam_row + i] = gb->oam[gb->accessed_oam_row - 8 + i];
} }
if (gb->accessed_oam_row == 0x80) {
memcpy(gb->oam, gb->oam + gb->accessed_oam_row, 8);
}
} }
} }
} }
void GB_trigger_oam_bug_read_increase(GB_gameboy_t *gb, uint16_t address)
{
if (GB_is_cgb(gb)) return;
if (address >= 0xFE00 && address < 0xFF00) {
if (gb->accessed_oam_row != 0xff && gb->accessed_oam_row >= 0x20 && gb->accessed_oam_row < 0x98) {
gb->oam[gb->accessed_oam_row - 0x8] = bitwise_glitch_read_increase(gb->oam[gb->accessed_oam_row - 0x10],
gb->oam[gb->accessed_oam_row - 0x08],
gb->oam[gb->accessed_oam_row ],
gb->oam[gb->accessed_oam_row - 0x04]
);
gb->oam[gb->accessed_oam_row - 0x7] = bitwise_glitch_read_increase(gb->oam[gb->accessed_oam_row - 0x0f],
gb->oam[gb->accessed_oam_row - 0x07],
gb->oam[gb->accessed_oam_row + 0x01],
gb->oam[gb->accessed_oam_row - 0x03]
);
for (unsigned i = 0; i < 8; i++) {
gb->oam[gb->accessed_oam_row + i] = gb->oam[gb->accessed_oam_row - 0x10 + i] = gb->oam[gb->accessed_oam_row - 0x08 + i];
}
}
}
}
static bool is_addr_in_dma_use(GB_gameboy_t *gb, uint16_t addr) static bool is_addr_in_dma_use(GB_gameboy_t *gb, uint16_t addr)
{ {
@ -113,11 +251,6 @@ static bool is_addr_in_dma_use(GB_gameboy_t *gb, uint16_t addr)
return bus_for_addr(gb, addr) == bus_for_addr(gb, gb->dma_current_src); return bus_for_addr(gb, addr) == bus_for_addr(gb, gb->dma_current_src);
} }
static bool effective_ir_input(GB_gameboy_t *gb)
{
return gb->infrared_input || (gb->io_registers[GB_IO_RP] & 1) || gb->cart_ir;
}
static uint8_t read_rom(GB_gameboy_t *gb, uint16_t addr) static uint8_t read_rom(GB_gameboy_t *gb, uint16_t addr)
{ {
if (addr < 0x100 && !gb->boot_rom_finished) { if (addr < 0x100 && !gb->boot_rom_finished) {
@ -173,7 +306,7 @@ static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr)
case 0xD: // RTC status case 0xD: // RTC status
return 1; return 1;
case 0xE: // IR mode case 0xE: // IR mode
return effective_ir_input(gb); // TODO: What are the other bits? return gb->effective_ir_input; // TODO: What are the other bits?
default: default:
GB_log(gb, "Unsupported HuC-3 mode %x read: %04x\n", gb->huc3_mode, addr); GB_log(gb, "Unsupported HuC-3 mode %x read: %04x\n", gb->huc3_mode, addr);
return 1; // TODO: What happens in this case? return 1; // TODO: What happens in this case?
@ -183,7 +316,25 @@ static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr)
} }
} }
if ((!gb->mbc_ram_enable) && if (gb->cartridge_type->mbc_type == GB_TPP1) {
switch (gb->tpp1_mode) {
case 0:
switch (addr & 3) {
case 0: return gb->tpp1_rom_bank;
case 1: return gb->tpp1_rom_bank >> 8;
case 2: return gb->tpp1_ram_bank;
case 3: return gb->rumble_strength | gb->tpp1_mr4;
}
case 2:
case 3:
break; // Read RAM
case 5:
return gb->rtc_latched.data[(addr & 3) ^ 3];
default:
return 0xFF;
}
}
else if ((!gb->mbc_ram_enable) &&
gb->cartridge_type->mbc_subtype != GB_CAMERA && gb->cartridge_type->mbc_subtype != GB_CAMERA &&
gb->cartridge_type->mbc_type != GB_HUC1 && gb->cartridge_type->mbc_type != GB_HUC1 &&
gb->cartridge_type->mbc_type != GB_HUC3) { gb->cartridge_type->mbc_type != GB_HUC3) {
@ -191,14 +342,20 @@ static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr)
} }
if (gb->cartridge_type->mbc_type == GB_HUC1 && gb->huc1.ir_mode) { if (gb->cartridge_type->mbc_type == GB_HUC1 && gb->huc1.ir_mode) {
return 0xc0 | effective_ir_input(gb); return 0xc0 | gb->effective_ir_input;
} }
if (gb->cartridge_type->has_rtc && gb->cartridge_type->mbc_type != GB_HUC3 && if (gb->cartridge_type->has_rtc && gb->cartridge_type->mbc_type != GB_HUC3 &&
gb->mbc3_rtc_mapped && gb->mbc_ram_bank <= 4) { gb->mbc3_rtc_mapped) {
/* RTC read */ /* RTC read */
gb->rtc_latched.high |= ~0xC1; /* Not all bytes in RTC high are used. */ if (gb->mbc_ram_bank <= 4) {
return gb->rtc_latched.data[gb->mbc_ram_bank]; gb->rtc_latched.seconds &= 0x3F;
gb->rtc_latched.minutes &= 0x3F;
gb->rtc_latched.hours &= 0x1F;
gb->rtc_latched.high &= 0xC1;
return gb->rtc_latched.data[gb->mbc_ram_bank];
}
return 0xFF;
} }
if (gb->camera_registers_mapped) { if (gb->camera_registers_mapped) {
@ -215,6 +372,9 @@ static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr)
uint8_t effective_bank = gb->mbc_ram_bank; uint8_t effective_bank = gb->mbc_ram_bank;
if (gb->cartridge_type->mbc_type == GB_MBC3 && !gb->is_mbc30) { if (gb->cartridge_type->mbc_type == GB_MBC3 && !gb->is_mbc30) {
if (gb->cartridge_type->has_rtc) {
if (effective_bank > 3) return 0xFF;
}
effective_bank &= 0x3; effective_bank &= 0x3;
} }
uint8_t ret = gb->mbc_ram[((addr & 0x1FFF) + effective_bank * 0x2000) & (gb->mbc_ram_size - 1)]; uint8_t ret = gb->mbc_ram[((addr & 0x1FFF) + effective_bank * 0x2000) & (gb->mbc_ram_size - 1)];
@ -259,26 +419,53 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr)
if (gb->oam_read_blocked) { if (gb->oam_read_blocked) {
if (!GB_is_cgb(gb)) { if (!GB_is_cgb(gb)) {
if (addr < 0xFEA0) { if (addr < 0xFEA0) {
uint16_t *oam = (uint16_t *)gb->oam;
if (gb->accessed_oam_row == 0) { if (gb->accessed_oam_row == 0) {
gb->oam[(addr & 0xf8)] = oam[(addr & 0xf8) >> 1] =
gb->oam[0] = bitwise_glitch_read(gb->oam[0], oam[0] = bitwise_glitch_read(oam[0],
gb->oam[(addr & 0xf8)], oam[(addr & 0xf8) >> 1],
gb->oam[(addr & 0xfe)]); oam[(addr & 0xff) >> 1]);
gb->oam[(addr & 0xf8) + 1] =
gb->oam[1] = bitwise_glitch_read(gb->oam[1],
gb->oam[(addr & 0xf8) + 1],
gb->oam[(addr & 0xfe) | 1]);
for (unsigned i = 2; i < 8; i++) { for (unsigned i = 2; i < 8; i++) {
gb->oam[i] = gb->oam[(addr & 0xf8) + i]; gb->oam[i] = gb->oam[(addr & 0xf8) + i];
} }
} }
else if (gb->accessed_oam_row == 0xa0) { else if (gb->accessed_oam_row == 0xa0) {
gb->oam[0x9e] = bitwise_glitch_read(gb->oam[0x9c], uint8_t target = (addr & 7) | 0x98;
gb->oam[0x9e], uint16_t a = oam[0x9c >> 1],
gb->oam[(addr & 0xf8) | 6]); b = oam[target >> 1],
gb->oam[0x9f] = bitwise_glitch_read(gb->oam[0x9d], c = oam[(addr & 0xf8) >> 1];
gb->oam[0x9f], switch (addr & 7) {
gb->oam[(addr & 0xf8) | 7]); case 0:
case 1:
/* Probably instance specific */
if ((gb->model & GB_MODEL_FAMILY_MASK) == GB_MODEL_DMG_FAMILY) {
oam[target >> 1] = (a & b) | (a & c) | (b & c);
}
else {
oam[target >> 1] = bitwise_glitch_read(a, b, c);
}
break;
case 2:
case 3: {
/* Probably instance specific */
c = oam[(addr & 0xfe) >> 1];
// MGB only: oam[target >> 1] = bitwise_glitch_read(a, b, c);
oam[target >> 1] = (a & b) | (a & c) | (b & c);
break;
}
case 4:
case 5:
break; // No additional corruption
case 6:
case 7:
oam[target >> 1] = bitwise_glitch_read(a, b, c);
break;
default:
break;
}
for (unsigned i = 0; i < 8; i++) { for (unsigned i = 0; i < 8; i++) {
gb->oam[(addr & 0xf8) + i] = gb->oam[0x98 + i]; gb->oam[(addr & 0xf8) + i] = gb->oam[0x98 + i];
@ -331,9 +518,7 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr)
} }
if (addr < 0xFF00) { if (addr < 0xFF00) {
return 0; return 0;
} }
if (addr < 0xFF80) { if (addr < 0xFF80) {
@ -428,9 +613,12 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr)
case GB_IO_RP: { case GB_IO_RP: {
if (!gb->cgb_mode) return 0xFF; if (!gb->cgb_mode) return 0xFF;
/* You will read your own IR LED if it's on. */ /* You will read your own IR LED if it's on. */
uint8_t ret = (gb->io_registers[GB_IO_RP] & 0xC1) | 0x3C; uint8_t ret = (gb->io_registers[GB_IO_RP] & 0xC1) | 0x2E;
if ((gb->io_registers[GB_IO_RP] & 0xC0) == 0xC0 && effective_ir_input(gb)) { if (gb->model != GB_MODEL_CGB_E) {
ret |= 2; ret |= 0x10;
}
if (((gb->io_registers[GB_IO_RP] & 0xC0) == 0xC0 && gb->effective_ir_input) && gb->model != GB_MODEL_AGB) {
ret &= ~2;
} }
return ret; return ret;
} }
@ -518,22 +706,19 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
gb->mbc3_rtc_mapped = value & 8; gb->mbc3_rtc_mapped = value & 8;
break; break;
case 0x6000: case 0x7000: case 0x6000: case 0x7000:
if (!gb->rtc_latch && (value & 1)) { /* Todo: verify condition is correct */ memcpy(&gb->rtc_latched, &gb->rtc_real, sizeof(gb->rtc_real));
memcpy(&gb->rtc_latched, &gb->rtc_real, sizeof(gb->rtc_real));
}
gb->rtc_latch = value & 1;
break; break;
} }
break; break;
case GB_MBC5: case GB_MBC5:
switch (addr & 0xF000) { switch (addr & 0xF000) {
case 0x0000: case 0x1000: gb->mbc_ram_enable = (value & 0xF) == 0xA; break; case 0x0000: case 0x1000: gb->mbc_ram_enable = value == 0x0A; break;
case 0x2000: gb->mbc5.rom_bank_low = value; break; case 0x2000: gb->mbc5.rom_bank_low = value; break;
case 0x3000: gb->mbc5.rom_bank_high = value; break; case 0x3000: gb->mbc5.rom_bank_high = value; break;
case 0x4000: case 0x5000: case 0x4000: case 0x5000:
if (gb->cartridge_type->has_rumble) { if (gb->cartridge_type->has_rumble) {
if (!!(value & 8) != gb->rumble_state) { if (!!(value & 8) != !!gb->rumble_strength) {
gb->rumble_state = !gb->rumble_state; gb->rumble_strength = gb->rumble_strength? 0 : 3;
} }
value &= 7; value &= 7;
} }
@ -560,6 +745,53 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
case 0x4000: case 0x5000: gb->huc3.ram_bank = value; break; case 0x4000: case 0x5000: gb->huc3.ram_bank = value; break;
} }
break; break;
case GB_TPP1:
switch (addr & 3) {
case 0:
gb->tpp1_rom_bank &= 0xFF00;
gb->tpp1_rom_bank |= value;
break;
case 1:
gb->tpp1_rom_bank &= 0xFF;
gb->tpp1_rom_bank |= value << 8;
break;
case 2:
gb->tpp1_ram_bank = value;
break;
case 3:
switch (value) {
case 0:
case 2:
case 3:
case 5:
gb->tpp1_mode = value;
break;
case 0x10:
memcpy(&gb->rtc_latched, &gb->rtc_real, sizeof(gb->rtc_real));
break;
case 0x11: {
memcpy(&gb->rtc_real, &gb->rtc_latched, sizeof(gb->rtc_real));
break;
}
case 0x14:
gb->tpp1_mr4 &= ~0x8;
break;
case 0x18:
gb->tpp1_mr4 &= ~0x4;
break;
case 0x19:
gb->tpp1_mr4 |= 0x4;
break;
case 0x20:
case 0x21:
case 0x22:
case 0x23:
gb->rumble_strength = value & 3;
break;
}
}
break;
} }
GB_update_mbc_mappings(gb); GB_update_mbc_mappings(gb);
} }
@ -652,14 +884,11 @@ static bool huc3_write(GB_gameboy_t *gb, uint8_t value)
// Not sure what writes here mean, they're always 0xFE // Not sure what writes here mean, they're always 0xFE
return true; return true;
case 0xE: { // IR mode case 0xE: { // IR mode
bool old_input = effective_ir_input(gb); if (gb->cart_ir != (value & 1)) {
gb->cart_ir = value & 1; gb->cart_ir = value & 1;
bool new_input = effective_ir_input(gb);
if (new_input != old_input) {
if (gb->infrared_callback) { if (gb->infrared_callback) {
gb->infrared_callback(gb, new_input, gb->cycles_since_ir_change); gb->infrared_callback(gb, value & 1);
} }
gb->cycles_since_ir_change = 0;
} }
return true; return true;
} }
@ -684,24 +913,38 @@ static void write_mbc_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
return; return;
} }
if (gb->cartridge_type->mbc_type == GB_TPP1) {
switch (gb->tpp1_mode) {
case 3:
break;
case 5:
gb->rtc_latched.data[(addr & 3) ^ 3] = value;
return;
default:
return;
}
}
if ((!gb->mbc_ram_enable) if ((!gb->mbc_ram_enable)
&& gb->cartridge_type->mbc_type != GB_HUC1) return; && gb->cartridge_type->mbc_type != GB_HUC1) return;
if (gb->cartridge_type->mbc_type == GB_HUC1 && gb->huc1.ir_mode) { if (gb->cartridge_type->mbc_type == GB_HUC1 && gb->huc1.ir_mode) {
bool old_input = effective_ir_input(gb); if (gb->cart_ir != (value & 1)) {
gb->cart_ir = value & 1; gb->cart_ir = value & 1;
bool new_input = effective_ir_input(gb);
if (new_input != old_input) {
if (gb->infrared_callback) { if (gb->infrared_callback) {
gb->infrared_callback(gb, new_input, gb->cycles_since_ir_change); gb->infrared_callback(gb, value & 1);
} }
gb->cycles_since_ir_change = 0;
} }
return; return;
} }
if (gb->cartridge_type->has_rtc && gb->mbc3_rtc_mapped && gb->mbc_ram_bank <= 4) { if (gb->cartridge_type->has_rtc && gb->mbc3_rtc_mapped) {
gb->rtc_latched.data[gb->mbc_ram_bank] = gb->rtc_real.data[gb->mbc_ram_bank] = value; if (gb->mbc_ram_bank <= 4) {
if (gb->mbc_ram_bank == 0) {
gb->rtc_cycles = 0;
}
gb->rtc_real.data[gb->mbc_ram_bank] = value;
}
return; return;
} }
@ -711,6 +954,9 @@ static void write_mbc_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
uint8_t effective_bank = gb->mbc_ram_bank; uint8_t effective_bank = gb->mbc_ram_bank;
if (gb->cartridge_type->mbc_type == GB_MBC3 && !gb->is_mbc30) { if (gb->cartridge_type->mbc_type == GB_MBC3 && !gb->is_mbc30) {
if (gb->cartridge_type->has_rtc) {
if (effective_bank > 3) return;
}
effective_bank &= 0x3; effective_bank &= 0x3;
} }
@ -903,15 +1149,18 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
if ((value & 0x80) && !(gb->io_registers[GB_IO_LCDC] & 0x80)) { if ((value & 0x80) && !(gb->io_registers[GB_IO_LCDC] & 0x80)) {
gb->display_cycles = 0; gb->display_cycles = 0;
gb->display_state = 0; gb->display_state = 0;
gb->double_speed_alignment = 0;
if (GB_is_sgb(gb)) { if (GB_is_sgb(gb)) {
gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED; gb->frame_skip_state = GB_FRAMESKIP_SECOND_FRAME_RENDERED;
} }
else if (gb->frame_skip_state == GB_FRAMESKIP_SECOND_FRAME_RENDERED) { else if (gb->frame_skip_state == GB_FRAMESKIP_SECOND_FRAME_RENDERED) {
gb->frame_skip_state = GB_FRAMESKIP_LCD_TURNED_ON; gb->frame_skip_state = GB_FRAMESKIP_LCD_TURNED_ON;
} }
GB_timing_sync(gb);
} }
else if (!(value & 0x80) && (gb->io_registers[GB_IO_LCDC] & 0x80)) { else if (!(value & 0x80) && (gb->io_registers[GB_IO_LCDC] & 0x80)) {
/* Sync after turning off LCD */ /* Sync after turning off LCD */
gb->double_speed_alignment = 0;
GB_timing_sync(gb); GB_timing_sync(gb);
GB_lcd_off(gb); GB_lcd_off(gb);
} }
@ -1108,15 +1357,13 @@ static void write_high_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
if (!GB_is_cgb(gb)) { if (!GB_is_cgb(gb)) {
return; return;
} }
bool old_input = effective_ir_input(gb); if ((gb->io_registers[GB_IO_RP] ^ value) & 1) {
gb->io_registers[GB_IO_RP] = value;
bool new_input = effective_ir_input(gb);
if (new_input != old_input) {
if (gb->infrared_callback) { if (gb->infrared_callback) {
gb->infrared_callback(gb, new_input, gb->cycles_since_ir_change); gb->infrared_callback(gb, value & 1);
} }
gb->cycles_since_ir_change = 0;
} }
gb->io_registers[GB_IO_RP] = value;
return; return;
} }

View File

@ -12,7 +12,6 @@ void GB_write_memory(GB_gameboy_t *gb, uint16_t addr, uint8_t value);
void GB_dma_run(GB_gameboy_t *gb); void GB_dma_run(GB_gameboy_t *gb);
void GB_hdma_run(GB_gameboy_t *gb); void GB_hdma_run(GB_gameboy_t *gb);
void GB_trigger_oam_bug(GB_gameboy_t *gb, uint16_t address); void GB_trigger_oam_bug(GB_gameboy_t *gb, uint16_t address);
void GB_trigger_oam_bug_read_increase(GB_gameboy_t *gb, uint16_t address);
#endif #endif
#endif /* memory_h */ #endif /* memory_h */

View File

@ -11,7 +11,6 @@
static void handle_command(GB_gameboy_t *gb) static void handle_command(GB_gameboy_t *gb)
{ {
switch (gb->printer.command_id) { switch (gb->printer.command_id) {
case GB_PRINTER_INIT_COMMAND: case GB_PRINTER_INIT_COMMAND:
gb->printer.status = 0; gb->printer.status = 0;
@ -71,7 +70,7 @@ static void handle_command(GB_gameboy_t *gb)
} }
static void byte_reieve_completed(GB_gameboy_t *gb, uint8_t byte_received) static void byte_recieve_completed(GB_gameboy_t *gb, uint8_t byte_received)
{ {
gb->printer.byte_to_send = 0; gb->printer.byte_to_send = 0;
switch (gb->printer.command_state) { switch (gb->printer.command_state) {
@ -189,11 +188,16 @@ static void byte_reieve_completed(GB_gameboy_t *gb, uint8_t byte_received)
static void serial_start(GB_gameboy_t *gb, bool bit_received) static void serial_start(GB_gameboy_t *gb, bool bit_received)
{ {
if (gb->printer.idle_time > GB_get_unmultiplied_clock_rate(gb)) {
gb->printer.command_state = GB_PRINTER_COMMAND_MAGIC1;
gb->printer.bits_received = 0;
}
gb->printer.idle_time = 0;
gb->printer.byte_being_received <<= 1; gb->printer.byte_being_received <<= 1;
gb->printer.byte_being_received |= bit_received; gb->printer.byte_being_received |= bit_received;
gb->printer.bits_received++; gb->printer.bits_received++;
if (gb->printer.bits_received == 8) { if (gb->printer.bits_received == 8) {
byte_reieve_completed(gb, gb->printer.byte_being_received); byte_recieve_completed(gb, gb->printer.byte_being_received);
gb->printer.bits_received = 0; gb->printer.bits_received = 0;
gb->printer.byte_being_received = 0; gb->printer.byte_being_received = 0;
} }

View File

@ -48,8 +48,7 @@ typedef struct
uint8_t image[160 * 200]; uint8_t image[160 * 200];
uint16_t image_offset; uint16_t image_offset;
/* TODO: Delete me. */ uint64_t idle_time;
uint64_t padding;
uint8_t compression_run_lenth; uint8_t compression_run_lenth;
bool compression_run_is_compressed; bool compression_run_is_compressed;

View File

@ -108,7 +108,7 @@ static void state_decompress(const uint8_t *prev, uint8_t *data, uint8_t *dest,
void GB_rewind_push(GB_gameboy_t *gb) void GB_rewind_push(GB_gameboy_t *gb)
{ {
const size_t save_size = GB_get_save_state_size(gb); const size_t save_size = GB_get_save_state_size_no_bess(gb);
if (!gb->rewind_sequences) { if (!gb->rewind_sequences) {
if (gb->rewind_buffer_length) { if (gb->rewind_buffer_length) {
gb->rewind_sequences = malloc(sizeof(*gb->rewind_sequences) * gb->rewind_buffer_length); gb->rewind_sequences = malloc(sizeof(*gb->rewind_sequences) * gb->rewind_buffer_length);
@ -140,11 +140,11 @@ void GB_rewind_push(GB_gameboy_t *gb)
if (!gb->rewind_sequences[gb->rewind_pos].key_state) { if (!gb->rewind_sequences[gb->rewind_pos].key_state) {
gb->rewind_sequences[gb->rewind_pos].key_state = malloc(save_size); gb->rewind_sequences[gb->rewind_pos].key_state = malloc(save_size);
GB_save_state_to_buffer(gb, gb->rewind_sequences[gb->rewind_pos].key_state); GB_save_state_to_buffer_no_bess(gb, gb->rewind_sequences[gb->rewind_pos].key_state);
} }
else { else {
uint8_t *save_state = malloc(save_size); uint8_t *save_state = malloc(save_size);
GB_save_state_to_buffer(gb, save_state); GB_save_state_to_buffer_no_bess(gb, save_state);
gb->rewind_sequences[gb->rewind_pos].compressed_states[gb->rewind_sequences[gb->rewind_pos].pos++] = gb->rewind_sequences[gb->rewind_pos].compressed_states[gb->rewind_sequences[gb->rewind_pos].pos++] =
state_compress(gb->rewind_sequences[gb->rewind_pos].key_state, save_state, save_size); state_compress(gb->rewind_sequences[gb->rewind_pos].key_state, save_state, save_size);
free(save_state); free(save_state);
@ -158,7 +158,7 @@ bool GB_rewind_pop(GB_gameboy_t *gb)
return false; return false;
} }
const size_t save_size = GB_get_save_state_size(gb); const size_t save_size = GB_get_save_state_size_no_bess(gb);
if (gb->rewind_sequences[gb->rewind_pos].pos == 0) { if (gb->rewind_sequences[gb->rewind_pos].pos == 0) {
GB_load_state_from_buffer(gb, gb->rewind_sequences[gb->rewind_pos].key_state, save_size); GB_load_state_from_buffer(gb, gb->rewind_sequences[gb->rewind_pos].key_state, save_size);
free(gb->rewind_sequences[gb->rewind_pos].key_state); free(gb->rewind_sequences[gb->rewind_pos].key_state);

View File

@ -15,7 +15,8 @@ void GB_handle_rumble(GB_gameboy_t *gb)
if (gb->rumble_mode == GB_RUMBLE_DISABLED) { if (gb->rumble_mode == GB_RUMBLE_DISABLED) {
return; return;
} }
if (gb->cartridge_type->has_rumble) { if (gb->cartridge_type->has_rumble &&
(gb->cartridge_type->mbc_type != GB_TPP1 || (gb->rom[0x153] & 1))) {
if (gb->rumble_on_cycles + gb->rumble_off_cycles) { if (gb->rumble_on_cycles + gb->rumble_off_cycles) {
gb->rumble_callback(gb, gb->rumble_on_cycles / (double)(gb->rumble_on_cycles + gb->rumble_off_cycles)); gb->rumble_callback(gb, gb->rumble_on_cycles / (double)(gb->rumble_on_cycles + gb->rumble_off_cycles));
gb->rumble_on_cycles = gb->rumble_off_cycles = 0; gb->rumble_on_cycles = gb->rumble_off_cycles = 0;
@ -25,14 +26,17 @@ void GB_handle_rumble(GB_gameboy_t *gb)
unsigned volume = (gb->io_registers[GB_IO_NR50] & 7) + 1 + ((gb->io_registers[GB_IO_NR50] >> 4) & 7) + 1; unsigned volume = (gb->io_registers[GB_IO_NR50] & 7) + 1 + ((gb->io_registers[GB_IO_NR50] >> 4) & 7) + 1;
unsigned ch4_volume = volume * (!!(gb->io_registers[GB_IO_NR51] & 8) + !!(gb->io_registers[GB_IO_NR51] & 0x80)); unsigned ch4_volume = volume * (!!(gb->io_registers[GB_IO_NR51] & 8) + !!(gb->io_registers[GB_IO_NR51] & 0x80));
unsigned ch1_volume = volume * (!!(gb->io_registers[GB_IO_NR51] & 1) + !!(gb->io_registers[GB_IO_NR51] & 0x10)); unsigned ch1_volume = volume * (!!(gb->io_registers[GB_IO_NR51] & 1) + !!(gb->io_registers[GB_IO_NR51] & 0x10));
unsigned ch4_divisor = (gb->io_registers[GB_IO_NR43] & 0x07) << 1;
double ch4_rumble = (MIN(gb->apu.noise_channel.sample_length * (gb->apu.noise_channel.narrow? 8 : 1) , 4096) * ((signed) gb->apu.noise_channel.current_volume * gb->apu.noise_channel.current_volume * ch4_volume / 32.0 - 50) - 2048) / 2048.0; if (!ch4_divisor) ch4_divisor = 1;
unsigned ch4_sample_length = (ch4_divisor << (gb->io_registers[GB_IO_NR43] >> 4)) - 1;
double ch4_rumble = (MIN(ch4_sample_length * (gb->apu.noise_channel.narrow? 8 : 1) , 4096) * ((signed) gb->apu.noise_channel.current_volume * gb->apu.noise_channel.current_volume * ch4_volume / 32.0 - 50) - 2048) / 2048.0;
ch4_rumble = MIN(ch4_rumble, 1.0); ch4_rumble = MIN(ch4_rumble, 1.0);
ch4_rumble = MAX(ch4_rumble, 0.0); ch4_rumble = MAX(ch4_rumble, 0.0);
double ch1_rumble = 0; double ch1_rumble = 0;
if (gb->apu.sweep_enabled && ((gb->io_registers[GB_IO_NR10] >> 4) & 7)) { if ((gb->io_registers[GB_IO_NR10] & 0x7) && (gb->io_registers[GB_IO_NR10] & 0x70)) {
double sweep_speed = (gb->io_registers[GB_IO_NR10] & 7) / (double)((gb->io_registers[GB_IO_NR10] >> 4) & 7); double sweep_speed = (gb->io_registers[GB_IO_NR10] & 7) / (double)((gb->io_registers[GB_IO_NR10] >> 4) & 7);
ch1_rumble = gb->apu.square_channels[GB_SQUARE_1].current_volume * ch1_volume / 32.0 * sweep_speed / 8.0 - 0.5; ch1_rumble = gb->apu.square_channels[GB_SQUARE_1].current_volume * ch1_volume / 32.0 * sweep_speed / 8.0 - 0.5;
ch1_rumble = MIN(ch1_rumble, 1.0); ch1_rumble = MIN(ch1_rumble, 1.0);

File diff suppressed because it is too large Load Diff

View File

@ -27,4 +27,17 @@ void GB_save_state_to_buffer(GB_gameboy_t *gb, uint8_t *buffer);
int GB_load_state(GB_gameboy_t *gb, const char *path); int GB_load_state(GB_gameboy_t *gb, const char *path);
int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t length); int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t length);
bool GB_is_stave_state(const char *path);
#ifdef GB_INTERNAL
static inline uint32_t state_magic(void)
{
if (sizeof(bool) == 1) return 'SAME';
return 'S4ME';
}
/* For internal in-memory save states (rewind, debugger) that do not need BESS */
size_t GB_get_save_state_size_no_bess(GB_gameboy_t *gb);
void GB_save_state_to_buffer_no_bess(GB_gameboy_t *gb, uint8_t *buffer);
#endif
#endif /* save_state_h */ #endif /* save_state_h */

View File

@ -7,8 +7,6 @@
#define M_PI 3.14159265358979323846 #define M_PI 3.14159265358979323846
#endif #endif
#define INTRO_ANIMATION_LENGTH 200
enum { enum {
PAL01 = 0x00, PAL01 = 0x00,
PAL23 = 0x01, PAL23 = 0x01,
@ -49,14 +47,14 @@ static inline void pal_command(GB_gameboy_t *gb, unsigned first, unsigned second
{ {
gb->sgb->effective_palettes[0] = gb->sgb->effective_palettes[4] = gb->sgb->effective_palettes[0] = gb->sgb->effective_palettes[4] =
gb->sgb->effective_palettes[8] = gb->sgb->effective_palettes[12] = gb->sgb->effective_palettes[8] = gb->sgb->effective_palettes[12] =
gb->sgb->command[1] | (gb->sgb->command[2] << 8); *(uint16_t *)&gb->sgb->command[1];
for (unsigned i = 0; i < 3; i++) { for (unsigned i = 0; i < 3; i++) {
gb->sgb->effective_palettes[first * 4 + i + 1] = gb->sgb->command[3 + i * 2] | (gb->sgb->command[4 + i * 2] << 8); gb->sgb->effective_palettes[first * 4 + i + 1] = *(uint16_t *)&gb->sgb->command[3 + i * 2];
} }
for (unsigned i = 0; i < 3; i++) { for (unsigned i = 0; i < 3; i++) {
gb->sgb->effective_palettes[second * 4 + i + 1] = gb->sgb->command[9 + i * 2] | (gb->sgb->command[10 + i * 2] << 8); gb->sgb->effective_palettes[second * 4 + i + 1] = *(uint16_t *)&gb->sgb->command[9 + i * 2];
} }
} }
@ -172,10 +170,10 @@ static void command_ready(GB_gameboy_t *gb)
gb->sgb->disable_commands = true; gb->sgb->disable_commands = true;
for (unsigned i = 0; i < sizeof(palette_assignments) / sizeof(palette_assignments[0]); i++) { for (unsigned i = 0; i < sizeof(palette_assignments) / sizeof(palette_assignments[0]); i++) {
if (memcmp(palette_assignments[i].name, &gb->sgb->received_header[0x30], sizeof(palette_assignments[i].name)) == 0) { if (memcmp(palette_assignments[i].name, &gb->sgb->received_header[0x30], sizeof(palette_assignments[i].name)) == 0) {
gb->sgb->effective_palettes[0] = built_in_palettes[palette_assignments[i].palette_index * 4 - 4]; gb->sgb->effective_palettes[0] = LE16(built_in_palettes[palette_assignments[i].palette_index * 4 - 4]);
gb->sgb->effective_palettes[1] = built_in_palettes[palette_assignments[i].palette_index * 4 + 1 - 4]; gb->sgb->effective_palettes[1] = LE16(built_in_palettes[palette_assignments[i].palette_index * 4 + 1 - 4]);
gb->sgb->effective_palettes[2] = built_in_palettes[palette_assignments[i].palette_index * 4 + 2 - 4]; gb->sgb->effective_palettes[2] = LE16(built_in_palettes[palette_assignments[i].palette_index * 4 + 2 - 4]);
gb->sgb->effective_palettes[3] = built_in_palettes[palette_assignments[i].palette_index * 4 + 3 - 4]; gb->sgb->effective_palettes[3] = LE16(built_in_palettes[palette_assignments[i].palette_index * 4 + 3 - 4]);
break; break;
} }
} }
@ -269,7 +267,7 @@ static void command_ready(GB_gameboy_t *gb)
#endif #endif
uint8_t x = command->x; uint8_t x = command->x;
uint8_t y = command->y; uint8_t y = command->y;
if (x >= 20 || y >= 18 || (count + 3) / 4 > sizeof(gb->sgb->command) - sizeof(*command) - 1) { if (x >= 20 || y >= 18) {
/* TODO: Verify with the SFC BIOS */ /* TODO: Verify with the SFC BIOS */
break; break;
} }
@ -283,7 +281,7 @@ static void command_ready(GB_gameboy_t *gb)
x++; x++;
y = 0; y = 0;
if (x == 20) { if (x == 20) {
x = 0; break;
} }
} }
} }
@ -293,7 +291,7 @@ static void command_ready(GB_gameboy_t *gb)
y++; y++;
x = 0; x = 0;
if (y == 18) { if (y == 18) {
y = 0; break;
} }
} }
} }
@ -556,8 +554,8 @@ static void render_boot_animation (GB_gameboy_t *gb)
else if (gb->sgb->intro_animation < 80) { else if (gb->sgb->intro_animation < 80) {
fade_blue = 80 - gb->sgb->intro_animation; fade_blue = 80 - gb->sgb->intro_animation;
} }
else if (gb->sgb->intro_animation > INTRO_ANIMATION_LENGTH - 32) { else if (gb->sgb->intro_animation > GB_SGB_INTRO_ANIMATION_LENGTH - 32) {
fade_red = fade_blue = gb->sgb->intro_animation - INTRO_ANIMATION_LENGTH + 32; fade_red = fade_blue = gb->sgb->intro_animation - GB_SGB_INTRO_ANIMATION_LENGTH + 32;
} }
uint32_t colors[] = { uint32_t colors[] = {
convert_rgb15(gb, 0), convert_rgb15(gb, 0),
@ -607,74 +605,75 @@ void GB_sgb_render(GB_gameboy_t *gb)
render_jingle(gb, gb->apu_output.sample_rate / GB_get_usual_frame_rate(gb)); render_jingle(gb, gb->apu_output.sample_rate / GB_get_usual_frame_rate(gb));
} }
if (gb->sgb->intro_animation < INTRO_ANIMATION_LENGTH) gb->sgb->intro_animation++; if (gb->sgb->intro_animation < GB_SGB_INTRO_ANIMATION_LENGTH) gb->sgb->intro_animation++;
if (gb->sgb->vram_transfer_countdown) { if (gb->sgb->vram_transfer_countdown) {
if (--gb->sgb->vram_transfer_countdown == 0) { if (--gb->sgb->vram_transfer_countdown == 0) {
if (gb->sgb->transfer_dest == TRANSFER_LOW_TILES || gb->sgb->transfer_dest == TRANSFER_HIGH_TILES) { unsigned size = 0;
uint8_t *base = &gb->sgb->pending_border.tiles[gb->sgb->transfer_dest == TRANSFER_HIGH_TILES ? 0x80 * 8 * 8 : 0]; uint16_t *data = NULL;
for (unsigned tile = 0; tile < 0x80; tile++) {
unsigned tile_x = (tile % 10) * 16; switch (gb->sgb->transfer_dest) {
unsigned tile_y = (tile / 10) * 8; case TRANSFER_LOW_TILES:
for (unsigned y = 0; y < 0x8; y++) { size = 0x100;
for (unsigned x = 0; x < 0x8; x++) { data = (uint16_t *)gb->sgb->pending_border.tiles;
base[tile * 8 * 8 + y * 8 + x] = gb->sgb->screen_buffer[(tile_x + x) + (tile_y + y) * 160] + break;
gb->sgb->screen_buffer[(tile_x + x + 8) + (tile_y + y) * 160] * 4; case TRANSFER_HIGH_TILES:
} size = 0x100;
} data = (uint16_t *)gb->sgb->pending_border.tiles + 0x800;
} break;
case TRANSFER_PALETTES:
size = 0x100;
data = gb->sgb->ram_palettes;
break;
case TRANSFER_BORDER_DATA:
size = 0x88;
data = gb->sgb->pending_border.raw_data;
break;
case TRANSFER_ATTRIBUTES:
size = 0xFE;
data = (uint16_t *)gb->sgb->attribute_files;
break;
default:
return; // Corrupt state?
} }
else {
unsigned size = 0; for (unsigned tile = 0; tile < size; tile++) {
uint16_t *data = NULL; unsigned tile_x = (tile % 20) * 8;
unsigned tile_y = (tile / 20) * 8;
switch (gb->sgb->transfer_dest) { for (unsigned y = 0; y < 0x8; y++) {
case TRANSFER_PALETTES: static const uint16_t pixel_to_bits[4] = {0x0000, 0x0080, 0x8000, 0x8080};
size = 0x100; *data = 0;
data = gb->sgb->ram_palettes; for (unsigned x = 0; x < 8; x++) {
break; *data |= pixel_to_bits[gb->sgb->screen_buffer[(tile_x + x) + (tile_y + y) * 160] & 3] >> x;
case TRANSFER_BORDER_DATA:
size = 0x88;
data = gb->sgb->pending_border.raw_data;
break;
case TRANSFER_ATTRIBUTES:
size = 0xFE;
data = (uint16_t *)gb->sgb->attribute_files;
break;
default:
return; // Corrupt state?
}
for (unsigned tile = 0; tile < size; tile++) {
unsigned tile_x = (tile % 20) * 8;
unsigned tile_y = (tile / 20) * 8;
for (unsigned y = 0; y < 0x8; y++) {
static const uint16_t pixel_to_bits[4] = {0x0000, 0x0080, 0x8000, 0x8080};
*data = 0;
for (unsigned x = 0; x < 8; x++) {
*data |= pixel_to_bits[gb->sgb->screen_buffer[(tile_x + x) + (tile_y + y) * 160] & 3] >> x;
}
#ifdef GB_BIG_ENDIAN
if (gb->sgb->transfer_dest == TRANSFER_ATTRIBUTES) {
*data = __builtin_bswap16(*data);
}
#endif
data++;
} }
#ifdef GB_BIG_ENDIAN
*data = __builtin_bswap16(*data);
#endif
data++;
} }
if (gb->sgb->transfer_dest == TRANSFER_BORDER_DATA) { }
gb->sgb->border_animation = 64; if (gb->sgb->transfer_dest == TRANSFER_BORDER_DATA) {
} gb->sgb->border_animation = 64;
} }
} }
} }
if (!gb->screen || !gb->rgb_encode_callback || gb->disable_rendering) return; if (!gb->screen || !gb->rgb_encode_callback || gb->disable_rendering) {
if (gb->sgb->border_animation > 32) {
gb->sgb->border_animation--;
}
else if (gb->sgb->border_animation != 0) {
gb->sgb->border_animation--;
}
if (gb->sgb->border_animation == 32) {
memcpy(&gb->sgb->border, &gb->sgb->pending_border, sizeof(gb->sgb->border));
}
return;
}
uint32_t colors[4 * 4]; uint32_t colors[4 * 4];
for (unsigned i = 0; i < 4 * 4; i++) { for (unsigned i = 0; i < 4 * 4; i++) {
colors[i] = convert_rgb15(gb, gb->sgb->effective_palettes[i]); colors[i] = convert_rgb15(gb, LE16(gb->sgb->effective_palettes[i]));
} }
if (gb->sgb->mask_mode != MASK_FREEZE) { if (gb->sgb->mask_mode != MASK_FREEZE) {
@ -683,7 +682,7 @@ void GB_sgb_render(GB_gameboy_t *gb)
sizeof(gb->sgb->effective_screen_buffer)); sizeof(gb->sgb->effective_screen_buffer));
} }
if (gb->sgb->intro_animation < INTRO_ANIMATION_LENGTH) { if (gb->sgb->intro_animation < GB_SGB_INTRO_ANIMATION_LENGTH) {
render_boot_animation(gb); render_boot_animation(gb);
} }
else { else {
@ -735,21 +734,21 @@ void GB_sgb_render(GB_gameboy_t *gb)
} }
uint32_t border_colors[16 * 4]; uint32_t border_colors[16 * 4];
if (gb->sgb->border_animation == 0 || gb->sgb->intro_animation < INTRO_ANIMATION_LENGTH) { if (gb->sgb->border_animation == 0 || gb->sgb->intro_animation < GB_SGB_INTRO_ANIMATION_LENGTH) {
for (unsigned i = 0; i < 16 * 4; i++) { for (unsigned i = 0; i < 16 * 4; i++) {
border_colors[i] = convert_rgb15(gb, gb->sgb->border.palette[i]); border_colors[i] = convert_rgb15(gb, LE16(gb->sgb->border.palette[i]));
} }
} }
else if (gb->sgb->border_animation > 32) { else if (gb->sgb->border_animation > 32) {
gb->sgb->border_animation--; gb->sgb->border_animation--;
for (unsigned i = 0; i < 16 * 4; i++) { for (unsigned i = 0; i < 16 * 4; i++) {
border_colors[i] = convert_rgb15_with_fade(gb, gb->sgb->border.palette[i], 64 - gb->sgb->border_animation); border_colors[i] = convert_rgb15_with_fade(gb, LE16(gb->sgb->border.palette[i]), 64 - gb->sgb->border_animation);
} }
} }
else { else {
gb->sgb->border_animation--; gb->sgb->border_animation--;
for (unsigned i = 0; i < 16 * 4; i++) { for (unsigned i = 0; i < 16 * 4; i++) {
border_colors[i] = convert_rgb15_with_fade(gb, gb->sgb->border.palette[i], gb->sgb->border_animation); border_colors[i] = convert_rgb15_with_fade(gb, LE16(gb->sgb->border.palette[i]), gb->sgb->border_animation);
} }
} }
@ -767,13 +766,19 @@ void GB_sgb_render(GB_gameboy_t *gb)
else if (gb->border_mode == GB_BORDER_NEVER) { else if (gb->border_mode == GB_BORDER_NEVER) {
continue; continue;
} }
uint16_t tile = gb->sgb->border.map[tile_x + tile_y * 32]; uint16_t tile = LE16(gb->sgb->border.map[tile_x + tile_y * 32]);
uint8_t flip_x = (tile & 0x4000)? 0x7 : 0; uint8_t flip_x = (tile & 0x4000)? 0:7;
uint8_t flip_y = (tile & 0x8000)? 0x7 : 0; uint8_t flip_y = (tile & 0x8000)? 7:0;
uint8_t palette = (tile >> 10) & 3; uint8_t palette = (tile >> 10) & 3;
for (unsigned y = 0; y < 8; y++) { for (unsigned y = 0; y < 8; y++) {
unsigned base = (tile & 0xFF) * 32 + (y ^ flip_y) * 2;
for (unsigned x = 0; x < 8; x++) { for (unsigned x = 0; x < 8; x++) {
uint8_t color = gb->sgb->border.tiles[(tile & 0xFF) * 64 + (x ^ flip_x) + (y ^ flip_y) * 8] & 0xF; uint8_t bit = 1 << (x ^ flip_x);
uint8_t color = ((gb->sgb->border.tiles[base] & bit) ? 1: 0) |
((gb->sgb->border.tiles[base + 1] & bit) ? 2: 0) |
((gb->sgb->border.tiles[base + 16] & bit) ? 4: 0) |
((gb->sgb->border.tiles[base + 17] & bit) ? 8: 0);
uint32_t *output = gb->screen; uint32_t *output = gb->screen;
if (gb->border_mode == GB_BORDER_NEVER) { if (gb->border_mode == GB_BORDER_NEVER) {
output += (tile_x - 6) * 8 + x + ((tile_y - 5) * 8 + y) * 160; output += (tile_x - 6) * 8 + x + ((tile_y - 5) * 8 + y) * 160;
@ -798,22 +803,19 @@ void GB_sgb_load_default_data(GB_gameboy_t *gb)
{ {
#include "graphics/sgb_border.inc" #include "graphics/sgb_border.inc"
#ifdef GB_BIG_ENDIAN
for (unsigned i = 0; i < sizeof(tilemap) / 2; i++) {
gb->sgb->border.map[i] = LE16(tilemap[i]);
}
for (unsigned i = 0; i < sizeof(palette) / 2; i++) {
gb->sgb->border.palette[i] = LE16(palette[i]);
}
#else
memcpy(gb->sgb->border.map, tilemap, sizeof(tilemap)); memcpy(gb->sgb->border.map, tilemap, sizeof(tilemap));
memcpy(gb->sgb->border.palette, palette, sizeof(palette)); memcpy(gb->sgb->border.palette, palette, sizeof(palette));
#endif
/* Expand tileset */ memcpy(gb->sgb->border.tiles, tiles, sizeof(tiles));
for (unsigned tile = 0; tile < sizeof(tiles) / 32; tile++) {
for (unsigned y = 0; y < 8; y++) {
for (unsigned x = 0; x < 8; x++) {
gb->sgb->border.tiles[tile * 8 * 8 + y * 8 + x] =
(tiles[tile * 32 + y * 2 + 0] & (1 << (7 ^ x)) ? 1 : 0) |
(tiles[tile * 32 + y * 2 + 1] & (1 << (7 ^ x)) ? 2 : 0) |
(tiles[tile * 32 + y * 2 + 16] & (1 << (7 ^ x)) ? 4 : 0) |
(tiles[tile * 32 + y * 2 + 17] & (1 << (7 ^ x)) ? 8 : 0);
}
}
}
if (gb->model != GB_MODEL_SGB2) { if (gb->model != GB_MODEL_SGB2) {
/* Delete the "2" */ /* Delete the "2" */
@ -825,10 +827,10 @@ void GB_sgb_load_default_data(GB_gameboy_t *gb)
/* Re-center */ /* Re-center */
memmove(&gb->sgb->border.map[25 * 32 + 1], &gb->sgb->border.map[25 * 32], (32 * 3 - 1) * sizeof(gb->sgb->border.map[0])); memmove(&gb->sgb->border.map[25 * 32 + 1], &gb->sgb->border.map[25 * 32], (32 * 3 - 1) * sizeof(gb->sgb->border.map[0]));
} }
gb->sgb->effective_palettes[0] = built_in_palettes[0]; gb->sgb->effective_palettes[0] = LE16(built_in_palettes[0]);
gb->sgb->effective_palettes[1] = built_in_palettes[1]; gb->sgb->effective_palettes[1] = LE16(built_in_palettes[1]);
gb->sgb->effective_palettes[2] = built_in_palettes[2]; gb->sgb->effective_palettes[2] = LE16(built_in_palettes[2]);
gb->sgb->effective_palettes[3] = built_in_palettes[3]; gb->sgb->effective_palettes[3] = LE16(built_in_palettes[3]);
} }
static double fm_synth(double phase) static double fm_synth(double phase)
@ -874,7 +876,7 @@ static void render_jingle(GB_gameboy_t *gb, size_t count)
return; return;
} }
if (gb->sgb->intro_animation >= INTRO_ANIMATION_LENGTH) return; if (gb->sgb->intro_animation >= GB_SGB_INTRO_ANIMATION_LENGTH) return;
signed jingle_stage = (gb->sgb->intro_animation - 64) / 3; signed jingle_stage = (gb->sgb->intro_animation - 64) / 3;
double sweep_cutoff_ratio = 2000.0 * pow(2, gb->sgb->intro_animation / 20.0) / gb->apu_output.sample_rate; double sweep_cutoff_ratio = 2000.0 * pow(2, gb->sgb->intro_animation / 20.0) / gb->apu_output.sample_rate;
@ -892,7 +894,7 @@ static void render_jingle(GB_gameboy_t *gb, size_t count)
gb->sgb_intro_jingle_phases[f] += frequencies[f] / gb->apu_output.sample_rate; gb->sgb_intro_jingle_phases[f] += frequencies[f] / gb->apu_output.sample_rate;
} }
if (gb->sgb->intro_animation > 100) { if (gb->sgb->intro_animation > 100) {
sample *= pow((INTRO_ANIMATION_LENGTH - gb->sgb->intro_animation) / (INTRO_ANIMATION_LENGTH - 100.0), 3); sample *= pow((GB_SGB_INTRO_ANIMATION_LENGTH - gb->sgb->intro_animation) / (GB_SGB_INTRO_ANIMATION_LENGTH - 100.0), 3);
} }
if (gb->sgb->intro_animation < 120) { if (gb->sgb->intro_animation < 120) {

View File

@ -6,7 +6,10 @@
typedef struct GB_sgb_s GB_sgb_t; typedef struct GB_sgb_s GB_sgb_t;
typedef struct { typedef struct {
uint8_t tiles[0x100 * 8 * 8]; /* High nibble not used*/ union {
uint8_t tiles[0x100 * 8 * 4];
uint8_t tiles_legacy[0x100 * 8 * 8]; /* High nibble not used; TODO: Remove when breaking save-state compatibility! */
};
union { union {
struct { struct {
uint16_t map[32 * 32]; uint16_t map[32 * 32];
@ -17,6 +20,8 @@ typedef struct {
} GB_sgb_border_t; } GB_sgb_border_t;
#ifdef GB_INTERNAL #ifdef GB_INTERNAL
#define GB_SGB_INTRO_ANIMATION_LENGTH 200
struct GB_sgb_s { struct GB_sgb_s {
uint8_t command[16 * 7]; uint8_t command[16 * 7];
uint16_t command_write_index; uint16_t command_write_index;
@ -46,7 +51,8 @@ struct GB_sgb_s {
uint16_t effective_palettes[4 * 4]; uint16_t effective_palettes[4 * 4];
uint16_t ram_palettes[4 * 512]; uint16_t ram_palettes[4 * 512];
uint8_t attribute_map[20 * 18]; uint8_t attribute_map[20 * 18];
uint8_t attribute_files[0xFE0]; uint8_t attribute_files[0xFD2];
uint8_t attribute_files_padding[0xFE0 - 0xFD2];
/* Intro */ /* Intro */
int16_t intro_animation; int16_t intro_animation;
@ -56,6 +62,8 @@ struct GB_sgb_s {
/* Multiplayer (cont) */ /* Multiplayer (cont) */
bool mlt_lock; bool mlt_lock;
bool v14_3; // True on save states created on 0.14.3 or newer; Remove when breaking save state compatibility!
}; };
void GB_sgb_write(GB_gameboy_t *gb, uint8_t value); void GB_sgb_write(GB_gameboy_t *gb, uint8_t value);

View File

@ -21,16 +21,20 @@ typedef enum {
GB_CONFLICT_DMG_LCDC, GB_CONFLICT_DMG_LCDC,
GB_CONFLICT_SGB_LCDC, GB_CONFLICT_SGB_LCDC,
GB_CONFLICT_WX, GB_CONFLICT_WX,
GB_CONFLICT_CGB_LCDC,
GB_CONFLICT_NR10,
} GB_conflict_t; } GB_conflict_t;
/* Todo: How does double speed mode affect these? */ /* Todo: How does double speed mode affect these? */
static const GB_conflict_t cgb_conflict_map[0x80] = { static const GB_conflict_t cgb_conflict_map[0x80] = {
[GB_IO_LCDC] = GB_CONFLICT_CGB_LCDC,
[GB_IO_IF] = GB_CONFLICT_WRITE_CPU, [GB_IO_IF] = GB_CONFLICT_WRITE_CPU,
[GB_IO_LYC] = GB_CONFLICT_WRITE_CPU, [GB_IO_LYC] = GB_CONFLICT_WRITE_CPU,
[GB_IO_STAT] = GB_CONFLICT_STAT_CGB, [GB_IO_STAT] = GB_CONFLICT_STAT_CGB,
[GB_IO_BGP] = GB_CONFLICT_PALETTE_CGB, [GB_IO_BGP] = GB_CONFLICT_PALETTE_CGB,
[GB_IO_OBP0] = GB_CONFLICT_PALETTE_CGB, [GB_IO_OBP0] = GB_CONFLICT_PALETTE_CGB,
[GB_IO_OBP1] = GB_CONFLICT_PALETTE_CGB, [GB_IO_OBP1] = GB_CONFLICT_PALETTE_CGB,
[GB_IO_NR10] = GB_CONFLICT_NR10,
/* Todo: most values not verified, and probably differ between revisions */ /* Todo: most values not verified, and probably differ between revisions */
}; };
@ -48,7 +52,8 @@ static const GB_conflict_t dmg_conflict_map[0x80] = {
[GB_IO_OBP1] = GB_CONFLICT_PALETTE_DMG, [GB_IO_OBP1] = GB_CONFLICT_PALETTE_DMG,
[GB_IO_WY] = GB_CONFLICT_READ_OLD, [GB_IO_WY] = GB_CONFLICT_READ_OLD,
[GB_IO_WX] = GB_CONFLICT_WX, [GB_IO_WX] = GB_CONFLICT_WX,
[GB_IO_NR10] = GB_CONFLICT_NR10,
/* Todo: these were not verified at all */ /* Todo: these were not verified at all */
[GB_IO_SCX] = GB_CONFLICT_READ_NEW, [GB_IO_SCX] = GB_CONFLICT_READ_NEW,
}; };
@ -66,7 +71,8 @@ static const GB_conflict_t sgb_conflict_map[0x80] = {
[GB_IO_OBP1] = GB_CONFLICT_READ_NEW, [GB_IO_OBP1] = GB_CONFLICT_READ_NEW,
[GB_IO_WY] = GB_CONFLICT_READ_OLD, [GB_IO_WY] = GB_CONFLICT_READ_OLD,
[GB_IO_WX] = GB_CONFLICT_WX, [GB_IO_WX] = GB_CONFLICT_WX,
[GB_IO_NR10] = GB_CONFLICT_NR10,
/* Todo: these were not verified at all */ /* Todo: these were not verified at all */
[GB_IO_SCX] = GB_CONFLICT_READ_NEW, [GB_IO_SCX] = GB_CONFLICT_READ_NEW,
}; };
@ -81,17 +87,6 @@ static uint8_t cycle_read(GB_gameboy_t *gb, uint16_t addr)
return ret; return ret;
} }
static uint8_t cycle_read_inc_oam_bug(GB_gameboy_t *gb, uint16_t addr)
{
if (gb->pending_cycles) {
GB_advance_cycles(gb, gb->pending_cycles);
}
GB_trigger_oam_bug_read_increase(gb, addr); /* Todo: test T-cycle timing */
uint8_t ret = GB_read_memory(gb, addr);
gb->pending_cycles = 4;
return ret;
}
/* A special case for IF during ISR, returns the old value of IF. */ /* A special case for IF during ISR, returns the old value of IF. */
/* TODO: Verify the timing, it might be wrong in cases where, in the same M cycle, IF /* TODO: Verify the timing, it might be wrong in cases where, in the same M cycle, IF
is both read be the CPU, modified by the ISR, and modified by an actual interrupt. is both read be the CPU, modified by the ISR, and modified by an actual interrupt.
@ -241,6 +236,56 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
gb->wx_just_changed = false; gb->wx_just_changed = false;
gb->pending_cycles = 3; gb->pending_cycles = 3;
return; return;
case GB_CONFLICT_CGB_LCDC:
if ((value ^ gb->io_registers[GB_IO_LCDC]) & 0x10) {
// Todo: This is difference is because my timing is off in one of the models
if (gb->model > GB_MODEL_CGB_C) {
GB_advance_cycles(gb, gb->pending_cycles);
GB_write_memory(gb, addr, value ^ 0x10); // Write with the old TILE_SET first
gb->tile_sel_glitch = true;
GB_advance_cycles(gb, 1);
gb->tile_sel_glitch = false;
GB_write_memory(gb, addr, value);
gb->pending_cycles = 3;
}
else {
GB_advance_cycles(gb, gb->pending_cycles - 1);
GB_write_memory(gb, addr, value ^ 0x10); // Write with the old TILE_SET first
gb->tile_sel_glitch = true;
GB_advance_cycles(gb, 1);
gb->tile_sel_glitch = false;
GB_write_memory(gb, addr, value);
gb->pending_cycles = 4;
}
}
else {
GB_advance_cycles(gb, gb->pending_cycles);
GB_write_memory(gb, addr, value);
gb->pending_cycles = 4;
}
return;
case GB_CONFLICT_NR10:
/* Hack: Due to the coupling between DIV and the APU, GB_apu_run only runs at M-cycle
resolutions, but this quirk requires 2MHz even in single speed mode. To work
around this, we specifically just step the calculate countdown if needed. */
GB_advance_cycles(gb, gb->pending_cycles);
if (gb->model <= GB_MODEL_CGB_C) {
// TODO: Double speed mode? This logic is also a bit weird, it needs more tests
if (gb->apu.square_sweep_calculate_countdown > 3 && gb->apu.enable_zombie_calculate_stepping) {
gb->apu.square_sweep_calculate_countdown -= 2;
}
gb->apu.enable_zombie_calculate_stepping = true;
/* TODO: this causes audio regressions in the Donkey Kong Land series.
The exact behavior of this quirk should be further investigated, as it seems
more complicated than a single FF pseudo-write. */
// GB_write_memory(gb, addr, 0xFF);
}
GB_write_memory(gb, addr, value);
gb->pending_cycles = 4;
return;
} }
} }
@ -261,7 +306,20 @@ static void cycle_oam_bug(GB_gameboy_t *gb, uint8_t register_id)
} }
GB_trigger_oam_bug(gb, gb->registers[register_id]); /* Todo: test T-cycle timing */ GB_trigger_oam_bug(gb, gb->registers[register_id]); /* Todo: test T-cycle timing */
gb->pending_cycles = 4; gb->pending_cycles = 4;
}
static void cycle_oam_bug_pc(GB_gameboy_t *gb)
{
if (GB_is_cgb(gb)) {
/* Slight optimization */
gb->pending_cycles += 4;
return;
}
if (gb->pending_cycles) {
GB_advance_cycles(gb, gb->pending_cycles);
}
GB_trigger_oam_bug(gb, gb->pc); /* Todo: test T-cycle timing */
gb->pending_cycles = 4;
} }
static void flush_pending_cycles(GB_gameboy_t *gb) static void flush_pending_cycles(GB_gameboy_t *gb)
@ -287,6 +345,10 @@ static void nop(GB_gameboy_t *gb, uint8_t opcode)
static void enter_stop_mode(GB_gameboy_t *gb) static void enter_stop_mode(GB_gameboy_t *gb)
{ {
GB_write_memory(gb, 0xFF00 + GB_IO_DIV, 0);
if (!gb->ime) { // TODO: I don't trust this if,
gb->div_cycles = -4; // Emulate the CPU-side DIV-reset signal being held
}
gb->stopped = true; gb->stopped = true;
gb->oam_ppu_blocked = !gb->oam_read_blocked; gb->oam_ppu_blocked = !gb->oam_read_blocked;
gb->vram_ppu_blocked = !gb->vram_read_blocked; gb->vram_ppu_blocked = !gb->vram_read_blocked;
@ -295,55 +357,71 @@ static void enter_stop_mode(GB_gameboy_t *gb)
static void leave_stop_mode(GB_gameboy_t *gb) static void leave_stop_mode(GB_gameboy_t *gb)
{ {
/* The CPU takes more time to wake up then the other components */
for (unsigned i = 0x200; i--;) {
GB_advance_cycles(gb, 0x10);
}
gb->stopped = false; gb->stopped = false;
gb->oam_ppu_blocked = false; gb->oam_ppu_blocked = false;
gb->vram_ppu_blocked = false; gb->vram_ppu_blocked = false;
gb->cgb_palettes_ppu_blocked = false; gb->cgb_palettes_ppu_blocked = false;
} }
/* TODO: Speed switch timing needs far more tests. Double to single is wrong to avoid odd mode. */
static void stop(GB_gameboy_t *gb, uint8_t opcode) static void stop(GB_gameboy_t *gb, uint8_t opcode)
{ {
if (gb->io_registers[GB_IO_KEY1] & 0x1) { flush_pending_cycles(gb);
flush_pending_cycles(gb); bool exit_by_joyp = ((gb->io_registers[GB_IO_JOYP] & 0xF) != 0xF);
bool needs_alignment = false; bool speed_switch = (gb->io_registers[GB_IO_KEY1] & 0x1) && !exit_by_joyp;
bool immediate_exit = speed_switch || exit_by_joyp;
GB_advance_cycles(gb, 0x4); bool interrupt_pending = (gb->interrupt_enable & gb->io_registers[GB_IO_IF] & 0x1F);
/* Make sure we keep the CPU ticks aligned correctly when returning from double speed mode */ // When entering with IF&IE, the 2nd byte of STOP is actually executed
if (gb->double_speed_alignment & 7) { if (!exit_by_joyp) {
GB_advance_cycles(gb, 0x4);
needs_alignment = true;
}
gb->cgb_double_speed ^= true;
gb->io_registers[GB_IO_KEY1] = 0;
enter_stop_mode(gb); enter_stop_mode(gb);
leave_stop_mode(gb);
if (!needs_alignment) {
GB_advance_cycles(gb, 0x4);
}
}
else {
GB_timing_sync(gb);
if ((gb->io_registers[GB_IO_JOYP] & 0xF) != 0xF) {
/* HW Bug? When STOP is executed while a button is down, the CPU halts forever
yet the other hardware keeps running. */
gb->interrupt_enable = 0;
gb->halted = true;
}
else {
enter_stop_mode(gb);
}
} }
/* Todo: is PC being actually read? */ if (!interrupt_pending) {
gb->pc++; cycle_read(gb, gb->pc++);
}
/* Todo: speed switching takes 2 extra T-cycles (so 2 PPU ticks in single->double and 1 PPU tick in double->single) */
if (speed_switch) {
flush_pending_cycles(gb);
if (gb->io_registers[GB_IO_LCDC] & 0x80 && gb->cgb_double_speed) {
GB_log(gb, "ROM triggered a PPU odd mode, which is currently not supported. Reverting to even-mode.\n");
if (gb->double_speed_alignment & 7) {
gb->speed_switch_freeze = 2;
}
}
if (gb->apu.global_enable && gb->cgb_double_speed) {
GB_log(gb, "ROM triggered an APU odd mode, which is currently not tested.\n");
}
if (gb->cgb_double_speed) {
gb->cgb_double_speed = false;
}
else {
gb->speed_switch_countdown = 6;
gb->speed_switch_freeze = 1;
}
if (interrupt_pending) {
}
else {
gb->speed_switch_halt_countdown = 0x20008;
gb->speed_switch_freeze = 5;
}
gb->io_registers[GB_IO_KEY1] = 0;
}
if (immediate_exit) {
leave_stop_mode(gb);
if (!interrupt_pending) {
gb->halted = true;
gb->just_halted = true;
}
else {
gb->speed_switch_halt_countdown = 0;
}
}
} }
/* Operand naming conventions for functions: /* Operand naming conventions for functions:
@ -362,8 +440,8 @@ static void ld_rr_d16(GB_gameboy_t *gb, uint8_t opcode)
uint8_t register_id; uint8_t register_id;
uint16_t value; uint16_t value;
register_id = (opcode >> 4) + 1; register_id = (opcode >> 4) + 1;
value = cycle_read_inc_oam_bug(gb, gb->pc++); value = cycle_read(gb, gb->pc++);
value |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8; value |= cycle_read(gb, gb->pc++) << 8;
gb->registers[register_id] = value; gb->registers[register_id] = value;
} }
@ -418,7 +496,7 @@ static void ld_hr_d8(GB_gameboy_t *gb, uint8_t opcode)
uint8_t register_id; uint8_t register_id;
register_id = ((opcode >> 4) + 1) & 0x03; register_id = ((opcode >> 4) + 1) & 0x03;
gb->registers[register_id] &= 0xFF; gb->registers[register_id] &= 0xFF;
gb->registers[register_id] |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8; gb->registers[register_id] |= cycle_read(gb, gb->pc++) << 8;
} }
static void rlca(GB_gameboy_t *gb, uint8_t opcode) static void rlca(GB_gameboy_t *gb, uint8_t opcode)
@ -449,8 +527,8 @@ static void ld_da16_sp(GB_gameboy_t *gb, uint8_t opcode)
{ {
/* Todo: Verify order is correct */ /* Todo: Verify order is correct */
uint16_t addr; uint16_t addr;
addr = cycle_read_inc_oam_bug(gb, gb->pc++); addr = cycle_read(gb, gb->pc++);
addr |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8; addr |= cycle_read(gb, gb->pc++) << 8;
cycle_write(gb, addr, gb->registers[GB_REGISTER_SP] & 0xFF); cycle_write(gb, addr, gb->registers[GB_REGISTER_SP] & 0xFF);
cycle_write(gb, addr + 1, gb->registers[GB_REGISTER_SP] >> 8); cycle_write(gb, addr + 1, gb->registers[GB_REGISTER_SP] >> 8);
} }
@ -536,7 +614,7 @@ static void ld_lr_d8(GB_gameboy_t *gb, uint8_t opcode)
uint8_t register_id; uint8_t register_id;
register_id = (opcode >> 4) + 1; register_id = (opcode >> 4) + 1;
gb->registers[register_id] &= 0xFF00; gb->registers[register_id] &= 0xFF00;
gb->registers[register_id] |= cycle_read_inc_oam_bug(gb, gb->pc++); gb->registers[register_id] |= cycle_read(gb, gb->pc++);
} }
static void rrca(GB_gameboy_t *gb, uint8_t opcode) static void rrca(GB_gameboy_t *gb, uint8_t opcode)
@ -566,7 +644,7 @@ static void rra(GB_gameboy_t *gb, uint8_t opcode)
static void jr_r8(GB_gameboy_t *gb, uint8_t opcode) static void jr_r8(GB_gameboy_t *gb, uint8_t opcode)
{ {
/* Todo: Verify timing */ /* Todo: Verify timing */
gb->pc += (int8_t)cycle_read_inc_oam_bug(gb, gb->pc) + 1; gb->pc += (int8_t)cycle_read(gb, gb->pc) + 1;
cycle_no_access(gb); cycle_no_access(gb);
} }
@ -588,7 +666,7 @@ static bool condition_code(GB_gameboy_t *gb, uint8_t opcode)
static void jr_cc_r8(GB_gameboy_t *gb, uint8_t opcode) static void jr_cc_r8(GB_gameboy_t *gb, uint8_t opcode)
{ {
int8_t offset = cycle_read_inc_oam_bug(gb, gb->pc++); int8_t offset = cycle_read(gb, gb->pc++);
if (condition_code(gb, opcode)) { if (condition_code(gb, opcode)) {
gb->pc += offset; gb->pc += offset;
cycle_no_access(gb); cycle_no_access(gb);
@ -663,13 +741,13 @@ static void ld_dhld_a(GB_gameboy_t *gb, uint8_t opcode)
static void ld_a_dhli(GB_gameboy_t *gb, uint8_t opcode) static void ld_a_dhli(GB_gameboy_t *gb, uint8_t opcode)
{ {
gb->registers[GB_REGISTER_AF] &= 0xFF; gb->registers[GB_REGISTER_AF] &= 0xFF;
gb->registers[GB_REGISTER_AF] |= cycle_read_inc_oam_bug(gb, gb->registers[GB_REGISTER_HL]++) << 8; gb->registers[GB_REGISTER_AF] |= cycle_read(gb, gb->registers[GB_REGISTER_HL]++) << 8;
} }
static void ld_a_dhld(GB_gameboy_t *gb, uint8_t opcode) static void ld_a_dhld(GB_gameboy_t *gb, uint8_t opcode)
{ {
gb->registers[GB_REGISTER_AF] &= 0xFF; gb->registers[GB_REGISTER_AF] &= 0xFF;
gb->registers[GB_REGISTER_AF] |= cycle_read_inc_oam_bug(gb, gb->registers[GB_REGISTER_HL]--) << 8; gb->registers[GB_REGISTER_AF] |= cycle_read(gb, gb->registers[GB_REGISTER_HL]--) << 8;
} }
static void inc_dhl(GB_gameboy_t *gb, uint8_t opcode) static void inc_dhl(GB_gameboy_t *gb, uint8_t opcode)
@ -707,7 +785,7 @@ static void dec_dhl(GB_gameboy_t *gb, uint8_t opcode)
static void ld_dhl_d8(GB_gameboy_t *gb, uint8_t opcode) static void ld_dhl_d8(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint8_t data = cycle_read_inc_oam_bug(gb, gb->pc++); uint8_t data = cycle_read(gb, gb->pc++);
cycle_write(gb, gb->registers[GB_REGISTER_HL], data); cycle_write(gb, gb->registers[GB_REGISTER_HL], data);
} }
@ -945,15 +1023,15 @@ static void pop_rr(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint8_t register_id; uint8_t register_id;
register_id = ((opcode >> 4) + 1) & 3; register_id = ((opcode >> 4) + 1) & 3;
gb->registers[register_id] = cycle_read_inc_oam_bug(gb, gb->registers[GB_REGISTER_SP]++); gb->registers[register_id] = cycle_read(gb, gb->registers[GB_REGISTER_SP]++);
gb->registers[register_id] |= cycle_read(gb, gb->registers[GB_REGISTER_SP]++) << 8; gb->registers[register_id] |= cycle_read(gb, gb->registers[GB_REGISTER_SP]++) << 8;
gb->registers[GB_REGISTER_AF] &= 0xFFF0; // Make sure we don't set impossible flags on F! See Blargg's PUSH AF test. gb->registers[GB_REGISTER_AF] &= 0xFFF0; // Make sure we don't set impossible flags on F! See Blargg's PUSH AF test.
} }
static void jp_cc_a16(GB_gameboy_t *gb, uint8_t opcode) static void jp_cc_a16(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint16_t addr = cycle_read_inc_oam_bug(gb, gb->pc++); uint16_t addr = cycle_read(gb, gb->pc++);
addr |= (cycle_read_inc_oam_bug(gb, gb->pc++) << 8); addr |= (cycle_read(gb, gb->pc++) << 8);
if (condition_code(gb, opcode)) { if (condition_code(gb, opcode)) {
cycle_no_access(gb); cycle_no_access(gb);
gb->pc = addr; gb->pc = addr;
@ -962,8 +1040,8 @@ static void jp_cc_a16(GB_gameboy_t *gb, uint8_t opcode)
static void jp_a16(GB_gameboy_t *gb, uint8_t opcode) static void jp_a16(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint16_t addr = cycle_read_inc_oam_bug(gb, gb->pc); uint16_t addr = cycle_read(gb, gb->pc);
addr |= (cycle_read_inc_oam_bug(gb, gb->pc + 1) << 8); addr |= (cycle_read(gb, gb->pc + 1) << 8);
cycle_no_access(gb); cycle_no_access(gb);
gb->pc = addr; gb->pc = addr;
@ -972,8 +1050,8 @@ static void jp_a16(GB_gameboy_t *gb, uint8_t opcode)
static void call_cc_a16(GB_gameboy_t *gb, uint8_t opcode) static void call_cc_a16(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint16_t call_addr = gb->pc - 1; uint16_t call_addr = gb->pc - 1;
uint16_t addr = cycle_read_inc_oam_bug(gb, gb->pc++); uint16_t addr = cycle_read(gb, gb->pc++);
addr |= (cycle_read_inc_oam_bug(gb, gb->pc++) << 8); addr |= (cycle_read(gb, gb->pc++) << 8);
if (condition_code(gb, opcode)) { if (condition_code(gb, opcode)) {
cycle_oam_bug(gb, GB_REGISTER_SP); cycle_oam_bug(gb, GB_REGISTER_SP);
cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) >> 8); cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) >> 8);
@ -996,7 +1074,7 @@ static void push_rr(GB_gameboy_t *gb, uint8_t opcode)
static void add_a_d8(GB_gameboy_t *gb, uint8_t opcode) static void add_a_d8(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint8_t value, a; uint8_t value, a;
value = cycle_read_inc_oam_bug(gb, gb->pc++); value = cycle_read(gb, gb->pc++);
a = gb->registers[GB_REGISTER_AF] >> 8; a = gb->registers[GB_REGISTER_AF] >> 8;
gb->registers[GB_REGISTER_AF] = (a + value) << 8; gb->registers[GB_REGISTER_AF] = (a + value) << 8;
if ((uint8_t) (a + value) == 0) { if ((uint8_t) (a + value) == 0) {
@ -1013,7 +1091,7 @@ static void add_a_d8(GB_gameboy_t *gb, uint8_t opcode)
static void adc_a_d8(GB_gameboy_t *gb, uint8_t opcode) static void adc_a_d8(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint8_t value, a, carry; uint8_t value, a, carry;
value = cycle_read_inc_oam_bug(gb, gb->pc++); value = cycle_read(gb, gb->pc++);
a = gb->registers[GB_REGISTER_AF] >> 8; a = gb->registers[GB_REGISTER_AF] >> 8;
carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0;
gb->registers[GB_REGISTER_AF] = (a + value + carry) << 8; gb->registers[GB_REGISTER_AF] = (a + value + carry) << 8;
@ -1032,7 +1110,7 @@ static void adc_a_d8(GB_gameboy_t *gb, uint8_t opcode)
static void sub_a_d8(GB_gameboy_t *gb, uint8_t opcode) static void sub_a_d8(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint8_t value, a; uint8_t value, a;
value = cycle_read_inc_oam_bug(gb, gb->pc++); value = cycle_read(gb, gb->pc++);
a = gb->registers[GB_REGISTER_AF] >> 8; a = gb->registers[GB_REGISTER_AF] >> 8;
gb->registers[GB_REGISTER_AF] = ((a - value) << 8) | GB_SUBTRACT_FLAG; gb->registers[GB_REGISTER_AF] = ((a - value) << 8) | GB_SUBTRACT_FLAG;
if (a == value) { if (a == value) {
@ -1049,7 +1127,7 @@ static void sub_a_d8(GB_gameboy_t *gb, uint8_t opcode)
static void sbc_a_d8(GB_gameboy_t *gb, uint8_t opcode) static void sbc_a_d8(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint8_t value, a, carry; uint8_t value, a, carry;
value = cycle_read_inc_oam_bug(gb, gb->pc++); value = cycle_read(gb, gb->pc++);
a = gb->registers[GB_REGISTER_AF] >> 8; a = gb->registers[GB_REGISTER_AF] >> 8;
carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0; carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0;
gb->registers[GB_REGISTER_AF] = ((a - value - carry) << 8) | GB_SUBTRACT_FLAG; gb->registers[GB_REGISTER_AF] = ((a - value - carry) << 8) | GB_SUBTRACT_FLAG;
@ -1068,7 +1146,7 @@ static void sbc_a_d8(GB_gameboy_t *gb, uint8_t opcode)
static void and_a_d8(GB_gameboy_t *gb, uint8_t opcode) static void and_a_d8(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint8_t value, a; uint8_t value, a;
value = cycle_read_inc_oam_bug(gb, gb->pc++); value = cycle_read(gb, gb->pc++);
a = gb->registers[GB_REGISTER_AF] >> 8; a = gb->registers[GB_REGISTER_AF] >> 8;
gb->registers[GB_REGISTER_AF] = ((a & value) << 8) | GB_HALF_CARRY_FLAG; gb->registers[GB_REGISTER_AF] = ((a & value) << 8) | GB_HALF_CARRY_FLAG;
if ((a & value) == 0) { if ((a & value) == 0) {
@ -1079,7 +1157,7 @@ static void and_a_d8(GB_gameboy_t *gb, uint8_t opcode)
static void xor_a_d8(GB_gameboy_t *gb, uint8_t opcode) static void xor_a_d8(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint8_t value, a; uint8_t value, a;
value = cycle_read_inc_oam_bug(gb, gb->pc++); value = cycle_read(gb, gb->pc++);
a = gb->registers[GB_REGISTER_AF] >> 8; a = gb->registers[GB_REGISTER_AF] >> 8;
gb->registers[GB_REGISTER_AF] = (a ^ value) << 8; gb->registers[GB_REGISTER_AF] = (a ^ value) << 8;
if ((a ^ value) == 0) { if ((a ^ value) == 0) {
@ -1090,7 +1168,7 @@ static void xor_a_d8(GB_gameboy_t *gb, uint8_t opcode)
static void or_a_d8(GB_gameboy_t *gb, uint8_t opcode) static void or_a_d8(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint8_t value, a; uint8_t value, a;
value = cycle_read_inc_oam_bug(gb, gb->pc++); value = cycle_read(gb, gb->pc++);
a = gb->registers[GB_REGISTER_AF] >> 8; a = gb->registers[GB_REGISTER_AF] >> 8;
gb->registers[GB_REGISTER_AF] = (a | value) << 8; gb->registers[GB_REGISTER_AF] = (a | value) << 8;
if ((a | value) == 0) { if ((a | value) == 0) {
@ -1101,7 +1179,7 @@ static void or_a_d8(GB_gameboy_t *gb, uint8_t opcode)
static void cp_a_d8(GB_gameboy_t *gb, uint8_t opcode) static void cp_a_d8(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint8_t value, a; uint8_t value, a;
value = cycle_read_inc_oam_bug(gb, gb->pc++); value = cycle_read(gb, gb->pc++);
a = gb->registers[GB_REGISTER_AF] >> 8; a = gb->registers[GB_REGISTER_AF] >> 8;
gb->registers[GB_REGISTER_AF] &= 0xFF00; gb->registers[GB_REGISTER_AF] &= 0xFF00;
gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG; gb->registers[GB_REGISTER_AF] |= GB_SUBTRACT_FLAG;
@ -1129,7 +1207,7 @@ static void rst(GB_gameboy_t *gb, uint8_t opcode)
static void ret(GB_gameboy_t *gb, uint8_t opcode) static void ret(GB_gameboy_t *gb, uint8_t opcode)
{ {
GB_debugger_ret_hook(gb); GB_debugger_ret_hook(gb);
gb->pc = cycle_read_inc_oam_bug(gb, gb->registers[GB_REGISTER_SP]++); gb->pc = cycle_read(gb, gb->registers[GB_REGISTER_SP]++);
gb->pc |= cycle_read(gb, gb->registers[GB_REGISTER_SP]++) << 8; gb->pc |= cycle_read(gb, gb->registers[GB_REGISTER_SP]++) << 8;
cycle_no_access(gb); cycle_no_access(gb);
} }
@ -1154,8 +1232,8 @@ static void ret_cc(GB_gameboy_t *gb, uint8_t opcode)
static void call_a16(GB_gameboy_t *gb, uint8_t opcode) static void call_a16(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint16_t call_addr = gb->pc - 1; uint16_t call_addr = gb->pc - 1;
uint16_t addr = cycle_read_inc_oam_bug(gb, gb->pc++); uint16_t addr = cycle_read(gb, gb->pc++);
addr |= (cycle_read_inc_oam_bug(gb, gb->pc++) << 8); addr |= (cycle_read(gb, gb->pc++) << 8);
cycle_oam_bug(gb, GB_REGISTER_SP); cycle_oam_bug(gb, GB_REGISTER_SP);
cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) >> 8); cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) >> 8);
cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) & 0xFF); cycle_write(gb, --gb->registers[GB_REGISTER_SP], (gb->pc) & 0xFF);
@ -1165,14 +1243,14 @@ static void call_a16(GB_gameboy_t *gb, uint8_t opcode)
static void ld_da8_a(GB_gameboy_t *gb, uint8_t opcode) static void ld_da8_a(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint8_t temp = cycle_read_inc_oam_bug(gb, gb->pc++); uint8_t temp = cycle_read(gb, gb->pc++);
cycle_write(gb, 0xFF00 + temp, gb->registers[GB_REGISTER_AF] >> 8); cycle_write(gb, 0xFF00 + temp, gb->registers[GB_REGISTER_AF] >> 8);
} }
static void ld_a_da8(GB_gameboy_t *gb, uint8_t opcode) static void ld_a_da8(GB_gameboy_t *gb, uint8_t opcode)
{ {
gb->registers[GB_REGISTER_AF] &= 0xFF; gb->registers[GB_REGISTER_AF] &= 0xFF;
uint8_t temp = cycle_read_inc_oam_bug(gb, gb->pc++); uint8_t temp = cycle_read(gb, gb->pc++);
gb->registers[GB_REGISTER_AF] |= cycle_read(gb, 0xFF00 + temp) << 8; gb->registers[GB_REGISTER_AF] |= cycle_read(gb, 0xFF00 + temp) << 8;
} }
@ -1191,7 +1269,7 @@ static void add_sp_r8(GB_gameboy_t *gb, uint8_t opcode)
{ {
int16_t offset; int16_t offset;
uint16_t sp = gb->registers[GB_REGISTER_SP]; uint16_t sp = gb->registers[GB_REGISTER_SP];
offset = (int8_t) cycle_read_inc_oam_bug(gb, gb->pc++); offset = (int8_t) cycle_read(gb, gb->pc++);
cycle_no_access(gb); cycle_no_access(gb);
cycle_no_access(gb); cycle_no_access(gb);
gb->registers[GB_REGISTER_SP] += offset; gb->registers[GB_REGISTER_SP] += offset;
@ -1216,8 +1294,8 @@ static void jp_hl(GB_gameboy_t *gb, uint8_t opcode)
static void ld_da16_a(GB_gameboy_t *gb, uint8_t opcode) static void ld_da16_a(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint16_t addr; uint16_t addr;
addr = cycle_read_inc_oam_bug(gb, gb->pc++); addr = cycle_read(gb, gb->pc++);
addr |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8; addr |= cycle_read(gb, gb->pc++) << 8;
cycle_write(gb, addr, gb->registers[GB_REGISTER_AF] >> 8); cycle_write(gb, addr, gb->registers[GB_REGISTER_AF] >> 8);
} }
@ -1225,8 +1303,8 @@ static void ld_a_da16(GB_gameboy_t *gb, uint8_t opcode)
{ {
uint16_t addr; uint16_t addr;
gb->registers[GB_REGISTER_AF] &= 0xFF; gb->registers[GB_REGISTER_AF] &= 0xFF;
addr = cycle_read_inc_oam_bug(gb, gb->pc++); addr = cycle_read(gb, gb->pc++);
addr |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8; addr |= cycle_read(gb, gb->pc++) << 8;
gb->registers[GB_REGISTER_AF] |= cycle_read(gb, addr) << 8; gb->registers[GB_REGISTER_AF] |= cycle_read(gb, addr) << 8;
} }
@ -1249,7 +1327,7 @@ static void ld_hl_sp_r8(GB_gameboy_t *gb, uint8_t opcode)
{ {
int16_t offset; int16_t offset;
gb->registers[GB_REGISTER_AF] &= 0xFF00; gb->registers[GB_REGISTER_AF] &= 0xFF00;
offset = (int8_t) cycle_read_inc_oam_bug(gb, gb->pc++); offset = (int8_t) cycle_read(gb, gb->pc++);
cycle_no_access(gb); cycle_no_access(gb);
gb->registers[GB_REGISTER_HL] = gb->registers[GB_REGISTER_SP] + offset; gb->registers[GB_REGISTER_HL] = gb->registers[GB_REGISTER_SP] + offset;
@ -1423,7 +1501,7 @@ static void bit_r(GB_gameboy_t *gb, uint8_t opcode)
static void cb_prefix(GB_gameboy_t *gb, uint8_t opcode) static void cb_prefix(GB_gameboy_t *gb, uint8_t opcode)
{ {
opcode = cycle_read_inc_oam_bug(gb, gb->pc++); opcode = cycle_read(gb, gb->pc++);
switch (opcode >> 3) { switch (opcode >> 3) {
case 0: case 0:
rlc_r(gb, opcode); rlc_r(gb, opcode);
@ -1456,8 +1534,8 @@ static void cb_prefix(GB_gameboy_t *gb, uint8_t opcode)
} }
static GB_opcode_t *opcodes[256] = { static GB_opcode_t *opcodes[256] = {
/* X0 X1 X2 X3 X4 X5 X6 X7 */ /* X0 X1 X2 X3 X4 X5 X6 X7 */
/* X8 X9 Xa Xb Xc Xd Xe Xf */ /* X8 X9 Xa Xb Xc Xd Xe Xf */
nop, ld_rr_d16, ld_drr_a, inc_rr, inc_hr, dec_hr, ld_hr_d8, rlca, /* 0X */ nop, ld_rr_d16, ld_drr_a, inc_rr, inc_hr, dec_hr, ld_hr_d8, rlca, /* 0X */
ld_da16_sp, add_hl_rr, ld_a_drr, dec_rr, inc_lr, dec_lr, ld_lr_d8, rrca, ld_da16_sp, add_hl_rr, ld_a_drr, dec_rr, inc_lr, dec_lr, ld_lr_d8, rrca,
stop, ld_rr_d16, ld_drr_a, inc_rr, inc_hr, dec_hr, ld_hr_d8, rla, /* 1X */ stop, ld_rr_d16, ld_drr_a, inc_rr, inc_hr, dec_hr, ld_hr_d8, rla, /* 1X */
@ -1531,15 +1609,18 @@ void GB_cpu_run(GB_gameboy_t *gb)
/* Wake up from HALT mode without calling interrupt code. */ /* Wake up from HALT mode without calling interrupt code. */
if (gb->halted && !effective_ime && interrupt_queue) { if (gb->halted && !effective_ime && interrupt_queue) {
gb->halted = false; gb->halted = false;
gb->speed_switch_halt_countdown = 0;
} }
/* Call interrupt */ /* Call interrupt */
else if (effective_ime && interrupt_queue) { else if (effective_ime && interrupt_queue) {
gb->halted = false; gb->halted = false;
gb->speed_switch_halt_countdown = 0;
uint16_t call_addr = gb->pc; uint16_t call_addr = gb->pc;
cycle_no_access(gb); gb->last_opcode_read = cycle_read(gb, gb->pc++);
cycle_no_access(gb); cycle_oam_bug_pc(gb);
gb->pc--;
GB_trigger_oam_bug(gb, gb->registers[GB_REGISTER_SP]); /* Todo: test T-cycle timing */ GB_trigger_oam_bug(gb, gb->registers[GB_REGISTER_SP]); /* Todo: test T-cycle timing */
cycle_no_access(gb); cycle_no_access(gb);
@ -1572,7 +1653,7 @@ void GB_cpu_run(GB_gameboy_t *gb)
} }
/* Run mode */ /* Run mode */
else if (!gb->halted) { else if (!gb->halted) {
gb->last_opcode_read = cycle_read_inc_oam_bug(gb, gb->pc++); gb->last_opcode_read = cycle_read(gb, gb->pc++);
if (gb->halt_bug) { if (gb->halt_bug) {
gb->pc--; gb->pc--;
gb->halt_bug = false; gb->halt_bug = false;

View File

@ -40,7 +40,7 @@ const GB_bank_symbol_t *GB_map_find_symbol(GB_symbol_map_t *map, uint16_t addr)
{ {
if (!map) return NULL; if (!map) return NULL;
size_t index = GB_map_find_symbol_index(map, addr); size_t index = GB_map_find_symbol_index(map, addr);
if (index < map->n_symbols && map->symbols[index].addr != addr) { if (index >= map->n_symbols || map->symbols[index].addr != addr) {
index--; index--;
} }
if (index < map->n_symbols) { if (index < map->n_symbols) {

View File

@ -64,11 +64,16 @@ void GB_timing_sync(GB_gameboy_t *gb)
uint64_t target_nanoseconds = gb->cycles_since_last_sync * 1000000000LL / 2 / GB_get_clock_rate(gb); /* / 2 because we use 8MHz units */ uint64_t target_nanoseconds = gb->cycles_since_last_sync * 1000000000LL / 2 / GB_get_clock_rate(gb); /* / 2 because we use 8MHz units */
int64_t nanoseconds = get_nanoseconds(); int64_t nanoseconds = get_nanoseconds();
int64_t time_to_sleep = target_nanoseconds + gb->last_sync - nanoseconds; int64_t time_to_sleep = target_nanoseconds + gb->last_sync - nanoseconds;
if (time_to_sleep > 0 && time_to_sleep < LCDC_PERIOD * 1000000000LL / GB_get_clock_rate(gb)) { if (time_to_sleep > 0 && time_to_sleep < LCDC_PERIOD * 1200000000LL / GB_get_clock_rate(gb)) { // +20% to be more forgiving
nsleep(time_to_sleep); nsleep(time_to_sleep);
gb->last_sync += target_nanoseconds; gb->last_sync += target_nanoseconds;
} }
else { else {
if (time_to_sleep < 0 && -time_to_sleep < LCDC_PERIOD * 1200000000LL / GB_get_clock_rate(gb)) {
// We're running a bit too slow, but the difference is small enough,
// just skip this sync and let it even out
return;
}
gb->last_sync = nanoseconds; gb->last_sync = nanoseconds;
} }
@ -89,15 +94,32 @@ void GB_timing_sync(GB_gameboy_t *gb)
} }
#endif #endif
static void GB_ir_run(GB_gameboy_t *gb)
#define IR_DECAY 31500
#define IR_THRESHOLD 19900
#define IR_MAX IR_THRESHOLD * 2 + IR_DECAY
static void GB_ir_run(GB_gameboy_t *gb, uint32_t cycles)
{ {
if (gb->ir_queue_length == 0) return; if (gb->model == GB_MODEL_AGB) return;
if (gb->cycles_since_input_ir_change >= gb->ir_queue[0].delay) { if (gb->infrared_input || gb->cart_ir || (gb->io_registers[GB_IO_RP] & 1)) {
gb->cycles_since_input_ir_change -= gb->ir_queue[0].delay; gb->ir_sensor += cycles;
gb->infrared_input = gb->ir_queue[0].state; if (gb->ir_sensor > IR_MAX) {
gb->ir_queue_length--; gb->ir_sensor = IR_MAX;
memmove(&gb->ir_queue[0], &gb->ir_queue[1], sizeof(gb->ir_queue[0]) * (gb->ir_queue_length)); }
gb->effective_ir_input = gb->ir_sensor >= IR_THRESHOLD && gb->ir_sensor <= IR_THRESHOLD + IR_DECAY;
} }
else {
if (gb->ir_sensor <= cycles) {
gb->ir_sensor = 0;
}
else {
gb->ir_sensor -= cycles;
}
gb->effective_ir_input = false;
}
} }
static void advance_tima_state_machine(GB_gameboy_t *gb) static void advance_tima_state_machine(GB_gameboy_t *gb)
@ -120,27 +142,36 @@ static void increase_tima(GB_gameboy_t *gb)
} }
} }
static void GB_set_internal_div_counter(GB_gameboy_t *gb, uint32_t value) static void GB_set_internal_div_counter(GB_gameboy_t *gb, uint16_t value)
{ {
/* TIMA increases when a specific high-bit becomes a low-bit. */ /* TIMA increases when a specific high-bit becomes a low-bit. */
value &= INTERNAL_DIV_CYCLES - 1; uint16_t triggers = gb->div_counter & ~value;
uint32_t triggers = gb->div_counter & ~value;
if ((gb->io_registers[GB_IO_TAC] & 4) && (triggers & GB_TAC_TRIGGER_BITS[gb->io_registers[GB_IO_TAC] & 3])) { if ((gb->io_registers[GB_IO_TAC] & 4) && (triggers & GB_TAC_TRIGGER_BITS[gb->io_registers[GB_IO_TAC] & 3])) {
increase_tima(gb); increase_tima(gb);
} }
/* TODO: Can switching to double speed mode trigger an event? */ /* TODO: Can switching to double speed mode trigger an event? */
if (triggers & (gb->cgb_double_speed? 0x2000 : 0x1000)) { uint16_t apu_bit = gb->cgb_double_speed? 0x2000 : 0x1000;
if (triggers & apu_bit) {
GB_apu_run(gb); GB_apu_run(gb);
GB_apu_div_event(gb); GB_apu_div_event(gb);
} }
else {
uint16_t secondary_triggers = ~gb->div_counter & value;
if (secondary_triggers & apu_bit) {
GB_apu_run(gb);
GB_apu_div_secondary_event(gb);
}
}
gb->div_counter = value; gb->div_counter = value;
} }
static void GB_timers_run(GB_gameboy_t *gb, uint8_t cycles) static void GB_timers_run(GB_gameboy_t *gb, uint8_t cycles)
{ {
if (gb->stopped) { if (gb->stopped) {
gb->apu.apu_cycles += 4 << !gb->cgb_double_speed; if (GB_is_cgb(gb)) {
gb->apu.apu_cycles += 4 << !gb->cgb_double_speed;
}
return; return;
} }
@ -171,6 +202,9 @@ main:
static void advance_serial(GB_gameboy_t *gb, uint8_t cycles) static void advance_serial(GB_gameboy_t *gb, uint8_t cycles)
{ {
if (gb->printer.command_state || gb->printer.bits_received) {
gb->printer.idle_time += cycles;
}
if (gb->serial_length == 0) { if (gb->serial_length == 0) {
gb->serial_cycles += cycles; gb->serial_cycles += cycles;
return; return;
@ -213,8 +247,121 @@ static void advance_serial(GB_gameboy_t *gb, uint8_t cycles)
} }
static void GB_rtc_run(GB_gameboy_t *gb, uint8_t cycles)
{
if (gb->cartridge_type->mbc_type != GB_HUC3 && !gb->cartridge_type->has_rtc) return;
gb->rtc_cycles += cycles;
time_t current_time = 0;
switch (gb->rtc_mode) {
case GB_RTC_MODE_SYNC_TO_HOST:
// Sync in a 1/32s resolution
if (gb->rtc_cycles < GB_get_unmultiplied_clock_rate(gb) / 16) return;
gb->rtc_cycles -= GB_get_unmultiplied_clock_rate(gb) / 16;
current_time = time(NULL);
break;
case GB_RTC_MODE_ACCURATE:
if (gb->cartridge_type->mbc_type != GB_HUC3 && (gb->rtc_real.high & 0x40)) {
gb->rtc_cycles -= cycles;
return;
}
if (gb->rtc_cycles < GB_get_unmultiplied_clock_rate(gb) * 2) return;
gb->rtc_cycles -= GB_get_unmultiplied_clock_rate(gb) * 2;
current_time = gb->last_rtc_second + 1;
break;
}
if (gb->cartridge_type->mbc_type == GB_HUC3) {
while (gb->last_rtc_second / 60 < current_time / 60) {
gb->last_rtc_second += 60;
gb->huc3_minutes++;
if (gb->huc3_minutes == 60 * 24) {
gb->huc3_days++;
gb->huc3_minutes = 0;
}
}
return;
}
bool running = false;
if (gb->cartridge_type->mbc_type == GB_TPP1) {
running = gb->tpp1_mr4 & 0x4;
}
else {
running = (gb->rtc_real.high & 0x40) == 0;
}
if (running) { /* is timer running? */
while (gb->last_rtc_second + 60 * 60 * 24 < current_time) {
gb->last_rtc_second += 60 * 60 * 24;
if (gb->cartridge_type->mbc_type == GB_TPP1) {
if (++gb->rtc_real.tpp1.weekday == 7) {
gb->rtc_real.tpp1.weekday = 0;
if (++gb->rtc_real.tpp1.weeks == 0) {
gb->tpp1_mr4 |= 8; /* Overflow bit */
}
}
}
else if (++gb->rtc_real.days == 0) {
if (gb->rtc_real.high & 1) { /* Bit 8 of days*/
gb->rtc_real.high |= 0x80; /* Overflow bit */
}
gb->rtc_real.high ^= 1;
}
}
while (gb->last_rtc_second < current_time) {
gb->last_rtc_second++;
if (++gb->rtc_real.seconds != 60) continue;
gb->rtc_real.seconds = 0;
if (++gb->rtc_real.minutes != 60) continue;
gb->rtc_real.minutes = 0;
if (gb->cartridge_type->mbc_type == GB_TPP1) {
if (++gb->rtc_real.tpp1.hours != 24) continue;
gb->rtc_real.tpp1.hours = 0;
if (++gb->rtc_real.tpp1.weekday != 7) continue;
gb->rtc_real.tpp1.weekday = 0;
if (++gb->rtc_real.tpp1.weeks == 0) {
gb->tpp1_mr4 |= 8; /* Overflow bit */
}
}
else {
if (++gb->rtc_real.hours != 24) continue;
gb->rtc_real.hours = 0;
if (++gb->rtc_real.days != 0) continue;
if (gb->rtc_real.high & 1) { /* Bit 8 of days*/
gb->rtc_real.high |= 0x80; /* Overflow bit */
}
gb->rtc_real.high ^= 1;
}
}
}
}
void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles) void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles)
{ {
if (gb->speed_switch_countdown) {
if (gb->speed_switch_countdown == cycles) {
gb->cgb_double_speed ^= true;
gb->speed_switch_countdown = 0;
}
else if (gb->speed_switch_countdown > cycles) {
gb->speed_switch_countdown -= cycles;
}
else {
uint8_t old_cycles = gb->speed_switch_countdown;
cycles -= old_cycles;
gb->speed_switch_countdown = 0;
GB_advance_cycles(gb, old_cycles);
gb->cgb_double_speed ^= true;
}
}
gb->apu.pcm_mask[0] = gb->apu.pcm_mask[1] = 0xFF; // Sort of hacky, but too many cross-component interactions to do it right gb->apu.pcm_mask[0] = gb->apu.pcm_mask[1] = 0xFF; // Sort of hacky, but too many cross-component interactions to do it right
// Affected by speed boost // Affected by speed boost
gb->dma_cycles += cycles; gb->dma_cycles += cycles;
@ -224,35 +371,51 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles)
advance_serial(gb, cycles); // TODO: Verify what happens in STOP mode advance_serial(gb, cycles); // TODO: Verify what happens in STOP mode
} }
if (gb->speed_switch_halt_countdown) {
gb->speed_switch_halt_countdown -= cycles;
if (gb->speed_switch_halt_countdown <= 0) {
gb->speed_switch_halt_countdown = 0;
gb->halted = false;
}
}
gb->debugger_ticks += cycles; gb->debugger_ticks += cycles;
if (gb->speed_switch_freeze) {
if (gb->speed_switch_freeze >= cycles) {
gb->speed_switch_freeze -= cycles;
return;
}
cycles -= gb->speed_switch_freeze;
gb->speed_switch_freeze = 0;
}
if (!gb->cgb_double_speed) { if (!gb->cgb_double_speed) {
cycles <<= 1; cycles <<= 1;
} }
gb->absolute_debugger_ticks += cycles;
// Not affected by speed boost // Not affected by speed boost
gb->double_speed_alignment += cycles; if (gb->io_registers[GB_IO_LCDC] & 0x80) {
gb->double_speed_alignment += cycles;
}
gb->hdma_cycles += cycles; gb->hdma_cycles += cycles;
gb->apu_output.sample_cycles += cycles; gb->apu_output.sample_cycles += cycles;
gb->cycles_since_ir_change += cycles;
gb->cycles_since_input_ir_change += cycles;
gb->cycles_since_last_sync += cycles; gb->cycles_since_last_sync += cycles;
gb->cycles_since_run += cycles; gb->cycles_since_run += cycles;
if (gb->rumble_state) { gb->rumble_on_cycles += gb->rumble_strength & 3;
gb->rumble_on_cycles++; gb->rumble_off_cycles += (gb->rumble_strength & 3) ^ 3;
}
else {
gb->rumble_off_cycles++;
}
if (!gb->stopped) { // TODO: Verify what happens in STOP mode if (!gb->stopped) { // TODO: Verify what happens in STOP mode
GB_dma_run(gb); GB_dma_run(gb);
GB_hdma_run(gb); GB_hdma_run(gb);
} }
GB_apu_run(gb); GB_apu_run(gb);
GB_display_run(gb, cycles); GB_display_run(gb, cycles);
GB_ir_run(gb); GB_ir_run(gb, cycles);
GB_rtc_run(gb, cycles);
} }
/* /*
@ -276,52 +439,3 @@ void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac)
} }
} }
} }
void GB_rtc_run(GB_gameboy_t *gb)
{
if (gb->cartridge_type->mbc_type == GB_HUC3) {
time_t current_time = time(NULL);
while (gb->last_rtc_second / 60 < current_time / 60) {
gb->last_rtc_second += 60;
gb->huc3_minutes++;
if (gb->huc3_minutes == 60 * 24) {
gb->huc3_days++;
gb->huc3_minutes = 0;
}
}
return;
}
if ((gb->rtc_real.high & 0x40) == 0) { /* is timer running? */
time_t current_time = time(NULL);
while (gb->last_rtc_second + 60 * 60 * 24 < current_time) {
gb->last_rtc_second += 60 * 60 * 24;
if (++gb->rtc_real.days == 0) {
if (gb->rtc_real.high & 1) { /* Bit 8 of days*/
gb->rtc_real.high |= 0x80; /* Overflow bit */
}
gb->rtc_real.high ^= 1;
}
}
while (gb->last_rtc_second < current_time) {
gb->last_rtc_second++;
if (++gb->rtc_real.seconds == 60) {
gb->rtc_real.seconds = 0;
if (++gb->rtc_real.minutes == 60) {
gb->rtc_real.minutes = 0;
if (++gb->rtc_real.hours == 24) {
gb->rtc_real.hours = 0;
if (++gb->rtc_real.days == 0) {
if (gb->rtc_real.high & 1) { /* Bit 8 of days*/
gb->rtc_real.high |= 0x80; /* Overflow bit */
}
gb->rtc_real.high ^= 1;
}
}
}
}
}
}
}

View File

@ -4,7 +4,6 @@
#ifdef GB_INTERNAL #ifdef GB_INTERNAL
void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles); void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles);
void GB_rtc_run(GB_gameboy_t *gb);
void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac); void GB_emulate_timer_glitch(GB_gameboy_t *gb, uint8_t old_tac, uint8_t new_tac);
bool GB_timing_sync_turbo(GB_gameboy_t *gb); /* Returns true if should skip frame */ bool GB_timing_sync_turbo(GB_gameboy_t *gb); /* Returns true if should skip frame */
void GB_timing_sync(GB_gameboy_t *gb); void GB_timing_sync(GB_gameboy_t *gb);

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,12 @@
[Desktop Entry]
Version=1.0
Type=Application
Icon=sameboy
Exec=sameboy
Name=SameBoy
Comment=Game Boy and Game Boy Color emulator
Keywords=game;boy;gameboy;color;emulator
Terminal=false
StartupNotify=false
Categories=Game;Emulator;
MimeType=application/x-gameboy-rom;application/x-gameboy-color-rom;application/x-gameboy-isx

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<mime-info xmlns='http://www.freedesktop.org/standards/shared-mime-info'>
<mime-type type="application/x-gameboy-rom">
<comment>Game Boy ROM</comment>
<icon name="x-gameboy-rom"/>
<glob-deleteall/>
<glob pattern="*.gb"/>
<glob pattern="*.sgb"/>
</mime-type>
<mime-type type="application/x-gameboy-color-rom">
<comment>Game Boy Color ROM</comment>
<icon name="x-gameboy-color-rom"/>
<glob-deleteall/>
<glob pattern="*.gbc"/>
<glob pattern="*.cgb"/>
</mime-type>
<mime-type type="application/x-gameboy-isx">
<comment>Game Boy ISX binary</comment>
<icon name="x-gameboy-rom"/>
<glob-deleteall/>
<glob pattern="*.isx"/>
</mime-type>
</mime-info>

View File

@ -4,8 +4,6 @@
hacksByManufacturer = @{ hacksByManufacturer = @{
@(0x045E): @{ // Microsoft @(0x045E): @{ // Microsoft
/* Generally untested, but Microsoft goes by the book when it comes to HID report descriptors, so
it should work out of the box. The hack is only here for automatic mapping */
JOYAxisGroups: @{ JOYAxisGroups: @{
@(kHIDUsage_GD_X): @(0), @(kHIDUsage_GD_X): @(0),
@ -13,7 +11,7 @@ hacksByManufacturer = @{
@(kHIDUsage_GD_Z): @(2), @(kHIDUsage_GD_Z): @(2),
@(kHIDUsage_GD_Rx): @(1), @(kHIDUsage_GD_Rx): @(1),
@(kHIDUsage_GD_Ry): @(1), @(kHIDUsage_GD_Ry): @(1),
@(kHIDUsage_GD_Rz): @(3), @(kHIDUsage_GD_Rz): @(2),
}, },
JOYButtonUsageMapping: @{ JOYButtonUsageMapping: @{
@ -37,8 +35,10 @@ hacksByManufacturer = @{
JOYAxes2DUsageMapping: @{ JOYAxes2DUsageMapping: @{
AXES2D(1): @(JOYAxes2DUsageLeftStick), AXES2D(1): @(JOYAxes2DUsageLeftStick),
AXES2D(4): @(JOYAxes2DUsageRightStick), AXES2D(3): @(JOYAxes2DUsageRightStick),
}, },
JOYEmulateAxisButtons: @YES,
}, },
@(0x054C): @{ // Sony @(0x054C): @{ // Sony
@ -71,14 +71,49 @@ hacksByManufacturer = @{
}, },
JOYAxisUsageMapping: @{ JOYAxisUsageMapping: @{
AXIS(4): @(JOYAxisUsageL1), AXIS(4): @(JOYAxisUsageL2),
AXIS(5): @(JOYAxisUsageR1), AXIS(5): @(JOYAxisUsageR2),
}, },
JOYAxes2DUsageMapping: @{ JOYAxes2DUsageMapping: @{
AXES2D(1): @(JOYAxes2DUsageLeftStick), AXES2D(1): @(JOYAxes2DUsageLeftStick),
AXES2D(4): @(JOYAxes2DUsageRightStick), AXES2D(4): @(JOYAxes2DUsageRightStick),
}, },
// When DualSense mode is activated on BT, The report ID is 0x31 and there's an extra byte
JOYCustomReports: @{
@(0x31): @[
/* 1D and 2D axes */
@{@"reportID": @(0x31), @"size":@8, @"offset":@0x08, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @255},
@{@"reportID": @(0x31), @"size":@8, @"offset":@0x10, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @0, @"max": @255},
@{@"reportID": @(0x31), @"size":@8, @"offset":@0x18, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Z), @"min": @0, @"max": @255},
@{@"reportID": @(0x31), @"size":@8, @"offset":@0x20, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rz), @"min": @0, @"max": @255},
@{@"reportID": @(0x31), @"size":@8, @"offset":@0x28, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255},
@{@"reportID": @(0x31), @"size":@8, @"offset":@0x30, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @0, @"max": @255},
/* Hat Switch*/
@{@"reportID": @(0x31), @"size":@4, @"offset":@0x40, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Hatswitch), @"min": @0, @"max": @7},
/* Buttons */
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x44, @"usagePage":@(kHIDPage_Button), @"usage":@1},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x45, @"usagePage":@(kHIDPage_Button), @"usage":@2},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x46, @"usagePage":@(kHIDPage_Button), @"usage":@3},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x47, @"usagePage":@(kHIDPage_Button), @"usage":@4},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x48, @"usagePage":@(kHIDPage_Button), @"usage":@5},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x49, @"usagePage":@(kHIDPage_Button), @"usage":@6},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x4a, @"usagePage":@(kHIDPage_Button), @"usage":@7},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x4b, @"usagePage":@(kHIDPage_Button), @"usage":@8},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x4c, @"usagePage":@(kHIDPage_Button), @"usage":@9},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x4d, @"usagePage":@(kHIDPage_Button), @"usage":@10},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x4e, @"usagePage":@(kHIDPage_Button), @"usage":@11},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x4f, @"usagePage":@(kHIDPage_Button), @"usage":@12},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x50, @"usagePage":@(kHIDPage_Button), @"usage":@13},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x51, @"usagePage":@(kHIDPage_Button), @"usage":@14},
@{@"reportID": @(0x31), @"size":@1, @"offset":@0x52, @"usagePage":@(kHIDPage_Button), @"usage":@15},
],
},
JOYIsSony: @YES,
} }
}; };
@ -407,10 +442,9 @@ hacksByName = @{
@{@"reportID": @(1), @"size":@12, @"offset":@76, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @0xFFF, @"max": @0}, @{@"reportID": @(1), @"size":@12, @"offset":@76, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @0xFFF, @"max": @0},
], ],
}, },
JOYIgnoredReports: @[@(0x30)], // Ignore the real 0x30 report as it's broken
}, },
JOYIgnoredReports: @(0x30), // Ignore the real 0x30 report as it's broken
@"PLAYSTATION(R)3 Controller": @{ // DualShock 3 @"PLAYSTATION(R)3 Controller": @{ // DualShock 3
JOYAxisGroups: @{ JOYAxisGroups: @{
@(kHIDUsage_GD_X): @(0), @(kHIDUsage_GD_X): @(0),

View File

@ -1,4 +1,5 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import "JOYButton.h"
typedef enum { typedef enum {
JOYAxisUsageNone, JOYAxisUsageNone,
@ -8,11 +9,16 @@ typedef enum {
JOYAxisUsageR1, JOYAxisUsageR1,
JOYAxisUsageR2, JOYAxisUsageR2,
JOYAxisUsageR3, JOYAxisUsageR3,
JOYAxisUsageSlider,
JOYAxisUsageDial,
JOYAxisUsageWheel, JOYAxisUsageWheel,
JOYAxisUsageRudder, JOYAxisUsageRudder,
JOYAxisUsageThrottle, JOYAxisUsageThrottle,
JOYAxisUsageAccelerator, JOYAxisUsageAccelerator,
JOYAxisUsageBrake, JOYAxisUsageBrake,
JOYAxisUsageNonGenericMax, JOYAxisUsageNonGenericMax,
JOYAxisUsageGeneric0 = 0x10000, JOYAxisUsageGeneric0 = 0x10000,
@ -23,6 +29,7 @@ typedef enum {
+ (NSString *)usageToString: (JOYAxisUsage) usage; + (NSString *)usageToString: (JOYAxisUsage) usage;
- (uint64_t)uniqueID; - (uint64_t)uniqueID;
- (double)value; - (double)value;
- (JOYButtonUsage)equivalentButtonUsage;
@property JOYAxisUsage usage; @property JOYAxisUsage usage;
@end @end

View File

@ -19,6 +19,8 @@
@"Analog R1", @"Analog R1",
@"Analog R2", @"Analog R2",
@"Analog R3", @"Analog R3",
@"Slider",
@"Dial",
@"Wheel", @"Wheel",
@"Rudder", @"Rudder",
@"Throttle", @"Throttle",
@ -57,10 +59,23 @@
if (element.usagePage == kHIDPage_GenericDesktop) { if (element.usagePage == kHIDPage_GenericDesktop) {
uint16_t usage = element.usage; switch (element.usage) {
_usage = JOYAxisUsageGeneric0 + usage - kHIDUsage_GD_X + 1; case kHIDUsage_GD_Slider: _usage = JOYAxisUsageSlider; break;
case kHIDUsage_GD_Dial: _usage = JOYAxisUsageDial; break;
case kHIDUsage_GD_Wheel: _usage = JOYAxisUsageWheel; break;
default:
_usage = JOYAxisUsageGeneric0 + element.usage - kHIDUsage_GD_X + 1;
break;
}
}
else if (element.usagePage == kHIDPage_Simulation) {
switch (element.usage) {
case kHIDUsage_Sim_Accelerator: _usage = JOYAxisUsageAccelerator; break;
case kHIDUsage_Sim_Brake: _usage = JOYAxisUsageBrake; break;
case kHIDUsage_Sim_Rudder: _usage = JOYAxisUsageRudder; break;
case kHIDUsage_Sim_Throttle: _usage = JOYAxisUsageThrottle; break;
}
} }
_min = 1.0; _min = 1.0;
return self; return self;
@ -87,4 +102,28 @@
return old != _state; return old != _state;
} }
- (JOYButtonUsage)equivalentButtonUsage
{
if (self.usage >= JOYAxisUsageGeneric0) {
return self.usage - JOYAxisUsageGeneric0 + JOYButtonUsageGeneric0;
}
switch (self.usage) {
case JOYAxisUsageL1: return JOYButtonUsageL1;
case JOYAxisUsageL2: return JOYButtonUsageL2;
case JOYAxisUsageL3: return JOYButtonUsageL3;
case JOYAxisUsageR1: return JOYButtonUsageR1;
case JOYAxisUsageR2: return JOYButtonUsageR2;
case JOYAxisUsageR3: return JOYButtonUsageR3;
case JOYAxisUsageSlider: return JOYButtonUsageSlider;
case JOYAxisUsageDial: return JOYButtonUsageDial;
case JOYAxisUsageWheel: return JOYButtonUsageWheel;
case JOYAxisUsageRudder: return JOYButtonUsageRudder;
case JOYAxisUsageThrottle: return JOYButtonUsageThrottle;
case JOYAxisUsageAccelerator: return JOYButtonUsageAccelerator;
case JOYAxisUsageBrake: return JOYButtonUsageBrake;
default: return JOYButtonUsageNone;
}
}
@end @end

View File

@ -26,6 +26,16 @@ typedef enum {
JOYButtonUsageDPadRight, JOYButtonUsageDPadRight,
JOYButtonUsageDPadUp, JOYButtonUsageDPadUp,
JOYButtonUsageDPadDown, JOYButtonUsageDPadDown,
JOYButtonUsageSlider,
JOYButtonUsageDial,
JOYButtonUsageWheel,
JOYButtonUsageRudder,
JOYButtonUsageThrottle,
JOYButtonUsageAccelerator,
JOYButtonUsageBrake,
JOYButtonUsageNonGenericMax, JOYButtonUsageNonGenericMax,
JOYButtonUsageGeneric0 = 0x10000, JOYButtonUsageGeneric0 = 0x10000,

View File

@ -1,5 +1,6 @@
#import "JOYButton.h" #import "JOYButton.h"
#import "JOYElement.h" #import "JOYElement.h"
#import <AppKit/AppKit.h>
@implementation JOYButton @implementation JOYButton
{ {
@ -80,6 +81,12 @@
case kHIDUsage_GD_SystemMainMenu: _usage = JOYButtonUsageHome; break; case kHIDUsage_GD_SystemMainMenu: _usage = JOYButtonUsageHome; break;
} }
} }
else if (element.usagePage == kHIDPage_Consumer) {
switch (element.usage) {
case kHIDUsage_Csmr_ACHome: _usage = JOYButtonUsageHome; break;
case kHIDUsage_Csmr_ACBack: _usage = JOYButtonUsageSelect; break;
}
}
return self; return self;
} }
@ -98,5 +105,4 @@
} }
return false; return false;
} }
@end @end

View File

@ -4,7 +4,6 @@
#import "JOYAxes2D.h" #import "JOYAxes2D.h"
#import "JOYHat.h" #import "JOYHat.h"
static NSString const *JOYAxesEmulateButtonsKey = @"JOYAxesEmulateButtons";
static NSString const *JOYAxes2DEmulateButtonsKey = @"JOYAxes2DEmulateButtons"; static NSString const *JOYAxes2DEmulateButtonsKey = @"JOYAxes2DEmulateButtons";
static NSString const *JOYHatsEmulateButtonsKey = @"JOYHatsEmulateButtons"; static NSString const *JOYHatsEmulateButtonsKey = @"JOYHatsEmulateButtons";
@ -35,6 +34,7 @@ static NSString const *JOYHatsEmulateButtonsKey = @"JOYHatsEmulateButtons";
- (NSArray<JOYHat *> *) hats; - (NSArray<JOYHat *> *) hats;
- (void)setRumbleAmplitude:(double)amp; - (void)setRumbleAmplitude:(double)amp;
- (void)setPlayerLEDs:(uint8_t)mask; - (void)setPlayerLEDs:(uint8_t)mask;
- (uint8_t)LEDMaskForPlayer:(unsigned)player;
@property (readonly, getter=isConnected) bool connected; @property (readonly, getter=isConnected) bool connected;
@end @end

View File

@ -7,6 +7,9 @@
#import "JOYEmulatedButton.h" #import "JOYEmulatedButton.h"
#include <IOKit/hid/IOHIDLib.h> #include <IOKit/hid/IOHIDLib.h>
#include <AppKit/AppKit.h>
extern NSTextField *globalDebugField;
#define PWM_RESOLUTION 16 #define PWM_RESOLUTION 16
static NSString const *JOYAxisGroups = @"JOYAxisGroups"; static NSString const *JOYAxisGroups = @"JOYAxisGroups";
@ -26,6 +29,8 @@ static NSString const *JOYSwapZRz = @"JOYSwapZRz";
static NSString const *JOYActivationReport = @"JOYActivationReport"; static NSString const *JOYActivationReport = @"JOYActivationReport";
static NSString const *JOYIgnoredReports = @"JOYIgnoredReports"; static NSString const *JOYIgnoredReports = @"JOYIgnoredReports";
static NSString const *JOYIsDualShock3 = @"JOYIsDualShock3"; static NSString const *JOYIsDualShock3 = @"JOYIsDualShock3";
static NSString const *JOYIsSony = @"JOYIsSony";
static NSString const *JOYEmulateAxisButtons = @"JOYEmulateAxisButtons";
static NSMutableDictionary<id, JOYController *> *controllers; // Physical controllers static NSMutableDictionary<id, JOYController *> *controllers; // Physical controllers
static NSMutableArray<JOYController *> *exposedControllers; // Logical controllers static NSMutableArray<JOYController *> *exposedControllers; // Logical controllers
@ -35,7 +40,6 @@ static NSDictionary *hacksByManufacturer = nil;
static NSMutableSet<id<JOYListener>> *listeners = nil; static NSMutableSet<id<JOYListener>> *listeners = nil;
static bool axesEmulateButtons = false;
static bool axes2DEmulateButtons = false; static bool axes2DEmulateButtons = false;
static bool hatsEmulateButtons = false; static bool hatsEmulateButtons = false;
@ -125,9 +129,37 @@ typedef struct __attribute__((packed)) {
uint8_t padding3[13]; uint8_t padding3[13];
} JOYDualShock3Output; } JOYDualShock3Output;
typedef struct __attribute__((packed)) {
uint8_t reportID;
uint8_t sequence;
union {
uint8_t tag;
uint8_t reportIDOnUSB;
};
uint16_t flags;
uint8_t rumbleRightStrength; // Weak
uint8_t rumbleLeftStrength; // Strong
uint8_t reserved[4];
uint8_t muteButtonLED;
uint8_t powerSaveControl;
uint8_t reserved2[28];
uint8_t flags2;
uint8_t reserved3[2];
uint8_t lightbarSetup;
uint8_t LEDBrightness;
uint8_t playerLEDs;
uint8_t lightbarRed;
uint8_t lightbarGreen;
uint8_t lightbarBlue;
uint8_t bluetoothSpecific[24];
uint32_t crc32;
} JOYDualSenseOutput;
typedef union { typedef union {
JOYSwitchPacket switchPacket; JOYSwitchPacket switchPacket;
JOYDualShock3Output ds3Output; JOYDualShock3Output ds3Output;
JOYDualSenseOutput dualsenseOutput;
} JOYVendorSpecificOutput; } JOYVendorSpecificOutput;
@implementation JOYController @implementation JOYController
@ -151,6 +183,10 @@ typedef union {
NSString *_serialSuffix; NSString *_serialSuffix;
bool _isSwitch; // Does this controller use the Switch protocol? bool _isSwitch; // Does this controller use the Switch protocol?
bool _isDualShock3; // Does this controller use DS3 outputs? bool _isDualShock3; // Does this controller use DS3 outputs?
bool _isSony; // Is this a DS4 or newer Sony controller?
bool _isDualSense;
bool _isUSBDualSense;
JOYVendorSpecificOutput _lastVendorSpecificOutput; JOYVendorSpecificOutput _lastVendorSpecificOutput;
volatile double _rumbleAmplitude; volatile double _rumbleAmplitude;
bool _physicallyConnected; bool _physicallyConnected;
@ -166,6 +202,7 @@ typedef union {
double _sentRumbleAmp; double _sentRumbleAmp;
unsigned _rumbleCounter; unsigned _rumbleCounter;
bool _deviceCantSendReports; bool _deviceCantSendReports;
dispatch_queue_t _rumbleQueue;
} }
- (instancetype)initWithDevice:(IOHIDDeviceRef) device hacks:(NSDictionary *)hacks - (instancetype)initWithDevice:(IOHIDDeviceRef) device hacks:(NSDictionary *)hacks
@ -200,29 +237,40 @@ typedef union {
return; return;
} }
if (element.usagePage == kHIDPage_Button) { NSDictionary *axisGroups = @{
@(kHIDUsage_GD_X): @(0),
@(kHIDUsage_GD_Y): @(0),
@(kHIDUsage_GD_Z): @(1),
@(kHIDUsage_GD_Rx): @(2),
@(kHIDUsage_GD_Ry): @(2),
@(kHIDUsage_GD_Rz): @(1),
};
axisGroups = _hacks[JOYAxisGroups] ?: axisGroups;
if (element.usagePage == kHIDPage_Button ||
(element.usagePage == kHIDPage_Consumer && (element.usage == kHIDUsage_Csmr_ACHome ||
element.usage == kHIDUsage_Csmr_ACBack))) {
button: { button: {
JOYButton *button = [[JOYButton alloc] initWithElement: element]; JOYButton *button = [[JOYButton alloc] initWithElement: element];
[_buttons setObject:button forKey:element]; [_buttons setObject:button forKey:element];
NSNumber *replacementUsage = _hacks[JOYButtonUsageMapping][@(button.usage)]; NSNumber *replacementUsage = element.usagePage == kHIDPage_Button? _hacks[JOYButtonUsageMapping][@(button.usage)] : nil;
if (replacementUsage) { if (replacementUsage) {
button.usage = [replacementUsage unsignedIntValue]; button.usage = [replacementUsage unsignedIntValue];
} }
return; return;
} }
} }
else if (element.usagePage == kHIDPage_Simulation) {
switch (element.usage) {
case kHIDUsage_Sim_Accelerator:
case kHIDUsage_Sim_Brake:
case kHIDUsage_Sim_Rudder:
case kHIDUsage_Sim_Throttle:
goto single;
}
}
else if (element.usagePage == kHIDPage_GenericDesktop) { else if (element.usagePage == kHIDPage_GenericDesktop) {
NSDictionary *axisGroups = @{
@(kHIDUsage_GD_X): @(0),
@(kHIDUsage_GD_Y): @(0),
@(kHIDUsage_GD_Z): @(1),
@(kHIDUsage_GD_Rx): @(2),
@(kHIDUsage_GD_Ry): @(2),
@(kHIDUsage_GD_Rz): @(1),
};
axisGroups = _hacks[JOYAxisGroups] ?: axisGroups;
switch (element.usage) { switch (element.usage) {
case kHIDUsage_GD_X: case kHIDUsage_GD_X:
case kHIDUsage_GD_Y: case kHIDUsage_GD_Y:
@ -284,30 +332,26 @@ typedef union {
}*/ }*/
break; break;
} }
single:
case kHIDUsage_GD_Slider: case kHIDUsage_GD_Slider:
case kHIDUsage_GD_Dial: case kHIDUsage_GD_Dial:
case kHIDUsage_GD_Wheel: { case kHIDUsage_GD_Wheel:
{ single: {
JOYAxis *axis = [[JOYAxis alloc] initWithElement: element]; JOYAxis *axis = [[JOYAxis alloc] initWithElement: element];
[_axes setObject:axis forKey:element]; [_axes setObject:axis forKey:element];
NSNumber *replacementUsage = _hacks[JOYAxisUsageMapping][@(axis.usage)]; NSNumber *replacementUsage = element.usagePage == kHIDPage_GenericDesktop? _hacks[JOYAxisUsageMapping][@(axis.usage)] : nil;
if (replacementUsage) { if (replacementUsage) {
axis.usage = [replacementUsage unsignedIntValue]; axis.usage = [replacementUsage unsignedIntValue];
} }
if (axesEmulateButtons && axis.usage >= JOYAxisUsageL1 && axis.usage <= JOYAxisUsageR3) { if ([_hacks[JOYEmulateAxisButtons] boolValue]) {
_axisEmulatedButtons[@(axis.uniqueID)] = _axisEmulatedButtons[@(axis.uniqueID)] =
[[JOYEmulatedButton alloc] initWithUsage:axis.usage - JOYAxisUsageL1 + JOYButtonUsageL1 uniqueID:axis.uniqueID]; [[JOYEmulatedButton alloc] initWithUsage:axis.equivalentButtonUsage uniqueID:axis.uniqueID];
} }
if (axesEmulateButtons && axis.usage >= JOYAxisUsageGeneric0) {
_axisEmulatedButtons[@(axis.uniqueID)] =
[[JOYEmulatedButton alloc] initWithUsage:axis.usage - JOYAxisUsageGeneric0 + JOYButtonUsageGeneric0 uniqueID:axis.uniqueID];
}
break; break;
} }}
case kHIDUsage_GD_DPadUp: case kHIDUsage_GD_DPadUp:
case kHIDUsage_GD_DPadDown: case kHIDUsage_GD_DPadDown:
case kHIDUsage_GD_DPadRight: case kHIDUsage_GD_DPadRight:
@ -364,6 +408,7 @@ typedef union {
_hacks = hacks; _hacks = hacks;
_isSwitch = [_hacks[JOYIsSwitch] boolValue]; _isSwitch = [_hacks[JOYIsSwitch] boolValue];
_isDualShock3 = [_hacks[JOYIsDualShock3] boolValue]; _isDualShock3 = [_hacks[JOYIsDualShock3] boolValue];
_isSony = [_hacks[JOYIsSony] boolValue];
NSDictionary *customReports = hacks[JOYCustomReports]; NSDictionary *customReports = hacks[JOYCustomReports];
_lastReport = [NSMutableData dataWithLength:MAX( _lastReport = [NSMutableData dataWithLength:MAX(
@ -416,8 +461,8 @@ typedef union {
id previous = nil; id previous = nil;
NSSet *ignoredReports = nil; NSSet *ignoredReports = nil;
if (hacks[ignoredReports]) { if (hacks[JOYIgnoredReports]) {
ignoredReports = [NSSet setWithArray:hacks[ignoredReports]]; ignoredReports = [NSSet setWithArray:hacks[JOYIgnoredReports]];
} }
for (id _element in array) { for (id _element in array) {
@ -490,8 +535,36 @@ typedef union {
{.timeEnabled = 0, .dutyLength = 0, .enabled = 0, .dutyOff = 0, .dutyOn = 0}, {.timeEnabled = 0, .dutyLength = 0, .enabled = 0, .dutyOff = 0, .dutyOn = 0},
} }
}; };
} }
if (_isSony) {
_isDualSense = [(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey)) unsignedIntValue] == 0xce6;
}
if (_isDualSense) {
_isUSBDualSense = [(__bridge NSString *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDTransportKey)) isEqualToString:@"USB"];
_lastVendorSpecificOutput.dualsenseOutput = (JOYDualSenseOutput){
.reportID = 0x31,
.tag = 0x10,
.flags = 0x1403, // Rumble, lightbar and player LEDs
.flags2 = 2,
.lightbarSetup = 2,
.lightbarBlue = 255,
};
if (_isUSBDualSense) {
_lastVendorSpecificOutput.dualsenseOutput.reportIDOnUSB = 1;
_lastVendorSpecificOutput.dualsenseOutput.lightbarBlue = 0;
_lastVendorSpecificOutput.dualsenseOutput.lightbarGreen = 96;
_lastVendorSpecificOutput.dualsenseOutput.lightbarRed = 255;
}
// Send a report to switch the controller to a more capable mode
[self sendDualSenseOutput];
_lastVendorSpecificOutput.dualsenseOutput.flags2 = 0;
_lastVendorSpecificOutput.dualsenseOutput.lightbarSetup = 0;
}
_rumbleQueue = dispatch_queue_create([NSString stringWithFormat:@"Rumble Queue for %@", self.deviceName].UTF8String,
NULL);
return self; return self;
} }
@ -564,7 +637,9 @@ typedef union {
} }
} }
} }
[self updateRumble]; dispatch_async(_rumbleQueue, ^{
[self updateRumble];
});
} }
- (void)elementChanged:(IOHIDElementRef)element - (void)elementChanged:(IOHIDElementRef)element
@ -699,7 +774,9 @@ typedef union {
_physicallyConnected = false; _physicallyConnected = false;
[exposedControllers removeObject:self]; [exposedControllers removeObject:self];
[self setRumbleAmplitude:0]; [self setRumbleAmplitude:0];
[self updateRumble]; dispatch_sync(_rumbleQueue, ^{
[self updateRumble];
});
_device = nil; _device = nil;
} }
@ -716,9 +793,92 @@ typedef union {
} }
} }
- (void) sendDualSenseOutput
{
if (_isUSBDualSense) {
[self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.dualsenseOutput.reportIDOnUSB length:_lastVendorSpecificOutput.dualsenseOutput.bluetoothSpecific - &_lastVendorSpecificOutput.dualsenseOutput.reportIDOnUSB]];
return;
}
_lastVendorSpecificOutput.dualsenseOutput.sequence += 0x10;
static const uint32_t table[] = {
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
};
const uint8_t *byte = (void *)&_lastVendorSpecificOutput.dualsenseOutput;
uint32_t size = sizeof(_lastVendorSpecificOutput.dualsenseOutput) - 4;
uint32_t ret = 0xFFFFFFFF;
ret = table[(ret ^ 0xa2) & 0xFF] ^ (ret >> 8);
while (size--) {
ret = table[(ret ^ *byte++) & 0xFF] ^ (ret >> 8);
}
_lastVendorSpecificOutput.dualsenseOutput.crc32 = ~ret;
[self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.dualsenseOutput length:sizeof(_lastVendorSpecificOutput.dualsenseOutput)]];
}
- (uint8_t)LEDMaskForPlayer:(unsigned)player
{
if (_isDualShock3) {
return 2 << player;
}
if (_isUSBDualSense) {
switch (player) {
case 0: return 0x04;
case 1: return 0x0A;
case 2: return 0x15;
case 3: return 0x1B;
default: return 0;
}
}
return 1 << player;
}
- (void)setPlayerLEDs:(uint8_t)mask - (void)setPlayerLEDs:(uint8_t)mask
{ {
mask &= 0xF;
if (mask == _playerLEDs) { if (mask == _playerLEDs) {
return; return;
} }
@ -728,14 +888,18 @@ typedef union {
_lastVendorSpecificOutput.switchPacket.sequence++; _lastVendorSpecificOutput.switchPacket.sequence++;
_lastVendorSpecificOutput.switchPacket.sequence &= 0xF; _lastVendorSpecificOutput.switchPacket.sequence &= 0xF;
_lastVendorSpecificOutput.switchPacket.command = 0x30; // LED _lastVendorSpecificOutput.switchPacket.command = 0x30; // LED
_lastVendorSpecificOutput.switchPacket.commandData[0] = mask; _lastVendorSpecificOutput.switchPacket.commandData[0] = mask & 0xF;
[self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.switchPacket length:sizeof(_lastVendorSpecificOutput.switchPacket)]]; [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.switchPacket length:sizeof(_lastVendorSpecificOutput.switchPacket)]];
} }
else if (_isDualShock3) { else if (_isDualShock3) {
_lastVendorSpecificOutput.ds3Output.reportID = 1; _lastVendorSpecificOutput.ds3Output.reportID = 1;
_lastVendorSpecificOutput.ds3Output.ledsEnabled = mask << 1; _lastVendorSpecificOutput.ds3Output.ledsEnabled = (mask & 0x1F);
[self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.ds3Output length:sizeof(_lastVendorSpecificOutput.ds3Output)]]; [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.ds3Output length:sizeof(_lastVendorSpecificOutput.ds3Output)]];
} }
else if (_isDualSense) {
_lastVendorSpecificOutput.dualsenseOutput.playerLEDs = mask & 0x1F;
[self sendDualSenseOutput];
}
} }
- (void)updateRumble - (void)updateRumble
@ -743,7 +907,7 @@ typedef union {
if (!self.connected) { if (!self.connected) {
return; return;
} }
if (!_rumbleElement && !_isSwitch && !_isDualShock3) { if (!_rumbleElement && !_isSwitch && !_isDualShock3 && !_isDualSense) {
return; return;
} }
if (_rumbleElement.max == 1 && _rumbleElement.min == 0) { if (_rumbleElement.max == 1 && _rumbleElement.min == 0) {
@ -802,6 +966,11 @@ typedef union {
_lastVendorSpecificOutput.ds3Output.rumbleLeftStrength = _lastVendorSpecificOutput.ds3Output.rumbleRightStrength = round(_rumbleAmplitude * 0xff); _lastVendorSpecificOutput.ds3Output.rumbleLeftStrength = _lastVendorSpecificOutput.ds3Output.rumbleRightStrength = round(_rumbleAmplitude * 0xff);
[self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.ds3Output length:sizeof(_lastVendorSpecificOutput.ds3Output)]]; [self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.ds3Output length:sizeof(_lastVendorSpecificOutput.ds3Output)]];
} }
else if (_isDualSense) {
_lastVendorSpecificOutput.dualsenseOutput.rumbleLeftStrength = round(_rumbleAmplitude * _rumbleAmplitude * 0xff);
_lastVendorSpecificOutput.dualsenseOutput.rumbleRightStrength = _rumbleAmplitude > 0.25 ? round(pow(_rumbleAmplitude - 0.25, 2) * 0xff) : 0;
[self sendDualSenseOutput];
}
else { else {
[_rumbleElement setValue:_rumbleAmplitude * (_rumbleElement.max - _rumbleElement.min) + _rumbleElement.min]; [_rumbleElement setValue:_rumbleAmplitude * (_rumbleElement.max - _rumbleElement.min) + _rumbleElement.min];
} }
@ -871,7 +1040,6 @@ typedef union {
+ (void)startOnRunLoop:(NSRunLoop *)runloop withOptions: (NSDictionary *)options + (void)startOnRunLoop:(NSRunLoop *)runloop withOptions: (NSDictionary *)options
{ {
axesEmulateButtons = [options[JOYAxesEmulateButtonsKey] boolValue];
axes2DEmulateButtons = [options[JOYAxes2DEmulateButtonsKey] boolValue]; axes2DEmulateButtons = [options[JOYAxes2DEmulateButtonsKey] boolValue];
hatsEmulateButtons = [options[JOYHatsEmulateButtonsKey] boolValue]; hatsEmulateButtons = [options[JOYHatsEmulateButtonsKey] boolValue];

View File

@ -1,4 +1,5 @@
#import "JOYEmulatedButton.h" #import "JOYEmulatedButton.h"
#import <AppKit/AppKit.h>
@interface JOYButton () @interface JOYButton ()
{ {
@ -28,7 +29,7 @@
- (bool)updateStateFromAxis:(JOYAxis *)axis - (bool)updateStateFromAxis:(JOYAxis *)axis
{ {
bool old = _state; bool old = _state;
_state = [axis value] > 0.5; _state = [axis value] > 0.8;
return _state != old; return _state != old;
} }

View File

@ -1,5 +1,6 @@
#import "JOYHat.h" #import "JOYHat.h"
#import "JOYElement.h" #import "JOYElement.h"
#import <AppKit/AppKit.h>
@implementation JOYHat @implementation JOYHat
{ {
@ -27,6 +28,7 @@
if (!self) return self; if (!self) return self;
_element = element; _element = element;
_state = -1;
return self; return self;
} }

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2015-2020 Lior Halphon Copyright (c) 2015-2021 Lior Halphon
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -30,6 +30,14 @@ else
DEFAULT := sdl DEFAULT := sdl
endif endif
ifneq ($(shell which xdg-open)$(FREEDESKTOP),)
# Running on an FreeDesktop environment, configure for (optional) installation
DESTDIR ?=
PREFIX ?= /usr/local
DATA_DIR ?= $(PREFIX)/share/sameboy/
FREEDESKTOP ?= true
endif
default: $(DEFAULT) default: $(DEFAULT)
ifeq ($(MAKECMDGOALS),) ifeq ($(MAKECMDGOALS),)
@ -98,8 +106,8 @@ OPEN_DIALOG = OpenDialog/cocoa.m
endif endif
# These must come before the -Wno- flags # These must come before the -Wno- flags
WARNINGS += -Werror -Wall -Wno-unknown-warning -Wno-unknown-warning-option WARNINGS += -Werror -Wall -Wno-unknown-warning -Wno-unknown-warning-option -Wno-missing-braces
WARNINGS += -Wno-nonnull -Wno-unused-result -Wno-strict-aliasing -Wno-multichar -Wno-int-in-bool-context WARNINGS += -Wno-nonnull -Wno-unused-result -Wno-strict-aliasing -Wno-multichar -Wno-int-in-bool-context -Wno-format-truncation
# Only add this flag if the compiler supports it # Only add this flag if the compiler supports it
ifeq ($(shell $(CC) -x c -c $(NULL) -o $(NULL) -Werror -Wpartial-availability 2> $(NULL); echo $$?),0) ifeq ($(shell $(CC) -x c -c $(NULL) -o $(NULL) -Werror -Wpartial-availability 2> $(NULL); echo $$?),0)
@ -113,7 +121,10 @@ endif
CFLAGS += $(WARNINGS) CFLAGS += $(WARNINGS)
CFLAGS += -std=gnu11 -D_GNU_SOURCE -DVERSION="$(VERSION)" -I. -D_USE_MATH_DEFINES CFLAGS += -std=gnu11 -D_GNU_SOURCE -DGB_VERSION='"$(VERSION)"' -I. -D_USE_MATH_DEFINES
ifneq (,$(UPDATE_SUPPORT))
CFLAGS += -DUPDATE_SUPPORT
endif
ifeq (,$(PKG_CONFIG)) ifeq (,$(PKG_CONFIG))
SDL_CFLAGS := $(shell sdl2-config --cflags) SDL_CFLAGS := $(shell sdl2-config --cflags)
@ -130,7 +141,7 @@ GL_LDFLAGS := $(shell $(PKG_CONFIG) --libs gl || echo -lGL)
endif endif
ifeq ($(PLATFORM),windows32) ifeq ($(PLATFORM),windows32)
CFLAGS += -IWindows -Drandom=rand --target=i386-pc-windows CFLAGS += -IWindows -Drandom=rand --target=i386-pc-windows
LDFLAGS += -lmsvcrt -lcomdlg32 -luser32 -lshell32 -lSDL2main -Wl,/MANIFESTFILE:NUL --target=i386-pc-windows LDFLAGS += -lmsvcrt -lcomdlg32 -luser32 -lshell32 -lole32 -lSDL2main -Wl,/MANIFESTFILE:NUL --target=i386-pc-windows
SDL_LDFLAGS := -lSDL2 SDL_LDFLAGS := -lSDL2
GL_LDFLAGS := -lopengl32 GL_LDFLAGS := -lopengl32
else else
@ -421,6 +432,49 @@ $(BIN)/BootROMs/%.bin: BootROMs/%.asm $(OBJ)/BootROMs/SameBoyLogo.pb12
libretro: libretro:
CFLAGS="$(WARNINGS)" $(MAKE) -C libretro CFLAGS="$(WARNINGS)" $(MAKE) -C libretro
# install for Linux/FreeDesktop/etc.
# Does not install mimetype icons because FreeDesktop is cursed abomination with no right to exist.
# If you somehow find a reasonable way to make associate an icon with an extension in this dumpster
# fire of a desktop environment, open an issue or a pull request
ifneq ($(FREEDESKTOP),)
ICON_NAMES := apps/sameboy mimetypes/x-gameboy-rom mimetypes/x-gameboy-color-rom
ICON_SIZES := 16x16 32x32 64x64 128x128 256x256 512x512
ICONS := $(foreach name,$(ICON_NAMES), $(foreach size,$(ICON_SIZES),$(DESTDIR)$(PREFIX)/share/icons/hicolor/$(size)/$(name).png))
install: sdl $(DESTDIR)$(PREFIX)/share/mime/packages/sameboy.xml $(ICONS) FreeDesktop/sameboy.desktop
-@$(MKDIR) -p $(dir $(DESTDIR)$(PREFIX))
mkdir -p $(DESTDIR)$(DATA_DIR)/ $(DESTDIR)$(PREFIX)/bin/
cp -rf $(BIN)/SDL/* $(DESTDIR)$(DATA_DIR)/
mv $(DESTDIR)$(DATA_DIR)/sameboy $(DESTDIR)$(PREFIX)/bin/sameboy
ifeq ($(DESTDIR),)
-update-mime-database -n $(PREFIX)/share/mime
-xdg-desktop-menu install --novendor --mode system FreeDesktop/sameboy.desktop
-xdg-icon-resource forceupdate --mode system
-xdg-desktop-menu forceupdate --mode system
ifneq ($(SUDO_USER),)
-su $(SUDO_USER) -c "xdg-desktop-menu forceupdate --mode system"
endif
else
-@$(MKDIR) -p $(DESTDIR)$(PREFIX)/share/applications/
cp FreeDesktop/sameboy.desktop $(DESTDIR)$(PREFIX)/share/applications/sameboy.desktop
endif
$(DESTDIR)$(PREFIX)/share/icons/hicolor/%/apps/sameboy.png: FreeDesktop/AppIcon/%.png
-@$(MKDIR) -p $(dir $@)
cp -f $^ $@
$(DESTDIR)$(PREFIX)/share/icons/hicolor/%/mimetypes/x-gameboy-rom.png: FreeDesktop/Cartridge/%.png
-@$(MKDIR) -p $(dir $@)
cp -f $^ $@
$(DESTDIR)$(PREFIX)/share/icons/hicolor/%/mimetypes/x-gameboy-color-rom.png: FreeDesktop/ColorCartridge/%.png
-@$(MKDIR) -p $(dir $@)
cp -f $^ $@
$(DESTDIR)$(PREFIX)/share/mime/packages/sameboy.xml: FreeDesktop/sameboy.xml
-@$(MKDIR) -p $(dir $@)
cp -f $^ $@
endif
# Clean # Clean
clean: clean:
rm -rf build rm -rf build

View File

@ -18,3 +18,21 @@ char *do_open_rom_dialog(void)
return NULL; return NULL;
} }
} }
char *do_open_folder_dialog(void)
{
@autoreleasepool {
NSWindow *key = [NSApp keyWindow];
NSOpenPanel *dialog = [NSOpenPanel openPanel];
dialog.title = @"Select Boot ROMs Folder";
dialog.canChooseDirectories = true;
dialog.canChooseFiles = false;
[dialog runModal];
[key makeKeyAndOrderFront:nil];
NSString *ret = [[[dialog URLs] firstObject] path];
if (ret) {
return strdup(ret.UTF8String);
}
return NULL;
}
}

View File

@ -6,6 +6,7 @@
#include <string.h> #include <string.h>
#define GTK_FILE_CHOOSER_ACTION_OPEN 0 #define GTK_FILE_CHOOSER_ACTION_OPEN 0
#define GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER 2
#define GTK_RESPONSE_ACCEPT -3 #define GTK_RESPONSE_ACCEPT -3
#define GTK_RESPONSE_CANCEL -6 #define GTK_RESPONSE_CANCEL -6
@ -111,3 +112,71 @@ lazy_error:
fprintf(stderr, "Failed to display GTK dialog\n"); fprintf(stderr, "Failed to display GTK dialog\n");
return NULL; return NULL;
} }
char *do_open_folder_dialog(void)
{
static void *handle = NULL;
TRY_DLOPEN("libgtk-3.so");
TRY_DLOPEN("libgtk-3.so.0");
TRY_DLOPEN("libgtk-2.so");
TRY_DLOPEN("libgtk-2.so.0");
if (!handle) {
goto lazy_error;
}
LAZY(gtk_init_check);
LAZY(gtk_file_chooser_dialog_new);
LAZY(gtk_dialog_run);
LAZY(g_free);
LAZY(gtk_widget_destroy);
LAZY(gtk_file_chooser_get_filename);
LAZY(g_log_set_default_handler);
LAZY(gtk_file_filter_new);
LAZY(gtk_file_filter_add_pattern);
LAZY(gtk_file_filter_set_name);
LAZY(gtk_file_chooser_add_filter);
LAZY(gtk_events_pending);
LAZY(gtk_main_iteration);
/* Shut up GTK */
g_log_set_default_handler(nop, NULL);
gtk_init_check(0, 0);
void *dialog = gtk_file_chooser_dialog_new("Select Boot ROMs Folder",
0,
GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
"_Cancel", GTK_RESPONSE_CANCEL,
"_Open", GTK_RESPONSE_ACCEPT,
NULL );
int res = gtk_dialog_run (dialog);
char *ret = NULL;
if (res == GTK_RESPONSE_ACCEPT) {
char *filename;
filename = gtk_file_chooser_get_filename(dialog);
ret = strdup(filename);
g_free(filename);
}
while (gtk_events_pending()) {
gtk_main_iteration();
}
gtk_widget_destroy(dialog);
while (gtk_events_pending()) {
gtk_main_iteration();
}
return ret;
lazy_error:
fprintf(stderr, "Failed to display GTK dialog\n");
return NULL;
}

Some files were not shown because too many files have changed in this diff Show More