diff --git a/README.md b/README.md index e104f65f..64dbf837 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ Links ----- - [Official git repository](https://github.com/bsnes-emu/bsnes) + - [Official Discord](https://discord.gg/B27hf27ZVf) Nightly Builds -------------- diff --git a/bsnes/gb/.gitattributes b/bsnes/gb/.gitattributes index 427cb285..2149ea1d 100644 --- a/bsnes/gb/.gitattributes +++ b/bsnes/gb/.gitattributes @@ -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 diff --git a/bsnes/gb/BESS.md b/bsnes/gb/BESS.md new file mode 100644 index 00000000..e040d903 --- /dev/null +++ b/bsnes/gb/BESS.md @@ -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 \ No newline at end of file diff --git a/bsnes/gb/BootROMs/cgb_boot.asm b/bsnes/gb/BootROMs/cgb_boot.asm index 1345915d..848305c5 100644 --- a/bsnes/gb/BootROMs/cgb_boot.asm +++ b/bsnes/gb/BootROMs/cgb_boot.asm @@ -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 diff --git a/bsnes/gb/Cocoa/AppDelegate.h b/bsnes/gb/Cocoa/AppDelegate.h index 22e0c365..a9b00487 100644 --- a/bsnes/gb/Cocoa/AppDelegate.h +++ b/bsnes/gb/Cocoa/AppDelegate.h @@ -1,15 +1,25 @@ #import +#import -@interface AppDelegate : NSObject +@interface AppDelegate : NSObject -@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 diff --git a/bsnes/gb/Cocoa/AppDelegate.m b/bsnes/gb/Cocoa/AppDelegate.m index 133fab7f..108a5c89 100644 --- a/bsnes/gb/Cocoa/AppDelegate.m +++ b/bsnes/gb/Cocoa/AppDelegate.m @@ -4,11 +4,33 @@ #include #import #import +#import + +#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 *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:@""]; + if (cutoffRange.location != NSNotFound) { + changes = [changes substringToIndex:cutoffRange.location]; + } + + NSString *html = [NSString stringWithFormat:@"" + "" + "%@", + 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)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 *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 diff --git a/bsnes/gb/Cocoa/AppIcon.icns b/bsnes/gb/Cocoa/AppIcon.icns index 0fe80059..92ad4c65 100644 Binary files a/bsnes/gb/Cocoa/AppIcon.icns and b/bsnes/gb/Cocoa/AppIcon.icns differ diff --git a/bsnes/gb/Cocoa/BigSurToolbar.h b/bsnes/gb/Cocoa/BigSurToolbar.h index ea8b3700..9057d340 100644 --- a/bsnes/gb/Cocoa/BigSurToolbar.h +++ b/bsnes/gb/Cocoa/BigSurToolbar.h @@ -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) diff --git a/bsnes/gb/Cocoa/Document.h b/bsnes/gb/Cocoa/Document.h index 660d7bc2..d6f89de9 100644 --- a/bsnes/gb/Cocoa/Document.h +++ b/bsnes/gb/Cocoa/Document.h @@ -2,44 +2,60 @@ #include "GBView.h" #include "GBImageView.h" #include "GBSplitView.h" +#include "GBVisualizerView.h" +#include "GBOSDView.h" @class GBCheatWindowController; @interface Document : NSDocument -@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 diff --git a/bsnes/gb/Cocoa/Document.m b/bsnes/gb/Cocoa/Document.m index 653179d6..64e3a215 100644 --- a/bsnes/gb/Cocoa/Document.m +++ b/bsnes/gb/Cocoa/Document.m @@ -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)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 diff --git a/bsnes/gb/Cocoa/Document.xib b/bsnes/gb/Cocoa/Document.xib index d02f5bd7..ec2b4508 100644 --- a/bsnes/gb/Cocoa/Document.xib +++ b/bsnes/gb/Cocoa/Document.xib @@ -25,6 +25,7 @@ + @@ -59,9 +60,16 @@ + + + + + + + @@ -115,7 +123,7 @@ - + @@ -152,7 +160,7 @@ - + @@ -186,7 +194,7 @@ - + diff --git a/bsnes/gb/Cocoa/GBAudioClient.h b/bsnes/gb/Cocoa/GBAudioClient.h index aa7be8c2..03ed7011 100644 --- a/bsnes/gb/Cocoa/GBAudioClient.h +++ b/bsnes/gb/Cocoa/GBAudioClient.h @@ -2,9 +2,9 @@ #import @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 diff --git a/bsnes/gb/Cocoa/GBCheatTextFieldCell.h b/bsnes/gb/Cocoa/GBCheatTextFieldCell.h index 473e0f30..e7fd9177 100644 --- a/bsnes/gb/Cocoa/GBCheatTextFieldCell.h +++ b/bsnes/gb/Cocoa/GBCheatTextFieldCell.h @@ -1,5 +1,5 @@ #import @interface GBCheatTextFieldCell : NSTextFieldCell -@property bool usesAddressFormat; +@property (nonatomic) bool usesAddressFormat; @end diff --git a/bsnes/gb/Cocoa/GBCheatWindowController.h b/bsnes/gb/Cocoa/GBCheatWindowController.h index f70553e6..eddebc5a 100644 --- a/bsnes/gb/Cocoa/GBCheatWindowController.h +++ b/bsnes/gb/Cocoa/GBCheatWindowController.h @@ -3,15 +3,14 @@ #import "Document.h" @interface GBCheatWindowController : NSObject -@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 - diff --git a/bsnes/gb/Cocoa/GBImageView.h b/bsnes/gb/Cocoa/GBImageView.h index d5ee534b..c15c4e74 100644 --- a/bsnes/gb/Cocoa/GBImageView.h +++ b/bsnes/gb/Cocoa/GBImageView.h @@ -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 *horizontalGrids; +@property (nonatomic, strong) NSArray *verticalGrids; @property (nonatomic) bool displayScrollRect; @property NSRect scrollRect; -@property (weak) IBOutlet id delegate; +@property (nonatomic, weak) IBOutlet id delegate; @end @protocol GBImageViewDelegate diff --git a/bsnes/gb/Cocoa/GBMemoryByteArray.h b/bsnes/gb/Cocoa/GBMemoryByteArray.h index e3ed71f8..17abe205 100644 --- a/bsnes/gb/Cocoa/GBMemoryByteArray.h +++ b/bsnes/gb/Cocoa/GBMemoryByteArray.h @@ -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 diff --git a/bsnes/gb/Cocoa/GBOSDView.h b/bsnes/gb/Cocoa/GBOSDView.h new file mode 100644 index 00000000..4771d2fd --- /dev/null +++ b/bsnes/gb/Cocoa/GBOSDView.h @@ -0,0 +1,6 @@ +#import + +@interface GBOSDView : NSView +@property bool usesSGBScale; +- (void)displayText:(NSString *)text; +@end diff --git a/bsnes/gb/Cocoa/GBOSDView.m b/bsnes/gb/Cocoa/GBOSDView.m new file mode 100644 index 00000000..710229ec --- /dev/null +++ b/bsnes/gb/Cocoa/GBOSDView.m @@ -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 diff --git a/bsnes/gb/Cocoa/GBOpenGLView.h b/bsnes/gb/Cocoa/GBOpenGLView.h index 5f875ab2..66001c9d 100644 --- a/bsnes/gb/Cocoa/GBOpenGLView.h +++ b/bsnes/gb/Cocoa/GBOpenGLView.h @@ -2,5 +2,5 @@ #import "GBGLShader.h" @interface GBOpenGLView : NSOpenGLView -@property GBGLShader *shader; +@property (nonatomic) GBGLShader *shader; @end diff --git a/bsnes/gb/Cocoa/GBPreferencesWindow.h b/bsnes/gb/Cocoa/GBPreferencesWindow.h index ee697a8d..260ebf99 100644 --- a/bsnes/gb/Cocoa/GBPreferencesWindow.h +++ b/bsnes/gb/Cocoa/GBPreferencesWindow.h @@ -2,26 +2,30 @@ #import @interface GBPreferencesWindow : NSWindow -@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 diff --git a/bsnes/gb/Cocoa/GBPreferencesWindow.m b/bsnes/gb/Cocoa/GBPreferencesWindow.m index 491f0c0f..47302dd6 100644 --- a/bsnes/gb/Cocoa/GBPreferencesWindow.m +++ b/bsnes/gb/Cocoa/GBPreferencesWindow.m @@ -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 diff --git a/bsnes/gb/Cocoa/GBS.xib b/bsnes/gb/Cocoa/GBS.xib new file mode 100644 index 00000000..534ff559 --- /dev/null +++ b/bsnes/gb/Cocoa/GBS.xib @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bsnes/gb/Cocoa/GBTerminalTextFieldCell.h b/bsnes/gb/Cocoa/GBTerminalTextFieldCell.h index 484e0c35..b7603360 100644 --- a/bsnes/gb/Cocoa/GBTerminalTextFieldCell.h +++ b/bsnes/gb/Cocoa/GBTerminalTextFieldCell.h @@ -2,5 +2,5 @@ #include @interface GBTerminalTextFieldCell : NSTextFieldCell -@property GB_gameboy_t *gb; +@property (nonatomic) GB_gameboy_t *gb; @end diff --git a/bsnes/gb/Cocoa/GBView.h b/bsnes/gb/Cocoa/GBView.h index 80721cd7..cd6a539a 100644 --- a/bsnes/gb/Cocoa/GBView.h +++ b/bsnes/gb/Cocoa/GBView.h @@ -1,6 +1,8 @@ #import #include #import +#import "GBOSDView.h" +@class Document; typedef enum { GB_FRAME_BLENDING_MODE_DISABLED, @@ -13,11 +15,13 @@ typedef enum { @interface GBView : NSView - (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; diff --git a/bsnes/gb/Cocoa/GBView.m b/bsnes/gb/Cocoa/GBView.m index 6854ba7b..1e584e41 100644 --- a/bsnes/gb/Cocoa/GBView.m +++ b/bsnes/gb/Cocoa/GBView.m @@ -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)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)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 diff --git a/bsnes/gb/Cocoa/GBViewMetal.m b/bsnes/gb/Cocoa/GBViewMetal.m index 9a1c78b6..580db2ca 100644 --- a/bsnes/gb/Cocoa/GBViewMetal.m +++ b/bsnes/gb/Cocoa/GBViewMetal.m @@ -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; diff --git a/bsnes/gb/Cocoa/GBVisualizerView.h b/bsnes/gb/Cocoa/GBVisualizerView.h new file mode 100644 index 00000000..5ee4638e --- /dev/null +++ b/bsnes/gb/Cocoa/GBVisualizerView.h @@ -0,0 +1,6 @@ +#import +#include + +@interface GBVisualizerView : NSView +- (void)addSample:(GB_sample_t *)sample; +@end diff --git a/bsnes/gb/Cocoa/GBVisualizerView.m b/bsnes/gb/Cocoa/GBVisualizerView.m new file mode 100644 index 00000000..61688e62 --- /dev/null +++ b/bsnes/gb/Cocoa/GBVisualizerView.m @@ -0,0 +1,87 @@ +#import "GBVisualizerView.h" +#include + +#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 diff --git a/bsnes/gb/Cocoa/Info.plist b/bsnes/gb/Cocoa/Info.plist index 44a21f0f..5e409c98 100644 --- a/bsnes/gb/Cocoa/Info.plist +++ b/bsnes/gb/Cocoa/Info.plist @@ -51,7 +51,7 @@ CFBundleTypeExtensions - gbc + isx CFBundleTypeIconFile ColorCartridge @@ -68,6 +68,26 @@ NSDocumentClass Document + + CFBundleTypeExtensions + + gbs + + CFBundleTypeIconFile + ColorCartridge + CFBundleTypeName + Game Boy Sound File + CFBundleTypeRole + Viewer + LSItemContentTypes + + com.github.liji32.sameboy.gbs + + LSTypeIsPackage + 0 + NSDocumentClass + Document + CFBundleExecutable SameBoy @@ -92,7 +112,7 @@ LSMinimumSystemVersion 10.9 NSHumanReadableCopyright - Copyright © 2015-2020 Lior Halphon + Copyright © 2015-2021 Lior Halphon NSMainNibFile MainMenu NSPrincipalClass @@ -156,6 +176,25 @@ + + UTTypeConformsTo + + public.data + + UTTypeDescription + Game Boy Sound File + UTTypeIconFile + ColorCartridge + UTTypeIdentifier + com.github.liji32.sameboy.gbs + UTTypeTagSpecification + + public.filename-extension + + gbs + + + NSCameraUsageDescription SameBoy needs to access your camera to emulate the Game Boy Camera diff --git a/bsnes/gb/Cocoa/License.html b/bsnes/gb/Cocoa/License.html index b21cf8d8..98465141 100644 --- a/bsnes/gb/Cocoa/License.html +++ b/bsnes/gb/Cocoa/License.html @@ -30,7 +30,7 @@

SameBoy

MIT License

-

Copyright © 2015-2020 Lior Halphon

+

Copyright © 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 diff --git a/bsnes/gb/Cocoa/MainMenu.xib b/bsnes/gb/Cocoa/MainMenu.xib index 586d872d..04bcf8f8 100644 --- a/bsnes/gb/Cocoa/MainMenu.xib +++ b/bsnes/gb/Cocoa/MainMenu.xib @@ -12,7 +12,11 @@ - + + + + +

@@ -373,6 +377,17 @@ + + + + + + + + + + + diff --git a/bsnes/gb/Cocoa/Next.png b/bsnes/gb/Cocoa/Next.png new file mode 100644 index 00000000..cd9a4c31 Binary files /dev/null and b/bsnes/gb/Cocoa/Next.png differ diff --git a/bsnes/gb/Cocoa/Next@2x.png b/bsnes/gb/Cocoa/Next@2x.png new file mode 100644 index 00000000..1debb1d5 Binary files /dev/null and b/bsnes/gb/Cocoa/Next@2x.png differ diff --git a/bsnes/gb/Cocoa/Pause.png b/bsnes/gb/Cocoa/Pause.png new file mode 100644 index 00000000..2bb380b7 Binary files /dev/null and b/bsnes/gb/Cocoa/Pause.png differ diff --git a/bsnes/gb/Cocoa/Pause@2x.png b/bsnes/gb/Cocoa/Pause@2x.png new file mode 100644 index 00000000..36b6da01 Binary files /dev/null and b/bsnes/gb/Cocoa/Pause@2x.png differ diff --git a/bsnes/gb/Cocoa/Play.png b/bsnes/gb/Cocoa/Play.png new file mode 100644 index 00000000..3f870921 Binary files /dev/null and b/bsnes/gb/Cocoa/Play.png differ diff --git a/bsnes/gb/Cocoa/Play@2x.png b/bsnes/gb/Cocoa/Play@2x.png new file mode 100644 index 00000000..0de05530 Binary files /dev/null and b/bsnes/gb/Cocoa/Play@2x.png differ diff --git a/bsnes/gb/Cocoa/Preferences.xib b/bsnes/gb/Cocoa/Preferences.xib index 99c65435..24e5de24 100644 --- a/bsnes/gb/Cocoa/Preferences.xib +++ b/bsnes/gb/Cocoa/Preferences.xib @@ -13,6 +13,9 @@ + + + @@ -49,17 +52,25 @@ + + + + + + + + @@ -73,21 +84,25 @@ + + + + - + - + @@ -96,7 +111,7 @@ - + @@ -133,7 +148,7 @@ - + @@ -142,7 +157,7 @@ - + @@ -156,6 +171,7 @@ + @@ -164,7 +180,7 @@ - + @@ -173,7 +189,7 @@ - + @@ -193,7 +209,7 @@ - + @@ -202,7 +218,7 @@ - + @@ -223,7 +239,7 @@ - + @@ -232,7 +248,7 @@ - + @@ -251,8 +267,25 @@ + + + + + + + + + + + + + + + + + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -302,8 +322,17 @@ + + + + + + + + + - + @@ -311,8 +340,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -335,104 +472,56 @@
- - + + - - - - - - - - - - + - + - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + + + + + + + + + + + + + + + + + + - + @@ -452,7 +541,7 @@ - + @@ -460,15 +549,32 @@ + + + + + + + + + + + + + + + + + - + - + @@ -486,14 +592,14 @@ - + - + - + @@ -512,7 +618,7 @@ - + @@ -553,7 +659,7 @@ - + @@ -573,7 +679,7 @@ - + @@ -582,7 +688,7 @@ - + @@ -601,7 +707,7 @@ - + @@ -630,9 +736,9 @@ + + + + + + + + + diff --git a/bsnes/gb/Cocoa/Previous.png b/bsnes/gb/Cocoa/Previous.png new file mode 100644 index 00000000..cc91221d Binary files /dev/null and b/bsnes/gb/Cocoa/Previous.png differ diff --git a/bsnes/gb/Cocoa/Previous@2x.png b/bsnes/gb/Cocoa/Previous@2x.png new file mode 100644 index 00000000..77b01575 Binary files /dev/null and b/bsnes/gb/Cocoa/Previous@2x.png differ diff --git a/bsnes/gb/Cocoa/Rewind.png b/bsnes/gb/Cocoa/Rewind.png new file mode 100644 index 00000000..999f358f Binary files /dev/null and b/bsnes/gb/Cocoa/Rewind.png differ diff --git a/bsnes/gb/Cocoa/Rewind@2x.png b/bsnes/gb/Cocoa/Rewind@2x.png new file mode 100644 index 00000000..d845b549 Binary files /dev/null and b/bsnes/gb/Cocoa/Rewind@2x.png differ diff --git a/bsnes/gb/Cocoa/UpdateWindow.xib b/bsnes/gb/Cocoa/UpdateWindow.xib new file mode 100644 index 00000000..e34f8f21 --- /dev/null +++ b/bsnes/gb/Cocoa/UpdateWindow.xib @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bsnes/gb/Core/apu.c b/bsnes/gb/Core/apu.c index 7e7ab31f..b159b2ea 100644 --- a/bsnes/gb/Core/apu.c +++ b/bsnes/gb/Core/apu.c @@ -137,12 +137,50 @@ static double smooth(double x) return 3*x*x - 2*x*x*x; } +static signed interference(GB_gameboy_t *gb) +{ + /* These aren't scientifically measured, but based on ear based on several recordings */ + signed ret = 0; + if (gb->halted) { + if (gb->model != GB_MODEL_AGB) { + ret -= MAX_CH_AMP / 5; + } + else { + ret -= MAX_CH_AMP / 12; + } + } + if (gb->io_registers[GB_IO_LCDC] & 0x80) { + ret += MAX_CH_AMP / 7; + if ((gb->io_registers[GB_IO_STAT] & 3) == 3 && gb->model != GB_MODEL_AGB) { + ret += MAX_CH_AMP / 14; + } + else if ((gb->io_registers[GB_IO_STAT] & 3) == 1) { + ret -= MAX_CH_AMP / 7; + } + } + + if (gb->apu.global_enable) { + ret += MAX_CH_AMP / 10; + } + + if (GB_is_cgb(gb) && gb->model < GB_MODEL_AGB && (gb->io_registers[GB_IO_RP] & 1)) { + ret += MAX_CH_AMP / 10; + } + + if (!GB_is_cgb(gb)) { + ret /= 4; + } + + ret += rand() % (MAX_CH_AMP / 12); + + return ret; +} + static void render(GB_gameboy_t *gb) { GB_sample_t output = {0, 0}; - UNROLL - for (unsigned i = 0; i < GB_N_CHANNELS; i++) { + unrolled for (unsigned i = 0; i < GB_N_CHANNELS; i++) { double multiplier = CH_STEP; if (gb->model < GB_MODEL_AGB) { @@ -201,8 +239,7 @@ static void render(GB_gameboy_t *gb) unsigned mask = gb->io_registers[GB_IO_NR51]; unsigned left_volume = 0; unsigned right_volume = 0; - UNROLL - for (unsigned i = GB_N_CHANNELS; i--;) { + unrolled for (unsigned i = GB_N_CHANNELS; i--;) { if (gb->apu.is_active[i]) { if (mask & 1) { left_volume += (gb->io_registers[GB_IO_NR50] & 7) * CH_STEP * 0xF; @@ -226,19 +263,21 @@ static void render(GB_gameboy_t *gb) } + + if (gb->apu_output.interference_volume) { + signed interference_bias = interference(gb); + int16_t interference_sample = (interference_bias - gb->apu_output.interference_highpass); + gb->apu_output.interference_highpass = gb->apu_output.interference_highpass * gb->apu_output.highpass_rate + + (1 - gb->apu_output.highpass_rate) * interference_sample; + interference_bias *= gb->apu_output.interference_volume; + + filtered_output.left = MAX(MIN(filtered_output.left + interference_bias, 0x7FFF), -0x8000); + filtered_output.right = MAX(MIN(filtered_output.right + interference_bias, 0x7FFF), -0x8000); + } assert(gb->apu_output.sample_callback); gb->apu_output.sample_callback(gb, &filtered_output); } -static uint16_t new_sweep_sample_length(GB_gameboy_t *gb) -{ - uint16_t delta = gb->apu.shadow_sweep_sample_length >> (gb->io_registers[GB_IO_NR10] & 7); - if (gb->io_registers[GB_IO_NR10] & 8) { - return gb->apu.shadow_sweep_sample_length - delta; - } - return gb->apu.shadow_sweep_sample_length + delta; -} - static void update_square_sample(GB_gameboy_t *gb, unsigned index) { if (gb->apu.square_channels[index].current_sample_index & 0x80) return; @@ -256,56 +295,110 @@ static void update_square_sample(GB_gameboy_t *gb, unsigned index) requires the PCM12 register. The behavior implemented here was verified on *my* CGB, which might behave differently from other CGB revisions, as well as from the DMG, MGB or SGB/2 */ -static void nrx2_glitch(uint8_t *volume, uint8_t value, uint8_t old_value) +static void _nrx2_glitch(uint8_t *volume, uint8_t value, uint8_t old_value, uint8_t *countdown, GB_envelope_clock_t *lock) { - if (value & 8) { - (*volume)++; + if (lock->clock) { + *countdown = value & 7; } - - if (((value ^ old_value) & 8)) { - *volume = 0x10 - *volume; + bool should_tick = (value & 7) && !(old_value & 7) && !lock->locked; + bool should_invert = (value & 8) ^ (old_value & 8); + + if ((value & 0xF) == 8 && (old_value & 0xF) == 8 && !lock->locked) { + should_tick = true; } - - if ((value & 7) && !(old_value & 7) && *volume && !(value & 8)) { - (*volume)--; + + if (should_invert) { + // The weird way and over-the-top way clocks for this counter are connected cause + // some weird ways for it to invert + if (value & 8) { + if (!(old_value & 7) && !lock->locked) { + *volume ^= 0xF; + } + else { + *volume = 0xE - *volume; + *volume &= 0xF; + } + should_tick = false; // Somehow prevents ticking? + } + else { + *volume = 0x10 - *volume; + *volume &= 0xF; + } } - - if ((old_value & 7) && (value & 8)) { - (*volume)--; + if (should_tick) { + if (value & 8) { + (*volume)++; + } + else { + (*volume)--; + } + *volume &= 0xF; } + else if (!(value & 7) && lock->clock) { + // *lock->locked = false; // Excepted from the schematics, but doesn't actually happen on any model? + if (!should_invert) { + if (*volume == 0xF && (value & 8)) { + lock->locked = true; + } + else if (*volume == 0 && !(value & 8)) { + lock->locked = true; + } + } + else if (*volume == 1 && !(value & 8)) { + lock->locked = true; + } + else if (*volume == 0xE && (value & 8)) { + lock->locked = true; + } + lock->clock = false; + } +} - (*volume) &= 0xF; +static void nrx2_glitch(GB_gameboy_t *gb, uint8_t *volume, uint8_t value, uint8_t old_value, uint8_t *countdown, GB_envelope_clock_t *lock) +{ + if (gb->model <= GB_MODEL_CGB_C) { + _nrx2_glitch(volume, 0xFF, old_value, countdown, lock); + _nrx2_glitch(volume, value, 0xFF, countdown, lock); + } + else { + _nrx2_glitch(volume, value, old_value, countdown, lock); + } } static void tick_square_envelope(GB_gameboy_t *gb, enum GB_CHANNELS index) { uint8_t nrx2 = gb->io_registers[index == GB_SQUARE_1? GB_IO_NR12 : GB_IO_NR22]; - - if (gb->apu.square_channels[index].volume_countdown || (nrx2 & 7)) { - if (!gb->apu.square_channels[index].volume_countdown || !--gb->apu.square_channels[index].volume_countdown) { - if (gb->cgb_double_speed) { - if (index == GB_SQUARE_1) { - gb->apu.pcm_mask[0] &= gb->apu.square_channels[GB_SQUARE_1].current_volume | 0xF1; - } - else { - gb->apu.pcm_mask[0] &= (gb->apu.square_channels[GB_SQUARE_2].current_volume << 2) | 0x1F; - } - } - - if ((nrx2 & 8) && gb->apu.square_channels[index].current_volume < 0xF) { - gb->apu.square_channels[index].current_volume++; - } - - else if (!(nrx2 & 8) && gb->apu.square_channels[index].current_volume > 0) { - gb->apu.square_channels[index].current_volume--; - } - - gb->apu.square_channels[index].volume_countdown = nrx2 & 7; - - if (gb->apu.is_active[index]) { - update_square_sample(gb, index); - } + + if (gb->apu.square_envelope_clock[index].locked) return; + if (!(nrx2 & 7)) return; + if (gb->cgb_double_speed) { + if (index == GB_SQUARE_1) { + gb->apu.pcm_mask[0] &= gb->apu.square_channels[GB_SQUARE_1].current_volume | 0xF1; } + else { + gb->apu.pcm_mask[0] &= (gb->apu.square_channels[GB_SQUARE_2].current_volume << 2) | 0x1F; + } + } + + if (nrx2 & 8) { + if (gb->apu.square_channels[index].current_volume < 0xF) { + gb->apu.square_channels[index].current_volume++; + } + else { + gb->apu.square_envelope_clock[index].locked = true; + } + } + else { + if (gb->apu.square_channels[index].current_volume > 0) { + gb->apu.square_channels[index].current_volume--; + } + else { + gb->apu.square_envelope_clock[index].locked = true; + } + } + + if (gb->apu.is_active[index]) { + update_square_sample(gb, index); } } @@ -313,28 +406,59 @@ static void tick_noise_envelope(GB_gameboy_t *gb) { uint8_t nr42 = gb->io_registers[GB_IO_NR42]; - if (gb->apu.noise_channel.volume_countdown || (nr42 & 7)) { - if (!gb->apu.noise_channel.volume_countdown || !--gb->apu.noise_channel.volume_countdown) { - if (gb->cgb_double_speed) { - gb->apu.pcm_mask[0] &= (gb->apu.noise_channel.current_volume << 2) | 0x1F; - } - if ((nr42 & 8) && gb->apu.noise_channel.current_volume < 0xF) { - gb->apu.noise_channel.current_volume++; - } + if (gb->apu.noise_envelope_clock.locked) return; + if (!(nr42 & 7)) return; - else if (!(nr42 & 8) && gb->apu.noise_channel.current_volume > 0) { - gb->apu.noise_channel.current_volume--; - } - - gb->apu.noise_channel.volume_countdown = nr42 & 7; - - if (gb->apu.is_active[GB_NOISE]) { - update_sample(gb, GB_NOISE, - (gb->apu.noise_channel.lfsr & 1) ? - gb->apu.noise_channel.current_volume : 0, - 0); - } + if (gb->cgb_double_speed) { + gb->apu.pcm_mask[0] &= (gb->apu.noise_channel.current_volume << 2) | 0x1F; + } + + if (nr42 & 8) { + if (gb->apu.noise_channel.current_volume < 0xF) { + gb->apu.noise_channel.current_volume++; } + else { + gb->apu.noise_envelope_clock.locked = true; + } + } + else { + if (gb->apu.noise_channel.current_volume > 0) { + gb->apu.noise_channel.current_volume--; + } + else { + gb->apu.noise_envelope_clock.locked = true; + } + } + + if (gb->apu.is_active[GB_NOISE]) { + update_sample(gb, GB_NOISE, + (gb->apu.noise_channel.lfsr & 1) ? + gb->apu.noise_channel.current_volume : 0, + 0); + } +} + +static void trigger_sweep_calculation(GB_gameboy_t *gb) +{ + if ((gb->io_registers[GB_IO_NR10] & 0x70) && gb->apu.square_sweep_countdown == 7) { + if (gb->io_registers[GB_IO_NR10] & 0x07) { + gb->apu.square_channels[GB_SQUARE_1].sample_length = + gb->apu.sweep_length_addend + gb->apu.shadow_sweep_sample_length + !!(gb->io_registers[GB_IO_NR10] & 0x8); + gb->apu.square_channels[GB_SQUARE_1].sample_length &= 0x7FF; + } + if (gb->apu.channel_1_restart_hold == 0) { + gb->apu.sweep_length_addend = gb->apu.square_channels[GB_SQUARE_1].sample_length; + gb->apu.sweep_length_addend >>= (gb->io_registers[GB_IO_NR10] & 7); + } + + /* Recalculation and overflow check only occurs after a delay */ + gb->apu.square_sweep_calculate_countdown = (gb->io_registers[GB_IO_NR10] & 0x7) * 2 + 5 - gb->apu.lf_div; + if (gb->model <= GB_MODEL_CGB_C && gb->apu.lf_div) { + // gb->apu.square_sweep_calculate_countdown += 2; + } + gb->apu.enable_zombie_calculate_stepping = false; + gb->apu.unshifted_sweep = !(gb->io_registers[GB_IO_NR10] & 0x7); + gb->apu.square_sweep_countdown = ((gb->io_registers[GB_IO_NR10] >> 4) & 7) ^ 7; } } @@ -352,29 +476,33 @@ void GB_apu_div_event(GB_gameboy_t *gb) gb->apu.div_divider++; } - if ((gb->apu.div_divider & 1) == 0) { - for (unsigned i = GB_SQUARE_2 + 1; i--;) { - uint8_t nrx2 = gb->io_registers[i == GB_SQUARE_1? GB_IO_NR12 : GB_IO_NR22]; - if (gb->apu.is_active[i] && gb->apu.square_channels[i].volume_countdown == 0 && (nrx2 & 7)) { - tick_square_envelope(gb, i); + if ((gb->apu.div_divider & 7) == 7) { + unrolled for (unsigned i = GB_SQUARE_2 + 1; i--;) { + if (!gb->apu.square_envelope_clock[i].clock) { + gb->apu.square_channels[i].volume_countdown--; + gb->apu.square_channels[i].volume_countdown &= 7; } } - - if (gb->apu.is_active[GB_NOISE] && gb->apu.noise_channel.volume_countdown == 0 && (gb->io_registers[GB_IO_NR42] & 7)) { - tick_noise_envelope(gb); + if (!gb->apu.noise_envelope_clock.clock) { + gb->apu.noise_channel.volume_countdown--; + gb->apu.noise_channel.volume_countdown &= 7; } } - if ((gb->apu.div_divider & 7) == 0) { - for (unsigned i = GB_SQUARE_2 + 1; i--;) { + unrolled for (unsigned i = GB_SQUARE_2 + 1; i--;) { + if (gb->apu.square_envelope_clock[i].clock) { tick_square_envelope(gb, i); + gb->apu.square_envelope_clock[i].clock = false; } - - tick_noise_envelope(gb); } - + + if (gb->apu.noise_envelope_clock.clock) { + tick_noise_envelope(gb); + gb->apu.noise_envelope_clock.clock = false; + } + if ((gb->apu.div_divider & 1) == 1) { - for (unsigned i = GB_SQUARE_2 + 1; i--;) { + unrolled for (unsigned i = GB_SQUARE_2 + 1; i--;) { if (gb->apu.square_channels[i].length_enabled) { if (gb->apu.square_channels[i].pulse_length) { if (!--gb->apu.square_channels[i].pulse_length) { @@ -405,29 +533,48 @@ void GB_apu_div_event(GB_gameboy_t *gb) } if ((gb->apu.div_divider & 3) == 3) { - if (!gb->apu.sweep_enabled) { - return; - } - if (gb->apu.square_sweep_countdown) { - if (!--gb->apu.square_sweep_countdown) { - if ((gb->io_registers[GB_IO_NR10] & 0x70) && (gb->io_registers[GB_IO_NR10] & 0x07)) { - gb->apu.square_channels[GB_SQUARE_1].sample_length = - gb->apu.shadow_sweep_sample_length = - gb->apu.new_sweep_sample_length; - } - - if (gb->io_registers[GB_IO_NR10] & 0x70) { - /* Recalculation and overflow check only occurs after a delay */ - gb->apu.square_sweep_calculate_countdown = 0x13 - gb->apu.lf_div; - } - - gb->apu.square_sweep_countdown = ((gb->io_registers[GB_IO_NR10] >> 4) & 7); - if (!gb->apu.square_sweep_countdown) gb->apu.square_sweep_countdown = 8; - } - } + gb->apu.square_sweep_countdown++; + gb->apu.square_sweep_countdown &= 7; + trigger_sweep_calculation(gb); } } +void GB_apu_div_secondary_event(GB_gameboy_t *gb) +{ + unrolled for (unsigned i = GB_SQUARE_2 + 1; i--;) { + uint8_t nrx2 = gb->io_registers[i == GB_SQUARE_1? GB_IO_NR12 : GB_IO_NR22]; + if (gb->apu.is_active[i] && gb->apu.square_channels[i].volume_countdown == 0) { + gb->apu.square_envelope_clock[i].clock = (gb->apu.square_channels[i].volume_countdown = nrx2 & 7); + } + } + + if (gb->apu.is_active[GB_NOISE] && gb->apu.noise_channel.volume_countdown == 0) { + gb->apu.noise_envelope_clock.clock = (gb->apu.noise_channel.volume_countdown = gb->io_registers[GB_IO_NR42] & 7); + } +} + +static void step_lfsr(GB_gameboy_t *gb, unsigned cycles_offset) +{ + unsigned high_bit_mask = gb->apu.noise_channel.narrow ? 0x4040 : 0x4000; + bool new_high_bit = (gb->apu.noise_channel.lfsr ^ (gb->apu.noise_channel.lfsr >> 1) ^ 1) & 1; + gb->apu.noise_channel.lfsr >>= 1; + + if (new_high_bit) { + gb->apu.noise_channel.lfsr |= high_bit_mask; + } + else { + /* This code is not redundent, it's relevant when switching LFSR widths */ + gb->apu.noise_channel.lfsr &= ~high_bit_mask; + } + + gb->apu.current_lfsr_sample = gb->apu.noise_channel.lfsr & 1; + if (gb->apu.is_active[GB_NOISE]) { + update_sample(gb, GB_NOISE, + gb->apu.current_lfsr_sample ? + gb->apu.noise_channel.current_volume : 0, + cycles_offset); + } +} void GB_apu_run(GB_gameboy_t *gb) { @@ -435,31 +582,62 @@ void GB_apu_run(GB_gameboy_t *gb) uint8_t cycles = gb->apu.apu_cycles >> 2; gb->apu.apu_cycles = 0; if (!cycles) return; - + + bool start_ch4 = false; if (likely(!gb->stopped || GB_is_cgb(gb))) { + if (gb->apu.channel_4_dmg_delayed_start) { + if (gb->apu.channel_4_dmg_delayed_start == cycles) { + gb->apu.channel_4_dmg_delayed_start = 0; + start_ch4 = true; + } + else if (gb->apu.channel_4_dmg_delayed_start > cycles) { + gb->apu.channel_4_dmg_delayed_start -= cycles; + } + else { + /* Split it into two */ + cycles -= gb->apu.channel_4_dmg_delayed_start; + gb->apu.apu_cycles = gb->apu.channel_4_dmg_delayed_start * 4; + GB_apu_run(gb); + } + } /* To align the square signal to 1MHz */ gb->apu.lf_div ^= cycles & 1; gb->apu.noise_channel.alignment += cycles; - if (gb->apu.square_sweep_calculate_countdown) { + if (gb->apu.square_sweep_calculate_countdown && + (((gb->io_registers[GB_IO_NR10] & 7) || gb->apu.unshifted_sweep) || + gb->apu.square_sweep_calculate_countdown <= 3)) { // Calculation is paused if the lower bits are 0 if (gb->apu.square_sweep_calculate_countdown > cycles) { gb->apu.square_sweep_calculate_countdown -= cycles; } else { /* APU bug: sweep frequency is checked after adding the sweep delta twice */ - gb->apu.new_sweep_sample_length = new_sweep_sample_length(gb); - if (gb->apu.new_sweep_sample_length > 0x7ff) { + if (gb->apu.channel_1_restart_hold == 0) { + gb->apu.shadow_sweep_sample_length = gb->apu.square_channels[GB_SQUARE_1].sample_length; + } + if (gb->io_registers[GB_IO_NR10] & 8) { + gb->apu.sweep_length_addend ^= 0x7FF; + } + if (gb->apu.shadow_sweep_sample_length + gb->apu.sweep_length_addend > 0x7FF && !(gb->io_registers[GB_IO_NR10] & 8)) { gb->apu.is_active[GB_SQUARE_1] = false; update_sample(gb, GB_SQUARE_1, 0, gb->apu.square_sweep_calculate_countdown - cycles); - gb->apu.sweep_enabled = false; } - gb->apu.sweep_decreasing |= gb->io_registers[GB_IO_NR10] & 8; + gb->apu.channel1_completed_addend = gb->apu.sweep_length_addend; + gb->apu.square_sweep_calculate_countdown = 0; } } + + if (gb->apu.channel_1_restart_hold) { + if (gb->apu.channel_1_restart_hold > cycles) { + gb->apu.channel_1_restart_hold -= cycles; + } + else { + gb->apu.channel_1_restart_hold = 0; + } + } - UNROLL - for (unsigned i = GB_SQUARE_1; i <= GB_SQUARE_2; i++) { + unrolled for (unsigned i = GB_SQUARE_1; i <= GB_SQUARE_2; i++) { if (gb->apu.is_active[i]) { uint8_t cycles_left = cycles; while (unlikely(cycles_left > gb->apu.square_channels[i].sample_countdown)) { @@ -499,39 +677,38 @@ void GB_apu_run(GB_gameboy_t *gb) gb->apu.wave_channel.wave_form_just_read = false; } } - - if (gb->apu.is_active[GB_NOISE]) { + + // The noise channel can step even if inactive on the DMG + if (gb->apu.is_active[GB_NOISE] || !GB_is_cgb(gb)) { uint8_t cycles_left = cycles; - while (unlikely(cycles_left > gb->apu.noise_channel.sample_countdown)) { - cycles_left -= gb->apu.noise_channel.sample_countdown + 1; - gb->apu.noise_channel.sample_countdown = gb->apu.noise_channel.sample_length * 4 + 3; + unsigned divisor = (gb->io_registers[GB_IO_NR43] & 0x07) << 2; + if (!divisor) divisor = 2; + if (gb->apu.noise_channel.counter_countdown == 0) { + gb->apu.noise_channel.counter_countdown = divisor; + } + while (unlikely(cycles_left >= gb->apu.noise_channel.counter_countdown)) { + cycles_left -= gb->apu.noise_channel.counter_countdown; + gb->apu.noise_channel.counter_countdown = divisor + gb->apu.channel_4_delta; + gb->apu.channel_4_delta = 0; + bool old_bit = (gb->apu.noise_channel.counter >> (gb->io_registers[GB_IO_NR43] >> 4)) & 1; + gb->apu.noise_channel.counter++; + gb->apu.noise_channel.counter &= 0x3FFF; + bool new_bit = (gb->apu.noise_channel.counter >> (gb->io_registers[GB_IO_NR43] >> 4)) & 1; /* Step LFSR */ - unsigned high_bit_mask = gb->apu.noise_channel.narrow ? 0x4040 : 0x4000; - bool new_high_bit = (gb->apu.noise_channel.lfsr ^ (gb->apu.noise_channel.lfsr >> 1) ^ 1) & 1; - gb->apu.noise_channel.lfsr >>= 1; - - if (new_high_bit) { - gb->apu.noise_channel.lfsr |= high_bit_mask; + if (new_bit && !old_bit) { + if (cycles_left == 0 && gb->apu.samples[GB_NOISE] == 0) { + gb->apu.pcm_mask[1] &= 0x0F; + } + step_lfsr(gb, cycles - cycles_left); } - else { - /* This code is not redundent, it's relevant when switching LFSR widths */ - gb->apu.noise_channel.lfsr &= ~high_bit_mask; - } - - gb->apu.current_lfsr_sample = gb->apu.noise_channel.lfsr & 1; - - if (cycles_left == 0 && gb->apu.samples[GB_NOISE] == 0) { - gb->apu.pcm_mask[1] &= 0x0F; - } - - update_sample(gb, GB_NOISE, - gb->apu.current_lfsr_sample ? - gb->apu.noise_channel.current_volume : 0, - 0); } if (cycles_left) { - gb->apu.noise_channel.sample_countdown -= cycles_left; + gb->apu.noise_channel.counter_countdown -= cycles_left; + gb->apu.channel_4_countdown_reloaded = false; + } + else { + gb->apu.channel_4_countdown_reloaded = true; } } } @@ -544,7 +721,11 @@ void GB_apu_run(GB_gameboy_t *gb) render(gb); } } + if (start_ch4) { + GB_apu_write(gb, GB_IO_NR44, gb->io_registers[GB_IO_NR44] | 0x80); + } } + void GB_apu_init(GB_gameboy_t *gb) { memset(&gb->apu, 0, sizeof(gb->apu)); @@ -596,12 +777,137 @@ uint8_t GB_apu_read(GB_gameboy_t *gb, uint8_t reg) if (!GB_is_cgb(gb) && !gb->apu.wave_channel.wave_form_just_read) { return 0xFF; } + if (gb->model == GB_MODEL_AGB) { + return 0xFF; + } reg = GB_IO_WAV_START + gb->apu.wave_channel.current_sample_index / 2; } return gb->io_registers[reg] | read_mask[reg - GB_IO_NR10]; } +static inline uint16_t effective_channel4_counter(GB_gameboy_t *gb) +{ + /* + TODO: On revisions older than the CGB-D, this behaves differently when + the counter advanced this exact T-cycle. Also, in these revisions, + it seems that "passive" changes (due to the temporary FF value NR43 + has during writes) behave slightly different from non-passive ones. + */ + uint16_t effective_counter = gb->apu.noise_channel.counter; + /* Ladies and gentlemen, I present you the holy grail glitch of revision detection! */ + switch (gb->model) { + /* Pre CGB revisions are assumed to be like CGB-C, A and 0 for the lack of a better guess. + TODO: It could be verified with audio based test ROMs. */ +#if 0 + case GB_MODEL_CGB_B: + if (effective_counter & 8) { + effective_counter |= 0xE; // Seems to me F under some circumstances? + } + if (effective_counter & 0x80) { + effective_counter |= 0xFF; + } + if (effective_counter & 0x100) { + effective_counter |= 0x1; + } + if (effective_counter & 0x200) { + effective_counter |= 0x2; + } + if (effective_counter & 0x400) { + effective_counter |= 0x4; + } + if (effective_counter & 0x800) { + effective_counter |= 0x408; // TODO: Only my CGB-B does that! Others behave like C! + } + if (effective_counter & 0x1000) { + effective_counter |= 0x10; + } + if (effective_counter & 0x2000) { + effective_counter |= 0x20; + } + break; +#endif + case GB_MODEL_DMG_B: + case GB_MODEL_SGB_NTSC: + case GB_MODEL_SGB_PAL: + case GB_MODEL_SGB_NTSC_NO_SFC: + case GB_MODEL_SGB_PAL_NO_SFC: + case GB_MODEL_SGB2: + case GB_MODEL_SGB2_NO_SFC: + // case GB_MODEL_CGB_0: + // case GB_MODEL_CGB_A: + case GB_MODEL_CGB_C: + if (effective_counter & 8) { + effective_counter |= 0xE; // Sometimes F on some instances + } + if (effective_counter & 0x80) { + effective_counter |= 0xFF; + } + if (effective_counter & 0x100) { + effective_counter |= 0x1; + } + if (effective_counter & 0x200) { + effective_counter |= 0x2; + } + if (effective_counter & 0x400) { + effective_counter |= 0x4; + } + if (effective_counter & 0x800) { + if ((gb->io_registers[GB_IO_NR43] & 8)) { + effective_counter |= 0x400; + } + effective_counter |= 0x8; + } + if (effective_counter & 0x1000) { + effective_counter |= 0x10; + } + if (effective_counter & 0x2000) { + effective_counter |= 0x20; + } + break; +#if 0 + case GB_MODEL_CGB_D: + if (effective_counter & ((gb->io_registers[GB_IO_NR43] & 8)? 0x40 : 0x80)) { // This is so weird + effective_counter |= 0xFF; + } + if (effective_counter & 0x100) { + effective_counter |= 0x1; + } + if (effective_counter & 0x200) { + effective_counter |= 0x2; + } + if (effective_counter & 0x400) { + effective_counter |= 0x4; + } + if (effective_counter & 0x800) { + effective_counter |= 0x8; + } + if (effective_counter & 0x1000) { + effective_counter |= 0x10; + } + break; +#endif + case GB_MODEL_CGB_E: + if (effective_counter & ((gb->io_registers[GB_IO_NR43] & 8)? 0x40 : 0x80)) { // This is so weird + effective_counter |= 0xFF; + } + if (effective_counter & 0x1000) { + effective_counter |= 0x10; + } + break; + case GB_MODEL_AGB: + /* TODO: AGBs are not affected, but AGSes are. They don't seem to follow a simple + pattern like the other revisions. */ + /* For the most part, AGS seems to do: + 0x20 -> 0xA0 + 0x200 -> 0xA00 + 0x1000 -> 0x1010, but only if wide + */ + break; + } + return effective_counter; +} + void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) { if (!gb->apu.global_enable && reg != GB_IO_NR52 && reg < GB_IO_WAV_START && (GB_is_cgb(gb) || @@ -636,11 +942,11 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) break; case GB_IO_NR52: { - uint8_t old_nrx1[] = { - gb->io_registers[GB_IO_NR11], - gb->io_registers[GB_IO_NR21], - gb->io_registers[GB_IO_NR31], - gb->io_registers[GB_IO_NR41] + uint8_t old_pulse_lengths[] = { + gb->apu.square_channels[0].pulse_length, + gb->apu.square_channels[1].pulse_length, + gb->apu.wave_channel.pulse_length, + gb->apu.noise_channel.pulse_length }; if ((value & 0x80) && !gb->apu.global_enable) { GB_apu_init(gb); @@ -652,35 +958,30 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } memset(&gb->apu, 0, sizeof(gb->apu)); memset(gb->io_registers + GB_IO_NR10, 0, GB_IO_WAV_START - GB_IO_NR10); - old_nrx1[0] &= 0x3F; - old_nrx1[1] &= 0x3F; - gb->apu.global_enable = false; } if (!GB_is_cgb(gb) && (value & 0x80)) { - GB_apu_write(gb, GB_IO_NR11, old_nrx1[0]); - GB_apu_write(gb, GB_IO_NR21, old_nrx1[1]); - GB_apu_write(gb, GB_IO_NR31, old_nrx1[2]); - GB_apu_write(gb, GB_IO_NR41, old_nrx1[3]); + gb->apu.square_channels[0].pulse_length = old_pulse_lengths[0]; + gb->apu.square_channels[1].pulse_length = old_pulse_lengths[1]; + gb->apu.wave_channel.pulse_length = old_pulse_lengths[2]; + gb->apu.noise_channel.pulse_length = old_pulse_lengths[3]; } } break; /* Square channels */ - case GB_IO_NR10: - if (gb->apu.sweep_decreasing && !(value & 8)) { + case GB_IO_NR10:{ + bool old_negate = gb->io_registers[GB_IO_NR10] & 8; + gb->io_registers[GB_IO_NR10] = value; + if (gb->apu.shadow_sweep_sample_length + gb->apu.channel1_completed_addend + old_negate > 0x7FF && + !(value & 8)) { gb->apu.is_active[GB_SQUARE_1] = false; update_sample(gb, GB_SQUARE_1, 0, 0); - gb->apu.sweep_enabled = false; - gb->apu.square_sweep_calculate_countdown = 0; - } - if ((value & 0x70) == 0) { - /* Todo: what happens if we set period to 0 while a calculate event is scheduled, and then - re-set it to non-zero? */ - gb->apu.square_sweep_calculate_countdown = 0; } + trigger_sweep_calculation(gb); break; + } case GB_IO_NR11: case GB_IO_NR21: { @@ -695,10 +996,6 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) case GB_IO_NR12: case GB_IO_NR22: { unsigned index = reg == GB_IO_NR22? GB_SQUARE_2: GB_SQUARE_1; - if (((value & 0x7) == 0) && ((gb->io_registers[reg] & 0x7) != 0)) { - /* Envelope disabled */ - gb->apu.square_channels[index].volume_countdown = 0; - } if ((value & 0xF8) == 0) { /* This disables the DAC */ gb->io_registers[reg] = value; @@ -706,7 +1003,9 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) update_sample(gb, index, 0, 0); } else if (gb->apu.is_active[index]) { - nrx2_glitch(&gb->apu.square_channels[index].current_volume, value, gb->io_registers[reg]); + nrx2_glitch(gb, &gb->apu.square_channels[index].current_volume, + value, gb->io_registers[reg], &gb->apu.square_channels[index].volume_countdown, + &gb->apu.square_envelope_clock[index]); update_square_sample(gb, index); } @@ -723,8 +1022,9 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) case GB_IO_NR14: case GB_IO_NR24: { + /* TODO: GB_MODEL_CGB_D fails channel_1_sweep_restart_2, don't forget when adding support for this revision! */ unsigned index = reg == GB_IO_NR24? GB_SQUARE_2: GB_SQUARE_1; - + bool was_active = gb->apu.is_active[index]; /* TODO: When the sample length changes right before being updated, the countdown should change to the old length, but the current sample should not change. Because our write timing isn't accurate to the T-cycle, we hack around it by stepping the sample index backwards. */ @@ -739,25 +1039,42 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } } + uint16_t old_sample_length = gb->apu.square_channels[index].sample_length; gb->apu.square_channels[index].sample_length &= 0xFF; gb->apu.square_channels[index].sample_length |= (value & 7) << 8; - if (index == GB_SQUARE_1) { - gb->apu.shadow_sweep_sample_length = - gb->apu.new_sweep_sample_length = - gb->apu.square_channels[0].sample_length; - } if (value & 0x80) { /* Current sample index remains unchanged when restarting channels 1 or 2. It is only reset by turning the APU off. */ + gb->apu.square_envelope_clock[index].locked = false; + gb->apu.square_envelope_clock[index].clock = false; if (!gb->apu.is_active[index]) { gb->apu.square_channels[index].sample_countdown = (gb->apu.square_channels[index].sample_length ^ 0x7FF) * 2 + 6 - gb->apu.lf_div; + if (gb->model <= GB_MODEL_CGB_C && gb->apu.lf_div) { + gb->apu.square_channels[index].sample_countdown += 2; + } } else { + unsigned extra_delay = 0; + if (gb->model == GB_MODEL_CGB_E /* || gb->model == GB_MODEL_CGB_D */) { + if (!(value & 4) && !(((gb->apu.square_channels[index].sample_countdown - 1) / 2) & 0x400)) { + gb->apu.square_channels[index].current_sample_index++; + gb->apu.square_channels[index].current_sample_index &= 0x7; + gb->apu.is_active[index] = true; + } + /* Todo: verify with the schematics what's going on in here */ + else if (gb->apu.square_channels[index].sample_length == 0x7FF && + old_sample_length != 0x7FF && + (gb->apu.square_channels[index].current_sample_index & 0x80)) { + extra_delay += 2; + } + } /* Timing quirk: if already active, sound starts 2 (2MHz) ticks earlier.*/ - gb->apu.square_channels[index].sample_countdown = (gb->apu.square_channels[index].sample_length ^ 0x7FF) * 2 + 4 - gb->apu.lf_div; + gb->apu.square_channels[index].sample_countdown = (gb->apu.square_channels[index].sample_length ^ 0x7FF) * 2 + 4 - gb->apu.lf_div + extra_delay; + if (gb->model <= GB_MODEL_CGB_C && gb->apu.lf_div) { + gb->apu.square_channels[index].sample_countdown += 2; + } } gb->apu.square_channels[index].current_volume = gb->io_registers[index == GB_SQUARE_1 ? GB_IO_NR12 : GB_IO_NR22] >> 4; - /* The volume changes caused by NRX4 sound start take effect instantly (i.e. the effect the previously started sound). The playback itself is not instant which is why we don't update the sample for other cases. */ @@ -779,19 +1096,34 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } if (index == GB_SQUARE_1) { - gb->apu.sweep_decreasing = false; + gb->apu.shadow_sweep_sample_length = 0; + gb->apu.channel1_completed_addend = 0; if (gb->io_registers[GB_IO_NR10] & 7) { /* APU bug: if shift is nonzero, overflow check also occurs on trigger */ - gb->apu.square_sweep_calculate_countdown = 0x13 - gb->apu.lf_div; + gb->apu.square_sweep_calculate_countdown = (gb->io_registers[GB_IO_NR10] & 0x7) * 2 + 5 - gb->apu.lf_div; + if (gb->model <= GB_MODEL_CGB_C && gb->apu.lf_div) { + /* TODO: I used to think this is correct, but it caused several regressions. + More research is needed to figure how calculation time is different + in models prior to CGB-D */ + // gb->apu.square_sweep_calculate_countdown += 2; + } + gb->apu.enable_zombie_calculate_stepping = false; + gb->apu.unshifted_sweep = false; + if (!was_active) { + gb->apu.square_sweep_calculate_countdown += 2; + } + gb->apu.sweep_length_addend = gb->apu.square_channels[GB_SQUARE_1].sample_length; + gb->apu.sweep_length_addend >>= (gb->io_registers[GB_IO_NR10] & 7); } else { - gb->apu.square_sweep_calculate_countdown = 0; + gb->apu.sweep_length_addend = 0; } - gb->apu.sweep_enabled = gb->io_registers[GB_IO_NR10] & 0x77; - gb->apu.square_sweep_countdown = ((gb->io_registers[GB_IO_NR10] >> 4) & 7); - if (!gb->apu.square_sweep_countdown) gb->apu.square_sweep_countdown = 8; + gb->apu.channel_1_restart_hold = 2 - gb->apu.lf_div + GB_is_cgb(gb) * 2; + if (gb->model <= GB_MODEL_CGB_C && gb->apu.lf_div) { + gb->apu.channel_1_restart_hold += 2; + } + gb->apu.square_sweep_countdown = ((gb->io_registers[GB_IO_NR10] >> 4) & 7) ^ 7; } - } /* APU glitch - if length is enabled while the DIV-divider's LSB is 1, tick the length once. */ @@ -915,10 +1247,6 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) } case GB_IO_NR42: { - if (((value & 0x7) == 0) && ((gb->io_registers[reg] & 0x7) != 0)) { - /* Envelope disabled */ - gb->apu.noise_channel.volume_countdown = 0; - } if ((value & 0xF8) == 0) { /* This disables the DAC */ gb->io_registers[reg] = value; @@ -926,7 +1254,9 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) update_sample(gb, GB_NOISE, 0, 0); } else if (gb->apu.is_active[GB_NOISE]) { - nrx2_glitch(&gb->apu.noise_channel.current_volume, value, gb->io_registers[reg]); + nrx2_glitch(gb, &gb->apu.noise_channel.current_volume, + value, gb->io_registers[reg], &gb->apu.noise_channel.volume_countdown, + &gb->apu.noise_envelope_clock); update_sample(gb, GB_NOISE, gb->apu.current_lfsr_sample ? gb->apu.noise_channel.current_volume : 0, @@ -937,58 +1267,118 @@ void GB_apu_write(GB_gameboy_t *gb, uint8_t reg, uint8_t value) case GB_IO_NR43: { gb->apu.noise_channel.narrow = value & 8; - unsigned divisor = (value & 0x07) << 1; - if (!divisor) divisor = 1; - gb->apu.noise_channel.sample_length = (divisor << (value >> 4)) - 1; - - /* Todo: changing the frequency sometimes delays the next sample. This is probably - due to how the frequency is actually calculated in the noise channel, which is probably - not by calculating the effective sample length and counting simiarly to the other channels. - This is not emulated correctly. */ + uint16_t effective_counter = effective_channel4_counter(gb); + bool old_bit = (effective_counter >> (gb->io_registers[GB_IO_NR43] >> 4)) & 1; + gb->io_registers[GB_IO_NR43] = value; + bool new_bit = (effective_counter >> (gb->io_registers[GB_IO_NR43] >> 4)) & 1; + if (gb->apu.channel_4_countdown_reloaded) { + unsigned divisor = (gb->io_registers[GB_IO_NR43] & 0x07) << 2; + if (!divisor) divisor = 2; + if (gb->model > GB_MODEL_CGB_C) { + gb->apu.noise_channel.counter_countdown = + divisor + (divisor == 2? 0 : (uint8_t[]){2, 1, 0, 3}[(gb->apu.noise_channel.alignment) & 3]); + } + else { + gb->apu.noise_channel.counter_countdown = + divisor + (divisor == 2? 0 : (uint8_t[]){2, 1, 4, 3}[(gb->apu.noise_channel.alignment) & 3]); + } + gb->apu.channel_4_delta = 0; + } + /* Step LFSR */ + if (new_bit && (!old_bit || gb->model <= GB_MODEL_CGB_C)) { + if (gb->model <= GB_MODEL_CGB_C) { + bool previous_narrow = gb->apu.noise_channel.narrow; + gb->apu.noise_channel.narrow = true; + step_lfsr(gb, 0); + gb->apu.noise_channel.narrow = previous_narrow; + } + else { + step_lfsr(gb, 0); + } + } break; } case GB_IO_NR44: { if (value & 0x80) { - gb->apu.noise_channel.sample_countdown = (gb->apu.noise_channel.sample_length) * 2 + 6 - gb->apu.lf_div; - - /* I'm COMPLETELY unsure about this logic, but it passes all relevant tests. - See comment in NR43. */ - if ((gb->io_registers[GB_IO_NR43] & 7) && (gb->apu.noise_channel.alignment & 2) == 0) { - if ((gb->io_registers[GB_IO_NR43] & 7) == 1) { - gb->apu.noise_channel.sample_countdown += 2; + gb->apu.noise_envelope_clock.locked = false; + gb->apu.noise_envelope_clock.clock = false; + if (!GB_is_cgb(gb) && (gb->apu.noise_channel.alignment & 3) != 0) { + gb->apu.channel_4_dmg_delayed_start = 6; + } + else { + unsigned divisor = (gb->io_registers[GB_IO_NR43] & 0x07) << 2; + if (!divisor) divisor = 2; + gb->apu.channel_4_delta = 0; + gb->apu.noise_channel.counter_countdown = divisor + 4; + if (divisor == 2) { + if (gb->model <= GB_MODEL_CGB_C) { + gb->apu.noise_channel.counter_countdown += gb->apu.lf_div; + if (!gb->cgb_double_speed) { + gb->apu.noise_channel.counter_countdown -= 1; + } + } + else { + gb->apu.noise_channel.counter_countdown += 1 - gb->apu.lf_div; + } } else { - gb->apu.noise_channel.sample_countdown -= 2; + if (gb->model <= GB_MODEL_CGB_C) { + gb->apu.noise_channel.counter_countdown += (uint8_t[]){2, 1, 4, 3}[gb->apu.noise_channel.alignment & 3]; + } + else { + gb->apu.noise_channel.counter_countdown += (uint8_t[]){2, 1, 0, 3}[gb->apu.noise_channel.alignment & 3]; + } + if (((gb->apu.noise_channel.alignment + 1) & 3) < 2) { + if ((gb->io_registers[GB_IO_NR43] & 0x07) == 1) { + gb->apu.noise_channel.counter_countdown -= 2; + gb->apu.channel_4_delta = 2; + } + else { + gb->apu.noise_channel.counter_countdown -= 4; + } + } } - } - if (gb->apu.is_active[GB_NOISE]) { - gb->apu.noise_channel.sample_countdown += 2; - } + + /* TODO: These are quite weird. Verify further */ + if (gb->model <= GB_MODEL_CGB_C) { + if (gb->cgb_double_speed) { + if (!(gb->io_registers[GB_IO_NR43] & 0xF0) && (gb->io_registers[GB_IO_NR43] & 0x07)) { + gb->apu.noise_channel.counter_countdown -= 1; + } + else if ((gb->io_registers[GB_IO_NR43] & 0xF0) && !(gb->io_registers[GB_IO_NR43] & 0x07)) { + gb->apu.noise_channel.counter_countdown += 1; + } + } + else { + gb->apu.noise_channel.counter_countdown -= 2; + } + } + + gb->apu.noise_channel.current_volume = gb->io_registers[GB_IO_NR42] >> 4; - gb->apu.noise_channel.current_volume = gb->io_registers[GB_IO_NR42] >> 4; + /* The volume changes caused by NRX4 sound start take effect instantly (i.e. the effect the previously + started sound). The playback itself is not instant which is why we don't update the sample for other + cases. */ + if (gb->apu.is_active[GB_NOISE]) { + update_sample(gb, GB_NOISE, + gb->apu.current_lfsr_sample ? + gb->apu.noise_channel.current_volume : 0, + 0); + } + gb->apu.noise_channel.lfsr = 0; + gb->apu.current_lfsr_sample = false; + gb->apu.noise_channel.volume_countdown = gb->io_registers[GB_IO_NR42] & 7; - /* The volume changes caused by NRX4 sound start take effect instantly (i.e. the effect the previously - started sound). The playback itself is not instant which is why we don't update the sample for other - cases. */ - if (gb->apu.is_active[GB_NOISE]) { - update_sample(gb, GB_NOISE, - gb->apu.current_lfsr_sample ? - gb->apu.noise_channel.current_volume : 0, - 0); - } - gb->apu.noise_channel.lfsr = 0; - gb->apu.current_lfsr_sample = false; - gb->apu.noise_channel.volume_countdown = gb->io_registers[GB_IO_NR42] & 7; + if (!gb->apu.is_active[GB_NOISE] && (gb->io_registers[GB_IO_NR42] & 0xF8) != 0) { + gb->apu.is_active[GB_NOISE] = true; + update_sample(gb, GB_NOISE, 0, 0); + } - if (!gb->apu.is_active[GB_NOISE] && (gb->io_registers[GB_IO_NR42] & 0xF8) != 0) { - gb->apu.is_active[GB_NOISE] = true; - update_sample(gb, GB_NOISE, 0, 0); - } - - if (gb->apu.noise_channel.pulse_length == 0) { - gb->apu.noise_channel.pulse_length = 0x40; - gb->apu.noise_channel.length_enabled = false; + if (gb->apu.noise_channel.pulse_length == 0) { + gb->apu.noise_channel.pulse_length = 0x40; + gb->apu.noise_channel.length_enabled = false; + } } } @@ -1062,3 +1452,8 @@ void GB_apu_update_cycles_per_sample(GB_gameboy_t *gb) gb->apu_output.cycles_per_sample = 2 * GB_get_clock_rate(gb) / (double)gb->apu_output.sample_rate; /* 2 * because we use 8MHz units */ } } + +void GB_set_interference_volume(GB_gameboy_t *gb, double volume) +{ + gb->apu_output.interference_volume = volume; +} diff --git a/bsnes/gb/Core/apu.h b/bsnes/gb/Core/apu.h index a3a36a63..e44b0b6f 100644 --- a/bsnes/gb/Core/apu.h +++ b/bsnes/gb/Core/apu.h @@ -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); diff --git a/bsnes/gb/Core/camera.c b/bsnes/gb/Core/camera.c index bef84890..7751f18f 100644 --- a/bsnes/gb/Core/camera.c +++ b/bsnes/gb/Core/camera.c @@ -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) diff --git a/bsnes/gb/Core/cheats.c b/bsnes/gb/Core/cheats.c index 14875e01..c7c43fe3 100644 --- a/bsnes/gb/Core/cheats.c +++ b/bsnes/gb/Core/cheats.c @@ -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; } } diff --git a/bsnes/gb/Core/debugger.c b/bsnes/gb/Core/debugger.c index 002d4554..6d4deb4d 100644 --- a/bsnes/gb/Core/debugger.c +++ b/bsnes/gb/Core/debugger.c @@ -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; } diff --git a/bsnes/gb/Core/display.c b/bsnes/gb/Core/display.c index 2eb8c424..2956cbc9 100644 --- a/bsnes/gb/Core/display.c +++ b/bsnes/gb/Core/display.c @@ -2,6 +2,7 @@ #include #include #include +#include #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 ); diff --git a/bsnes/gb/Core/display.h b/bsnes/gb/Core/display.h index 5bdeba8d..c9411dc8 100644 --- a/bsnes/gb/Core/display.h +++ b/bsnes/gb/Core/display.h @@ -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 */ diff --git a/bsnes/gb/Core/gb.c b/bsnes/gb/Core/gb.c index 7325d79a..aa36e3eb 100644 --- a/bsnes/gb/Core/gb.c +++ b/bsnes/gb/Core/gb.c @@ -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; +} diff --git a/bsnes/gb/Core/gb.h b/bsnes/gb/Core/gb.h index 90439368..80465dfb 100644 --- a/bsnes/gb/Core/gb.h +++ b/bsnes/gb/Core/gb.h @@ -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 */ diff --git a/bsnes/gb/Core/mbc.c b/bsnes/gb/Core/mbc.c index 22596817..1e15f8a6 100644 --- a/bsnes/gb/Core/mbc.c +++ b/bsnes/gb/Core/mbc.c @@ -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); } diff --git a/bsnes/gb/Core/mbc.h b/bsnes/gb/Core/mbc.h index 6a23300f..3bbe7827 100644 --- a/bsnes/gb/Core/mbc.h +++ b/bsnes/gb/Core/mbc.h @@ -12,6 +12,7 @@ typedef struct { GB_MBC5, GB_HUC1, GB_HUC3, + GB_TPP1, } mbc_type; enum { GB_STANDARD_MBC, diff --git a/bsnes/gb/Core/memory.c b/bsnes/gb/Core/memory.c index f73209e3..1720356a 100644 --- a/bsnes/gb/Core/memory.c +++ b/bsnes/gb/Core/memory.c @@ -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; } diff --git a/bsnes/gb/Core/memory.h b/bsnes/gb/Core/memory.h index f0d03907..80020f17 100644 --- a/bsnes/gb/Core/memory.h +++ b/bsnes/gb/Core/memory.h @@ -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 */ diff --git a/bsnes/gb/Core/printer.c b/bsnes/gb/Core/printer.c index f04e54dd..c8514b41 100644 --- a/bsnes/gb/Core/printer.c +++ b/bsnes/gb/Core/printer.c @@ -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; } diff --git a/bsnes/gb/Core/printer.h b/bsnes/gb/Core/printer.h index b29650ff..f5c9b277 100644 --- a/bsnes/gb/Core/printer.h +++ b/bsnes/gb/Core/printer.h @@ -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; diff --git a/bsnes/gb/Core/rewind.c b/bsnes/gb/Core/rewind.c index c3900d60..d3055284 100644 --- a/bsnes/gb/Core/rewind.c +++ b/bsnes/gb/Core/rewind.c @@ -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); diff --git a/bsnes/gb/Core/rumble.c b/bsnes/gb/Core/rumble.c index 8cbe20d1..5f83c479 100644 --- a/bsnes/gb/Core/rumble.c +++ b/bsnes/gb/Core/rumble.c @@ -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); diff --git a/bsnes/gb/Core/save_state.c b/bsnes/gb/Core/save_state.c index 9ef6ae35..14cc5dbe 100644 --- a/bsnes/gb/Core/save_state.c +++ b/bsnes/gb/Core/save_state.c @@ -1,67 +1,238 @@ #include "gb.h" #include #include +#include -static bool dump_section(FILE *f, const void *src, uint32_t size) +#ifdef GB_BIG_ENDIAN +#define BESS_NAME "SameBoy v" GB_VERSION " (Big Endian)" +#else +#define BESS_NAME "SameBoy v" GB_VERSION +#endif + +typedef struct __attribute__((packed)) { + uint32_t magic; + uint32_t size; +} BESS_block_t; + +typedef struct __attribute__((packed)) { + uint32_t size; + uint32_t offset; +} BESS_buffer_t; + +typedef struct __attribute__((packed)) { + uint32_t start_offset; + uint32_t magic; +} BESS_footer_t; + +typedef struct __attribute__((packed)) { + BESS_block_t header; + uint16_t major; + uint16_t minor; + union { + struct { + char family; + char model; + char revision; + char padding; + }; + uint32_t full_model; + }; + + uint16_t pc; + uint16_t af; + uint16_t bc; + uint16_t de; + uint16_t hl; + uint16_t sp; + + uint8_t ime; + uint8_t ie; + uint8_t execution_mode; // 0 = running; 1 = halted; 2 = stopped + uint8_t _padding; + + uint8_t io_registers[0x80]; + + BESS_buffer_t ram; + BESS_buffer_t vram; + BESS_buffer_t mbc_ram; + BESS_buffer_t oam; + BESS_buffer_t hram; + BESS_buffer_t background_palettes; + BESS_buffer_t sprite_palettes; +} BESS_CORE_t; + +typedef struct __attribute__((packed)) { + BESS_block_t header; + uint8_t extra_oam[96]; +} BESS_XOAM_t; + +typedef struct __attribute__((packed)) { + BESS_block_t header; + BESS_buffer_t border_tiles; + BESS_buffer_t border_tilemap; + BESS_buffer_t border_palettes; + + BESS_buffer_t active_palettes; + BESS_buffer_t ram_palettes; + BESS_buffer_t attribute_map; + BESS_buffer_t attribute_files; + + uint8_t multiplayer_state; +} BESS_SGB_t; + +typedef struct __attribute__((packed)){ + BESS_block_t header; + char title[0x10]; + uint8_t checksum[2]; +} BESS_INFO_t; + +/* Same RTC format as used by VBA, BGB and SameBoy in battery saves*/ +typedef struct __attribute__((packed)){ + BESS_block_t header; + struct { + uint8_t seconds; + uint8_t padding1[3]; + uint8_t minutes; + uint8_t padding2[3]; + uint8_t hours; + uint8_t padding3[3]; + uint8_t days; + uint8_t padding4[3]; + uint8_t high; + uint8_t padding5[3]; + } real, latched; + uint64_t last_rtc_second; +} BESS_RTC_t; + +/* Same HuC3 RTC format as used by SameBoy and BGB in battery saves */ +typedef struct __attribute__((packed)){ + BESS_block_t header; + GB_huc3_rtc_time_t data; +} BESS_HUC3_t; + +typedef struct __attribute__((packed)){ + BESS_block_t header; + uint64_t last_rtc_second; + uint8_t real_rtc_data[4]; + uint8_t latched_rtc_data[4]; + uint8_t mr4; +} BESS_TPP1_t; + +typedef struct __attribute__((packed)) { + uint16_t address; + uint8_t value; +} BESS_MBC_pair_t; + +typedef struct virtual_file_s virtual_file_t; +struct virtual_file_s { - if (fwrite(&size, 1, sizeof(size), f) != sizeof(size)) { - return false; - } - - if (fwrite(src, 1, size, f) != size) { - return false; - } - - return true; + size_t (*read)(virtual_file_t *file, void *dest, size_t length); + size_t (*write)(virtual_file_t *file, const void *dest, size_t length); + void (*seek)(virtual_file_t *file, ssize_t ammount, int origin); + size_t (*tell)(virtual_file_t *file); + union { + FILE *file; + struct { + uint8_t *buffer; + size_t position; + size_t size; + }; + }; +}; + +static size_t file_read(virtual_file_t *file, void *dest, size_t length) +{ + return fread(dest, 1, length, file->file); } -#define DUMP_SECTION(gb, f, section) dump_section(f, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section)) - -/* Todo: we need a sane and protable save state format. */ -int GB_save_state(GB_gameboy_t *gb, const char *path) +static void file_seek(virtual_file_t *file, ssize_t ammount, int origin) { - FILE *f = fopen(path, "wb"); - if (!f) { - GB_log(gb, "Could not open save state: %s.\n", strerror(errno)); - return errno; + fseek(file->file, ammount, origin); +} + +static size_t file_write(virtual_file_t *file, const void *src, size_t length) +{ + return fwrite(src, 1, length, file->file); +} + +static size_t file_tell(virtual_file_t *file) +{ + return ftell(file->file); +} + +static size_t buffer_read(virtual_file_t *file, void *dest, size_t length) +{ + if (length & 0x80000000) { + return 0; } - - if (fwrite(GB_GET_SECTION(gb, header), 1, GB_SECTION_SIZE(header), f) != GB_SECTION_SIZE(header)) goto error; - if (!DUMP_SECTION(gb, f, core_state)) goto error; - if (!DUMP_SECTION(gb, f, dma )) goto error; - if (!DUMP_SECTION(gb, f, mbc )) goto error; - if (!DUMP_SECTION(gb, f, hram )) goto error; - if (!DUMP_SECTION(gb, f, timing )) goto error; - if (!DUMP_SECTION(gb, f, apu )) goto error; - if (!DUMP_SECTION(gb, f, rtc )) goto error; - if (!DUMP_SECTION(gb, f, video )) goto error; - - if (GB_is_hle_sgb(gb)) { - if (!dump_section(f, gb->sgb, sizeof(*gb->sgb))) goto error; - } - - if (fwrite(gb->mbc_ram, 1, gb->mbc_ram_size, f) != gb->mbc_ram_size) { - goto error; - } - - if (fwrite(gb->ram, 1, gb->ram_size, f) != gb->ram_size) { - goto error; - } - - if (fwrite(gb->vram, 1, gb->vram_size, f) != gb->vram_size) { - goto error; - } - errno = 0; + if (length > file->size - file->position) { + errno = EIO; + length = file->size - file->position; + } -error: - fclose(f); - return errno; + memcpy(dest, file->buffer + file->position, length); + file->position += length; + + return length; } -#undef DUMP_SECTION +static void buffer_seek(virtual_file_t *file, ssize_t ammount, int origin) +{ + switch (origin) { + case SEEK_SET: + file->position = ammount; + break; + case SEEK_CUR: + file->position += ammount; + break; + case SEEK_END: + file->position = file->size + ammount; + break; + default: + break; + } + + if (file->position > file->size) { + file->position = file->size; + } +} -size_t GB_get_save_state_size(GB_gameboy_t *gb) +static size_t buffer_write(virtual_file_t *file, const void *src, size_t size) +{ + memcpy(file->buffer + file->position, src, size); + file->position += size; + return size; +} + +static size_t buffer_tell(virtual_file_t *file) +{ + return file->position; +} + +static size_t bess_size_for_cartridge(const GB_cartridge_t *cart) +{ + switch (cart->mbc_type) { + default: + case GB_NO_MBC: return 0; + case GB_MBC1: + return sizeof(BESS_block_t) + 4 * sizeof(BESS_MBC_pair_t); + case GB_MBC2: + return sizeof(BESS_block_t) + 2 * sizeof(BESS_MBC_pair_t); + case GB_MBC3: + return sizeof(BESS_block_t) + 3 * sizeof(BESS_MBC_pair_t) + (cart->has_rtc? sizeof(BESS_RTC_t) : 0); + case GB_MBC5: + return sizeof(BESS_block_t) + 4 * sizeof(BESS_MBC_pair_t); + case GB_HUC1: + return sizeof(BESS_block_t) + 4 * sizeof(BESS_MBC_pair_t); + case GB_HUC3: + return sizeof(BESS_block_t) + 3 * sizeof(BESS_MBC_pair_t) + sizeof(BESS_HUC3_t); + case GB_TPP1: + return sizeof(BESS_block_t) + 4 * sizeof(BESS_MBC_pair_t) + sizeof(BESS_TPP1_t); + } +} + +size_t GB_get_save_state_size_no_bess(GB_gameboy_t *gb) { return GB_SECTION_SIZE(header) + GB_SECTION_SIZE(core_state) + sizeof(uint32_t) @@ -78,79 +249,27 @@ size_t GB_get_save_state_size(GB_gameboy_t *gb) + gb->vram_size; } -/* A write-line function for memory copying */ -static void buffer_write(const void *src, size_t size, uint8_t **dest) +size_t GB_get_save_state_size(GB_gameboy_t *gb) { - memcpy(*dest, src, size); - *dest += size; + return GB_get_save_state_size_no_bess(gb) + + // BESS + + sizeof(BESS_block_t) // NAME + + sizeof(BESS_NAME) - 1 + + sizeof(BESS_INFO_t) // INFO + + sizeof(BESS_CORE_t) + + sizeof(BESS_XOAM_t) + + (gb->sgb? sizeof(BESS_SGB_t) : 0) + + bess_size_for_cartridge(gb->cartridge_type) // MBC & RTC/HUC3/TPP1 block + + sizeof(BESS_block_t) // END block + + sizeof(BESS_footer_t); } -static void buffer_dump_section(uint8_t **buffer, const void *src, uint32_t size) -{ - buffer_write(&size, sizeof(size), buffer); - buffer_write(src, size, buffer); -} - -#define DUMP_SECTION(gb, buffer, section) buffer_dump_section(&buffer, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section)) -void GB_save_state_to_buffer(GB_gameboy_t *gb, uint8_t *buffer) -{ - buffer_write(GB_GET_SECTION(gb, header), GB_SECTION_SIZE(header), &buffer); - DUMP_SECTION(gb, buffer, core_state); - DUMP_SECTION(gb, buffer, dma ); - DUMP_SECTION(gb, buffer, mbc ); - DUMP_SECTION(gb, buffer, hram ); - DUMP_SECTION(gb, buffer, timing ); - DUMP_SECTION(gb, buffer, apu ); - DUMP_SECTION(gb, buffer, rtc ); - DUMP_SECTION(gb, buffer, video ); - - if (GB_is_hle_sgb(gb)) { - buffer_dump_section(&buffer, gb->sgb, sizeof(*gb->sgb)); - } - - - buffer_write(gb->mbc_ram, gb->mbc_ram_size, &buffer); - buffer_write(gb->ram, gb->ram_size, &buffer); - buffer_write(gb->vram, gb->vram_size, &buffer); -} - -/* Best-effort read function for maximum future compatibility. */ -static bool read_section(FILE *f, void *dest, uint32_t size, bool fix_broken_windows_saves) -{ - uint32_t saved_size = 0; - if (fread(&saved_size, 1, sizeof(size), f) != sizeof(size)) { - return false; - } - - if (fix_broken_windows_saves) { - if (saved_size < 4) { - return false; - } - saved_size -= 4; - fseek(f, 4, SEEK_CUR); - } - - if (saved_size <= size) { - if (fread(dest, 1, saved_size, f) != saved_size) { - return false; - } - } - else { - if (fread(dest, 1, size, f) != size) { - return false; - } - fseek(f, saved_size - size, SEEK_CUR); - } - - return true; -} -#undef DUMP_SECTION - -static bool verify_and_update_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t *save) +static bool verify_and_update_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t *save, bool *attempt_bess) { + *attempt_bess = false; if (save->ram_size == 0 && (&save->ram_size)[-1] == gb->ram_size) { /* This is a save state with a bad printer struct from a 32-bit OS */ - memcpy(save->extra_oam + 4, save->extra_oam, (uintptr_t)&save->ram_size - (uintptr_t)&save->extra_oam); + memmove(save->extra_oam + 4, save->extra_oam, (uintptr_t)&save->ram_size - (uintptr_t)&save->extra_oam); } if (save->ram_size == 0) { /* Save doesn't have ram size specified, it's a pre 0.12 save state with potentially @@ -163,8 +282,19 @@ static bool verify_and_update_state_compatibility(GB_gameboy_t *gb, GB_gameboy_t } } + if (save->model & GB_MODEL_PAL_BIT_OLD) { + save->model &= ~GB_MODEL_PAL_BIT_OLD; + save->model |= GB_MODEL_PAL_BIT; + } + + if (save->model & GB_MODEL_NO_SFC_BIT_OLD) { + save->model &= ~GB_MODEL_NO_SFC_BIT_OLD; + save->model |= GB_MODEL_NO_SFC_BIT; + } + if (gb->version != save->version) { GB_log(gb, "The save state is for a different version of SameBoy.\n"); + *attempt_bess = true; return false; } @@ -217,11 +347,814 @@ static void sanitize_state(GB_gameboy_t *gb) if (gb->object_priority == GB_OBJECT_PRIORITY_UNDEFINED) { gb->object_priority = gb->cgb_mode? GB_OBJECT_PRIORITY_INDEX : GB_OBJECT_PRIORITY_X; } + if (gb->sgb && !gb->sgb->v14_3) { +#ifdef GB_BIG_ENDIAN + for (unsigned i = 0; i < sizeof(gb->sgb->border.raw_data) / 2; i++) { + gb->sgb->border.raw_data[i] = LE16(gb->sgb->border.raw_data[i]); + } + + for (unsigned i = 0; i < sizeof(gb->sgb->pending_border.raw_data) / 2; i++) { + gb->sgb->pending_border.raw_data[i] = LE16(gb->sgb->pending_border.raw_data[i]); + } + + for (unsigned i = 0; i < sizeof(gb->sgb->effective_palettes) / 2; i++) { + gb->sgb->effective_palettes[i] = LE16(gb->sgb->effective_palettes[i]); + } + + for (unsigned i = 0; i < sizeof(gb->sgb->ram_palettes) / 2; i++) { + gb->sgb->ram_palettes[i] = LE16(gb->sgb->ram_palettes[i]); + } +#endif + uint8_t converted_tiles[sizeof(gb->sgb->border.tiles)] = {0,}; + for (unsigned tile = 0; tile < sizeof(gb->sgb->border.tiles_legacy) / 64; tile++) { + for (unsigned y = 0; y < 8; y++) { + unsigned base = tile * 32 + y * 2; + for (unsigned x = 0; x < 8; x++) { + uint8_t pixel = gb->sgb->border.tiles_legacy[tile * 8 * 8 + y * 8 + x]; + if (pixel & 1) converted_tiles[base] |= (1 << (7 ^ x)); + if (pixel & 2) converted_tiles[base + 1] |= (1 << (7 ^ x)); + if (pixel & 4) converted_tiles[base + 16] |= (1 << (7 ^ x)); + if (pixel & 8) converted_tiles[base + 17] |= (1 << (7 ^ x)); + } + } + } + memcpy(gb->sgb->border.tiles, converted_tiles, sizeof(converted_tiles)); + memset(converted_tiles, 0, sizeof(converted_tiles)); + for (unsigned tile = 0; tile < sizeof(gb->sgb->pending_border.tiles_legacy) / 64; tile++) { + for (unsigned y = 0; y < 8; y++) { + unsigned base = tile * 32 + y * 2; + for (unsigned x = 0; x < 8; x++) { + uint8_t pixel = gb->sgb->pending_border.tiles_legacy[tile * 8 * 8 + y * 8 + x]; + if (pixel & 1) converted_tiles[base] |= (1 << (7 ^ x)); + if (pixel & 2) converted_tiles[base + 1] |= (1 << (7 ^ x)); + if (pixel & 4) converted_tiles[base + 16] |= (1 << (7 ^ x)); + if (pixel & 8) converted_tiles[base + 17] |= (1 << (7 ^ x)); + } + } + } + memcpy(gb->sgb->pending_border.tiles, converted_tiles, sizeof(converted_tiles)); + } } -#define READ_SECTION(gb, f, section) read_section(f, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section), fix_broken_windows_saves) +static bool dump_section(virtual_file_t *file, const void *src, uint32_t size) +{ + if (file->write(file, &size, sizeof(size)) != sizeof(size)) { + return false; + } + + if (file->write(file, src, size) != size) { + return false; + } + + return true; +} -int GB_load_state(GB_gameboy_t *gb, const char *path) +#define DUMP_SECTION(gb, f, section) dump_section(f, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section)) + +static int save_bess_mbc_block(GB_gameboy_t *gb, virtual_file_t *file) +{ + + BESS_block_t mbc_block = {BE32('MBC '), 0}; + BESS_MBC_pair_t pairs[4]; + switch (gb->cartridge_type->mbc_type) { + default: + case GB_NO_MBC: return 0; + case GB_MBC1: + pairs[0] = (BESS_MBC_pair_t){LE16(0x0000), gb->mbc_ram_enable? 0xA : 0x0}; + pairs[1] = (BESS_MBC_pair_t){LE16(0x2000), gb->mbc1.bank_low}; + pairs[2] = (BESS_MBC_pair_t){LE16(0x4000), gb->mbc1.bank_high}; + pairs[3] = (BESS_MBC_pair_t){LE16(0x6000), gb->mbc1.mode}; + mbc_block.size = 4 * sizeof(pairs[0]); + break; + case GB_MBC2: + pairs[0] = (BESS_MBC_pair_t){LE16(0x0000), gb->mbc_ram_enable? 0xA : 0x0}; + pairs[1] = (BESS_MBC_pair_t){LE16(0x0100), gb->mbc2.rom_bank}; + mbc_block.size = 2 * sizeof(pairs[0]); + break; + case GB_MBC3: + pairs[0] = (BESS_MBC_pair_t){LE16(0x0000), gb->mbc_ram_enable? 0xA : 0x0}; + pairs[1] = (BESS_MBC_pair_t){LE16(0x2000), gb->mbc3.rom_bank}; + pairs[2] = (BESS_MBC_pair_t){LE16(0x4000), gb->mbc3.ram_bank | (gb->mbc3_rtc_mapped? 8 : 0)}; + mbc_block.size = 3 * sizeof(pairs[0]); + break; + case GB_MBC5: + pairs[0] = (BESS_MBC_pair_t){LE16(0x0000), gb->mbc_ram_enable? 0xA : 0x0}; + pairs[1] = (BESS_MBC_pair_t){LE16(0x2000), gb->mbc5.rom_bank_low}; + pairs[2] = (BESS_MBC_pair_t){LE16(0x3000), gb->mbc5.rom_bank_high}; + pairs[3] = (BESS_MBC_pair_t){LE16(0x4000), gb->mbc5.ram_bank}; + mbc_block.size = 4 * sizeof(pairs[0]); + break; + case GB_HUC1: + pairs[0] = (BESS_MBC_pair_t){LE16(0x0000), gb->huc1.ir_mode? 0xE : 0x0}; + pairs[1] = (BESS_MBC_pair_t){LE16(0x2000), gb->huc1.bank_low}; + pairs[2] = (BESS_MBC_pair_t){LE16(0x4000), gb->huc1.bank_high}; + pairs[3] = (BESS_MBC_pair_t){LE16(0x6000), gb->huc1.mode}; + mbc_block.size = 4 * sizeof(pairs[0]); + + case GB_HUC3: + pairs[0] = (BESS_MBC_pair_t){LE16(0x0000), gb->huc3_mode}; + pairs[1] = (BESS_MBC_pair_t){LE16(0x2000), gb->huc3.rom_bank}; + pairs[2] = (BESS_MBC_pair_t){LE16(0x4000), gb->huc3.ram_bank}; + mbc_block.size = 3 * sizeof(pairs[0]); + break; + + case GB_TPP1: + pairs[0] = (BESS_MBC_pair_t){LE16(0x0000), gb->tpp1_rom_bank}; + pairs[1] = (BESS_MBC_pair_t){LE16(0x0001), gb->tpp1_rom_bank >> 8}; + pairs[2] = (BESS_MBC_pair_t){LE16(0x0002), gb->tpp1_rom_bank}; + pairs[3] = (BESS_MBC_pair_t){LE16(0x0003), gb->tpp1_mode}; + mbc_block.size = 4 * sizeof(pairs[0]); + break; + } + + mbc_block.size = LE32(mbc_block.size); + + if (file->write(file, &mbc_block, sizeof(mbc_block)) != sizeof(mbc_block)) { + return errno; + } + + if (file->write(file, &pairs, LE32(mbc_block.size)) != LE32(mbc_block.size)) { + return errno; + } + + return 0; +} + +static int save_state_internal(GB_gameboy_t *gb, virtual_file_t *file, bool append_bess) +{ + if (file->write(file, GB_GET_SECTION(gb, header), GB_SECTION_SIZE(header)) != GB_SECTION_SIZE(header)) goto error; + if (!DUMP_SECTION(gb, file, core_state)) goto error; + if (!DUMP_SECTION(gb, file, dma )) goto error; + if (!DUMP_SECTION(gb, file, mbc )) goto error; + uint32_t hram_offset = file->tell(file) + 4; + if (!DUMP_SECTION(gb, file, hram )) goto error; + if (!DUMP_SECTION(gb, file, timing )) goto error; + if (!DUMP_SECTION(gb, file, apu )) goto error; + if (!DUMP_SECTION(gb, file, rtc )) goto error; + uint32_t video_offset = file->tell(file) + 4; + if (!DUMP_SECTION(gb, file, video )) goto error; + + uint32_t sgb_offset = 0; + + if (GB_is_hle_sgb(gb)) { + gb->sgb->v14_3 = true; + sgb_offset = file->tell(file) + 4; + if (!dump_section(file, gb->sgb, sizeof(*gb->sgb))) goto error; + } + + + BESS_CORE_t bess_core = {0,}; + + bess_core.mbc_ram.offset = LE32(file->tell(file)); + bess_core.mbc_ram.size = LE32(gb->mbc_ram_size); + if (file->write(file, gb->mbc_ram, gb->mbc_ram_size) != gb->mbc_ram_size) { + goto error; + } + + bess_core.ram.offset = LE32(file->tell(file)); + bess_core.ram.size = LE32(gb->ram_size); + if (file->write(file, gb->ram, gb->ram_size) != gb->ram_size) { + goto error; + } + + bess_core.vram.offset = LE32(file->tell(file)); + bess_core.vram.size = LE32(gb->vram_size); + if (file->write(file, gb->vram, gb->vram_size) != gb->vram_size) { + goto error; + } + + if (!append_bess) return 0; + + BESS_footer_t bess_footer = { + .start_offset = LE32(file->tell(file)), + .magic = BE32('BESS'), + }; + + /* BESS NAME */ + + static const BESS_block_t bess_name = {BE32('NAME'), LE32(sizeof(BESS_NAME) - 1)}; + + if (file->write(file, &bess_name, sizeof(bess_name)) != sizeof(bess_name)) { + goto error; + } + + if (file->write(file, BESS_NAME, sizeof(BESS_NAME) - 1) != sizeof(BESS_NAME) - 1) { + goto error; + } + + /* BESS INFO */ + + static const BESS_block_t bess_info = {BE32('INFO'), LE32(sizeof(BESS_INFO_t) - sizeof(BESS_block_t))}; + + if (file->write(file, &bess_info, sizeof(bess_info)) != sizeof(bess_info)) { + goto error; + } + + if (file->write(file, gb->rom + 0x134, 0x10) != 0x10) { + goto error; + } + + if (file->write(file, gb->rom + 0x14e, 2) != 2) { + goto error; + } + + /* BESS CORE */ + + bess_core.header = (BESS_block_t){BE32('CORE'), LE32(sizeof(bess_core) - sizeof(bess_core.header))}; + bess_core.major = LE16(1); + bess_core.minor = LE16(1); + switch (gb->model) { + + case GB_MODEL_DMG_B: bess_core.full_model = BE32('GDB '); break; + + case GB_MODEL_SGB_NTSC: + case GB_MODEL_SGB_NTSC_NO_SFC: + bess_core.full_model = BE32('SN '); break; + + case GB_MODEL_SGB_PAL_NO_SFC: + case GB_MODEL_SGB_PAL: + bess_core.full_model = BE32('SP '); break; + + case GB_MODEL_SGB2_NO_SFC: + case GB_MODEL_SGB2: + bess_core.full_model = BE32('S2 '); break; + + + case GB_MODEL_CGB_C: bess_core.full_model = BE32('CCC '); break; + case GB_MODEL_CGB_E: bess_core.full_model = BE32('CCE '); break; + case GB_MODEL_AGB: bess_core.full_model = BE32('CA '); break; // SameBoy doesn't emulate a specific AGB revision yet + } + + bess_core.pc = LE16(gb->pc); + bess_core.af = LE16(gb->af); + bess_core.bc = LE16(gb->bc); + bess_core.de = LE16(gb->de); + bess_core.hl = LE16(gb->hl); + bess_core.sp = LE16(gb->sp); + + bess_core.ime = gb->ime; + bess_core.ie = gb->interrupt_enable; + bess_core.execution_mode = 0; + if (gb->halted) { + bess_core.execution_mode = 1; + } + else if (gb->stopped) { + bess_core.execution_mode = 2; + } + + memcpy(bess_core.io_registers, gb->io_registers, sizeof(gb->io_registers)); + bess_core.io_registers[GB_IO_DIV] = gb->div_counter >> 8; + bess_core.io_registers[GB_IO_BANK] = gb->boot_rom_finished; + bess_core.io_registers[GB_IO_KEY1] |= gb->cgb_double_speed? 0x80 : 0; + bess_core.hram.size = LE32(sizeof(gb->hram)); + bess_core.hram.offset = LE32(hram_offset + offsetof(GB_gameboy_t, hram) - GB_SECTION_OFFSET(hram)); + bess_core.oam.size = LE32(sizeof(gb->oam)); + bess_core.oam.offset = LE32(video_offset + offsetof(GB_gameboy_t, oam) - GB_SECTION_OFFSET(video)); + if (GB_is_cgb(gb)) { + bess_core.background_palettes.size = LE32(sizeof(gb->background_palettes_data)); + bess_core.background_palettes.offset = LE32(video_offset + offsetof(GB_gameboy_t, background_palettes_data) - GB_SECTION_OFFSET(video)); + bess_core.sprite_palettes.size = LE32(sizeof(gb->sprite_palettes_data)); + bess_core.sprite_palettes.offset = LE32(video_offset + offsetof(GB_gameboy_t, sprite_palettes_data) - GB_SECTION_OFFSET(video)); + } + + if (file->write(file, &bess_core, sizeof(bess_core)) != sizeof(bess_core)) { + goto error; + } + + /* BESS XOAM */ + + BESS_XOAM_t bess_xoam = {0,}; + bess_xoam.header = (BESS_block_t){BE32('XOAM'), LE32(sizeof(bess_xoam) - sizeof(bess_xoam.header))}; + if (GB_is_cgb(gb)) { + memcpy(bess_xoam.extra_oam, gb->extra_oam, sizeof(bess_xoam.extra_oam)); + } + + if (file->write(file, &bess_xoam, sizeof(bess_xoam)) != sizeof(bess_xoam)) { + goto error; + } + + save_bess_mbc_block(gb, file); + if (gb->cartridge_type->has_rtc) { + if (gb->cartridge_type ->mbc_type == GB_TPP1) { + BESS_TPP1_t bess_tpp1 = {0,}; + bess_tpp1.header = (BESS_block_t){BE32('TPP1'), LE32(sizeof(bess_tpp1) - sizeof(bess_tpp1.header))}; + + bess_tpp1.last_rtc_second = LE64(gb->last_rtc_second); + unrolled for (unsigned i = 4; i--;) { + bess_tpp1.real_rtc_data[i] = gb->rtc_real.data[i ^ 3]; + bess_tpp1.latched_rtc_data[i] = gb->rtc_latched.data[i ^ 3]; + } + bess_tpp1.mr4 = gb->tpp1_mr4; + + if (file->write(file, &bess_tpp1, sizeof(bess_tpp1)) != sizeof(bess_tpp1)) { + goto error; + } + } + else if (gb->cartridge_type ->mbc_type != GB_HUC3) { + BESS_RTC_t bess_rtc = {0,}; + bess_rtc.header = (BESS_block_t){BE32('RTC '), LE32(sizeof(bess_rtc) - sizeof(bess_rtc.header))}; + bess_rtc.real.seconds = gb->rtc_real.seconds; + bess_rtc.real.minutes = gb->rtc_real.minutes; + bess_rtc.real.hours = gb->rtc_real.hours; + bess_rtc.real.days = gb->rtc_real.days; + bess_rtc.real.high = gb->rtc_real.high; + bess_rtc.latched.seconds = gb->rtc_latched.seconds; + bess_rtc.latched.minutes = gb->rtc_latched.minutes; + bess_rtc.latched.hours = gb->rtc_latched.hours; + bess_rtc.latched.days = gb->rtc_latched.days; + bess_rtc.latched.high = gb->rtc_latched.high; + bess_rtc.last_rtc_second = LE64(gb->last_rtc_second); + if (file->write(file, &bess_rtc, sizeof(bess_rtc)) != sizeof(bess_rtc)) { + goto error; + } + } + else { + BESS_HUC3_t bess_huc3 = {0,}; + bess_huc3.header = (BESS_block_t){BE32('HUC3'), LE32(sizeof(bess_huc3) - sizeof(bess_huc3.header))}; + + bess_huc3.data = (GB_huc3_rtc_time_t) { + LE64(gb->last_rtc_second), + LE16(gb->huc3_minutes), + LE16(gb->huc3_days), + LE16(gb->huc3_alarm_minutes), + LE16(gb->huc3_alarm_days), + gb->huc3_alarm_enabled, + }; + if (file->write(file, &bess_huc3, sizeof(bess_huc3)) != sizeof(bess_huc3)) { + goto error; + } + } + } + + bool needs_sgb_padding = false; + if (gb->sgb) { + /* BESS SGB */ + if (gb->sgb->disable_commands) { + needs_sgb_padding = true; + } + else { + BESS_SGB_t bess_sgb = {{BE32('SGB '), LE32(sizeof(bess_sgb) - sizeof(bess_sgb.header))}, }; + + bess_sgb.border_tiles = (BESS_buffer_t){LE32(sizeof(gb->sgb->pending_border.tiles)), + LE32(sgb_offset + offsetof(GB_sgb_t, pending_border.tiles))}; + bess_sgb.border_tilemap = (BESS_buffer_t){LE32(sizeof(gb->sgb->pending_border.map)), + LE32(sgb_offset + offsetof(GB_sgb_t, pending_border.map))}; + bess_sgb.border_palettes = (BESS_buffer_t){LE32(sizeof(gb->sgb->pending_border.palette)), + LE32(sgb_offset + offsetof(GB_sgb_t, pending_border.palette))}; + + bess_sgb.active_palettes = (BESS_buffer_t){LE32(sizeof(gb->sgb->effective_palettes)), + LE32(sgb_offset + offsetof(GB_sgb_t, effective_palettes))}; + bess_sgb.ram_palettes = (BESS_buffer_t){LE32(sizeof(gb->sgb->ram_palettes)), + LE32(sgb_offset + offsetof(GB_sgb_t, ram_palettes))}; + bess_sgb.attribute_map = (BESS_buffer_t){LE32(sizeof(gb->sgb->attribute_map)), + LE32(sgb_offset + offsetof(GB_sgb_t, attribute_map))}; + bess_sgb.attribute_files = (BESS_buffer_t){LE32(sizeof(gb->sgb->attribute_files)), + LE32(sgb_offset + offsetof(GB_sgb_t, attribute_files))}; + + bess_sgb.multiplayer_state = (gb->sgb->player_count << 4) | (gb->sgb->current_player & (gb->sgb->player_count - 1)); + if (file->write(file, &bess_sgb, sizeof(bess_sgb)) != sizeof(bess_sgb)) { + goto error; + } + } + } + + /* BESS END */ + + static const BESS_block_t bess_end = {BE32('END '), 0}; + + if (file->write(file, &bess_end, sizeof(bess_end)) != sizeof(bess_end)) { + goto error; + } + + if (needs_sgb_padding) { + static const uint8_t sgb_padding[sizeof(BESS_SGB_t)] = {0,}; + file->write(file, sgb_padding, sizeof(sgb_padding)); + } + + /* BESS Footer */ + + if (file->write(file, &bess_footer, sizeof(bess_footer)) != sizeof(bess_footer)) { + goto error; + } + + errno = 0; +error: + return errno; +} + +int GB_save_state(GB_gameboy_t *gb, const char *path) +{ + FILE *f = fopen(path, "wb"); + if (!f) { + GB_log(gb, "Could not open save state: %s.\n", strerror(errno)); + return errno; + } + virtual_file_t file = { + .write = file_write, + .seek = file_seek, + .tell = file_tell, + .file = f, + }; + int ret = save_state_internal(gb, &file, true); + fclose(f); + return ret; +} + +void GB_save_state_to_buffer(GB_gameboy_t *gb, uint8_t *buffer) +{ + virtual_file_t file = { + .write = buffer_write, + .seek = buffer_seek, + .tell = buffer_tell, + .buffer = (uint8_t *)buffer, + .position = 0, + }; + + save_state_internal(gb, &file, true); + assert(file.position == GB_get_save_state_size(gb)); +} + +void GB_save_state_to_buffer_no_bess(GB_gameboy_t *gb, uint8_t *buffer) +{ + virtual_file_t file = { + .write = buffer_write, + .seek = buffer_seek, + .tell = buffer_tell, + .buffer = (uint8_t *)buffer, + .position = 0, + }; + + save_state_internal(gb, &file, false); + assert(file.position == GB_get_save_state_size_no_bess(gb)); +} + +static bool read_section(virtual_file_t *file, void *dest, uint32_t size, bool fix_broken_windows_saves) +{ + uint32_t saved_size = 0; + if (file->read(file, &saved_size, sizeof(size)) != sizeof(size)) { + return false; + } + + if (fix_broken_windows_saves) { + if (saved_size < 4) { + return false; + } + saved_size -= 4; + file->seek(file, 4, SEEK_CUR); + } + + if (saved_size <= size) { + if (file->read(file, dest, saved_size) != saved_size) { + return false; + } + } + else { + if (file->read(file, dest, size) != size) { + return false; + } + file->seek(file, saved_size - size, SEEK_CUR); + } + + return true; +} + +static void read_bess_buffer(const BESS_buffer_t *buffer, virtual_file_t *file, uint8_t *dest, size_t max_size) +{ + size_t pos = file->tell(file); + file->seek(file, LE32(buffer->offset), SEEK_SET); + file->read(file, dest, MIN(LE32(buffer->size), max_size)); + file->seek(file, pos, SEEK_SET); + + if (LE32(buffer->size) < max_size) { + memset(dest + LE32(buffer->size), 0, max_size - LE32(buffer->size)); + } +} + +static int load_bess_save(GB_gameboy_t *gb, virtual_file_t *file, bool is_sameboy) +{ + char emulator_name[65] = {0,}; + file->seek(file, -sizeof(BESS_footer_t), SEEK_END); + BESS_footer_t footer = {0, }; + file->read(file, &footer, sizeof(footer)); + if (footer.magic != BE32('BESS')) { + // Not a BESS file + if (!is_sameboy) { + GB_log(gb, "The file is not a save state, or is from an incompatible operating system.\n"); + } + return -1; + } + + GB_gameboy_t save; + GB_init(&save, gb->model); + save.cartridge_type = gb->cartridge_type; + + file->seek(file, LE32(footer.start_offset), SEEK_SET); + bool found_core = false; + BESS_CORE_t core = {0,}; + bool found_sgb = false; + BESS_SGB_t sgb = {0,}; + while (true) { + BESS_block_t block; + if (file->read(file, &block, sizeof(block)) != sizeof(block)) goto error; + switch (block.magic) { + case BE32('CORE'): + if (found_core) goto parse_error; + found_core = true; + if (LE32(block.size) > sizeof(core) - sizeof(block)) { + if (file->read(file, &core.header + 1, sizeof(core) - sizeof(block)) != sizeof(core) - sizeof(block)) goto error; + file->seek(file, LE32(block.size) - (sizeof(core) - sizeof(block)), SEEK_CUR); + } + else { + if (file->read(file, &core.header + 1, LE32(block.size)) != LE32(block.size)) goto error; + } + + if (core.major != LE16(1)) { + GB_log(gb, "This save state uses an incompatible version of the BESS specification"); + GB_free(&save); + return -1; + } + + switch (core.family) { + case 'C': + if (!GB_is_cgb(&save)) goto wrong_model; + break; + case 'S': + if (!GB_is_sgb(&save)) goto wrong_model; + break; + case 'G': + if (GB_is_cgb(&save) || GB_is_sgb(&save)) goto wrong_model; + break; + default: + wrong_model: + GB_log(gb, "The save state is for a different model. Try changing the emulated model.\n"); + GB_free(&save); + return -1; + } + + + save.pc = LE16(core.pc); + save.af = LE16(core.af); + save.bc = LE16(core.bc); + save.de = LE16(core.de); + save.hl = LE16(core.hl); + save.sp = LE16(core.sp); + + save.ime = core.ime; + save.interrupt_enable = core.ie; + + save.halted = core.execution_mode == 1; + save.stopped = core.execution_mode == 2; + + // CPU related + + // Determines DMG mode + GB_write_memory(&save, 0xFF00 + GB_IO_KEY0, core.io_registers[GB_IO_KEY0]); + save.boot_rom_finished = core.io_registers[GB_IO_BANK]; + GB_write_memory(&save, 0xFF00 + GB_IO_KEY1, core.io_registers[GB_IO_KEY1]); + if (save.cgb_mode) { + save.cgb_double_speed = core.io_registers[GB_IO_KEY1] & 0x80; + save.object_priority = GB_OBJECT_PRIORITY_INDEX; + } + else { + save.object_priority = GB_OBJECT_PRIORITY_X; + } + + // Timers, Joypad and Serial + GB_write_memory(&save, 0xFF00 + GB_IO_JOYP, core.io_registers[GB_IO_JOYP]); + GB_write_memory(&save, 0xFF00 + GB_IO_SB, core.io_registers[GB_IO_SB]); + save.io_registers[GB_IO_SC] = core.io_registers[GB_IO_SC]; + save.div_counter = core.io_registers[GB_IO_DIV] << 8; + GB_write_memory(&save, 0xFF00 + GB_IO_TIMA, core.io_registers[GB_IO_TIMA]); + GB_write_memory(&save, 0xFF00 + GB_IO_TMA, core.io_registers[GB_IO_TMA]); + GB_write_memory(&save, 0xFF00 + GB_IO_TAC, core.io_registers[GB_IO_TAC]); + + // APU + GB_write_memory(&save, 0xFF00 + GB_IO_NR52, core.io_registers[GB_IO_NR52]); + for (unsigned i = GB_IO_NR10; i < GB_IO_NR52; i++) { + uint8_t value = core.io_registers[i]; + if (i == GB_IO_NR14 || i == GB_IO_NR24 || i == GB_IO_NR34 || i == GB_IO_NR44) { + value &= ~0x80; + } + GB_write_memory(&save, 0xFF00 + i, value); + } + + for (unsigned i = GB_IO_WAV_START; i <= GB_IO_WAV_END; i++) { + GB_write_memory(&save, 0xFF00 + i, core.io_registers[i]); + } + + // PPU + GB_write_memory(&save, 0xFF00 + GB_IO_LCDC, core.io_registers[GB_IO_LCDC]); + GB_write_memory(&save, 0xFF00 + GB_IO_STAT, core.io_registers[GB_IO_STAT]); + GB_write_memory(&save, 0xFF00 + GB_IO_SCY, core.io_registers[GB_IO_SCY]); + GB_write_memory(&save, 0xFF00 + GB_IO_SCX, core.io_registers[GB_IO_SCX]); + GB_write_memory(&save, 0xFF00 + GB_IO_LYC, core.io_registers[GB_IO_LYC]); + save.io_registers[GB_IO_DMA] = core.io_registers[GB_IO_DMA]; + GB_write_memory(&save, 0xFF00 + GB_IO_BGP, core.io_registers[GB_IO_BGP]); + GB_write_memory(&save, 0xFF00 + GB_IO_OBP0, core.io_registers[GB_IO_OBP0]); + GB_write_memory(&save, 0xFF00 + GB_IO_OBP1, core.io_registers[GB_IO_OBP1]); + GB_write_memory(&save, 0xFF00 + GB_IO_WX, core.io_registers[GB_IO_WX]); + GB_write_memory(&save, 0xFF00 + GB_IO_WY, core.io_registers[GB_IO_WY]); + + // Other registers + GB_write_memory(&save, 0xFF00 + GB_IO_VBK, core.io_registers[GB_IO_VBK]); + GB_write_memory(&save, 0xFF00 + GB_IO_HDMA1, core.io_registers[GB_IO_HDMA1]); + GB_write_memory(&save, 0xFF00 + GB_IO_HDMA2, core.io_registers[GB_IO_HDMA2]); + GB_write_memory(&save, 0xFF00 + GB_IO_HDMA3, core.io_registers[GB_IO_HDMA3]); + GB_write_memory(&save, 0xFF00 + GB_IO_HDMA4, core.io_registers[GB_IO_HDMA4]); + GB_write_memory(&save, 0xFF00 + GB_IO_RP, core.io_registers[GB_IO_RP]); + GB_write_memory(&save, 0xFF00 + GB_IO_BGPI, core.io_registers[GB_IO_BGPI]); + GB_write_memory(&save, 0xFF00 + GB_IO_OBPI, core.io_registers[GB_IO_OBPI]); + GB_write_memory(&save, 0xFF00 + GB_IO_OPRI, core.io_registers[GB_IO_OPRI]); + GB_write_memory(&save, 0xFF00 + GB_IO_SVBK, core.io_registers[GB_IO_SVBK]); + + // Interrupts + GB_write_memory(&save, 0xFF00 + GB_IO_IF, core.io_registers[GB_IO_IF]); + + break; + case BE32('NAME'): + if (LE32(block.size) > sizeof(emulator_name) - 1) { + file->seek(file, LE32(block.size), SEEK_CUR); + } + else { + file->read(file, emulator_name, LE32(block.size)); + } + break; + case BE32('INFO'): { + BESS_INFO_t bess_info = {0,}; + if (LE32(block.size) != sizeof(bess_info) - sizeof(block)) goto parse_error; + if (file->read(file, &bess_info.header + 1, LE32(block.size)) != LE32(block.size)) goto error; + if (memcmp(bess_info.title, gb->rom + 0x134, sizeof(bess_info.title))) { + char ascii_title[0x11] = {0,}; + for (unsigned i = 0; i < 0x10; i++) { + if (bess_info.title[i] < 0x20 || bess_info.title[i] > 0x7E) break; + ascii_title[i] = bess_info.title[i]; + } + GB_log(gb, "Save state was made on another ROM: '%s'\n", ascii_title); + } + else if (memcmp(bess_info.checksum, gb->rom + 0x14E, 2)) { + GB_log(gb, "Save state was potentially made on another revision of the same ROM.\n"); + } + break; + } + case BE32('XOAM'): + if (!found_core) goto parse_error; + if (LE32(block.size) != 96) goto parse_error; + file->read(file, save.extra_oam, sizeof(save.extra_oam)); + break; + case BE32('MBC '): + if (!found_core) goto parse_error; + if (LE32(block.size) % 3 != 0) goto parse_error; + for (unsigned i = LE32(block.size); i > 0; i -= 3) { + BESS_MBC_pair_t pair; + file->read(file, &pair, sizeof(pair)); + if (LE16(pair.address) >= 0x8000 && LE16(pair.address) < 0xA000) goto parse_error; + if (LE16(pair.address) >= 0xC000) goto parse_error; + GB_write_memory(&save, LE16(pair.address), pair.value); + } + break; + case BE32('RTC '): + if (!found_core) goto parse_error; + BESS_RTC_t bess_rtc; + if (LE32(block.size) != sizeof(bess_rtc) - sizeof(block)) goto parse_error; + if (file->read(file, &bess_rtc.header + 1, LE32(block.size)) != LE32(block.size)) goto error; + if (!gb->cartridge_type->has_rtc || gb->cartridge_type->mbc_type != GB_MBC3) break; + save.rtc_real.seconds = bess_rtc.real.seconds; + save.rtc_real.minutes = bess_rtc.real.minutes; + save.rtc_real.hours = bess_rtc.real.hours; + save.rtc_real.days = bess_rtc.real.days; + save.rtc_real.high = bess_rtc.real.high; + save.rtc_latched.seconds = bess_rtc.latched.seconds; + save.rtc_latched.minutes = bess_rtc.latched.minutes; + save.rtc_latched.hours = bess_rtc.latched.hours; + save.rtc_latched.days = bess_rtc.latched.days; + save.rtc_latched.high = bess_rtc.latched.high; + if (gb->rtc_mode == GB_RTC_MODE_SYNC_TO_HOST) { + save.last_rtc_second = MIN(LE64(bess_rtc.last_rtc_second), time(NULL)); + } + + break; + case BE32('HUC3'): + if (!found_core) goto parse_error; + BESS_HUC3_t bess_huc3; + if (LE32(block.size) != sizeof(bess_huc3) - sizeof(block)) goto parse_error; + if (file->read(file, &bess_huc3.header + 1, LE32(block.size)) != LE32(block.size)) goto error; + if (gb->cartridge_type->mbc_type != GB_HUC3) break; + if (gb->rtc_mode == GB_RTC_MODE_SYNC_TO_HOST) { + save.last_rtc_second = MIN(LE64(bess_huc3.data.last_rtc_second), time(NULL)); + } + save.huc3_minutes = LE16(bess_huc3.data.minutes); + save.huc3_days = LE16(bess_huc3.data.days); + save.huc3_alarm_minutes = LE16(bess_huc3.data.alarm_minutes); + save.huc3_alarm_days = LE16(bess_huc3.data.alarm_days); + save.huc3_alarm_enabled = bess_huc3.data.alarm_enabled; + break; + case BE32('TPP1'): + if (!found_core) goto parse_error; + BESS_TPP1_t bess_tpp1; + if (LE32(block.size) != sizeof(bess_tpp1) - sizeof(block)) goto parse_error; + if (file->read(file, &bess_tpp1.header + 1, LE32(block.size)) != LE32(block.size)) goto error; + if (gb->cartridge_type->mbc_type != GB_TPP1) break; + if (gb->rtc_mode == GB_RTC_MODE_SYNC_TO_HOST) { + save.last_rtc_second = MIN(LE64(bess_tpp1.last_rtc_second), time(NULL)); + } + unrolled for (unsigned i = 4; i--;) { + save.rtc_real.data[i ^ 3] = bess_tpp1.real_rtc_data[i]; + save.rtc_latched.data[i ^ 3] = bess_tpp1.latched_rtc_data[i]; + } + save.tpp1_mr4 = bess_tpp1.mr4; + break; + case BE32('SGB '): + if (!found_core) goto parse_error; + if (!gb->sgb) goto parse_error; + if (LE32(block.size) > sizeof(sgb) - sizeof(block)) { + if (file->read(file, &sgb.header + 1, sizeof(sgb) - sizeof(block)) != sizeof(sgb) - sizeof(block)) goto error; + file->seek(file, LE32(block.size) - (sizeof(sgb) - sizeof(block)), SEEK_CUR); + } + else { + if (file->read(file, &sgb.header + 1, LE32(block.size)) != LE32(block.size)) goto error; + } + found_sgb = true; + break; + case BE32('END '): + if (!found_core) goto parse_error; + if (LE32(block.size) != 0) goto parse_error; + goto done; + default: + file->seek(file, LE32(block.size), SEEK_CUR); + break; + } + } +done: + save.mbc_ram_size = gb->mbc_ram_size; + memcpy(gb, &save, GB_SECTION_OFFSET(unsaved)); + assert(GB_get_save_state_size(gb) == GB_get_save_state_size(&save)); + GB_free(&save); + read_bess_buffer(&core.ram, file, gb->ram, gb->ram_size); + read_bess_buffer(&core.vram, file, gb->vram, gb->vram_size); + read_bess_buffer(&core.mbc_ram, file, gb->mbc_ram, gb->mbc_ram_size); + read_bess_buffer(&core.oam, file, gb->oam, sizeof(gb->oam)); + read_bess_buffer(&core.hram, file, gb->hram, sizeof(gb->hram)); + read_bess_buffer(&core.background_palettes, file, gb->background_palettes_data, sizeof(gb->background_palettes_data)); + read_bess_buffer(&core.sprite_palettes, file, gb->sprite_palettes_data, sizeof(gb->sprite_palettes_data)); + if (gb->sgb) { + memset(gb->sgb, 0, sizeof(*gb->sgb)); + GB_sgb_load_default_data(gb); + if (gb->boot_rom_finished) { + gb->sgb->intro_animation = GB_SGB_INTRO_ANIMATION_LENGTH; + if (!found_sgb) { + gb->sgb->disable_commands = true; + } + else { + read_bess_buffer(&sgb.border_tiles, file, gb->sgb->border.tiles, sizeof(gb->sgb->border.tiles)); + read_bess_buffer(&sgb.border_tilemap, file, (void *)gb->sgb->border.map, sizeof(gb->sgb->border.map)); + read_bess_buffer(&sgb.border_palettes, file, (void *)gb->sgb->border.palette, sizeof(gb->sgb->border.palette)); + + read_bess_buffer(&sgb.active_palettes, file, (void *)gb->sgb->effective_palettes, sizeof(gb->sgb->effective_palettes)); + read_bess_buffer(&sgb.ram_palettes, file, (void *)gb->sgb->ram_palettes, sizeof(gb->sgb->ram_palettes)); + read_bess_buffer(&sgb.attribute_map, file, (void *)gb->sgb->attribute_map, sizeof(gb->sgb->attribute_map)); + read_bess_buffer(&sgb.attribute_files, file, (void *)gb->sgb->attribute_files, sizeof(gb->sgb->attribute_files)); + + gb->sgb->effective_palettes[12] = gb->sgb->effective_palettes[8] = + gb->sgb->effective_palettes[4] = gb->sgb->effective_palettes[0]; + + gb->sgb->player_count = sgb.multiplayer_state >> 4; + gb->sgb->current_player = sgb.multiplayer_state & 0xF; + if (gb->sgb->player_count > 4 || gb->sgb->player_count == 3 || gb->sgb->player_count == 0) { + gb->sgb->player_count = 1; + gb->sgb->current_player = 0; + } + } + } + else { + // Effectively reset if didn't finish the boot ROM + gb->pc = 0; + } + } + if (emulator_name[0]) { + GB_log(gb, "Save state imported from %s.\n", emulator_name); + } + else { + GB_log(gb, "Save state imported from another emulator.\n"); // SameBoy always contains a NAME block + } + for (unsigned i = 0; i < 32; i++) { + GB_palette_changed(gb, false, i * 2); + GB_palette_changed(gb, true, i * 2); + } + return 0; +parse_error: + errno = -1; +error: + if (emulator_name[0]) { + GB_log(gb, "Attempted to import a save state from %s, but the save state is invalid.\n", emulator_name); + } + else { + GB_log(gb, "Attempted to import a save state from a different emulator or incompatible version, but the save state is invalid.\n"); + } + GB_free(&save); + return errno; +} + +static int load_state_internal(GB_gameboy_t *gb, virtual_file_t *file) { GB_gameboy_t save; @@ -230,184 +1163,129 @@ int GB_load_state(GB_gameboy_t *gb, const char *path) /* ...Except ram size, we use it to detect old saves with incorrect ram sizes */ save.ram_size = 0; - FILE *f = fopen(path, "rb"); - if (!f) { - GB_log(gb, "Could not open save state: %s.\n", strerror(errno)); - return errno; - } - bool fix_broken_windows_saves = false; - if (fread(GB_GET_SECTION(&save, header), 1, GB_SECTION_SIZE(header), f) != GB_SECTION_SIZE(header)) goto error; + + if (file->read(file, GB_GET_SECTION(&save, header), GB_SECTION_SIZE(header)) != GB_SECTION_SIZE(header)) return errno; if (save.magic == 0) { - /* Potentially legacy, broken Windows save state */ - fseek(f, 4, SEEK_SET); - if (fread(GB_GET_SECTION(&save, header), 1, GB_SECTION_SIZE(header), f) != GB_SECTION_SIZE(header)) goto error; + /* Potentially legacy, broken Windows save state*/ + + file->seek(file, 4, SEEK_SET); + if (file->read(file, GB_GET_SECTION(&save, header), GB_SECTION_SIZE(header)) != GB_SECTION_SIZE(header)) return errno; fix_broken_windows_saves = true; } if (gb->magic != save.magic) { - GB_log(gb, "The file is not a save state, or is from an incompatible operating system.\n"); - return false; + return load_bess_save(gb, file, false); } - if (!READ_SECTION(&save, f, core_state)) goto error; - if (!READ_SECTION(&save, f, dma )) goto error; - if (!READ_SECTION(&save, f, mbc )) goto error; - if (!READ_SECTION(&save, f, hram )) goto error; - if (!READ_SECTION(&save, f, timing )) goto error; - if (!READ_SECTION(&save, f, apu )) goto error; - if (!READ_SECTION(&save, f, rtc )) goto error; - if (!READ_SECTION(&save, f, video )) goto error; +#define READ_SECTION(gb, file, section) read_section(file, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section), fix_broken_windows_saves) + if (!READ_SECTION(&save, file, core_state)) return errno ?: EIO; + if (!READ_SECTION(&save, file, dma )) return errno ?: EIO; + if (!READ_SECTION(&save, file, mbc )) return errno ?: EIO; + if (!READ_SECTION(&save, file, hram )) return errno ?: EIO; + if (!READ_SECTION(&save, file, timing )) return errno ?: EIO; + if (!READ_SECTION(&save, file, apu )) return errno ?: EIO; + if (!READ_SECTION(&save, file, rtc )) return errno ?: EIO; + if (!READ_SECTION(&save, file, video )) return errno ?: EIO; +#undef READ_SECTION - if (!verify_and_update_state_compatibility(gb, &save)) { - errno = -1; - goto error; + + bool attempt_bess = false; + if (!verify_and_update_state_compatibility(gb, &save, &attempt_bess)) { + if (attempt_bess) { + return load_bess_save(gb, file, true); + } + return errno; } if (GB_is_hle_sgb(gb)) { - if (!read_section(f, gb->sgb, sizeof(*gb->sgb), false)) goto error; + if (!read_section(file, gb->sgb, sizeof(*gb->sgb), false)) return errno ?: EIO; } memset(gb->mbc_ram + save.mbc_ram_size, 0xFF, gb->mbc_ram_size - save.mbc_ram_size); - if (fread(gb->mbc_ram, 1, save.mbc_ram_size, f) != save.mbc_ram_size) { - fclose(f); - return EIO; + if (file->read(file, gb->mbc_ram, save.mbc_ram_size) != save.mbc_ram_size) { + return errno ?: EIO; } - if (fread(gb->ram, 1, gb->ram_size, f) != gb->ram_size) { - fclose(f); - return EIO; + if (file->read(file, gb->ram, gb->ram_size) != gb->ram_size) { + return errno ?: EIO; } - + /* Fix for 0.11 save states that allocate twice the amount of RAM in CGB instances */ - fseek(f, save.ram_size - gb->ram_size, SEEK_CUR); + file->seek(file, save.ram_size - gb->ram_size, SEEK_CUR); - if (fread(gb->vram, 1, gb->vram_size, f) != gb->vram_size) { - fclose(f); - return EIO; + if (file->read(file, gb->vram, gb->vram_size) != gb->vram_size) { + return errno ?: EIO; } size_t orig_ram_size = gb->ram_size; memcpy(gb, &save, sizeof(save)); gb->ram_size = orig_ram_size; - - errno = 0; - - sanitize_state(gb); - -error: - fclose(f); - return errno; -} - -#undef READ_SECTION - -/* An read-like function for buffer-copying */ -static size_t buffer_read(void *dest, size_t length, const uint8_t **buffer, size_t *buffer_length) -{ - if (length > *buffer_length) { - length = *buffer_length; - } - - memcpy(dest, *buffer, length); - *buffer += length; - *buffer_length -= length; - - return length; -} - -static bool buffer_read_section(const uint8_t **buffer, size_t *buffer_length, void *dest, uint32_t size, bool fix_broken_windows_saves) -{ - uint32_t saved_size = 0; - if (buffer_read(&saved_size, sizeof(size), buffer, buffer_length) != sizeof(size)) { - return false; - } - - if (saved_size > *buffer_length) return false; - - if (fix_broken_windows_saves) { - if (saved_size < 4) { - return false; - } - saved_size -= 4; - *buffer += 4; - } - - if (saved_size <= size) { - if (buffer_read(dest, saved_size, buffer, buffer_length) != saved_size) { - return false; - } - } - else { - if (buffer_read(dest, size, buffer, buffer_length) != size) { - return false; - } - *buffer += saved_size - size; - *buffer_length -= saved_size - size; - } - - return true; -} - -#define READ_SECTION(gb, buffer, length, section) buffer_read_section(&buffer, &length, GB_GET_SECTION(gb, section), GB_SECTION_SIZE(section), fix_broken_windows_saves) -int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t length) -{ - GB_gameboy_t save; - - /* Every unread value should be kept the same. */ - memcpy(&save, gb, sizeof(save)); - bool fix_broken_windows_saves = false; - - if (buffer_read(GB_GET_SECTION(&save, header), GB_SECTION_SIZE(header), &buffer, &length) != GB_SECTION_SIZE(header)) return -1; - if (save.magic == 0) { - /* Potentially legacy, broken Windows save state*/ - buffer -= GB_SECTION_SIZE(header) - 4; - length += GB_SECTION_SIZE(header) - 4; - if (buffer_read(GB_GET_SECTION(&save, header), GB_SECTION_SIZE(header), &buffer, &length) != GB_SECTION_SIZE(header)) return -1; - fix_broken_windows_saves = true; - } - if (gb->magic != save.magic) { - GB_log(gb, "The file is not a save state, or is from an incompatible operating system.\n"); - return false; - } - if (!READ_SECTION(&save, buffer, length, core_state)) return -1; - if (!READ_SECTION(&save, buffer, length, dma )) return -1; - if (!READ_SECTION(&save, buffer, length, mbc )) return -1; - if (!READ_SECTION(&save, buffer, length, hram )) return -1; - if (!READ_SECTION(&save, buffer, length, timing )) return -1; - if (!READ_SECTION(&save, buffer, length, apu )) return -1; - if (!READ_SECTION(&save, buffer, length, rtc )) return -1; - if (!READ_SECTION(&save, buffer, length, video )) return -1; - - - if (!verify_and_update_state_compatibility(gb, &save)) { - return -1; - } - - if (GB_is_hle_sgb(gb)) { - if (!buffer_read_section(&buffer, &length, gb->sgb, sizeof(*gb->sgb), false)) return -1; - } - - memset(gb->mbc_ram + save.mbc_ram_size, 0xFF, gb->mbc_ram_size - save.mbc_ram_size); - if (buffer_read(gb->mbc_ram, save.mbc_ram_size, &buffer, &length) != save.mbc_ram_size) { - return -1; - } - - if (buffer_read(gb->ram, gb->ram_size, &buffer, &length) != gb->ram_size) { - return -1; - } - - if (buffer_read(gb->vram, gb->vram_size, &buffer, &length) != gb->vram_size) { - return -1; - } - - /* Fix for 0.11 save states that allocate twice the amount of RAM in CGB instances */ - buffer += save.ram_size - gb->ram_size; - length -= save.ram_size - gb->ram_size; - - memcpy(gb, &save, sizeof(save)); sanitize_state(gb); return 0; } -#undef READ_SECTION +int GB_load_state(GB_gameboy_t *gb, const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) { + GB_log(gb, "Could not open save state: %s.\n", strerror(errno)); + return errno; + } + virtual_file_t file = { + .read = file_read, + .seek = file_seek, + .tell = file_tell, + .file = f, + }; + int ret = load_state_internal(gb, &file); + fclose(f); + return ret; +} + +int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t length) +{ + virtual_file_t file = { + .read = buffer_read, + .seek = buffer_seek, + .tell = buffer_tell, + .buffer = (uint8_t *)buffer, + .position = 0, + .size = length, + }; + + return load_state_internal(gb, &file); +} + + +bool GB_is_stave_state(const char *path) +{ + bool ret = false; + FILE *f = fopen(path, "rb"); + if (!f) return false; + uint32_t magic = 0; + fread(&magic, sizeof(magic), 1, f); + if (magic == state_magic()) { + ret = true; + goto exit; + } + + // Legacy corrupted Windows save state + if (magic == 0) { + fread(&magic, sizeof(magic), 1, f); + if (magic == state_magic()) { + ret = true; + goto exit; + } + } + + fseek(f, -sizeof(magic), SEEK_END); + fread(&magic, sizeof(magic), 1, f); + if (magic == BE32('BESS')) { + ret = true; + } + +exit: + fclose(f); + return ret; +} diff --git a/bsnes/gb/Core/save_state.h b/bsnes/gb/Core/save_state.h index 8e5fc4e0..79e8c061 100644 --- a/bsnes/gb/Core/save_state.h +++ b/bsnes/gb/Core/save_state.h @@ -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 */ diff --git a/bsnes/gb/Core/sgb.c b/bsnes/gb/Core/sgb.c index c77b0db6..894ae966 100644 --- a/bsnes/gb/Core/sgb.c +++ b/bsnes/gb/Core/sgb.c @@ -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) { diff --git a/bsnes/gb/Core/sgb.h b/bsnes/gb/Core/sgb.h index aae9f755..1e67835d 100644 --- a/bsnes/gb/Core/sgb.h +++ b/bsnes/gb/Core/sgb.h @@ -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); diff --git a/bsnes/gb/Core/sm83_cpu.c b/bsnes/gb/Core/sm83_cpu.c index 3b3ecebb..09499bc4 100644 --- a/bsnes/gb/Core/sm83_cpu.c +++ b/bsnes/gb/Core/sm83_cpu.c @@ -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; diff --git a/bsnes/gb/Core/symbol_hash.c b/bsnes/gb/Core/symbol_hash.c index 75a7837d..a3718b83 100644 --- a/bsnes/gb/Core/symbol_hash.c +++ b/bsnes/gb/Core/symbol_hash.c @@ -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) { diff --git a/bsnes/gb/Core/timing.c b/bsnes/gb/Core/timing.c index 965ba27c..a755ac52 100644 --- a/bsnes/gb/Core/timing.c +++ b/bsnes/gb/Core/timing.c @@ -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; - } - } - } - } - } - } -} diff --git a/bsnes/gb/Core/timing.h b/bsnes/gb/Core/timing.h index d4fa07f9..07e04734 100644 --- a/bsnes/gb/Core/timing.h +++ b/bsnes/gb/Core/timing.h @@ -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); diff --git a/bsnes/gb/FreeDesktop/AppIcon/128x128.png b/bsnes/gb/FreeDesktop/AppIcon/128x128.png new file mode 100644 index 00000000..6303f237 Binary files /dev/null and b/bsnes/gb/FreeDesktop/AppIcon/128x128.png differ diff --git a/bsnes/gb/FreeDesktop/AppIcon/16x16.png b/bsnes/gb/FreeDesktop/AppIcon/16x16.png new file mode 100644 index 00000000..6c3f81d0 Binary files /dev/null and b/bsnes/gb/FreeDesktop/AppIcon/16x16.png differ diff --git a/bsnes/gb/FreeDesktop/AppIcon/256x256.png b/bsnes/gb/FreeDesktop/AppIcon/256x256.png new file mode 100644 index 00000000..e2a6ceee Binary files /dev/null and b/bsnes/gb/FreeDesktop/AppIcon/256x256.png differ diff --git a/bsnes/gb/FreeDesktop/AppIcon/32x32.png b/bsnes/gb/FreeDesktop/AppIcon/32x32.png new file mode 100644 index 00000000..d7f2e4eb Binary files /dev/null and b/bsnes/gb/FreeDesktop/AppIcon/32x32.png differ diff --git a/bsnes/gb/FreeDesktop/AppIcon/512x512.png b/bsnes/gb/FreeDesktop/AppIcon/512x512.png new file mode 100644 index 00000000..1608c71f Binary files /dev/null and b/bsnes/gb/FreeDesktop/AppIcon/512x512.png differ diff --git a/bsnes/gb/FreeDesktop/AppIcon/64x64.png b/bsnes/gb/FreeDesktop/AppIcon/64x64.png new file mode 100644 index 00000000..4a54e94e Binary files /dev/null and b/bsnes/gb/FreeDesktop/AppIcon/64x64.png differ diff --git a/bsnes/gb/FreeDesktop/Cartridge/128x128.png b/bsnes/gb/FreeDesktop/Cartridge/128x128.png new file mode 100644 index 00000000..bc14d792 Binary files /dev/null and b/bsnes/gb/FreeDesktop/Cartridge/128x128.png differ diff --git a/bsnes/gb/FreeDesktop/Cartridge/16x16.png b/bsnes/gb/FreeDesktop/Cartridge/16x16.png new file mode 100644 index 00000000..3cbd9ae7 Binary files /dev/null and b/bsnes/gb/FreeDesktop/Cartridge/16x16.png differ diff --git a/bsnes/gb/FreeDesktop/Cartridge/256x256.png b/bsnes/gb/FreeDesktop/Cartridge/256x256.png new file mode 100644 index 00000000..14258ea8 Binary files /dev/null and b/bsnes/gb/FreeDesktop/Cartridge/256x256.png differ diff --git a/bsnes/gb/FreeDesktop/Cartridge/32x32.png b/bsnes/gb/FreeDesktop/Cartridge/32x32.png new file mode 100644 index 00000000..c8ef62fd Binary files /dev/null and b/bsnes/gb/FreeDesktop/Cartridge/32x32.png differ diff --git a/bsnes/gb/FreeDesktop/Cartridge/512x512.png b/bsnes/gb/FreeDesktop/Cartridge/512x512.png new file mode 100644 index 00000000..71314f72 Binary files /dev/null and b/bsnes/gb/FreeDesktop/Cartridge/512x512.png differ diff --git a/bsnes/gb/FreeDesktop/Cartridge/64x64.png b/bsnes/gb/FreeDesktop/Cartridge/64x64.png new file mode 100644 index 00000000..8835f79c Binary files /dev/null and b/bsnes/gb/FreeDesktop/Cartridge/64x64.png differ diff --git a/bsnes/gb/FreeDesktop/ColorCartridge/128x128.png b/bsnes/gb/FreeDesktop/ColorCartridge/128x128.png new file mode 100644 index 00000000..da4757e8 Binary files /dev/null and b/bsnes/gb/FreeDesktop/ColorCartridge/128x128.png differ diff --git a/bsnes/gb/FreeDesktop/ColorCartridge/16x16.png b/bsnes/gb/FreeDesktop/ColorCartridge/16x16.png new file mode 100644 index 00000000..50e6b2b4 Binary files /dev/null and b/bsnes/gb/FreeDesktop/ColorCartridge/16x16.png differ diff --git a/bsnes/gb/FreeDesktop/ColorCartridge/256x256.png b/bsnes/gb/FreeDesktop/ColorCartridge/256x256.png new file mode 100644 index 00000000..186f5d30 Binary files /dev/null and b/bsnes/gb/FreeDesktop/ColorCartridge/256x256.png differ diff --git a/bsnes/gb/FreeDesktop/ColorCartridge/32x32.png b/bsnes/gb/FreeDesktop/ColorCartridge/32x32.png new file mode 100644 index 00000000..47e45b50 Binary files /dev/null and b/bsnes/gb/FreeDesktop/ColorCartridge/32x32.png differ diff --git a/bsnes/gb/FreeDesktop/ColorCartridge/512x512.png b/bsnes/gb/FreeDesktop/ColorCartridge/512x512.png new file mode 100644 index 00000000..715d68f3 Binary files /dev/null and b/bsnes/gb/FreeDesktop/ColorCartridge/512x512.png differ diff --git a/bsnes/gb/FreeDesktop/ColorCartridge/64x64.png b/bsnes/gb/FreeDesktop/ColorCartridge/64x64.png new file mode 100644 index 00000000..403e307a Binary files /dev/null and b/bsnes/gb/FreeDesktop/ColorCartridge/64x64.png differ diff --git a/bsnes/gb/FreeDesktop/sameboy.desktop b/bsnes/gb/FreeDesktop/sameboy.desktop new file mode 100644 index 00000000..80d0902f --- /dev/null +++ b/bsnes/gb/FreeDesktop/sameboy.desktop @@ -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 diff --git a/bsnes/gb/FreeDesktop/sameboy.xml b/bsnes/gb/FreeDesktop/sameboy.xml new file mode 100644 index 00000000..18123edc --- /dev/null +++ b/bsnes/gb/FreeDesktop/sameboy.xml @@ -0,0 +1,23 @@ + + + + Game Boy ROM + + + + + + + Game Boy Color ROM + + + + + + + Game Boy ISX binary + + + + + diff --git a/bsnes/gb/GNUmakefile b/bsnes/gb/GNUmakefile index b94dfa0c..e88ac418 100644 --- a/bsnes/gb/GNUmakefile +++ b/bsnes/gb/GNUmakefile @@ -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 diff --git a/bsnes/gb/JoyKit/ControllerConfiguration.inc b/bsnes/gb/JoyKit/ControllerConfiguration.inc index ea3ba9a4..fb7df630 100644 --- a/bsnes/gb/JoyKit/ControllerConfiguration.inc +++ b/bsnes/gb/JoyKit/ControllerConfiguration.inc @@ -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), diff --git a/bsnes/gb/JoyKit/JOYAxis.h b/bsnes/gb/JoyKit/JOYAxis.h index 5a4c1669..8d4b7abe 100644 --- a/bsnes/gb/JoyKit/JOYAxis.h +++ b/bsnes/gb/JoyKit/JOYAxis.h @@ -1,4 +1,5 @@ #import +#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 diff --git a/bsnes/gb/JoyKit/JOYAxis.m b/bsnes/gb/JoyKit/JOYAxis.m index 169eaee8..afe90d26 100644 --- a/bsnes/gb/JoyKit/JOYAxis.m +++ b/bsnes/gb/JoyKit/JOYAxis.m @@ -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 diff --git a/bsnes/gb/JoyKit/JOYButton.h b/bsnes/gb/JoyKit/JOYButton.h index f732c8e6..6a67c6c1 100644 --- a/bsnes/gb/JoyKit/JOYButton.h +++ b/bsnes/gb/JoyKit/JOYButton.h @@ -26,6 +26,16 @@ typedef enum { JOYButtonUsageDPadRight, JOYButtonUsageDPadUp, JOYButtonUsageDPadDown, + + JOYButtonUsageSlider, + JOYButtonUsageDial, + JOYButtonUsageWheel, + + JOYButtonUsageRudder, + JOYButtonUsageThrottle, + JOYButtonUsageAccelerator, + JOYButtonUsageBrake, + JOYButtonUsageNonGenericMax, JOYButtonUsageGeneric0 = 0x10000, diff --git a/bsnes/gb/JoyKit/JOYButton.m b/bsnes/gb/JoyKit/JOYButton.m index 3e6026d1..18970cde 100644 --- a/bsnes/gb/JoyKit/JOYButton.m +++ b/bsnes/gb/JoyKit/JOYButton.m @@ -1,5 +1,6 @@ #import "JOYButton.h" #import "JOYElement.h" +#import @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 diff --git a/bsnes/gb/JoyKit/JOYController.h b/bsnes/gb/JoyKit/JOYController.h index 9ed7cf7b..8f5f6f44 100644 --- a/bsnes/gb/JoyKit/JOYController.h +++ b/bsnes/gb/JoyKit/JOYController.h @@ -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 *) hats; - (void)setRumbleAmplitude:(double)amp; - (void)setPlayerLEDs:(uint8_t)mask; +- (uint8_t)LEDMaskForPlayer:(unsigned)player; @property (readonly, getter=isConnected) bool connected; @end diff --git a/bsnes/gb/JoyKit/JOYController.m b/bsnes/gb/JoyKit/JOYController.m index ca2d1b18..9b33b00b 100644 --- a/bsnes/gb/JoyKit/JOYController.m +++ b/bsnes/gb/JoyKit/JOYController.m @@ -7,6 +7,9 @@ #import "JOYEmulatedButton.h" #include +#include +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 *controllers; // Physical controllers static NSMutableArray *exposedControllers; // Logical controllers @@ -35,7 +40,6 @@ static NSDictionary *hacksByManufacturer = nil; static NSMutableSet> *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]; diff --git a/bsnes/gb/JoyKit/JOYEmulatedButton.m b/bsnes/gb/JoyKit/JOYEmulatedButton.m index 1ebed3ae..b62670a6 100644 --- a/bsnes/gb/JoyKit/JOYEmulatedButton.m +++ b/bsnes/gb/JoyKit/JOYEmulatedButton.m @@ -1,4 +1,5 @@ #import "JOYEmulatedButton.h" +#import @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; } diff --git a/bsnes/gb/JoyKit/JOYHat.m b/bsnes/gb/JoyKit/JOYHat.m index 743e49c3..b5a18f0b 100644 --- a/bsnes/gb/JoyKit/JOYHat.m +++ b/bsnes/gb/JoyKit/JOYHat.m @@ -1,5 +1,6 @@ #import "JOYHat.h" #import "JOYElement.h" +#import @implementation JOYHat { @@ -27,6 +28,7 @@ if (!self) return self; _element = element; + _state = -1; return self; } diff --git a/bsnes/gb/LICENSE b/bsnes/gb/LICENSE index 17619e99..3303e0d7 100644 --- a/bsnes/gb/LICENSE +++ b/bsnes/gb/LICENSE @@ -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 diff --git a/bsnes/gb/Makefile b/bsnes/gb/Makefile index c3d030aa..f6e4c4c0 100644 --- a/bsnes/gb/Makefile +++ b/bsnes/gb/Makefile @@ -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 diff --git a/bsnes/gb/OpenDialog/cocoa.m b/bsnes/gb/OpenDialog/cocoa.m index 76b9606b..aeeb98aa 100644 --- a/bsnes/gb/OpenDialog/cocoa.m +++ b/bsnes/gb/OpenDialog/cocoa.m @@ -18,3 +18,21 @@ char *do_open_rom_dialog(void) return NULL; } } + +char *do_open_folder_dialog(void) +{ + @autoreleasepool { + NSWindow *key = [NSApp keyWindow]; + NSOpenPanel *dialog = [NSOpenPanel openPanel]; + dialog.title = @"Select Boot ROMs Folder"; + dialog.canChooseDirectories = true; + dialog.canChooseFiles = false; + [dialog runModal]; + [key makeKeyAndOrderFront:nil]; + NSString *ret = [[[dialog URLs] firstObject] path]; + if (ret) { + return strdup(ret.UTF8String); + } + return NULL; + } +} diff --git a/bsnes/gb/OpenDialog/gtk.c b/bsnes/gb/OpenDialog/gtk.c index 5b1caa3d..378dcb4e 100644 --- a/bsnes/gb/OpenDialog/gtk.c +++ b/bsnes/gb/OpenDialog/gtk.c @@ -6,6 +6,7 @@ #include #define GTK_FILE_CHOOSER_ACTION_OPEN 0 +#define GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER 2 #define GTK_RESPONSE_ACCEPT -3 #define GTK_RESPONSE_CANCEL -6 @@ -111,3 +112,71 @@ lazy_error: fprintf(stderr, "Failed to display GTK dialog\n"); return NULL; } + +char *do_open_folder_dialog(void) +{ + static void *handle = NULL; + + TRY_DLOPEN("libgtk-3.so"); + TRY_DLOPEN("libgtk-3.so.0"); + TRY_DLOPEN("libgtk-2.so"); + TRY_DLOPEN("libgtk-2.so.0"); + + if (!handle) { + goto lazy_error; + } + + + LAZY(gtk_init_check); + LAZY(gtk_file_chooser_dialog_new); + LAZY(gtk_dialog_run); + LAZY(g_free); + LAZY(gtk_widget_destroy); + LAZY(gtk_file_chooser_get_filename); + LAZY(g_log_set_default_handler); + LAZY(gtk_file_filter_new); + LAZY(gtk_file_filter_add_pattern); + LAZY(gtk_file_filter_set_name); + LAZY(gtk_file_chooser_add_filter); + LAZY(gtk_events_pending); + LAZY(gtk_main_iteration); + + /* Shut up GTK */ + g_log_set_default_handler(nop, NULL); + + gtk_init_check(0, 0); + + + void *dialog = gtk_file_chooser_dialog_new("Select Boot ROMs Folder", + 0, + GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, + "_Cancel", GTK_RESPONSE_CANCEL, + "_Open", GTK_RESPONSE_ACCEPT, + NULL ); + + + int res = gtk_dialog_run (dialog); + char *ret = NULL; + + if (res == GTK_RESPONSE_ACCEPT) { + char *filename; + filename = gtk_file_chooser_get_filename(dialog); + ret = strdup(filename); + g_free(filename); + } + + while (gtk_events_pending()) { + gtk_main_iteration(); + } + + gtk_widget_destroy(dialog); + + while (gtk_events_pending()) { + gtk_main_iteration(); + } + return ret; + +lazy_error: + fprintf(stderr, "Failed to display GTK dialog\n"); + return NULL; +} diff --git a/bsnes/gb/OpenDialog/open_dialog.h b/bsnes/gb/OpenDialog/open_dialog.h index 85e5721f..6d7fb5b2 100644 --- a/bsnes/gb/OpenDialog/open_dialog.h +++ b/bsnes/gb/OpenDialog/open_dialog.h @@ -2,5 +2,5 @@ #define open_rom_h char *do_open_rom_dialog(void); - +char *do_open_folder_dialog(void); #endif /* open_rom_h */ diff --git a/bsnes/gb/OpenDialog/windows.c b/bsnes/gb/OpenDialog/windows.c index 52e281da..e7110320 100644 --- a/bsnes/gb/OpenDialog/windows.c +++ b/bsnes/gb/OpenDialog/windows.c @@ -1,10 +1,11 @@ #include +#include #include "open_dialog.h" char *do_open_rom_dialog(void) { OPENFILENAMEW dialog; - wchar_t filename[MAX_PATH] = {0}; + static wchar_t filename[MAX_PATH] = {0}; memset(&dialog, 0, sizeof(dialog)); dialog.lStructSize = sizeof(dialog); @@ -25,3 +26,32 @@ char *do_open_rom_dialog(void) return NULL; } + +char *do_open_folder_dialog(void) +{ + + BROWSEINFOW dialog; + memset(&dialog, 0, sizeof(dialog)); + + dialog.ulFlags = BIF_USENEWUI; + dialog.lpszTitle = L"Select Boot ROMs Folder"; + + OleInitialize(NULL); + + LPITEMIDLIST list = SHBrowseForFolderW(&dialog); + static wchar_t filename[MAX_PATH] = {0}; + + if (list) { + if (!SHGetPathFromIDListW(list, filename)) { + OleUninitialize(); + return NULL; + } + char *ret = malloc(MAX_PATH * 4); + WideCharToMultiByte(CP_UTF8, 0, filename, sizeof(filename), ret, MAX_PATH * 4, NULL, NULL); + CoTaskMemFree(list); + OleUninitialize(); + return ret; + } + OleUninitialize(); + return NULL; +} diff --git a/bsnes/gb/QuickLook/Info.plist b/bsnes/gb/QuickLook/Info.plist index b01aae1c..9b369ec4 100644 --- a/bsnes/gb/QuickLook/Info.plist +++ b/bsnes/gb/QuickLook/Info.plist @@ -48,7 +48,7 @@ CFPlugInUnloadFunction NSHumanReadableCopyright - Copyright © 2015-2020 Lior Halphon + Copyright © 2015-2021 Lior Halphon QLNeedsToBeRunInMainThread QLPreviewHeight diff --git a/bsnes/gb/QuickLook/exports.sym b/bsnes/gb/QuickLook/exports.sym index f9796877..2e7fddeb 100644 --- a/bsnes/gb/QuickLook/exports.sym +++ b/bsnes/gb/QuickLook/exports.sym @@ -1,3 +1 @@ -_DeallocQuickLookGeneratorPluginType -_QuickLookGeneratorQueryInterface _QuickLookGeneratorPluginFactory diff --git a/bsnes/gb/QuickLook/main.c b/bsnes/gb/QuickLook/main.c index 1d1676ac..4e45313b 100644 --- a/bsnes/gb/QuickLook/main.c +++ b/bsnes/gb/QuickLook/main.c @@ -41,12 +41,12 @@ typedef struct __QuickLookGeneratorPluginType // Forward declaration for the IUnknown implementation. // -QuickLookGeneratorPluginType *AllocQuickLookGeneratorPluginType(CFUUIDRef inFactoryID); -void DeallocQuickLookGeneratorPluginType(QuickLookGeneratorPluginType *thisInstance); -HRESULT QuickLookGeneratorQueryInterface(void *thisInstance, REFIID iid, LPVOID *ppv); -void *QuickLookGeneratorPluginFactory(CFAllocatorRef allocator, CFUUIDRef typeID); -ULONG QuickLookGeneratorPluginAddRef(void *thisInstance); -ULONG QuickLookGeneratorPluginRelease(void *thisInstance); +static QuickLookGeneratorPluginType *AllocQuickLookGeneratorPluginType(CFUUIDRef inFactoryID); +static void DeallocQuickLookGeneratorPluginType(QuickLookGeneratorPluginType *thisInstance); +static HRESULT QuickLookGeneratorQueryInterface(void *thisInstance, REFIID iid, LPVOID *ppv); +extern void *QuickLookGeneratorPluginFactory(CFAllocatorRef allocator, CFUUIDRef typeID); +static ULONG QuickLookGeneratorPluginAddRef(void *thisInstance); +static ULONG QuickLookGeneratorPluginRelease(void *thisInstance); // ----------------------------------------------------------------------------- // myInterfaceFtbl definition diff --git a/bsnes/gb/README.md b/bsnes/gb/README.md index a2dbcb2e..00283a06 100644 --- a/bsnes/gb/README.md +++ b/bsnes/gb/README.md @@ -46,7 +46,7 @@ SameBoy requires the following tools and libraries to build: * make * macOS Cocoa port: macOS SDK and Xcode (For command line tools and ibtool) * SDL port: libsdl2 - * [rgbds](https://github.com/bentley/rgbds/releases/), for boot ROM compilation + * [rgbds](https://github.com/gbdev/rgbds/releases/), for boot ROM compilation On Windows, SameBoy also requires: * Visual Studio (For headers, etc.) @@ -55,6 +55,8 @@ On Windows, SameBoy also requires: To compile, simply run `make`. The targets are `cocoa` (Default for macOS), `sdl` (Default for everything else), `libretro`, `bootroms` and `tester`. You may also specify `CONF=debug` (default), `CONF=release`, `CONF=native_release` or `CONF=fat_release` to control optimization, symbols and multi-architectures. `native_release` is faster than `release`, but is optimized to the host's CPU and therefore is not portable. `fat_release` is exclusive to macOS and builds x86-64 and ARM64 fat binaries; this requires using a recent enough `clang` and macOS SDK using `xcode-select`, or setting them explicitly with `CC=` and `SYSROOT=`, respectively. All other configurations will build to your host architecture. You may set `BOOTROMS_DIR=...` to a directory containing precompiled boot ROM files, otherwise the build system will compile and use SameBoy's own boot ROMs. -By default, the SDL port will look for resource files with a path relative to executable. If you are packaging SameBoy, you may wish to override this by setting the `DATA_DIR` variable during compilation to the target path of the directory containing all files (apart from the executable, that's not necessary) from the `build/bin/SDL` directory in the source tree. Make sure the variable ends with a `/` character. +The SDL port will look for resource files with a path relative to executable and inside the directory specified by the `DATA_DIR` variable. If you are packaging SameBoy, you may wish to override this by setting the `DATA_DIR` variable during compilation to the target path of the directory containing all files (apart from the executable, that's not necessary) from the `build/bin/SDL` directory in the source tree. Make sure the variable ends with a `/` character. On FreeDesktop environments, `DATA_DIR` will default to `/usr/local/share/sameboy/`. `PREFIX` and `DESTDIR` follow their standard usage and default to an empty string an `/usr/local`, respectively -SameBoy was compiled and tested on macOS, Ubuntu and 64-bit Windows 7. +Linux, BSD, and other FreeDesktop users can run `sudo make install` to install SameBoy as both a GUI app and a command line tool. + +SameBoy is compiled and tested on macOS, Ubuntu and 64-bit Windows 10. diff --git a/bsnes/gb/SDL/font.c b/bsnes/gb/SDL/font.c index 93f3fa94..ea2c590c 100644 --- a/bsnes/gb/SDL/font.c +++ b/bsnes/gb/SDL/font.c @@ -1033,6 +1033,92 @@ uint8_t font[] = { _, _, _, X, X, _, _, _, _, _, X, _, _, _, _, _, _, _, + + /* Elipsis */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + X, _, X, _, X, _, + _, _, _, _, _, _, + + /* Mojibake */ + X, X, X, X, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, _, _, _, X, _, + X, X, X, X, X, _, + + /* Slider */ + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, X, X, X, X, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + X, X, X, X, X, X, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + X, X, X, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, + + /* Slider, selected */ + _, X, X, X, _, _, + _, X, X, X, _, _, + _, X, X, X, _, _, + _, X, X, X, X, X, + _, X, X, X, _, _, + _, X, X, X, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + _, X, X, X, _, _, + _, X, X, X, _, _, + _, X, X, X, _, _, + X, X, X, X, X, X, + _, X, X, X, _, _, + _, X, X, X, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + _, X, X, X, _, _, + _, X, X, X, _, _, + _, X, X, X, _, _, + X, X, X, X, _, _, + _, X, X, X, _, _, + _, X, X, X, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + + /* Slider, tick*/ + _, _, _, _, _, _, + _, _, X, _, _, _, + _, _, X, _, _, _, + X, X, X, X, X, X, + _, _, X, _, _, _, + _, _, X, _, _, _, + _, _, _, _, _, _, + _, _, _, _, _, _, }; const uint8_t font_max = sizeof(font) / GLYPH_HEIGHT / GLYPH_WIDTH + ' '; diff --git a/bsnes/gb/SDL/font.h b/bsnes/gb/SDL/font.h index 21753a8d..f2111c3f 100644 --- a/bsnes/gb/SDL/font.h +++ b/bsnes/gb/SDL/font.h @@ -12,5 +12,9 @@ extern const uint8_t font_max; #define CTRL_STRING "\x80\x81\x82" #define SHIFT_STRING "\x83" #define CMD_STRING "\x84\x85" +#define ELLIPSIS_STRING "\x87" +#define MOJIBAKE_STRING "\x88" +#define SLIDER_STRING "\x89\x8A\x8A\x8A\x8A\x8A\x8A\x8A\x8A\x8A\x8F\x8A\x8A\x8A\x8A\x8A\x8A\x8A\x8A\x8A\x8B" +#define SELECTED_SLIDER_STRING "\x8C\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8D\x8E" #endif /* font_h */ diff --git a/bsnes/gb/SDL/gui.c b/bsnes/gb/SDL/gui.c index 62656e8d..630e6e24 100644 --- a/bsnes/gb/SDL/gui.c +++ b/bsnes/gb/SDL/gui.c @@ -18,6 +18,7 @@ SDL_Texture *texture = NULL; SDL_PixelFormat *pixel_format = NULL; enum pending_command pending_command; unsigned command_parameter; +char *dropped_state_file = NULL; #ifdef __APPLE__ #define MODIFIER_NAME " " CMD_STRING @@ -110,6 +111,7 @@ configuration_t configuration = .volume = 100, .rumble_mode = GB_RUMBLE_ALL_GAMES, .default_scale = 2, + .color_temperature = 10, }; @@ -176,8 +178,7 @@ static void rescale_window(void) SDL_SetWindowSize(window, GB_get_screen_width(&gb) * configuration.default_scale, GB_get_screen_height(&gb) * configuration.default_scale); } -/* Does NOT check for bounds! */ -static void draw_char(uint32_t *buffer, unsigned width, unsigned height, unsigned char ch, uint32_t color) +static void draw_char(uint32_t *buffer, unsigned width, unsigned height, unsigned char ch, uint32_t color, uint32_t *mask_top, uint32_t *mask_bottom) { if (ch < ' ' || ch > font_max) { ch = '?'; @@ -187,7 +188,7 @@ static void draw_char(uint32_t *buffer, unsigned width, unsigned height, unsigne for (unsigned y = GLYPH_HEIGHT; y--;) { for (unsigned x = GLYPH_WIDTH; x--;) { - if (*(data++)) { + if (*(data++) && buffer >= mask_top && buffer < mask_bottom) { (*buffer) = color; } buffer++; @@ -196,12 +197,14 @@ static void draw_char(uint32_t *buffer, unsigned width, unsigned height, unsigne } } -static unsigned scroll = 0; -static void draw_unbordered_text(uint32_t *buffer, unsigned width, unsigned height, unsigned x, unsigned y, const char *string, uint32_t color) +static signed scroll = 0; +static void draw_unbordered_text(uint32_t *buffer, unsigned width, unsigned height, unsigned x, signed y, const char *string, uint32_t color, bool is_osd) { - y -= scroll; + if (!is_osd) { + y -= scroll; + } unsigned orig_x = x; - unsigned y_offset = (GB_get_screen_height(&gb) - 144) / 2; + unsigned y_offset = is_osd? 0 : (GB_get_screen_height(&gb) - 144) / 2; while (*string) { if (*string == '\n') { x = orig_x; @@ -210,25 +213,43 @@ static void draw_unbordered_text(uint32_t *buffer, unsigned width, unsigned heig continue; } - if (x > width - GLYPH_WIDTH || y == 0 || y - y_offset > 144 - GLYPH_HEIGHT) { + if (x > width - GLYPH_WIDTH) { break; } - draw_char(&buffer[x + width * y], width, height, *string, color); + draw_char(&buffer[(signed)(x + width * y)], width, height, *string, color, &buffer[width * y_offset], &buffer[width * (is_osd? GB_get_screen_height(&gb) : y_offset + 144)]); x += GLYPH_WIDTH; string++; } } -static void draw_text(uint32_t *buffer, unsigned width, unsigned height, unsigned x, unsigned y, const char *string, uint32_t color, uint32_t border) +void draw_text(uint32_t *buffer, unsigned width, unsigned height, unsigned x, signed y, const char *string, uint32_t color, uint32_t border, bool is_osd) { - draw_unbordered_text(buffer, width, height, x - 1, y, string, border); - draw_unbordered_text(buffer, width, height, x + 1, y, string, border); - draw_unbordered_text(buffer, width, height, x, y - 1, string, border); - draw_unbordered_text(buffer, width, height, x, y + 1, string, border); - draw_unbordered_text(buffer, width, height, x, y, string, color); + draw_unbordered_text(buffer, width, height, x - 1, y, string, border, is_osd); + draw_unbordered_text(buffer, width, height, x + 1, y, string, border, is_osd); + draw_unbordered_text(buffer, width, height, x, y - 1, string, border, is_osd); + draw_unbordered_text(buffer, width, height, x, y + 1, string, border, is_osd); + draw_unbordered_text(buffer, width, height, x, y, string, color, is_osd); } +const char *osd_text = NULL; +unsigned osd_countdown = 0; +unsigned osd_text_lines = 1; + +void show_osd_text(const char *text) +{ + osd_text_lines = 1; + osd_text = text; + osd_countdown = 30; + while (*text++) { + if (*text == '\n') { + osd_text_lines++; + osd_countdown += 30; + } + } +} + + enum decoration { DECORATION_NONE, DECORATION_SELECTION, @@ -238,14 +259,14 @@ enum decoration { static void draw_text_centered(uint32_t *buffer, unsigned width, unsigned height, unsigned y, const char *string, uint32_t color, uint32_t border, enum decoration decoration) { unsigned x = width / 2 - (unsigned) strlen(string) * GLYPH_WIDTH / 2; - draw_text(buffer, width, height, x, y, string, color, border); + draw_text(buffer, width, height, x, y, string, color, border, false); switch (decoration) { case DECORATION_SELECTION: - draw_text(buffer, width, height, x - GLYPH_WIDTH, y, SELECTION_STRING, color, border); + draw_text(buffer, width, height, x - GLYPH_WIDTH, y, SELECTION_STRING, color, border, false); break; case DECORATION_ARROWS: - draw_text(buffer, width, height, x - GLYPH_WIDTH, y, LEFT_ARROW_STRING, color, border); - draw_text(buffer, width, height, width - x, y, RIGHT_ARROW_STRING, color, border); + draw_text(buffer, width, height, x - GLYPH_WIDTH, y, LEFT_ARROW_STRING, color, border, false); + draw_text(buffer, width, height, width - x, y, RIGHT_ARROW_STRING, color, border, false); break; case DECORATION_NONE: @@ -261,6 +282,10 @@ struct menu_item { }; static const struct menu_item *current_menu = NULL; static const struct menu_item *root_menu = NULL; +static unsigned menu_height; +static unsigned scrollbar_size; +static bool mouse_scroling = false; + static unsigned current_selection = 0; static enum { @@ -302,6 +327,23 @@ static void open_rom(unsigned index) } } +static void recalculate_menu_height(void) +{ + menu_height = 24; + scrollbar_size = 0; + if (gui_state == SHOWING_MENU) { + for (const struct menu_item *item = current_menu; item->string; item++) { + menu_height += 12; + if (item->backwards_handler) { + menu_height += 12; + } + } + } + if (menu_height > 144) { + scrollbar_size = 144 * 140 / menu_height; + } +} + static const struct menu_item paused_menu[] = { {"Resume", NULL}, {"Open ROM", open_rom}, @@ -322,6 +364,7 @@ static void return_to_root_menu(unsigned index) current_menu = root_menu; current_selection = 0; scroll = 0; + recalculate_menu_height(); } static void cycle_model(unsigned index) @@ -420,10 +463,66 @@ const char *current_rewind_string(unsigned index) return "Custom"; } +const char *current_bootrom_string(unsigned index) +{ + if (!configuration.bootrom_path[0]) { + return "Built-in Boot ROMs"; + } + size_t length = strlen(configuration.bootrom_path); + static char ret[24] = {0,}; + if (length <= 23) { + strcpy(ret, configuration.bootrom_path); + } + else { + memcpy(ret, configuration.bootrom_path, 11); + memcpy(ret + 12, configuration.bootrom_path + length - 11, 11); + } + for (unsigned i = 0; i < 24; i++) { + if (ret[i] < 0) { + ret[i] = MOJIBAKE_STRING[0]; + } + } + if (length > 23) { + ret[11] = ELLIPSIS_STRING[0]; + } + return ret; +} + +static void toggle_bootrom(unsigned index) +{ + if (configuration.bootrom_path[0]) { + configuration.bootrom_path[0] = 0; + } + else { + char *folder = do_open_folder_dialog(); + if (!folder) return; + if (strlen(folder) < sizeof(configuration.bootrom_path) - 1) { + strcpy(configuration.bootrom_path, folder); + } + free(folder); + } +} + +static void toggle_rtc_mode(unsigned index) +{ + configuration.rtc_mode = !configuration.rtc_mode; +} + +const char *current_rtc_mode_string(unsigned index) +{ + switch (configuration.rtc_mode) { + case GB_RTC_MODE_SYNC_TO_HOST: return "Sync to System Clock"; + case GB_RTC_MODE_ACCURATE: return "Accurate"; + } + return ""; +} + static const struct menu_item emulation_menu[] = { {"Emulated Model:", cycle_model, current_model_string, cycle_model_backwards}, {"SGB Revision:", cycle_sgb_revision, current_sgb_revision_string, cycle_sgb_revision_backwards}, + {"Boot ROMs Folder:", toggle_bootrom, current_bootrom_string, toggle_bootrom}, {"Rewind Length:", cycle_rewind, current_rewind_string, cycle_rewind_backwards}, + {"Real Time Clock:", toggle_rtc_mode, current_rtc_mode_string, toggle_rtc_mode}, {"Back", return_to_root_menu}, {NULL,} }; @@ -433,6 +532,7 @@ static void enter_emulation_menu(unsigned index) current_menu = emulation_menu; current_selection = 0; scroll = 0; + recalculate_menu_height(); } const char *current_scaling_mode(unsigned index) @@ -449,10 +549,19 @@ const char *current_default_scale(unsigned index) const char *current_color_correction_mode(unsigned index) { - return (const char *[]){"Disabled", "Correct Color Curves", "Emulate Hardware", "Preserve Brightness", "Reduce Contrast"} + return (const char *[]){"Disabled", "Correct Color Curves", "Emulate Hardware", "Preserve Brightness", "Reduce Contrast", "Harsh Reality"} [configuration.color_correction_mode]; } +const char *current_color_temperature(unsigned index) +{ + static char ret[22]; + strcpy(ret, SLIDER_STRING); + ret[configuration.color_temperature] = SELECTED_SLIDER_STRING[configuration.color_temperature]; + return ret; +} + + const char *current_palette(unsigned index) { return (const char *[]){"Greyscale", "Lime (Game Boy)", "Olive (Pocket)", "Teal (Light)"} @@ -515,7 +624,7 @@ void cycle_default_scale_backwards(unsigned index) static void cycle_color_correction(unsigned index) { - if (configuration.color_correction_mode == GB_COLOR_CORRECTION_REDUCE_CONTRAST) { + if (configuration.color_correction_mode == GB_COLOR_CORRECTION_LOW_CONTRAST) { configuration.color_correction_mode = GB_COLOR_CORRECTION_DISABLED; } else { @@ -526,13 +635,27 @@ static void cycle_color_correction(unsigned index) static void cycle_color_correction_backwards(unsigned index) { if (configuration.color_correction_mode == GB_COLOR_CORRECTION_DISABLED) { - configuration.color_correction_mode = GB_COLOR_CORRECTION_REDUCE_CONTRAST; + configuration.color_correction_mode = GB_COLOR_CORRECTION_LOW_CONTRAST; } else { configuration.color_correction_mode--; } } +static void decrease_color_temperature(unsigned index) +{ + if (configuration.color_temperature < 20) { + configuration.color_temperature++; + } +} + +static void increase_color_temperature(unsigned index) +{ + if (configuration.color_temperature > 0) { + configuration.color_temperature--; + } +} + static void cycle_palette(unsigned index) { if (configuration.dmg_palette == 3) { @@ -573,6 +696,7 @@ static void cycle_border_mode_backwards(unsigned index) } } +extern bool uses_gl(void); struct shader_name { const char *file_name; const char *display_name; @@ -596,6 +720,7 @@ struct shader_name { static void cycle_filter(unsigned index) { + if (!uses_gl()) return; unsigned i = 0; for (; i < sizeof(shaders) / sizeof(shaders[0]); i++) { if (strcmp(shaders[i].file_name, configuration.filter) == 0) { @@ -618,6 +743,7 @@ static void cycle_filter(unsigned index) static void cycle_filter_backwards(unsigned index) { + if (!uses_gl()) return; unsigned i = 0; for (; i < sizeof(shaders) / sizeof(shaders[0]); i++) { if (strcmp(shaders[i].file_name, configuration.filter) == 0) { @@ -637,8 +763,9 @@ static void cycle_filter_backwards(unsigned index) } } -const char *current_filter_name(unsigned index) +static const char *current_filter_name(unsigned index) { + if (!uses_gl()) return "Requires OpenGL 3.2+"; unsigned i = 0; for (; i < sizeof(shaders) / sizeof(shaders[0]); i++) { if (strcmp(shaders[i].file_name, configuration.filter) == 0) { @@ -655,6 +782,7 @@ const char *current_filter_name(unsigned index) static void cycle_blending_mode(unsigned index) { + if (!uses_gl()) return; if (configuration.blending_mode == GB_FRAME_BLENDING_MODE_ACCURATE) { configuration.blending_mode = GB_FRAME_BLENDING_MODE_DISABLED; } @@ -665,6 +793,7 @@ static void cycle_blending_mode(unsigned index) static void cycle_blending_mode_backwards(unsigned index) { + if (!uses_gl()) return; if (configuration.blending_mode == GB_FRAME_BLENDING_MODE_DISABLED) { configuration.blending_mode = GB_FRAME_BLENDING_MODE_ACCURATE; } @@ -673,20 +802,35 @@ static void cycle_blending_mode_backwards(unsigned index) } } -const char *blending_mode_string(unsigned index) +static const char *blending_mode_string(unsigned index) { + if (!uses_gl()) return "Requires OpenGL 3.2+"; return (const char *[]){"Disabled", "Simple", "Accurate"} [configuration.blending_mode]; } +static void toggle_osd(unsigned index) +{ + osd_countdown = 0; + configuration.osd = !configuration.osd; +} + +static const char *current_osd_mode(unsigned index) +{ + return configuration.osd? "Enabled" : "Disabled"; +} + static const struct menu_item graphics_menu[] = { {"Scaling Mode:", cycle_scaling, current_scaling_mode, cycle_scaling_backwards}, {"Default Window Scale:", cycle_default_scale, current_default_scale, cycle_default_scale_backwards}, {"Scaling Filter:", cycle_filter, current_filter_name, cycle_filter_backwards}, {"Color Correction:", cycle_color_correction, current_color_correction_mode, cycle_color_correction_backwards}, + {"Ambient Light Temp.:", decrease_color_temperature, current_color_temperature, increase_color_temperature}, {"Frame Blending:", cycle_blending_mode, blending_mode_string, cycle_blending_mode_backwards}, {"Mono Palette:", cycle_palette, current_palette, cycle_palette_backwards}, {"Display Border:", cycle_border_mode, current_border_mode, cycle_border_mode_backwards}, + {"On-Screen Display:", toggle_osd, current_osd_mode, toggle_osd}, + {"Back", return_to_root_menu}, {NULL,} }; @@ -696,6 +840,7 @@ static void enter_graphics_menu(unsigned index) current_menu = graphics_menu; current_selection = 0; scroll = 0; + recalculate_menu_height(); } const char *highpass_filter_string(unsigned index) @@ -745,9 +890,33 @@ void decrease_volume(unsigned index) } } +const char *interference_volume_string(unsigned index) +{ + static char ret[5]; + sprintf(ret, "%d%%", configuration.interference_volume); + return ret; +} + +void increase_interference_volume(unsigned index) +{ + configuration.interference_volume += 5; + if (configuration.interference_volume > 100) { + configuration.interference_volume = 100; + } +} + +void decrease_interference_volume(unsigned index) +{ + configuration.interference_volume -= 5; + if (configuration.interference_volume > 100) { + configuration.interference_volume = 0; + } +} + static const struct menu_item audio_menu[] = { {"Highpass Filter:", cycle_highpass_filter, highpass_filter_string, cycle_highpass_filter_backwards}, {"Volume:", increase_volume, volume_string, decrease_volume}, + {"Interference Volume:", increase_interference_volume, interference_volume_string, decrease_interference_volume}, {"Back", return_to_root_menu}, {NULL,} }; @@ -757,6 +926,7 @@ static void enter_audio_menu(unsigned index) current_menu = audio_menu; current_selection = 0; scroll = 0; + recalculate_menu_height(); } static void modify_key(unsigned index) @@ -784,10 +954,7 @@ static const struct menu_item controls_menu[] = { static const char *key_name(unsigned index) { - if (index >= 8) { - if (index == 8) { - return SDL_GetScancodeName(configuration.keys[8]); - } + if (index > 8) { return SDL_GetScancodeName(configuration.keys_2[index - 9]); } return SDL_GetScancodeName(configuration.keys[index]); @@ -798,6 +965,7 @@ static void enter_controls_menu(unsigned index) current_menu = controls_menu; current_selection = 0; scroll = 0; + recalculate_menu_height(); } static unsigned joypad_index = 0; @@ -815,7 +983,7 @@ const char *current_joypad_name(unsigned index) // SDL returns a name with repeated and trailing spaces while (*orig_name && i < sizeof(name) - 2) { if (orig_name[0] != ' ' || orig_name[1] != ' ') { - name[i++] = *orig_name; + name[i++] = *orig_name > 0? *orig_name : MOJIBAKE_STRING[0]; } orig_name++; } @@ -933,6 +1101,7 @@ static void enter_joypad_menu(unsigned index) current_menu = joypad_menu; current_selection = 0; scroll = 0; + recalculate_menu_height(); } joypad_button_t get_joypad_button(uint8_t physical_button) @@ -1017,6 +1186,7 @@ void run_gui(bool is_running) gui_state = is_running? SHOWING_MENU : SHOWING_DROP_MESSAGE; bool should_render = true; current_menu = root_menu = is_running? paused_menu : nonpaused_menu; + recalculate_menu_height(); current_selection = 0; scroll = 0; do { @@ -1168,9 +1338,21 @@ void run_gui(bool is_running) break; } case SDL_DROPFILE: { - set_filename(event.drop.file, SDL_free); - pending_command = GB_SDL_NEW_FILE_COMMAND; - return; + if (GB_is_stave_state(event.drop.file)) { + if (GB_is_inited(&gb)) { + dropped_state_file = event.drop.file; + pending_command = GB_SDL_LOAD_STATE_FROM_FILE_COMMAND; + } + else { + SDL_free(event.drop.file); + } + break; + } + else { + set_filename(event.drop.file, SDL_free); + pending_command = GB_SDL_NEW_FILE_COMMAND; + return; + } } case SDL_JOYBUTTONDOWN: { @@ -1204,9 +1386,36 @@ void run_gui(bool is_running) } break; } + + case SDL_MOUSEWHEEL: { + if (menu_height > 144) { + scroll -= event.wheel.y; + if (scroll < 0) { + scroll = 0; + } + if (scroll >= menu_height - 144) { + scroll = menu_height - 144; + } + + mouse_scroling = true; + should_render = true; + } + break; + } + case SDL_KEYDOWN: - if (event.key.keysym.scancode == SDL_SCANCODE_F && event.key.keysym.mod & MODIFIER) { + if (gui_state == WAITING_FOR_KEY) { + if (current_selection > 8) { + configuration.keys_2[current_selection - 9] = event.key.keysym.scancode; + } + else { + configuration.keys[current_selection] = event.key.keysym.scancode; + } + gui_state = SHOWING_MENU; + should_render = true; + } + else if (event_hotkey_code(&event) == SDL_SCANCODE_F && event.key.keysym.mod & MODIFIER) { if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == false) { SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); } @@ -1215,7 +1424,7 @@ void run_gui(bool is_running) } update_viewport(); } - if (event.key.keysym.scancode == SDL_SCANCODE_O) { + else if (event_hotkey_code(&event) == SDL_SCANCODE_O) { if (event.key.keysym.mod & MODIFIER) { char *filename = do_open_rom_dialog(); if (filename) { @@ -1241,7 +1450,11 @@ void run_gui(bool is_running) } } else if (event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) { - if (is_running) { + if (gui_state == SHOWING_MENU && current_menu != root_menu) { + return_to_root_menu(0); + should_render = true; + } + else if (is_running) { return; } else { @@ -1252,18 +1465,22 @@ void run_gui(bool is_running) gui_state = SHOWING_DROP_MESSAGE; } current_selection = 0; + mouse_scroling = false; scroll = 0; current_menu = root_menu; + recalculate_menu_height(); should_render = true; } } else if (gui_state == SHOWING_MENU) { if (event.key.keysym.scancode == SDL_SCANCODE_DOWN && current_menu[current_selection + 1].string) { current_selection++; + mouse_scroling = false; should_render = true; } else if (event.key.keysym.scancode == SDL_SCANCODE_UP && current_selection) { current_selection--; + mouse_scroling = false; should_render = true; } else if (event.key.keysym.scancode == SDL_SCANCODE_RETURN && !current_menu[current_selection].backwards_handler) { @@ -1300,21 +1517,6 @@ void run_gui(bool is_running) } should_render = true; } - else if (gui_state == WAITING_FOR_KEY) { - if (current_selection >= 8) { - if (current_selection == 8) { - configuration.keys[8] = event.key.keysym.scancode; - } - else { - configuration.keys_2[current_selection - 9] = event.key.keysym.scancode; - } - } - else { - configuration.keys[current_selection] = event.key.keysym.scancode; - } - gui_state = SHOWING_MENU; - should_render = true; - } break; } @@ -1340,13 +1542,21 @@ void run_gui(bool is_running) draw_text_centered(pixels, width, height, 8 + y_offset, "SameBoy", gui_palette_native[3], gui_palette_native[0], false); unsigned i = 0, y = 24; for (const struct menu_item *item = current_menu; item->string; item++, i++) { - if (i == current_selection) { - if (y < scroll) { - scroll = y - 4; - goto rerender; + if (i == current_selection && !mouse_scroling) { + if (i == 0) { + if (y < scroll) { + scroll = (y - 4) / 12 * 12; + goto rerender; + } + } + else { + if (y < scroll + 24) { + scroll = (y - 24) / 12 * 12; + goto rerender; + } } } - if (i == current_selection && i == 0 && scroll != 0) { + if (i == current_selection && i == 0 && scroll != 0 && !mouse_scroling) { scroll = 0; goto rerender; } @@ -1363,22 +1573,41 @@ void run_gui(bool is_running) i == current_selection && !item->value_getter ? DECORATION_SELECTION : DECORATION_NONE); y += 12; if (item->value_getter) { - draw_text_centered(pixels, width, height, y + y_offset, item->value_getter(i), gui_palette_native[3], gui_palette_native[0], + draw_text_centered(pixels, width, height, y + y_offset - 1, item->value_getter(i), gui_palette_native[3], gui_palette_native[0], i == current_selection ? DECORATION_ARROWS : DECORATION_NONE); y += 12; } } - if (i == current_selection) { + if (i == current_selection && !mouse_scroling) { if (y > scroll + 144) { - scroll = y - 144; + scroll = (y - 144) / 12 * 12; + if (scroll > menu_height - 144) { + scroll = menu_height - 144; + } goto rerender; } } } + if (scrollbar_size) { + unsigned scrollbar_offset = (140 - scrollbar_size) * scroll / (menu_height - 144); + if (scrollbar_offset + scrollbar_size > 140) { + scrollbar_offset = 140 - scrollbar_size; + } + for (unsigned y = 0; y < 140; y++) { + uint32_t *pixel = pixels + x_offset + 156 + width * (y + y_offset + 2); + if (y >= scrollbar_offset && y < scrollbar_offset + scrollbar_size) { + pixel[0] = pixel[1]= gui_palette_native[2]; + } + else { + pixel[0] = pixel[1]= gui_palette_native[1]; + } + + } + } break; case SHOWING_HELP: - draw_text(pixels, width, height, 2 + x_offset, 2 + y_offset, help[current_help_page], gui_palette_native[3], gui_palette_native[0]); + draw_text(pixels, width, height, 2 + x_offset, 2 + y_offset, help[current_help_page], gui_palette_native[3], gui_palette_native[0], false); break; case WAITING_FOR_KEY: draw_text_centered(pixels, width, height, 68 + y_offset, "Press a Key", gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); diff --git a/bsnes/gb/SDL/gui.h b/bsnes/gb/SDL/gui.h index c9509076..d0352615 100644 --- a/bsnes/gb/SDL/gui.h +++ b/bsnes/gb/SDL/gui.h @@ -39,12 +39,14 @@ enum pending_command { GB_SDL_RESET_COMMAND, GB_SDL_NEW_FILE_COMMAND, GB_SDL_QUIT_COMMAND, + GB_SDL_LOAD_STATE_FROM_FILE_COMMAND, }; #define GB_SDL_DEFAULT_SCALE_MAX 8 extern enum pending_command pending_command; extern unsigned command_parameter; +extern char *dropped_state_file; typedef enum { JOYPAD_BUTTON_LEFT, @@ -110,6 +112,16 @@ typedef struct { GB_rumble_mode_t rumble_mode; uint8_t default_scale; + + /* v0.14 */ + unsigned padding; + uint8_t color_temperature; + char bootrom_path[4096]; + uint8_t interference_volume; + GB_rtc_mode_t rtc_mode; + + /* v0.14.4 */ + bool osd; } configuration_t; extern configuration_t configuration; @@ -122,4 +134,19 @@ void connect_joypad(void); joypad_button_t get_joypad_button(uint8_t physical_button); joypad_axis_t get_joypad_axis(uint8_t physical_axis); +static SDL_Scancode event_hotkey_code(SDL_Event *event) +{ + if (event->key.keysym.sym >= SDLK_a && event->key.keysym.sym < SDLK_z) { + return SDL_SCANCODE_A + event->key.keysym.sym - SDLK_a; + } + + return event->key.keysym.scancode; +} + +void draw_text(uint32_t *buffer, unsigned width, unsigned height, unsigned x, signed y, const char *string, uint32_t color, uint32_t border, bool is_osd); +void show_osd_text(const char *text); +extern const char *osd_text; +extern unsigned osd_countdown; +extern unsigned osd_text_lines; + #endif diff --git a/bsnes/gb/SDL/main.c b/bsnes/gb/SDL/main.c index e79d0b33..796eeffb 100644 --- a/bsnes/gb/SDL/main.c +++ b/bsnes/gb/SDL/main.c @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #endif +static bool stop_on_start = false; GB_gameboy_t gb; static bool paused = false; static uint32_t pixel_buffer_1[256 * 224], pixel_buffer_2[256 * 224]; @@ -25,8 +27,13 @@ static double clock_mutliplier = 1.0; static char *filename = NULL; static typeof(free) *free_function = NULL; -static char *battery_save_path_ptr; +static char *battery_save_path_ptr = NULL; +static SDL_GLContext gl_context = NULL; +bool uses_gl(void) +{ + return gl_context; +} void set_filename(const char *new_filename, typeof(free) *new_free_function) { @@ -58,7 +65,7 @@ static void start_capturing_logs(void) GB_set_log_callback(&gb, log_capture_callback); } -static const char *end_capturing_logs(bool show_popup, bool should_exit) +static const char *end_capturing_logs(bool show_popup, bool should_exit, uint32_t popup_flags, const char *title) { GB_set_log_callback(&gb, NULL); if (captured_log[0] == 0) { @@ -67,7 +74,7 @@ static const char *end_capturing_logs(bool show_popup, bool should_exit) } else { if (show_popup) { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", captured_log, window); + SDL_ShowSimpleMessageBox(popup_flags, title, captured_log, window); } if (should_exit) { exit(1); @@ -120,9 +127,13 @@ static void open_menu(void) GB_audio_set_paused(false); } GB_set_color_correction_mode(&gb, configuration.color_correction_mode); + GB_set_light_temperature(&gb, (configuration.color_temperature - 10.0) / 10.0); + GB_set_interference_volume(&gb, configuration.interference_volume / 100.0); GB_set_border_mode(&gb, configuration.border_mode); update_palette(); GB_set_highpass_filter_mode(&gb, configuration.highpass_mode); + GB_set_rewind_length(&gb, configuration.rewind_length); + GB_set_rtc_mode(&gb, configuration.rtc_mode); if (previous_width != GB_get_screen_width(&gb)) { screen_size_changed(); } @@ -131,15 +142,21 @@ static void open_menu(void) static void handle_events(GB_gameboy_t *gb) { SDL_Event event; - while (SDL_PollEvent(&event)) { + while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_QUIT: pending_command = GB_SDL_QUIT_COMMAND; break; case SDL_DROPFILE: { - set_filename(event.drop.file, SDL_free); - pending_command = GB_SDL_NEW_FILE_COMMAND; + if (GB_is_stave_state(event.drop.file)) { + dropped_state_file = event.drop.file; + pending_command = GB_SDL_LOAD_STATE_FROM_FILE_COMMAND; + } + else { + set_filename(event.drop.file, SDL_free); + pending_command = GB_SDL_NEW_FILE_COMMAND; + } break; } @@ -175,7 +192,7 @@ static void handle_events(GB_gameboy_t *gb) open_menu(); } } - break; + break; case SDL_JOYAXISMOTION: { static bool axis_active[2] = {false, false}; @@ -215,25 +232,24 @@ static void handle_events(GB_gameboy_t *gb) } } } - break; - - case SDL_JOYHATMOTION: - { + break; + + case SDL_JOYHATMOTION: { uint8_t value = event.jhat.value; int8_t updown = - value == SDL_HAT_LEFTUP || value == SDL_HAT_UP || value == SDL_HAT_RIGHTUP ? -1 : (value == SDL_HAT_LEFTDOWN || value == SDL_HAT_DOWN || value == SDL_HAT_RIGHTDOWN ? 1 : 0); + value == SDL_HAT_LEFTUP || value == SDL_HAT_UP || value == SDL_HAT_RIGHTUP ? -1 : (value == SDL_HAT_LEFTDOWN || value == SDL_HAT_DOWN || value == SDL_HAT_RIGHTDOWN ? 1 : 0); int8_t leftright = - value == SDL_HAT_LEFTUP || value == SDL_HAT_LEFT || value == SDL_HAT_LEFTDOWN ? -1 : (value == SDL_HAT_RIGHTUP || value == SDL_HAT_RIGHT || value == SDL_HAT_RIGHTDOWN ? 1 : 0); + value == SDL_HAT_LEFTUP || value == SDL_HAT_LEFT || value == SDL_HAT_LEFTDOWN ? -1 : (value == SDL_HAT_RIGHTUP || value == SDL_HAT_RIGHT || value == SDL_HAT_RIGHTDOWN ? 1 : 0); GB_set_key_state(gb, GB_KEY_LEFT, leftright == -1); GB_set_key_state(gb, GB_KEY_RIGHT, leftright == 1); GB_set_key_state(gb, GB_KEY_UP, updown == -1); GB_set_key_state(gb, GB_KEY_DOWN, updown == 1); break; - }; + }; case SDL_KEYDOWN: - switch (event.key.keysym.scancode) { + switch (event_hotkey_code(&event)) { case SDL_SCANCODE_ESCAPE: { open_menu(); break; @@ -241,7 +257,6 @@ static void handle_events(GB_gameboy_t *gb) case SDL_SCANCODE_C: if (event.type == SDL_KEYDOWN && (event.key.keysym.mod & KMOD_CTRL)) { GB_debugger_break(gb); - } break; @@ -261,7 +276,7 @@ static void handle_events(GB_gameboy_t *gb) } break; } - + case SDL_SCANCODE_P: if (event.key.keysym.mod & MODIFIER) { paused = !paused; @@ -277,14 +292,14 @@ static void handle_events(GB_gameboy_t *gb) #endif GB_audio_set_paused(GB_audio_is_playing()); } - break; - + break; + case SDL_SCANCODE_F: if (event.key.keysym.mod & MODIFIER) { if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == false) { SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); } - else { + else { SDL_SetWindowFullscreen(window, 0); } update_viewport(); @@ -333,9 +348,14 @@ static void handle_events(GB_gameboy_t *gb) break; default: break; - } } } +} + +static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) +{ + return SDL_MapRGB(pixel_format, r, g, b); +} static void vblank(GB_gameboy_t *gb) { @@ -347,6 +367,26 @@ static void vblank(GB_gameboy_t *gb) clock_mutliplier += 1.0/16; GB_set_clock_multiplier(gb, clock_mutliplier); } + + if (turbo_down) { + show_osd_text("Fast forward..."); + } + else if (underclock_down) { + show_osd_text("Slow motion..."); + } + else if (rewind_down) { + show_osd_text("Rewinding..."); + } + + if (osd_countdown && configuration.osd) { + unsigned width = GB_get_screen_width(gb); + unsigned height = GB_get_screen_height(gb); + draw_text(active_pixel_buffer, + width, height, 8, height - 8 - osd_text_lines * 12, osd_text, + rgb_encode(gb, 255, 255, 255), rgb_encode(gb, 0, 0, 0), + true); + osd_countdown--; + } if (configuration.blending_mode) { render_texture(active_pixel_buffer, previous_pixel_buffer); uint32_t *temp = active_pixel_buffer; @@ -361,12 +401,6 @@ static void vblank(GB_gameboy_t *gb) handle_events(gb); } - -static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) -{ - return SDL_MapRGB(pixel_format, r, g, b); -} - static void rumble(GB_gameboy_t *gb, double amp) { SDL_HapticRumblePlay(haptic, amp, 250); @@ -415,21 +449,49 @@ static bool handle_pending_command(void) switch (pending_command) { case GB_SDL_LOAD_STATE_COMMAND: case GB_SDL_SAVE_STATE_COMMAND: { - char save_path[strlen(filename) + 4]; + char save_path[strlen(filename) + 5]; char save_extension[] = ".s0"; save_extension[2] += command_parameter; replace_extension(filename, strlen(filename), save_path, save_extension); start_capturing_logs(); + bool success; if (pending_command == GB_SDL_LOAD_STATE_COMMAND) { - GB_load_state(&gb, save_path); + int result = GB_load_state(&gb, save_path); + if (result == ENOENT) { + char save_extension[] = ".sn0"; + save_extension[3] += command_parameter; + replace_extension(filename, strlen(filename), save_path, save_extension); + start_capturing_logs(); + result = GB_load_state(&gb, save_path); + } + success = result == 0; } else { - GB_save_state(&gb, save_path); + success = GB_save_state(&gb, save_path) == 0; + } + end_capturing_logs(true, + false, + success? SDL_MESSAGEBOX_INFORMATION : SDL_MESSAGEBOX_ERROR, + success? "Notice" : "Error"); + if (success) { + show_osd_text(pending_command == GB_SDL_LOAD_STATE_COMMAND? "State loaded" : "State saved"); } - end_capturing_logs(true, false); return false; } + + case GB_SDL_LOAD_STATE_FROM_FILE_COMMAND: + start_capturing_logs(); + bool success = GB_load_state(&gb, dropped_state_file) == 0; + end_capturing_logs(true, + false, + success? SDL_MESSAGEBOX_INFORMATION : SDL_MESSAGEBOX_ERROR, + success? "Notice" : "Error"); + SDL_free(dropped_state_file); + if (success) { + show_osd_text("State loaded"); + } + return false; case GB_SDL_NO_COMMAND: return false; @@ -448,8 +510,6 @@ static bool handle_pending_command(void) static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) { - bool error = false; - start_capturing_logs(); static const char *const names[] = { [GB_BOOT_ROM_DMG0] = "dmg0_boot.bin", [GB_BOOT_ROM_DMG] = "dmg_boot.bin", @@ -460,8 +520,17 @@ static void load_boot_rom(GB_gameboy_t *gb, GB_boot_rom_t type) [GB_BOOT_ROM_CGB] = "cgb_boot.bin", [GB_BOOT_ROM_AGB] = "agb_boot.bin", }; - GB_load_boot_rom(gb, resource_path(names[type])); - end_capturing_logs(true, error); + bool use_built_in = true; + if (configuration.bootrom_path[0]) { + static char path[4096]; + snprintf(path, sizeof(path), "%s/%s", configuration.bootrom_path, names[type]); + use_built_in = GB_load_boot_rom(gb, path); + } + if (use_built_in) { + start_capturing_logs(); + GB_load_boot_rom(gb, resource_path(names[type])); + end_capturing_logs(true, false, SDL_MESSAGEBOX_ERROR, "Error"); + } } static void run(void) @@ -497,15 +566,22 @@ restart: GB_set_rumble_mode(&gb, configuration.rumble_mode); GB_set_sample_rate(&gb, GB_audio_get_frequency()); GB_set_color_correction_mode(&gb, configuration.color_correction_mode); + GB_set_light_temperature(&gb, (configuration.color_temperature - 10.0) / 10.0); + GB_set_interference_volume(&gb, configuration.interference_volume / 100.0); update_palette(); if ((unsigned)configuration.border_mode <= GB_BORDER_ALWAYS) { GB_set_border_mode(&gb, configuration.border_mode); } GB_set_highpass_filter_mode(&gb, configuration.highpass_mode); GB_set_rewind_length(&gb, configuration.rewind_length); + GB_set_rtc_mode(&gb, configuration.rtc_mode); GB_set_update_input_hint_callback(&gb, handle_events); GB_apu_set_sample_callback(&gb, gb_audio_callback); } + if (stop_on_start) { + stop_on_start = false; + GB_debugger_break(&gb); + } bool error = false; GB_debugger_clear_symbols(&gb); @@ -514,9 +590,9 @@ restart: char extension[4] = {0,}; if (path_length > 4) { if (filename[path_length - 4] == '.') { - extension[0] = tolower(filename[path_length - 3]); - extension[1] = tolower(filename[path_length - 2]); - extension[2] = tolower(filename[path_length - 1]); + extension[0] = tolower((unsigned char)filename[path_length - 3]); + extension[1] = tolower((unsigned char)filename[path_length - 2]); + extension[2] = tolower((unsigned char)filename[path_length - 1]); } } if (strcmp(extension, "isx") == 0) { @@ -530,8 +606,14 @@ restart: else { GB_load_rom(&gb, filename); } - end_capturing_logs(true, error); + end_capturing_logs(true, error, SDL_MESSAGEBOX_WARNING, "Warning"); + static char start_text[64]; + static char title[17]; + GB_get_rom_title(&gb, title); + sprintf(start_text, "SameBoy v" GB_VERSION "\n%s\n%08X", title, GB_get_rom_crc32(&gb)); + show_osd_text(start_text); + /* Configure battery */ char battery_save_path[path_length + 5]; /* At the worst case, size is strlen(path) + 4 bytes for .sav + NULL */ @@ -605,14 +687,14 @@ int main(int argc, char **argv) #ifdef _WIN32 SetProcessDPIAware(); #endif -#define str(x) #x -#define xstr(x) str(x) - fprintf(stderr, "SameBoy v" xstr(VERSION) "\n"); + fprintf(stderr, "SameBoy v" GB_VERSION "\n"); - bool fullscreen = get_arg_flag("--fullscreen", &argc, argv); + bool fullscreen = get_arg_flag("--fullscreen", &argc, argv) || get_arg_flag("-f", &argc, argv); + bool nogl = get_arg_flag("--nogl", &argc, argv); + stop_on_start = get_arg_flag("--stop-debugger", &argc, argv) || get_arg_flag("-s", &argc, argv); - if (argc > 2) { - fprintf(stderr, "Usage: %s [--fullscreen] [rom]\n", argv[0]); + if (argc > 2 || (argc == 2 && argv[1][0] == '-')) { + fprintf(stderr, "Usage: %s [--fullscreen|-f] [--nogl] [--stop-debugger|-s] [rom]\n", argv[0]); exit(1); } @@ -637,7 +719,7 @@ int main(int argc, char **argv) fclose(prefs_file); /* Sanitize for stability */ - configuration.color_correction_mode %= GB_COLOR_CORRECTION_REDUCE_CONTRAST +1; + configuration.color_correction_mode %= GB_COLOR_CORRECTION_LOW_CONTRAST +1; configuration.scaling_mode %= GB_SDL_SCALING_MAX; configuration.default_scale %= GB_SDL_DEFAULT_SCALE_MAX + 1; configuration.blending_mode %= GB_FRAME_BLENDING_MODE_ACCURATE + 1; @@ -647,6 +729,8 @@ int main(int argc, char **argv) configuration.dmg_palette %= 3; configuration.border_mode %= GB_BORDER_ALWAYS + 1; configuration.rumble_mode %= GB_RUMBLE_ALL_GAMES + 1; + configuration.color_temperature %= 21; + configuration.bootrom_path[sizeof(configuration.bootrom_path) - 1] = 0; } if (configuration.model >= MODEL_MAX) { @@ -663,21 +747,27 @@ int main(int argc, char **argv) SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); - window = SDL_CreateWindow("SameBoy v" xstr(VERSION), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + window = SDL_CreateWindow("SameBoy v" GB_VERSION, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 160 * configuration.default_scale, 144 * configuration.default_scale, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); + if (window == NULL) { + fputs(SDL_GetError(), stderr); + exit(1); + } SDL_SetWindowMinimumSize(window, 160, 144); if (fullscreen) { SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP); } - SDL_GLContext gl_context = SDL_GL_CreateContext(window); + gl_context = nogl? NULL : SDL_GL_CreateContext(window); GLint major = 0, minor = 0; - glGetIntegerv(GL_MAJOR_VERSION, &major); - glGetIntegerv(GL_MINOR_VERSION, &minor); + if (gl_context) { + glGetIntegerv(GL_MAJOR_VERSION, &major); + glGetIntegerv(GL_MINOR_VERSION, &minor); + } - if (major * 0x100 + minor < 0x302) { + if (gl_context && major * 0x100 + minor < 0x302) { SDL_GL_DeleteContext(gl_context); gl_context = NULL; } @@ -701,6 +791,7 @@ int main(int argc, char **argv) update_viewport(); if (filename == NULL) { + stop_on_start = false; run_gui(false); } else { diff --git a/bsnes/gb/SDL/shader.c b/bsnes/gb/SDL/shader.c index de2ba564..44de2904 100644 --- a/bsnes/gb/SDL/shader.c +++ b/bsnes/gb/SDL/shader.c @@ -62,8 +62,11 @@ static GLuint create_program(const char *vsh, const char *fsh) return program; } +extern bool uses_gl(void); bool init_shader_with_name(shader_t *shader, const char *name) { + if (!uses_gl()) return false; + GLint major = 0, minor = 0; glGetIntegerv(GL_MAJOR_VERSION, &major); glGetIntegerv(GL_MINOR_VERSION, &minor); @@ -187,6 +190,7 @@ void render_bitmap_with_shader(shader_t *shader, void *bitmap, void *previous, void free_shader(shader_t *shader) { + if (!uses_gl()) return; GLint major = 0, minor = 0; glGetIntegerv(GL_MAJOR_VERSION, &major); glGetIntegerv(GL_MINOR_VERSION, &minor); diff --git a/bsnes/gb/SDL/utils.c b/bsnes/gb/SDL/utils.c index 8cdd00b9..603e34a8 100644 --- a/bsnes/gb/SDL/utils.c +++ b/bsnes/gb/SDL/utils.c @@ -1,13 +1,11 @@ #include #include #include +#include #include "utils.h" -const char *resource_folder(void) +static const char *resource_folder(void) { -#ifdef DATA_DIR - return DATA_DIR; -#else static const char *ret = NULL; if (!ret) { ret = SDL_GetBasePath(); @@ -16,13 +14,19 @@ const char *resource_folder(void) } } return ret; -#endif } char *resource_path(const char *filename) { static char path[1024]; + snprintf(path, sizeof(path), "%s%s", resource_folder(), filename); +#ifdef DATA_DIR + if (access(path, F_OK) == 0) { + return path; + } + snprintf(path, sizeof(path), "%s%s", DATA_DIR, filename); +#endif return path; } diff --git a/bsnes/gb/SDL/utils.h b/bsnes/gb/SDL/utils.h index 216e723e..5c0383d3 100644 --- a/bsnes/gb/SDL/utils.h +++ b/bsnes/gb/SDL/utils.h @@ -2,7 +2,6 @@ #define utils_h #include -const char *resource_folder(void); char *resource_path(const char *filename); void replace_extension(const char *src, size_t length, char *dest, const char *ext); diff --git a/bsnes/gb/Shaders/MasterShader.metal b/bsnes/gb/Shaders/MasterShader.metal index b900176f..2f3113e3 100644 --- a/bsnes/gb/Shaders/MasterShader.metal +++ b/bsnes/gb/Shaders/MasterShader.metal @@ -66,7 +66,7 @@ fragment float4 fragment_shader(rasterizer_data in [[stage_in]], switch (*frame_blending_mode) { default: case DISABLED: - return scale(image, in.texcoords, input_resolution, *output_resolution); + return pow(scale(image, in.texcoords, input_resolution, *output_resolution), 1 / GAMMA); case SIMPLE: ratio = 0.5; break; diff --git a/bsnes/gb/Tester/main.c b/bsnes/gb/Tester/main.c index 16dbf7bb..912563c3 100755 --- a/bsnes/gb/Tester/main.c +++ b/bsnes/gb/Tester/main.c @@ -22,25 +22,33 @@ static bool running = false; static char *filename; static char *bmp_filename; static char *log_filename; +static char *sav_filename; static FILE *log_file; static void replace_extension(const char *src, size_t length, char *dest, const char *ext); static bool push_start_a, start_is_not_first, a_is_bad, b_is_confirm, push_faster, push_slower, do_not_stop, push_a_twice, start_is_bad, allow_weird_sp_values, large_stack, push_right, - semi_random, limit_start, pointer_control; + semi_random, limit_start, pointer_control, unsafe_speed_switch; static unsigned int test_length = 60 * 40; GB_gameboy_t gb; static unsigned int frames = 0; -const char bmp_header[] = { -0x42, 0x4D, 0x48, 0x68, 0x01, 0x00, 0x00, 0x00, -0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x38, 0x00, -0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x70, 0xFF, -0xFF, 0xFF, 0x01, 0x00, 0x20, 0x00, 0x03, 0x00, -0x00, 0x00, 0x02, 0x68, 0x01, 0x00, 0x12, 0x0B, -0x00, 0x00, 0x12, 0x0B, 0x00, 0x00, 0x00, 0x00, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, -0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +static bool use_tga = false; +static const uint8_t bmp_header[] = { + 0x42, 0x4D, 0x48, 0x68, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x46, 0x00, 0x00, 0x00, 0x38, 0x00, + 0x00, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x70, 0xFF, + 0xFF, 0xFF, 0x01, 0x00, 0x20, 0x00, 0x03, 0x00, + 0x00, 0x00, 0x02, 0x68, 0x01, 0x00, 0x12, 0x0B, + 0x00, 0x00, 0x12, 0x0B, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static const uint8_t tga_header[] = { + 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xA0, 0x00, 0x90, 0x00, + 0x20, 0x28, }; uint32_t bitmap[160*144]; @@ -52,6 +60,9 @@ static char *async_input_callback(GB_gameboy_t *gb) static void handle_buttons(GB_gameboy_t *gb) { + if (!gb->cgb_double_speed && unsafe_speed_switch) { + return; + } /* Do not press any buttons during the last two seconds, this might cause a screenshot to be taken while the LCD is off if the press makes the game load graphics. */ @@ -121,7 +132,7 @@ static void vblank(GB_gameboy_t *gb) gb->registers[GB_REGISTER_SP], gb->backtrace_size); frames = test_length - 1; } - if (gb->halted && !gb->interrupt_enable) { + if (gb->halted && !gb->interrupt_enable && gb->speed_switch_halt_countdown == 0) { GB_log(gb, "The game is deadlocked.\n"); frames = test_length - 1; } @@ -139,7 +150,12 @@ static void vblank(GB_gameboy_t *gb) /* Let the test run for extra four seconds if the screen is off/disabled */ if (!is_screen_blank || frames >= test_length + 60 * 4) { FILE *f = fopen(bmp_filename, "wb"); - fwrite(&bmp_header, 1, sizeof(bmp_header), f); + if (use_tga) { + fwrite(&tga_header, 1, sizeof(tga_header), f); + } + else { + fwrite(&bmp_header, 1, sizeof(bmp_header), f); + } fwrite(&bitmap, 1, sizeof(bitmap), f); fclose(f); if (!gb->boot_rom_finished) { @@ -148,6 +164,9 @@ static void vblank(GB_gameboy_t *gb) if (is_screen_blank) { GB_log(gb, "Game probably stuck with blank screen. \n"); } + if (sav_filename) { + GB_save_battery(gb, sav_filename); + } running = false; } } @@ -215,7 +234,17 @@ static char *executable_relative_path(const char *filename) static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) { +#ifdef GB_BIG_ENDIAN + if (use_tga) { + return (r << 8) | (g << 16) | (b << 24); + } + return (r << 0) | (g << 8) | (b << 16); +#else + if (use_tga) { + return (r << 16) | (g << 8) | (b); + } return (r << 24) | (g << 16) | (b << 8); +#endif } static void replace_extension(const char *src, size_t length, char *dest, const char *ext) @@ -239,12 +268,10 @@ static void replace_extension(const char *src, size_t length, char *dest, const int main(int argc, char **argv) { -#define str(x) #x -#define xstr(x) str(x) - fprintf(stderr, "SameBoy Tester v" xstr(VERSION) "\n"); + fprintf(stderr, "SameBoy Tester v" GB_VERSION "\n"); if (argc == 1) { - fprintf(stderr, "Usage: %s [--dmg] [--start] [--length seconds] [--boot path to boot ROM]" + fprintf(stderr, "Usage: %s [--dmg] [--start] [--length seconds] [--sav] [--boot path to boot ROM]" #ifndef _WIN32 " [--jobs number of tests to run simultaneously]" #endif @@ -258,6 +285,7 @@ int main(int argc, char **argv) #endif bool dmg = false; + bool sav = false; const char *boot_rom_path = NULL; GB_random_set_enabled(false); @@ -268,6 +296,12 @@ int main(int argc, char **argv) dmg = true; continue; } + + if (strcmp(argv[i], "--tga") == 0) { + fprintf(stderr, "Using TGA output\n"); + use_tga = true; + continue; + } if (strcmp(argv[i], "--start") == 0) { fprintf(stderr, "Pushing Start and A\n"); @@ -287,6 +321,12 @@ int main(int argc, char **argv) continue; } + if (strcmp(argv[i], "--sav") == 0) { + fprintf(stderr, "Saving a battery save\n"); + sav = true; + continue; + } + #ifndef _WIN32 if (strcmp(argv[i], "--jobs") == 0 && i != argc - 1) { max_forks = atoi(argv[++i]); @@ -312,13 +352,19 @@ int main(int argc, char **argv) size_t path_length = strlen(filename); char bitmap_path[path_length + 5]; /* At the worst case, size is strlen(path) + 4 bytes for .bmp + NULL */ - replace_extension(filename, path_length, bitmap_path, ".bmp"); + replace_extension(filename, path_length, bitmap_path, use_tga? ".tga" : ".bmp"); bmp_filename = &bitmap_path[0]; char log_path[path_length + 5]; replace_extension(filename, path_length, log_path, ".log"); log_filename = &log_path[0]; + char sav_path[path_length + 5]; + if (sav) { + replace_extension(filename, path_length, sav_path, ".sav"); + sav_filename = &sav_path[0]; + } + fprintf(stderr, "Testing ROM %s\n", filename); if (dmg) { @@ -342,6 +388,7 @@ int main(int argc, char **argv) GB_set_log_callback(&gb, log_callback); GB_set_async_input_callback(&gb, async_input_callback); GB_set_color_correction_mode(&gb, GB_COLOR_CORRECTION_EMULATE_HARDWARE); + GB_set_rtc_mode(&gb, GB_RTC_MODE_ACCURATE); if (GB_load_rom(&gb, filename)) { perror("Failed to load ROM"); @@ -360,7 +407,8 @@ int main(int argc, char **argv) strcmp((const char *)(gb.rom + 0x134), "ONI 5") == 0; b_is_confirm = strcmp((const char *)(gb.rom + 0x134), "ELITE SOCCER") == 0 || strcmp((const char *)(gb.rom + 0x134), "SOCCER") == 0 || - strcmp((const char *)(gb.rom + 0x134), "GEX GECKO") == 0; + strcmp((const char *)(gb.rom + 0x134), "GEX GECKO") == 0 || + strcmp((const char *)(gb.rom + 0x134), "BABE") == 0; push_faster = strcmp((const char *)(gb.rom + 0x134), "MOGURA DE PON!") == 0 || strcmp((const char *)(gb.rom + 0x134), "HUGO2 1/2") == 0 || strcmp((const char *)(gb.rom + 0x134), "HUGO") == 0; @@ -399,6 +447,11 @@ int main(int argc, char **argv) pointer_control = memcmp((const char *)(gb.rom + 0x134), "LEGO ATEAM BLPP", strlen("LEGO ATEAM BLPP")) == 0; push_faster |= pointer_control; + /* Games that perform an unsafe speed switch, don't input until in double speed */ + unsafe_speed_switch = strcmp((const char *)(gb.rom + 0x134), "GBVideo") == 0 || // lulz this is my fault + strcmp((const char *)(gb.rom + 0x134), "POKEMONGOLD 2") == 0; // Pokemon Adventure + + /* Run emulation */ running = true; gb.turbo = gb.turbo_dont_skip = gb.disable_rendering = true; diff --git a/bsnes/gb/Windows/sameboy.ico b/bsnes/gb/Windows/sameboy.ico index 685523e5..bd8e372d 100644 Binary files a/bsnes/gb/Windows/sameboy.ico and b/bsnes/gb/Windows/sameboy.ico differ diff --git a/bsnes/gb/Windows/stdio.h b/bsnes/gb/Windows/stdio.h index ef21ea48..1e6ec02f 100755 --- a/bsnes/gb/Windows/stdio.h +++ b/bsnes/gb/Windows/stdio.h @@ -11,7 +11,7 @@ int access(const char *filename, int mode); static inline int vasprintf(char **str, const char *fmt, va_list args) { size_t size = _vscprintf(fmt, args) + 1; - *str = malloc(size); + *str = (char*)malloc(size); int ret = vsprintf(*str, fmt, args); if (ret != size - 1) { free(*str); @@ -24,7 +24,7 @@ static inline int vasprintf(char **str, const char *fmt, va_list args) #endif /* This code is public domain -- Will Hartung 4/9/09 */ -static inline size_t getline(char **lineptr, size_t *n, FILE *stream) +static inline size_t getline(char **lineptr, size_t *n, FILE *stream) { char *bufptr = NULL; char *p = bufptr; @@ -48,7 +48,7 @@ static inline size_t getline(char **lineptr, size_t *n, FILE *stream) return -1; } if (bufptr == NULL) { - bufptr = malloc(128); + bufptr = (char*)malloc(128); if (bufptr == NULL) { return -1; } @@ -58,7 +58,7 @@ static inline size_t getline(char **lineptr, size_t *n, FILE *stream) while (c != EOF) { if ((p - bufptr) > (size - 1)) { size = size + 128; - bufptr = realloc(bufptr, size); + bufptr = (char*)realloc(bufptr, size); if (bufptr == NULL) { return -1; } diff --git a/bsnes/gb/build-faq.md b/bsnes/gb/build-faq.md index 9def1349..09214363 100644 --- a/bsnes/gb/build-faq.md +++ b/bsnes/gb/build-faq.md @@ -24,7 +24,7 @@ The following examples will be referenced later: ### rgbds -After downloading [rgbds](https://github.com/bentley/rgbds/releases/), ensure that it is added to the `%PATH%`. This may be done by adding it to the user's or SYSTEM's Environment Variables, or may be added to the command line at compilation time via `set path=%path%;C:\path\to\rgbds`. +After downloading [rgbds](https://github.com/gbdev/rgbds/releases/), ensure that it is added to the `%PATH%`. This may be done by adding it to the user's or SYSTEM's Environment Variables, or may be added to the command line at compilation time via `set path=%path%;C:\path\to\rgbds`. ### GnuWin diff --git a/bsnes/gb/libretro/Makefile b/bsnes/gb/libretro/Makefile index 2ed87b8c..ada200df 100644 --- a/bsnes/gb/libretro/Makefile +++ b/bsnes/gb/libretro/Makefile @@ -119,6 +119,17 @@ else ifeq ($(platform), classic_armv7_a7) LDFLAGS += -static-libgcc -static-libstdc++ endif endif + +########################### +# Raspberry Pi 4 in 64 mode +else ifneq (,$(findstring rpi4_64,$(platform))) + EXT ?= so + TARGET := $(TARGET_NAME)_libretro.$(EXT) + fpic := -fPIC + SHARED := -shared -Wl,--version-script=$(CORE_DIR)/libretro/link.T -Wl,--no-undefined + CFLAGS += -O2 -march=armv8-a+crc+simd -mtune=cortex-a72 +########################### + ####################################### # Nintendo Switch (libtransistor) else ifeq ($(platform), switch) @@ -300,7 +311,7 @@ endif include Makefile.common -CFLAGS += -DSAMEBOY_CORE_VERSION=\"$(VERSION)\" +CFLAGS += -DGB_VERSION=\"$(VERSION)\" OBJECTS := $(patsubst $(CORE_DIR)/%.c,$(CORE_DIR)/build/obj/%_libretro.c.o,$(SOURCES_C)) diff --git a/bsnes/gb/libretro/jni/Android.mk b/bsnes/gb/libretro/jni/Android.mk index e0646b9a..8ac1b3ba 100644 --- a/bsnes/gb/libretro/jni/Android.mk +++ b/bsnes/gb/libretro/jni/Android.mk @@ -8,7 +8,7 @@ include $(CORE_DIR)/libretro/Makefile.common GENERATED_SOURCES := $(filter %_boot.c,$(SOURCES_C)) -COREFLAGS := -DINLINE=inline -D__LIBRETRO__ -DGB_INTERNAL $(INCFLAGS) -DSAMEBOY_CORE_VERSION=\"$(VERSION)\" -Wno-multichar -DANDROID +COREFLAGS := -DINLINE=inline -D__LIBRETRO__ -DGB_INTERNAL $(INCFLAGS) -DGB_VERSION=\"$(VERSION)\" -Wno-multichar -DANDROID GIT_VERSION := " $(shell git rev-parse --short HEAD || echo unknown)" ifneq ($(GIT_VERSION)," unknown") diff --git a/bsnes/gb/libretro/libretro.c b/bsnes/gb/libretro/libretro.c index 9e27f031..5a559c4c 100644 --- a/bsnes/gb/libretro/libretro.c +++ b/bsnes/gb/libretro/libretro.c @@ -6,7 +6,7 @@ #include #include #include - +#include #ifndef WIIU #define AUDIO_FREQUENCY 384000 #else @@ -22,6 +22,7 @@ #include #include "libretro.h" +#include "libretro_core_options.inc" #ifdef _WIN32 static const char slash = '\\'; @@ -45,20 +46,24 @@ char battery_save_path[512]; char symbols_path[512]; enum model { - MODEL_DMG, - MODEL_CGB, + MODEL_DMG_B, + MODEL_CGB_C, + MODEL_CGB_E, MODEL_AGB, - MODEL_SGB, + MODEL_SGB_PAL, + MODEL_SGB_NTSC, MODEL_SGB2, MODEL_AUTO }; static const GB_model_t libretro_to_internal_model[] = { - [MODEL_DMG] = GB_MODEL_DMG_B, - [MODEL_CGB] = GB_MODEL_CGB_E, + [MODEL_DMG_B] = GB_MODEL_DMG_B, + [MODEL_CGB_C] = GB_MODEL_CGB_C, + [MODEL_CGB_E] = GB_MODEL_CGB_E, [MODEL_AGB] = GB_MODEL_AGB, - [MODEL_SGB] = GB_MODEL_SGB, + [MODEL_SGB_PAL] = GB_MODEL_SGB_PAL, + [MODEL_SGB_NTSC] = GB_MODEL_SGB_NTSC, [MODEL_SGB2] = GB_MODEL_SGB2 }; @@ -73,7 +78,7 @@ enum audio_out { }; static enum model model[2]; -static enum model auto_model = MODEL_CGB; +static enum model auto_model = MODEL_CGB_E; static uint32_t *frame_buf = NULL; static uint32_t *frame_buf_copy = NULL; @@ -131,7 +136,7 @@ static void GB_update_keys_status(GB_gameboy_t *gb, unsigned port) else { unsigned j; - for (j = 0; j < (RETRO_DEVICE_ID_JOYPAD_R3+1); j++) { + for (j = 0; j < (RETRO_DEVICE_ID_JOYPAD_R3 + 1); j++) { if (input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, j)) { joypad_bits |= (1 << j); } @@ -160,7 +165,7 @@ static void GB_update_keys_status(GB_gameboy_t *gb, unsigned port) static void rumble_callback(GB_gameboy_t *gb, double amplitude) { if (!rumble.set_rumble_state) return; - + if (gb == &gameboy[0]) { rumble.set_rumble_state(0, RETRO_RUMBLE_STRONG, 65535 * amplitude); } @@ -213,6 +218,16 @@ static bool serial_end2(GB_gameboy_t *gb) return ret; } +static void infrared_callback1(GB_gameboy_t *gb, bool output) +{ + GB_set_infrared_input(&gameboy[1], output); +} + +static void infrared_callback2(GB_gameboy_t *gb, bool output) +{ + GB_set_infrared_input(&gameboy[0], output); +} + static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) { return r <<16 | g <<8 | b; @@ -220,32 +235,68 @@ static uint32_t rgb_encode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b) static retro_environment_t environ_cb; -/* variables for single cart mode */ -static const struct retro_variable vars_single[] = { - { "sameboy_color_correction_mode", "Color correction; emulate hardware|preserve brightness|reduce contrast|off|correct curves" }, - { "sameboy_high_pass_filter_mode", "High-pass filter; accurate|remove dc offset|off" }, - { "sameboy_model", "Emulated model (Restart game); Auto|Game Boy|Game Boy Color|Game Boy Advance|Super Game Boy|Super Game Boy 2" }, - { "sameboy_border", "Display border; Super Game Boy only|always|never" }, - { "sameboy_rumble", "Enable rumble; rumble-enabled games|all games|never" }, - { NULL } -}; +static void set_variable_visibility(void) +{ + struct retro_core_option_display option_display_singlecart; + struct retro_core_option_display option_display_dualcart; -/* variables for dual cart dual gameboy mode */ -static const struct retro_variable vars_dual[] = { - { "sameboy_link", "Link cable emulation; enabled|disabled" }, - /*{ "sameboy_ir", "Infrared Sensor Emulation; disabled|enabled" },*/ - { "sameboy_screen_layout", "Screen layout; top-down|left-right" }, - { "sameboy_audio_output", "Audio output; Game Boy #1|Game Boy #2" }, - { "sameboy_model_1", "Emulated model for Game Boy #1 (Restart game); Auto|Game Boy|Game Boy Color|Game Boy Advance" }, - { "sameboy_model_2", "Emulated model for Game Boy #2 (Restart game); Auto|Game Boy|Game Boy Color|Game Boy Advance" }, - { "sameboy_color_correction_mode_1", "Color correction for Game Boy #1; emulate hardware|preserve brightness|reduce contrast|off|correct curves" }, - { "sameboy_color_correction_mode_2", "Color correction for Game Boy #2; emulate hardware|preserve brightness|reduce contrast|off|correct curves" }, - { "sameboy_high_pass_filter_mode_1", "High-pass filter for Game Boy #1; accurate|remove dc offset|off" }, - { "sameboy_high_pass_filter_mode_2", "High-pass filter for Game Boy #2; accurate|remove dc offset|off" }, - { "sameboy_rumble_1", "Enable rumble for Game Boy #1; rumble-enabled games|all games|never" }, - { "sameboy_rumble_2", "Enable rumble for Game Boy #2; rumble-enabled games|all games|never" }, - { NULL } -}; + size_t i; + size_t num_options = 0; + + // Show/hide options depending on the number of emulated devices + if (emulated_devices == 1) { + option_display_singlecart.visible = true; + option_display_dualcart.visible = false; + } + else if (emulated_devices == 2) { + option_display_singlecart.visible = false; + option_display_dualcart.visible = true; + } + + // Determine number of options + while (true) { + if (!option_defs_us[num_options].key) break; + num_options++; + } + + // Copy parameters from option_defs_us array + for (i = 0; i < num_options; i++) { + const char *key = option_defs_us[i].key; + if ((strcmp(key, "sameboy_model") == 0) || + (strcmp(key, "sameboy_rtc") == 0) || + (strcmp(key, "sameboy_scaling_filter") == 0) || + (strcmp(key, "sameboy_mono_palette") == 0) || + (strcmp(key, "sameboy_color_correction_mode") == 0) || + (strcmp(key, "sameboy_light_temperature") == 0) || + (strcmp(key, "sameboy_border") == 0) || + (strcmp(key, "sameboy_high_pass_filter_mode") == 0) || + (strcmp(key, "sameboy_audio_interference") == 0) || + (strcmp(key, "sameboy_rumble") == 0)) { + option_display_singlecart.key = key; + environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display_singlecart); + } + else if ((strcmp(key, "sameboy_link") == 0) || + (strcmp(key, "sameboy_screen_layout") == 0) || + (strcmp(key, "sameboy_audio_output") == 0) || + (strcmp(key, "sameboy_model_1") == 0) || + (strcmp(key, "sameboy_model_2") == 0) || + (strcmp(key, "sameboy_mono_palette_1") == 0) || + (strcmp(key, "sameboy_mono_palette_2") == 0) || + (strcmp(key, "sameboy_color_correction_mode_1") == 0) || + (strcmp(key, "sameboy_color_correction_mode_2") == 0) || + (strcmp(key, "sameboy_light_temperature_1") == 0) || + (strcmp(key, "sameboy_light_temperature_2") == 0) || + (strcmp(key, "sameboy_high_pass_filter_mode_1") == 0) || + (strcmp(key, "sameboy_high_pass_filter_mode_2") == 0) || + (strcmp(key, "sameboy_audio_interference_1") == 0) || + (strcmp(key, "sameboy_audio_interference_2") == 0) || + (strcmp(key, "sameboy_rumble_1") == 0) || + (strcmp(key, "sameboy_rumble_2") == 0)) { + option_display_dualcart.key = key; + environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY, &option_display_dualcart); + } + } +} static const struct retro_subsystem_memory_info gb1_memory[] = { { "srm", RETRO_MEMORY_GAMEBOY_1_SRAM }, @@ -346,23 +397,27 @@ static struct retro_input_descriptor descriptors_4p[] = { static void set_link_cable_state(bool state) { - if (state && emulated_devices == 2) { + if (state && emulated_devices == 2) { GB_set_serial_transfer_bit_start_callback(&gameboy[0], serial_start1); GB_set_serial_transfer_bit_end_callback(&gameboy[0], serial_end1); GB_set_serial_transfer_bit_start_callback(&gameboy[1], serial_start2); GB_set_serial_transfer_bit_end_callback(&gameboy[1], serial_end2); + GB_set_infrared_callback(&gameboy[0], infrared_callback1); + GB_set_infrared_callback(&gameboy[1], infrared_callback2); } - else if (!state) { + else if (!state) { GB_set_serial_transfer_bit_start_callback(&gameboy[0], NULL); GB_set_serial_transfer_bit_end_callback(&gameboy[0], NULL); GB_set_serial_transfer_bit_start_callback(&gameboy[1], NULL); GB_set_serial_transfer_bit_end_callback(&gameboy[1], NULL); + GB_set_infrared_callback(&gameboy[0], NULL); + GB_set_infrared_callback(&gameboy[1], NULL); } } static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) { - const char *model_name = (char *[]){ + const char *model_name = (char *[]) { [GB_BOOT_ROM_DMG0] = "dmg0", [GB_BOOT_ROM_DMG] = "dmg", [GB_BOOT_ROM_MGB] = "mgb", @@ -372,9 +427,8 @@ static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) [GB_BOOT_ROM_CGB] = "cgb", [GB_BOOT_ROM_AGB] = "agb", }[type]; - - const uint8_t *boot_code = (const unsigned char *[]) - { + + const uint8_t *boot_code = (const unsigned char *[]) { [GB_BOOT_ROM_DMG0] = dmg_boot, // dmg0 not implemented yet [GB_BOOT_ROM_DMG] = dmg_boot, [GB_BOOT_ROM_MGB] = dmg_boot, // mgb not implemented yet @@ -384,8 +438,8 @@ static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) [GB_BOOT_ROM_CGB] = cgb_boot, [GB_BOOT_ROM_AGB] = agb_boot, }[type]; - - unsigned boot_length = (unsigned []){ + + unsigned boot_length = (unsigned []) { [GB_BOOT_ROM_DMG0] = dmg_boot_length, // dmg0 not implemented yet [GB_BOOT_ROM_DMG] = dmg_boot_length, [GB_BOOT_ROM_MGB] = dmg_boot_length, // mgb not implemented yet @@ -395,7 +449,7 @@ static void boot_rom_load(GB_gameboy_t *gb, GB_boot_rom_t type) [GB_BOOT_ROM_CGB] = cgb_boot_length, [GB_BOOT_ROM_AGB] = agb_boot_length, }[type]; - + char buf[256]; snprintf(buf, sizeof(buf), "%s%c%s_boot.bin", retro_system_directory, slash, model_name); log_cb(RETRO_LOG_INFO, "Initializing as model: %s\n", model_name); @@ -512,7 +566,7 @@ static void init_for_current_model(unsigned id) } /* Let's be extremely nitpicky about how devices and descriptors are set */ - if (emulated_devices == 1 && (model[0] == MODEL_SGB || model[0] == MODEL_SGB2)) { + if (emulated_devices == 1 && (model[0] == MODEL_SGB_PAL || model[0] == MODEL_SGB_NTSC || model[0] == MODEL_SGB2)) { static const struct retro_controller_info ports[] = { { controllers_sgb, 1 }, { controllers_sgb, 1 }, @@ -523,7 +577,7 @@ static void init_for_current_model(unsigned id) environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports); environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, descriptors_4p); } - else if (emulated_devices == 1) { + else if (emulated_devices == 1) { static const struct retro_controller_info ports[] = { { controllers, 1 }, { NULL, 0 }, @@ -531,7 +585,7 @@ static void init_for_current_model(unsigned id) environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports); environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, descriptors_1p); } - else { + else { static const struct retro_controller_info ports[] = { { controllers, 1 }, { controllers, 1 }, @@ -540,16 +594,76 @@ static void init_for_current_model(unsigned id) environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports); environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, descriptors_2p); } - } static void check_variables() { struct retro_variable var = {0}; - if (emulated_devices == 1) { + if (emulated_devices == 1) { + + var.key = "sameboy_model"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + enum model new_model = model[0]; + if (strcmp(var.value, "Game Boy") == 0) { + new_model = MODEL_DMG_B; + } + else if (strcmp(var.value, "Game Boy Color C") == 0) { + new_model = MODEL_CGB_C; + } + else if (strcmp(var.value, "Game Boy Color") == 0) { + new_model = MODEL_CGB_E; + } + else if (strcmp(var.value, "Game Boy Advance") == 0) { + new_model = MODEL_AGB; + } + else if (strcmp(var.value, "Super Game Boy") == 0) { + new_model = MODEL_SGB_NTSC; + } + else if (strcmp(var.value, "Super Game Boy PAL") == 0) { + new_model = MODEL_SGB_PAL; + } + else if (strcmp(var.value, "Super Game Boy 2") == 0) { + new_model = MODEL_SGB2; + } + else { + new_model = MODEL_AUTO; + } + + model[0] = new_model; + } + + var.key = "sameboy_rtc"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "sync to system clock") == 0) { + GB_set_rtc_mode(&gameboy[0], GB_RTC_MODE_SYNC_TO_HOST); + } + else if (strcmp(var.value, "accurate") == 0) { + GB_set_rtc_mode(&gameboy[0], GB_RTC_MODE_ACCURATE); + } + } + + var.key = "sameboy_mono_palette"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "greyscale") == 0) { + GB_set_palette(&gameboy[0], &GB_PALETTE_GREY); + } + else if (strcmp(var.value, "lime") == 0) { + GB_set_palette(&gameboy[0], &GB_PALETTE_DMG); + } + else if (strcmp(var.value, "olive") == 0) { + GB_set_palette(&gameboy[0], &GB_PALETTE_MGB); + } + else if (strcmp(var.value, "teal") == 0) { + GB_set_palette(&gameboy[0], &GB_PALETTE_GBL); + } + } + var.key = "sameboy_color_correction_mode"; var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { if (strcmp(var.value, "off") == 0) { GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_DISABLED); } @@ -565,60 +679,15 @@ static void check_variables() else if (strcmp(var.value, "reduce contrast") == 0) { GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_REDUCE_CONTRAST); } + else if (strcmp(var.value, "harsh reality") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_LOW_CONTRAST); + } } - - var.key = "sameboy_rumble"; + + var.key = "sameboy_light_temperature"; var.value = NULL; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { - if (strcmp(var.value, "never") == 0) { - GB_set_rumble_mode(&gameboy[0], GB_RUMBLE_DISABLED); - } - else if (strcmp(var.value, "rumble-enabled games") == 0) { - GB_set_rumble_mode(&gameboy[0], GB_RUMBLE_CARTRIDGE_ONLY); - } - else if (strcmp(var.value, "all games") == 0) { - GB_set_rumble_mode(&gameboy[0], GB_RUMBLE_ALL_GAMES); - } - } - - var.key = "sameboy_high_pass_filter_mode"; - var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { - if (strcmp(var.value, "off") == 0) { - GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_OFF); - } - else if (strcmp(var.value, "accurate") == 0) { - GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_ACCURATE); - } - else if (strcmp(var.value, "remove dc offset") == 0) { - GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_REMOVE_DC_OFFSET); - } - } - - var.key = "sameboy_model"; - var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { - enum model new_model = model[0]; - if (strcmp(var.value, "Game Boy") == 0) { - new_model = MODEL_DMG; - } - else if (strcmp(var.value, "Game Boy Color") == 0) { - new_model = MODEL_CGB; - } - else if (strcmp(var.value, "Game Boy Advance") == 0) { - new_model = MODEL_AGB; - } - else if (strcmp(var.value, "Super Game Boy") == 0) { - new_model = MODEL_SGB; - } - else if (strcmp(var.value, "Super Game Boy 2") == 0) { - new_model = MODEL_SGB2; - } - else { - new_model = MODEL_AUTO; - } - - model[0] = new_model; + GB_set_light_temperature(&gameboy[0], atof(var.value)); } var.key = "sameboy_border"; @@ -633,55 +702,30 @@ static void check_variables() else if (strcmp(var.value, "always") == 0) { GB_set_border_mode(&gameboy[0], GB_BORDER_ALWAYS); } - geometry_updated = true; } - } - else { - GB_set_border_mode(&gameboy[0], GB_BORDER_NEVER); - GB_set_border_mode(&gameboy[1], GB_BORDER_NEVER); - var.key = "sameboy_color_correction_mode_1"; + + var.key = "sameboy_high_pass_filter_mode"; var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { if (strcmp(var.value, "off") == 0) { - GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_DISABLED); + GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_OFF); } - else if (strcmp(var.value, "correct curves") == 0) { - GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_CORRECT_CURVES); + else if (strcmp(var.value, "accurate") == 0) { + GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_ACCURATE); } - else if (strcmp(var.value, "emulate hardware") == 0) { - GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_EMULATE_HARDWARE); - } - else if (strcmp(var.value, "preserve brightness") == 0) { - GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS); - } - else if (strcmp(var.value, "reduce contrast") == 0) { - GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_REDUCE_CONTRAST); + else if (strcmp(var.value, "remove dc offset") == 0) { + GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_REMOVE_DC_OFFSET); } } - var.key = "sameboy_color_correction_mode_2"; + var.key = "sameboy_audio_interference"; var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { - if (strcmp(var.value, "off") == 0) { - GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_DISABLED); - } - else if (strcmp(var.value, "correct curves") == 0) { - GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_CORRECT_CURVES); - } - else if (strcmp(var.value, "emulate hardware") == 0) { - GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_EMULATE_HARDWARE); - } - else if (strcmp(var.value, "preserve brightness") == 0) { - GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS); - } - else if (strcmp(var.value, "reduce contrast") == 0) { - GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_REDUCE_CONTRAST); - } - + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + GB_set_interference_volume(&gameboy[0], atoi(var.value) / 100.0); } - - var.key = "sameboy_rumble_1"; + + var.key = "sameboy_rumble"; var.value = NULL; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { if (strcmp(var.value, "never") == 0) { @@ -694,117 +738,17 @@ static void check_variables() GB_set_rumble_mode(&gameboy[0], GB_RUMBLE_ALL_GAMES); } } - - var.key = "sameboy_rumble_2"; - var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { - if (strcmp(var.value, "never") == 0) { - GB_set_rumble_mode(&gameboy[1], GB_RUMBLE_DISABLED); - } - else if (strcmp(var.value, "rumble-enabled games") == 0) { - GB_set_rumble_mode(&gameboy[1], GB_RUMBLE_CARTRIDGE_ONLY); - } - else if (strcmp(var.value, "all games") == 0) { - GB_set_rumble_mode(&gameboy[1], GB_RUMBLE_ALL_GAMES); - } - } - var.key = "sameboy_high_pass_filter_mode_1"; - var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { - if (strcmp(var.value, "off") == 0) { - GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_OFF); - } - else if (strcmp(var.value, "accurate") == 0) { - GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_ACCURATE); - } - else if (strcmp(var.value, "remove dc offset") == 0) { - GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_REMOVE_DC_OFFSET); - } - } - - var.key = "sameboy_high_pass_filter_mode_2"; - var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { - if (strcmp(var.value, "off") == 0) { - GB_set_highpass_filter_mode(&gameboy[1], GB_HIGHPASS_OFF); - } - else if (strcmp(var.value, "accurate") == 0) { - GB_set_highpass_filter_mode(&gameboy[1], GB_HIGHPASS_ACCURATE); - } - else if (strcmp(var.value, "remove dc offset") == 0) { - GB_set_highpass_filter_mode(&gameboy[1], GB_HIGHPASS_REMOVE_DC_OFFSET); - } - } - - var.key = "sameboy_model_1"; - var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { - enum model new_model = model[0]; - if (strcmp(var.value, "Game Boy") == 0) { - new_model = MODEL_DMG; - } - else if (strcmp(var.value, "Game Boy Color") == 0) { - new_model = MODEL_CGB; - } - else if (strcmp(var.value, "Game Boy Advance") == 0) { - new_model = MODEL_AGB; - } - else if (strcmp(var.value, "Super Game Boy") == 0) { - new_model = MODEL_SGB; - } - else if (strcmp(var.value, "Super Game Boy 2") == 0) { - new_model = MODEL_SGB2; - } - else { - new_model = MODEL_AUTO; - } - - model[0] = new_model; - } - - var.key = "sameboy_model_2"; - var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { - enum model new_model = model[1]; - if (strcmp(var.value, "Game Boy") == 0) { - new_model = MODEL_DMG; - } - else if (strcmp(var.value, "Game Boy Color") == 0) { - new_model = MODEL_CGB; - } - else if (strcmp(var.value, "Game Boy Advance") == 0) { - new_model = MODEL_AGB; - } - else if (strcmp(var.value, "Super Game Boy") == 0) { - new_model = MODEL_SGB; - } - else if (strcmp(var.value, "Super Game Boy 2") == 0) { - new_model = MODEL_SGB; - } - else { - new_model = MODEL_AUTO; - } - - model[1] = new_model; - } - - var.key = "sameboy_screen_layout"; - var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { - if (strcmp(var.value, "top-down") == 0) { - screen_layout = LAYOUT_TOP_DOWN; - } - else { - screen_layout = LAYOUT_LEFT_RIGHT; - } - - geometry_updated = true; - } + } + else { + GB_set_border_mode(&gameboy[0], GB_BORDER_NEVER); + GB_set_border_mode(&gameboy[1], GB_BORDER_NEVER); + GB_set_rtc_mode(&gameboy[0], GB_RTC_MODE_ACCURATE); + GB_set_rtc_mode(&gameboy[1], GB_RTC_MODE_ACCURATE); var.key = "sameboy_link"; var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { bool tmp = link_cable_emulation; if (strcmp(var.value, "enabled") == 0) { link_cable_emulation = true; @@ -820,9 +764,22 @@ static void check_variables() } } + var.key = "sameboy_screen_layout"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "top-down") == 0) { + screen_layout = LAYOUT_TOP_DOWN; + } + else { + screen_layout = LAYOUT_LEFT_RIGHT; + } + + geometry_updated = true; + } + var.key = "sameboy_audio_output"; var.value = NULL; - if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { if (strcmp(var.value, "Game Boy #1") == 0) { audio_out = GB_1; } @@ -830,7 +787,233 @@ static void check_variables() audio_out = GB_2; } } + + var.key = "sameboy_model_1"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + enum model new_model = model[0]; + if (strcmp(var.value, "Game Boy") == 0) { + new_model = MODEL_DMG_B; + } + else if (strcmp(var.value, "Game Boy Color C") == 0) { + new_model = MODEL_CGB_C; + } + else if (strcmp(var.value, "Game Boy Color") == 0) { + new_model = MODEL_CGB_E; + } + else if (strcmp(var.value, "Game Boy Advance") == 0) { + new_model = MODEL_AGB; + } + else if (strcmp(var.value, "Super Game Boy") == 0) { + new_model = MODEL_SGB_NTSC; + } + else if (strcmp(var.value, "Super Game Boy PAL") == 0) { + new_model = MODEL_SGB_PAL; + } + else if (strcmp(var.value, "Super Game Boy 2") == 0) { + new_model = MODEL_SGB2; + } + else { + new_model = MODEL_AUTO; + } + + model[0] = new_model; + } + + var.key = "sameboy_model_2"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + enum model new_model = model[1]; + if (strcmp(var.value, "Game Boy") == 0) { + new_model = MODEL_DMG_B; + } + else if (strcmp(var.value, "Game Boy Color C") == 0) { + new_model = MODEL_CGB_C; + } + else if (strcmp(var.value, "Game Boy Color") == 0) { + new_model = MODEL_CGB_E; + } + else if (strcmp(var.value, "Game Boy Advance") == 0) { + new_model = MODEL_AGB; + } + else if (strcmp(var.value, "Super Game Boy") == 0) { + new_model = MODEL_SGB_NTSC; + } + else if (strcmp(var.value, "Super Game Boy PAL") == 0) { + new_model = MODEL_SGB_PAL; + } + else if (strcmp(var.value, "Super Game Boy 2") == 0) { + new_model = MODEL_SGB2; + } + else { + new_model = MODEL_AUTO; + } + + model[1] = new_model; + } + + var.key = "sameboy_mono_palette_1"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "greyscale") == 0) { + GB_set_palette(&gameboy[0], &GB_PALETTE_GREY); + } + else if (strcmp(var.value, "lime") == 0) { + GB_set_palette(&gameboy[0], &GB_PALETTE_DMG); + } + else if (strcmp(var.value, "olive") == 0) { + GB_set_palette(&gameboy[0], &GB_PALETTE_MGB); + } + else if (strcmp(var.value, "teal") == 0) { + GB_set_palette(&gameboy[0], &GB_PALETTE_GBL); + } + } + + var.key = "sameboy_mono_palette_2"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "greyscale") == 0) { + GB_set_palette(&gameboy[1], &GB_PALETTE_GREY); + } + else if (strcmp(var.value, "lime") == 0) { + GB_set_palette(&gameboy[1], &GB_PALETTE_DMG); + } + else if (strcmp(var.value, "olive") == 0) { + GB_set_palette(&gameboy[1], &GB_PALETTE_MGB); + } + else if (strcmp(var.value, "teal") == 0) { + GB_set_palette(&gameboy[1], &GB_PALETTE_GBL); + } + } + + var.key = "sameboy_color_correction_mode_1"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "off") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_DISABLED); + } + else if (strcmp(var.value, "correct curves") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_CORRECT_CURVES); + } + else if (strcmp(var.value, "emulate hardware") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_EMULATE_HARDWARE); + } + else if (strcmp(var.value, "preserve brightness") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS); + } + else if (strcmp(var.value, "reduce contrast") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_REDUCE_CONTRAST); + } + else if (strcmp(var.value, "harsh reality") == 0) { + GB_set_color_correction_mode(&gameboy[0], GB_COLOR_CORRECTION_LOW_CONTRAST); + } + } + + var.key = "sameboy_color_correction_mode_2"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "off") == 0) { + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_DISABLED); + } + else if (strcmp(var.value, "correct curves") == 0) { + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_CORRECT_CURVES); + } + else if (strcmp(var.value, "emulate hardware") == 0) { + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_EMULATE_HARDWARE); + } + else if (strcmp(var.value, "preserve brightness") == 0) { + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_PRESERVE_BRIGHTNESS); + } + else if (strcmp(var.value, "reduce contrast") == 0) { + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_REDUCE_CONTRAST); + } + else if (strcmp(var.value, "harsh reality") == 0) { + GB_set_color_correction_mode(&gameboy[1], GB_COLOR_CORRECTION_LOW_CONTRAST); + } + } + + var.key = "sameboy_light_temperature_1"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + GB_set_light_temperature(&gameboy[0], atof(var.value)); + } + + var.key = "sameboy_light_temperature_2"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + GB_set_light_temperature(&gameboy[1], atof(var.value)); + } + + var.key = "sameboy_high_pass_filter_mode_1"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "off") == 0) { + GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_OFF); + } + else if (strcmp(var.value, "accurate") == 0) { + GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_ACCURATE); + } + else if (strcmp(var.value, "remove dc offset") == 0) { + GB_set_highpass_filter_mode(&gameboy[0], GB_HIGHPASS_REMOVE_DC_OFFSET); + } + } + + var.key = "sameboy_high_pass_filter_mode_2"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "off") == 0) { + GB_set_highpass_filter_mode(&gameboy[1], GB_HIGHPASS_OFF); + } + else if (strcmp(var.value, "accurate") == 0) { + GB_set_highpass_filter_mode(&gameboy[1], GB_HIGHPASS_ACCURATE); + } + else if (strcmp(var.value, "remove dc offset") == 0) { + GB_set_highpass_filter_mode(&gameboy[1], GB_HIGHPASS_REMOVE_DC_OFFSET); + } + } + + var.key = "sameboy_audio_interference_1"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + GB_set_interference_volume(&gameboy[0], atoi(var.value) / 100.0); + } + + var.key = "sameboy_audio_interference_2"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + GB_set_interference_volume(&gameboy[1], atoi(var.value) / 100.0); + } + + var.key = "sameboy_rumble_1"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "never") == 0) { + GB_set_rumble_mode(&gameboy[0], GB_RUMBLE_DISABLED); + } + else if (strcmp(var.value, "rumble-enabled games") == 0) { + GB_set_rumble_mode(&gameboy[0], GB_RUMBLE_CARTRIDGE_ONLY); + } + else if (strcmp(var.value, "all games") == 0) { + GB_set_rumble_mode(&gameboy[0], GB_RUMBLE_ALL_GAMES); + } + } + + var.key = "sameboy_rumble_2"; + var.value = NULL; + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) { + if (strcmp(var.value, "never") == 0) { + GB_set_rumble_mode(&gameboy[1], GB_RUMBLE_DISABLED); + } + else if (strcmp(var.value, "rumble-enabled games") == 0) { + GB_set_rumble_mode(&gameboy[1], GB_RUMBLE_CARTRIDGE_ONLY); + } + else if (strcmp(var.value, "all games") == 0) { + GB_set_rumble_mode(&gameboy[1], GB_RUMBLE_ALL_GAMES); + } + } + } + set_variable_visibility(); } void retro_init(void) @@ -888,9 +1071,9 @@ void retro_get_system_info(struct retro_system_info *info) memset(info, 0, sizeof(*info)); info->library_name = "SameBoy"; #ifdef GIT_VERSION - info->library_version = SAMEBOY_CORE_VERSION GIT_VERSION; + info->library_version = GB_VERSION GIT_VERSION; #else - info->library_version = SAMEBOY_CORE_VERSION; + info->library_version = GB_VERSION; #endif info->need_fullpath = true; info->valid_extensions = "gb|gbc"; @@ -901,7 +1084,7 @@ void retro_get_system_av_info(struct retro_system_av_info *info) struct retro_game_geometry geom; struct retro_system_timing timing = { GB_get_usual_frame_rate(&gameboy[0]), AUDIO_FREQUENCY }; - if (emulated_devices == 2) { + if (emulated_devices == 2) { if (screen_layout == LAYOUT_TOP_DOWN) { geom.base_width = GB_get_screen_width(&gameboy[0]); geom.base_height = GB_get_screen_height(&gameboy[0]) * emulated_devices; @@ -913,7 +1096,7 @@ void retro_get_system_av_info(struct retro_system_av_info *info) geom.aspect_ratio = ((double)GB_get_screen_width(&gameboy[0]) * emulated_devices) / GB_get_screen_height(&gameboy[0]); } } - else { + else { geom.base_width = GB_get_screen_width(&gameboy[0]); geom.base_height = GB_get_screen_height(&gameboy[0]); geom.aspect_ratio = (double)GB_get_screen_width(&gameboy[0]) / GB_get_screen_height(&gameboy[0]); @@ -931,6 +1114,8 @@ void retro_set_environment(retro_environment_t cb) { environ_cb = cb; + libretro_set_core_options(environ_cb); + cb(RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO, (void*)subsystems); } @@ -994,11 +1179,11 @@ void retro_run(void) check_variables(); } - if (emulated_devices == 2) { + if (emulated_devices == 2) { GB_update_keys_status(&gameboy[0], 0); GB_update_keys_status(&gameboy[1], 1); } - else if (emulated_devices == 1 && (model[0] == MODEL_SGB || model[0] == MODEL_SGB2)) { + else if (emulated_devices == 1 && (model[0] == MODEL_SGB_PAL || model[0] == MODEL_SGB_NTSC || model[0] == MODEL_SGB2)) { for (unsigned i = 0; i < 4; i++) { GB_update_keys_status(&gameboy[0], i); } @@ -1009,7 +1194,7 @@ void retro_run(void) vblank1_occurred = vblank2_occurred = false; signed delta = 0; - if (emulated_devices == 2) { + if (emulated_devices == 2) { while (!vblank1_occurred || !vblank2_occurred) { if (delta >= 0) { delta -= GB_run(&gameboy[0]); @@ -1019,11 +1204,11 @@ void retro_run(void) } } } - else { + else { GB_run_frame(&gameboy[0]); } - if (emulated_devices == 2) { + if (emulated_devices == 2) { if (screen_layout == LAYOUT_TOP_DOWN) { video_cb(frame_buf, GB_get_screen_width(&gameboy[0]), @@ -1057,24 +1242,23 @@ void retro_run(void) bool retro_load_game(const struct retro_game_info *info) { - environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, (void *)vars_single); check_variables(); frame_buf = (uint32_t *)malloc(MAX_VIDEO_PIXELS * emulated_devices * sizeof(uint32_t)); memset(frame_buf, 0, MAX_VIDEO_PIXELS * emulated_devices * sizeof(uint32_t)); enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_XRGB8888; - if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt)) { + if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt)) { log_cb(RETRO_LOG_INFO, "XRGB8888 is not supported\n"); return false; } - auto_model = (info->path[strlen(info->path) - 1] & ~0x20) == 'C' ? MODEL_CGB : MODEL_DMG; + auto_model = (info->path[strlen(info->path) - 1] & ~0x20) == 'C' ? MODEL_CGB_E : MODEL_DMG_B; snprintf(retro_game_path, sizeof(retro_game_path), "%s", info->path); - for (int i = 0; i < emulated_devices; i++) { + for (int i = 0; i < emulated_devices; i++) { init_for_current_model(i); - if (GB_load_rom(&gameboy[i], info->path)) { + if (GB_load_rom(&gameboy[i], info->path)) { log_cb(RETRO_LOG_INFO, "Failed to load ROM at %s\n", info->path); return false; } @@ -1100,6 +1284,7 @@ bool retro_load_game(const struct retro_game_info *info) void retro_unload_game(void) { for (int i = 0; i < emulated_devices; i++) { + log_cb(RETRO_LOG_INFO, "Unloading GB: %d\n", emulated_devices); GB_free(&gameboy[i]); } } @@ -1119,7 +1304,6 @@ bool retro_load_game_special(unsigned type, const struct retro_game_info *info, return false; /* all other types are unhandled for now */ } - environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, (void *)vars_dual); check_variables(); frame_buf = (uint32_t*)malloc(emulated_devices * MAX_VIDEO_PIXELS * sizeof(uint32_t)); @@ -1129,17 +1313,17 @@ bool retro_load_game_special(unsigned type, const struct retro_game_info *info, memset(frame_buf_copy, 0, emulated_devices * MAX_VIDEO_PIXELS * sizeof(uint32_t)); enum retro_pixel_format fmt = RETRO_PIXEL_FORMAT_XRGB8888; - if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt)) { + if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt)) { log_cb(RETRO_LOG_INFO, "XRGB8888 is not supported\n"); return false; } - auto_model = (info->path[strlen(info->path) - 1] & ~0x20) == 'C' ? MODEL_CGB : MODEL_DMG; + auto_model = (info->path[strlen(info->path) - 1] & ~0x20) == 'C' ? MODEL_CGB_E : MODEL_DMG_B; snprintf(retro_game_path, sizeof(retro_game_path), "%s", info->path); - for (int i = 0; i < emulated_devices; i++) { + for (int i = 0; i < emulated_devices; i++) { init_for_current_model(i); - if (GB_load_rom(&gameboy[i], info[i].path)) { + if (GB_load_rom(&gameboy[i], info[i].path)) { log_cb(RETRO_LOG_INFO, "Failed to load ROM\n"); return false; } @@ -1165,21 +1349,21 @@ size_t retro_serialize_size(void) if (maximum_save_size) { return maximum_save_size * 2; } - + GB_gameboy_t temp; - + GB_init(&temp, GB_MODEL_DMG_B); maximum_save_size = GB_get_save_state_size(&temp); GB_free(&temp); - + GB_init(&temp, GB_MODEL_CGB_E); maximum_save_size = MAX(maximum_save_size, GB_get_save_state_size(&temp)); GB_free(&temp); - + GB_init(&temp, GB_MODEL_SGB2); maximum_save_size = MAX(maximum_save_size, GB_get_save_state_size(&temp)); GB_free(&temp); - + return maximum_save_size * 2; } @@ -1197,7 +1381,7 @@ bool retro_serialize(void *data, size_t size) if (state_size > size) { return false; } - + GB_save_state_to_buffer(&gameboy[i], ((uint8_t *) data) + offset); offset += state_size; size -= state_size; @@ -1208,7 +1392,7 @@ bool retro_serialize(void *data, size_t size) bool retro_unserialize(const void *data, size_t size) { - for (int i = 0; i < emulated_devices; i++) { + for (int i = 0; i < emulated_devices; i++) { size_t state_size = GB_get_save_state_size(&gameboy[i]); if (state_size > size) { return false; @@ -1217,7 +1401,7 @@ bool retro_unserialize(const void *data, size_t size) if (GB_load_state_from_buffer(&gameboy[i], data, state_size)) { return false; } - + size -= state_size; data = ((uint8_t *)data) + state_size; } @@ -1229,8 +1413,8 @@ bool retro_unserialize(const void *data, size_t size) void *retro_get_memory_data(unsigned type) { void *data = NULL; - if (emulated_devices == 1) { - switch (type) { + if (emulated_devices == 1) { + switch (type) { case RETRO_MEMORY_SYSTEM_RAM: data = gameboy[0].ram; break; @@ -1257,8 +1441,8 @@ void *retro_get_memory_data(unsigned type) break; } } - else { - switch (type) { + else { + switch (type) { case RETRO_MEMORY_GAMEBOY_1_SRAM: if (gameboy[0].cartridge_type->has_battery && gameboy[0].mbc_ram_size != 0) { data = gameboy[0].mbc_ram; @@ -1302,8 +1486,8 @@ void *retro_get_memory_data(unsigned type) size_t retro_get_memory_size(unsigned type) { size_t size = 0; - if (emulated_devices == 1) { - switch (type) { + if (emulated_devices == 1) { + switch (type) { case RETRO_MEMORY_SYSTEM_RAM: size = gameboy[0].ram_size; break; @@ -1330,8 +1514,8 @@ size_t retro_get_memory_size(unsigned type) break; } } - else { - switch (type) { + else { + switch (type) { case RETRO_MEMORY_GAMEBOY_1_SRAM: if (gameboy[0].cartridge_type->has_battery && gameboy[0].mbc_ram_size != 0) { size = gameboy[0].mbc_ram_size; diff --git a/bsnes/gb/libretro/libretro.h b/bsnes/gb/libretro/libretro.h index 1fd2f5b7..e6ee6269 100644 --- a/bsnes/gb/libretro/libretro.h +++ b/bsnes/gb/libretro/libretro.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2010-2018 The RetroArch team +/* Copyright (C) 2010-2020 The RetroArch team * * --------------------------------------------------------------------------------------- * The following license statement only applies to this libretro API header (libretro.h). @@ -69,7 +69,7 @@ extern "C" { # endif # endif # else -# if defined(__GNUC__) && __GNUC__ >= 4 && !defined(__CELLOS_LV2__) +# if defined(__GNUC__) && __GNUC__ >= 4 # define RETRO_API RETRO_CALLCONV __attribute__((__visibility__("default"))) # else # define RETRO_API RETRO_CALLCONV @@ -278,6 +278,11 @@ enum retro_language RETRO_LANGUAGE_ARABIC = 16, RETRO_LANGUAGE_GREEK = 17, RETRO_LANGUAGE_TURKISH = 18, + RETRO_LANGUAGE_SLOVAK = 19, + RETRO_LANGUAGE_PERSIAN = 20, + RETRO_LANGUAGE_HEBREW = 21, + RETRO_LANGUAGE_ASTURIAN = 22, + RETRO_LANGUAGE_FINNISH = 23, RETRO_LANGUAGE_LAST, /* Ensure sizeof(enum) == sizeof(int) */ @@ -708,6 +713,9 @@ enum retro_mod * state of rumble motors in controllers. * A strong and weak motor is supported, and they can be * controlled indepedently. + * Should be called from either retro_init() or retro_load_game(). + * Should not be called from retro_set_environment(). + * Returns false if rumble functionality is unavailable. */ #define RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES 24 /* uint64_t * -- @@ -1087,10 +1095,10 @@ enum retro_mod #define RETRO_ENVIRONMENT_GET_TARGET_REFRESH_RATE (50 | RETRO_ENVIRONMENT_EXPERIMENTAL) /* float * -- - * Float value that lets us know what target refresh rate + * Float value that lets us know what target refresh rate * is curently in use by the frontend. * - * The core can use the returned value to set an ideal + * The core can use the returned value to set an ideal * refresh rate/framerate. */ @@ -1098,7 +1106,7 @@ enum retro_mod /* bool * -- * Boolean value that indicates whether or not the frontend supports * input bitmasks being returned by retro_input_state_t. The advantage - * of this is that retro_input_state_t has to be only called once to + * of this is that retro_input_state_t has to be only called once to * grab all button states instead of multiple times. * * If it returns true, you can pass RETRO_DEVICE_ID_JOYPAD_MASK as 'id' @@ -1117,7 +1125,7 @@ enum retro_mod * This may be still be done regardless of the core options * interface version. * - * If version is 1 however, core options may instead be set by + * If version is >= 1 however, core options may instead be set by * passing an array of retro_core_option_definition structs to * RETRO_ENVIRONMENT_SET_CORE_OPTIONS, or a 2D array of * retro_core_option_definition structs to RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL. @@ -1132,8 +1140,8 @@ enum retro_mod * GET_VARIABLE. * This allows the frontend to present these variables to * a user dynamically. - * This should only be called if RETRO_ENVIRONMENT_GET_ENHANCED_CORE_OPTIONS - * returns an API version of 1. + * This should only be called if RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION + * returns an API version of >= 1. * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES. * This should be called the first time as early as * possible (ideally in retro_set_environment). @@ -1169,8 +1177,6 @@ enum retro_mod * i.e. it should be feasible to cycle through options * without a keyboard. * - * First entry should be treated as a default. - * * Example entry: * { * "foo_option", @@ -1196,8 +1202,8 @@ enum retro_mod * GET_VARIABLE. * This allows the frontend to present these variables to * a user dynamically. - * This should only be called if RETRO_ENVIRONMENT_GET_ENHANCED_CORE_OPTIONS - * returns an API version of 1. + * This should only be called if RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION + * returns an API version of >= 1. * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES. * This should be called the first time as early as * possible (ideally in retro_set_environment). @@ -1248,6 +1254,140 @@ enum retro_mod * default when calling SET_VARIABLES/SET_CORE_OPTIONS. */ +#define RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER 56 + /* unsigned * -- + * + * Allows an implementation to ask frontend preferred hardware + * context to use. Core should use this information to deal + * with what specific context to request with SET_HW_RENDER. + * + * 'data' points to an unsigned variable + */ + +#define RETRO_ENVIRONMENT_GET_DISK_CONTROL_INTERFACE_VERSION 57 + /* unsigned * -- + * Unsigned value is the API version number of the disk control + * interface supported by the frontend. If callback return false, + * API version is assumed to be 0. + * + * In legacy code, the disk control interface is defined by passing + * a struct of type retro_disk_control_callback to + * RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE. + * This may be still be done regardless of the disk control + * interface version. + * + * If version is >= 1 however, the disk control interface may + * instead be defined by passing a struct of type + * retro_disk_control_ext_callback to + * RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE. + * This allows the core to provide additional information about + * disk images to the frontend and/or enables extra + * disk control functionality by the frontend. + */ + +#define RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE 58 + /* const struct retro_disk_control_ext_callback * -- + * Sets an interface which frontend can use to eject and insert + * disk images, and also obtain information about individual + * disk image files registered by the core. + * This is used for games which consist of multiple images and + * must be manually swapped out by the user (e.g. PSX, floppy disk + * based systems). + */ + +#define RETRO_ENVIRONMENT_GET_MESSAGE_INTERFACE_VERSION 59 + /* unsigned * -- + * Unsigned value is the API version number of the message + * interface supported by the frontend. If callback returns + * false, API version is assumed to be 0. + * + * In legacy code, messages may be displayed in an + * implementation-specific manner by passing a struct + * of type retro_message to RETRO_ENVIRONMENT_SET_MESSAGE. + * This may be still be done regardless of the message + * interface version. + * + * If version is >= 1 however, messages may instead be + * displayed by passing a struct of type retro_message_ext + * to RETRO_ENVIRONMENT_SET_MESSAGE_EXT. This allows the + * core to specify message logging level, priority and + * destination (OSD, logging interface or both). + */ + +#define RETRO_ENVIRONMENT_SET_MESSAGE_EXT 60 + /* const struct retro_message_ext * -- + * Sets a message to be displayed in an implementation-specific + * manner for a certain amount of 'frames'. Additionally allows + * the core to specify message logging level, priority and + * destination (OSD, logging interface or both). + * Should not be used for trivial messages, which should simply be + * logged via RETRO_ENVIRONMENT_GET_LOG_INTERFACE (or as a + * fallback, stderr). + */ + +#define RETRO_ENVIRONMENT_GET_INPUT_MAX_USERS 61 + /* unsigned * -- + * Unsigned value is the number of active input devices + * provided by the frontend. This may change between + * frames, but will remain constant for the duration + * of each frame. + * If callback returns true, a core need not poll any + * input device with an index greater than or equal to + * the number of active devices. + * If callback returns false, the number of active input + * devices is unknown. In this case, all input devices + * should be considered active. + */ + +#define RETRO_ENVIRONMENT_SET_AUDIO_BUFFER_STATUS_CALLBACK 62 + /* const struct retro_audio_buffer_status_callback * -- + * Lets the core know the occupancy level of the frontend + * audio buffer. Can be used by a core to attempt frame + * skipping in order to avoid buffer under-runs. + * A core may pass NULL to disable buffer status reporting + * in the frontend. + */ + +#define RETRO_ENVIRONMENT_SET_MINIMUM_AUDIO_LATENCY 63 + /* const unsigned * -- + * Sets minimum frontend audio latency in milliseconds. + * Resultant audio latency may be larger than set value, + * or smaller if a hardware limit is encountered. A frontend + * is expected to honour requests up to 512 ms. + * + * - If value is less than current frontend + * audio latency, callback has no effect + * - If value is zero, default frontend audio + * latency is set + * + * May be used by a core to increase audio latency and + * therefore decrease the probability of buffer under-runs + * (crackling) when performing 'intensive' operations. + * A core utilising RETRO_ENVIRONMENT_SET_AUDIO_BUFFER_STATUS_CALLBACK + * to implement audio-buffer-based frame skipping may achieve + * optimal results by setting the audio latency to a 'high' + * (typically 6x or 8x) integer multiple of the expected + * frame time. + * + * WARNING: This can only be called from within retro_run(). + * Calling this can require a full reinitialization of audio + * drivers in the frontend, so it is important to call it very + * sparingly, and usually only with the users explicit consent. + * An eventual driver reinitialize will happen so that audio + * callbacks happening after this call within the same retro_run() + * call will target the newly initialized driver. + */ + +#define RETRO_ENVIRONMENT_SET_FASTFORWARDING_OVERRIDE 64 + /* const struct retro_fastforwarding_override * -- + * Used by a libretro core to override the current + * fastforwarding mode of the frontend. + * If NULL is passed to this function, the frontend + * will return true if fastforwarding override + * functionality is supported (no change in + * fastforwarding state will occur in this case). + */ + /* VFS functionality */ /* File paths: @@ -1383,28 +1523,28 @@ typedef int (RETRO_CALLCONV *retro_vfs_closedir_t)(struct retro_vfs_dir_handle * struct retro_vfs_interface { - /* VFS API v1 */ - retro_vfs_get_path_t get_path; - retro_vfs_open_t open; - retro_vfs_close_t close; - retro_vfs_size_t size; - retro_vfs_tell_t tell; - retro_vfs_seek_t seek; - retro_vfs_read_t read; - retro_vfs_write_t write; - retro_vfs_flush_t flush; - retro_vfs_remove_t remove; - retro_vfs_rename_t rename; - /* VFS API v2 */ - retro_vfs_truncate_t truncate; - /* VFS API v3 */ - retro_vfs_stat_t stat; - retro_vfs_mkdir_t mkdir; - retro_vfs_opendir_t opendir; - retro_vfs_readdir_t readdir; - retro_vfs_dirent_get_name_t dirent_get_name; - retro_vfs_dirent_is_dir_t dirent_is_dir; - retro_vfs_closedir_t closedir; + /* VFS API v1 */ + retro_vfs_get_path_t get_path; + retro_vfs_open_t open; + retro_vfs_close_t close; + retro_vfs_size_t size; + retro_vfs_tell_t tell; + retro_vfs_seek_t seek; + retro_vfs_read_t read; + retro_vfs_write_t write; + retro_vfs_flush_t flush; + retro_vfs_remove_t remove; + retro_vfs_rename_t rename; + /* VFS API v2 */ + retro_vfs_truncate_t truncate; + /* VFS API v3 */ + retro_vfs_stat_t stat; + retro_vfs_mkdir_t mkdir; + retro_vfs_opendir_t opendir; + retro_vfs_readdir_t readdir; + retro_vfs_dirent_get_name_t dirent_get_name; + retro_vfs_dirent_is_dir_t dirent_is_dir; + retro_vfs_closedir_t closedir; }; struct retro_vfs_interface_info @@ -1422,13 +1562,13 @@ struct retro_vfs_interface_info enum retro_hw_render_interface_type { - RETRO_HW_RENDER_INTERFACE_VULKAN = 0, - RETRO_HW_RENDER_INTERFACE_D3D9 = 1, - RETRO_HW_RENDER_INTERFACE_D3D10 = 2, - RETRO_HW_RENDER_INTERFACE_D3D11 = 3, - RETRO_HW_RENDER_INTERFACE_D3D12 = 4, - RETRO_HW_RENDER_INTERFACE_GSKIT_PS2 = 5, - RETRO_HW_RENDER_INTERFACE_DUMMY = INT_MAX + RETRO_HW_RENDER_INTERFACE_VULKAN = 0, + RETRO_HW_RENDER_INTERFACE_D3D9 = 1, + RETRO_HW_RENDER_INTERFACE_D3D10 = 2, + RETRO_HW_RENDER_INTERFACE_D3D11 = 3, + RETRO_HW_RENDER_INTERFACE_D3D12 = 4, + RETRO_HW_RENDER_INTERFACE_GSKIT_PS2 = 5, + RETRO_HW_RENDER_INTERFACE_DUMMY = INT_MAX }; /* Base struct. All retro_hw_render_interface_* types @@ -1924,6 +2064,10 @@ enum retro_sensor_action { RETRO_SENSOR_ACCELEROMETER_ENABLE = 0, RETRO_SENSOR_ACCELEROMETER_DISABLE, + RETRO_SENSOR_GYROSCOPE_ENABLE, + RETRO_SENSOR_GYROSCOPE_DISABLE, + RETRO_SENSOR_ILLUMINANCE_ENABLE, + RETRO_SENSOR_ILLUMINANCE_DISABLE, RETRO_SENSOR_DUMMY = INT_MAX }; @@ -1932,6 +2076,10 @@ enum retro_sensor_action #define RETRO_SENSOR_ACCELEROMETER_X 0 #define RETRO_SENSOR_ACCELEROMETER_Y 1 #define RETRO_SENSOR_ACCELEROMETER_Z 2 +#define RETRO_SENSOR_GYROSCOPE_X 3 +#define RETRO_SENSOR_GYROSCOPE_Y 4 +#define RETRO_SENSOR_GYROSCOPE_Z 5 +#define RETRO_SENSOR_ILLUMINANCE 6 typedef bool (RETRO_CALLCONV *retro_set_sensor_state_t)(unsigned port, enum retro_sensor_action action, unsigned rate); @@ -2129,6 +2277,30 @@ struct retro_frame_time_callback retro_usec_t reference; }; +/* Notifies a libretro core of the current occupancy + * level of the frontend audio buffer. + * + * - active: 'true' if audio buffer is currently + * in use. Will be 'false' if audio is + * disabled in the frontend + * + * - occupancy: Given as a value in the range [0,100], + * corresponding to the occupancy percentage + * of the audio buffer + * + * - underrun_likely: 'true' if the frontend expects an + * audio buffer underrun during the + * next frame (indicates that a core + * should attempt frame skipping) + * + * It will be called right before retro_run() every frame. */ +typedef void (RETRO_CALLCONV *retro_audio_buffer_status_callback_t)( + bool active, unsigned occupancy, bool underrun_likely); +struct retro_audio_buffer_status_callback +{ + retro_audio_buffer_status_callback_t callback; +}; + /* Pass this to retro_video_refresh_t if rendering to hardware. * Passing NULL to retro_video_refresh_t is still a frame dupe as normal. * */ @@ -2289,7 +2461,8 @@ struct retro_keyboard_callback retro_keyboard_event_t callback; }; -/* Callbacks for RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE. +/* Callbacks for RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE & + * RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE. * Should be set for implementations which can swap out multiple disk * images in runtime. * @@ -2347,6 +2520,53 @@ typedef bool (RETRO_CALLCONV *retro_replace_image_index_t)(unsigned index, * with replace_image_index. */ typedef bool (RETRO_CALLCONV *retro_add_image_index_t)(void); +/* Sets initial image to insert in drive when calling + * core_load_game(). + * Since we cannot pass the initial index when loading + * content (this would require a major API change), this + * is set by the frontend *before* calling the core's + * retro_load_game()/retro_load_game_special() implementation. + * A core should therefore cache the index/path values and handle + * them inside retro_load_game()/retro_load_game_special(). + * - If 'index' is invalid (index >= get_num_images()), the + * core should ignore the set value and instead use 0 + * - 'path' is used purely for error checking - i.e. when + * content is loaded, the core should verify that the + * disk specified by 'index' has the specified file path. + * This is to guard against auto selecting the wrong image + * if (for example) the user should modify an existing M3U + * playlist. We have to let the core handle this because + * set_initial_image() must be called before loading content, + * i.e. the frontend cannot access image paths in advance + * and thus cannot perform the error check itself. + * If set path and content path do not match, the core should + * ignore the set 'index' value and instead use 0 + * Returns 'false' if index or 'path' are invalid, or core + * does not support this functionality + */ +typedef bool (RETRO_CALLCONV *retro_set_initial_image_t)(unsigned index, const char *path); + +/* Fetches the path of the specified disk image file. + * Returns 'false' if index is invalid (index >= get_num_images()) + * or path is otherwise unavailable. + */ +typedef bool (RETRO_CALLCONV *retro_get_image_path_t)(unsigned index, char *path, size_t len); + +/* Fetches a core-provided 'label' for the specified disk + * image file. In the simplest case this may be a file name + * (without extension), but for cores with more complex + * content requirements information may be provided to + * facilitate user disk swapping - for example, a core + * running floppy-disk-based content may uniquely label + * save disks, data disks, level disks, etc. with names + * corresponding to in-game disk change prompts (so the + * frontend can provide better user guidance than a 'dumb' + * disk index value). + * Returns 'false' if index is invalid (index >= get_num_images()) + * or label is otherwise unavailable. + */ +typedef bool (RETRO_CALLCONV *retro_get_image_label_t)(unsigned index, char *label, size_t len); + struct retro_disk_control_callback { retro_set_eject_state_t set_eject_state; @@ -2360,6 +2580,27 @@ struct retro_disk_control_callback retro_add_image_index_t add_image_index; }; +struct retro_disk_control_ext_callback +{ + retro_set_eject_state_t set_eject_state; + retro_get_eject_state_t get_eject_state; + + retro_get_image_index_t get_image_index; + retro_set_image_index_t set_image_index; + retro_get_num_images_t get_num_images; + + retro_replace_image_index_t replace_image_index; + retro_add_image_index_t add_image_index; + + /* NOTE: Frontend will only attempt to record/restore + * last used disk index if both set_initial_image() + * and get_image_path() are implemented */ + retro_set_initial_image_t set_initial_image; /* Optional - may be NULL */ + + retro_get_image_path_t get_image_path; /* Optional - may be NULL */ + retro_get_image_label_t get_image_label; /* Optional - may be NULL */ +}; + enum retro_pixel_format { /* 0RGB1555, native endian. @@ -2390,6 +2631,104 @@ struct retro_message unsigned frames; /* Duration in frames of message. */ }; +enum retro_message_target +{ + RETRO_MESSAGE_TARGET_ALL = 0, + RETRO_MESSAGE_TARGET_OSD, + RETRO_MESSAGE_TARGET_LOG +}; + +enum retro_message_type +{ + RETRO_MESSAGE_TYPE_NOTIFICATION = 0, + RETRO_MESSAGE_TYPE_NOTIFICATION_ALT, + RETRO_MESSAGE_TYPE_STATUS, + RETRO_MESSAGE_TYPE_PROGRESS +}; + +struct retro_message_ext +{ + /* Message string to be displayed/logged */ + const char *msg; + /* Duration (in ms) of message when targeting the OSD */ + unsigned duration; + /* Message priority when targeting the OSD + * > When multiple concurrent messages are sent to + * the frontend and the frontend does not have the + * capacity to display them all, messages with the + * *highest* priority value should be shown + * > There is no upper limit to a message priority + * value (within the bounds of the unsigned data type) + * > In the reference frontend (RetroArch), the same + * priority values are used for frontend-generated + * notifications, which are typically assigned values + * between 0 and 3 depending upon importance */ + unsigned priority; + /* Message logging level (info, warn, error, etc.) */ + enum retro_log_level level; + /* Message destination: OSD, logging interface or both */ + enum retro_message_target target; + /* Message 'type' when targeting the OSD + * > RETRO_MESSAGE_TYPE_NOTIFICATION: Specifies that a + * message should be handled in identical fashion to + * a standard frontend-generated notification + * > RETRO_MESSAGE_TYPE_NOTIFICATION_ALT: Specifies that + * message is a notification that requires user attention + * or action, but that it should be displayed in a manner + * that differs from standard frontend-generated notifications. + * This would typically correspond to messages that should be + * displayed immediately (independently from any internal + * frontend message queue), and/or which should be visually + * distinguishable from frontend-generated notifications. + * For example, a core may wish to inform the user of + * information related to a disk-change event. It is + * expected that the frontend itself may provide a + * notification in this case; if the core sends a + * message of type RETRO_MESSAGE_TYPE_NOTIFICATION, an + * uncomfortable 'double-notification' may occur. A message + * of RETRO_MESSAGE_TYPE_NOTIFICATION_ALT should therefore + * be presented such that visual conflict with regular + * notifications does not occur + * > RETRO_MESSAGE_TYPE_STATUS: Indicates that message + * is not a standard notification. This typically + * corresponds to 'status' indicators, such as a core's + * internal FPS, which are intended to be displayed + * either permanently while a core is running, or in + * a manner that does not suggest user attention or action + * is required. 'Status' type messages should therefore be + * displayed in a different on-screen location and in a manner + * easily distinguishable from both standard frontend-generated + * notifications and messages of type RETRO_MESSAGE_TYPE_NOTIFICATION_ALT + * > RETRO_MESSAGE_TYPE_PROGRESS: Indicates that message reports + * the progress of an internal core task. For example, in cases + * where a core itself handles the loading of content from a file, + * this may correspond to the percentage of the file that has been + * read. Alternatively, an audio/video playback core may use a + * message of type RETRO_MESSAGE_TYPE_PROGRESS to display the current + * playback position as a percentage of the runtime. 'Progress' type + * messages should therefore be displayed as a literal progress bar, + * where: + * - 'retro_message_ext.msg' is the progress bar title/label + * - 'retro_message_ext.progress' determines the length of + * the progress bar + * NOTE: Message type is a *hint*, and may be ignored + * by the frontend. If a frontend lacks support for + * displaying messages via alternate means than standard + * frontend-generated notifications, it will treat *all* + * messages as having the type RETRO_MESSAGE_TYPE_NOTIFICATION */ + enum retro_message_type type; + /* Task progress when targeting the OSD and message is + * of type RETRO_MESSAGE_TYPE_PROGRESS + * > -1: Unmetered/indeterminate + * > 0-100: Current progress percentage + * NOTE: Since message type is a hint, a frontend may ignore + * progress values. Where relevant, a core should therefore + * include progress percentage within the message string, + * such that the message intent remains clear when displayed + * as a standard frontend-generated notification */ + int8_t progress; +}; + /* Describes how the libretro implementation maps a libretro input bind * to its internal input system through a human readable string. * This string can be used to better let a user configure input. */ @@ -2410,7 +2749,7 @@ struct retro_input_descriptor struct retro_system_info { /* All pointers are owned by libretro implementation, and pointers must - * remain valid until retro_deinit() is called. */ + * remain valid until it is unloaded. */ const char *library_name; /* Descriptive name of library. Should not * contain any version numbers, etc. */ @@ -2504,8 +2843,20 @@ struct retro_core_option_display }; /* Maximum number of values permitted for a core option - * NOTE: This may be increased on a core-by-core basis - * if required (doing so has no effect on the frontend) */ + * > Note: We have to set a maximum value due the limitations + * of the C language - i.e. it is not possible to create an + * array of structs each containing a variable sized array, + * so the retro_core_option_definition values array must + * have a fixed size. The size limit of 128 is a balancing + * act - it needs to be large enough to support all 'sane' + * core options, but setting it too large may impact low memory + * platforms. In practise, if a core option has more than + * 128 values then the implementation is likely flawed. + * To quote the above API reference: + * "The number of possible options should be very limited + * i.e. it should be feasible to cycle through options + * without a keyboard." + */ #define RETRO_NUM_CORE_OPTION_VALUES_MAX 128 struct retro_core_option_value @@ -2597,6 +2948,47 @@ struct retro_framebuffer Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. */ }; +/* Used by a libretro core to override the current + * fastforwarding mode of the frontend */ +struct retro_fastforwarding_override +{ + /* Specifies the runtime speed multiplier that + * will be applied when 'fastforward' is true. + * For example, a value of 5.0 when running 60 FPS + * content will cap the fast-forward rate at 300 FPS. + * Note that the target multiplier may not be achieved + * if the host hardware has insufficient processing + * power. + * Setting a value of 0.0 (or greater than 0.0 but + * less than 1.0) will result in an uncapped + * fast-forward rate (limited only by hardware + * capacity). + * If the value is negative, it will be ignored + * (i.e. the frontend will use a runtime speed + * multiplier of its own choosing) */ + float ratio; + + /* If true, fastforwarding mode will be enabled. + * If false, fastforwarding mode will be disabled. */ + bool fastforward; + + /* If true, and if supported by the frontend, an + * on-screen notification will be displayed while + * 'fastforward' is true. + * If false, and if supported by the frontend, any + * on-screen fast-forward notifications will be + * suppressed */ + bool notification; + + /* If true, the core will have sole control over + * when fastforwarding mode is enabled/disabled; + * the frontend will not be able to change the + * state set by 'fastforward' until either + * 'inhibit_toggle' is set to false, or the core + * is unloaded */ + bool inhibit_toggle; +}; + /* Callbacks */ /* Environment callback. Gives implementations a way of performing diff --git a/bsnes/gb/libretro/libretro_core_options.inc b/bsnes/gb/libretro/libretro_core_options.inc new file mode 100644 index 00000000..11857632 --- /dev/null +++ b/bsnes/gb/libretro/libretro_core_options.inc @@ -0,0 +1,679 @@ +#ifndef LIBRETRO_CORE_OPTIONS_H__ +#define LIBRETRO_CORE_OPTIONS_H__ + +#include +#include + +#include "libretro.h" +#include "retro_inline.h" + +/* + ******************************** + * VERSION: 1.3 + ******************************** + * + * - 1.3: Move translations to libretro_core_options_intl.h + * - libretro_core_options_intl.h includes BOM and utf-8 + * fix for MSVC 2010-2013 + * - Added HAVE_NO_LANGEXTRA flag to disable translations + * on platforms/compilers without BOM support + * - 1.2: Use core options v1 interface when + * RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION is >= 1 + * (previously required RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION == 1) + * - 1.1: Support generation of core options v0 retro_core_option_value + * arrays containing options with a single value + * - 1.0: First commit + */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* + ******************************** + * Core Option Definitions + ******************************** + */ + +/* RETRO_LANGUAGE_ENGLISH */ + +/* Default language: + * - All other languages must include the same keys and values + * - Will be used as a fallback in the event that frontend language + * is not available + * - Will be used as a fallback for any missing entries in + * frontend language definition */ + +struct retro_core_option_definition option_defs_us[] = { + + /* Core options used in single cart mode */ + + { + "sameboy_model", + "System - Emulated Model (Requires Restart)", + "Chooses which system model the content should be started on. Certain games may activate special in-game features when ran on specific models. This option requires a content restart in order to take effect.", + { + { "Auto", "Detect automatically" }, + { "Game Boy", "Game Boy (DMG-CPU B)" }, + { "Game Boy Color C", "Game Boy Color (CPU-CGB C) (Experimental)" }, + { "Game Boy Color", "Game Boy Color (CPU-CGB E)" }, + { "Game Boy Advance", NULL }, + { "Super Game Boy", "Super Game Boy NTSC" }, + { "Super Game Boy PAL", NULL }, + { "Super Game Boy 2", NULL }, + { NULL, NULL }, + }, + "Auto" + }, + { + "sameboy_rtc", + "System - Real Time Clock Emulation", + "Specifies how the emulation of the real-time clock feature used in certain Game Boy and Game Boy Color games should function.", + { + { "sync to system clock", "Sync to System Clock" }, + { "accurate", "Accurate" }, + { NULL, NULL }, + }, + "sync to system clock" + }, + + { + "sameboy_mono_palette", + "Video - GB Mono Palette", + "Selects the color palette that should be used when playing Game Boy games.", + { + { "greyscale", "Greyscale" }, + { "lime", "Lime (Game Boy)" }, + { "olive", "Olive (Game Boy Pocket)" }, + { "teal", "Teal (Game Boy Light)" }, + { NULL, NULL }, + }, + "greyscale" + }, + { + "sameboy_color_correction_mode", + "Video - GBC Color Correction", + "Defines which type of color correction should be applied when playing Game Boy Color games.", + { + { "emulate hardware", "Emulate Hardware" }, + { "preserve brightness", "Preserve Brightness" }, + { "reduce contrast", "Reduce Contrast" }, + { "correct curves", "Correct Color Curves" }, + { "harsh reality", "Harsh Reality (Low Contrast)" }, + { "off", "Disabled" }, + { NULL, NULL }, + }, + "emulate hardware" + }, + { + "sameboy_light_temperature", + "Video - Ambient Light Temperature", + "Simulates an ambient light’s effect on non-backlit Game Boy screens, by setting a user-controlled color temperature. This option has no effect if the content is run on an original Game Boy (DMG) emulated model.", + { + { "1.0", "1000K (Warmest)" }, + { "0.9", "1550K" }, + { "0.8", "2100K" }, + { "0.7", "2650K" }, + { "0.6", "3200K" }, + { "0.5", "3750K" }, + { "0.4", "4300K" }, + { "0.3", "4850K" }, + { "0.2", "5400K" }, + { "0.1", "5950K" }, + { "0", "6500K (Neutral White)" }, + { "-0.1", "7050K" }, + { "-0.2", "7600K" }, + { "-0.3", "8150K" }, + { "-0.4", "8700K" }, + { "-0.5", "9250K" }, + { "-0.6", "9800K" }, + { "-0.7", "10350K" }, + { "-0.8", "10900K" }, + { "-0.9", "11450K" }, + { "-1.0", "12000K (Coolest)" }, + { NULL, NULL }, + }, + "0" + }, + { + "sameboy_border", + "Video - Display Border", + "Defines when to display an on-screen border around the content.", + { + { "always", "Always" }, + { "Super Game Boy only", "Only for Super Game Boy" }, + { "never", "Disabled" }, + { NULL, NULL }, + }, + "Super Game Boy only" + }, + { + "sameboy_high_pass_filter_mode", + "Audio - Highpass Filter", + "Applies a filter to the audio output, removing certain pop sounds caused by the DC Offset. If disabled, the sound will be rendered as output by the Game Boy APU, but popping effects will be heard when the emulator is paused or resumed. 'Accurate' will apply a global filter, masking popping sounds while also reducing lower frequencies. 'Preserve Waveform' applies the filter only to the DC Offset.", + { + { "accurate", "Accurate" }, + { "remove dc offset", "Preserve Waveform" }, + { "off", "Disabled" }, + { NULL, NULL }, + }, + "accurate" + }, + { + "sameboy_audio_interference", + "Audio - Interference Volume", + "Controls the volume of the buzzing effect caused by the electrical interference between the Game Boy SoC and the system speakers.", + { + { "0", "0%" }, + { "5", "5%" }, + { "10", "10%" }, + { "15", "15%" }, + { "20", "20%" }, + { "25", "25%" }, + { "30", "30%" }, + { "35", "35%" }, + { "40", "40%" }, + { "45", "45%" }, + { "50", "50%" }, + { "55", "55%" }, + { "60", "60%" }, + { "65", "65%" }, + { "70", "70%" }, + { "75", "75%" }, + { "80", "80%" }, + { "85", "85%" }, + { "90", "90%" }, + { "95", "95%" }, + { "100", "100%" }, + { NULL, NULL }, + }, + "0" + }, + { + "sameboy_rumble", + "Input - Rumble Mode", + "Defines which type of content should trigger rumble effects.", + { + { "all games", "Always" }, + { "rumble-enabled games", "Only for rumble-enabled games" }, + { "never", "Never" }, + { NULL, NULL }, + }, + "rumble-enabled games" + }, + + /* Core options used in dual cart mode */ + + { + "sameboy_link", + "System - Link Cable Emulation", + "Enables the emulation of the link cable, allowing communication and exchange of data between two Game Boy systems.", + { + { "enabled", "Enabled" }, + { "disabled", "Disabled" }, + { NULL, NULL }, + }, + "enabled" + }, + { + "sameboy_screen_layout", + "System - Screen Layout", + "When emulating two systems at once, this option defines the respective position of the two screens.", + { + { "top-down", "Top-Down" }, + { "left-right", "Left-Right" }, + { NULL, NULL }, + }, + "top-down" + }, + { + "sameboy_audio_output", + "System - Audio Output", + "Selects which of the two emulated Game Boy systems should output audio.", + { + { "Game Boy #1", NULL }, + { "Game Boy #2", NULL }, + { NULL, NULL }, + }, + "Game Boy #1" + }, + { + "sameboy_model_1", + "System - Emulated Model for Game Boy #1 (Requires Restart)", + "Chooses which system model the content should be started on for Game Boy #1. Certain games may activate special in-game features when ran on specific models. This option requires a content restart in order to take effect.", + { + { "Auto", "Detect automatically" }, + { "Game Boy", "Game Boy (DMG-CPU B)" }, + { "Game Boy Color C", "Game Boy Color (CPU-CGB C) (Experimental)" }, + { "Game Boy Color", "Game Boy Color (CPU-CGB E)" }, + { "Game Boy Advance", NULL }, + { "Super Game Boy", "Super Game Boy NTSC" }, + { "Super Game Boy PAL", NULL }, + { "Super Game Boy 2", NULL }, + { NULL, NULL }, + }, + "Auto" + }, + { + "sameboy_model_2", + "System - Emulated Model for Game Boy #2 (Requires Restart)", + "Chooses which system model the content should be started on for Game Boy #2. Certain games may activate special in-game features when ran on specific models. This option requires a content restart in order to take effect.", + { + { "Auto", "Detect automatically" }, + { "Game Boy", "Game Boy (DMG-CPU B)" }, + { "Game Boy Color C", "Game Boy Color (CPU-CGB C) (Experimental)" }, + { "Game Boy Color", "Game Boy Color (CPU-CGB E)" }, + { "Game Boy Advance", NULL }, + { "Super Game Boy", "Super Game Boy NTSC" }, + { "Super Game Boy PAL", NULL }, + { "Super Game Boy 2", NULL }, + { NULL, NULL }, + }, + "Auto" + }, + { + "sameboy_mono_palette_1", + "Video - GB Mono Palette for Game Boy #1", + "Selects the color palette that should be used when playing Game Boy games on Game Boy #1.", + { + { "greyscale", "Greyscale" }, + { "lime", "Lime (Game Boy)" }, + { "olive", "Olive (Game Boy Pocket)" }, + { "teal", "Teal (Game Boy Light)" }, + { NULL, NULL }, + }, + "greyscale" + }, + { + "sameboy_mono_palette_2", + "Video - GB Mono Palette for Game Boy #2", + "Selects the color palette that should be used when playing Game Boy games on Game Boy #2.", + { + { "greyscale", "Greyscale" }, + { "lime", "Lime (Game Boy)" }, + { "olive", "Olive (Game Boy Pocket)" }, + { "teal", "Teal (Game Boy Light)" }, + { NULL, NULL }, + }, + "greyscale" + }, + { + "sameboy_color_correction_mode_1", + "Video - GBC Color Correction for Game Boy #1", + "Defines which type of color correction should be applied when playing Game Boy Color games on Game Boy #1.", + { + { "emulate hardware", "Emulate Hardware" }, + { "preserve brightness", "Preserve Brightness" }, + { "reduce contrast", "Reduce Contrast" }, + { "correct curves", "Correct Color Curves" }, + { "harsh reality", "Harsh Reality (Low Contrast)" }, + { "off", "Disabled" }, + { NULL, NULL }, + }, + "emulate hardware" + }, + { + "sameboy_color_correction_mode_2", + "Video - GBC Color Correction for Game Boy #2", + "Defines which type of color correction should be applied when playing Game Boy Color games on Game Boy #2.", + { + { "emulate hardware", "Emulate Hardware" }, + { "preserve brightness", "Preserve Brightness" }, + { "reduce contrast", "Reduce Contrast" }, + { "correct curves", "Correct Color Curves" }, + { "harsh reality", "Harsh Reality (Low Contrast)" }, + { "off", "Disabled" }, + { NULL, NULL }, + }, + "emulate hardware" + }, + { + "sameboy_light_temperature_1", + "Video - Ambient Light Temperature for Game Boy #1", + "Simulates an ambient light’s effect on non-backlit Game Boy screens, by setting a user-controlled color temperature applied to the screen of Game Boy #1. This option has no effect if the content is run on an original Game Boy (DMG) emulated model.", + { + { "1.0", "1000K (Warmest)" }, + { "0.9", "1550K" }, + { "0.8", "2100K" }, + { "0.7", "2650K" }, + { "0.6", "3200K" }, + { "0.5", "3750K" }, + { "0.4", "4300K" }, + { "0.3", "4850K" }, + { "0.2", "5400K" }, + { "0.1", "5950K" }, + { "0", "6500K (Neutral White)" }, + { "-0.1", "7050K" }, + { "-0.2", "7600K" }, + { "-0.3", "8150K" }, + { "-0.4", "8700K" }, + { "-0.5", "9250K" }, + { "-0.6", "9800K" }, + { "-0.7", "10350K" }, + { "-0.8", "10900K" }, + { "-0.9", "11450K" }, + { "-1.0", "12000K (Coolest)" }, + { NULL, NULL }, + }, + "0" + }, + { + "sameboy_light_temperature_2", + "Video - Ambient Light Temperature for Game Boy #2", + "Simulates an ambient light’s effect on non-backlit Game Boy screens, by setting a user-controlled color temperature applied to the screen of Game Boy #2. This option has no effect if the content is run on an original Game Boy (DMG) emulated model.", + { + { "1.0", "1000K (Warmest)" }, + { "0.9", "1550K" }, + { "0.8", "2100K" }, + { "0.7", "2650K" }, + { "0.6", "3200K" }, + { "0.5", "3750K" }, + { "0.4", "4300K" }, + { "0.3", "4850K" }, + { "0.2", "5400K" }, + { "0.1", "5950K" }, + { "0", "6500K (Neutral White)" }, + { "-0.1", "7050K" }, + { "-0.2", "7600K" }, + { "-0.3", "8150K" }, + { "-0.4", "8700K" }, + { "-0.5", "9250K" }, + { "-0.6", "9800K" }, + { "-0.7", "10350K" }, + { "-0.8", "10900K" }, + { "-0.9", "11450K" }, + { "-1.0", "12000K (Coolest)" }, + { NULL, NULL }, + }, + "0" + }, + { + "sameboy_high_pass_filter_mode_1", + "Audio - Highpass Filter for Game Boy #1", + "Applies a filter to the audio output for Game Boy #1, removing certain pop sounds caused by the DC Offset. If disabled, the sound will be rendered as output by the Game Boy APU, but popping effects will be heard when the emulator is paused or resumed. 'Accurate' will apply a global filter, masking popping sounds while also reducing lower frequencies. 'Preserve Waveform' applies the filter only to the DC Offset.", + { + { "accurate", "Accurate" }, + { "remove dc offset", "Preserve Waveform" }, + { "off", "disabled" }, + { NULL, NULL }, + }, + "accurate" + }, + { + "sameboy_high_pass_filter_mode_2", + "Audio - Highpass Filter for Game Boy #2", + "Applies a filter to the audio output for Game Boy #2, removing certain pop sounds caused by the DC Offset. If disabled, the sound will be rendered as output by the Game Boy APU, but popping effects will be heard when the emulator is paused or resumed. 'Accurate' will apply a global filter, masking popping sounds while also reducing lower frequencies. 'Preserve Waveform' applies the filter only to the DC Offset.", + { + { "accurate", "Accurate" }, + { "remove dc offset", "Preserve Waveform" }, + { "off", "disabled" }, + { NULL, NULL }, + }, + "accurate" + }, + { + "sameboy_audio_interference_1", + "Audio - Interference Volume for Game Boy #1", + "Controls the volume of the buzzing effect caused by the electrical interference between the Game Boy SoC and the system speakers on Game Boy #1.", + { + { "0", "0%" }, + { "5", "5%" }, + { "10", "10%" }, + { "15", "15%" }, + { "20", "20%" }, + { "25", "25%" }, + { "30", "30%" }, + { "35", "35%" }, + { "40", "40%" }, + { "45", "45%" }, + { "50", "50%" }, + { "55", "55%" }, + { "60", "60%" }, + { "65", "65%" }, + { "70", "70%" }, + { "75", "75%" }, + { "80", "80%" }, + { "85", "85%" }, + { "90", "90%" }, + { "95", "95%" }, + { "100", "100%" }, + { NULL, NULL }, + }, + "0" + }, + { + "sameboy_audio_interference_2", + "Audio - Interference Volume for Game Boy #2", + "Controls the volume of the buzzing effect caused by the electrical interference between the Game Boy SoC and the system speakers on Game Boy #2.", + { + { "0", "0%" }, + { "5", "5%" }, + { "10", "10%" }, + { "15", "15%" }, + { "20", "20%" }, + { "25", "25%" }, + { "30", "30%" }, + { "35", "35%" }, + { "40", "40%" }, + { "45", "45%" }, + { "50", "50%" }, + { "55", "55%" }, + { "60", "60%" }, + { "65", "65%" }, + { "70", "70%" }, + { "75", "75%" }, + { "80", "80%" }, + { "85", "85%" }, + { "90", "90%" }, + { "95", "95%" }, + { "100", "100%" }, + { NULL, NULL }, + }, + "0" + }, + { + "sameboy_rumble_1", + "Input - Rumble Mode for Game Boy #1", + "Defines which type of content should trigger rumble effects when played on Game Boy #1.", + { + { "all games", "Always" }, + { "rumble-enabled games", "Only for rumble-enabled games" }, + { "never", "Never" }, + { NULL, NULL }, + }, + "rumble-enabled games" + }, + { + "sameboy_rumble_2", + "Input - Rumble Mode for Game Boy #2", + "Defines which type of content should trigger rumble effects when played on Game Boy #2.", + { + { "all games", "Always" }, + { "rumble-enabled games", "Only for rumble-enabled games" }, + { "never", "Never" }, + { NULL, NULL }, + }, + "rumble-enabled games" + }, + + { NULL, NULL, NULL, {{0}}, NULL }, +}; + +/* + ******************************** + * Language Mapping + ******************************** + */ + +#ifndef HAVE_NO_LANGEXTRA +struct retro_core_option_definition *option_defs_intl[RETRO_LANGUAGE_LAST] = { + option_defs_us, /* RETRO_LANGUAGE_ENGLISH */ + NULL, /* RETRO_LANGUAGE_JAPANESE */ + NULL, /* RETRO_LANGUAGE_FRENCH */ + NULL, /* RETRO_LANGUAGE_SPANISH */ + NULL, /* RETRO_LANGUAGE_GERMAN */ + NULL, /* RETRO_LANGUAGE_ITALIAN */ + NULL, /* RETRO_LANGUAGE_DUTCH */ + NULL, /* RETRO_LANGUAGE_PORTUGUESE_BRAZIL */ + NULL, /* RETRO_LANGUAGE_PORTUGUESE_PORTUGAL */ + NULL, /* RETRO_LANGUAGE_RUSSIAN */ + NULL, /* RETRO_LANGUAGE_KOREAN */ + NULL, /* RETRO_LANGUAGE_CHINESE_TRADITIONAL */ + NULL, /* RETRO_LANGUAGE_CHINESE_SIMPLIFIED */ + NULL, /* RETRO_LANGUAGE_ESPERANTO */ + NULL, /* RETRO_LANGUAGE_POLISH */ + NULL, /* RETRO_LANGUAGE_VIETNAMESE */ + NULL, /* RETRO_LANGUAGE_ARABIC */ + NULL, /* RETRO_LANGUAGE_GREEK */ + NULL, /* RETRO_LANGUAGE_TURKISH */ + NULL, /* RETRO_LANGUAGE_SLOVAK */ + NULL, /* RETRO_LANGUAGE_PERSIAN */ + NULL, /* RETRO_LANGUAGE_HEBREW */ + NULL, /* RETRO_LANGUAGE_ASTURIAN */ + NULL, /* RETRO_LANGUAGE_FINNISH */ + +}; +#endif + +/* + ******************************** + * Functions + ******************************** + */ + +/* Handles configuration/setting of core options. + * Should be called as early as possible - ideally inside + * retro_set_environment(), and no later than retro_load_game() + * > We place the function body in the header to avoid the + * necessity of adding more .c files (i.e. want this to + * be as painless as possible for core devs) + */ + +static INLINE void libretro_set_core_options(retro_environment_t environ_cb) +{ + unsigned version = 0; + + if (!environ_cb) return; + + if (environ_cb(RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION, &version) && (version >= 1)) { +#ifndef HAVE_NO_LANGEXTRA + struct retro_core_options_intl core_options_intl; + unsigned language = 0; + + core_options_intl.us = option_defs_us; + core_options_intl.local = NULL; + + if (environ_cb(RETRO_ENVIRONMENT_GET_LANGUAGE, &language) && + (language < RETRO_LANGUAGE_LAST) && (language != RETRO_LANGUAGE_ENGLISH)) + core_options_intl.local = option_defs_intl[language]; + + environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL, &core_options_intl); +#else + environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS, &option_defs_us); +#endif + } + else { + size_t i; + size_t num_options = 0; + struct retro_variable *variables = NULL; + char **values_buf = NULL; + + /* Determine number of options */ + while (true) { + if (!option_defs_us[num_options].key) break; + num_options++; + } + + /* Allocate arrays */ + variables = (struct retro_variable *)calloc(num_options + 1, sizeof(struct retro_variable)); + values_buf = (char **)calloc(num_options, sizeof(char *)); + + if (!variables || !values_buf) goto error; + + /* Copy parameters from option_defs_us array */ + for (i = 0; i < num_options; i++) { + const char *key = option_defs_us[i].key; + const char *desc = option_defs_us[i].desc; + const char *default_value = option_defs_us[i].default_value; + struct retro_core_option_value *values = option_defs_us[i].values; + size_t buf_len = 3; + size_t default_index = 0; + + values_buf[i] = NULL; + + if (desc) { + size_t num_values = 0; + + /* Determine number of values */ + while (true) { + if (!values[num_values].value) break; + + /* Check if this is the default value */ + if (default_value) { + if (strcmp(values[num_values].value, default_value) == 0) default_index = num_values; + } + + buf_len += strlen(values[num_values].value); + num_values++; + } + + /* Build values string */ + if (num_values > 0) { + size_t j; + + buf_len += num_values - 1; + buf_len += strlen(desc); + + values_buf[i] = (char *)calloc(buf_len, sizeof(char)); + if (!values_buf[i]) goto error; + + strcpy(values_buf[i], desc); + strcat(values_buf[i], "; "); + + /* Default value goes first */ + strcat(values_buf[i], values[default_index].value); + + /* Add remaining values */ + for (j = 0; j < num_values; j++) { + if (j != default_index) { + strcat(values_buf[i], "|"); + strcat(values_buf[i], values[j].value); + } + } + } + } + + variables[i].key = key; + variables[i].value = values_buf[i]; + } + + /* Set variables */ + environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, variables); + + error: + + /* Clean up */ + if (values_buf) { + for (i = 0; i < num_options; i++) { + if (values_buf[i]) { + free(values_buf[i]); + values_buf[i] = NULL; + } + } + + free(values_buf); + values_buf = NULL; + } + + if (variables) { + free(variables); + variables = NULL; + } + } +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/bsnes/gb/libretro/retro_inline.h b/bsnes/gb/libretro/retro_inline.h new file mode 100644 index 00000000..14c038cc --- /dev/null +++ b/bsnes/gb/libretro/retro_inline.h @@ -0,0 +1,39 @@ +/* Copyright (C) 2010-2020 The RetroArch team + * + * --------------------------------------------------------------------------------------- + * The following license statement only applies to this file (retro_inline.h). + * --------------------------------------------------------------------------------------- + * + * Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef __LIBRETRO_SDK_INLINE_H +#define __LIBRETRO_SDK_INLINE_H + +#ifndef INLINE + +#if defined(_WIN32) || defined(__INTEL_COMPILER) +#define INLINE __inline +#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L +#define INLINE inline +#elif defined(__GNUC__) +#define INLINE __inline__ +#else +#define INLINE +#endif + +#endif +#endif diff --git a/bsnes/gb/version.mk b/bsnes/gb/version.mk index 35ae0ad6..baa0494f 100644 --- a/bsnes/gb/version.mk +++ b/bsnes/gb/version.mk @@ -1 +1 @@ -VERSION := 0.13.6 +VERSION := 0.14.4 \ No newline at end of file diff --git a/bsnes/sfc/coprocessor/icd/icd.cpp b/bsnes/sfc/coprocessor/icd/icd.cpp index 1b5199c4..ae56d819 100644 --- a/bsnes/sfc/coprocessor/icd/icd.cpp +++ b/bsnes/sfc/coprocessor/icd/icd.cpp @@ -44,6 +44,9 @@ namespace SameBoy { static auto vblank(GB_gameboy_t*) -> void { } + + static auto log(GB_gameboy_t *gb, const char *string, GB_log_attributes attributes) -> void { + } } auto ICD::synchronizeCPU() -> void { @@ -99,6 +102,7 @@ auto ICD::load() -> bool { GB_set_rgb_encode_callback(&sameboy, &SameBoy::rgb_encode); GB_apu_set_sample_callback(&sameboy, &SameBoy::sample); GB_set_vblank_callback(&sameboy, &SameBoy::vblank); + GB_set_log_callback(&sameboy, &SameBoy::log); GB_set_pixels_output(&sameboy, &bitmap[0]); if(auto loaded = platform->load(ID::GameBoy, "Game Boy", "gb")) { information.pathID = loaded.pathID; diff --git a/bsnes/target-bsnes/presentation/presentation.cpp b/bsnes/target-bsnes/presentation/presentation.cpp index 562b99d8..42783339 100644 --- a/bsnes/target-bsnes/presentation/presentation.cpp +++ b/bsnes/target-bsnes/presentation/presentation.cpp @@ -183,7 +183,7 @@ auto Presentation::create() -> void { .setName("SameBoy") .setLogo(Resource::SameBoy) .setDescription("Super Game Boy emulator") - .setVersion("0.13.6") + .setVersion("0.14.4") .setCopyright("Lior Halphon") .setLicense("MIT") .setWebsite("https://sameboy.github.io") diff --git a/libco/doc/examples/.gitignore b/libco/doc/examples/.gitignore new file mode 100644 index 00000000..d3db2568 --- /dev/null +++ b/libco/doc/examples/.gitignore @@ -0,0 +1,4 @@ +test_args +test_serialization +test_timing +*.o diff --git a/libco/doc/examples/build.bat b/libco/doc/examples/build.bat new file mode 100755 index 00000000..f8fecb26 --- /dev/null +++ b/libco/doc/examples/build.bat @@ -0,0 +1,8 @@ +cc -O3 -fomit-frame-pointer -I../.. -o libco.o -c ../../libco.c +c++ -O3 -fomit-frame-pointer -I../.. -c test_timing.cpp +c++ -O3 -fomit-frame-pointer -o test_timing libco.o test_timing.o +c++ -O3 -fomit-frame-pointer -I../.. -c test_args.cpp +c++ -O3 -fomit-frame-pointer -o test_args libco.o test_args.o +c++ -O3 -fomit-frame-pointer -I../.. -c test_serialization.cpp +c++ -O3 -fomit-frame-pointer -o test_serialization libco.o test_serialization.o +@del *.o diff --git a/libco/doc/examples/build.sh b/libco/doc/examples/build.sh new file mode 100755 index 00000000..dd187a99 --- /dev/null +++ b/libco/doc/examples/build.sh @@ -0,0 +1,8 @@ +cc -O3 -fomit-frame-pointer -I../.. -o libco.o -c ../../libco.c +c++ -O3 -fomit-frame-pointer -I../.. -c test_timing.cpp +c++ -O3 -fomit-frame-pointer -o test_timing libco.o test_timing.o +c++ -O3 -fomit-frame-pointer -I../.. -c test_args.cpp +c++ -O3 -fomit-frame-pointer -o test_args libco.o test_args.o +c++ -O3 -fomit-frame-pointer -I../.. -c test_serialization.cpp +c++ -O3 -fomit-frame-pointer -o test_serialization libco.o test_serialization.o +rm -f *.o diff --git a/libco/doc/examples/test.h b/libco/doc/examples/test.h new file mode 100644 index 00000000..753ea52f --- /dev/null +++ b/libco/doc/examples/test.h @@ -0,0 +1,6 @@ +#include +#include +#include +#include + +#include diff --git a/libco/doc/examples/test_args.cpp b/libco/doc/examples/test_args.cpp new file mode 100644 index 00000000..1e6e2bbb --- /dev/null +++ b/libco/doc/examples/test_args.cpp @@ -0,0 +1,76 @@ +/***** + * cothread parameterized function example + ***** + * entry point to cothreads cannot take arguments. + * this is due to portability issues: each processor, + * operating system, programming language and compiler + * can use different parameter passing methods, so + * arguments to the cothread entry points were omitted. + * + * however, the behavior can easily be simulated by use + * of a specialized co_switch to set global parameters to + * be used as function arguments. + * + * in this way, with a bit of extra red tape, one gains + * even more flexibility than would be possible with a + * fixed argument list entry point, such as void (*)(void*), + * as any number of arguments can be used. + * + * this also eliminates race conditions where a pointer + * passed to co_create may have changed or become invalidated + * before call to co_switch, as said pointer would now be set + * when calling co_switch, instead. + *****/ + +#include "test.h" + +cothread_t thread[3]; + +namespace co_arg { + int param_x; + int param_y; +}; + +//one could also call this co_init or somesuch if they preferred ... +void co_switch(cothread_t thread, int param_x, int param_y) { + co_arg::param_x = param_x; + co_arg::param_y = param_y; + co_switch(thread); +} + +void co_entrypoint() { +int param_x = co_arg::param_x; +int param_y = co_arg::param_y; + printf("co_entrypoint(%d, %d)\n", param_x, param_y); + co_switch(thread[0]); + +//co_arg::param_x will change here (due to co_switch(cothread_t, int, int) call changing values), +//however, param_x and param_y will persist as they are thread local + + printf("co_entrypoint(%d, %d)\n", param_x, param_y); + co_switch(thread[0]); + throw; +} + +int main() { + printf("cothread parameterized function example\n\n"); + + thread[0] = co_active(); + thread[1] = co_create(65536, co_entrypoint); + thread[2] = co_create(65536, co_entrypoint); + +//use specialized co_switch(cothread_t, int, int) for initial co_switch call + co_switch(thread[1], 1, 2); + co_switch(thread[2], 4, 8); + +//after first call, entry point arguments have been initialized, standard +//co_switch(cothread_t) can be used from now on + co_switch(thread[2]); + co_switch(thread[1]); + + printf("\ndone\n"); +#if defined(_MSC_VER) || defined(__DJGPP__) + getch(); +#endif + return 0; +} diff --git a/libco/doc/examples/test_serialization.cpp b/libco/doc/examples/test_serialization.cpp new file mode 100644 index 00000000..15fbdb19 --- /dev/null +++ b/libco/doc/examples/test_serialization.cpp @@ -0,0 +1,117 @@ +#include "test.h" +#include +#include + +namespace Thread { + cothread_t host; + cothread_t cpu; + cothread_t apu; +} + +namespace Buffer { + uint8_t cpu[65536]; + uint8_t apu[65536]; +} + +namespace Memory { + uint8_t* buffer; +} + +struct CPU { + static auto Enter() -> void; + auto main() -> void; + auto sub() -> void; + auto leaf() -> void; +} cpu; + +struct APU { + static auto Enter() -> void; + auto main() -> void; + auto sub() -> void; + auto leaf() -> void; +} apu; + +auto CPU::Enter() -> void { + while(true) cpu.main(); +} + +auto CPU::main() -> void { + printf("2\n"); + sub(); +} + +auto CPU::sub() -> void { + co_switch(Thread::apu); + printf("4\n"); + leaf(); +} + +auto CPU::leaf() -> void { + int x = 42; + co_switch(Thread::host); + printf("6\n"); + co_switch(Thread::apu); + printf("8 (%d)\n", x); + co_switch(Thread::host); +} + +auto APU::Enter() -> void { + while(true) apu.main(); +} + +auto APU::main() -> void { + printf("3\n"); + sub(); +} + +auto APU::sub() -> void { + co_switch(Thread::cpu); + printf("7\n"); + leaf(); +} + +auto APU::leaf() -> void { + co_switch(Thread::cpu); +} + +auto main() -> int { + if(!co_serializable()) { + printf("This implementation does not support serialization\n"); + return 1; + } + + Memory::buffer = (uint8_t*)mmap( + (void*)0x10'0000'0000, 2 * 65536, + PROT_READ | PROT_WRITE | PROT_EXEC, + MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0 + ); + Memory::buffer[0] = 42; + printf("%p (%u)\n", Memory::buffer, Memory::buffer[0]); + + Thread::host = co_active(); + Thread::cpu = co_derive((void*)(Memory::buffer + 0 * 65536), 65536, CPU::Enter); + Thread::apu = co_derive((void*)(Memory::buffer + 1 * 65536), 65536, APU::Enter); + + printf("1\n"); + co_switch(Thread::cpu); + + printf("5\n"); + memcpy(Buffer::cpu, Thread::cpu, 65536); + memcpy(Buffer::apu, Thread::apu, 65536); + co_switch(Thread::cpu); + + Thread::cpu = nullptr; + Thread::apu = nullptr; + Thread::cpu = co_derive((void*)(Memory::buffer + 0 * 65536), 65536, CPU::Enter); + Thread::apu = co_derive((void*)(Memory::buffer + 1 * 65536), 65536, APU::Enter); + + printf("9\n"); + memcpy(Thread::cpu, Buffer::cpu, 65536); + memcpy(Thread::apu, Buffer::apu, 65536); + co_switch(Thread::cpu); + + Thread::cpu = nullptr; + Thread::apu = nullptr; + munmap((void*)0x900000000, 2 * 65536); + return 0; +} diff --git a/libco/doc/examples/test_timing.cpp b/libco/doc/examples/test_timing.cpp new file mode 100644 index 00000000..0232e47e --- /dev/null +++ b/libco/doc/examples/test_timing.cpp @@ -0,0 +1,52 @@ +#include "test.h" +enum { Iterations = 500000000 }; + +namespace thread { + cothread_t x; + cothread_t y; + volatile int counter; +} + +void co_timingtest() { + for(;;) { + thread::counter++; + co_switch(thread::x); + } +} + +void sub_timingtest() { + thread::counter++; +} + +int main() { + printf("context-switching timing test\n\n"); + time_t start, end; + int i, t1, t2; + + start = clock(); + for(thread::counter = 0, i = 0; i < Iterations; i++) { + sub_timingtest(); + } + end = clock(); + + t1 = (int)difftime(end, start); + printf("%2.3f seconds per 50 million subroutine calls (%d iterations)\n", (float)t1 / CLOCKS_PER_SEC, thread::counter); + + thread::x = co_active(); + thread::y = co_create(65536, co_timingtest); + + start = clock(); + for(thread::counter = 0, i = 0; i < Iterations; i++) { + co_switch(thread::y); + } + end = clock(); + + co_delete(thread::y); + + t2 = (int)difftime(end, start); + printf("%2.3f seconds per 100 million co_switch calls (%d iterations)\n", (float)t2 / CLOCKS_PER_SEC, thread::counter); + + printf("co_switch skew = %fx\n\n", (double)t2 / (double)t1); + return 0; +} + diff --git a/libco/doc/targets.md b/libco/doc/targets.md index 29400c48..d01dcbea 100644 --- a/libco/doc/targets.md +++ b/libco/doc/targets.md @@ -9,7 +9,7 @@ C function call. ## libco.x86 * **Overhead:** ~5x * **Supported processor(s):** 32-bit x86 -*** Supported compiler(s**): any +* **Supported compiler(s):** any * **Supported operating system(s):** * Windows * Mac OS X @@ -19,7 +19,7 @@ C function call. ## libco.amd64 * **Overhead:** ~10x (Windows), ~6x (all other platforms) * **Supported processor(s):** 64-bit amd64 -*** Supported compiler(s**): any +* **Supported compiler(s):** any * **Supported operating system(s):** * Windows * Mac OS X diff --git a/libco/doc/usage.md b/libco/doc/usage.md index cb0d1929..a3b0f04d 100644 --- a/libco/doc/usage.md +++ b/libco/doc/usage.md @@ -54,7 +54,9 @@ Handle to cothread. Handle must be of type `void*`. -A value of `null` (0) indicates an uninitialized or invalid handle, whereas a non-zero value indicates a valid handle. +A value of null (0) indicates an uninitialized or invalid handle, whereas a +non-zero value indicates a valid handle. A valid handle is backed by execution +state to which the execution can be co_switch()ed to. ## co_active ```c @@ -62,7 +64,12 @@ cothread_t co_active(); ``` Return handle to current cothread. -Always returns a valid handle, even when called from the main program thread. +Note that the handle is valid even if the function is called from a non-cothread +context. To achieve this, we save the execution state in an internal buffer, +instead of using the user-provided memory. Since this handle is valid, it can +be used to co_switch to this context from another cothread. In multi-threaded +applications, make sure to not switch non-cothread context across CPU cores, +to prevent any possible conflicts with the OS scheduler. ## co_derive ```c @@ -119,6 +126,19 @@ Passing handle of active cothread to this function is not allowed. Passing handle of primary cothread is not allowed. +## co_serializable + +```c +int co_serializable(void); +``` + +Returns non-zero if the implementation keeps the entire coroutine state in the +buffer passed to `co_derive()`. That is, if `co_serializable()` returns +non-zero, and if your cothread does not modify the heap or any process-wide +state, then you can "snapshot" the cothread's state by taking a copy of the +buffer originally passed to `co_derive()`, and "restore" a previous state +by copying the snapshot back into the buffer it came from. + ## co_switch ```c void co_switch(cothread_t cothread); diff --git a/libco/settings.h b/libco/settings.h index 00ab2071..7c5d9b9d 100644 --- a/libco/settings.h +++ b/libco/settings.h @@ -14,7 +14,7 @@ #if !defined(LIBCO_MP) /* Running in single-threaded environment */ #define thread_local #else /* Running in multi-threaded environment */ - #if defined(__STDC_VERSION__) /* Compiling as C Language */ + #if defined(__STDC__) /* Compiling as C Language */ #if defined(_MSC_VER) /* Don't rely on MSVC's C11 support */ #define thread_local __declspec(thread) #elif __STDC_VERSION__ < 201112L /* If we are on C90/99 */ @@ -55,7 +55,7 @@ - alignas (TYPE) is equivalent to alignas (alignof (TYPE)). */ #if !defined(alignas) - #if defined(__STDC_VERSION__) /* C Language */ + #if defined(__STDC__) /* C Language */ #if defined(_MSC_VER) /* Don't rely on MSVC's C11 support */ #define alignas(bytes) __declspec(align(bytes)) #elif __STDC_VERSION__ >= 201112L /* C11 and above */ @@ -85,6 +85,30 @@ #define LIBCO_ASSERT assert #endif +#if defined (__OpenBSD__) + #if !defined(LIBCO_MALLOC) || !defined(LIBCO_FREE) + #include + #include + + static void* malloc_obsd(size_t size) { + long pagesize = sysconf(_SC_PAGESIZE); + char* memory = (char*)mmap(NULL, size + pagesize, PROT_READ|PROT_WRITE, MAP_STACK|MAP_PRIVATE|MAP_ANON, -1, 0); + if (memory == MAP_FAILED) return NULL; + *(size_t*)memory = size + pagesize; + memory += pagesize; + return (void*)memory; + } + + static void free_obsd(void *ptr) { + char* memory = (char*)ptr - sysconf(_SC_PAGESIZE); + munmap(memory, *(size_t*)memory); + } + + #define LIBCO_MALLOC malloc_obsd + #define LIBCO_FREE free_obsd + #endif +#endif + #if !defined(LIBCO_MALLOC) || !defined(LIBCO_FREE) #include #define LIBCO_MALLOC malloc diff --git a/nall/inode.hpp b/nall/inode.hpp index 89bebb24..0f740ae6 100755 --- a/nall/inode.hpp +++ b/nall/inode.hpp @@ -82,6 +82,12 @@ struct inode { #if defined(PLATFORM_WINDOWS) //on Windows, the last status change time (ctime) holds the file creation time instead case time::create: return data.st_ctime; + #elif defined(__OpenBSD__) + // OpenBSD is a special case that must be handled separately from other BSDs + case time::create: return min((uint)data.__st_birthtime, (uint)data.st_mtime); + #elif defined (__DragonFly__) + // DragonFly BSD does not support file creation time, use modified time instead + case time::create: return data.st_mtime; #elif defined(PLATFORM_BSD) || defined(PLATFORM_MACOS) //st_birthtime may return -1 or st_atime if it is not supported by the file system //the best that can be done in this case is to return st_mtime if it's older diff --git a/nall/intrinsics.hpp b/nall/intrinsics.hpp index 685eff88..065344d5 100755 --- a/nall/intrinsics.hpp +++ b/nall/intrinsics.hpp @@ -105,7 +105,7 @@ namespace nall { constexpr auto platform() -> Platform { return Platform::Linux; } constexpr auto api() -> API { return API::Posix; } constexpr auto display() -> DisplayServer { return DisplayServer::Xorg; } -#elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__NetBSD__) || defined(__OpenBSD__) +#elif defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined (__DragonFly__) #define PLATFORM_BSD #define API_POSIX #define DISPLAY_XORG diff --git a/nall/primitives/bit-range.hpp b/nall/primitives/bit-range.hpp index 1efc21cc..7d8112e2 100644 --- a/nall/primitives/bit-range.hpp +++ b/nall/primitives/bit-range.hpp @@ -58,7 +58,8 @@ template struct BitRange { } template inline auto& operator=(const T& source) { - target = target & ~mask | source << shift & mask; + type value = source; + target = target & ~mask | value << shift & mask; return *this; } @@ -105,17 +106,20 @@ template struct BitRange { } template inline auto& operator&=(const T& source) { - target = target & (~mask | source << shift & mask); + type value = source; + target = target & (~mask | value << shift & mask); return *this; } template inline auto& operator^=(const T& source) { - target = target ^ source << shift & mask; + type value = source; + target = target ^ value << shift & mask; return *this; } template inline auto& operator|=(const T& source) { - target = target | source << shift & mask; + type value = source; + target = target | value << shift & mask; return *this; } @@ -185,7 +189,8 @@ template struct BitRange { } template inline auto& operator=(const T& source) { - target = target & ~mask | source << shift & mask; + type value = source; + target = target & ~mask | value << shift & mask; return *this; } @@ -232,17 +237,20 @@ template struct BitRange { } template inline auto& operator&=(const T& source) { - target = target & (~mask | source << shift & mask); + type value = source; + target = target & (~mask | value << shift & mask); return *this; } template inline auto& operator^=(const T& source) { - target = target ^ source << shift & mask; + type value = source; + target = target ^ value << shift & mask; return *this; } template inline auto& operator|=(const T& source) { - target = target | source << shift & mask; + type value = source; + target = target | value << shift & mask; return *this; } diff --git a/nall/vector.hpp b/nall/vector.hpp index 04ffc535..3662a768 100644 --- a/nall/vector.hpp +++ b/nall/vector.hpp @@ -45,7 +45,7 @@ struct vector_base { //memory.hpp auto reset() -> void; - auto acquire(const T* data, uint64_t size, uint64_t capacity = 0) -> void; + auto acquire(T* data, uint64_t size, uint64_t capacity = 0) -> void; auto release() -> T*; auto reserveLeft(uint64_t capacity) -> bool; diff --git a/nall/vector/memory.hpp b/nall/vector/memory.hpp index 15d7dd4f..863d406c 100644 --- a/nall/vector/memory.hpp +++ b/nall/vector/memory.hpp @@ -19,7 +19,7 @@ template auto vector::reset() -> void { //acquire ownership of allocated memory -template auto vector::acquire(const T* data, uint64_t size, uint64_t capacity) -> void { +template auto vector::acquire(T* data, uint64_t size, uint64_t capacity) -> void { reset(); _pool = data; _size = size; diff --git a/nall/windows/guard.hpp b/nall/windows/guard.hpp index 353874cb..66e2fa86 100644 --- a/nall/windows/guard.hpp +++ b/nall/windows/guard.hpp @@ -6,10 +6,8 @@ #undef UNICODE #undef WINVER -#undef WIN32_LEAN_AND_LEAN +#undef WIN32_LEAN_AND_MEAN #undef _WIN32_WINNT -#undef _WIN32_IE -#undef __MSVCRT_VERSION__ #undef NOMINMAX #undef PATH_MAX @@ -17,8 +15,6 @@ #define WINVER 0x0601 #define WIN32_LEAN_AND_MEAN #define _WIN32_WINNT WINVER -#define _WIN32_IE WINVER -#define __MSVCRT_VERSION__ WINVER #define NOMINMAX #define PATH_MAX 260