Sync to Upstream.

This commit is contained in:
ds22x 2021-08-06 23:12:55 +02:00
commit 26a3eaaace
146 changed files with 8847 additions and 1998 deletions

View File

@ -57,6 +57,7 @@ Links
-----
- [Official git repository](https://github.com/bsnes-emu/bsnes)
- [Official Discord](https://discord.gg/B27hf27ZVf)
Nightly Builds
--------------

View File

@ -1,3 +1,7 @@
# Always use LF line endings for shaders
*.fsh text eol=lf
*.metal text eol=lf
HexFiend/* linguist-vendored
*.inc 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 $49 ;
db $4E ; WAVERACE
db $43 | $80 ;
db $43 ;
db $68 ; LOLO2
db $E0 ; YOSHI'S COOKIE
db $8B ; MYSTIC QUEST
@ -330,7 +330,7 @@ ChecksumsEnd:
PalettePerChecksum:
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
palette_index 0, 0 ; Default Palette
palette_index 4, 0 ; ALLEY WAY
@ -374,7 +374,7 @@ ENDM
palette_index 45, 0 ; STAR WARS-NOA
palette_index 36, 0 ;
palette_index 38, 0 ; WAVERACE
palette_index 26, 0 ;
palette_index 26, $80 ;
palette_index 42, 0 ; LOLO2
palette_index 30, 0 ; YOSHI'S COOKIE
palette_index 41, 0 ; MYSTIC QUEST
@ -475,7 +475,7 @@ ENDM
palette_comb 17, 4, 13
raw_palette_comb 28 * 4 - 1, 0 * 4, 14 * 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 4, 23, 28
palette_comb 17, 22, 2
@ -918,7 +918,10 @@ EmulateDMG:
call GetPaletteIndex
bit 7, a
call nz, LoadDMGTilemap
and $7F
res 7, a
ld b, a
add b
add b
ld b, a
ldh a, [InputPalette]
and a
@ -978,7 +981,7 @@ GetPaletteIndex:
; We might have a match, Do duplicate/4th letter check
ld a, l
sub FirstChecksumWithDuplicate - TitleChecksums
sub FirstChecksumWithDuplicate - TitleChecksums + 1
jr c, .match ; Does not have a duplicate, must be a match!
; Has a duplicate; check 4th letter
push hl

View File

@ -1,15 +1,25 @@
#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 (strong) IBOutlet NSView *graphicsTab;
@property (strong) IBOutlet NSView *emulationTab;
@property (strong) IBOutlet NSView *audioTab;
@property (strong) IBOutlet NSView *controlsTab;
@property (nonatomic, strong) IBOutlet NSWindow *preferencesWindow;
@property (nonatomic, strong) IBOutlet NSView *graphicsTab;
@property (nonatomic, strong) IBOutlet NSView *emulationTab;
@property (nonatomic, strong) IBOutlet NSView *audioTab;
@property (nonatomic, strong) IBOutlet NSView *controlsTab;
@property (nonatomic, strong) IBOutlet NSView *updatesTab;
- (IBAction)showPreferences: (id) sender;
- (IBAction)toggleDeveloperMode:(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

View File

@ -4,11 +4,33 @@
#include <Core/gb.h>
#import <Carbon/Carbon.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
{
NSWindow *preferences_window;
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
@ -44,6 +66,8 @@
@"GBCGBModel": @(GB_MODEL_CGB_E),
@"GBSGBModel": @(GB_MODEL_SGB2),
@"GBRumbleMode": @(GB_RUMBLE_CARTRIDGE_ONLY),
@"GBVolume": @(1.0),
}];
[JOYController startOnRunLoop:[NSRunLoop currentRunLoop] withOptions:@{
@ -54,6 +78,16 @@
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) {
[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
@ -81,10 +115,29 @@
if ([anItem action] == @selector(toggleDeveloperMode:)) {
[(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]];
}
if (anItem == self.linkCableMenuItem) {
return [[NSDocumentController sharedDocumentController] documents].count > 1;
}
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
{
NSArray *objects;
@ -92,21 +145,305 @@
[[NSBundle mainBundle] loadNibNamed:@"Preferences" owner:self topLevelObjects:&objects];
NSToolbarItem *first_toolbar_item = [_preferencesWindow.toolbar.items firstObject];
_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];
[_preferencesWindow center];
#ifndef UPDATE_SUPPORT
[_preferencesWindow.toolbar removeItemAtIndex:4];
#endif
}
[_preferencesWindow makeKeyAndOrderFront:self];
}
- (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];
return YES;
return true;
}
- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification
{
[[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

Binary file not shown.

View File

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

View File

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

View File

@ -28,6 +28,7 @@ enum model {
NSMutableAttributedString *pending_console_output;
NSRecursiveLock *console_output_lock;
NSTimer *console_output_timer;
NSTimer *hex_timer;
bool fullScreen;
bool in_sync_input;
@ -47,7 +48,7 @@ enum model {
bool oamUpdating;
NSMutableData *currentPrinterImageData;
enum {GBAccessoryNone, GBAccessoryPrinter, GBAccessoryWorkboy} accessory;
enum {GBAccessoryNone, GBAccessoryPrinter, GBAccessoryWorkboy, GBAccessoryLinkCable} accessory;
bool rom_warning_issued;
@ -66,6 +67,12 @@ enum model {
size_t audioBufferNeeded;
bool borderModeChanged;
/* Link cable*/
Document *master;
Document *slave;
signed linkOffset;
bool linkCableBit;
}
@property GBAudioClient *audioClient;
@ -81,6 +88,10 @@ enum model {
- (void) gotNewSample:(GB_sample_t *)sample;
- (void) rumbleChanged:(double)amp;
- (void) loadBootROM:(GB_boot_rom_t)type;
- (void)linkCableBitStart:(bool)bit;
- (bool)linkCableBitEnd;
- (void)infraredStateChanged:(bool)state;
@end
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];
}
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
{
GB_gameboy_t gb;
@ -206,8 +237,13 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
case MODEL_CGB:
return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"];
case MODEL_SGB:
return (GB_model_t)[[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"];
case MODEL_SGB: {
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:
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_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_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"]);
[self updatePalette];
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_highpass_filter_mode(&gb, (GB_highpass_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"]);
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_set_rumble_callback(&gb, rumbleCallback);
GB_set_infrared_callback(&gb, infraredStateChanged);
[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 zoom:nil];
}
self.osdView.usesSGBScale = GB_get_screen_width(&gb) == 256;
}
- (void) vblank
{
if (_gbsVisualizer) {
dispatch_async(dispatch_get_main_queue(), ^{
[_gbsVisualizer setNeedsDisplay:YES];
});
}
[self.view flip];
if (borderModeChanged) {
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);
if (self.vramWindow.isVisible) {
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];
});
}
if (self.view.isRewinding) {
rewind = true;
[self.osdView displayText:@"Rewinding..."];
}
}
- (void)gotNewSample:(GB_sample_t *)sample
{
if (_gbsVisualizer) {
[_gbsVisualizer addSample:sample];
}
[audioLock lock];
if (self.audioClient.isPlaying) {
if (audioBufferPosition == audioBufferSize) {
@ -321,6 +371,11 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
}
audioBuffer = realloc(audioBuffer, sizeof(*sample) * audioBufferSize);
}
double volume = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBVolume"];
if (volume != 1) {
sample->left *= volume;
sample->right *= volume;
}
audioBuffer[audioBufferPosition++] = *sample;
}
if (audioBufferPosition == audioBufferNeeded) {
@ -335,9 +390,8 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[_view setRumble:amp];
}
- (void) run
- (void) preRun
{
running = true;
GB_set_pixels_output(&gb, self.view.pixels);
GB_set_sample_rate(&gb, 96000);
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];
}
if (stopping) {
if (stopping || GB_debugger_is_stopped(&gb)) {
memset(buffer, 0, nFrames * sizeof(*buffer));
[audioLock unlock];
return;
@ -368,39 +422,78 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"Mute"]) {
[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];
/* Clear pending alarms, don't play alarms while playing */
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) {
NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
for (NSUserNotification *notification in [center scheduledNotifications]) {
if ([notification.identifier isEqualToString:self.fileName]) {
if ([notification.identifier isEqualToString:self.fileURL.path]) {
[center removeScheduledNotification:notification];
break;
}
}
for (NSUserNotification *notification in [center deliveredNotifications]) {
if ([notification.identifier isEqualToString:self.fileName]) {
if ([notification.identifier isEqualToString:self.fileURL.path]) {
[center removeDeliveredNotification:notification];
break;
}
}
}
while (running) {
if (rewind) {
rewind = false;
GB_rewind_pop(&gb);
if (!GB_rewind_pop(&gb)) {
rewind = self.view.isRewinding;
}
static unsigned *multiplication_table_for_frequency(unsigned frequency)
{
unsigned *ret = malloc(sizeof(*ret) * 0x100);
for (unsigned i = 0; i < 0x100; i++) {
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 {
GB_run(&gb);
free(masterTable);
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];
[audioLock lock];
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 = nil;
self.view.mouseHidingEnabled = NO;
GB_save_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]);
GB_save_cheats(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"cht"] UTF8String]);
GB_save_battery(&gb, [[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"sav"].path UTF8String]);
GB_save_cheats(&gb, [[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"cht"].path UTF8String]);
unsigned time_to_alarm = GB_time_to_alarm(&gb);
if (time_to_alarm) {
[NSUserNotificationCenter defaultUserNotificationCenter].delegate = (id)[NSApp delegate];
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];
friendlyName = [regex stringByReplacingMatchesInString:friendlyName options:0 range:NSMakeRange(0, [friendlyName length]) withTemplate:@""];
friendlyName = [friendlyName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
notification.title = [NSString stringWithFormat:@"%@ Played an 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.soundName = NSUserNotificationDefaultSoundName;
[[NSUserNotificationCenter defaultUserNotificationCenter] scheduleNotification:notification];
[[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBNotificationsUsed"];
}
[_view setRumble:0];
stopping = false;
}
- (void) start
{
self.gbsPlayPauseButton.state = true;
self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSWindowStyleMaskFullScreen) != 0;
if (master) {
[master start];
return;
}
if (running) return;
self.view.mouseHidingEnabled = (self.mainWindow.styleMask & NSFullScreenWindowMask) != 0;
[[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start];
}
- (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;
GB_debugger_set_disabled(&gb, true);
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];
[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
{
if (master) {
[master togglePause:sender];
return;
}
if (running) {
[self stop];
}
@ -575,10 +691,14 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
window_frame.size.height);
[self.mainWindow setFrame:window_frame display:YES];
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];
if (@available(macOS 11.0, *)) {
self.memoryWindow.toolbarStyle = NSWindowToolbarStyleExpanded;
@ -602,6 +722,16 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
name:@"GBColorCorrectionChanged"
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
selector:@selector(updateFrameBlendingMode)
name:@"GBFrameBlendingModeChanged"
@ -627,6 +757,12 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
name:@"GBRewindLengthChanged"
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateRTCMode)
name:@"GBRTCModeChanged"
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(dmgModelChanged)
name:@"GBDMGModelChanged"
@ -654,10 +790,17 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[self initCommon];
self.view.gb = &gb;
self.view.osdView = _osdView;
[self.view screenSizeChanged];
[self loadROM];
[self reset:nil];
if ([self loadROM]) {
_mainWindow.alphaValue = 0; // Hack hack ugly hack
dispatch_async(dispatch_get_main_queue(), ^{
[self close];
});
}
else {
[self reset:nil];
}
}
- (void) initMemoryView
@ -723,36 +866,131 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
return YES;
}
- (void) loadROM
- (IBAction)changeGBSTrack:(id)sender
{
NSString *rom_warnings = [self captureOutputForBlock:^{
GB_debugger_clear_symbols(&gb);
if ([[self.fileType pathExtension] isEqualToString:@"isx"]) {
GB_load_isx(&gb, [self.fileName UTF8String]);
GB_load_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"ram"] UTF8String]);
if (!running) {
[self start];
}
[self performAtomicBlock:^{
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 {
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];
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;
[GBWarningPopover popoverWithContents:rom_warnings onWindow:self.mainWindow];
}
return ret;
}
- (void)close
{
[[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.width forKey:@"LastWindowWidth"];
[[NSUserDefaults standardUserDefaults] setInteger:self.mainWindow.frame.size.height forKey:@"LastWindowHeight"];
[self disconnectLinkCable];
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.consoleWindow close];
[self.memoryWindow close];
[self.vramWindow close];
[self.printerFeedWindow close];
[self.cheatsWindow close];
[super close];
}
@ -760,9 +998,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
{
[self log:"^C\n"];
GB_debugger_break(&gb);
if (!running) {
[self start];
}
[self start];
[self.consoleWindow makeKeyAndOrderFront:nil];
[self.consoleInput becomeFirstResponder];
}
@ -774,6 +1010,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
}
else {
[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"];
}
@ -781,10 +1020,13 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)anItem
{
if ([anItem action] == @selector(mute:)) {
[(NSMenuItem*)anItem setState:!self.audioClient.isPlaying];
[(NSMenuItem *)anItem setState:!self.audioClient.isPlaying];
}
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);
}
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:)) {
[(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:)) {
[(NSMenuItem*)anItem setState:GB_cheats_enabled(&gb)];
}
@ -1000,6 +1246,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (char *) getDebuggerInput
{
[audioLock lock];
[audioLock signal];
[audioLock unlock];
[self updateSideView];
[self log:">"];
in_sync_input = true;
@ -1039,28 +1288,47 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
{
bool __block success = false;
[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) {
[GBWarningPopover popoverWithContents:@"Failed to write save state." onWindow:self.mainWindow];
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
{
bool __block success = false;
NSString *error =
[self captureOutputForBlock:^{
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();
int ret = [self loadStateFile:[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"s%ld", (long)[sender tag]]].path.UTF8String noErrorOnNotFound:true];
if (ret == ENOENT) {
[self loadStateFile:[[self.fileURL URLByDeletingPathExtension] URLByAppendingPathExtension:[NSString stringWithFormat:@"sn%ld", (long)[sender tag]]].path.UTF8String noErrorOnNotFound:false];
}
}
@ -1090,6 +1358,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
{
while (!GB_is_inited(&gb));
bool was_running = running && !GB_debugger_is_stopped(&gb);
if (master) {
was_running |= master->running;
}
if (was_running) {
[self stop];
}
@ -1347,7 +1618,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (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 {
if (!cameraSession) {
if (@available(macOS 10.14, *)) {
@ -1584,7 +1855,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
switch (columnIndex) {
case 0:
return [Document imageFromData:[NSData dataWithBytesNoCopy:oamInfo[row].image
length:64 * 4
length:64 * 4 * 2
freeWhenDone:NO]
width:8
height:oamHeight
@ -1681,14 +1952,14 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
NSSavePanel * savePanel = [NSSavePanel savePanel];
[savePanel setAllowedFileTypes:@[@"png"]];
[savePanel beginSheetModalForWindow:self.printerFeedWindow completionHandler:^(NSInteger result) {
if (result == NSFileHandlingPanelOKButton) {
if (result == NSModalResponseOK) {
[savePanel orderOut:self];
CGImageRef cgRef = [self.feedImageView.image CGImageForProposedRect:NULL
context:nil
hints:nil];
NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef];
[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];
[self.printerFeedWindow setIsVisible:NO];
}
@ -1700,6 +1971,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (IBAction)disconnectAllAccessories:(id)sender
{
[self disconnectLinkCable];
[self performAtomicBlock:^{
accessory = GBAccessoryNone;
GB_disconnect_serial(&gb);
@ -1708,6 +1980,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (IBAction)connectPrinter:(id)sender
{
[self disconnectLinkCable];
[self performAtomicBlock:^{
accessory = GBAccessoryPrinter;
GB_connect_printer(&gb, printImage);
@ -1716,6 +1989,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
- (IBAction)connectWorkboy:(id)sender
{
[self disconnectLinkCable];
[self performAtomicBlock:^{
accessory = GBAccessoryWorkboy;
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
{
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
{
modelsChanging = true;
@ -1832,4 +2127,83 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
{
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

View File

@ -25,6 +25,7 @@
<outlet property="memoryBankItem" destination="bWC-FW-IYP" id="Lf2-dh-z32"/>
<outlet property="memoryView" destination="8hr-8o-3rN" id="fF0-rh-8ND"/>
<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="printerFeedWindow" destination="NdE-0B-WCf" id="yVK-cS-NOJ"/>
<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">
<rect key="frame" x="0.0" y="0.0" width="160" height="144"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<connections>
<outlet property="document" destination="-2" id="Fvh-rD-z4r"/>
</connections>
</view>
</subviews>
</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>
</view>
<connections>
@ -115,7 +123,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<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"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<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"/>
@ -152,7 +160,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<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"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<autoresizingMask key="autoresizingMask"/>
<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">
<rect key="frame" x="0.0" y="0.0" width="329" height="47"/>
@ -186,7 +194,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<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"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<autoresizingMask key="autoresizingMask"/>
<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">
<rect key="frame" x="0.0" y="0.0" width="329" height="328"/>

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,6 @@ typedef enum {
@interface GBMemoryByteArray : HFByteArray
- (instancetype) initWithDocument:(Document *)document;
@property uint16_t selectedBank;
@property GB_memory_mode_t mode;
@property (nonatomic) uint16_t selectedBank;
@property (nonatomic) GB_memory_mode_t mode;
@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"
@interface GBOpenGLView : NSOpenGLView
@property GBGLShader *shader;
@property (nonatomic) GBGLShader *shader;
@end

View File

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

View File

@ -19,6 +19,7 @@
NSPopUpButton *_colorPalettePopupButton;
NSPopUpButton *_displayBorderPopupButton;
NSPopUpButton *_rewindPopupButton;
NSPopUpButton *_rtcPopupButton;
NSButton *_aspectRatioCheckbox;
NSButton *_analogControlsCheckbox;
NSEventModifierFlags previousModifiers;
@ -26,6 +27,11 @@
NSPopUpButton *_dmgPopupButton, *_sgbPopupButton, *_cgbPopupButton;
NSPopUpButton *_preferredJoypadButton;
NSPopUpButton *_rumbleModePopupButton;
NSSlider *_temperatureSlider;
NSSlider *_interferenceSlider;
NSSlider *_volumeSlider;
NSButton *_autoUpdatesCheckbox;
NSButton *_OSDCheckbox;
}
+ (NSArray *)filterList
@ -91,11 +97,45 @@
[_colorCorrectionPopupButton selectItemAtIndex:mode];
}
- (NSPopUpButton *)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
{
_frameBlendingModePopupButton = frameBlendingModePopupButton;
@ -156,6 +196,18 @@
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
{
_highpassFilterPopupButton = highpassFilterPopupButton;
@ -284,6 +336,26 @@
[[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
{
[[NSUserDefaults standardUserDefaults] setObject:@([sender indexOfSelectedItem])
@ -320,6 +392,20 @@
[[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
{
[self.configureJoypadButton setEnabled:NO];
@ -392,30 +478,37 @@
static const unsigned gb_to_joykit[] = {
[GBRight]=JOYButtonUsageDPadRight,
[GBLeft]=JOYButtonUsageDPadLeft,
[GBUp]=JOYButtonUsageDPadUp,
[GBDown]=JOYButtonUsageDPadDown,
[GBA]=JOYButtonUsageA,
[GBB]=JOYButtonUsageB,
[GBSelect]=JOYButtonUsageSelect,
[GBStart]=JOYButtonUsageStart,
[GBTurbo]=JOYButtonUsageL1,
[GBRewind]=JOYButtonUsageL2,
[GBUnderclock]=JOYButtonUsageR1,
[GBRight] = JOYButtonUsageDPadRight,
[GBLeft] = JOYButtonUsageDPadLeft,
[GBUp] = JOYButtonUsageDPadUp,
[GBDown] = JOYButtonUsageDPadDown,
[GBA] = JOYButtonUsageA,
[GBB] = JOYButtonUsageB,
[GBSelect] = JOYButtonUsageSelect,
[GBStart] = JOYButtonUsageStart,
[GBTurbo] = JOYButtonUsageL1,
[GBRewind] = JOYButtonUsageL2,
[GBUnderclock] = JOYButtonUsageR1,
};
if (joystick_configuration_state == GBUnderclock) {
mapping[@"AnalogUnderclock"] = nil;
double max = 0;
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);
break;
}
}
}
if (joystick_configuration_state == GBTurbo) {
mapping[@"AnalogTurbo"] = nil;
double max = 0;
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);
}
}
@ -644,4 +737,33 @@
}
[[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

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>
@interface GBTerminalTextFieldCell : NSTextFieldCell
@property GB_gameboy_t *gb;
@property (nonatomic) GB_gameboy_t *gb;
@end

View File

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

View File

@ -5,6 +5,7 @@
#import "GBViewMetal.h"
#import "GBButtons.h"
#import "NSString+StringForKey.h"
#import "Document.h"
#define JOYSTICK_HIGH 0x4000
#define JOYSTICK_LOW 0x3800
@ -116,6 +117,7 @@ static const uint8_t workboy_vk_to_key[] = {
NSEventModifierFlags previousModifiers;
JOYController *lastController;
GB_frame_blending_mode_t _frameBlendingMode;
bool _turbo;
}
+ (instancetype)alloc
@ -141,6 +143,8 @@ static const uint8_t workboy_vk_to_key[] = {
- (void) _init
{
[self registerForDraggedTypes:[NSArray arrayWithObjects: NSFilenamesPboardType, nil]];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(ratioKeepingChanged) name:@"GBAspectChanged" object:nil];
tracking_area = [ [NSTrackingArea alloc] initWithRect:(NSRect){}
options:NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect
@ -256,21 +260,45 @@ static const uint8_t workboy_vk_to_key[] = {
- (void) flip
{
if (analogClockMultiplierValid && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAnalogControls"]) {
clockMultiplier = 1.0;
GB_set_clock_multiplier(_gb, analogClockMultiplier);
if (self.document.partner) {
GB_set_clock_multiplier(self.document.partner.gb, analogClockMultiplier);
}
if (analogClockMultiplier == 1.0) {
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 {
if (underclockKeyDown && clockMultiplier > 0.5) {
clockMultiplier -= 1.0/16;
GB_set_clock_multiplier(_gb, clockMultiplier);
if (self.document.partner) {
GB_set_clock_multiplier(self.document.partner.gb, clockMultiplier);
}
}
if (!underclockKeyDown && clockMultiplier < 1.0) {
clockMultiplier += 1.0/16;
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;
}
@ -299,6 +327,9 @@ static const uint8_t workboy_vk_to_key[] = {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
unsigned player_count = GB_get_player_count(_gb);
if (self.document.partner) {
player_count = 2;
}
for (unsigned player = 0; player < player_count; player++) {
for (GBButton button = 0; button < GBButtonCount; button++) {
NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)];
@ -308,13 +339,22 @@ static const uint8_t workboy_vk_to_key[] = {
handled = true;
switch (button) {
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;
break;
case GBRewind:
self.isRewinding = true;
GB_set_turbo_mode(_gb, false, false);
if (!self.document.partner) {
self.isRewinding = true;
GB_set_turbo_mode(_gb, false, false);
_turbo = false;
}
break;
case GBUnderclock:
@ -323,7 +363,17 @@ static const uint8_t workboy_vk_to_key[] = {
break;
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;
}
}
@ -351,6 +401,9 @@ static const uint8_t workboy_vk_to_key[] = {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
unsigned player_count = GB_get_player_count(_gb);
if (self.document.partner) {
player_count = 2;
}
for (unsigned player = 0; player < player_count; player++) {
for (GBButton button = 0; button < GBButtonCount; button++) {
NSNumber *key = [defaults valueForKey:button_to_preference_name(button, player)];
@ -360,7 +413,13 @@ static const uint8_t workboy_vk_to_key[] = {
handled = true;
switch (button) {
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;
break;
@ -374,7 +433,17 @@ static const uint8_t workboy_vk_to_key[] = {
break;
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;
}
}
@ -401,13 +470,13 @@ static const uint8_t workboy_vk_to_key[] = {
if ((axis.usage == JOYAxisUsageR1 && !mapping) ||
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;
}
else if ((axis.usage == JOYAxisUsageL1 && !mapping) ||
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;
}
}
@ -415,13 +484,11 @@ static const uint8_t workboy_vk_to_key[] = {
- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button
{
if (![self.window isMainWindow]) return;
if (controller != lastController) {
[self setRumble:0];
lastController = controller;
}
unsigned player_count = GB_get_player_count(_gb);
if (self.document.partner) {
player_count = 2;
}
IOPMAssertionID assertionID;
IOPMAssertionDeclareUserActivity(CFSTR(""), kIOPMUserActiveLocal, &assertionID);
@ -435,7 +502,7 @@ static const uint8_t workboy_vk_to_key[] = {
continue;
}
dispatch_async(dispatch_get_main_queue(), ^{
[controller setPlayerLEDs:1 << player];
[controller setPlayerLEDs:[controller LEDMaskForPlayer:player]];
});
NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID];
if (!mapping) {
@ -447,33 +514,68 @@ static const uint8_t workboy_vk_to_key[] = {
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) {
case JOYButtonUsageNone: break;
case JOYButtonUsageA: GB_set_key_state_for_player(_gb, GB_KEY_A, player, button.isPressed); break;
case JOYButtonUsageB: GB_set_key_state_for_player(_gb, GB_KEY_B, 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(effectiveGB, GB_KEY_B, effectivePlayer, button.isPressed); break;
case JOYButtonUsageC: break;
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 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 JOYButtonUsageL2:
case JOYButtonUsageZ: {
self.isRewinding = 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;
}
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 JOYButtonUsageDPadLeft: GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, button.isPressed); break;
case JOYButtonUsageDPadRight: GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, button.isPressed); break;
case JOYButtonUsageDPadUp: GB_set_key_state_for_player(_gb, GB_KEY_UP, player, button.isPressed); break;
case JOYButtonUsageDPadDown: GB_set_key_state_for_player(_gb, GB_KEY_DOWN, 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(effectiveGB, GB_KEY_RIGHT, effectivePlayer, 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(effectiveGB, GB_KEY_DOWN, effectivePlayer, button.isPressed); break;
default:
break;
@ -550,4 +652,29 @@ static const uint8_t workboy_vk_to_key[] = {
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

View File

@ -123,7 +123,7 @@ static const vector_float2 rect[] =
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};
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 (!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>
<key>CFBundleTypeExtensions</key>
<array>
<string>gbc</string>
<string>isx</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>ColorCartridge</string>
@ -68,6 +68,26 @@
<key>NSDocumentClass</key>
<string>Document</string>
</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>
<key>CFBundleExecutable</key>
<string>SameBoy</string>
@ -92,7 +112,7 @@
<key>LSMinimumSystemVersion</key>
<string>10.9</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2015-2020 Lior Halphon</string>
<string>Copyright © 2015-2021 Lior Halphon</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
@ -156,6 +176,25 @@
</array>
</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>
<key>NSCameraUsageDescription</key>
<string>SameBoy needs to access your camera to emulate the Game Boy Camera</string>

View File

@ -30,7 +30,7 @@
<h1>SameBoy</h1>
<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
of this software and associated documentation files (the "Software"), to deal

View File

@ -12,7 +12,11 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<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"/>
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
@ -373,6 +377,17 @@
<action selector="disconnectAllAccessories:" target="-1" id="5hY-9U-nRn"/>
</connections>
</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">
<modifierMask key="keyEquivalentModifierMask"/>
<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="graphicsTab" destination="sRK-wO-K6R" id="pfP-Di-i0Q"/>
<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>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
@ -49,17 +52,25 @@
<action selector="switchPreferencesTab:" target="-2" id="Tio-D7-PaA"/>
</connections>
</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>
<defaultToolbarItems>
<toolbarItem reference="zdp-Z7-yZM"/>
<toolbarItem reference="fCd-4a-SKR"/>
<toolbarItem reference="EMp-g1-eKu"/>
<toolbarItem reference="uNZ-j1-Dfx"/>
<toolbarItem reference="lMV-68-ntz"/>
</defaultToolbarItems>
</toolbar>
<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="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="bootROMsFolderItem" destination="Dzv-Gc-zoL" id="yhV-ZI-avD"/>
<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="graphicsFilterPopupButton" destination="6pP-kK-EEC" id="LS7-HY-kHC"/>
<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="preferredJoypadButton" destination="0Az-0R-oNw" id="7JM-tw-BhK"/>
<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="sgbPopupButton" destination="dza-T7-RkX" id="B0o-Nb-pIH"/>
<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>
<point key="canvasLocation" x="183" y="354"/>
</window>
<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"/>
<subviews>
<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"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Graphics filter:" id="pXg-WY-8Q5">
<font key="font" metaFont="system"/>
@ -96,7 +111,7 @@
</textFieldCell>
</textField>
<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"/>
<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"/>
@ -133,7 +148,7 @@
</connections>
</popUpButton>
<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"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Color correction:" id="5Si-hz-EK3">
<font key="font" metaFont="system"/>
@ -142,7 +157,7 @@
</textFieldCell>
</textField>
<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"/>
<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"/>
@ -156,6 +171,7 @@
<menuItem title="Emulate hardware" id="XBL-hS-7ke"/>
<menuItem title="Preserve brightness" id="tlx-WB-Bkw"/>
<menuItem title="Reduce contrast" id="wuO-Xv-0mQ"/>
<menuItem title="Harsh reality (low contrast)" id="98I-hs-vP2"/>
</items>
</menu>
</popUpButtonCell>
@ -164,7 +180,7 @@
</connections>
</popUpButton>
<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"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Frame blending" id="UCa-EO-tzh">
<font key="font" metaFont="system"/>
@ -173,7 +189,7 @@
</textFieldCell>
</textField>
<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"/>
<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"/>
@ -193,7 +209,7 @@
</connections>
</popUpButton>
<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"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Color palette for monochrome models:" id="LAN-8Y-T7H">
<font key="font" metaFont="system"/>
@ -202,7 +218,7 @@
</textFieldCell>
</textField>
<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"/>
<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"/>
@ -223,7 +239,7 @@
</connections>
</popUpButton>
<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"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Display border:" id="HZd-qi-yyk">
<font key="font" metaFont="system"/>
@ -232,7 +248,7 @@
</textFieldCell>
</textField>
<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"/>
<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"/>
@ -251,8 +267,25 @@
<action selector="displayBorderChanged:" target="QvC-M9-y7g" id="GoA-BU-v3h"/>
</connections>
</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">
<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"/>
<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"/>
@ -262,39 +295,26 @@
<action selector="changeAspectRatio:" target="QvC-M9-y7g" id="mQG-Ib-1jN"/>
</connections>
</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>
<point key="canvasLocation" x="-176" y="667.5"/>
<point key="canvasLocation" x="-176" y="677.5"/>
</customView>
<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"/>
<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">
<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"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Rewinding duration:" id="JaO-5h-ugl">
<font key="font" metaFont="system"/>
@ -302,8 +322,17 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</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">
<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"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Boot ROMs location:" id="nj0-Cb-gEA">
<font key="font" metaFont="system"/>
@ -311,8 +340,116 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</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">
<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"/>
<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"/>
@ -335,104 +472,56 @@
</menu>
</popUpButtonCell>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Wg8-hJ-df9">
<rect key="frame" x="18" y="157" width="256" height="17"/>
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7fg-Ww-JjR">
<rect key="frame" x="30" y="250" width="234" height="26"/>
<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="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">
<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" autoenablesItems="NO" id="R0h-k6-Q5l">
<menu key="menu" id="lbS-Lw-kQX">
<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"/>
<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="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="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"/>
<action selector="rewindLengthChanged:" target="QvC-M9-y7g" id="5NQ-1T-RNc"/>
</connections>
</popUpButton>
</subviews>
<point key="canvasLocation" x="-176" y="848"/>
<point key="canvasLocation" x="-501" y="667.5"/>
</customView>
<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"/>
<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">
<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"/>
<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"/>
@ -452,7 +541,7 @@
</connections>
</popUpButton>
<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"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="High-pass filter:" id="YLF-RL-b2D">
<font key="font" metaFont="system"/>
@ -460,15 +549,32 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</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>
<point key="canvasLocation" x="-176" y="890"/>
<point key="canvasLocation" x="-825" y="446.5"/>
</customView>
<customView id="8TU-6J-NCg">
<rect key="frame" x="0.0" y="0.0" width="292" height="467"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<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"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Control settings for" id="YqW-Ds-VIC">
<font key="font" metaFont="system"/>
@ -486,14 +592,14 @@
</textFieldCell>
</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">
<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"/>
<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"/>
<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">
<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"/>
<size key="intercellSpacing" width="3" height="2"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
@ -512,7 +618,7 @@
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
</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">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
@ -553,7 +659,7 @@
</textFieldCell>
</textField>
<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"/>
<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"/>
@ -573,7 +679,7 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</box>
<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"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title=":" id="VhO-3T-glt">
<font key="font" metaFont="system"/>
@ -582,7 +688,7 @@
</textFieldCell>
</textField>
<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"/>
<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"/>
@ -601,7 +707,7 @@
</connections>
</popUpButton>
<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"/>
<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"/>
@ -630,9 +736,9 @@
</connections>
</button>
<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"/>
<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"/>
<font key="font" metaFont="system"/>
</buttonCell>
@ -641,9 +747,9 @@
</connections>
</button>
<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"/>
<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"/>
<font key="font" metaFont="system"/>
</buttonCell>
@ -654,8 +760,42 @@
</subviews>
<point key="canvasLocation" x="-159" y="1161.5"/>
</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>
<resources>
<image name="AppIcon" width="128" height="128"/>
<image name="CPU" width="32" height="32"/>
<image name="Display" 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
};
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 struct
@ -64,10 +71,10 @@ typedef struct
uint8_t square_sweep_countdown; // In 128Hz
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;
bool sweep_enabled;
bool sweep_decreasing;
bool unshifted_sweep;
bool enable_zombie_calculate_stepping;
struct {
uint16_t pulse_length; // Reloaded from NRX1 (xorred), in 256Hz DIV ticks
@ -105,8 +112,9 @@ typedef struct
uint16_t lfsr;
bool narrow;
uint16_t sample_countdown; // in APU ticks (Reloaded from sample_length)
uint16_t sample_length; // From NR43, in APU ticks
uint8_t counter_countdown; // Counts from 0-7 to 0 to tick counter (Scaled from 512KHz to 2MHz)
uint8_t __padding;
uint16_t counter; // A bit from this 14-bit register ticks LFSR
bool length_enabled; // NR44
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;
bool current_lfsr_sample;
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;
typedef enum {
@ -149,17 +165,22 @@ typedef struct {
GB_sample_callback_t sample_callback;
bool rate_set_in_clocks;
double interference_volume;
double interference_highpass;
} GB_apu_output_t;
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_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);
#ifdef GB_INTERNAL
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);
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_secondary_event(GB_gameboy_t *gb);
void GB_apu_init(GB_gameboy_t *gb);
void GB_apu_run(GB_gameboy_t *gb);
void GB_apu_update_cycles_per_sample(GB_gameboy_t *gb);

View File

@ -1,26 +1,26 @@
#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. */
static uint8_t generate_noise(uint8_t x, uint8_t y)
{
signed value = (x + y * 128 + noise_seed);
uint8_t *data = (uint8_t *) &value;
unsigned hash = 0;
uint32_t value = (x * 151 + y * 149) ^ noise_seed;
uint32_t hash = 0;
while ((signed *) data != &value + 1) {
hash ^= (*data << 8);
if (hash & 0x8000) {
hash ^= 0x8a00;
hash ^= *data;
}
data++;
while (value) {
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)

View File

@ -69,7 +69,7 @@ void GB_add_cheat(GB_gameboy_t *gb, const char *description, uint16_t address, u
cheat->enabled = enabled;
strncpy(cheat->description, description, sizeof(cheat->description));
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_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;
}
else {
gb->cheats = realloc(gb->cheats, gb->cheat_count * sizeof(*cheat));
gb->cheats = realloc(gb->cheats, gb->cheat_count * sizeof(gb->cheats[0]));
}
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)];
for (unsigned i = 0; i < (*hash)->size; i++) {
if ((*hash)->cheats[i] == cheat) {
(*hash)->cheats[i] = (*hash)->cheats[(*hash)->size--];
(*hash)->cheats[i] = (*hash)->cheats[--(*hash)->size];
if ((*hash)->size == 0) {
free(*hash);
*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)];
for (unsigned i = 0; i < (*hash)->size; i++) {
if ((*hash)->cheats[i] == cheat) {
(*hash)->cheats[i] = (*hash)->cheats[(*hash)->size--];
(*hash)->cheats[i] = (*hash)->cheats[--(*hash)->size];
if ((*hash)->size == 0) {
free(*hash);
*hash = NULL;
@ -222,7 +222,7 @@ void GB_update_cheat(GB_gameboy_t *gb, const GB_cheat_t *_cheat, const char *des
}
else {
(*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;
}
}

View File

@ -131,30 +131,25 @@ static const char *value_to_string(GB_gameboy_t *gb, uint16_t value, bool prefer
symbol = NULL;
}
/* Avoid overflow */
if (symbol && strlen(symbol->name) >= 240) {
symbol = NULL;
}
if (!symbol) {
sprintf(output, "$%04x", value);
snprintf(output, sizeof(output), "$%04x", value);
}
else if (symbol->addr == value) {
if (prefer_name) {
sprintf(output, "%s ($%04x)", symbol->name, value);
snprintf(output, sizeof(output), "%s ($%04x)", symbol->name, value);
}
else {
sprintf(output, "$%04x (%s)", value, symbol->name);
snprintf(output, sizeof(output), "$%04x (%s)", value, symbol->name);
}
}
else {
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 {
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;
@ -171,30 +166,25 @@ static const char *debugger_value_to_string(GB_gameboy_t *gb, value_t value, boo
symbol = NULL;
}
/* Avoid overflow */
if (symbol && strlen(symbol->name) >= 240) {
symbol = NULL;
}
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) {
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 {
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 {
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 {
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;
@ -473,7 +463,7 @@ value_t debugger_evaluate(GB_gameboy_t *gb, const char *string,
size_t length, bool *error,
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;
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;
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 {
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");
}
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");
}
@ -1613,8 +1606,12 @@ static bool ticks(GB_gameboy_t *gb, char *arguments, char *modifiers, const debu
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->absolute_debugger_ticks = 0;
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)" : "");
if (channel == GB_SQUARE_1) {
GB_log(gb, " Frequency sweep %s and %s (next in %u APU ticks)\n",
gb->apu.sweep_enabled? "active" : "inactive",
gb->apu.sweep_decreasing? "decreasing" : "increasing",
gb->apu.square_sweep_calculate_countdown);
GB_log(gb, " Frequency sweep %s and %s\n",
((gb->io_registers[GB_IO_NR10] & 0x7) && (gb->io_registers[GB_IO_NR10] & 0x70))? "active" : "inactive",
(gb->io_registers[GB_IO_NR10] & 0x8) ? "decreasing" : "increasing");
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) {
@ -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, " 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.sample_length * 4 + 3,
gb->apu.noise_channel.sample_countdown);
gb->apu.noise_channel.counter,
gb->apu.noise_channel.counter_countdown);
GB_log(gb, " %u 256 Hz ticks till next volume %screase (out of %u)\n",
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;
}
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);
#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"},
{"step", 1, step, "Run the next instruction, stepping into function calls"},
{"finish", 1, finish, "Run until the current function returns"},
{"undo", 1, undo, "Reverts the last command"},
{"backtrace", 2, backtrace, "Displays the current call stack"},
{"bt", 2, }, /* Alias */
{"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);
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 {
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)
{
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;
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) {
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;
should_delete_state = false;
}

View File

@ -2,6 +2,7 @@
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <math.h>
#include "gb.h"
/* 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)
{
if (!flip_x) {
UNROLL
for (unsigned i = 8; i--;) {
unrolled for (unsigned i = 8; i--;) {
fifo->fifo[fifo->write_end] = (GB_fifo_item_t) {
(lower >> 7) | ((upper >> 7) << 1),
palette,
@ -43,8 +43,7 @@ static void fifo_push_bg_row(GB_fifo_t *fifo, uint8_t lower, uint8_t upper, uint
}
}
else {
UNROLL
for (unsigned i = 8; i--;) {
unrolled for (unsigned i = 8; i--;) {
fifo->fifo[fifo->write_end] = (GB_fifo_item_t) {
(lower & 1) | ((upper & 1) << 1),
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;
UNROLL
for (unsigned i = 8; i--;) {
unrolled for (unsigned i = 8; i--;) {
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)];
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;
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) */
if (!GB_is_sgb(gb)) {
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);
uint32_t border_colors[16 * 4];
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,
0x30DA, 0x69AD, 0x2B57, 0x2B5D, 0x632C,
0x1050, 0x3C84, 0x0E07, 0x0E18, 0x2964,
};
unsigned index = gb->rom? gb->rom[0x14e] % 5 : 0;
gb->borrowed_border.palette[0] = colors[index];
gb->borrowed_border.palette[10] = colors[5 + index];
gb->borrowed_border.palette[14] = colors[10 + index];
gb->borrowed_border.palette[0] = LE16(colors[index]);
gb->borrowed_border.palette[10] = LE16(colors[5 + index]);
gb->borrowed_border.palette[14] = LE16(colors[10 + index]);
}
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++) {
@ -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) {
continue;
}
uint16_t tile = gb->borrowed_border.map[tile_x + tile_y * 32];
uint8_t flip_x = (tile & 0x4000)? 0x7 : 0;
uint8_t flip_y = (tile & 0x8000)? 0x7 : 0;
uint16_t tile = LE16(gb->borrowed_border.map[tile_x + tile_y * 32]);
uint8_t flip_x = (tile & 0x4000)? 0:7;
uint8_t flip_y = (tile & 0x8000)? 7:0;
uint8_t palette = (tile >> 10) & 3;
for (unsigned y = 0; y < 8; y++) {
unsigned base = (tile & 0xFF) * 32 + (y ^ flip_y) * 2;
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;
if (color == 0) {
*output = border_colors[0];
@ -208,6 +211,26 @@ static void display_vblank(GB_gameboy_t *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)
{
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)
{
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)
{
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)
@ -240,13 +263,12 @@ uint32_t GB_convert_rgb15(GB_gameboy_t *gb, uint16_t color, bool for_border)
g = scale_channel(g);
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 {
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;
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);
@ -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_g = new_g * 7 / 8 + (r + b) / 16;
new_b = new_b * 7 / 8 + (r + g ) / 16;
new_r = new_r * (224 - 32) / 255 + 32;
new_g = new_g * (220 - 36) / 255 + 36;
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) {
uint8_t old_max = MAX(r, MAX(g, 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);
}
@ -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:
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)
{
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) {
if (gb->icd_pixel_callback) {
icd_pixel = pixel;
//gb->icd_pixel_callback(gb, pixel);
}
}
else if (gb->cgb_palettes_ppu_blocked) {
@ -621,6 +690,10 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb)
break;
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;
uint16_t tile_address = 0;
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) {
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];
if (gb->vram_ppu_blocked) {
gb->current_tile_data[0] = 0xFF;
if (gb->vram_ppu_blocked) {
gb->data_for_sel_glitch = 0xFF;
}
}
}
gb->fetcher_state++;
break;
case GB_FETCHER_GET_TILE_DATA_HIGH: {
/* 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
more logical DMG version. */
/* Todo: Verified for DMG (Tested: SGB2), CGB timing is wrong. */
bool use_glitched = false;
if (gb->tile_sel_glitch) {
gb->current_tile_data[1] = data_for_tile_sel_glitch(gb, &use_glitched);
}
uint16_t tile_address = 0;
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;
}
gb->last_tile_data_address = tile_address + ((y & 7) ^ y_flip) * 2 + 1;
gb->current_tile_data[1] =
gb->vram[gb->last_tile_data_address];
if (gb->vram_ppu_blocked) {
gb->current_tile_data[1] = 0xFF;
if (!use_glitched) {
gb->current_tile_data[1] =
gb->vram[gb->last_tile_data_address];
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) {
@ -771,14 +866,14 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles)
GB_STATE(gb, display, 21);
GB_STATE(gb, display, 22);
GB_STATE(gb, display, 23);
// GB_STATE(gb, display, 24);
GB_STATE(gb, display, 24);
GB_STATE(gb, display, 25);
GB_STATE(gb, display, 26);
GB_STATE(gb, display, 27);
GB_STATE(gb, display, 28);
GB_STATE(gb, display, 29);
GB_STATE(gb, display, 30);
// GB_STATE(gb, display, 31);
GB_STATE(gb, display, 31);
GB_STATE(gb, display, 32);
GB_STATE(gb, display, 33);
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 */
gb->current_line = 0;
gb->window_y = -1;
/* Todo: verify timings */
if (gb->io_registers[GB_IO_WY] == 0) {
gb->wy_triggered = true;
}
else {
gb->wy_triggered = false;
}
gb->wy_triggered = false;
gb->ly_for_comparison = 0;
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 */
gb->window_y = -1;
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->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_SLEEP(gb, display, 32, 2);
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->oam_fifo);
@ -1112,7 +1201,8 @@ void GB_display_run(GB_gameboy_t *gb, uint8_t cycles)
object->flags & 0x80,
gb->object_priority == GB_OBJECT_PRIORITY_INDEX? gb->visible_objs[gb->n_visible_objs - 1] : 0,
object->flags & 0x20);
gb->data_for_sel_glitch = gb->vram_ppu_blocked? 0xFF : gb->vram[line_address + 1];
gb->n_visible_objs--;
}
@ -1128,6 +1218,14 @@ abort_fetching_object:
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) {
/* Oh no! The PPU and LCD desynced! Fill the rest of the line whith white. */
uint32_t *dest = NULL;
@ -1191,8 +1289,19 @@ abort_fetching_object:
if (gb->hdma_on_hblank) {
gb->hdma_starting = true;
}
GB_SLEEP(gb, display, 11, LINE_LENGTH - gb->cycles_for_line);
gb->mode_for_interrupt = 2;
GB_SLEEP(gb, display, 11, LINE_LENGTH - gb->cycles_for_line - 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
gb->current_lcd_line++;
@ -1210,20 +1319,22 @@ abort_fetching_object:
gb->io_registers[GB_IO_LY] = gb->current_line;
gb->ly_for_comparison = -1;
GB_SLEEP(gb, display, 26, 2);
if (gb->current_line == LINES) {
gb->mode_for_interrupt = 2;
if (gb->current_line == LINES && !gb->stat_interrupt_line && (gb->io_registers[GB_IO_STAT] & 0x20)) {
gb->io_registers[GB_IO_IF] |= 2;
}
GB_STAT_update(gb);
GB_SLEEP(gb, display, 12, 2);
gb->ly_for_comparison = gb->current_line;
GB_STAT_update(gb);
GB_SLEEP(gb, display, 24, 1);
if (gb->current_line == LINES) {
/* Entering VBlank state triggers the OAM interrupt */
gb->io_registers[GB_IO_STAT] &= ~3;
gb->io_registers[GB_IO_STAT] |= 1;
gb->io_registers[GB_IO_IF] |= 1;
gb->mode_for_interrupt = 2;
GB_STAT_update(gb);
if (!gb->stat_interrupt_line && (gb->io_registers[GB_IO_STAT] & 0x20)) {
gb->io_registers[GB_IO_IF] |= 2;
}
gb->mode_for_interrupt = 1;
GB_STAT_update(gb);
@ -1255,8 +1366,7 @@ abort_fetching_object:
}
}
GB_STAT_update(gb);
GB_SLEEP(gb, display, 13, LINE_LENGTH - 4);
GB_SLEEP(gb, display, 13, LINE_LENGTH - 5);
}
/* TODO: Verified on SGB2 and CGB-E. Actual interrupt timings not tested. */
@ -1285,14 +1395,7 @@ abort_fetching_object:
gb->current_line = 0;
/* Todo: verify timings */
if ((gb->io_registers[GB_IO_LCDC] & 0x20) &&
(gb->io_registers[GB_IO_WY] == 0)) {
gb->wy_triggered = true;
}
else {
gb->wy_triggered = false;
}
gb->wy_triggered = false;
// TODO: not the correct timing
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++) {
UNROLL
for (unsigned x = 0; x < 8; x++) {
unrolled for (unsigned x = 0; x < 8; x++) {
uint8_t color = (((gb->vram[vram_address ] >> ((~x)&7)) & 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_PRESERVE_BRIGHTNESS,
GB_COLOR_CORRECTION_REDUCE_CONTRAST,
GB_COLOR_CORRECTION_LOW_CONTRAST,
} 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);
@ -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);
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_light_temperature(GB_gameboy_t *gb, double temperature);
bool GB_is_odd_frame(GB_gameboy_t *gb);
#endif /* display_h */

View File

@ -19,12 +19,6 @@
#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)
{
char *string = NULL;
@ -115,21 +109,18 @@ static void load_default_border(GB_gameboy_t *gb)
#define LOAD_BORDER() do { \
memcpy(gb->borrowed_border.map, tilemap, sizeof(tilemap));\
memcpy(gb->borrowed_border.palette, palette, sizeof(palette));\
\
/* 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);\
}\
}\
}\
memcpy(gb->borrowed_border.tiles, tiles, sizeof(tiles));\
} 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) {
#include "graphics/agb_border.inc"
LOAD_BORDER();
@ -202,6 +193,9 @@ void GB_free(GB_gameboy_t *gb)
if (gb->nontrivial_jump_state) {
free(gb->nontrivial_jump_state);
}
if (gb->undo_state) {
free(gb->undo_state);
}
#ifndef GB_DISABLE_DEBUGGER
GB_debugger_clear_symbols(gb);
#endif
@ -302,9 +296,183 @@ int GB_load_rom(GB_gameboy_t *gb, const char *path)
fread(gb->rom, 1, gb->rom_size, f);
fclose(f);
GB_configure_cart(gb);
gb->tried_loading_sgb_border = false;
gb->has_sgb_border = false;
load_default_border(gb);
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)
{
FILE *f = fopen(path, "rb");
@ -534,6 +702,9 @@ error:
gb->rom_size = old_size;
}
fclose(f);
gb->tried_loading_sgb_border = false;
gb->has_sgb_border = false;
load_default_border(gb);
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);
memcpy(gb->rom, buffer, size);
GB_configure_cart(gb);
gb->tried_loading_sgb_border = false;
gb->has_sgb_border = false;
load_default_border(gb);
}
typedef struct {
@ -570,12 +744,13 @@ typedef struct {
} GB_vba_rtc_time_t;
typedef struct __attribute__((packed)) {
uint32_t magic;
uint16_t version;
uint8_t mr4;
uint8_t reserved;
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;
uint8_t rtc_data[4];
} GB_tpp1_rtc_save_t;
typedef union {
struct __attribute__((packed)) {
@ -594,14 +769,33 @@ typedef union {
} vba64;
} 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)
{
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->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;
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)
{
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 (size < GB_save_battery_size(gb)) return EIO;
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;
#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.high = gb->rtc_latched.high;
#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
rtc_save.vba64.last_rtc_second = gb->last_rtc_second;
rtc_save.vba64.last_rtc_second = time(NULL);
#endif
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)
{
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 */
FILE *f = fopen(path, "wb");
if (!f) {
@ -677,7 +879,16 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path)
fclose(f);
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
GB_huc3_rtc_time_t rtc_save = {
__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.high = gb->rtc_latched.high;
#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
rtc_save.vba64.last_rtc_second = gb->last_rtc_second;
rtc_save.vba64.last_rtc_second = time(NULL);
#endif
if (fwrite(&rtc_save.vba64, 1, sizeof(rtc_save.vba64), f) != sizeof(rtc_save.vba64)) {
fclose(f);
@ -732,6 +943,14 @@ int GB_save_battery(GB_gameboy_t *gb, const char *path)
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)
{
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;
}
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) {
GB_huc3_rtc_time_t 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;
}
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) {
GB_huc3_rtc_time_t rtc_save;
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_cpu_run(gb);
if (gb->vblank_just_occured) {
GB_rtc_run(gb);
GB_debugger_handle_async_commands(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)
{
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)
@ -1110,6 +1348,7 @@ bool GB_serial_get_data_bit(GB_gameboy_t *gb)
}
return gb->io_registers[GB_IO_SB] & 0x80;
}
void GB_serial_set_data_bit(GB_gameboy_t *gb, bool data)
{
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->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_reset(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)
{
if (gb->model & GB_MODEL_PAL_BIT) {
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;
return GB_get_unmultiplied_clock_rate(gb) * 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)
{
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;
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_MGB_FAMILY 0x100
#define GB_MODEL_CGB_FAMILY 0x200
#define GB_MODEL_PAL_BIT 0x1000
#define GB_MODEL_NO_SFC_BIT 0x2000
#define GB_MODEL_PAL_BIT 0x40
#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
#if __clang__
#define UNROLL _Pragma("unroll")
#define unrolled _Pragma("unroll")
#elif __GNUC__ >= 8
#define UNROLL _Pragma("GCC unroll 8")
#define unrolled _Pragma("GCC unroll 8")
#else
#define UNROLL
#define unrolled
#endif
#endif
@ -53,6 +56,25 @@
#error Unable to detect endianess
#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)
#define __builtin_bswap16(x) ({ typeof(x) _x = (x); _x >> 8 | _x << 8; })
#endif
@ -76,9 +98,24 @@ typedef union {
uint8_t days;
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];
} 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 {
// GB_MODEL_DMG_0 = 0x000,
// GB_MODEL_DMG_A = 0x001,
@ -125,8 +162,6 @@ typedef enum {
GB_BORDER_ALWAYS,
} GB_border_mode_t;
#define GB_MAX_IR_QUEUE 256
enum {
/* Joypad and Serial */
GB_IO_JOYP = 0x00, // Joypad (R/W)
@ -251,13 +286,17 @@ typedef enum {
GB_BOOT_ROM_AGB,
} GB_boot_rom_t;
typedef enum {
GB_RTC_MODE_SYNC_TO_HOST,
GB_RTC_MODE_ACCURATE,
} GB_rtc_mode_t;
#ifdef GB_INTERNAL
#define LCDC_PERIOD 70224
#define CPU_FREQUENCY 0x400000
#define SGB_NTSC_FREQUENCY (21477272 / 5)
#define SGB_PAL_FREQUENCY (21281370 / 5)
#define DIV_CYCLES (0x100)
#define INTERNAL_DIV_CYCLES (0x40000)
#if !defined(MIN)
#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 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 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_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);
@ -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_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_watchpoint_s;
@ -305,6 +339,29 @@ typedef struct {
uint8_t write_end;
} 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.
This allows adding data to the end of the section without worrying about future compatibility.
Some other changes might be "safe" as well.
@ -374,6 +431,9 @@ struct GB_gameboy_internal_s {
uint8_t extra_oam[0xff00 - 0xfea0];
uint32_t ram_size; // Different between CGB and DMG
GB_workboy_t workboy;
int32_t ir_sensor;
bool effective_ir_input;
);
/* DMA and HDMA */
@ -437,10 +497,10 @@ struct GB_gameboy_internal_s {
uint16_t mbc_rom0_bank; /* For some MBC1 wirings. */
bool camera_registers_mapped;
uint8_t camera_registers[0x36];
bool rumble_state;
uint8_t rumble_strength;
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_access_index;
uint16_t huc3_minutes, huc3_days;
@ -449,6 +509,9 @@ struct GB_gameboy_internal_s {
uint8_t huc3_read;
uint8_t huc3_access_flags;
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;
uint8_t double_speed_alignment;
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 */
@ -479,7 +545,9 @@ struct GB_gameboy_internal_s {
GB_SECTION(rtc,
GB_rtc_time_t rtc_real, rtc_latched;
uint64_t last_rtc_second;
bool rtc_latch;
GB_PADDING(bool, rtc_latch);
uint32_t rtc_cycles;
uint8_t tpp1_mr4;
);
/* Video Display */
@ -548,6 +616,7 @@ struct GB_gameboy_internal_s {
uint16_t last_tile_data_address;
uint16_t last_tile_index_address;
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 */
@ -576,6 +645,7 @@ struct GB_gameboy_internal_s {
uint32_t sprite_palettes_rgb[0x20];
const GB_palette_t *dmg_palette;
GB_color_correction_mode_t color_correction_mode;
double light_temperature;
bool keys[4][GB_KEY_MAX];
GB_border_mode_t border_mode;
GB_sgb_border_t borrowed_border;
@ -585,6 +655,7 @@ struct GB_gameboy_internal_s {
/* Timing */
uint64_t last_sync;
uint64_t cycles_since_last_sync; // In 8MHz units
GB_rtc_mode_t rtc_mode;
/* Audio */
GB_apu_output_t apu_output;
@ -612,12 +683,6 @@ struct GB_gameboy_internal_s {
GB_print_image_callback_t printer_callback;
GB_workboy_set_time_callback workboy_set_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 ***/
volatile bool debug_stopped, debug_disable;
@ -654,7 +719,12 @@ struct GB_gameboy_internal_s {
/* Ticks command */
uint64_t debugger_ticks;
uint64_t absolute_debugger_ticks;
/* Undo */
uint8_t *undo_state;
const char *undo_label;
/* Rewind */
#define GB_REWIND_FRAMES_PER_KEY 255
size_t rewind_buffer_length;
@ -692,6 +762,9 @@ struct GB_gameboy_internal_s {
/* Temporary state */
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_set_user_data(GB_gameboy_t *gb, void *data);
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);
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);
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_to_buffer(GB_gameboy_t *gb, uint8_t *buffer, size_t size);
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_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_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 */
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 */
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_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);
#ifdef GB_INTERNAL
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);
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);
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 */

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_ram_bank = gb->huc3.ram_bank;
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)
{
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) {
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) {
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->mbc_type == GB_MBC2) {
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 {
static const unsigned ram_sizes[256] = {0, 0x800, 0x2000, 0x8000, 0x20000, 0x10000};
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);
}
/* 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);
}

View File

@ -12,6 +12,7 @@ typedef struct {
GB_MBC5,
GB_HUC1,
GB_HUC3,
GB_TPP1,
} mbc_type;
enum {
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;
}
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;
}
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);
}
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);
}
/*
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)
{
if (GB_is_cgb(gb)) return;
if (address >= 0xFE00 && address < 0xFF00) {
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],
gb->oam[gb->accessed_oam_row - 8],
gb->oam[gb->accessed_oam_row - 4]);
gb->oam[gb->accessed_oam_row + 1] = bitwise_glitch(gb->oam[gb->accessed_oam_row + 1],
gb->oam[gb->accessed_oam_row - 7],
gb->oam[gb->accessed_oam_row - 3]);
uint16_t *base = (uint16_t *)(gb->oam + gb->accessed_oam_row);
base[0] = bitwise_glitch(base[0],
base[-4],
base[-2]);
for (unsigned i = 2; i < 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)
{
if (GB_is_cgb(gb)) return;
if (address >= 0xFE00 && address < 0xFF00) {
if (gb->accessed_oam_row != 0xff && gb->accessed_oam_row >= 8) {
gb->oam[gb->accessed_oam_row - 8] =
gb->oam[gb->accessed_oam_row] = bitwise_glitch_read(gb->oam[gb->accessed_oam_row],
gb->oam[gb->accessed_oam_row - 8],
gb->oam[gb->accessed_oam_row - 4]);
gb->oam[gb->accessed_oam_row - 7] =
gb->oam[gb->accessed_oam_row + 1] = bitwise_glitch_read(gb->oam[gb->accessed_oam_row + 1],
gb->oam[gb->accessed_oam_row - 7],
gb->oam[gb->accessed_oam_row - 3]);
for (unsigned i = 2; i < 8; i++) {
if ((gb->accessed_oam_row & 0x18) == 0x10) {
oam_bug_secondary_read_corruption(gb);
}
else if ((gb->accessed_oam_row & 0x18) == 0x00) {
/* Everything in this specific case is *extremely* revision and instance specific. */
if (gb->accessed_oam_row == 0x40) {
oam_bug_quaternary_read_corruption(gb,
((gb->model & ~GB_MODEL_NO_SFC_BIT) == GB_MODEL_SGB2)?
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];
}
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)
{
@ -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);
}
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)
{
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
return 1;
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:
GB_log(gb, "Unsupported HuC-3 mode %x read: %04x\n", gb->huc3_mode, addr);
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_type != GB_HUC1 &&
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) {
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 &&
gb->mbc3_rtc_mapped && gb->mbc_ram_bank <= 4) {
gb->mbc3_rtc_mapped) {
/* RTC read */
gb->rtc_latched.high |= ~0xC1; /* Not all bytes in RTC high are used. */
return gb->rtc_latched.data[gb->mbc_ram_bank];
if (gb->mbc_ram_bank <= 4) {
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) {
@ -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;
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;
}
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_is_cgb(gb)) {
if (addr < 0xFEA0) {
uint16_t *oam = (uint16_t *)gb->oam;
if (gb->accessed_oam_row == 0) {
gb->oam[(addr & 0xf8)] =
gb->oam[0] = bitwise_glitch_read(gb->oam[0],
gb->oam[(addr & 0xf8)],
gb->oam[(addr & 0xfe)]);
gb->oam[(addr & 0xf8) + 1] =
gb->oam[1] = bitwise_glitch_read(gb->oam[1],
gb->oam[(addr & 0xf8) + 1],
gb->oam[(addr & 0xfe) | 1]);
oam[(addr & 0xf8) >> 1] =
oam[0] = bitwise_glitch_read(oam[0],
oam[(addr & 0xf8) >> 1],
oam[(addr & 0xff) >> 1]);
for (unsigned i = 2; i < 8; i++) {
gb->oam[i] = gb->oam[(addr & 0xf8) + i];
}
}
else if (gb->accessed_oam_row == 0xa0) {
gb->oam[0x9e] = bitwise_glitch_read(gb->oam[0x9c],
gb->oam[0x9e],
gb->oam[(addr & 0xf8) | 6]);
gb->oam[0x9f] = bitwise_glitch_read(gb->oam[0x9d],
gb->oam[0x9f],
gb->oam[(addr & 0xf8) | 7]);
uint8_t target = (addr & 7) | 0x98;
uint16_t a = oam[0x9c >> 1],
b = oam[target >> 1],
c = oam[(addr & 0xf8) >> 1];
switch (addr & 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++) {
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) {
return 0;
}
if (addr < 0xFF80) {
@ -428,9 +613,12 @@ static uint8_t read_high_memory(GB_gameboy_t *gb, uint16_t addr)
case GB_IO_RP: {
if (!gb->cgb_mode) return 0xFF;
/* You will read your own IR LED if it's on. */
uint8_t ret = (gb->io_registers[GB_IO_RP] & 0xC1) | 0x3C;
if ((gb->io_registers[GB_IO_RP] & 0xC0) == 0xC0 && effective_ir_input(gb)) {
ret |= 2;
uint8_t ret = (gb->io_registers[GB_IO_RP] & 0xC1) | 0x2E;
if (gb->model != GB_MODEL_CGB_E) {
ret |= 0x10;
}
if (((gb->io_registers[GB_IO_RP] & 0xC0) == 0xC0 && gb->effective_ir_input) && gb->model != GB_MODEL_AGB) {
ret &= ~2;
}
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;
break;
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));
}
gb->rtc_latch = value & 1;
memcpy(&gb->rtc_latched, &gb->rtc_real, sizeof(gb->rtc_real));
break;
}
break;
case GB_MBC5:
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 0x3000: gb->mbc5.rom_bank_high = value; break;
case 0x4000: case 0x5000:
if (gb->cartridge_type->has_rumble) {
if (!!(value & 8) != gb->rumble_state) {
gb->rumble_state = !gb->rumble_state;
if (!!(value & 8) != !!gb->rumble_strength) {
gb->rumble_strength = gb->rumble_strength? 0 : 3;
}
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;
}
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);
}
@ -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
return true;
case 0xE: { // IR mode
bool old_input = effective_ir_input(gb);
gb->cart_ir = value & 1;
bool new_input = effective_ir_input(gb);
if (new_input != old_input) {
if (gb->cart_ir != (value & 1)) {
gb->cart_ir = value & 1;
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;
}
@ -684,24 +913,38 @@ static void write_mbc_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
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)
&& gb->cartridge_type->mbc_type != GB_HUC1) return;
if (gb->cartridge_type->mbc_type == GB_HUC1 && gb->huc1.ir_mode) {
bool old_input = effective_ir_input(gb);
gb->cart_ir = value & 1;
bool new_input = effective_ir_input(gb);
if (new_input != old_input) {
if (gb->cart_ir != (value & 1)) {
gb->cart_ir = value & 1;
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;
}
if (gb->cartridge_type->has_rtc && gb->mbc3_rtc_mapped && gb->mbc_ram_bank <= 4) {
gb->rtc_latched.data[gb->mbc_ram_bank] = gb->rtc_real.data[gb->mbc_ram_bank] = value;
if (gb->cartridge_type->has_rtc && gb->mbc3_rtc_mapped) {
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;
}
@ -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;
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;
}
@ -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)) {
gb->display_cycles = 0;
gb->display_state = 0;
gb->double_speed_alignment = 0;
if (GB_is_sgb(gb)) {
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_timing_sync(gb);
}
else if (!(value & 0x80) && (gb->io_registers[GB_IO_LCDC] & 0x80)) {
/* Sync after turning off LCD */
gb->double_speed_alignment = 0;
GB_timing_sync(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)) {
return;
}
bool old_input = effective_ir_input(gb);
gb->io_registers[GB_IO_RP] = value;
bool new_input = effective_ir_input(gb);
if (new_input != old_input) {
if ((gb->io_registers[GB_IO_RP] ^ value) & 1) {
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;
}

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_hdma_run(GB_gameboy_t *gb);
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 /* memory_h */

View File

@ -11,7 +11,6 @@
static void handle_command(GB_gameboy_t *gb)
{
switch (gb->printer.command_id) {
case GB_PRINTER_INIT_COMMAND:
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;
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)
{
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 |= bit_received;
gb->printer.bits_received++;
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.byte_being_received = 0;
}

View File

@ -48,8 +48,7 @@ typedef struct
uint8_t image[160 * 200];
uint16_t image_offset;
/* TODO: Delete me. */
uint64_t padding;
uint64_t idle_time;
uint8_t compression_run_lenth;
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)
{
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_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) {
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 {
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++] =
state_compress(gb->rewind_sequences[gb->rewind_pos].key_state, save_state, save_size);
free(save_state);
@ -158,7 +158,7 @@ bool GB_rewind_pop(GB_gameboy_t *gb)
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) {
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);

View File

@ -15,7 +15,8 @@ void GB_handle_rumble(GB_gameboy_t *gb)
if (gb->rumble_mode == GB_RUMBLE_DISABLED) {
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) {
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;
@ -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 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));
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;
unsigned ch4_divisor = (gb->io_registers[GB_IO_NR43] & 0x07) << 1;
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 = MAX(ch4_rumble, 0.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);
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);

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_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 */

View File

@ -7,8 +7,6 @@
#define M_PI 3.14159265358979323846
#endif
#define INTRO_ANIMATION_LENGTH 200
enum {
PAL01 = 0x00,
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[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++) {
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++) {
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;
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) {
gb->sgb->effective_palettes[0] = 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[2] = 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[0] = LE16(built_in_palettes[palette_assignments[i].palette_index * 4 - 4]);
gb->sgb->effective_palettes[1] = LE16(built_in_palettes[palette_assignments[i].palette_index * 4 + 1 - 4]);
gb->sgb->effective_palettes[2] = LE16(built_in_palettes[palette_assignments[i].palette_index * 4 + 2 - 4]);
gb->sgb->effective_palettes[3] = LE16(built_in_palettes[palette_assignments[i].palette_index * 4 + 3 - 4]);
break;
}
}
@ -269,7 +267,7 @@ static void command_ready(GB_gameboy_t *gb)
#endif
uint8_t x = command->x;
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 */
break;
}
@ -283,7 +281,7 @@ static void command_ready(GB_gameboy_t *gb)
x++;
y = 0;
if (x == 20) {
x = 0;
break;
}
}
}
@ -293,7 +291,7 @@ static void command_ready(GB_gameboy_t *gb)
y++;
x = 0;
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) {
fade_blue = 80 - gb->sgb->intro_animation;
}
else if (gb->sgb->intro_animation > INTRO_ANIMATION_LENGTH - 32) {
fade_red = fade_blue = 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 - GB_SGB_INTRO_ANIMATION_LENGTH + 32;
}
uint32_t colors[] = {
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));
}
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 == 0) {
if (gb->sgb->transfer_dest == TRANSFER_LOW_TILES || gb->sgb->transfer_dest == TRANSFER_HIGH_TILES) {
uint8_t *base = &gb->sgb->pending_border.tiles[gb->sgb->transfer_dest == TRANSFER_HIGH_TILES ? 0x80 * 8 * 8 : 0];
for (unsigned tile = 0; tile < 0x80; tile++) {
unsigned tile_x = (tile % 10) * 16;
unsigned tile_y = (tile / 10) * 8;
for (unsigned y = 0; y < 0x8; y++) {
for (unsigned x = 0; x < 0x8; x++) {
base[tile * 8 * 8 + y * 8 + x] = gb->sgb->screen_buffer[(tile_x + x) + (tile_y + y) * 160] +
gb->sgb->screen_buffer[(tile_x + x + 8) + (tile_y + y) * 160] * 4;
}
}
}
unsigned size = 0;
uint16_t *data = NULL;
switch (gb->sgb->transfer_dest) {
case TRANSFER_LOW_TILES:
size = 0x100;
data = (uint16_t *)gb->sgb->pending_border.tiles;
break;
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;
uint16_t *data = NULL;
switch (gb->sgb->transfer_dest) {
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?
}
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++;
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
*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];
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) {
@ -683,7 +682,7 @@ void GB_sgb_render(GB_gameboy_t *gb)
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);
}
else {
@ -735,21 +734,21 @@ void GB_sgb_render(GB_gameboy_t *gb)
}
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++) {
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) {
gb->sgb->border_animation--;
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 {
gb->sgb->border_animation--;
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) {
continue;
}
uint16_t tile = gb->sgb->border.map[tile_x + tile_y * 32];
uint8_t flip_x = (tile & 0x4000)? 0x7 : 0;
uint8_t flip_y = (tile & 0x8000)? 0x7 : 0;
uint16_t tile = LE16(gb->sgb->border.map[tile_x + tile_y * 32]);
uint8_t flip_x = (tile & 0x4000)? 0:7;
uint8_t flip_y = (tile & 0x8000)? 7:0;
uint8_t palette = (tile >> 10) & 3;
for (unsigned y = 0; y < 8; y++) {
unsigned base = (tile & 0xFF) * 32 + (y ^ flip_y) * 2;
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;
if (gb->border_mode == GB_BORDER_NEVER) {
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"
#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.palette, palette, sizeof(palette));
/* 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->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);
}
}
}
#endif
memcpy(gb->sgb->border.tiles, tiles, sizeof(tiles));
if (gb->model != GB_MODEL_SGB2) {
/* Delete the "2" */
@ -825,10 +827,10 @@ void GB_sgb_load_default_data(GB_gameboy_t *gb)
/* 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]));
}
gb->sgb->effective_palettes[0] = built_in_palettes[0];
gb->sgb->effective_palettes[1] = built_in_palettes[1];
gb->sgb->effective_palettes[2] = built_in_palettes[2];
gb->sgb->effective_palettes[3] = built_in_palettes[3];
gb->sgb->effective_palettes[0] = LE16(built_in_palettes[0]);
gb->sgb->effective_palettes[1] = LE16(built_in_palettes[1]);
gb->sgb->effective_palettes[2] = LE16(built_in_palettes[2]);
gb->sgb->effective_palettes[3] = LE16(built_in_palettes[3]);
}
static double fm_synth(double phase)
@ -874,7 +876,7 @@ static void render_jingle(GB_gameboy_t *gb, size_t count)
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;
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;
}
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) {

View File

@ -6,7 +6,10 @@
typedef struct GB_sgb_s GB_sgb_t;
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 {
struct {
uint16_t map[32 * 32];
@ -17,6 +20,8 @@ typedef struct {
} GB_sgb_border_t;
#ifdef GB_INTERNAL
#define GB_SGB_INTRO_ANIMATION_LENGTH 200
struct GB_sgb_s {
uint8_t command[16 * 7];
uint16_t command_write_index;
@ -46,7 +51,8 @@ struct GB_sgb_s {
uint16_t effective_palettes[4 * 4];
uint16_t ram_palettes[4 * 512];
uint8_t attribute_map[20 * 18];
uint8_t attribute_files[0xFE0];
uint8_t attribute_files[0xFD2];
uint8_t attribute_files_padding[0xFE0 - 0xFD2];
/* Intro */
int16_t intro_animation;
@ -56,6 +62,8 @@ struct GB_sgb_s {
/* Multiplayer (cont) */
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);

View File

@ -21,16 +21,20 @@ typedef enum {
GB_CONFLICT_DMG_LCDC,
GB_CONFLICT_SGB_LCDC,
GB_CONFLICT_WX,
GB_CONFLICT_CGB_LCDC,
GB_CONFLICT_NR10,
} GB_conflict_t;
/* Todo: How does double speed mode affect these? */
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_LYC] = GB_CONFLICT_WRITE_CPU,
[GB_IO_STAT] = GB_CONFLICT_STAT_CGB,
[GB_IO_BGP] = GB_CONFLICT_PALETTE_CGB,
[GB_IO_OBP0] = 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 */
};
@ -48,7 +52,8 @@ static const GB_conflict_t dmg_conflict_map[0x80] = {
[GB_IO_OBP1] = GB_CONFLICT_PALETTE_DMG,
[GB_IO_WY] = GB_CONFLICT_READ_OLD,
[GB_IO_WX] = GB_CONFLICT_WX,
[GB_IO_NR10] = GB_CONFLICT_NR10,
/* Todo: these were not verified at all */
[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_WY] = GB_CONFLICT_READ_OLD,
[GB_IO_WX] = GB_CONFLICT_WX,
[GB_IO_NR10] = GB_CONFLICT_NR10,
/* Todo: these were not verified at all */
[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;
}
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. */
/* 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.
@ -241,6 +236,56 @@ static void cycle_write(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
gb->wx_just_changed = false;
gb->pending_cycles = 3;
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->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)
@ -287,6 +345,10 @@ static void nop(GB_gameboy_t *gb, uint8_t opcode)
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->oam_ppu_blocked = !gb->oam_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)
{
/* 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->oam_ppu_blocked = false;
gb->vram_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)
{
if (gb->io_registers[GB_IO_KEY1] & 0x1) {
flush_pending_cycles(gb);
bool needs_alignment = false;
GB_advance_cycles(gb, 0x4);
/* Make sure we keep the CPU ticks aligned correctly when returning from double speed mode */
if (gb->double_speed_alignment & 7) {
GB_advance_cycles(gb, 0x4);
needs_alignment = true;
}
gb->cgb_double_speed ^= true;
gb->io_registers[GB_IO_KEY1] = 0;
flush_pending_cycles(gb);
bool exit_by_joyp = ((gb->io_registers[GB_IO_JOYP] & 0xF) != 0xF);
bool speed_switch = (gb->io_registers[GB_IO_KEY1] & 0x1) && !exit_by_joyp;
bool immediate_exit = speed_switch || exit_by_joyp;
bool interrupt_pending = (gb->interrupt_enable & gb->io_registers[GB_IO_IF] & 0x1F);
// When entering with IF&IE, the 2nd byte of STOP is actually executed
if (!exit_by_joyp) {
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? */
gb->pc++;
if (!interrupt_pending) {
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:
@ -362,8 +440,8 @@ static void ld_rr_d16(GB_gameboy_t *gb, uint8_t opcode)
uint8_t register_id;
uint16_t value;
register_id = (opcode >> 4) + 1;
value = cycle_read_inc_oam_bug(gb, gb->pc++);
value |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8;
value = cycle_read(gb, gb->pc++);
value |= cycle_read(gb, gb->pc++) << 8;
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;
register_id = ((opcode >> 4) + 1) & 0x03;
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)
@ -449,8 +527,8 @@ static void ld_da16_sp(GB_gameboy_t *gb, uint8_t opcode)
{
/* Todo: Verify order is correct */
uint16_t addr;
addr = cycle_read_inc_oam_bug(gb, gb->pc++);
addr |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8;
addr = cycle_read(gb, gb->pc++);
addr |= cycle_read(gb, gb->pc++) << 8;
cycle_write(gb, addr, gb->registers[GB_REGISTER_SP] & 0xFF);
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;
register_id = (opcode >> 4) + 1;
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)
@ -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)
{
/* 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);
}
@ -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)
{
int8_t offset = cycle_read_inc_oam_bug(gb, gb->pc++);
int8_t offset = cycle_read(gb, gb->pc++);
if (condition_code(gb, opcode)) {
gb->pc += offset;
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)
{
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)
{
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)
@ -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)
{
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);
}
@ -945,15 +1023,15 @@ static void pop_rr(GB_gameboy_t *gb, uint8_t opcode)
{
uint8_t register_id;
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[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)
{
uint16_t addr = cycle_read_inc_oam_bug(gb, gb->pc++);
addr |= (cycle_read_inc_oam_bug(gb, gb->pc++) << 8);
uint16_t addr = cycle_read(gb, gb->pc++);
addr |= (cycle_read(gb, gb->pc++) << 8);
if (condition_code(gb, opcode)) {
cycle_no_access(gb);
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)
{
uint16_t addr = cycle_read_inc_oam_bug(gb, gb->pc);
addr |= (cycle_read_inc_oam_bug(gb, gb->pc + 1) << 8);
uint16_t addr = cycle_read(gb, gb->pc);
addr |= (cycle_read(gb, gb->pc + 1) << 8);
cycle_no_access(gb);
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)
{
uint16_t call_addr = gb->pc - 1;
uint16_t addr = cycle_read_inc_oam_bug(gb, gb->pc++);
addr |= (cycle_read_inc_oam_bug(gb, gb->pc++) << 8);
uint16_t addr = cycle_read(gb, gb->pc++);
addr |= (cycle_read(gb, gb->pc++) << 8);
if (condition_code(gb, opcode)) {
cycle_oam_bug(gb, GB_REGISTER_SP);
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)
{
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;
gb->registers[GB_REGISTER_AF] = (a + value) << 8;
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)
{
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;
carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0;
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)
{
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;
gb->registers[GB_REGISTER_AF] = ((a - value) << 8) | GB_SUBTRACT_FLAG;
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)
{
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;
carry = (gb->registers[GB_REGISTER_AF] & GB_CARRY_FLAG) != 0;
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)
{
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;
gb->registers[GB_REGISTER_AF] = ((a & value) << 8) | GB_HALF_CARRY_FLAG;
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)
{
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;
gb->registers[GB_REGISTER_AF] = (a ^ value) << 8;
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)
{
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;
gb->registers[GB_REGISTER_AF] = (a | value) << 8;
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)
{
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;
gb->registers[GB_REGISTER_AF] &= 0xFF00;
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)
{
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;
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)
{
uint16_t call_addr = gb->pc - 1;
uint16_t addr = cycle_read_inc_oam_bug(gb, gb->pc++);
addr |= (cycle_read_inc_oam_bug(gb, gb->pc++) << 8);
uint16_t addr = cycle_read(gb, gb->pc++);
addr |= (cycle_read(gb, gb->pc++) << 8);
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) & 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)
{
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);
}
static void ld_a_da8(GB_gameboy_t *gb, uint8_t opcode)
{
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;
}
@ -1191,7 +1269,7 @@ static void add_sp_r8(GB_gameboy_t *gb, uint8_t opcode)
{
int16_t offset;
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);
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)
{
uint16_t addr;
addr = cycle_read_inc_oam_bug(gb, gb->pc++);
addr |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8;
addr = cycle_read(gb, gb->pc++);
addr |= cycle_read(gb, gb->pc++) << 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;
gb->registers[GB_REGISTER_AF] &= 0xFF;
addr = cycle_read_inc_oam_bug(gb, gb->pc++);
addr |= cycle_read_inc_oam_bug(gb, gb->pc++) << 8;
addr = cycle_read(gb, gb->pc++);
addr |= cycle_read(gb, gb->pc++) << 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;
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);
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)
{
opcode = cycle_read_inc_oam_bug(gb, gb->pc++);
opcode = cycle_read(gb, gb->pc++);
switch (opcode >> 3) {
case 0:
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] = {
/* X0 X1 X2 X3 X4 X5 X6 X7 */
/* X8 X9 Xa Xb Xc Xd Xe Xf */
/* X0 X1 X2 X3 X4 X5 X6 X7 */
/* 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 */
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 */
@ -1531,15 +1609,18 @@ void GB_cpu_run(GB_gameboy_t *gb)
/* Wake up from HALT mode without calling interrupt code. */
if (gb->halted && !effective_ime && interrupt_queue) {
gb->halted = false;
gb->speed_switch_halt_countdown = 0;
}
/* Call interrupt */
else if (effective_ime && interrupt_queue) {
gb->halted = false;
gb->speed_switch_halt_countdown = 0;
uint16_t call_addr = gb->pc;
cycle_no_access(gb);
cycle_no_access(gb);
gb->last_opcode_read = cycle_read(gb, gb->pc++);
cycle_oam_bug_pc(gb);
gb->pc--;
GB_trigger_oam_bug(gb, gb->registers[GB_REGISTER_SP]); /* Todo: test T-cycle timing */
cycle_no_access(gb);
@ -1572,7 +1653,7 @@ void GB_cpu_run(GB_gameboy_t *gb)
}
/* Run mode */
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) {
gb->pc--;
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;
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--;
}
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 */
int64_t nanoseconds = get_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);
gb->last_sync += target_nanoseconds;
}
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;
}
@ -89,15 +94,32 @@ void GB_timing_sync(GB_gameboy_t *gb)
}
#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->cycles_since_input_ir_change >= gb->ir_queue[0].delay) {
gb->cycles_since_input_ir_change -= gb->ir_queue[0].delay;
gb->infrared_input = gb->ir_queue[0].state;
gb->ir_queue_length--;
memmove(&gb->ir_queue[0], &gb->ir_queue[1], sizeof(gb->ir_queue[0]) * (gb->ir_queue_length));
if (gb->model == GB_MODEL_AGB) return;
if (gb->infrared_input || gb->cart_ir || (gb->io_registers[GB_IO_RP] & 1)) {
gb->ir_sensor += cycles;
if (gb->ir_sensor > IR_MAX) {
gb->ir_sensor = IR_MAX;
}
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)
@ -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. */
value &= INTERNAL_DIV_CYCLES - 1;
uint32_t triggers = gb->div_counter & ~value;
uint16_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])) {
increase_tima(gb);
}
/* 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_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;
}
static void GB_timers_run(GB_gameboy_t *gb, uint8_t cycles)
{
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;
}
@ -171,6 +202,9 @@ main:
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) {
gb->serial_cycles += cycles;
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)
{
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
// Affected by speed boost
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
}
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;
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) {
cycles <<= 1;
}
gb->absolute_debugger_ticks += cycles;
// 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->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_run += cycles;
if (gb->rumble_state) {
gb->rumble_on_cycles++;
}
else {
gb->rumble_off_cycles++;
}
gb->rumble_on_cycles += gb->rumble_strength & 3;
gb->rumble_off_cycles += (gb->rumble_strength & 3) ^ 3;
if (!gb->stopped) { // TODO: Verify what happens in STOP mode
GB_dma_run(gb);
GB_hdma_run(gb);
}
GB_apu_run(gb);
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
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);
bool GB_timing_sync_turbo(GB_gameboy_t *gb); /* Returns true if should skip frame */
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

@ -1,4 +1,7 @@
flags += -DGB_INTERNAL -DGB_DISABLE_DEBUGGER -DGB_DISABLE_CHEATS -D_GNU_SOURCE -Wno-multichar
include gb/version.mk
export VERSION
flags += -DGB_INTERNAL -DGB_DISABLE_DEBUGGER -DGB_DISABLE_CHEATS -D_GNU_SOURCE -DGB_VERSION=\"$(VERSION)\" -Wno-multichar
options += -I../sameboy
objects += gb-apu gb-camera gb-rumble gb-display gb-gb gb-joypad gb-mbc

View File

@ -4,8 +4,6 @@
hacksByManufacturer = @{
@(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: @{
@(kHIDUsage_GD_X): @(0),
@ -13,7 +11,7 @@ hacksByManufacturer = @{
@(kHIDUsage_GD_Z): @(2),
@(kHIDUsage_GD_Rx): @(1),
@(kHIDUsage_GD_Ry): @(1),
@(kHIDUsage_GD_Rz): @(3),
@(kHIDUsage_GD_Rz): @(2),
},
JOYButtonUsageMapping: @{
@ -37,8 +35,10 @@ hacksByManufacturer = @{
JOYAxes2DUsageMapping: @{
AXES2D(1): @(JOYAxes2DUsageLeftStick),
AXES2D(4): @(JOYAxes2DUsageRightStick),
AXES2D(3): @(JOYAxes2DUsageRightStick),
},
JOYEmulateAxisButtons: @YES,
},
@(0x054C): @{ // Sony
@ -71,14 +71,49 @@ hacksByManufacturer = @{
},
JOYAxisUsageMapping: @{
AXIS(4): @(JOYAxisUsageL1),
AXIS(5): @(JOYAxisUsageR1),
AXIS(4): @(JOYAxisUsageL2),
AXIS(5): @(JOYAxisUsageR2),
},
JOYAxes2DUsageMapping: @{
AXES2D(1): @(JOYAxes2DUsageLeftStick),
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},
],
},
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
JOYAxisGroups: @{
@(kHIDUsage_GD_X): @(0),

View File

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

View File

@ -19,6 +19,8 @@
@"Analog R1",
@"Analog R2",
@"Analog R3",
@"Slider",
@"Dial",
@"Wheel",
@"Rudder",
@"Throttle",
@ -57,10 +59,23 @@
if (element.usagePage == kHIDPage_GenericDesktop) {
uint16_t usage = element.usage;
_usage = JOYAxisUsageGeneric0 + usage - kHIDUsage_GD_X + 1;
switch (element.usage) {
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;
return self;
@ -87,4 +102,28 @@
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

View File

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

View File

@ -1,5 +1,6 @@
#import "JOYButton.h"
#import "JOYElement.h"
#import <AppKit/AppKit.h>
@implementation JOYButton
{
@ -80,6 +81,12 @@
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;
}
@ -98,5 +105,4 @@
}
return false;
}
@end

View File

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

View File

@ -7,6 +7,9 @@
#import "JOYEmulatedButton.h"
#include <IOKit/hid/IOHIDLib.h>
#include <AppKit/AppKit.h>
extern NSTextField *globalDebugField;
#define PWM_RESOLUTION 16
static NSString const *JOYAxisGroups = @"JOYAxisGroups";
@ -26,6 +29,8 @@ static NSString const *JOYSwapZRz = @"JOYSwapZRz";
static NSString const *JOYActivationReport = @"JOYActivationReport";
static NSString const *JOYIgnoredReports = @"JOYIgnoredReports";
static NSString const *JOYIsDualShock3 = @"JOYIsDualShock3";
static NSString const *JOYIsSony = @"JOYIsSony";
static NSString const *JOYEmulateAxisButtons = @"JOYEmulateAxisButtons";
static NSMutableDictionary<id, JOYController *> *controllers; // Physical controllers
static NSMutableArray<JOYController *> *exposedControllers; // Logical controllers
@ -35,7 +40,6 @@ static NSDictionary *hacksByManufacturer = nil;
static NSMutableSet<id<JOYListener>> *listeners = nil;
static bool axesEmulateButtons = false;
static bool axes2DEmulateButtons = false;
static bool hatsEmulateButtons = false;
@ -125,9 +129,37 @@ typedef struct __attribute__((packed)) {
uint8_t padding3[13];
} 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 {
JOYSwitchPacket switchPacket;
JOYDualShock3Output ds3Output;
JOYDualSenseOutput dualsenseOutput;
} JOYVendorSpecificOutput;
@implementation JOYController
@ -151,6 +183,10 @@ typedef union {
NSString *_serialSuffix;
bool _isSwitch; // Does this controller use the Switch protocol?
bool _isDualShock3; // Does this controller use DS3 outputs?
bool _isSony; // Is this a DS4 or newer Sony controller?
bool _isDualSense;
bool _isUSBDualSense;
JOYVendorSpecificOutput _lastVendorSpecificOutput;
volatile double _rumbleAmplitude;
bool _physicallyConnected;
@ -166,6 +202,7 @@ typedef union {
double _sentRumbleAmp;
unsigned _rumbleCounter;
bool _deviceCantSendReports;
dispatch_queue_t _rumbleQueue;
}
- (instancetype)initWithDevice:(IOHIDDeviceRef) device hacks:(NSDictionary *)hacks
@ -200,29 +237,40 @@ typedef union {
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: {
JOYButton *button = [[JOYButton alloc] initWithElement: 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) {
button.usage = [replacementUsage unsignedIntValue];
}
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) {
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) {
case kHIDUsage_GD_X:
case kHIDUsage_GD_Y:
@ -284,30 +332,26 @@ typedef union {
}*/
break;
}
single:
case kHIDUsage_GD_Slider:
case kHIDUsage_GD_Dial:
case kHIDUsage_GD_Wheel: {
case kHIDUsage_GD_Wheel:
{ single: {
JOYAxis *axis = [[JOYAxis alloc] initWithElement: 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) {
axis.usage = [replacementUsage unsignedIntValue];
}
if (axesEmulateButtons && axis.usage >= JOYAxisUsageL1 && axis.usage <= JOYAxisUsageR3) {
if ([_hacks[JOYEmulateAxisButtons] boolValue]) {
_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;
}
}}
case kHIDUsage_GD_DPadUp:
case kHIDUsage_GD_DPadDown:
case kHIDUsage_GD_DPadRight:
@ -364,6 +408,7 @@ typedef union {
_hacks = hacks;
_isSwitch = [_hacks[JOYIsSwitch] boolValue];
_isDualShock3 = [_hacks[JOYIsDualShock3] boolValue];
_isSony = [_hacks[JOYIsSony] boolValue];
NSDictionary *customReports = hacks[JOYCustomReports];
_lastReport = [NSMutableData dataWithLength:MAX(
@ -416,8 +461,8 @@ typedef union {
id previous = nil;
NSSet *ignoredReports = nil;
if (hacks[ignoredReports]) {
ignoredReports = [NSSet setWithArray:hacks[ignoredReports]];
if (hacks[JOYIgnoredReports]) {
ignoredReports = [NSSet setWithArray:hacks[JOYIgnoredReports]];
}
for (id _element in array) {
@ -490,8 +535,36 @@ typedef union {
{.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;
}
@ -564,7 +637,9 @@ typedef union {
}
}
}
[self updateRumble];
dispatch_async(_rumbleQueue, ^{
[self updateRumble];
});
}
- (void)elementChanged:(IOHIDElementRef)element
@ -699,7 +774,9 @@ typedef union {
_physicallyConnected = false;
[exposedControllers removeObject:self];
[self setRumbleAmplitude:0];
[self updateRumble];
dispatch_sync(_rumbleQueue, ^{
[self updateRumble];
});
_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
{
mask &= 0xF;
if (mask == _playerLEDs) {
return;
}
@ -728,14 +888,18 @@ typedef union {
_lastVendorSpecificOutput.switchPacket.sequence++;
_lastVendorSpecificOutput.switchPacket.sequence &= 0xF;
_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)]];
}
else if (_isDualShock3) {
_lastVendorSpecificOutput.ds3Output.reportID = 1;
_lastVendorSpecificOutput.ds3Output.ledsEnabled = mask << 1;
_lastVendorSpecificOutput.ds3Output.ledsEnabled = (mask & 0x1F);
[self sendReport:[NSData dataWithBytes:&_lastVendorSpecificOutput.ds3Output length:sizeof(_lastVendorSpecificOutput.ds3Output)]];
}
else if (_isDualSense) {
_lastVendorSpecificOutput.dualsenseOutput.playerLEDs = mask & 0x1F;
[self sendDualSenseOutput];
}
}
- (void)updateRumble
@ -743,7 +907,7 @@ typedef union {
if (!self.connected) {
return;
}
if (!_rumbleElement && !_isSwitch && !_isDualShock3) {
if (!_rumbleElement && !_isSwitch && !_isDualShock3 && !_isDualSense) {
return;
}
if (_rumbleElement.max == 1 && _rumbleElement.min == 0) {
@ -802,6 +966,11 @@ typedef union {
_lastVendorSpecificOutput.ds3Output.rumbleLeftStrength = _lastVendorSpecificOutput.ds3Output.rumbleRightStrength = round(_rumbleAmplitude * 0xff);
[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 {
[_rumbleElement setValue:_rumbleAmplitude * (_rumbleElement.max - _rumbleElement.min) + _rumbleElement.min];
}
@ -871,7 +1040,6 @@ typedef union {
+ (void)startOnRunLoop:(NSRunLoop *)runloop withOptions: (NSDictionary *)options
{
axesEmulateButtons = [options[JOYAxesEmulateButtonsKey] boolValue];
axes2DEmulateButtons = [options[JOYAxes2DEmulateButtonsKey] boolValue];
hatsEmulateButtons = [options[JOYHatsEmulateButtonsKey] boolValue];

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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
of this software and associated documentation files (the "Software"), to deal

View File

@ -30,6 +30,14 @@ else
DEFAULT := sdl
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)
ifeq ($(MAKECMDGOALS),)
@ -98,8 +106,8 @@ OPEN_DIALOG = OpenDialog/cocoa.m
endif
# These must come before the -Wno- flags
WARNINGS += -Werror -Wall -Wno-unknown-warning -Wno-unknown-warning-option
WARNINGS += -Wno-nonnull -Wno-unused-result -Wno-strict-aliasing -Wno-multichar -Wno-int-in-bool-context
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 -Wno-format-truncation
# 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)
@ -113,7 +121,10 @@ endif
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))
SDL_CFLAGS := $(shell sdl2-config --cflags)
@ -130,7 +141,7 @@ GL_LDFLAGS := $(shell $(PKG_CONFIG) --libs gl || echo -lGL)
endif
ifeq ($(PLATFORM),windows32)
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
GL_LDFLAGS := -lopengl32
else
@ -421,6 +432,49 @@ $(BIN)/BootROMs/%.bin: BootROMs/%.asm $(OBJ)/BootROMs/SameBoyLogo.pb12
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:
rm -rf build

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